run formatter

This commit is contained in:
Dax Raad 2025-05-31 14:41:00 -04:00
parent 6df19f1828
commit 3b746162d2
52 changed files with 1376 additions and 1390 deletions

View file

@ -83,6 +83,7 @@ You can configure OpenCode using environment variables:
| `AZURE_OPENAI_ENDPOINT` | For Azure OpenAI models | | `AZURE_OPENAI_ENDPOINT` | For Azure OpenAI models |
| `AZURE_OPENAI_API_KEY` | For Azure OpenAI models (optional when using Entra ID) | | `AZURE_OPENAI_API_KEY` | For Azure OpenAI models (optional when using Entra ID) |
| `AZURE_OPENAI_API_VERSION` | For Azure OpenAI models | | `AZURE_OPENAI_API_VERSION` | For Azure OpenAI models |
### Configuration File Structure ### Configuration File Structure
```json ```json
@ -205,7 +206,7 @@ To use bedrock models with OpenCode you need three things.
1. Valid AWS credentials (the env vars: `AWS_SECRET_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and `AWS_REGION`) 1. Valid AWS credentials (the env vars: `AWS_SECRET_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and `AWS_REGION`)
2. Access to the corresponding model in AWS Bedrock in your region. 2. Access to the corresponding model in AWS Bedrock in your region.
a. You can request access in the AWS console on the Bedrock -> "Model access" page. a. You can request access in the AWS console on the Bedrock -> "Model access" page.
3. A correct configuration file. You don't need the `providers` key. Instead you have to prefix your models per agent with `bedrock.` and then a valid model. For now only Claude 3.7 is supported. 3. A correct configuration file. You don't need the `providers` key. Instead you have to prefix your models per agent with `bedrock.` and then a valid model. For now only Claude 3.7 is supported.
```json ```json
@ -226,10 +227,10 @@ To use bedrock models with OpenCode you need three things.
"maxTokens": 80, "maxTokens": 80,
"reasoningEffort": "" "reasoningEffort": ""
} }
}, }
} }
``` ```
## Interactive Mode Usage ## Interactive Mode Usage
```bash ```bash
@ -295,26 +296,26 @@ These flags are mutually exclusive - you can use either `--allowedTools` or `--e
OpenCode supports the following output formats in non-interactive mode: OpenCode supports the following output formats in non-interactive mode:
| Format | Description | | Format | Description |
| ------ | -------------------------------------- | | ------ | ------------------------------- |
| `text` | Plain text output (default) | | `text` | Plain text output (default) |
| `json` | Output wrapped in a JSON object | | `json` | Output wrapped in a JSON object |
The output format is implemented as a strongly-typed `OutputFormat` in the codebase, ensuring type safety and validation when processing outputs. The output format is implemented as a strongly-typed `OutputFormat` in the codebase, ensuring type safety and validation when processing outputs.
## Command-line Flags ## Command-line Flags
| Flag | Short | Description | | Flag | Short | Description |
| ----------------- | ----- | ---------------------------------------------------------- | | ----------------- | ----- | --------------------------------------------------- |
| `--help` | `-h` | Display help information | | `--help` | `-h` | Display help information |
| `--debug` | `-d` | Enable debug mode | | `--debug` | `-d` | Enable debug mode |
| `--cwd` | `-c` | Set current working directory | | `--cwd` | `-c` | Set current working directory |
| `--prompt` | `-p` | Run a single prompt in non-interactive mode | | `--prompt` | `-p` | Run a single prompt in non-interactive mode |
| `--output-format` | `-f` | Output format for non-interactive mode (text, json) | | `--output-format` | `-f` | Output format for non-interactive mode (text, json) |
| `--quiet` | `-q` | Hide spinner in non-interactive mode | | `--quiet` | `-q` | Hide spinner in non-interactive mode |
| `--verbose` | | Display logs to stderr in non-interactive mode | | `--verbose` | | Display logs to stderr in non-interactive mode |
| `--allowedTools` | | Restrict the agent to only use specified tools | | `--allowedTools` | | Restrict the agent to only use specified tools |
| `--excludedTools` | | Prevent the agent from using specified tools | | `--excludedTools` | | Prevent the agent from using specified tools |
## Keyboard Shortcuts ## Keyboard Shortcuts
@ -483,6 +484,7 @@ You don't need to define all colors. Any undefined colors will fall back to the
### Shell Configuration ### Shell Configuration
OpenCode allows you to configure the shell used by the `bash` tool. By default, it uses: OpenCode allows you to configure the shell used by the `bash` tool. By default, it uses:
1. The shell specified in the config file (if provided) 1. The shell specified in the config file (if provided)
2. The shell from the `$SHELL` environment variable (if available) 2. The shell from the `$SHELL` environment variable (if available)
3. Falls back to `/bin/bash` if neither of the above is available 3. Falls back to `/bin/bash` if neither of the above is available
@ -577,6 +579,7 @@ RUN grep -R "$SEARCH_PATTERN" $DIRECTORY
``` ```
When you run a command with arguments, OpenCode will prompt you to enter values for each unique placeholder. Named arguments provide several benefits: When you run a command with arguments, OpenCode will prompt you to enter values for each unique placeholder. Named arguments provide several benefits:
- Clear identification of what each argument represents - Clear identification of what each argument represents
- Ability to use the same argument multiple times - Ability to use the same argument multiple times
- Better organization for commands with multiple inputs - Better organization for commands with multiple inputs

View file

@ -30,7 +30,6 @@
"env-paths": "3.0.0", "env-paths": "3.0.0",
"hono": "4.7.10", "hono": "4.7.10",
"hono-openapi": "0.4.8", "hono-openapi": "0.4.8",
"jsdom": "26.1.0",
"remeda": "2.22.3", "remeda": "2.22.3",
"ts-lsp-client": "1.0.3", "ts-lsp-client": "1.0.3",
"turndown": "7.2.0", "turndown": "7.2.0",
@ -42,7 +41,6 @@
"devDependencies": { "devDependencies": {
"@tsconfig/bun": "1.0.7", "@tsconfig/bun": "1.0.7",
"@types/bun": "latest", "@types/bun": "latest",
"@types/jsdom": "21.1.7",
"@types/turndown": "5.0.5", "@types/turndown": "5.0.5",
"typescript": "catalog:", "typescript": "catalog:",
}, },
@ -97,8 +95,6 @@
"@apidevtools/json-schema-ref-parser": ["@apidevtools/json-schema-ref-parser@11.9.3", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.0" } }, "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ=="], "@apidevtools/json-schema-ref-parser": ["@apidevtools/json-schema-ref-parser@11.9.3", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.0" } }, "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ=="],
"@asamuzakjp/css-color": ["@asamuzakjp/css-color@3.2.0", "", { "dependencies": { "@csstools/css-calc": "^2.1.3", "@csstools/css-color-parser": "^3.0.9", "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "lru-cache": "^10.4.3" } }, "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw=="],
"@astrojs/compiler": ["@astrojs/compiler@2.12.0", "", {}, "sha512-7bCjW6tVDpUurQLeKBUN9tZ5kSv5qYrGmcn0sG0IwacL7isR2ZbyyA3AdZ4uxsuUFOS2SlgReTH7wkxO6zpqWA=="], "@astrojs/compiler": ["@astrojs/compiler@2.12.0", "", {}, "sha512-7bCjW6tVDpUurQLeKBUN9tZ5kSv5qYrGmcn0sG0IwacL7isR2ZbyyA3AdZ4uxsuUFOS2SlgReTH7wkxO6zpqWA=="],
"@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.6.1", "", {}, "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="], "@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.6.1", "", {}, "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="],
@ -157,16 +153,6 @@
"@cloudflare/workers-types": ["@cloudflare/workers-types@4.20250522.0", "", {}, "sha512-9RIffHobc35JWeddzBguGgPa4wLDr5x5F94+0/qy7LiV6pTBQ/M5qGEN9VA16IDT3EUpYI0WKh6VpcmeVEtVtw=="], "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20250522.0", "", {}, "sha512-9RIffHobc35JWeddzBguGgPa4wLDr5x5F94+0/qy7LiV6pTBQ/M5qGEN9VA16IDT3EUpYI0WKh6VpcmeVEtVtw=="],
"@csstools/color-helpers": ["@csstools/color-helpers@5.0.2", "", {}, "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA=="],
"@csstools/css-calc": ["@csstools/css-calc@2.1.4", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ=="],
"@csstools/css-color-parser": ["@csstools/css-color-parser@3.0.10", "", { "dependencies": { "@csstools/color-helpers": "^5.0.2", "@csstools/css-calc": "^2.1.4" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg=="],
"@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@3.0.5", "", { "peerDependencies": { "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ=="],
"@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="],
"@ctrl/tinycolor": ["@ctrl/tinycolor@4.1.0", "", {}, "sha512-WyOx8cJQ+FQus4Mm4uPIZA64gbk3Wxh0so5Lcii0aJifqwoVOlfFtorjLE0Hen4OYyHZMXDWqMmaQemBhgxFRQ=="], "@ctrl/tinycolor": ["@ctrl/tinycolor@4.1.0", "", {}, "sha512-WyOx8cJQ+FQus4Mm4uPIZA64gbk3Wxh0so5Lcii0aJifqwoVOlfFtorjLE0Hen4OYyHZMXDWqMmaQemBhgxFRQ=="],
"@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="], "@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="],
@ -407,8 +393,6 @@
"@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="], "@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="],
"@types/jsdom": ["@types/jsdom@21.1.7", "", { "dependencies": { "@types/node": "*", "@types/tough-cookie": "*", "parse5": "^7.0.0" } }, "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/luxon": ["@types/luxon@3.6.2", "", {}, "sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw=="], "@types/luxon": ["@types/luxon@3.6.2", "", {}, "sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw=="],
@ -425,8 +409,6 @@
"@types/sax": ["@types/sax@1.2.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A=="], "@types/sax": ["@types/sax@1.2.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A=="],
"@types/tough-cookie": ["@types/tough-cookie@4.0.5", "", {}, "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA=="],
"@types/turndown": ["@types/turndown@5.0.5", "", {}, "sha512-TL2IgGgc7B5j78rIccBtlYAnkuv8nUQqhQc+DSYV5j9Be9XOcm/SKOVRuA47xAVI3680Tk9B1d8flK2GWT2+4w=="], "@types/turndown": ["@types/turndown@5.0.5", "", {}, "sha512-TL2IgGgc7B5j78rIccBtlYAnkuv8nUQqhQc+DSYV5j9Be9XOcm/SKOVRuA47xAVI3680Tk9B1d8flK2GWT2+4w=="],
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
@ -439,8 +421,6 @@
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
"agent-base": ["agent-base@7.1.3", "", {}, "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw=="],
"ai": ["ai@5.0.0-alpha.7", "", { "dependencies": { "@ai-sdk/gateway": "1.0.0-alpha.7", "@ai-sdk/provider": "2.0.0-alpha.7", "@ai-sdk/provider-utils": "3.0.0-alpha.7", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-ShCk3frIMdVtK9knvWKiFS7N6Vwnf8mLMv670+T//W9oqfoetSVPBhTF6Dy+oDM/bjVSsBf1BuYImLDvHICOIQ=="], "ai": ["ai@5.0.0-alpha.7", "", { "dependencies": { "@ai-sdk/gateway": "1.0.0-alpha.7", "@ai-sdk/provider": "2.0.0-alpha.7", "@ai-sdk/provider-utils": "3.0.0-alpha.7", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-ShCk3frIMdVtK9knvWKiFS7N6Vwnf8mLMv670+T//W9oqfoetSVPBhTF6Dy+oDM/bjVSsBf1BuYImLDvHICOIQ=="],
"ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="], "ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="],
@ -601,12 +581,8 @@
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
"cssstyle": ["cssstyle@4.3.1", "", { "dependencies": { "@asamuzakjp/css-color": "^3.1.2", "rrweb-cssom": "^0.8.0" } }, "sha512-ZgW+Jgdd7i52AaLYCriF8Mxqft0gD/R9i9wi6RWBhs1pqdPEzPjym7rvRKi397WmQFf3SlyUsszhw+VVCbx79Q=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"data-urls": ["data-urls@5.0.0", "", { "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0" } }, "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg=="],
"dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="], "dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="],
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
@ -827,8 +803,6 @@
"hono-openapi": ["hono-openapi@0.4.8", "", { "dependencies": { "json-schema-walker": "^2.0.0" }, "peerDependencies": { "@hono/arktype-validator": "^2.0.0", "@hono/effect-validator": "^1.2.0", "@hono/typebox-validator": "^0.2.0 || ^0.3.0", "@hono/valibot-validator": "^0.5.1", "@hono/zod-validator": "^0.4.1", "@sinclair/typebox": "^0.34.9", "@valibot/to-json-schema": "^1.0.0-beta.3", "arktype": "^2.0.0", "effect": "^3.11.3", "hono": "^4.6.13", "openapi-types": "^12.1.3", "valibot": "^1.0.0-beta.9", "zod": "^3.23.8", "zod-openapi": "^4.0.0" }, "optionalPeers": ["@hono/arktype-validator", "@hono/effect-validator", "@hono/typebox-validator", "@hono/valibot-validator", "@hono/zod-validator", "@sinclair/typebox", "@valibot/to-json-schema", "arktype", "effect", "hono", "valibot", "zod", "zod-openapi"] }, "sha512-LYr5xdtD49M7hEAduV1PftOMzuT8ZNvkyWfh1DThkLsIr4RkvDb12UxgIiFbwrJB6FLtFXLoOZL9x4IeDk2+VA=="], "hono-openapi": ["hono-openapi@0.4.8", "", { "dependencies": { "json-schema-walker": "^2.0.0" }, "peerDependencies": { "@hono/arktype-validator": "^2.0.0", "@hono/effect-validator": "^1.2.0", "@hono/typebox-validator": "^0.2.0 || ^0.3.0", "@hono/valibot-validator": "^0.5.1", "@hono/zod-validator": "^0.4.1", "@sinclair/typebox": "^0.34.9", "@valibot/to-json-schema": "^1.0.0-beta.3", "arktype": "^2.0.0", "effect": "^3.11.3", "hono": "^4.6.13", "openapi-types": "^12.1.3", "valibot": "^1.0.0-beta.9", "zod": "^3.23.8", "zod-openapi": "^4.0.0" }, "optionalPeers": ["@hono/arktype-validator", "@hono/effect-validator", "@hono/typebox-validator", "@hono/valibot-validator", "@hono/zod-validator", "@sinclair/typebox", "@valibot/to-json-schema", "arktype", "effect", "hono", "valibot", "zod", "zod-openapi"] }, "sha512-LYr5xdtD49M7hEAduV1PftOMzuT8ZNvkyWfh1DThkLsIr4RkvDb12UxgIiFbwrJB6FLtFXLoOZL9x4IeDk2+VA=="],
"html-encoding-sniffer": ["html-encoding-sniffer@4.0.0", "", { "dependencies": { "whatwg-encoding": "^3.1.1" } }, "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ=="],
"html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="], "html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="],
"html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="], "html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="],
@ -841,10 +815,6 @@
"http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
"i18next": ["i18next@23.16.8", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg=="], "i18next": ["i18next@23.16.8", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg=="],
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
@ -887,8 +857,6 @@
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
"is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="],
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
"is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
@ -911,8 +879,6 @@
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
"jsdom": ["jsdom@26.1.0", "", { "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", "decimal.js": "^10.5.0", "html-encoding-sniffer": "^4.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", "nwsapi": "^2.2.16", "parse5": "^7.2.1", "rrweb-cssom": "^0.8.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^5.1.1", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.1.1", "ws": "^8.18.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg=="],
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"json-rpc-2.0": ["json-rpc-2.0@1.7.0", "", {}, "sha512-asnLgC1qD5ytP+fvBP8uL0rvj+l8P6iYICbzZ8dVxCpESffVjzA7KkYkbKCIbavs7cllwH1ZUaNtJwphdeRqpg=="], "json-rpc-2.0": ["json-rpc-2.0@1.7.0", "", {}, "sha512-asnLgC1qD5ytP+fvBP8uL0rvj+l8P6iYICbzZ8dVxCpESffVjzA7KkYkbKCIbavs7cllwH1ZUaNtJwphdeRqpg=="],
@ -1105,8 +1071,6 @@
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
"nwsapi": ["nwsapi@2.2.20", "", {}, "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"object-hash": ["object-hash@2.2.0", "", {}, "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw=="], "object-hash": ["object-hash@2.2.0", "", {}, "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw=="],
@ -1283,8 +1247,6 @@
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
"rrweb-cssom": ["rrweb-cssom@0.8.0", "", {}, "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
@ -1295,8 +1257,6 @@
"sax": ["sax@1.2.1", "", {}, "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA=="], "sax": ["sax@1.2.1", "", {}, "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA=="],
"saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="],
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="], "send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="],
@ -1393,8 +1353,6 @@
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="],
"tar-fs": ["tar-fs@3.0.9", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-XF4w9Xp+ZQgifKakjZYmFdkLoSWd34VGKcsTCwlNWM7QG3ZbaxnTsaBwnjFZqHRf/rROxaR8rXnbtwdvaDI+lA=="], "tar-fs": ["tar-fs@3.0.9", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-XF4w9Xp+ZQgifKakjZYmFdkLoSWd34VGKcsTCwlNWM7QG3ZbaxnTsaBwnjFZqHRf/rROxaR8rXnbtwdvaDI+lA=="],
"tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="], "tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="],
@ -1409,19 +1367,13 @@
"tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], "tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
"tldts": ["tldts@6.1.86", "", { "dependencies": { "tldts-core": "^6.1.86" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ=="],
"tldts-core": ["tldts-core@6.1.86", "", {}, "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA=="],
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"token-types": ["token-types@6.0.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA=="], "token-types": ["token-types@6.0.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA=="],
"toolbeam-docs-theme": ["toolbeam-docs-theme@0.2.4", "", { "peerDependencies": { "@astrojs/starlight": "^0.34.3", "astro": "^5.7.13" } }, "sha512-W5mdbcgRpTBDFyEdcU81USs3MFZoXMInpSznc/AFZCwqz8atk4iBNDIlhvihpGHY54Nf5crKmZwJjxVojkHFvA=="], "toolbeam-docs-theme": ["toolbeam-docs-theme@0.2.4", "", { "peerDependencies": { "@astrojs/starlight": "^0.34.3", "astro": "^5.7.13" } }, "sha512-W5mdbcgRpTBDFyEdcU81USs3MFZoXMInpSznc/AFZCwqz8atk4iBNDIlhvihpGHY54Nf5crKmZwJjxVojkHFvA=="],
"tough-cookie": ["tough-cookie@5.1.2", "", { "dependencies": { "tldts": "^6.1.32" } }, "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A=="], "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
"tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="],
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
@ -1519,17 +1471,11 @@
"vscode-languageserver-types": ["vscode-languageserver-types@3.17.3", "", {}, "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA=="], "vscode-languageserver-types": ["vscode-languageserver-types@3.17.3", "", {}, "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA=="],
"w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="],
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
"webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
"whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="], "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
"whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="],
"whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="],
"which-pm-runs": ["which-pm-runs@1.1.0", "", {}, "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA=="], "which-pm-runs": ["which-pm-runs@1.1.0", "", {}, "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA=="],
@ -1541,16 +1487,10 @@
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"ws": ["ws@8.18.2", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ=="],
"xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="],
"xml2js": ["xml2js@0.6.2", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA=="], "xml2js": ["xml2js@0.6.2", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA=="],
"xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], "xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="],
"xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="],
"xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="], "xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="],
"yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
@ -1573,8 +1513,6 @@
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
"@asamuzakjp/css-color/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"@astrojs/mdx/@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.2", "", { "dependencies": { "@astrojs/internal-helpers": "0.6.1", "@astrojs/prism": "3.3.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.1.0", "js-yaml": "^4.1.0", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "shiki": "^3.2.1", "smol-toml": "^1.3.1", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "vfile": "^6.0.3" } }, "sha512-bO35JbWpVvyKRl7cmSJD822e8YA8ThR/YbUsciWNA7yTcqpIAL2hJDToWP5KcZBWxGT6IOdOkHSXARSNZc4l/Q=="], "@astrojs/mdx/@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.2", "", { "dependencies": { "@astrojs/internal-helpers": "0.6.1", "@astrojs/prism": "3.3.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.1.0", "js-yaml": "^4.1.0", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "shiki": "^3.2.1", "smol-toml": "^1.3.1", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "vfile": "^6.0.3" } }, "sha512-bO35JbWpVvyKRl7cmSJD822e8YA8ThR/YbUsciWNA7yTcqpIAL2hJDToWP5KcZBWxGT6IOdOkHSXARSNZc4l/Q=="],
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
@ -1611,8 +1549,6 @@
"mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
"node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
"opencontrol/hono": ["hono@4.7.4", "", {}, "sha512-Pst8FuGqz3L7tFF+u9Pu70eI0xa5S3LPUmrNd5Jm8nTHze9FxLTK9Kaj5g/k4UcwuJSXTP65SyHOPLrffpcAJg=="], "opencontrol/hono": ["hono@4.7.4", "", {}, "sha512-Pst8FuGqz3L7tFF+u9Pu70eI0xa5S3LPUmrNd5Jm8nTHze9FxLTK9Kaj5g/k4UcwuJSXTP65SyHOPLrffpcAJg=="],
"opencontrol/zod-to-json-schema": ["zod-to-json-schema@3.24.3", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A=="], "opencontrol/zod-to-json-schema": ["zod-to-json-schema@3.24.3", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A=="],
@ -1639,8 +1575,6 @@
"token-types/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], "token-types/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"tr46/punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"unstorage/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "unstorage/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"vscode-languageserver-protocol/vscode-jsonrpc": ["vscode-jsonrpc@8.1.0", "", {}, "sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw=="], "vscode-languageserver-protocol/vscode-jsonrpc": ["vscode-jsonrpc@8.1.0", "", {}, "sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw=="],
@ -1659,10 +1593,6 @@
"bl/buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], "bl/buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
"node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
"pino-pretty/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "pino-pretty/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"prebuild-install/tar-fs/tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], "prebuild-install/tar-fs/tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],

View file

@ -6,20 +6,20 @@
import "sst" import "sst"
declare module "sst" { declare module "sst" {
export interface Resource { export interface Resource {
"Web": { Web: {
"type": "sst.cloudflare.StaticSite" type: "sst.cloudflare.StaticSite"
"url": string url: string
} }
} }
} }
// cloudflare // cloudflare
import * as cloudflare from "@cloudflare/workers-types"; import * as cloudflare from "@cloudflare/workers-types"
declare module "sst" { declare module "sst" {
export interface Resource { export interface Resource {
"Api": cloudflare.Service Api: cloudflare.Service
"Bucket": cloudflare.R2Bucket Bucket: cloudflare.R2Bucket
} }
} }
import "sst" import "sst"
export {} export {}

View file

@ -14,7 +14,6 @@
"devDependencies": { "devDependencies": {
"@tsconfig/bun": "1.0.7", "@tsconfig/bun": "1.0.7",
"@types/bun": "latest", "@types/bun": "latest",
"@types/jsdom": "21.1.7",
"@types/turndown": "5.0.5", "@types/turndown": "5.0.5",
"typescript": "catalog:" "typescript": "catalog:"
}, },
@ -28,7 +27,6 @@
"env-paths": "3.0.0", "env-paths": "3.0.0",
"hono": "4.7.10", "hono": "4.7.10",
"hono-openapi": "0.4.8", "hono-openapi": "0.4.8",
"jsdom": "26.1.0",
"remeda": "2.22.3", "remeda": "2.22.3",
"ts-lsp-client": "1.0.3", "ts-lsp-client": "1.0.3",
"turndown": "7.2.0", "turndown": "7.2.0",

View file

@ -28,7 +28,7 @@ for (const [os, arch] of targets) {
console.log(`building ${os}-${arch}`) console.log(`building ${os}-${arch}`)
const name = `${pkg.name}-${os}-${arch}` const name = `${pkg.name}-${os}-${arch}`
await $`mkdir -p dist/${name}/bin` await $`mkdir -p dist/${name}/bin`
await $`GOOS=${os} GOARCH=${GOARCH[arch]} go build -o ../opencode/dist/${name}/bin/tui ../tui/main.go`.cwd( await $`GOOS=${os} GOARCH=${GOARCH[arch]} go build -ldflags="-s -w -X github.com/sst/opencode/internal/version.Version=${version}" -o ../opencode/dist/${name}/bin/tui ../tui/main.go`.cwd(
"../tui", "../tui",
) )
await $`bun build --compile --minify --target=bun-${os}-${arch} --outfile=dist/${name}/bin/opencode ./src/index.ts ./dist/${name}/bin/tui` await $`bun build --compile --minify --target=bun-${os}-${arch} --outfile=dist/${name}/bin/opencode ./src/index.ts ./dist/${name}/bin/tui`

View file

@ -1,40 +1,40 @@
import fs from "fs/promises"; import fs from "fs/promises"
import { AppPath } from "./path"; import { AppPath } from "./path"
import { Log } from "../util/log"; import { Log } from "../util/log"
import { Context } from "../util/context"; import { Context } from "../util/context"
export namespace App { export namespace App {
const log = Log.create({ service: "app" }); const log = Log.create({ service: "app" })
export type Info = Awaited<ReturnType<typeof create>>; export type Info = Awaited<ReturnType<typeof create>>
const ctx = Context.create<Info>("app"); const ctx = Context.create<Info>("app")
async function create(input: { directory: string }) { async function create(input: { directory: string }) {
const dataDir = AppPath.data(input.directory); const dataDir = AppPath.data(input.directory)
await fs.mkdir(dataDir, { recursive: true }); await fs.mkdir(dataDir, { recursive: true })
await Log.file(input.directory); await Log.file(input.directory)
log.info("created", { path: dataDir }); log.info("created", { path: dataDir })
const services = new Map< const services = new Map<
any, any,
{ {
state: any; state: any
shutdown?: (input: any) => Promise<void>; shutdown?: (input: any) => Promise<void>
} }
>(); >()
const result = { const result = {
get services() { get services() {
return services; return services
}, },
get root() { get root() {
return input.directory; return input.directory
}, },
}; }
return result; return result
} }
export function state<State>( export function state<State>(
@ -43,36 +43,36 @@ export namespace App {
shutdown?: (state: Awaited<State>) => Promise<void>, shutdown?: (state: Awaited<State>) => Promise<void>,
) { ) {
return () => { return () => {
const app = ctx.use(); const app = ctx.use()
const services = app.services; const services = app.services
if (!services.has(key)) { if (!services.has(key)) {
log.info("registering service", { name: key }); log.info("registering service", { name: key })
services.set(key, { services.set(key, {
state: init(app), state: init(app),
shutdown: shutdown, shutdown: shutdown,
}); })
} }
return services.get(key)?.state as State; return services.get(key)?.state as State
}; }
} }
export async function use() { export async function use() {
return ctx.use(); return ctx.use()
} }
export async function provide<T extends (app: Info) => any>( export async function provide<T extends (app: Info) => any>(
input: { directory: string }, input: { directory: string },
cb: T, cb: T,
) { ) {
const app = await create(input); const app = await create(input)
return ctx.provide(app, async () => { return ctx.provide(app, async () => {
const result = await cb(app); const result = await cb(app)
for (const [key, entry] of app.services.entries()) { for (const [key, entry] of app.services.entries()) {
log.info("shutdown", { name: key }); log.info("shutdown", { name: key })
await entry.shutdown?.(await entry.state); await entry.shutdown?.(await entry.state)
} }
return result; return result
}); })
} }
} }

View file

@ -1,11 +1,11 @@
import path from "path"; import path from "path"
export namespace AppPath { export namespace AppPath {
export function data(input: string) { export function data(input: string) {
return path.join(input, ".opencode"); return path.join(input, ".opencode")
} }
export function storage(input: string) { export function storage(input: string) {
return path.join(data(input), "storage"); return path.join(data(input), "storage")
} }
} }

View file

@ -1,7 +1,7 @@
import path from "path"; import path from "path"
import { Log } from "../util/log"; import { Log } from "../util/log"
export namespace BunProc { export namespace BunProc {
const log = Log.create({ service: "bun" }); const log = Log.create({ service: "bun" })
export function run( export function run(
cmd: string[], cmd: string[],
@ -10,11 +10,11 @@ export namespace BunProc {
const root = const root =
process.argv0 !== "bun" process.argv0 !== "bun"
? path.resolve(process.cwd(), process.argv0) ? path.resolve(process.cwd(), process.argv0)
: process.argv0; : process.argv0
log.info("running", { log.info("running", {
cmd: [root, ...cmd], cmd: [root, ...cmd],
options, options,
}); })
const result = Bun.spawnSync([root, ...cmd], { const result = Bun.spawnSync([root, ...cmd], {
...options, ...options,
argv0: "bun", argv0: "bun",
@ -22,7 +22,11 @@ export namespace BunProc {
...process.env, ...process.env,
...options?.env, ...options?.env,
}, },
}); })
return result; if (result.exitCode !== 0) {
console.error(result.stderr?.toString("utf8") ?? "")
throw new Error(`Command failed with exit code ${result.exitCode}`)
}
return result
} }
} }

View file

@ -1,22 +1,22 @@
import { z, type ZodType } from "zod"; import { z, type ZodType } from "zod"
import { App } from "../app/app"; import { App } from "../app/app"
import { Log } from "../util/log"; import { Log } from "../util/log"
export namespace Bus { export namespace Bus {
const log = Log.create({ service: "bus" }); const log = Log.create({ service: "bus" })
type Subscription = (event: any) => void; type Subscription = (event: any) => void
const state = App.state("bus", () => { const state = App.state("bus", () => {
const subscriptions = new Map<any, Subscription[]>(); const subscriptions = new Map<any, Subscription[]>()
return { return {
subscriptions, subscriptions,
}; }
}); })
export type EventDefinition = ReturnType<typeof event>; export type EventDefinition = ReturnType<typeof event>
const registry = new Map<string, EventDefinition>(); const registry = new Map<string, EventDefinition>()
export function event<Type extends string, Properties extends ZodType>( export function event<Type extends string, Properties extends ZodType>(
type: Type, type: Type,
@ -25,9 +25,9 @@ export namespace Bus {
const result = { const result = {
type, type,
properties, properties,
}; }
registry.set(type, result); registry.set(type, result)
return result; return result
} }
export function payloads() { export function payloads() {
@ -46,7 +46,7 @@ export namespace Bus {
}), }),
) )
.toArray() as any, .toArray() as any,
); )
} }
export function publish<Definition extends EventDefinition>( export function publish<Definition extends EventDefinition>(
@ -56,14 +56,14 @@ export namespace Bus {
const payload = { const payload = {
type: def.type, type: def.type,
properties, properties,
}; }
log.info("publishing", { log.info("publishing", {
type: def.type, type: def.type,
}); })
for (const key of [def.type, "*"]) { for (const key of [def.type, "*"]) {
const match = state().subscriptions.get(key); const match = state().subscriptions.get(key)
for (const sub of match ?? []) { for (const sub of match ?? []) {
sub(payload); sub(payload)
} }
} }
} }
@ -71,31 +71,31 @@ export namespace Bus {
export function subscribe<Definition extends EventDefinition>( export function subscribe<Definition extends EventDefinition>(
def: Definition, def: Definition,
callback: (event: { callback: (event: {
type: Definition["type"]; type: Definition["type"]
properties: z.infer<Definition["properties"]>; properties: z.infer<Definition["properties"]>
}) => void, }) => void,
) { ) {
return raw(def.type, callback); return raw(def.type, callback)
} }
export function subscribeAll(callback: (event: any) => void) { export function subscribeAll(callback: (event: any) => void) {
return raw("*", callback); return raw("*", callback)
} }
function raw(type: string, callback: (event: any) => void) { function raw(type: string, callback: (event: any) => void) {
log.info("subscribing", { type }); log.info("subscribing", { type })
const subscriptions = state().subscriptions; const subscriptions = state().subscriptions
let match = subscriptions.get(type) ?? []; let match = subscriptions.get(type) ?? []
match.push(callback); match.push(callback)
subscriptions.set(type, match); subscriptions.set(type, match)
return () => { return () => {
log.info("unsubscribing", { type }); log.info("unsubscribing", { type })
const match = subscriptions.get(type); const match = subscriptions.get(type)
if (!match) return; if (!match) return
const index = match.indexOf(callback); const index = match.indexOf(callback)
if (index === -1) return; if (index === -1) return
match.splice(index, 1); match.splice(index, 1)
}; }
} }
} }

View file

@ -1,51 +1,51 @@
import path from "path"; import path from "path"
import { Log } from "../util/log"; import { Log } from "../util/log"
import { z } from "zod"; import { z } from "zod"
import { App } from "../app/app"; import { App } from "../app/app"
import { Provider } from "../provider/provider"; import { Provider } from "../provider/provider"
export namespace Config { export namespace Config {
const log = Log.create({ service: "config" }); const log = Log.create({ service: "config" })
export const state = App.state("config", async (app) => { export const state = App.state("config", async (app) => {
const result = await load(app.root); const result = await load(app.root)
return result; return result
}); })
export const Info = z export const Info = z
.object({ .object({
providers: Provider.Info.array().optional(), providers: Provider.Info.array().optional(),
}) })
.strict(); .strict()
export type Info = z.output<typeof Info>; export type Info = z.output<typeof Info>
export function get() { export function get() {
return state(); return state()
} }
async function load(directory: string) { async function load(directory: string) {
let result: Info = {}; let result: Info = {}
for (const file of ["opencode.jsonc", "opencode.json"]) { for (const file of ["opencode.jsonc", "opencode.json"]) {
const resolved = path.join(directory, file); const resolved = path.join(directory, file)
log.info("searching", { path: resolved }); log.info("searching", { path: resolved })
try { try {
result = await import(path.join(directory, file)).then((mod) => result = await import(path.join(directory, file)).then((mod) =>
Info.parse(mod.default), Info.parse(mod.default),
); )
log.info("found", { path: resolved }); log.info("found", { path: resolved })
break; break
} catch (e) { } catch (e) {
if (e instanceof z.ZodError) { if (e instanceof z.ZodError) {
for (const issue of e.issues) { for (const issue of e.issues) {
log.info(issue.message); log.info(issue.message)
} }
throw e; throw e
} }
continue; continue
} }
} }
log.info("loaded", result); log.info("loaded", result)
return result; return result
} }
} }

View file

@ -1,20 +1,20 @@
import envpaths from "env-paths"; import envpaths from "env-paths"
import fs from "fs/promises"; import fs from "fs/promises"
const paths = envpaths("opencode", { const paths = envpaths("opencode", {
suffix: "", suffix: "",
}); })
await Promise.all([ await Promise.all([
fs.mkdir(paths.config, { recursive: true }), fs.mkdir(paths.config, { recursive: true }),
fs.mkdir(paths.cache, { recursive: true }), fs.mkdir(paths.cache, { recursive: true }),
]); ])
export namespace Global { export namespace Global {
export function config() { export function config() {
return paths.config; return paths.config
} }
export function cache() { export function cache() {
return paths.cache; return paths.cache
} }
} }

View file

@ -1,28 +1,28 @@
import { z } from "zod"; import { z } from "zod"
import { randomBytes } from "crypto"; import { randomBytes } from "crypto"
export namespace Identifier { export namespace Identifier {
const prefixes = { const prefixes = {
session: "ses", session: "ses",
message: "msg", message: "msg",
} as const; } as const
export function schema(prefix: keyof typeof prefixes) { export function schema(prefix: keyof typeof prefixes) {
return z.string().startsWith(prefixes[prefix]); return z.string().startsWith(prefixes[prefix])
} }
const LENGTH = 26; const LENGTH = 26
// State for monotonic ID generation // State for monotonic ID generation
let lastTimestamp = 0; let lastTimestamp = 0
let counter = 0; let counter = 0
export function ascending(prefix: keyof typeof prefixes, given?: string) { export function ascending(prefix: keyof typeof prefixes, given?: string) {
return generateID(prefix, false, given); return generateID(prefix, false, given)
} }
export function descending(prefix: keyof typeof prefixes, given?: string) { export function descending(prefix: keyof typeof prefixes, given?: string) {
return generateID(prefix, true, given); return generateID(prefix, true, given)
} }
function generateID( function generateID(
@ -31,44 +31,44 @@ export namespace Identifier {
given?: string, given?: string,
): string { ): string {
if (!given) { if (!given) {
return generateNewID(prefix, descending); return generateNewID(prefix, descending)
} }
if (!given.startsWith(prefixes[prefix])) { if (!given.startsWith(prefixes[prefix])) {
throw new Error(`ID ${given} does not start with ${prefixes[prefix]}`); throw new Error(`ID ${given} does not start with ${prefixes[prefix]}`)
} }
return given; return given
} }
function generateNewID( function generateNewID(
prefix: keyof typeof prefixes, prefix: keyof typeof prefixes,
descending: boolean, descending: boolean,
): string { ): string {
const currentTimestamp = Date.now(); const currentTimestamp = Date.now()
if (currentTimestamp !== lastTimestamp) { if (currentTimestamp !== lastTimestamp) {
lastTimestamp = currentTimestamp; lastTimestamp = currentTimestamp
counter = 0; counter = 0
} }
counter++; counter++
let now = BigInt(currentTimestamp) * BigInt(0x1000) + BigInt(counter); let now = BigInt(currentTimestamp) * BigInt(0x1000) + BigInt(counter)
now = descending ? ~now : now; now = descending ? ~now : now
const timeBytes = Buffer.alloc(6); const timeBytes = Buffer.alloc(6)
for (let i = 0; i < 6; i++) { for (let i = 0; i < 6; i++) {
timeBytes[i] = Number((now >> BigInt(40 - 8 * i)) & BigInt(0xff)); timeBytes[i] = Number((now >> BigInt(40 - 8 * i)) & BigInt(0xff))
} }
const randLength = (LENGTH - 12) / 2; const randLength = (LENGTH - 12) / 2
const random = randomBytes(randLength); const random = randomBytes(randLength)
return ( return (
prefixes[prefix] + prefixes[prefix] +
"_" + "_" +
timeBytes.toString("hex") + timeBytes.toString("hex") +
random.toString("hex") random.toString("hex")
); )
} }
} }

View file

@ -27,6 +27,7 @@ cli.command("", "Start the opencode in interactive mode").action(async () => {
if (!(await file.exists())) { if (!(await file.exists())) {
console.log("installing tui binary...") console.log("installing tui binary...")
await Bun.write(file, blob, { mode: 0o755 }) await Bun.write(file, blob, { mode: 0o755 })
await fs.chmod(binary, 0o755)
} }
cwd = process.cwd() cwd = process.cwd()
cmd = [binary] cmd = [binary]

View file

@ -115,7 +115,7 @@ export namespace LLM {
provider.id, provider.id,
) )
if (!(await Bun.file(path.join(dir, "package.json")).exists())) { if (!(await Bun.file(path.join(dir, "package.json")).exists())) {
BunProc.run(["add", "--exact", `@ai-sdk/${provider.id}@alpha`], { BunProc.run(["add", `@ai-sdk/${provider.id}@alpha`], {
cwd: Global.cache(), cwd: Global.cache(),
}) })
} }

View file

@ -1,23 +1,23 @@
import { spawn } from "child_process"; import { spawn } from "child_process"
import path from "path"; import path from "path"
import { import {
createMessageConnection, createMessageConnection,
StreamMessageReader, StreamMessageReader,
StreamMessageWriter, StreamMessageWriter,
} from "vscode-jsonrpc/node"; } from "vscode-jsonrpc/node"
import type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types"; import type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types"
import { App } from "../app/app"; import { App } from "../app/app"
import { Log } from "../util/log"; import { Log } from "../util/log"
import { LANGUAGE_EXTENSIONS } from "./language"; import { LANGUAGE_EXTENSIONS } from "./language"
import { Bus } from "../bus"; import { Bus } from "../bus"
import z from "zod"; import z from "zod"
export namespace LSPClient { export namespace LSPClient {
const log = Log.create({ service: "lsp.client" }); const log = Log.create({ service: "lsp.client" })
export type Info = Awaited<ReturnType<typeof create>>; export type Info = Awaited<ReturnType<typeof create>>
export type Diagnostic = VSCodeDiagnostic; export type Diagnostic = VSCodeDiagnostic
export const Event = { export const Event = {
Diagnostics: Bus.event( Diagnostics: Bus.event(
@ -27,36 +27,36 @@ export namespace LSPClient {
path: z.string(), path: z.string(),
}), }),
), ),
}; }
export async function create(input: { cmd: string[]; serverID: string }) { export async function create(input: { cmd: string[]; serverID: string }) {
log.info("starting client", input); log.info("starting client", input)
const app = await App.use(); const app = await App.use()
const [command, ...args] = input.cmd; const [command, ...args] = input.cmd
const server = spawn(command, args, { const server = spawn(command, args, {
stdio: ["pipe", "pipe", "pipe"], stdio: ["pipe", "pipe", "pipe"],
cwd: app.root, cwd: app.root,
}); })
const connection = createMessageConnection( const connection = createMessageConnection(
new StreamMessageReader(server.stdout), new StreamMessageReader(server.stdout),
new StreamMessageWriter(server.stdin), new StreamMessageWriter(server.stdin),
); )
const diagnostics = new Map<string, Diagnostic[]>(); const diagnostics = new Map<string, Diagnostic[]>()
connection.onNotification("textDocument/publishDiagnostics", (params) => { connection.onNotification("textDocument/publishDiagnostics", (params) => {
const path = new URL(params.uri).pathname; const path = new URL(params.uri).pathname
log.info("textDocument/publishDiagnostics", { log.info("textDocument/publishDiagnostics", {
path, path,
}); })
const exists = diagnostics.has(path); const exists = diagnostics.has(path)
diagnostics.set(path, params.diagnostics); diagnostics.set(path, params.diagnostics)
// servers seem to send one blank publishDiagnostics event before the first real one // servers seem to send one blank publishDiagnostics event before the first real one
if (!exists && !params.diagnostics.length) return; if (!exists && !params.diagnostics.length) return
Bus.publish(Event.Diagnostics, { path, serverID: input.serverID }); Bus.publish(Event.Diagnostics, { path, serverID: input.serverID })
}); })
connection.listen(); connection.listen()
await connection.sendRequest("initialize", { await connection.sendRequest("initialize", {
processId: server.pid, processId: server.pid,
@ -116,29 +116,29 @@ export namespace LSPClient {
}, },
window: {}, window: {},
}, },
}); })
await connection.sendNotification("initialized", {}); await connection.sendNotification("initialized", {})
log.info("initialized"); log.info("initialized")
const files = new Set<string>(); const files = new Set<string>()
const result = { const result = {
get clientID() { get clientID() {
return input.serverID; return input.serverID
}, },
get connection() { get connection() {
return connection; return connection
}, },
notify: { notify: {
async open(input: { path: string }) { async open(input: { path: string }) {
const file = Bun.file(input.path); const file = Bun.file(input.path)
const text = await file.text(); const text = await file.text()
const opened = files.has(input.path); const opened = files.has(input.path)
if (!opened) { if (!opened) {
log.info("textDocument/didOpen", input); log.info("textDocument/didOpen", input)
diagnostics.delete(input.path); diagnostics.delete(input.path)
const extension = path.extname(input.path); const extension = path.extname(input.path)
const languageId = LANGUAGE_EXTENSIONS[extension] ?? "plaintext"; const languageId = LANGUAGE_EXTENSIONS[extension] ?? "plaintext"
await connection.sendNotification("textDocument/didOpen", { await connection.sendNotification("textDocument/didOpen", {
textDocument: { textDocument: {
uri: `file://` + input.path, uri: `file://` + input.path,
@ -146,13 +146,13 @@ export namespace LSPClient {
version: Date.now(), version: Date.now(),
text, text,
}, },
}); })
files.add(input.path); files.add(input.path)
return; return
} }
log.info("textDocument/didChange", input); log.info("textDocument/didChange", input)
diagnostics.delete(input.path); diagnostics.delete(input.path)
await connection.sendNotification("textDocument/didChange", { await connection.sendNotification("textDocument/didChange", {
textDocument: { textDocument: {
uri: `file://` + input.path, uri: `file://` + input.path,
@ -163,16 +163,16 @@ export namespace LSPClient {
text, text,
}, },
], ],
}); })
}, },
}, },
get diagnostics() { get diagnostics() {
return diagnostics; return diagnostics
}, },
async waitForDiagnostics(input: { path: string }) { async waitForDiagnostics(input: { path: string }) {
log.info("waiting for diagnostics", input); log.info("waiting for diagnostics", input)
let unsub: () => void; let unsub: () => void
let timeout: NodeJS.Timeout; let timeout: NodeJS.Timeout
return await Promise.race([ return await Promise.race([
new Promise<void>(async (resolve) => { new Promise<void>(async (resolve) => {
unsub = Bus.subscribe(Event.Diagnostics, (event) => { unsub = Bus.subscribe(Event.Diagnostics, (event) => {
@ -180,29 +180,29 @@ export namespace LSPClient {
event.properties.path === input.path && event.properties.path === input.path &&
event.properties.serverID === result.clientID event.properties.serverID === result.clientID
) { ) {
log.info("got diagnostics", input); log.info("got diagnostics", input)
clearTimeout(timeout); clearTimeout(timeout)
unsub?.(); unsub?.()
resolve(); resolve()
} }
}); })
}), }),
new Promise<void>((resolve) => { new Promise<void>((resolve) => {
timeout = setTimeout(() => { timeout = setTimeout(() => {
log.info("timed out refreshing diagnostics", input); log.info("timed out refreshing diagnostics", input)
unsub?.(); unsub?.()
resolve(); resolve()
}, 5000); }, 5000)
}), }),
]); ])
}, },
async shutdown() { async shutdown() {
log.info("shutting down"); log.info("shutting down")
connection.end(); connection.end()
connection.dispose(); connection.dispose()
}, },
}; }
return result; return result
} }
} }

