Compare commits

...

92 Commits

Author SHA1 Message Date
opencode e9f52934e9 release: v0.5.18 2025-08-23 16:27:02 +00:00
Dax Raad 732b67f8ce ci: stuff 2025-08-23 12:21:58 -04:00
Dax Raad d47bb96784 ci: ignore 2025-08-23 12:10:08 -04:00
Johnny 6456350564 docs: fix nodejs installation commands (#2193) 2025-08-23 08:23:24 -05:00
GitHub Action c5c0a2ca6e ignore: update download stats 2025-08-23 2025-08-23 12:03:48 +00:00
Vasiliy Kulikov 3706b2bca7 feat(lsp): option to disable lsps installing automatically (#1997)
Co-authored-by: rekram1-node <aidenpcline@gmail.com>
2025-08-22 22:39:19 -05:00
Aiden Cline 1f57b9a70f fix: count reasoning tokens (#2187) 2025-08-22 18:21:39 -05:00
Aiden Cline 004f53f741 ignore: update json schema for better lsp dx (#2186) 2025-08-22 17:59:18 -05:00
Jay V cf29ec0a59 docs: edit 2025-08-22 18:36:17 -04:00
Jay V b5e08acdf7 docs: update 2025-08-22 18:34:35 -04:00
Dax Raad 7ddeeeb4f8 ignore: typecheck 2025-08-22 18:31:51 -04:00
Dax Raad 0f1697b2ab add sse streaming to sdk 2025-08-22 18:30:25 -04:00
Lubos 6e626afdcb chore(openapi): set correct content type for server-sent events (#2045) 2025-08-22 17:51:24 -04:00
Dax Raad 0fe94c1616 docs: add file names to code block titles in commands.mdx 2025-08-22 17:23:59 -04:00
Dax Raad a42b004c72 docs: add commands page to sidebar 2025-08-22 17:23:59 -04:00
opencode 35f57768fd release: v0.5.15 2025-08-22 21:16:23 +00:00
Aiden Cline 9a90ce84fb fix: format error log (#2184) 2025-08-22 16:09:15 -05:00
Dax 133fe41cd5 slash commands (#2157)
Co-authored-by: adamdotdevin <2363879+adamdottv@users.noreply.github.com>
2025-08-22 17:04:28 -04:00
Jay V 74c1085103 docs: edit 2025-08-22 15:14:02 -04:00
Jay V 497fc170fd docs: edit 2025-08-22 13:54:56 -04:00
Aiden Cline 3edab60560 docs: remove fake model (#2175) 2025-08-22 11:55:11 -05:00
Dax 3f2ac2b9b0 Update duplicate-issues.yml 2025-08-22 08:52:39 -04:00
GitHub Action 1577b44087 ignore: update download stats 2025-08-22 2025-08-22 12:04:13 +00:00
Thai Nguyen Hung 39f52f48f2 fix: correct typo in LSP documentation (#2164) 2025-08-22 06:43:24 -05:00
Aiden Cline 4fadbcfb90 fix: error logging (#2165) 2025-08-21 23:27:49 -05:00
Dax Raad 08c5c401ba deal with non existing cache folder 2025-08-21 22:58:39 -04:00
Aiden Cline ba2e86c7ef tweak: adjust plan agent to ask when running bash, give it edit tooli… (#2150) 2025-08-21 18:25:31 -04:00
Dax Raad 6d056789c7 ignore: docs agent 2025-08-21 17:15:43 -04:00
Dax Raad 5d508cc9c2 docs: update SDK documentation 2025-08-21 17:15:21 -04:00
Dax Raad d9233872b9 add createOpencodeServer to js sdk and wait for readiness. always use random port for opencode serve. add /client and /server imports for js sdk 2025-08-21 17:13:24 -04:00
Aiden Cline aa4dba1541 fix: if lsp fails to spawn it shouldn't inject errors into edit diagnostics (#2145) 2025-08-21 12:06:32 -05:00
Dax Raad 947a3e8aff fix sdk config type 2025-08-21 13:00:16 -04:00
Dax Raad 9a3186317b allow importing sdk from @opencode-ai/sdk/server and @opencode-ai/sdk/client 2025-08-21 12:58:37 -04:00
zWing b1e584ca1d chore: add export types in js-sdk (#1923)
Co-authored-by: zwingzheng <zwingzheng@tencent.com>
2025-08-21 11:06:27 -05:00
zWing bca523eb63 fix(js-sdk): fix types in session.chat (#1925)
Co-authored-by: zwingzheng <zwingzheng@tencent.com>
Co-authored-by: rekram1-node <aidenpcline@gmail.com>
2025-08-21 10:44:20 -05:00
Denys Pavlov 2ff4cd2c2b fix: preserve cache dir on cleanup (#2126) 2025-08-21 15:27:25 +00:00
Dax Raad d686269377 await config hooks 2025-08-21 15:27:25 +00:00
opencode 491abd6b5b release: v0.5.13 2025-08-21 15:27:25 +00:00
Dax Raad 4518f96e3d add plugin hook for config 2025-08-21 11:22:24 -04:00
GitHub Action a9dcbedf99 ignore: update download stats 2025-08-21 2025-08-21 12:04:21 +00:00
Aiden Cline 9231043eb4 tweak: adjust plan -> build transition prompt (#2111) 2025-08-21 06:52:38 -05:00
Frank 04dcd87170 fix do migration 2025-08-21 18:02:32 +08:00
Frank c31fd9ed79 fix do migration 2025-08-21 17:52:55 +08:00
Jay V 2989d92794 docs: update 2025-08-20 17:58:56 -04:00
Jay V 256d074411 docs: gitlab 2025-08-20 17:51:16 -04:00
Jay V 8b01676ec0 docs: edit 2025-08-20 17:39:37 -04:00
Lee Tickett 34c6c8494a docs: Add GitLab CLI agent integration doc (#2103) 2025-08-20 17:37:43 -04:00
Dax Raad 522bed6b7d ignore: cloud stuff 2025-08-20 17:01:18 -04:00
Vincent Bernat dda672284c fix: ignore case when checking Qwen in model ID for todos (#2122) 2025-08-20 14:44:27 -05:00
Jay V 6018364164 docs: edit 2025-08-20 18:22:48 +00:00
opencode bc0d438cee release: v0.5.12 2025-08-20 18:22:48 +00:00
Jay V abef91c223 docs: edit server 2025-08-20 14:13:02 -04:00
Dax Raad 1bbf6d38e5 ci: turn back on aur 2025-08-20 12:46:17 -04:00
opencode c9c9db1e8d release: v0.5.11 2025-08-20 16:36:05 +00:00
Dax Raad b11fe9fbc6 ignore: remove import 2025-08-20 12:29:24 -04:00
Dax Raad 60f3d413de remove auto browser open for now 2025-08-20 12:28:00 -04:00
opencode 1df2d78b85 release: v0.5.10 2025-08-20 16:12:00 +00:00
opencode 2286a872c1 release: v0.5.9 2025-08-20 15:51:24 +00:00
Dax Raad 8a83301e0d copilot auth update version 2025-08-20 11:46:14 -04:00
GitHub Action 9bc40f00e3 ignore: update download stats 2025-08-20 2025-08-20 12:04:23 +00:00
opencode c3c440948a release: v0.5.8 2025-08-20 05:08:31 +00:00
Dax Raad aa10f8a7f6 sonic model 2025-08-20 01:02:41 -04:00
Aiden Cline a2db58f125 fix: don't let --continue access subagent session (#2091) 2025-08-19 22:40:07 -05:00
Aiden Cline 574be9febf fix: keybind panic (#2092) 2025-08-19 22:39:59 -05:00
Aiden Cline 5b05ede748 fix: agent casing issue (#2081) 2025-08-19 18:08:56 -05:00
Aiden Cline 4032426185 docs: remove non existent keybind (#2080) 2025-08-19 17:39:02 -05:00
Jay V 8d8045ff95 docs: add sdk doc 2025-08-19 18:11:36 -04:00
Jay V b3c8bec019 docs: edit server 2025-08-19 17:21:45 -04:00
Aiden Cline 25f43adaa0 tweak: notify agent it is in build mode when switching from plan mode (#2065) 2025-08-19 15:32:31 -05:00
Timo Clasen 4913ee6afd fix(TUI): make it less shimmer (#2076) 2025-08-19 15:30:54 -05:00
Zack Jackson c59ded82b3 docs: document server API endpoints (#2019)
Co-authored-by: Jay <air@live.ca>
2025-08-19 16:13:02 -04:00
Aiden Cline 40bdbf92a3 fix: tui panic from logger (#2075) 2025-08-19 14:47:44 -05:00
Aiden Cline ad76d7e57d fix: add type checking for MCP tool path parameters (#2073)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2025-08-19 13:38:33 -05:00
GitHub Action 863ae6fa7d ignore: update download stats 2025-08-19 2025-08-19 12:04:15 +00:00
Aiden Cline 8f230ad4b4 fix: interface conversion panic (#2060) 2025-08-19 05:25:46 -05:00
Aiden Cline c0f90eb564 tweak: better agent create error handling (#2058) 2025-08-19 00:14:50 -05:00
Dax 50fb337270 Update duplicate-issues.yml 2025-08-18 20:00:59 -04:00
Aiden Cline e08ec077b0 fix: ensure name isn't added as field in options: {...} (#2053) 2025-08-18 18:15:20 -05:00
Aiden Cline 796245d146 blacklist gpt-5-chat-latest (#2048) 2025-08-18 17:50:38 -04:00
opencode 303a1044a8 release: v0.5.7 2025-08-18 21:43:17 +00:00
Dax f19586cebd fix anthropic console auth (#2049) 2025-08-18 17:12:21 -04:00
Jay V 5d12cadba7 docs:edit 2025-08-18 13:52:53 -04:00
Jay V 745988f9e3 docs:edit 2025-08-18 13:51:08 -04:00
Jay V 61580e6dce docs: edits 2025-08-18 13:31:01 -04:00
Jay V 2dea8f0f6b docs: add tui doc 2025-08-18 13:31:01 -04:00
opencode 446ce488c0 release: v0.5.6 2025-08-18 15:56:22 +00:00
John Connor 21b000aed0 Remove redundant line from agents.mdx (#2031) 2025-08-18 08:34:57 -05:00
GitHub Action 0cdd8be70a ignore: update download stats 2025-08-18 2025-08-18 12:04:33 +00:00
adamdotdevin 2f4db2777c fix(tui): title bg color missing on system theme 2025-08-18 06:00:38 -05:00
Ytzhak 667ff90dd6 feat: add shimmer text rendering (#2027) 2025-08-18 05:55:01 -05:00
spoons-and-mirrors cd3d91209a tweak(timeline): add a dot to the session timeline modal for better visual cue of session's revert point (#1978) 2025-08-18 05:50:43 -05:00
Frank 75ed131abf sync 2025-08-18 07:58:46 +00:00
103 changed files with 2865 additions and 513 deletions
+2 -4
View File
@@ -32,14 +32,12 @@ jobs:
"webfetch": "deny"
}
run: |
opencode run -m anthropic/claude-sonnet-4-20250514 "A new issue has been created: '${{ github.event.issue.title }}'
opencode run -m anthropic/claude-sonnet-4-20250514 "A new issue has been created:'
Issue number:
${{ github.event.issue.number }}
Issue body:
${{ github.event.issue.body }}
Please search through existing issues (excluding #${{ github.event.issue.number }}) in this repository to find any potential duplicates of this new issue.
Lookup this issue and search through existing issues (excluding #${{ github.event.issue.number }}) in this repository to find any potential duplicates of this new issue.
Consider:
1. Similar titles or descriptions
2. Same error messages or symptoms
+22 -5
View File
@@ -1,6 +1,4 @@
---
model: openai/gpt-5
reasoningEffort: medium
description: ALWAYS use this when writing docs
---
@@ -8,7 +6,26 @@ You are an expert technical documentation writer
You are not verbose
Every chunk of text should be followed by an example or something besides text
to look at.
The title of the page should be a word or a 2-3 word phrase
Chunks of text should not be more than 2 sentences long.
The description should be one short line, should not start with "The", should
avoid repeating the title of the page, should be 5-10 words long
Chunks of text should not be more than 2 sentences long
Each section is spearated by a divider of 3 dashes
The section titles are short with only the first letter of the word capitalized
The section titles are in the imperative mood
The section titles should not repeat the term used in the page title, for
example, if the page title is "Models", avoid using a section title like "Add
new models". This might be unavoidable in some cases, but try to avoid it.
Check out the /packages/web/src/content/docs/docs/index.mdx as an example.
For JS or TS code snippets remove trailing semicolons and any trailing commas
that might not be needed.
If you are making a commit prefix the commit message with `docs:`
+9
View File
@@ -0,0 +1,9 @@
commit and push
make sure it includes a prefix like
docs:
tui:
core:
ci:
ignore:
wip:
+8
View File
@@ -0,0 +1,8 @@
---
description: hello world
---
hey there $ARGUMENTS
!`ls`
check out @README.md
+6
View File
@@ -51,3 +51,9 @@
| 2025-08-15 | 193,608 (+4,545) | 181,792 (+2,051) | 375,400 (+6,596) |
| 2025-08-16 | 198,118 (+4,510) | 184,558 (+2,766) | 382,676 (+7,276) |
| 2025-08-17 | 201,299 (+3,181) | 186,269 (+1,711) | 387,568 (+4,892) |
| 2025-08-18 | 204,559 (+3,260) | 187,399 (+1,130) | 391,958 (+4,390) |
| 2025-08-19 | 209,814 (+5,255) | 189,668 (+2,269) | 399,482 (+7,524) |
| 2025-08-20 | 214,497 (+4,683) | 191,481 (+1,813) | 405,978 (+6,496) |
| 2025-08-21 | 220,465 (+5,968) | 194,784 (+3,303) | 415,249 (+9,271) |
| 2025-08-22 | 225,899 (+5,434) | 197,204 (+2,420) | 423,103 (+7,854) |
| 2025-08-23 | 229,005 (+3,106) | 199,238 (+2,034) | 428,243 (+5,140) |
+24 -90
View File
@@ -15,6 +15,8 @@
"name": "@opencode/cloud-app",
"dependencies": {
"@ibm/plex": "6.4.1",
"@openauthjs/openauth": "0.0.0-20250322224806",
"@opencode/cloud-core": "workspace:*",
"@solidjs/meta": "^0.29.4",
"@solidjs/router": "^0.15.0",
"@solidjs/start": "^1.1.0",
@@ -24,7 +26,7 @@
},
"cloud/core": {
"name": "@opencode/cloud-core",
"version": "0.5.4",
"version": "0.5.15",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"drizzle-orm": "0.41.0",
@@ -38,7 +40,7 @@
},
"cloud/function": {
"name": "@opencode/cloud-function",
"version": "0.5.4",
"version": "0.5.15",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -58,7 +60,7 @@
},
"cloud/web": {
"name": "@opencode/cloud-web",
"version": "0.5.4",
"version": "0.5.15",
"dependencies": {
"@kobalte/core": "0.13.9",
"@openauthjs/solid": "0.0.0-20250322224806",
@@ -77,7 +79,7 @@
},
"packages/function": {
"name": "@opencode/function",
"version": "0.5.4",
"version": "0.5.15",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "22.0.0",
@@ -92,18 +94,14 @@
},
"packages/opencode": {
"name": "opencode",
"version": "0.5.4",
"version": "0.5.15",
"bin": {
"opencode": "./bin/opencode",
},
"dependencies": {
"@actions/core": "1.11.1",
"@actions/github": "6.0.1",
"@clack/prompts": "1.0.0-alpha.1",
"@hono/zod-validator": "catalog:",
"@modelcontextprotocol/sdk": "1.15.1",
"@octokit/graphql": "9.0.1",
"@octokit/rest": "22.0.0",
"@openauthjs/openauth": "0.4.3",
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/sdk": "workspace:*",
@@ -146,19 +144,22 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "0.5.4",
"version": "0.5.15",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
},
"devDependencies": {
"@hey-api/openapi-ts": "0.80.1",
"@hey-api/openapi-ts": "0.81.0",
"@tsconfig/node22": "catalog:",
"typescript": "catalog:",
},
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "0.5.4",
"version": "0.5.15",
"dependencies": {
"@hey-api/openapi-ts": "0.80.1",
},
"devDependencies": {
"@hey-api/openapi-ts": "0.80.1",
"@tsconfig/node22": "catalog:",
@@ -167,7 +168,7 @@
},
"packages/web": {
"name": "@opencode/web",
"version": "0.5.4",
"version": "0.5.15",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@@ -189,7 +190,7 @@
"sharp": "0.32.5",
"shiki": "3.4.2",
"solid-js": "1.9.7",
"toolbeam-docs-theme": "0.4.3",
"toolbeam-docs-theme": "0.4.6",
},
"devDependencies": {
"@types/node": "catalog:",
@@ -206,6 +207,9 @@
"web-tree-sitter",
"tree-sitter-bash",
],
"overrides": {
"zod": "3.25.76",
},
"catalog": {
"@hono/zod-validator": "0.4.2",
"@tsconfig/node22": "22.0.2",
@@ -217,16 +221,6 @@
"zod": "3.25.76",
},
"packages": {
"@actions/core": ["@actions/core@1.11.1", "", { "dependencies": { "@actions/exec": "^1.1.1", "@actions/http-client": "^2.0.1" } }, "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A=="],
"@actions/exec": ["@actions/exec@1.1.1", "", { "dependencies": { "@actions/io": "^1.0.1" } }, "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w=="],
"@actions/github": ["@actions/github@6.0.1", "", { "dependencies": { "@actions/http-client": "^2.2.0", "@octokit/core": "^5.0.1", "@octokit/plugin-paginate-rest": "^9.2.2", "@octokit/plugin-rest-endpoint-methods": "^10.4.0", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "undici": "^5.28.5" } }, "sha512-xbZVcaqD4XnQAe35qSQqskb3SqIAfRyLBrHMd/8TuL7hJSz2QtbDwnNM8zWx4zO5l2fnGtseNE3MbEvD7BxVMw=="],
"@actions/http-client": ["@actions/http-client@2.2.3", "", { "dependencies": { "tunnel": "^0.0.6", "undici": "^5.25.4" } }, "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA=="],
"@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="],
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@2.2.10", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-icLGO7Q0NinnHIPgT+y1QjHVwH4HwV+brWbvM+FfCG2Afpa89PyKa3Ret91kGjZpBgM/xnj1B7K5eM+rRlsXQA=="],
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-uyyaO4KhxoIKZztREqLPh+6/K3ZJx/rp72JKoUEL9/kC+vfQTThUfPnY/bUryUpcnawx8IY/tSoYNOi/8PCv7w=="],
@@ -485,7 +479,7 @@
"@expressive-code/plugin-text-markers": ["@expressive-code/plugin-text-markers@0.41.3", "", { "dependencies": { "@expressive-code/core": "^0.41.3" } }, "sha512-SN8tkIzDpA0HLAscEYD2IVrfLiid6qEdE9QLlGVSxO1KEw7qYvjpbNBQjUjMr5/jvTJ7ys6zysU2vLPHE0sb2g=="],
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
"@fastify/busboy": ["@fastify/busboy@3.1.1", "", {}, "sha512-5DGmA8FTdB2XbDeEwc/5ZXBl6UbBAyBOOLlPuBnZ/N1SwdH9Ii+cOX3tBROlDgcTXxjOYnLMVoKk9+FXAw0CJw=="],
"@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="],
@@ -501,7 +495,7 @@
"@hey-api/json-schema-ref-parser": ["@hey-api/json-schema-ref-parser@1.0.6", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.0", "lodash": "^4.17.21" } }, "sha512-yktiFZoWPtEW8QKS65eqKwA5MTKp88CyiL8q72WynrBs/73SAaxlSWlA2zW/DZlywZ5hX1OYzrCC0wFdvO9c2w=="],
"@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.80.1", "", { "dependencies": { "@hey-api/json-schema-ref-parser": "1.0.6", "ansi-colors": "4.1.3", "c12": "2.0.1", "color-support": "1.1.3", "commander": "13.0.0", "handlebars": "4.7.8", "open": "10.1.2", "semver": "7.7.2" }, "peerDependencies": { "typescript": "^5.5.3" }, "bin": { "openapi-ts": "bin/index.cjs" } }, "sha512-AC478kg36vmmrseLZNFonZ/cmXXmDzW5yWz4PVg1S8ebJsRtVRJ/QU+mtnXfzf9avN2P0pz/AO4WAe4jyFY2gA=="],
"@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.81.0", "", { "dependencies": { "@hey-api/json-schema-ref-parser": "1.0.6", "ansi-colors": "4.1.3", "c12": "2.0.1", "color-support": "1.1.3", "commander": "13.0.0", "handlebars": "4.7.8", "js-yaml": "4.1.0", "open": "10.1.2", "semver": "7.7.2" }, "peerDependencies": { "typescript": "^5.5.3" }, "bin": { "openapi-ts": "bin/index.cjs" } }, "sha512-PoJukNBkUfHOoMDpN33bBETX49TUhy7Hu8Sa0jslOvFndvZ5VjQr4Nl/Dzjb9LG1Lp5HjybyTJMA6a1zYk/q6A=="],
"@hono/zod-validator": ["@hono/zod-validator@0.4.2", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.19.1" } }, "sha512-1rrlBg+EpDPhzOV4hT9pxr5+xDVmKuz6YJl+la7VCwK6ass5ldyKm5fD+umJdV2zhHD6jROoCCv8NbTwyfhT0g=="],
@@ -1469,8 +1463,6 @@
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
"deprecation": ["deprecation@2.3.1", "", {}, "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="],
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
"destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="],
@@ -2819,7 +2811,7 @@
"toml": ["toml@3.0.0", "", {}, "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="],
"toolbeam-docs-theme": ["toolbeam-docs-theme@0.4.3", "", { "peerDependencies": { "@astrojs/starlight": "^0.34.3", "astro": "^5.7.13" } }, "sha512-3um/NsSq4xFeKbKrNGPHIzfTixwnEVvroqA8Q+lecnYHHJ5TtiYTggHDqewOW+I67t0J1IVBwVKUPjxiQfIcog=="],
"toolbeam-docs-theme": ["toolbeam-docs-theme@0.4.6", "", { "peerDependencies": { "@astrojs/starlight": "^0.34.3", "astro": "^5.7.13" } }, "sha512-s4yKn3PYnmPCl6MHPkQ2MedSgVh7FMAP64DiXnAfCls/H/RR6iTe0/SFgPTprz+HXXaKvv2iPo3XuvmDRY6cxQ=="],
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
@@ -2843,8 +2835,6 @@
"tuf-js": ["tuf-js@2.2.1", "", { "dependencies": { "@tufjs/models": "2.0.1", "debug": "^4.3.4", "make-fetch-happen": "^13.0.1" } }, "sha512-GwIJau9XaA8nLVbUXsN3IlFi7WmQ48gBUrl3FTkkL/XLu/POhBzfmX9hd33FNMX1qAsfl6ozO1iMmW9NC8YniA=="],
"tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="],
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
"turndown": ["turndown@7.2.0", "", { "dependencies": { "@mixmark-io/domino": "^2.2.0" } }, "sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A=="],
@@ -2869,7 +2859,7 @@
"unctx": ["unctx@2.4.1", "", { "dependencies": { "acorn": "^8.14.0", "estree-walker": "^3.0.3", "magic-string": "^0.30.17", "unplugin": "^2.1.0" } }, "sha512-AbaYw0Nm4mK4qjhns67C+kgxR2YWiwlDBPzxrN8h8C6VtAdCgditAY5Dezu3IJy4XVqAnbrXt9oQJvsn3fyozg=="],
"undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="],
"undici": ["undici@7.13.0", "", {}, "sha512-l+zSMssRqrzDcb3fjMkjjLGmuiiK2pMIcV++mJaAc9vhjSGpvM7h43QgP+OAMb1GImHmbPyG2tBXeuyG5iY4gA=="],
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
@@ -3063,16 +3053,6 @@
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
"@actions/github/@octokit/core": ["@octokit/core@5.2.2", "", { "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "@octokit/types": "^13.0.0", "before-after-hook": "^2.2.0", "universal-user-agent": "^6.0.0" } }, "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg=="],
"@actions/github/@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@9.2.2", "", { "dependencies": { "@octokit/types": "^12.6.0" }, "peerDependencies": { "@octokit/core": "5" } }, "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ=="],
"@actions/github/@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@10.4.1", "", { "dependencies": { "@octokit/types": "^12.6.0" }, "peerDependencies": { "@octokit/core": "5" } }, "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg=="],
"@actions/github/@octokit/request": ["@octokit/request@8.4.1", "", { "dependencies": { "@octokit/endpoint": "^9.0.6", "@octokit/request-error": "^5.1.1", "@octokit/types": "^13.1.0", "universal-user-agent": "^6.0.0" } }, "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw=="],
"@actions/github/@octokit/request-error": ["@octokit/request-error@5.1.1", "", { "dependencies": { "@octokit/types": "^13.1.0", "deprecation": "^2.0.0", "once": "^1.4.0" } }, "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g=="],
"@ai-sdk/amazon-bedrock/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="],
"@ai-sdk/amazon-bedrock/@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=="],
@@ -3089,8 +3069,6 @@
"@astrojs/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
"@astrojs/sitemap/zod": ["zod@3.25.49", "", {}, "sha512-JMMPMy9ZBk3XFEdbM3iL1brx4NUSejd6xr3ELrrGEfGb355gjhiAWtG3K5o+AViV/3ZfkIrCzXsZn6SbLwTR8Q=="],
"@astrojs/solid-js/vite": ["vite@6.3.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="],
"@astrojs/solid-js/vite-plugin-solid": ["vite-plugin-solid@2.11.8", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-hFrCxBfv3B1BmFqnJF4JOCYpjrmi/zwyeKjcomQ0khh8HFyQ8SbuBWQ7zGojfrz6HUOBFrJBNySDi/JgAHytWg=="],
@@ -3187,8 +3165,6 @@
"@mdx-js/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
"@modelcontextprotocol/sdk/zod": ["zod@3.25.49", "", {}, "sha512-JMMPMy9ZBk3XFEdbM3iL1brx4NUSejd6xr3ELrrGEfGb355gjhiAWtG3K5o+AViV/3ZfkIrCzXsZn6SbLwTR8Q=="],
"@netlify/dev-utils/find-up": ["find-up@7.0.0", "", { "dependencies": { "locate-path": "^7.2.0", "path-exists": "^5.0.0", "unicorn-magic": "^0.1.0" } }, "sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g=="],
"@netlify/dev-utils/uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="],
@@ -3235,6 +3211,8 @@
"@openauthjs/solid/@openauthjs/openauth": ["@openauthjs/openauth@0.4.2", "", { "dependencies": { "@standard-schema/spec": "1.0.0-beta.3", "aws4fetch": "1.0.20", "jose": "5.9.6" }, "peerDependencies": { "arctic": "^2.2.2", "hono": "^4.0.0" } }, "sha512-8+Bia559iffrZXfQ0LWXrVVVriochS88pDtB8indyQ1S+40MQgDBu8aBzKt+fgSrTmoQGCTT+wlOXgbjc9qIcw=="],
"@opencode-ai/sdk/@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.80.1", "", { "dependencies": { "@hey-api/json-schema-ref-parser": "1.0.6", "ansi-colors": "4.1.3", "c12": "2.0.1", "color-support": "1.1.3", "commander": "13.0.0", "handlebars": "4.7.8", "open": "10.1.2", "semver": "7.7.2" }, "peerDependencies": { "typescript": "^5.5.3" }, "bin": { "openapi-ts": "bin/index.cjs" } }, "sha512-AC478kg36vmmrseLZNFonZ/cmXXmDzW5yWz4PVg1S8ebJsRtVRJ/QU+mtnXfzf9avN2P0pz/AO4WAe4jyFY2gA=="],
"@opencode/cloud-web/solid-js": ["solid-js@1.9.5", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "^1.1.0", "seroval-plugins": "^1.1.0" } }, "sha512-ogI3DaFcyn6UhYhrgcyRAMbu/buBJitYQASZz5WzfQVPP10RD2AbCoRZ517psnezrasyCbWzIxZ6kVqet768xw=="],
"@opentelemetry/instrumentation-grpc/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.27.0", "", {}, "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg=="],
@@ -3347,8 +3325,6 @@
"@whatwg-node/fetch/urlpattern-polyfill": ["urlpattern-polyfill@10.1.0", "", {}, "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw=="],
"@whatwg-node/node-fetch/@fastify/busboy": ["@fastify/busboy@3.1.1", "", {}, "sha512-5DGmA8FTdB2XbDeEwc/5ZXBl6UbBAyBOOLlPuBnZ/N1SwdH9Ii+cOX3tBROlDgcTXxjOYnLMVoKk9+FXAw0CJw=="],
"ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.1", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-/iP1sKc6UdJgGH98OCly7sWJKv+J9G47PnTjIj40IJMUQKwDrUMyf7zOOfRtPwSuNifYhSoJQ4s1WltI65gJ/g=="],
"ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
@@ -3367,8 +3343,6 @@
"astro/vite": ["vite@6.3.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="],
"astro/zod": ["zod@3.25.49", "", {}, "sha512-JMMPMy9ZBk3XFEdbM3iL1brx4NUSejd6xr3ELrrGEfGb355gjhiAWtG3K5o+AViV/3ZfkIrCzXsZn6SbLwTR8Q=="],
"babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="],
"bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
@@ -3443,12 +3417,8 @@
"miniflare/sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="],
"miniflare/undici": ["undici@7.13.0", "", {}, "sha512-l+zSMssRqrzDcb3fjMkjjLGmuiiK2pMIcV++mJaAc9vhjSGpvM7h43QgP+OAMb1GImHmbPyG2tBXeuyG5iY4gA=="],
"miniflare/youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="],
"miniflare/zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="],
"minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
@@ -3493,8 +3463,6 @@
"opencontrol/hono": ["hono@4.7.4", "", {}, "sha512-Pst8FuGqz3L7tFF+u9Pu70eI0xa5S3LPUmrNd5Jm8nTHze9FxLTK9Kaj5g/k4UcwuJSXTP65SyHOPLrffpcAJg=="],
"opencontrol/zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="],
"opencontrol/zod-to-json-schema": ["zod-to-json-schema@3.24.3", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A=="],
"openid-client/jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="],
@@ -3615,28 +3583,6 @@
"zip-stream/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="],
"@actions/github/@octokit/core/@octokit/auth-token": ["@octokit/auth-token@4.0.0", "", {}, "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA=="],
"@actions/github/@octokit/core/@octokit/graphql": ["@octokit/graphql@7.1.1", "", { "dependencies": { "@octokit/request": "^8.4.1", "@octokit/types": "^13.0.0", "universal-user-agent": "^6.0.0" } }, "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g=="],
"@actions/github/@octokit/core/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="],
"@actions/github/@octokit/core/before-after-hook": ["before-after-hook@2.2.3", "", {}, "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="],
"@actions/github/@octokit/core/universal-user-agent": ["universal-user-agent@6.0.1", "", {}, "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="],
"@actions/github/@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="],
"@actions/github/@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="],
"@actions/github/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@9.0.6", "", { "dependencies": { "@octokit/types": "^13.1.0", "universal-user-agent": "^6.0.0" } }, "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw=="],
"@actions/github/@octokit/request/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="],
"@actions/github/@octokit/request/universal-user-agent": ["universal-user-agent@6.0.1", "", {}, "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="],
"@actions/github/@octokit/request-error/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="],
"@astrojs/mdx/@astrojs/markdown-remark/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.6.1", "", {}, "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="],
"@astrojs/mdx/@astrojs/markdown-remark/@astrojs/prism": ["@astrojs/prism@3.3.0", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ=="],
@@ -3935,8 +3881,6 @@
"opencontrol/@modelcontextprotocol/sdk/pkce-challenge": ["pkce-challenge@4.1.0", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="],
"opencontrol/@modelcontextprotocol/sdk/zod": ["zod@3.25.49", "", {}, "sha512-JMMPMy9ZBk3XFEdbM3iL1brx4NUSejd6xr3ELrrGEfGb355gjhiAWtG3K5o+AViV/3ZfkIrCzXsZn6SbLwTR8Q=="],
"opencontrol/@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="],
"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=="],
@@ -4021,16 +3965,6 @@
"zip-stream/readable-stream/events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
"@actions/github/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
"@actions/github/@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="],
"@actions/github/@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="],
"@actions/github/@octokit/request-error/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
"@actions/github/@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
"@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
+4 -2
View File
@@ -5,15 +5,17 @@
"dev": "vinxi dev --host 0.0.0.0",
"build": "vinxi build",
"start": "vinxi start",
"version": "0.5.5"
"version": "0.5.18"
},
"dependencies": {
"@ibm/plex": "6.4.1",
"@openauthjs/openauth": "0.0.0-20250322224806",
"@solidjs/meta": "^0.29.4",
"@solidjs/router": "^0.15.0",
"@solidjs/start": "^1.1.0",
"solid-js": "^1.9.5",
"vinxi": "^0.5.7"
"vinxi": "^0.5.7",
"@opencode/cloud-core": "workspace:*"
},
"engines": {
"node": ">=22"
+4 -2
View File
@@ -1,7 +1,7 @@
import { MetaProvider, Title } from "@solidjs/meta";
import { Router } from "@solidjs/router";
import { FileRoutes } from "@solidjs/start/router";
import { Suspense } from "solid-js";
import { ErrorBoundary, Suspense } from "solid-js";
import "@ibm/plex/css/ibm-plex.css";
import "./app.css";
@@ -11,7 +11,9 @@ export default function App() {
root={props => (
<MetaProvider>
<Title>SolidStart - Basic</Title>
<Suspense>{props.children}</Suspense>
<ErrorBoundary fallback={<div>Something went wrong</div>}>
<Suspense>{props.children}</Suspense>
</ErrorBoundary>
</MetaProvider>
)}
>
+109
View File
@@ -0,0 +1,109 @@
import { useSession } from "vinxi/http"
import { createClient } from "@openauthjs/openauth/client"
import { getRequestEvent } from "solid-js/web"
import { and, Database, eq, inArray } from "@opencode/cloud-core/drizzle/index.js"
import { WorkspaceTable } from "@opencode/cloud-core/schema/workspace.sql.js"
import { UserTable } from "@opencode/cloud-core/schema/user.sql.js"
import { query, redirect } from "@solidjs/router"
import { AccountTable } from "@opencode/cloud-core/schema/account.sql.js"
import { Actor } from "@opencode/cloud-core/actor.js"
export async function withActor<T>(fn: () => T) {
const actor = await getActor()
return Actor.provide(actor.type, actor.properties, fn)
}
export const getActor = query(async (): Promise<Actor.Info> => {
"use server"
const evt = getRequestEvent()
const url = new URL(evt!.request.headers.get("referer") ?? evt!.request.url)
const auth = await useAuthSession()
const [workspaceHint] = url.pathname.split("/").filter((x) => x.length > 0)
if (!workspaceHint) {
if (auth.data.current) {
const current = auth.data.account[auth.data.current]
return {
type: "account",
properties: {
email: current.email,
accountID: current.id,
},
}
}
if (Object.keys(auth.data.account).length > 0) {
const current = Object.values(auth.data.account)[0]
await auth.update(val => ({
...val,
current: current.id,
}))
return {
type: "account",
properties: {
email: current.email,
accountID: current.id,
},
}
}
return {
type: "public",
properties: {},
}
}
const accounts = Object.keys(auth.data.account)
const result = await Database.transaction(async (tx) => {
return await tx.select({
user: UserTable
})
.from(AccountTable)
.innerJoin(UserTable, and(eq(UserTable.email, AccountTable.email)))
.innerJoin(WorkspaceTable, eq(WorkspaceTable.id, UserTable.workspaceID))
.where(
and(
inArray(AccountTable.id, accounts),
eq(WorkspaceTable.id, workspaceHint),
)
)
.limit(1)
.execute()
.then((x) => x[0])
})
if (result) {
return {
type: "user",
properties: {
userID: result.user.id,
workspaceID: result.user.workspaceID,
},
}
}
throw redirect("/auth/authorize")
}, "actor")
export const AuthClient = createClient({
clientID: "app",
issuer: import.meta.env.VITE_AUTH_URL,
})
export interface AuthSession {
account: Record<string, {
id: string
email: string
}>
current?: string
}
export function useAuthSession() {
return useSession<AuthSession>({
password: "0".repeat(32),
name: "auth"
})
}
export function AuthProvider() {
}
+15
View File
@@ -0,0 +1,15 @@
import { createAsync, query } from "@solidjs/router"
import { getActor, withActor } from "~/context/auth"
const getPosts = query(async () => {
"use server"
return withActor(() => {
return "ok"
})
}, "posts")
export default function () {
const actor = createAsync(async () => getActor())
return <div>{JSON.stringify(actor())}</div>
}
+7
View File
@@ -0,0 +1,7 @@
import type { APIEvent } from "@solidjs/start/server"
import { AuthClient } from "~/context/auth"
export async function GET(input: APIEvent) {
const result = await AuthClient.authorize(new URL("./callback", input.request.url).toString(), "code")
return Response.redirect(result.url, 302)
}
+31
View File
@@ -0,0 +1,31 @@
import type { APIEvent } from "@solidjs/start/server"
import { AuthClient, useAuthSession } from "~/context/auth"
export async function GET(input: APIEvent) {
const url = new URL(input.request.url)
const code = url.searchParams.get("code")
if (!code) throw new Error("No code found")
const result = await AuthClient.exchange(code, `${url.origin}${url.pathname}`)
if (result.err) {
throw new Error(result.err.message)
}
const decoded = AuthClient.decode(result.tokens.access, {} as any)
if (decoded.err) throw new Error(decoded.err.message)
const session = await useAuthSession()
const id = decoded.subject.properties.accountID
await session.update((value) => {
return {
...value,
account: {
[id]: {
id,
email: decoded.subject.properties.email,
},
},
current: id,
}
})
return {
result,
}
}
+19 -1
View File
@@ -6,6 +6,9 @@ import IMG_SPLASH from "../asset/screenshot-splash.webp"
import IMG_VSCODE from "../asset/screenshot-vscode.webp"
import IMG_GITHUB from "../asset/screenshot-github.webp"
import { IconCopy, IconCheck } from "../component/icon"
import { createAsync, query, redirect, RouteDefinition } from "@solidjs/router"
import { getActor, withActor } from "~/context/auth"
import { Account } from "@opencode/cloud-core/account.js"
function CopyStatus() {
return (
@@ -16,7 +19,22 @@ function CopyStatus() {
)
}
const isLoggedIn = query(async () => {
"use server"
const actor = await getActor()
if (actor.type === "account") {
const workspaces = await withActor(() => Account.workspaces())
throw redirect("/" + workspaces[0].id)
}
return
}, "isLoggedIn")
export default function Home() {
createAsync(() => isLoggedIn(), {
deferStream: true,
})
onMount(() => {
const commands = document.querySelectorAll("[data-copy]")
for (const button of commands) {
@@ -51,7 +69,7 @@ export default function Home() {
<a href="/docs">Get Started</a>
</div>
<div data-slot="right">
<button data-copy data-slot="command" data-command="curl -fsSL https://opencode.ai/install | bash">
<button data-copy data-slot="command">
<span>
<span>curl -fsSL&nbsp;</span>
<span data-slot="protocol">https://</span>
+9
View File
@@ -0,0 +1,9 @@
/* This file is auto-generated by SST. Do not edit. */
/* tslint:disable */
/* eslint-disable */
/* deno-fmt-ignore-file */
/// <reference path="../../sst-env.d.ts" />
import "sst"
export {}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/cloud-core",
"version": "0.5.5",
"version": "0.5.18",
"private": true,
"type": "module",
"dependencies": {
-1
View File
@@ -20,7 +20,6 @@ export namespace Actor {
properties: {
userID: string
workspaceID: string
email: string
}
}
+3 -2
View File
@@ -3,7 +3,7 @@ import { Resource } from "sst"
export * from "drizzle-orm"
import postgres from "postgres"
function createClient() {
const createClient = memo(() => {
const client = postgres({
idle_timeout: 30000,
connect_timeout: 30000,
@@ -19,12 +19,13 @@ function createClient() {
})
return drizzle(client, {})
}
})
import { PgTransaction, type PgTransactionConfig } from "drizzle-orm/pg-core"
import type { ExtractTablesWithRelations } from "drizzle-orm"
import type { PostgresJsQueryResultHKT } from "drizzle-orm/postgres-js"
import { Context } from "../context"
import { memo } from "../util/memo"
export namespace Database {
export type Transaction = PgTransaction<
+11
View File
@@ -0,0 +1,11 @@
export function memo<T>(fn: () => T) {
let value: T | undefined
let loaded = false
return (): T => {
if (loaded) return value as T
loaded = true
value = fn()
return value as T
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/cloud-function",
"version": "0.5.5",
"version": "0.5.18",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+8 -1
View File
@@ -2,11 +2,12 @@ import { Resource } from "sst"
import { z } from "zod"
import { issuer } from "@openauthjs/openauth"
import { createSubjects } from "@openauthjs/openauth/subject"
import { CodeProvider } from "@openauthjs/openauth/provider/code"
import { GithubProvider } from "@openauthjs/openauth/provider/github"
import { GoogleOidcProvider } from "@openauthjs/openauth/provider/google"
import { CloudflareStorage } from "@openauthjs/openauth/storage/cloudflare"
import { Account } from "@opencode/cloud-core/account.js"
import { Workspace } from "@opencode/cloud-core/workspace.js"
import { Actor } from "@opencode/cloud-core/actor.js"
type Env = {
AuthStorage: KVNamespace
@@ -117,6 +118,12 @@ export default {
email: email!,
})
}
await Actor.provide("account", { accountID, email }, async () => {
const workspaces = await Account.workspaces()
if (workspaces.length === 0) {
await Workspace.create()
}
})
return ctx.subject("account", accountID, { accountID, email })
},
}).fetch(request, env, ctx)
-4
View File
@@ -14,10 +14,6 @@ declare module "sst" {
"type": "sst.sst.Linkable"
"value": string
}
"Console": {
"type": "sst.cloudflare.StaticSite"
"url": string
}
"DATABASE_PASSWORD": {
"type": "sst.sst.Secret"
"value": string
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/cloud-web",
"version": "0.5.5",
"version": "0.5.18",
"private": true,
"description": "",
"type": "module",
+1 -1
View File
@@ -8,7 +8,7 @@
"@actions/github": "6.0.1",
"@octokit/graphql": "9.0.1",
"@octokit/rest": "22.0.0",
"@opencode-ai/sdk": "^0.5.4",
"@opencode-ai/sdk": "0.5.4",
},
"devDependencies": {
"@types/bun": "latest",
+9
View File
@@ -0,0 +1,9 @@
/* This file is auto-generated by SST. Do not edit. */
/* tslint:disable */
/* eslint-disable */
/* deno-fmt-ignore-file */
/// <reference path="../sst-env.d.ts" />
import "sst"
export {}
+2 -2
View File
@@ -25,8 +25,8 @@ export const api = new sst.cloudflare.Worker("Api", {
])
args.migrations = {
// Note: when releasing the next tag, make sure all stages use tag v2
oldTag: $app.stage === "production" ? "" : "v1",
newTag: $app.stage === "production" ? "" : "v1",
oldTag: $app.stage === "production" || $app.stage === "thdxr" ? "" : "v1",
newTag: $app.stage === "production" || $app.stage === "thdxr" ? "" : "v1",
//newSqliteClasses: ["SyncServer"],
}
},
+14 -1
View File
@@ -10,7 +10,7 @@ const DATABASE_USERNAME = new sst.Secret("DATABASE_USERNAME")
const DATABASE_PASSWORD = new sst.Secret("DATABASE_PASSWORD")
export const database = new sst.Linkable("Database", {
properties: {
host: "aws-us-east-2-1.pg.psdb.cloud",
host: `aws-us-east-2-${$app.stage === "thdxr" ? "2" : "1"}.pg.psdb.cloud`,
database: "postgres",
username: DATABASE_USERNAME.value,
password: DATABASE_PASSWORD.value,
@@ -106,6 +106,7 @@ export const gateway = new sst.cloudflare.Worker("GatewayApi", {
// CONSOLE
////////////////
/*
export const console = new sst.cloudflare.x.StaticSite("Console", {
domain: `console.${domain}`,
path: "cloud/web",
@@ -119,3 +120,15 @@ export const console = new sst.cloudflare.x.StaticSite("Console", {
VITE_AUTH_URL: auth.url.apply((url) => url!),
},
})
*/
new sst.x.DevCommand("Solid", {
link: [database],
dev: {
directory: "cloud/app",
command: "bun dev",
},
environment: {
VITE_AUTH_URL: auth.url.apply((url) => url!),
},
})
@@ -0,0 +1,15 @@
{
"keep": {
"days": true,
"amount": 14
},
"auditLog": "/Users/adam/code/opencode/dev/logs/.496fc674ed58d31f8b883da41cc2adb4564aad58-audit.json",
"files": [
{
"date": 1755891797740,
"name": "/Users/adam/code/opencode/dev/logs/mcp-puppeteer-2025-08-22.log",
"hash": "dd9b1f2e98b661ba2f56b91dd9afbdb25e50adbdd52ed1b0eef1d2045235d17c"
}
],
"hashType": "sha256"
}
+6
View File
@@ -0,0 +1,6 @@
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-08-22 14:43:17.765"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-08-22 14:43:17.766"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-08-22 14:46:45.539"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-08-22 14:46:45.540"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-08-22 14:53:08.159"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-08-22 14:53:08.160"}
+1 -4
View File
@@ -1,10 +1,7 @@
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"context7": {
"type": "remote",
"url": "https://mcp.context7.com/sse"
},
"weather": {
"type": "local",
"command": ["opencode", "x", "@h1deya/mcp-server-weather"]
+3
View File
@@ -51,5 +51,8 @@
"tree-sitter-bash",
"web-tree-sitter"
],
"overrides": {
"zod": "3.25.76"
},
"patchedDependencies": {}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/function",
"version": "0.5.5",
"version": "0.5.18",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
-4
View File
@@ -14,10 +14,6 @@ declare module "sst" {
"type": "sst.sst.Linkable"
"value": string
}
"Console": {
"type": "sst.cloudflare.StaticSite"
"url": string
}
"DATABASE_PASSWORD": {
"type": "sst.sst.Secret"
"value": string
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "0.5.5",
"version": "0.5.18",
"name": "opencode",
"type": "module",
"private": true,
-2
View File
@@ -97,7 +97,6 @@ if (!snapshot) {
const macX64Sha = await $`sha256sum ./dist/opencode-darwin-x64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
const macArm64Sha = await $`sha256sum ./dist/opencode-darwin-arm64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
/* AUR package - commented out as AUR is down
const pkgbuild = [
"# Maintainer: dax",
"# Maintainer: adam",
@@ -136,7 +135,6 @@ if (!snapshot) {
await $`cd ./dist/aur-${pkg} && git commit -m "Update to v${version}"`
if (!dry) await $`cd ./dist/aur-${pkg} && git push`
}
*/
// Homebrew formula
const homebrewFormula = [
+10 -1
View File
@@ -5,6 +5,7 @@ import { Config } from "../src/config/config"
import { zodToJsonSchema } from "zod-to-json-schema"
const file = process.argv[2]
console.log(file)
const result = zodToJsonSchema(Config.Info, {
/**
@@ -31,5 +32,13 @@ const result = zodToJsonSchema(Config.Info, {
return jsonSchema
},
})
}) as Record<string, unknown> & {
allowComments?: boolean
allowTrailingCommas?: boolean
}
// used for json lsps since config supports jsonc
result.allowComments = true
result.allowTrailingCommas = true
await Bun.write(file, JSON.stringify(result, null, 2))
+13 -5
View File
@@ -48,6 +48,15 @@ export namespace Agent {
}
const agentPermission = mergeAgentPermissions(defaultPermission, cfg.permission ?? {})
const planPermission = mergeAgentPermissions(
{
edit: "ask",
bash: "ask",
webfetch: "allow",
},
cfg.permission ?? {},
)
const result: Record<string, Info> = {
general: {
name: "general",
@@ -74,11 +83,8 @@ export namespace Agent {
plan: {
name: "plan",
options: {},
permission: agentPermission,
permission: planPermission,
tools: {
write: false,
edit: false,
patch: false,
...defaultTools,
},
mode: "primary",
@@ -100,7 +106,7 @@ export namespace Agent {
tools: {},
builtIn: false,
}
const { model, prompt, tools, description, temperature, top_p, mode, permission, ...extra } = value
const { name, model, prompt, tools, description, temperature, top_p, mode, permission, ...extra } = value
item.options = {
...item.options,
...extra,
@@ -120,6 +126,8 @@ export namespace Agent {
if (temperature != undefined) item.temperature = temperature
if (top_p != undefined) item.topP = top_p
if (mode) item.mode = mode
// just here for consistency & to prevent it from being added as an option
if (name) item.name = name
if (permission ?? cfg.permission) {
item.permission = mergeAgentPermissions(cfg.permission ?? {}, permission ?? {})
+1 -1
View File
@@ -8,9 +8,9 @@ import { Snapshot } from "../snapshot"
export async function bootstrap<T>(input: App.Input, cb: (app: App.Info) => Promise<T>) {
return App.provide(input, async (app) => {
await Plugin.init()
Share.init()
Format.init()
Plugin.init()
ConfigHooks.init()
LSP.init()
Snapshot.init()
+4 -1
View File
@@ -46,7 +46,10 @@ const AgentCreateCommand = cmd({
const spinner = prompts.spinner()
spinner.start("Generating agent configuration...")
const generated = await Agent.generate({ description: query })
const generated = await Agent.generate({ description: query }).catch((error) => {
spinner.stop(`LLM failed to generate agent: ${error.message}`, 1)
throw new UI.CancelledError()
})
spinner.stop(`Agent ${generated.identifier} generated`)
const availableTools = [
+28 -16
View File
@@ -1,7 +1,6 @@
import { Auth } from "../../auth"
import { cmd } from "./cmd"
import * as prompts from "@clack/prompts"
import open from "open"
import { UI } from "../ui"
import { ModelsDev } from "../../provider/models"
import { map, pipe, sortBy, values } from "remeda"
@@ -159,9 +158,6 @@ export const AuthLoginCommand = cmd({
const authorize = await method.authorize()
if (authorize.url) {
try {
await open(authorize.url)
} catch (e) {}
prompts.log.info("Go to: " + authorize.url)
}
@@ -176,12 +172,20 @@ export const AuthLoginCommand = cmd({
spinner.stop("Failed to authorize", 1)
}
if (result.type === "success") {
await Auth.set(provider, {
type: "oauth",
refresh: result.refresh,
access: result.access,
expires: result.expires,
})
if ("refresh" in result) {
await Auth.set(provider, {
type: "oauth",
refresh: result.refresh,
access: result.access,
expires: result.expires,
})
}
if ("key" in result) {
await Auth.set(provider, {
type: "api",
key: result.key,
})
}
spinner.stop("Login successful")
}
}
@@ -197,12 +201,20 @@ export const AuthLoginCommand = cmd({
prompts.log.error("Failed to authorize")
}
if (result.type === "success") {
await Auth.set(provider, {
type: "oauth",
refresh: result.refresh,
access: result.access,
expires: result.expires,
})
if ("refresh" in result) {
await Auth.set(provider, {
type: "oauth",
refresh: result.refresh,
access: result.access,
expires: result.expires,
})
}
if ("key" in result) {
await Auth.set(provider, {
type: "api",
key: result.key,
})
}
prompts.log.success("Login successful")
}
}
+11 -5
View File
@@ -67,11 +67,17 @@ export const RunCommand = cmd({
await bootstrap({ cwd: process.cwd() }, async () => {
const session = await (async () => {
if (args.continue) {
const list = Session.list()
const first = await list.next()
await list.return()
if (first.done) return
return first.value
const it = Session.list()
try {
for await (const s of it) {
if (s.parentID === undefined) {
return s
}
}
return
} finally {
await it.return()
}
}
if (args.session) return Session.get(args.session)
+1 -1
View File
@@ -11,7 +11,7 @@ export const ServeCommand = cmd({
alias: ["p"],
type: "number",
describe: "port to listen on",
default: 4096,
default: 0,
})
.option("hostname", {
alias: ["h"],
+11 -5
View File
@@ -82,11 +82,17 @@ export const TuiCommand = cmd({
const result = await bootstrap({ cwd }, async (app) => {
const sessionID = await (async () => {
if (args.continue) {
const list = Session.list()
const first = await list.next()
await list.return()
if (first.done) return
return first.value.id
const it = Session.list()
try {
for await (const s of it) {
if (s.parentID === undefined) {
return s.id
}
}
return
} finally {
await it.return()
}
}
if (args.session) {
return args.session
+1 -1
View File
@@ -45,7 +45,7 @@ export const UpgradeCommand = {
spinner.start("Upgrading...")
const err = await Installation.upgrade(method, target).catch((err) => err)
if (err) {
spinner.stop("Upgrade failed")
spinner.stop("Upgrade failed", 1)
if (err instanceof Installation.UpgradeFailedError) prompts.log.error(err.data.stderr)
else if (err instanceof Error) prompts.log.error(err.message)
prompts.outro("Done")
+44
View File
@@ -0,0 +1,44 @@
import z from "zod"
import { App } from "../app/app"
import { Config } from "../config/config"
export namespace Command {
export const Info = z
.object({
name: z.string(),
description: z.string().optional(),
agent: z.string().optional(),
model: z.string().optional(),
template: z.string(),
})
.openapi({
ref: "Command",
})
export type Info = z.infer<typeof Info>
const state = App.state("command", async () => {
const cfg = await Config.get()
const result: Record<string, Info> = {}
for (const [name, command] of Object.entries(cfg.command ?? {})) {
result[name] = {
name,
agent: command.agent,
model: command.model,
description: command.description,
template: command.template,
}
}
return result
})
export async function get(name: string) {
return state().then((x) => x[name])
}
export async function list() {
return state().then((x) => Object.values(x))
}
}
+37 -1
View File
@@ -64,7 +64,7 @@ export namespace Config {
if (agentFolderPath.includes("/")) {
const relativePath = agentFolderPath.replace(".md", "")
const pathParts = relativePath.split("/")
agentName = pathParts.slice(0, -1).join("/").toUpperCase() + "/" + pathParts[pathParts.length - 1].toUpperCase()
agentName = pathParts.slice(0, -1).join("/") + "/" + pathParts[pathParts.length - 1]
}
const config = {
@@ -107,6 +107,32 @@ export namespace Config {
}
throw new InvalidError({ path: item }, { cause: parsed.error })
}
// Load command markdown files
result.command = result.command || {}
const markdownCommands = [
...(await Filesystem.globUp("command/*.md", Global.Path.config, Global.Path.config)),
...(await Filesystem.globUp(".opencode/command/*.md", app.path.cwd, app.path.root)),
]
for (const item of markdownCommands) {
const content = await Bun.file(item).text()
const md = matter(content)
if (!md.data) continue
const config = {
name: path.basename(item, ".md"),
...md.data,
template: md.content.trim(),
}
const parsed = Command.safeParse(config)
if (parsed.success) {
result.command = mergeDeep(result.command, {
[config.name]: parsed.data,
})
continue
}
throw new InvalidError({ path: item }, { cause: parsed.error })
}
// Migrate deprecated mode field to agent field
for (const [name, mode] of Object.entries(result.mode)) {
result.agent = mergeDeep(result.agent ?? {}, {
@@ -192,6 +218,14 @@ export namespace Config {
export const Permission = z.union([z.literal("ask"), z.literal("allow"), z.literal("deny")])
export type Permission = z.infer<typeof Permission>
export const Command = z.object({
template: z.string(),
description: z.string().optional(),
agent: z.string().optional(),
model: z.string().optional(),
})
export type Command = z.infer<typeof Command>
export const Agent = z
.object({
model: z.string().optional(),
@@ -229,6 +263,7 @@ export namespace Config {
session_export: z.string().optional().default("<leader>x").describe("Export session to editor"),
session_new: z.string().optional().default("<leader>n").describe("Create a new session"),
session_list: z.string().optional().default("<leader>l").describe("List all sessions"),
session_timeline: z.string().optional().default("<leader>g").describe("Show session timeline"),
session_share: z.string().optional().default("<leader>s").describe("Share current session"),
session_unshare: z.string().optional().default("none").describe("Unshare current session"),
session_interrupt: z.string().optional().default("esc").describe("Interrupt current session"),
@@ -304,6 +339,7 @@ export namespace Config {
theme: z.string().optional().describe("Theme name to use for the interface"),
keybinds: Keybinds.optional().describe("Custom keybind configurations"),
tui: TUI.optional().describe("TUI specific settings"),
command: z.record(z.string(), Command).optional(),
plugin: z.string().array().optional(),
snapshot: z.boolean().optional(),
share: z
+1
View File
@@ -5,6 +5,7 @@ export namespace Flag {
export const OPENCODE_DISABLE_AUTOUPDATE = truthy("OPENCODE_DISABLE_AUTOUPDATE")
export const OPENCODE_PERMISSION = process.env["OPENCODE_PERMISSION"]
export const OPENCODE_DISABLE_DEFAULT_PLUGINS = truthy("OPENCODE_DISABLE_DEFAULT_PLUGINS")
export const OPENCODE_DISABLE_LSP_DOWNLOAD = truthy("OPENCODE_DISABLE_LSP_DOWNLOAD")
function truthy(key: string) {
const value = process.env[key]?.toLowerCase()
+19 -9
View File
@@ -68,19 +68,29 @@ export namespace Format {
for (const item of await getFormatter(ext)) {
log.info("running", { command: item.command })
const proc = Bun.spawn({
cmd: item.command.map((x) => x.replace("$FILE", file)),
cwd: App.info().path.cwd,
env: { ...process.env, ...item.environment },
stdout: "ignore",
stderr: "ignore",
})
const exit = await proc.exited
if (exit !== 0)
try {
const proc = Bun.spawn({
cmd: item.command.map((x) => x.replace("$FILE", file)),
cwd: App.info().path.cwd,
env: { ...process.env, ...item.environment },
stdout: "ignore",
stderr: "ignore",
})
const exit = await proc.exited
if (exit !== 0)
log.error("failed", {
command: item.command,
...item.environment,
})
} catch (error) {
log.error("failed", {
error,
command: item.command,
...item.environment,
})
// re-raising
throw error
}
}
})
}
+7 -2
View File
@@ -28,13 +28,18 @@ await Promise.all([
fs.mkdir(Global.Path.bin, { recursive: true }),
])
const CACHE_VERSION = "8"
const CACHE_VERSION = "9"
const version = await Bun.file(path.join(Global.Path.cache, "version"))
.text()
.catch(() => "0")
if (version !== CACHE_VERSION) {
await fs.rm(Global.Path.cache, { recursive: true, force: true })
try {
const contents = await fs.readdir(Global.Path.cache)
await Promise.all(
contents.map((item) => fs.rm(path.join(Global.Path.cache, item), { recursive: true, force: true })),
)
} catch (e) {}
await Bun.file(path.join(Global.Path.cache, "version")).write(CACHE_VERSION)
}
+6 -2
View File
@@ -126,7 +126,11 @@ export namespace LSP {
result.push(match)
continue
}
const handle = await server.spawn(App.info(), root)
const handle = await server.spawn(App.info(), root).catch((err) => {
s.broken.add(root + server.id)
log.error(`Failed to spawn LSP server ${server.id}`, { error: err })
return undefined
})
if (!handle) continue
const client = await LSPClient.create({
serverID: server.id,
@@ -135,7 +139,7 @@ export namespace LSP {
}).catch((err) => {
s.broken.add(root + server.id)
handle.process.kill()
log.error("", { error: err })
log.error(`Failed to initialize LSP client ${server.id}`, { error: err })
})
if (!client) continue
s.clients.push(client)
+11
View File
@@ -7,6 +7,7 @@ import { BunProc } from "../bun"
import { $ } from "bun"
import fs from "fs/promises"
import { Filesystem } from "../util/filesystem"
import { Flag } from "../flag/flag"
export namespace LSPServer {
const log = Log.create({ service: "lsp.server" })
@@ -95,6 +96,7 @@ export namespace LSPServer {
"vue-language-server.js",
)
if (!(await Bun.file(js).exists())) {
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
await Bun.spawn([BunProc.which(), "install", "@vue/language-server"], {
cwd: Global.Path.bin,
env: {
@@ -148,6 +150,7 @@ export namespace LSPServer {
if (!eslint) return
const serverPath = path.join(Global.Path.bin, "vscode-eslint", "server", "out", "eslintServer.js")
if (!(await Bun.file(serverPath).exists())) {
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
log.info("downloading and building VS Code ESLint server")
const response = await fetch("https://github.com/microsoft/vscode-eslint/archive/refs/heads/main.zip")
if (!response.ok) return
@@ -200,6 +203,8 @@ export namespace LSPServer {
})
if (!bin) {
if (!Bun.which("go")) return
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
log.info("installing gopls")
const proc = Bun.spawn({
cmd: ["go", "install", "golang.org/x/tools/gopls@latest"],
@@ -241,6 +246,7 @@ export namespace LSPServer {
log.info("Ruby not found, please install Ruby first")
return
}
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
log.info("installing ruby-lsp")
const proc = Bun.spawn({
cmd: ["gem", "install", "ruby-lsp", "--bindir", Global.Path.bin],
@@ -276,6 +282,7 @@ export namespace LSPServer {
if (!binary) {
const js = path.join(Global.Path.bin, "node_modules", "pyright", "dist", "pyright-langserver.js")
if (!(await Bun.file(js).exists())) {
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
await Bun.spawn([BunProc.which(), "install", "pyright"], {
cwd: Global.Path.bin,
env: {
@@ -323,6 +330,7 @@ export namespace LSPServer {
return
}
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
log.info("downloading elixir-ls from GitHub releases")
const response = await fetch("https://github.com/elixir-lsp/elixir-ls/archive/refs/heads/master.zip")
@@ -372,6 +380,7 @@ export namespace LSPServer {
return
}
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
log.info("downloading zls from GitHub releases")
const releaseResponse = await fetch("https://api.github.com/repos/zigtools/zls/releases/latest")
@@ -475,6 +484,7 @@ export namespace LSPServer {
return
}
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
log.info("installing csharp-ls via dotnet tool")
const proc = Bun.spawn({
cmd: ["dotnet", "tool", "install", "csharp-ls", "--tool-path", Global.Path.bin],
@@ -527,6 +537,7 @@ export namespace LSPServer {
PATH: process.env["PATH"] + ":" + Global.Path.bin,
})
if (!bin) {
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
log.info("downloading clangd from GitHub releases")
const releaseResponse = await fetch("https://api.github.com/repos/clangd/clangd/releases/latest")
+8 -3
View File
@@ -25,8 +25,8 @@ export namespace Plugin {
}
const plugins = [...(config.plugin ?? [])]
if (!Flag.OPENCODE_DISABLE_DEFAULT_PLUGINS) {
plugins.push("opencode-copilot-auth")
plugins.push("opencode-anthropic-auth")
plugins.push("opencode-copilot-auth@0.0.2")
plugins.push("opencode-anthropic-auth@0.0.2")
}
for (let plugin of plugins) {
log.info("loading plugin", { path: plugin })
@@ -68,7 +68,12 @@ export namespace Plugin {
return state().then((x) => x.hooks)
}
export function init() {
export async function init() {
const hooks = await state().then((x) => x.hooks)
const config = await Config.get()
for (const hook of hooks) {
await hook.config?.(config)
}
Bus.subscribeAll(async (input) => {
const hooks = await state().then((x) => x.hooks)
for (const hook of hooks) {
@@ -36,6 +36,12 @@ export namespace Provider {
},
}
},
async opencode(input) {
return {
autoload: Object.keys(input.models).length > 0,
options: {},
}
},
openai: async () => {
return {
autoload: false,
@@ -276,6 +282,15 @@ export namespace Provider {
}
for (const [providerID, provider] of Object.entries(providers)) {
// Filter out blacklisted models
const filteredModels = Object.fromEntries(
Object.entries(provider.info.models).filter(
([modelID]) =>
modelID !== "gpt-5-chat-latest" && !(providerID === "openrouter" && modelID === "openai/gpt-5-chat"),
),
)
provider.info.models = filteredModels
if (Object.keys(provider.info.models).length === 0) {
delete providers[providerID]
continue
+65 -3
View File
@@ -21,6 +21,7 @@ import { Permission } from "../permission"
import { lazy } from "../util/lazy"
import { Agent } from "../agent/agent"
import { Auth } from "../auth"
import { Command } from "../command"
const ERRORS = {
400: {
@@ -102,7 +103,7 @@ export namespace Server {
200: {
description: "Event stream",
content: {
"application/json": {
"text/event-stream": {
schema: resolver(
Bus.payloads().openapi({
ref: "Event",
@@ -611,7 +612,12 @@ export namespace Server {
description: "Created message",
content: {
"application/json": {
schema: resolver(MessageV2.Assistant),
schema: resolver(
z.object({
info: MessageV2.Assistant,
parts: MessageV2.Part.array(),
}),
),
},
},
},
@@ -631,6 +637,41 @@ export namespace Server {
return c.json(msg)
},
)
.post(
"/session/:id/command",
describeRoute({
description: "Send a new command to a session",
operationId: "session.command",
responses: {
200: {
description: "Created message",
content: {
"application/json": {
schema: resolver(
z.object({
info: MessageV2.Assistant,
parts: MessageV2.Part.array(),
}),
),
},
},
},
},
}),
zValidator(
"param",
z.object({
id: z.string().openapi({ description: "Session ID" }),
}),
),
zValidator("json", Session.CommandInput.omit({ sessionID: true })),
async (c) => {
const sessionID = c.req.valid("param").id
const body = c.req.valid("json")
const msg = await Session.command({ ...body, sessionID })
return c.json(msg)
},
)
.post(
"/session/:id/shell",
describeRoute({
@@ -653,7 +694,7 @@ export namespace Server {
id: z.string().openapi({ description: "Session ID" }),
}),
),
zValidator("json", Session.CommandInput.omit({ sessionID: true })),
zValidator("json", Session.ShellInput.omit({ sessionID: true })),
async (c) => {
const sessionID = c.req.valid("param").id
const body = c.req.valid("json")
@@ -750,6 +791,27 @@ export namespace Server {
return c.json(true)
},
)
.get(
"/command",
describeRoute({
description: "List all commands",
operationId: "command.list",
responses: {
200: {
description: "List of commands",
content: {
"application/json": {
schema: resolver(Command.Info.array()),
},
},
},
},
}),
async (c) => {
const commands = await Command.list()
return c.json(commands)
},
)
.get(
"/config/providers",
describeRoute({
+92 -4
View File
@@ -17,6 +17,7 @@ import {
import PROMPT_INITIALIZE from "../session/prompt/initialize.txt"
import PROMPT_PLAN from "../session/prompt/plan.txt"
import BUILD_SWITCH from "../session/prompt/build-switch.txt"
import { App } from "../app/app"
import { Bus } from "../bus"
@@ -46,6 +47,8 @@ import { Permission } from "../permission"
import { Wildcard } from "../util/wildcard"
import { ulid } from "ulid"
import { defer } from "../util/defer"
import { Command } from "../command"
import { $ } from "bun"
export namespace Session {
const log = Log.create({ service: "session" })
@@ -726,6 +729,18 @@ export namespace Session {
synthetic: true,
})
}
const lastAssistantMsg = msgs.filter((x) => x.info.role === "assistant").at(-1)?.info as MessageV2.Assistant
if (lastAssistantMsg?.mode === "plan" && agent.name === "build") {
msgs.at(-1)?.parts.push({
id: Identifier.ascending("part"),
messageID: userMsg.id,
sessionID: input.sessionID,
type: "text",
text: BUILD_SWITCH,
synthetic: true,
})
}
let system = SystemPrompt.header(input.providerID)
system.push(
...(() => {
@@ -945,6 +960,13 @@ export namespace Session {
toolName: "invalid",
}
},
headers:
input.providerID === "opencode"
? {
"x-opencode-session": input.sessionID,
"x-opencode-request": userMsg.id,
}
: undefined,
maxRetries: 3,
activeTools: Object.keys(tools).filter((x) => x !== "invalid"),
maxOutputTokens: outputLimit,
@@ -1005,13 +1027,13 @@ export namespace Session {
return result
}
export const CommandInput = z.object({
export const ShellInput = z.object({
sessionID: Identifier.schema("session"),
agent: z.string(),
command: z.string(),
})
export type CommandInput = z.infer<typeof CommandInput>
export async function shell(input: CommandInput) {
export type ShellInput = z.infer<typeof ShellInput>
export async function shell(input: ShellInput) {
using abort = lock(input.sessionID)
const msg: MessageV2.Assistant = {
id: Identifier.ascending("message"),
@@ -1135,6 +1157,72 @@ export namespace Session {
return { info: msg, parts: [part] }
}
export const CommandInput = z.object({
messageID: Identifier.schema("message").optional(),
sessionID: Identifier.schema("session"),
agent: z.string().optional(),
model: z.string().optional(),
arguments: z.string(),
command: z.string(),
})
export type CommandInput = z.infer<typeof CommandInput>
const bashRegex = /!`([^`]+)`/g
const fileRegex = /@([^\s]+)/g
export async function command(input: CommandInput) {
const command = await Command.get(input.command)
const agent = input.agent ?? command.agent ?? "build"
const model =
input.model ??
command.model ??
(await Agent.get(agent).then((x) => (x.model ? `${x.model.providerID}/${x.model.modelID}` : undefined))) ??
(await Provider.defaultModel().then((x) => `${x.providerID}/${x.modelID}`))
let template = command.template.replace("$ARGUMENTS", input.arguments)
const bash = Array.from(template.matchAll(bashRegex))
if (bash.length > 0) {
const results = await Promise.all(
bash.map(async ([, cmd]) => {
try {
return await $`${{ raw: cmd }}`.nothrow().text()
} catch (error) {
return `Error executing command: ${error instanceof Error ? error.message : String(error)}`
}
}),
)
let index = 0
template = template.replace(bashRegex, () => results[index++])
}
const parts = [
{
type: "text",
text: template,
},
] as ChatInput["parts"]
const matches = template.matchAll(fileRegex)
const app = App.info()
for (const match of matches) {
const file = path.join(app.path.cwd, match[1])
parts.push({
type: "file",
url: `file://${file}`,
filename: match[1],
mime: "text/plain",
})
}
return chat({
sessionID: input.sessionID,
messageID: input.messageID,
...Provider.parseModel(model!),
agent,
parts,
})
}
function createProcessor(assistantMsg: MessageV2.Assistant, model: ModelsDev.Model) {
const toolcalls: Record<string, MessageV2.ToolPart> = {}
let snapshot: string | undefined
@@ -1592,7 +1680,7 @@ export namespace Session {
const tokens = {
input: usage.inputTokens ?? 0,
output: usage.outputTokens ?? 0,
reasoning: 0,
reasoning: usage?.reasoningTokens ?? 0,
cache: {
write: (metadata?.["anthropic"]?.["cacheCreationInputTokens"] ??
// @ts-expect-error
@@ -0,0 +1 @@
Your operational mode has changed from plan to build. You are no longer in read-only mode. You are permitted to make file changes as necessary and utilize your arsenal of tools as needed.
+1 -1
View File
@@ -86,7 +86,7 @@ export namespace ToolRegistry {
result["webfetch"] = false
}
if (modelID.includes("qwen")) {
if (modelID.toLowerCase().includes("qwen")) {
result["todowrite"] = false
result["todoread"] = false
}
+6 -1
View File
@@ -101,7 +101,12 @@ export namespace Log {
...extra,
})
.filter(([_, value]) => value !== undefined && value !== null)
.map(([key, value]) => `${key}=${typeof value === "object" ? JSON.stringify(value) : value}`)
.map(([key, value]) => {
const prefix = `${key}=`
if (value instanceof Error) return prefix + value.message
if (typeof value === "object") return prefix + JSON.stringify(value)
return prefix + value
})
.join(" ")
const next = new Date()
const diff = next.getTime() - last
+4 -4
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "0.5.5",
"version": "0.5.18",
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit"
@@ -19,8 +19,8 @@
"@opencode-ai/sdk": "workspace:*"
},
"devDependencies": {
"typescript": "catalog:",
"@hey-api/openapi-ts": "0.80.1",
"@tsconfig/node22": "catalog:"
"@hey-api/openapi-ts": "0.81.0",
"@tsconfig/node22": "catalog:",
"typescript": "catalog:"
}
}
+20 -10
View File
@@ -8,6 +8,7 @@ import type {
UserMessage,
Part,
Auth,
Config,
} from "@opencode-ai/sdk"
import type { BunShell } from "./shell"
@@ -20,6 +21,7 @@ export type Plugin = (input: PluginInput) => Promise<Hooks>
export interface Hooks {
event?: (input: { event: Event }) => Promise<void>
config?: (input: Config) => Promise<void>
auth?: {
provider: string
loader?: (auth: () => Promise<Auth>, provider: Provider) => Promise<Record<string, any>>
@@ -32,12 +34,16 @@ export interface Hooks {
| {
method: "auto"
callback(): Promise<
| {
| ({
type: "success"
refresh: string
access: string
expires: number
}
} & (
| {
refresh: string
access: string
expires: number
}
| { key: string }
))
| {
type: "failed"
}
@@ -46,12 +52,16 @@ export interface Hooks {
| {
method: "code"
callback(code: string): Promise<
| {
| ({
type: "success"
refresh: string
access: string
expires: number
}
} & (
| {
refresh: string
access: string
expires: number
}
| { key: string }
))
| {
type: "failed"
}
+5
View File
@@ -6,6 +6,11 @@
"module": "preserve",
"declaration": true,
"moduleResolution": "bundler",
"lib": [
"es2022",
"dom",
"dom.iterable"
],
"customConditions": [
"development"
]
+4 -4
View File
@@ -1,4 +1,4 @@
configured_endpoints: 39
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-e4b6496e5f2c68fa8b3ea1b88e40041eaf5ce2652001344df80bf130675d1766.yml
openapi_spec_hash: df474311dc9e4a89cd483bd8b8d971d8
config_hash: eab3723c4c2232a6ba1821151259d6da
configured_endpoints: 41
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-6d8e9dfd438cac63fc7d689ea29adfff81ff8880c2d8e1e10fc36f375a721594.yml
openapi_spec_hash: 7ac6028dd5957c67a98d91e790863c80
config_hash: fb625e876313a9f8f31532348fa91f59
+14 -1
View File
@@ -70,6 +70,16 @@ Methods:
- <code title="get /config">client.Config.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ConfigService.Get">Get</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Config">Config</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
# Command
Response Types:
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Command">Command</a>
Methods:
- <code title="get /command">client.Command.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#CommandService.List">List</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Command">Command</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
# Session
Params Types:
@@ -105,6 +115,8 @@ Response Types:
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolStatePending">ToolStatePending</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolStateRunning">ToolStateRunning</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#UserMessage">UserMessage</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionChatResponse">SessionChatResponse</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionCommandResponse">SessionCommandResponse</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionMessageResponse">SessionMessageResponse</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionMessagesResponse">SessionMessagesResponse</a>
@@ -115,8 +127,9 @@ Methods:
- <code title="get /session">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.List">List</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="delete /session/{id}">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Delete">Delete</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /session/{id}/abort">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Abort">Abort</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /session/{id}/message">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Chat">Chat</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionChatParams">SessionChatParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AssistantMessage">AssistantMessage</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /session/{id}/message">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Chat">Chat</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionChatParams">SessionChatParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionChatResponse">SessionChatResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="get /session/{id}/children">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Children">Children</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /session/{id}/command">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Command">Command</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionCommandParams">SessionCommandParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionCommandResponse">SessionCommandResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="get /session/{id}">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Get">Get</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /session/{id}/init">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Init">Init</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionInitParams">SessionInitParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="get /session/{id}/message/{messageID}">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Message">Message</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, messageID <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionMessageResponse">SessionMessageResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
+2
View File
@@ -21,6 +21,7 @@ type Client struct {
Find *FindService
File *FileService
Config *ConfigService
Command *CommandService
Session *SessionService
Tui *TuiService
}
@@ -49,6 +50,7 @@ func NewClient(opts ...option.RequestOption) (r *Client) {
r.Find = NewFindService(opts...)
r.File = NewFileService(opts...)
r.Config = NewConfigService(opts...)
r.Command = NewCommandService(opts...)
r.Session = NewSessionService(opts...)
r.Tui = NewTuiService(opts...)
+67
View File
@@ -0,0 +1,67 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
package opencode
import (
"context"
"net/http"
"github.com/sst/opencode-sdk-go/internal/apijson"
"github.com/sst/opencode-sdk-go/internal/requestconfig"
"github.com/sst/opencode-sdk-go/option"
)
// CommandService contains methods and other services that help with interacting
// with the opencode API.
//
// Note, unlike clients, this service does not read variables from the environment
// automatically. You should not instantiate this service directly, and instead use
// the [NewCommandService] method instead.
type CommandService struct {
Options []option.RequestOption
}
// NewCommandService generates a new service that applies the given options to each
// request. These options are applied after the parent client's options (if there
// is one), and before any request-specific options.
func NewCommandService(opts ...option.RequestOption) (r *CommandService) {
r = &CommandService{}
r.Options = opts
return
}
// List all commands
func (r *CommandService) List(ctx context.Context, opts ...option.RequestOption) (res *[]Command, err error) {
opts = append(r.Options[:], opts...)
path := "command"
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
return
}
type Command struct {
Name string `json:"name,required"`
Template string `json:"template,required"`
Agent string `json:"agent"`
Description string `json:"description"`
Model string `json:"model"`
JSON commandJSON `json:"-"`
}
// commandJSON contains the JSON metadata for the struct [Command]
type commandJSON struct {
Name apijson.Field
Template apijson.Field
Agent apijson.Field
Description apijson.Field
Model apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *Command) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r commandJSON) RawJSON() string {
return r.raw
}
+36
View File
@@ -0,0 +1,36 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
package opencode_test
import (
"context"
"errors"
"os"
"testing"
"github.com/sst/opencode-sdk-go"
"github.com/sst/opencode-sdk-go/internal/testutil"
"github.com/sst/opencode-sdk-go/option"
)
func TestCommandList(t *testing.T) {
t.Skip("skipped: tests are disabled for the time being")
baseURL := "http://localhost:4010"
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
baseURL = envURL
}
if !testutil.CheckTestServer(t, baseURL) {
return
}
client := opencode.NewClient(
option.WithBaseURL(baseURL),
)
_, err := client.Command.List(context.TODO())
if err != nil {
var apierr *opencode.Error
if errors.As(err, &apierr) {
t.Log(string(apierr.DumpRequest(true)))
}
t.Fatalf("err should be nil: %s", err.Error())
}
}
+35 -2
View File
@@ -49,7 +49,8 @@ type Config struct {
// automatically
Autoshare bool `json:"autoshare"`
// Automatically update to the latest version
Autoupdate bool `json:"autoupdate"`
Autoupdate bool `json:"autoupdate"`
Command map[string]ConfigCommand `json:"command"`
// Disable providers that are loaded automatically
DisabledProviders []string `json:"disabled_providers"`
Experimental ConfigExperimental `json:"experimental"`
@@ -79,7 +80,8 @@ type Config struct {
SmallModel string `json:"small_model"`
Snapshot bool `json:"snapshot"`
// Theme name to use for the interface
Theme string `json:"theme"`
Theme string `json:"theme"`
Tools map[string]bool `json:"tools"`
// TUI specific settings
Tui ConfigTui `json:"tui"`
// Custom username to display in conversations instead of system username
@@ -93,6 +95,7 @@ type configJSON struct {
Agent apijson.Field
Autoshare apijson.Field
Autoupdate apijson.Field
Command apijson.Field
DisabledProviders apijson.Field
Experimental apijson.Field
Formatter apijson.Field
@@ -110,6 +113,7 @@ type configJSON struct {
SmallModel apijson.Field
Snapshot apijson.Field
Theme apijson.Field
Tools apijson.Field
Tui apijson.Field
Username apijson.Field
raw string
@@ -662,6 +666,32 @@ func (r ConfigAgentPlanPermissionWebfetch) IsKnown() bool {
return false
}
type ConfigCommand struct {
Template string `json:"template,required"`
Agent string `json:"agent"`
Description string `json:"description"`
Model string `json:"model"`
JSON configCommandJSON `json:"-"`
}
// configCommandJSON contains the JSON metadata for the struct [ConfigCommand]
type configCommandJSON struct {
Template apijson.Field
Agent apijson.Field
Description apijson.Field
Model apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *ConfigCommand) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r configCommandJSON) RawJSON() string {
return r.raw
}
type ConfigExperimental struct {
Hook ConfigExperimentalHook `json:"hook"`
JSON configExperimentalJSON `json:"-"`
@@ -1760,6 +1790,8 @@ type KeybindsConfig struct {
SessionNew string `json:"session_new,required"`
// Share current session
SessionShare string `json:"session_share,required"`
// Show session timeline
SessionTimeline string `json:"session_timeline,required"`
// Unshare current session
SessionUnshare string `json:"session_unshare,required"`
// @deprecated use agent_cycle. Next agent
@@ -1821,6 +1853,7 @@ type keybindsConfigJSON struct {
SessionList apijson.Field
SessionNew apijson.Field
SessionShare apijson.Field
SessionTimeline apijson.Field
SessionUnshare apijson.Field
SwitchAgent apijson.Field
SwitchAgentReverse apijson.Field
+1
View File
@@ -41,6 +41,7 @@ func (r *EventService) ListStreaming(ctx context.Context, opts ...option.Request
err error
)
opts = append(r.Options[:], opts...)
opts = append([]option.RequestOption{option.WithHeader("Accept", "text/event-stream")}, opts...)
path := "event"
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &raw, opts...)
return ssestream.NewStream[EventListResponse](ssestream.NewDecoder(raw), err)
+71 -1
View File
@@ -91,7 +91,7 @@ func (r *SessionService) Abort(ctx context.Context, id string, opts ...option.Re
}
// Create and send a new message to a session
func (r *SessionService) Chat(ctx context.Context, id string, body SessionChatParams, opts ...option.RequestOption) (res *AssistantMessage, err error) {
func (r *SessionService) Chat(ctx context.Context, id string, body SessionChatParams, opts ...option.RequestOption) (res *SessionChatResponse, err error) {
opts = append(r.Options[:], opts...)
if id == "" {
err = errors.New("missing required id parameter")
@@ -114,6 +114,18 @@ func (r *SessionService) Children(ctx context.Context, id string, opts ...option
return
}
// Send a new command to a session
func (r *SessionService) Command(ctx context.Context, id string, body SessionCommandParams, opts ...option.RequestOption) (res *SessionCommandResponse, err error) {
opts = append(r.Options[:], opts...)
if id == "" {
err = errors.New("missing required id parameter")
return
}
path := fmt.Sprintf("session/%s/command", id)
err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...)
return
}
// Get session
func (r *SessionService) Get(ctx context.Context, id string, opts ...option.RequestOption) (res *Session, err error) {
opts = append(r.Options[:], opts...)
@@ -2278,6 +2290,52 @@ func (r userMessageTimeJSON) RawJSON() string {
return r.raw
}
type SessionChatResponse struct {
Info AssistantMessage `json:"info,required"`
Parts []Part `json:"parts,required"`
JSON sessionChatResponseJSON `json:"-"`
}
// sessionChatResponseJSON contains the JSON metadata for the struct
// [SessionChatResponse]
type sessionChatResponseJSON struct {
Info apijson.Field
Parts apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *SessionChatResponse) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r sessionChatResponseJSON) RawJSON() string {
return r.raw
}
type SessionCommandResponse struct {
Info AssistantMessage `json:"info,required"`
Parts []Part `json:"parts,required"`
JSON sessionCommandResponseJSON `json:"-"`
}
// sessionCommandResponseJSON contains the JSON metadata for the struct
// [SessionCommandResponse]
type sessionCommandResponseJSON struct {
Info apijson.Field
Parts apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *SessionCommandResponse) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r sessionCommandResponseJSON) RawJSON() string {
return r.raw
}
type SessionMessageResponse struct {
Info Message `json:"info,required"`
Parts []Part `json:"parts,required"`
@@ -2396,6 +2454,18 @@ func (r SessionChatParamsPartsType) IsKnown() bool {
return false
}
type SessionCommandParams struct {
Arguments param.Field[string] `json:"arguments,required"`
Command param.Field[string] `json:"command,required"`
Agent param.Field[string] `json:"agent"`
MessageID param.Field[string] `json:"messageID"`
Model param.Field[string] `json:"model"`
}
func (r SessionCommandParams) MarshalJSON() (data []byte, err error) {
return apijson.MarshalRoot(r)
}
type SessionInitParams struct {
MessageID param.Field[string] `json:"messageID,required"`
ModelID param.Field[string] `json:"modelID,required"`
+32
View File
@@ -199,6 +199,38 @@ func TestSessionChildren(t *testing.T) {
}
}
func TestSessionCommandWithOptionalParams(t *testing.T) {
t.Skip("skipped: tests are disabled for the time being")
baseURL := "http://localhost:4010"
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
baseURL = envURL
}
if !testutil.CheckTestServer(t, baseURL) {
return
}
client := opencode.NewClient(
option.WithBaseURL(baseURL),
)
_, err := client.Session.Command(
context.TODO(),
"id",
opencode.SessionCommandParams{
Arguments: opencode.F("arguments"),
Command: opencode.F("command"),
Agent: opencode.F("agent"),
MessageID: opencode.F("msg"),
Model: opencode.F("model"),
},
)
if err != nil {
var apierr *opencode.Error
if errors.As(err, &apierr) {
t.Log(string(apierr.DumpRequest(true)))
}
t.Fatalf("err should be nil: %s", err.Error())
}
}
func TestSessionGet(t *testing.T) {
t.Skip("skipped: tests are disabled for the time being")
baseURL := "http://localhost:4010"
+12 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "0.5.5",
"version": "0.5.18",
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit"
@@ -10,6 +10,14 @@
".": {
"development": "./src/index.ts",
"import": "./dist/index.js"
},
"./client": {
"development": "./src/client.ts",
"import": "./dist/client.js"
},
"./server": {
"development": "./src/server.ts",
"import": "./dist/server.js"
}
},
"files": [
@@ -19,5 +27,8 @@
"typescript": "catalog:",
"@hey-api/openapi-ts": "0.80.1",
"@tsconfig/node22": "catalog:"
},
"dependencies": {
"@hey-api/openapi-ts": "0.80.1"
}
}
+11
View File
@@ -0,0 +1,11 @@
export * from "./gen/types.gen.js"
export { type Config as OpencodeClientConfig, OpencodeClient }
import { createClient } from "./gen/client/client.js"
import { type Config } from "./gen/client/types.js"
import { OpencodeClient } from "./gen/sdk.gen.js"
export function createOpencodeClient(config?: Config) {
const client = createClient(config)
return new OpencodeClient({ client })
}
+31
View File
@@ -39,6 +39,8 @@ import type {
SessionChatResponses,
SessionMessageData,
SessionMessageResponses,
SessionCommandData,
SessionCommandResponses,
SessionShellData,
SessionShellResponses,
SessionRevertData,
@@ -47,6 +49,8 @@ import type {
SessionUnrevertResponses,
PostSessionByIdPermissionsByPermissionIdData,
PostSessionByIdPermissionsByPermissionIdResponses,
CommandListData,
CommandListResponses,
ConfigProvidersData,
ConfigProvidersResponses,
FindTextData,
@@ -355,6 +359,20 @@ class Session extends _HeyApiClient {
})
}
/**
* Send a new command to a session
*/
public command<ThrowOnError extends boolean = false>(options: Options<SessionCommandData, ThrowOnError>) {
return (options.client ?? this._client).post<SessionCommandResponses, unknown, ThrowOnError>({
url: "/session/{id}/command",
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
})
}
/**
* Run a shell command
*/
@@ -394,6 +412,18 @@ class Session extends _HeyApiClient {
}
}
class Command extends _HeyApiClient {
/**
* List all commands
*/
public list<ThrowOnError extends boolean = false>(options?: Options<CommandListData, ThrowOnError>) {
return (options?.client ?? this._client).get<CommandListResponses, unknown, ThrowOnError>({
url: "/command",
...options,
})
}
}
class Find extends _HeyApiClient {
/**
* Find text in files
@@ -592,6 +622,7 @@ export class OpencodeClient extends _HeyApiClient {
app = new App({ client: this._client })
config = new Config({ client: this._client })
session = new Session({ client: this._client })
command = new Command({ client: this._client })
find = new Find({ client: this._client })
file = new File({ client: this._client })
tui = new Tui({ client: this._client })
+73 -1
View File
@@ -585,6 +585,14 @@ export type Config = {
*/
scroll_speed: number
}
command?: {
[key: string]: {
template: string
description?: string
agent?: string
model?: string
}
}
plugin?: Array<string>
snapshot?: boolean
/**
@@ -729,6 +737,9 @@ export type Config = {
}
webfetch?: "ask" | "allow" | "deny"
}
tools?: {
[key: string]: boolean
}
experimental?: {
hook?: {
file_edited?: {
@@ -794,6 +805,10 @@ export type KeybindsConfig = {
* List all sessions
*/
session_list: string
/**
* Show session timeline
*/
session_timeline: string
/**
* Share current session
*/
@@ -1103,6 +1118,14 @@ export type AgentPartInput = {
}
}
export type Command = {
name: string
description?: string
agent?: string
model?: string
template: string
}
export type Symbol = {
name: string
kind: number
@@ -1520,7 +1543,10 @@ export type SessionChatResponses = {
/**
* Created message
*/
200: AssistantMessage
200: {
info: AssistantMessage
parts: Array<Part>
}
}
export type SessionChatResponse = SessionChatResponses[keyof SessionChatResponses]
@@ -1553,6 +1579,36 @@ export type SessionMessageResponses = {
export type SessionMessageResponse = SessionMessageResponses[keyof SessionMessageResponses]
export type SessionCommandData = {
body?: {
messageID?: string
agent?: string
model?: string
arguments: string
command: string
}
path: {
/**
* Session ID
*/
id: string
}
query?: never
url: "/session/{id}/command"
}
export type SessionCommandResponses = {
/**
* Created message
*/
200: {
info: AssistantMessage
parts: Array<Part>
}
}
export type SessionCommandResponse = SessionCommandResponses[keyof SessionCommandResponses]
export type SessionShellData = {
body?: {
agent: string
@@ -1638,6 +1694,22 @@ export type PostSessionByIdPermissionsByPermissionIdResponses = {
export type PostSessionByIdPermissionsByPermissionIdResponse =
PostSessionByIdPermissionsByPermissionIdResponses[keyof PostSessionByIdPermissionsByPermissionIdResponses]
export type CommandListData = {
body?: never
path?: never
query?: never
url: "/command"
}
export type CommandListResponses = {
/**
* List of commands
*/
200: Array<Command>
}
export type CommandListResponse = CommandListResponses[keyof CommandListResponses]
export type ConfigProvidersData = {
body?: never
path?: never
+2 -35
View File
@@ -1,35 +1,2 @@
import { createClient } from "./gen/client/client.js"
import { type Config } from "./gen/client/types.js"
import { OpencodeClient } from "./gen/sdk.gen.js"
export * from "./gen/types.gen.js"
import { spawn } from "child_process"
export function createOpencodeClient(config?: Config) {
const client = createClient(config)
return new OpencodeClient({ client })
}
export type ServerConfig = {
host?: string
port?: number
}
export async function createOpencodeServer(config?: ServerConfig) {
config = Object.assign(
{
host: "127.0.0.1",
port: 4096,
},
config ?? {},
)
const proc = spawn(`opencode`, [`serve`, `--host=${config.host}`, `--port=${config.port}`])
const url = `http://${config.host}:${config.port}`
return {
url,
close() {
proc.kill()
},
}
}
export * from "./client.js"
export * from "./server.js"
+73
View File
@@ -0,0 +1,73 @@
import { spawn } from "node:child_process"
export type ServerConfig = {
hostname?: string
port?: number
signal?: AbortSignal
timeout?: number
}
export async function createOpencodeServer(config?: ServerConfig) {
config = Object.assign(
{
hostname: "127.0.0.1",
port: 4096,
timeout: 5000,
},
config ?? {},
)
const proc = spawn(`opencode`, [`serve`, `--hostname=${config.hostname}`, `--port=${config.port}`], {
signal: config.signal,
})
const url = await new Promise<string>((resolve, reject) => {
const id = setTimeout(() => {
reject(new Error(`Timeout waiting for server to start after ${config.timeout}ms`))
}, config.timeout)
let output = ""
proc.stdout?.on("data", (chunk) => {
output += chunk.toString()
const lines = output.split("\n")
for (const line of lines) {
if (line.startsWith("opencode server listening")) {
const match = line.match(/on\s+(https?:\/\/[^\s]+)/)
if (!match) {
throw new Error(`Failed to parse server url from output: ${line}`)
}
clearTimeout(id)
resolve(match[1])
return
}
}
})
proc.stderr?.on("data", (chunk) => {
output += chunk.toString()
})
proc.on("exit", (code) => {
clearTimeout(id)
let msg = `Server exited with code ${code}`
if (output.trim()) {
msg += `\nServer output: ${output}`
}
reject(new Error(msg))
})
proc.on("error", (error) => {
clearTimeout(id)
reject(error)
})
if (config.signal) {
config.signal.addEventListener("abort", () => {
clearTimeout(id)
reject(new Error("Aborted"))
})
}
})
return {
url,
close() {
proc.kill()
},
}
}
+7
View File
@@ -85,6 +85,12 @@ resources:
methods:
get: get /config
command:
models:
command: Command
methods:
list: get /command
session:
models:
session: Session
@@ -126,6 +132,7 @@ resources:
message: get /session/{id}/message/{messageID}
messages: get /session/{id}/message
chat: post /session/{id}/message
command: post /session/{id}/command
shell: post /session/{id}/shell
update: patch /session/{id}
revert: post /session/{id}/revert
+64 -1
View File
@@ -84,6 +84,10 @@ type SendPrompt = Prompt
type SendShell = struct {
Command string
}
type SendCommand = struct {
Command string
Args string
}
type SetEditorContentMsg struct {
Text string
}
@@ -183,6 +187,11 @@ func New(
slog.Debug("Loaded config", "config", configInfo)
customCommands, err := httpClient.Command.List(ctx)
if err != nil {
return nil, err
}
app := &App{
Info: appInfo,
Agents: agents,
@@ -194,7 +203,7 @@ func New(
AgentIndex: agentIndex,
Session: &opencode.Session{},
Messages: []Message{},
Commands: commands.LoadFromConfig(configInfo),
Commands: commands.LoadFromConfig(configInfo, *customCommands),
InitialModel: initialModel,
InitialPrompt: initialPrompt,
InitialAgent: initialAgent,
@@ -207,6 +216,9 @@ func New(
func (a *App) Keybind(commandName commands.CommandName) string {
command := a.Commands[commandName]
if len(command.Keybindings) == 0 {
return ""
}
kb := command.Keybindings[0]
key := kb.Key
if kb.RequiresLeader {
@@ -650,6 +662,25 @@ func (a *App) IsBusy() bool {
return false
}
func (a *App) HasAnimatingWork() bool {
for _, msg := range a.Messages {
switch casted := msg.Info.(type) {
case opencode.AssistantMessage:
if casted.Time.Completed == 0 {
return true
}
}
for _, p := range msg.Parts {
if tp, ok := p.(opencode.ToolPart); ok {
if tp.State.Status == opencode.ToolPartStateStatusPending {
return true
}
}
}
}
return false
}
func (a *App) SaveState() tea.Cmd {
return func() tea.Msg {
err := SaveState(a.StatePath, a.State)
@@ -771,6 +802,38 @@ func (a *App) SendPrompt(ctx context.Context, prompt Prompt) (*App, tea.Cmd) {
return a, tea.Batch(cmds...)
}
func (a *App) SendCommand(ctx context.Context, command string, args string) (*App, tea.Cmd) {
var cmds []tea.Cmd
if a.Session.ID == "" {
session, err := a.CreateSession(ctx)
if err != nil {
return a, toast.NewErrorToast(err.Error())
}
a.Session = session
cmds = append(cmds, util.CmdHandler(SessionCreatedMsg{Session: session}))
}
cmds = append(cmds, func() tea.Msg {
_, err := a.Client.Session.Command(
context.Background(),
a.Session.ID,
opencode.SessionCommandParams{
Command: opencode.F(command),
Arguments: opencode.F(args),
},
)
if err != nil {
slog.Error("Failed to execute command", "error", err)
return toast.NewErrorToast("Failed to execute command")
}
return nil
})
// The actual response will come through SSE
// For now, just return success
return a, tea.Batch(cmds...)
}
func (a *App) SendShell(ctx context.Context, command string) (*App, tea.Cmd) {
var cmds []tea.Cmd
if a.Session.ID == "" {
+48 -37
View File
@@ -31,6 +31,7 @@ type Command struct {
Description string
Keybindings []Keybinding
Trigger []string
Custom bool
}
func (c Command) Keys() []string {
@@ -96,6 +97,7 @@ func (r CommandRegistry) Sorted() []Command {
})
return commands
}
func (r CommandRegistry) Matches(msg tea.KeyPressMsg, leader bool) []Command {
var matched []Command
for _, command := range r.Sorted() {
@@ -107,7 +109,6 @@ func (r CommandRegistry) Matches(msg tea.KeyPressMsg, leader bool) []Command {
}
const (
SessionChildCycleCommand CommandName = "session_child_cycle"
SessionChildCycleReverseCommand CommandName = "session_child_cycle_reverse"
ModelCycleRecentReverseCommand CommandName = "model_cycle_recent_reverse"
@@ -119,40 +120,40 @@ const (
EditorOpenCommand CommandName = "editor_open"
SessionNewCommand CommandName = "session_new"
SessionListCommand CommandName = "session_list"
SessionNavigationCommand CommandName = "session_navigation"
SessionTimelineCommand CommandName = "session_timeline"
SessionShareCommand CommandName = "session_share"
SessionUnshareCommand CommandName = "session_unshare"
SessionInterruptCommand CommandName = "session_interrupt"
SessionCompactCommand CommandName = "session_compact"
SessionExportCommand CommandName = "session_export"
ToolDetailsCommand CommandName = "tool_details"
ThinkingBlocksCommand CommandName = "thinking_blocks"
ModelListCommand CommandName = "model_list"
AgentListCommand CommandName = "agent_list"
ModelCycleRecentCommand CommandName = "model_cycle_recent"
ThemeListCommand CommandName = "theme_list"
FileListCommand CommandName = "file_list"
FileCloseCommand CommandName = "file_close"
FileSearchCommand CommandName = "file_search"
FileDiffToggleCommand CommandName = "file_diff_toggle"
ProjectInitCommand CommandName = "project_init"
InputClearCommand CommandName = "input_clear"
InputPasteCommand CommandName = "input_paste"
InputSubmitCommand CommandName = "input_submit"
InputNewlineCommand CommandName = "input_newline"
MessagesPageUpCommand CommandName = "messages_page_up"
MessagesPageDownCommand CommandName = "messages_page_down"
MessagesHalfPageUpCommand CommandName = "messages_half_page_up"
MessagesHalfPageDownCommand CommandName = "messages_half_page_down"
MessagesPreviousCommand CommandName = "messages_previous"
MessagesNextCommand CommandName = "messages_next"
MessagesFirstCommand CommandName = "messages_first"
MessagesLastCommand CommandName = "messages_last"
MessagesLayoutToggleCommand CommandName = "messages_layout_toggle"
MessagesCopyCommand CommandName = "messages_copy"
MessagesUndoCommand CommandName = "messages_undo"
MessagesRedoCommand CommandName = "messages_redo"
AppExitCommand CommandName = "app_exit"
SessionInterruptCommand CommandName = "session_interrupt"
SessionCompactCommand CommandName = "session_compact"
SessionExportCommand CommandName = "session_export"
ToolDetailsCommand CommandName = "tool_details"
ThinkingBlocksCommand CommandName = "thinking_blocks"
ModelListCommand CommandName = "model_list"
AgentListCommand CommandName = "agent_list"
ModelCycleRecentCommand CommandName = "model_cycle_recent"
ThemeListCommand CommandName = "theme_list"
FileListCommand CommandName = "file_list"
FileCloseCommand CommandName = "file_close"
FileSearchCommand CommandName = "file_search"
FileDiffToggleCommand CommandName = "file_diff_toggle"
ProjectInitCommand CommandName = "project_init"
InputClearCommand CommandName = "input_clear"
InputPasteCommand CommandName = "input_paste"
InputSubmitCommand CommandName = "input_submit"
InputNewlineCommand CommandName = "input_newline"
MessagesPageUpCommand CommandName = "messages_page_up"
MessagesPageDownCommand CommandName = "messages_page_down"
MessagesHalfPageUpCommand CommandName = "messages_half_page_up"
MessagesHalfPageDownCommand CommandName = "messages_half_page_down"
MessagesPreviousCommand CommandName = "messages_previous"
MessagesNextCommand CommandName = "messages_next"
MessagesFirstCommand CommandName = "messages_first"
MessagesLastCommand CommandName = "messages_last"
MessagesLayoutToggleCommand CommandName = "messages_layout_toggle"
MessagesCopyCommand CommandName = "messages_copy"
MessagesUndoCommand CommandName = "messages_undo"
MessagesRedoCommand CommandName = "messages_redo"
AppExitCommand CommandName = "app_exit"
)
func (k Command) Matches(msg tea.KeyPressMsg, leader bool) bool {
@@ -183,7 +184,7 @@ func parseBindings(bindings ...string) []Keybinding {
return parsedBindings
}
func LoadFromConfig(config *opencode.Config) CommandRegistry {
func LoadFromConfig(config *opencode.Config, customCommands []opencode.Command) CommandRegistry {
defaults := []Command{
{
Name: AppHelpCommand,
@@ -216,10 +217,10 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry {
Trigger: []string{"sessions", "resume", "continue"},
},
{
Name: SessionNavigationCommand,
Description: "jump to message",
Name: SessionTimelineCommand,
Description: "show session timeline",
Keybindings: parseBindings("<leader>g"),
Trigger: []string{"jump", "goto", "navigate"},
Trigger: []string{"timeline", "history", "goto"},
},
{
Name: SessionShareCommand,
@@ -401,6 +402,16 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry {
}
registry[command.Name] = command
}
for _, command := range customCommands {
registry[CommandName(command.Name)] = Command{
Name: CommandName(command.Name),
Description: command.Description,
Trigger: []string{command.Name},
Keybindings: []Keybinding{},
Custom: true,
}
}
slog.Info("Loaded commands", "commands", registry)
return registry
}
@@ -224,10 +224,17 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case dialog.CompletionSelectedMsg:
switch msg.Item.ProviderID {
case "commands":
commandName := strings.TrimPrefix(msg.Item.Value, "/")
command := msg.Item.RawData.(commands.Command)
if command.Custom {
m.SetValue("/" + command.PrimaryTrigger() + " ")
return m, nil
}
updated, cmd := m.Clear()
m = updated.(*editorComponent)
cmds = append(cmds, cmd)
commandName := strings.TrimPrefix(msg.Item.Value, "/")
cmds = append(cmds, util.CmdHandler(commands.ExecuteCommandMsg(m.app.Commands[commands.CommandName(commandName)])))
return m, tea.Batch(cmds...)
case "files":
@@ -339,6 +346,7 @@ func (m *editorComponent) Content() string {
t := theme.CurrentTheme()
base := styles.NewStyle().Foreground(t.Text()).Background(t.Background()).Render
muted := styles.NewStyle().Foreground(t.TextMuted()).Background(t.Background()).Render
promptStyle := styles.NewStyle().Foreground(t.Primary()).
Padding(0, 0, 0, 1).
Bold(true)
@@ -480,6 +488,25 @@ func (m *editorComponent) Submit() (tea.Model, tea.Cmd) {
}
var cmds []tea.Cmd
if strings.HasPrefix(value, "/") {
value = value[1:]
commandName := strings.Split(value, " ")[0]
command := m.app.Commands[commands.CommandName(commandName)]
if command.Custom {
args := strings.TrimPrefix(value, command.PrimaryTrigger()+" ")
cmds = append(
cmds,
util.CmdHandler(app.SendCommand{Command: string(command.Name), Args: args}),
)
updated, cmd := m.Clear()
m = updated.(*editorComponent)
cmds = append(cmds, cmd)
return m, tea.Batch(cmds...)
}
}
attachments := m.textarea.GetAttachments()
prompt := app.Prompt{Text: value, Attachments: attachments}
@@ -213,6 +213,7 @@ func renderText(
extra string,
isThinking bool,
isQueued bool,
shimmer bool,
fileParts []opencode.FilePart,
agentParts []opencode.AgentPart,
toolCalls ...opencode.ToolPart,
@@ -234,7 +235,18 @@ func renderText(
}
content = util.ToMarkdown(text, width, backgroundColor)
if isThinking {
content = styles.NewStyle().Background(backgroundColor).Foreground(t.TextMuted()).Render("Thinking") + "\n\n" + content
var label string
if shimmer {
label = util.Shimmer("Thinking...", backgroundColor, t.TextMuted(), t.Accent())
} else {
label = styles.NewStyle().Background(backgroundColor).Foreground(t.TextMuted()).Render("Thinking...")
}
label = styles.NewStyle().Background(backgroundColor).Width(width - 6).Render(label)
content = label + "\n\n" + content
} else if strings.TrimSpace(text) == "Generating..." {
label := util.Shimmer(text, backgroundColor, t.TextMuted(), t.Text())
label = styles.NewStyle().Background(backgroundColor).Width(width - 6).Render(label)
content = label
}
case opencode.UserMessage:
ts = time.UnixMilli(int64(casted.Time.Created))
@@ -659,10 +671,22 @@ func renderToolDetails(
body = strings.Join(steps, "\n")
body += "\n\n"
body += baseStyle(app.Keybind(commands.SessionChildCycleCommand)) +
mutedStyle(", ") +
baseStyle(app.Keybind(commands.SessionChildCycleReverseCommand)) +
mutedStyle(" navigate child sessions")
// Build navigation hint with proper spacing
cycleKeybind := app.Keybind(commands.SessionChildCycleCommand)
cycleReverseKeybind := app.Keybind(commands.SessionChildCycleReverseCommand)
var navParts []string
if cycleKeybind != "" {
navParts = append(navParts, baseStyle(cycleKeybind))
}
if cycleReverseKeybind != "" {
navParts = append(navParts, baseStyle(cycleReverseKeybind))
}
if len(navParts) > 0 {
body += strings.Join(navParts, mutedStyle(", ")) + mutedStyle(" navigate child sessions")
}
}
body = defaultStyle(body)
default:
@@ -779,7 +803,9 @@ func renderToolTitle(
) string {
if toolCall.State.Status == opencode.ToolPartStateStatusPending {
title := renderToolAction(toolCall.Tool)
return styles.NewStyle().Width(width - 6).Render(title)
t := theme.CurrentTheme()
shiny := util.Shimmer(title, t.BackgroundPanel(), t.TextMuted(), t.Accent())
return styles.NewStyle().Background(t.BackgroundPanel()).Width(width - 6).Render(shiny)
}
toolArgs := ""
@@ -895,7 +921,9 @@ func renderArgs(args *map[string]any, titleKey string) string {
continue
}
if key == "filePath" || key == "path" {
value = util.Relative(value.(string))
if strValue, ok := value.(string); ok {
value = util.Relative(strValue)
}
}
if key == titleKey {
title = fmt.Sprintf("%s", value)
@@ -8,6 +8,7 @@ import (
"sort"
"strconv"
"strings"
"time"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
@@ -59,6 +60,7 @@ type messagesComponent struct {
lineCount int
selection *selection
messagePositions map[string]int // map message ID to line position
animating bool
}
type selection struct {
@@ -99,6 +101,7 @@ func (s selection) coords(offset int) *selection {
type ToggleToolDetailsMsg struct{}
type ToggleThinkingBlocksMsg struct{}
type shimmerTickMsg struct{}
func (m *messagesComponent) Init() tea.Cmd {
return tea.Batch(m.viewport.Init())
@@ -107,6 +110,15 @@ func (m *messagesComponent) Init() tea.Cmd {
func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
case shimmerTickMsg:
if !m.app.HasAnimatingWork() {
m.animating = false
return m, nil
}
return m, tea.Sequence(
m.renderView(),
tea.Tick(90*time.Millisecond, func(t time.Time) tea.Msg { return shimmerTickMsg{} }),
)
case tea.MouseClickMsg:
slog.Info("mouse", "x", msg.X, "y", msg.Y, "offset", m.viewport.YOffset)
y := msg.Y + m.viewport.YOffset
@@ -162,6 +174,10 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.viewport.GotoBottom()
m.tail = true
return m, nil
case app.SendCommand:
m.viewport.GotoBottom()
m.tail = true
return m, nil
case dialog.ThemeSelectedMsg:
m.cache.Clear()
m.loading = true
@@ -270,6 +286,12 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.dirty {
cmds = append(cmds, m.renderView())
}
// Start shimmer ticks if any assistant/tool is in-flight
if !m.animating && m.app.HasAnimatingWork() {
m.animating = true
cmds = append(cmds, tea.Tick(90*time.Millisecond, func(t time.Time) tea.Msg { return shimmerTickMsg{} }))
}
}
m.tail = m.viewport.AtBottom()
@@ -318,6 +340,25 @@ func (m *messagesComponent) renderView() tea.Cmd {
width := m.width // always use full width
// Find the last streaming ReasoningPart to only shimmer that one
lastStreamingReasoningID := ""
if m.showThinkingBlocks {
for mi := len(m.app.Messages) - 1; mi >= 0 && lastStreamingReasoningID == ""; mi-- {
if _, ok := m.app.Messages[mi].Info.(opencode.AssistantMessage); !ok {
continue
}
parts := m.app.Messages[mi].Parts
for pi := len(parts) - 1; pi >= 0; pi-- {
if rp, ok := parts[pi].(opencode.ReasoningPart); ok {
if strings.TrimSpace(rp.Text) != "" && rp.Time.End == 0 {
lastStreamingReasoningID = rp.ID
break
}
}
}
}
}
reverted := false
revertedMessageCount := 0
revertedToolCount := 0
@@ -419,6 +460,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
files,
false,
isQueued,
false,
fileParts,
agentParts,
)
@@ -495,6 +537,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
"",
false,
false,
false,
[]opencode.FilePart{},
[]opencode.AgentPart{},
toolCallParts...,
@@ -512,6 +555,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
"",
false,
false,
false,
[]opencode.FilePart{},
[]opencode.AgentPart{},
toolCallParts...,
@@ -582,6 +626,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
}
if part.Text != "" {
text := part.Text
shimmer := part.Time.End == 0 && part.ID == lastStreamingReasoningID
content = renderText(
m.app,
message.Info,
@@ -592,6 +637,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
"",
true,
false,
shimmer,
[]opencode.FilePart{},
[]opencode.AgentPart{},
)
@@ -626,6 +672,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
"",
false,
false,
false,
[]opencode.FilePart{},
[]opencode.AgentPart{},
)
@@ -728,16 +775,18 @@ func (m *messagesComponent) renderView() tea.Cmd {
} else {
for _, part := range response.Parts {
if part.CallID == m.app.CurrentPermission.CallID {
content := renderToolDetails(
m.app,
part.AsUnion().(opencode.ToolPart),
m.app.CurrentPermission,
width,
)
if content != "" {
partCount++
lineCount += lipgloss.Height(content) + 1
blocks = append(blocks, content)
if toolPart, ok := part.AsUnion().(opencode.ToolPart); ok {
content := renderToolDetails(
m.app,
toolPart,
m.app.CurrentPermission,
width,
)
if content != "" {
partCount++
lineCount += lipgloss.Height(content) + 1
blocks = append(blocks, content)
}
}
}
}
@@ -18,8 +18,8 @@ import (
"github.com/sst/opencode/internal/util"
)
// NavigationDialog interface for the session navigation dialog
type NavigationDialog interface {
// TimelineDialog interface for the session timeline dialog
type TimelineDialog interface {
layout.Modal
}
@@ -34,8 +34,8 @@ type RestoreToMessageMsg struct {
Index int
}
// navigationItem represents a user message in the navigation list
type navigationItem struct {
// timelineItem represents a user message in the timeline list
type timelineItem struct {
messageID string
content string
timestamp time.Time
@@ -43,25 +43,38 @@ type navigationItem struct {
toolCount int // Number of tools used in this message
}
func (n navigationItem) Render(
func (n timelineItem) Render(
selected bool,
width int,
isFirstInViewport bool,
baseStyle styles.Style,
isCurrent bool,
) string {
t := theme.CurrentTheme()
infoStyle := baseStyle.Background(t.BackgroundPanel()).Foreground(t.Info()).Render
textStyle := baseStyle.Background(t.BackgroundPanel()).Foreground(t.Text()).Render
// Add dot after timestamp if this is the current message - only apply color when not selected
var dot string
var dotVisualLen int
if isCurrent {
if selected {
dot = "● "
} else {
dot = lipgloss.NewStyle().Foreground(t.Success()).Render("● ")
}
dotVisualLen = 2 // "● " is 2 characters wide
}
// Format timestamp - only apply color when not selected
var timeStr string
var timeVisualLen int
if selected {
timeStr = n.timestamp.Format("15:04") + " "
timeVisualLen = lipgloss.Width(timeStr)
timeStr = n.timestamp.Format("15:04") + " " + dot
timeVisualLen = lipgloss.Width(n.timestamp.Format("15:04")+" ") + dotVisualLen
} else {
timeStr = infoStyle(n.timestamp.Format("15:04") + " ")
timeVisualLen = lipgloss.Width(timeStr)
timeStr = infoStyle(n.timestamp.Format("15:04")+" ") + dot
timeVisualLen = lipgloss.Width(n.timestamp.Format("15:04")+" ") + dotVisualLen
}
// Tool count display (fixed width for alignment) - only apply color when not selected
@@ -78,7 +91,7 @@ func (n navigationItem) Render(
}
// Calculate available space for content
// Reserve space for: timestamp + space + toolInfo + padding + some buffer
// Reserve space for: timestamp + dot + space + toolInfo + padding + some buffer
reservedSpace := timeVisualLen + 1 + toolInfoVisualLen + 4
contentWidth := max(width-reservedSpace, 8)
@@ -135,23 +148,23 @@ func (n navigationItem) Render(
return itemStyle.Render(text)
}
func (n navigationItem) Selectable() bool {
func (n timelineItem) Selectable() bool {
return true
}
type navigationDialog struct {
type timelineDialog struct {
width int
height int
modal *modal.Modal
list list.List[navigationItem]
list list.List[timelineItem]
app *app.App
}
func (n *navigationDialog) Init() tea.Cmd {
func (n *timelineDialog) Init() tea.Cmd {
return nil
}
func (n *navigationDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (n *timelineDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
n.width = msg.Width
@@ -163,7 +176,7 @@ func (n *navigationDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// Handle navigation and immediately scroll to selected message
var cmd tea.Cmd
listModel, cmd := n.list.Update(msg)
n.list = listModel.(list.List[navigationItem])
n.list = listModel.(list.List[timelineItem])
// Get the newly selected item and scroll to it immediately
if item, idx := n.list.GetSelectedItem(); idx >= 0 {
@@ -191,11 +204,11 @@ func (n *navigationDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
listModel, cmd := n.list.Update(msg)
n.list = listModel.(list.List[navigationItem])
n.list = listModel.(list.List[timelineItem])
return n, cmd
}
func (n *navigationDialog) Render(background string) string {
func (n *timelineDialog) Render(background string) string {
listView := n.list.View()
t := theme.CurrentTheme()
@@ -229,7 +242,7 @@ func (n *navigationDialog) Render(background string) string {
return n.modal.Render(content, background)
}
func (n *navigationDialog) Close() tea.Cmd {
func (n *timelineDialog) Close() tea.Cmd {
return nil
}
@@ -268,9 +281,9 @@ func countToolsInResponse(messages []app.Message, userMessageIndex int) int {
return count
}
// NewNavigationDialog creates a new session navigation dialog
func NewNavigationDialog(app *app.App) NavigationDialog {
var items []navigationItem
// NewTimelineDialog creates a new session timeline dialog
func NewTimelineDialog(app *app.App) TimelineDialog { // renamed from NewNavigationDialog
var items []timelineItem
// Filter to only user messages and extract relevant info
for i, message := range app.Messages {
@@ -278,7 +291,7 @@ func NewNavigationDialog(app *app.App) NavigationDialog {
preview := extractMessagePreview(message.Parts)
toolCount := countToolsInResponse(app.Messages, i)
items = append(items, navigationItem{
items = append(items, timelineItem{
messageID: userMsg.ID,
content: preview,
timestamp: time.UnixMilli(int64(userMsg.Time.Created)),
@@ -290,25 +303,50 @@ func NewNavigationDialog(app *app.App) NavigationDialog {
listComponent := list.NewListComponent(
list.WithItems(items),
list.WithMaxVisibleHeight[navigationItem](12),
list.WithFallbackMessage[navigationItem]("No user messages in this session"),
list.WithAlphaNumericKeys[navigationItem](true),
list.WithMaxVisibleHeight[timelineItem](12),
list.WithFallbackMessage[timelineItem]("No user messages in this session"),
list.WithAlphaNumericKeys[timelineItem](true),
list.WithRenderFunc(
func(item navigationItem, selected bool, width int, baseStyle styles.Style) string {
return item.Render(selected, width, false, baseStyle)
func(item timelineItem, selected bool, width int, baseStyle styles.Style) string {
// Determine if this item is the current message for the session
isCurrent := false
if app.Session.Revert.MessageID != "" {
// When reverted, Session.Revert.MessageID contains the NEXT user message ID
// So we need to find the previous user message to highlight the correct one
for i, navItem := range items {
if navItem.messageID == app.Session.Revert.MessageID && i > 0 {
// Found the next message, so the previous one is current
isCurrent = item.messageID == items[i-1].messageID
break
}
}
} else if len(app.Messages) > 0 {
// If not reverted, highlight the last user message
lastUserMsgID := ""
for i := len(app.Messages) - 1; i >= 0; i-- {
if userMsg, ok := app.Messages[i].Info.(opencode.UserMessage); ok {
lastUserMsgID = userMsg.ID
break
}
}
isCurrent = item.messageID == lastUserMsgID
}
// Only show the dot if undo/redo/restore is available
showDot := app.Session.Revert.MessageID != ""
return item.Render(selected, width, false, baseStyle, isCurrent && showDot)
},
),
list.WithSelectableFunc(func(item navigationItem) bool {
list.WithSelectableFunc(func(item timelineItem) bool {
return true
}),
)
listComponent.SetMaxWidth(layout.Current.Container.Width - 12)
return &navigationDialog{
return &timelineDialog{
list: listComponent,
app: app,
modal: modal.New(
modal.WithTitle("Jump to Message"),
modal.WithTitle("Session Timeline"),
modal.WithMaxWidth(layout.Current.Container.Width-8),
),
}
+22 -4
View File
@@ -408,6 +408,24 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
a.app, cmd = a.app.SendPrompt(context.Background(), msg)
cmds = append(cmds, cmd)
}
case app.SendCommand:
// If we're in a child session, switch back to parent before sending prompt
if a.app.Session.ParentID != "" {
parentSession, err := a.app.Client.Session.Get(context.Background(), a.app.Session.ParentID)
if err != nil {
slog.Error("Failed to get parent session", "error", err)
return a, toast.NewErrorToast("Failed to get parent session")
}
a.app.Session = parentSession
a.app, cmd = a.app.SendCommand(context.Background(), msg.Command, msg.Args)
cmds = append(cmds, tea.Sequence(
util.CmdHandler(app.SessionSelectedMsg(parentSession)),
cmd,
))
} else {
a.app, cmd = a.app.SendCommand(context.Background(), msg.Command, msg.Args)
cmds = append(cmds, cmd)
}
case app.SendShell:
// If we're in a child session, switch back to parent before sending prompt
if a.app.Session.ParentID != "" {
@@ -728,8 +746,8 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case "/tui/open-sessions":
sessionDialog := dialog.NewSessionDialog(a.app)
a.modal = sessionDialog
case "/tui/open-navigation":
navigationDialog := dialog.NewNavigationDialog(a.app)
case "/tui/open-timeline":
navigationDialog := dialog.NewTimelineDialog(a.app)
a.modal = navigationDialog
case "/tui/open-themes":
themeDialog := dialog.NewThemeDialog()
@@ -1146,11 +1164,11 @@ func (a Model) executeCommand(command commands.Command) (tea.Model, tea.Cmd) {
case commands.SessionListCommand:
sessionDialog := dialog.NewSessionDialog(a.app)
a.modal = sessionDialog
case commands.SessionNavigationCommand:
case commands.SessionTimelineCommand:
if a.app.Session.ID == "" {
return a, toast.NewErrorToast("No active session")
}
navigationDialog := dialog.NewNavigationDialog(a.app)
navigationDialog := dialog.NewTimelineDialog(a.app)
a.modal = navigationDialog
case commands.SessionShareCommand:
if a.app.Session.ID == "" {
+21 -10
View File
@@ -2,12 +2,31 @@ package util
import (
"context"
"fmt"
"log/slog"
"reflect"
"sync"
opencode "github.com/sst/opencode-sdk-go"
)
func sanitizeValue(val any) any {
if val == nil {
return nil
}
if err, ok := val.(error); ok {
return err.Error()
}
v := reflect.ValueOf(val)
if v.Kind() == reflect.Interface && !v.IsNil() {
return fmt.Sprintf("%T", val)
}
return val
}
type APILogHandler struct {
client *opencode.Client
service string
@@ -67,21 +86,13 @@ func (h *APILogHandler) Handle(ctx context.Context, r slog.Record) error {
h.mu.Lock()
for _, attr := range h.attrs {
val := attr.Value.Any()
if err, ok := val.(error); ok {
extra[attr.Key] = err.Error()
} else {
extra[attr.Key] = val
}
extra[attr.Key] = sanitizeValue(val)
}
h.mu.Unlock()
r.Attrs(func(attr slog.Attr) bool {
val := attr.Value.Any()
if err, ok := val.(error); ok {
extra[attr.Key] = err.Error()
} else {
extra[attr.Key] = val
}
extra[attr.Key] = sanitizeValue(val)
return true
})
+143
View File
@@ -0,0 +1,143 @@
package util
import (
"math"
"os"
"strings"
"time"
"github.com/charmbracelet/lipgloss/v2"
"github.com/charmbracelet/lipgloss/v2/compat"
"github.com/sst/opencode/internal/styles"
)
var shimmerStart = time.Now()
// Shimmer renders text with a moving foreground highlight.
// bg is the background color, dim is the base text color, bright is the highlight color.
func Shimmer(s string, bg compat.AdaptiveColor, _ compat.AdaptiveColor, _ compat.AdaptiveColor) string {
if s == "" {
return ""
}
runes := []rune(s)
n := len(runes)
if n == 0 {
return s
}
pad := 10
period := float64(n + pad*2)
sweep := 2.5
elapsed := time.Since(shimmerStart).Seconds()
pos := (math.Mod(elapsed, sweep) / sweep) * period
half := 4.0
type seg struct {
useHex bool
hex string
bold bool
faint bool
text string
}
var segs []seg
useHex := hasTrueColor()
for i, r := range runes {
ip := float64(i + pad)
dist := math.Abs(ip - pos)
t := 0.0
if dist <= half {
x := math.Pi * (dist / half)
t = 0.5 * (1.0 + math.Cos(x))
}
// Cosine brightness: base + amp*t (quantized for grouping)
base := 0.55
amp := 0.45
brightness := base
if t > 0 {
brightness = base + amp*t
}
lvl := int(math.Round(brightness * 255.0))
if !useHex {
step := 24 // ~11 steps across range for non-truecolor
lvl = int(math.Round(float64(lvl)/float64(step))) * step
}
bold := lvl >= 208
faint := lvl <= 128
// truecolor if possible; else fallback to modifiers only
hex := ""
if useHex {
if lvl < 0 {
lvl = 0
}
if lvl > 255 {
lvl = 255
}
hex = rgbHex(lvl, lvl, lvl)
}
if len(segs) == 0 {
segs = append(segs, seg{useHex: useHex, hex: hex, bold: bold, faint: faint, text: string(r)})
} else {
last := &segs[len(segs)-1]
if last.useHex == useHex && last.hex == hex && last.bold == bold && last.faint == faint {
last.text += string(r)
} else {
segs = append(segs, seg{useHex: useHex, hex: hex, bold: bold, faint: faint, text: string(r)})
}
}
}
var b strings.Builder
for _, g := range segs {
st := styles.NewStyle().Background(bg)
if g.useHex && g.hex != "" {
c := compat.AdaptiveColor{Dark: lipgloss.Color(g.hex), Light: lipgloss.Color(g.hex)}
st = st.Foreground(c)
}
if g.bold {
st = st.Bold(true)
}
if g.faint {
st = st.Faint(true)
}
b.WriteString(st.Render(g.text))
}
return b.String()
}
func hasTrueColor() bool {
c := strings.ToLower(os.Getenv("COLORTERM"))
return strings.Contains(c, "truecolor") || strings.Contains(c, "24bit")
}
func rgbHex(r, g, b int) string {
if r < 0 {
r = 0
}
if r > 255 {
r = 255
}
if g < 0 {
g = 0
}
if g > 255 {
g = 255
}
if b < 0 {
b = 0
}
if b > 255 {
b = 255
}
return "#" + hex2(r) + hex2(g) + hex2(b)
}
func hex2(v int) string {
const digits = "0123456789abcdef"
return string([]byte{digits[(v>>4)&0xF], digits[v&0xF]})
}
+8 -5
View File
@@ -9,8 +9,6 @@ import { rehypeHeadingIds } from "@astrojs/markdown-remark"
import rehypeAutolinkHeadings from "rehype-autolink-headings"
import { spawnSync } from "child_process"
const github = "https://github.com/sst/opencode"
// https://astro.build/config
export default defineConfig({
site: config.url,
@@ -49,7 +47,7 @@ export default defineConfig({
},
],
editLink: {
baseUrl: `${github}/edit/dev/packages/web/`,
baseUrl: `${config.github}/edit/dev/packages/web/`,
},
markdown: {
headingLinks: false,
@@ -69,7 +67,7 @@ export default defineConfig({
{
label: "Usage",
items: ["docs/cli", "docs/ide", "docs/share", "docs/github"],
items: ["docs/tui", "docs/cli", "docs/ide", "docs/share", "docs/github", "docs/gitlab"],
},
{
@@ -79,14 +77,19 @@ export default defineConfig({
"docs/agents",
"docs/models",
"docs/themes",
"docs/plugins",
"docs/keybinds",
"docs/commands",
"docs/formatters",
"docs/permissions",
"docs/lsp",
"docs/mcp-servers",
],
},
{
label: "Develop",
items: ["docs/sdk", "docs/server", "docs/plugins"],
},
],
components: {
Hero: "./src/components/Hero.astro",
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode/web",
"type": "module",
"version": "0.5.5",
"version": "0.5.18",
"scripts": {
"dev": "astro dev",
"dev:remote": "sst shell --stage=dev --target=Web astro dev",
@@ -31,7 +31,7 @@
"sharp": "0.32.5",
"shiki": "3.4.2",
"solid-js": "1.9.7",
"toolbeam-docs-theme": "0.4.3"
"toolbeam-docs-theme": "0.4.6"
},
"devDependencies": {
"opencode": "workspace:*",
@@ -179,8 +179,6 @@ Provide constructive feedback without making direct changes.
The markdown file name becomes the agent name. For example, `review.md` creates a `review` agent.
Let's look at these configuration options in detail.
---
## Options
@@ -535,7 +533,7 @@ For example, with OpenAI's reasoning models, you can control the reasoning effor
"agent": {
"deep-thinker": {
"description": "Agent that uses high reasoning effort for complex problems",
"model": "openai/gpt-5-turbo",
"model": "openai/gpt-5",
"reasoningEffort": "high",
"textVerbosity": "low"
}
+5 -118
View File
@@ -1,136 +1,23 @@
---
title: CLI
description: The opencode CLI options and commands.
description: opencode CLI options and commands.
---
import { Tabs, TabItem } from "@astrojs/starlight/components"
Running the opencode CLI starts it for the current directory.
The opencode CLI by default starts the [TUI](/docs/tui) when run without any arguments.
```bash
opencode
```
Or you can start it for a specific working directory.
But it also accepts commands as documented on this page. This allows you to interact with opencode programmatically.
```bash
opencode /path/to/project
opencode run "Explain how closures work in JavaScript"
```
---
## Slash commands
When using the opencode CLI, you can type `/` followed by a command name to quickly execute actions. For example:
:::tip
You can also use `@` to reference files in your messages.
:::
```bash frame="none"
/help
```
Here are all available slash commands:
- **`/help`**
Show the help dialog.
- **`/editor`**
Open external editor for composing messages. Uses the editor set in your `EDITOR` environment variable. [Learn more](#editor-setup).
- **`/export`**
Export current conversation to Markdown and open in your default editor. Uses the editor set in your `EDITOR` environment variable. [Learn more](#editor-setup).
- **`/new`**
Start a new session. _Alias_: `/clear`
- **`/sessions`**
List and switch between sessions. _Aliases_: `/resume`, `/continue`
- **`/share`**
Share current session. [Learn more](/docs/share).
- **`/unshare`**
Unshare current session. [Learn more](/docs/share#un-sharing).
- **`/compact`**
Compact the current session. _Alias_: `/summarize`
- **`/details`**
Toggle tool execution details.
- **`/models`**
List available models.
- **`/themes`**
List available themes.
- **`/init`**
Create or update `AGENTS.md` file. [Learn more](/docs/rules).
- **`/undo`**
Undo last message.
- **`/redo`**
Redo message.
- **`/exit`**
Exit opencode. _Aliases_: `/quit`, `/q`
---
### Bash commands
Start a line with `!` to run a shell command. The output is added to the conversation as a tool result.
```bash frame="none"
!ls -la
```
---
### Editor setup
Both the `/editor` and `/export` commands use the editor specified in your `EDITOR` environment variable.
<Tabs>
<TabItem label="Linux/macOS">
```bash export EDITOR=nano # or vim, code, etc. ``` To make it permanent, add this to your shell profile;
`~/.bashrc`, `~/.zshrc`, etc.
</TabItem>
<TabItem label="Windows (CMD)">
```bash set EDITOR=notepad # or code, vim, etc. ``` To make it permanent, use **System Properties** > **Environment
Variables**.
</TabItem>
<TabItem label="Windows (PowerShell)">
```bash $env:EDITOR = "notepad" # or "code", "vim", etc. ``` To make it permanent, add this to your PowerShell
profile.
</TabItem>
</Tabs>
Popular editor options include:
- `code` - Visual Studio Code
- `vim` - Vim editor
- `nano` - Nano editor
- `notepad` - Windows Notepad
- `subl` - Sublime Text
---
@@ -289,7 +176,7 @@ opencode run Explain the use of context in Go
### serve
Start a headless opencode server for API access.
Start a headless opencode server for API access. Check out the [server docs](/docs/server) for the full HTTP interface.
```bash
opencode serve
@@ -0,0 +1,156 @@
---
title: Commands
description: Create custom commands for repetitive tasks.
---
Define custom commands to automate repetitive coding tasks.
---
## Create command files
Create markdown files in the `command/` directory to define custom commands.
Create `.opencode/command/test.md`:
```md title=".opencode/command/test.md"
---
description: Run tests with coverage
agent: build
model: anthropic/claude-3-5-sonnet-20241022
---
Run the full test suite with coverage report and show any failures.
Focus on the failing tests and suggest fixes.
```
The frontmatter defines command properties. The content becomes the template.
Use the command by typing `/` followed by the command name.
```bash frame="none"
"/test"
```
---
## Use arguments
Pass arguments to commands using the `$ARGUMENTS` placeholder.
Create `.opencode/command/component.md`:
```md title=".opencode/command/component.md"
---
description: Create a new component
---
Create a new React component named $ARGUMENTS with TypeScript support.
Include proper typing and basic structure.
```
Run the command with arguments:
```bash frame="none"
"/component Button"
```
---
## Inject shell output
Use `!command` to inject shell command output into your prompt.
Create `.opencode/command/analyze-coverage.md`:
```md title=".opencode/command/analyze-coverage.md"
---
description: Analyze test coverage
---
Here are the current test results:
`!npm test`
Based on these results, suggest improvements to increase coverage.
```
Create `.opencode/command/review-changes.md`:
```md title=".opencode/command/review-changes.md"
---
description: Review recent changes
---
Recent git commits:
`!git log --oneline -10`
Review these changes and suggest any improvements.
```
Commands run in your project's root directory and their output becomes part of the prompt.
---
## Reference files
Include files in your command using `@` followed by the filename.
Create `.opencode/command/review-component.md`:
```md title=".opencode/command/review-component.md"
---
description: Review component
---
Review the component in @src/components/Button.tsx.
Check for performance issues and suggest improvements.
```
The file content gets included in the prompt automatically.
---
## Command properties
Configure commands with these optional frontmatter properties:
- **description**: Brief explanation of what the command does
- **agent**: Agent to use (defaults to "build")
- **model**: Specific model to use for this command
Create `.opencode/command/code-review.md`:
```md title=".opencode/command/code-review.md"
---
description: Code review assistant
agent: build
model: anthropic/claude-3-5-sonnet-20241022
---
Review the code for best practices and suggest improvements.
```
---
## Command directory
Store command files in these locations:
- `.opencode/command/` - Project-specific commands
- `command/` - Global commands in config directory
Project commands take precedence over global ones.
---
## Built-in commands
opencode includes several built-in commands:
- `/init` - Initialize project and create AGENTS.md
- `/undo` - Revert the last changes
- `/redo` - Restore reverted changes
- `/share` - Share the current conversation
- `/help` - Show available commands and keybinds
Use `/help` to see all available commands in your setup.
@@ -1,6 +1,6 @@
---
title: GitHub
description: Use opencode in GitHub issues and pull-requests
description: Use opencode in GitHub issues and pull-requests.
---
opencode integrates with your GitHub workflow. Mention `/opencode` or `/oc` in your comment, and opencode will execute tasks within your GitHub Actions runner.
@@ -0,0 +1,151 @@
---
title: GitLab
description: Use opencode in GitLab issues and merge requests.
---
opencode integrates with your GitLab workflow.
Mention `@opencode` in a comment, and opencode will execute tasks within your GitLab CI pipeline.
---
## Features
- **Triage issues**: Ask opencode to look into an issue and explain it to you.
- **Fix and implement**: Ask opencode to fix an issue or implement a feature.
It will work create a new branch and raised a merge request with the changes.
- **Secure**: opencode runs on your GitLab runners.
---
## Setup
opencode runs in your GitLab CI/CD pipeline, here's what you'll need to set it up:
:::tip
Check out the [**GitLab docs**](https://docs.gitlab.com/user/duo_agent_platform/agent_assistant/) for up to date instructions.
:::
1. Configure your GitLab environment
2. Set up CI/CD
3. Get an AI model provider API key
4. Create a service account
5. Configure CI/CD variables
6. Create a flow config file, here's an example:
<details>
<summary>Flow configuration</summary>
```yaml
image: node:22-slim
commands:
- echo "Installing opencode"
- npm install --global opencode-ai
- echo "Installing glab"
- export GITLAB_TOKEN=$GITLAB_TOKEN_OPENCODE
- apt-get update --quiet && apt-get install --yes curl wget gpg git && rm --recursive --force /var/lib/apt/lists/*
- curl --silent --show-error --location "https://raw.githubusercontent.com/upciti/wakemeops/main/assets/install_repository" | bash
- apt-get install --yes glab
- echo "Configuring glab"
- echo $GITLAB_HOST
- echo "Creating opencode auth configuration"
- mkdir --parents ~/.local/share/opencode
- |
cat > ~/.local/share/opencode/auth.json << EOF
{
"anthropic": {
"type": "api",
"key": "$ANTHROPIC_API_KEY"
}
}
EOF
- echo "Configuring git"
- git config --global user.email "opencode@gitlab.com"
- git config --global user.name "Opencode"
- echo "Testing glab"
- glab issue list
- echo "Running Opencode"
- |
opencode run "
You are an AI assistant helping with GitLab operations.
Context: $AI_FLOW_CONTEXT
Task: $AI_FLOW_INPUT
Event: $AI_FLOW_EVENT
Please execute the requested task using the available GitLab tools.
Be thorough in your analysis and provide clear explanations.
<important>
Please use the glab CLI to access data from GitLab. The glab CLI has already been authenticated. You can run the corresponding commands.
If you are asked to summarise an MR or issue or asked to provide more information then please post back a note to the MR/Issue so that the user can see it.
You don't need to commit or push up changes, those will be done automatically based on the file changes you make.
</important>
"
- git checkout --branch $CI_WORKLOAD_REF origin/$CI_WORKLOAD_REF
- echo "Checking for git changes and pushing if any exist"
- |
if ! git diff --quiet || ! git diff --cached --quiet || [ --not --zero "$(git ls-files --others --exclude-standard)" ]; then
echo "Git changes detected, adding and pushing..."
git add .
if git diff --cached --quiet; then
echo "No staged changes to commit"
else
echo "Committing changes to branch: $CI_WORKLOAD_REF"
git commit --message "Codex changes"
echo "Pushing changes up to $CI_WORKLOAD_REF"
git push https://gitlab-ci-token:$GITLAB_TOKEN@$GITLAB_HOST/gl-demo-ultimate-dev-ai-epic-17570/test-java-project.git $CI_WORKLOAD_REF
echo "Changes successfully pushed"
fi
else
echo "No git changes detected, skipping push"
fi
variables:
- ANTHROPIC_API_KEY
- GITLAB_TOKEN_OPENCODE
- GITLAB_HOST
```
</details>
You can refer to the [GitLab CLI agents docs](https://docs.gitlab.com/user/duo_agent_platform/agent_assistant/) for detailed instructions.
---
## Examples
Here are some examples of how you can use opencode in GitLab.
:::tip
You can configure to use a different trigger phrase than `@opencode`.
:::
- **Explain an issue**
Add this comment in a GitLab issue.
```
@opencode explain this issue
```
opencode will read the issue and reply with a clear explanation.
- **Fix an issue**
In a GitLab issue, say:
```
@opencode fix this
```
opencode will create a new branch, implement the changes, and open a merge request with the changes.
- **Review merge requests**
Leave the following comment on a GitLab merge request.
```
@opencode review this merge request
```
opencode will review the merge request and provide feedback.
+21 -21
View File
@@ -42,24 +42,24 @@ You can also install it with the following:
<Tabs>
<TabItem label="npm">
```bash
npm install -g opencode-ai
```
```bash
npm install -g opencode-ai
```
</TabItem>
<TabItem label="Bun">
```bash
bun install -g opencode-ai
```
```bash
bun install -g opencode-ai
```
</TabItem>
<TabItem label="pnpm">
```bash
pnpm install -g opencode-ai
```
```bash
pnpm install -g opencode-ai
```
</TabItem>
<TabItem label="Yarn">
```bash
yarn global add opencode-ai
```
```bash
yarn global add opencode-ai
```
</TabItem>
</Tabs>
@@ -157,16 +157,16 @@ help.
You can ask opencode to explain the codebase to you.
```txt frame="none"
:::tip
Use the `@` key to fuzzy search for files in the project.
:::
```txt frame="none" "@packages/functions/src/api/index.ts"
How is authentication handled in @packages/functions/src/api/index.ts
```
This is helpful if there's a part of the codebase that you didn't work on.
:::tip
Use the `@` key to fuzzy search for files in the project.
:::
---
### Add features
@@ -238,7 +238,7 @@ You can ask opencode to add new features to your project. Though we first recomm
For more straightforward changes, you can ask opencode to directly build it
without having to review the plan first.
```txt frame="none"
```txt frame="none" "@packages/functions/src/settings.ts" "@packages/functions/src/notes.ts"
We need to add authentication to the /settings route. Take a look at how this is
handled in the /notes route in @packages/functions/src/notes.ts and implement
the same logic in @packages/functions/src/settings.ts
@@ -253,7 +253,7 @@ changes.
Let's say you ask opencode to make some changes.
```txt frame="none"
```txt frame="none" "@packages/functions/src/api/index.ts"
Can you refactor the function in @packages/functions/src/api/index.ts?
```
@@ -267,7 +267,7 @@ using the `/undo` command.
opencode will now revert the changes you made and show your original message
again.
```txt frame="none"
```txt frame="none" "@packages/functions/src/api/index.ts"
Can you refactor the function in @packages/functions/src/api/index.ts?
```
@@ -308,4 +308,4 @@ Here's an [example conversation](https://opencode.ai/s/4XP1fce5) with opencode.
And that's it! You are now a pro at using opencode.
To make it your own, we recommend [picking a theme](/docs/themes), [customizing the keybinds](/docs/keybinds), [configuring code formatters](/docs/formatters), or playing around with the [opencode config](/docs/config).
To make it your own, we recommend [picking a theme](/docs/themes), [customizing the keybinds](/docs/keybinds), [configuring code formatters](/docs/formatters), [creating custom commands](/docs/commands), or playing around with the [opencode config](/docs/config).
@@ -20,7 +20,6 @@ opencode has a list of keybinds that you can customize through the opencode conf
"session_export": "<leader>x",
"session_new": "<leader>n",
"session_list": "<leader>l",
"session_navigation": "<leader>g",
"session_share": "<leader>s",
"session_unshare": "none",
"session_interrupt": "esc",
+1 -1
View File
@@ -3,7 +3,7 @@ title: LSP Servers
description: opencode integrates with your LSP servers.
---
opencode integrates with your Language Server Protocol (LSP) to help the LLM interacts with your codebase. It uses diagnostics to provide feebdack to the LLM. And _go-to-definition_ and _find-references_ to help navigate your codebase.
opencode integrates with your Language Server Protocol (LSP) to help the LLM interacts with your codebase. It uses diagnostics to provide feedback to the LLM. And _go-to-definition_ and _find-references_ to help navigate your codebase.
---
@@ -614,8 +614,6 @@ In this example:
### OpenAI
https://platform.openai.com/api-keys
1. Head over to the [OpenAI Platform console](https://platform.openai.com/api-keys), click **Create new secret key**, and copy the key.
2. Run `opencode auth login` and select OpenAI.
+314
View File
@@ -0,0 +1,314 @@
---
title: SDK
description: JS SDK for the opencode server.
---
import config from "../../../../config.mjs"
export const typesUrl = `${config.github}/blob/dev/packages/sdk/js/src/gen/types.gen.ts`
The opencode [JS/TS SDK](https://www.npmjs.com/package/@opencode-ai/sdk) provides a type-safe client for interacting with the opencode server. You can use it to build custom integrations and control opencode programmatically.
[Learn more](/docs/server) about how the opencode server works.
---
## Install
Install the SDK from npm:
```bash
npm install @opencode-ai/sdk
```
---
## Create client
Create a client instance to connect to your opencode server:
```javascript
import { createOpencodeClient } from "@opencode-ai/sdk"
const client = createOpencodeClient({
baseUrl: "http://localhost:4096",
})
```
#### Options
| Option | Type | Description | Default |
| --------------- | ---------- | --------------------------------- | ----------------------- |
| `baseUrl` | `string` | URL of the opencode server | `http://localhost:4096` |
| `fetch` | `function` | Custom fetch implementation | `globalThis.fetch` |
| `parseAs` | `string` | Response parsing method | `auto` |
| `responseStyle` | `string` | Return style: `data` or `fields` | `fields` |
| `throwOnError` | `boolean` | Throw errors instead of returning | `false` |
---
## Start server
You can also programmatically start an opencode server:
````javascript
import { createOpencodeServer } from "@opencode-ai/sdk"
const server = await createOpencodeServer({
hostname: "127.0.0.1",
port: 4096,
})
console.log(`Server running at ${server.url}`)
server.close()
}
#### Options
| Option | Type | Description | Default |
| ---------- | -------------- | ------------------------------ | ------------- |
| `hostname` | `string` | Server hostname | `127.0.0.1` |
| `port` | `number` | Server port | `4096` |
| `signal` | `AbortSignal` | Abort signal for cancellation | `undefined` |
| `timeout` | `number` | Timeout in ms for server start | `5000` |
---
## Types
The SDK includes TypeScript definitions for all API types. Import them directly:
```typescript
import type { Session, Message, Part } from "@opencode-ai/sdk"
````
All types are generated from the server's OpenAPI specification and available in the <a href={typesUrl}>types file</a>.
---
## Errors
The SDK throws typed errors that you can catch and handle:
```typescript
try {
const session = await client.session.get({ id: "invalid-id" })
} catch (error) {
console.error("Failed to get session:", error.message)
}
```
---
## APIs
The SDK exposes all server APIs through a type-safe client interface.
---
### App
| Method | Description | Response |
| ------------ | ------------------ | --------------------------------------- |
| `app.get()` | Get app info | <a href={typesUrl}><code>App</code></a> |
| `app.init()` | Initialize the app | `boolean` |
---
#### Examples
```javascript
const app = await client.app.get()
await client.app.init()
```
---
### Config
| Method | Description | Response |
| -------------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `config.get()` | Get config info | <a href={typesUrl}><code>Config</code></a> |
| `config.providers()` | List providers and default models | `{ providers: `<a href={typesUrl}><code>Provider[]</code></a>`, default: { [key: string]: string } }` |
---
#### Examples
```javascript
const config = await client.config.get()
const { providers, default: defaults } = await client.config.providers()
```
---
### Sessions
| Method | Description | Notes |
| ------------------------------------------------------------- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `session.list()` | List sessions | Returns <a href={typesUrl}><code>Session[]</code></a> |
| `session.get({ id })` | Get session | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.children({ id })` | List child sessions | Returns <a href={typesUrl}><code>Session[]</code></a> |
| `session.create({ parentID?, title? })` | Create session | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.delete({ id })` | Delete session | Returns `boolean` |
| `session.update({ id, title? })` | Update session properties | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.init({ id, messageID, providerID, modelID })` | Analyze app and create `AGENTS.md` | Returns `boolean` |
| `session.abort({ id })` | Abort a running session | Returns `boolean` |
| `session.share({ id })` | Share session | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.unshare({ id })` | Unshare session | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.summarize({ id, providerID, modelID })` | Summarize session | Returns `boolean` |
| `session.messages({ id })` | List messages in a session | Returns `{ info: `<a href={typesUrl}><code>Message</code></a>`, parts: `<a href={typesUrl}><code>Part[]</code></a>`}[]` |
| `session.message({ id, messageID })` | Get message details | Returns `{ info: `<a href={typesUrl}><code>Message</code></a>`, parts: `<a href={typesUrl}><code>Part[]</code></a>`}` |
| `session.chat({ id, ...chatInput })` | Send chat message | Returns <a href={typesUrl}><code>Message</code></a> |
| `session.shell({ id, agent, command })` | Run a shell command | Returns <a href={typesUrl}><code>Message</code></a> |
| `session.revert({ id, messageID, partID? })` | Revert a message | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.unrevert({ id })` | Restore reverted messages | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.permissions.respond({ id, permissionID, response })` | Respond to a permission request | Returns `boolean` |
---
#### Examples
```javascript
// Create and manage sessions
const session = await client.session.create({ title: "My session" })
const sessions = await client.session.list()
// Send messages
const message = await client.session.chat({
id: session.id,
providerID: "anthropic",
modelID: "claude-3-5-sonnet-20241022",
parts: [{ type: "text", text: "Hello!" }],
})
```
---
### Files
| Method | Description | Response |
| ------------------------- | ---------------------------- | ------------------------------------------------------------------------------------------- |
| `find.text({ pattern })` | Search for text in files | Array of match objects with `path`, `lines`, `line_number`, `absolute_offset`, `submatches` |
| `find.files({ query })` | Find files by name | `string[]` (file paths) |
| `find.symbols({ query })` | Find workspace symbols | <a href={typesUrl}><code>Symbol[]</code></a> |
| `file.read({ path })` | Read a file | `{ type: "raw" \| "patch", content: string }` |
| `file.status()` | Get status for tracked files | <a href={typesUrl}><code>File[]</code></a> |
---
#### Examples
```javascript
// Search and read files
const textResults = await client.find.text({ pattern: "function.*opencode" })
const files = await client.find.files({ query: "*.ts" })
const content = await client.file.read({ path: "src/index.ts" })
```
---
### Logging
| Method | Description | Response |
| ------------------------------------------------ | --------------- | --------- |
| `log.write({ service, level, message, extra? })` | Write log entry | `boolean` |
---
#### Examples
```javascript
await client.log.write({
service: "my-app",
level: "info",
message: "Operation completed",
})
```
---
### Agents
| Method | Description | Response |
| -------------- | ------------------------- | ------------------------------------------- |
| `agent.list()` | List all available agents | <a href={typesUrl}><code>Agent[]</code></a> |
---
#### Examples
```javascript
const agents = await client.agent.list()
```
---
### TUI
| Method | Description | Response |
| --------------------------------------------- | --------------------------------- | ---------------------- |
| `tui.appendPrompt({ text })` | Append text to the prompt | `boolean` |
| `tui.openHelp()` | Open the help dialog | `boolean` |
| `tui.openSessions()` | Open the session selector | `boolean` |
| `tui.openThemes()` | Open the theme selector | `boolean` |
| `tui.openModels()` | Open the model selector | `boolean` |
| `tui.submitPrompt()` | Submit the current prompt | `boolean` |
| `tui.clearPrompt()` | Clear the prompt | `boolean` |
| `tui.executeCommand({ command })` | Execute a command | `boolean` |
| `tui.showToast({ title?, message, variant })` | Show toast notification | `boolean` |
| `tui.control.next()` | Wait for the next control request | Control request object |
| `tui.control.response({ body })` | Respond to a control request | `boolean` |
---
#### Examples
```javascript
// Control TUI interface
await client.tui.appendPrompt({ text: "Add this to prompt" })
await client.tui.showToast({
message: "Task completed",
variant: "success",
})
```
---
### Auth
| Method | Description | Response |
| ------------------------------- | ------------------------------ | --------- |
| `auth.set({ id, ...authData })` | Set authentication credentials | `boolean` |
---
#### Examples
```javascript
await client.auth.set({
id: "anthropic",
type: "api",
key: "your-api-key",
})
```
---
### Events
| Method | Description | Response |
| ------------------- | ------------------------- | ------------------------- |
| `event.subscribe()` | Server-sent events stream | Server-sent events stream |
---
#### Examples
```javascript
// Listen to real-time events
const eventStream = await client.event.subscribe()
for await (const event of eventStream) {
console.log("Event:", event.type, event.properties)
}
```
@@ -0,0 +1,180 @@
---
title: Server
description: Interact with opencode server over HTTP.
---
import config from "../../../../config.mjs"
export const typesUrl = `${config.github}/blob/dev/packages/sdk/js/src/gen/types.gen.ts`
The `opencode serve` command runs a headless HTTP server that exposes an OpenAPI endpoint that an opencode client can use.
---
### Usage
```bash
opencode serve [--port <number>] [--hostname <string>]
```
#### Options
| Flag | Short | Description | Default |
| ------------ | ----- | --------------------- | ----------- |
| `--port` | `-p` | Port to listen on | `4096` |
| `--hostname` | `-h` | Hostname to listen on | `127.0.0.1` |
---
### How it works
When you run `opencode` it starts a TUI and a server. Where the TUI is the
client that talks to the server. The server exposes an OpenAPI 3.1 spec
endpoint. This endpoint is also used to generate an [SDK](/docs/sdk).
:::tip
Use the opencode server to interact with opencode programmatically.
:::
This architecture lets opencode support multiple clients and allows you to interact with opencode programmatically.
You can run `opencode serve` to start a standalone server. If you have the
opencode TUI running, `opencode serve` will start a new server.
---
#### Connect to an existing server
When you start the TUI it randomly assigns a port and hostname. You can instead pass in the `--hostname` and `--port` [flags](/docs/cli). Then use this to connect to its server.
The [`/tui`](#tui) endpoint can be used to drive the TUI through the server. For example, you can prefill or run a prompt. This setup is used by the opencode [IDE](/docs/ide) plugins.
---
## Spec
The server publishes an OpenAPI 3.1 spec that can be viewed at:
```
http://<hostname>:<port>/doc
```
For example, `http://localhost:4096/doc`. Use the spec to generate clients or inspect request and response types. Or view it in a Swagger explorer.
---
## APIs
The opencode server exposes the following APIs.
---
### App
| Method | Path | Description | Response |
| ------ | ----------- | ------------------ | --------------------------------------- |
| `GET` | `/app` | Get app info | <a href={typesUrl}><code>App</code></a> |
| `POST` | `/app/init` | Initialize the app | `boolean` |
---
### Config
| Method | Path | Description | Response |
| ------ | ------------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `GET` | `/config` | Get config info | <a href={typesUrl}><code>Config</code></a> |
| `GET` | `/config/providers` | List providers and default models | `{ providers: `<a href={typesUrl}>Provider[]</a>`, default: { [key: string]: string } }` |
---
### Sessions
| Method | Path | Description | Notes |
| -------- | ---------------------------------------- | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET` | `/session` | List sessions | Returns <a href={typesUrl}><code>Session[]</code></a> |
| `GET` | `/session/:id` | Get session | Returns <a href={typesUrl}><code>Session</code></a> |
| `GET` | `/session/:id/children` | List child sessions | Returns <a href={typesUrl}><code>Session[]</code></a> |
| `POST` | `/session` | Create session | body: `{ parentID?, title? }`, returns <a href={typesUrl}><code>Session</code></a> |
| `DELETE` | `/session/:id` | Delete session | |
| `PATCH` | `/session/:id` | Update session properties | body: `{ title? }`, returns <a href={typesUrl}><code>Session</code></a> |
| `POST` | `/session/:id/init` | Analyze app and create `AGENTS.md` | body: `{ messageID, providerID, modelID }` |
| `POST` | `/session/:id/abort` | Abort a running session | |
| `POST` | `/session/:id/share` | Share session | Returns <a href={typesUrl}><code>Session</code></a> |
| `DELETE` | `/session/:id/share` | Unshare session | Returns <a href={typesUrl}><code>Session</code></a> |
| `POST` | `/session/:id/summarize` | Summarize session | |
| `GET` | `/session/:id/message` | List messages in a session | Returns `{ info: `<a href={typesUrl}>Message</a>`, parts: `<a href={typesUrl}>Part[]</a>`}[]` |
| `GET` | `/session/:id/message/:messageID` | Get message details | Returns `{ info: `<a href={typesUrl}>Message</a>`, parts: `<a href={typesUrl}>Part[]</a>`}` |
| `POST` | `/session/:id/message` | Send chat message | body matches [`ChatInput`](https://github.com/sst/opencode/blob/main/packages/opencode/src/session/index.ts#L358), returns <a href={typesUrl}><code>Message</code></a> |
| `POST` | `/session/:id/shell` | Run a shell command | body matches [`CommandInput`](https://github.com/sst/opencode/blob/main/packages/opencode/src/session/index.ts#L1007), returns <a href={typesUrl}><code>Message</code></a> |
| `POST` | `/session/:id/revert` | Revert a message | body: `{ messageID }` |
| `POST` | `/session/:id/unrevert` | Restore reverted messages | |
| `POST` | `/session/:id/permissions/:permissionID` | Respond to a permission request | body: `{ response }` |
---
### Files
| Method | Path | Description | Response |
| ------ | ------------------------ | ---------------------------- | ------------------------------------------------------------------------------------------- |
| `GET` | `/find?pattern=<pat>` | Search for text in files | Array of match objects with `path`, `lines`, `line_number`, `absolute_offset`, `submatches` |
| `GET` | `/find/file?query=<q>` | Find files by name | `string[]` (file paths) |
| `GET` | `/find/symbol?query=<q>` | Find workspace symbols | <a href={typesUrl}><code>Symbol[]</code></a> |
| `GET` | `/file?path=<path>` | Read a file | `{ type: "raw" \| "patch", content: string }` |
| `GET` | `/file/status` | Get status for tracked files | <a href={typesUrl}><code>File[]</code></a> |
---
### Logging
| Method | Path | Description | Response |
| ------ | ------ | ------------------------------------------------------------ | --------- |
| `POST` | `/log` | Write log entry. Body: `{ service, level, message, extra? }` | `boolean` |
---
### Agents
| Method | Path | Description | Response |
| ------ | -------- | ------------------------- | ------------------------------------------- |
| `GET` | `/agent` | List all available agents | <a href={typesUrl}><code>Agent[]</code></a> |
---
### TUI
| Method | Path | Description | Response |
| ------ | ----------------------- | ------------------------------------------- | ---------------------- |
| `POST` | `/tui/append-prompt` | Append text to the prompt | `boolean` |
| `POST` | `/tui/open-help` | Open the help dialog | `boolean` |
| `POST` | `/tui/open-sessions` | Open the session selector | `boolean` |
| `POST` | `/tui/open-themes` | Open the theme selector | `boolean` |
| `POST` | `/tui/open-models` | Open the model selector | `boolean` |
| `POST` | `/tui/submit-prompt` | Submit the current prompt | `boolean` |
| `POST` | `/tui/clear-prompt` | Clear the prompt | `boolean` |
| `POST` | `/tui/execute-command` | Execute a command (`{ command }`) | `boolean` |
| `POST` | `/tui/show-toast` | Show toast (`{ title?, message, variant }`) | `boolean` |
| `GET` | `/tui/control/next` | Wait for the next control request | Control request object |
| `POST` | `/tui/control/response` | Respond to a control request (`{ body }`) | `boolean` |
---
### Auth
| Method | Path | Description | Response |
| ------ | ----------- | --------------------------------------------------------------- | --------- |
| `PUT` | `/auth/:id` | Set authentication credentials. Body must match provider schema | `boolean` |
---
### Events
| Method | Path | Description | Response |
| ------ | -------- | ----------------------------------------------------------------------------- | ------------------------- |
| `GET` | `/event` | Server-sent events stream. First event is `server.connected`, then bus events | Server-sent events stream |
---
### Docs
| Method | Path | Description | Response |
| ------ | ------ | -------------------------------------- | ------------------------------------------ |
| `GET` | `/doc` | OpenAPI 3.1 specification | HTML page with OpenAPI spec |
+290
View File
@@ -0,0 +1,290 @@
---
title: TUI
description: Using the opencode terminal user interface.
---
import { Tabs, TabItem } from "@astrojs/starlight/components"
opencode provides an interactive terminal interface or TUI for working on your projects with an LLM.
Running opencode starts the TUI for the current directory.
```bash
opencode
```
Or you can start it for a specific working directory.
```bash
opencode /path/to/project
```
Once you're in the TUI, you can prompt it with a message.
```text
Give me a quick summary of the codebase.
```
:::tip
You can also use `@` to reference files in your messages.
:::
```text "@packages/functions/src/api/index.ts"
How is auth handled in @packages/functions/src/api/index.ts?
```
---
## Commands
When using the opencode TUI, you can type `/` followed by a command name to quickly execute actions. For example:
```bash frame="none"
/help
```
Most commands also have keybind using `ctrl+x` as the leader key, where `ctrl+x` is the default leader key. [Learn more](/docs/keybinds).
Here are all available slash commands:
---
### compact
Compact the current session. _Alias_: `/summarize`
```bash frame="none"
/compact
```
**Keybind:** `ctrl+x c`
---
### details
Toggle tool execution details.
```bash frame="none"
/details
```
**Keybind:** `ctrl+x d`
---
### editor
Open external editor for composing messages. Uses the editor set in your `EDITOR` environment variable. [Learn more](#editor-setup).
```bash frame="none"
/editor
```
**Keybind:** `ctrl+x e`
---
### exit
Exit opencode. _Aliases_: `/quit`, `/q`
```bash frame="none"
/exit
```
**Keybind:** `ctrl+x q`
---
### export
Export current conversation to Markdown and open in your default editor. Uses the editor set in your `EDITOR` environment variable. [Learn more](#editor-setup).
```bash frame="none"
/export
```
**Keybind:** `ctrl+x x`
---
### help
Show the help dialog.
```bash frame="none"
/help
```
**Keybind:** `ctrl+x h`
---
### init
Create or update `AGENTS.md` file. [Learn more](/docs/rules).
```bash frame="none"
/init
```
**Keybind:** `ctrl+x i`
---
### models
List available models.
```bash frame="none"
/models
```
**Keybind:** `ctrl+x m`
---
### new
Start a new session. _Alias_: `/clear`
```bash frame="none"
/new
```
**Keybind:** `ctrl+x n`
---
### redo
Redo a previously undone message. Only available after using `/undo`.
:::tip
Any file changes will also be restored.
:::
```bash frame="none"
/redo
```
**Keybind:** `ctrl+x r`
---
### sessions
List and switch between sessions. _Aliases_: `/resume`, `/continue`
```bash frame="none"
/sessions
```
**Keybind:** `ctrl+x l`
---
### share
Share current session. [Learn more](/docs/share).
```bash frame="none"
/share
```
**Keybind:** `ctrl+x s`
---
### themes
List available themes.
```bash frame="none"
/themes
```
**Keybind:** `ctrl+x t`
---
### undo
Undo last message in the conversation. Removes the most recent user message, all subsequent responses, and any file changes.
:::tip
Any file changes made will also be reverted.
:::
```bash frame="none"
/undo
```
**Keybind:** `ctrl+x u`
---
### unshare
Unshare current session. [Learn more](/docs/share#un-sharing).
```bash frame="none"
/unshare
```
---
## Bash commands
Start a message with `!` to run a shell command.
```bash frame="none"
!ls -la
```
The output of the command is added to the conversation as a tool result.
---
## Editor setup
Both the `/editor` and `/export` commands use the editor specified in your `EDITOR` environment variable.
<Tabs>
<TabItem label="Linux/macOS">
```bash
export EDITOR=nano # or vim, code, etc.
```
To make it permanent, add this to your shell profile;
`~/.bashrc`, `~/.zshrc`, etc.
</TabItem>
<TabItem label="Windows (CMD)">
```bash
set EDITOR=notepad # or code, vim, etc.
```
To make it permanent, use **System Properties** > **Environment
Variables**.
</TabItem>
<TabItem label="Windows (PowerShell)">
```bash
$env:EDITOR = "notepad" # or "code", "vim", etc.
```
To make it permanent, add this to your PowerShell
profile.
</TabItem>
</Tabs>
Popular editor options include:
- `code` - Visual Studio Code
- `vim` - Vim editor
- `nano` - Nano editor
- `notepad` - Windows Notepad
- `subl` - Sublime Text

Some files were not shown because too many files have changed in this diff Show More