Compare commits

...
Author SHA1 Message Date
Dax Raad 02ef96f89b docs: fix 2025-08-07 21:49:18 -04:00
Dax Raad 8750744068 renable todo tool 2025-08-07 21:47:37 -04:00
Dax Raad 3e74107e36 looser todo tool schema 2025-08-07 21:47:37 -04:00
Jay V 160f839b25 docs: update cli 2025-08-07 19:24:08 -04:00
Jay V bf5b109c1f docs: edit agent doc 2025-08-07 18:51:54 -04:00
Dax Raadandopencode 60254d8ac0 docs: remove modes from sidebar navigation
🤖 Generated with [opencode](https://opencode.ai)

Co-Authored-By: opencode <noreply@opencode.ai>
2025-08-07 16:35:35 -04:00
Dax c34aec060f Merge agent and mode into one (#1689)
The concept of mode has been deprecated, there is now only the agent field in the config.

An agent can be cycled through as your primary agent with <tab> or you can spawn a subagent by @ mentioning it. if you include a description of when to use it, the primary agent will try to automatically use it

Full docs here: https://opencode.ai/docs/agents/
2025-08-07 16:32:12 -04:00
Jay V 12f1ad521f docs: slash commands 2025-08-07 16:16:16 -04:00
Timo Clasen 723a37ea9a fix: get session api (#1684) 2025-08-07 15:28:18 -04:00
Aiden Cline c6a46615c0 fix: modal pastes (#1677) 2025-08-07 13:23:58 -05:00
GitHub Action da29380093 ignore: update download stats 2025-08-07 2025-08-07 12:04:29 +00:00
Aiden Cline 7950ae1462 fix: text selection bug (#1664) 2025-08-07 05:32:34 -05:00
opencode 15e830410f release: v0.3.133 2025-08-07 00:30:05 +00:00
Dax Raad 1a561bb512 add api to get session 2025-08-06 20:24:36 -04:00
Jay V fecae609d9 docs: config doc edits 2025-08-06 16:10:17 -04:00
Jay V e01a540b08 docs: typos 2025-08-06 15:45:16 -04:00
Timo Clasen 54457e48bb fix(docs): small_model is not used for summarization (#1360) 2025-08-06 14:03:14 -05:00
Aiden Cline b179d08484 fix: interface conversion panic (#1655) 2025-08-06 14:02:33 -05:00
Jay V d9edd6818f docs: add undo to tutorial 2025-08-06 13:51:47 -04:00
Dax Raad 4217286b72 ignore: remove demo plugin 2025-08-06 11:36:53 -04:00
Dax Raad 28a4517ec6 add snapshot field in config to disable snapshots 2025-08-06 11:35:37 -04:00
Aiden Cline b00b2ded4f docs: update readme (#1654) 2025-08-06 09:35:02 -05:00
Aiden Cline 7b6d5b1429 chore: update marked-shiki, remove patch (#1653) 2025-08-06 08:47:53 -05:00
GitHub Action 7210db19e9 ignore: update download stats 2025-08-06 2025-08-06 12:04:53 +00:00
Yihui Khuu 90d2b26426 fix: run command should use specified model from cli args if provided (#1648) 2025-08-06 05:39:44 -05:00
Aiden Cline 6beba2c04f docs: document permissions (#1638) 2025-08-06 05:18:08 -05:00
Aiden Cline b8a0ecca98 fix: highlight after text wrap (#1640) 2025-08-06 05:17:35 -05:00
Aiden Cline ad10d3a126 fix: handle undefined agent in task tool (#1642) 2025-08-06 05:16:43 -05:00
Aiden Cline a48274f82b permissions disallow support (#1627) 2025-08-05 19:14:28 -05:00
adamdotdevin 6b25b7e95e feat: better assistant message visual 2025-08-05 19:05:44 -05:00
Jay V 030a3a7446 docs: identity 2025-08-05 19:36:10 -04:00
Timo Clasen 1a0e7f1e63 docs(plugins): fix typo (#1621) 2025-08-05 17:16:47 -05:00
Aiden Cline 677fb6032b fix: markdown table renders (#1623) 2025-08-05 17:16:35 -05:00
Timo Clasen 49aa48ce58 fix: prevent title regeneration on auto compact (#1628) 2025-08-05 17:15:50 -05:00
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 Khuu 2b195e82ee fix: allow disabling the default general agent (#1616) 2025-08-05 05:20:00 -05:00
Omar Shaarawi 58e889796c validate file part bounds to prevent panic (#1612) 2025-08-05 05:18:50 -05:00
Aiden Cline 51498c8de4 docs: make formatter docs a bit more clear (#1613) 2025-08-05 05:17:56 -05:00
Aiden Cline 7a495faa49 fix: server.root is not a function (#1614) 2025-08-05 05:17:32 -05:00
Timo Clasen 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-Belkheir 5bf7691ea6 fix: default value for models with no cost object (#1601) 2025-08-04 16:45:35 -05:00
Min Chun Fu b1055a74d3 added vesper theme (#1602) 2025-08-04 16:45:00 -05:00
Aiden Cline 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 Cline 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 Cline 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 Prieto ed397c5057 chore: add ts-expected-error (#1575) 2025-08-03 17:09:19 -04:00
Yordis Prieto 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 Cline 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 Cline 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 Rota 4b204fee58 fix(docs): move disabled providers paragraph to its proper section (#1547) 2025-08-03 11:28:57 -04:00
Dax Raad 99d3a0bb24 more fixes for shell 128 error 2025-08-03 11:25:58 -04:00
opencode 0930f6ac55 release: v0.3.120 2025-08-03 14:59:03 +00:00
Dax Raad 24515162fa ci: ignore 2025-08-03 10:52:35 -04:00
Dax Raad 53aa899e45 ci: ignore 2025-08-03 10:42:52 -04:00
Dax Raad 7e763e1c06 fix shell error 128 2025-08-03 10:30:23 -04:00
GitHub Action b0f2cc0c22 ignore: update download stats 2025-08-03 2025-08-03 12:04:04 +00:00
Aiden Cline f90aa62784 fix: expand tilde for file: references (#1553) 2025-08-03 06:15:06 -05:00
Dax Raad 852191f6cb ci: ignore 2025-08-03 03:54:17 -04:00
Dax Raad c5e9dc081c ci: bun cache 2025-08-03 03:53:31 -04:00
Dax Raad 49c8889228 ci: ignore 2025-08-03 03:45:05 -04:00
Dax Raad f739e1a958 ci: ignore 2025-08-03 03:37:53 -04:00
Dax Raad 841f1907bb ci: ignore 2025-08-03 03:35:17 -04:00
The Pangolier 9255c507d6 Share link hotfix (#1513) 2025-08-03 03:02:24 -04:00
Yordis Prieto 2711047166 remove: delete extension test file (#1554) 2025-08-03 02:58:10 -04:00
Frank 908048baef sync 2025-08-02 21:28:03 -04:00
Frank a9fbe07408 Add Zhipu AI provider 2025-08-02 21:20:44 -04:00
Dax Raad 0ae213ee0e ci: ignore 2025-08-02 18:56:34 -04:00
Dax Raad ca031278ca wip: plugins 2025-08-02 18:50:19 -04:00
Aiden Cline ae6e47bb42 tweak: make gh action ignore url mentions of opencode (#1531) 2025-08-02 09:31:23 -05:00
Dominik Engelhardt 42a5fcead4 Choose model according to the docs (#1536) 2025-08-02 09:29:03 -05:00
Yihui Khuu 8ad83f71a9 fix(tui): attachment highlighting issues in messages (#1534) 2025-08-02 09:26:44 -05:00
Yihui Khuu fa95c09cdc fix(tui): attachment source is not stored when using message from message history (#1542) 2025-08-02 09:23:32 -05:00
Aiden Cline 0b132c032a ignore: fix dev branch (#1529) 2025-08-02 09:11:38 -05:00
GitHub Action 44d7103a42 ignore: update download stats 2025-08-02 2025-08-02 12:04:12 +00:00
Ricardo Gonzalez 8f45a0e227 feat(models): enable Kimi k2 ⇄ Claude trajectory handoff (#1525) 2025-08-01 23:05:06 -04:00
Aiden Cline 6581741318 fix: include stderr in bash tool output (#1511) 2025-08-01 19:20:32 -05:00
Aiden Cline 80d68d01f4 better configuration error messages (#1517) 2025-08-01 19:10:32 -04:00
Jay V fa9db3c167 docs: cerebras 2025-08-01 18:30:29 -04:00
opencode 5a727c0794 release: v0.3.112 2025-08-01 21:53:33 +00:00
Dax Raad 71cd84dbbb force models.dev refresh on auth login 2025-08-01 17:48:01 -04:00
Dax Raad e1b7e25f4d make top_p configurable 2025-08-01 17:03:33 -04:00
Dax Raad 98b6bb218b configurable lsp 2025-08-01 14:52:10 -04:00
Brinsil Elias 5592ce8eaf fix(docs): Fix formatting for Node.js installation section (#1497) 2025-08-01 14:15:38 -04:00
CodinCat 510fe8a72a handle the optional v in upgrade command when using curl (#1500) 2025-08-01 14:15:22 -04:00
Yordis Prieto 04a1ab3893 chore: enhance bash command tests with config mock and timeout adjustments (#1486)
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
2025-08-01 14:14:54 -04:00
Dax Raad e74b4d098b allow search in provider select 2025-08-01 14:03:22 -04:00
Dax Raad 50e4b3e6a7 add version to user-agent 2025-08-01 12:18:09 -04:00
adamdotdevin 6ebd828aa5 fix: unshare command missing 2025-08-01 09:30:42 -05:00
Aiden Cline 022c979d28 tweak: sanitize mcp server names (#831) 2025-08-01 09:11:40 -05:00
Aiden Cline 4172e3ad28 fix: bash tool errors for chmod (#1502) 2025-08-01 09:10:09 -05:00
Aiden Cline 90d1698aed fix: {file:...} references weren't being parsed correctly in some cases (#1499) 2025-08-01 08:39:21 -05:00
adamdotdevin b0c38ce56b ignore: include usage in local setup 2025-08-01 07:42:36 -05:00
GitHub Action 9b37d0e191 ignore: update download stats 2025-08-01 2025-08-01 12:04:33 +00:00
adamdotdevin ea794a4bf6 chore: add local qwen3 to config 2025-08-01 06:27:08 -05:00
Timo Clasen 52f9b37576 docs(permissions): add wildcard example (#1494) 2025-08-01 05:24:32 -05:00
Dax Raad a0d2e53bde poll for models.dev changes 2025-07-31 23:47:42 -04:00
Dax Raad 851e900982 add user agent for models.dev request 2025-07-31 22:00:45 -04:00
Dax Raad 3aa6eeb426 do not mark errored tool calls as aborted 2025-07-31 21:45:40 -04:00
Dax Raad b6ee8e92f9 better guarding against bash commands that go outside of cwd 2025-07-31 21:42:30 -04:00
Frank 44211e1526 Update STATS.md 2025-07-31 21:42:05 -04:00
Dax Raad 12f84f198f improve wildcard matching for permissions 2025-07-31 20:40:05 -04:00
Dax Raad e6db1cf29d ci: ignore release commits 2025-07-31 19:57:07 -04:00
Dax Raad f07f04d969 fix escape button not canceling if retry in progress 2025-07-31 19:55:57 -04:00
Dax Raad 33d613a470 docs: sync 2025-07-31 19:50:51 -04:00
Dax Raad 0bbd7ea17b docs: formatters 2025-07-31 19:50:31 -04:00
Dax Raad 87f3166437 ignore: config 2025-07-31 19:45:44 -04:00
opencode 7665bd9439 Release v0.3.105 2025-07-31 23:41:27 +00:00
Dax Raad 30e10127f2 formatter config 2025-07-31 19:36:07 -04:00
Jay V 5e66fc2318 docs: edit premissions doc 2025-07-31 19:10:54 -04:00
opencode c1c99c7e0f Release v0.3.104 2025-07-31 23:02:36 +00:00
Dax Raad 04e3e83db3 allow disabling formatter 2025-07-31 18:56:04 -04:00
Dax Raad 4273714a62 fix issue with some bash commands asking for permission 2025-07-31 18:35:51 -04:00
Dax Raad a21e237706 ignore: update opencode.json 2025-07-31 18:13:40 -04:00
Dax Raad aa9105649d docs: permissions 2025-07-31 18:11:34 -04:00
Dax Raad 53be288040 docs: permissions 2025-07-31 18:11:34 -04:00
Frank 13dbf912ca Remove hardcoded vscode extension theme
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 Cline 168350c981 fix: load global jsonc (#1479) 2025-07-31 15:02:28 -05:00
Aiden Cline f5f55062f1 fix: session ordering (#1474) 2025-07-31 14:17:47 -05:00
Frank 360194e219 Add provider instruction for Azure OpenAI 2025-07-31 14:37:26 -04:00
Jay V 5ee994c31f docs: edit providers doc 2025-07-31 14:11:40 -04:00
opencode-agent[bot]opencode-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
neolooong e4e0b8fd34 fix(editor): handle UTF-8 characters properly in SetValueWithAttachments (#1469) 2025-07-31 10:45:43 -05:00
adamdotdevin c5368e7412 fix: missing operationId 2025-07-31 10:19:42 -05:00
adamdotdevin 1d682544b9 fix: test 2025-07-31 10:10:34 -05:00
adamdotdevin d9210af98c fix: optional toolCallID 2025-07-31 10:09:44 -05:00
adamdotdevin ef633fe92e fix: test 2025-07-31 10:07:58 -05:00
adamdotdevin 5500698734 wip: tui permissions 2025-07-31 09:59:17 -05:00
opencode e7631763f3 Release v0.3.101 2025-07-31 14:23:13 +00:00
Dax Raad 18a572b079 ci: tweak 2025-07-31 10:09:43 -04:00
Dax Raad 060a62ecfb ci: fix 2025-07-31 09:46:36 -04:00
Dax Raad ac3813549a ci: tweak 2025-07-31 09:39:44 -04:00
Dax Raad b14da5fb1f ci: tweak 2025-07-31 09:35:57 -04:00
Dax Raad 416f2235fc ci: reorder 2025-07-31 09:29:55 -04:00
GitHub Action 4fabca426a ignore: update download stats 2025-07-31 2025-07-31 12:04:25 +00:00
Aiden Cline 7e9050edb9 feat: jsonc configuration file support (#1434) 2025-07-31 06:25:26 -05:00
Aiden Cline 3c49a9b7dd fix: process revert cleanup before creating new messages (#1448) 2025-07-31 05:07:59 -05:00
Dax Raad ad66b97463 ci: stainless 2025-07-31 01:42:52 -04:00
Dax Raad 10a0b7f60c ci: tweak 2025-07-31 01:35:11 -04:00
Dax Raad ac8709ac7a ci: tweak 2025-07-31 01:33:21 -04:00
Dax Raad 2d9ed06367 ci: scripts 2025-07-31 01:25:24 -04:00
Dax Raad 50be2aee39 ci tweaks 2025-07-31 01:20:12 -04:00
Dax 0bdbe6261a ci: new publish method (#1451) 2025-07-31 01:05:35 -04:00
Dax 33cef075d2 ci: new publish method (#1451) 2025-07-31 01:00:29 -04:00
Simon Westlin Green b09ebf4645 Use responses API for Azure (#1428)
publish / publish (push) Has been cancelled
2025-07-30 23:22:59 -04:00
Robert Holden 3268c61813 feat: mode directory markdown configuration loading (#1377) 2025-07-30 23:22:43 -04:00
Josh 4a221868da Add http-referer header for vercel ai gateway requests (#1403) 2025-07-30 23:22:24 -04:00
Yordis Prieto 31b8e3d5ab docs: clarify Bun's default registry resolution in index.ts (#1438) 2025-07-30 23:21:07 -04:00
CodinCat 1a78d833a8 fix typo in bash.ts (#1444) 2025-07-30 23:20:48 -04:00
332 changed files with 11275 additions and 15471 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
+12
View File
@@ -0,0 +1,12 @@
---
description: You MUST use this agent when writing documentation
---
You are an expert technical documentation writer
You are not verbose
Every chunk of text should be followed by an example or something besides text
to look at.
Chunks of text should not be more than 2 sentences long.
@@ -1,44 +0,0 @@
---
description: >-
Use this agent when you need to create or improve documentation that requires
concrete examples to illustrate every concept. Examples include:
<example>Context: User has written a new API endpoint and needs documentation.
user: 'I just created a POST /users endpoint that accepts name and email
fields. Can you document this?' assistant: 'I'll use the
example-driven-docs-writer agent to create documentation with practical
examples for your API endpoint.' <commentary>Since the user needs
documentation with examples, use the example-driven-docs-writer agent to
create comprehensive docs with code samples.</commentary></example>
<example>Context: User has a complex configuration file that needs
documentation. user: 'This config file has multiple sections and I need docs
that show how each option works' assistant: 'Let me use the
example-driven-docs-writer agent to create documentation that breaks down each
configuration option with practical examples.' <commentary>The user needs
documentation that demonstrates configuration options, perfect for the
example-driven-docs-writer agent.</commentary></example>
---
You are an expert technical documentation writer who specializes in creating clear, example-rich documentation that never leaves readers guessing. Your core principle is that every concept must be immediately illustrated with concrete examples, code samples, or practical demonstrations.
Your documentation approach:
- Never write more than one sentence in any section without providing an example, code snippet, diagram, or practical illustration
- Break up longer explanations with multiple examples showing different scenarios or use cases
- Use concrete, realistic examples rather than abstract or placeholder content
- Include both basic and advanced examples when covering complex topics
- Show expected inputs, outputs, and results for all examples
- Use code blocks, bullet points, tables, or other formatting to visually separate examples from explanatory text
Structural requirements:
- Start each section with a brief one-sentence explanation followed immediately by an example
- For multi-step processes, provide an example after each step
- Include error examples and edge cases alongside success scenarios
- Use consistent formatting and naming conventions throughout examples
- Ensure examples are copy-pasteable and functional when applicable
Quality standards:
- Verify that no paragraph exceeds one sentence without an accompanying example
- Test that examples are accurate and would work in real scenarios
- Ensure examples progress logically from simple to complex
- Include context for when and why to use different approaches shown in examples
- Provide troubleshooting examples for common issues
When you receive a documentation request, immediately identify what needs examples and plan to illustrate every single concept, feature, or instruction with concrete demonstrations. Ask for clarification if you need more context to create realistic, useful examples.
+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()
+1 -1
View File
@@ -83,7 +83,7 @@ And run.
```bash
$ bun install
$ bun run packages/opencode/src/index.ts
$ bun dev
```
#### Development Notes
+41 -32
View File
@@ -1,34 +1,43 @@
# 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) |
| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) |
| 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) |
| 2025-08-06 | 147,067 (+5,471) | 158,309 (+2,557) | 305,376 (+8,028) |
| 2025-08-07 | 152,591 (+5,524) | 160,889 (+2,580) | 313,480 (+8,104) |
+510 -929
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",
+6 -4
View File
@@ -5,18 +5,20 @@
"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.33",
"@tsconfig/node22": "22.0.2",
"ai": "5.0.0-beta.34",
"hono": "4.7.10",
"typescript": "5.8.2",
"zod": "3.25.49",
+8 -3
View File
@@ -1,17 +1,22 @@
{
"name": "@opencode/function",
"version": "0.0.1",
"version": "0.3.133",
"$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
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

+12
View File
@@ -0,0 +1,12 @@
<svg width="289" height="50" viewBox="0 0 289 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M264.5 0H288.5V8.5H272.5V16.5H288.5V25H272.5V33H288.5V41.5H264.5V0Z" fill="white"/>
<path d="M248.5 0H224.5V41.5H248.5V33H232.5V8.5H248.5V0Z" fill="white"/>
<path d="M256.5 8.5H248.5V33H256.5V8.5Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M184.5 0H216.5V41.5H184.5V0ZM208.5 8.5H192.5V33H208.5V8.5Z" fill="white"/>
<path d="M144.5 8.5H136.5V41.5H144.5V8.5Z" fill="white"/>
<path d="M136.5 0H112.5V41.5H120.5V8.5H136.5V0Z" fill="white"/>
<path d="M80.5 0H104.5V8.5H88.5V16.5H104.5V25H88.5V33H104.5V41.5H80.5V0Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M40.5 0H72.5V41.5H48.5V49.5H40.5V0ZM64.5 8.5H48.5V33H64.5V8.5Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.5 0H32.5V41.5955H0.5V0ZM24.5 8.5H8.5V33H24.5V8.5Z" fill="white"/>
<path d="M152.5 0H176.5V8.5H160.5V33H176.5V41.5H152.5V0Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 981 B

+12
View File
@@ -0,0 +1,12 @@
<svg width="289" height="50" viewBox="0 0 289 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M264.5 0H288.5V8.5H272.5V16.5H288.5V25H272.5V33H288.5V41.5H264.5V0Z" fill="black"/>
<path d="M248.5 0H224.5V41.5H248.5V33H232.5V8.5H248.5V0Z" fill="black"/>
<path d="M256.5 8.5H248.5V33H256.5V8.5Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M184.5 0H216.5V41.5H184.5V0ZM208.5 8.5H192.5V33H208.5V8.5Z" fill="black"/>
<path d="M144.5 8.5H136.5V41.5H144.5V8.5Z" fill="black"/>
<path d="M136.5 0H112.5V41.5H120.5V8.5H136.5V0Z" fill="black"/>
<path d="M80.5 0H104.5V8.5H88.5V16.5H104.5V25H88.5V33H104.5V41.5H80.5V0Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M40.5 0H72.5V41.5H48.5V49.5H40.5V0ZM64.5 8.5H48.5V33H64.5V8.5Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.5 0H32.5V41.5955H0.5V0ZM24.5 8.5H8.5V33H24.5V8.5Z" fill="black"/>
<path d="M152.5 0H176.5V8.5H160.5V33H176.5V41.5H152.5V0Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 981 B

+18
View File
@@ -0,0 +1,18 @@
<svg width="289" height="50" viewBox="0 0 289 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.5 16.5H24.5V33H8.5V16.5Z" fill="white" fill-opacity="0.2"/>
<path d="M48.5 16.5H64.5V33H48.5V16.5Z" fill="white" fill-opacity="0.2"/>
<path d="M120.5 16.5H136.5V33H120.5V16.5Z" fill="white" fill-opacity="0.2"/>
<path d="M160.5 16.5H176.5V33H160.5V16.5Z" fill="white" fill-opacity="0.2"/>
<path d="M192.5 16.5H208.5V33H192.5V16.5Z" fill="white" fill-opacity="0.2"/>
<path d="M232.5 16.5H248.5V33H232.5V16.5Z" fill="white" fill-opacity="0.2"/>
<path d="M264.5 0H288.5V8.5H272.5V16.5H288.5V25H272.5V33H288.5V41.5H264.5V0Z" fill="white" fill-opacity="0.95"/>
<path d="M248.5 0H224.5V41.5H248.5V33H232.5V8.5H248.5V0Z" fill="white" fill-opacity="0.95"/>
<path d="M256.5 8.5H248.5V33H256.5V8.5Z" fill="white" fill-opacity="0.95"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M184.5 0H216.5V41.5H184.5V0ZM208.5 8.5H192.5V33H208.5V8.5Z" fill="white" fill-opacity="0.95"/>
<path d="M144.5 8.5H136.5V41.5H144.5V8.5Z" fill="white" fill-opacity="0.5"/>
<path d="M136.5 0H112.5V41.5H120.5V8.5H136.5V0Z" fill="white" fill-opacity="0.5"/>
<path d="M80.5 0H104.5V8.5H88.5V16.5H104.5V25H88.5V33H104.5V41.5H80.5V0Z" fill="white" fill-opacity="0.5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M40.5 0H72.5V41.5H48.5V49.5H40.5V0ZM64.5 8.5H48.5V33H64.5V8.5Z" fill="white" fill-opacity="0.5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.5 0H32.5V41.5955H0.5V0ZM24.5 8.5H8.5V33H24.5V8.5Z" fill="white" fill-opacity="0.5"/>
<path d="M152.5 0H176.5V8.5H160.5V33H176.5V41.5H152.5V0Z" fill="white" fill-opacity="0.95"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+18
View File
@@ -0,0 +1,18 @@
<svg width="288" height="50" viewBox="0 0 288 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 16.5H24V33H8V16.5Z" fill="black" fill-opacity="0.15"/>
<path d="M48 16.5H64V33H48V16.5Z" fill="black" fill-opacity="0.15"/>
<path d="M120 16.5H136V33H120V16.5Z" fill="black" fill-opacity="0.15"/>
<path d="M160 16.5H176V33H160V16.5Z" fill="black" fill-opacity="0.15"/>
<path d="M192 16.5H208V33H192V16.5Z" fill="black" fill-opacity="0.15"/>
<path d="M232 16.5H248V33H232V16.5Z" fill="black" fill-opacity="0.15"/>
<path d="M264 0H288V8.5H272V16.5H288V25H272V33H288V41.5H264V0Z" fill="black" fill-opacity="0.95"/>
<path d="M248 0H224V41.5H248V33H232V8.5H248V0Z" fill="black" fill-opacity="0.95"/>
<path d="M256 8.5H248V33H256V8.5Z" fill="black" fill-opacity="0.95"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M184 0H216V41.5H184V0ZM208 8.5H192V33H208V8.5Z" fill="black" fill-opacity="0.95"/>
<path d="M144 8.5H136V41.5H144V8.5Z" fill="black" fill-opacity="0.55"/>
<path d="M136 0H112V41.5H120V8.5H136V0Z" fill="black" fill-opacity="0.55"/>
<path d="M80 0H104V8.5H88V16.5H104V25H88V33H104V41.5H80V0Z" fill="black" fill-opacity="0.55"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M40 0H72V41.5H48V49.5H40V0ZM64 8.5H48V33H64V8.5Z" fill="black" fill-opacity="0.55"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0H32V41.5955H0V0ZM24 8.5H8V33H24V8.5Z" fill="black" fill-opacity="0.55"/>
<path d="M152 0H176V8.5H160V33H176V41.5H152V0Z" fill="black" fill-opacity="0.95"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+4
View File
@@ -0,0 +1,4 @@
<svg width="70" height="70" viewBox="0 0 70 70" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 13H35V58H0V13ZM26.25 22.1957H8.75V48.701H26.25V22.1957Z" fill="white"/>
<path d="M43.75 13H70V22.1957H52.5V48.701H70V57.8967H43.75V13Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 306 B

+4
View File
@@ -0,0 +1,4 @@
<svg width="70" height="70" viewBox="0 0 70 70" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 13H35V58H0V13ZM26.25 22.1957H8.75V48.701H26.25V22.1957Z" fill="black"/>
<path d="M43.75 13H70V22.1957H52.5V48.701H70V57.8967H43.75V13Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 306 B

+4
View File
@@ -0,0 +1,4 @@
<svg width="64" height="42" viewBox="0 0 64 42" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0H32V41.5955H0V0ZM24 8.5H8V33H24V8.5Z" fill="white"/>
<path d="M40 0H64V8.5H48V33H64V41.5H40V0Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 267 B

+4
View File
@@ -0,0 +1,4 @@
<svg width="64" height="42" viewBox="0 0 64 42" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0H32V41.5955H0V0ZM24 8.5H8V33H24V8.5Z" fill="black"/>
<path d="M40 0H64V8.5H48V33H64V41.5H40V0Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 267 B

-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
+15 -4
View File
@@ -1,12 +1,12 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "0.0.5",
"version": "0.3.133",
"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,12 +30,21 @@
"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:*",
"@opentelemetry/auto-instrumentations-node": "0.62.0",
"@opentelemetry/exporter-jaeger": "2.0.1",
"@opentelemetry/exporter-otlp-http": "0.26.0",
"@opentelemetry/exporter-trace-otlp-http": "0.203.0",
"@opentelemetry/instrumentation-fetch": "0.203.0",
"@opentelemetry/sdk-node": "0.203.0",
"@opentelemetry/sdk-trace-node": "2.0.1",
"@standard-schema/spec": "1.0.0",
"@zip.js/zip.js": "2.7.62",
"ai": "catalog:",
@@ -45,11 +54,13 @@
"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": "catalog:",
"turndown": "7.2.0",
"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",
"yargs": "18.0.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:") &&
+30 -9
View File
@@ -10,13 +10,16 @@ export namespace Agent {
export const Info = z
.object({
name: z.string(),
description: z.string().optional(),
mode: z.union([z.literal("subagent"), z.literal("primary"), z.literal("all")]),
topP: z.number().optional(),
temperature: z.number().optional(),
model: z
.object({
modelID: z.string(),
providerID: z.string(),
})
.optional(),
description: z.string(),
prompt: z.string().optional(),
tools: z.record(z.boolean()),
})
@@ -24,6 +27,7 @@ export namespace Agent {
ref: "Agent",
})
export type Info = z.infer<typeof Info>
const state = App.state("agent", async () => {
const cfg = await Config.get()
const result: Record<string, Info> = {
@@ -35,22 +39,36 @@ export namespace Agent {
todoread: false,
todowrite: false,
},
mode: "subagent",
},
build: {
name: "build",
tools: {},
mode: "primary",
},
plan: {
name: "plan",
tools: {
write: false,
edit: false,
patch: false,
},
mode: "primary",
},
}
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] = {
name: key,
description: "",
tools: {
todowrite: false,
todoread: false,
},
mode: "all",
tools: {},
}
const model = value.model ?? cfg.model
if (model) item.model = Provider.parseModel(model)
if (value.model) item.model = Provider.parseModel(value.model)
if (value.prompt) item.prompt = value.prompt
if (value.tools)
item.tools = {
@@ -58,6 +76,9 @@ export namespace Agent {
...value.tools,
}
if (value.description) item.description = value.description
if (value.temperature != undefined) item.temperature = value.temperature
if (value.top_p != undefined) item.topP = value.top_p
if (value.mode) item.mode = value.mode
}
return result
})
+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, {
+8 -6
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
@@ -639,7 +641,7 @@ export const GithubRunCommand = cmd({
messageID: Identifier.ascending("message"),
providerID,
modelID,
mode: "build",
agent: "build",
parts: [
{
id: Identifier.ascending("part"),
@@ -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"
+19 -9
View File
@@ -8,8 +8,8 @@ import { Flag } from "../../flag/flag"
import { Config } from "../../config/config"
import { bootstrap } from "../bootstrap"
import { MessageV2 } from "../../session/message-v2"
import { Mode } from "../../session/mode"
import { Identifier } from "../../id/id"
import { Agent } from "../../agent/agent"
const TOOL: Record<string, [string, string]> = {
todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
@@ -54,9 +54,9 @@ export const RunCommand = cmd({
alias: ["m"],
describe: "model to use in the format of provider/model",
})
.option("mode", {
.option("agent", {
type: "string",
describe: "mode to use",
describe: "agent to use",
})
},
handler: async (args) => {
@@ -103,7 +103,19 @@ export const RunCommand = cmd({
}
UI.empty()
const { providerID, modelID } = args.model ? Provider.parseModel(args.model) : await Provider.defaultModel()
const agent = await (async () => {
if (args.agent) return Agent.get(args.agent)
const build = Agent.get("build")
if (build) return build
return Agent.list().then((x) => x[0])
})()
const { providerID, modelID } = await (() => {
if (args.model) return Provider.parseModel(args.model)
if (agent.model) return agent.model
return Provider.defaultModel()
})()
UI.println(UI.Style.TEXT_NORMAL_BOLD + "@ ", UI.Style.TEXT_NORMAL + `${providerID}/${modelID}`)
UI.empty()
@@ -156,19 +168,17 @@ export const RunCommand = cmd({
UI.error(err)
})
const mode = args.mode ? await Mode.get(args.mode) : await Mode.list().then((x) => x[0])
const messageID = Identifier.ascending("message")
const result = await Session.chat({
sessionID: session.id,
messageID,
...(mode.model
? mode.model
...(agent.model
? agent.model
: {
providerID,
modelID,
}),
mode: mode.name,
agent: agent.name,
parts: [
{
id: Identifier.ascending("part"),
+16 -6
View File
@@ -11,8 +11,18 @@ import { Config } from "../../config/config"
import { Bus } from "../../bus"
import { Log } from "../../util/log"
import { FileWatcher } from "../../file/watch"
import { Mode } from "../../session/mode"
import { Ide } from "../../ide"
import { Agent } from "../../agent/agent"
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]",
@@ -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()
@@ -105,7 +115,7 @@ export const TuiCommand = cmd({
CGO_ENABLED: "0",
OPENCODE_SERVER: server.url.toString(),
OPENCODE_APP_INFO: JSON.stringify(app),
OPENCODE_MODES: JSON.stringify(await Mode.list()),
OPENCODE_AGENTS: JSON.stringify(await Agent.list()),
},
onExit: () => {
server.stop()
+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`,
+168 -44
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,49 @@ 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 = Agent.safeParse(config)
if (parsed.success) {
result.mode = mergeDeep(result.mode, {
[config.name]: parsed.data,
})
continue
}
throw new InvalidError({ path: item }, { cause: parsed.error })
}
// Migrate deprecated mode field to agent field
for (const [name, mode] of Object.entries(result.mode)) {
result.agent = mergeDeep(result.agent ?? {}, {
[name]: {
...mode,
mode: "primary" as const,
},
})
}
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"
@@ -72,6 +117,12 @@ export namespace Config {
if (result.keybinds?.messages_revert && !result.keybinds.messages_undo) {
result.keybinds.messages_undo = result.keybinds.messages_revert
}
if (result.keybinds?.switch_mode && !result.keybinds.switch_agent) {
result.keybinds.switch_agent = result.keybinds.switch_mode
}
if (result.keybinds?.switch_mode_reverse && !result.keybinds.switch_agent_reverse) {
result.keybinds.switch_agent_reverse = result.keybinds.switch_mode_reverse
}
if (!result.username) {
const os = await import("os")
@@ -113,31 +164,34 @@ export namespace Config {
export const Mcp = z.discriminatedUnion("type", [McpLocal, McpRemote])
export type Mcp = z.infer<typeof Mcp>
export const Mode = z
export const Agent = z
.object({
model: z.string().optional(),
temperature: z.number().optional(),
top_p: z.number().optional(),
prompt: z.string().optional(),
tools: z.record(z.string(), z.boolean()).optional(),
disable: z.boolean().optional(),
description: z.string().optional().describe("Description of when to use the agent"),
mode: z.union([z.literal("subagent"), z.literal("primary"), z.literal("all")]).optional(),
})
.openapi({
ref: "ModeConfig",
ref: "AgentConfig",
})
export type Mode = z.infer<typeof Mode>
export const Agent = Mode.extend({
description: z.string(),
}).openapi({
ref: "AgentConfig",
})
export type Agent = z.infer<typeof Agent>
export const Keybinds = z
.object({
leader: z.string().optional().default("ctrl+x").describe("Leader key for keybind combinations"),
app_help: z.string().optional().default("<leader>h").describe("Show help dialog"),
switch_mode: z.string().optional().default("tab").describe("Next mode"),
switch_mode_reverse: z.string().optional().default("shift+tab").describe("Previous Mode"),
switch_mode: z.string().optional().default("none").describe("@deprecated use switch_agent. Next mode"),
switch_mode_reverse: z
.string()
.optional()
.default("none")
.describe("@deprecated use switch_agent_reverse. Previous mode"),
switch_agent: z.string().optional().default("tab").describe("Next agent"),
switch_agent_reverse: z.string().optional().default("shift+tab").describe("Previous agent"),
editor_open: z.string().optional().default("<leader>e").describe("Open external editor"),
session_export: z.string().optional().default("<leader>x").describe("Export session to editor"),
session_new: z.string().optional().default("<leader>n").describe("Create a new session"),
@@ -187,7 +241,7 @@ export namespace Config {
})
export type Layout = z.infer<typeof Layout>
export const Permission = z.union([z.literal("ask"), z.literal("allow")])
export const Permission = z.union([z.literal("ask"), z.literal("allow"), z.literal("deny")])
export type Permission = z.infer<typeof Permission>
export const Info = z
@@ -195,6 +249,8 @@ export namespace Config {
$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(),
snapshot: z.boolean().optional(),
share: z
.enum(["manual", "auto", "disabled"])
.optional()
@@ -210,9 +266,7 @@ export namespace Config {
model: z.string().describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(),
small_model: z
.string()
.describe(
"Small model to use for tasks like summarization and title generation in the format of provider/model",
)
.describe("Small model to use for tasks like title generation in the format of provider/model")
.optional(),
username: z
.string()
@@ -220,19 +274,21 @@ export namespace Config {
.describe("Custom username to display in conversations instead of system username"),
mode: z
.object({
build: Mode.optional(),
plan: Mode.optional(),
build: Agent.optional(),
plan: Agent.optional(),
})
.catchall(Mode)
.catchall(Agent)
.optional()
.describe("Modes configuration, see https://opencode.ai/docs/modes"),
.describe("@deprecated Use `agent` field instead."),
agent: z
.object({
plan: Agent.optional(),
build: Agent.optional(),
general: Agent.optional(),
})
.catchall(Agent)
.optional()
.describe("Modes configuration, see https://opencode.ai/docs/modes"),
.describe("Agent configuration, see https://opencode.ai/docs/agent"),
provider: z
.record(
ModelsDev.Provider.partial()
@@ -251,6 +307,34 @@ 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
@@ -294,10 +378,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"), {
@@ -318,54 +403,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
+3 -3
View File
@@ -1,4 +1,6 @@
import "zod-openapi/extend"
import { Trace } from "./trace"
Trace.init()
import yargs from "yargs"
import { hideBin } from "yargs/helpers"
import { RunCommand } from "./cli/cmd/run"
@@ -18,9 +20,6 @@ import { DebugCommand } from "./cli/cmd/debug"
import { StatsCommand } from "./cli/cmd/stats"
import { McpCommand } from "./cli/cmd/mcp"
import { GithubCommand } from "./cli/cmd/github"
import { Trace } from "./trace"
Trace.init()
const cancel = new AbortController()
@@ -83,6 +82,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
}
}
+243 -8
View File
@@ -16,8 +16,10 @@ import { Config } from "../config/config"
import { File } from "../file"
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"
import { Agent } from "../agent/agent"
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",
@@ -212,10 +219,39 @@ export namespace Server {
return c.json(sessions)
},
)
.get(
"/session/:id",
describeRoute({
description: "Get session",
operationId: "session.get",
responses: {
200: {
description: "Get session",
content: {
"application/json": {
schema: resolver(Session.Info),
},
},
},
},
}),
zValidator(
"param",
z.object({
id: z.string(),
}),
),
async (c) => {
const sessionID = c.req.valid("param").id
const session = await Session.get(sessionID)
return c.json(session)
},
)
.post(
"/session",
describeRoute({
description: "Create a new session",
operationId: "session.create",
responses: {
...ERRORS,
200: {
@@ -237,6 +273,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 +300,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 +337,7 @@ export namespace Server {
"/session/:id/abort",
describeRoute({
description: "Abort a session",
operationId: "session.abort",
responses: {
200: {
description: "Aborted session",
@@ -324,6 +363,7 @@ export namespace Server {
"/session/:id/share",
describeRoute({
description: "Share a session",
operationId: "session.share",
responses: {
200: {
description: "Successfully shared session",
@@ -352,6 +392,7 @@ export namespace Server {
"/session/:id/share",
describeRoute({
description: "Unshare the session",
operationId: "session.unshare",
responses: {
200: {
description: "Successfully unshared session",
@@ -380,6 +421,7 @@ export namespace Server {
"/session/:id/summarize",
describeRoute({
description: "Summarize the session",
operationId: "session.summarize",
responses: {
200: {
description: "Summarized session",
@@ -415,6 +457,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 +487,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 +555,7 @@ export namespace Server {
"/session/:id/revert",
describeRoute({
description: "Revert a message",
operationId: "session.revert",
responses: {
200: {
description: "Updated session",
@@ -506,6 +585,7 @@ export namespace Server {
"/session/:id/unrevert",
describeRoute({
description: "Restore all reverted messages",
operationId: "session.unrevert",
responses: {
200: {
description: "Updated session",
@@ -529,10 +609,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 +673,7 @@ export namespace Server {
"/find",
describeRoute({
description: "Find text in files",
operationId: "find.text",
responses: {
200: {
description: "Matches",
@@ -593,6 +706,7 @@ export namespace Server {
"/find/file",
describeRoute({
description: "Find files",
operationId: "find.files",
responses: {
200: {
description: "File paths",
@@ -625,6 +739,7 @@ export namespace Server {
"/find/symbol",
describeRoute({
description: "Find workspace symbols",
operationId: "find.symbols",
responses: {
200: {
description: "Symbols",
@@ -652,6 +767,7 @@ export namespace Server {
"/file",
describeRoute({
description: "Read a file",
operationId: "file.read",
responses: {
200: {
description: "File content",
@@ -688,6 +804,7 @@ export namespace Server {
"/file/status",
describeRoute({
description: "Get file status",
operationId: "file.status",
responses: {
200: {
description: "File status",
@@ -708,6 +825,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",
@@ -754,22 +872,23 @@ export namespace Server {
},
)
.get(
"/mode",
"/agent",
describeRoute({
description: "List all modes",
description: "List all agents",
operationId: "app.agents",
responses: {
200: {
description: "List of modes",
description: "List of agents",
content: {
"application/json": {
schema: resolver(Mode.Info.array()),
schema: resolver(Agent.Info.array()),
},
},
},
},
}),
async (c) => {
const modes = await Mode.list()
const modes = await Agent.list()
return c.json(modes)
},
)
@@ -777,6 +896,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 +920,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 +934,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_agent)",
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()
+200 -98
View File
@@ -36,17 +36,29 @@ import { NamedError } from "../util/error"
import { SystemPrompt } from "./system"
import { FileTime } from "../file/time"
import { MessageV2 } from "./message-v2"
import { Mode } from "./mode"
import { LSP } from "../lsp"
import { ReadTool } from "../tool/read"
import { mergeDeep, pipe, splitWhen } from "remeda"
import { ToolRegistry } from "../tool/registry"
import { Plugin } from "../plugin"
import { Agent } from "../agent/agent"
export namespace Session {
const log = Log.create({ service: "session" })
const OUTPUT_TOKEN_MAX = 32_000
const parentSessionTitlePrefix = "New session - "
const childSessionTitlePrefix = "Child session - "
function createDefaultTitle(isChild = false) {
return (isChild ? childSessionTitlePrefix : parentSessionTitlePrefix) + new Date().toISOString()
}
function isDefaultTitle(title: string) {
return title.startsWith(parentSessionTitlePrefix)
}
export const Info = z
.object({
id: Identifier.schema("session"),
@@ -120,6 +132,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 +148,7 @@ export namespace Session {
sessions,
messages,
pending,
autoCompacting,
queued,
}
},
@@ -150,7 +164,7 @@ export namespace Session {
id: Identifier.descending("session"),
version: Installation.VERSION,
parentID,
title: (parentID ? "Child session - " : "New Session - ") + new Date().toISOString(),
title: createDefaultTitle(!!parentID),
time: {
created: Date.now(),
updated: Date.now(),
@@ -256,7 +270,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) {
@@ -340,7 +357,7 @@ export namespace Session {
messageID: Identifier.schema("message").optional(),
providerID: z.string(),
modelID: z.string(),
mode: z.string().optional(),
agent: z.string().optional(),
system: z.string().optional(),
tools: z.record(z.boolean()).optional(),
parts: z.array(
@@ -365,6 +382,16 @@ export namespace Session {
.openapi({
ref: "FilePartInput",
}),
MessageV2.AgentPart.omit({
messageID: true,
sessionID: true,
})
.partial({
id: true,
})
.openapi({
ref: "AgentPartInput",
}),
]),
),
})
@@ -376,7 +403,37 @@ export namespace Session {
const l = log.clone().tag("session", input.sessionID)
l.info("chatting")
const inputMode = input.mode ?? "build"
const inputAgent = input.agent ?? "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",
@@ -519,6 +576,28 @@ export namespace Session {
]
}
}
if (part.type === "agent") {
return [
{
id: Identifier.ascending("part"),
...part,
messageID: userMsg.id,
sessionID: input.sessionID,
},
{
id: Identifier.ascending("part"),
messageID: userMsg.id,
sessionID: input.sessionID,
type: "text",
synthetic: true,
text:
"Use the above message and context to generate a prompt and call the task tool with subagent: " +
part.name,
},
]
}
return [
{
id: Identifier.ascending("part"),
@@ -529,7 +608,7 @@ export namespace Session {
]
}),
).then((x) => x.flat())
if (inputMode === "plan")
if (inputAgent === "plan")
userParts.push({
id: Identifier.ascending("part"),
messageID: userMsg.id,
@@ -538,12 +617,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) ?? []
@@ -560,35 +650,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
@@ -598,6 +659,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,
@@ -606,13 +669,12 @@ export namespace Session {
return chat(input)
}
}
using abort = lock(input.sessionID)
const lastSummary = msgs.findLast((msg) => msg.info.role === "assistant" && msg.info.summary === true)
if (lastSummary) msgs = msgs.filter((msg) => msg.info.id >= lastSummary.info.id)
if (msgs.length === 1 && !session.parentID) {
if (msgs.length === 1 && !session.parentID && isDefaultTitle(session.title)) {
const small = (await Provider.getSmallModel(input.providerID)) ?? model
generateText({
maxOutputTokens: small.info.reasoning ? 1024 : 20,
@@ -653,12 +715,12 @@ export namespace Session {
.catch(() => {})
}
const mode = await Mode.get(inputMode)
const agent = await Agent.get(inputAgent)
let system = SystemPrompt.header(input.providerID)
system.push(
...(() => {
if (input.system) return [input.system]
if (mode.prompt) return [mode.prompt]
if (agent.prompt) return [agent.prompt]
return SystemPrompt.provider(input.modelID)
})(),
)
@@ -672,7 +734,7 @@ export namespace Session {
id: Identifier.ascending("message"),
role: "assistant",
system,
mode: inputMode,
mode: inputAgent,
path: {
cwd: app.path.cwd,
root: app.path.root,
@@ -697,8 +759,8 @@ export namespace Session {
const processor = createProcessor(assistantMsg, model.info)
const enabledTools = pipe(
mode.tools,
mergeDeep(ToolRegistry.enabled(input.providerID, input.modelID)),
agent.tools,
mergeDeep(await ToolRegistry.enabled(input.providerID, input.modelID)),
mergeDeep(input.tools ?? {}),
)
for (const item of await ToolRegistry.tools(input.providerID, input.modelID)) {
@@ -708,11 +770,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") {
@@ -731,6 +804,15 @@ export namespace Session {
}
},
})
await Plugin.trigger(
"tool.execute.after",
{
tool: item.id,
sessionID: input.sessionID,
callID: options.toolCallId,
},
result,
)
return result
},
toModelOutput(result) {
@@ -743,11 +825,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")
@@ -767,6 +848,21 @@ export namespace Session {
tools[key] = item
}
const params = {
temperature: model.info.temperature
? (agent.temperature ?? ProviderTransform.temperature(input.providerID, input.modelID))
: undefined,
topP: agent.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(e) {
log.error("streamText error", {
@@ -807,7 +903,7 @@ export namespace Session {
},
modelID: input.modelID,
providerID: input.providerID,
mode: inputMode,
mode: inputAgent,
time: {
created: Date.now(),
},
@@ -819,13 +915,26 @@ export namespace Session {
messages,
}
},
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 => ({
@@ -835,9 +944,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,
@@ -869,15 +975,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 {
@@ -893,7 +995,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",
@@ -903,7 +1005,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":
@@ -913,10 +1015,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,
@@ -925,12 +1028,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,
@@ -946,27 +1049,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,
@@ -980,19 +1069,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
}
@@ -1007,6 +1084,7 @@ export namespace Session {
sessionID: assistantMsg.sessionID,
type: "step-start",
})
snapshot = await Snapshot.track()
break
case "finish-step":
@@ -1022,6 +1100,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":
@@ -1106,7 +1198,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: {
@@ -1269,9 +1361,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,
})
@@ -1294,10 +1396,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,
}
+25 -1
View File
@@ -172,6 +172,21 @@ export namespace MessageV2 {
})
export type FilePart = z.infer<typeof FilePart>
export const AgentPart = PartBase.extend({
type: z.literal("agent"),
name: z.string(),
source: z
.object({
value: z.string(),
start: z.number().int(),
end: z.number().int(),
})
.optional(),
}).openapi({
ref: "AgentPart",
})
export type AgentPart = z.infer<typeof AgentPart>
export const StepStartPart = PartBase.extend({
type: z.literal("step-start"),
}).openapi({
@@ -212,7 +227,16 @@ export namespace MessageV2 {
export type User = z.infer<typeof User>
export const Part = z
.discriminatedUnion("type", [TextPart, FilePart, ToolPart, StepStartPart, StepFinishPart, SnapshotPart, PatchPart])
.discriminatedUnion("type", [
TextPart,
FilePart,
ToolPart,
StepStartPart,
StepFinishPart,
SnapshotPart,
PatchPart,
AgentPart,
])
.openapi({
ref: "Part",
})
-72
View File
@@ -1,72 +0,0 @@
import { App } from "../app/app"
import { Config } from "../config/config"
import z from "zod"
import { Provider } from "../provider/provider"
export namespace Mode {
export const Info = z
.object({
name: z.string(),
temperature: z.number().optional(),
model: z
.object({
modelID: z.string(),
providerID: z.string(),
})
.optional(),
prompt: z.string().optional(),
tools: z.record(z.boolean()),
})
.openapi({
ref: "Mode",
})
export type Info = z.infer<typeof Info>
const state = App.state("mode", async () => {
const cfg = await Config.get()
const model = cfg.model ? Provider.parseModel(cfg.model) : undefined
const result: Record<string, Info> = {
build: {
model,
name: "build",
tools: {},
},
plan: {
name: "plan",
model,
tools: {
write: false,
edit: false,
patch: false,
},
},
}
for (const [key, value] of Object.entries(cfg.mode ?? {})) {
if (value.disable) continue
let item = result[key]
if (!item)
item = result[key] = {
name: key,
tools: {},
}
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.tools)
item.tools = {
...value.tools,
...item.tools,
}
}
return result
})
export async function get(mode: string) {
return state().then((x) => x[mode])
}
export async function list() {
return state().then((x) => Object.values(x))
}
}
@@ -93,18 +93,6 @@ Carefully read the issue and think hard about a plan to solve it before coding.
- To test hypotheses, you can also add test statements or functions
- Revisit your assumptions if unexpected behavior occurs.
# How to create a Todo List
Use the following format to create a todo list:
```markdown
- [ ] Step 1: Description of the first step
- [ ] Step 2: Description of the second step
- [ ] Step 3: Description of the third step
```
Do not ever use HTML tags or any other formatting for the todo list, as it will not be rendered correctly. Always use the markdown format shown above. Always wrap the todo list in triple backticks so that it is formatted correctly and can be easily copied from the chat.
Always show the completed todo list to the user as the last item in your message, so that they can see that you have addressed all of the steps.
# Communication Guidelines
Always communicate clearly and concisely in a casual, friendly yet professional tone.
+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))
}
+4 -1
View File
@@ -5,6 +5,7 @@ import fs from "fs/promises"
import { Log } from "../util/log"
import { Global } from "../global"
import { z } from "zod"
import { Config } from "../config/config"
export namespace Snapshot {
const log = Log.create({ service: "snapshot" })
@@ -26,6 +27,8 @@ export namespace Snapshot {
export async function track() {
const app = App.info()
if (!app.git) return
const cfg = await Config.get()
if (cfg.snapshot === false) return
const git = gitdir()
if (await fs.mkdir(git, { recursive: true })) {
await $`git init`
@@ -39,7 +42,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()
}
+54 -27
View File
@@ -1,21 +1,30 @@
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 path from "path"
import Parser from "tree-sitter"
import Bash from "tree-sitter-bash"
import { Permission } from "../permission"
import { Config } from "../config/config"
import { Filesystem } from "../util/filesystem"
import { Permission } from "../permission"
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 parser = new Parser()
parser.setLanguage(Bash.language as any)
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,
@@ -30,9 +39,9 @@ export const BashTool = Tool.define("bash", {
}),
async execute(params, ctx) {
const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT)
const tree = parser.parse(params.command)
const cfg = await Config.get()
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)
@@ -67,9 +76,14 @@ export const BashTool = Tool.define("bash", {
// 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("-")) continue
const resolved = path.resolve(app.path.cwd, arg)
if (!Filesystem.contains(app.path.cwd, resolved)) {
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.`,
)
@@ -79,22 +93,29 @@ export const BashTool = Tool.define("bash", {
// always allow cd if it passes above check
if (!needsAsk && command[0] !== "cd") {
const ask = (() => {
const action = (() => {
for (const [pattern, value] of Object.entries(permissions)) {
if (new Bun.Glob(pattern).match(node.text)) {
return value
}
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 (action === "deny") {
throw new Error(
"The user has specifically restricted access to this command, you are not allowed to execute it.",
)
}
if (action === "ask") needsAsk = true
}
}
if (needsAsk) {
await Permission.ask({
id: "basj",
type: "bash",
sessionID: ctx.sessionID,
messageID: ctx.messageID,
callID: ctx.callID,
title: params.command,
metadata: {
command: params.command,
@@ -102,18 +123,24 @@ export const BashTool = Tool.define("bash", {
})
}
const process = Bun.spawn({
cmd: ["bash", "-c", params.command],
const process = exec(params.command, {
cwd: app.path.cwd,
maxBuffer: MAX_OUTPUT_LENGTH,
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,
+46 -29
View File
@@ -35,61 +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)
if (!Filesystem.contains(app.path.cwd, filepath)) {
throw new Error(`File ${filepath} is not in the current working directory`)
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()
if (cfg.permission?.edit === "ask")
await Permission.ask({
id: "edit",
sessionID: ctx.sessionID,
title: "Edit this file: " + filepath,
metadata: {
filePath: filepath,
oldString: params.oldString,
newString: params.newString,
},
})
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
}
@@ -104,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: {},
}
},
})
+16 -18
View File
@@ -10,9 +10,12 @@ import { TaskTool } from "./task"
import { TodoWriteTool, TodoReadTool } from "./todo"
import { WebFetchTool } from "./webfetch"
import { WriteTool } from "./write"
import { InvalidTool } from "./invalid"
import { Config } from "../config/config"
export namespace ToolRegistry {
const ALL = [
InvalidTool,
BashTool,
EditTool,
WebFetchTool,
@@ -63,26 +66,21 @@ export namespace ToolRegistry {
return result
}
export function enabled(_providerID: string, modelID: string): Record<string, boolean> {
if (modelID.toLowerCase().includes("claude")) {
return {
patch: false,
}
export async function enabled(_providerID: string, _modelID: string): Promise<Record<string, boolean>> {
const cfg = await Config.get()
const result: Record<string, boolean> = {}
result["patch"] = false
if (cfg.permission?.edit === "deny") {
result["edit"] = false
result["patch"] = false
result["write"] = false
}
if (cfg?.permission?.bash === "deny") {
result["bash"] = false
}
if (
modelID.toLowerCase().includes("qwen") ||
modelID.includes("gpt-") ||
modelID.includes("o1") ||
modelID.includes("o3")
) {
return {
patch: false,
todowrite: false,
todoread: false,
}
}
return {}
return result
}
function sanitizeGeminiParameters(schema: z.ZodTypeAny, visited = new Set()): z.ZodTypeAny {
+15 -8
View File
@@ -8,8 +8,13 @@ import { Identifier } from "../id/id"
import { Agent } from "../agent/agent"
export const TaskTool = Tool.define("task", async () => {
const agents = await Agent.list()
const description = DESCRIPTION.replace("{agents}", agents.map((a) => `- ${a.name}: ${a.description}`).join("\n"))
const agents = await Agent.list().then((x) => x.filter((a) => a.mode !== "primary"))
const description = DESCRIPTION.replace(
"{agents}",
agents
.map((a) => `- ${a.name}: ${a.description ?? "This subagent should only be called manually by the user."}`)
.join("\n"),
)
return {
description,
parameters: z.object({
@@ -20,8 +25,9 @@ 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)
if (!agent) throw new Error(`Unknown agent type: ${params.subagent_type} is not a valid agent type`)
const messageID = Identifier.ascending("message")
const parts: Record<string, MessageV2.ToolPart> = {}
const unsub = Bus.subscribe(MessageV2.Event.PartUpdated, async (evt) => {
@@ -38,8 +44,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,11 +56,12 @@ export const TaskTool = Tool.define("task", async () => {
sessionID: session.id,
modelID: model.modelID,
providerID: model.providerID,
mode: msg.mode,
system: agent.prompt,
agent: agent.name,
tools: {
...agent.tools,
todowrite: false,
todoread: false,
task: false,
...agent.tools,
},
parts: [
{
+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(", ")}]`);
});
+2 -2
View File
@@ -5,8 +5,8 @@ import { App } from "../app/app"
const TodoInfo = z.object({
content: z.string().describe("Brief description of the task"),
status: z.enum(["pending", "in_progress", "completed", "cancelled"]).describe("Current status of the task"),
priority: z.enum(["high", "medium", "low"]).describe("Priority level of the task"),
status: z.string().describe("Current status of the task: pending, in_progress, completed, cancelled"),
priority: z.string().describe("Priority level of the task: high, medium, low"),
id: z.string().describe("Unique identifier for the todo item"),
})
type TodoInfo = z.infer<typeof TodoInfo>
+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
}
+3 -1
View File
@@ -31,8 +31,10 @@ export const WriteTool = Tool.define("write", {
const cfg = await Config.get()
if (cfg.permission?.edit === "ask")
await Permission.ask({
id: "write",
type: "write",
sessionID: ctx.sessionID,
messageID: ctx.messageID,
callID: ctx.callID,
title: exists ? "Overwrite this file: " + filepath : "Create new file: " + filepath,
metadata: {
filePath: filepath,
+11 -47
View File
@@ -1,53 +1,17 @@
import { Global } from "../global"
import { Installation } from "../installation"
import path from "path"
import { NodeSDK } from "@opentelemetry/sdk-node"
import { FetchInstrumentation } from "@opentelemetry/instrumentation-fetch"
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"
export namespace Trace {
export function init() {
if (!Installation.isDev()) return
const writer = Bun.file(path.join(Global.Path.data, "log", "fetch.log")).writer()
const sdk = new NodeSDK({
serviceName: "opencode",
instrumentations: [new FetchInstrumentation()],
traceExporter: new OTLPTraceExporter({
url: "http://localhost:4318/v1/traces", // or your OTLP endpoint
}),
})
const originalFetch = globalThis.fetch
// @ts-expect-error
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url
const method = init?.method || "GET"
const urlObj = new URL(url)
writer.write(`\n${method} ${urlObj.pathname}${urlObj.search} HTTP/1.1\n`)
writer.write(`Host: ${urlObj.host}\n`)
if (init?.headers) {
if (init.headers instanceof Headers) {
init.headers.forEach((value, key) => {
writer.write(`${key}: ${value}\n`)
})
} else {
for (const [key, value] of Object.entries(init.headers)) {
writer.write(`${key}: ${value}\n`)
}
}
}
if (init?.body) {
writer.write(`\n${init.body}`)
}
writer.flush()
const response = await originalFetch(input, init)
const clonedResponse = response.clone()
writer.write(`\nHTTP/1.1 ${response.status} ${response.statusText}\n`)
response.headers.forEach((value, key) => {
writer.write(`${key}: ${value}\n`)
})
if (clonedResponse.body) {
clonedResponse.text().then(async (x) => {
writer.write(`\n${x}\n`)
})
}
writer.flush()
return response
}
sdk.start()
}
}
+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)
}
}
+9 -6
View File
@@ -7,6 +7,7 @@ import { Log } from "../../src/util/log"
const ctx = {
sessionID: "test",
messageID: "",
toolCallID: "",
abort: AbortSignal.any([]),
metadata: () => {},
}
@@ -18,19 +19,21 @@ Log.init({ print: false })
describe("tool.bash", () => {
test("basic", async () => {
await App.provide({ cwd: projectRoot }, async () => {
await bash.execute(
const result = await bash.execute(
{
command: "cd foo/bar && ls",
description: "List files in foo/bar",
command: "echo 'test'",
description: "Echo test message",
},
ctx,
)
expect(result.metadata.exit).toBe(0)
expect(result.metadata.stdout).toContain("test")
})
})
test("cd ../ should fail", async () => {
test("cd ../ should fail outside of project root", async () => {
await App.provide({ cwd: projectRoot }, async () => {
expect(
await expect(
bash.execute(
{
command: "cd ../",
@@ -38,7 +41,7 @@ describe("tool.bash", () => {
},
ctx,
),
).rejects.toThrow()
).rejects.toThrow("This command references paths outside of")
})
})
})
+1
View File
@@ -7,6 +7,7 @@ import path from "path"
const ctx = {
sessionID: "test",
messageID: "",
toolCallID: "",
abort: AbortSignal.any([]),
metadata: () => {},
}
+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.133",
"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"

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