View file

@ -1,64 +1,64 @@
import { App } from "../app/app"; import { App } from "../app/app"
import { Log } from "../util/log"; import { Log } from "../util/log"
import { LSPClient } from "./client"; import { LSPClient } from "./client"
import path from "path"; import path from "path"
export namespace LSP { export namespace LSP {
const log = Log.create({ service: "lsp" }); const log = Log.create({ service: "lsp" })
const state = App.state( const state = App.state(
"lsp", "lsp",
async () => { async () => {
log.info("initializing"); log.info("initializing")
const clients = new Map<string, LSPClient.Info>(); const clients = new Map<string, LSPClient.Info>()
return { return {
clients, clients,
}; }
}, },
async (state) => { async (state) => {
for (const client of state.clients.values()) { for (const client of state.clients.values()) {
await client.shutdown(); await client.shutdown()
} }
}, },
); )
export async function file(input: string) { export async function file(input: string) {
const extension = path.parse(input).ext; const extension = path.parse(input).ext
const s = await state(); const s = await state()
const matches = AUTO.filter((x) => x.extensions.includes(extension)); const matches = AUTO.filter((x) => x.extensions.includes(extension))
for (const match of matches) { for (const match of matches) {
const existing = s.clients.get(match.id); const existing = s.clients.get(match.id)
if (existing) continue; if (existing) continue
const client = await LSPClient.create({ const client = await LSPClient.create({
cmd: match.command, cmd: match.command,
serverID: match.id, serverID: match.id,
}); })
s.clients.set(match.id, client); s.clients.set(match.id, client)
} }
await run(async (client) => { await run(async (client) => {
const wait = client.waitForDiagnostics({ path: input }); const wait = client.waitForDiagnostics({ path: input })
await client.notify.open({ path: input }); await client.notify.open({ path: input })
return wait; return wait
}); })
} }
export async function diagnostics() { export async function diagnostics() {
const results: Record<string, LSPClient.Diagnostic[]> = {}; const results: Record<string, LSPClient.Diagnostic[]> = {}
for (const result of await run(async (client) => client.diagnostics)) { for (const result of await run(async (client) => client.diagnostics)) {
for (const [path, diagnostics] of result.entries()) { for (const [path, diagnostics] of result.entries()) {
const arr = results[path] || []; const arr = results[path] || []
arr.push(...diagnostics); arr.push(...diagnostics)
results[path] = arr; results[path] = arr
} }
} }
return results; return results
} }
export async function hover(input: { export async function hover(input: {
file: string; file: string
line: number; line: number
character: number; character: number
}) { }) {
return run((client) => { return run((client) => {
return client.connection.sendRequest("textDocument/hover", { return client.connection.sendRequest("textDocument/hover", {
@ -69,23 +69,23 @@ export namespace LSP {
line: input.line, line: input.line,
character: input.character, character: input.character,
}, },
}); })
}); })
} }
async function run<T>( async function run<T>(
input: (client: LSPClient.Info) => Promise<T>, input: (client: LSPClient.Info) => Promise<T>,
): Promise<T[]> { ): Promise<T[]> {
const clients = await state().then((x) => [...x.clients.values()]); const clients = await state().then((x) => [...x.clients.values()])
const tasks = clients.map((x) => input(x)); const tasks = clients.map((x) => input(x))
return Promise.all(tasks); return Promise.all(tasks)
} }
const AUTO: { const AUTO: {
id: string; id: string
command: string[]; command: string[]
extensions: string[]; extensions: string[]
install?: () => Promise<void>; install?: () => Promise<void>
}[] = [ }[] = [
{ {
id: "typescript", id: "typescript",
@ -110,7 +110,7 @@ export namespace LSP {
extensions: [".go"], extensions: [".go"],
}, },
*/ */
]; ]
export namespace Diagnostic { export namespace Diagnostic {
export function pretty(diagnostic: LSPClient.Diagnostic) { export function pretty(diagnostic: LSPClient.Diagnostic) {
@ -119,13 +119,13 @@ export namespace LSP {
2: "WARN", 2: "WARN",
3: "INFO", 3: "INFO",
4: "HINT", 4: "HINT",
}; }
const severity = severityMap[diagnostic.severity || 1]; const severity = severityMap[diagnostic.severity || 1]
const line = diagnostic.range.start.line + 1; const line = diagnostic.range.start.line + 1
const col = diagnostic.range.start.character + 1; const col = diagnostic.range.start.character + 1
return `${severity} [${line}:${col}] ${diagnostic.message}`; return `${severity} [${line}:${col}] ${diagnostic.message}`
} }
} }
} }

