Compare commits

...

178 Commits

Author SHA1 Message Date
Dax Raad
4a1868ffd3 Snapshot release v0.0.0-202508031629 2025-08-03 12:30:22 -04:00
opencode
263b266476 release: v0.3.122 2025-08-03 16:19:09 +00:00
Dax Raad
06830327e7 more efficient snapshots in parallel toolcalls 2025-08-03 12:12:28 -04:00
Giuseppe Rota
4b204fee58 fix(docs): move disabled providers paragraph to its proper section (#1547) 2025-08-03 11:28:57 -04:00
Dax Raad
99d3a0bb24 more fixes for shell 128 error 2025-08-03 11:25:58 -04:00
opencode
0930f6ac55 release: v0.3.120 2025-08-03 14:59:03 +00:00
Dax Raad
24515162fa ci: ignore 2025-08-03 10:52:35 -04:00
Dax Raad
53aa899e45 ci: ignore 2025-08-03 10:42:52 -04:00
Dax Raad
7e763e1c06 fix shell error 128 2025-08-03 10:30:23 -04:00
GitHub Action
b0f2cc0c22 ignore: update download stats 2025-08-03 2025-08-03 12:04:04 +00:00
Aiden Cline
f90aa62784 fix: expand tilde for file: references (#1553) 2025-08-03 06:15:06 -05:00
Dax Raad
852191f6cb ci: ignore 2025-08-03 03:54:17 -04:00
Dax Raad
c5e9dc081c ci: bun cache 2025-08-03 03:53:31 -04:00
Dax Raad
49c8889228 ci: ignore 2025-08-03 03:45:05 -04:00
Dax Raad
f739e1a958 ci: ignore 2025-08-03 03:37:53 -04:00
Dax Raad
841f1907bb ci: ignore 2025-08-03 03:35:17 -04:00
The Pangolier
9255c507d6 Share link hotfix (#1513) 2025-08-03 03:02:24 -04:00
Yordis Prieto
2711047166 remove: delete extension test file (#1554) 2025-08-03 02:58:10 -04:00
Frank
908048baef sync 2025-08-02 21:28:03 -04:00
Frank
a9fbe07408 Add Zhipu AI provider 2025-08-02 21:20:44 -04:00
Dax Raad
0ae213ee0e ci: ignore 2025-08-02 18:56:34 -04:00
Dax Raad
ca031278ca wip: plugins 2025-08-02 18:50:19 -04:00
Aiden Cline
ae6e47bb42 tweak: make gh action ignore url mentions of opencode (#1531) 2025-08-02 09:31:23 -05:00
Dominik Engelhardt
42a5fcead4 Choose model according to the docs (#1536) 2025-08-02 09:29:03 -05:00
Yihui Khuu
8ad83f71a9 fix(tui): attachment highlighting issues in messages (#1534) 2025-08-02 09:26:44 -05:00
Yihui Khuu
fa95c09cdc fix(tui): attachment source is not stored when using message from message history (#1542) 2025-08-02 09:23:32 -05:00
Aiden Cline
0b132c032a ignore: fix dev branch (#1529) 2025-08-02 09:11:38 -05:00
GitHub Action
44d7103a42 ignore: update download stats 2025-08-02 2025-08-02 12:04:12 +00:00
Ricardo Gonzalez
8f45a0e227 feat(models): enable Kimi k2 ⇄ Claude trajectory handoff (#1525) 2025-08-01 23:05:06 -04:00
Aiden Cline
6581741318 fix: include stderr in bash tool output (#1511) 2025-08-01 19:20:32 -05:00
Aiden Cline
80d68d01f4 better configuration error messages (#1517) 2025-08-01 19:10:32 -04:00
Jay V
fa9db3c167 docs: cerebras 2025-08-01 18:30:29 -04:00
opencode
5a727c0794 release: v0.3.112 2025-08-01 21:53:33 +00:00
Dax Raad
71cd84dbbb force models.dev refresh on auth login 2025-08-01 17:48:01 -04:00
Dax Raad
e1b7e25f4d make top_p configurable 2025-08-01 17:03:33 -04:00
Dax Raad
98b6bb218b configurable lsp 2025-08-01 14:52:10 -04:00
Brinsil Elias
5592ce8eaf fix(docs): Fix formatting for Node.js installation section (#1497) 2025-08-01 14:15:38 -04:00
CodinCat
510fe8a72a handle the optional v in upgrade command when using curl (#1500) 2025-08-01 14:15:22 -04:00
Yordis Prieto
04a1ab3893 chore: enhance bash command tests with config mock and timeout adjustments (#1486)
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
2025-08-01 14:14:54 -04:00
Dax Raad
e74b4d098b allow search in provider select 2025-08-01 14:03:22 -04:00
Dax Raad
50e4b3e6a7 add version to user-agent 2025-08-01 12:18:09 -04:00
adamdotdevin
6ebd828aa5 fix: unshare command missing 2025-08-01 09:30:42 -05:00
Aiden Cline
022c979d28 tweak: sanitize mcp server names (#831) 2025-08-01 09:11:40 -05:00
Aiden Cline
4172e3ad28 fix: bash tool errors for chmod (#1502) 2025-08-01 09:10:09 -05:00
Aiden Cline
90d1698aed fix: {file:...} references weren't being parsed correctly in some cases (#1499) 2025-08-01 08:39:21 -05:00
adamdotdevin
b0c38ce56b ignore: include usage in local setup 2025-08-01 07:42:36 -05:00
GitHub Action
9b37d0e191 ignore: update download stats 2025-08-01 2025-08-01 12:04:33 +00:00
adamdotdevin
ea794a4bf6 chore: add local qwen3 to config 2025-08-01 06:27:08 -05:00
Timo Clasen
52f9b37576 docs(permissions): add wildcard example (#1494) 2025-08-01 05:24:32 -05:00
Dax Raad
a0d2e53bde poll for models.dev changes 2025-07-31 23:47:42 -04:00
Dax Raad
851e900982 add user agent for models.dev request 2025-07-31 22:00:45 -04:00
Dax Raad
3aa6eeb426 do not mark errored tool calls as aborted 2025-07-31 21:45:40 -04:00
Dax Raad
b6ee8e92f9 better guarding against bash commands that go outside of cwd 2025-07-31 21:42:30 -04:00
Frank
44211e1526 Update STATS.md 2025-07-31 21:42:05 -04:00
Dax Raad
12f84f198f improve wildcard matching for permissions 2025-07-31 20:40:05 -04:00
Dax Raad
e6db1cf29d ci: ignore release commits 2025-07-31 19:57:07 -04:00
Dax Raad
f07f04d969 fix escape button not canceling if retry in progress 2025-07-31 19:55:57 -04:00
Dax Raad
33d613a470 docs: sync 2025-07-31 19:50:51 -04:00
Dax Raad
0bbd7ea17b docs: formatters 2025-07-31 19:50:31 -04:00
Dax Raad
87f3166437 ignore: config 2025-07-31 19:45:44 -04:00
opencode
7665bd9439 Release v0.3.105 2025-07-31 23:41:27 +00:00
Dax Raad
30e10127f2 formatter config 2025-07-31 19:36:07 -04:00
Jay V
5e66fc2318 docs: edit premissions doc 2025-07-31 19:10:54 -04:00
opencode
c1c99c7e0f Release v0.3.104 2025-07-31 23:02:36 +00:00
Dax Raad
04e3e83db3 allow disabling formatter 2025-07-31 18:56:04 -04:00
Dax Raad
4273714a62 fix issue with some bash commands asking for permission 2025-07-31 18:35:51 -04:00
Dax Raad
a21e237706 ignore: update opencode.json 2025-07-31 18:13:40 -04:00
Dax Raad
aa9105649d docs: permissions 2025-07-31 18:11:34 -04:00
Dax Raad
53be288040 docs: permissions 2025-07-31 18:11:34 -04:00
Frank
13dbf912ca Remove hardcoded vscode extension theme 2025-07-31 17:53:18 -04:00
Jay V
69966c73f8 docs: add more providers 2025-07-31 17:47:24 -04:00
opencode
a00de2df08 Release v0.3.102 2025-07-31 21:25:12 +00:00
Dax Raad
5e72f50554 wip: permissions 2025-07-31 17:19:56 -04:00
Dax Raad
d558f15c91 ignore: ts optimization 2025-07-31 16:54:15 -04:00
Dax Raad
614a23698f wip: permissions 2025-07-31 16:51:55 -04:00
Dax Raad
a2191ce6fb wip: permissions 2025-07-31 16:38:37 -04:00
Aiden Cline
168350c981 fix: load global jsonc (#1479) 2025-07-31 15:02:28 -05:00
Aiden Cline
f5f55062f1 fix: session ordering (#1474) 2025-07-31 14:17:47 -05:00
Frank
360194e219 Add provider instruction for Azure OpenAI 2025-07-31 14:37:26 -04:00
Jay V
5ee994c31f docs: edit providers doc 2025-07-31 14:11:40 -04:00
opencode-agent[bot]
fc73d4b1f9 docs: Enhanced providers docs with troubleshooting (#1441)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: jayair <jayair@users.noreply.github.com>
2025-07-31 13:08:12 -04:00
adamdotdevin
936f4cb0c6 fix: permission state hangs 2025-07-31 11:36:08 -05:00
Dax Raad
a5b20f973f wip: refactor permissions 2025-07-31 12:26:47 -04:00
adamdotdevin
872b1e068f feat: more scriptable tui (api) 2025-07-31 11:24:23 -05:00
neolooong
e4e0b8fd34 fix(editor): handle UTF-8 characters properly in SetValueWithAttachments (#1469) 2025-07-31 10:45:43 -05:00
adamdotdevin
c5368e7412 fix: missing operationId 2025-07-31 10:19:42 -05:00
adamdotdevin
1d682544b9 fix: test 2025-07-31 10:10:34 -05:00
adamdotdevin
d9210af98c fix: optional toolCallID 2025-07-31 10:09:44 -05:00
adamdotdevin
ef633fe92e fix: test 2025-07-31 10:07:58 -05:00
adamdotdevin
5500698734 wip: tui permissions 2025-07-31 09:59:17 -05:00
opencode
e7631763f3 Release v0.3.101 2025-07-31 14:23:13 +00:00
Dax Raad
18a572b079 ci: tweak 2025-07-31 10:09:43 -04:00
Dax Raad
060a62ecfb ci: fix 2025-07-31 09:46:36 -04:00
Dax Raad
ac3813549a ci: tweak 2025-07-31 09:39:44 -04:00
Dax Raad
b14da5fb1f ci: tweak 2025-07-31 09:35:57 -04:00
Dax Raad
416f2235fc ci: reorder 2025-07-31 09:29:55 -04:00
GitHub Action
4fabca426a ignore: update download stats 2025-07-31 2025-07-31 12:04:25 +00:00
Aiden Cline
7e9050edb9 feat: jsonc configuration file support (#1434) 2025-07-31 06:25:26 -05:00
Aiden Cline
3c49a9b7dd fix: process revert cleanup before creating new messages (#1448) 2025-07-31 05:07:59 -05:00
Dax Raad
ad66b97463 ci: stainless 2025-07-31 01:42:52 -04:00
Dax Raad
10a0b7f60c ci: tweak 2025-07-31 01:35:11 -04:00
Dax Raad
ac8709ac7a ci: tweak 2025-07-31 01:33:21 -04:00
Dax Raad
2d9ed06367 ci: scripts 2025-07-31 01:25:24 -04:00
Dax Raad
50be2aee39 ci tweaks 2025-07-31 01:20:12 -04:00
Dax
0bdbe6261a ci: new publish method (#1451) 2025-07-31 01:05:35 -04:00
Dax
33cef075d2 ci: new publish method (#1451) 2025-07-31 01:00:29 -04:00
Simon Westlin Green
b09ebf4645 Use responses API for Azure (#1428) 2025-07-30 23:22:59 -04:00
Robert Holden
3268c61813 feat: mode directory markdown configuration loading (#1377) 2025-07-30 23:22:43 -04:00
Josh
4a221868da Add http-referer header for vercel ai gateway requests (#1403) 2025-07-30 23:22:24 -04:00
Yordis Prieto
31b8e3d5ab docs: clarify Bun's default registry resolution in index.ts (#1438) 2025-07-30 23:21:07 -04:00
CodinCat
1a78d833a8 fix typo in bash.ts (#1444) 2025-07-30 23:20:48 -04:00
Dax
18888351e9 use treesitter to parse bash commands and catch commands that go outside of cwd (#1443) 2025-07-30 20:57:52 -04:00
Jay V
3b7085ca28 docs: edit 2025-07-30 19:11:36 -04:00
Jay V
160923dcf0 docs: add new providers doc, reorg sidebar, edits 2025-07-30 18:16:11 -04:00
Yordis Prieto
c38b091895 fix: update glob pattern and path in tool test (#1436) 2025-07-30 15:42:13 -05:00
Yordis Prieto
eecfd6d0ca fix: unit test assertion (#1435)
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
2025-07-30 15:13:37 -05:00
Dax Raad
6ef4cfa2fa lower max retries to 3 - ai sdk currently cannot abort during a retry delay so things appear to be frozen 2025-07-30 15:08:25 -04:00
Dax Raad
190dee080c release undo/redo 2025-07-30 13:09:18 -04:00
Aiden Cline
09074dc639 fix: attachment highlighting (#1427) 2025-07-30 11:43:34 -05:00
Aiden Cline
1b3d58e791 fix: prevent read tool from opening binary files and corrupting session (#1425) 2025-07-30 11:00:23 -05:00
GitHub Action
772c83c1d5 ignore: update download stats 2025-07-30 2025-07-30 12:04:23 +00:00
Sam Huckaby
54dc937fa1 fix: quick grammar and spelling check (#1402) 2025-07-30 05:54:47 -05:00
Aiden Cline
b5219f7585 tweak: adjust astro css to render mixed nested lists (#1411) 2025-07-30 05:51:52 -05:00
municorn
0bd0453866 build: add @octokit/rest to opencode dependencies (#1396)
Co-authored-by: Frank <frank@sst.dev>
2025-07-29 22:33:25 -04:00
Dax Raad
8bf36d174b update beast prompt for openai models 2025-07-29 22:15:13 -04:00
Dax Raad
9bedd62da4 experimental well-known auth support 2025-07-29 19:30:51 -04:00
Yordis Prieto
4c34b69ae6 chore: fix test to have deterministic testing (#1401) 2025-07-29 17:54:22 -05:00
Dax Raad
7e9ac35666 remove min/max in tool schemas 2025-07-29 17:39:47 -04:00
Frank
4a46144419 convert share backend to hono app 2025-07-29 16:39:48 -04:00
adamdotdevin
a129e122aa feat: show git diff in reverted messages 2025-07-29 13:11:38 -05:00
Yordis Prieto
c0ee6a6d05 fix: update file name extraction in uploads test to use __filename (#1395) 2025-07-29 12:28:44 -05:00
Yordis Prieto
68ae0d107c fix: improve handling of global File object in uploads tests (#1394) 2025-07-29 11:30:39 -05:00
Yordis Prieto
df63008a94 chore: fix null handling in multipartFormRequestOptions test (#1385) 2025-07-29 11:17:03 -05:00
Andrea Grandi
3bd2b340c8 feat: show current git branch in status bar, and make it responsive (#1339)
Co-authored-by: adamdotdevin <2363879+adamdottv@users.noreply.github.com>
2025-07-29 11:15:04 -05:00
Dax Raad
df03e182d2 strip todo tool instructions from non anthropic models 2025-07-29 11:56:53 -04:00
Jacob Hands
862a50d61d feat: add OPENCODE_CONFIG env var for specifying a custom config file (#1370) 2025-07-29 11:03:11 -04:00
GitHub Action
a7cfd36b07 ignore: update download stats 2025-07-29 2025-07-29 12:04:40 +00:00
Aiden Cline
c165360e17 fix: task type error (#1384) 2025-07-29 06:18:34 -05:00
Dax Raad
9cb0f21b4e trim opencode title 2025-07-28 23:24:38 -04:00
Dax Raad
9c9cbb3e81 wip: undo properly remove messages from UI 2025-07-28 22:58:31 -04:00
Dax Raad
c24fbb4292 wip: snapshot 2025-07-28 22:58:31 -04:00
Jay V
99dfe65862 docs: share page hide patch part 2025-07-28 20:04:00 -04:00
Jay V
4506e5a824 docs: adding 2025-07-28 20:00:30 -04:00
Jay V
b65172a2b7 Tweak auth cli copy 2025-07-28 20:00:30 -04:00
Dax Raad
081f100c93 ignore: tweak 2025-07-28 12:20:37 -04:00
Dax Raad
f2bdb8159f fix phantom tool call failed messages and empty text parts with some models 2025-07-28 12:19:38 -04:00
GitHub Action
10d749a85e ignore: update download stats 2025-07-28 2025-07-28 12:04:21 +00:00
Frank
a07d149e28 vscode: add cmd+shift+esc keybinding 2025-07-27 15:54:45 -04:00
Frank
3eb982c8cd vscode: bring oc terminal to front if already opened 2025-07-27 14:57:45 -04:00
Frank
45c4e0b8f8 show opencode button in vscode when focused on terminal 2025-07-27 14:44:14 -04:00
Aiden Cline
b18b646f8e fix: attachment bugs (#1335) 2025-07-27 12:21:31 -05:00
Frank
9741a6703c fix input format affected by installing vscode extension 2025-07-27 11:56:18 -04:00
Frank
27a079d9cb simplify github action 2025-07-27 09:56:09 -04:00
GitHub Action
2eeb987680 ignore: update download stats 2025-07-27 2025-07-27 12:04:15 +00:00
Aiden Cline
e827294c9b docs: document small_model cfg option (#1347) 2025-07-26 16:56:38 -05:00
Dax Raad
7cf4ed6ad6 ci: fix opencode github 2025-07-26 10:02:31 -04:00
Aiden Cline
ad8a4bc744 fix: strip thinking blocks from title (#1325) 2025-07-26 08:29:04 -05:00
GitHub Action
2630104f18 ignore: update download stats 2025-07-26 2025-07-26 12:03:59 +00:00
Frank
670f470eee wip: github actions 2025-07-26 02:49:05 -04:00
Frank
c2b3c52b76 wip: github action 2025-07-26 01:03:23 -04:00
Frank
a007d65f62 wip: github actions 2025-07-25 20:27:42 -04:00
Didier Durand
2c924b9fdb fixing various typos in text. (#1185) 2025-07-25 20:20:01 -04:00
Dax Raad
e8eaa77bf1 better mcp support - should fix hanging when streamable http server is added 2025-07-25 19:19:47 -04:00
Frank
a07f37073b wip: github actions 2025-07-25 19:05:55 -04:00
Frank
4d760a1984 wip: github action 2025-07-25 18:33:45 -04:00
Dax Raad
6b7058fe1c qwen optimizations it works good now 2025-07-25 18:31:08 -04:00
Frank
1149b984d9 wip: github actions 2025-07-25 18:29:53 -04:00
Michael Hanson
81fb1b313e Fix a broken example in the MCP documentation and add more clarity (#1322) 2025-07-25 17:47:01 -04:00
Frank
3a7a2a838e wip: github actions 2025-07-25 17:34:47 -04:00
Dax Raad
10ae43a121 wip: sync 2025-07-25 15:52:27 -04:00
Dax Raad
c85b970903 wip: drop 2025-07-25 15:51:02 -04:00
Dax Raad
7044662cfa handle uploaded text/plain 2025-07-25 15:48:42 -04:00
kehanzhang
92656fdf29 fix(headless): respect mode passed to /message endpoint (#1300) 2025-07-25 15:26:49 -04:00
Dax Raad
c65e7aff86 docs: mode temperature 2025-07-25 13:45:04 -04:00
Dax Raad
e97613ef9f allow temperature to be configured per mode 2025-07-25 13:29:44 -04:00
Dominik Engelhardt
827469c725 fix: apply content-level caching for non-anthropic providers (#1305) 2025-07-25 12:19:44 -04:00
Yihui Khuu
613b5fbe48 feat: add csharp lsp (#1312) 2025-07-25 12:17:06 -04:00
Dax Raad
7ed05962db fix issue with trailing whitespace error in assistant message 2025-07-25 10:56:16 -04:00
340 changed files with 24343 additions and 15676 deletions

View File

@@ -6,9 +6,14 @@ on:
jobs:
opencode:
if: startsWith(github.event.comment.body, 'hey opencode')
if: |
contains(github.event.comment.body, ' /oc') ||
startsWith(github.event.comment.body, '/oc') ||
contains(github.event.comment.body, ' /opencode') ||
startsWith(github.event.comment.body, '/opencode')
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- name: Checkout repository
@@ -17,7 +22,7 @@ jobs:
fetch-depth: 1
- name: Run opencode
uses: sst/opencode/sdks/github@github-v1
uses: sst/opencode/github@latest
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
with:

View File

@@ -27,4 +27,4 @@ jobs:
git config --global user.email "opencode@sst.dev"
git config --global user.name "opencode"
./script/publish
working-directory: ./sdks/github
working-directory: ./github

View File

@@ -1,14 +1,17 @@
name: publish
run-name: "${{ format('v{0}', inputs.version) }}"
on:
workflow_dispatch:
push:
branches:
- dev
tags:
- "*"
- "!vscode-v*"
- "!github-v*"
inputs:
version:
description: "Version to publish"
required: true
type: string
title:
description: "Custom title for this run"
required: false
type: string
concurrency: ${{ github.workflow }}-${{ github.ref }}
@@ -34,7 +37,16 @@ jobs:
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.2.17
bun-version: 1.2.19
- name: Cache ~/.bun
id: cache-bun
uses: actions/cache@v3
with:
path: ~/.bun
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-
- name: Install makepkg
run: |
@@ -50,15 +62,12 @@ jobs:
git config --global user.email "opencode@sst.dev"
git config --global user.name "opencode"
- name: Install dependencies
run: bun install
- name: Publish
run: |
bun install
if [ "${{ startsWith(github.ref, 'refs/tags/') }}" = "true" ]; then
./script/publish.ts
else
./script/publish.ts --snapshot
fi
working-directory: ./packages/opencode
OPENCODE_VERSION=${{ inputs.version }} ./script/publish.ts
env:
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
AUR_KEY: ${{ secrets.AUR_KEY }}

View File

@@ -21,7 +21,7 @@ jobs:
bun-version: latest
- name: Run stats script
run: bun scripts/stats.ts
run: bun script/stats.ts
- name: Commit stats
run: |

View File

@@ -97,7 +97,7 @@ $ bun run packages/opencode/src/index.ts
It's very similar to Claude Code in terms of capability. Here are the key differences:
- 100% open source
- Not coupled to any provider. Although Anthropic is recommended, opencode can be used with OpenAI, Google or even local models. As models evolve the gaps between them will close and pricing will drop so being provider agnostic is important.
- Not coupled to any provider. Although Anthropic is recommended, opencode can be used with OpenAI, Google or even local models. As models evolve the gaps between them will close and pricing will drop so being provider-agnostic is important.
- A focus on TUI. opencode is built by neovim users and the creators of [terminal.shop](https://terminal.shop); we are going to push the limits of what's possible in the terminal.
- A client/server architecture. This for example can allow opencode to run on your computer, while you can drive it remotely from a mobile app. Meaning that the TUI frontend is just one of the possible clients.

View File

@@ -1,29 +1,39 @@
# Download Stats
| Date | GitHub Downloads | npm Downloads | Total |
| ---------- | ---------------- | ---------------- | ----------------- |
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
| 2025-07-10 | 43,796 (+5,744) | 71,402 (+6,934) | 115,198 (+12,678) |
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
| Date | GitHub Downloads | npm Downloads | Total |
| ---------- | ---------------- | ---------------- | ---------------- |
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
| 2025-07-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) |
| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) |
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) |
| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) |
| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) |
| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) |
| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) |
| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) |

1260
bun.lock

File diff suppressed because it is too large Load Diff

137
github/README.md Normal file
View File

@@ -0,0 +1,137 @@
# opencode GitHub Action
A GitHub Action that integrates [opencode](https://opencode.ai) directly into your GitHub workflow.
Mention `/opencode` in your comment, and opencode will execute tasks within your GitHub Actions runner.
## Features
#### Explain an issues
Leave the following comment on a GitHub issue. `opencode` will read the entire thread, including all comments, and reply with a clear explanation.
```
/opencode explain this issue
```
#### Fix an issues
Leave the following comment on a GitHub issue. opencode will create a new branch, implement the changes, and open a PR with the changes.
```
/opencode fix this
```
#### Review PRs and make changes
Leave the following comment on a GitHub PR. opencode will implement the requested change and commit it to the same PR.
```
Delete the attachment from S3 when the note is removed /oc
```
## Installation
Run the following command in the terminal from your GitHub repo:
```bash
opencode github install
```
This will walk you through installing the GitHub app, creating the workflow, and setting up secrets.
### Manual Setup
1. Install the GitHub app https://github.com/apps/opencode-agent. Make sure it is installed on the target repository.
2. Add the following workflow file to `.github/workflows/opencode.yml` in your repo. Set the appropriate `model` and required API keys in `env`.
```yml
name: opencode
on:
issue_comment:
types: [created]
jobs:
opencode:
if: |
contains(github.event.comment.body, '/oc') ||
contains(github.event.comment.body, '/opencode')
runs-on: ubuntu-latest
permissions:
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run opencode
uses: sst/opencode/github@latest
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
with:
model: anthropic/claude-sonnet-4-20250514
```
3. Store the API keys in secrets. In your organization or project **settings**, expand **Secrets and variables** on the left and select **Actions**. Add the required API keys.
## Support
This is an early release. If you encounter issues or have feedback, please create an issue at https://github.com/sst/opencode/issues.
## Development
To test locally:
1. Navigate to a test repo (e.g. `hello-world`):
```bash
cd hello-world
```
2. Run:
```bash
MODEL=anthropic/claude-sonnet-4-20250514 \
ANTHROPIC_API_KEY=sk-ant-api03-1234567890 \
GITHUB_RUN_ID=dummy \
bun /path/to/opencode/packages/opencode/src/index.ts github run \
--token 'github_pat_1234567890' \
--event '{"eventName":"issue_comment",...}'
```
- `MODEL`: The model used by opencode. Same as the `MODEL` defined in the GitHub workflow.
- `ANTHROPIC_API_KEY`: Your model provider API key. Same as the keys defined in the GitHub workflow.
- `GITHUB_RUN_ID`: Dummy value to emulate GitHub action environment.
- `/path/to/opencode`: Path to your cloned opencode repo. `bun /path/to/opencode/packages/opencode/src/index.ts` runs your local version of `opencode`.
- `--token`: A GitHub persontal access token. This token is used to verify you have `admin` or `write` access to the test repo. Generate a token [here](https://github.com/settings/personal-access-tokens).
- `--event`: Mock GitHub event payload (see templates below).
### Issue comment event
```
--event '{"eventName":"issue_comment","repo":{"owner":"sst","repo":"hello-world"},"actor":"fwang","payload":{"issue":{"number":4},"comment":{"id":1,"body":"hey opencode, summarize thread"}}}'
```
Replace:
- `"owner":"sst"` with repo owner
- `"repo":"hello-world"` with repo name
- `"actor":"fwang"` with the GitHub username of commentor
- `"number":4` with the GitHub issue id
- `"body":"hey opencode, summarize thread"` with comment body
### Issue comment with image attachment.
```
--event '{"eventName":"issue_comment","repo":{"owner":"sst","repo":"hello-world"},"actor":"fwang","payload":{"issue":{"number":4},"comment":{"id":1,"body":"hey opencode, what is in my image ![Image](https://github.com/user-attachments/assets/xxxxxxxx)"}}}'
```
Replace the image URL `https://github.com/user-attachments/assets/xxxxxxxx` with a valid GitHub attachment (you can generate one by commenting with an image in any issue).
### PR comment event
```
--event '{"eventName":"issue_comment","repo":{"owner":"sst","repo":"hello-world"},"actor":"fwang","payload":{"issue":{"number":4,"pull_request":{}},"comment":{"id":1,"body":"hey opencode, summarize thread"}}}'
```

29
github/action.yml Normal file
View File

@@ -0,0 +1,29 @@
name: "opencode GitHub Action"
description: "Run opencode in GitHub Actions workflows"
branding:
icon: "code"
color: "orange"
inputs:
model:
description: "Model to use"
required: true
share:
description: "Share the opencode session (defaults to true for public repos)"
required: false
runs:
using: "composite"
steps:
- name: Install opencode
shell: bash
run: curl -fsSL https://opencode.ai/install | bash
- name: Run opencode
shell: bash
id: run_opencode
run: opencode github run
env:
MODEL: ${{ inputs.model }}
SHARE: ${{ inputs.share }}

View File

@@ -8,8 +8,8 @@ if [ -z "$latest_tag" ]; then
fi
echo "Latest tag: $latest_tag"
# Update github-v1 to latest
git tag -d github-v1
git push origin :refs/tags/github-v1
git tag -a github-v1 $latest_tag -m "Update github-v1 to $latest_tag"
git push origin github-v1
# Update latest tag
git tag -d latest
git push origin :refs/tags/latest
git tag -a latest $latest_tag -m "Update latest to $latest_tag"
git push origin latest

View File

@@ -1,25 +1,11 @@
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"openrouter": {
"models": {
"moonshotai/kimi-k2": {
"options": {
"provider": {
"order": ["baseten"],
"allow_fallbacks": false
}
}
}
}
},
"huggingface": {
"models": {
"Qwen/Qwen3-235B-A22B-Instruct-2507:fireworks-ai": {}
}
}
},
"plugin": ["./packages/plugin/src/example.ts"],
"mcp": {
"context7": {
"type": "remote",
"url": "https://mcp.context7.com/sse"
},
"weather": {
"type": "local",
"command": ["opencode", "x", "@h1deya/mcp-server-weather"]

View File

@@ -8,17 +8,21 @@
"dev": "bun run packages/opencode/src/index.ts",
"typecheck": "bun run --filter='*' typecheck",
"stainless": "./scripts/stainless",
"postinstall": "./scripts/hooks"
"postinstall": "./script/hooks"
},
"workspaces": {
"packages": [
"packages/*"
"packages/*",
"packages/sdk/js"
],
"catalog": {
"typescript": "5.8.2",
"@types/node": "22.13.9",
"@tsconfig/node22": "22.0.2",
"ai": "5.0.0-beta.34",
"hono": "4.7.10",
"typescript": "5.8.2",
"zod": "3.25.49",
"ai": "5.0.0-beta.21"
"remeda": "2.26.0"
}
},
"devDependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode/function",
"version": "0.0.1",
"version": "0.3.122",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
@@ -12,6 +12,7 @@
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "22.0.0",
"hono": "catalog:",
"jose": "6.0.11"
}
}

View File

@@ -1,3 +1,4 @@
import { Hono } from "hono"
import { DurableObject } from "cloudflare:workers"
import { randomUUID } from "node:crypto"
import { jwtVerify, createRemoteJWKSet } from "jose"
@@ -111,165 +112,156 @@ export class SyncServer extends DurableObject<Env> {
}
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const url = new URL(request.url)
const splits = url.pathname.split("/")
const method = splits[1]
if (request.method === "GET" && method === "") {
return new Response("Hello, world!", {
headers: { "Content-Type": "text/plain" },
})
export default new Hono<{ Bindings: Env }>()
.get("/", (c) => c.text("Hello, world!"))
.post("/share_create", async (c) => {
const body = await c.req.json<{ sessionID: string }>()
const sessionID = body.sessionID
const short = SyncServer.shortName(sessionID)
const id = c.env.SYNC_SERVER.idFromName(short)
const stub = c.env.SYNC_SERVER.get(id)
const secret = await stub.share(sessionID)
return c.json({
secret,
url: `https://${c.env.WEB_DOMAIN}/s/${short}`,
})
})
.post("/share_delete", async (c) => {
const body = await c.req.json<{ sessionID: string; secret: string }>()
const sessionID = body.sessionID
const secret = body.secret
const id = c.env.SYNC_SERVER.idFromName(SyncServer.shortName(sessionID))
const stub = c.env.SYNC_SERVER.get(id)
await stub.assertSecret(secret)
await stub.clear()
return c.json({})
})
.post("/share_delete_admin", async (c) => {
const id = c.env.SYNC_SERVER.idFromName("oVF8Rsiv")
const stub = c.env.SYNC_SERVER.get(id)
await stub.clear()
return c.json({})
})
.post("/share_sync", async (c) => {
const body = await c.req.json<{
sessionID: string
secret: string
key: string
content: any
}>()
const name = SyncServer.shortName(body.sessionID)
const id = c.env.SYNC_SERVER.idFromName(name)
const stub = c.env.SYNC_SERVER.get(id)
await stub.assertSecret(body.secret)
await stub.publish(body.key, body.content)
return c.json({})
})
.get("/share_poll", async (c) => {
const upgradeHeader = c.req.header("Upgrade")
if (!upgradeHeader || upgradeHeader !== "websocket") {
return c.text("Error: Upgrade header is required", { status: 426 })
}
const id = c.req.query("id")
console.log("share_poll", id)
if (!id) return c.text("Error: Share ID is required", { status: 400 })
const stub = c.env.SYNC_SERVER.get(c.env.SYNC_SERVER.idFromName(id))
return stub.fetch(c.req.raw)
})
.get("/share_data", async (c) => {
const id = c.req.query("id")
console.log("share_data", id)
if (!id) return c.text("Error: Share ID is required", { status: 400 })
const stub = c.env.SYNC_SERVER.get(c.env.SYNC_SERVER.idFromName(id))
const data = await stub.getData()
if (request.method === "POST" && method === "share_create") {
const body = await request.json<any>()
const sessionID = body.sessionID
const short = SyncServer.shortName(sessionID)
const id = env.SYNC_SERVER.idFromName(short)
const stub = env.SYNC_SERVER.get(id)
const secret = await stub.share(sessionID)
return new Response(
JSON.stringify({
secret,
url: `https://${env.WEB_DOMAIN}/s/${short}`,
}),
{
headers: { "Content-Type": "application/json" },
},
)
}
if (request.method === "POST" && method === "share_delete") {
const body = await request.json<any>()
const sessionID = body.sessionID
const secret = body.secret
const id = env.SYNC_SERVER.idFromName(SyncServer.shortName(sessionID))
const stub = env.SYNC_SERVER.get(id)
await stub.assertSecret(secret)
await stub.clear()
return new Response(JSON.stringify({}), {
headers: { "Content-Type": "application/json" },
})
}
if (request.method === "POST" && method === "share_delete_admin") {
const id = env.SYNC_SERVER.idFromName("oVF8Rsiv")
const stub = env.SYNC_SERVER.get(id)
await stub.clear()
return new Response(JSON.stringify({}), {
headers: { "Content-Type": "application/json" },
})
}
if (request.method === "POST" && method === "share_sync") {
const body = await request.json<{
sessionID: string
secret: string
key: string
content: any
}>()
const name = SyncServer.shortName(body.sessionID)
const id = env.SYNC_SERVER.idFromName(name)
const stub = env.SYNC_SERVER.get(id)
await stub.assertSecret(body.secret)
await stub.publish(body.key, body.content)
return new Response(JSON.stringify({}), {
headers: { "Content-Type": "application/json" },
})
}
if (request.method === "GET" && method === "share_poll") {
const upgradeHeader = request.headers.get("Upgrade")
if (!upgradeHeader || upgradeHeader !== "websocket") {
return new Response("Error: Upgrade header is required", {
status: 426,
})
let info
const messages: Record<string, any> = {}
data.forEach((d) => {
const [root, type, ...splits] = d.key.split("/")
if (root !== "session") return
if (type === "info") {
info = d.content
return
}
const id = url.searchParams.get("id")
console.log("share_poll", id)
if (!id) return new Response("Error: Share ID is required", { status: 400 })
const stub = env.SYNC_SERVER.get(env.SYNC_SERVER.idFromName(id))
return stub.fetch(request)
}
if (type === "message") {
messages[d.content.id] = {
parts: [],
...d.content,
}
}
if (type === "part") {
messages[d.content.messageID].parts.push(d.content)
}
})
if (request.method === "GET" && method === "share_data") {
const id = url.searchParams.get("id")
console.log("share_data", id)
if (!id) return new Response("Error: Share ID is required", { status: 400 })
const stub = env.SYNC_SERVER.get(env.SYNC_SERVER.idFromName(id))
const data = await stub.getData()
return c.json({ info, messages })
})
/**
* Used by the GitHub action to get GitHub installation access token given the OIDC token
*/
.post("/exchange_github_app_token", async (c) => {
const EXPECTED_AUDIENCE = "opencode-github-action"
const GITHUB_ISSUER = "https://token.actions.githubusercontent.com"
const JWKS_URL = `${GITHUB_ISSUER}/.well-known/jwks`
let info
const messages: Record<string, any> = {}
data.forEach((d) => {
const [root, type, ...splits] = d.key.split("/")
if (root !== "session") return
if (type === "info") {
info = d.content
return
}
if (type === "message") {
messages[d.content.id] = {
parts: [],
...d.content,
}
}
if (type === "part") {
messages[d.content.messageID].parts.push(d.content)
}
// get Authorization header
const token = c.req.header("Authorization")?.replace(/^Bearer /, "")
if (!token) return c.json({ error: "Authorization header is required" }, { status: 401 })
// verify token
const JWKS = createRemoteJWKSet(new URL(JWKS_URL))
let owner, repo
try {
const { payload } = await jwtVerify(token, JWKS, {
issuer: GITHUB_ISSUER,
audience: EXPECTED_AUDIENCE,
})
return new Response(
JSON.stringify({
info,
messages,
}),
{
headers: { "Content-Type": "application/json" },
},
)
const sub = payload.sub // e.g. 'repo:my-org/my-repo:ref:refs/heads/main'
const parts = sub.split(":")[1].split("/")
owner = parts[0]
repo = parts[1]
} catch (err) {
console.error("Token verification failed:", err)
return c.json({ error: "Invalid or expired token" }, { status: 403 })
}
/**
* Used by the GitHub action to get GitHub installation access token given the OIDC token
*/
if (request.method === "POST" && method === "exchange_github_app_token") {
const EXPECTED_AUDIENCE = "opencode-github-action"
const GITHUB_ISSUER = "https://token.actions.githubusercontent.com"
const JWKS_URL = `${GITHUB_ISSUER}/.well-known/jwks`
// Create app JWT token
const auth = createAppAuth({
appId: Resource.GITHUB_APP_ID.value,
privateKey: Resource.GITHUB_APP_PRIVATE_KEY.value,
})
const appAuth = await auth({ type: "app" })
// Lookup installation
const octokit = new Octokit({ auth: appAuth.token })
const { data: installation } = await octokit.apps.getRepoInstallation({ owner, repo })
// Get installation token
const installationAuth = await auth({ type: "installation", installationId: installation.id })
return c.json({ token: installationAuth.token })
})
/**
* Used by the GitHub action to get GitHub installation access token given user PAT token (used when testing `opencode github run` locally)
*/
.post("/exchange_github_app_token_with_pat", async (c) => {
const body = await c.req.json<{ owner: string; repo: string }>()
const owner = body.owner
const repo = body.repo
try {
// get Authorization header
const authHeader = request.headers.get("Authorization")
const authHeader = c.req.header("Authorization")
const token = authHeader?.replace(/^Bearer /, "")
if (!token)
return new Response(JSON.stringify({ error: "Authorization header is required" }), {
status: 401,
headers: { "Content-Type": "application/json" },
})
if (!token) throw new Error("Authorization header is required")
// verify token
const JWKS = createRemoteJWKSet(new URL(JWKS_URL))
let owner, repo
try {
const { payload } = await jwtVerify(token, JWKS, {
issuer: GITHUB_ISSUER,
audience: EXPECTED_AUDIENCE,
})
const sub = payload.sub // e.g. 'repo:my-org/my-repo:ref:refs/heads/main'
const parts = sub.split(":")[1].split("/")
owner = parts[0]
repo = parts[1]
} catch (err) {
console.error("Token verification failed:", err)
return new Response(JSON.stringify({ error: "Invalid or expired token" }), {
status: 403,
headers: { "Content-Type": "application/json" },
})
}
// Verify permissions
const userClient = new Octokit({ auth: token })
const { data: repoData } = await userClient.repos.get({ owner, repo })
if (!repoData.permissions.admin && !repoData.permissions.push && !repoData.permissions.maintain)
throw new Error("User does not have write permissions")
// Create app JWT token
// Get installation token
const auth = createAppAuth({
appId: Resource.GITHUB_APP_ID.value,
privateKey: Resource.GITHUB_APP_PRIVATE_KEY.value,
@@ -277,99 +269,49 @@ export default {
const appAuth = await auth({ type: "app" })
// Lookup installation
const octokit = new Octokit({ auth: appAuth.token })
const { data: installation } = await octokit.apps.getRepoInstallation({ owner, repo })
const appClient = new Octokit({ auth: appAuth.token })
const { data: installation } = await appClient.apps.getRepoInstallation({ owner, repo })
// Get installation token
const installationAuth = await auth({ type: "installation", installationId: installation.id })
return new Response(JSON.stringify({ token: installationAuth.token }), {
headers: { "Content-Type": "application/json" },
})
return c.json({ token: installationAuth.token })
} catch (e: any) {
let error = e
if (e instanceof Error) {
error = e.message
}
return c.json({ error }, { status: 401 })
}
})
/**
* Used by the opencode CLI to check if the GitHub app is installed
*/
.get("/get_github_app_installation", async (c) => {
const owner = c.req.query("owner")
const repo = c.req.query("repo")
/**
* Used by the GitHub action to get GitHub installation access token given user PAT token (used when testing `opencode github run` locally)
*/
if (request.method === "POST" && method === "exchange_github_app_token_with_pat") {
const body = await request.json<any>()
const owner = body.owner
const repo = body.repo
const auth = createAppAuth({
appId: Resource.GITHUB_APP_ID.value,
privateKey: Resource.GITHUB_APP_PRIVATE_KEY.value,
})
const appAuth = await auth({ type: "app" })
try {
// get Authorization header
const authHeader = request.headers.get("Authorization")
const token = authHeader?.replace(/^Bearer /, "")
if (!token) throw new Error("Authorization header is required")
// Verify permissions
const userClient = new Octokit({ auth: token })
const { data: repoData } = await userClient.repos.get({ owner, repo })
if (!repoData.permissions.admin && !repoData.permissions.push && !repoData.permissions.maintain)
throw new Error("User does not have write permissions")
// Get installation token
const auth = createAppAuth({
appId: Resource.GITHUB_APP_ID.value,
privateKey: Resource.GITHUB_APP_PRIVATE_KEY.value,
})
const appAuth = await auth({ type: "app" })
// Lookup installation
const appClient = new Octokit({ auth: appAuth.token })
const { data: installation } = await appClient.apps.getRepoInstallation({ owner, repo })
// Get installation token
const installationAuth = await auth({ type: "installation", installationId: installation.id })
return new Response(JSON.stringify({ token: installationAuth.token }), {
headers: { "Content-Type": "application/json" },
})
} catch (e: any) {
let error = e
if (e instanceof Error) {
error = e.message
}
return new Response(JSON.stringify({ error }), {
status: 401,
headers: { "Content-Type": "application/json" },
})
// Lookup installation
const octokit = new Octokit({ auth: appAuth.token })
let installation
try {
const ret = await octokit.apps.getRepoInstallation({ owner, repo })
installation = ret.data
} catch (err) {
if (err instanceof Error && err.message.includes("Not Found")) {
// not installed
} else {
throw err
}
}
/**
* Used by the opencode CLI to check if the GitHub app is installed
*/
if (request.method === "GET" && method === "get_github_app_installation") {
const owner = url.searchParams.get("owner")
const repo = url.searchParams.get("repo")
const auth = createAppAuth({
appId: Resource.GITHUB_APP_ID.value,
privateKey: Resource.GITHUB_APP_PRIVATE_KEY.value,
})
const appAuth = await auth({ type: "app" })
// Lookup installation
const octokit = new Octokit({ auth: appAuth.token })
let installation
try {
const ret = await octokit.apps.getRepoInstallation({ owner, repo })
installation = ret.data
} catch (err) {
if (err instanceof Error && err.message.includes("Not Found")) {
// not installed
} else {
throw err
}
}
return new Response(JSON.stringify({ installation }), {
headers: { "Content-Type": "application/json" },
})
}
return new Response("Not Found", { status: 404 })
},
}
return c.json({ installation })
})
.all("*", (c) => c.text("Not Found"))

View File

@@ -1,12 +1,12 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "0.0.5",
"version": "0.3.122",
"name": "opencode",
"type": "module",
"private": true,
"scripts": {
"typecheck": "tsc --noEmit",
"dev": "bun run ./src/index.ts"
"dev": "bun run --conditions=development ./src/index.ts"
},
"bin": {
"opencode": "./bin/opencode"
@@ -17,6 +17,7 @@
"devDependencies": {
"@ai-sdk/amazon-bedrock": "2.2.10",
"@ai-sdk/anthropic": "1.2.12",
"@octokit/webhooks-types": "7.6.1",
"@standard-schema/spec": "1.0.0",
"@tsconfig/bun": "1.0.7",
"@types/bun": "latest",
@@ -27,21 +28,31 @@
"zod-to-json-schema": "3.24.5"
},
"dependencies": {
"@clack/prompts": "0.11.0",
"@actions/core": "1.11.1",
"@actions/github": "6.0.1",
"@clack/prompts": "1.0.0-alpha.1",
"@hono/zod-validator": "0.4.2",
"@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:*",
"@standard-schema/spec": "1.0.0",
"@zip.js/zip.js": "2.7.62",
"ai": "catalog:",
"decimal.js": "10.5.0",
"diff": "8.0.2",
"gray-matter": "4.0.3",
"hono": "4.7.10",
"hono": "catalog:",
"hono-openapi": "0.4.8",
"isomorphic-git": "1.32.1",
"jsonc-parser": "3.3.1",
"minimatch": "10.0.3",
"open": "10.1.2",
"remeda": "2.22.3",
"remeda": "catalog:",
"tree-sitter": "0.22.4",
"tree-sitter-bash": "0.23.3",
"turndown": "7.2.0",
"vscode-jsonrpc": "8.2.1",
"xdg-basedir": "5.1.0",

View File

@@ -1,21 +1,13 @@
#!/usr/bin/env bun
const dir = new URL("..", import.meta.url).pathname
process.chdir(dir)
import { $ } from "bun"
import pkg from "../package.json"
const dry = process.argv.includes("--dry")
const snapshot = process.argv.includes("--snapshot")
const version = snapshot
? `0.0.0-${new Date().toISOString().slice(0, 16).replace(/[-:T]/g, "")}`
: await $`git describe --tags --abbrev=0`
.text()
.then((x) => x.substring(1).trim())
.catch(() => {
console.error("tag not found")
process.exit(1)
})
const dry = process.env["OPENCODE_DRY"] === "true"
const version = process.env["OPENCODE_VERSION"]!
const snapshot = process.env["OPENCODE_SNAPSHOT"] === "true"
console.log(`publishing ${version}`)
@@ -26,12 +18,13 @@ const GOARCH: Record<string, string> = {
}
const targets = [
["windows", "x64"],
["linux", "arm64"],
["linux", "x64"],
["linux", "x64-baseline"],
["darwin", "x64"],
["darwin", "x64-baseline"],
["darwin", "arm64"],
["windows", "x64"],
]
await $`rm -rf dist`
@@ -45,7 +38,7 @@ for (const [os, arch] of targets) {
await $`CGO_ENABLED=0 GOOS=${os} GOARCH=${GOARCH[arch]} go build -ldflags="-s -w -X main.Version=${version}" -o ../opencode/dist/${name}/bin/tui ../tui/cmd/opencode/main.go`.cwd(
"../tui",
)
await $`bun build --define OPENCODE_VERSION="'${version}'" --compile --minify --target=bun-${os}-${arch} --outfile=dist/${name}/bin/opencode ./src/index.ts ./dist/${name}/bin/tui`
await $`bun build --define OPENCODE_TUI_PATH="'../../../dist/${name}/bin/tui'" --define OPENCODE_VERSION="'${version}'" --compile --target=bun-${os}-${arch} --outfile=dist/${name}/bin/opencode ./src/index.ts`
await $`rm -rf ./dist/${name}/bin/tui`
await Bun.file(`dist/${name}/package.json`).write(
JSON.stringify(
@@ -59,7 +52,7 @@ for (const [os, arch] of targets) {
2,
),
)
if (!dry) await $`cd dist/${name} && bun publish --access public --tag ${npmTag}`
if (!dry) await $`cd dist/${name} && chmod 777 -R . && bun publish --access public --tag ${npmTag}`
optionalDependencies[name] = version
}
@@ -111,6 +104,7 @@ if (!snapshot) {
.filter((x: string) => {
const lower = x.toLowerCase()
return (
!lower.includes("release:") &&
!lower.includes("ignore:") &&
!lower.includes("chore:") &&
!lower.includes("ci:") &&

View File

@@ -16,7 +16,13 @@ export namespace Auth {
key: z.string(),
})
export const Info = z.discriminatedUnion("type", [Oauth, Api])
export const WellKnown = z.object({
type: z.literal("wellknown"),
key: z.string(),
token: z.string(),
})
export const Info = z.discriminatedUnion("type", [Oauth, Api, WellKnown])
export type Info = z.infer<typeof Info>
const filepath = path.join(Global.Path.data, "auth.json")

View File

@@ -73,6 +73,7 @@ export namespace BunProc {
// Let Bun handle registry resolution:
// - If .npmrc files exist, Bun will use them automatically
// - If no .npmrc files exist, Bun will default to https://registry.npmjs.org
// - No need to pass --registry flag
log.info("installing package using Bun's default registry resolution", { pkg, version })
await BunProc.run(args, {

View File

@@ -2,6 +2,7 @@ import { App } from "../app/app"
import { ConfigHooks } from "../config/hooks"
import { Format } from "../format"
import { LSP } from "../lsp"
import { Plugin } from "../plugin"
import { Share } from "../share/share"
import { Snapshot } from "../snapshot"
@@ -9,6 +10,7 @@ export async function bootstrap<T>(input: App.Input, cb: (app: App.Info) => Prom
return App.provide(input, async (app) => {
Share.init()
Format.init()
Plugin.init()
ConfigHooks.init()
LSP.init()
Snapshot.init()

View File

@@ -39,7 +39,7 @@ const AgentCreateCommand = cmd({
const query = await prompts.text({
message: "Description",
placeholder: "What should this agent do?",
validate: (x) => (x.length > 0 ? undefined : "Required"),
validate: (x) => x && (x.length > 0 ? undefined : "Required"),
})
if (prompts.isCancel(query)) throw new UI.CancelledError()

View File

@@ -61,17 +61,46 @@ export const AuthListCommand = cmd({
prompts.log.info(`${provider} ${UI.Style.TEXT_DIM}${envVar}`)
}
prompts.outro(`${activeEnvVars.length} environment variables`)
prompts.outro(`${activeEnvVars.length} environment variable` + (activeEnvVars.length === 1 ? "" : "s"))
}
},
})
export const AuthLoginCommand = cmd({
command: "login",
command: "login [url]",
describe: "log in to a provider",
async handler() {
builder: (yargs) =>
yargs.positional("url", {
describe: "opencode auth provider",
type: "string",
}),
async handler(args) {
UI.empty()
prompts.intro("Add credential")
if (args.url) {
const wellknown = await fetch(`${args.url}/.well-known/opencode`).then((x) => x.json())
prompts.log.info(`Running \`${wellknown.auth.command.join(" ")}\``)
const proc = Bun.spawn({
cmd: wellknown.auth.command,
stdout: "pipe",
})
const exit = await proc.exited
if (exit !== 0) {
prompts.log.error("Failed")
prompts.outro("Done")
return
}
const token = await new Response(proc.stdout).text()
await Auth.set(args.url, {
type: "wellknown",
key: wellknown.auth.env,
token: token.trim(),
})
prompts.log.success("Logged into " + args.url)
prompts.outro("Done")
return
}
await ModelsDev.refresh().catch(() => {})
const providers = await ModelsDev.get()
const priority: Record<string, number> = {
anthropic: 0,
@@ -81,7 +110,7 @@ export const AuthLoginCommand = cmd({
openrouter: 4,
vercel: 5,
}
let provider = await prompts.select({
let provider = await prompts.autocomplete({
message: "Select provider",
maxItems: 8,
options: [
@@ -110,7 +139,7 @@ export const AuthLoginCommand = cmd({
if (provider === "other") {
provider = await prompts.text({
message: "Enter provider id",
validate: (x) => (x.match(/^[0-9a-z-]+$/) ? undefined : "a-z, 0-9 and hyphens only"),
validate: (x) => x && (x.match(/^[0-9a-z-]+$/) ? undefined : "a-z, 0-9 and hyphens only"),
})
if (prompts.isCancel(provider)) throw new UI.CancelledError()
provider = provider.replace(/^@ai-sdk\//, "")
@@ -164,7 +193,7 @@ export const AuthLoginCommand = cmd({
const code = await prompts.text({
message: "Paste the authorization code here: ",
validate: (x) => (x.length > 0 ? undefined : "Required"),
validate: (x) => x && (x.length > 0 ? undefined : "Required"),
})
if (prompts.isCancel(code)) throw new UI.CancelledError()
@@ -200,7 +229,7 @@ export const AuthLoginCommand = cmd({
const code = await prompts.text({
message: "Paste the authorization code here: ",
validate: (x) => (x.length > 0 ? undefined : "Required"),
validate: (x) => x && (x.length > 0 ? undefined : "Required"),
})
if (prompts.isCancel(code)) throw new UI.CancelledError()
@@ -273,7 +302,7 @@ export const AuthLoginCommand = cmd({
const key = await prompts.password({
message: "Enter your API key",
validate: (x) => (x.length > 0 ? undefined : "Required"),
validate: (x) => x && (x.length > 0 ? undefined : "Required"),
})
if (prompts.isCancel(key)) throw new UI.CancelledError()
await Auth.set(provider, {

View File

@@ -4,7 +4,7 @@ import { cmd } from "../cmd"
export const SnapshotCommand = cmd({
command: "snapshot",
builder: (yargs) => yargs.command(TrackCommand).command(PatchCommand).demandCommand(),
builder: (yargs) => yargs.command(TrackCommand).command(PatchCommand).command(DiffCommand).demandCommand(),
async handler() {},
})
@@ -31,3 +31,18 @@ const PatchCommand = cmd({
})
},
})
const DiffCommand = cmd({
command: "diff <hash>",
builder: (yargs) =>
yargs.positional("hash", {
type: "string",
description: "hash",
demandOption: true,
}),
async handler(args) {
await bootstrap({ cwd: process.cwd() }, async () => {
console.log(await Snapshot.diff(args.hash))
})
},
})

View File

@@ -1,14 +1,10 @@
import { Server } from "../../server/server"
import fs from "fs/promises"
import type { CommandModule } from "yargs"
export const GenerateCommand = {
command: "generate",
handler: async () => {
const specs = await Server.openapi()
const dir = "gen"
await fs.rmdir(dir, { recursive: true }).catch(() => {})
await fs.mkdir(dir, { recursive: true })
process.stdout.write(JSON.stringify(specs, null, 2))
},
} satisfies CommandModule

File diff suppressed because it is too large Load Diff

View File

@@ -1,221 +0,0 @@
import { $ } from "bun"
import path from "path"
import { exec } from "child_process"
import * as prompts from "@clack/prompts"
import { map, pipe, sortBy, values } from "remeda"
import { UI } from "../ui"
import { cmd } from "./cmd"
import { ModelsDev } from "../../provider/models"
import { App } from "../../app/app"
const WORKFLOW_FILE = ".github/workflows/opencode.yml"
export const InstallGithubCommand = cmd({
command: "install-github",
describe: "install the GitHub agent",
async handler() {
await App.provide({ cwd: process.cwd() }, async () => {
UI.empty()
prompts.intro("Install GitHub agent")
const app = await getAppInfo()
await installGitHubApp()
const providers = await ModelsDev.get()
const provider = await promptProvider()
const model = await promptModel()
//const key = await promptKey()
await addWorkflowFiles()
printNextSteps()
function printNextSteps() {
let step2
if (provider === "amazon-bedrock") {
step2 =
"Configure OIDC in AWS - https://docs.github.com/en/actions/how-tos/security-for-github-actions/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services"
} else {
const url = `https://github.com/organizations/${app.owner}/settings/secrets/actions`
const env = providers[provider].env
const envStr =
env.length === 1
? `\`${env[0]}\` secret`
: `\`${[env.slice(0, -1).join("\`, \`"), ...env.slice(-1)].join("\` and \`")}\` secrets`
step2 = `Add ${envStr} for ${providers[provider].name} - ${url}`
}
prompts.outro(
[
"Next steps:",
` 1. Commit "${WORKFLOW_FILE}" file and push`,
` 2. ${step2}`,
" 3. Learn how to use the GitHub agent - https://docs.opencode.ai/docs/github/getting-started",
].join("\n"),
)
}
async function getAppInfo() {
const app = App.info()
if (!app.git) {
prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
throw new UI.CancelledError()
}
// Get repo info
const info = await $`git remote get-url origin`.quiet().nothrow().text()
// match https or git pattern
// ie. https://github.com/sst/opencode.git
// ie. git@github.com:sst/opencode.git
const parsed = info.match(/git@github\.com:(.*)\.git/) ?? info.match(/github\.com\/(.*)\.git/)
if (!parsed) {
prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
throw new UI.CancelledError()
}
const [owner, repo] = parsed[1].split("/")
return { owner, repo, root: app.path.root }
}
async function promptProvider() {
const priority: Record<string, number> = {
anthropic: 0,
"github-copilot": 1,
openai: 2,
google: 3,
}
let provider = await prompts.select({
message: "Select provider",
maxItems: 8,
options: pipe(
providers,
values(),
sortBy(
(x) => priority[x.id] ?? 99,
(x) => x.name ?? x.id,
),
map((x) => ({
label: x.name,
value: x.id,
hint: priority[x.id] === 0 ? "recommended" : undefined,
})),
),
})
if (prompts.isCancel(provider)) throw new UI.CancelledError()
return provider
}
async function promptModel() {
const providerData = providers[provider]!
const model = await prompts.select({
message: "Select model",
maxItems: 8,
options: pipe(
providerData.models,
values(),
sortBy((x) => x.name ?? x.id),
map((x) => ({
label: x.name ?? x.id,
value: x.id,
})),
),
})
if (prompts.isCancel(model)) throw new UI.CancelledError()
return model
}
async function installGitHubApp() {
const s = prompts.spinner()
s.start("Installing GitHub app")
// Get installation
const installation = await getInstallation()
if (installation) return s.stop("GitHub app already installed")
// Open browser
const url = "https://github.com/apps/opencode-agent"
const command =
process.platform === "darwin"
? `open "${url}"`
: process.platform === "win32"
? `start "${url}"`
: `xdg-open "${url}"`
exec(command, (error) => {
if (error) {
prompts.log.warn(`Could not open browser. Please visit: ${url}`)
}
})
// Wait for installation
s.message("Waiting for GitHub app to be installed")
const MAX_RETRIES = 60
let retries = 0
do {
const installation = await getInstallation()
if (installation) break
if (retries > MAX_RETRIES) {
s.stop(
`Failed to detect GitHub app installation. Make sure to install the app for the \`${app.owner}/${app.repo}\` repository.`,
)
throw new UI.CancelledError()
}
retries++
await new Promise((resolve) => setTimeout(resolve, 1000))
} while (true)
s.stop("Installed GitHub app")
async function getInstallation() {
return await fetch(`https://api.opencode.ai/get_github_app_installation?owner=${app.owner}&repo=${app.repo}`)
.then((res) => res.json())
.then((data) => data.installation)
}
}
async function addWorkflowFiles() {
const envStr =
provider === "amazon-bedrock"
? ""
: `\n env:${providers[provider].env.map((e) => `\n ${e}: \${{ secrets.${e} }}`).join("")}`
await Bun.write(
path.join(app.root, WORKFLOW_FILE),
`
name: opencode
on:
issue_comment:
types: [created]
jobs:
opencode:
if: |
startsWith(github.event.comment.body, 'opencode') ||
startsWith(github.event.comment.body, 'hi opencode') ||
startsWith(github.event.comment.body, 'hey opencode') ||
contains(github.event.comment.body, '@opencode-agent')
runs-on: ubuntu-latest
permissions:
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run opencode
uses: sst/opencode/sdks/github@github-v1${envStr}
with:
model: ${provider}/${model}
`.trim(),
)
prompts.log.success(`Added workflow file: "${WORKFLOW_FILE}"`)
}
})
},
})

View File

@@ -19,7 +19,7 @@ export const McpAddCommand = cmd({
const name = await prompts.text({
message: "Enter MCP server name",
validate: (x) => (x.length > 0 ? undefined : "Required"),
validate: (x) => x && (x.length > 0 ? undefined : "Required"),
})
if (prompts.isCancel(name)) throw new UI.CancelledError()
@@ -44,7 +44,7 @@ export const McpAddCommand = cmd({
const command = await prompts.text({
message: "Enter command to run",
placeholder: "e.g., opencode x @modelcontextprotocol/server-filesystem",
validate: (x) => (x.length > 0 ? undefined : "Required"),
validate: (x) => x && (x.length > 0 ? undefined : "Required"),
})
if (prompts.isCancel(command)) throw new UI.CancelledError()
@@ -58,6 +58,7 @@ export const McpAddCommand = cmd({
message: "Enter MCP server URL",
placeholder: "e.g., https://example.com/mcp",
validate: (x) => {
if (!x) return "Required"
if (x.length === 0) return "Required"
const isValid = URL.canParse(x)
return isValid ? undefined : "Invalid URL"

View File

@@ -14,6 +14,16 @@ import { FileWatcher } from "../../file/watch"
import { Mode } from "../../session/mode"
import { Ide } from "../../ide"
declare global {
const OPENCODE_TUI_PATH: string
}
if (typeof OPENCODE_TUI_PATH !== "undefined") {
await import(OPENCODE_TUI_PATH as string, {
with: { type: "file" },
})
}
export const TuiCommand = cmd({
command: "$0 [project]",
describe: "start opencode tui",
@@ -71,16 +81,16 @@ export const TuiCommand = cmd({
let cmd = ["go", "run", "./main.go"]
let cwd = Bun.fileURLToPath(new URL("../../../../tui/cmd/opencode", import.meta.url))
if (Bun.embeddedFiles.length > 0) {
const blob = Bun.embeddedFiles[0] as File
let binaryName = blob.name
const tui = Bun.embeddedFiles.find((item) => (item as File).name.includes("tui")) as File
if (tui) {
let binaryName = tui.name
if (process.platform === "win32" && !binaryName.endsWith(".exe")) {
binaryName += ".exe"
}
const binary = path.join(Global.Path.cache, "tui", binaryName)
const file = Bun.file(binary)
if (!(await file.exists())) {
await Bun.write(file, blob, { mode: 0o755 })
await Bun.write(file, tui, { mode: 0o755 })
await fs.chmod(binary, 0o755)
}
cwd = process.cwd()
@@ -113,7 +123,7 @@ export const TuiCommand = cmd({
})
;(async () => {
if (Installation.VERSION === "dev") return
if (Installation.isDev()) return
if (Installation.isSnapshot()) return
const config = await Config.global()
if (config.autoupdate === false) return
@@ -123,19 +133,15 @@ export const TuiCommand = cmd({
const method = await Installation.method()
if (method === "unknown") return
await Installation.upgrade(method, latest)
.then(() => {
Bus.publish(Installation.Event.Updated, { version: latest })
})
.then(() => Bus.publish(Installation.Event.Updated, { version: latest }))
.catch(() => {})
})()
;(async () => {
if (Ide.alreadyInstalled()) return
const ide = await Ide.ide()
const ide = Ide.ide()
if (ide === "unknown") return
await Ide.install(ide)
.then(() => {
Bus.publish(Ide.Event.Installed, { ide })
})
.then(() => Bus.publish(Ide.Event.Installed, { ide }))
.catch(() => {})
})()

View File

@@ -32,7 +32,7 @@ export const UpgradeCommand = {
return
}
prompts.log.info("Using method: " + method)
const target = args.target ?? (await Installation.latest())
const target = args.target ? args.target.replace(/^v/, "") : await Installation.latest()
if (Installation.VERSION === target) {
prompts.log.warn(`opencode upgrade skipped: ${target} is already installed`)

View File

@@ -5,7 +5,11 @@ import { UI } from "./ui"
export function FormatError(input: unknown) {
if (MCP.Failed.isInstance(input))
return `MCP server "${input.data.name}" failed. Note, opencode does not support MCP authentication yet.`
if (Config.JsonError.isInstance(input)) return `Config file at ${input.data.path} is not valid JSON`
if (Config.JsonError.isInstance(input)) {
return (
`Config file at ${input.data.path} is not valid JSON(C)` + (input.data.message ? `: ${input.data.message}` : "")
)
}
if (Config.InvalidError.isInstance(input))
return [
`Config file at ${input.data.path} is invalid`,

View File

@@ -1,5 +1,6 @@
import { Log } from "../util/log"
import path from "path"
import os from "os"
import { z } from "zod"
import { App } from "../app/app"
import { Filesystem } from "../util/filesystem"
@@ -10,16 +11,34 @@ import fs from "fs/promises"
import { lazy } from "../util/lazy"
import { NamedError } from "../util/error"
import matter from "gray-matter"
import { Flag } from "../flag/flag"
import { Auth } from "../auth"
import { type ParseError as JsoncParseError, parse as parseJsonc, printParseErrorCode } from "jsonc-parser"
export namespace Config {
const log = Log.create({ service: "config" })
export const state = App.state("config", async (app) => {
const auth = await Auth.all()
let result = await global()
for (const file of ["opencode.jsonc", "opencode.json"]) {
const found = await Filesystem.findUp(file, app.path.cwd, app.path.root)
for (const resolved of found.toReversed()) {
result = mergeDeep(result, await load(resolved))
result = mergeDeep(result, await loadFile(resolved))
}
}
// Override with custom config if provided
if (Flag.OPENCODE_CONFIG) {
result = mergeDeep(result, await loadFile(Flag.OPENCODE_CONFIG))
log.debug("loaded custom config", { path: Flag.OPENCODE_CONFIG })
}
for (const [key, value] of Object.entries(auth)) {
if (value.type === "wellknown") {
process.env[value.key] = value.token
const wellknown = await fetch(`${key}/.well-known/opencode`).then((x) => x.json())
result = mergeDeep(result, await load(JSON.stringify(wellknown.config ?? {}), process.cwd()))
}
}
@@ -48,6 +67,32 @@ export namespace Config {
throw new InvalidError({ path: item }, { cause: parsed.error })
}
// Load mode markdown files
result.mode = result.mode || {}
const markdownModes = [
...(await Filesystem.globUp("mode/*.md", Global.Path.config, Global.Path.config)),
...(await Filesystem.globUp(".opencode/mode/*.md", app.path.cwd, app.path.root)),
]
for (const item of markdownModes) {
const content = await Bun.file(item).text()
const md = matter(content)
if (!md.data) continue
const config = {
name: path.basename(item, ".md"),
...md.data,
prompt: md.content.trim(),
}
const parsed = Mode.safeParse(config)
if (parsed.success) {
result.mode = mergeDeep(result.mode, {
[config.name]: parsed.data,
})
continue
}
throw new InvalidError({ path: item }, { cause: parsed.error })
}
// Handle migration from autoshare to share field
if (result.autoshare === true && !result.share) {
result.share = "auto"
@@ -99,6 +144,8 @@ export namespace Config {
export const Mode = z
.object({
model: z.string().optional(),
temperature: z.number().optional(),
top_p: z.number().optional(),
prompt: z.string().optional(),
tools: z.record(z.string(), z.boolean()).optional(),
disable: z.boolean().optional(),
@@ -169,11 +216,15 @@ export namespace Config {
})
export type Layout = z.infer<typeof Layout>
export const Permission = z.union([z.literal("ask"), z.literal("allow")])
export type Permission = z.infer<typeof Permission>
export const Info = z
.object({
$schema: z.string().optional().describe("JSON schema reference for configuration validation"),
theme: z.string().optional().describe("Theme name to use for the interface"),
keybinds: Keybinds.optional().describe("Custom keybind configurations"),
plugin: z.string().array().optional(),
share: z
.enum(["manual", "auto", "disabled"])
.optional()
@@ -230,8 +281,42 @@ export namespace Config {
.optional()
.describe("Custom provider configurations and model overrides"),
mcp: z.record(z.string(), Mcp).optional().describe("MCP (Model Context Protocol) server configurations"),
formatter: z
.record(
z.string(),
z.object({
disabled: z.boolean().optional(),
command: z.array(z.string()).optional(),
environment: z.record(z.string(), z.string()).optional(),
extensions: z.array(z.string()).optional(),
}),
)
.optional(),
lsp: z
.record(
z.string(),
z.union([
z.object({
disabled: z.literal(true),
}),
z.object({
command: z.array(z.string()),
extensions: z.array(z.string()).optional(),
disabled: z.boolean().optional(),
env: z.record(z.string(), z.string()).optional(),
initialization: z.record(z.string(), z.any()).optional(),
}),
]),
)
.optional(),
instructions: z.array(z.string()).optional().describe("Additional instruction files or patterns to include"),
layout: Layout.optional().describe("@deprecated Always uses stretch layout."),
permission: z
.object({
edit: Permission.optional(),
bash: z.union([Permission, z.record(z.string(), Permission)]).optional(),
})
.optional(),
experimental: z
.object({
hook: z
@@ -267,10 +352,11 @@ export namespace Config {
export type Info = z.output<typeof Info>
export const global = lazy(async () => {
let result = pipe(
let result: Info = pipe(
{},
mergeDeep(await load(path.join(Global.Path.config, "config.json"))),
mergeDeep(await load(path.join(Global.Path.config, "opencode.json"))),
mergeDeep(await loadFile(path.join(Global.Path.config, "config.json"))),
mergeDeep(await loadFile(path.join(Global.Path.config, "opencode.json"))),
mergeDeep(await loadFile(path.join(Global.Path.config, "opencode.jsonc"))),
)
await import(path.join(Global.Path.config, "config"), {
@@ -291,51 +377,93 @@ export namespace Config {
return result
})
async function load(configPath: string) {
let text = await Bun.file(configPath)
async function loadFile(filepath: string): Promise<Info> {
log.info("loading", { path: filepath })
let text = await Bun.file(filepath)
.text()
.catch((err) => {
if (err.code === "ENOENT") return
throw new JsonError({ path: configPath }, { cause: err })
throw new JsonError({ path: filepath }, { cause: err })
})
if (!text) return {}
return load(text, filepath)
}
async function load(text: string, filepath: string) {
text = text.replace(/\{env:([^}]+)\}/g, (_, varName) => {
return process.env[varName] || ""
})
const fileMatches = text.match(/"?\{file:([^}]+)\}"?/g)
const fileMatches = text.match(/\{file:[^}]+\}/g)
if (fileMatches) {
const configDir = path.dirname(configPath)
const configDir = path.dirname(filepath)
const lines = text.split("\n")
for (const match of fileMatches) {
const filePath = match.replace(/^"?\{file:/, "").replace(/\}"?$/, "")
const lineIndex = lines.findIndex((line) => line.includes(match))
if (lineIndex !== -1 && lines[lineIndex].trim().startsWith("//")) {
continue // Skip if line is commented
}
let filePath = match.replace(/^\{file:/, "").replace(/\}$/, "")
if (filePath.startsWith("~/")) {
filePath = path.join(os.homedir(), filePath.slice(2))
}
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(configDir, filePath)
const fileContent = await Bun.file(resolvedPath).text()
text = text.replace(match, JSON.stringify(fileContent))
const fileContent = (await Bun.file(resolvedPath).text()).trim()
// escape newlines/quotes, strip outer quotes
text = text.replace(match, JSON.stringify(fileContent).slice(1, -1))
}
}
let data: any
try {
data = JSON.parse(text)
} catch (err) {
throw new JsonError({ path: configPath }, { cause: err as Error })
const errors: JsoncParseError[] = []
const data = parseJsonc(text, errors, { allowTrailingComma: true })
if (errors.length) {
const lines = text.split("\n")
const errorDetails = errors
.map((e) => {
const beforeOffset = text.substring(0, e.offset).split("\n")
const line = beforeOffset.length
const column = beforeOffset[beforeOffset.length - 1].length + 1
const problemLine = lines[line - 1]
const error = `${printParseErrorCode(e.error)} at line ${line}, column ${column}`
if (!problemLine) return error
return `${error}\n Line ${line}: ${problemLine}\n${"".padStart(column + 9)}^`
})
.join("\n")
throw new JsonError({
path: filepath,
message: `\n--- JSONC Input ---\n${text}\n--- Errors ---\n${errorDetails}\n--- End ---`,
})
}
const parsed = Info.safeParse(data)
if (parsed.success) {
if (!parsed.data.$schema) {
parsed.data.$schema = "https://opencode.ai/config.json"
await Bun.write(configPath, JSON.stringify(parsed.data, null, 2))
await Bun.write(filepath, JSON.stringify(parsed.data, null, 2))
}
return parsed.data
const data = parsed.data
if (data.plugin) {
for (let i = 0; i < data.plugin?.length; i++) {
const plugin = data.plugin[i]
if (typeof plugin === "string") {
data.plugin[i] = path.resolve(path.dirname(filepath), plugin)
}
}
}
return data
}
throw new InvalidError({ path: configPath, issues: parsed.error.issues })
throw new InvalidError({ path: filepath, issues: parsed.error.issues })
}
export const JsonError = NamedError.create(
"ConfigJsonError",
z.object({
path: z.string(),
message: z.string().optional(),
}),
)

View File

@@ -1,6 +1,7 @@
export namespace Flag {
export const OPENCODE_AUTO_SHARE = truthy("OPENCODE_AUTO_SHARE")
export const OPENCODE_DISABLE_WATCHER = truthy("OPENCODE_DISABLE_WATCHER")
export const OPENCODE_CONFIG = process.env["OPENCODE_CONFIG"]
function truthy(key: string) {
const value = process.env[key]?.toLowerCase()

View File

@@ -1,7 +1,6 @@
import { App } from "../app/app"
import { BunProc } from "../bun"
import { Filesystem } from "../util/filesystem"
import path from "path"
export interface Info {
name: string
@@ -65,14 +64,57 @@ export const prettier: Info = {
],
async enabled() {
const app = App.info()
const nms = await Filesystem.findUp("node_modules", app.path.cwd, app.path.root)
for (const item of nms) {
if (await Bun.file(path.join(item, ".bin", "prettier")).exists()) return true
const items = await Filesystem.findUp("package.json", app.path.cwd, app.path.root)
for (const item of items) {
const json = await Bun.file(item).json()
if (json.dependencies?.prettier) return true
if (json.devDependencies?.prettier) return true
}
return false
},
}
export const biome: Info = {
name: "biome",
command: [BunProc.which(), "x", "biome", "format", "--write", "$FILE"],
environment: {
BUN_BE_BUN: "1",
},
extensions: [
".js",
".jsx",
".mjs",
".cjs",
".ts",
".tsx",
".mts",
".cts",
".html",
".htm",
".css",
".scss",
".sass",
".less",
".vue",
".svelte",
".json",
".jsonc",
".yaml",
".yml",
".toml",
".xml",
".md",
".mdx",
".graphql",
".gql",
],
async enabled() {
const app = App.info()
const items = await Filesystem.findUp("biome.json", app.path.cwd, app.path.root)
return items.length > 0
},
}
export const zig: Info = {
name: "zig",
command: ["zig", "fmt", "$FILE"],
@@ -87,7 +129,9 @@ export const clang: Info = {
command: ["clang-format", "-i", "$FILE"],
extensions: [".c", ".cc", ".cpp", ".cxx", ".c++", ".h", ".hh", ".hpp", ".hxx", ".h++", ".ino", ".C", ".H"],
async enabled() {
return Bun.which("clang-format") !== null
const app = App.info()
const items = await Filesystem.findUp(".clang-format", app.path.cwd, app.path.root)
return items.length > 0
},
}

View File

@@ -5,20 +5,40 @@ import { Log } from "../util/log"
import path from "path"
import * as Formatter from "./formatter"
import { Config } from "../config/config"
import { mergeDeep } from "remeda"
export namespace Format {
const log = Log.create({ service: "format" })
const state = App.state("format", () => {
const state = App.state("format", async () => {
const enabled: Record<string, boolean> = {}
const cfg = await Config.get()
const formatters = { ...Formatter } as Record<string, Formatter.Info>
for (const [name, item] of Object.entries(cfg.formatter ?? {})) {
if (item.disabled) {
delete formatters[name]
continue
}
const result: Formatter.Info = mergeDeep(formatters[name] ?? {}, {
command: [],
extensions: [],
...item,
})
result.enabled = async () => true
result.name = name
formatters[name] = result
}
return {
enabled,
formatters,
}
})
async function isEnabled(item: Formatter.Info) {
const s = state()
const s = await state()
let status = s.enabled[item.name]
if (status === undefined) {
status = await item.enabled()
@@ -28,8 +48,10 @@ export namespace Format {
}
async function getFormatter(ext: string) {
const formatters = await state().then((x) => x.formatters)
const result = []
for (const item of Object.values(Formatter)) {
for (const item of Object.values(formatters)) {
log.info("checking", { name: item.name, ext })
if (!item.extensions.includes(ext)) continue
if (!(await isEnabled(item))) continue
result.push(item)

View File

@@ -13,6 +13,7 @@ export namespace Global {
export const Path = {
data,
bin: path.join(data, "bin"),
log: path.join(data, "log"),
cache,
config,
state,
@@ -23,9 +24,10 @@ await Promise.all([
fs.mkdir(Global.Path.data, { recursive: true }),
fs.mkdir(Global.Path.config, { recursive: true }),
fs.mkdir(Global.Path.state, { recursive: true }),
fs.mkdir(Global.Path.log, { recursive: true }),
])
const CACHE_VERSION = "3"
const CACHE_VERSION = "4"
const version = await Bun.file(path.join(Global.Path.cache, "version"))
.text()

View File

@@ -5,6 +5,7 @@ export namespace Identifier {
const prefixes = {
session: "ses",
message: "msg",
permission: "per",
user: "usr",
part: "prt",
} as const

View File

@@ -1,10 +1,15 @@
import { $ } from "bun"
import { spawn } from "bun"
import { z } from "zod"
import { NamedError } from "../util/error"
import { Log } from "../util/log"
import { Bus } from "../bus"
const SUPPORTED_IDES = ["Windsurf", "Visual Studio Code", "Cursor", "VSCodium"] as const
const SUPPORTED_IDES = [
{ name: "Windsurf" as const, cmd: "windsurf" },
{ name: "Visual Studio Code" as const, cmd: "code" },
{ name: "Cursor" as const, cmd: "cursor" },
{ name: "VSCodium" as const, cmd: "codium" },
]
export namespace Ide {
const log = Log.create({ service: "ide" })
@@ -18,8 +23,6 @@ export namespace Ide {
),
}
export type Ide = Awaited<ReturnType<typeof ide>>
export const AlreadyInstalledError = NamedError.create("AlreadyInstalledError", z.object({}))
export const InstallFailedError = NamedError.create(
@@ -29,11 +32,11 @@ export namespace Ide {
}),
)
export async function ide() {
export function ide() {
if (process.env["TERM_PROGRAM"] === "vscode") {
const v = process.env["GIT_ASKPASS"]
for (const ide of SUPPORTED_IDES) {
if (v?.includes(ide)) return ide
if (v?.includes(ide.name)) return ide.name
}
}
return "unknown"
@@ -43,32 +46,29 @@ export namespace Ide {
return process.env["OPENCODE_CALLER"] === "vscode"
}
export async function install(ide: Ide) {
const cmd = (() => {
switch (ide) {
case "Windsurf":
return $`windsurf --install-extension sst-dev.opencode`
case "Visual Studio Code":
return $`code --install-extension sst-dev.opencode`
case "Cursor":
return $`cursor --install-extension sst-dev.opencode`
case "VSCodium":
return $`codium --install-extension sst-dev.opencode`
default:
throw new Error(`Unknown IDE: ${ide}`)
}
})()
// TODO: check OPENCODE_CALLER
const result = await cmd.quiet().throws(false)
export async function install(ide: (typeof SUPPORTED_IDES)[number]["name"]) {
const cmd = SUPPORTED_IDES.find((i) => i.name === ide)?.cmd
if (!cmd) throw new Error(`Unknown IDE: ${ide}`)
const p = spawn([cmd, "--install-extension", "sst-dev.opencode"], {
stdout: "pipe",
stderr: "pipe",
})
await p.exited
const stdout = await new Response(p.stdout).text()
const stderr = await new Response(p.stderr).text()
log.info("installed", {
ide,
stdout: result.stdout.toString(),
stderr: result.stderr.toString(),
stdout,
stderr,
})
if (result.exitCode !== 0)
throw new InstallFailedError({
stderr: result.stderr.toString("utf8"),
})
if (result.stdout.toString().includes("already installed")) throw new AlreadyInstalledError({})
if (p.exitCode !== 0) {
throw new InstallFailedError({ stderr })
}
if (stdout.includes("already installed")) {
throw new AlreadyInstalledError({})
}
}
}

View File

@@ -17,7 +17,7 @@ import { TuiCommand } from "./cli/cmd/tui"
import { DebugCommand } from "./cli/cmd/debug"
import { StatsCommand } from "./cli/cmd/stats"
import { McpCommand } from "./cli/cmd/mcp"
import { InstallGithubCommand } from "./cli/cmd/install-github"
import { GithubCommand } from "./cli/cmd/github"
import { Trace } from "./trace"
Trace.init()
@@ -78,7 +78,7 @@ const cli = yargs(hideBin(process.argv))
.command(ServeCommand)
.command(ModelsCommand)
.command(StatsCommand)
.command(InstallGithubCommand)
.command(GithubCommand)
.fail((msg) => {
if (msg.startsWith("Unknown argument") || msg.startsWith("Not enough non-option arguments")) {
cli.showHelp("log")

View File

@@ -136,6 +136,7 @@ export namespace Installation {
}
export const VERSION = typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "dev"
export const USER_AGENT = `opencode/${VERSION}`
export async function latest() {
return fetch("https://api.github.com/repos/sst/opencode/releases/latest")

View File

@@ -4,6 +4,8 @@ import { LSPClient } from "./client"
import path from "path"
import { LSPServer } from "./server"
import { z } from "zod"
import { Config } from "../config/config"
import { spawn } from "child_process"
export namespace LSP {
const log = Log.create({ service: "lsp" })
@@ -55,8 +57,34 @@ export namespace LSP {
"lsp",
async () => {
const clients: LSPClient.Info[] = []
const servers: Record<string, LSPServer.Info> = LSPServer
const cfg = await Config.get()
for (const [name, item] of Object.entries(cfg.lsp ?? {})) {
const existing = servers[name]
if (item.disabled) {
delete servers[name]
continue
}
servers[name] = {
...existing,
extensions: item.extensions ?? existing.extensions,
spawn: async (_app, root) => {
return {
process: spawn(item.command[0], item.command.slice(1), {
cwd: root,
env: {
...process.env,
...item.env,
},
}),
initialization: item.initialization,
}
},
}
}
return {
broken: new Set<string>(),
servers,
clients,
}
},
@@ -76,7 +104,7 @@ export namespace LSP {
const extension = path.parse(file).ext
const result: LSPClient.Info[] = []
for (const server of Object.values(LSPServer)) {
if (!server.extensions.includes(extension)) continue
if (server.extensions.length && !server.extensions.includes(extension)) continue
const root = await server.root(file, App.info())
if (!root) continue
if (s.broken.has(root + server.id)) continue

View File

@@ -322,4 +322,43 @@ export namespace LSPServer {
}
},
}
export const CSharp: Info = {
id: "csharp",
root: NearestRoot([".sln", ".csproj", "global.json"]),
extensions: [".cs"],
async spawn(_, root) {
let bin = Bun.which("csharp-ls", {
PATH: process.env["PATH"] + ":" + Global.Path.bin,
})
if (!bin) {
if (!Bun.which("dotnet")) {
log.error(".NET SDK is required to install csharp-ls")
return
}
log.info("installing csharp-ls via dotnet tool")
const proc = Bun.spawn({
cmd: ["dotnet", "tool", "install", "csharp-ls", "--tool-path", Global.Path.bin],
stdout: "pipe",
stderr: "pipe",
stdin: "pipe",
})
const exit = await proc.exited
if (exit !== 0) {
log.error("Failed to install csharp-ls")
return
}
bin = path.join(Global.Path.bin, "csharp-ls" + (process.platform === "win32" ? ".exe" : ""))
log.info(`installed csharp-ls`, { bin })
}
return {
process: spawn(bin, {
cwd: root,
}),
}
},
}
}

View File

@@ -1,5 +1,7 @@
import { experimental_createMCPClient, type Tool } from "ai"
import { Experimental_StdioMCPTransport } from "ai/mcp-stdio"
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
import { App } from "../app/app"
import { Config } from "../config/config"
import { Log } from "../util/log"
@@ -32,15 +34,28 @@ export namespace MCP {
}
log.info("found", { key, type: mcp.type })
if (mcp.type === "remote") {
const client = await experimental_createMCPClient({
name: key,
transport: {
type: "sse",
url: mcp.url,
headers: mcp.headers,
},
}).catch(() => {})
if (!client) {
const transports = [
new StreamableHTTPClientTransport(new URL(mcp.url), {
requestInit: {
headers: mcp.headers,
},
}),
new SSEClientTransport(new URL(mcp.url), {
requestInit: {
headers: mcp.headers,
},
}),
]
for (const transport of transports) {
const client = await experimental_createMCPClient({
name: key,
transport,
}).catch(() => {})
if (!client) continue
clients[key] = client
break
}
if (!clients[key])
Bus.publish(Session.Event.Error, {
error: {
name: "UnknownError",
@@ -49,16 +64,13 @@ export namespace MCP {
},
},
})
continue
}
clients[key] = client
}
if (mcp.type === "local") {
const [cmd, ...args] = mcp.command
const client = await experimental_createMCPClient({
name: key,
transport: new Experimental_StdioMCPTransport({
transport: new StdioClientTransport({
stderr: "ignore",
command: cmd,
args,
@@ -103,7 +115,8 @@ export namespace MCP {
const result: Record<string, Tool> = {}
for (const [clientName, client] of Object.entries(await clients())) {
for (const [toolName, tool] of Object.entries(await client.tools())) {
result[clientName + "_" + toolName] = tool
const sanitizedClientName = clientName.replace(/\s+/g, "_")
result[sanitizedClientName + "_" + toolName] = tool
}
}
return result

View File

@@ -2,6 +2,8 @@ import { App } from "../app/app"
import { z } from "zod"
import { Bus } from "../bus"
import { Log } from "../util/log"
import { Identifier } from "../id/id"
import { Plugin } from "../plugin"
export namespace Permission {
const log = Log.create({ service: "permission" })
@@ -9,7 +11,11 @@ export namespace Permission {
export const Info = z
.object({
id: z.string(),
type: z.string(),
pattern: z.string().optional(),
sessionID: z.string(),
messageID: z.string(),
callID: z.string().optional(),
title: z.string(),
metadata: z.record(z.any()),
time: z.object({
@@ -17,12 +23,16 @@ export namespace Permission {
}),
})
.openapi({
ref: "permission.info",
ref: "Permission",
})
export type Info = z.infer<typeof Info>
export const Event = {
Updated: Bus.event("permission.updated", Info),
Replied: Bus.event(
"permission.replied",
z.object({ sessionID: z.string(), permissionID: z.string(), response: z.string() }),
),
}
const state = App.state(
@@ -40,7 +50,7 @@ export namespace Permission {
const approved: {
[sessionID: string]: {
[permissionID: string]: Info
[permissionID: string]: boolean
}
} = {}
@@ -52,76 +62,90 @@ export namespace Permission {
async (state) => {
for (const pending of Object.values(state.pending)) {
for (const item of Object.values(pending)) {
item.reject(new RejectedError(item.info.sessionID, item.info.id))
item.reject(new RejectedError(item.info.sessionID, item.info.id, item.info.callID))
}
}
},
)
export function ask(input: {
id: Info["id"]
sessionID: Info["sessionID"]
export async function ask(input: {
type: Info["type"]
title: Info["title"]
pattern?: Info["pattern"]
callID?: Info["callID"]
sessionID: Info["sessionID"]
messageID: Info["messageID"]
metadata: Info["metadata"]
}) {
return
const { pending, approved } = state()
log.info("asking", {
sessionID: input.sessionID,
permissionID: input.id,
messageID: input.messageID,
toolCallID: input.callID,
})
if (approved[input.sessionID]?.[input.id]) {
log.info("previously approved", {
sessionID: input.sessionID,
permissionID: input.id,
})
return
}
if (approved[input.sessionID]?.[input.pattern ?? input.type]) return
const info: Info = {
id: input.id,
id: Identifier.ascending("permission"),
type: input.type,
sessionID: input.sessionID,
messageID: input.messageID,
callID: input.callID,
title: input.title,
metadata: input.metadata,
time: {
created: Date.now(),
},
}
switch (
await Plugin.trigger("permission.ask", info, {
status: "ask",
}).then((x) => x.status)
) {
case "deny":
throw new RejectedError(info.sessionID, info.id, info.callID)
case "allow":
return
}
pending[input.sessionID] = pending[input.sessionID] || {}
return new Promise<void>((resolve, reject) => {
pending[input.sessionID][input.id] = {
pending[input.sessionID][info.id] = {
info,
resolve,
reject,
}
setTimeout(() => {
respond({
sessionID: input.sessionID,
permissionID: input.id,
response: "always",
})
}, 1000)
Bus.publish(Event.Updated, info)
})
}
export function respond(input: {
sessionID: Info["sessionID"]
permissionID: Info["id"]
response: "once" | "always" | "reject"
}) {
export const Response = z.enum(["once", "always", "reject"])
export type Response = z.infer<typeof Response>
export function respond(input: { sessionID: Info["sessionID"]; permissionID: Info["id"]; response: Response }) {
log.info("response", input)
const { pending, approved } = state()
const match = pending[input.sessionID]?.[input.permissionID]
if (!match) return
delete pending[input.sessionID][input.permissionID]
if (input.response === "reject") {
match.reject(new RejectedError(input.sessionID, input.permissionID))
match.reject(new RejectedError(input.sessionID, input.permissionID, match.info.callID))
return
}
match.resolve()
Bus.publish(Event.Replied, {
sessionID: input.sessionID,
permissionID: input.permissionID,
response: input.response,
})
if (input.response === "always") {
approved[input.sessionID] = approved[input.sessionID] || {}
approved[input.sessionID][input.permissionID] = match.info
approved[input.sessionID][match.info.pattern ?? match.info.type] = true
for (const item of Object.values(pending[input.sessionID])) {
if ((item.info.pattern ?? item.info.type) === (match.info.pattern ?? match.info.type)) {
respond({ sessionID: item.info.sessionID, permissionID: item.info.id, response: input.response })
}
}
}
}
@@ -129,6 +153,7 @@ export namespace Permission {
constructor(
public readonly sessionID: string,
public readonly permissionID: string,
public readonly toolCallID?: string,
) {
super(`The user rejected permission to use this functionality`)
}

View File

@@ -0,0 +1,85 @@
import type { Hooks, Plugin as PluginInstance } from "@opencode-ai/plugin"
import { App } from "../app/app"
import { Config } from "../config/config"
import { Bus } from "../bus"
import { Log } from "../util/log"
import { createOpencodeClient } from "@opencode-ai/sdk"
import { Server } from "../server/server"
import { pathOr } from "remeda"
export namespace Plugin {
const log = Log.create({ service: "plugin" })
const state = App.state("plugin", async (app) => {
const client = createOpencodeClient({
baseUrl: "http://localhost:4096",
fetch: async (...args) => Server.app().fetch(...args),
})
const config = await Config.get()
const hooks = []
for (const plugin of config.plugin ?? []) {
log.info("loading plugin", { path: plugin })
const mod = await import(plugin)
for (const [_name, fn] of Object.entries<PluginInstance>(mod)) {
const init = await fn({
client,
app,
$: Bun.$,
})
hooks.push(init)
}
}
return {
hooks,
}
})
type Path<T, Prefix extends string = ""> = T extends object
? {
[K in keyof T]: K extends string
? T[K] extends Function | undefined
? `${Prefix}${K}`
: Path<T[K], `${Prefix}${K}.`>
: never
}[keyof T]
: never
export type FunctionFromKey<T, P extends Path<T>> = P extends `${infer K}.${infer R}`
? K extends keyof T
? R extends Path<T[K]>
? FunctionFromKey<T[K], R>
: never
: never
: P extends keyof T
? T[P]
: never
export async function trigger<
Name extends Path<Required<Hooks>>,
Input = Parameters<FunctionFromKey<Required<Hooks>, Name>>[0],
Output = Parameters<FunctionFromKey<Required<Hooks>, Name>>[1],
>(fn: Name, input: Input, output: Output): Promise<Output> {
if (!fn) return output
const path = fn.split(".")
for (const hook of await state().then((x) => x.hooks)) {
// @ts-expect-error
const fn = pathOr(hook, path, undefined)
if (!fn) continue
// @ts-expect-error
await fn(input, output)
}
return output
}
export function init() {
Bus.subscribeAll(async (input) => {
const hooks = await state().then((x) => x.hooks)
for (const hook of hooks) {
hook["event"]?.({
event: input,
})
}
})
}
}

View File

@@ -3,6 +3,7 @@ import { Log } from "../util/log"
import path from "path"
import { z } from "zod"
import { data } from "./models-macro" with { type: "macro" }
import { Installation } from "../installation"
export namespace ModelsDev {
const log = Log.create({ service: "models.dev" })
@@ -50,21 +51,30 @@ export namespace ModelsDev {
export type Provider = z.infer<typeof Provider>
export async function get() {
refresh()
const file = Bun.file(filepath)
const result = await file.json().catch(() => {})
if (result) {
refresh()
return result as Record<string, Provider>
}
refresh()
if (result) return result as Record<string, Provider>
const json = await data()
return JSON.parse(json) as Record<string, Provider>
}
async function refresh() {
export async function refresh() {
const file = Bun.file(filepath)
log.info("refreshing")
const result = await fetch("https://models.dev/api.json").catch(() => {})
if (result && result.ok) await Bun.write(file, result)
log.info("refreshing", {
file,
})
const result = await fetch("https://models.dev/api.json", {
headers: {
"User-Agent": Installation.USER_AGENT,
},
}).catch((e) => {
log.error("Failed to fetch models.dev", {
error: e,
})
})
if (result && result.ok) await Bun.write(file, await result.text())
}
}
setInterval(() => ModelsDev.refresh(), 60 * 1000).unref()

View File

@@ -5,22 +5,11 @@ import { mergeDeep, sortBy } from "remeda"
import { NoSuchModelError, type LanguageModel, type Provider as SDK } from "ai"
import { Log } from "../util/log"
import { BunProc } from "../bun"
import { BashTool } from "../tool/bash"
import { EditTool } from "../tool/edit"
import { WebFetchTool } from "../tool/webfetch"
import { GlobTool } from "../tool/glob"
import { GrepTool } from "../tool/grep"
import { ListTool } from "../tool/ls"
import { PatchTool } from "../tool/patch"
import { ReadTool } from "../tool/read"
import { WriteTool } from "../tool/write"
import { TodoReadTool, TodoWriteTool } from "../tool/todo"
import { AuthAnthropic } from "../auth/anthropic"
import { AuthCopilot } from "../auth/copilot"
import { ModelsDev } from "./models"
import { NamedError } from "../util/error"
import { Auth } from "../auth"
import { TaskTool } from "../tool/task"
export namespace Provider {
const log = Log.create({ service: "provider" })
@@ -137,6 +126,15 @@ export namespace Provider {
options: {},
}
},
azure: async () => {
return {
autoload: false,
async getModel(sdk: any, modelID: string) {
return sdk.responses(modelID)
},
options: {},
}
},
"amazon-bedrock": async () => {
if (!process.env["AWS_PROFILE"] && !process.env["AWS_ACCESS_KEY_ID"] && !process.env["AWS_BEARER_TOKEN_BEDROCK"])
return { autoload: false }
@@ -205,6 +203,17 @@ export namespace Provider {
},
}
},
vercel: async () => {
return {
autoload: false,
options: {
headers: {
"http-referer": "https://opencode.ai/",
"x-title": "opencode",
},
},
}
},
}
const state = App.state("provider", async () => {
@@ -377,6 +386,10 @@ export namespace Provider {
})
}
export async function getProvider(providerID: string) {
return state().then((s) => s.providers[providerID])
}
export async function getModel(providerID: string, modelID: string) {
const key = `${providerID}/${modelID}`
const s = await state()
@@ -468,137 +481,6 @@ export namespace Provider {
}
}
const TOOLS = [
BashTool,
EditTool,
WebFetchTool,
GlobTool,
GrepTool,
ListTool,
// LspDiagnosticTool,
// LspHoverTool,
PatchTool,
ReadTool,
// MultiEditTool,
WriteTool,
TodoWriteTool,
TodoReadTool,
TaskTool,
]
export async function tools(providerID: string) {
const result = await Promise.all(TOOLS.map((t) => t()))
switch (providerID) {
case "anthropic":
return result.filter((t) => t.id !== "patch")
case "openai":
return result.map((t) => ({
...t,
parameters: optionalToNullable(t.parameters),
}))
case "azure":
return result.map((t) => ({
...t,
parameters: optionalToNullable(t.parameters),
}))
case "google":
return result.map((t) => ({
...t,
parameters: sanitizeGeminiParameters(t.parameters),
}))
default:
return result
}
}
function sanitizeGeminiParameters(schema: z.ZodTypeAny, visited = new Set()): z.ZodTypeAny {
if (!schema || visited.has(schema)) {
return schema
}
visited.add(schema)
if (schema instanceof z.ZodDefault) {
const innerSchema = schema.removeDefault()
// Handle Gemini's incompatibility with `default` on `anyOf` (unions).
if (innerSchema instanceof z.ZodUnion) {
// The schema was `z.union(...).default(...)`, which is not allowed.
// We strip the default and return the sanitized union.
return sanitizeGeminiParameters(innerSchema, visited)
}
// Otherwise, the default is on a regular type, which is allowed.
// We recurse on the inner type and then re-apply the default.
return sanitizeGeminiParameters(innerSchema, visited).default(schema._def.defaultValue())
}
if (schema instanceof z.ZodOptional) {
return z.optional(sanitizeGeminiParameters(schema.unwrap(), visited))
}
if (schema instanceof z.ZodObject) {
const newShape: Record<string, z.ZodTypeAny> = {}
for (const [key, value] of Object.entries(schema.shape)) {
newShape[key] = sanitizeGeminiParameters(value as z.ZodTypeAny, visited)
}
return z.object(newShape)
}
if (schema instanceof z.ZodArray) {
return z.array(sanitizeGeminiParameters(schema.element, visited))
}
if (schema instanceof z.ZodUnion) {
// This schema corresponds to `anyOf` in JSON Schema.
// We recursively sanitize each option in the union.
const sanitizedOptions = schema.options.map((option: z.ZodTypeAny) => sanitizeGeminiParameters(option, visited))
return z.union(sanitizedOptions as [z.ZodTypeAny, z.ZodTypeAny, ...z.ZodTypeAny[]])
}
if (schema instanceof z.ZodString) {
const newSchema = z.string({ description: schema.description })
const safeChecks = ["min", "max", "length", "regex", "startsWith", "endsWith", "includes", "trim"]
// rome-ignore lint/suspicious/noExplicitAny: <explanation>
;(newSchema._def as any).checks = (schema._def as z.ZodStringDef).checks.filter((check) =>
safeChecks.includes(check.kind),
)
return newSchema
}
return schema
}
function optionalToNullable(schema: z.ZodTypeAny): z.ZodTypeAny {
if (schema instanceof z.ZodObject) {
const shape = schema.shape
const newShape: Record<string, z.ZodTypeAny> = {}
for (const [key, value] of Object.entries(shape)) {
const zodValue = value as z.ZodTypeAny
if (zodValue instanceof z.ZodOptional) {
newShape[key] = zodValue.unwrap().nullable()
} else {
newShape[key] = optionalToNullable(zodValue)
}
}
return z.object(newShape)
}
if (schema instanceof z.ZodArray) {
return z.array(optionalToNullable(schema.element))
}
if (schema instanceof z.ZodUnion) {
return z.union(
schema.options.map((option: z.ZodTypeAny) => optionalToNullable(option)) as [
z.ZodTypeAny,
z.ZodTypeAny,
...z.ZodTypeAny[],
],
)
}
return schema
}
export const ModelNotFoundError = NamedError.create(
"ProviderModelNotFoundError",
z.object({

View File

@@ -2,29 +2,83 @@ import type { ModelMessage } from "ai"
import { unique } from "remeda"
export namespace ProviderTransform {
export function message(msgs: ModelMessage[], providerID: string, modelID: string) {
if (providerID === "anthropic" || modelID.includes("anthropic") || modelID.includes("claude")) {
const system = msgs.filter((msg) => msg.role === "system").slice(0, 2)
const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
function normalizeToolCallIds(msgs: ModelMessage[]): ModelMessage[] {
return msgs.map((msg) => {
if ((msg.role === "assistant" || msg.role === "tool") && Array.isArray(msg.content)) {
msg.content = msg.content.map((part) => {
if ((part.type === "tool-call" || part.type === "tool-result") && "toolCallId" in part) {
return {
...part,
toolCallId: part.toolCallId.replace(/[^a-zA-Z0-9_-]/g, "_"),
}
}
return part
})
}
return msg
})
}
for (const msg of unique([...system, ...final])) {
msg.providerOptions = {
...msg.providerOptions,
anthropic: {
cacheControl: { type: "ephemeral" },
},
openrouter: {
cache_control: { type: "ephemeral" },
},
bedrock: {
cachePoint: { type: "ephemeral" },
},
openaiCompatible: {
cache_control: { type: "ephemeral" },
},
function applyCaching(msgs: ModelMessage[], providerID: string): ModelMessage[] {
const system = msgs.filter((msg) => msg.role === "system").slice(0, 2)
const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
const providerOptions = {
anthropic: {
cacheControl: { type: "ephemeral" },
},
openrouter: {
cache_control: { type: "ephemeral" },
},
bedrock: {
cachePoint: { type: "ephemeral" },
},
openaiCompatible: {
cache_control: { type: "ephemeral" },
},
}
for (const msg of unique([...system, ...final])) {
const shouldUseContentOptions = providerID !== "anthropic" && Array.isArray(msg.content) && msg.content.length > 0
if (shouldUseContentOptions) {
const lastContent = msg.content[msg.content.length - 1]
if (lastContent && typeof lastContent === "object") {
lastContent.providerOptions = {
...lastContent.providerOptions,
...providerOptions,
}
continue
}
}
msg.providerOptions = {
...msg.providerOptions,
...providerOptions,
}
}
return msgs
}
export function message(msgs: ModelMessage[], providerID: string, modelID: string) {
if (modelID.includes("claude")) {
msgs = normalizeToolCallIds(msgs)
}
if (providerID === "anthropic" || modelID.includes("anthropic") || modelID.includes("claude")) {
msgs = applyCaching(msgs, providerID)
}
return msgs
}
export function temperature(_providerID: string, modelID: string) {
if (modelID.toLowerCase().includes("qwen")) return 0.55
return 0
}
export function topP(_providerID: string, modelID: string) {
if (modelID.toLowerCase().includes("qwen")) return 1
return undefined
}
}

View File

@@ -18,6 +18,8 @@ import { LSP } from "../lsp"
import { MessageV2 } from "../session/message-v2"
import { Mode } from "../session/mode"
import { callTui, TuiRoute } from "./tui"
import { Permission } from "../permission"
import { lazy } from "../util/lazy"
const ERRORS = {
400: {
@@ -43,7 +45,11 @@ export namespace Server {
export type Routes = ReturnType<typeof app>
function app() {
export const Event = {
Connected: Bus.event("server.connected", z.object({})),
}
export const app = lazy(() => {
const app = new Hono()
const result = app
@@ -90,6 +96,7 @@ export namespace Server {
"/event",
describeRoute({
description: "Get events",
operationId: "event.subscribe",
responses: {
200: {
description: "Event stream",
@@ -109,7 +116,10 @@ export namespace Server {
log.info("event connected")
return streamSSE(c, async (stream) => {
stream.writeSSE({
data: JSON.stringify({}),
data: JSON.stringify({
type: "server.connected",
properties: {},
}),
})
const unsub = Bus.subscribeAll(async (event) => {
await stream.writeSSE({
@@ -130,6 +140,7 @@ export namespace Server {
"/app",
describeRoute({
description: "Get app info",
operationId: "app.get",
responses: {
200: {
description: "200",
@@ -149,6 +160,7 @@ export namespace Server {
"/app/init",
describeRoute({
description: "Initialize the app",
operationId: "app.init",
responses: {
200: {
description: "Initialize the app",
@@ -169,6 +181,7 @@ export namespace Server {
"/config",
describeRoute({
description: "Get config info",
operationId: "config.get",
responses: {
200: {
description: "Get config info",
@@ -188,6 +201,7 @@ export namespace Server {
"/session",
describeRoute({
description: "List all sessions",
operationId: "session.list",
responses: {
200: {
description: "List of sessions",
@@ -209,6 +223,7 @@ export namespace Server {
"/session",
describeRoute({
description: "Create a new session",
operationId: "session.create",
responses: {
...ERRORS,
200: {
@@ -230,6 +245,7 @@ export namespace Server {
"/session/:id",
describeRoute({
description: "Delete a session and all its data",
operationId: "session.delete",
responses: {
200: {
description: "Successfully deleted session",
@@ -256,6 +272,7 @@ export namespace Server {
"/session/:id/init",
describeRoute({
description: "Analyze the app and create an AGENTS.md file",
operationId: "session.init",
responses: {
200: {
description: "200",
@@ -292,6 +309,7 @@ export namespace Server {
"/session/:id/abort",
describeRoute({
description: "Abort a session",
operationId: "session.abort",
responses: {
200: {
description: "Aborted session",
@@ -317,6 +335,7 @@ export namespace Server {
"/session/:id/share",
describeRoute({
description: "Share a session",
operationId: "session.share",
responses: {
200: {
description: "Successfully shared session",
@@ -345,6 +364,7 @@ export namespace Server {
"/session/:id/share",
describeRoute({
description: "Unshare the session",
operationId: "session.unshare",
responses: {
200: {
description: "Successfully unshared session",
@@ -373,6 +393,7 @@ export namespace Server {
"/session/:id/summarize",
describeRoute({
description: "Summarize the session",
operationId: "session.summarize",
responses: {
200: {
description: "Summarized session",
@@ -408,6 +429,7 @@ export namespace Server {
"/session/:id/message",
describeRoute({
description: "List messages for a session",
operationId: "session.messages",
responses: {
200: {
description: "List of messages",
@@ -437,10 +459,45 @@ export namespace Server {
return c.json(messages)
},
)
.get(
"/session/:id/message/:messageID",
describeRoute({
description: "Get a message from a session",
operationId: "session.message",
responses: {
200: {
description: "Message",
content: {
"application/json": {
schema: resolver(
z.object({
info: MessageV2.Info,
parts: MessageV2.Part.array(),
}),
),
},
},
},
},
}),
zValidator(
"param",
z.object({
id: z.string().openapi({ description: "Session ID" }),
messageID: z.string().openapi({ description: "Message ID" }),
}),
),
async (c) => {
const params = c.req.valid("param")
const message = await Session.getMessage(params.id, params.messageID)
return c.json(message)
},
)
.post(
"/session/:id/message",
describeRoute({
description: "Create and send a new message to a session",
operationId: "session.chat",
responses: {
200: {
description: "Created message",
@@ -470,6 +527,7 @@ export namespace Server {
"/session/:id/revert",
describeRoute({
description: "Revert a message",
operationId: "session.revert",
responses: {
200: {
description: "Updated session",
@@ -499,6 +557,7 @@ export namespace Server {
"/session/:id/unrevert",
describeRoute({
description: "Restore all reverted messages",
operationId: "session.unrevert",
responses: {
200: {
description: "Updated session",
@@ -522,10 +581,42 @@ export namespace Server {
return c.json(session)
},
)
.post(
"/session/:id/permissions/:permissionID",
describeRoute({
description: "Respond to a permission request",
responses: {
200: {
description: "Permission processed successfully",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
},
}),
zValidator(
"param",
z.object({
id: z.string(),
permissionID: z.string(),
}),
),
zValidator("json", z.object({ response: Permission.Response })),
async (c) => {
const params = c.req.valid("param")
const id = params.id
const permissionID = params.permissionID
Permission.respond({ sessionID: id, permissionID, response: c.req.valid("json").response })
return c.json(true)
},
)
.get(
"/config/providers",
describeRoute({
description: "List all providers",
operationId: "config.providers",
responses: {
200: {
description: "List of providers",
@@ -554,6 +645,7 @@ export namespace Server {
"/find",
describeRoute({
description: "Find text in files",
operationId: "find.text",
responses: {
200: {
description: "Matches",
@@ -586,6 +678,7 @@ export namespace Server {
"/find/file",
describeRoute({
description: "Find files",
operationId: "find.files",
responses: {
200: {
description: "File paths",
@@ -618,6 +711,7 @@ export namespace Server {
"/find/symbol",
describeRoute({
description: "Find workspace symbols",
operationId: "find.symbols",
responses: {
200: {
description: "Symbols",
@@ -645,6 +739,7 @@ export namespace Server {
"/file",
describeRoute({
description: "Read a file",
operationId: "file.read",
responses: {
200: {
description: "File content",
@@ -681,6 +776,7 @@ export namespace Server {
"/file/status",
describeRoute({
description: "Get file status",
operationId: "file.status",
responses: {
200: {
description: "File status",
@@ -701,6 +797,7 @@ export namespace Server {
"/log",
describeRoute({
description: "Write a log entry to the server logs",
operationId: "app.log",
responses: {
200: {
description: "Log entry written successfully",
@@ -750,6 +847,7 @@ export namespace Server {
"/mode",
describeRoute({
description: "List all modes",
operationId: "app.modes",
responses: {
200: {
description: "List of modes",
@@ -770,6 +868,7 @@ export namespace Server {
"/tui/append-prompt",
describeRoute({
description: "Append prompt to the TUI",
operationId: "tui.appendPrompt",
responses: {
200: {
description: "Prompt processed successfully",
@@ -793,6 +892,7 @@ export namespace Server {
"/tui/open-help",
describeRoute({
description: "Open the help dialog",
operationId: "tui.openHelp",
responses: {
200: {
description: "Help dialog opened successfully",
@@ -806,10 +906,124 @@ export namespace Server {
}),
async (c) => c.json(await callTui(c)),
)
.post(
"/tui/open-sessions",
describeRoute({
description: "Open the session dialog",
operationId: "tui.openSessions",
responses: {
200: {
description: "Session dialog opened successfully",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
},
}),
async (c) => c.json(await callTui(c)),
)
.post(
"/tui/open-themes",
describeRoute({
description: "Open the theme dialog",
operationId: "tui.openThemes",
responses: {
200: {
description: "Theme dialog opened successfully",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
},
}),
async (c) => c.json(await callTui(c)),
)
.post(
"/tui/open-models",
describeRoute({
description: "Open the model dialog",
operationId: "tui.openModels",
responses: {
200: {
description: "Model dialog opened successfully",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
},
}),
async (c) => c.json(await callTui(c)),
)
.post(
"/tui/submit-prompt",
describeRoute({
description: "Submit the prompt",
operationId: "tui.submitPrompt",
responses: {
200: {
description: "Prompt submitted successfully",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
},
}),
async (c) => c.json(await callTui(c)),
)
.post(
"/tui/clear-prompt",
describeRoute({
description: "Clear the prompt",
operationId: "tui.clearPrompt",
responses: {
200: {
description: "Prompt cleared successfully",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
},
}),
async (c) => c.json(await callTui(c)),
)
.post(
"/tui/execute-command",
describeRoute({
description: "Execute a TUI command (e.g. switch_mode)",
operationId: "tui.executeCommand",
responses: {
200: {
description: "Command executed successfully",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
},
}),
zValidator(
"json",
z.object({
command: z.string(),
}),
),
async (c) => c.json(await callTui(c)),
)
.route("/tui/control", TuiRoute)
return result
}
})
export async function openapi() {
const a = app()

View File

@@ -39,7 +39,9 @@ import { MessageV2 } from "./message-v2"
import { Mode } from "./mode"
import { LSP } from "../lsp"
import { ReadTool } from "../tool/read"
import { splitWhen } from "remeda"
import { mergeDeep, pipe, splitWhen } from "remeda"
import { ToolRegistry } from "../tool/registry"
import { Plugin } from "../plugin"
export namespace Session {
const log = Log.create({ service: "session" })
@@ -66,6 +68,7 @@ export namespace Session {
messageID: z.string(),
partID: z.string().optional(),
snapshot: z.string().optional(),
diff: z.string().optional(),
})
.optional(),
})
@@ -254,7 +257,10 @@ export namespace Session {
}
export async function getMessage(sessionID: string, messageID: string) {
return Storage.readJSON<MessageV2.Info>("session/message/" + sessionID + "/" + messageID)
return {
info: await Storage.readJSON<MessageV2.Info>("session/message/" + sessionID + "/" + messageID),
parts: await getParts(sessionID, messageID),
}
}
export async function getParts(sessionID: string, messageID: string) {
@@ -288,6 +294,9 @@ export namespace Session {
export function abort(sessionID: string) {
const controller = state().pending.get(sessionID)
if (!controller) return false
log.info("aborting", {
sessionID,
})
controller.abort()
state().pending.delete(sessionID)
return true
@@ -372,6 +381,36 @@ export namespace Session {
l.info("chatting")
const inputMode = input.mode ?? "build"
// Process revert cleanup first, before creating new messages
const session = await get(input.sessionID)
if (session.revert) {
let msgs = await messages(input.sessionID)
const messageID = session.revert.messageID
const [preserve, remove] = splitWhen(msgs, (x) => x.info.id === messageID)
msgs = preserve
for (const msg of remove) {
await Storage.remove(`session/message/${input.sessionID}/${msg.info.id}`)
await Bus.publish(MessageV2.Event.Removed, { sessionID: input.sessionID, messageID: msg.info.id })
}
const last = preserve.at(-1)
if (session.revert.partID && last) {
const partID = session.revert.partID
const [preserveParts, removeParts] = splitWhen(last.parts, (x) => x.id === partID)
last.parts = preserveParts
for (const part of removeParts) {
await Storage.remove(`session/part/${input.sessionID}/${last.info.id}/${part.id}`)
await Bus.publish(MessageV2.Event.PartRemoved, {
sessionID: input.sessionID,
messageID: last.info.id,
partID: part.id,
})
}
}
await update(input.sessionID, (draft) => {
draft.revert = undefined
})
}
const userMsg: MessageV2.Info = {
id: input.messageID ?? Identifier.ascending("message"),
role: "user",
@@ -387,12 +426,38 @@ export namespace Session {
if (part.type === "file") {
const url = new URL(part.url)
switch (url.protocol) {
case "data:":
if (part.mime === "text/plain") {
return [
{
id: Identifier.ascending("part"),
messageID: userMsg.id,
sessionID: input.sessionID,
type: "text",
synthetic: true,
text: `Called the Read tool with the following input: ${JSON.stringify({ filePath: part.filename })}`,
},
{
id: Identifier.ascending("part"),
messageID: userMsg.id,
sessionID: input.sessionID,
type: "text",
synthetic: true,
text: Buffer.from(part.url, "base64url").toString(),
},
{
...part,
id: part.id ?? Identifier.ascending("part"),
messageID: userMsg.id,
sessionID: input.sessionID,
},
]
}
break
case "file:":
// have to normalize, symbol search returns absolute paths
// Decode the pathname since URL constructor doesn't automatically decode it
const pathname = decodeURIComponent(url.pathname)
const relativePath = pathname.replace(app.path.cwd, ".")
const filePath = path.join(app.path.cwd, relativePath)
const filePath = decodeURIComponent(url.pathname)
if (part.mime === "text/plain") {
let offset: number | undefined = undefined
@@ -430,7 +495,7 @@ export namespace Session {
}
}
const args = { filePath, offset, limit }
const result = await ReadTool().then((t) =>
const result = await ReadTool.init().then((t) =>
t.execute(args, {
sessionID: input.sessionID,
abort: new AbortController().signal,
@@ -472,7 +537,7 @@ export namespace Session {
messageID: userMsg.id,
sessionID: input.sessionID,
type: "text",
text: `Called the Read tool with the following input: {\"filePath\":\"${pathname}\"}`,
text: `Called the Read tool with the following input: {\"filePath\":\"${filePath}\"}`,
synthetic: true,
},
{
@@ -507,12 +572,21 @@ export namespace Session {
text: PROMPT_PLAN,
synthetic: true,
})
await Plugin.trigger(
"chat.message",
{},
{
message: userMsg,
parts: userParts,
},
)
await updateMessage(userMsg)
for (const part of userParts) {
await updatePart(part)
}
// mark session as updated since a message has been added to it
// mark session as updated
// used for session list sorting (indicates when session was most recently interacted with)
await update(input.sessionID, (_draft) => {})
if (isLocked(input.sessionID)) {
@@ -531,30 +605,6 @@ export namespace Session {
const model = await Provider.getModel(input.providerID, input.modelID)
let msgs = await messages(input.sessionID)
const session = await get(input.sessionID)
if (session.revert) {
const messageID = session.revert.messageID
const [preserve, remove] = splitWhen(msgs, (x) => x.info.id === messageID)
msgs = preserve
for (const msg of remove) {
await Storage.remove(`session/message/${input.sessionID}/${msg.info.id}`)
await Bus.publish(MessageV2.Event.Removed, { sessionID: input.sessionID, messageID: msg.info.id })
}
const last = preserve.at(-1)
if (session.revert.partID && last) {
const partID = session.revert.partID
const [preserveParts, removeParts] = splitWhen(last.parts, (x) => x.id === partID)
last.parts = preserveParts
for (const part of removeParts) {
await Storage.remove(`session/part/${input.sessionID}/${last.info.id}/${part.id}`)
await Bus.publish(MessageV2.Event.PartRemoved, {
messageID: last.info.id,
partID: part.id,
})
}
}
}
const previous = msgs.filter((x) => x.info.role === "assistant").at(-1)?.info as MessageV2.Assistant
const outputLimit = Math.min(model.info.limit.output, OUTPUT_TOKEN_MAX) || OUTPUT_TOKEN_MAX
@@ -611,7 +661,9 @@ export namespace Session {
.then((result) => {
if (result.text)
return Session.update(input.sessionID, (draft) => {
draft.title = result.text
const cleaned = result.text.replace(/<think>[\s\S]*?<\/think>\s*/g, "")
const title = cleaned.length > 100 ? cleaned.substring(0, 97) + "..." : cleaned
draft.title = title.trim()
})
})
.catch(() => {})
@@ -660,20 +712,34 @@ export namespace Session {
const processor = createProcessor(assistantMsg, model.info)
for (const item of await Provider.tools(input.providerID)) {
if (mode.tools[item.id] === false) continue
if (input.tools?.[item.id] === false) continue
if (session.parentID && item.id === "task") continue
const enabledTools = pipe(
mode.tools,
mergeDeep(ToolRegistry.enabled(input.providerID, input.modelID)),
mergeDeep(input.tools ?? {}),
)
for (const item of await ToolRegistry.tools(input.providerID, input.modelID)) {
if (enabledTools[item.id] === false) continue
tools[item.id] = tool({
id: item.id as any,
description: item.description,
inputSchema: item.parameters as ZodSchema,
async execute(args, options) {
await processor.track(options.toolCallId)
await Plugin.trigger(
"tool.execute.before",
{
tool: item.id,
sessionID: input.sessionID,
callID: options.toolCallId,
},
{
args,
},
)
const result = await item.execute(args, {
sessionID: input.sessionID,
abort: abort.signal,
messageID: assistantMsg.id,
callID: options.toolCallId,
metadata: async (val) => {
const match = processor.partFromToolCall(options.toolCallId)
if (match && match.state.status === "running") {
@@ -692,6 +758,15 @@ export namespace Session {
}
},
})
await Plugin.trigger(
"tool.execute.after",
{
tool: item.id,
sessionID: input.sessionID,
callID: options.toolCallId,
},
result,
)
return result
},
toModelOutput(result) {
@@ -708,7 +783,6 @@ export namespace Session {
const execute = item.execute
if (!execute) continue
item.execute = async (args, opts) => {
await processor.track(opts.toolCallId)
const result = await execute(args, opts)
const output = result.content
.filter((x: any) => x.type === "text")
@@ -728,8 +802,27 @@ export namespace Session {
tools[key] = item
}
const params = {
temperature: model.info.temperature
? (mode.temperature ?? ProviderTransform.temperature(input.providerID, input.modelID))
: undefined,
topP: mode.topP ?? ProviderTransform.topP(input.providerID, input.modelID),
}
await Plugin.trigger(
"chat.params",
{
model: model.info,
provider: await Provider.getProvider(input.providerID),
message: userMsg,
},
params,
)
const stream = streamText({
onError() {},
onError(e) {
log.error("streamText error", {
error: e,
})
},
async prepareStep({ messages }) {
const queue = (state().queued.get(input.sessionID) ?? []).filter((x) => !x.processed)
if (queue.length) {
@@ -764,6 +857,7 @@ export namespace Session {
},
modelID: input.modelID,
providerID: input.providerID,
mode: inputMode,
time: {
created: Date.now(),
},
@@ -775,13 +869,15 @@ export namespace Session {
messages,
}
},
maxRetries: 10,
maxRetries: 3,
maxOutputTokens: outputLimit,
abortSignal: abort.signal,
stopWhen: stepCountIs(1000),
providerOptions: {
[input.providerID]: model.info.options,
},
temperature: params.temperature,
topP: params.topP,
messages: [
...system.map(
(x): ModelMessage => ({
@@ -791,7 +887,6 @@ export namespace Session {
),
...MessageV2.toModelMessage(msgs),
],
temperature: model.info.temperature ? 0 : undefined,
tools: model.info.tool_call === false ? undefined : tools,
model: wrapLanguageModel({
model: model.language,
@@ -823,15 +918,11 @@ export namespace Session {
}
function createProcessor(assistantMsg: MessageV2.Assistant, model: ModelsDev.Model) {
const toolCalls: Record<string, MessageV2.ToolPart> = {}
const snapshots: Record<string, string> = {}
const toolcalls: Record<string, MessageV2.ToolPart> = {}
let snapshot: string | undefined
return {
async track(toolCallID: string) {
const hash = await Snapshot.track()
if (hash) snapshots[toolCallID] = hash
},
partFromToolCall(toolCallID: string) {
return toolCalls[toolCallID]
return toolcalls[toolCallID]
},
async process(stream: StreamTextResult<Record<string, AITool>, never>) {
try {
@@ -847,7 +938,7 @@ export namespace Session {
case "tool-input-start":
const part = await updatePart({
id: Identifier.ascending("part"),
id: toolcalls[value.id]?.id ?? Identifier.ascending("part"),
messageID: assistantMsg.id,
sessionID: assistantMsg.sessionID,
type: "tool",
@@ -857,7 +948,7 @@ export namespace Session {
status: "pending",
},
})
toolCalls[value.id] = part as MessageV2.ToolPart
toolcalls[value.id] = part as MessageV2.ToolPart
break
case "tool-input-delta":
@@ -867,7 +958,7 @@ export namespace Session {
break
case "tool-call": {
const match = toolCalls[value.toolCallId]
const match = toolcalls[value.toolCallId]
if (match) {
const part = await updatePart({
...match,
@@ -879,12 +970,12 @@ export namespace Session {
},
},
})
toolCalls[value.toolCallId] = part as MessageV2.ToolPart
toolcalls[value.toolCallId] = part as MessageV2.ToolPart
}
break
}
case "tool-result": {
const match = toolCalls[value.toolCallId]
const match = toolcalls[value.toolCallId]
if (match && match.state.status === "running") {
await updatePart({
...match,
@@ -900,27 +991,13 @@ export namespace Session {
},
},
})
delete toolCalls[value.toolCallId]
const snapshot = snapshots[value.toolCallId]
if (snapshot) {
const patch = await Snapshot.patch(snapshot)
if (patch.files.length) {
await updatePart({
id: Identifier.ascending("part"),
messageID: assistantMsg.id,
sessionID: assistantMsg.sessionID,
type: "patch",
hash: patch.hash,
files: patch.files,
})
}
}
delete toolcalls[value.toolCallId]
}
break
}
case "tool-error": {
const match = toolCalls[value.toolCallId]
const match = toolcalls[value.toolCallId]
if (match && match.state.status === "running") {
await updatePart({
...match,
@@ -934,19 +1011,7 @@ export namespace Session {
},
},
})
delete toolCalls[value.toolCallId]
const snapshot = snapshots[value.toolCallId]
if (snapshot) {
const patch = await Snapshot.patch(snapshot)
await updatePart({
id: Identifier.ascending("part"),
messageID: assistantMsg.id,
sessionID: assistantMsg.sessionID,
type: "patch",
hash: patch.hash,
files: patch.files,
})
}
delete toolcalls[value.toolCallId]
}
break
}
@@ -961,6 +1026,7 @@ export namespace Session {
sessionID: assistantMsg.sessionID,
type: "step-start",
})
snapshot = await Snapshot.track()
break
case "finish-step":
@@ -976,6 +1042,20 @@ export namespace Session {
cost: usage.cost,
})
await updateMessage(assistantMsg)
if (snapshot) {
const patch = await Snapshot.patch(snapshot)
if (patch.files.length) {
await updatePart({
id: Identifier.ascending("part"),
messageID: assistantMsg.id,
sessionID: assistantMsg.sessionID,
type: "patch",
hash: patch.hash,
files: patch.files,
})
}
snapshot = undefined
}
break
case "text-start":
@@ -991,15 +1071,16 @@ export namespace Session {
}
break
case "text":
case "text-delta":
if (currentText) {
currentText.text += value.text
await updatePart(currentText)
if (currentText.text) await updatePart(currentText)
}
break
case "text-end":
if (currentText && currentText.text) {
if (currentText) {
currentText.text = currentText.text.trimEnd()
currentText.time = {
start: Date.now(),
end: Date.now(),
@@ -1059,7 +1140,7 @@ export namespace Session {
}
const p = await getParts(assistantMsg.sessionID, assistantMsg.id)
for (const part of p) {
if (part.type === "tool" && part.state.status !== "completed") {
if (part.type === "tool" && part.state.status !== "completed" && part.state.status !== "error") {
updatePart({
...part,
state: {
@@ -1124,6 +1205,7 @@ export namespace Session {
const session = await get(input.sessionID)
revert.snapshot = session.revert?.snapshot ?? (await Snapshot.track())
await Snapshot.revert(patches)
if (revert.snapshot) revert.diff = await Snapshot.diff(revert.snapshot)
return update(input.sessionID, (draft) => {
draft.revert = revert
})

View File

@@ -284,6 +284,7 @@ export namespace MessageV2 {
PartRemoved: Bus.event(
"message.part.removed",
z.object({
sessionID: z.string(),
messageID: z.string(),
partID: z.string(),
}),

View File

@@ -7,6 +7,8 @@ export namespace Mode {
export const Info = z
.object({
name: z.string(),
temperature: z.number().optional(),
topP: z.number().optional(),
model: z
.object({
modelID: z.string(),
@@ -50,6 +52,8 @@ export namespace Mode {
item.name = key
if (value.model) item.model = Provider.parseModel(value.model)
if (value.prompt) item.prompt = value.prompt
if (value.temperature != undefined) item.temperature = value.temperature
if (value.top_p != undefined) item.topP = value.top_p
if (value.tools)
item.tools = {
...value.tools,

View File

@@ -1,4 +1,4 @@
You are opencode, an autonomous agent - please keep going until the user's query is completely resolved, before ending your turn and yielding back to the user.
You are opencode, an agent - please keep going until the users query is completely resolved, before ending your turn and yielding back to the user.
Your thinking should be thorough and so it's fine if it's very long. However, avoid unnecessary repetition and verbosity. You should be concise, but thorough.
@@ -6,25 +6,26 @@ You MUST iterate and keep going until the problem is solved.
You have everything you need to resolve this problem. I want you to fully solve this autonomously before coming back to me.
Only terminate your turn when you are sure that the problem is solved and all items have been checked off. Use the TodoWrite and TodoRead tools to track and manage steps. Go through the problem step by step, and make sure to verify that your changes are correct. Once each step is finished mark it as completed with the TodoWrite tool. NEVER end your turn without having truly and completely solved the problem, use the TodoRead tool to make sure all steps are complete, and when you say you are going to make a tool call, make sure you ACTUALLY make the tool call, instead of ending your turn. If a step is impossible to complete, mark it as cancelled using the TodoWrite tool.
Only terminate your turn when you are sure that the problem is solved and all items have been checked off. Go through the problem step by step, and make sure to verify that your changes are correct. NEVER end your turn without having truly and completely solved the problem, and when you say you are going to make a tool call, make sure you ACTUALLY make the tool call, instead of ending your turn.
THE PROBLEM CAN NOT BE SOLVED WITHOUT EXTENSIVE INTERNET RESEARCH.
You must use the webfetch tool to recursively gather all information from URLs provided to you by the user, as well as any links you find in the content of those pages.
You must use the webfetch tool to recursively gather all information from URL's provided to you by the user, as well as any links you find in the content of those pages.
Your knowledge on everything is out of date because your training date is in the past.
Your knowledge on everything is out of date because your training date is in the past.
You CANNOT successfully complete this task without using Bing to verify your understanding of third party packages and dependencies is up to date. You must use the webfetch tool to search bing for how to properly use libraries, packages, frameworks, dependencies, etc. every single time you install or implement one. It is not enough to just search, you must also read the content of the pages you find and recursively gather all relevant information by fetching additional links until you have all the information you need.
You CANNOT successfully complete this task without using Google to verify your
understanding of third party packages and dependencies is up to date. You must use the webfetch tool to search google for how to properly use libraries, packages, frameworks, dependencies, etc. every single time you install or implement one. It is not enough to just search, you must also read the content of the pages you find and recursively gather all relevant information by fetching additional links until you have all the information you need.
If the user request is "resume" or "continue" or "try again",use the TodoRead tool to find the next pending step. Continue from that step, and do not hand back control to the user until the entire todo list is complete and all steps are marked as complete or cancelled. Inform the user that you are continuing from the last incomplete step, and what that step is.
Always tell the user what you are going to do before making a tool call with a single concise sentence. This will help them understand what you are doing and why.
Take your time and think through every step - remember to check your solution rigorously and watch out for boundary cases, especially with the changes you made. Use the sequential thinking tool if available. Your solution must be perfect. If not, continue working on it. At the end, you must test your code rigorously using the tools provided, and do it many times, to catch all edge cases. If it is not robust, update the plan and iterate more and make it perfect. Failing to test your code sufficiently rigorously is the NUMBER ONE failure mode on these types of tasks; run the build, and verify that the changes you made actually build; make sure you handle all edge cases, and run existing tests if they are provided.
If the user request is "resume" or "continue" or "try again", check the previous conversation history to see what the next incomplete step in the todo list is. Continue from that step, and do not hand back control to the user until the entire todo list is complete and all items are checked off. Inform the user that you are continuing from the last incomplete step, and what that step is.
You MUST plan extensively before each tool call, and reflect extensively on the outcomes of the previous tool calls. DO NOT do this entire process by making tool calls only, as this can impair your ability to solve the problem and think insightfully.
Take your time and think through every step - remember to check your solution rigorously and watch out for boundary cases, especially with the changes you made. Use the sequential thinking tool if available. Your solution must be perfect. If not, continue working on it. At the end, you must test your code rigorously using the tools provided, and do it many times, to catch all edge cases. If it is not robust, iterate more and make it perfect. Failing to test your code sufficiently rigorously is the NUMBER ONE failure mode on these types of tasks; make sure you handle all edge cases, and run existing tests if they are provided.
You MUST keep working until the problem is completely solved, and all steps in the todo list are complete. Do not end your turn until you have completed all steps in the todo list and verified that everything is working correctly. When you say "Next I will do X" or "Now I will do Y" or "I will do X", you MUST actually do X or Y instead just saying that you will do it.
You MUST plan extensively before each function call, and reflect extensively on the outcomes of the previous function calls. DO NOT do this entire process by making function calls only, as this can impair your ability to solve the problem and think insightfully.
You MUST use the ToolRead tool to verify that all steps are complete or cancelled before ending your turn. If any steps are incomplete, you MUST continue working on them until they are all complete.
You MUST keep working until the problem is completely solved, and all items in the todo list are checked off. Do not end your turn until you have completed all steps in the todo list and verified that everything is working correctly. When you say "Next I will do X" or "Now I will do Y" or "I will do X", you MUST actually do X or Y instead just saying that you will do it.
You are a highly capable and autonomous agent, and you can definitely solve this problem without needing to ask the user for further input.
@@ -38,7 +39,7 @@ You are a highly capable and autonomous agent, and you can definitely solve this
- What are the dependencies and interactions with other parts of the code?
3. Investigate the codebase. Explore relevant files, search for key functions, and gather context.
4. Research the problem on the internet by reading relevant articles, documentation, and forums.
5. Develop a clear, step-by-step plan. Break down the fix into manageable, incremental steps. Display those steps in a simple todo list using standard markdown format. Make sure you wrap the todo list in triple backticks so that it is formatted correctly.
5. Develop a clear, step-by-step plan. Break down the fix into manageable, incremental steps. Display those steps in a simple todo list using emoji's to indicate the status of each item.
6. Implement the fix incrementally. Make small, testable code changes.
7. Debug as needed. Use debugging techniques to isolate and resolve issues.
8. Test frequently. Run tests after each change to verify correctness.
@@ -49,12 +50,12 @@ Refer to the detailed sections below for more information on each step.
## 1. Fetch Provided URLs
- If the user provides a URL, use the `webfetch` tool to retrieve the content of the provided URL.
- After fetching, review the content returned by the fetch tool.
- After fetching, review the content returned by the webfetch tool.
- If you find any additional URLs or links that are relevant, use the `webfetch` tool again to retrieve those links.
- Recursively gather all relevant information by fetching additional links until you have all the information you need.
## 2. Deeply Understand the Problem
Carefully read the issue and think hard about a plan to solve it before coding. Use the sequential thinking tool if available.
Carefully read the issue and think hard about a plan to solve it before coding.
## 3. Codebase Investigation
- Explore relevant files and directories.
@@ -64,49 +65,49 @@ Carefully read the issue and think hard about a plan to solve it before coding.
- Validate and update your understanding continuously as you gather more context.
## 4. Internet Research
- Use the `webfetch` tool to search bing by fetching the URL `https://www.bing.com/search?q=your+search+query`.
- Use the `webfetch` tool to search google by fetching the URL `https://www.google.com/search?q=your+search+query`.
- After fetching, review the content returned by the fetch tool.
- If you find any additional URLs or links that are relevant, use the `webfetch` tool again to retrieve those links.
- Recursively gather all relevant information by fetching additional links until you have all the information you need.
- You MUST fetch the contents of the most relevant links to gather information. Do not rely on the summary that you find in the search results.
- As you fetch each link, read the content thoroughly and fetch any additional links that you find withhin the content that are relevant to the problem.
- Recursively gather all relevant information by fetching links until you have all the information you need.
## 5. Develop a Detailed Plan
## 5. Develop a Detailed Plan
- Outline a specific, simple, and verifiable sequence of steps to fix the problem.
- Add steps using the TodoWrite tool.
- Each time you complete a step, mark it as complete using the TodoWrite tool.
- Each time you check off a step, use the TodoRead tool and display the updated todo list to the user in markdown format.
- You MUST continue on to the next step after checking off a step instead of ending your turn and asking the user what they want to do next.
- You may only end your turn when all steps in the todo list are marked as complete or cancelled.
- Create a todo list in markdown format to track your progress.
- Each time you complete a step, check it off using `[x]` syntax.
- Each time you check off a step, display the updated todo list to the user.
- Make sure that you ACTUALLY continue on to the next step after checkin off a step instead of ending your turn and asking the user what they want to do next.
## 6. Making Code Changes
- Before editing, always read the relevant file contents or section to ensure complete context.
- Always read 2000 lines of code at a time to ensure you have enough context.
- If a patch is not applied correctly, attempt to reapply it.
- Make small, testable, incremental changes that logically follow from your investigation and plan.
- When using the edit tool, include 3-5 lines of unchanged code before and after the string you want to replace, to make it unambiguous which part of the file should be edited.
- If a patch or edit is not applied correctly, attempt to reapply it.
- Always validate that your changes build and pass tests after each change.
- If the build fails or test fail, debug why before proceeding, update the plan as needed.
- Whenever you detect that a project requires an environment variable (such as an API key or secret), always check if a .env file exists in the project root. If it does not exist, automatically create a .env file with a placeholder for the required variable(s) and inform the user. Do this proactively, without waiting for the user to request it.
## 7. Debugging
- Use the `lsp_diagnostics` tool to check for any problems in the code.
- Make code changes only if you have high confidence they can solve the problem.
- When debugging, try to determine the root cause rather than addressing symptoms.
- Debug for as long as needed to identify the root cause and identify a fix.
- Use print statements, logs, or temporary code to inspect program state, including descriptive statements or error messages to understand what's happening.
- To test hypotheses, you can also add test statements or functions.
- Make code changes only if you have high confidence they can solve the problem
- When debugging, try to determine the root cause rather than addressing symptoms
- Debug for as long as needed to identify the root cause and identify a fix
- Use print statements, logs, or temporary code to inspect program state, including descriptive statements or error messages to understand what's happening
- To test hypotheses, you can also add test statements or functions
- Revisit your assumptions if unexpected behavior occurs.
# How to create a Todo List
Use the following format to show the todo list:
```markdown
- [ ] Step 1: Description of the first step
- [ ] Step 2: Description of the second step
- [ ] Step 3: Description of the third step
```
Do not ever use HTML tags or any other formatting for the todo list, as it will not be rendered correctly. Always use the markdown format shown above.
Use the following format to create a todo list:
```markdown
- [ ] Step 1: Description of the first step
- [ ] Step 2: Description of the second step
- [ ] Step 3: Description of the third step
```
Do not ever use HTML tags or any other formatting for the todo list, as it will not be rendered correctly. Always use the markdown format shown above. Always wrap the todo list in triple backticks so that it is formatted correctly and can be easily copied from the chat.
Always show the completed todo list to the user as the last item in your message, so that they can see that you have addressed all of the steps.
# Communication Guidelines
Always communicate clearly and concisely in a casual, friendly yet professional tone.
Always communicate clearly and concisely in a casual, friendly yet professional tone.
<examples>
"Let me fetch the URL you provided to gather more information."
"Ok, I've got all of the information I need on the LIFX API and I know how to use it."
@@ -115,3 +116,44 @@ Always communicate clearly and concisely in a casual, friendly yet professional
"OK! Now let's run the tests to make sure everything is working correctly."
"Whelp - I see we have some problems. Let's fix those up."
</examples>
- Respond with clear, direct answers. Use bullet points and code blocks for structure. - Avoid unnecessary explanations, repetition, and filler.
- Always write code directly to the correct files.
- Do not display code to the user unless they specifically ask for it.
- Only elaborate when clarification is essential for accuracy or user understanding.
# Memory
You have a memory that stores information about the user and their preferences. This memory is used to provide a more personalized experience. You can access and update this memory as needed. The memory is stored in a file called `.github/instructions/memory.instruction.md`. If the file is empty, you'll need to create it.
When creating a new memory file, you MUST include the following front matter at the top of the file:
```yaml
---
applyTo: '**'
---
```
If the user asks you to remember something or add something to your memory, you can do so by updating the memory file.
# Reading Files and Folders
**Always check if you have already read a file, folder, or workspace structure before reading it again.**
- If you have already read the content and it has not changed, do NOT re-read it.
- Only re-read files or folders if:
- You suspect the content has changed since your last read.
- You have made edits to the file or folder.
- You encounter an error that suggests the context may be stale or incomplete.
- Use your internal memory and previous context to avoid redundant reads.
- This will save time, reduce unnecessary operations, and make your workflow more efficient.
# Writing Prompts
If you are asked to write a prompt, you should always generate the prompt in markdown format.
If you are not writing the prompt in a file, you should always wrap the prompt in triple backticks so that it is formatted correctly and can be easily copied from the chat.
Remember that todo lists must always be written in markdown format and must always be wrapped in triple backticks.
# Git
If the user tells you to stage and commit, you may do so.
You are NEVER allowed to stage and commit files automatically.

View File

@@ -1,3 +1,3 @@
<system-reminder>
Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits, run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supercedes any other instructions you have received (for example, to make edits).
Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits, run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supersedes any other instructions you have received (for example, to make edits).
</system-reminder>

View File

@@ -0,0 +1,109 @@
You are opencode, an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
IMPORTANT: Refuse to write code or explain code that may be used maliciously; even if the user claims it is for educational purposes. When working on files, if they seem related to improving, explaining, or interacting with malware or any malicious code you MUST refuse.
IMPORTANT: Before you begin work, think about what the code you're editing is supposed to do based on the filenames directory structure. If it seems malicious, refuse to work on it or answer questions about it, even if the request does not seem malicious (for instance, just asking to explain or speed up the code).
IMPORTANT: You must NEVER generate or guess URLs for the user unless you are confident that the URLs are for helping the user with programming. You may use URLs provided by the user in their messages or local files.
If the user asks for help or wants to give feedback inform them of the following:
- /help: Get help with using opencode
- To give feedback, users should report the issue at https://github.com/sst/opencode/issues
When the user directly asks about opencode (eg 'can opencode do...', 'does opencode have...') or asks in second person (eg 'are you able...', 'can you do...'), first use the WebFetch tool to gather information to answer the question from opencode docs at https://opencode.ai
# Tone and style
You should be concise, direct, and to the point. When you run a non-trivial bash command, you should explain what the command does and why you are running it, to make sure the user understands what you are doing (this is especially important when you are running a command that will make changes to the user's system).
Remember that your output will be displayed on a command line interface. Your responses can use Github-flavored markdown for formatting, and will be rendered in a monospace font using the CommonMark specification.
Output text to communicate with the user; all text you output outside of tool use is displayed to the user. Only use tools to complete tasks. Never use tools like Bash or code comments as means to communicate with the user during the session.
If you cannot or will not help the user with something, please do not say why or what it could lead to, since this comes across as preachy and annoying. Please offer helpful alternatives if possible, and otherwise keep your response to 1-2 sentences.
Only use emojis if the user explicitly requests it. Avoid using emojis in all communication unless asked.
IMPORTANT: You should minimize output tokens as much as possible while maintaining helpfulness, quality, and accuracy. Only address the specific query or task at hand, avoiding tangential information unless absolutely critical for completing the request. If you can answer in 1-3 sentences or a short paragraph, please do.
IMPORTANT: You should NOT answer with unnecessary preamble or postamble (such as explaining your code or summarizing your action), unless the user asks you to.
IMPORTANT: Keep your responses short, since they will be displayed on a command line interface. You MUST answer concisely with fewer than 4 lines (not including tool use or code generation), unless user asks for detail. Answer the user's question directly, without elaboration, explanation, or details. One word answers are best. Avoid introductions, conclusions, and explanations. You MUST avoid text before/after your response, such as "The answer is <answer>.", "Here is the content of the file..." or "Based on the information provided, the answer is..." or "Here is what I will do next...". Here are some examples to demonstrate appropriate verbosity:
<example>
user: 2 + 2
assistant: 4
</example>
<example>
user: what is 2+2?
assistant: 4
</example>
<example>
user: is 11 a prime number?
assistant: Yes
</example>
<example>
user: what command should I run to list files in the current directory?
assistant: ls
</example>
<example>
user: what command should I run to watch files in the current directory?
assistant: [use the ls tool to list the files in the current directory, then read docs/commands in the relevant file to find out how to watch files]
npm run dev
</example>
<example>
user: How many golf balls fit inside a jetta?
assistant: 150000
</example>
<example>
user: what files are in the directory src/?
assistant: [runs ls and sees foo.c, bar.c, baz.c]
user: which file contains the implementation of foo?
assistant: src/foo.c
</example>
<example>
user: write tests for new feature
assistant: [uses grep and glob search tools to find where similar tests are defined, uses concurrent read file tool use blocks in one tool call to read relevant files at the same time, uses edit file tool to write new tests]
</example>
# Proactiveness
You are allowed to be proactive, but only when the user asks you to do something. You should strive to strike a balance between:
1. Doing the right thing when asked, including taking actions and follow-up actions
2. Not surprising the user with actions you take without asking
For example, if the user asks you how to approach something, you should do your best to answer their question first, and not immediately jump into taking actions.
3. Do not add additional code explanation summary unless requested by the user. After working on a file, just stop, rather than providing an explanation of what you did.
# Following conventions
When making changes to files, first understand the file's code conventions. Mimic code style, use existing libraries and utilities, and follow existing patterns.
- NEVER assume that a given library is available, even if it is well known. Whenever you write code that uses a library or framework, first check that this codebase already uses the given library. For example, you might look at neighboring files, or check the package.json (or cargo.toml, and so on depending on the language).
- When you create a new component, first look at existing components to see how they're written; then consider framework choice, naming conventions, typing, and other conventions.
- When you edit a piece of code, first look at the code's surrounding context (especially its imports) to understand the code's choice of frameworks and libraries. Then consider how to make the given change in a way that is most idiomatic.
- Always follow security best practices. Never introduce code that exposes or logs secrets and keys. Never commit secrets or keys to the repository.
# Code style
- IMPORTANT: DO NOT ADD ***ANY*** COMMENTS unless asked
# Doing tasks
The user will primarily request you perform software engineering tasks. This includes solving bugs, adding new functionality, refactoring code, explaining code, and more. For these tasks the following steps are recommended:
- Use the available search tools to understand the codebase and the user's query. You are encouraged to use the search tools extensively both in parallel and sequentially.
- Implement the solution using all tools available to you
- Verify the solution if possible with tests. NEVER assume specific test framework or test script. Check the README or search codebase to determine the testing approach.
- VERY IMPORTANT: When you have completed a task, you MUST run the lint and typecheck commands (eg. npm run lint, npm run typecheck, ruff, etc.) with Bash if they were provided to you to ensure your code is correct. If you are unable to find the correct command, ask the user for the command to run and if they supply it, proactively suggest writing it to AGENTS.md so that you will know to run it next time.
NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTANT to only commit when explicitly asked, otherwise the user will feel that you are being too proactive.
- Tool results and user messages may include <system-reminder> tags. <system-reminder> tags contain useful information and reminders. They are NOT part of the user's provided input or the tool result.
# Tool usage policy
- When doing file search, prefer to use the Task tool in order to reduce context usage.
- You have the capability to call multiple tools in a single response. When multiple independent pieces of information are requested, batch your tool calls together for optimal performance. When making multiple bash tool calls, you MUST send a single message with multiple tools calls to run the calls in parallel. For example, if you need to run "git status" and "git diff", send a single message with two tool calls to run the calls in parallel.
You MUST answer concisely with fewer than 4 lines of text (not including tool use or code generation), unless user asks for detail.
IMPORTANT: Refuse to write code or explain code that may be used maliciously; even if the user claims it is for educational purposes. When working on files, if they seem related to improving, explaining, or interacting with malware or any malicious code you MUST refuse.
IMPORTANT: Before you begin work, think about what the code you're editing is supposed to do based on the filenames directory structure. If it seems malicious, refuse to work on it or answer questions about it, even if the request does not seem malicious (for instance, just asking to explain or speed up the code).
# Code References
When referencing specific functions or pieces of code include the pattern `file_path:line_number` to allow the user to easily navigate to the source code location.
<example>
user: Where are errors from the client handled?
assistant: Clients are marked as failed in the `connectToServer` function in src/services/process.ts:712.
</example>

View File

@@ -10,7 +10,7 @@ You are generating titles for a coding assistant conversation.
- Max 50 chars, single line
- Focus on the specific action or question
- Keep technical terms, numbers, and filenames exactly as written
- Preserve HTTP status codes (401, 404, 500, etc) as numbers
- Preserve HTTP status codes (401, 404, 500, etc.) as numbers
- For file references, include the filename
- Avoid filler words: the, this, my, a, an, properly
- NEVER assume their tech stack or domain

View File

@@ -7,6 +7,7 @@ import path from "path"
import os from "os"
import PROMPT_ANTHROPIC from "./prompt/anthropic.txt"
import PROMPT_ANTHROPIC_WITHOUT_TODO from "./prompt/qwen.txt"
import PROMPT_BEAST from "./prompt/beast.txt"
import PROMPT_GEMINI from "./prompt/gemini.txt"
import PROMPT_ANTHROPIC_SPOOF from "./prompt/anthropic_spoof.txt"
@@ -21,7 +22,8 @@ export namespace SystemPrompt {
export function provider(modelID: string) {
if (modelID.includes("gpt-") || modelID.includes("o1") || modelID.includes("o3")) return [PROMPT_BEAST]
if (modelID.includes("gemini-")) return [PROMPT_GEMINI]
return [PROMPT_ANTHROPIC]
if (modelID.includes("claude")) return [PROMPT_ANTHROPIC]
return [PROMPT_ANTHROPIC_WITHOUT_TODO]
}
export async function environment() {

View File

@@ -39,7 +39,7 @@ export namespace Snapshot {
log.info("initialized")
}
await $`git --git-dir ${git} add .`.quiet().cwd(app.path.cwd).nothrow()
const hash = await $`git --git-dir ${git} write-tree`.quiet().cwd(app.path.cwd).text()
const hash = await $`git --git-dir ${git} write-tree`.quiet().cwd(app.path.cwd).nothrow().text()
return hash.trim()
}
@@ -94,6 +94,13 @@ export namespace Snapshot {
}
}
export async function diff(hash: string) {
const app = App.info()
const git = gitdir()
const result = await $`git --git-dir=${git} diff ${hash} -- .`.quiet().cwd(app.path.root).text()
return result.trim()
}
function gitdir() {
const app = App.info()
return path.join(app.path.data, "snapshots")

View File

@@ -2,17 +2,33 @@ import { z } from "zod"
import { Tool } from "./tool"
import DESCRIPTION from "./bash.txt"
import { App } from "../app/app"
import { Permission } from "../permission"
import { Config } from "../config/config"
import { Filesystem } from "../util/filesystem"
import { lazy } from "../util/lazy"
import { Log } from "../util/log"
import { Wildcard } from "../util/wildcard"
import { $ } from "bun"
const MAX_OUTPUT_LENGTH = 30000
const DEFAULT_TIMEOUT = 1 * 60 * 1000
const MAX_TIMEOUT = 10 * 60 * 1000
export const BashTool = Tool.define({
id: "bash",
const log = Log.create({ service: "bash-tool" })
const parser = lazy(async () => {
const { default: Parser } = await import("tree-sitter")
const Bash = await import("tree-sitter-bash")
const p = new Parser()
p.setLanguage(Bash.language as any)
return p
})
export const BashTool = Tool.define("bash", {
description: DESCRIPTION,
parameters: z.object({
command: z.string().describe("The command to execute"),
timeout: z.number().min(0).max(MAX_TIMEOUT).describe("Optional timeout in milliseconds").optional(),
timeout: z.number().describe("Optional timeout in milliseconds").optional(),
description: z
.string()
.describe(
@@ -21,13 +37,92 @@ export const BashTool = Tool.define({
}),
async execute(params, ctx) {
const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT)
const app = App.info()
const cfg = await Config.get()
const tree = await parser().then((p) => p.parse(params.command))
const permissions = (() => {
const value = cfg.permission?.bash
if (!value)
return {
"*": "allow",
}
if (typeof value === "string")
return {
"*": value,
}
return value
})()
let needsAsk = false
for (const node of tree.rootNode.descendantsOfType("command")) {
const command = []
for (let i = 0; i < node.childCount; i++) {
const child = node.child(i)
if (!child) continue
if (
child.type !== "command_name" &&
child.type !== "word" &&
child.type !== "string" &&
child.type !== "raw_string" &&
child.type !== "concatenation"
) {
continue
}
command.push(child.text)
}
// not an exhaustive list, but covers most common cases
if (["cd", "rm", "cp", "mv", "mkdir", "touch", "chmod", "chown"].includes(command[0])) {
for (const arg of command.slice(1)) {
if (arg.startsWith("-") || (command[0] === "chmod" && arg.startsWith("+"))) continue
const resolved = await $`realpath ${arg}`
.quiet()
.nothrow()
.text()
.then((x) => x.trim())
log.info("resolved path", { arg, resolved })
if (resolved && !Filesystem.contains(app.path.cwd, resolved)) {
throw new Error(
`This command references paths outside of ${app.path.cwd} so it is not allowed to be executed.`,
)
}
}
}
// always allow cd if it passes above check
if (!needsAsk && command[0] !== "cd") {
const ask = (() => {
for (const [pattern, value] of Object.entries(permissions)) {
const match = Wildcard.match(node.text, pattern)
log.info("checking", { text: node.text.trim(), pattern, match })
if (match) return value
}
return "ask"
})()
if (ask === "ask") needsAsk = true
}
}
if (needsAsk) {
await Permission.ask({
type: "bash",
sessionID: ctx.sessionID,
messageID: ctx.messageID,
callID: ctx.callID,
title: params.command,
metadata: {
command: params.command,
},
})
}
const process = Bun.spawn({
cmd: ["bash", "-c", params.command],
cwd: App.info().path.cwd,
cwd: app.path.cwd,
maxBuffer: MAX_OUTPUT_LENGTH,
signal: ctx.abort,
timeout: timeout,
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
})

View File

@@ -2,6 +2,7 @@
// https://github.com/cline/cline/blob/main/evals/diff-edits/diff-apply/diff-06-23-25.ts
// https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/utils/editCorrector.ts
// https://github.com/cline/cline/blob/main/evals/diff-edits/diff-apply/diff-06-26-25.ts
import { z } from "zod"
import * as path from "path"
import { Tool } from "./tool"
@@ -13,9 +14,10 @@ import { App } from "../app/app"
import { File } from "../file"
import { Bus } from "../bus"
import { FileTime } from "../file/time"
import { Config } from "../config/config"
import { Filesystem } from "../util/filesystem"
export const EditTool = Tool.define({
id: "edit",
export const EditTool = Tool.define("edit", {
description: DESCRIPTION,
parameters: z.object({
filePath: z.string().describe("The absolute path to the file to modify"),
@@ -33,56 +35,78 @@ export const EditTool = Tool.define({
}
const app = App.info()
const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(app.path.cwd, params.filePath)
await Permission.ask({
id: "edit",
sessionID: ctx.sessionID,
title: "Edit this file: " + filepath,
metadata: {
filePath: filepath,
oldString: params.oldString,
newString: params.newString,
},
})
const filePath = path.isAbsolute(params.filePath) ? params.filePath : path.join(app.path.cwd, params.filePath)
if (!Filesystem.contains(app.path.cwd, filePath)) {
throw new Error(`File ${filePath} is not in the current working directory`)
}
const cfg = await Config.get()
let diff = ""
let contentOld = ""
let contentNew = ""
await (async () => {
if (params.oldString === "") {
contentNew = params.newString
await Bun.write(filepath, params.newString)
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
if (cfg.permission?.edit === "ask") {
await Permission.ask({
type: "edit",
sessionID: ctx.sessionID,
messageID: ctx.messageID,
callID: ctx.callID,
title: "Edit this file: " + filePath,
metadata: {
filePath,
diff,
},
})
}
await Bun.write(filePath, params.newString)
await Bus.publish(File.Event.Edited, {
file: filepath,
file: filePath,
})
return
}
const file = Bun.file(filepath)
const file = Bun.file(filePath)
const stats = await file.stat().catch(() => {})
if (!stats) throw new Error(`File ${filepath} not found`)
if (stats.isDirectory()) throw new Error(`Path is a directory, not a file: ${filepath}`)
await FileTime.assert(ctx.sessionID, filepath)
if (!stats) throw new Error(`File ${filePath} not found`)
if (stats.isDirectory()) throw new Error(`Path is a directory, not a file: ${filePath}`)
await FileTime.assert(ctx.sessionID, filePath)
contentOld = await file.text()
contentNew = replace(contentOld, params.oldString, params.newString, params.replaceAll)
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
if (cfg.permission?.edit === "ask") {
await Permission.ask({
type: "edit",
sessionID: ctx.sessionID,
messageID: ctx.messageID,
callID: ctx.callID,
title: "Edit this file: " + filePath,
metadata: {
filePath,
diff,
},
})
}
await file.write(contentNew)
await Bus.publish(File.Event.Edited, {
file: filepath,
file: filePath,
})
contentNew = await file.text()
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
})()
const diff = trimDiff(createTwoFilesPatch(filepath, filepath, contentOld, contentNew))
FileTime.read(ctx.sessionID, filepath)
FileTime.read(ctx.sessionID, filePath)
let output = ""
await LSP.touchFile(filepath, true)
await LSP.touchFile(filePath, true)
const diagnostics = await LSP.diagnostics()
for (const [file, issues] of Object.entries(diagnostics)) {
if (issues.length === 0) continue
if (file === filepath) {
if (file === filePath) {
output += `\nThis file has errors, please fix\n<file_diagnostics>\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</file_diagnostics>\n`
continue
}
@@ -97,7 +121,7 @@ export const EditTool = Tool.define({
diagnostics,
diff,
},
title: `${path.relative(app.path.root, filepath)}`,
title: `${path.relative(app.path.root, filePath)}`,
output,
}
},

View File

@@ -5,8 +5,7 @@ import { App } from "../app/app"
import DESCRIPTION from "./glob.txt"
import { Ripgrep } from "../file/ripgrep"
export const GlobTool = Tool.define({
id: "glob",
export const GlobTool = Tool.define("glob", {
description: DESCRIPTION,
parameters: z.object({
pattern: z.string().describe("The glob pattern to match files against"),

View File

@@ -5,8 +5,7 @@ import { Ripgrep } from "../file/ripgrep"
import DESCRIPTION from "./grep.txt"
export const GrepTool = Tool.define({
id: "grep",
export const GrepTool = Tool.define("grep", {
description: DESCRIPTION,
parameters: z.object({
pattern: z.string().describe("The regex pattern to search for in file contents"),

View File

@@ -33,8 +33,7 @@ export const IGNORE_PATTERNS = [
const LIMIT = 100
export const ListTool = Tool.define({
id: "list",
export const ListTool = Tool.define("list", {
description: DESCRIPTION,
parameters: z.object({
path: z.string().describe("The absolute path to the directory to list (must be absolute, not relative)").optional(),

View File

@@ -5,8 +5,7 @@ import { LSP } from "../lsp"
import { App } from "../app/app"
import DESCRIPTION from "./lsp-diagnostics.txt"
export const LspDiagnosticTool = Tool.define({
id: "lsp_diagnostics",
export const LspDiagnosticTool = Tool.define("lsp_diagnostics", {
description: DESCRIPTION,
parameters: z.object({
path: z.string().describe("The path to the file to get diagnostics."),

View File

@@ -5,8 +5,7 @@ import { LSP } from "../lsp"
import { App } from "../app/app"
import DESCRIPTION from "./lsp-hover.txt"
export const LspHoverTool = Tool.define({
id: "lsp_hover",
export const LspHoverTool = Tool.define("lsp_hover", {
description: DESCRIPTION,
parameters: z.object({
file: z.string().describe("The path to the file to get diagnostics."),

View File

@@ -5,8 +5,7 @@ import DESCRIPTION from "./multiedit.txt"
import path from "path"
import { App } from "../app/app"
export const MultiEditTool = Tool.define({
id: "multiedit",
export const MultiEditTool = Tool.define("multiedit", {
description: DESCRIPTION,
parameters: z.object({
filePath: z.string().describe("The absolute path to the file to modify"),
@@ -22,7 +21,7 @@ export const MultiEditTool = Tool.define({
.describe("Array of edit operations to perform sequentially on the file"),
}),
async execute(params, ctx) {
const tool = await EditTool()
const tool = await EditTool.init()
const results = []
for (const [, edit] of params.edits.entries()) {
const result = await tool.execute(

View File

@@ -210,8 +210,7 @@ async function applyCommit(
}
}
export const PatchTool = Tool.define({
id: "patch",
export const PatchTool = Tool.define("patch", {
description: DESCRIPTION,
parameters: PatchParams,
execute: async (params, ctx) => {

View File

@@ -6,28 +6,32 @@ import { LSP } from "../lsp"
import { FileTime } from "../file/time"
import DESCRIPTION from "./read.txt"
import { App } from "../app/app"
import { Filesystem } from "../util/filesystem"
const DEFAULT_READ_LIMIT = 2000
const MAX_LINE_LENGTH = 2000
export const ReadTool = Tool.define({
id: "read",
export const ReadTool = Tool.define("read", {
description: DESCRIPTION,
parameters: z.object({
filePath: z.string().describe("The path to the file to read"),
offset: z.number().describe("The line number to start reading from (0-based)").optional(),
limit: z.number().describe("The number of lines to read (defaults to 2000)").optional(),
offset: z.coerce.number().describe("The line number to start reading from (0-based)").optional(),
limit: z.coerce.number().describe("The number of lines to read (defaults to 2000)").optional(),
}),
async execute(params, ctx) {
let filePath = params.filePath
if (!path.isAbsolute(filePath)) {
filePath = path.join(process.cwd(), filePath)
let filepath = params.filePath
if (!path.isAbsolute(filepath)) {
filepath = path.join(process.cwd(), filepath)
}
const app = App.info()
if (!Filesystem.contains(app.path.cwd, filepath)) {
throw new Error(`File ${filepath} is not in the current working directory`)
}
const file = Bun.file(filePath)
const file = Bun.file(filepath)
if (!(await file.exists())) {
const dir = path.dirname(filePath)
const base = path.basename(filePath)
const dir = path.dirname(filepath)
const base = path.basename(filepath)
const dirEntries = fs.readdirSync(dir)
const suggestions = dirEntries
@@ -39,16 +43,18 @@ export const ReadTool = Tool.define({
.slice(0, 3)
if (suggestions.length > 0) {
throw new Error(`File not found: ${filePath}\n\nDid you mean one of these?\n${suggestions.join("\n")}`)
throw new Error(`File not found: ${filepath}\n\nDid you mean one of these?\n${suggestions.join("\n")}`)
}
throw new Error(`File not found: ${filePath}`)
throw new Error(`File not found: ${filepath}`)
}
const limit = params.limit ?? DEFAULT_READ_LIMIT
const offset = params.offset || 0
const isImage = isImageFile(filePath)
const isImage = isImageFile(filepath)
if (isImage) throw new Error(`This is an image file of type: ${isImage}\nUse a different tool to process images`)
const isBinary = await isBinaryFile(file)
if (isBinary) throw new Error(`Cannot read binary file: ${filepath}`)
const lines = await file.text().then((text) => text.split("\n"))
const raw = lines.slice(offset, offset + limit).map((line) => {
return line.length > MAX_LINE_LENGTH ? line.substring(0, MAX_LINE_LENGTH) + "..." : line
@@ -67,11 +73,11 @@ export const ReadTool = Tool.define({
output += "\n</file>"
// just warms the lsp client
LSP.touchFile(filePath, false)
FileTime.read(ctx.sessionID, filePath)
LSP.touchFile(filepath, false)
FileTime.read(ctx.sessionID, filepath)
return {
title: path.relative(App.info().path.root, filePath),
title: path.relative(App.info().path.root, filepath),
output,
metadata: {
preview,
@@ -100,3 +106,14 @@ function isImageFile(filePath: string): string | false {
return false
}
}
async function isBinaryFile(file: Bun.BunFile): Promise<boolean> {
const buffer = await file.arrayBuffer()
const bytes = new Uint8Array(buffer.slice(0, 512)) // Check first 512 bytes
for (let i = 0; i < bytes.length; i++) {
if (bytes[i] === 0) return true // Null byte indicates binary
}
return false
}

View File

@@ -0,0 +1,177 @@
import z from "zod"
import { BashTool } from "./bash"
import { EditTool } from "./edit"
import { GlobTool } from "./glob"
import { GrepTool } from "./grep"
import { ListTool } from "./ls"
import { PatchTool } from "./patch"
import { ReadTool } from "./read"
import { TaskTool } from "./task"
import { TodoWriteTool, TodoReadTool } from "./todo"
import { WebFetchTool } from "./webfetch"
import { WriteTool } from "./write"
export namespace ToolRegistry {
const ALL = [
BashTool,
EditTool,
WebFetchTool,
GlobTool,
GrepTool,
ListTool,
PatchTool,
ReadTool,
WriteTool,
TodoWriteTool,
TodoReadTool,
TaskTool,
]
export function ids() {
return ALL.map((t) => t.id)
}
export async function tools(providerID: string, _modelID: string) {
const result = await Promise.all(
ALL.map(async (t) => ({
id: t.id,
...(await t.init()),
})),
)
if (providerID === "openai") {
return result.map((t) => ({
...t,
parameters: optionalToNullable(t.parameters),
}))
}
if (providerID === "azure") {
return result.map((t) => ({
...t,
parameters: optionalToNullable(t.parameters),
}))
}
if (providerID === "google") {
return result.map((t) => ({
...t,
parameters: sanitizeGeminiParameters(t.parameters),
}))
}
return result
}
export function enabled(_providerID: string, modelID: string): Record<string, boolean> {
if (modelID.toLowerCase().includes("claude")) {
return {
patch: false,
}
}
if (
modelID.toLowerCase().includes("qwen") ||
modelID.includes("gpt-") ||
modelID.includes("o1") ||
modelID.includes("o3") ||
modelID.includes("codex")
) {
return {
patch: false,
todowrite: false,
todoread: false,
}
}
return {}
}
function sanitizeGeminiParameters(schema: z.ZodTypeAny, visited = new Set()): z.ZodTypeAny {
if (!schema || visited.has(schema)) {
return schema
}
visited.add(schema)
if (schema instanceof z.ZodDefault) {
const innerSchema = schema.removeDefault()
// Handle Gemini's incompatibility with `default` on `anyOf` (unions).
if (innerSchema instanceof z.ZodUnion) {
// The schema was `z.union(...).default(...)`, which is not allowed.
// We strip the default and return the sanitized union.
return sanitizeGeminiParameters(innerSchema, visited)
}
// Otherwise, the default is on a regular type, which is allowed.
// We recurse on the inner type and then re-apply the default.
return sanitizeGeminiParameters(innerSchema, visited).default(schema._def.defaultValue())
}
if (schema instanceof z.ZodOptional) {
return z.optional(sanitizeGeminiParameters(schema.unwrap(), visited))
}
if (schema instanceof z.ZodObject) {
const newShape: Record<string, z.ZodTypeAny> = {}
for (const [key, value] of Object.entries(schema.shape)) {
newShape[key] = sanitizeGeminiParameters(value as z.ZodTypeAny, visited)
}
return z.object(newShape)
}
if (schema instanceof z.ZodArray) {
return z.array(sanitizeGeminiParameters(schema.element, visited))
}
if (schema instanceof z.ZodUnion) {
// This schema corresponds to `anyOf` in JSON Schema.
// We recursively sanitize each option in the union.
const sanitizedOptions = schema.options.map((option: z.ZodTypeAny) => sanitizeGeminiParameters(option, visited))
return z.union(sanitizedOptions as [z.ZodTypeAny, z.ZodTypeAny, ...z.ZodTypeAny[]])
}
if (schema instanceof z.ZodString) {
const newSchema = z.string({ description: schema.description })
const safeChecks = ["min", "max", "length", "regex", "startsWith", "endsWith", "includes", "trim"]
// rome-ignore lint/suspicious/noExplicitAny: <explanation>
;(newSchema._def as any).checks = (schema._def as z.ZodStringDef).checks.filter((check) =>
safeChecks.includes(check.kind),
)
return newSchema
}
return schema
}
function optionalToNullable(schema: z.ZodTypeAny): z.ZodTypeAny {
if (schema instanceof z.ZodObject) {
const shape = schema.shape
const newShape: Record<string, z.ZodTypeAny> = {}
for (const [key, value] of Object.entries(shape)) {
const zodValue = value as z.ZodTypeAny
if (zodValue instanceof z.ZodOptional) {
newShape[key] = zodValue.unwrap().nullable()
} else {
newShape[key] = optionalToNullable(zodValue)
}
}
return z.object(newShape)
}
if (schema instanceof z.ZodArray) {
return z.array(optionalToNullable(schema.element))
}
if (schema instanceof z.ZodUnion) {
return z.union(
schema.options.map((option: z.ZodTypeAny) => optionalToNullable(option)) as [
z.ZodTypeAny,
z.ZodTypeAny,
...z.ZodTypeAny[],
],
)
}
return schema
}
}

View File

@@ -7,11 +7,10 @@ import { MessageV2 } from "../session/message-v2"
import { Identifier } from "../id/id"
import { Agent } from "../agent/agent"
export const TaskTool = Tool.define(async () => {
export const TaskTool = Tool.define("task", async () => {
const agents = await Agent.list()
const description = DESCRIPTION.replace("{agents}", agents.map((a) => `- ${a.name}: ${a.description}`).join("\n"))
return {
id: "task",
description,
parameters: z.object({
description: z.string().describe("A short (3-5 words) description of the task"),
@@ -21,7 +20,7 @@ export const TaskTool = Tool.define(async () => {
async execute(params, ctx) {
const session = await Session.create(ctx.sessionID)
const msg = await Session.getMessage(ctx.sessionID, ctx.messageID)
if (msg.role !== "assistant") throw new Error("Not an assistant message")
if (msg.info.role !== "assistant") throw new Error("Not an assistant message")
const agent = await Agent.get(params.subagent_type)
const messageID = Identifier.ascending("message")
const parts: Record<string, MessageV2.ToolPart> = {}
@@ -39,8 +38,8 @@ export const TaskTool = Tool.define(async () => {
})
const model = agent.model ?? {
modelID: msg.modelID,
providerID: msg.providerID,
modelID: msg.info.modelID,
providerID: msg.info.providerID,
}
ctx.abort.addEventListener("abort", () => {
@@ -51,9 +50,12 @@ export const TaskTool = Tool.define(async () => {
sessionID: session.id,
modelID: model.modelID,
providerID: model.providerID,
mode: msg.mode,
mode: msg.info.mode,
system: agent.prompt,
tools: agent.tools,
tools: {
...agent.tools,
task: false,
},
parts: [
{
id: Identifier.ascending("part"),
@@ -68,7 +70,7 @@ export const TaskTool = Tool.define(async () => {
metadata: {
summary: result.parts.filter((x) => x.type === "tool"),
},
output: result.parts.findLast((x) => x.type === "text")!.text,
output: result.parts.findLast((x) => x.type === "text")?.text ?? "",
}
},
}

View File

@@ -0,0 +1,53 @@
import Parser from "tree-sitter";
import Bash from "tree-sitter-bash";
const parser = new Parser();
parser.setLanguage(Bash.language as any);
const sourceCode = `cd --foo foo/bar && echo "hello" && cd ../baz`;
const tree = parser.parse(sourceCode);
// Function to extract commands and arguments
function extractCommands(
node: any,
): Array<{ command: string; args: string[] }> {
const commands: Array<{ command: string; args: string[] }> = [];
function traverse(node: any) {
if (node.type === "command") {
const commandNode = node.child(0);
if (commandNode) {
const command = commandNode.text;
const args: string[] = [];
// Extract arguments
for (let i = 1; i < node.childCount; i++) {
const child = node.child(i);
if (child && child.type === "word") {
args.push(child.text);
}
}
commands.push({ command, args });
}
}
// Traverse children
for (let i = 0; i < node.childCount; i++) {
traverse(node.child(i));
}
}
traverse(node);
return commands;
}
// Extract and display commands
console.log("Source code: " + sourceCode);
const commands = extractCommands(tree.rootNode);
console.log("Extracted commands:");
commands.forEach((cmd, index) => {
console.log(`${index + 1}. Command: ${cmd.command}`);
console.log(` Args: [${cmd.args.join(", ")}]`);
});

View File

@@ -4,7 +4,7 @@ import DESCRIPTION_WRITE from "./todowrite.txt"
import { App } from "../app/app"
const TodoInfo = z.object({
content: z.string().min(1).describe("Brief description of the task"),
content: z.string().describe("Brief description of the task"),
status: z.enum(["pending", "in_progress", "completed", "cancelled"]).describe("Current status of the task"),
priority: z.enum(["high", "medium", "low"]).describe("Priority level of the task"),
id: z.string().describe("Unique identifier for the todo item"),
@@ -18,8 +18,7 @@ const state = App.state("todo-tool", () => {
return todos
})
export const TodoWriteTool = Tool.define({
id: "todowrite",
export const TodoWriteTool = Tool.define("todowrite", {
description: DESCRIPTION_WRITE,
parameters: z.object({
todos: z.array(TodoInfo).describe("The updated todo list"),
@@ -37,8 +36,7 @@ export const TodoWriteTool = Tool.define({
},
})
export const TodoReadTool = Tool.define({
id: "todoread",
export const TodoReadTool = Tool.define("todoread", {
description: "Use this tool to read your todo list",
parameters: z.object({}),
async execute(_params, opts) {

View File

@@ -7,26 +7,36 @@ export namespace Tool {
export type Context<M extends Metadata = Metadata> = {
sessionID: string
messageID: string
callID?: string
abort: AbortSignal
metadata(input: { title?: string; metadata?: M }): void
}
export interface Info<Parameters extends StandardSchemaV1 = StandardSchemaV1, M extends Metadata = Metadata> {
id: string
description: string
parameters: Parameters
execute(
args: StandardSchemaV1.InferOutput<Parameters>,
ctx: Context,
): Promise<{
title: string
metadata: M
output: string
init: () => Promise<{
description: string
parameters: Parameters
execute(
args: StandardSchemaV1.InferOutput<Parameters>,
ctx: Context,
): Promise<{
title: string
metadata: M
output: string
}>
}>
}
export function define<Parameters extends StandardSchemaV1, Result extends Metadata>(
input: Info<Parameters, Result> | (() => Promise<Info<Parameters, Result>>),
): () => Promise<Info<Parameters, Result>> {
return input instanceof Function ? input : async () => input
id: string,
init: Info<Parameters, Result>["init"] | Awaited<ReturnType<Info<Parameters, Result>["init"]>>,
): Info<Parameters, Result> {
return {
id,
init: async () => {
if (init instanceof Function) return init()
return init
},
}
}
}

View File

@@ -7,20 +7,14 @@ const MAX_RESPONSE_SIZE = 5 * 1024 * 1024 // 5MB
const DEFAULT_TIMEOUT = 30 * 1000 // 30 seconds
const MAX_TIMEOUT = 120 * 1000 // 2 minutes
export const WebFetchTool = Tool.define({
id: "webfetch",
export const WebFetchTool = Tool.define("webfetch", {
description: DESCRIPTION,
parameters: z.object({
url: z.string().describe("The URL to fetch content from"),
format: z
.enum(["text", "markdown", "html"])
.describe("The format to return the content in (text, markdown, or html)"),
timeout: z
.number()
.min(0)
.max(MAX_TIMEOUT / 1000)
.describe("Optional timeout in seconds (max 120)")
.optional(),
timeout: z.number().describe("Optional timeout in seconds (max 120)").optional(),
}),
async execute(params, ctx) {
// Validate URL

View File

@@ -8,9 +8,10 @@ import { App } from "../app/app"
import { Bus } from "../bus"
import { File } from "../file"
import { FileTime } from "../file/time"
import { Config } from "../config/config"
import { Filesystem } from "../util/filesystem"
export const WriteTool = Tool.define({
id: "write",
export const WriteTool = Tool.define("write", {
description: DESCRIPTION,
parameters: z.object({
filePath: z.string().describe("The absolute path to the file to write (must be absolute, not relative)"),
@@ -19,21 +20,28 @@ export const WriteTool = Tool.define({
async execute(params, ctx) {
const app = App.info()
const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(app.path.cwd, params.filePath)
if (!Filesystem.contains(app.path.cwd, filepath)) {
throw new Error(`File ${filepath} is not in the current working directory`)
}
const file = Bun.file(filepath)
const exists = await file.exists()
if (exists) await FileTime.assert(ctx.sessionID, filepath)
await Permission.ask({
id: "write",
sessionID: ctx.sessionID,
title: exists ? "Overwrite this file: " + filepath : "Create new file: " + filepath,
metadata: {
filePath: filepath,
content: params.content,
exists,
},
})
const cfg = await Config.get()
if (cfg.permission?.edit === "ask")
await Permission.ask({
type: "write",
sessionID: ctx.sessionID,
messageID: ctx.messageID,
callID: ctx.callID,
title: exists ? "Overwrite this file: " + filepath : "Create new file: " + filepath,
metadata: {
filePath: filepath,
content: params.content,
exists,
},
})
await Bun.write(filepath, params.content)
await Bus.publish(File.Event.Edited, {

View File

@@ -9,7 +9,7 @@ export namespace Filesystem {
}
export function contains(parent: string, child: string) {
return relative(parent, child).startsWith("..")
return !relative(parent, child).startsWith("..")
}
export async function findUp(target: string, start: string, stop?: string) {

View File

@@ -53,12 +53,10 @@ export namespace Log {
export async function init(options: Options) {
if (options.level) level = options.level
const dir = path.join(Global.Path.data, "log")
await fs.mkdir(dir, { recursive: true })
cleanup(dir)
cleanup(Global.Path.log)
if (options.print) return
logpath = path.join(
dir,
Global.Path.log,
options.dev ? "dev.log" : new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log",
)
const logfile = Bun.file(logpath)

View File

@@ -0,0 +1,14 @@
export namespace Wildcard {
export function match(str: string, pattern: string) {
const regex = new RegExp(
"^" +
pattern
.replace(/[.+^${}()|[\]\\]/g, "\\$&") // escape special regex chars
.replace(/\*/g, ".*") // * becomes .*
.replace(/\?/g, ".") + // ? becomes .
"$",
"s", // s flag enables multiline matching
)
return regex.test(str)
}
}

View File

@@ -0,0 +1 @@
// Test fixture for ListTool

View File

@@ -0,0 +1 @@
// Test fixture for ListTool

View File

@@ -0,0 +1 @@
// Test fixture for ListTool

View File

@@ -1,17 +1,9 @@
// Bun Snapshot v1, https://goo.gl/fbAQLP
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
exports[`tool.ls basic 1`] = `
"- /home/thdxr/dev/projects/sst/opencode/js/example/
- home/
- thdxr/
- dev/
- projects/
- sst/
- opencode/
- js/
- example/
- ink.tsx
- broken.ts
- cli.ts
"packages/opencode/test/fixtures/example/
broken.ts
cli.ts
ink.tsx
"
`;

View File

@@ -0,0 +1,47 @@
import { describe, expect, test } from "bun:test"
import { App } from "../../src/app/app"
import path from "path"
import { BashTool } from "../../src/tool/bash"
import { Log } from "../../src/util/log"
const ctx = {
sessionID: "test",
messageID: "",
toolCallID: "",
abort: AbortSignal.any([]),
metadata: () => {},
}
const bash = await BashTool.init()
const projectRoot = path.join(__dirname, "../..")
Log.init({ print: false })
describe("tool.bash", () => {
test("basic", async () => {
await App.provide({ cwd: projectRoot }, async () => {
const result = await bash.execute(
{
command: "echo 'test'",
description: "Echo test message",
},
ctx,
)
expect(result.metadata.exit).toBe(0)
expect(result.metadata.stdout).toContain("test")
})
})
test("cd ../ should fail outside of project root", async () => {
await App.provide({ cwd: projectRoot }, async () => {
await expect(
bash.execute(
{
command: "cd ../",
description: "Try to cd to parent directory",
},
ctx,
),
).rejects.toThrow("This command references paths outside of")
})
})
})

View File

@@ -2,23 +2,28 @@ import { describe, expect, test } from "bun:test"
import { App } from "../../src/app/app"
import { GlobTool } from "../../src/tool/glob"
import { ListTool } from "../../src/tool/ls"
import path from "path"
const ctx = {
sessionID: "test",
messageID: "",
toolCallID: "",
abort: AbortSignal.any([]),
metadata: () => {},
}
const glob = await GlobTool()
const list = await ListTool()
const glob = await GlobTool.init()
const list = await ListTool.init()
const projectRoot = path.join(__dirname, "../..")
const fixturePath = path.join(__dirname, "../fixtures/example")
describe("tool.glob", () => {
test("truncate", async () => {
await App.provide({ cwd: process.cwd() }, async () => {
await App.provide({ cwd: projectRoot }, async () => {
let result = await glob.execute(
{
pattern: "../../node_modules/**/*",
path: undefined,
pattern: "**/*",
path: "../../node_modules",
},
ctx,
)
@@ -26,7 +31,7 @@ describe("tool.glob", () => {
})
})
test("basic", async () => {
await App.provide({ cwd: process.cwd() }, async () => {
await App.provide({ cwd: projectRoot }, async () => {
let result = await glob.execute(
{
pattern: "*.json",
@@ -36,7 +41,7 @@ describe("tool.glob", () => {
)
expect(result.metadata).toMatchObject({
truncated: false,
count: 3,
count: 2,
})
})
})
@@ -44,9 +49,12 @@ describe("tool.glob", () => {
describe("tool.ls", () => {
test("basic", async () => {
const result = await App.provide({ cwd: process.cwd() }, async () => {
return await list.execute({ path: "./example", ignore: [".git"] }, ctx)
const result = await App.provide({ cwd: projectRoot }, async () => {
return await list.execute({ path: fixturePath, ignore: [".git"] }, ctx)
})
expect(result.output).toMatchSnapshot()
// Normalize absolute path to relative for consistent snapshots
const normalizedOutput = result.output.replace(fixturePath, "packages/opencode/test/fixtures/example")
expect(normalizedOutput).toMatchSnapshot()
})
})

View File

@@ -1,5 +1,14 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@tsconfig/bun/tsconfig.json",
"compilerOptions": {}
"compilerOptions": {
"lib": [
"ESNext",
"DOM",
"DOM.Iterable"
],
"customConditions": [
"development"
]
}
}

1
packages/plugin/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
dist

View File

@@ -0,0 +1,21 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "0.3.122",
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit"
},
"exports": {
".": "./src/index.ts"
},
"files": [
"dist"
],
"devDependencies": {
"typescript": "catalog:",
"@hey-api/openapi-ts": "0.80.1",
"@tsconfig/node22": "catalog:",
"@opencode-ai/sdk": "workspace:*"
}
}

View File

@@ -0,0 +1,18 @@
#!/usr/bin/env bun
const dir = new URL("..", import.meta.url).pathname
process.chdir(dir)
import { $ } from "bun"
const snapshot = process.env["OPENCODE_SNAPSHOT"] === "true"
await $`bun tsc`
if (snapshot) {
await $`bun publish --tag snapshot --access public`
await $`git checkout package.json`
}
if (!snapshot) {
await $`bun publish --access public`
}

View File

@@ -0,0 +1,7 @@
import { Plugin } from "./index"
export const ExamplePlugin: Plugin = async ({ app, client }) => {
return {
permission: {},
}
}

View File

@@ -0,0 +1,56 @@
import type { Event, createOpencodeClient, App, Model, Provider, Permission, UserMessage, Part } from "@opencode-ai/sdk"
import { $ } from "bun"
export type PluginInput = {
client: ReturnType<typeof createOpencodeClient>
app: App
$: $
}
export type Plugin = (input: PluginInput) => Promise<Hooks>
export interface Hooks {
event?: (input: { event: Event }) => Promise<void>
chat?: {
/**
* Called when a new message is received
*/
message?: (input: {}, output: { message: UserMessage; parts: Part[] }) => Promise<void>
/**
* Modify parameters sent to LLM
*/
params?: (
input: { model: Model; provider: Provider; message: UserMessage },
output: { temperature: number; topP: number },
) => Promise<void>
}
permission?: {
/**
* Called when a permission is asked
*/
ask?: (input: Permission, output: { status: "ask" | "deny" | "allow" }) => Promise<void>
}
tool?: {
execute?: {
/**
* Called before a tool is executed
*/
before?: (
input: { tool: string; sessionID: string; callID: string },
output: {
args: any
},
) => Promise<void>
/**
* Called after a tool is executed
*/
after?: (
input: { tool: string; sessionID: string; callID: string },
output: {
title: string
output: string
metadata: any
},
) => Promise<void>
}
}
}

View File

@@ -0,0 +1,16 @@
{
"$schema": "https://json.schemastore.org/tsconfig.json",
"extends": "@tsconfig/node22/tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"module": "preserve",
"declaration": true,
"moduleResolution": "bundler",
"customConditions": [
"development"
]
},
"include": [
"src"
]
}

View File

@@ -1,15 +0,0 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/debian
{
"name": "Development",
"image": "mcr.microsoft.com/devcontainers/typescript-node:latest",
"features": {
"ghcr.io/devcontainers/features/node:1": {}
},
"postCreateCommand": "yarn install",
"customizations": {
"vscode": {
"extensions": ["esbenp.prettier-vscode"]
}
}
}

View File

@@ -1,7 +0,0 @@
CHANGELOG.md
/ecosystem-tests/*/**
/node_modules
/deno
# don't format tsc output, will break source maps
/dist

View File

@@ -1,7 +0,0 @@
{
"arrowParens": "always",
"experimentalTernaries": true,
"printWidth": 110,
"singleQuote": true,
"trailingComma": "all"
}

View File

@@ -1,3 +0,0 @@
{
".": "0.1.0-alpha.20"
}

View File

@@ -1,4 +0,0 @@
configured_endpoints: 26
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-5748199af356c3243a46a466e73b5d0bab7eaa0c56895e1d0f903d637f61d0bb.yml
openapi_spec_hash: c04f6b6be54b05d9b1283c24e870163b
config_hash: 1ae82c93499b9f0b9ba828b8919f9cb3

View File

@@ -1 +0,0 @@
brew "node"

View File

@@ -1,196 +0,0 @@
# Changelog
## 0.1.0-alpha.20 (2025-07-16)
Full Changelog: [v0.1.0-alpha.19...v0.1.0-alpha.20](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.19...v0.1.0-alpha.20)
### Features
* **api:** api update ([d296473](https://github.com/sst/opencode-sdk-js/commit/d296473db58378932b85d1afaa60942ac5599c49))
* **api:** api update ([af2b587](https://github.com/sst/opencode-sdk-js/commit/af2b5875534a4782fac186542ecb9b04393c9b0a))
## 0.1.0-alpha.19 (2025-07-16)
Full Changelog: [v0.1.0-alpha.18...v0.1.0-alpha.19](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.18...v0.1.0-alpha.19)
### Features
* **api:** api update ([2e505ef](https://github.com/sst/opencode-sdk-js/commit/2e505ef451fdcf49358189c5f76bdc42fb821352))
## 0.1.0-alpha.18 (2025-07-15)
Full Changelog: [v0.1.0-alpha.17...v0.1.0-alpha.18](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.17...v0.1.0-alpha.18)
### Features
* **api:** api update ([25a23e5](https://github.com/sst/opencode-sdk-js/commit/25a23e599f1180754910961df65f0cc044aa2935))
## 0.1.0-alpha.17 (2025-07-15)
Full Changelog: [v0.1.0-alpha.16...v0.1.0-alpha.17](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.16...v0.1.0-alpha.17)
### Features
* **api:** api update ([8b5d592](https://github.com/sst/opencode-sdk-js/commit/8b5d59243a0212f98269412f4483e729e2367a77))
* **api:** api update ([ebd8986](https://github.com/sst/opencode-sdk-js/commit/ebd89862c48be2742eda727c83c01430413e00c0))
## 0.1.0-alpha.16 (2025-07-15)
Full Changelog: [v0.1.0-alpha.15...v0.1.0-alpha.16](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.15...v0.1.0-alpha.16)
### Features
* **api:** api update ([f26379d](https://github.com/sst/opencode-sdk-js/commit/f26379d83ae7094d6ba91c6705a97a3fbd88a55a))
### Chores
* make some internal functions async ([36b1db9](https://github.com/sst/opencode-sdk-js/commit/36b1db9ca9d47d9199e2eab5f0b454b7cd31f58f))
## 0.1.0-alpha.15 (2025-07-05)
Full Changelog: [v0.1.0-alpha.14...v0.1.0-alpha.15](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.14...v0.1.0-alpha.15)
### Features
* **api:** manual updates ([f6ee467](https://github.com/sst/opencode-sdk-js/commit/f6ee46752d0c174c8b934894cf2b140864864208))
### Chores
* **internal:** codegen related update ([47a1a97](https://github.com/sst/opencode-sdk-js/commit/47a1a972e755735d6b5472c61f726ab2face9e62))
## 0.1.0-alpha.14 (2025-07-03)
Full Changelog: [v0.1.0-alpha.13...v0.1.0-alpha.14](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.13...v0.1.0-alpha.14)
### Features
* **api:** api update ([a1d7cf9](https://github.com/sst/opencode-sdk-js/commit/a1d7cf948a2ff47ce4e98b4a52d0e4d213b87bf6))
### Chores
* **internal:** version bump ([f8ad145](https://github.com/sst/opencode-sdk-js/commit/f8ad145b9af0c4a465642630043e59236d5f4e8d))
## 0.1.0-alpha.13 (2025-07-03)
Full Changelog: [v0.1.0-alpha.12...v0.1.0-alpha.13](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.12...v0.1.0-alpha.13)
### Bug Fixes
* avoid console usage ([f96ac97](https://github.com/sst/opencode-sdk-js/commit/f96ac97fbaf7417efda306d8727654d1a4138386))
### Chores
* add docs to RequestOptions type ([1ca6677](https://github.com/sst/opencode-sdk-js/commit/1ca667765c22b706732c61ea3d9d2823aeda0a8e))
## 0.1.0-alpha.12 (2025-07-02)
Full Changelog: [v0.1.0-alpha.11...v0.1.0-alpha.12](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.11...v0.1.0-alpha.12)
### Features
* **api:** update via SDK Studio ([7739340](https://github.com/sst/opencode-sdk-js/commit/77393403648067fe937637c39e80067c347a8c5b))
## 0.1.0-alpha.11 (2025-06-30)
Full Changelog: [v0.1.0-alpha.10...v0.1.0-alpha.11](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.10...v0.1.0-alpha.11)
### Features
* **api:** update via SDK Studio ([2ce98e5](https://github.com/sst/opencode-sdk-js/commit/2ce98e55bf330cca0c38f60f011ffd9063b34ea0))
## 0.1.0-alpha.10 (2025-06-30)
Full Changelog: [v0.1.0-alpha.9...v0.1.0-alpha.10](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.9...v0.1.0-alpha.10)
### Features
* **api:** update via SDK Studio ([fa7c91c](https://github.com/sst/opencode-sdk-js/commit/fa7c91cc2fe52d42be7365ca2c4ce3e48c2e76ac))
### Chores
* **ci:** only run for pushes and fork pull requests ([0e850e5](https://github.com/sst/opencode-sdk-js/commit/0e850e51daac413dcf2d5e30c0ea7a1cd5346c4b))
* **client:** improve path param validation ([bc3ff0e](https://github.com/sst/opencode-sdk-js/commit/bc3ff0ee2de9af8be42deae87d12f003fb5f7aa5))
## 0.1.0-alpha.9 (2025-06-27)
Full Changelog: [v0.1.0-alpha.8...v0.1.0-alpha.9](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.8...v0.1.0-alpha.9)
### Features
* **api:** update via SDK Studio ([7009d10](https://github.com/sst/opencode-sdk-js/commit/7009d10aab99be7102371cee49013ab3edae4450))
* **api:** update via SDK Studio ([e60aa00](https://github.com/sst/opencode-sdk-js/commit/e60aa0024079671e3725ee6f6bfbf8c2dad78da2))
## 0.1.0-alpha.8 (2025-06-27)
Full Changelog: [v0.1.0-alpha.7...v0.1.0-alpha.8](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.7...v0.1.0-alpha.8)
### Features
* **api:** update via SDK Studio ([171e3d5](https://github.com/sst/opencode-sdk-js/commit/171e3d5f3ba69ff9ba8547dac90d85b1a0a137c1))
## 0.1.0-alpha.7 (2025-06-27)
Full Changelog: [v0.1.0-alpha.6...v0.1.0-alpha.7](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.6...v0.1.0-alpha.7)
### Features
* **api:** update via SDK Studio ([14d2d04](https://github.com/sst/opencode-sdk-js/commit/14d2d04d80c1d5880940c9c70a5c1ea200df2ebc))
## 0.1.0-alpha.6 (2025-06-27)
Full Changelog: [v0.1.0-alpha.5...v0.1.0-alpha.6](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.5...v0.1.0-alpha.6)
### Features
* **api:** update via SDK Studio ([45e78b2](https://github.com/sst/opencode-sdk-js/commit/45e78b2f0fca18f537de9986e358aa876fb0b686))
## 0.1.0-alpha.5 (2025-06-27)
Full Changelog: [v0.1.0-alpha.4...v0.1.0-alpha.5](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.4...v0.1.0-alpha.5)
### Features
* **api:** update via SDK Studio ([10a5be9](https://github.com/sst/opencode-sdk-js/commit/10a5be9261c4ba8aeece7bb6921752f5fa6d9f28))
## 0.1.0-alpha.4 (2025-06-27)
Full Changelog: [v0.1.0-alpha.3...v0.1.0-alpha.4](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.3...v0.1.0-alpha.4)
### Features
* **api:** update via SDK Studio ([20dcd17](https://github.com/sst/opencode-sdk-js/commit/20dcd171405b05801e5a56f1b40fd635259b6a94))
## 0.1.0-alpha.3 (2025-06-27)
Full Changelog: [v0.1.0-alpha.2...v0.1.0-alpha.3](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.2...v0.1.0-alpha.3)
### Bug Fixes
* **ci:** release-doctor — report correct token name ([128884f](https://github.com/sst/opencode-sdk-js/commit/128884f4bc64e618177a0b090cd6d52b122a059a))
## 0.1.0-alpha.2 (2025-06-24)
Full Changelog: [v0.1.0-alpha.1...v0.1.0-alpha.2](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.1...v0.1.0-alpha.2)
### Features
* **api:** update via SDK Studio ([2320f32](https://github.com/sst/opencode-sdk-js/commit/2320f32190ab58d15d00d7c3328f9fba2421536c))
## 0.1.0-alpha.1 (2025-06-24)
Full Changelog: [v0.0.1-alpha.0...v0.1.0-alpha.1](https://github.com/sst/opencode-sdk-js/compare/v0.0.1-alpha.0...v0.1.0-alpha.1)
### Features
* **api:** update via SDK Studio ([e448306](https://github.com/sst/opencode-sdk-js/commit/e4483068738cbb10233fca5a9d9d44a9c9815c8b))
* **api:** update via SDK Studio ([b222c96](https://github.com/sst/opencode-sdk-js/commit/b222c96a679a8aeecb60bcf92c247fef90c75b3d))
### Chores
* update SDK settings ([c1481ea](https://github.com/sst/opencode-sdk-js/commit/c1481ea7949c1422bedaeac278600b4ec3f58038))

View File

@@ -1,107 +0,0 @@
## Setting up the environment
This repository uses [`yarn@v1`](https://classic.yarnpkg.com/lang/en/docs/install).
Other package managers may work but are not officially supported for development.
To set up the repository, run:
```sh
$ yarn
$ yarn build
```
This will install all the required dependencies and build output files to `dist/`.
## Modifying/Adding code
Most of the SDK is generated code. Modifications to code will be persisted between generations, but may
result in merge conflicts between manual patches and changes from the generator. The generator will never
modify the contents of the `src/lib/` and `examples/` directories.
## Adding and running examples
All files in the `examples/` directory are not modified by the generator and can be freely edited or added to.
```ts
// add an example to examples/<your-example>.ts
#!/usr/bin/env -S npm run tsn -T
```
```sh
$ chmod +x examples/<your-example>.ts
# run the example against your api
$ yarn tsn -T examples/<your-example>.ts
```
## Using the repository from source
If youd like to use the repository from source, you can either install from git or link to a cloned repository:
To install via git:
```sh
$ npm install git+ssh://git@github.com:sst/opencode-sdk-js.git
```
Alternatively, to link a local copy of the repo:
```sh
# Clone
$ git clone https://www.github.com/sst/opencode-sdk-js
$ cd opencode-sdk-js
# With yarn
$ yarn link
$ cd ../my-package
$ yarn link @opencode-ai/sdk
# With pnpm
$ pnpm link --global
$ cd ../my-package
$ pnpm link -—global @opencode-ai/sdk
```
## Running tests
Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests.
```sh
$ npx prism mock path/to/your/openapi.yml
```
```sh
$ yarn run test
```
## Linting and formatting
This repository uses [prettier](https://www.npmjs.com/package/prettier) and
[eslint](https://www.npmjs.com/package/eslint) to format the code in the repository.
To lint:
```sh
$ yarn lint
```
To format and fix all lint issues automatically:
```sh
$ yarn fix
```
## Publishing and releases
Changes made to this repository via the automated release PR pipeline should publish to npm automatically. If
the changes aren't made through the automated pipeline, you may want to make releases manually.
### Publish with a GitHub workflow
You can release to package managers by using [the `Publish NPM` GitHub action](https://www.github.com/sst/opencode-sdk-js/actions/workflows/publish-npm.yml). This requires a setup organization or repository secret to be set up.
### Publish manually
If you need to manually release a package, you can run the `bin/publish-npm` script with an `NPM_TOKEN` set on
the environment.

View File

@@ -1,370 +0,0 @@
# Opencode TypeScript API Library
[![NPM version](<https://img.shields.io/npm/v/@opencode-ai/sdk.svg?label=npm%20(stable)>)](https://npmjs.org/package/@opencode-ai/sdk) ![npm bundle size](https://img.shields.io/bundlephobia/minzip/@opencode-ai/sdk)
This library provides convenient access to the Opencode REST API from server-side TypeScript or JavaScript.
The REST API documentation can be found on [opencode.ai](https://opencode.ai/docs). The full API of this library can be found in [api.md](api.md).
It is generated with [Stainless](https://www.stainless.com/).
## Installation
```sh
npm install @opencode-ai/sdk
```
## Usage
The full API of this library can be found in [api.md](api.md).
<!-- prettier-ignore -->
```js
import Opencode from '@opencode-ai/sdk';
const client = new Opencode();
const sessions = await client.session.list();
```
## Streaming responses
We provide support for streaming responses using Server Sent Events (SSE).
```ts
import Opencode from '@opencode-ai/sdk';
const client = new Opencode();
const stream = await client.event.list();
for await (const eventListResponse of stream) {
console.log(eventListResponse);
}
```
If you need to cancel a stream, you can `break` from the loop
or call `stream.controller.abort()`.
### Request & Response types
This library includes TypeScript definitions for all request params and response fields. You may import and use them like so:
<!-- prettier-ignore -->
```ts
import Opencode from '@opencode-ai/sdk';
const client = new Opencode();
const sessions: Opencode.SessionListResponse = await client.session.list();
```
Documentation for each method, request param, and response field are available in docstrings and will appear on hover in most modern editors.
## Handling errors
When the library is unable to connect to the API,
or if the API returns a non-success status code (i.e., 4xx or 5xx response),
a subclass of `APIError` will be thrown:
<!-- prettier-ignore -->
```ts
const sessions = await client.session.list().catch(async (err) => {
if (err instanceof Opencode.APIError) {
console.log(err.status); // 400
console.log(err.name); // BadRequestError
console.log(err.headers); // {server: 'nginx', ...}
} else {
throw err;
}
});
```
Error codes are as follows:
| Status Code | Error Type |
| ----------- | -------------------------- |
| 400 | `BadRequestError` |
| 401 | `AuthenticationError` |
| 403 | `PermissionDeniedError` |
| 404 | `NotFoundError` |
| 422 | `UnprocessableEntityError` |
| 429 | `RateLimitError` |
| >=500 | `InternalServerError` |
| N/A | `APIConnectionError` |
### Retries
Certain errors will be automatically retried 2 times by default, with a short exponential backoff.
Connection errors (for example, due to a network connectivity problem), 408 Request Timeout, 409 Conflict,
429 Rate Limit, and >=500 Internal errors will all be retried by default.
You can use the `maxRetries` option to configure or disable this:
<!-- prettier-ignore -->
```js
// Configure the default for all requests:
const client = new Opencode({
maxRetries: 0, // default is 2
});
// Or, configure per-request:
await client.session.list({
maxRetries: 5,
});
```
### Timeouts
Requests time out after 1 minute by default. You can configure this with a `timeout` option:
<!-- prettier-ignore -->
```ts
// Configure the default for all requests:
const client = new Opencode({
timeout: 20 * 1000, // 20 seconds (default is 1 minute)
});
// Override per-request:
await client.session.list({
timeout: 5 * 1000,
});
```
On timeout, an `APIConnectionTimeoutError` is thrown.
Note that requests which time out will be [retried twice by default](#retries).
## Advanced Usage
### Accessing raw Response data (e.g., headers)
The "raw" `Response` returned by `fetch()` can be accessed through the `.asResponse()` method on the `APIPromise` type that all methods return.
This method returns as soon as the headers for a successful response are received and does not consume the response body, so you are free to write custom parsing or streaming logic.
You can also use the `.withResponse()` method to get the raw `Response` along with the parsed data.
Unlike `.asResponse()` this method consumes the body, returning once it is parsed.
<!-- prettier-ignore -->
```ts
const client = new Opencode();
const response = await client.session.list().asResponse();
console.log(response.headers.get('X-My-Header'));
console.log(response.statusText); // access the underlying Response object
const { data: sessions, response: raw } = await client.session.list().withResponse();
console.log(raw.headers.get('X-My-Header'));
console.log(sessions);
```
### Logging
> [!IMPORTANT]
> All log messages are intended for debugging only. The format and content of log messages
> may change between releases.
#### Log levels
The log level can be configured in two ways:
1. Via the `OPENCODE_LOG` environment variable
2. Using the `logLevel` client option (overrides the environment variable if set)
```ts
import Opencode from '@opencode-ai/sdk';
const client = new Opencode({
logLevel: 'debug', // Show all log messages
});
```
Available log levels, from most to least verbose:
- `'debug'` - Show debug messages, info, warnings, and errors
- `'info'` - Show info messages, warnings, and errors
- `'warn'` - Show warnings and errors (default)
- `'error'` - Show only errors
- `'off'` - Disable all logging
At the `'debug'` level, all HTTP requests and responses are logged, including headers and bodies.
Some authentication-related headers are redacted, but sensitive data in request and response bodies
may still be visible.
#### Custom logger
By default, this library logs to `globalThis.console`. You can also provide a custom logger.
Most logging libraries are supported, including [pino](https://www.npmjs.com/package/pino), [winston](https://www.npmjs.com/package/winston), [bunyan](https://www.npmjs.com/package/bunyan), [consola](https://www.npmjs.com/package/consola), [signale](https://www.npmjs.com/package/signale), and [@std/log](https://jsr.io/@std/log). If your logger doesn't work, please open an issue.
When providing a custom logger, the `logLevel` option still controls which messages are emitted, messages
below the configured level will not be sent to your logger.
```ts
import Opencode from '@opencode-ai/sdk';
import pino from 'pino';
const logger = pino();
const client = new Opencode({
logger: logger.child({ name: 'Opencode' }),
logLevel: 'debug', // Send all messages to pino, allowing it to filter
});
```
### Making custom/undocumented requests
This library is typed for convenient access to the documented API. If you need to access undocumented
endpoints, params, or response properties, the library can still be used.
#### Undocumented endpoints
To make requests to undocumented endpoints, you can use `client.get`, `client.post`, and other HTTP verbs.
Options on the client, such as retries, will be respected when making these requests.
```ts
await client.post('/some/path', {
body: { some_prop: 'foo' },
query: { some_query_arg: 'bar' },
});
```
#### Undocumented request params
To make requests using undocumented parameters, you may use `// @ts-expect-error` on the undocumented
parameter. This library doesn't validate at runtime that the request matches the type, so any extra values you
send will be sent as-is.
```ts
client.session.list({
// ...
// @ts-expect-error baz is not yet public
baz: 'undocumented option',
});
```
For requests with the `GET` verb, any extra params will be in the query, all other requests will send the
extra param in the body.
If you want to explicitly send an extra argument, you can do so with the `query`, `body`, and `headers` request
options.
#### Undocumented response properties
To access undocumented response properties, you may access the response object with `// @ts-expect-error` on
the response object, or cast the response object to the requisite type. Like the request params, we do not
validate or strip extra properties from the response from the API.
### Customizing the fetch client
By default, this library expects a global `fetch` function is defined.
If you want to use a different `fetch` function, you can either polyfill the global:
```ts
import fetch from 'my-fetch';
globalThis.fetch = fetch;
```
Or pass it to the client:
```ts
import Opencode from '@opencode-ai/sdk';
import fetch from 'my-fetch';
const client = new Opencode({ fetch });
```
### Fetch options
If you want to set custom `fetch` options without overriding the `fetch` function, you can provide a `fetchOptions` object when instantiating the client or making a request. (Request-specific options override client options.)
```ts
import Opencode from '@opencode-ai/sdk';
const client = new Opencode({
fetchOptions: {
// `RequestInit` options
},
});
```
#### Configuring proxies
To modify proxy behavior, you can provide custom `fetchOptions` that add runtime-specific proxy
options to requests:
<img src="https://raw.githubusercontent.com/stainless-api/sdk-assets/refs/heads/main/node.svg" align="top" width="18" height="21"> **Node** <sup>[[docs](https://github.com/nodejs/undici/blob/main/docs/docs/api/ProxyAgent.md#example---proxyagent-with-fetch)]</sup>
```ts
import Opencode from '@opencode-ai/sdk';
import * as undici from 'undici';
const proxyAgent = new undici.ProxyAgent('http://localhost:8888');
const client = new Opencode({
fetchOptions: {
dispatcher: proxyAgent,
},
});
```
<img src="https://raw.githubusercontent.com/stainless-api/sdk-assets/refs/heads/main/bun.svg" align="top" width="18" height="21"> **Bun** <sup>[[docs](https://bun.sh/guides/http/proxy)]</sup>
```ts
import Opencode from '@opencode-ai/sdk';
const client = new Opencode({
fetchOptions: {
proxy: 'http://localhost:8888',
},
});
```
<img src="https://raw.githubusercontent.com/stainless-api/sdk-assets/refs/heads/main/deno.svg" align="top" width="18" height="21"> **Deno** <sup>[[docs](https://docs.deno.com/api/deno/~/Deno.createHttpClient)]</sup>
```ts
import Opencode from 'npm:@opencode-ai/sdk';
const httpClient = Deno.createHttpClient({ proxy: { url: 'http://localhost:8888' } });
const client = new Opencode({
fetchOptions: {
client: httpClient,
},
});
```
## Frequently Asked Questions
## Semantic versioning
This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions:
1. Changes that only affect static types, without breaking runtime behavior.
2. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals.)_
3. Changes that we do not expect to impact the vast majority of users in practice.
We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience.
We are keen for your feedback; please open an [issue](https://www.github.com/sst/opencode-sdk-js/issues) with questions, bugs, or suggestions.
## Requirements
TypeScript >= 4.9 is supported.
The following runtimes are supported:
- Web browsers (Up-to-date Chrome, Firefox, Safari, Edge, and more)
- Node.js 20 LTS or later ([non-EOL](https://endoflife.date/nodejs)) versions.
- Deno v1.28.0 or higher.
- Bun 1.0 or later.
- Cloudflare Workers.
- Vercel Edge Runtime.
- Jest 28 or greater with the `"node"` environment (`"jsdom"` is not supported at this time).
- Nitro v2.6 or greater.
Note that React Native is not supported at this time.
If you are interested in other runtime environments, please open or upvote an issue on GitHub.
## Contributing
See [the contributing documentation](./CONTRIBUTING.md).

View File

@@ -1,140 +0,0 @@
# Shared
Types:
- <code><a href="./src/resources/shared.ts">MessageAbortedError</a></code>
- <code><a href="./src/resources/shared.ts">ProviderAuthError</a></code>
- <code><a href="./src/resources/shared.ts">UnknownError</a></code>
# Event
Types:
- <code><a href="./src/resources/event.ts">EventListResponse</a></code>
Methods:
- <code title="get /event">client.event.<a href="./src/resources/event.ts">list</a>() -> EventListResponse</code>
# App
Types:
- <code><a href="./src/resources/app.ts">App</a></code>
- <code><a href="./src/resources/app.ts">Mode</a></code>
- <code><a href="./src/resources/app.ts">Model</a></code>
- <code><a href="./src/resources/app.ts">Provider</a></code>
- <code><a href="./src/resources/app.ts">AppInitResponse</a></code>
- <code><a href="./src/resources/app.ts">AppLogResponse</a></code>
- <code><a href="./src/resources/app.ts">AppModesResponse</a></code>
- <code><a href="./src/resources/app.ts">AppProvidersResponse</a></code>
Methods:
- <code title="get /app">client.app.<a href="./src/resources/app.ts">get</a>() -> App</code>
- <code title="post /app/init">client.app.<a href="./src/resources/app.ts">init</a>() -> AppInitResponse</code>
- <code title="post /log">client.app.<a href="./src/resources/app.ts">log</a>({ ...params }) -> AppLogResponse</code>
- <code title="get /mode">client.app.<a href="./src/resources/app.ts">modes</a>() -> AppModesResponse</code>
- <code title="get /config/providers">client.app.<a href="./src/resources/app.ts">providers</a>() -> AppProvidersResponse</code>
# Find
Types:
- <code><a href="./src/resources/find.ts">Match</a></code>
- <code><a href="./src/resources/find.ts">Symbol</a></code>
- <code><a href="./src/resources/find.ts">FindFilesResponse</a></code>
- <code><a href="./src/resources/find.ts">FindSymbolsResponse</a></code>
- <code><a href="./src/resources/find.ts">FindTextResponse</a></code>
Methods:
- <code title="get /find/file">client.find.<a href="./src/resources/find.ts">files</a>({ ...params }) -> FindFilesResponse</code>
- <code title="get /find/symbol">client.find.<a href="./src/resources/find.ts">symbols</a>({ ...params }) -> FindSymbolsResponse</code>
- <code title="get /find">client.find.<a href="./src/resources/find.ts">text</a>({ ...params }) -> FindTextResponse</code>
# File
Types:
- <code><a href="./src/resources/file.ts">File</a></code>
- <code><a href="./src/resources/file.ts">FileReadResponse</a></code>
- <code><a href="./src/resources/file.ts">FileStatusResponse</a></code>
Methods:
- <code title="get /file">client.file.<a href="./src/resources/file.ts">read</a>({ ...params }) -> FileReadResponse</code>
- <code title="get /file/status">client.file.<a href="./src/resources/file.ts">status</a>() -> FileStatusResponse</code>
# Config
Types:
- <code><a href="./src/resources/config.ts">Config</a></code>
- <code><a href="./src/resources/config.ts">KeybindsConfig</a></code>
- <code><a href="./src/resources/config.ts">McpLocalConfig</a></code>
- <code><a href="./src/resources/config.ts">McpRemoteConfig</a></code>
- <code><a href="./src/resources/config.ts">ModeConfig</a></code>
Methods:
- <code title="get /config">client.config.<a href="./src/resources/config.ts">get</a>() -> Config</code>
# Session
Types:
- <code><a href="./src/resources/session.ts">AssistantMessage</a></code>
- <code><a href="./src/resources/session.ts">FilePart</a></code>
- <code><a href="./src/resources/session.ts">FilePartInput</a></code>
- <code><a href="./src/resources/session.ts">FilePartSource</a></code>
- <code><a href="./src/resources/session.ts">FilePartSourceText</a></code>
- <code><a href="./src/resources/session.ts">FileSource</a></code>
- <code><a href="./src/resources/session.ts">Message</a></code>
- <code><a href="./src/resources/session.ts">Part</a></code>
- <code><a href="./src/resources/session.ts">Session</a></code>
- <code><a href="./src/resources/session.ts">SnapshotPart</a></code>
- <code><a href="./src/resources/session.ts">StepFinishPart</a></code>
- <code><a href="./src/resources/session.ts">StepStartPart</a></code>
- <code><a href="./src/resources/session.ts">SymbolSource</a></code>
- <code><a href="./src/resources/session.ts">TextPart</a></code>
- <code><a href="./src/resources/session.ts">TextPartInput</a></code>
- <code><a href="./src/resources/session.ts">ToolPart</a></code>
- <code><a href="./src/resources/session.ts">ToolStateCompleted</a></code>
- <code><a href="./src/resources/session.ts">ToolStateError</a></code>
- <code><a href="./src/resources/session.ts">ToolStatePending</a></code>
- <code><a href="./src/resources/session.ts">ToolStateRunning</a></code>
- <code><a href="./src/resources/session.ts">UserMessage</a></code>
- <code><a href="./src/resources/session.ts">SessionListResponse</a></code>
- <code><a href="./src/resources/session.ts">SessionDeleteResponse</a></code>
- <code><a href="./src/resources/session.ts">SessionAbortResponse</a></code>
- <code><a href="./src/resources/session.ts">SessionInitResponse</a></code>
- <code><a href="./src/resources/session.ts">SessionMessagesResponse</a></code>
- <code><a href="./src/resources/session.ts">SessionSummarizeResponse</a></code>
Methods:
- <code title="post /session">client.session.<a href="./src/resources/session.ts">create</a>() -> Session</code>
- <code title="get /session">client.session.<a href="./src/resources/session.ts">list</a>() -> SessionListResponse</code>
- <code title="delete /session/{id}">client.session.<a href="./src/resources/session.ts">delete</a>(id) -> SessionDeleteResponse</code>
- <code title="post /session/{id}/abort">client.session.<a href="./src/resources/session.ts">abort</a>(id) -> SessionAbortResponse</code>
- <code title="post /session/{id}/message">client.session.<a href="./src/resources/session.ts">chat</a>(id, { ...params }) -> AssistantMessage</code>
- <code title="post /session/{id}/init">client.session.<a href="./src/resources/session.ts">init</a>(id, { ...params }) -> SessionInitResponse</code>
- <code title="get /session/{id}/message">client.session.<a href="./src/resources/session.ts">messages</a>(id) -> SessionMessagesResponse</code>
- <code title="post /session/{id}/revert">client.session.<a href="./src/resources/session.ts">revert</a>(id, { ...params }) -> Session</code>
- <code title="post /session/{id}/share">client.session.<a href="./src/resources/session.ts">share</a>(id) -> Session</code>
- <code title="post /session/{id}/summarize">client.session.<a href="./src/resources/session.ts">summarize</a>(id, { ...params }) -> SessionSummarizeResponse</code>
- <code title="post /session/{id}/unrevert">client.session.<a href="./src/resources/session.ts">unrevert</a>(id) -> Session</code>
- <code title="delete /session/{id}/share">client.session.<a href="./src/resources/session.ts">unshare</a>(id) -> Session</code>
# Tui
Types:
- <code><a href="./src/resources/tui.ts">TuiAppendPromptResponse</a></code>
- <code><a href="./src/resources/tui.ts">TuiOpenHelpResponse</a></code>
Methods:
- <code title="post /tui/append-prompt">client.tui.<a href="./src/resources/tui.ts">appendPrompt</a>({ ...params }) -> TuiAppendPromptResponse</code>
- <code title="post /tui/open-help">client.tui.<a href="./src/resources/tui.ts">openHelp</a>() -> TuiOpenHelpResponse</code>

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