Compare commits

...

19 Commits

Author SHA1 Message Date
Dax Raad fa1266263d downgrade to ai sdk v4.x 2025-06-14 18:44:08 -04:00
Gal Schlezinger fe109c921e add focus tracking for tui so cursor will hide when not in focus (#103) 2025-06-14 14:53:43 -05:00
Dax Raad 37bb8895fe docs: readme 2025-06-14 14:52:02 -04:00
Dax Raad 89b95be4de docs: provider config 2025-06-14 14:45:59 -04:00
Dax Raad eaf295bac7 docs: faq 2025-06-14 14:39:13 -04:00
Mantena Rama Raju 27d3cec477 typo (#94) 2025-06-14 14:36:29 -04:00
Dax Raad 574d494c3c Enhance provider system with dynamic package resolution and improved logging
- Add npm registry lookup for AI SDK packages with fallback support
- Enhance error logging with cause information
- Add timing deltas to log output for performance monitoring

🤖 Generated with [opencode](https://opencode.ai)

Co-Authored-By: opencode <noreply@opencode.ai>
2025-06-14 14:35:33 -04:00
Albert Ilagan 0239761f31 tui: remove quit dialog (#97) 2025-06-14 12:47:34 -05:00
Dax Raad a53f9165e9 doc: remove dev script 2025-06-14 13:05:23 -04:00
Dax Raad ffc231bd8b docs: contributing 2025-06-14 12:45:26 -04:00
Dax Raad 3cf4ef56fb sync 2025-06-14 12:32:41 -04:00
Dax Raad c738e26438 docs: mcp 2025-06-14 12:25:26 -04:00
Dax Raad 9c6aa82ac1 docs: config schema 2025-06-14 12:22:07 -04:00
Dax Raad ef74d97491 ci: update publish script 2025-06-14 12:13:59 -04:00
Dax Raad af892e5432 docs: readme 2025-06-14 12:13:46 -04:00
Dax Raad d7aca6230d naming fixes 2025-06-14 01:54:28 -04:00
Dax Raad 0f9c2c5c27 Add flag system and auto-share functionality
- Add Flag module for environment variable configuration
- Implement OPENCODE_AUTO_SHARE flag to automatically share new sessions
- Update session creation to conditionally auto-share based on flag

🤖 Generated with [OpenCode](https://opencode.ai)

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-14 01:51:04 -04:00
Dax Raad 6a261dedb4 Improve logging and simplify fzf implementation
- Refactor fzf search to use Bun's $ syntax for cleaner command execution
- Add request/response duration logging to server middleware
- Set default service name for logging to improve log clarity

🤖 Generated with [OpenCode](https://opencode.ai)

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-14 01:51:04 -04:00
Alireza Bahrami ec928d88b5 fix(install): check if the path export command already exists (#28) 2025-06-13 23:28:33 -04:00
26 changed files with 651 additions and 227 deletions
+124 -21
View File
@@ -2,39 +2,142 @@
AI coding agent, built for the terminal.
Note: version 0.1.x is a full rewrite and we do not have proper documentation for it yet. Should have this out week of June 17th 2025
⚠️ **Note:** version 0.1.x is a full rewrite and we do not have proper documentation for it yet. Should have this out week of June 17th 2025 📚
## Installation
### Installation
If you have a previous version of opencode < 0.1.x installed you might have to remove it first.
### Curl
```
```bash
# YOLO
curl -fsSL https://opencode.ai/install | bash
# Package managers
npm i -g opencode-ai@latest # or bun/pnpm/yarn
brew install sst/tap/opencode # macOS
paru -S opencode-bin # Arch Linux
```
### NPM
> **Note:** Remove previous versions < 0.1.x first if installed
```
npm i -g opencode-ai@latest
bun i -g opencode-ai@latest
pnpm i -g opencode-ai@latest
yarn global add opencode-ai@latest
### Providers
The recommended approach is to sign up for claude pro or max and do `opencode auth login` and select Anthropic. It is the most cost effective way to use this tool.
Additionally opencode is powered by the provider list at [models.dev](https://models.dev) so you can use `opencode auth login` to configure api keys for any provider you'd like to use. This is stored in `~/.local/share/opencode/auth.json`
```bash
$ opencode auth login
┌ Add credential
◆ Select provider
│ ● Anthropic (recommended)
│ ○ OpenAI
│ ○ Google
│ ○ Amazon Bedrock
│ ○ Azure
│ ○ DeepSeek
│ ○ Groq
│ ...
```
### Brew
The models.dev dataset is also used to detect common environment variables like `OPENAI_API_KEY` to autoload that provider.
```
brew install sst/tap/opencode
If there are additional providers you want to use you can submit a PR to the [models.dev repo](https://github.com/sst/models.dev). If configuring just for yourself check out the Config section below
### Project Config
Project configuration is optional. You can place an `opencode.json` file in the root of your repo and it will be loaded.
```json title="opencode.json"
{
"$schema": "http://opencode.ai/config.json"
}
```
### AUR
#### MCP
```
paru -S opencode-bin
```json title="opencode.json"
{
"$schema": "http://opencode.ai/config.json",
"mcp": {
"localmcp": {
"type": "local",
"command": ["bun", "x", "my-mcp-command"],
"environment": {
"MY_ENV_VAR": "my_env_var_value"
}
},
"remotemcp": {
"type": "remote",
"url": "https://my-mcp-server.com"
}
}
}
```
## Usage
#### Providers
We are working on proper keybinds - right now it's the various function keys press F1 to see them
You can use opencode with any provider listed at [here](https://ai-sdk.dev/providers/ai-sdk-providers). Use the npm package name as the key in your config. Note we use v5 of the ai-sdk and not all providers support that yet.
```json title="opencode.json"
{
"$schema": "http://opencode.ai/config.json",
"provider": {
"@ai-sdk/openai-compatible": {
"name": "MySpecialProvider",
"options": {
"apiKey": "xxx",
"baseURL": "https://api.provider.com/v1"
},
"models": {
"my-model-name": {
"name": "My Model Name"
}
}
}
}
}
```
### Contributing
To run opencode locally you need
- bun
- golang 1.24.x
To run
```
$ bun install
$ cd packages/opencode
$ bun run src/index.ts
```
### FAQ
#### How do I use this with OpenRouter
Theoretically you can use this with OpenRouter with config like this
```json title="opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"@openrouter/ai-sdk-provider": {
"name": "OpenRouter",
"options": {
"apiKey": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
"models": {
"anthropic/claude-3.5-sonnet": {
"name": "Claude 3.5 Sonnet"
}
}
}
}
}
```
However we are using [ai-sdk v5](https://ai-sdk.dev) which OpenRouter does not support yet. The moment they do this will work
+24 -5
View File
@@ -50,6 +50,7 @@
"@types/turndown": "5.0.5",
"@types/yargs": "17.0.33",
"typescript": "catalog:",
"zod-to-json-schema": "3.24.5",
},
},
"packages/web": {
@@ -90,16 +91,18 @@
},
"catalog": {
"@types/node": "22.13.9",
"ai": "5.0.0-alpha.7",
"ai": "4.3.16",
"typescript": "5.8.2",
"zod": "3.24.2",
},
"packages": {
"@ai-sdk/gateway": ["@ai-sdk/gateway@1.0.0-alpha.7", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-alpha.7", "@ai-sdk/provider-utils": "3.0.0-alpha.7" }, "peerDependencies": { "zod": "^3.24.0" } }, "sha512-gz1V165eiJnQIexfLyKm11vimrmQ3zdcJhPpjeLFmDU9wrvZwLuklfZ0WgfYSb+EjiP1cKypwt6JSGvWkfKIAQ=="],
"@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="],
"@ai-sdk/provider": ["@ai-sdk/provider@2.0.0-alpha.7", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-lhdrARU3SSmt5p/GNNK7VhazvZpKSCIOjpHUfX7f5jIhVGi/vvlxP1rD6Go57nn1MtuGKNqL04AebSRFDQsQbw=="],
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.2.8", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA=="],
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0-alpha.7", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-alpha.7", "@standard-schema/spec": "^1.0.0", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-AYkT3jskmo7Lwzijo/yHKD1jC+UZizsROO8ULTg9aJZUwR4ABZzAxh4NxDIEy4TWRfBGufp+/9ICHAn6pkU71w=="],
"@ai-sdk/react": ["@ai-sdk/react@1.2.12", "", { "dependencies": { "@ai-sdk/provider-utils": "2.2.8", "@ai-sdk/ui-utils": "1.2.11", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.23.8" }, "optionalPeers": ["zod"] }, "sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g=="],
"@ai-sdk/ui-utils": ["@ai-sdk/ui-utils@1.2.11", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w=="],
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
@@ -429,6 +432,8 @@
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
"@types/diff-match-patch": ["@types/diff-match-patch@1.0.36", "", {}, "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg=="],
"@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
"@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
@@ -473,7 +478,7 @@
"acorn-walk": ["acorn-walk@8.3.2", "", {}, "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A=="],
"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@4.3.16", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "@ai-sdk/react": "1.2.12", "@ai-sdk/ui-utils": "1.2.11", "@opentelemetry/api": "1.9.0", "jsondiffpatch": "0.6.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.23.8" }, "optionalPeers": ["react"] }, "sha512-KUDwlThJ5tr2Vw0A1ZkbDKNME3wzWhuVfAOwIvFUzl1TPVDFAXDFTXio3p+jaKneB+dKNCvFFlolYmmgHttG1g=="],
"ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="],
@@ -685,6 +690,8 @@
"diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="],
"diff-match-patch": ["diff-match-patch@1.0.5", "", {}, "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="],
"direction": ["direction@2.0.1", "", { "bin": { "direction": "cli.js" } }, "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA=="],
"dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="],
@@ -969,6 +976,8 @@
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"jsondiffpatch": ["jsondiffpatch@0.6.0", "", { "dependencies": { "@types/diff-match-patch": "^1.0.36", "chalk": "^5.3.0", "diff-match-patch": "^1.0.5" }, "bin": { "jsondiffpatch": "bin/jsondiffpatch.js" } }, "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ=="],
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
"klona": ["klona@2.0.6", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="],
@@ -1273,6 +1282,8 @@
"rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
"react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
@@ -1353,6 +1364,8 @@
"sax": ["sax@1.2.1", "", {}, "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA=="],
"secure-json-parse": ["secure-json-parse@2.7.0", "", {}, "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="],
"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=="],
@@ -1453,6 +1466,8 @@
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"swr": ["swr@2.3.3", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A=="],
"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=="],
@@ -1461,6 +1476,8 @@
"thread-stream": ["thread-stream@0.15.2", "", { "dependencies": { "real-require": "^0.1.0" } }, "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA=="],
"throttleit": ["throttleit@2.1.0", "", {}, "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw=="],
"tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
"tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
@@ -1545,6 +1562,8 @@
"url": ["url@0.10.3", "", { "dependencies": { "punycode": "1.3.2", "querystring": "0.2.0" } }, "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ=="],
"use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="],
"util": ["util@0.12.5", "", { "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", "is-generator-function": "^1.0.7", "is-typed-array": "^1.1.3", "which-typed-array": "^1.1.2" } }, "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
+4 -1
View File
@@ -107,7 +107,9 @@ add_to_path() {
local config_file=$1
local command=$2
if [[ -w $config_file ]]; then
if grep -Fxq "$command" "$config_file"; then
print_message info "Command already exists in $config_file, skipping write."
elif [[ -w $config_file ]]; then
echo -e "\n# opencode" >> "$config_file"
echo "$command" >> "$config_file"
print_message info "Successfully added ${ORANGE}opencode ${GREEN}to \$PATH in $config_file"
@@ -173,6 +175,7 @@ if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
;;
*)
export PATH=$INSTALL_DIR:$PATH
print_message warning "Manually add the directory to $config_file (or similar):"
print_message info " export PATH=$INSTALL_DIR:\$PATH"
;;
+16
View File
@@ -0,0 +1,16 @@
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"@openrouter/ai-sdk-provider": {
"name": "OpenRouter",
"options": {
"apiKey": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
"models": {
"anthropic/claude-3.5-sonnet": {
"name": "claude-3.5-sonnet"
}
}
}
}
}
+2 -3
View File
@@ -5,8 +5,7 @@
"type": "module",
"packageManager": "bun@1.2.14",
"scripts": {
"typecheck": "bun run --filter='*' typecheck",
"dev": "sst dev"
"typecheck": "bun run --filter='*' typecheck"
},
"workspaces": {
"packages": [
@@ -16,7 +15,7 @@
"typescript": "5.8.2",
"@types/node": "22.13.9",
"zod": "3.24.2",
"ai": "5.0.0-alpha.7"
"ai": "4.3.16"
}
},
"devDependencies": {
+152
View File
@@ -0,0 +1,152 @@
{
"type": "object",
"properties": {
"$schema": {
"type": "string"
},
"provider": {
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"env": {
"type": "array",
"items": {
"type": "string"
}
},
"id": {
"type": "string"
},
"models": {
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"attachment": {
"type": "boolean"
},
"reasoning": {
"type": "boolean"
},
"temperature": {
"type": "boolean"
},
"cost": {
"type": "object",
"properties": {
"input": {
"type": "number"
},
"output": {
"type": "number"
},
"inputCached": {
"type": "number"
},
"outputCached": {
"type": "number"
}
},
"required": [
"input",
"output",
"inputCached",
"outputCached"
],
"additionalProperties": false
},
"limit": {
"type": "object",
"properties": {
"context": {
"type": "number"
},
"output": {
"type": "number"
}
},
"required": [
"context",
"output"
],
"additionalProperties": false
},
"id": {
"type": "string"
}
},
"additionalProperties": false
}
},
"options": {
"type": "object",
"additionalProperties": {}
}
},
"required": [
"models"
],
"additionalProperties": false
}
},
"mcp": {
"type": "object",
"additionalProperties": {
"anyOf": [
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "local"
},
"command": {
"type": "array",
"items": {
"type": "string"
}
},
"environment": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"required": [
"type",
"command"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "remote"
},
"url": {
"type": "string"
}
},
"required": [
"type",
"url"
],
"additionalProperties": false
}
]
}
}
},
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}
+4 -2
View File
@@ -5,7 +5,8 @@
"type": "module",
"private": true,
"scripts": {
"typecheck": "tsc --noEmit"
"typecheck": "tsc --noEmit",
"dev": "bun run ./src/index.ts"
},
"exports": {
"./*": [
@@ -18,7 +19,8 @@
"@types/bun": "latest",
"@types/turndown": "5.0.5",
"@types/yargs": "17.0.33",
"typescript": "catalog:"
"typescript": "catalog:",
"zod-to-json-schema": "3.24.5"
},
"dependencies": {
"@clack/prompts": "0.11.0",
+2 -1
View File
@@ -110,7 +110,8 @@ if (!snapshot) {
return (
!lower.includes("chore:") &&
!lower.includes("ci:") &&
!lower.includes("docs:")
!lower.includes("docs:") &&
!lower.includes("doc:")
)
})
.join("\n")
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env bun
import "zod-openapi/extend"
import { Config } from "../src/config/config"
import { zodToJsonSchema } from "zod-to-json-schema"
const result = zodToJsonSchema(Config.Info)
await Bun.write("config.schema.json", JSON.stringify(result, null, 2))
+19 -10
View File
@@ -44,15 +44,24 @@ export namespace BunProc {
}),
)
export async function install(pkg: string, version = "latest") {
const dir = path.join(Global.Path.cache, `node_modules`, pkg)
if (!(await Bun.file(path.join(dir, "package.json")).exists())) {
log.info("installing", { pkg })
await BunProc.run(["add", `${pkg}@${version}`], {
cwd: Global.Path.cache,
}).catch(() => {
throw new InstallFailedError({ pkg, version })
})
}
return dir
const mod = path.join(Global.Path.cache, "node_modules", pkg)
const pkgjson = Bun.file(path.join(Global.Path.cache, "package.json"))
const parsed = await pkgjson.json().catch(() => ({
dependencies: {},
}))
if (parsed.dependencies[pkg] === version) return mod
parsed.dependencies[pkg] = version
await Bun.write(pkgjson, JSON.stringify(parsed, null, 2))
await BunProc.run(["install"], {
cwd: Global.Path.cache,
}).catch((e) => {
new InstallFailedError(
{ pkg, version },
{
cause: e,
},
)
})
return mod
}
}
@@ -2,6 +2,7 @@ import { Server } from "../../server/server"
import fs from "fs/promises"
import path from "path"
import type { CommandModule } from "yargs"
import { Config } from "../../config/config"
export const GenerateCommand = {
command: "generate",
+1 -5
View File
@@ -50,6 +50,7 @@ export namespace Config {
export const Info = z
.object({
$schema: z.string().optional(),
provider: z
.record(
ModelsDev.Provider.partial().extend({
@@ -58,11 +59,6 @@ export namespace Config {
}),
)
.optional(),
tool: z
.object({
provider: z.record(z.string(), z.string().array()).optional(),
})
.optional(),
mcp: z.record(z.string(), Mcp).optional(),
})
.strict()
+11 -11
View File
@@ -1,4 +1,3 @@
import { App } from "../app/app"
import path from "path"
import { Global } from "../global"
import fs from "fs/promises"
@@ -6,6 +5,7 @@ import { z } from "zod"
import { NamedError } from "../util/error"
import { lazy } from "../util/lazy"
import { Log } from "../util/log"
import { $ } from "bun"
export namespace Fzf {
const log = Log.create({ service: "fzf" })
@@ -117,18 +117,18 @@ export namespace Fzf {
}
export async function search(cwd: string, query: string) {
const process = Bun.spawn({
cwd,
stdin: "inherit",
stdout: "pipe",
stderr: "pipe",
cmd: [await filepath(), "--filter", query],
})
await process.exited
const stdout = await Bun.readableStreamToText(process.stdout)
return stdout
const results = await $`${await filepath()} --filter ${query}`
.quiet()
.throws(false)
.cwd(cwd)
.text()
const split = results
.trim()
.split("\n")
.filter((line) => line.length > 0)
log.info("results", {
count: split.length,
})
return split
}
}
+8
View File
@@ -0,0 +1,8 @@
export namespace Flag {
export const OPENCODE_AUTO_SHARE = truthy("OPENCODE_AUTO_SHARE")
function truthy(key: string) {
const value = process.env[key]?.toLowerCase()
return value === "true" || value === "1"
}
}
+1 -1
View File
@@ -72,7 +72,7 @@ const cli = yargs(hideBin(process.argv))
cmd = [binary]
}
const proc = Bun.spawn({
cmd,
cmd: [...cmd, ...process.argv.slice(2)],
cwd,
stdout: "inherit",
stderr: "inherit",
+27
View File
@@ -1,4 +1,5 @@
import { Global } from "../global"
import { lazy } from "../util/lazy"
import { Log } from "../util/log"
import path from "path"
import { z } from "zod"
@@ -62,4 +63,30 @@ export namespace ModelsDev {
throw new Error(`Failed to fetch models.dev: ${result.statusText}`)
await Bun.write(file, result)
}
const aisdk = lazy(async () => {
log.info("fetching ai-sdk")
const response = await fetch(
"https://registry.npmjs.org/-/v1/search?text=scope:@ai-sdk",
)
if (!response.ok)
throw new Error(
`Failed to fetch ai-sdk information: ${response.statusText}`,
)
const result = await response.json()
log.info("found ai-sdk", result.objects.length)
return result.objects
.filter((obj: any) => obj.package.name.startsWith("@ai-sdk/"))
.reduce((acc: any, obj: any) => {
acc[obj.package.name] = obj
return acc
}, {})
})
export async function pkg(providerID: string): Promise<[string, string]> {
const packages = await aisdk()
const match = packages[`@ai-sdk/${providerID}`]
if (match) return [match.package.name, "latest"]
return [providerID, "latest"]
}
}
+4 -3
View File
@@ -186,9 +186,8 @@ export namespace Provider {
const s = await state()
const existing = s.sdk.get(providerID)
if (existing) return existing
const mod = await import(
await BunProc.install(`@ai-sdk/${providerID}`, "alpha")
)
const [pkg, version] = await ModelsDev.pkg(providerID)
const mod = await import(await BunProc.install(pkg, version))
const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
const loaded = fn(s.providers[providerID]?.options)
s.sdk.set(providerID, loaded)
@@ -289,11 +288,13 @@ export namespace Provider {
google: TOOLS,
}
export async function tools(providerID: string) {
/*
const cfg = await Config.get()
if (cfg.tool?.provider?.[providerID])
return cfg.tool.provider[providerID].map(
(id) => TOOLS.find((t) => t.id === id)!,
)
*/
return TOOL_MAPPING[providerID] ?? TOOLS
}
+6 -2
View File
@@ -56,12 +56,16 @@ export namespace Server {
},
)
})
.use((c, next) => {
.use(async (c, next) => {
log.info("request", {
method: c.req.method,
path: c.req.path,
})
return next()
const start = Date.now()
await next()
log.info("response", {
duration: Date.now() - start,
})
})
.get(
"/openapi",
+212 -138
View File
@@ -4,15 +4,15 @@ import { Identifier } from "../id/id"
import { Storage } from "../storage/storage"
import { Log } from "../util/log"
import {
convertToModelMessages,
generateText,
LoadAPIKeyError,
stepCountIs,
streamText,
tool,
type Tool as AITool,
type LanguageModelUsage,
type UIMessage,
type CoreMessage,
type UserContent,
type AssistantContent,
} from "ai"
import { z, ZodSchema } from "zod"
import { Decimal } from "decimal.js"
@@ -27,6 +27,8 @@ import { MCP } from "../mcp"
import { NamedError } from "../util/error"
import type { Tool } from "../tool/tool"
import { SystemPrompt } from "./system"
import { Flag } from "../flag/flag"
import type { ModelsDev } from "../provider/models"
export namespace Session {
const log = Log.create({ service: "session" })
@@ -92,6 +94,12 @@ export namespace Session {
log.info("created", result)
state().sessions.set(result.id, result)
await Storage.writeJSON("session/info/" + result.id, result)
if (!result.parentID && Flag.OPENCODE_AUTO_SHARE)
share(result.id).then((share) => {
update(result.id, (draft) => {
draft.share = share
})
})
Bus.publish(Event.Updated, {
info: result,
})
@@ -220,34 +228,28 @@ export namespace Session {
const session = await get(input.sessionID)
if (msgs.length === 0 && !session.parentID) {
generateText({
maxOutputTokens: 20,
messages: convertToModelMessages([
maxTokens: input.providerID === "google" ? 1024 : 20,
messages: [
...SystemPrompt.title(input.providerID).map(
(x): UIMessage => ({
id: Identifier.ascending("message"),
(x): CoreMessage => ({
role: "system",
parts: [
{
type: "text",
text: x,
},
],
content: x,
}),
),
{
role: "user",
parts: input.parts,
content: toUserContent(input.parts),
},
]),
temperature: 0,
],
model: model.language,
})
.then((result) => {
return Session.update(input.sessionID, (draft) => {
draft.title = result.text
})
if (result.text)
return Session.update(input.sessionID, (draft) => {
draft.title = result.text
})
})
.catch(() => {})
.catch((e) => {})
}
const msg: Message.Info = {
role: "user",
@@ -393,90 +395,9 @@ export namespace Session {
}
text = undefined
},
async onChunk(input) {
const value = input.chunk
l.info("part", {
type: value.type,
})
switch (value.type) {
case "text":
if (!text) {
text = value
next.parts.push(value)
break
} else text.text += value.text
break
case "tool-call": {
const [match] = next.parts.flatMap((p) =>
p.type === "tool-invocation" &&
p.toolInvocation.toolCallId === value.toolCallId
? [p]
: [],
)
if (!match) break
match.toolInvocation.args = value.args
match.toolInvocation.state = "call"
Bus.publish(Message.Event.PartUpdated, {
part: match,
messageID: next.id,
sessionID: next.metadata.sessionID,
})
break
}
case "tool-call-streaming-start":
next.parts.push({
type: "tool-invocation",
toolInvocation: {
state: "partial-call",
toolName: value.toolName,
toolCallId: value.toolCallId,
args: {},
},
})
Bus.publish(Message.Event.PartUpdated, {
part: next.parts[next.parts.length - 1],
messageID: next.id,
sessionID: next.metadata.sessionID,
})
break
case "tool-call-delta":
break
case "tool-result":
const match = next.parts.find(
(p) =>
p.type === "tool-invocation" &&
p.toolInvocation.toolCallId === value.toolCallId,
)
if (match && match.type === "tool-invocation") {
match.toolInvocation = {
args: value.args,
toolCallId: value.toolCallId,
toolName: value.toolName,
state: "result",
result: value.result as string,
}
Bus.publish(Message.Event.PartUpdated, {
part: match,
messageID: next.id,
sessionID: next.metadata.sessionID,
})
}
break
default:
l.info("unhandled", {
type: value.type,
})
}
await updateMessage(next)
},
async onFinish(input) {
const assistant = next.metadata!.assistant!
const usage = getUsage(input.totalUsage, model.info)
const usage = getUsage(input.usage, model.info)
assistant.cost = usage.cost
await updateMessage(next)
},
@@ -508,31 +429,44 @@ export namespace Session {
error: next.metadata.error,
})
},
async prepareStep(step) {
next.parts.push({
type: "step-start",
})
await updateMessage(next)
return step
},
// async prepareStep(step) {
// next.parts.push({
// type: "step-start",
// })
// await updateMessage(next)
// return step
// },
toolCallStreaming: true,
abortSignal: abort.signal,
stopWhen: stepCountIs(1000),
messages: convertToModelMessages([
maxSteps: 1000,
messages: [
...system.map(
(x): UIMessage => ({
id: Identifier.ascending("message"),
(x): CoreMessage => ({
role: "system",
parts: [
{
type: "text",
text: x,
},
],
content: x,
}),
),
...msgs,
]),
...msgs.flatMap((msg): CoreMessage[] => {
switch (msg.role) {
case "user":
return [
{
role: "user",
content: toUserContent(msg.parts),
},
]
case "assistant":
return [
{
role: "assistant",
content: toAssistantContent(msg.parts),
},
]
default:
return []
}
}),
],
temperature: model.info.id === "codex-mini-latest" ? undefined : 0,
tools: {
...(await MCP.tools()),
@@ -540,6 +474,101 @@ export namespace Session {
},
model: model.language,
})
for await (const value of result.fullStream) {
l.info("part", {
type: value.type,
})
switch (value.type) {
case "step-start":
next.parts.push({
type: "step-start",
})
break
case "text-delta":
if (!text) {
text = {
type: "text",
text: value.textDelta,
}
next.parts.push(text)
break
} else text.text += value.textDelta
break
case "tool-call": {
const [match] = next.parts.flatMap((p) =>
p.type === "tool-invocation" &&
p.toolInvocation.toolCallId === value.toolCallId
? [p]
: [],
)
if (!match) break
match.toolInvocation.args = value.args
match.toolInvocation.state = "call"
Bus.publish(Message.Event.PartUpdated, {
part: match,
messageID: next.id,
sessionID: next.metadata.sessionID,
})
break
}
case "tool-call-streaming-start":
next.parts.push({
type: "tool-invocation",
toolInvocation: {
state: "partial-call",
toolName: value.toolName,
toolCallId: value.toolCallId,
args: {},
},
})
Bus.publish(Message.Event.PartUpdated, {
part: next.parts[next.parts.length - 1],
messageID: next.id,
sessionID: next.metadata.sessionID,
})
break
case "tool-call-delta":
break
// for some reason ai sdk claims to not send this part but it does
// @ts-expect-error
case "tool-result":
const match = next.parts.find(
(p) =>
p.type === "tool-invocation" &&
// @ts-expect-error
p.toolInvocation.toolCallId === value.toolCallId,
)
if (match && match.type === "tool-invocation") {
match.toolInvocation = {
// @ts-expect-error
args: value.args,
// @ts-expect-error
toolCallId: value.toolCallId,
// @ts-expect-error
toolName: value.toolName,
state: "result",
// @ts-expect-error
result: value.result as string,
}
Bus.publish(Message.Event.PartUpdated, {
part: match,
messageID: next.id,
sessionID: next.metadata.sessionID,
})
}
break
default:
l.info("unhandled", {
type: value.type,
})
}
await updateMessage(next)
}
await result.consumeStream({
onError: (err) => {
log.error("stream error", {
@@ -611,30 +640,23 @@ export namespace Session {
const result = await generateText({
abortSignal: abort.signal,
model: model.language,
messages: convertToModelMessages([
messages: [
...system.map(
(x): UIMessage => ({
id: Identifier.ascending("message"),
(x): CoreMessage => ({
role: "system",
parts: [
{
type: "text",
text: x,
},
],
content: x,
}),
),
...filtered,
{
role: "user",
parts: [
content: toUserContent([
{
type: "text",
text: "Provide a detailed but concise summary of our conversation above. Focus on information that would be helpful for continuing the conversation, including what we did, what we're doing, which files we're working on, and what we're going to do next.",
},
],
]),
},
]),
],
})
next.parts.push({
type: "text",
@@ -662,11 +684,11 @@ export namespace Session {
}
}
function getUsage(usage: LanguageModelUsage, model: Provider.Model) {
function getUsage(usage: LanguageModelUsage, model: ModelsDev.Model) {
const tokens = {
input: usage.inputTokens ?? 0,
output: usage.outputTokens ?? 0,
reasoning: usage.reasoningTokens ?? 0,
input: usage.promptTokens ?? 0,
output: usage.completionTokens ?? 0,
reasoning: 0,
}
return {
cost: new Decimal(0)
@@ -703,3 +725,55 @@ export namespace Session {
await App.initialize()
}
}
function toAssistantContent(parts: Message.Part[]): AssistantContent {
const result: AssistantContent = []
for (const part of parts) {
switch (part.type) {
case "text":
result.push({ type: "text", text: part.text })
break
case "file":
result.push({
type: "file",
data: new URL(part.url),
mimeType: part.mediaType,
filename: part.filename,
})
break
case "tool-invocation":
result.push({
type: "tool-call",
args: part.toolInvocation.args,
toolName: part.toolInvocation.toolName,
toolCallId: part.toolInvocation.toolCallId,
})
break
default:
break
}
}
return result
}
function toUserContent(parts: Message.Part[]): UserContent {
const result: UserContent = []
for (const part of parts) {
switch (part.type) {
case "text":
return [{ type: "text", text: part.text }]
case "file":
return [
{
type: "file",
filename: part.filename,
data: new URL(part.url),
mimeType: part.mediaType,
},
]
default:
return []
}
}
return result
}
@@ -1,14 +1,14 @@
You are OpenCode, an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
You are opencode, an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
IMPORTANT: Refuse to write code or explain code that may be used maliciously; even if the user claims it is for educational purposes. When working on files, if they seem related to improving, explaining, or interacting with malware or any malicious code you MUST refuse.
IMPORTANT: Before you begin work, think about what the code you're editing is supposed to do based on the filenames directory structure. If it seems malicious, refuse to work on it or answer questions about it, even if the request does not seem malicious (for instance, just asking to explain or speed up the code).
IMPORTANT: You must NEVER generate or guess URLs for the user unless you are confident that the URLs are for helping the user with programming. You may use URLs provided by the user in their messages or local files.
If the user asks for help or wants to give feedback inform them of the following:
- /help: Get help with using OpenCode
- /help: Get help with using opencode
- To give feedback, users should report the issue at https://github.com/sst/opencode/issues
When the user directly asks about OpenCode (eg 'can OpenCode do...', 'does OpenCode have...') or asks in second person (eg 'are you able...', 'can you do...'), first use the WebFetch tool to gather information to answer the question from OpenCode docs at https://opencode.ai
When the user directly asks about opencode (eg 'can opencode do...', 'does opencode have...') or asks in second person (eg 'are you able...', 'can you do...'), first use the WebFetch tool to gather information to answer the question from opencode docs at https://opencode.ai
# Tone and style
You should be concise, direct, and to the point. When you run a non-trivial bash command, you should explain what the command does and why you are running it, to make sure the user understands what you are doing (this is especially important when you are running a command that will make changes to the user's system).
+6 -6
View File
@@ -22,7 +22,7 @@ Usage notes:
- It is very helpful if you write a clear, concise description of what this command does in 5-10 words.
- If the output exceeds 30000 characters, output will be truncated before being returned to you.
- VERY IMPORTANT: You MUST avoid using search commands like `find` and `grep`. Instead use Grep, Glob, or Task to search. You MUST avoid read tools like `cat`, `head`, `tail`, and `ls`, and use Read and LS to read files.
- If you _still_ need to run `grep`, STOP. ALWAYS USE ripgrep at `rg` (or /usr/bin/rg) first, which all OpenCode users have pre-installed.
- If you _still_ need to run `grep`, STOP. ALWAYS USE ripgrep at `rg` (or /usr/bin/rg) first, which all opencode users have pre-installed.
- When issuing multiple commands, use the ';' or '&&' operator to separate them. DO NOT use newlines (newlines are ok in quoted strings).
- Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of `cd`. You may use `cd` if the User explicitly requests it.
<good-example>
@@ -60,9 +60,9 @@ When the user asks you to create a new git commit, follow these steps carefully:
3. You have the capability to call multiple tools in a single response. When multiple independent pieces of information are requested, batch your tool calls together for optimal performance. ALWAYS run the following commands in parallel:
- Add relevant untracked files to the staging area.
- Create the commit with a message ending with:
🤖 Generated with [OpenCode](https://opencode.ai)
🤖 Generated with [opencode](https://opencode.ai)
Co-Authored-By: OpenCode <noreply@opencode.ai>
Co-Authored-By: opencode <noreply@opencode.ai>
- Run git status to make sure the commit succeeded.
4. If the commit fails due to pre-commit hook changes, retry the commit ONCE to include these automated changes. If it fails again, it usually means a pre-commit hook is preventing the commit. If the commit succeeds but you notice that files were modified by the pre-commit hook, you MUST amend your commit to include them.
@@ -81,9 +81,9 @@ Important notes:
git commit -m "$(cat <<'EOF'
Commit message here.
🤖 Generated with [OpenCode](https://opencode.ai)
🤖 Generated with [opencode](https://opencode.ai)
Co-Authored-By: OpenCode <noreply@opencode.ai>
Co-Authored-By: opencode <noreply@opencode.ai>
EOF
)"
</example>
@@ -128,7 +128,7 @@ gh pr create --title "the pr title" --body "$(cat <<'EOF'
## Test plan
[Checklist of TODOs for testing the pull request...]
🤖 Generated with [OpenCode](https://opencode.ai)
🤖 Generated with [opencode](https://opencode.ai)
EOF
)"
</example>
+4 -1
View File
@@ -29,7 +29,10 @@ export abstract class NamedError extends Error {
) {
super(name, options)
this.name = name
log.error(name, this.data)
log.error(name, {
...this.data,
cause: options?.cause?.toString(),
})
}
static isInstance(input: any): input is InstanceType<typeof result> {
+8 -3
View File
@@ -2,7 +2,7 @@ import path from "path"
import fs from "fs/promises"
import { Global } from "../global"
export namespace Log {
export const Default = create()
export const Default = create({ service: "default" })
export interface Options {
print: boolean
@@ -45,6 +45,7 @@ export namespace Log {
)
}
let last = Date.now()
export function create(tags?: Record<string, any>) {
tags = tags || {}
@@ -56,9 +57,13 @@ export namespace Log {
.filter(([_, value]) => value !== undefined && value !== null)
.map(([key, value]) => `${key}=${value}`)
.join(" ")
const next = new Date()
const diff = next.getTime() - last
last = next.getTime()
return (
[new Date().toISOString(), prefix, message].filter(Boolean).join(" ") +
"\n"
[next.toISOString().split(".")[0], "+" + diff + "ms", prefix, message]
.filter(Boolean)
.join(" ") + "\n"
)
}
const result = {
@@ -93,7 +93,7 @@ const (
)
func (m *editorComponent) Init() tea.Cmd {
return tea.Batch(textarea.Blink, m.spinner.Tick)
return tea.Batch(textarea.Blink, m.spinner.Tick, tea.EnableReportFocus)
}
func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
@@ -316,7 +316,7 @@ func (m *messagesComponent) home() string {
{"/new", "start a new session"},
{"/model", "switch model"},
{"/share", "share the current session"},
{"/exit", "exit the app"},
{"/quit", "exit the app"},
}
commandLines := []string{}
+2 -9
View File
@@ -92,13 +92,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
a.modal = nil
return a, nil
case "ctrl+c":
if _, ok := a.modal.(dialog.QuitDialog); ok {
return a, tea.Quit
} else {
quitDialog := dialog.NewQuitDialog()
a.modal = quitDialog
return a, nil
}
return a, tea.Quit
}
// don't send commands to the modal
@@ -135,8 +129,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case commands.ExecuteCommandMsg:
switch msg.Name {
case "quit":
quitDialog := dialog.NewQuitDialog()
a.modal = quitDialog
return a, tea.Quit
case "new":
a.app.Session = &client.SessionInfo{}
a.app.Messages = []client.MessageInfo{}