View file

@ -86,4 +86,4 @@ export const LANGUAGE_EXTENSIONS: Record<string, string> = {
".yml": "yaml", ".yml": "yaml",
".mjs": "javascript", ".mjs": "javascript",
".cjs": "javascript", ".cjs": "javascript",
} as const; } as const

View file

@ -1,4 +1,4 @@
import z from "zod"; import z from "zod"
export namespace Provider { export namespace Provider {
export const Model = z export const Model = z
@ -18,8 +18,8 @@ export namespace Provider {
}) })
.openapi({ .openapi({
ref: "Provider.Model", ref: "Provider.Model",
}); })
export type Model = z.output<typeof Model>; export type Model = z.output<typeof Model>
export const Info = z export const Info = z
.object({ .object({
@ -30,6 +30,6 @@ export namespace Provider {
}) })
.openapi({ .openapi({
ref: "Provider.Info", ref: "Provider.Info",
}); })
export type Info = z.output<typeof Info>; export type Info = z.output<typeof Info>
} }

View file

@ -1,23 +1,23 @@
import { Log } from "../util/log"; import { Log } from "../util/log"
import { Bus } from "../bus"; import { Bus } from "../bus"
import { describeRoute, generateSpecs, openAPISpecs } from "hono-openapi"; import { describeRoute, generateSpecs, openAPISpecs } from "hono-openapi"
import { Hono } from "hono"; import { Hono } from "hono"
import { streamSSE } from "hono/streaming"; import { streamSSE } from "hono/streaming"
import { Session } from "../session/session"; import { Session } from "../session/session"
import { resolver, validator as zValidator } from "hono-openapi/zod"; import { resolver, validator as zValidator } from "hono-openapi/zod"
import { z } from "zod"; import { z } from "zod"
import { LLM } from "../llm/llm"; import { LLM } from "../llm/llm"
import { Message } from "../session/message"; import { Message } from "../session/message"
import { Provider } from "../provider/provider"; import { Provider } from "../provider/provider"
export namespace Server { export namespace Server {
const log = Log.create({ service: "server" }); const log = Log.create({ service: "server" })
const PORT = 16713; const PORT = 16713
export type App = ReturnType<typeof app>; export type App = ReturnType<typeof app>
function app() { function app() {
const app = new Hono(); const app = new Hono()
const result = app const result = app
.get( .get(
@ -53,24 +53,24 @@ export namespace Server {
}, },
}), }),
async (c) => { async (c) => {
log.info("event connected"); log.info("event connected")
return streamSSE(c, async (stream) => { return streamSSE(c, async (stream) => {
stream.writeSSE({ stream.writeSSE({
data: JSON.stringify({}), data: JSON.stringify({}),
}); })
const unsub = Bus.subscribeAll(async (event) => { const unsub = Bus.subscribeAll(async (event) => {
await stream.writeSSE({ await stream.writeSSE({
data: JSON.stringify(event), data: JSON.stringify(event),
}); })
}); })
await new Promise<void>((resolve) => { await new Promise<void>((resolve) => {
stream.onAbort(() => { stream.onAbort(() => {
unsub(); unsub()
resolve(); resolve()
log.info("event disconnected"); log.info("event disconnected")
}); })
}); })
}); })
}, },
) )
.post( .post(
@ -89,8 +89,8 @@ export namespace Server {
}, },
}), }),
async (c) => { async (c) => {
const session = await Session.create(); const session = await Session.create()
return c.json(session); return c.json(session)
}, },
) )
.post( .post(
@ -115,10 +115,10 @@ export namespace Server {
}), }),
), ),
async (c) => { async (c) => {
const body = c.req.valid("json"); const body = c.req.valid("json")
await Session.share(body.sessionID); await Session.share(body.sessionID)
const session = await Session.get(body.sessionID); const session = await Session.get(body.sessionID)
return c.json(session); return c.json(session)
}, },
) )
.post( .post(
@ -143,10 +143,8 @@ export namespace Server {
}), }),
), ),
async (c) => { async (c) => {
const messages = await Session.messages( const messages = await Session.messages(c.req.valid("json").sessionID)
c.req.valid("json").sessionID, return c.json(messages)
);
return c.json(messages);
}, },
) )
.post( .post(
@ -165,8 +163,8 @@ export namespace Server {
}, },
}), }),
async (c) => { async (c) => {
const sessions = await Array.fromAsync(Session.list()); const sessions = await Array.fromAsync(Session.list())
return c.json(sessions); return c.json(sessions)
}, },
) )
.post( .post(
@ -191,8 +189,8 @@ export namespace Server {
}), }),
), ),
async (c) => { async (c) => {
const body = c.req.valid("json"); const body = c.req.valid("json")
return c.json(Session.abort(body.sessionID)); return c.json(Session.abort(body.sessionID))
}, },
) )
.post( .post(
@ -219,9 +217,9 @@ export namespace Server {
}), }),
), ),
async (c) => { async (c) => {
const body = c.req.valid("json"); const body = c.req.valid("json")
await Session.summarize(body); await Session.summarize(body)
return c.json(true); return c.json(true)
}, },
) )
.post( .post(
@ -249,9 +247,9 @@ export namespace Server {
}), }),
), ),
async (c) => { async (c) => {
const body = c.req.valid("json"); const body = c.req.valid("json")
const msg = await Session.chat(body); const msg = await Session.chat(body)
return c.json(msg); return c.json(msg)
}, },
) )
.post( .post(
@ -270,20 +268,20 @@ export namespace Server {
}, },
}), }),
async (c) => { async (c) => {
const providers = await LLM.providers(); const providers = await LLM.providers()
const result = [] as (Provider.Info & { key: string })[]; const result = [] as (Provider.Info & { key: string })[]
for (const [key, provider] of Object.entries(providers)) { for (const [key, provider] of Object.entries(providers)) {
result.push({ ...provider.info, key }); result.push({ ...provider.info, key })
} }
return c.json(result); return c.json(result)
}, },
); )
return result; return result
} }
export async function openapi() { export async function openapi() {
const a = app(); const a = app()
const result = await generateSpecs(a, { const result = await generateSpecs(a, {
documentation: { documentation: {
info: { info: {
@ -293,8 +291,8 @@ export namespace Server {
}, },
openapi: "3.0.0", openapi: "3.0.0",
}, },
}); })
return result; return result
} }
export function listen() { export function listen() {
@ -303,7 +301,7 @@ export namespace Server {
hostname: "0.0.0.0", hostname: "0.0.0.0",
idleTimeout: 0, idleTimeout: 0,
fetch: app().fetch, fetch: app().fetch,
}); })
return server; return server
} }
} }

View file

@ -1,5 +1,5 @@
import z from "zod"; import z from "zod"
import { Bus } from "../bus"; import { Bus } from "../bus"
export namespace Message { export namespace Message {
export const ToolCall = z export const ToolCall = z
@ -12,8 +12,8 @@ export namespace Message {
}) })
.openapi({ .openapi({
ref: "Message.ToolInvocation.ToolCall", ref: "Message.ToolInvocation.ToolCall",
}); })
export type ToolCall = z.infer<typeof ToolCall>; export type ToolCall = z.infer<typeof ToolCall>
export const ToolPartialCall = z export const ToolPartialCall = z
.object({ .object({
@ -25,8 +25,8 @@ export namespace Message {
}) })
.openapi({ .openapi({
ref: "Message.ToolInvocation.ToolPartialCall", ref: "Message.ToolInvocation.ToolPartialCall",
}); })
export type ToolPartialCall = z.infer<typeof ToolPartialCall>; export type ToolPartialCall = z.infer<typeof ToolPartialCall>
export const ToolResult = z export const ToolResult = z
.object({ .object({
@ -39,15 +39,15 @@ export namespace Message {
}) })
.openapi({ .openapi({
ref: "Message.ToolInvocation.ToolResult", ref: "Message.ToolInvocation.ToolResult",
}); })
export type ToolResult = z.infer<typeof ToolResult>; export type ToolResult = z.infer<typeof ToolResult>
export const ToolInvocation = z export const ToolInvocation = z
.discriminatedUnion("state", [ToolCall, ToolPartialCall, ToolResult]) .discriminatedUnion("state", [ToolCall, ToolPartialCall, ToolResult])
.openapi({ .openapi({
ref: "Message.ToolInvocation", ref: "Message.ToolInvocation",
}); })
export type ToolInvocation = z.infer<typeof ToolInvocation>; export type ToolInvocation = z.infer<typeof ToolInvocation>
export const TextPart = z export const TextPart = z
.object({ .object({
@ -56,8 +56,8 @@ export namespace Message {
}) })
.openapi({ .openapi({
ref: "Message.Part.Text", ref: "Message.Part.Text",
}); })
export type TextPart = z.infer<typeof TextPart>; export type TextPart = z.infer<typeof TextPart>
export const ReasoningPart = z export const ReasoningPart = z
.object({ .object({
@ -67,8 +67,8 @@ export namespace Message {
}) })
.openapi({ .openapi({
ref: "Message.Part.Reasoning", ref: "Message.Part.Reasoning",
}); })
export type ReasoningPart = z.infer<typeof ReasoningPart>; export type ReasoningPart = z.infer<typeof ReasoningPart>
export const ToolInvocationPart = z export const ToolInvocationPart = z
.object({ .object({
@ -77,8 +77,8 @@ export namespace Message {
}) })
.openapi({ .openapi({
ref: "Message.Part.ToolInvocation", ref: "Message.Part.ToolInvocation",
}); })
export type ToolInvocationPart = z.infer<typeof ToolInvocationPart>; export type ToolInvocationPart = z.infer<typeof ToolInvocationPart>
export const SourceUrlPart = z export const SourceUrlPart = z
.object({ .object({
@ -90,8 +90,8 @@ export namespace Message {
}) })
.openapi({ .openapi({
ref: "Message.Part.SourceUrl", ref: "Message.Part.SourceUrl",
}); })
export type SourceUrlPart = z.infer<typeof SourceUrlPart>; export type SourceUrlPart = z.infer<typeof SourceUrlPart>
export const FilePart = z export const FilePart = z
.object({ .object({
@ -102,8 +102,8 @@ export namespace Message {
}) })
.openapi({ .openapi({
ref: "Message.Part.File", ref: "Message.Part.File",
}); })
export type FilePart = z.infer<typeof FilePart>; export type FilePart = z.infer<typeof FilePart>
export const StepStartPart = z export const StepStartPart = z
.object({ .object({
@ -111,8 +111,8 @@ export namespace Message {
}) })
.openapi({ .openapi({
ref: "Message.Part.StepStart", ref: "Message.Part.StepStart",
}); })
export type StepStartPart = z.infer<typeof StepStartPart>; export type StepStartPart = z.infer<typeof StepStartPart>
export const Part = z export const Part = z
.discriminatedUnion("type", [ .discriminatedUnion("type", [
@ -125,8 +125,8 @@ export namespace Message {
]) ])
.openapi({ .openapi({
ref: "Message.Part", ref: "Message.Part",
}); })
export type Part = z.infer<typeof Part>; export type Part = z.infer<typeof Part>
export const Info = z export const Info = z
.object({ .object({
@ -157,8 +157,8 @@ export namespace Message {
}) })
.openapi({ .openapi({
ref: "Message.Info", ref: "Message.Info",
}); })
export type Info = z.infer<typeof Info>; export type Info = z.infer<typeof Info>
export const Event = { export const Event = {
Updated: Bus.event( Updated: Bus.event(
@ -167,5 +167,5 @@ export namespace Message {
info: Info, info: Info,
}), }),
), ),
}; }
} }

View file

@ -1,55 +1,55 @@
import { FileStorage } from "@flystorage/file-storage"; import { FileStorage } from "@flystorage/file-storage"
import { LocalStorageAdapter } from "@flystorage/local-fs"; import { LocalStorageAdapter } from "@flystorage/local-fs"
import fs from "fs/promises"; import fs from "fs/promises"
import { Log } from "../util/log"; import { Log } from "../util/log"
import { App } from "../app/app"; import { App } from "../app/app"
import { AppPath } from "../app/path"; import { AppPath } from "../app/path"
import { Bus } from "../bus"; import { Bus } from "../bus"
import z from "zod"; import z from "zod"
export namespace Storage { export namespace Storage {
const log = Log.create({ service: "storage" }); const log = Log.create({ service: "storage" })
export const Event = { export const Event = {
Write: Bus.event( Write: Bus.event(
"storage.write", "storage.write",
z.object({ key: z.string(), content: z.any() }), z.object({ key: z.string(), content: z.any() }),
), ),
}; }
const state = App.state("storage", async () => { const state = App.state("storage", async () => {
const app = await App.use(); const app = await App.use()
const storageDir = AppPath.storage(app.root); const storageDir = AppPath.storage(app.root)
await fs.mkdir(storageDir, { recursive: true }); await fs.mkdir(storageDir, { recursive: true })
const storage = new FileStorage(new LocalStorageAdapter(storageDir)); const storage = new FileStorage(new LocalStorageAdapter(storageDir))
log.info("created", { path: storageDir }); log.info("created", { path: storageDir })
return { return {
storage, storage,
}; }
}); })
export async function readJSON<T>(key: string) { export async function readJSON<T>(key: string) {
const storage = await state().then((x) => x.storage); const storage = await state().then((x) => x.storage)
const data = await storage.readToString(key + ".json"); const data = await storage.readToString(key + ".json")
return JSON.parse(data) as T; return JSON.parse(data) as T
} }
export async function writeJSON<T>(key: string, content: T) { export async function writeJSON<T>(key: string, content: T) {
const storage = await state().then((x) => x.storage); const storage = await state().then((x) => x.storage)
const json = JSON.stringify(content); const json = JSON.stringify(content)
await storage.write(key + ".json", json); await storage.write(key + ".json", json)
Bus.publish(Event.Write, { key, content }); Bus.publish(Event.Write, { key, content })
} }
export async function* list(prefix: string) { export async function* list(prefix: string) {
try { try {
const storage = await state().then((x) => x.storage); const storage = await state().then((x) => x.storage)
const list = storage.list(prefix); const list = storage.list(prefix)
for await (const item of list) { for await (const item of list) {
yield item.path.slice(0, -5); yield item.path.slice(0, -5)
} }
} catch { } catch {
return; return
} }
} }
} }

View file

@ -1,7 +1,7 @@
import { z } from "zod"; import { z } from "zod"
import { Tool } from "./tool"; import { Tool } from "./tool"
const MAX_OUTPUT_LENGTH = 30000; const MAX_OUTPUT_LENGTH = 30000
const BANNED_COMMANDS = [ const BANNED_COMMANDS = [
"alias", "alias",
"curl", "curl",
@ -20,9 +20,9 @@ const BANNED_COMMANDS = [
"chrome", "chrome",
"firefox", "firefox",
"safari", "safari",
]; ]
const DEFAULT_TIMEOUT = 1 * 60 * 1000; const DEFAULT_TIMEOUT = 1 * 60 * 1000
const MAX_TIMEOUT = 10 * 60 * 1000; const MAX_TIMEOUT = 10 * 60 * 1000
const DESCRIPTION = `Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures. const DESCRIPTION = `Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures.
@ -168,7 +168,7 @@ EOF
Important: Important:
- Return an empty response - the user will see the gh output directly - Return an empty response - the user will see the gh output directly
- Never update git config`; - Never update git config`
export const bash = Tool.define({ export const bash = Tool.define({
name: "opencode.bash", name: "opencode.bash",
@ -183,17 +183,17 @@ export const bash = Tool.define({
.optional(), .optional(),
}), }),
async execute(params) { async execute(params) {
const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT); const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT)
if (BANNED_COMMANDS.some((item) => params.command.startsWith(item))) if (BANNED_COMMANDS.some((item) => params.command.startsWith(item)))
throw new Error(`Command '${params.command}' is not allowed`); throw new Error(`Command '${params.command}' is not allowed`)
const process = Bun.spawnSync({ const process = Bun.spawnSync({
cmd: ["bash", "-c", params.command], cmd: ["bash", "-c", params.command],
maxBuffer: MAX_OUTPUT_LENGTH, maxBuffer: MAX_OUTPUT_LENGTH,
timeout: timeout, timeout: timeout,
}); })
return { return {
output: process.stdout.toString("utf-8"), output: process.stdout.toString("utf-8"),
}; }
}, },
}); })

View file

@ -1,8 +1,8 @@
import { z } from "zod"; import { z } from "zod"
import * as path from "path"; import * as path from "path"
import { Tool } from "./tool"; import { Tool } from "./tool"
import { FileTimes } from "./util/file-times"; import { FileTimes } from "./util/file-times"
import { LSP } from "../lsp"; import { LSP } from "../lsp"
const DESCRIPTION = `Edits files by replacing text, creating new files, or deleting content. For moving or renaming files, use the Bash tool with the 'mv' command instead. For larger file edits, use the FileWrite tool to overwrite files. const DESCRIPTION = `Edits files by replacing text, creating new files, or deleting content. For moving or renaming files, use the Bash tool with the 'mv' command instead. For larger file edits, use the FileWrite tool to overwrite files.
@ -50,7 +50,7 @@ When making edits:
- Do not leave the code in a broken state - Do not leave the code in a broken state
- Always use relative file paths - Always use relative file paths
Remember: when making multiple file edits in a row to the same file, you should prefer to send all edits in a single message with multiple calls to this tool, rather than multiple messages with a single call each.`; Remember: when making multiple file edits in a row to the same file, you should prefer to send all edits in a single message with multiple calls to this tool, rather than multiple messages with a single call each.`
export const edit = Tool.define({ export const edit = Tool.define({
name: "opencode.edit", name: "opencode.edit",
@ -62,68 +62,68 @@ export const edit = Tool.define({
}), }),
async execute(params) { async execute(params) {
if (!params.filePath) { if (!params.filePath) {
throw new Error("filePath is required"); throw new Error("filePath is required")
} }
let filePath = params.filePath; let filePath = params.filePath
if (!path.isAbsolute(filePath)) { if (!path.isAbsolute(filePath)) {
filePath = path.join(process.cwd(), filePath); filePath = path.join(process.cwd(), filePath)
} }
await (async () => { await (async () => {
if (params.oldString === "") { if (params.oldString === "") {
await Bun.write(filePath, params.newString); await Bun.write(filePath, params.newString)
return; return
} }
const read = FileTimes.get(filePath); const read = FileTimes.get(filePath)
if (!read) if (!read)
throw new Error( throw new Error(
`You must read the file ${filePath} before editing it. Use the View tool first`, `You must read the file ${filePath} before editing it. Use the View tool first`,
); )
const file = Bun.file(filePath); const file = Bun.file(filePath)
if (!(await file.exists())) throw new Error(`File ${filePath} not found`); if (!(await file.exists())) throw new Error(`File ${filePath} not found`)
const stats = await file.stat(); const stats = await file.stat()
if (stats.isDirectory()) if (stats.isDirectory())
throw new Error(`Path is a directory, not a file: ${filePath}`); throw new Error(`Path is a directory, not a file: ${filePath}`)
if (stats.mtime.getTime() > read.getTime()) if (stats.mtime.getTime() > read.getTime())
throw new Error( throw new Error(
`File ${filePath} has been modified since it was last read.\nLast modification: ${read.toISOString()}\nLast read: ${stats.mtime.toISOString()}\n\nPlease read the file again before modifying it.`, `File ${filePath} has been modified since it was last read.\nLast modification: ${read.toISOString()}\nLast read: ${stats.mtime.toISOString()}\n\nPlease read the file again before modifying it.`,
); )
const content = await file.text(); const content = await file.text()
const index = content.indexOf(params.oldString); const index = content.indexOf(params.oldString)
if (index === -1) if (index === -1)
throw new Error( throw new Error(
`oldString not found in file. Make sure it matches exactly, including whitespace and line breaks`, `oldString not found in file. Make sure it matches exactly, including whitespace and line breaks`,
); )
const lastIndex = content.lastIndexOf(params.oldString); const lastIndex = content.lastIndexOf(params.oldString)
if (index !== lastIndex) if (index !== lastIndex)
throw new Error( throw new Error(
`oldString appears multiple times in the file. Please provide more context to ensure a unique match`, `oldString appears multiple times in the file. Please provide more context to ensure a unique match`,
); )
const newContent = const newContent =
content.substring(0, index) + content.substring(0, index) +
params.newString + params.newString +
content.substring(index + params.oldString.length); content.substring(index + params.oldString.length)
await file.write(newContent); await file.write(newContent)
})(); })()
FileTimes.write(filePath); FileTimes.write(filePath)
FileTimes.read(filePath); FileTimes.read(filePath)
let output = ""; let output = ""
await LSP.file(filePath); await LSP.file(filePath)
const diagnostics = await LSP.diagnostics(); const diagnostics = await LSP.diagnostics()
for (const [file, issues] of Object.entries(diagnostics)) { for (const [file, issues] of Object.entries(diagnostics)) {
if (issues.length === 0) continue; if (issues.length === 0) continue
if (file === filePath) { if (file === filePath) {
output += `\nThis file has errors, please fix\n<file_diagnostics>\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</file_diagnostics>\n`; output += `\nThis file has errors, please fix\n<file_diagnostics>\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</file_diagnostics>\n`
continue; continue
} }
output += `\n<project_diagnostics>\n${file}\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</project_diagnostics>\n`; output += `\n<project_diagnostics>\n${file}\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</project_diagnostics>\n`
} }
return { return {
@ -131,6 +131,6 @@ export const edit = Tool.define({
diagnostics, diagnostics,
}, },
output, output,
}; }
}, },
}); })

