Compare commits

..
168 Commits
Author SHA1 Message Date
Dax Raad 857a3cd522 hint back to llm when tool does not exist 2025-08-05 15:58:12 -04:00
Jay V 6ed774ef62 docs: edit 2025-08-05 12:55:57 -04:00
adamdotdevin 5e825a4b6a chore: cleanup old sdk 2025-08-05 11:46:12 -05:00
Dax Raad 3db8e7c2b6 ci: send stats to posthog 2025-08-05 12:01:48 -04:00
GitHub Action b459055757 ignore: update download stats 2025-08-05 2025-08-05 12:04:48 +00:00
Yihui KhuuandGitHub 2b195e82ee fix: allow disabling the default general agent (#1616) 2025-08-05 05:20:00 -05:00
Omar ShaarawiandGitHub 58e889796c validate file part bounds to prevent panic (#1612) 2025-08-05 05:18:50 -05:00
Aiden ClineandGitHub 51498c8de4 docs: make formatter docs a bit more clear (#1613) 2025-08-05 05:17:56 -05:00
Aiden ClineandGitHub 7a495faa49 fix: server.root is not a function (#1614) 2025-08-05 05:17:32 -05:00
Timo ClasenandGitHub 4957fca718 fix(plugins): improve session idle event (#1615) 2025-08-05 05:16:43 -05:00
opencode 8168626cd3 release: v0.3.130 2025-08-05 03:25:28 +00:00
Dax Raad b824809605 re-export shell $ for plugin 2025-08-04 23:20:04 -04:00
opencode 5536b14347 release: v0.3.129 2025-08-05 01:18:50 +00:00
Dax Raad 01efe236ef fix @opencode-ai/plugin exports 2025-08-04 21:12:18 -04:00
Frank 7a1f96399d sync 2025-08-04 21:09:44 -04:00
Frank 40036abb9d wip: gateway 2025-08-04 21:08:29 -04:00
Jay V 2970ba6416 docs: lock 2025-08-04 19:53:50 -04:00
Jay V 81412b6197 docs: edit new docs 2025-08-04 19:52:03 -04:00
Mahamed-BelkheirandGitHub 5bf7691ea6 fix: default value for models with no cost object (#1601) 2025-08-04 16:45:35 -05:00
Min Chun FuandGitHub b1055a74d3 added vesper theme (#1602) 2025-08-04 16:45:00 -05:00
Aiden ClineandGitHub ffcb27fa9a docs: make plugins page exposed (#1603) 2025-08-04 16:44:28 -05:00
opencode 38819e89b8 release: v0.3.128 2025-08-04 16:20:39 +00:00
Dax Raad 0a42068fbb hack to return tool call errors back to model 2025-08-04 12:15:24 -04:00
opencode b05decc572 release: v0.3.127 2025-08-04 16:06:13 +00:00
GitHub Action c2f487906a ignore: update download stats 2025-08-04 2025-08-04 12:04:34 +00:00
Aiden ClineandGitHub ae78ec7a0c fix double help printing (#1580) 2025-08-04 05:03:27 -05:00
Frank e8c03f13dd fix docs 2025-08-04 00:23:02 -04:00
Dax Raad f85d30c484 wip: plugins 2025-08-03 21:43:58 -04:00
Dax Raad 1bac46612c wip: plugin load from package 2025-08-03 21:19:03 -04:00
Dax Raad 9ab3462821 Add workflow_dispatch trigger to typecheck workflow 2025-08-03 17:16:20 -04:00
Aiden ClineandGitHub 3b36822696 fix: patch marked-shiki (#1569) 2025-08-03 16:13:35 -05:00
Dax Raad 5b731479d5 Add typecheck workflow 2025-08-03 17:12:23 -04:00
Dax Raad a50bef6913 ignore: cleanup 2025-08-03 17:09:30 -04:00
Yordis PrietoandGitHub ed397c5057 chore: add ts-expected-error (#1575) 2025-08-03 17:09:19 -04:00
Yordis PrietoandGitHub c9187a9f3a chore: remove unnecessary TypeScript error suppression (#1571) 2025-08-03 15:50:08 -04:00
opencode 2c67b26b5d release: v0.3.126 2025-08-03 19:45:14 +00:00
Dax Raad 170b94a99e ci: ignore 2025-08-03 15:39:34 -04:00
Dax Raad cd58f10e3c ci: ignore 2025-08-03 15:38:39 -04:00
Dax Raad ea85fdf3cd fix bash tool not showing stderr 2025-08-03 15:34:52 -04:00
Aiden ClineandGitHub edda26ab33 tweak: filter out duplicate instructions (#1567) 2025-08-03 15:10:21 -04:00
Dax Raad ea4e1913c0 increase models.dev polling interval to hourly 2025-08-03 14:58:35 -04:00
Aiden ClineandGitHub 5eebc8ab51 docs: fix mixed up documentation (#1564) 2025-08-03 13:01:09 -05:00
Dax Raad 21c52fd5cb fix bash tool getting stuck on interactive commands 2025-08-03 13:52:50 -04:00
opencode 5e8634afaf release: v0.3.123 2025-08-03 17:13:33 +00:00
Dax Raad d4bac5cdbd ci: ignore 2025-08-03 13:12:35 -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 RotaandGitHub 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 ClineandGitHub 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 PangolierandGitHub 9255c507d6 Share link hotfix (#1513) 2025-08-03 03:02:24 -04:00
Yordis PrietoandGitHub 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 ClineandGitHub ae6e47bb42 tweak: make gh action ignore url mentions of opencode (#1531) 2025-08-02 09:31:23 -05:00
Dominik EngelhardtandGitHub 42a5fcead4 Choose model according to the docs (#1536) 2025-08-02 09:29:03 -05:00
Yihui KhuuandGitHub 8ad83f71a9 fix(tui): attachment highlighting issues in messages (#1534) 2025-08-02 09:26:44 -05:00
Yihui KhuuandGitHub fa95c09cdc fix(tui): attachment source is not stored when using message from message history (#1542) 2025-08-02 09:23:32 -05:00
Aiden ClineandGitHub 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 GonzalezandGitHub 8f45a0e227 feat(models): enable Kimi k2 ⇄ Claude trajectory handoff (#1525) 2025-08-01 23:05:06 -04:00
Aiden ClineandGitHub 6581741318 fix: include stderr in bash tool output (#1511) 2025-08-01 19:20:32 -05:00
Aiden ClineandGitHub 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 EliasandGitHub 5592ce8eaf fix(docs): Fix formatting for Node.js installation section (#1497) 2025-08-01 14:15:38 -04:00
CodinCatandGitHub 510fe8a72a handle the optional v in upgrade command when using curl (#1500) 2025-08-01 14:15:22 -04:00
Yordis PrietoandGitHub 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 ClineandGitHub 022c979d28 tweak: sanitize mcp server names (#831) 2025-08-01 09:11:40 -05:00
Aiden ClineandGitHub 4172e3ad28 fix: bash tool errors for chmod (#1502) 2025-08-01 09:10:09 -05:00
Aiden ClineandGitHub 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 ClasenandGitHub 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
FrankandGitHub 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
publish-vscode / publish (push) Has been cancelled
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 ClineandGitHub 168350c981 fix: load global jsonc (#1479) 2025-07-31 15:02:28 -05:00
Aiden ClineandGitHub 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]GitHubopencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>jayair
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
neolooongandGitHub 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 ClineandGitHub 7e9050edb9 feat: jsonc configuration file support (#1434) 2025-07-31 06:25:26 -05:00
Aiden ClineandGitHub 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
DaxandDax Raad 0bdbe6261a ci: new publish method (#1451) 2025-07-31 01:05:35 -04:00
DaxandGitHub 33cef075d2 ci: new publish method (#1451) 2025-07-31 01:00:29 -04:00
Simon Westlin GreenandGitHub b09ebf4645 Use responses API for Azure (#1428)
publish / publish (push) Has been cancelled
2025-07-30 23:22:59 -04:00
Robert HoldenandGitHub 3268c61813 feat: mode directory markdown configuration loading (#1377) 2025-07-30 23:22:43 -04:00
JoshandGitHub 4a221868da Add http-referer header for vercel ai gateway requests (#1403) 2025-07-30 23:22:24 -04:00
Yordis PrietoandGitHub 31b8e3d5ab docs: clarify Bun's default registry resolution in index.ts (#1438) 2025-07-30 23:21:07 -04:00
CodinCatandGitHub 1a78d833a8 fix typo in bash.ts (#1444) 2025-07-30 23:20:48 -04:00
DaxandGitHub 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 PrietoandGitHub c38b091895 fix: update glob pattern and path in tool test (#1436) 2025-07-30 15:42:13 -05:00
Yordis PrietoandGitHub 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
publish / publish (push) Has been cancelled
2025-07-30 15:08:25 -04:00
Dax Raad 190dee080c release undo/redo 2025-07-30 13:09:18 -04:00
Aiden ClineandGitHub 09074dc639 fix: attachment highlighting (#1427) 2025-07-30 11:43:34 -05:00
Aiden ClineandGitHub 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 HuckabyandGitHub 54dc937fa1 fix: quick grammar and spelling check (#1402) 2025-07-30 05:54:47 -05:00
Aiden ClineandGitHub b5219f7585 tweak: adjust astro css to render mixed nested lists (#1411) 2025-07-30 05:51:52 -05:00
0bd0453866 build: add @octokit/rest to opencode dependencies (#1396)
Co-authored-by: Frank <frank@sst.dev>
2025-07-29 22:33:25 -04:00
310 changed files with 9725 additions and 14872 deletions
+5 -3
View File
@@ -7,8 +7,10 @@ on:
jobs:
opencode:
if: |
contains(github.event.comment.body, '/oc') ||
contains(github.event.comment.body, '/opencode')
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
@@ -24,4 +26,4 @@ jobs:
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
with:
model: anthropic/claude-sonnet-4-20250514
model: anthropic/claude-sonnet-4-20250514
+24 -15
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 }}
+3 -1
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: |
@@ -30,3 +30,5 @@ jobs:
git add STATS.md
git diff --staged --quiet || git commit -m "ignore: update download stats $(date -I)"
git push
env:
POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
+24
View File
@@ -0,0 +1,24 @@
name: Typecheck
on:
pull_request:
branches: [dev]
workflow_dispatch:
jobs:
typecheck:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.2.19
- name: Install dependencies
run: bun install
- name: Run typecheck
run: bun typecheck
+1 -1
View File
@@ -5,4 +5,4 @@ node_modules
.idea
.vscode
openapi.json
scratch
playground
+10
View File
@@ -0,0 +1,10 @@
import { Plugin } from "./index"
export const ExamplePlugin: Plugin = async ({ app, client, $ }) => {
return {
permission: {},
async "chat.params"(input, output) {
output.topP = 1
},
}
}
+11 -12
View File
@@ -1,13 +1,12 @@
## Style
## IMPORTANT
- prefer single word variable/function names
- avoid try catch where possible - prefer to let exceptions bubble up
- avoid else statements where possible
- do not make useless helper functions - inline functionality unless the
function is reusable or composable
- prefer Bun apis
## Workflow
- you can regenerate the golang sdk by calling ./scripts/stainless.ts
- we use bun for everything
- Try to keep things in one function unless composable or reusable
- DO NOT do unnecessary destructuring of variables
- DO NOT use `else` statements unless necessary
- DO NOT use `try`/`catch` if it can be avoided
- AVOID `try`/`catch` where possible
- AVOID `else` statements
- AVOID using `any` type
- AVOID `let` statements
- PREFER single word variable names where possible
- Use as many bun apis as possible like Bun.file()
+39 -31
View File
@@ -1,33 +1,41 @@
# 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) |
| 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) |
| 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) |
| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) |
| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) |
+231 -988
View File
File diff suppressed because it is too large Load Diff
+12
View File
@@ -46,3 +46,15 @@ new sst.cloudflare.x.Astro("Web", {
VITE_API_URL: api.url,
},
})
const OPENCODE_API_KEY = new sst.Secret("OPENCODE_API_KEY")
const ANTHROPIC_API_KEY = new sst.Secret("ANTHROPIC_API_KEY")
const OPENAI_API_KEY = new sst.Secret("OPENAI_API_KEY")
const ZHIPU_API_KEY = new sst.Secret("ZHIPU_API_KEY")
export const gateway = new sst.cloudflare.Worker("GatewayApi", {
domain: `api.gateway.${domain}`,
handler: "packages/function/src/gateway.ts",
url: true,
link: [OPENCODE_API_KEY, ANTHROPIC_API_KEY, OPENAI_API_KEY, ZHIPU_API_KEY],
})
-19
View File
@@ -1,24 +1,5 @@
{
"$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": {}
}
}
},
"mcp": {
"context7": {
"type": "remote",
+11 -6
View File
@@ -5,21 +5,24 @@
"type": "module",
"packageManager": "bun@1.2.14",
"scripts": {
"dev": "bun run packages/opencode/src/index.ts",
"dev": "bun run --conditions=development 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": {
"@types/node": "22.13.9",
"ai": "5.0.0-beta.21",
"@tsconfig/node22": "22.0.2",
"ai": "5.0.0-beta.34",
"hono": "4.7.10",
"typescript": "5.8.2",
"zod": "3.25.49"
"zod": "3.25.49",
"remeda": "2.26.0"
}
},
"devDependencies": {
@@ -40,5 +43,7 @@
"protobufjs",
"sharp"
],
"patchedDependencies": {}
"patchedDependencies": {
"marked-shiki@1.2.0": "patches/marked-shiki@1.2.0.patch"
}
}
+8 -3
View File
@@ -1,17 +1,22 @@
{
"name": "@opencode/function",
"version": "0.0.1",
"version": "0.3.130",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
"devDependencies": {
"@cloudflare/workers-types": "4.20250522.0",
"typescript": "catalog:",
"@types/node": "catalog:"
"@types/node": "catalog:",
"openai": "5.11.0",
"typescript": "catalog:"
},
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
"@ai-sdk/openai-compatible": "1.0.1",
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "22.0.0",
"ai": "catalog:",
"hono": "catalog:",
"jose": "6.0.11"
}
+499
View File
@@ -0,0 +1,499 @@
import { Hono, Context, Next } from "hono"
import { Resource } from "sst"
import { generateText, streamText } from "ai"
import { createAnthropic } from "@ai-sdk/anthropic"
import { createOpenAI } from "@ai-sdk/openai"
import { createOpenAICompatible } from "@ai-sdk/openai-compatible"
import { type LanguageModelV2Prompt } from "@ai-sdk/provider"
import { type ChatCompletionCreateParamsBase } from "openai/resources/chat/completions"
type Env = {}
const auth = async (c: Context, next: Next) => {
const authHeader = c.req.header("authorization")
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return c.json(
{
error: {
message: "Missing API key.",
type: "invalid_request_error",
param: null,
code: "unauthorized",
},
},
401,
)
}
const apiKey = authHeader.split(" ")[1]
// Replace with your validation logic
if (apiKey !== Resource.OPENCODE_API_KEY.value) {
return c.json(
{
error: {
message: "Invalid API key.",
type: "invalid_request_error",
param: null,
code: "unauthorized",
},
},
401,
)
}
await next()
}
export default new Hono<{ Bindings: Env }>()
.get("/", (c) => c.text("Hello, world!"))
.post("/v1/chat/completions", auth, async (c) => {
try {
const body = await c.req.json<ChatCompletionCreateParamsBase>()
console.log(body)
const model = (() => {
const [provider, ...parts] = body.model.split("/")
const model = parts.join("/")
if (provider === "anthropic" && model === "claude-sonnet-4") {
return createAnthropic({
apiKey: Resource.ANTHROPIC_API_KEY.value,
})("claude-sonnet-4-20250514")
}
if (provider === "openai" && model === "gpt-4.1") {
return createOpenAI({
apiKey: Resource.OPENAI_API_KEY.value,
})("gpt-4.1")
}
if (provider === "zhipuai" && model === "glm-4.5-flash") {
return createOpenAICompatible({
name: "Zhipu AI",
baseURL: "https://api.z.ai/api/paas/v4",
apiKey: Resource.ZHIPU_API_KEY.value,
})("glm-4.5-flash")
}
throw new Error(`Unsupported provider: ${provider}`)
})()
const requestBody = transformOpenAIRequestToAiSDK()
return body.stream ? await handleStream() : await handleGenerate()
async function handleStream() {
const result = await streamText({
model,
...requestBody,
})
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
const id = `chatcmpl-${Date.now()}`
const created = Math.floor(Date.now() / 1000)
try {
for await (const chunk of result.fullStream) {
// TODO
//console.log("!!! CHUCK !!!", chunk);
switch (chunk.type) {
case "text-delta": {
const data = {
id,
object: "chat.completion.chunk",
created,
model: body.model,
choices: [
{
index: 0,
delta: {
content: chunk.text,
},
finish_reason: null,
},
],
}
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
break
}
case "reasoning-delta": {
const data = {
id,
object: "chat.completion.chunk",
created,
model: body.model,
choices: [
{
index: 0,
delta: {
reasoning_content: chunk.text,
},
finish_reason: null,
},
],
}
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
break
}
case "tool-call": {
const data = {
id,
object: "chat.completion.chunk",
created,
model: body.model,
choices: [
{
index: 0,
delta: {
tool_calls: [
{
id: chunk.toolCallId,
type: "function",
function: {
name: chunk.toolName,
arguments: JSON.stringify(chunk.input),
},
},
],
},
finish_reason: null,
},
],
}
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
break
}
case "error": {
const data = {
id,
object: "chat.completion.chunk",
created,
model: body.model,
error: {
message: chunk.error,
type: "server_error",
},
}
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
controller.enqueue(encoder.encode("data: [DONE]\n\n"))
controller.close()
break
}
case "finish": {
const finishReason =
{
stop: "stop",
length: "length",
"content-filter": "content_filter",
"tool-calls": "tool_calls",
error: "stop",
other: "stop",
unknown: "stop",
}[chunk.finishReason] || "stop"
const data = {
id,
object: "chat.completion.chunk",
created,
model: body.model,
choices: [
{
index: 0,
delta: {},
finish_reason: finishReason,
},
],
usage: {
prompt_tokens: chunk.totalUsage.inputTokens,
completion_tokens: chunk.totalUsage.outputTokens,
total_tokens: chunk.totalUsage.totalTokens,
completion_tokens_details: {
reasoning_tokens: chunk.totalUsage.reasoningTokens,
},
prompt_tokens_details: {
cached_tokens: chunk.totalUsage.cachedInputTokens,
},
},
}
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
controller.enqueue(encoder.encode("data: [DONE]\n\n"))
controller.close()
break
}
//case "stream-start":
//case "response-metadata":
case "start-step":
case "finish-step":
case "text-start":
case "text-end":
case "reasoning-start":
case "reasoning-end":
case "tool-input-start":
case "tool-input-delta":
case "tool-input-end":
case "raw":
default:
// Log unknown chunk types for debugging
console.warn(`Unknown chunk type: ${(chunk as any).type}`)
break
}
}
} catch (error) {
controller.error(error)
}
},
})
return new Response(stream, {
headers: {
"Content-Type": "text/plain; charset=utf-8",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
})
}
async function handleGenerate() {
const response = await generateText({
model,
...requestBody,
})
return c.json({
id: `chatcmpl-${Date.now()}`,
object: "chat.completion" as const,
created: Math.floor(Date.now() / 1000),
model: body.model,
choices: [
{
index: 0,
message: {
role: "assistant" as const,
content: response.content?.find((c) => c.type === "text")?.text ?? "",
reasoning_content: response.content?.find((c) => c.type === "reasoning")?.text,
tool_calls: response.content
?.filter((c) => c.type === "tool-call")
.map((toolCall) => ({
id: toolCall.toolCallId,
type: "function" as const,
function: {
name: toolCall.toolName,
arguments: toolCall.input,
},
})),
},
finish_reason:
(
{
stop: "stop",
length: "length",
"content-filter": "content_filter",
"tool-calls": "tool_calls",
error: "stop",
other: "stop",
unknown: "stop",
} as const
)[response.finishReason] || "stop",
},
],
usage: {
prompt_tokens: response.usage?.inputTokens,
completion_tokens: response.usage?.outputTokens,
total_tokens: response.usage?.totalTokens,
completion_tokens_details: {
reasoning_tokens: response.usage?.reasoningTokens,
},
prompt_tokens_details: {
cached_tokens: response.usage?.cachedInputTokens,
},
},
})
}
function transformOpenAIRequestToAiSDK() {
const prompt = transformMessages()
return {
prompt,
maxOutputTokens: body.max_tokens ?? body.max_completion_tokens ?? undefined,
temperature: body.temperature ?? undefined,
topP: body.top_p ?? undefined,
frequencyPenalty: body.frequency_penalty ?? undefined,
presencePenalty: body.presence_penalty ?? undefined,
providerOptions: body.reasoning_effort
? {
anthropic: {
reasoningEffort: body.reasoning_effort,
},
}
: undefined,
stopSequences: (typeof body.stop === "string" ? [body.stop] : body.stop) ?? undefined,
responseFormat: (() => {
if (!body.response_format) return { type: "text" }
if (body.response_format.type === "json_schema")
return {
type: "json",
schema: body.response_format.json_schema.schema,
name: body.response_format.json_schema.name,
description: body.response_format.json_schema.description,
}
if (body.response_format.type === "json_object") return { type: "json" }
throw new Error("Unsupported response format")
})(),
seed: body.seed ?? undefined,
}
function transformTools() {
const { tools, tool_choice } = body
if (!tools || tools.length === 0) {
return { tools: undefined, toolChoice: undefined }
}
const aiSdkTools = tools.reduce(
(acc, tool) => {
acc[tool.function.name] = {
type: "function" as const,
name: tool.function.name,
description: tool.function.description,
inputSchema: tool.function.parameters,
}
return acc
},
{} as Record<string, any>,
)
let aiSdkToolChoice
if (tool_choice == null) {
aiSdkToolChoice = undefined
} else if (tool_choice === "auto") {
aiSdkToolChoice = "auto"
} else if (tool_choice === "none") {
aiSdkToolChoice = "none"
} else if (tool_choice === "required") {
aiSdkToolChoice = "required"
} else if (tool_choice.type === "function") {
aiSdkToolChoice = {
type: "tool",
toolName: tool_choice.function.name,
}
}
return { tools: aiSdkTools, toolChoice: aiSdkToolChoice }
}
function transformMessages() {
const { messages } = body
const prompt: LanguageModelV2Prompt = []
for (const message of messages) {
switch (message.role) {
case "system": {
prompt.push({
role: "system",
content: message.content as string,
})
break
}
case "user": {
if (typeof message.content === "string") {
prompt.push({
role: "user",
content: [{ type: "text", text: message.content }],
})
} else {
const content = message.content.map((part) => {
switch (part.type) {
case "text":
return { type: "text" as const, text: part.text }
case "image_url":
return {
type: "file" as const,
mediaType: "image/jpeg" as const,
data: part.image_url.url,
}
default:
throw new Error(`Unsupported content part type: ${(part as any).type}`)
}
})
prompt.push({
role: "user",
content,
})
}
break
}
case "assistant": {
const content: Array<
| { type: "text"; text: string }
| {
type: "tool-call"
toolCallId: string
toolName: string
input: any
}
> = []
if (message.content) {
content.push({
type: "text",
text: message.content as string,
})
}
if (message.tool_calls) {
for (const toolCall of message.tool_calls) {
content.push({
type: "tool-call",
toolCallId: toolCall.id,
toolName: toolCall.function.name,
input: JSON.parse(toolCall.function.arguments),
})
}
}
prompt.push({
role: "assistant",
content,
})
break
}
case "tool": {
prompt.push({
role: "tool",
content: [
{
type: "tool-result",
toolName: "placeholder",
toolCallId: message.tool_call_id,
output: {
type: "text",
value: message.content as string,
},
},
],
})
break
}
default: {
throw new Error(`Unsupported message role: ${message.role}`)
}
}
}
return prompt
}
}
} catch (error: any) {
return c.json({ error: { message: error.message } }, 500)
}
})
.all("*", (c) => c.text("Not Found"))
+17
View File
@@ -6,6 +6,10 @@
import "sst"
declare module "sst" {
export interface Resource {
"ANTHROPIC_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
"GITHUB_APP_ID": {
"type": "sst.sst.Secret"
"value": string
@@ -14,10 +18,22 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
"OPENAI_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
"OPENCODE_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
"Web": {
"type": "sst.cloudflare.Astro"
"url": string
}
"ZHIPU_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
}
}
// cloudflare
@@ -26,6 +42,7 @@ declare module "sst" {
export interface Resource {
"Api": cloudflare.Service
"Bucket": cloudflare.R2Bucket
"GatewayApi": cloudflare.Service
}
}
-13
View File
@@ -17,19 +17,6 @@
- **Error handling**: Use Result patterns, avoid throwing exceptions in tools
- **File structure**: Namespace-based organization (e.g., `Tool.define()`, `Session.create()`)
## IMPORTANT
- Try to keep things in one function unless composable or reusable
- DO NOT do unnecessary destructuring of variables
- DO NOT use `else` statements unless necessary
- DO NOT use `try`/`catch` if it can be avoided
- AVOID `try`/`catch` where possible
- AVOID `else` statements
- AVOID using `any` type
- AVOID `let` statements
- PREFER single word variable names where possible
- Use as many bun apis as possible like Bun.file()
## Architecture
- **Tools**: Implement `Tool.Info` interface with `execute()` method
+12 -4
View File
@@ -1,12 +1,12 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "0.0.5",
"version": "0.3.130",
"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"
@@ -30,10 +30,14 @@
"dependencies": {
"@actions/core": "1.11.1",
"@actions/github": "6.0.1",
"@clack/prompts": "0.11.0",
"@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:",
@@ -43,8 +47,12 @@
"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",
+10 -16
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:") &&
+4 -1
View File
@@ -38,7 +38,10 @@ export namespace Agent {
},
}
for (const [key, value] of Object.entries(cfg.agent ?? {})) {
if (value.disable) continue
if (value.disable) {
delete result[key]
continue
}
let item = result[key]
if (!item)
item = result[key] = {
+1
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, {
+2
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()
+1 -1
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()
+7 -6
View File
@@ -75,6 +75,7 @@ export const AuthLoginCommand = cmd({
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())
@@ -99,7 +100,7 @@ export const AuthLoginCommand = cmd({
prompts.outro("Done")
return
}
UI.empty()
await ModelsDev.refresh().catch(() => {})
const providers = await ModelsDev.get()
const priority: Record<string, number> = {
anthropic: 0,
@@ -109,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: [
@@ -138,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\//, "")
@@ -192,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()
@@ -228,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()
@@ -301,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, {
+7 -5
View File
@@ -318,8 +318,10 @@ on:
jobs:
opencode:
if: |
contains(github.event.comment.body, '/oc') ||
contains(github.event.comment.body, '/opencode')
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
@@ -784,7 +786,7 @@ export const GithubRunCommand = cmd({
console.log("Pushing to new branch...")
await $`git add .`
await $`git commit -m "${summary}
Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
await $`git push -u origin ${branch}`
}
@@ -793,7 +795,7 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
console.log("Pushing to local branch...")
await $`git add .`
await $`git commit -m "${summary}
Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
await $`git push`
}
@@ -805,7 +807,7 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
await $`git add .`
await $`git commit -m "${summary}
Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
await $`git push fork HEAD:${remoteBranch}`
}
+3 -2
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"
+14 -4
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()
+1 -1
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`)
+5 -1
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`,
+139 -24
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"
@@ -12,6 +13,7 @@ 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" })
@@ -22,13 +24,13 @@ export namespace Config {
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 load(Flag.OPENCODE_CONFIG))
result = mergeDeep(result, await loadFile(Flag.OPENCODE_CONFIG))
log.debug("loaded custom config", { path: Flag.OPENCODE_CONFIG })
}
@@ -36,7 +38,7 @@ export namespace Config {
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 loadRaw(JSON.stringify(wellknown.config ?? {}), process.cwd()))
result = mergeDeep(result, await load(JSON.stringify(wellknown.config ?? {}), process.cwd()))
}
}
@@ -65,6 +67,40 @@ 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 })
}
result.plugin = result.plugin || []
result.plugin.push(
...[
...(await Filesystem.globUp("plugin/*.ts", Global.Path.config, Global.Path.config)),
...(await Filesystem.globUp(".opencode/plugin/*.ts", app.path.cwd, app.path.root)),
].map((x) => "file://" + x),
)
// Handle migration from autoshare to share field
if (result.autoshare === true && !result.share) {
result.share = "auto"
@@ -117,6 +153,7 @@ export namespace Config {
.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(),
@@ -187,11 +224,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()
@@ -248,8 +289,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
@@ -285,10 +360,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"), {
@@ -309,54 +385,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 loadRaw(text, configPath)
return load(text, filepath)
}
async function loadRaw(text: string, configPath: string) {
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]
try {
data.plugin[i] = import.meta.resolve(plugin, filepath)
} catch (err) {}
}
}
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(),
}),
)
+49 -5
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
},
}
+25 -3
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)
+3 -1
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()
+1
View File
@@ -5,6 +5,7 @@ export namespace Identifier {
const prefixes = {
session: "ses",
message: "msg",
permission: "per",
user: "usr",
part: "prt",
} as const
+1
View File
@@ -83,6 +83,7 @@ const cli = yargs(hideBin(process.argv))
if (msg.startsWith("Unknown argument") || msg.startsWith("Not enough non-option arguments")) {
cli.showHelp("log")
}
process.exit(1)
})
.strict()
@@ -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")
+30 -1
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,35 @@ 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,
root: existing?.root ?? (async (_file, app) => app.path.root),
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 +105,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
+2 -1
View File
@@ -115,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
+56 -31
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`)
}
+69
View File
@@ -0,0 +1,69 @@
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 { BunProc } from "../bun"
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 (let plugin of config.plugin ?? []) {
log.info("loading plugin", { path: plugin })
if (!plugin.startsWith("file://")) {
const [pkg, version] = plugin.split("@")
plugin = await BunProc.install(pkg, version ?? "latest")
}
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,
}
})
export async function trigger<
Name extends keyof Required<Hooks>,
Input = Parameters<Required<Hooks>[Name]>[0],
Output = Parameters<Required<Hooks>[Name]>[1],
>(name: Name, input: Input, output: Output): Promise<Output> {
if (!name) return output
for (const hook of await state().then((x) => x.hooks)) {
const fn = hook[name]
if (!fn) continue
// @ts-expect-error if you feel adventurous, please fix the typing, make sure to bump the try-counter if you
// give up.
// try-counter: 2
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,
})
}
})
}
}
+19 -9
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 * 60).unref()
@@ -126,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 }
@@ -194,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 () => {
@@ -366,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()
+67 -36
View File
@@ -2,47 +2,73 @@ 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
})
}
const 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
}
}
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,
}
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
}
@@ -50,4 +76,9 @@ export namespace ProviderTransform {
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
}
}
+209 -2
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: {
@@ -47,7 +49,7 @@ export namespace Server {
Connected: Bus.event("server.connected", z.object({})),
}
function app() {
export const app = lazy(() => {
const app = new Hono()
const result = app
@@ -94,6 +96,7 @@ export namespace Server {
"/event",
describeRoute({
description: "Get events",
operationId: "event.subscribe",
responses: {
200: {
description: "Event stream",
@@ -137,6 +140,7 @@ export namespace Server {
"/app",
describeRoute({
description: "Get app info",
operationId: "app.get",
responses: {
200: {
description: "200",
@@ -156,6 +160,7 @@ export namespace Server {
"/app/init",
describeRoute({
description: "Initialize the app",
operationId: "app.init",
responses: {
200: {
description: "Initialize the app",
@@ -176,6 +181,7 @@ export namespace Server {
"/config",
describeRoute({
description: "Get config info",
operationId: "config.get",
responses: {
200: {
description: "Get config info",
@@ -195,6 +201,7 @@ export namespace Server {
"/session",
describeRoute({
description: "List all sessions",
operationId: "session.list",
responses: {
200: {
description: "List of sessions",
@@ -216,6 +223,7 @@ export namespace Server {
"/session",
describeRoute({
description: "Create a new session",
operationId: "session.create",
responses: {
...ERRORS,
200: {
@@ -237,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",
@@ -263,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",
@@ -299,6 +309,7 @@ export namespace Server {
"/session/:id/abort",
describeRoute({
description: "Abort a session",
operationId: "session.abort",
responses: {
200: {
description: "Aborted session",
@@ -324,6 +335,7 @@ export namespace Server {
"/session/:id/share",
describeRoute({
description: "Share a session",
operationId: "session.share",
responses: {
200: {
description: "Successfully shared session",
@@ -352,6 +364,7 @@ export namespace Server {
"/session/:id/share",
describeRoute({
description: "Unshare the session",
operationId: "session.unshare",
responses: {
200: {
description: "Successfully unshared session",
@@ -380,6 +393,7 @@ export namespace Server {
"/session/:id/summarize",
describeRoute({
description: "Summarize the session",
operationId: "session.summarize",
responses: {
200: {
description: "Summarized session",
@@ -415,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",
@@ -444,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",
@@ -477,6 +527,7 @@ export namespace Server {
"/session/:id/revert",
describeRoute({
description: "Revert a message",
operationId: "session.revert",
responses: {
200: {
description: "Updated session",
@@ -506,6 +557,7 @@ export namespace Server {
"/session/:id/unrevert",
describeRoute({
description: "Restore all reverted messages",
operationId: "session.unrevert",
responses: {
200: {
description: "Updated session",
@@ -529,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",
@@ -561,6 +645,7 @@ export namespace Server {
"/find",
describeRoute({
description: "Find text in files",
operationId: "find.text",
responses: {
200: {
description: "Matches",
@@ -593,6 +678,7 @@ export namespace Server {
"/find/file",
describeRoute({
description: "Find files",
operationId: "find.files",
responses: {
200: {
description: "File paths",
@@ -625,6 +711,7 @@ export namespace Server {
"/find/symbol",
describeRoute({
description: "Find workspace symbols",
operationId: "find.symbols",
responses: {
200: {
description: "Symbols",
@@ -652,6 +739,7 @@ export namespace Server {
"/file",
describeRoute({
description: "Read a file",
operationId: "file.read",
responses: {
200: {
description: "File content",
@@ -688,6 +776,7 @@ export namespace Server {
"/file/status",
describeRoute({
description: "Get file status",
operationId: "file.status",
responses: {
200: {
description: "File status",
@@ -708,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",
@@ -757,6 +847,7 @@ export namespace Server {
"/mode",
describeRoute({
description: "List all modes",
operationId: "app.modes",
responses: {
200: {
description: "List of modes",
@@ -777,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",
@@ -800,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",
@@ -813,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()
+155 -89
View File
@@ -41,6 +41,7 @@ import { LSP } from "../lsp"
import { ReadTool } from "../tool/read"
import { mergeDeep, pipe, splitWhen } from "remeda"
import { ToolRegistry } from "../tool/registry"
import { Plugin } from "../plugin"
export namespace Session {
const log = Log.create({ service: "session" })
@@ -120,6 +121,7 @@ export namespace Session {
const sessions = new Map<string, Info>()
const messages = new Map<string, MessageV2.Info[]>()
const pending = new Map<string, AbortController>()
const autoCompacting = new Map<string, boolean>()
const queued = new Map<
string,
{
@@ -135,6 +137,7 @@ export namespace Session {
sessions,
messages,
pending,
autoCompacting,
queued,
}
},
@@ -256,7 +259,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) {
@@ -290,6 +296,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
@@ -374,6 +383,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",
@@ -535,12 +574,23 @@ 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
// used for session list sorting (indicates when session was most recently interacted with)
await update(input.sessionID, (_draft) => {})
if (isLocked(input.sessionID)) {
return new Promise((resolve) => {
const queue = state().queued.get(input.sessionID) ?? []
@@ -557,35 +607,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) {
if (msg.info.id === userMsg.id) continue
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 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
@@ -595,6 +616,8 @@ export namespace Session {
const tokens =
previous.tokens.input + previous.tokens.cache.read + previous.tokens.cache.write + previous.tokens.output
if (model.info.limit.context && tokens > Math.max((model.info.limit.context - outputLimit) * 0.9, 0)) {
state().autoCompacting.set(input.sessionID, true)
await summarize({
sessionID: input.sessionID,
providerID: input.providerID,
@@ -603,7 +626,6 @@ export namespace Session {
return chat(input)
}
}
using abort = lock(input.sessionID)
const lastSummary = msgs.findLast((msg) => msg.info.role === "assistant" && msg.info.summary === true)
@@ -705,11 +727,22 @@ export namespace Session {
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,
abort: options.abortSignal!,
messageID: assistantMsg.id,
callID: options.toolCallId,
metadata: async (val) => {
const match = processor.partFromToolCall(options.toolCallId)
if (match && match.state.status === "running") {
@@ -728,6 +761,15 @@ export namespace Session {
}
},
})
await Plugin.trigger(
"tool.execute.after",
{
tool: item.id,
sessionID: input.sessionID,
callID: options.toolCallId,
},
result,
)
return result
},
toModelOutput(result) {
@@ -740,11 +782,10 @@ export namespace Session {
}
for (const [key, item] of Object.entries(await MCP.tools())) {
if (mode.tools[key] === false) continue
if (enabledTools[key] === false) continue
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")
@@ -764,8 +805,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) {
@@ -812,13 +872,26 @@ export namespace Session {
messages,
}
},
maxRetries: 10,
async experimental_repairToolCall(input) {
return {
...input.toolCall,
input: JSON.stringify({
tool: input.toolCall.toolName,
error: input.error.message,
}),
toolName: "invalid",
}
},
maxRetries: 3,
activeTools: Object.keys(tools).filter((x) => x !== "invalid"),
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 => ({
@@ -828,9 +901,6 @@ export namespace Session {
),
...MessageV2.toModelMessage(msgs),
],
temperature: model.info.temperature
? (mode.temperature ?? ProviderTransform.temperature(input.providerID, input.modelID))
: undefined,
tools: model.info.tool_call === false ? undefined : tools,
model: wrapLanguageModel({
model: model.language,
@@ -862,15 +932,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 {
@@ -886,7 +952,7 @@ export namespace Session {
case "tool-input-start":
const part = await updatePart({
id: toolCalls[value.id]?.id ?? Identifier.ascending("part"),
id: toolcalls[value.id]?.id ?? Identifier.ascending("part"),
messageID: assistantMsg.id,
sessionID: assistantMsg.sessionID,
type: "tool",
@@ -896,7 +962,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":
@@ -906,10 +972,11 @@ export namespace Session {
break
case "tool-call": {
const match = toolCalls[value.toolCallId]
const match = toolcalls[value.toolCallId]
if (match) {
const part = await updatePart({
...match,
tool: value.toolName,
state: {
status: "running",
input: value.input,
@@ -918,12 +985,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,
@@ -939,27 +1006,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,
@@ -973,19 +1026,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
}
@@ -1000,6 +1041,7 @@ export namespace Session {
sessionID: assistantMsg.sessionID,
type: "step-start",
})
snapshot = await Snapshot.track()
break
case "finish-step":
@@ -1015,6 +1057,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":
@@ -1030,7 +1086,7 @@ export namespace Session {
}
break
case "text":
case "text-delta":
if (currentText) {
currentText.text += value.text
if (currentText.text) await updatePart(currentText)
@@ -1099,7 +1155,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: {
@@ -1262,9 +1318,19 @@ export namespace Session {
state().pending.set(sessionID, controller)
return {
signal: controller.signal,
[Symbol.dispose]() {
async [Symbol.dispose]() {
log.info("unlocking", { sessionID })
state().pending.delete(sessionID)
const isAutoCompacting = state().autoCompacting.get(sessionID) ?? false
if (isAutoCompacting) {
state().autoCompacting.delete(sessionID)
return
}
const session = await get(sessionID)
if (session.parentID) return
Bus.publish(Event.Idle, {
sessionID,
})
@@ -1287,10 +1353,10 @@ export namespace Session {
}
return {
cost: new Decimal(0)
.add(new Decimal(tokens.input).mul(model.cost.input).div(1_000_000))
.add(new Decimal(tokens.output).mul(model.cost.output).div(1_000_000))
.add(new Decimal(tokens.cache.read).mul(model.cost.cache_read ?? 0).div(1_000_000))
.add(new Decimal(tokens.cache.write).mul(model.cost.cache_write ?? 0).div(1_000_000))
.add(new Decimal(tokens.input).mul(model.cost?.input ?? 0).div(1_000_000))
.add(new Decimal(tokens.output).mul(model.cost?.output ?? 0).div(1_000_000))
.add(new Decimal(tokens.cache.read).mul(model.cost?.cache_read ?? 0).div(1_000_000))
.add(new Decimal(tokens.cache.write).mul(model.cost?.cache_write ?? 0).div(1_000_000))
.toNumber(),
tokens,
}
+3 -1
View File
@@ -8,6 +8,7 @@ export namespace Mode {
.object({
name: z.string(),
temperature: z.number().optional(),
topP: z.number().optional(),
model: z
.object({
modelID: z.string(),
@@ -51,7 +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) item.temperature = value.temperature
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,
@@ -1,5 +1,3 @@
# Beast Mode 3.1
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.
+13 -18
View File
@@ -60,33 +60,28 @@ export namespace SystemPrompt {
export async function custom() {
const { cwd, root } = App.info().path
const config = await Config.get()
const found = []
const paths = new Set<string>()
for (const item of CUSTOM_FILES) {
const matches = await Filesystem.findUp(item, cwd, root)
found.push(...matches.map((x) => Bun.file(x).text()))
matches.forEach((path) => paths.add(path))
}
found.push(
Bun.file(path.join(Global.Path.config, "AGENTS.md"))
.text()
.catch(() => ""),
)
found.push(
Bun.file(path.join(os.homedir(), ".claude", "CLAUDE.md"))
.text()
.catch(() => ""),
)
paths.add(path.join(Global.Path.config, "AGENTS.md"))
paths.add(path.join(os.homedir(), ".claude", "CLAUDE.md"))
if (config.instructions) {
for (const instruction of config.instructions) {
try {
const matches = await Filesystem.globUp(instruction, cwd, root)
found.push(...matches.map((x) => Bun.file(x).text()))
} catch {
continue // Skip invalid glob patterns
}
const matches = await Filesystem.globUp(instruction, cwd, root).catch(() => [])
matches.forEach((path) => paths.add(path))
}
}
const found = Array.from(paths).map((p) =>
Bun.file(p)
.text()
.catch(() => ""),
)
return Promise.all(found).then((result) => result.filter(Boolean))
}
+1 -1
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()
}
+113 -10
View File
@@ -1,12 +1,31 @@
import { z } from "zod"
import { exec } from "child_process"
import { text } from "stream/consumers"
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
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({
@@ -20,19 +39,103 @@ export const BashTool = Tool.define("bash", {
}),
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
})()
const process = Bun.spawn({
cmd: ["bash", "-c", params.command],
cwd: App.info().path.cwd,
maxBuffer: MAX_OUTPUT_LENGTH,
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 = exec(params.command, {
cwd: app.path.cwd,
signal: ctx.abort,
timeout: timeout,
stdout: "pipe",
stderr: "pipe",
maxBuffer: MAX_OUTPUT_LENGTH,
timeout,
})
await process.exited
const stdout = await new Response(process.stdout).text()
const stderr = await new Response(process.stderr).text()
const stdoutPromise = text(process.stdout!)
const stderrPromise = text(process.stderr!)
await new Promise<void>((resolve) => {
process.on("close", () => {
resolve()
})
})
const stdout = await stdoutPromise
const stderr = await stderrPromise
return {
title: params.command,
+51 -26
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,6 +14,8 @@ 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("edit", {
description: DESCRIPTION,
@@ -32,56 +35,78 @@ export const EditTool = Tool.define("edit", {
}
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
}
@@ -96,7 +121,7 @@ export const EditTool = Tool.define("edit", {
diagnostics,
diff,
},
title: `${path.relative(app.path.root, filepath)}`,
title: `${path.relative(app.path.root, filePath)}`,
output,
}
},
+17
View File
@@ -0,0 +1,17 @@
import { z } from "zod"
import { Tool } from "./tool"
export const InvalidTool = Tool.define("invalid", {
description: "Do not use",
parameters: z.object({
tool: z.string(),
error: z.string(),
}),
async execute(params) {
return {
title: "Invalid Tool",
output: `The arguments provided to the tool are invalid: ${params.error}`,
metadata: {},
}
},
})
+30 -12
View File
@@ -6,6 +6,7 @@ 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
@@ -18,15 +19,19 @@ export const ReadTool = Tool.define("read", {
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
@@ -38,16 +43,18 @@ export const ReadTool = Tool.define("read", {
.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
@@ -66,11 +73,11 @@ export const ReadTool = Tool.define("read", {
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,
@@ -99,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
}
+4 -1
View File
@@ -10,9 +10,11 @@ import { TaskTool } from "./task"
import { TodoWriteTool, TodoReadTool } from "./todo"
import { WebFetchTool } from "./webfetch"
import { WriteTool } from "./write"
import { InvalidTool } from "./invalid"
export namespace ToolRegistry {
const ALL = [
InvalidTool,
BashTool,
EditTool,
WebFetchTool,
@@ -74,7 +76,8 @@ export namespace ToolRegistry {
modelID.toLowerCase().includes("qwen") ||
modelID.includes("gpt-") ||
modelID.includes("o1") ||
modelID.includes("o3")
modelID.includes("o3") ||
modelID.includes("codex")
) {
return {
patch: false,
+4 -4
View File
@@ -20,7 +20,7 @@ export const TaskTool = Tool.define("task", 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> = {}
@@ -38,8 +38,8 @@ export const TaskTool = Tool.define("task", async () => {
})
const model = agent.model ?? {
modelID: msg.modelID,
providerID: msg.providerID,
modelID: msg.info.modelID,
providerID: msg.info.providerID,
}
ctx.abort.addEventListener("abort", () => {
@@ -50,7 +50,7 @@ export const TaskTool = Tool.define("task", async () => {
sessionID: session.id,
modelID: model.modelID,
providerID: model.providerID,
mode: msg.mode,
mode: msg.info.mode,
system: agent.prompt,
tools: {
...agent.tools,
+53
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(", ")}]`);
});
+1
View File
@@ -7,6 +7,7 @@ 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
}
+19 -10
View File
@@ -8,6 +8,8 @@ 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("write", {
description: DESCRIPTION,
@@ -18,21 +20,28 @@ export const WriteTool = Tool.define("write", {
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, {
+1 -1
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) {
+2 -4
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)
+14
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)
}
}
+47
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")
})
})
})
+4 -3
View File
@@ -7,6 +7,7 @@ import path from "path"
const ctx = {
sessionID: "test",
messageID: "",
toolCallID: "",
abort: AbortSignal.any([]),
metadata: () => {},
}
@@ -21,8 +22,8 @@ describe("tool.glob", () => {
await App.provide({ cwd: projectRoot }, async () => {
let result = await glob.execute(
{
pattern: "../../node_modules/**/*",
path: undefined,
pattern: "**/*",
path: "../../node_modules",
},
ctx,
)
@@ -40,7 +41,7 @@ describe("tool.glob", () => {
)
expect(result.metadata).toMatchObject({
truncated: false,
count: 3,
count: 2,
})
})
})
+10 -1
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
View File
@@ -0,0 +1 @@
dist
+26
View File
@@ -0,0 +1,26 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "0.3.130",
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit"
},
"exports": {
".": {
"development": "./src/index.ts",
"import": "./dist/index.js"
}
},
"files": [
"dist"
],
"dependencies": {
"@opencode-ai/sdk": "workspace:*"
},
"devDependencies": {
"typescript": "catalog:",
"@hey-api/openapi-ts": "0.80.1",
"@tsconfig/node22": "catalog:"
}
}
+18
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`
}
+10
View File
@@ -0,0 +1,10 @@
import { Plugin } from "./index"
export const ExamplePlugin: Plugin = async ({ app, client, $ }) => {
return {
permission: {},
async "chat.params"(input, output) {
output.topP = 1
},
}
}
+37
View File
@@ -0,0 +1,37 @@
import type { Event, createOpencodeClient, App, Model, Provider, Permission, UserMessage, Part } from "@opencode-ai/sdk"
import type { BunShell } from "./shell"
export type PluginInput = {
client: ReturnType<typeof createOpencodeClient>
app: App
$: BunShell
}
export type Plugin = (input: PluginInput) => Promise<Hooks>
export interface Hooks {
event?: (input: { event: Event }) => Promise<void>
/**
* Called when a new message is received
*/
"chat.message"?: (input: {}, output: { message: UserMessage; parts: Part[] }) => Promise<void>
/**
* Modify parameters sent to LLM
*/
"chat.params"?: (
input: { model: Model; provider: Provider; message: UserMessage },
output: { temperature: number; topP: number },
) => Promise<void>
"permission.ask"?: (input: Permission, output: { status: "ask" | "deny" | "allow" }) => Promise<void>
"tool.execute.before"?: (
input: { tool: string; sessionID: string; callID: string },
output: { args: any },
) => Promise<void>
"tool.execute.after"?: (
input: { tool: string; sessionID: string; callID: string },
output: {
title: string
output: string
metadata: any
},
) => Promise<void>
}
+136
View File
@@ -0,0 +1,136 @@
export type ShellFunction = (input: Uint8Array) => Uint8Array
export type ShellExpression =
| { toString(): string }
| Array<ShellExpression>
| string
| { raw: string }
| ReadableStream
export interface BunShell {
(strings: TemplateStringsArray, ...expressions: ShellExpression[]): BunShellPromise
/**
* Perform bash-like brace expansion on the given pattern.
* @param pattern - Brace pattern to expand
*/
braces(pattern: string): string[]
/**
* Escape strings for input into shell commands.
*/
escape(input: string): string
/**
* Change the default environment variables for shells created by this instance.
*/
env(newEnv?: Record<string, string | undefined>): BunShell
/**
* Default working directory to use for shells created by this instance.
*/
cwd(newCwd?: string): BunShell
/**
* Configure the shell to not throw an exception on non-zero exit codes.
*/
nothrow(): BunShell
/**
* Configure whether or not the shell should throw an exception on non-zero exit codes.
*/
throws(shouldThrow: boolean): BunShell
}
export interface BunShellPromise extends Promise<BunShellOutput> {
readonly stdin: WritableStream
/**
* Change the current working directory of the shell.
*/
cwd(newCwd: string): this
/**
* Set environment variables for the shell.
*/
env(newEnv: Record<string, string> | undefined): this
/**
* By default, the shell will write to the current process's stdout and stderr, as well as buffering that output.
* This configures the shell to only buffer the output.
*/
quiet(): this
/**
* Read from stdout as a string, line by line
* Automatically calls quiet() to disable echoing to stdout.
*/
lines(): AsyncIterable<string>
/**
* Read from stdout as a string.
* Automatically calls quiet() to disable echoing to stdout.
*/
text(encoding?: BufferEncoding): Promise<string>
/**
* Read from stdout as a JSON object
* Automatically calls quiet()
*/
json(): Promise<any>
/**
* Read from stdout as an ArrayBuffer
* Automatically calls quiet()
*/
arrayBuffer(): Promise<ArrayBuffer>
/**
* Read from stdout as a Blob
* Automatically calls quiet()
*/
blob(): Promise<Blob>
/**
* Configure the shell to not throw an exception on non-zero exit codes.
*/
nothrow(): this
/**
* Configure whether or not the shell should throw an exception on non-zero exit codes.
*/
throws(shouldThrow: boolean): this
}
export interface BunShellOutput {
readonly stdout: Buffer
readonly stderr: Buffer
readonly exitCode: number
/**
* Read from stdout as a string
*/
text(encoding?: BufferEncoding): string
/**
* Read from stdout as a JSON object
*/
json(): any
/**
* Read from stdout as an ArrayBuffer
*/
arrayBuffer(): ArrayBuffer
/**
* Read from stdout as an Uint8Array
*/
bytes(): Uint8Array
/**
* Read from stdout as a Blob
*/
blob(): Blob
}
export type BunShellError = Error & BunShellOutput
+16
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"
]
}
@@ -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"]
}
}
}
-88
View File
@@ -1,88 +0,0 @@
name: CI
on:
push:
branches-ignore:
- 'generated'
- 'codegen/**'
- 'integrated/**'
- 'stl-preview-head/**'
- 'stl-preview-base/**'
pull_request:
branches-ignore:
- 'stl-preview-head/**'
- 'stl-preview-base/**'
jobs:
lint:
timeout-minutes: 10
name: lint
runs-on: ${{ github.repository == 'stainless-sdks/opencode-typescript' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Bootstrap
run: ./scripts/bootstrap
- name: Check types
run: ./scripts/lint
build:
timeout-minutes: 5
name: build
runs-on: ${{ github.repository == 'stainless-sdks/opencode-typescript' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Bootstrap
run: ./scripts/bootstrap
- name: Check build
run: ./scripts/build
- name: Get GitHub OIDC Token
if: github.repository == 'stainless-sdks/opencode-typescript'
id: github-oidc
uses: actions/github-script@v6
with:
script: core.setOutput('github_token', await core.getIDToken());
- name: Upload tarball
if: github.repository == 'stainless-sdks/opencode-typescript'
env:
URL: https://pkg.stainless.com/s
AUTH: ${{ steps.github-oidc.outputs.github_token }}
SHA: ${{ github.sha }}
run: ./scripts/utils/upload-artifact.sh
test:
timeout-minutes: 10
name: test
runs-on: ${{ github.repository == 'stainless-sdks/opencode-typescript' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Bootstrap
run: ./scripts/bootstrap
- name: Run tests
run: ./scripts/test
-32
View File
@@ -1,32 +0,0 @@
# This workflow is triggered when a GitHub release is created.
# It can also be run manually to re-publish to NPM in case it failed for some reason.
# You can run this workflow by navigating to https://www.github.com/sst/opencode-sdk-js/actions/workflows/publish-npm.yml
name: Publish NPM
on:
workflow_dispatch:
release:
types: [published]
jobs:
publish:
name: publish
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v3
with:
node-version: '20'
- name: Install dependencies
run: |
yarn install
- name: Publish to NPM
run: |
bash ./bin/publish-npm
env:
NPM_TOKEN: ${{ secrets.OPENCODE_NPM_TOKEN || secrets.NPM_TOKEN }}
-21
View File
@@ -1,21 +0,0 @@
name: Release Doctor
on:
pull_request:
branches:
- main
workflow_dispatch:
jobs:
release_doctor:
name: release doctor
runs-on: ubuntu-latest
if: github.repository == 'sst/opencode-sdk-js' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next')
steps:
- uses: actions/checkout@v4
- name: Check release environment
run: |
bash ./bin/check-release-environment
env:
NPM_TOKEN: ${{ secrets.OPENCODE_NPM_TOKEN || secrets.NPM_TOKEN }}
-7
View File
@@ -1,7 +0,0 @@
CHANGELOG.md
/ecosystem-tests/*/**
/node_modules
/deno
# don't format tsc output, will break source maps
/dist
-7
View File
@@ -1,7 +0,0 @@
{
"arrowParens": "always",
"experimentalTernaries": true,
"printWidth": 110,
"singleQuote": true,
"trailingComma": "all"
}
@@ -1,3 +0,0 @@
{
".": "0.1.0-alpha.20"
}
-4
View File
@@ -1,4 +0,0 @@
configured_endpoints: 26
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-62d8fccba4eb8dc3a80434e0849eab3352e49fb96a718bb7b6d17ed8e582b716.yml
openapi_spec_hash: 4ff9376cf9634e91731e63fe482ea532
config_hash: 1ae82c93499b9f0b9ba828b8919f9cb3
-1
View File
@@ -1 +0,0 @@
brew "node"
-196
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))
-107
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.
-370
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).
-139
View File
@@ -1,139 +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">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>
@@ -1,22 +0,0 @@
#!/usr/bin/env bash
errors=()
if [ -z "${NPM_TOKEN}" ]; then
errors+=("The NPM_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets")
fi
lenErrors=${#errors[@]}
if [[ lenErrors -gt 0 ]]; then
echo -e "Found the following errors in the release environment:\n"
for error in "${errors[@]}"; do
echo -e "- $error\n"
done
exit 1
fi
echo "The environment is ready to push releases!"
-61
View File
@@ -1,61 +0,0 @@
#!/usr/bin/env bash
set -eux
npm config set '//registry.npmjs.org/:_authToken' "$NPM_TOKEN"
yarn build
cd dist
# Get package name and version from package.json
PACKAGE_NAME="$(jq -r -e '.name' ./package.json)"
VERSION="$(jq -r -e '.version' ./package.json)"
# Get latest version from npm
#
# If the package doesn't exist, npm will return:
# {
# "error": {
# "code": "E404",
# "summary": "Unpublished on 2025-06-05T09:54:53.528Z",
# "detail": "'the_package' is not in this registry..."
# }
# }
NPM_INFO="$(npm view "$PACKAGE_NAME" version --json 2>/dev/null || true)"
# Check if we got an E404 error
if echo "$NPM_INFO" | jq -e '.error.code == "E404"' > /dev/null 2>&1; then
# Package doesn't exist yet, no last version
LAST_VERSION=""
elif echo "$NPM_INFO" | jq -e '.error' > /dev/null 2>&1; then
# Report other errors
echo "ERROR: npm returned unexpected data:"
echo "$NPM_INFO"
exit 1
else
# Success - get the version
LAST_VERSION=$(echo "$NPM_INFO" | jq -r '.') # strip quotes
fi
# Check if current version is pre-release (e.g. alpha / beta / rc)
CURRENT_IS_PRERELEASE=false
if [[ "$VERSION" =~ -([a-zA-Z]+) ]]; then
CURRENT_IS_PRERELEASE=true
CURRENT_TAG="${BASH_REMATCH[1]}"
fi
# Check if last version is a stable release
LAST_IS_STABLE_RELEASE=true
if [[ -z "$LAST_VERSION" || "$LAST_VERSION" =~ -([a-zA-Z]+) ]]; then
LAST_IS_STABLE_RELEASE=false
fi
# Use a corresponding alpha/beta tag if there already is a stable release and we're publishing a prerelease.
if $CURRENT_IS_PRERELEASE && $LAST_IS_STABLE_RELEASE; then
TAG="$CURRENT_TAG"
else
TAG="latest"
fi
# Publish with the appropriate tag
yarn publish --access public --tag "$TAG"
-42
View File
@@ -1,42 +0,0 @@
// @ts-check
import tseslint from 'typescript-eslint';
import unusedImports from 'eslint-plugin-unused-imports';
import prettier from 'eslint-plugin-prettier';
export default tseslint.config(
{
languageOptions: {
parser: tseslint.parser,
parserOptions: { sourceType: 'module' },
},
files: ['**/*.ts', '**/*.mts', '**/*.cts', '**/*.js', '**/*.mjs', '**/*.cjs'],
ignores: ['dist/'],
plugins: {
'@typescript-eslint': tseslint.plugin,
'unused-imports': unusedImports,
prettier,
},
rules: {
'no-unused-vars': 'off',
'prettier/prettier': 'error',
'unused-imports/no-unused-imports': 'error',
'no-restricted-imports': [
'error',
{
patterns: [
{
regex: '^@opencode-ai/sdk(/.*)?',
message: 'Use a relative import, not a package import.',
},
],
},
],
},
},
{
files: ['tests/**', 'examples/**'],
rules: {
'no-restricted-imports': 'off',
},
},
);
-4
View File
@@ -1,4 +0,0 @@
File generated from our OpenAPI spec by Stainless.
This directory can be used to store example files demonstrating usage of this SDK.
It is ignored by Stainless code generation and its content (other than this keep file) won't be touched.
+4
View File
@@ -0,0 +1,4 @@
configured_endpoints: 34
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-52fd0b61e84fdc1cdd31ec12e1600510e9dd2f9d4fb20c2315b4975cb763ee98.yml
openapi_spec_hash: e851b8d5a2412f5fc9be82ab88ebdfde
config_hash: 11a6f0803eb407367c3f677d3e524c37

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