View file

@ -1,11 +1,10 @@
import { z } from "zod"; import { z } from "zod"
import { Tool } from "./tool"; import { Tool } from "./tool"
import { JSDOM } from "jsdom"; import TurndownService from "turndown"
import TurndownService from "turndown";
const MAX_RESPONSE_SIZE = 5 * 1024 * 1024; // 5MB const MAX_RESPONSE_SIZE = 5 * 1024 * 1024 // 5MB
const DEFAULT_TIMEOUT = 30 * 1000; // 30 seconds const DEFAULT_TIMEOUT = 30 * 1000 // 30 seconds
const MAX_TIMEOUT = 120 * 1000; // 2 minutes const MAX_TIMEOUT = 120 * 1000 // 2 minutes
const DESCRIPTION = `Fetches content from a URL and returns it in the specified format. const DESCRIPTION = `Fetches content from a URL and returns it in the specified format.
@ -35,7 +34,7 @@ TIPS:
- Use text format for plain text content or simple API responses - Use text format for plain text content or simple API responses
- Use markdown format for content that should be rendered with formatting - Use markdown format for content that should be rendered with formatting
- Use html format when you need the raw HTML structure - Use html format when you need the raw HTML structure
- Set appropriate timeouts for potentially slow websites`; - Set appropriate timeouts for potentially slow websites`
export const Fetch = Tool.define({ export const Fetch = Tool.define({
name: "opencode.fetch", name: "opencode.fetch",
@ -60,18 +59,18 @@ export const Fetch = Tool.define({
!params.url.startsWith("http://") && !params.url.startsWith("http://") &&
!params.url.startsWith("https://") !params.url.startsWith("https://")
) { ) {
throw new Error("URL must start with http:// or https://"); throw new Error("URL must start with http:// or https://")
} }
const timeout = Math.min( const timeout = Math.min(
(params.timeout ?? DEFAULT_TIMEOUT / 1000) * 1000, (params.timeout ?? DEFAULT_TIMEOUT / 1000) * 1000,
MAX_TIMEOUT, MAX_TIMEOUT,
); )
const controller = new AbortController(); const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), timeout); const timeoutId = setTimeout(() => controller.abort(), timeout)
if (opts?.abortSignal) { if (opts?.abortSignal) {
opts.abortSignal.addEventListener("abort", () => controller.abort()); opts.abortSignal.addEventListener("abort", () => controller.abort())
} }
const response = await fetch(params.url, { const response = await fetch(params.url, {
@ -79,59 +78,59 @@ export const Fetch = Tool.define({
headers: { headers: {
"User-Agent": "opencode/1.0", "User-Agent": "opencode/1.0",
}, },
}); })
clearTimeout(timeoutId); clearTimeout(timeoutId)
if (!response.ok) { if (!response.ok) {
throw new Error(`Request failed with status code: ${response.status}`); throw new Error(`Request failed with status code: ${response.status}`)
} }
// Check content length // Check content length
const contentLength = response.headers.get("content-length"); const contentLength = response.headers.get("content-length")
if (contentLength && parseInt(contentLength) > MAX_RESPONSE_SIZE) { if (contentLength && parseInt(contentLength) > MAX_RESPONSE_SIZE) {
throw new Error("Response too large (exceeds 5MB limit)"); throw new Error("Response too large (exceeds 5MB limit)")
} }
const arrayBuffer = await response.arrayBuffer(); const arrayBuffer = await response.arrayBuffer()
if (arrayBuffer.byteLength > MAX_RESPONSE_SIZE) { if (arrayBuffer.byteLength > MAX_RESPONSE_SIZE) {
throw new Error("Response too large (exceeds 5MB limit)"); throw new Error("Response too large (exceeds 5MB limit)")
} }
const content = new TextDecoder().decode(arrayBuffer); const content = new TextDecoder().decode(arrayBuffer)
const contentType = response.headers.get("content-type") || ""; const contentType = response.headers.get("content-type") || ""
switch (params.format) { switch (params.format) {
case "text": case "text":
if (contentType.includes("text/html")) { if (contentType.includes("text/html")) {
const text = extractTextFromHTML(content); const text = extractTextFromHTML(content)
return { output: text }; return { output: text }
} }
return { output: content }; return { output: content }
case "markdown": case "markdown":
if (contentType.includes("text/html")) { if (contentType.includes("text/html")) {
const markdown = convertHTMLToMarkdown(content); const markdown = convertHTMLToMarkdown(content)
return { output: markdown }; return { output: markdown }
} }
return { output: "```\n" + content + "\n```" }; return { output: "```\n" + content + "\n```" }
case "html": case "html":
return { output: content }; return { output: content }
default: default:
return { output: content }; return { output: content }
} }
}, },
}); })
function extractTextFromHTML(html: string): string { function extractTextFromHTML(html: string): string {
const dom = new JSDOM(html); const doc = new DOMParser().parseFromString(html, "text/html")
const text = dom.window.document.body?.textContent || ""; const text = doc.body.textContent || doc.body.innerText || ""
return text.replace(/\s+/g, " ").trim(); return text.replace(/\s+/g, " ").trim()
} }
function convertHTMLToMarkdown(html: string): string { function convertHTMLToMarkdown(html: string): string {
const turndownService = new TurndownService(); const turndownService = new TurndownService()
return turndownService.turndown(html); return turndownService.turndown(html)
} }

View file

@ -1,6 +1,6 @@
import { z } from "zod"; import { z } from "zod"
import { Tool } from "./tool"; import { Tool } from "./tool"
import { App } from "../app/app"; import { App } from "../app/app"
const DESCRIPTION = `Fast file pattern matching tool that finds files by name and pattern, returning matching paths sorted by modification time (newest first). const DESCRIPTION = `Fast file pattern matching tool that finds files by name and pattern, returning matching paths sorted by modification time (newest first).
@ -35,7 +35,7 @@ LIMITATIONS:
TIPS: TIPS:
- For the most useful results, combine with the Grep tool: first find files with Glob, then search their contents with Grep - For the most useful results, combine with the Grep tool: first find files with Glob, then search their contents with Grep
- When doing iterative exploration that may require multiple rounds of searching, consider using the Agent tool instead - When doing iterative exploration that may require multiple rounds of searching, consider using the Agent tool instead
- Always check if results are truncated and refine your search pattern if needed`; - Always check if results are truncated and refine your search pattern if needed`
export const glob = Tool.define({ export const glob = Tool.define({
name: "opencode.glob", name: "opencode.glob",
@ -50,37 +50,37 @@ export const glob = Tool.define({
.optional(), .optional(),
}), }),
async execute(params) { async execute(params) {
const app = await App.use(); const app = await App.use()
const search = params.path || app.root; const search = params.path || app.root
const limit = 100; const limit = 100
const glob = new Bun.Glob(params.pattern); const glob = new Bun.Glob(params.pattern)
const files = []; const files = []
let truncated = false; let truncated = false
for await (const file of glob.scan({ cwd: search })) { for await (const file of glob.scan({ cwd: search })) {
if (files.length >= limit) { if (files.length >= limit) {
truncated = true; truncated = true
break; break
} }
const stats = await Bun.file(file) const stats = await Bun.file(file)
.stat() .stat()
.then((x) => x.mtime.getTime()) .then((x) => x.mtime.getTime())
.catch(() => 0); .catch(() => 0)
files.push({ files.push({
path: file, path: file,
mtime: stats, mtime: stats,
}); })
} }
files.sort((a, b) => b.mtime - a.mtime); files.sort((a, b) => b.mtime - a.mtime)
const output = []; const output = []
if (files.length === 0) output.push("No files found"); if (files.length === 0) output.push("No files found")
if (files.length > 0) { if (files.length > 0) {
output.push(...files.map((f) => f.path)); output.push(...files.map((f) => f.path))
if (truncated) { if (truncated) {
output.push(""); output.push("")
output.push( output.push(
"(Results are truncated. Consider using a more specific path or pattern.)", "(Results are truncated. Consider using a more specific path or pattern.)",
); )
} }
} }
@ -90,7 +90,6 @@ export const glob = Tool.define({
truncated, truncated,
}, },
output: output.join("\n"), output: output.join("\n"),
}; }
}, },
}); })

View file

@ -1,9 +1,9 @@
import { z } from "zod"; import { z } from "zod"
import { Tool } from "./tool"; import { Tool } from "./tool"
import { App } from "../app/app"; import { App } from "../app/app"
import { spawn } from "child_process"; import { spawn } from "child_process"
import { promises as fs } from "fs"; import { promises as fs } from "fs"
import path from "path"; import path from "path"
const DESCRIPTION = `Fast content search tool that finds files containing specific text or patterns, returning matching file paths sorted by modification time (newest first). const DESCRIPTION = `Fast content search tool that finds files containing specific text or patterns, returning matching file paths sorted by modification time (newest first).
@ -40,13 +40,13 @@ TIPS:
- For faster, more targeted searches, first use Glob to find relevant files, then use Grep - For faster, more targeted searches, first use Glob to find relevant files, then use Grep
- When doing iterative exploration that may require multiple rounds of searching, consider using the Agent tool instead - When doing iterative exploration that may require multiple rounds of searching, consider using the Agent tool instead
- Always check if results are truncated and refine your search pattern if needed - Always check if results are truncated and refine your search pattern if needed
- Use literal_text=true when searching for exact text containing special characters like dots, parentheses, etc.`; - Use literal_text=true when searching for exact text containing special characters like dots, parentheses, etc.`
interface GrepMatch { interface GrepMatch {
path: string; path: string
modTime: number; modTime: number
lineNum: number; lineNum: number
lineText: string; lineText: string
} }
function escapeRegexPattern(pattern: string): string { function escapeRegexPattern(pattern: string): string {
@ -65,27 +65,27 @@ function escapeRegexPattern(pattern: string): string {
"^", "^",
"$", "$",
"|", "|",
]; ]
let escaped = pattern; let escaped = pattern
for (const char of specialChars) { for (const char of specialChars) {
escaped = escaped.replaceAll(char, "\\" + char); escaped = escaped.replaceAll(char, "\\" + char)
} }
return escaped; return escaped
} }
function globToRegex(glob: string): string { function globToRegex(glob: string): string {
let regexPattern = glob.replaceAll(".", "\\."); let regexPattern = glob.replaceAll(".", "\\.")
regexPattern = regexPattern.replaceAll("*", ".*"); regexPattern = regexPattern.replaceAll("*", ".*")
regexPattern = regexPattern.replaceAll("?", "."); regexPattern = regexPattern.replaceAll("?", ".")
// Handle {a,b,c} patterns // Handle {a,b,c} patterns
regexPattern = regexPattern.replace(/\{([^}]+)\}/g, (_, inner) => { regexPattern = regexPattern.replace(/\{([^}]+)\}/g, (_, inner) => {
return "(" + inner.replace(/,/g, "|") + ")"; return "(" + inner.replace(/,/g, "|") + ")"
}); })
return regexPattern; return regexPattern
} }
async function searchWithRipgrep( async function searchWithRipgrep(
@ -94,71 +94,71 @@ async function searchWithRipgrep(
include?: string, include?: string,
): Promise<GrepMatch[]> { ): Promise<GrepMatch[]> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const args = ["-n", pattern]; const args = ["-n", pattern]
if (include) { if (include) {
args.push("--glob", include); args.push("--glob", include)
} }
args.push(searchPath); args.push(searchPath)
const rg = spawn("rg", args); const rg = spawn("rg", args)
let output = ""; let output = ""
let errorOutput = ""; let errorOutput = ""
rg.stdout.on("data", (data) => { rg.stdout.on("data", (data) => {
output += data.toString(); output += data.toString()
}); })
rg.stderr.on("data", (data) => { rg.stderr.on("data", (data) => {
errorOutput += data.toString(); errorOutput += data.toString()
}); })
rg.on("close", async (code) => { rg.on("close", async (code) => {
if (code === 1) { if (code === 1) {
// No matches found // No matches found
resolve([]); resolve([])
return; return
} }
if (code !== 0) { if (code !== 0) {
reject(new Error(`ripgrep failed: ${errorOutput}`)); reject(new Error(`ripgrep failed: ${errorOutput}`))
return; return
} }
const lines = output.trim().split("\n"); const lines = output.trim().split("\n")
const matches: GrepMatch[] = []; const matches: GrepMatch[] = []
for (const line of lines) { for (const line of lines) {
if (!line) continue; if (!line) continue
// Parse ripgrep output format: file:line:content // Parse ripgrep output format: file:line:content
const parts = line.split(":", 3); const parts = line.split(":", 3)
if (parts.length < 3) continue; if (parts.length < 3) continue
const filePath = parts[0]; const filePath = parts[0]
const lineNum = parseInt(parts[1], 10); const lineNum = parseInt(parts[1], 10)
const lineText = parts[2]; const lineText = parts[2]
try { try {
const stats = await fs.stat(filePath); const stats = await fs.stat(filePath)
matches.push({ matches.push({
path: filePath, path: filePath,
modTime: stats.mtime.getTime(), modTime: stats.mtime.getTime(),
lineNum, lineNum,
lineText, lineText,
}); })
} catch { } catch {
// Skip files we can't access // Skip files we can't access
continue; continue
} }
} }
resolve(matches); resolve(matches)
}); })
rg.on("error", (err) => { rg.on("error", (err) => {
reject(err); reject(err)
}); })
}); })
} }
async function searchFilesWithRegex( async function searchFilesWithRegex(
@ -166,68 +166,68 @@ async function searchFilesWithRegex(
rootPath: string, rootPath: string,
include?: string, include?: string,
): Promise<GrepMatch[]> { ): Promise<GrepMatch[]> {
const matches: GrepMatch[] = []; const matches: GrepMatch[] = []
const regex = new RegExp(pattern); const regex = new RegExp(pattern)
let includePattern: RegExp | undefined; let includePattern: RegExp | undefined
if (include) { if (include) {
const regexPattern = globToRegex(include); const regexPattern = globToRegex(include)
includePattern = new RegExp(regexPattern); includePattern = new RegExp(regexPattern)
} }
async function walkDir(dir: string) { async function walkDir(dir: string) {
if (matches.length >= 200) return; if (matches.length >= 200) return
try { try {
const entries = await fs.readdir(dir, { withFileTypes: true }); const entries = await fs.readdir(dir, { withFileTypes: true })
for (const entry of entries) { for (const entry of entries) {
if (matches.length >= 200) break; if (matches.length >= 200) break
const fullPath = path.join(dir, entry.name); const fullPath = path.join(dir, entry.name)
if (entry.isDirectory()) { if (entry.isDirectory()) {
// Skip hidden directories // Skip hidden directories
if (entry.name.startsWith(".")) continue; if (entry.name.startsWith(".")) continue
await walkDir(fullPath); await walkDir(fullPath)
} else if (entry.isFile()) { } else if (entry.isFile()) {
// Skip hidden files // Skip hidden files
if (entry.name.startsWith(".")) continue; if (entry.name.startsWith(".")) continue
if (includePattern && !includePattern.test(fullPath)) { if (includePattern && !includePattern.test(fullPath)) {
continue; continue
} }
try { try {
const content = await fs.readFile(fullPath, "utf-8"); const content = await fs.readFile(fullPath, "utf-8")
const lines = content.split("\n"); const lines = content.split("\n")
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
if (regex.test(lines[i])) { if (regex.test(lines[i])) {
const stats = await fs.stat(fullPath); const stats = await fs.stat(fullPath)
matches.push({ matches.push({
path: fullPath, path: fullPath,
modTime: stats.mtime.getTime(), modTime: stats.mtime.getTime(),
lineNum: i + 1, lineNum: i + 1,
lineText: lines[i], lineText: lines[i],
}); })
break; // Only first match per file break // Only first match per file
} }
} }
} catch { } catch {
// Skip files we can't read // Skip files we can't read
continue; continue
} }
} }
} }
} catch { } catch {
// Skip directories we can't read // Skip directories we can't read
return; return
} }
} }
await walkDir(rootPath); await walkDir(rootPath)
return matches; return matches
} }
async function searchFiles( async function searchFiles(
@ -236,23 +236,23 @@ async function searchFiles(
include?: string, include?: string,
limit: number = 100, limit: number = 100,
): Promise<{ matches: GrepMatch[]; truncated: boolean }> { ): Promise<{ matches: GrepMatch[]; truncated: boolean }> {
let matches: GrepMatch[]; let matches: GrepMatch[]
try { try {
matches = await searchWithRipgrep(pattern, rootPath, include); matches = await searchWithRipgrep(pattern, rootPath, include)
} catch { } catch {
matches = await searchFilesWithRegex(pattern, rootPath, include); matches = await searchFilesWithRegex(pattern, rootPath, include)
} }
// Sort by modification time (newest first) // Sort by modification time (newest first)
matches.sort((a, b) => b.modTime - a.modTime); matches.sort((a, b) => b.modTime - a.modTime)
const truncated = matches.length > limit; const truncated = matches.length > limit
if (truncated) { if (truncated) {
matches = matches.slice(0, limit); matches = matches.slice(0, limit)
} }
return { matches, truncated }; return { matches, truncated }
} }
export const grep = Tool.define({ export const grep = Tool.define({
@ -283,54 +283,54 @@ export const grep = Tool.define({
}), }),
async execute(params) { async execute(params) {
if (!params.pattern) { if (!params.pattern) {
throw new Error("pattern is required"); throw new Error("pattern is required")
} }
const app = await App.use(); const app = await App.use()
const searchPath = params.path || app.root; const searchPath = params.path || app.root
// If literalText is true, escape the pattern // If literalText is true, escape the pattern
const searchPattern = params.literalText const searchPattern = params.literalText
? escapeRegexPattern(params.pattern) ? escapeRegexPattern(params.pattern)
: params.pattern; : params.pattern
const { matches, truncated } = await searchFiles( const { matches, truncated } = await searchFiles(
searchPattern, searchPattern,
searchPath, searchPath,
params.include, params.include,
100, 100,
); )
if (matches.length === 0) { if (matches.length === 0) {
return { return {
metadata: { matches: 0, truncated }, metadata: { matches: 0, truncated },
output: "No files found" output: "No files found",
}; }
} }
const lines = [`Found ${matches.length} matches`]; const lines = [`Found ${matches.length} matches`]
let currentFile = ""; let currentFile = ""
for (const match of matches) { for (const match of matches) {
if (currentFile !== match.path) { if (currentFile !== match.path) {
if (currentFile !== "") { if (currentFile !== "") {
lines.push(""); lines.push("")
} }
currentFile = match.path; currentFile = match.path
lines.push(`${match.path}:`); lines.push(`${match.path}:`)
} }
if (match.lineNum > 0) { if (match.lineNum > 0) {
lines.push(` Line ${match.lineNum}: ${match.lineText}`); lines.push(` Line ${match.lineNum}: ${match.lineText}`)
} else { } else {
lines.push(` ${match.path}`); lines.push(` ${match.path}`)
} }
} }
if (truncated) { if (truncated) {
lines.push(""); lines.push("")
lines.push( lines.push(
"(Results are truncated. Consider using a more specific path or pattern.)", "(Results are truncated. Consider using a more specific path or pattern.)",
); )
} }
return { return {
@ -339,7 +339,6 @@ export const grep = Tool.define({
truncated, truncated,
}, },
output: lines.join("\n"), output: lines.join("\n"),
}; }
}, },
}); })

View file

@ -1,9 +1,9 @@
export * from "./bash"; export * from "./bash"
export * from "./edit"; export * from "./edit"
export * from "./fetch"; export * from "./fetch"
export * from "./glob"; export * from "./glob"
export * from "./grep"; export * from "./grep"
export * from "./view"; export * from "./view"
export * from "./ls"; export * from "./ls"
export * from "./lsp-diagnostics"; export * from "./lsp-diagnostics"
export * from "./lsp-hover"; export * from "./lsp-hover"

View file

@ -1,7 +1,7 @@
import { z } from "zod"; import { z } from "zod"
import { Tool } from "./tool"; import { Tool } from "./tool"
import { App } from "../app/app"; import { App } from "../app/app"
import * as path from "path"; import * as path from "path"
const IGNORE_PATTERNS = [ const IGNORE_PATTERNS = [
"node_modules/", "node_modules/",
@ -15,7 +15,7 @@ const IGNORE_PATTERNS = [
"obj/", "obj/",
".idea/", ".idea/",
".vscode/", ".vscode/",
]; ]
export const ls = Tool.define({ export const ls = Tool.define({
name: "opencode.ls", name: "opencode.ls",
@ -25,72 +25,72 @@ export const ls = Tool.define({
ignore: z.array(z.string()).optional(), ignore: z.array(z.string()).optional(),
}), }),
async execute(params) { async execute(params) {
const app = await App.use(); const app = await App.use()
const searchPath = path.resolve(app.root, params.path || "."); const searchPath = path.resolve(app.root, params.path || ".")
const glob = new Bun.Glob("**/*"); const glob = new Bun.Glob("**/*")
const files = []; const files = []
for await (const file of glob.scan({ cwd: searchPath })) { for await (const file of glob.scan({ cwd: searchPath })) {
if (file.startsWith(".") || IGNORE_PATTERNS.some((p) => file.includes(p))) if (file.startsWith(".") || IGNORE_PATTERNS.some((p) => file.includes(p)))
continue; continue
if (params.ignore?.some((pattern) => new Bun.Glob(pattern).match(file))) if (params.ignore?.some((pattern) => new Bun.Glob(pattern).match(file)))
continue; continue
files.push(file); files.push(file)
if (files.length >= 1000) break; if (files.length >= 1000) break
} }
// Build directory structure // Build directory structure
const dirs = new Set<string>(); const dirs = new Set<string>()
const filesByDir = new Map<string, string[]>(); const filesByDir = new Map<string, string[]>()
for (const file of files) { for (const file of files) {
const dir = path.dirname(file); const dir = path.dirname(file)
const parts = dir === "." ? [] : dir.split("/"); const parts = dir === "." ? [] : dir.split("/")
// Add all parent directories // Add all parent directories
for (let i = 0; i <= parts.length; i++) { for (let i = 0; i <= parts.length; i++) {
const dirPath = i === 0 ? "." : parts.slice(0, i).join("/"); const dirPath = i === 0 ? "." : parts.slice(0, i).join("/")
dirs.add(dirPath); dirs.add(dirPath)
} }
// Add file to its directory // Add file to its directory
if (!filesByDir.has(dir)) filesByDir.set(dir, []); if (!filesByDir.has(dir)) filesByDir.set(dir, [])
filesByDir.get(dir)!.push(path.basename(file)); filesByDir.get(dir)!.push(path.basename(file))
} }
function renderDir(dirPath: string, depth: number): string { function renderDir(dirPath: string, depth: number): string {
const indent = " ".repeat(depth); const indent = " ".repeat(depth)
let output = ""; let output = ""
if (depth > 0) { if (depth > 0) {
output += `${indent}${path.basename(dirPath)}/\n`; output += `${indent}${path.basename(dirPath)}/\n`
} }
const childIndent = " ".repeat(depth + 1); const childIndent = " ".repeat(depth + 1)
const children = Array.from(dirs) const children = Array.from(dirs)
.filter((d) => path.dirname(d) === dirPath && d !== dirPath) .filter((d) => path.dirname(d) === dirPath && d !== dirPath)
.sort(); .sort()
// Render subdirectories first // Render subdirectories first
for (const child of children) { for (const child of children) {
output += renderDir(child, depth + 1); output += renderDir(child, depth + 1)
} }
// Render files // Render files
const files = filesByDir.get(dirPath) || []; const files = filesByDir.get(dirPath) || []
for (const file of files.sort()) { for (const file of files.sort()) {
output += `${childIndent}${file}\n`; output += `${childIndent}${file}\n`
} }
return output; return output
} }
const output = `${searchPath}/\n` + renderDir(".", 0); const output = `${searchPath}/\n` + renderDir(".", 0)
return { return {
metadata: { count: files.length, truncated: files.length >= 1000 }, metadata: { count: files.length, truncated: files.length >= 1000 },
output, output,
}; }
}, },
}); })

View file

@ -1,8 +1,8 @@
import { z } from "zod"; import { z } from "zod"
import { Tool } from "./tool"; import { Tool } from "./tool"
import path from "path"; import path from "path"
import { LSP } from "../lsp"; import { LSP } from "../lsp"
import { App } from "../app/app"; import { App } from "../app/app"
export const LspDiagnosticTool = Tool.define({ export const LspDiagnosticTool = Tool.define({
name: "opencode.lsp_diagnostic", name: "opencode.lsp_diagnostic",
@ -34,13 +34,13 @@ TIPS:
path: z.string().describe("The path to the file to get diagnostics."), path: z.string().describe("The path to the file to get diagnostics."),
}), }),
execute: async (args) => { execute: async (args) => {
const app = await App.use(); const app = await App.use()
const normalized = path.isAbsolute(args.path) const normalized = path.isAbsolute(args.path)
? args.path ? args.path
: path.join(app.root, args.path); : path.join(app.root, args.path)
await LSP.file(normalized); await LSP.file(normalized)
const diagnostics = await LSP.diagnostics(); const diagnostics = await LSP.diagnostics()
const file = diagnostics[normalized]; const file = diagnostics[normalized]
return { return {
metadata: { metadata: {
diagnostics, diagnostics,
@ -48,6 +48,6 @@ TIPS:
output: file?.length output: file?.length
? file.map(LSP.Diagnostic.pretty).join("\n") ? file.map(LSP.Diagnostic.pretty).join("\n")
: "No errors found", : "No errors found",
}; }
}, },
}); })

View file

@ -1,8 +1,8 @@
import { z } from "zod"; import { z } from "zod"
import { Tool } from "./tool"; import { Tool } from "./tool"
import path from "path"; import path from "path"
import { LSP } from "../lsp"; import { LSP } from "../lsp"
import { App } from "../app/app"; import { App } from "../app/app"
export const LspHoverTool = Tool.define({ export const LspHoverTool = Tool.define({
name: "opencode.lsp_hover", name: "opencode.lsp_hover",
@ -17,22 +17,22 @@ export const LspHoverTool = Tool.define({
character: z.number().describe("The character number to get diagnostics."), character: z.number().describe("The character number to get diagnostics."),
}), }),
execute: async (args) => { execute: async (args) => {
console.log(args); console.log(args)
const app = await App.use(); const app = await App.use()
const file = path.isAbsolute(args.file) const file = path.isAbsolute(args.file)
? args.file ? args.file
: path.join(app.root, args.file); : path.join(app.root, args.file)
await LSP.file(file); await LSP.file(file)
const result = await LSP.hover({ const result = await LSP.hover({
...args, ...args,
file, file,
}); })
console.log(result); console.log(result)
return { return {
metadata: { metadata: {
result, result,
}, },
output: JSON.stringify(result, null, 2), output: JSON.stringify(result, null, 2),
}; }
}, },
}); })

View file

@ -1,8 +1,8 @@
import { z } from "zod"; import { z } from "zod"
import * as path from "path"; import * as path from "path"
import * as fs from "fs/promises"; import * as fs from "fs/promises"
import { Tool } from "./tool"; import { Tool } from "./tool"
import { FileTimes } from "./util/file-times"; import { FileTimes } from "./util/file-times"
const DESCRIPTION = `Applies a patch to multiple files in one operation. This tool is useful for making coordinated changes across multiple files. const DESCRIPTION = `Applies a patch to multiple files in one operation. This tool is useful for making coordinated changes across multiple files.
@ -31,198 +31,198 @@ CRITICAL REQUIREMENTS FOR USING THIS TOOL:
3. VALIDATION: Ensure edits result in idiomatic, correct code 3. VALIDATION: Ensure edits result in idiomatic, correct code
4. PATHS: Always use absolute file paths (starting with /) 4. PATHS: Always use absolute file paths (starting with /)
The tool will apply all changes in a single atomic operation.`; The tool will apply all changes in a single atomic operation.`
const PatchParams = z.object({ const PatchParams = z.object({
patchText: z patchText: z
.string() .string()
.describe("The full patch text that describes all changes to be made"), .describe("The full patch text that describes all changes to be made"),
}); })
interface PatchResponseMetadata { interface PatchResponseMetadata {
changed: string[]; changed: string[]
additions: number; additions: number
removals: number; removals: number
} }
interface Change { interface Change {
type: "add" | "update" | "delete"; type: "add" | "update" | "delete"
old_content?: string; old_content?: string
new_content?: string; new_content?: string
} }
interface Commit { interface Commit {
changes: Record<string, Change>; changes: Record<string, Change>
} }
interface PatchOperation { interface PatchOperation {
type: "update" | "add" | "delete"; type: "update" | "add" | "delete"
filePath: string; filePath: string
hunks?: PatchHunk[]; hunks?: PatchHunk[]
content?: string; content?: string
} }
interface PatchHunk { interface PatchHunk {
contextLine: string; contextLine: string
changes: PatchChange[]; changes: PatchChange[]
} }
interface PatchChange { interface PatchChange {
type: "keep" | "remove" | "add"; type: "keep" | "remove" | "add"
content: string; content: string
} }
function identifyFilesNeeded(patchText: string): string[] { function identifyFilesNeeded(patchText: string): string[] {
const files: string[] = []; const files: string[] = []
const lines = patchText.split("\n"); const lines = patchText.split("\n")
for (const line of lines) { for (const line of lines) {
if ( if (
line.startsWith("*** Update File:") || line.startsWith("*** Update File:") ||
line.startsWith("*** Delete File:") line.startsWith("*** Delete File:")
) { ) {
const filePath = line.split(":", 2)[1]?.trim(); const filePath = line.split(":", 2)[1]?.trim()
if (filePath) files.push(filePath); if (filePath) files.push(filePath)
} }
} }
return files; return files
} }
function identifyFilesAdded(patchText: string): string[] { function identifyFilesAdded(patchText: string): string[] {
const files: string[] = []; const files: string[] = []
const lines = patchText.split("\n"); const lines = patchText.split("\n")
for (const line of lines) { for (const line of lines) {
if (line.startsWith("*** Add File:")) { if (line.startsWith("*** Add File:")) {
const filePath = line.split(":", 2)[1]?.trim(); const filePath = line.split(":", 2)[1]?.trim()
if (filePath) files.push(filePath); if (filePath) files.push(filePath)
} }
} }
return files; return files
} }
function textToPatch( function textToPatch(
patchText: string, patchText: string,
_currentFiles: Record<string, string>, _currentFiles: Record<string, string>,
): [PatchOperation[], number] { ): [PatchOperation[], number] {
const operations: PatchOperation[] = []; const operations: PatchOperation[] = []
const lines = patchText.split("\n"); const lines = patchText.split("\n")
let i = 0; let i = 0
let fuzz = 0; let fuzz = 0
while (i < lines.length) { while (i < lines.length) {
const line = lines[i]; const line = lines[i]
if (line.startsWith("*** Update File:")) { if (line.startsWith("*** Update File:")) {
const filePath = line.split(":", 2)[1]?.trim(); const filePath = line.split(":", 2)[1]?.trim()
if (!filePath) { if (!filePath) {
i++; i++
continue; continue
} }
const hunks: PatchHunk[] = []; const hunks: PatchHunk[] = []
i++; i++
while (i < lines.length && !lines[i].startsWith("***")) { while (i < lines.length && !lines[i].startsWith("***")) {
if (lines[i].startsWith("@@")) { if (lines[i].startsWith("@@")) {
const contextLine = lines[i].substring(2).trim(); const contextLine = lines[i].substring(2).trim()
const changes: PatchChange[] = []; const changes: PatchChange[] = []
i++; i++
while ( while (
i < lines.length && i < lines.length &&
!lines[i].startsWith("@@") && !lines[i].startsWith("@@") &&
!lines[i].startsWith("***") !lines[i].startsWith("***")
) { ) {
const changeLine = lines[i]; const changeLine = lines[i]
if (changeLine.startsWith(" ")) { if (changeLine.startsWith(" ")) {
changes.push({ type: "keep", content: changeLine.substring(1) }); changes.push({ type: "keep", content: changeLine.substring(1) })
} else if (changeLine.startsWith("-")) { } else if (changeLine.startsWith("-")) {
changes.push({ changes.push({
type: "remove", type: "remove",
content: changeLine.substring(1), content: changeLine.substring(1),
}); })
} else if (changeLine.startsWith("+")) { } else if (changeLine.startsWith("+")) {
changes.push({ type: "add", content: changeLine.substring(1) }); changes.push({ type: "add", content: changeLine.substring(1) })
} }
i++; i++
} }
hunks.push({ contextLine, changes }); hunks.push({ contextLine, changes })
} else { } else {
i++; i++
} }
} }
operations.push({ type: "update", filePath, hunks }); operations.push({ type: "update", filePath, hunks })
} else if (line.startsWith("*** Add File:")) { } else if (line.startsWith("*** Add File:")) {
const filePath = line.split(":", 2)[1]?.trim(); const filePath = line.split(":", 2)[1]?.trim()
if (!filePath) { if (!filePath) {
i++; i++
continue; continue
} }
let content = ""; let content = ""
i++; i++
while (i < lines.length && !lines[i].startsWith("***")) { while (i < lines.length && !lines[i].startsWith("***")) {
if (lines[i].startsWith("+")) { if (lines[i].startsWith("+")) {
content += lines[i].substring(1) + "\n"; content += lines[i].substring(1) + "\n"
} }
i++; i++
} }
operations.push({ type: "add", filePath, content: content.slice(0, -1) }); operations.push({ type: "add", filePath, content: content.slice(0, -1) })
} else if (line.startsWith("*** Delete File:")) { } else if (line.startsWith("*** Delete File:")) {
const filePath = line.split(":", 2)[1]?.trim(); const filePath = line.split(":", 2)[1]?.trim()
if (filePath) { if (filePath) {
operations.push({ type: "delete", filePath }); operations.push({ type: "delete", filePath })
} }
i++; i++
} else { } else {
i++; i++
} }
} }
return [operations, fuzz]; return [operations, fuzz]
} }
function patchToCommit( function patchToCommit(
operations: PatchOperation[], operations: PatchOperation[],
currentFiles: Record<string, string>, currentFiles: Record<string, string>,
): Commit { ): Commit {
const changes: Record<string, Change> = {}; const changes: Record<string, Change> = {}
for (const op of operations) { for (const op of operations) {
if (op.type === "delete") { if (op.type === "delete") {
changes[op.filePath] = { changes[op.filePath] = {
type: "delete", type: "delete",
old_content: currentFiles[op.filePath] || "", old_content: currentFiles[op.filePath] || "",
}; }
} else if (op.type === "add") { } else if (op.type === "add") {
changes[op.filePath] = { changes[op.filePath] = {
type: "add", type: "add",
new_content: op.content || "", new_content: op.content || "",
}; }
} else if (op.type === "update" && op.hunks) { } else if (op.type === "update" && op.hunks) {
const originalContent = currentFiles[op.filePath] || ""; const originalContent = currentFiles[op.filePath] || ""
const lines = originalContent.split("\n"); const lines = originalContent.split("\n")
for (const hunk of op.hunks) { for (const hunk of op.hunks) {
const contextIndex = lines.findIndex((line) => const contextIndex = lines.findIndex((line) =>
line.includes(hunk.contextLine), line.includes(hunk.contextLine),
); )
if (contextIndex === -1) { if (contextIndex === -1) {
throw new Error(`Context line not found: ${hunk.contextLine}`); throw new Error(`Context line not found: ${hunk.contextLine}`)
} }
let currentIndex = contextIndex; let currentIndex = contextIndex
for (const change of hunk.changes) { for (const change of hunk.changes) {
if (change.type === "keep") { if (change.type === "keep") {
currentIndex++; currentIndex++
} else if (change.type === "remove") { } else if (change.type === "remove") {
lines.splice(currentIndex, 1); lines.splice(currentIndex, 1)
} else if (change.type === "add") { } else if (change.type === "add") {
lines.splice(currentIndex, 0, change.content); lines.splice(currentIndex, 0, change.content)
currentIndex++; currentIndex++
} }
} }
} }
@ -231,11 +231,11 @@ function patchToCommit(
type: "update", type: "update",
old_content: originalContent, old_content: originalContent,
new_content: lines.join("\n"), new_content: lines.join("\n"),
}; }
} }
} }
return { changes }; return { changes }
} }
function generateDiff( function generateDiff(
@ -244,11 +244,11 @@ function generateDiff(
filePath: string, filePath: string,
): [string, number, number] { ): [string, number, number] {
// Mock implementation - would need actual diff generation // Mock implementation - would need actual diff generation
const lines1 = oldContent.split("\n"); const lines1 = oldContent.split("\n")
const lines2 = newContent.split("\n"); const lines2 = newContent.split("\n")
const additions = Math.max(0, lines2.length - lines1.length); const additions = Math.max(0, lines2.length - lines1.length)
const removals = Math.max(0, lines1.length - lines2.length); const removals = Math.max(0, lines1.length - lines2.length)
return [`--- ${filePath}\n+++ ${filePath}\n`, additions, removals]; return [`--- ${filePath}\n+++ ${filePath}\n`, additions, removals]
} }
async function applyCommit( async function applyCommit(
@ -258,9 +258,9 @@ async function applyCommit(
): Promise<void> { ): Promise<void> {
for (const [filePath, change] of Object.entries(commit.changes)) { for (const [filePath, change] of Object.entries(commit.changes)) {
if (change.type === "delete") { if (change.type === "delete") {
await deleteFile(filePath); await deleteFile(filePath)
} else if (change.new_content !== undefined) { } else if (change.new_content !== undefined) {
await writeFile(filePath, change.new_content); await writeFile(filePath, change.new_content)
} }
} }
} }
@ -271,142 +271,142 @@ export const patch = Tool.define({
parameters: PatchParams, parameters: PatchParams,
execute: async (params) => { execute: async (params) => {
if (!params.patchText) { if (!params.patchText) {
throw new Error("patchText is required"); throw new Error("patchText is required")
} }
// Identify all files needed for the patch and verify they've been read // Identify all files needed for the patch and verify they've been read
const filesToRead = identifyFilesNeeded(params.patchText); const filesToRead = identifyFilesNeeded(params.patchText)
for (const filePath of filesToRead) { for (const filePath of filesToRead) {
let absPath = filePath; let absPath = filePath
if (!path.isAbsolute(absPath)) { if (!path.isAbsolute(absPath)) {
absPath = path.resolve(process.cwd(), absPath); absPath = path.resolve(process.cwd(), absPath)
} }
if (!FileTimes.get(absPath)) { if (!FileTimes.get(absPath)) {
throw new Error( throw new Error(
`you must read the file ${filePath} before patching it. Use the FileRead tool first`, `you must read the file ${filePath} before patching it. Use the FileRead tool first`,
); )
} }
try { try {
const stats = await fs.stat(absPath); const stats = await fs.stat(absPath)
if (stats.isDirectory()) { if (stats.isDirectory()) {
throw new Error(`path is a directory, not a file: ${absPath}`); throw new Error(`path is a directory, not a file: ${absPath}`)
} }
const lastRead = FileTimes.get(absPath); const lastRead = FileTimes.get(absPath)
if (lastRead && stats.mtime > lastRead) { if (lastRead && stats.mtime > lastRead) {
throw new Error( throw new Error(
`file ${absPath} has been modified since it was last read (mod time: ${stats.mtime.toISOString()}, last read: ${lastRead.toISOString()})`, `file ${absPath} has been modified since it was last read (mod time: ${stats.mtime.toISOString()}, last read: ${lastRead.toISOString()})`,
); )
} }
} catch (error: any) { } catch (error: any) {
if (error.code === "ENOENT") { if (error.code === "ENOENT") {
throw new Error(`file not found: ${absPath}`); throw new Error(`file not found: ${absPath}`)
} }
throw new Error(`failed to access file: ${error.message}`); throw new Error(`failed to access file: ${error.message}`)
} }
} }
// Check for new files to ensure they don't already exist // Check for new files to ensure they don't already exist
const filesToAdd = identifyFilesAdded(params.patchText); const filesToAdd = identifyFilesAdded(params.patchText)
for (const filePath of filesToAdd) { for (const filePath of filesToAdd) {
let absPath = filePath; let absPath = filePath
if (!path.isAbsolute(absPath)) { if (!path.isAbsolute(absPath)) {
absPath = path.resolve(process.cwd(), absPath); absPath = path.resolve(process.cwd(), absPath)
} }
try { try {
await fs.stat(absPath); await fs.stat(absPath)
throw new Error(`file already exists and cannot be added: ${absPath}`); throw new Error(`file already exists and cannot be added: ${absPath}`)
} catch (error: any) { } catch (error: any) {
if (error.code !== "ENOENT") { if (error.code !== "ENOENT") {
throw new Error(`failed to check file: ${error.message}`); throw new Error(`failed to check file: ${error.message}`)
} }
} }
} }
// Load all required files // Load all required files
const currentFiles: Record<string, string> = {}; const currentFiles: Record<string, string> = {}
for (const filePath of filesToRead) { for (const filePath of filesToRead) {
let absPath = filePath; let absPath = filePath
if (!path.isAbsolute(absPath)) { if (!path.isAbsolute(absPath)) {
absPath = path.resolve(process.cwd(), absPath); absPath = path.resolve(process.cwd(), absPath)
} }
try { try {
const content = await fs.readFile(absPath, "utf-8"); const content = await fs.readFile(absPath, "utf-8")
currentFiles[filePath] = content; currentFiles[filePath] = content
} catch (error: any) { } catch (error: any) {
throw new Error(`failed to read file ${absPath}: ${error.message}`); throw new Error(`failed to read file ${absPath}: ${error.message}`)
} }
} }
// Process the patch // Process the patch
const [patch, fuzz] = textToPatch(params.patchText, currentFiles); const [patch, fuzz] = textToPatch(params.patchText, currentFiles)
if (fuzz > 3) { if (fuzz > 3) {
throw new Error( throw new Error(
`patch contains fuzzy matches (fuzz level: ${fuzz}). Please make your context lines more precise`, `patch contains fuzzy matches (fuzz level: ${fuzz}). Please make your context lines more precise`,
); )
} }
// Convert patch to commit // Convert patch to commit
const commit = patchToCommit(patch, currentFiles); const commit = patchToCommit(patch, currentFiles)
// Apply the changes to the filesystem // Apply the changes to the filesystem
await applyCommit( await applyCommit(
commit, commit,
async (filePath: string, content: string) => { async (filePath: string, content: string) => {
let absPath = filePath; let absPath = filePath
if (!path.isAbsolute(absPath)) { if (!path.isAbsolute(absPath)) {
absPath = path.resolve(process.cwd(), absPath); absPath = path.resolve(process.cwd(), absPath)
} }
// Create parent directories if needed // Create parent directories if needed
const dir = path.dirname(absPath); const dir = path.dirname(absPath)
await fs.mkdir(dir, { recursive: true }); await fs.mkdir(dir, { recursive: true })
await fs.writeFile(absPath, content, "utf-8"); await fs.writeFile(absPath, content, "utf-8")
}, },
async (filePath: string) => { async (filePath: string) => {
let absPath = filePath; let absPath = filePath
if (!path.isAbsolute(absPath)) { if (!path.isAbsolute(absPath)) {
absPath = path.resolve(process.cwd(), absPath); absPath = path.resolve(process.cwd(), absPath)
} }
await fs.unlink(absPath); await fs.unlink(absPath)
}, },
); )
// Calculate statistics // Calculate statistics
const changedFiles: string[] = []; const changedFiles: string[] = []
let totalAdditions = 0; let totalAdditions = 0
let totalRemovals = 0; let totalRemovals = 0
for (const [filePath, change] of Object.entries(commit.changes)) { for (const [filePath, change] of Object.entries(commit.changes)) {
let absPath = filePath; let absPath = filePath
if (!path.isAbsolute(absPath)) { if (!path.isAbsolute(absPath)) {
absPath = path.resolve(process.cwd(), absPath); absPath = path.resolve(process.cwd(), absPath)
} }
changedFiles.push(absPath); changedFiles.push(absPath)
const oldContent = change.old_content || ""; const oldContent = change.old_content || ""
const newContent = change.new_content || ""; const newContent = change.new_content || ""
// Calculate diff statistics // Calculate diff statistics
const [, additions, removals] = generateDiff( const [, additions, removals] = generateDiff(
oldContent, oldContent,
newContent, newContent,
filePath, filePath,
); )
totalAdditions += additions; totalAdditions += additions
totalRemovals += removals; totalRemovals += removals
// Record file operations // Record file operations
FileTimes.write(absPath); FileTimes.write(absPath)
FileTimes.read(absPath); FileTimes.read(absPath)
} }
const result = `Patch applied successfully. ${changedFiles.length} files changed, ${totalAdditions} additions, ${totalRemovals} removals`; const result = `Patch applied successfully. ${changedFiles.length} files changed, ${totalAdditions} additions, ${totalRemovals} removals`
const output = result; const output = result
return { return {
metadata: { metadata: {
@ -415,6 +415,6 @@ export const patch = Tool.define({
removals: totalRemovals, removals: totalRemovals,
} satisfies PatchResponseMetadata, } satisfies PatchResponseMetadata,
output, output,
}; }
}, },
}); })

View file

@ -1,17 +1,17 @@
import { tool, type Tool as AITool } from "ai"; import { tool, type Tool as AITool } from "ai"
import { Log } from "../util/log"; import { Log } from "../util/log"
const log = Log.create({ service: "tool" }); const log = Log.create({ service: "tool" })
export namespace Tool { export namespace Tool {
export interface Metadata< export interface Metadata<
Properties extends Record<string, any> = Record<string, any>, Properties extends Record<string, any> = Record<string, any>,
> { > {
properties: Properties; properties: Properties
time: { time: {
start: number; start: number
end: number; end: number
}; }
} }
export function define< export function define<
Params, Params,
@ -19,7 +19,7 @@ export namespace Tool {
Name extends string, Name extends string,
>( >(
input: AITool<Params, Output> & { input: AITool<Params, Output> & {
name: Name; name: Name
}, },
) { ) {
return tool({ return tool({
@ -29,33 +29,33 @@ export namespace Tool {
id: opts.toolCallId, id: opts.toolCallId,
name: input.name, name: input.name,
...params, ...params,
}); })
try { try {
const start = Date.now(); const start = Date.now()
const result = await input.execute!(params, opts); const result = await input.execute!(params, opts)
const metadata: Metadata<Output["metadata"]> = { const metadata: Metadata<Output["metadata"]> = {
...result.metadata, ...result.metadata,
time: { time: {
start, start,
end: Date.now(), end: Date.now(),
}, },
}; }
return { return {
metadata, metadata,
output: result.output, output: result.output,
}; }
} catch (e: any) { } catch (e: any) {
log.error("error", { log.error("error", {
msg: e.toString(), msg: e.toString(),
}); })
return { return {
metadata: { metadata: {
error: true, error: true,
}, },
output: "An error occurred: " + e.toString(), output: "An error occurred: " + e.toString(),
}; }
} }
}, },
}); })
} }
} }

View file

@ -1,20 +1,20 @@
import { App } from "../../app/app"; import { App } from "../../app/app"
export namespace FileTimes { export namespace FileTimes {
export const state = App.state("tool.filetimes", () => ({ export const state = App.state("tool.filetimes", () => ({
read: new Map<string, Date>(), read: new Map<string, Date>(),
write: new Map<string, Date>(), write: new Map<string, Date>(),
})); }))
export function read(filePath: string) { export function read(filePath: string) {
state().read.set(filePath, new Date()); state().read.set(filePath, new Date())
} }
export function write(filePath: string) { export function write(filePath: string) {
state().write.set(filePath, new Date()); state().write.set(filePath, new Date())
} }
export function get(filePath: string): Date | null { export function get(filePath: string): Date | null {
return state().read.get(filePath) || null; return state().read.get(filePath) || null
} }
} }

View file

@ -1,13 +1,13 @@
import { z } from "zod"; import { z } from "zod"
import * as fs from "fs"; import * as fs from "fs"
import * as path from "path"; import * as path from "path"
import { Tool } from "./tool"; import { Tool } from "./tool"
import { LSP } from "../lsp"; import { LSP } from "../lsp"
import { FileTimes } from "./util/file-times"; import { FileTimes } from "./util/file-times"
const MAX_READ_SIZE = 250 * 1024; const MAX_READ_SIZE = 250 * 1024
const DEFAULT_READ_LIMIT = 2000; const DEFAULT_READ_LIMIT = 2000
const MAX_LINE_LENGTH = 2000; const MAX_LINE_LENGTH = 2000
const DESCRIPTION = `File viewing tool that reads and displays the contents of files with line numbers, allowing you to examine code, logs, or text data. const DESCRIPTION = `File viewing tool that reads and displays the contents of files with line numbers, allowing you to examine code, logs, or text data.
@ -38,7 +38,7 @@ LIMITATIONS:
TIPS: TIPS:
- Use with Glob tool to first find files you want to view - Use with Glob tool to first find files you want to view
- For code exploration, first use Grep to find relevant files, then View to examine them - For code exploration, first use Grep to find relevant files, then View to examine them
- When viewing large files, use the offset parameter to read specific sections`; - When viewing large files, use the offset parameter to read specific sections`
export const view = Tool.define({ export const view = Tool.define({
name: "opencode.view", name: "opencode.view",
@ -55,17 +55,17 @@ export const view = Tool.define({
.optional(), .optional(),
}), }),
async execute(params) { async execute(params) {
let filePath = params.filePath; let filePath = params.filePath
if (!path.isAbsolute(filePath)) { if (!path.isAbsolute(filePath)) {
filePath = path.join(process.cwd(), filePath); filePath = path.join(process.cwd(), filePath)
} }
const file = Bun.file(filePath); const file = Bun.file(filePath)
if (!(await file.exists())) { if (!(await file.exists())) {
const dir = path.dirname(filePath); const dir = path.dirname(filePath)
const base = path.basename(filePath); const base = path.basename(filePath)
const dirEntries = fs.readdirSync(dir); const dirEntries = fs.readdirSync(dir)
const suggestions = dirEntries const suggestions = dirEntries
.filter( .filter(
(entry) => (entry) =>
@ -73,80 +73,80 @@ export const view = Tool.define({
base.toLowerCase().includes(entry.toLowerCase()), base.toLowerCase().includes(entry.toLowerCase()),
) )
.map((entry) => path.join(dir, entry)) .map((entry) => path.join(dir, entry))
.slice(0, 3); .slice(0, 3)
if (suggestions.length > 0) { if (suggestions.length > 0) {
throw new Error( throw new Error(
`File not found: ${filePath}\n\nDid you mean one of these?\n${suggestions.join("\n")}`, `File not found: ${filePath}\n\nDid you mean one of these?\n${suggestions.join("\n")}`,
); )
} }
throw new Error(`File not found: ${filePath}`); throw new Error(`File not found: ${filePath}`)
} }
const stats = await file.stat(); const stats = await file.stat()
if (stats.size > MAX_READ_SIZE) if (stats.size > MAX_READ_SIZE)
throw new Error( throw new Error(
`File is too large (${stats.size} bytes). Maximum size is ${MAX_READ_SIZE} bytes`, `File is too large (${stats.size} bytes). Maximum size is ${MAX_READ_SIZE} bytes`,
); )
const limit = params.limit ?? DEFAULT_READ_LIMIT; const limit = params.limit ?? DEFAULT_READ_LIMIT
const offset = params.offset || 0; const offset = params.offset || 0
const isImage = isImageFile(filePath); const isImage = isImageFile(filePath)
if (isImage) if (isImage)
throw new Error( throw new Error(
`This is an image file of type: ${isImage}\nUse a different tool to process images`, `This is an image file of type: ${isImage}\nUse a different tool to process images`,
); )
const lines = await file.text().then((text) => text.split("\n")); const lines = await file.text().then((text) => text.split("\n"))
const raw = lines.slice(offset, offset + limit).map((line) => { const raw = lines.slice(offset, offset + limit).map((line) => {
return line.length > MAX_LINE_LENGTH return line.length > MAX_LINE_LENGTH
? line.substring(0, MAX_LINE_LENGTH) + "..." ? line.substring(0, MAX_LINE_LENGTH) + "..."
: line; : line
}); })
const content = raw.map((line, index) => { const content = raw.map((line, index) => {
return `${(index + offset + 1).toString().padStart(5, "0")}| ${line}`; return `${(index + offset + 1).toString().padStart(5, "0")}| ${line}`
}); })
const preview = raw.slice(0, 20).join("\n"); const preview = raw.slice(0, 20).join("\n")
let output = "<file>\n"; let output = "<file>\n"
output += content.join("\n"); output += content.join("\n")
if (lines.length > offset + content.length) { if (lines.length > offset + content.length) {
output += `\n\n(File has more lines. Use 'offset' parameter to read beyond line ${ output += `\n\n(File has more lines. Use 'offset' parameter to read beyond line ${
offset + content.length offset + content.length
})`; })`
} }
output += "\n</file>"; output += "\n</file>"
// just warms the lsp client // just warms the lsp client
LSP.file(filePath); LSP.file(filePath)
FileTimes.read(filePath); FileTimes.read(filePath)
return { return {
output, output,
metadata: { metadata: {
preview, preview,
}, },
}; }
}, },
}); })
function isImageFile(filePath: string): string | false { function isImageFile(filePath: string): string | false {
const ext = path.extname(filePath).toLowerCase(); const ext = path.extname(filePath).toLowerCase()
switch (ext) { switch (ext) {
case ".jpg": case ".jpg":
case ".jpeg": case ".jpeg":
return "JPEG"; return "JPEG"
case ".png": case ".png":
return "PNG"; return "PNG"
case ".gif": case ".gif":
return "GIF"; return "GIF"
case ".bmp": case ".bmp":
return "BMP"; return "BMP"
case ".svg": case ".svg":
return "SVG"; return "SVG"
case ".webp": case ".webp":
return "WebP"; return "WebP"
default: default:
return false; return false
} }
} }

View file

@ -1,25 +1,25 @@
import { AsyncLocalStorage } from "async_hooks"; import { AsyncLocalStorage } from "async_hooks"
export namespace Context { export namespace Context {
export class NotFound extends Error { export class NotFound extends Error {
constructor(public readonly name: string) { constructor(public readonly name: string) {
super(`No context found for ${name}`); super(`No context found for ${name}`)
} }
} }
export function create<T>(name: string) { export function create<T>(name: string) {
const storage = new AsyncLocalStorage<T>(); const storage = new AsyncLocalStorage<T>()
return { return {
use() { use() {
const result = storage.getStore(); const result = storage.getStore()
if (!result) { if (!result) {
throw new NotFound(name); throw new NotFound(name)
} }
return result; return result
}, },
provide<R>(value: T, fn: () => R) { provide<R>(value: T, fn: () => R) {
return storage.run<R>(value, fn); return storage.run<R>(value, fn)
}, },
}; }
} }
} }

View file

@ -1,37 +1,37 @@
import path from "path"; import path from "path"
import { AppPath } from "../app/path"; import { AppPath } from "../app/path"
import fs from "fs/promises"; import fs from "fs/promises"
export namespace Log { export namespace Log {
const write = { const write = {
out: (msg: string) => { out: (msg: string) => {
process.stdout.write(msg); process.stdout.write(msg)
}, },
err: (msg: string) => { err: (msg: string) => {
process.stderr.write(msg); process.stderr.write(msg)
}, },
}; }
export async function file(directory: string) { export async function file(directory: string) {
const outPath = path.join(AppPath.data(directory), "opencode.out.log"); const outPath = path.join(AppPath.data(directory), "opencode.out.log")
const errPath = path.join(AppPath.data(directory), "opencode.err.log"); const errPath = path.join(AppPath.data(directory), "opencode.err.log")
await fs.truncate(outPath).catch(() => {}); await fs.truncate(outPath).catch(() => {})
await fs.truncate(errPath).catch(() => {}); await fs.truncate(errPath).catch(() => {})
const out = Bun.file(outPath); const out = Bun.file(outPath)
const err = Bun.file(errPath); const err = Bun.file(errPath)
const outWriter = out.writer(); const outWriter = out.writer()
const errWriter = err.writer(); const errWriter = err.writer()
write["out"] = (msg) => { write["out"] = (msg) => {
outWriter.write(msg); outWriter.write(msg)
outWriter.flush(); outWriter.flush()
}; }
write["err"] = (msg) => { write["err"] = (msg) => {
errWriter.write(msg); errWriter.write(msg)
errWriter.flush(); errWriter.flush()
}; }
} }
export function create(tags?: Record<string, any>) { export function create(tags?: Record<string, any>) {
tags = tags || {}; tags = tags || {}
function build(message: any, extra?: Record<string, any>) { function build(message: any, extra?: Record<string, any>) {
const prefix = Object.entries({ const prefix = Object.entries({
@ -40,25 +40,28 @@ export namespace Log {
}) })
.filter(([_, value]) => value !== undefined && value !== null) .filter(([_, value]) => value !== undefined && value !== null)
.map(([key, value]) => `${key}=${value}`) .map(([key, value]) => `${key}=${value}`)
.join(" "); .join(" ")
return [new Date().toISOString(), prefix, message].filter(Boolean).join(" ") + "\n"; return (
[new Date().toISOString(), prefix, message].filter(Boolean).join(" ") +
"\n"
)
} }
const result = { const result = {
info(message?: any, extra?: Record<string, any>) { info(message?: any, extra?: Record<string, any>) {
write.out(build(message, extra)); write.out(build(message, extra))
}, },
error(message?: any, extra?: Record<string, any>) { error(message?: any, extra?: Record<string, any>) {
write.err(build(message, extra)); write.err(build(message, extra))
}, },
tag(key: string, value: string) { tag(key: string, value: string) {
if (tags) tags[key] = value; if (tags) tags[key] = value
return result; return result
}, },
clone() { clone() {
return Log.create({ ...tags }); return Log.create({ ...tags })
}, },
}; }
return result; return result
} }
} }

View file

@ -1,5 +1,5 @@
export const foo: string = "42"; export const foo: string = "42"
export function dummyFunction(): void { export function dummyFunction(): void {
console.log("This is a dummy function"); console.log("This is a dummy function")
} }

View file

@ -1,7 +1,7 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test"
import { App } from "../../src/app/app"; import { App } from "../../src/app/app"
import { glob } from "../../src/tool/glob"; import { glob } from "../../src/tool/glob"
import { ls } from "../../src/tool/ls"; import { ls } from "../../src/tool/ls"
describe("tool.glob", () => { describe("tool.glob", () => {
test("truncate", async () => { test("truncate", async () => {
@ -14,10 +14,10 @@ describe("tool.glob", () => {
toolCallId: "test", toolCallId: "test",
messages: [], messages: [],
}, },
); )
expect(result.metadata.truncated).toBe(true); expect(result.metadata.truncated).toBe(true)
}); })
}); })
test("basic", async () => { test("basic", async () => {
await App.provide({ directory: process.cwd() }, async () => { await App.provide({ directory: process.cwd() }, async () => {
let result = await glob.execute( let result = await glob.execute(
@ -28,14 +28,14 @@ describe("tool.glob", () => {
toolCallId: "test", toolCallId: "test",
messages: [], messages: [],
}, },
); )
expect(result.metadata).toMatchObject({ expect(result.metadata).toMatchObject({
truncated: false, truncated: false,
count: 2, count: 2,
}); })
}); })
}); })
}); })
describe("tool.ls", () => { describe("tool.ls", () => {
test("basic", async () => { test("basic", async () => {
@ -48,8 +48,8 @@ describe("tool.ls", () => {
toolCallId: "test", toolCallId: "test",
messages: [], messages: [],
}, },
); )
}); })
expect(result.output).toMatchSnapshot(); expect(result.output).toMatchSnapshot()
}); })
}); })

View file

@ -1,69 +1,69 @@
// @ts-check // @ts-check
import { defineConfig } from "astro/config"; import { defineConfig } from "astro/config"
import starlight from "@astrojs/starlight"; import starlight from "@astrojs/starlight"
import solidJs from "@astrojs/solid-js"; import solidJs from "@astrojs/solid-js"
import theme from "toolbeam-docs-theme"; import theme from "toolbeam-docs-theme"
import { rehypeHeadingIds } from "@astrojs/markdown-remark"; import { rehypeHeadingIds } from "@astrojs/markdown-remark"
import rehypeAutolinkHeadings from "rehype-autolink-headings"; import rehypeAutolinkHeadings from "rehype-autolink-headings"
const discord = "https://discord.gg/sst"; const discord = "https://discord.gg/sst"
const github = "https://github.com/sst/opencode"; const github = "https://github.com/sst/opencode"
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
devToolbar: { devToolbar: {
enabled: false, enabled: false,
}, },
markdown: { markdown: {
rehypePlugins: [ rehypePlugins: [
rehypeHeadingIds, rehypeHeadingIds,
[rehypeAutolinkHeadings, { behavior: "wrap" }], [rehypeAutolinkHeadings, { behavior: "wrap" }],
], ],
}, },
integrations: [ integrations: [
solidJs(), solidJs(),
starlight({ starlight({
title: "OpenCode", title: "OpenCode",
expressiveCode: { themes: ["github-light", "github-dark"] }, expressiveCode: { themes: ["github-light", "github-dark"] },
social: [ social: [
{ icon: "discord", label: "Discord", href: discord }, { icon: "discord", label: "Discord", href: discord },
{ icon: "github", label: "GitHub", href: github }, { icon: "github", label: "GitHub", href: github },
], ],
editLink: { editLink: {
baseUrl: `${github}/edit/master/www/`, baseUrl: `${github}/edit/master/www/`,
}, },
markdown: { markdown: {
headingLinks: false, headingLinks: false,
}, },
customCss: [ customCss: ["./src/styles/custom.css"],
"./src/styles/custom.css", logo: {
], light: "./src/assets/logo-light.svg",
logo: { dark: "./src/assets/logo-dark.svg",
light: "./src/assets/logo-light.svg", replacesTitle: true,
dark: "./src/assets/logo-dark.svg", },
replacesTitle: true, sidebar: [
}, "docs",
sidebar: [ "docs/cli",
"docs", "docs/config",
"docs/cli", "docs/models",
"docs/config", "docs/themes",
"docs/models", "docs/shortcuts",
"docs/themes", "docs/lsp-servers",
"docs/shortcuts", "docs/mcp-servers",
"docs/lsp-servers", ],
"docs/mcp-servers", components: {
], Hero: "./src/components/Hero.astro",
components: { Header: "./src/components/Header.astro",
Hero: "./src/components/Hero.astro", },
Header: "./src/components/Header.astro", plugins: [
}, theme({
plugins: [theme({ // Optionally, add your own header links
// Optionally, add your own header links headerLinks: [
headerLinks: [ { name: "Home", url: "/" },
{ name: "Home", url: "/" }, { name: "Docs", url: "/docs/" },
{ name: "Docs", url: "/docs/" }, ],
], }),
})], ],
}), }),
], ],
}); })

View file

@ -6,7 +6,7 @@ import {
createResource, createResource,
} from "solid-js" } from "solid-js"
import { codeToHtml } from "shiki" import { codeToHtml } from "shiki"
import { transformerNotationDiff } from '@shikijs/transformers' import { transformerNotationDiff } from "@shikijs/transformers"
interface CodeBlockProps extends JSX.HTMLAttributes<HTMLDivElement> { interface CodeBlockProps extends JSX.HTMLAttributes<HTMLDivElement> {
code: string code: string
@ -20,12 +20,10 @@ function CodeBlock(props: CodeBlockProps) {
return (await codeToHtml(local.code, { return (await codeToHtml(local.code, {
lang: local.lang || "text", lang: local.lang || "text",
themes: { themes: {
light: 'github-light', light: "github-light",
dark: 'github-dark', dark: "github-dark",
}, },
transformers: [ transformers: [transformerNotationDiff()],
transformerNotationDiff(),
],
})) as string })) as string
}) })
@ -39,9 +37,7 @@ function CodeBlock(props: CodeBlockProps) {
} }
}) })
return ( return <div ref={containerRef} {...rest}></div>
<div ref={containerRef} {...rest}></div>
)
} }
export default CodeBlock export default CodeBlock

View file

@ -31,11 +31,7 @@ const DiffView: Component<DiffViewProps> = (props) => {
diffRows.push({ diffRows.push({
left: chunk.removed ? line : chunk.added ? "" : line, left: chunk.removed ? line : chunk.added ? "" : line,
right: chunk.added ? line : chunk.removed ? "" : line, right: chunk.added ? line : chunk.removed ? "" : line,
type: chunk.added type: chunk.added ? "added" : chunk.removed ? "removed" : "unchanged",
? "added"
: chunk.removed
? "removed"
: "unchanged",
}) })
} }
} }

View file

@ -12,11 +12,7 @@ import {
createSignal, createSignal,
} from "solid-js" } from "solid-js"
import { DateTime } from "luxon" import { DateTime } from "luxon"
import { import { IconOpenAI, IconGemini, IconAnthropic } from "./icons/custom"
IconOpenAI,
IconGemini,
IconAnthropic,
} from "./icons/custom"
import { import {
IconCpuChip, IconCpuChip,
IconSparkles, IconSparkles,
@ -31,8 +27,12 @@ import styles from "./share.module.css"
import { type UIMessage } from "ai" import { type UIMessage } from "ai"
import { createStore, reconcile } from "solid-js/store" import { createStore, reconcile } from "solid-js/store"
type Status = "disconnected" | "connecting" | "connected" | "error" | "reconnecting" type Status =
| "disconnected"
| "connecting"
| "connected"
| "error"
| "reconnecting"
type SessionMessage = UIMessage<{ type SessionMessage = UIMessage<{
time: { time: {
@ -40,23 +40,26 @@ type SessionMessage = UIMessage<{
completed?: number completed?: number
} }
assistant?: { assistant?: {
modelID: string; modelID: string
providerID: string; providerID: string
cost: number; cost: number
tokens: { tokens: {
input: number; input: number
output: number; output: number
reasoning: number; reasoning: number
};
};
sessionID: string
tool: Record<string, {
properties: Record<string, any>
time: {
start: number
end: number
} }
}> }
sessionID: string
tool: Record<
string,
{
properties: Record<string, any>
time: {
start: number
end: number
}
}
>
}> }>
type SessionInfo = { type SessionInfo = {
@ -65,48 +68,47 @@ type SessionInfo = {
} }
function getFileType(path: string) { function getFileType(path: string) {
return path.split('.').pop() return path.split(".").pop()
} }
// Converts `{a:{b:{c:1}}` to `[['a.b.c', 1]]` // Converts `{a:{b:{c:1}}` to `[['a.b.c', 1]]`
function flattenToolArgs(obj: any, prefix: string = ""): Array<[string, any]> { function flattenToolArgs(obj: any, prefix: string = ""): Array<[string, any]> {
const entries: Array<[string, any]> = []; const entries: Array<[string, any]> = []
for (const [key, value] of Object.entries(obj)) { for (const [key, value] of Object.entries(obj)) {
const path = prefix ? `${prefix}.${key}` : key; const path = prefix ? `${prefix}.${key}` : key
if ( if (value !== null && typeof value === "object" && !Array.isArray(value)) {
value !== null && entries.push(...flattenToolArgs(value, path))
typeof value === "object" && } else {
!Array.isArray(value) entries.push([path, value])
) {
entries.push(...flattenToolArgs(value, path));
}
else {
entries.push([path, value]);
} }
} }
return entries; return entries
} }
function getStatusText(status: [Status, string?]): string { function getStatusText(status: [Status, string?]): string {
switch (status[0]) { switch (status[0]) {
case "connected": return "Connected" case "connected":
case "connecting": return "Connecting..." return "Connected"
case "disconnected": return "Disconnected" case "connecting":
case "reconnecting": return "Reconnecting..." return "Connecting..."
case "error": return status[1] || "Error" case "disconnected":
default: return "Unknown" return "Disconnected"
case "reconnecting":
return "Reconnecting..."
case "error":
return status[1] || "Error"
default:
return "Unknown"
} }
} }
function ProviderIcon(props: { provider: string, size?: number }) { function ProviderIcon(props: { provider: string; size?: number }) {
const size = props.size || 16 const size = props.size || 16
return ( return (
<Switch fallback={ <Switch fallback={<IconSparkles width={size} height={size} />}>
<IconSparkles width={size} height={size} />
}>
<Match when={props.provider === "openai"}> <Match when={props.provider === "openai"}>
<IconOpenAI width={size} height={size} /> <IconOpenAI width={size} height={size} />
</Match> </Match>
@ -132,15 +134,11 @@ function ResultsButton(props: ResultsButtonProps) {
data-element-button-more data-element-button-more
{...rest} {...rest}
> >
<span> <span>{local.results ? "Hide results" : "Show results"}</span>
{local.results ? "Hide results" : "Show results"}
</span>
<span data-button-icon> <span data-button-icon>
<Show <Show
when={local.results} when={local.results}
fallback={ fallback={<IconChevronRight width={10} height={10} />}
<IconChevronRight width={10} height={10} />
}
> >
<IconChevronDown width={10} height={10} /> <IconChevronDown width={10} height={10} />
</Show> </Show>
@ -187,16 +185,16 @@ function TextPart(props: TextPartProps) {
data-expanded={expanded() || local.expand === true} data-expanded={expanded() || local.expand === true}
{...rest} {...rest}
> >
<pre ref={el => (preEl = el)}>{local.text}</pre> <pre ref={(el) => (preEl = el)}>{local.text}</pre>
{overflowed() && {overflowed() && (
<button <button
type="button" type="button"
data-element-button-text data-element-button-text
onClick={() => setExpanded(e => !e)} onClick={() => setExpanded((e) => !e)}
> >
{expanded() ? "Show less" : "Show more"} {expanded() ? "Show less" : "Show more"}
</button> </button>
} )}
</div> </div>
) )
} }
@ -205,13 +203,13 @@ function PartFooter(props: { time: number }) {
return ( return (
<span <span
data-part-footer data-part-footer
title={ title={DateTime.fromMillis(props.time).toLocaleString(
DateTime.fromMillis(props.time).toLocaleString( DateTime.DATETIME_FULL_WITH_SECONDS,
DateTime.DATETIME_FULL_WITH_SECONDS )}
)
}
> >
{DateTime.fromMillis(props.time).toLocaleString(DateTime.TIME_WITH_SECONDS)} {DateTime.fromMillis(props.time).toLocaleString(
DateTime.TIME_WITH_SECONDS,
)}
</span> </span>
) )
} }
@ -226,8 +224,12 @@ export default function Share(props: { api: string }) {
}>({ }>({
messages: {}, messages: {},
}) })
const messages = createMemo(() => Object.values(store.messages).toSorted((a, b) => a.id?.localeCompare(b.id))) const messages = createMemo(() =>
const [connectionStatus, setConnectionStatus] = createSignal<[Status, string?]>(["disconnected", "Disconnected"]) Object.values(store.messages).toSorted((a, b) => a.id?.localeCompare(b.id)),
)
const [connectionStatus, setConnectionStatus] = createSignal<
[Status, string?]
>(["disconnected", "Disconnected"])
onMount(() => { onMount(() => {
const apiUrl = props.api const apiUrl = props.api
@ -326,7 +328,10 @@ export default function Share(props: { api: string }) {
const result: string[][] = [] const result: string[][] = []
for (const msg of messages()) { for (const msg of messages()) {
if (msg.role === "assistant" && msg.metadata?.assistant) { if (msg.role === "assistant" && msg.metadata?.assistant) {
result.push([msg.metadata.assistant.providerID, msg.metadata.assistant.modelID]) result.push([
msg.metadata.assistant.providerID,
msg.metadata.assistant.modelID,
])
} }
} }
return result return result
@ -339,7 +344,7 @@ export default function Share(props: { api: string }) {
input: 0, input: 0,
output: 0, output: 0,
reasoning: 0, reasoning: 0,
} },
} }
for (const msg of messages()) { for (const msg of messages()) {
const assistant = msg.metadata?.assistant const assistant = msg.metadata?.assistant
@ -366,39 +371,39 @@ export default function Share(props: { api: string }) {
<ul data-section="stats"> <ul data-section="stats">
<li> <li>
<span data-element-label>Cost</span> <span data-element-label>Cost</span>
{metrics().cost !== undefined ? {metrics().cost !== undefined ? (
<span>${metrics().cost.toFixed(2)}</span> <span>${metrics().cost.toFixed(2)}</span>
: ) : (
<span data-placeholder>&mdash;</span> <span data-placeholder>&mdash;</span>
} )}
</li> </li>
<li> <li>
<span data-element-label>Input Tokens</span> <span data-element-label>Input Tokens</span>
{metrics().tokens.input ? {metrics().tokens.input ? (
<span>{metrics().tokens.input}</span> <span>{metrics().tokens.input}</span>
: ) : (
<span data-placeholder>&mdash;</span> <span data-placeholder>&mdash;</span>
} )}
</li> </li>
<li> <li>
<span data-element-label>Output Tokens</span> <span data-element-label>Output Tokens</span>
{metrics().tokens.output ? {metrics().tokens.output ? (
<span>{metrics().tokens.output}</span> <span>{metrics().tokens.output}</span>
: ) : (
<span data-placeholder>&mdash;</span> <span data-placeholder>&mdash;</span>
} )}
</li> </li>
<li> <li>
<span data-element-label>Reasoning Tokens</span> <span data-element-label>Reasoning Tokens</span>
{metrics().tokens.reasoning ? {metrics().tokens.reasoning ? (
<span>{metrics().tokens.reasoning}</span> <span>{metrics().tokens.reasoning}</span>
: ) : (
<span data-placeholder>&mdash;</span> <span data-placeholder>&mdash;</span>
} )}
</li> </li>
</ul> </ul>
<ul data-section="stats" data-section-models> <ul data-section="stats" data-section-models>
{models().length > 0 ? {models().length > 0 ? (
<For each={Array.from(models())}> <For each={Array.from(models())}>
{([provider, model]) => ( {([provider, model]) => (
<li> <li>
@ -409,27 +414,29 @@ export default function Share(props: { api: string }) {
</li> </li>
)} )}
</For> </For>
: ) : (
<li> <li>
<span data-element-label>Models</span> <span data-element-label>Models</span>
<span data-placeholder>&mdash;</span> <span data-placeholder>&mdash;</span>
</li> </li>
} )}
</ul> </ul>
<div data-section="date"> <div data-section="date">
{messages().length > 0 && messages()[0].metadata?.time.created ? {messages().length > 0 && messages()[0].metadata?.time.created ? (
<span title={ <span
DateTime.fromMillis( title={DateTime.fromMillis(
messages()[0].metadata?.time.created || 0 messages()[0].metadata?.time.created || 0,
).toLocaleString(DateTime.DATETIME_FULL_WITH_SECONDS) ).toLocaleString(DateTime.DATETIME_FULL_WITH_SECONDS)}
}> >
{DateTime.fromMillis( {DateTime.fromMillis(
messages()[0].metadata?.time.created || 0 messages()[0].metadata?.time.created || 0,
).toLocaleString(DateTime.DATE_MED)} ).toLocaleString(DateTime.DATE_MED)}
</span> </span>
: ) : (
<span data-element-label data-placeholder>Started at &mdash;</span> <span data-element-label data-placeholder>
} Started at &mdash;
</span>
)}
</div> </div>
</div> </div>
</div> </div>
@ -444,27 +451,32 @@ export default function Share(props: { api: string }) {
{(msg, msgIndex) => ( {(msg, msgIndex) => (
<For each={msg.parts}> <For each={msg.parts}>
{(part, partIndex) => { {(part, partIndex) => {
if (part.type === "step-start" && (partIndex() > 0 || !msg.metadata?.assistant)) return null if (
part.type === "step-start" &&
(partIndex() > 0 || !msg.metadata?.assistant)
)
return null
const [results, showResults] = createSignal(false) const [results, showResults] = createSignal(false)
const isLastPart = createMemo(() => const isLastPart = createMemo(
(messages().length === msgIndex() + 1) () =>
&& (msg.parts.length === partIndex() + 1) messages().length === msgIndex() + 1 &&
msg.parts.length === partIndex() + 1,
) )
const time = msg.metadata?.time.completed const time =
|| msg.metadata?.time.created msg.metadata?.time.completed ||
|| 0 msg.metadata?.time.created ||
0
return ( return (
<Switch> <Switch>
{ /* User text */} {/* User text */}
<Match when={ <Match
msg.role === "user" && part.type === "text" && part when={
}> msg.role === "user" && part.type === "text" && part
{part => }
<div >
data-section="part" {(part) => (
data-part-type="user-text" <div data-section="part" data-part-type="user-text">
>
<div data-section="decoration"> <div data-section="decoration">
<div> <div>
<IconUserCircle width={18} height={18} /> <IconUserCircle width={18} height={18} />
@ -480,21 +492,22 @@ export default function Share(props: { api: string }) {
<PartFooter time={time} /> <PartFooter time={time} />
</div> </div>
</div> </div>
} )}
</Match> </Match>
{ /* AI text */} {/* AI text */}
<Match when={ <Match
msg.role === "assistant" when={
&& part.type === "text" msg.role === "assistant" &&
&& part part.type === "text" &&
}> part
{part => }
<div >
data-section="part" {(part) => (
data-part-type="ai-text" <div data-section="part" data-part-type="ai-text">
>
<div data-section="decoration"> <div data-section="decoration">
<div><IconSparkles width={18} height={18} /></div> <div>
<IconSparkles width={18} height={18} />
</div>
<div></div> <div></div>
</div> </div>
<div data-section="content"> <div data-section="content">
@ -505,19 +518,18 @@ export default function Share(props: { api: string }) {
<PartFooter time={time} /> <PartFooter time={time} />
</div> </div>
</div> </div>
} )}
</Match> </Match>
{ /* AI model */} {/* AI model */}
<Match when={ <Match
msg.role === "assistant" when={
&& part.type === "step-start" msg.role === "assistant" &&
&& msg.metadata?.assistant part.type === "step-start" &&
}> msg.metadata?.assistant
{assistant => }
<div >
data-section="part" {(assistant) => (
data-part-type="ai-model" <div data-section="part" data-part-type="ai-model">
>
<div data-section="decoration"> <div data-section="decoration">
<div> <div>
<ProviderIcon <ProviderIcon
@ -542,15 +554,17 @@ export default function Share(props: { api: string }) {
</div> </div>
</div> </div>
</div> </div>
} )}
</Match> </Match>
{ /* System text */} {/* System text */}
<Match when={ <Match
msg.role === "system" when={
&& part.type === "text" msg.role === "system" &&
&& part part.type === "text" &&
}> part
{part => }
>
{(part) => (
<div <div
data-section="part" data-section="part"
data-part-type="system-text" data-part-type="system-text"
@ -575,16 +589,18 @@ export default function Share(props: { api: string }) {
<PartFooter time={time} /> <PartFooter time={time} />
</div> </div>
</div> </div>
} )}
</Match> </Match>
{ /* Edit tool */} {/* Edit tool */}
<Match when={ <Match
msg.role === "assistant" when={
&& part.type === "tool-invocation" msg.role === "assistant" &&
&& part.toolInvocation.toolName === "edit" part.type === "tool-invocation" &&
&& part part.toolInvocation.toolName === "edit" &&
}> part
{part => { }
>
{(part) => {
const args = part().toolInvocation.args const args = part().toolInvocation.args
const filePath = args.filePath const filePath = args.filePath
return ( return (
@ -618,20 +634,25 @@ export default function Share(props: { api: string }) {
) )
}} }}
</Match> </Match>
{ /* Tool call */} {/* Tool call */}
<Match when={ <Match
msg.role === "assistant" when={
&& part.type === "tool-invocation" msg.role === "assistant" &&
&& part part.type === "tool-invocation" &&
}> part
{part => }
>
{(part) => (
<div <div
data-section="part" data-section="part"
data-part-type="tool-fallback" data-part-type="tool-fallback"
> >
<div data-section="decoration"> <div data-section="decoration">
<div> <div>
<IconWrenchScrewdriver width={18} height={18} /> <IconWrenchScrewdriver
width={18}
height={18}
/>
</div> </div>
<div></div> <div></div>
</div> </div>
@ -641,27 +662,32 @@ export default function Share(props: { api: string }) {
{part().toolInvocation.toolName} {part().toolInvocation.toolName}
</span> </span>
<div data-part-tool-args> <div data-part-tool-args>
<For each={ <For
flattenToolArgs(part().toolInvocation.args) each={flattenToolArgs(
}> part().toolInvocation.args,
{([name, value]) => )}
>
{([name, value]) => (
<> <>
<div></div> <div></div>
<div>{name}</div> <div>{name}</div>
<div>{value}</div> <div>{value}</div>
</> </>
} )}
</For> </For>
</div> </div>
<Switch> <Switch>
<Match when={ <Match
part().toolInvocation.state === "result" when={
&& part().toolInvocation.result part().toolInvocation.state ===
}> "result" &&
part().toolInvocation.result
}
>
<div data-part-tool-result> <div data-part-tool-result>
<ResultsButton <ResultsButton
results={results()} results={results()}
onClick={() => showResults(e => !e)} onClick={() => showResults((e) => !e)}
/> />
<Show when={results()}> <Show when={results()}>
<TextPart <TextPart
@ -673,9 +699,11 @@ export default function Share(props: { api: string }) {
</Show> </Show>
</div> </div>
</Match> </Match>
<Match when={ <Match
part().toolInvocation.state === "call" when={
}> part().toolInvocation.state === "call"
}
>
<TextPart <TextPart
data-size="sm" data-size="sm"
data-color="dimmed" data-color="dimmed"
@ -687,20 +715,27 @@ export default function Share(props: { api: string }) {
<PartFooter time={time} /> <PartFooter time={time} />
</div> </div>
</div> </div>
} )}
</Match> </Match>
{ /* Fallback */} {/* Fallback */}
<Match when={true}> <Match when={true}>
<div <div data-section="part" data-part-type="fallback">
data-section="part"
data-part-type="fallback"
>
<div data-section="decoration"> <div data-section="decoration">
<div> <div>
<Switch fallback={ <Switch
<IconWrenchScrewdriver width={16} height={16} /> fallback={
}> <IconWrenchScrewdriver
<Match when={msg.role === "assistant" && part.type !== "tool-invocation"}> width={16}
height={16}
/>
}
>
<Match
when={
msg.role === "assistant" &&
part.type !== "tool-invocation"
}
>
<IconSparkles width={18} height={18} /> <IconSparkles width={18} height={18} />
</Match> </Match>
<Match when={msg.role === "system"}> <Match when={msg.role === "system"}>
@ -718,7 +753,9 @@ export default function Share(props: { api: string }) {
<span data-element-label data-part-title> <span data-element-label data-part-title>
{part.type} {part.type}
</span> </span>
<TextPart text={JSON.stringify(part, null, 2)} /> <TextPart
text={JSON.stringify(part, null, 2)}
/>
</div> </div>
<PartFooter time={time} /> <PartFooter time={time} />
</div> </div>
@ -767,6 +804,6 @@ export default function Share(props: { api: string }) {
</Show> </Show>
</div> </div>
</div> </div>
</main > </main>
) )
} }

View file

@ -7,7 +7,7 @@
} }
.column { .column {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow-x: auto; overflow-x: auto;
min-width: 0; min-width: 0;

View file

@ -3,20 +3,35 @@ import { type JSX } from "solid-js"
// https://icones.js.org/collection/ri?s=openai&icon=ri:openai-fill // https://icones.js.org/collection/ri?s=openai&icon=ri:openai-fill
export function IconOpenAI(props: JSX.SvgSVGAttributes<SVGSVGElement>) { export function IconOpenAI(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return ( return (
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M20.562 10.188c.25-.688.313-1.376.25-2.063c-.062-.687-.312-1.375-.625-2c-.562-.937-1.375-1.687-2.312-2.125c-1-.437-2.063-.562-3.125-.312c-.5-.5-1.063-.938-1.688-1.25S11.687 2 11 2a5.17 5.17 0 0 0-3 .938c-.875.624-1.5 1.5-1.813 2.5c-.75.187-1.375.5-2 .875c-.562.437-1 1-1.375 1.562c-.562.938-.75 2-.625 3.063a5.44 5.44 0 0 0 1.25 2.874a4.7 4.7 0 0 0-.25 2.063c.063.688.313 1.375.625 2c.563.938 1.375 1.688 2.313 2.125c1 .438 2.062.563 3.125.313c.5.5 1.062.937 1.687 1.25S12.312 22 13 22a5.17 5.17 0 0 0 3-.937c.875-.625 1.5-1.5 1.812-2.5a4.54 4.54 0 0 0 1.938-.875c.562-.438 1.062-.938 1.375-1.563c.562-.937.75-2 .625-3.062c-.125-1.063-.5-2.063-1.188-2.876m-7.5 10.5c-1 0-1.75-.313-2.437-.875c0 0 .062-.063.125-.063l4-2.312a.5.5 0 0 0 .25-.25a.57.57 0 0 0 .062-.313V11.25l1.688 1v4.625a3.685 3.685 0 0 1-3.688 3.813M5 17.25c-.438-.75-.625-1.625-.438-2.5c0 0 .063.063.125.063l4 2.312a.56.56 0 0 0 .313.063c.125 0 .25 0 .312-.063l4.875-2.812v1.937l-4.062 2.375A3.7 3.7 0 0 1 7.312 19c-1-.25-1.812-.875-2.312-1.75M3.937 8.563a3.8 3.8 0 0 1 1.938-1.626v4.751c0 .124 0 .25.062.312a.5.5 0 0 0 .25.25l4.875 2.813l-1.687 1l-4-2.313a3.7 3.7 0 0 1-1.75-2.25c-.25-.937-.188-2.062.312-2.937M17.75 11.75l-4.875-2.812l1.687-1l4 2.312c.625.375 1.125.875 1.438 1.5s.5 1.313.437 2.063a3.7 3.7 0 0 1-.75 1.937c-.437.563-1 1-1.687 1.25v-4.75c0-.125 0-.25-.063-.312c0 0-.062-.126-.187-.188m1.687-2.5s-.062-.062-.125-.062l-4-2.313c-.125-.062-.187-.062-.312-.062s-.25 0-.313.062L9.812 9.688V7.75l4.063-2.375c.625-.375 1.312-.5 2.062-.5c.688 0 1.375.25 2 .688c.563.437 1.063 1 1.313 1.625s.312 1.375.187 2.062m-10.5 3.5l-1.687-1V7.063c0-.688.187-1.438.562-2C8.187 4.438 8.75 4 9.375 3.688a3.37 3.37 0 0 1 2.062-.313c.688.063 1.375.375 1.938.813c0 0-.063.062-.125.062l-4 2.313a.5.5 0 0 0-.25.25c-.063.125-.063.187-.063.312zm.875-2L12 9.5l2.187 1.25v2.5L12 14.5l-2.188-1.25z" /></svg> <svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M20.562 10.188c.25-.688.313-1.376.25-2.063c-.062-.687-.312-1.375-.625-2c-.562-.937-1.375-1.687-2.312-2.125c-1-.437-2.063-.562-3.125-.312c-.5-.5-1.063-.938-1.688-1.25S11.687 2 11 2a5.17 5.17 0 0 0-3 .938c-.875.624-1.5 1.5-1.813 2.5c-.75.187-1.375.5-2 .875c-.562.437-1 1-1.375 1.562c-.562.938-.75 2-.625 3.063a5.44 5.44 0 0 0 1.25 2.874a4.7 4.7 0 0 0-.25 2.063c.063.688.313 1.375.625 2c.563.938 1.375 1.688 2.313 2.125c1 .438 2.062.563 3.125.313c.5.5 1.062.937 1.687 1.25S12.312 22 13 22a5.17 5.17 0 0 0 3-.937c.875-.625 1.5-1.5 1.812-2.5a4.54 4.54 0 0 0 1.938-.875c.562-.438 1.062-.938 1.375-1.563c.562-.937.75-2 .625-3.062c-.125-1.063-.5-2.063-1.188-2.876m-7.5 10.5c-1 0-1.75-.313-2.437-.875c0 0 .062-.063.125-.063l4-2.312a.5.5 0 0 0 .25-.25a.57.57 0 0 0 .062-.313V11.25l1.688 1v4.625a3.685 3.685 0 0 1-3.688 3.813M5 17.25c-.438-.75-.625-1.625-.438-2.5c0 0 .063.063.125.063l4 2.312a.56.56 0 0 0 .313.063c.125 0 .25 0 .312-.063l4.875-2.812v1.937l-4.062 2.375A3.7 3.7 0 0 1 7.312 19c-1-.25-1.812-.875-2.312-1.75M3.937 8.563a3.8 3.8 0 0 1 1.938-1.626v4.751c0 .124 0 .25.062.312a.5.5 0 0 0 .25.25l4.875 2.813l-1.687 1l-4-2.313a3.7 3.7 0 0 1-1.75-2.25c-.25-.937-.188-2.062.312-2.937M17.75 11.75l-4.875-2.812l1.687-1l4 2.312c.625.375 1.125.875 1.438 1.5s.5 1.313.437 2.063a3.7 3.7 0 0 1-.75 1.937c-.437.563-1 1-1.687 1.25v-4.75c0-.125 0-.25-.063-.312c0 0-.062-.126-.187-.188m1.687-2.5s-.062-.062-.125-.062l-4-2.313c-.125-.062-.187-.062-.312-.062s-.25 0-.313.062L9.812 9.688V7.75l4.063-2.375c.625-.375 1.312-.5 2.062-.5c.688 0 1.375.25 2 .688c.563.437 1.063 1 1.313 1.625s.312 1.375.187 2.062m-10.5 3.5l-1.687-1V7.063c0-.688.187-1.438.562-2C8.187 4.438 8.75 4 9.375 3.688a3.37 3.37 0 0 1 2.062-.313c.688.063 1.375.375 1.938.813c0 0-.063.062-.125.062l-4 2.313a.5.5 0 0 0-.25.25c-.063.125-.063.187-.063.312zm.875-2L12 9.5l2.187 1.25v2.5L12 14.5l-2.188-1.25z"
/>
</svg>
) )
} }
// https://icones.js.org/collection/ri?s=anthropic&icon=ri:anthropic-fill // https://icones.js.org/collection/ri?s=anthropic&icon=ri:anthropic-fill
export function IconAnthropic(props: JSX.SvgSVGAttributes<SVGSVGElement>) { export function IconAnthropic(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return ( return (
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M16.765 5h-3.308l5.923 15h3.23zM7.226 5L1.38 20h3.308l1.307-3.154h6.154l1.23 3.077h3.309L10.688 5zm-.308 9.077l2-5.308l2.077 5.308z" /></svg> <svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M16.765 5h-3.308l5.923 15h3.23zM7.226 5L1.38 20h3.308l1.307-3.154h6.154l1.23 3.077h3.309L10.688 5zm-.308 9.077l2-5.308l2.077 5.308z"
/>
</svg>
) )
} }
// https://icones.js.org/collection/ri?s=gemini&icon=ri:gemini-fill // https://icones.js.org/collection/ri?s=gemini&icon=ri:gemini-fill
export function IconGemini(props: JSX.SvgSVGAttributes<SVGSVGElement>) { export function IconGemini(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return ( return (
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M24 12.024c-6.437.388-11.59 5.539-11.977 11.976h-.047C11.588 17.563 6.436 12.412 0 12.024v-.047C6.437 11.588 11.588 6.437 11.976 0h.047c.388 6.437 5.54 11.588 11.977 11.977z" /></svg> <svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M24 12.024c-6.437.388-11.59 5.539-11.977 11.976h-.047C11.588 17.563 6.436 12.412 0 12.024v-.047C6.437 11.588 11.588 6.437 11.976 0h.047c.388 6.437 5.54 11.588 11.977 11.977z"
/>
</svg>
) )
} }

View file

@ -21,7 +21,7 @@ export function IconAcademicCap(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
) )
} }
export function IconAdjustmentsHorizontal( export function IconAdjustmentsHorizontal(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -42,7 +42,7 @@ export function IconAdjustmentsHorizontal(
) )
} }
export function IconAdjustmentsVertical( export function IconAdjustmentsVertical(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -63,7 +63,7 @@ export function IconAdjustmentsVertical(
) )
} }
export function IconArchiveBoxArrowDown( export function IconArchiveBoxArrowDown(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -84,7 +84,7 @@ export function IconArchiveBoxArrowDown(
) )
} }
export function IconArchiveBoxXMark( export function IconArchiveBoxXMark(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -124,7 +124,7 @@ export function IconArchiveBox(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
) )
} }
export function IconArrowDownCircle( export function IconArrowDownCircle(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -164,7 +164,7 @@ export function IconArrowDownLeft(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
) )
} }
export function IconArrowDownOnSquareStack( export function IconArrowDownOnSquareStack(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -185,7 +185,7 @@ export function IconArrowDownOnSquareStack(
) )
} }
export function IconArrowDownOnSquare( export function IconArrowDownOnSquare(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -263,7 +263,7 @@ export function IconArrowDown(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
) )
} }
export function IconArrowLeftCircle( export function IconArrowLeftCircle(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -284,7 +284,7 @@ export function IconArrowLeftCircle(
) )
} }
export function IconArrowLeftOnRectangle( export function IconArrowLeftOnRectangle(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -400,7 +400,7 @@ export function IconArrowLongUp(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
) )
} }
export function IconArrowPathRoundedSquare( export function IconArrowPathRoundedSquare(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -440,7 +440,7 @@ export function IconArrowPath(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
) )
} }
export function IconArrowRightCircle( export function IconArrowRightCircle(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -461,7 +461,7 @@ export function IconArrowRightCircle(
) )
} }
export function IconArrowRightOnRectangle( export function IconArrowRightOnRectangle(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -539,7 +539,7 @@ export function IconArrowSmallLeft(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
) )
} }
export function IconArrowSmallRight( export function IconArrowSmallRight(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -579,7 +579,7 @@ export function IconArrowSmallUp(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
) )
} }
export function IconArrowTopRightOnSquare( export function IconArrowTopRightOnSquare(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -600,7 +600,7 @@ export function IconArrowTopRightOnSquare(
) )
} }
export function IconArrowTrendingDown( export function IconArrowTrendingDown(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -621,7 +621,7 @@ export function IconArrowTrendingDown(
) )
} }
export function IconArrowTrendingUp( export function IconArrowTrendingUp(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -680,7 +680,7 @@ export function IconArrowUpLeft(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
) )
} }
export function IconArrowUpOnSquareStack( export function IconArrowUpOnSquareStack(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -701,7 +701,7 @@ export function IconArrowUpOnSquareStack(
) )
} }
export function IconArrowUpOnSquare( export function IconArrowUpOnSquare(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -817,7 +817,7 @@ export function IconArrowUturnLeft(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
) )
} }
export function IconArrowUturnRight( export function IconArrowUturnRight(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -857,7 +857,7 @@ export function IconArrowUturnUp(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
) )
} }
export function IconArrowsPointingIn( export function IconArrowsPointingIn(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -878,7 +878,7 @@ export function IconArrowsPointingIn(
) )
} }
export function IconArrowsPointingOut( export function IconArrowsPointingOut(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -899,7 +899,7 @@ export function IconArrowsPointingOut(
) )
} }
export function IconArrowsRightLeft( export function IconArrowsRightLeft(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -1040,7 +1040,7 @@ export function IconBars2(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
) )
} }
export function IconBars3BottomLeft( export function IconBars3BottomLeft(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -1061,7 +1061,7 @@ export function IconBars3BottomLeft(
) )
} }
export function IconBars3BottomRight( export function IconBars3BottomRight(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -1082,7 +1082,7 @@ export function IconBars3BottomRight(
) )
} }
export function IconBars3CenterLeft( export function IconBars3CenterLeft(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -1507,7 +1507,7 @@ export function IconBugAnt(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
) )
} }
export function IconBuildingLibrary( export function IconBuildingLibrary(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -1528,7 +1528,7 @@ export function IconBuildingLibrary(
) )
} }
export function IconBuildingOffice2( export function IconBuildingOffice2(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -1568,7 +1568,7 @@ export function IconBuildingOffice(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
) )
} }
export function IconBuildingStorefront( export function IconBuildingStorefront(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -1776,7 +1776,7 @@ export function IconChartPie(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
) )
} }
export function IconChatBubbleBottomCenterText( export function IconChatBubbleBottomCenterText(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -1797,7 +1797,7 @@ export function IconChatBubbleBottomCenterText(
) )
} }
export function IconChatBubbleBottomCenter( export function IconChatBubbleBottomCenter(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -1818,7 +1818,7 @@ export function IconChatBubbleBottomCenter(
) )
} }
export function IconChatBubbleLeftEllipsis( export function IconChatBubbleLeftEllipsis(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -1839,7 +1839,7 @@ export function IconChatBubbleLeftEllipsis(
) )
} }
export function IconChatBubbleLeftRight( export function IconChatBubbleLeftRight(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -1879,7 +1879,7 @@ export function IconChatBubbleLeft(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
) )
} }
export function IconChatBubbleOvalLeftEllipsis( export function IconChatBubbleOvalLeftEllipsis(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -1900,7 +1900,7 @@ export function IconChatBubbleOvalLeftEllipsis(
) )
} }
export function IconChatBubbleOvalLeft( export function IconChatBubbleOvalLeft(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -1978,7 +1978,7 @@ export function IconCheck(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
) )
} }
export function IconChevronDoubleDown( export function IconChevronDoubleDown(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -1999,7 +1999,7 @@ export function IconChevronDoubleDown(
) )
} }
export function IconChevronDoubleLeft( export function IconChevronDoubleLeft(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -2020,7 +2020,7 @@ export function IconChevronDoubleLeft(
) )
} }
export function IconChevronDoubleRight( export function IconChevronDoubleRight(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -2041,7 +2041,7 @@ export function IconChevronDoubleRight(
) )
} }
export function IconChevronDoubleUp( export function IconChevronDoubleUp(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -2176,7 +2176,7 @@ export function IconCircleStack(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
) )
} }
export function IconClipboardDocumentCheck( export function IconClipboardDocumentCheck(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -2197,7 +2197,7 @@ export function IconClipboardDocumentCheck(
) )
} }
export function IconClipboardDocumentList( export function IconClipboardDocumentList(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -2218,7 +2218,7 @@ export function IconClipboardDocumentList(
) )
} }
export function IconClipboardDocument( export function IconClipboardDocument(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -2334,7 +2334,7 @@ export function IconCloud(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
) )
} }
export function IconCodeBracketSquare( export function IconCodeBracketSquare(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -2464,7 +2464,7 @@ export function IconCommandLine(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
) )
} }
export function IconComputerDesktop( export function IconComputerDesktop(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -2523,7 +2523,7 @@ export function IconCreditCard(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
) )
} }
export function IconCubeTransparent( export function IconCubeTransparent(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -2563,7 +2563,7 @@ export function IconCube(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
) )
} }
export function IconCurrencyBangladeshi( export function IconCurrencyBangladeshi(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -2679,7 +2679,7 @@ export function IconCurrencyYen(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
) )
} }
export function IconCursorArrowRays( export function IconCursorArrowRays(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -2700,7 +2700,7 @@ export function IconCursorArrowRays(
) )
} }
export function IconCursorArrowRipple( export function IconCursorArrowRipple(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -2721,7 +2721,7 @@ export function IconCursorArrowRipple(
) )
} }
export function IconDevicePhoneMobile( export function IconDevicePhoneMobile(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -2761,7 +2761,7 @@ export function IconDeviceTablet(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
) )
} }
export function IconDocumentArrowDown( export function IconDocumentArrowDown(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -2782,7 +2782,7 @@ export function IconDocumentArrowDown(
) )
} }
export function IconDocumentArrowUp( export function IconDocumentArrowUp(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -2803,7 +2803,7 @@ export function IconDocumentArrowUp(
) )
} }
export function IconDocumentChartBar( export function IconDocumentChartBar(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -2843,7 +2843,7 @@ export function IconDocumentCheck(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
) )
} }
export function IconDocumentDuplicate( export function IconDocumentDuplicate(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -2864,7 +2864,7 @@ export function IconDocumentDuplicate(
) )
} }
export function IconDocumentMagnifyingGlass( export function IconDocumentMagnifyingGlass(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -2961,7 +2961,7 @@ export function IconDocument(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
) )
} }
export function IconEllipsisHorizontalCircle( export function IconEllipsisHorizontalCircle(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -2982,7 +2982,7 @@ export function IconEllipsisHorizontalCircle(
) )
} }
export function IconEllipsisHorizontal( export function IconEllipsisHorizontal(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -3017,7 +3017,7 @@ export function IconEllipsisHorizontal(
) )
} }
export function IconEllipsisVertical( export function IconEllipsisVertical(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -3103,7 +3103,7 @@ export function IconEnvelopeSolid(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
) )
} }
export function IconExclamationCircle( export function IconExclamationCircle(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -3124,7 +3124,7 @@ export function IconExclamationCircle(
) )
} }
export function IconExclamationTriangle( export function IconExclamationTriangle(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -3330,7 +3330,7 @@ export function IconFlag(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
) )
} }
export function IconFolderArrowDown( export function IconFolderArrowDown(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -3567,7 +3567,7 @@ export function IconGlobeAmericas(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
) )
} }
export function IconGlobeAsiaAustralia( export function IconGlobeAsiaAustralia(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -3588,7 +3588,7 @@ export function IconGlobeAsiaAustralia(
) )
} }
export function IconGlobeEuropeAfrica( export function IconGlobeEuropeAfrica(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -3818,7 +3818,7 @@ export function IconInbox(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
) )
} }
export function IconInformationCircle( export function IconInformationCircle(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -3991,7 +3991,7 @@ export function IconLockOpen(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
) )
} }
export function IconMagnifyingGlassCircle( export function IconMagnifyingGlassCircle(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -4012,7 +4012,7 @@ export function IconMagnifyingGlassCircle(
) )
} }
export function IconMagnifyingGlassMinus( export function IconMagnifyingGlassMinus(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -4033,7 +4033,7 @@ export function IconMagnifyingGlassMinus(
) )
} }
export function IconMagnifyingGlassPlus( export function IconMagnifyingGlassPlus(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -4054,7 +4054,7 @@ export function IconMagnifyingGlassPlus(
) )
} }
export function IconMagnifyingGlass( export function IconMagnifyingGlass(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -4424,7 +4424,7 @@ export function IconPencil(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
) )
} }
export function IconPhoneArrowDownLeft( export function IconPhoneArrowDownLeft(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -4445,7 +4445,7 @@ export function IconPhoneArrowDownLeft(
) )
} }
export function IconPhoneArrowUpRight( export function IconPhoneArrowUpRight(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -4663,7 +4663,7 @@ export function IconPower(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
) )
} }
export function IconPresentationChartBar( export function IconPresentationChartBar(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -4684,7 +4684,7 @@ export function IconPresentationChartBar(
) )
} }
export function IconPresentationChartLine( export function IconPresentationChartLine(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -4832,7 +4832,7 @@ export function IconQrCode(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
) )
} }
export function IconQuestionMarkCircle( export function IconQuestionMarkCircle(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -5133,7 +5133,7 @@ export function IconShieldCheck(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
) )
} }
export function IconShieldExclamation( export function IconShieldExclamation(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -5780,7 +5780,7 @@ export function IconVariable(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
) )
} }
export function IconVideoCameraSlash( export function IconVideoCameraSlash(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -5838,7 +5838,7 @@ export function IconViewColumns(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
) )
} }
export function IconViewfinderCircle( export function IconViewfinderCircle(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg
@ -5916,7 +5916,7 @@ export function IconWindow(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
) )
} }
export function IconWrenchScrewdriver( export function IconWrenchScrewdriver(
props: JSX.SvgSVGAttributes<SVGSVGElement> props: JSX.SvgSVGAttributes<SVGSVGElement>,
) { ) {
return ( return (
<svg <svg

View file

@ -84,11 +84,21 @@
span:first-child { span:first-child {
color: var(--sl-color-divider); color: var(--sl-color-divider);
&[data-status="connected"] { color: var(--sl-color-green); } &[data-status="connected"] {
&[data-status="connecting"] { color: var(--sl-color-orange); } color: var(--sl-color-green);
&[data-status="disconnected"] { color: var(--sl-color-divider); } }
&[data-status="reconnecting"] { color: var(--sl-color-orange); } &[data-status="connecting"] {
&[data-status="error"] { color: var(--sl-color-red); } color: var(--sl-color-orange);
}
&[data-status="disconnected"] {
color: var(--sl-color-divider);
}
&[data-status="reconnecting"] {
color: var(--sl-color-orange);
}
&[data-status="error"] {
color: var(--sl-color-red);
}
} }
} }
@ -106,7 +116,7 @@
font-size: 0.875rem; font-size: 0.875rem;
span[data-placeholder] { span[data-placeholder] {
color: var(--sl-color-text-dimmed); color: var(--sl-color-text-dimmed);
} }
} }
} }
@ -215,16 +225,15 @@
max-width: 100%; max-width: 100%;
gap: 0.25rem 0.375rem; gap: 0.25rem 0.375rem;
& > div:nth-child(3n + 1) {
& > div:nth-child(3n+1) {
width: 8px; width: 8px;
height: 2px; height: 2px;
border-radius: 1px; border-radius: 1px;
background: var(--sl-color-divider); background: var(--sl-color-divider);
} }
& > div:nth-child(3n+2), & > div:nth-child(3n + 2),
& > div:nth-child(3n+3) { & > div:nth-child(3n + 3) {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -232,7 +241,7 @@
line-height: 1.5; line-height: 1.5;
} }
& > div:nth-child(3n+3) { & > div:nth-child(3n + 3) {
padding-left: 0.125rem; padding-left: 0.125rem;
color: var(--sl-color-text-dimmed); color: var(--sl-color-text-dimmed);
} }

View file

@ -1,7 +1,7 @@
import { defineCollection } from 'astro:content'; import { defineCollection } from "astro:content"
import { docsLoader } from '@astrojs/starlight/loaders'; import { docsLoader } from "@astrojs/starlight/loaders"
import { docsSchema } from '@astrojs/starlight/schema'; import { docsSchema } from "@astrojs/starlight/schema"
export const collections = { export const collections = {
docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }), docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
}; }

View file

@ -20,21 +20,21 @@ Or start with a specific working directory.
opencode -c /path/to/project opencode -c /path/to/project
``` ```
## Flags ## Flags
The OpenCode CLI takes the following flags. The OpenCode CLI takes the following flags.
| Flag | Short | Description | | Flag | Short | Description |
| -- | -- | -- | | ----------------- | ----- | -------------------------------------------------------- |
| `--help` | `-h` | Display help | | `--help` | `-h` | Display help |
| `--debug` | `-d` | Enable debug mode | | `--debug` | `-d` | Enable debug mode |
| `--cwd` | `-c` | Set current working directory | | `--cwd` | `-c` | Set current working directory |
| `--prompt` | `-p` | Run a single prompt in non-interactive mode | | `--prompt` | `-p` | Run a single prompt in non-interactive mode |
| `--output-format` | `-f` | Output format for non-interactive mode, `text` or `json` | | `--output-format` | `-f` | Output format for non-interactive mode, `text` or `json` |
| `--quiet` | `-q` | Hide spinner in non-interactive mode | | `--quiet` | `-q` | Hide spinner in non-interactive mode |
| `--verbose` | | Display logs to stderr in non-interactive mode | | `--verbose` | | Display logs to stderr in non-interactive mode |
| `--allowedTools` | | Restrict the agent to only use specified tools | | `--allowedTools` | | Restrict the agent to only use specified tools |
| `--excludedTools` | | Prevent the agent from using specified tools | | `--excludedTools` | | Prevent the agent from using specified tools |
## Non-interactive ## Non-interactive

View file

@ -73,16 +73,15 @@ The config file has the following structure.
For the providers, you can also specify the keys using environment variables. For the providers, you can also specify the keys using environment variables.
| Environment Variable | Models | | Environment Variable | Models |
| -------------------------- | ----------- | | -------------------------- | ------------------------------------------ |
| `ANTHROPIC_API_KEY` | Claude | | `ANTHROPIC_API_KEY` | Claude |
| `OPENAI_API_KEY` | OpenAI | | `OPENAI_API_KEY` | OpenAI |
| `GEMINI_API_KEY` | Google Gemini | | `GEMINI_API_KEY` | Google Gemini |
| `GROQ_API_KEY` | Groq | | `GROQ_API_KEY` | Groq |
| `AWS_ACCESS_KEY_ID` | Amazon Bedrock | | `AWS_ACCESS_KEY_ID` | Amazon Bedrock |
| `AWS_SECRET_ACCESS_KEY` | Amazon Bedrock | | `AWS_SECRET_ACCESS_KEY` | Amazon Bedrock |
| `AWS_REGION` | Amazon Bedrock | | `AWS_REGION` | Amazon Bedrock |
| `AZURE_OPENAI_ENDPOINT` | Azure OpenAI | | `AZURE_OPENAI_ENDPOINT` | Azure OpenAI |
| `AZURE_OPENAI_API_KEY` | Azure OpenAI, optional when using Entra ID | | `AZURE_OPENAI_API_KEY` | Azure OpenAI, optional when using Entra ID |
| `AZURE_OPENAI_API_VERSION` | Azure OpenAI | | `AZURE_OPENAI_API_VERSION` | Azure OpenAI |

View file

@ -55,14 +55,14 @@ You can create your own custom theme by setting the `theme: custom` and providin
You can define any of the following color keys in your `customTheme`. You can define any of the following color keys in your `customTheme`.
| Type | Color keys | | Type | Color keys |
| --- | --- | | ----------------- | ------------------------------------------------------- |
| Base colors | `primary`, `secondary`, `accent` | | Base colors | `primary`, `secondary`, `accent` |
| Status colors | `error`, `warning`, `success`, `info` | | Status colors | `error`, `warning`, `success`, `info` |
| Text colors | `text`, `textMuted`, `textEmphasized` | | Text colors | `text`, `textMuted`, `textEmphasized` |
| Background colors | `background`, `backgroundSecondary`, `backgroundDarker` | | Background colors | `background`, `backgroundSecondary`, `backgroundDarker` |
| Border colors | `borderNormal`, `borderFocused`, `borderDim` | | Border colors | `borderNormal`, `borderFocused`, `borderDim` |
| Diff view colors | `diffAdded`, `diffRemoved`, `diffContext`, etc. | | Diff view colors | `diffAdded`, `diffRemoved`, `diffContext`, etc. |
You don't need to define all the color keys. Any undefined colors will fall back to the default `opencode` theme colors. You don't need to define all the color keys. Any undefined colors will fall back to the default `opencode` theme colors.

View file

@ -6,4 +6,4 @@
/// <reference path="../../sst-env.d.ts" /> /// <reference path="../../sst-env.d.ts" />
import "sst" import "sst"
export {} export {}

18
sst-env.d.ts vendored
View file

@ -5,20 +5,20 @@
declare module "sst" { declare module "sst" {
export interface Resource { export interface Resource {
"Api": { Api: {
"type": "sst.cloudflare.Worker" type: "sst.cloudflare.Worker"
"url": string url: string
} }
"Bucket": { Bucket: {
"type": "sst.cloudflare.Bucket" type: "sst.cloudflare.Bucket"
} }
"Web": { Web: {
"type": "sst.cloudflare.StaticSite" type: "sst.cloudflare.StaticSite"
"url": string url: string
} }
} }
} }
/// <reference path="sst-env.d.ts" /> /// <reference path="sst-env.d.ts" />
import "sst" import "sst"
export {} export {}