Compare commits

..

726 Commits

Author SHA1 Message Date
Dax Raad 3bd7d33132 docs: fix 2025-08-07 21:49:18 -04:00
Dax Raad d408886e43 renable todo tool 2025-08-07 21:47:37 -04:00
Dax Raad e46ecf67d9 looser todo tool schema 2025-08-07 21:47:37 -04:00
Jay V 60452a4d73 docs: update cli 2025-08-07 19:24:08 -04:00
Jay V 66023f4402 docs: edit agent doc 2025-08-07 18:51:54 -04:00
Dax Raad 83fb7dfbb6 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 77c101d742 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 62bff76d3a docs: slash commands 2025-08-07 16:16:16 -04:00
Timo Clasen 134a698437 fix: get session api (#1684) 2025-08-07 15:28:18 -04:00
Aiden Cline 12ab0c9048 fix: modal pastes (#1677) 2025-08-07 13:23:58 -05:00
GitHub Action 763a37f880 ignore: update download stats 2025-08-07 2025-08-07 12:04:29 +00:00
Aiden Cline 3913566e28 fix: text selection bug (#1664) 2025-08-07 05:32:34 -05:00
opencode 8a68dfdcbf release: v0.3.133 2025-08-07 00:30:05 +00:00
Dax Raad 4bd6efe2a9 add api to get session 2025-08-06 20:24:36 -04:00
Jay V 99ec88287b docs: config doc edits 2025-08-06 16:10:17 -04:00
Jay V 0d239dbe88 docs: typos 2025-08-06 15:45:16 -04:00
Timo Clasen 8f65d92165 fix(docs): small_model is not used for summarization (#1360) 2025-08-06 14:03:14 -05:00
Aiden Cline 77a4ffd7e0 fix: interface conversion panic (#1655) 2025-08-06 14:02:33 -05:00
Jay V 65ff65a5e7 docs: add undo to tutorial 2025-08-06 13:51:47 -04:00
Dax Raad 92c79f40b5 ignore: remove demo plugin 2025-08-06 11:36:53 -04:00
Dax Raad ed3d49d74a add snapshot field in config to disable snapshots 2025-08-06 11:35:37 -04:00
Aiden Cline 2019b160b6 docs: update readme (#1654) 2025-08-06 09:35:02 -05:00
Aiden Cline 0590e57765 chore: update marked-shiki, remove patch (#1653) 2025-08-06 08:47:53 -05:00
GitHub Action c0e62ad88d ignore: update download stats 2025-08-06 2025-08-06 12:04:53 +00:00
Yihui Khuu 7ba89b3d85 fix: run command should use specified model from cli args if provided (#1648) 2025-08-06 05:39:44 -05:00
Aiden Cline 41943011c0 docs: document permissions (#1638) 2025-08-06 05:18:08 -05:00
Aiden Cline a1a434a627 fix: highlight after text wrap (#1640) 2025-08-06 05:17:35 -05:00
Aiden Cline 947bf020a4 fix: handle undefined agent in task tool (#1642) 2025-08-06 05:16:43 -05:00
Aiden Cline 4fa81b899d permissions disallow support (#1627) 2025-08-05 19:14:28 -05:00
adamdotdevin a05e74f539 feat: better assistant message visual 2025-08-05 19:05:44 -05:00
Jay V 72bd8ab912 docs: identity 2025-08-05 19:36:10 -04:00
Timo Clasen 27c6625dd6 docs(plugins): fix typo (#1621) 2025-08-05 17:16:47 -05:00
Aiden Cline 2319c08054 fix: markdown table renders (#1623) 2025-08-05 17:16:35 -05:00
Timo Clasen 97677d8f05 fix: prevent title regeneration on auto compact (#1628) 2025-08-05 17:15:50 -05:00
Dax Raad 7ec57f1fd9 hint back to llm when tool does not exist 2025-08-05 15:58:12 -04:00
Jay V ecef453e51 docs: edit 2025-08-05 12:55:57 -04:00
adamdotdevin ef1f836f01 chore: cleanup old sdk 2025-08-05 11:46:12 -05:00
Dax Raad 5e9bf32f82 ci: send stats to posthog 2025-08-05 12:01:48 -04:00
GitHub Action 0d76317631 ignore: update download stats 2025-08-05 2025-08-05 12:04:48 +00:00
Yihui Khuu 6598713d0e fix: allow disabling the default general agent (#1616) 2025-08-05 05:20:00 -05:00
Omar Shaarawi 771f7bbf41 validate file part bounds to prevent panic (#1612) 2025-08-05 05:18:50 -05:00
Aiden Cline 4dd304caed docs: make formatter docs a bit more clear (#1613) 2025-08-05 05:17:56 -05:00
Aiden Cline 29f1bd9ead fix: server.root is not a function (#1614) 2025-08-05 05:17:32 -05:00
Timo Clasen 35865c50c5 fix(plugins): improve session idle event (#1615) 2025-08-05 05:16:43 -05:00
opencode 126b82c65b release: v0.3.130 2025-08-05 03:25:28 +00:00
Dax Raad 1fbfb6435a re-export shell $ for plugin 2025-08-04 23:20:04 -04:00
opencode 5918e5dcb2 release: v0.3.129 2025-08-05 01:18:50 +00:00
Dax Raad a147526677 fix @opencode-ai/plugin exports 2025-08-04 21:12:18 -04:00
Frank 51ac18cf4a sync 2025-08-04 21:09:44 -04:00
Frank facf209885 wip: gateway 2025-08-04 21:08:29 -04:00
Jay V c093eeb759 docs: lock 2025-08-04 19:53:50 -04:00
Jay V 14d3a9893f docs: edit new docs 2025-08-04 19:52:03 -04:00
Mahamed-Belkheir 93575f4a51 fix: default value for models with no cost object (#1601) 2025-08-04 16:45:35 -05:00
Min Chun Fu 7acd4fa85d added vesper theme (#1602) 2025-08-04 16:45:00 -05:00
Aiden Cline 5f154b69d8 docs: make plugins page exposed (#1603) 2025-08-04 16:44:28 -05:00
opencode c3bc30a155 release: v0.3.128 2025-08-04 16:20:39 +00:00
Dax Raad eca7b8fd11 hack to return tool call errors back to model 2025-08-04 12:15:24 -04:00
opencode 927e6f121a release: v0.3.127 2025-08-04 16:06:13 +00:00
GitHub Action a2962a4f65 ignore: update download stats 2025-08-04 2025-08-04 12:04:34 +00:00
Aiden Cline 29bc4c07ae fix double help printing (#1580) 2025-08-04 05:03:27 -05:00
Frank cd7af235a2 fix docs 2025-08-04 00:23:02 -04:00
Dax Raad 48faf9d3ce wip: plugins 2025-08-03 21:43:58 -04:00
Dax Raad 4fc04bbac2 wip: plugin load from package 2025-08-03 21:19:03 -04:00
Dax Raad 759e948e2b Add workflow_dispatch trigger to typecheck workflow 2025-08-03 17:16:20 -04:00
Aiden Cline 3f55f62607 fix: patch marked-shiki (#1569) 2025-08-03 16:13:35 -05:00
Dax Raad 624a7d038a Add typecheck workflow 2025-08-03 17:12:23 -04:00
Dax Raad fa03e06c2d ignore: cleanup 2025-08-03 17:09:30 -04:00
Yordis Prieto ab3d6c9797 chore: add ts-expected-error (#1575) 2025-08-03 17:09:19 -04:00
Yordis Prieto ff427fd1fe chore: remove unnecessary TypeScript error suppression (#1571) 2025-08-03 15:50:08 -04:00
opencode 2db4143286 release: v0.3.126 2025-08-03 19:45:14 +00:00
Dax Raad 75f7fa8cf4 ci: ignore 2025-08-03 15:39:34 -04:00
Dax Raad 4693b55dba ci: ignore 2025-08-03 15:38:39 -04:00
Dax Raad d5dacdb63c fix bash tool not showing stderr 2025-08-03 15:34:52 -04:00
Aiden Cline bf1699617f tweak: filter out duplicate instructions (#1567) 2025-08-03 15:10:21 -04:00
Dax Raad e3781104c4 increase models.dev polling interval to hourly 2025-08-03 14:58:35 -04:00
Aiden Cline 24d063b7ae docs: fix mixed up documentation (#1564) 2025-08-03 13:01:09 -05:00
Dax Raad 2cadf5a093 fix bash tool getting stuck on interactive commands 2025-08-03 13:52:50 -04:00
opencode 3cfddc7a09 release: v0.3.123 2025-08-03 17:13:33 +00:00
Dax Raad 878c7ed67e ci: ignore 2025-08-03 13:12:35 -04:00
opencode 48188bf294 release: v0.3.122 2025-08-03 16:19:09 +00:00
Dax Raad 614501c1a2 more efficient snapshots in parallel toolcalls 2025-08-03 12:12:28 -04:00
Giuseppe Rota 09aa285ae5 fix(docs): move disabled providers paragraph to its proper section (#1547) 2025-08-03 11:28:57 -04:00
Dax Raad b926d0c010 more fixes for shell 128 error 2025-08-03 11:25:58 -04:00
opencode 9436c7f774 release: v0.3.120 2025-08-03 14:59:03 +00:00
Dax Raad 1eca9531ea ci: ignore 2025-08-03 10:52:35 -04:00
Dax Raad 1fc92d91c0 ci: ignore 2025-08-03 10:42:52 -04:00
Dax Raad dfcff892a1 fix shell error 128 2025-08-03 10:30:23 -04:00
GitHub Action 2d68127b07 ignore: update download stats 2025-08-03 2025-08-03 12:04:04 +00:00
Aiden Cline e159922c06 fix: expand tilde for file: references (#1553) 2025-08-03 06:15:06 -05:00
Dax Raad 298a444df0 ci: ignore 2025-08-03 03:54:17 -04:00
Dax Raad 7079098206 ci: bun cache 2025-08-03 03:53:31 -04:00
Dax Raad e73e51597d ci: ignore 2025-08-03 03:45:05 -04:00
Dax Raad 461e5469e9 ci: ignore 2025-08-03 03:37:53 -04:00
Dax Raad 54f81220df ci: ignore 2025-08-03 03:35:17 -04:00
The Pangolier acd601220a Share link hotfix (#1513) 2025-08-03 03:02:24 -04:00
Yordis Prieto 0c86e73e95 remove: delete extension test file (#1554) 2025-08-03 02:58:10 -04:00
Frank 2cad480fed sync 2025-08-02 21:28:03 -04:00
Frank 77ab87abeb Add Zhipu AI provider 2025-08-02 21:20:44 -04:00
Dax Raad 765c149bba ci: ignore 2025-08-02 18:56:34 -04:00
Dax Raad d3ca086b59 wip: plugins 2025-08-02 18:50:19 -04:00
Aiden Cline 62d67208e3 tweak: make gh action ignore url mentions of opencode (#1531) 2025-08-02 09:31:23 -05:00
Dominik Engelhardt 6b58ab0b47 Choose model according to the docs (#1536) 2025-08-02 09:29:03 -05:00
Yihui Khuu 128bafa095 fix(tui): attachment highlighting issues in messages (#1534) 2025-08-02 09:26:44 -05:00
Yihui Khuu 680e611a10 fix(tui): attachment source is not stored when using message from message history (#1542) 2025-08-02 09:23:32 -05:00
Aiden Cline 587ab4f16a ignore: fix dev branch (#1529) 2025-08-02 09:11:38 -05:00
GitHub Action e2636b39e4 ignore: update download stats 2025-08-02 2025-08-02 12:04:12 +00:00
Ricardo Gonzalez 55a73b3e2e feat(models): enable Kimi k2 ⇄ Claude trajectory handoff (#1525) 2025-08-01 23:05:06 -04:00
Aiden Cline eebd7ed5ff fix: include stderr in bash tool output (#1511) 2025-08-01 19:20:32 -05:00
Aiden Cline 63a7f04f65 better configuration error messages (#1517) 2025-08-01 19:10:32 -04:00
Jay V 766d7b9f7d docs: cerebras 2025-08-01 18:30:29 -04:00
opencode 508734438b release: v0.3.112 2025-08-01 21:53:33 +00:00
Dax Raad f75fc5af22 force models.dev refresh on auth login 2025-08-01 17:48:01 -04:00
Dax Raad cbd7db5a73 make top_p configurable 2025-08-01 17:03:33 -04:00
Dax Raad 36318f74f7 configurable lsp 2025-08-01 14:52:10 -04:00
Brinsil Elias 4bacee2db6 fix(docs): Fix formatting for Node.js installation section (#1497) 2025-08-01 14:15:38 -04:00
CodinCat e20ab5b7ee handle the optional v in upgrade command when using curl (#1500) 2025-08-01 14:15:22 -04:00
Yordis Prieto 12172203d7 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 b6fcc7047f allow search in provider select 2025-08-01 14:03:22 -04:00
Dax Raad feb43e8550 add version to user-agent 2025-08-01 12:18:09 -04:00
adamdotdevin 5aa811a6c5 fix: unshare command missing 2025-08-01 09:30:42 -05:00
Aiden Cline 2c61674f83 tweak: sanitize mcp server names (#831) 2025-08-01 09:11:40 -05:00
Aiden Cline b9c143096b fix: bash tool errors for chmod (#1502) 2025-08-01 09:10:09 -05:00
Aiden Cline 45d835e5e8 fix: {file:...} references weren't being parsed correctly in some cases (#1499) 2025-08-01 08:39:21 -05:00
adamdotdevin ce317ff90f ignore: include usage in local setup 2025-08-01 07:42:36 -05:00
GitHub Action 892ba79c44 ignore: update download stats 2025-08-01 2025-08-01 12:04:33 +00:00
adamdotdevin 3729f2647a chore: add local qwen3 to config 2025-08-01 06:27:08 -05:00
Timo Clasen 427cc1df23 docs(permissions): add wildcard example (#1494) 2025-08-01 05:24:32 -05:00
Dax Raad c64cd54d5d poll for models.dev changes 2025-07-31 23:47:42 -04:00
Dax Raad d944eed4d5 add user agent for models.dev request 2025-07-31 22:00:45 -04:00
Dax Raad 56f41c11a8 do not mark errored tool calls as aborted 2025-07-31 21:45:40 -04:00
Dax Raad 88842a633e better guarding against bash commands that go outside of cwd 2025-07-31 21:42:30 -04:00
Frank 3e19c2dc6c Update STATS.md 2025-07-31 21:42:05 -04:00
Dax Raad f4254751e6 improve wildcard matching for permissions 2025-07-31 20:40:05 -04:00
Dax Raad ebdc891c88 ci: ignore release commits 2025-07-31 19:57:07 -04:00
Dax Raad 0ae036bf22 fix escape button not canceling if retry in progress 2025-07-31 19:55:57 -04:00
Dax Raad 8294606576 docs: sync 2025-07-31 19:50:51 -04:00
Dax Raad a383f1a833 docs: formatters 2025-07-31 19:50:31 -04:00
Dax Raad 9da86a35cf ignore: config 2025-07-31 19:45:44 -04:00
opencode c2eee24590 Release v0.3.105 2025-07-31 23:41:27 +00:00
Dax Raad 918b7019c7 formatter config 2025-07-31 19:36:07 -04:00
Jay V adeb89b964 docs: edit premissions doc 2025-07-31 19:10:54 -04:00
opencode 8af1310831 Release v0.3.104 2025-07-31 23:02:36 +00:00
Dax Raad a64165b456 allow disabling formatter 2025-07-31 18:56:04 -04:00
Dax Raad 4c849f6a63 fix issue with some bash commands asking for permission 2025-07-31 18:35:51 -04:00
Dax Raad cea323b743 ignore: update opencode.json 2025-07-31 18:13:40 -04:00
Dax Raad 9b931f0498 docs: permissions 2025-07-31 18:11:34 -04:00
Dax Raad 69cb5756c0 docs: permissions 2025-07-31 18:11:34 -04:00
Frank 7391d0ff61 Remove hardcoded vscode extension theme
publish-vscode / publish (push) Has been cancelled
2025-07-31 17:53:18 -04:00
Jay V fd8287705c docs: add more providers 2025-07-31 17:47:24 -04:00
opencode dfe1deeb67 Release v0.3.102 2025-07-31 21:25:12 +00:00
Dax Raad 82c0dcc62d wip: permissions 2025-07-31 17:19:56 -04:00
Dax Raad c6a7ab6bd6 ignore: ts optimization 2025-07-31 16:54:15 -04:00
Dax Raad 701cd937a8 wip: permissions 2025-07-31 16:51:55 -04:00
Dax Raad f1e0e40868 wip: permissions 2025-07-31 16:38:37 -04:00
Aiden Cline 8788e221b8 fix: load global jsonc (#1479) 2025-07-31 15:02:28 -05:00
Aiden Cline 1d56302bb5 fix: session ordering (#1474) 2025-07-31 14:17:47 -05:00
Frank 85605f8d88 Add provider instruction for Azure OpenAI 2025-07-31 14:37:26 -04:00
Jay V f1c12a7257 docs: edit providers doc 2025-07-31 14:11:40 -04:00
opencode-agent[bot] 77e6d78d91 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 b18a96eb67 fix: permission state hangs 2025-07-31 11:36:08 -05:00
Dax Raad ca454c91ca wip: refactor permissions 2025-07-31 12:26:47 -04:00
adamdotdevin 22c1112903 feat: more scriptable tui (api) 2025-07-31 11:24:23 -05:00
neolooong 1d503b1536 fix(editor): handle UTF-8 characters properly in SetValueWithAttachments (#1469) 2025-07-31 10:45:43 -05:00
adamdotdevin 2f1302e1a2 fix: missing operationId 2025-07-31 10:19:42 -05:00
adamdotdevin 3c8930192f fix: test 2025-07-31 10:10:34 -05:00
adamdotdevin 1606b52fd7 fix: optional toolCallID 2025-07-31 10:09:44 -05:00
adamdotdevin a0f82ecef8 fix: test 2025-07-31 10:07:58 -05:00
adamdotdevin 9f429d8dd9 wip: tui permissions 2025-07-31 09:59:17 -05:00
opencode 6aa555c492 Release v0.3.101 2025-07-31 14:23:13 +00:00
Dax Raad 0501054320 ci: tweak 2025-07-31 10:09:43 -04:00
Dax Raad 82505c3582 ci: fix 2025-07-31 09:46:36 -04:00
Dax Raad 5382dc70cb ci: tweak 2025-07-31 09:39:44 -04:00
Dax Raad f85635f8dd ci: tweak 2025-07-31 09:35:57 -04:00
Dax Raad bc4274315c ci: reorder 2025-07-31 09:29:55 -04:00
GitHub Action 878a126d22 ignore: update download stats 2025-07-31 2025-07-31 12:04:25 +00:00
Aiden Cline d60e346cbb feat: jsonc configuration file support (#1434) 2025-07-31 06:25:26 -05:00
Aiden Cline 3f8f517081 fix: process revert cleanup before creating new messages (#1448) 2025-07-31 05:07:59 -05:00
Dax Raad d2b67b67dd ci: stainless 2025-07-31 01:42:52 -04:00
Dax Raad 31c6306a7d ci: tweak 2025-07-31 01:35:11 -04:00
Dax Raad ea409b3874 ci: tweak 2025-07-31 01:33:21 -04:00
Dax Raad e7aa83fa8e ci: scripts 2025-07-31 01:25:24 -04:00
Dax Raad 617e80c14f ci tweaks 2025-07-31 01:20:12 -04:00
Dax df4a6a74cd ci: new publish method (#1451) 2025-07-31 01:05:35 -04:00
Dax 083682c6a8 ci: new publish method (#1451) 2025-07-31 01:00:29 -04:00
Simon Westlin Green b67c367a40 Use responses API for Azure (#1428)
publish / publish (push) Has been cancelled
2025-07-30 23:22:59 -04:00
Robert Holden 1167e12f72 feat: mode directory markdown configuration loading (#1377) 2025-07-30 23:22:43 -04:00
Josh 30e5d32ed8 Add http-referer header for vercel ai gateway requests (#1403) 2025-07-30 23:22:24 -04:00
Yordis Prieto afb3edd234 docs: clarify Bun's default registry resolution in index.ts (#1438) 2025-07-30 23:21:07 -04:00
CodinCat 8f8bd188dd fix typo in bash.ts (#1444) 2025-07-30 23:20:48 -04:00
Dax 50c93a349f use treesitter to parse bash commands and catch commands that go outside of cwd (#1443) 2025-07-30 20:57:52 -04:00
Jay V 2f51f9d88d docs: edit 2025-07-30 19:11:36 -04:00
Jay V dcb54d465b docs: add new providers doc, reorg sidebar, edits 2025-07-30 18:16:11 -04:00
Yordis Prieto f74cd37818 fix: update glob pattern and path in tool test (#1436) 2025-07-30 15:42:13 -05:00
Yordis Prieto f2bac1532b fix: unit test assertion (#1435)
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
2025-07-30 15:13:37 -05:00
Dax Raad f181fbf3f0 lower max retries to 3 - ai sdk currently cannot abort during a retry delay so things appear to be frozen
publish / publish (push) Has been cancelled
2025-07-30 15:08:25 -04:00
Dax Raad 4a53957e9d release undo/redo 2025-07-30 13:09:18 -04:00
Aiden Cline a33870ea66 fix: attachment highlighting (#1427) 2025-07-30 11:43:34 -05:00
Aiden Cline d4e91d3d5f fix: prevent read tool from opening binary files and corrupting session (#1425) 2025-07-30 11:00:23 -05:00
GitHub Action 66891b148b ignore: update download stats 2025-07-30 2025-07-30 12:04:23 +00:00
Sam Huckaby 3bd652b22e fix: quick grammar and spelling check (#1402) 2025-07-30 05:54:47 -05:00
Aiden Cline 02d838c68e tweak: adjust astro css to render mixed nested lists (#1411) 2025-07-30 05:51:52 -05:00
municorn 8618f0d88b build: add @octokit/rest to opencode dependencies (#1396)
Co-authored-by: Frank <frank@sst.dev>
2025-07-29 22:33:25 -04:00
Dax Raad e9af4d7216 update beast prompt for openai models
publish / publish (push) Has been cancelled
2025-07-29 22:15:13 -04:00
Dax Raad 8528f471c7 experimental well-known auth support
publish / publish (push) Has been cancelled
2025-07-29 19:30:51 -04:00
Yordis Prieto 9790340493 chore: fix test to have deterministic testing (#1401) 2025-07-29 17:54:22 -05:00
Dax Raad c9b9c844db remove min/max in tool schemas
publish / publish (push) Has been cancelled
2025-07-29 17:39:47 -04:00
Frank f30b5c0ec4 convert share backend to hono app 2025-07-29 16:39:48 -04:00
adamdotdevin c3685d1efe feat: show git diff in reverted messages 2025-07-29 13:11:38 -05:00
Yordis Prieto f294293e2f fix: update file name extraction in uploads test to use __filename (#1395) 2025-07-29 12:28:44 -05:00
Yordis Prieto 83ebda87d2 fix: improve handling of global File object in uploads tests (#1394) 2025-07-29 11:30:39 -05:00
Yordis Prieto 1588863883 chore: fix null handling in multipartFormRequestOptions test (#1385) 2025-07-29 11:17:03 -05:00
Andrea Grandi 173da3caaf feat: show current git branch in status bar, and make it responsive (#1339)
Co-authored-by: adamdotdevin <2363879+adamdottv@users.noreply.github.com>
2025-07-29 11:15:04 -05:00
Dax Raad 5a98110ab0 strip todo tool instructions from non anthropic models 2025-07-29 11:56:53 -04:00
Jacob Hands 9e0edaa5d6 feat: add OPENCODE_CONFIG env var for specifying a custom config file (#1370) 2025-07-29 11:03:11 -04:00
GitHub Action 9e6f0de575 ignore: update download stats 2025-07-29 2025-07-29 12:04:40 +00:00
Aiden Cline d9278e4dcc fix: task type error (#1384) 2025-07-29 06:18:34 -05:00
Dax Raad 2160f0815c trim opencode title 2025-07-28 23:24:38 -04:00
Dax Raad 53ecda4ef1 wip: undo properly remove messages from UI 2025-07-28 22:58:31 -04:00
Dax Raad 4f62117758 wip: snapshot 2025-07-28 22:58:31 -04:00
Jay V 26335aeb01 docs: share page hide patch part 2025-07-28 20:04:00 -04:00
Jay V 1931d70e77 docs: adding 2025-07-28 20:00:30 -04:00
Jay V 3ae8e63ce5 Tweak auth cli copy 2025-07-28 20:00:30 -04:00
Dax Raad fa6fad2bac ignore: tweak
publish / publish (push) Has been cancelled
2025-07-28 12:20:37 -04:00
Dax Raad ff611ae792 fix phantom tool call failed messages and empty text parts with some models 2025-07-28 12:19:38 -04:00
GitHub Action 90d8c47c95 ignore: update download stats 2025-07-28 2025-07-28 12:04:21 +00:00
Frank 2b9ab27a6f vscode: add cmd+shift+esc keybinding
publish-vscode / publish (push) Has been cancelled
2025-07-27 15:54:45 -04:00
Frank 5a024afa87 vscode: bring oc terminal to front if already opened 2025-07-27 14:57:45 -04:00
Frank f56daab456 show opencode button in vscode when focused on terminal 2025-07-27 14:44:14 -04:00
Aiden Cline f9d45b7d3b fix: attachment bugs (#1335) 2025-07-27 12:21:31 -05:00
Frank adfcd1a9eb fix input format affected by installing vscode extension
publish / publish (push) Has been cancelled
2025-07-27 11:56:18 -04:00
Frank ebc7c8dfd6 simplify github action 2025-07-27 09:56:09 -04:00
GitHub Action 487275dce8 ignore: update download stats 2025-07-27 2025-07-27 12:04:15 +00:00
Aiden Cline 377a66d415 docs: document small_model cfg option (#1347) 2025-07-26 16:56:38 -05:00
Dax Raad 1cd12ca740 ci: fix opencode github 2025-07-26 10:02:31 -04:00
Aiden Cline aa30428e76 fix: strip thinking blocks from title (#1325) 2025-07-26 08:29:04 -05:00
GitHub Action 2ff7883f78 ignore: update download stats 2025-07-26 2025-07-26 12:03:59 +00:00
Frank 6692d1c5dc wip: github actions
publish / publish (push) Has been cancelled
2025-07-26 02:49:05 -04:00
Frank 6182b4a0c1 wip: github action 2025-07-26 01:03:23 -04:00
Frank 6b7cfde20a wip: github actions
publish / publish (push) Has been cancelled
2025-07-25 20:27:42 -04:00
Didier Durand 03e9ee7293 fixing various typos in text. (#1185) 2025-07-25 20:20:01 -04:00
Dax Raad 4fc32f7c9a better mcp support - should fix hanging when streamable http server is added
publish / publish (push) Has been cancelled
2025-07-25 19:19:47 -04:00
Frank 39e0ae70fe wip: github actions
publish / publish (push) Has been cancelled
2025-07-25 19:05:55 -04:00
Frank a67106b700 wip: github action
publish-github-action / publish (push) Has been cancelled
2025-07-25 18:33:45 -04:00
Dax Raad 5a3aed4e88 qwen optimizations it works good now 2025-07-25 18:31:08 -04:00
Frank a3bd92ee60 wip: github actions
publish-github-action / publish (push) Has been cancelled
publish / publish (push) Has been cancelled
2025-07-25 18:29:53 -04:00
Michael Hanson 7e3fe04b06 Fix a broken example in the MCP documentation and add more clarity (#1322) 2025-07-25 17:47:01 -04:00
Frank 35a914ad99 wip: github actions
publish / publish (push) Has been cancelled
2025-07-25 17:34:47 -04:00
Dax Raad 01198a5fdc wip: sync
publish / publish (push) Has been cancelled
2025-07-25 15:52:27 -04:00
Dax Raad a765b14ad5 wip: drop 2025-07-25 15:51:02 -04:00
Dax Raad 2767f4f9c3 handle uploaded text/plain 2025-07-25 15:48:42 -04:00
kehanzhang a155713a52 fix(headless): respect mode passed to /message endpoint (#1300) 2025-07-25 15:26:49 -04:00
Dax Raad d655601c47 docs: mode temperature 2025-07-25 13:45:04 -04:00
Dax Raad 4b94f390ce allow temperature to be configured per mode
publish / publish (push) Has been cancelled
2025-07-25 13:29:44 -04:00
Dominik Engelhardt 8e036cf17b fix: apply content-level caching for non-anthropic providers (#1305) 2025-07-25 12:19:44 -04:00
Yihui Khuu 0254c612ee feat: add csharp lsp (#1312) 2025-07-25 12:17:06 -04:00
Dax Raad 5febb0ff94 fix issue with trailing whitespace error in assistant message 2025-07-25 10:56:16 -04:00
Dax Raad 79edca063e fix reading model from config
publish / publish (push) Has been cancelled
2025-07-25 10:53:37 -04:00
Yihui Khuu 58c47c20a2 fix: header width to display header in one line when sharing disabled (#1310) 2025-07-25 09:32:06 -05:00
Dax Raad 4f6a568d80 fix double system prompt 2025-07-25 10:28:42 -04:00
Dax Raad 1823d20d26 disable todo tools by default in agent
publish / publish (push) Has been cancelled
2025-07-25 10:23:23 -04:00
Dax Raad 4ecfa69a07 fix symlinked agents
publish / publish (push) Has been cancelled
2025-07-25 10:20:16 -04:00
GitHub Action 34bdc43a7f ignore: update download stats 2025-07-25 2025-07-25 12:04:21 +00:00
Dax Raad 82e28e2717 agents better display when spawning
publish / publish (push) Has been cancelled
2025-07-24 23:08:03 -04:00
Dax Raad 31febec1c5 docs: agents
publish / publish (push) Has been cancelled
2025-07-24 22:18:49 -04:00
Dax Raad 149bfd5e36 docs: agents 2025-07-24 22:16:16 -04:00
Dax Raad 61dc0a7b3c docs: agents 2025-07-24 22:07:30 -04:00
Dax Raad 0de957a2f0 real life totally configurabl ai subasians
publish / publish (push) Has been cancelled
2025-07-24 21:21:02 -04:00
Frank 0caf100598 wip: github actions 2025-07-24 19:03:10 -04:00
Jay V 9f51d713ce docs: add to quick start 2025-07-24 18:57:54 -04:00
Filip c903c82704 Fix: added environment() to summarize() (#1290) 2025-07-24 18:24:54 -04:00
Dax Raad 621e7f8697 wip: handle deleting file 2025-07-24 17:49:23 -04:00
Dax Raad 877cbfc45b wip: fix type errors 2025-07-24 17:38:11 -04:00
Dax Raad 0d12a8c4d9 wip: more snapshot stuff 2025-07-24 17:38:11 -04:00
Aiden Cline 0463894731 (tui) tweak: add setting for scroll speed (#1288) 2025-07-24 16:34:59 -05:00
Clay Warren 9b94c9c57f feat: Replace unzip with @zip.js/zip.js for Windows compatibility (#662) 2025-07-24 16:49:04 -04:00
Filip 0ef0b3404b fix: add custom() to system prompt on summarize (#1289) 2025-07-24 16:48:17 -04:00
Filip 771c714b10 fix: edit tool (#1287) 2025-07-24 16:18:04 -04:00
Dax Raad 03cd495c20 ci: tweak
publish / publish (push) Has been cancelled
2025-07-24 15:55:45 -04:00
Dax Raad 85c4cd83bc ci: tweak
publish / publish (push) Has been cancelled
2025-07-24 15:51:33 -04:00
Dax Raad a480809965 includ baseline builds
publish / publish (push) Has been cancelled
2025-07-24 14:37:38 -04:00
Jesse van der Pluijm 5c1d6d3c8d Check if modelID includes "claude" for antropic/claude prompt caching (#1284) 2025-07-24 11:31:28 -04:00
Dax Raad 99f1c60633 disable snapshots
publish / publish (push) Has been cancelled
2025-07-24 11:08:20 -04:00
Aiden Cline cbffb6baad fix: slog error log serialization (#1276) 2025-07-24 07:19:00 -05:00
danielfyhr c1ce8564d2 add aura theme (#1280) 2025-07-24 07:17:27 -05:00
GitHub Action a6e69ae533 ignore: update download stats 2025-07-24 2025-07-24 12:04:18 +00:00
Dax Raad da84388d6c disable undo/redo for now 2025-07-23 21:02:13 -04:00
Rico Sta. Cruz cd758913cc fix: update max visible height in list tests (#1269) 2025-07-23 20:49:15 -04:00
Dax Raad 1d0162ebae fix header width 2025-07-23 20:48:35 -04:00
Dax Raad 0475fb5137 wip: always force create snapshot 2025-07-23 20:46:43 -04:00
Dax Raad cbf56d2cbd wip: fix redoing 2025-07-23 20:42:02 -04:00
Dax 11d236609d basic undo feature (#1268)
Co-authored-by: adamdotdevin <2363879+adamdottv@users.noreply.github.com>
Co-authored-by: Jay V <air@live.ca>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Andrew Joslin <andrew@ajoslin.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Tobias Walle <9933601+tobias-walle@users.noreply.github.com>
2025-07-23 20:30:46 -04:00
Yihui Khuu 9e80e6d855 feat: pass mode into task tool (#1248) 2025-07-23 20:29:59 -04:00
Aiden Cline 2e376833f6 docs: remove deprecated 'log_level' reference in docs (#1258) 2025-07-23 18:53:58 -04:00
Aiden Cline 4caae581b8 tweak: handle pasted attachment references (#1257) 2025-07-23 15:41:17 -05:00
Tobias Walle 1b848a00a5 fix "working" spinner animation (#1054) (#1259) 2025-07-23 15:40:34 -05:00
Dax Raad c2c65c796a remove providers path
publish / publish (push) Has been cancelled
2025-07-23 12:15:31 -04:00
Dax Raad 38b4d1e03d add debug paths command 2025-07-23 12:14:54 -04:00
GitHub Action 117d54e948 ignore: update download stats 2025-07-23 2025-07-23 12:04:18 +00:00
Aiden Cline 7f3c98aa1f tweak: ensure most recently interacted with session appears at the top (#1239) 2025-07-22 22:37:36 -05:00
Dax Raad 1acf196e98 config: improve config schema
publish / publish (push) Has been cancelled
2025-07-22 20:35:40 -04:00
Jay V 36abbe1d7b docs: edit ide 2025-07-22 19:02:30 -04:00
Andrew Joslin 7ac284c977 Allow multiline prompts for github agent (#1225) 2025-07-22 18:30:51 -04:00
Aiden Cline 774678077f feat(tui): support pipe into tui (#1230) 2025-07-22 17:19:20 -05:00
Jay V 7ed687975e docs: add tutorial closes #740 2025-07-22 17:54:53 -04:00
Frank a382ad9b1c wip: vscode extension 2025-07-22 17:13:58 -04:00
Dax Raad 4b52e5bd49 ci: error github api fail 2025-07-22 17:06:06 -04:00
Frank c6eed8c6fe wip: vscode extension 2025-07-22 16:50:56 -04:00
Frank 824da2a300 wip: vscode extension
publish / publish (push) Has been cancelled
2025-07-22 16:46:44 -04:00
Frank 06bee6d089 wip: vscode extension 2025-07-22 16:46:44 -04:00
Frank 76fb049963 wip: vscode extension 2025-07-22 16:46:44 -04:00
Dax Raad 0eebf55d07 ignore: lock changes 2025-07-22 15:49:36 -04:00
Dax Raad 09e8d33f5f wip: tui api 2025-07-22 15:49:24 -04:00
Frank 23aa869162 wip: vscode extension
publish-vscode / publish (push) Has been cancelled
2025-07-22 15:36:55 -04:00
Frank 64d43e06ec wip: vscode extension 2025-07-22 15:28:09 -04:00
Frank c7027ea017 wip: vscode extension 2025-07-22 13:21:49 -04:00
Aiden Cline 6eb987cbb9 tweak: cleanup cancelled markdown (#1222) 2025-07-22 12:08:03 -05:00
adamdotdevin 0b2d6c20ff chore: generate sdk into packages/sdk 2025-07-22 11:50:51 -05:00
Dax Raad f4ecb5e90b wip: append-prompt is better 2025-07-22 12:27:02 -04:00
Dax Raad 9dd85ea2c7 wip: tui api 2025-07-22 12:15:50 -04:00
Frank eca975848a wip: vscode extension 2025-07-22 11:21:29 -04:00
adamdotdevin a05547f189 fix(tui): more defensive attachment conversion
publish / publish (push) Has been cancelled
2025-07-22 09:28:13 -05:00
GitHub Action 51aba439ec ignore: update download stats 2025-07-22 2025-07-22 12:04:22 +00:00
Aiden Cline 7d6d060f7d fix: mouse text selection bug (#1206) 2025-07-21 19:15:36 -05:00
Dax Raad 26e93c4d11 wip: api for tui 2025-07-21 19:53:58 -04:00
Frank cfe5f3bbbc wip: vscode extension 2025-07-21 19:10:57 -04:00
Timo Clasen bb7aec2213 feat(tui): add ctrl+p and ctrl-n to history navigation (#1199) 2025-07-21 15:10:50 -05:00
Frank cf3006d133 wip: vscode extension
publish-vscode / publish (push) Has been cancelled
2025-07-21 15:48:46 -04:00
Dax Raad a6cd7c220a ci: tweaks 2025-07-21 15:45:44 -04:00
Dax Raad 4d6e7f818d fix broken attachments
publish / publish (push) Has been cancelled
2025-07-21 15:38:41 -04:00
Dax Raad adf56be05f fix title generation bug
publish / publish (push) Has been cancelled
2025-07-21 15:23:47 -04:00
Dax 531d035547 message queuing (#1200)
publish / publish (push) Has been cancelled
2025-07-21 15:14:54 -04:00
adamdotdevin 0aec866a8c fix(tui): up/down arrow handling
publish / publish (push) Has been cancelled
2025-07-21 10:44:21 -05:00
plyght fc18dbaa4c feat(tui): paste minimizing (#784)
Co-authored-by: adamdotdevin <2363879+adamdottv@users.noreply.github.com>
2025-07-21 10:31:29 -05:00
Dax Raad 6f15934a6b do not copy empty strings 2025-07-21 11:27:15 -04:00
Dax Raad d705b484c8 include newline between messages when copying
publish / publish (push) Has been cancelled
2025-07-21 11:22:51 -04:00
Dax Raad adcc58a2b0 add toast for copy
publish / publish (push) Has been cancelled
2025-07-21 11:19:54 -04:00
adamelmore 76a98df85f feat(tui): disable keybinds 2025-07-21 10:08:25 -05:00
adamelmore f356058fb4 docs: cleanup 2025-07-21 10:02:58 -05:00
adamelmore db24514d3e feat(tui): theme override with OPENCODE_THEME 2025-07-21 10:02:57 -05:00
adamdotdevin 4c5bbcf74e fix(tui): restore spinner ticks
publish / publish (push) Has been cancelled
2025-07-21 05:58:24 -05:00
adamdotdevin b6f723f092 feat(tui): message history select with up/down arrows
publish / publish (push) Has been cancelled
2025-07-21 05:52:11 -05:00
Aiden Cline e4f4915136 fix: pasting issue (#1182) 2025-07-21 04:09:16 -05:00
Aiden Cline f5180998dd fix: export cmd (#1184) 2025-07-21 04:08:26 -05:00
Dax 9b672c6b44 Update STATS.md 2025-07-20 20:36:23 -04:00
Dax Raad 22c807f5ee trimmed selection ui
publish / publish (push) Has been cancelled
2025-07-20 19:36:56 -04:00
Dax Raad bce6822bcc ignore: remove log 2025-07-20 19:08:19 -04:00
Dax Raad 004f6504e2 implement copy paste much wow can you believe we went this long without it so stupid i blame adam
publish / publish (push) Has been cancelled
2025-07-20 19:05:38 -04:00
Dax Raad 9278f64588 do not snapshot nongit projects for now
publish / publish (push) Has been cancelled
2025-07-20 13:59:30 -04:00
Frank 4bdb686e1b wip: vscode extension
publish-vscode / publish (push) Has been cancelled
2025-07-20 13:31:16 -04:00
Frank c32923723b wip: vscode extension
publish-github-action / publish (push) Has been cancelled
publish-vscode / publish (push) Has been cancelled
2025-07-20 13:27:37 -04:00
Frank 6c42dbd7be wip: vscode extension 2025-07-20 13:17:51 -04:00
Frank 3e7a0104a9 wip: github action 2025-07-20 13:14:14 -04:00
Frank 4f962812b4 wip: vscode extension 2025-07-20 13:13:18 -04:00
Frank 52779c5c0d wip: github action 2025-07-20 13:13:00 -04:00
Frank 5b44b34d67 wip: github action 2025-07-20 13:07:48 -04:00
Frank 01c51675e1 wip: github action 2025-07-20 12:41:02 -04:00
Frank fbda909a07 wip: github actions
publish-github-action / publish (push) Has been cancelled
2025-07-20 12:36:53 -04:00
Frank f25979853c wip: vscode extension 2025-07-20 12:00:09 -04:00
Dax Raad d4ad35748d wip: make api logger sort correctly 2025-07-20 11:54:56 -04:00
Frank 7ba59fbecf wip: vscode extension 2025-07-20 11:54:30 -04:00
Frank 063076fa5b wip: vscode extension 2025-07-20 11:47:18 -04:00
Frank f12dbbcfc1 wip: vscode extension 2025-07-20 11:45:35 -04:00
Frank 21b002f72f wip: vscode extension
publish-vscode / publish (push) Has been cancelled
publish / publish (push) Has been cancelled
2025-07-20 11:33:44 -04:00
GitHub Action 6618a0af43 ignore: update download stats 2025-07-20 2025-07-20 12:04:10 +00:00
adamdotdevin b9aea8f19f wip: file part source in server/api (optional) 2025-07-20 05:39:18 -05:00
adamdotdevin 1654a78cd8 chore: updated tui gitignore 2025-07-20 05:39:18 -05:00
adamdotdevin 667c638f00 chore: updated stainless script 2025-07-20 05:39:18 -05:00
b0tmtl c824f50bc7 fix(windows): resolve numlock and French keyboard input issues (#1165) 2025-07-20 05:28:15 -05:00
Dax Raad c1f56a162d fix bad layout on first render of chat history
publish / publish (push) Has been cancelled
2025-07-19 22:38:36 -04:00
Dax Raad 079526743f fix bash tool rendering
publish / publish (push) Has been cancelled
2025-07-19 22:25:15 -04:00
Aiden Cline 07d771ba64 fix: title generation for certain providers (#1159) 2025-07-19 20:01:55 -05:00
Aiden Cline 977f474038 fix: adjust editor parsing to handle flags like --wait (#1160) 2025-07-19 20:01:25 -05:00
Dax Raad 3b8f2bd4e5 ci: ignore
publish / publish (push) Has been cancelled
2025-07-19 19:30:12 -04:00
Dax Raad b562f88cd8 ci: ignore
publish / publish (push) Has been cancelled
2025-07-19 19:29:05 -04:00
Dax Raad 3f0f80297f Revert "fix: prevent sparse spacing in hyphenated words (#1102)"
publish / publish (push) Has been cancelled
This reverts commit d01fd0a344.
2025-07-19 19:25:15 -04:00
Timo Clasen 9f7246e08b fix(hooks): prevent session_complete hook from firing on subagent sessions (#1149)
publish / publish (push) Has been cancelled
2025-07-19 18:20:07 -05:00
Dax Raad 73554b6909 fix first run github copilot
publish / publish (push) Has been cancelled
2025-07-19 19:19:38 -04:00
Dax Raad fda02482c7 ci: ignore
publish / publish (push) Has been cancelled
2025-07-19 19:00:17 -04:00
Dax Raad b10f0868d6 ci: ignore
publish / publish (push) Has been cancelled
2025-07-19 18:54:46 -04:00
Dax Raad 23d1f9380a ignore: ci
publish / publish (push) Has been cancelled
2025-07-19 18:48:43 -04:00
Dax Raad 64c9908db4 perf: more performance improvements
publish / publish (push) Has been cancelled
2025-07-19 18:41:21 -04:00
Dax Raad 85bbcce7db perf: scroll to bottom in thread
publish / publish (push) Has been cancelled
2025-07-19 17:55:01 -04:00
Dax Raad b236673fa4 disable todo tools when running as task
publish / publish (push) Has been cancelled
2025-07-19 15:54:11 -04:00
Dax Raad 44c91d8c55 ignore: create memo abstraction
publish / publish (push) Has been cancelled
2025-07-19 15:26:26 -04:00
Dax Raad f5f0f57098 fix openrouter caching
publish / publish (push) Has been cancelled
2025-07-19 15:11:21 -04:00
Dax Raad 86541ff922 shitty hack for terrible charm bubbletea performance
publish / publish (push) Has been cancelled
2025-07-19 15:00:11 -04:00
Dax Raad 9b65da318d remove log level from config 2025-07-19 13:37:02 -04:00
Aiden Cline 4e3c46b941 fix: bullet display (#1148) 2025-07-19 12:36:50 -05:00
Aiden Cline b6b4c4421a fix: escape ansi sequences (#1139) 2025-07-19 12:02:24 -05:00
Dax Raad c8a7c38f13 docs: fix typo 2025-07-19 12:45:33 -04:00
Yihui Khuu c26b67e045 feat(tui): collapse session header into single line when sharing is disabled (#1145) 2025-07-19 11:43:04 -05:00
Dax Raad c7ae049c93 pass through model.options properly without having to nest it under provider name. you may have to update your configs see https://opencode.ai/docs/models/#openrouter for an example
publish / publish (push) Has been cancelled
2025-07-19 12:41:58 -04:00
Tom d01fd0a344 fix: prevent sparse spacing in hyphenated words (#1102) 2025-07-19 09:28:40 -05:00
Dax Raad 3f68f03881 vercel ai gateway
publish / publish (push) Has been cancelled
2025-07-19 10:08:36 -04:00
GitHub Action eb04879cd5 ignore: update download stats 2025-07-19 2025-07-19 12:04:11 +00:00
CodinCat a1436fa8f9 fix table heading rendering (#1138) 2025-07-18 20:17:22 -05:00
Dax Raad 335890af8b ci: rollback install script 2025-07-18 18:57:58 -04:00
Michael Hanson 3a2803d2b2 docs: Clarify MCP config instructions (#1026)
publish / publish (push) Has been cancelled
2025-07-18 16:04:29 -04:00
Rami Chowdhury 549e66fc8b feat(provider): add Gemini tool schema sanitization (#1132) 2025-07-18 16:02:54 -04:00
Aiden Cline 4fc069059c fix: func called before definition (#1134) 2025-07-18 15:00:32 -05:00
Timo Clasen aef44471dd feat(config): make small model configurable (#1030)
publish / publish (push) Has been cancelled
2025-07-18 14:16:50 -04:00
opencode-agent[bot] 31105a6ad7 Added install dir priority & user feedback (#1129)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: thdxr <thdxr@users.noreply.github.com>
2025-07-18 14:15:10 -04:00
adamdotdevin 3b74b92924 feat(tui): simpler layout, always stretched 2025-07-18 13:03:27 -05:00
Dax d8dd1f2929 wip: optional IDs in api (#1128)
publish / publish (push) Has been cancelled
2025-07-18 13:42:50 -04:00
Dax Raad f8e2ad06d9 message rendering performance improvements 2025-07-18 13:40:07 -04:00
GitHub Action 062ac8442c ignore: update download stats 2025-07-18 2025-07-18 12:04:28 +00:00
GitHub Action bf31641f64 ignore: update download stats 2025-07-18 2025-07-18 12:02:18 +00:00
Yihui Khuu 1c4b5526c9 fix: \{return} should be replaced with new line on all lines (#1119) 2025-07-18 06:22:36 -05:00
adamdotdevin d45c1a4980 fix: model cost overrides
publish / publish (push) Has been cancelled
2025-07-18 05:08:35 -05:00
Timo Clasen 41b9c1378d feat(tui): parse for file attachments when exiting EDITOR (#1117) 2025-07-18 04:47:20 -05:00
Aiden Cline 772324ac8e tweak: remove needless resorting (#1116) 2025-07-18 04:42:43 -05:00
Aiden Cline ec70d563cb tweak: fix [object Object] in logging (#1114) 2025-07-18 04:41:23 -05:00
Tom b3410e83ad fix(tui): ensure viewport scrolls to bottom on new messages (#1110) 2025-07-18 04:41:03 -05:00
Mike Wallio a556c3b774 Update to a customized beast mode v3 for opencode. (#1109) 2025-07-17 20:10:06 -05:00
Dax Raad 2ef832f926 only enable ruff if it seems to be used
publish / publish (push) Has been cancelled
2025-07-17 18:07:06 -04:00
Wendell Misiedjan d09f2f72e5 feat: support AWS_BEARER_TOKEN_BEDROCK for amazon bedrock provider autoloading (#1094)
publish / publish (push) Has been cancelled
2025-07-17 09:12:30 -05:00
Aiden Cline 5fe00d154e tweak: allow mcp servers to include headers (#1096) 2025-07-17 09:11:48 -05:00
GitHub Action 932e12b749 ignore: update download stats 2025-07-17 2025-07-17 14:07:13 +00:00
Alexander Drottsgård 33dc00b47c feat(tui): highlight current session in sessions modal (#1093) 2025-07-17 07:40:15 -05:00
Timo Clasen 693e540323 feat(tui): remove share commands from help if sharing is disabled (#1087) 2025-07-17 04:28:12 -05:00
Aiden Cline 69160619d7 fix: message error centering (#1085) 2025-07-17 04:27:40 -05:00
Timo Clasen 642cb27584 fix(tui): user defined ctrl+z should take precedence over suspending (#1088) 2025-07-17 04:27:02 -05:00
Timo Clasen 2ae3d05101 feat(tui): remove sharing info from session header when sharing is disabled (#1076) 2025-07-16 17:36:48 -05:00
Timo Clasen 0ec4035865 docs(share): add explicit manual share mode (#1074) 2025-07-16 16:08:25 -05:00
Aiden Cline 82c7339eda fix: type 'reasoning' was provided without its required following item (#1072) 2025-07-16 15:59:40 -05:00
Jay V 779f5a396d docs: edit 2025-07-16 16:59:12 -04:00
Jay V 1202f80d13 docs: update enterprise doc 2025-07-16 16:44:28 -04:00
Jay V 84b9e4d5a3 docs: config doc 2025-07-16 16:27:44 -04:00
Jay V dbc2c017d5 docs: edits 2025-07-16 16:20:09 -04:00
Mike Wallio d568f52f7f Remove binary file opencode (#1069) 2025-07-16 15:10:40 -05:00
Daniel Saldarriaga López 85a767c872 docs: fix keybinds documentation to match actual config schema (#867)
publish / publish (push) Has been cancelled
2025-07-16 15:34:52 -04:00
adamdotdevin 88915e2b0b fix(tui): expand edit calls 2025-07-16 14:33:57 -05:00
adamdotdevin 39f63ed5e0 fix(tui): slightly faster scroll speed 2025-07-16 14:26:46 -05:00
Dax Raad 6b9e111e94 only include severity 1 diagnostics from lsp in edit tool output
publish / publish (push) Has been cancelled
2025-07-16 15:25:37 -04:00
adamdotdevin e333bffb54 feat(tui): more ways to quit 2025-07-16 14:20:28 -05:00
Dax Raad ca52f94b7a docs: better variable examples 2025-07-16 14:56:24 -04:00
adamdotdevin 69efe04b03 docs: copying text 2025-07-16 13:26:26 -05:00
adamdotdevin 48b1b6e7d2 fix: missing dependency
publish / publish (push) Has been cancelled
2025-07-16 12:58:29 -05:00
adamdotdevin e47e9ad4ee fix: missing dependency 2025-07-16 12:56:11 -05:00
Aiden Cline 7ebfef5953 fix: generate title should use same options as model it uses to gen (#1064) 2025-07-16 12:46:52 -05:00
adamdotdevin 63befaffc1 feat(tui): layout config to render full width
publish / publish (push) Has been cancelled
2025-07-16 12:43:02 -05:00
Jay V b0f3ce8251 docs: rename workflow 2025-07-16 13:38:00 -04:00
Jay V 8d92e16d0d docs: discord releases 2025-07-16 13:17:04 -04:00
Aiden Cline e71d862c40 Fix: better title generation (needs to change due to small models) (#1059) 2025-07-16 11:47:56 -05:00
John Henry Rudden 81c478771b Fix: Add escape sequence for @ symbols to prevent send blocking (#1029) 2025-07-16 11:18:48 -05:00
Tom fe9f156dff fix: Prevent division by zero in context percentage calculation (#1055) 2025-07-16 09:35:20 -05:00
Jeremy Mack 8c1fa52068 fix: grep omitting text after a colon (#1053) 2025-07-16 09:09:05 -05:00
Adi Yeroslav ca4c92aafb feat(tui): shift+tab to cycle modes backward (#1049) 2025-07-16 07:43:48 -05:00
Nipuna Perera f5cc1f5fbf Enhance private npm registry support (#998) 2025-07-16 08:31:38 -04:00
GitHub Action 21af07df8f ignore: update download stats 2025-07-16 2025-07-16 12:04:30 +00:00
adamdotdevin fb9071b1f0 fix(tui): diagnostics rendering 2025-07-16 06:55:14 -05:00
adamdotdevin 0415c0be73 fix(tui): pending tool call width 2025-07-16 06:27:32 -05:00
adamdotdevin fbbb6ef407 fix(gh): build 2025-07-16 06:13:43 -05:00
adamdotdevin d87addc89f fix(tui): render attachments in user messages in accent color 2025-07-16 06:09:27 -05:00
adamdotdevin d795b494c3 chore: messages cleanup 2025-07-16 06:09:27 -05:00
adamdotdevin a270e1e4b9 fix(tui): faster cache algo 2025-07-16 06:09:27 -05:00
adamdotdevin 23a5b323ba chore(tui): simplify messages component, remove navigate, add copy last message 2025-07-16 06:09:26 -05:00
Robin Moser 9caeae6d51 fix: don't sign snapshot commits (#1046) 2025-07-16 04:46:32 -05:00
Matias Insaurralde 092c710810 perf: move ANSI regex compilations to package level (#1040)
Signed-off-by: Matías Insaurralde <matias@insaurral.de>
2025-07-16 04:20:25 -05:00
Frank 5261b2a010 wip: github actions 2025-07-16 16:20:06 +08:00
Frank 94a821066e sync 2025-07-16 16:12:31 +08:00
Frank 8b839e62a6 wip: github actions 2025-07-16 16:05:51 +08:00
Frank 56b2e3657a wip: github actions 2025-07-16 03:57:14 -04:00
Frank d09954390a wip: github actions 2025-07-16 03:54:20 -04:00
Frank c05fea78bf wip: github actions 2025-07-16 15:36:23 +08:00
Frank af439a2ea0 wip: github actions 2025-07-16 14:59:53 +08:00
Frank 20926ac764 wip: github action 2025-07-16 14:59:53 +08:00
Dax Raad a78f3065ee clean up export command
publish / publish (push) Has been cancelled
2025-07-15 21:50:43 -04:00
Dax Raad c7234a6189 fix share page v1 message 2025-07-15 21:35:32 -04:00
Dax Raad 470fc2d37c enable bash tool in plan mode 2025-07-15 21:28:03 -04:00
aron b788400de4 move spoof prompt to support anthropic with custom modes (#1031)
publish / publish (push) Has been cancelled
2025-07-15 21:16:27 -04:00
Stephen Murray 9e17f1ba40 add support for modified gemini-cli system prompt (#1033)
Co-authored-by: Dax Raad <d@ironbay.co>
2025-07-15 21:13:11 -04:00
Jay 90e3394cf7 docs: Update README.md 2025-07-15 20:09:26 -04:00
Jay V 59b4bb7285 docs: fix config 2025-07-15 19:54:51 -04:00
Jay V b9f958bfe3 docs: adding action to notify discord 2025-07-15 19:49:38 -04:00
Jay V 8628b5a8d9 docs: readme 2025-07-15 18:32:04 -04:00
Jay V cc21cd941f docs: share tweak copy button 2025-07-15 18:25:25 -04:00
John Henry Rudden 85565f58ee docs: share add copy button to messages in web interface (#902)
Co-authored-by: Jay <air@live.ca>
2025-07-15 17:56:33 -04:00
Jay V 00722c593a docs: share handle non bundled langs 2025-07-15 17:47:22 -04:00
Jay V db379a7ddb docs: share fix diff bugs 2025-07-15 17:47:22 -04:00
Jay V 25aad8cd09 docs: share page task tool 2025-07-15 17:47:22 -04:00
Dax Raad 5d4e7f3393 docs: document base URL 2025-07-15 14:57:50 -04:00
Joe Schmitt cd99781e2c feat(tui): add /export command to export conversation to editor (#989)
Co-authored-by: opencode <noreply@opencode.ai>
2025-07-15 13:53:21 -05:00
Dax Raad 7d7ed19879 add small model for title generation
publish / publish (push) Has been cancelled
2025-07-15 14:00:52 -04:00
adamdotdevin 0ae563491b fix(tui): wider max width
publish / publish (push) Has been cancelled
2025-07-15 12:44:41 -05:00
adamdotdevin 2a87b01e00 fix(tui): mouse scroll ansi parsing and perf 2025-07-15 12:03:30 -05:00
Aiden Cline 5cd944b7a8 tweak: (opencode run): adjust tool call rendering, reduce number of "Unknowns" (#1012) 2025-07-15 11:22:57 -05:00
Timo Clasen 4fee633821 feat(tui): add keymap to remove entries from recently used models (#1019) 2025-07-15 11:20:56 -05:00
adamdotdevin 311161438c chore: update stainless defs 2025-07-15 10:03:11 -05:00
adamdotdevin 2104e709e4 chore: update stainless script to kick off prod build 2025-07-15 08:15:31 -05:00
adamdotdevin 39ea702db2 fix(tui): rework lists and search dialog 2025-07-15 08:07:26 -05:00
Dax Raad 44e2b7f4b6 fix logic for suprpessing snapshots in big directories
publish / publish (push) Has been cancelled
2025-07-15 09:07:04 -04:00
Dax Raad c6e01adde4 upgrade ai sdk 2025-07-15 09:06:35 -04:00
GitHub Action 39a477a8c4 ignore: update download stats 2025-07-15 2025-07-15 12:04:28 +00:00
Joohoon Cha 205d451375 fix(tui): close completion dialog on ctrl+h (#1005) 2025-07-15 06:24:05 -05:00
Aiden Cline 08f6d35e69 Fix: failed to open session (#999) 2025-07-15 05:40:29 -05:00
Frank 92f706fe56 wip: github actions 2025-07-15 17:45:16 +08:00
Dax Raad ba498370e9 ignore: fix types 2025-07-15 00:56:03 -04:00
Dax Raad fc9c56d857 fix panic
publish / publish (push) Has been cancelled
2025-07-15 00:35:02 -04:00
Dax Raad f72567716f bring back task tool
publish / publish (push) Has been cancelled
2025-07-15 00:05:54 -04:00
Jase Kraft cd8946d0a6 fix: --continue pull the latest session id consistently (#918)
Co-authored-by: Dax Raad <d@ironbay.co>
2025-07-14 20:32:00 -04:00
Dax Raad c902fee054 docs: fix agents.md 2025-07-14 20:23:05 -04:00
Ryan Roden-Corrent 3f66b8efa1 Add support for job-control suspend (ctrl+z/SIGSTP). (#944) 2025-07-14 20:13:46 -04:00
CodinCat 5dd6cd290e improve error handling and logging for GitHub API failures in upgrade and install script (#972) 2025-07-14 20:13:12 -04:00
Dax Raad cee5d3fcf2 docs: README 2025-07-14 20:10:43 -04:00
Dax Raad bc360ab9b0 docs: README 2025-07-14 20:06:15 -04:00
Mike Wallio 1f474435ae Standardize parameter description references in Edit and MultiEdit tools (#984) 2025-07-14 20:03:59 -04:00
Dax Raad 4f6cd379ce support anthropic console login flow 2025-07-14 18:07:55 -04:00
Timo Clasen c79eccad7d docs: fix typo (#982) 2025-07-14 16:40:16 -04:00
Dax Raad f1be27eafd wip: snapshot 2025-07-14 15:29:08 -04:00
adamdotdevin e16805ef0b fix(tui): support readline nav in new search component
publish / publish (push) Has been cancelled
2025-07-14 12:20:58 -05:00
adamdotdevin d55a872b25 chore: use new search component in find dialog 2025-07-14 12:15:47 -05:00
adamdotdevin d661e2c5cc feat(tui): even better model selector 2025-07-14 12:15:46 -05:00
Dax Raad 7a6d9be335 ignore: share page fix 2025-07-14 13:13:33 -04:00
Dorian Karter aae9b5c524 feat(tui): add support for readline list nav (ctrl-p/ctrl-n) (#955) 2025-07-14 10:21:09 -05:00
Dax Raad 8823b8824d wip: update sdk
publish / publish (push) Has been cancelled
2025-07-14 11:18:08 -04:00
opencode-agent[bot] fbac0e65fd Fix issue: Option to update username shown in conversations. (#975)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: thdxr <thdxr@users.noreply.github.com>
2025-07-14 11:03:04 -04:00
Dax Raad 5d718fbcbd fix init command and escape to cancel
publish / publish (push) Has been cancelled
2025-07-14 10:48:17 -04:00
adamdotdevin 3a041244e6 fix(tui): build and bg color 2025-07-14 09:14:02 -05:00
adamdotdevin b30a9de8a5 fix(tui): find dialog bg color 2025-07-14 09:09:55 -05:00
Nicholas Hamilton 436460b1ac feat: ability to create new session from session dialog (#920) 2025-07-14 09:04:43 -05:00
Almir Sarajčić d54f62abcb Fix failing git hooks (#966) 2025-07-14 07:52:29 -05:00
Munawwar Firoz 496c589b02 feat: ctrl+left arrow / ctrl+right arrow key support (#969) 2025-07-14 07:16:06 -05:00
Almir Sarajčić 92a85d42ef Explain usage of external references in AGENTS.md (#965) 2025-07-14 07:06:37 -05:00
GitHub Action 4a7a45bf60 ignore: update download stats 2025-07-14 2025-07-14 12:04:16 +00:00
Aiden Cline e1ebd652bf fix: file command visual bug (#959) 2025-07-14 07:03:02 -05:00
Daniel Nouri 01f75e66a7 Fix clipboard on Wayland systems (#941)
Co-authored-by: Daniel Nouri <daniel@redhotcar>
2025-07-14 06:57:45 -05:00
Dax Raad cc32b499ab get rid of cli markdown dep
publish / publish (push) Has been cancelled
2025-07-13 23:06:31 -04:00
Dax Raad fd57a04d9c ci
publish / publish (push) Has been cancelled
2025-07-13 22:58:33 -04:00
Dax Raad eb9cb012a2 fix missing tokens
publish / publish (push) Has been cancelled
2025-07-13 22:56:29 -04:00
Dax Raad 61665b6565 fix type error 2025-07-13 17:25:13 -04:00
Dax 4c802ae916 Part data model (#950) 2025-07-13 17:22:11 -04:00
opencode-agent[bot] c69f6fe76f Added sharing config with auto/disabled options (#951)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: thdxr <thdxr@users.noreply.github.com>
2025-07-13 16:43:58 -04:00
Dax Raad d574de1edf ci: github action 2025-07-13 16:22:58 -04:00
Dax Raad 87943c97c8 wip: mcp 2025-07-13 16:22:16 -04:00
John Henry Rudden 4e5419ec80 fix: deduplicate command suggestions (#934) 2025-07-13 14:47:26 -05:00
Aiden Cline b000555522 Fix: title generation (#949) 2025-07-13 14:46:36 -05:00
Frank 9dc1bdfa09 wip: github actions 2025-07-13 23:59:25 +08:00
GitHub Action 6e2352fed0 ignore: update download stats 2025-07-13 2025-07-13 12:04:11 +00:00
Dax Raad 27b59c58b7 fix packages being reinstalled on every start
publish / publish (push) Has been cancelled
2025-07-12 12:41:12 -04:00
Muzammil Khan 63f3821a13 feat: add more ignore patterns to the ls tool (#913) 2025-07-12 12:06:58 -04:00
GitHub Action 4f7ddb3582 ignore: update download stats 2025-07-12 2025-07-12 12:03:59 +00:00
Dax Raad 94e522d9d3 fix share page timestamps 2025-07-11 21:49:20 -04:00
Dax Raad 67cec833e0 share page hide step-finish events 2025-07-11 21:45:56 -04:00
Dax Raad c61e6f0f0b fix issue preventing things from working when node_modules or package.json present in ~/
publish / publish (push) Has been cancelled
2025-07-11 21:09:39 -04:00
Dax Raad a799fdb96f switch global config to ~/.config/opencode/opencode.json
publish / publish (push) Has been cancelled
2025-07-11 20:51:23 -04:00
Dax Raad 15604eedd9 docs: add model docs 2025-07-11 20:33:06 -04:00
Aiden Cline bea6c97468 tweak: plan interactions should match web (TUI) (#895) 2025-07-11 18:03:22 -04:00
zWing c74a8a566d fix: avoid overwriting the provider.option.baseURL (#880) 2025-07-11 18:01:28 -04:00
Dax Raad 0cd4bb22d0 introduce cache version concept for auto cleanup when breaking cache changes happen
publish / publish (push) Has been cancelled
2025-07-11 17:50:49 -04:00
Jay a3dbb0bc35 docs: Update troubleshooting.mdx 2025-07-11 17:50:25 -04:00
Dax Raad f4bd561dfd wip: stats 2025-07-11 17:37:41 -04:00
Jay V 584a7ddbf9 docs: share page add time footer back
publish / publish (push) Has been cancelled
2025-07-11 14:24:20 -04:00
Jay V 45e5b7cd82 docs: share page mobile bugs 2025-07-11 14:24:20 -04:00
Jay V e90c53711f docs: share fix mobile diffs 2025-07-11 14:24:20 -04:00
Sergii Kozak cc5c2aa8b1 fix(session): add fallback for undefined output token limit (#860)
Co-authored-by: opencode <noreply@opencode.ai>
2025-07-11 10:55:13 -04:00
alexz ac06a9d6e5 fix: ENAMETOOLONG: name too long when adding custom mode (#881) 2025-07-11 10:54:52 -04:00
GitHub Action 1b00b49947 ignore: update download stats 2025-07-11 2025-07-11 12:04:08 +00:00
adamdotdevin 2f80f91f12 fix(tui): help commands bg color 2025-07-11 06:03:21 -05:00
adamdotdevin 31e475e604 fix(tui): don't include /mode trigger 2025-07-11 06:01:51 -05:00
adamdotdevin cac163bf6e fix(tui): actually fix mouse ansi codes leaking 2025-07-11 06:00:20 -05:00
Aiden Cline 0ebd5d6e9e fix: avoid worker exception, graceful 404 (#869) 2025-07-11 04:55:56 -05:00
Frank 1336a5cbd7 wip: github actions 2025-07-11 06:55:13 +08:00
Frank 1db31df227 wip: github actions 2025-07-11 05:23:24 +08:00
Jay V fee8ffa819 docs: share page fix terminal part 2025-07-10 17:21:21 -04:00
Frank 9784f14d89 Api: add endpoint for getting github app token 2025-07-11 05:01:27 +08:00
adamdotdevin 71507928a4 fix(tui): handle SIGTERM, closes #319
publish / publish (push) Has been cancelled
2025-07-10 15:59:03 -05:00
Timo Clasen 4172c25864 fix(tui): center help dilaog (#853) 2025-07-10 15:56:19 -05:00
adamdotdevin 94ea176b93 fix(tui): mouse wheel ansi codes leaking into editor 2025-07-10 15:49:58 -05:00
Jay V c9f3e18736 docs: fix diag styles 2025-07-10 16:38:51 -04:00
Jay V 059781e896 docs: share page attachment 2025-07-10 16:38:51 -04:00
Dax Raad 93eda31be4 add step finish part
publish / publish (push) Has been cancelled
2025-07-10 16:25:38 -04:00
Dax Raad 0479cab022 run: properly close session.list 2025-07-10 16:13:01 -04:00
Dax Raad dc7b9cd0a8 disable filewatcher, fixes file descriptor leak
publish / publish (push) Has been cancelled
2025-07-10 15:58:45 -04:00
Dax Raad d06dd4bfbc config: escape file: string content
publish / publish (push) Has been cancelled
2025-07-10 15:38:58 -04:00
Dax Raad ab3233ca00 ci: slow down stats 2025-07-10 15:31:06 -04:00
adamdotdevin 7528f1e762 docs: clipboard requirements on linux 2025-07-10 13:12:37 -05:00
adamdotdevin 33f10685cb feat(tui): accent editor border on leader key 2025-07-10 12:57:22 -05:00
Jay V d2cd57fb50 docs: edit local models 2025-07-10 13:49:24 -04:00
Jay V 92539d1d55 docs: edit mode 2025-07-10 13:29:37 -04:00
Gabriel Garrett 09f86335ff Adds real example in docs of how to configure custom provider (#840) 2025-07-10 13:29:30 -04:00
Aiden Cline 2bb843cafa fix: encode & decode file paths (#843) 2025-07-10 11:19:54 -05:00
Jay V 6365fc12a1 docs: add modes to sidebar 2025-07-10 12:07:44 -04:00
Jay V 17b7f47592 docs: add modes to sidebar 2025-07-10 12:07:18 -04:00
Dax Raad a34e7c5529 docs: modes
publish / publish (push) Has been cancelled
2025-07-10 11:53:28 -04:00
Dax Raad 4c076e7451 docs: add config variable docs 2025-07-10 11:48:55 -04:00
Dax Raad 38bf2e962d support env and file pointers in config 2025-07-10 11:45:31 -04:00
Dax Raad 42d9f342e6 opencode run respects mode 2025-07-10 11:28:28 -04:00
Dax Raad 4b083bc2ae wip: scrap 2025-07-10 11:25:37 -04:00
Dax Raad 0e42976355 set max output token limit to 32_000 2025-07-10 11:25:37 -04:00
adamdottv 4679dc2c28 feat: --mode flag passed to tui 2025-07-10 10:19:25 -05:00
adamdottv cd785e4adc feat: mode flag in cli run command 2025-07-10 10:13:15 -05:00
adamdottv 65cca91e5f feat(tui): add token and cost info to session header 2025-07-10 10:06:51 -05:00
adamdottv bc4f2faf6d feat(tui): modes 2025-07-10 10:06:51 -05:00
Dax Raad b39c0cc191 make LSP lazy again
publish / publish (push) Has been cancelled
2025-07-10 09:37:40 -04:00
adamdottv 24f9b3a455 fix(tui): fouc in textarea on app load 2025-07-10 08:20:17 -05:00
adamdottv 13ab902b5b fix(tui): textarea cursor sync issues with attachments 2025-07-10 07:49:36 -05:00
adamdottv 5dd6b16eb9 fix(tui): textarea issues 2025-07-10 07:38:57 -05:00
GitHub Action 32def4ba6a ignore: update download stats 2025-07-10 2025-07-10 12:04:15 +00:00
Dax Raad 65f4c3188b temporary grok 4 patch
publish / publish (push) Has been cancelled
2025-07-10 07:57:55 -04:00
Dax Raad 39b65d92e1 disable lsp on non-git folders 2025-07-10 07:39:02 -04:00
Dax Raad 254a17c8e3 fix session is busy error
publish / publish (push) Has been cancelled
2025-07-10 07:27:03 -04:00
Josh Medeski ebca03f1b4 feat(tui): subsitute cwd home path on status bar (#808) 2025-07-10 06:12:19 -05:00
Adi Yeroslav d405f87b9d feat(tui): /editor - change the auto-send behavior to put content in input box instead (#827) 2025-07-10 05:57:52 -05:00
Timo Clasen 23956f4671 feat(tui): hide cost if using subscription model (#828) 2025-07-10 05:56:36 -05:00
adamdottv fb4da89f9a feat(tui): @symbol attachments 2025-07-10 05:53:00 -05:00
Dax Raad 10721053ba respect go.work when spawning LSP
publish / publish (push) Has been cancelled
2025-07-09 22:54:47 -04:00
Dax Raad e179ade5d8 better detection of prettier formatter
publish / publish (push) Has been cancelled
2025-07-09 22:37:31 -04:00
Dax Raad 1ba82bc7cb do not wait for LSP to be fully ready
publish / publish (push) Has been cancelled
2025-07-09 21:59:38 -04:00
Dax Raad 01f89a477e modes concept 2025-07-09 21:59:38 -04:00
Jay V cf21a32712 docs: share doc edit 2025-07-09 20:26:31 -04:00
Jay V f8154d5b2f docs: share doc 2025-07-09 20:24:09 -04:00
Jay V 885c4d6d81 docs: edit troubleshooting 2025-07-09 19:55:14 -04:00
Timo Clasen 9d5b4bac37 feat(tui): treat pasted text file paths as file references (#809) 2025-07-09 18:37:39 -05:00
Jay V 3321a6b3da docs: enterprise doc 2025-07-09 15:46:57 -04:00
Jay V 0c57b3844d docs: share page last part fix 2025-07-09 15:46:57 -04:00
Mike Wallio 85ff74a05c Add gpt-4.1 beast prompt (#778)
publish / publish (push) Has been cancelled
Co-authored-by: Dax Raad <d@ironbay.co>
2025-07-09 12:11:54 -04:00
Frank Denis b94fb4c4d0 LSP: fix SimpleRoots to actually search in the root directory (#795) 2025-07-09 10:35:06 -05:00
Dax Raad 37b894dbe6 ci: remove checked in config.schema.json 2025-07-09 11:30:42 -04:00
Dax Raad 25ab1cfb8a ci: generate config schema as part of build 2025-07-09 11:25:58 -04:00
adamdottv 637ddcc177 chore: troubleshooting docs 2025-07-09 10:12:36 -05:00
Dax Raad 049c07ea9f lsp: spawn only a single tsserver in project root
publish / publish (push) Has been cancelled
2025-07-09 11:06:44 -04:00
adamdottv 632d014220 feat: discord redirect 2025-07-09 10:01:42 -05:00
adamdottv 79e8a64349 feat: configurable log levels 2025-07-09 10:00:03 -05:00
adamdottv c5610af3a2 feat(tui): move logging to server logs 2025-07-09 08:16:10 -05:00
adamdottv 3ee6a90551 fix(tui): don't panic on missing linux clipboard tool 2025-07-09 06:51:58 -05:00
adamdottv 3ee6eb5252 fix(tui): markdown wrapping off sometimes 2025-07-09 06:41:53 -05:00
adamdottv 2fd6c2adeb chore: internal clipboard package 2025-07-09 04:55:24 -05:00
Aiden Cline 76c292442e tweak: keep completion options open when trigger is still present (#789) 2025-07-09 04:42:31 -05:00
adamdottv ef6b98fa13 fix(tui): clear command priority
publish / publish (push) Has been cancelled
2025-07-08 19:26:50 -05:00
Dax Raad 36b5772e30 ci: ignore chore commits 2025-07-08 20:05:33 -04:00
Dax Raad 850655e8d4 wip: symbols endpoint 2025-07-08 20:05:33 -04:00
Yihui Khuu d5158cdf14 Add debounce before exit when using non-leader exit command (#759) 2025-07-08 18:53:38 -05:00
Dax Raad 983f3de849 wip: remove excess import
publish / publish (push) Has been cancelled
2025-07-08 19:51:09 -04:00
adamdottv 2c3558df56 chore: vendor clipboard into go package 2025-07-08 18:48:40 -05:00
adamdottv 86d8e09307 chore: remove clipboard temp 2025-07-08 18:47:59 -05:00
adamdottv bfef7b322c chore: vendor clipboard into go package 2025-07-08 18:46:42 -05:00
Aiden Cline d3369711ca fix: documentation typo (#781) 2025-07-08 18:30:46 -05:00
adamdottv f640a3a27c fix(tui): panic when reading/writing clipboard on linux 2025-07-08 18:29:45 -05:00
Jay V 7e9b6a64da docs: share page image 2025-07-08 19:24:21 -04:00
Jay V e9d5f43f7f docs: share page many model case 2025-07-08 19:08:33 -04:00
Dax Raad 646d986dfc add typescript lsp timeout if it fails to start
publish / publish (push) Has been cancelled
2025-07-08 18:33:12 -04:00
Dax Raad 57cd48c53e properly load typescript lsp in subpaths
publish / publish (push) Has been cancelled
2025-07-08 18:18:45 -04:00
Dax Raad 31974e1b6e big rework of LSP system 2025-07-08 18:14:49 -04:00
Jay V 34ff2aaded docs: share bugs 2025-07-08 18:14:36 -04:00
Jay V 2b0a6b7a49 docs: share error part 2025-07-08 18:00:08 -04:00
Jay V 3b836b5464 docs: share page bugs 2025-07-08 17:18:38 -04:00
Jay V 489f6dcc49 docs: share image model 2025-07-08 17:18:38 -04:00
Timo Clasen 4c88ecacab fix: make compact command interruptible (#691)
publish / publish (push) Has been cancelled
Co-authored-by: GitHub Action <action@github.com>
2025-07-08 15:37:25 -05:00
strager 2ebf959895 fix(tool): fix ripgrep invocation on Windows (#700)
Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com>
2025-07-08 15:36:26 -05:00
Rami Chowdhury 8e438cc61f feat(storage): ensure storage directory exists and handle paths correctly (#771) 2025-07-08 15:34:11 -05:00
Robb Currall 061c8aa3e9 fix: support cancelled task state (#775) 2025-07-08 15:33:39 -05:00
Frank Denis 48d18f0472 Add support for the Zig Language Server (ZLS) (#756) 2025-07-08 15:31:11 -05:00
Tommy 72a22257b7 docs: add terminal requirements (#708) 2025-07-08 15:30:05 -05:00
Gal Schlezinger 1f4b17906a [config json schema] declare default values and examples for in-ide documentation (#754) 2025-07-08 15:29:07 -05:00
adamdottv f0c30861c5 fix: env has to be string
publish / publish (push) Has been cancelled
2025-07-08 14:59:03 -05:00
adamdottv 5ea5ebbbdd fix(tui): pasting causes panic on macos 2025-07-08 14:57:17 -05:00
adamdottv b2a0eb3e6a fix(tui): model/provider arg parsing 2025-07-08 14:11:57 -05:00
Timo Clasen 1dd8a8c48a fix(copilot): add vision request header (#773) 2025-07-08 14:01:54 -05:00
adamdottv e5dee9c760 fix: image reading
publish / publish (push) Has been cancelled
2025-07-08 13:02:13 -05:00
Aiden Cline d2575eea6c fix: display errors when using opencode run ... (#751) 2025-07-08 10:38:11 -05:00
adamdottv ea7c1f7ea6 fix: include symlinks in ripgrep searches 2025-07-08 10:02:19 -05:00
Dax eb30f5eb59 Update STATS.md 2025-07-08 10:30:02 -04:00
adamdottv 90c521226f feat(tui): command aliases 2025-07-08 08:20:55 -05:00
adamdottv 162497783b feat(tui): paste images and pdfs 2025-07-08 08:09:01 -05:00
GitHub Action 23327b47e6 ignore: update download stats 2025-07-08 2025-07-08 12:04:27 +00:00
GitHub Action 2daed93355 ignore: update download stats 2025-07-08 2025-07-08 10:51:06 +00:00
adamdottv 9007c6b36d feat(tui): handle --model and --prompt flags 2025-07-08 05:50:18 -05:00
Dax Raad 1d6edec2c2 start file watcher only for tui 2025-07-07 21:05:04 -04:00
Dax Raad ca926515b2 better handling of aborting sessions
publish / publish (push) Has been cancelled
2025-07-07 20:59:00 -04:00
Jay V 713141175a docs: intro 2025-07-07 17:41:46 -04:00
Dax Raad 992bbcbd60 fix crash when running on new project
publish / publish (push) Has been cancelled
2025-07-07 17:39:52 -04:00
Dax Raad 5cd08e988a fix mcp tools corrupting session
publish / publish (push) Has been cancelled
2025-07-07 17:05:16 -04:00
Dax Raad 2a750c2333 run: truncate prompt 2025-07-07 16:41:42 -04:00
Jay V 2c4a516b47 docs: canonical url 2025-07-07 16:37:00 -04:00
Dax Raad 739716aef9 append piped stdin to prompt
publish / publish (push) Has been cancelled
2025-07-07 16:33:21 -04:00
Jay V 1fe7c77504 docs: dynamic domain 2025-07-07 16:31:15 -04:00
Dax Raad 446932ceec Add stdin support to run command
Allow piping content to opencode run when no message arguments are provided, enabling standard Unix pipe patterns for better CLI integration.

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

Co-Authored-By: opencode <noreply@opencode.ai>
2025-07-07 16:29:13 -04:00
Jay V 2a3f77d6c2 docs: debug 2025-07-07 16:26:23 -04:00
Dax Raad e8bbb3e7ab wip: model and prompt flags for tui 2025-07-07 16:24:37 -04:00
Jay V 1efcaade0f docs: debug info 2025-07-07 16:13:26 -04:00
Dax Raad 69ef316d7d fix interrupt
publish / publish (push) Has been cancelled
2025-07-07 16:12:47 -04:00
Dax 9cad702612 v2 message format and upgrade to ai sdk v5 (#743)
publish / publish (push) Has been cancelled
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Liang-Shih Lin <liangshihlin@proton.me>
Co-authored-by: Dominik Engelhardt <dominikengelhardt@ymail.com>
Co-authored-by: Jay V <air@live.ca>
Co-authored-by: adamdottv <2363879+adamdottv@users.noreply.github.com>
2025-07-07 15:53:43 -04:00
Jay V fad07dc6e7 docs: discord
publish / publish (push) Has been cancelled
2025-07-07 14:44:37 -04:00
Dominik Engelhardt ca1f14a523 Fix Elixir LSP startup (#726) 2025-07-06 23:37:46 -04:00
Liang-Shih Lin d7b2f0079c fix: Skip opencode upgrade if same version (#720) 2025-07-06 23:36:59 -04:00
GitHub Action 65710a3bac Update download stats 2025-07-06 2025-07-06 12:03:56 +00:00
Dax Raad 207c73bc6a fix openrouter caching with anthropic, should be a lot cheaper 2025-07-05 11:39:54 -04:00
GitHub Action b5eead4dd1 Update download stats 2025-07-05 2025-07-05 12:03:56 +00:00
Dax Raad e5275df48e use full filepath
publish / publish (push) Has been cancelled
2025-07-04 17:58:03 -04:00
Jay V a536d88f81 docs: fix show more in share page 2025-07-04 17:57:12 -04:00
Dax Raad 79d0545e0a fix input bar not rendering capital letters
publish / publish (push) Has been cancelled
2025-07-04 17:21:51 -04:00
Timo Clasen 00e50865f1 fix(tui): optimistic rendering (#692) 2025-07-04 16:06:57 -05:00
Dax Raad 4a274ef8a0 make file attachments work good like
publish / publish (push) Has been cancelled
2025-07-04 16:21:26 -04:00
Jay V 51e5be4f89 docs: share fix scrolling again 2025-07-04 13:53:25 -04:00
Jay V 1ee272138f docs: share improve markdown rendering of ai responses 2025-07-04 13:53:25 -04:00
Dax Raad f81777575a wip: fix media type 2025-07-04 12:50:52 -04:00
adamdottv 0848fc6164 fix(tui): full paths for attachments 2025-07-04 11:42:22 -05:00
Dax Raad 4d2ed3c930 file attachments 2025-07-04 12:24:01 -04:00
adamdottv 973a6708ce fix(tui): markdown wrapping off sometimes 2025-07-04 11:16:38 -05:00
adamdottv c399f75f2f fix(tui): handle pdf and image @ files 2025-07-04 11:13:09 -05:00
adamdottv 6270feab77 feat(tui): render attachments 2025-07-04 10:55:02 -05:00
adamdottv 9ac592b58b feat(tui): file attachments 2025-07-04 10:55:02 -05:00
adamdottv f062b39ad3 fix(tui): slower startup due to file.status 2025-07-04 10:55:01 -05:00
Dax Raad 37824a539f removed banned command concept 2025-07-04 11:32:12 -04:00
Vladimir 9f2c80d51d docs: Fix invalid json in the mcp example config (#645) 2025-07-04 11:24:13 -04:00
Aiden Cline 764375b5bb chore: document instructions configuration option (#670) 2025-07-04 11:22:45 -04:00
Dax Raad 76577d2db4 disable snapshots temporarily
publish / publish (push) Has been cancelled
2025-07-04 08:45:18 -04:00
GitHub Action fd63682808 Update download stats 2025-07-04 2025-07-04 12:26:16 +00:00
Dax Raad 2ba7a4e337 improve snapshotting speed further
publish / publish (push) Has been cancelled
2025-07-03 21:36:09 -04:00
Jay V 6d04925de2 docs: share fix scroll to anchor 2025-07-03 20:30:21 -04:00
Dax Raad 0e648fe1c3 ci: ignore 2025-07-03 20:30:02 -04:00
Dax Raad a59340b05d improve snapshot speed
publish / publish (push) Has been cancelled
2025-07-03 20:16:25 -04:00
Jay V ac76a81354 docs: share fix last message not expandable 2025-07-03 19:33:55 -04:00
Dax Raad a0764b2c3a add elixir lsp support
publish / publish (push) Has been cancelled
2025-07-03 19:29:51 -04:00
Dax Raad 0d798b90aa wip: logs 2025-07-03 19:29:51 -04:00
Jay V b14b9af9d8 docs: fix share page scroll performance 2025-07-03 19:15:38 -04:00
Jay V c627b7f4b2 docs: tweak page title 2025-07-03 16:23:08 -04:00
Timo Clasen a42379df25 chore: add dev script (#666) 2025-07-03 14:43:25 -05:00
Dax Raad 1843630b21 fix /unshare
publish / publish (push) Has been cancelled
2025-07-03 15:34:04 -04:00
adamdottv d49a1304f8 chore: simplify completions 2025-07-03 12:48:22 -05:00
Jay V a7cfb68335 docs: share handle slow loading pages 2025-07-03 13:15:21 -04:00
adamdottv 0763cff26c chore: better local dev with stainless script 2025-07-03 11:49:15 -05:00
adamdottv dfff25a241 fix(tui): panic on completions failure
publish / publish (push) Has been cancelled
2025-07-03 10:53:43 -05:00
adamdottv b7bd7c28e2 fix(tui): redundant tool calls in each message in collapsed mode
publish / publish (push) Has been cancelled
2025-07-03 10:42:27 -05:00
Dax Raad 89821f3f69 flag for disabling file watcher 2025-07-03 10:37:08 -04:00
421 changed files with 51394 additions and 10862 deletions
+14
View File
@@ -0,0 +1,14 @@
name: discord
on:
release:
types: [published] # fires only when a release is published
jobs:
notify:
runs-on: ubuntu-latest
steps:
- name: Send nicely-formatted embed to Discord
uses: SethCohen/github-releases-to-discord@v1
with:
webhook_url: ${{ secrets.DISCORD_WEBHOOK }}
+29
View File
@@ -0,0 +1,29 @@
name: opencode
on:
issue_comment:
types: [created]
jobs:
opencode:
if: |
contains(github.event.comment.body, ' /oc') ||
startsWith(github.event.comment.body, '/oc') ||
contains(github.event.comment.body, ' /opencode') ||
startsWith(github.event.comment.body, '/opencode')
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run opencode
uses: sst/opencode/github@latest
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
with:
model: anthropic/claude-sonnet-4-20250514
@@ -0,0 +1,30 @@
name: publish-github-action
on:
workflow_dispatch:
push:
tags:
- "github-v*.*.*"
- "!github-v1"
concurrency: ${{ github.workflow }}-${{ github.ref }}
permissions:
contents: write
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- run: git fetch --force --tags
- name: Publish
run: |
git config --global user.email "opencode@sst.dev"
git config --global user.name "opencode"
./script/publish
working-directory: ./github
+36
View File
@@ -0,0 +1,36 @@
name: publish-vscode
on:
workflow_dispatch:
push:
tags:
- "vscode-v*.*.*"
concurrency: ${{ github.workflow }}-${{ github.ref }}
permissions:
contents: write
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.2.17
- run: git fetch --force --tags
- run: bun install -g @vscode/vsce
- name: Publish
run: |
bun install
./script/publish
working-directory: ./sdks/vscode
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
OPENVSX_TOKEN: ${{ secrets.OPENVSX_TOKEN }}
+24 -13
View File
@@ -1,12 +1,17 @@
name: publish
run-name: "${{ format('v{0}', inputs.version) }}"
on:
workflow_dispatch:
push:
branches:
- dev
tags:
- "*"
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 }}
@@ -32,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: |
@@ -48,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 }}
+4 -2
View File
@@ -21,12 +21,14 @@ jobs:
bun-version: latest
- name: Run stats script
run: bun scripts/stats.ts
run: bun script/stats.ts
- name: Commit stats
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add STATS.md
git diff --staged --quiet || git commit -m "Update download stats $(date -I)"
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
+2 -1
View File
@@ -1,7 +1,8 @@
.DS_Store
node_modules
.opencode
.sst
.env
.idea
.vscode
openapi.json
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.
+12
View File
@@ -0,0 +1,12 @@
## 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()
+39 -8
View File
@@ -9,7 +9,7 @@
</p>
<p align="center">AI coding agent, built for the terminal.</p>
<p align="center">
<a href="https://opencode.ai/docs"><img alt="View docs" src="https://img.shields.io/badge/view-docs-blue?style=flat-square" /></a>
<a href="https://opencode.ai/discord"><img alt="Discord" src="https://img.shields.io/discord/1391832426048651334?style=flat-square&label=discord" /></a>
<a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
<a href="https://github.com/sst/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/sst/opencode/publish.yml?style=flat-square&branch=dev" /></a>
</p>
@@ -30,7 +30,23 @@ brew install sst/tap/opencode # macOS
paru -S opencode-bin # Arch Linux
```
> **Note:** Remove versions older than 0.1.x before installing
> [!TIP]
> Remove versions older than 0.1.x before installing.
#### Installation Directory
The install script respects the following priority order for the installation path:
1. `$OPENCODE_INSTALL_DIR` - Custom installation directory
2. `$XDG_BIN_DIR` - XDG Base Directory Specification compliant path
3. `$HOME/bin` - Standard user binary directory (if exists or can be created)
4. `$HOME/.opencode/bin` - Default fallback
```bash
# Examples
OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash
XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash
```
### Documentation
@@ -38,10 +54,25 @@ For more info on how to configure opencode [**head over to our docs**](https://o
### Contributing
For any new features we'd appreciate it if you could open an issue first to discuss what you'd like to implement. We're pretty responsive there and it'll save you from working on something that we don't end up using. No need to do this for simpler fixes.
opencode is an opinionated tool so any fundamental feature needs to go through a
design process with the core team.
> **Note**: Please talk to us via github issues before spending time working on
> a new feature
> [!IMPORTANT]
> We do not accept PRs for core features.
However we still merge a ton of PRs - you can contribute:
- Bug fixes
- Improvements to LLM performance
- Support for new providers
- Fixes for env specific quirks
- Missing standard behavior
- Documentation
Take a look at the git history to see what kind of PRs we end up merging.
> [!NOTE]
> If you do not follow the above guidelines we might close your PR.
To run opencode locally you need.
@@ -52,7 +83,7 @@ And run.
```bash
$ bun install
$ bun run packages/opencode/src/index.ts
$ bun dev
```
#### Development Notes
@@ -66,7 +97,7 @@ $ bun run packages/opencode/src/index.ts
It's very similar to Claude Code in terms of capability. Here are the key differences:
- 100% open source
- Not coupled to any provider. Although Anthropic is recommended, opencode can be used with OpenAI, Google or even local models. As models evolve the gaps between them will close and pricing will drop so being provider agnostic is important.
- Not coupled to any provider. Although Anthropic is recommended, opencode can be used with OpenAI, Google or even local models. As models evolve the gaps between them will close and pricing will drop so being provider-agnostic is important.
- A focus on TUI. opencode is built by neovim users and the creators of [terminal.shop](https://terminal.shop); we are going to push the limits of what's possible in the terminal.
- A client/server architecture. This for example can allow opencode to run on your computer, while you can drive it remotely from a mobile app. Meaning that the TUI frontend is just one of the possible clients.
@@ -76,4 +107,4 @@ The other confusingly named repo has no relation to this one. You can [read the
---
**Join our community** [YouTube](https://www.youtube.com/c/sst-dev) | [X.com](https://x.com/SST_dev)
**Join our community** [Discord](https://discord.gg/opencode) | [YouTube](https://www.youtube.com/c/sst-dev) | [X.com](https://x.com/SST_dev)
+41 -7
View File
@@ -1,9 +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) |
| 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) |
+854 -319
View File
File diff suppressed because it is too large Load Diff
+137
View File
@@ -0,0 +1,137 @@
# opencode GitHub Action
A GitHub Action that integrates [opencode](https://opencode.ai) directly into your GitHub workflow.
Mention `/opencode` in your comment, and opencode will execute tasks within your GitHub Actions runner.
## Features
#### Explain an issues
Leave the following comment on a GitHub issue. `opencode` will read the entire thread, including all comments, and reply with a clear explanation.
```
/opencode explain this issue
```
#### Fix an issues
Leave the following comment on a GitHub issue. opencode will create a new branch, implement the changes, and open a PR with the changes.
```
/opencode fix this
```
#### Review PRs and make changes
Leave the following comment on a GitHub PR. opencode will implement the requested change and commit it to the same PR.
```
Delete the attachment from S3 when the note is removed /oc
```
## Installation
Run the following command in the terminal from your GitHub repo:
```bash
opencode github install
```
This will walk you through installing the GitHub app, creating the workflow, and setting up secrets.
### Manual Setup
1. Install the GitHub app https://github.com/apps/opencode-agent. Make sure it is installed on the target repository.
2. Add the following workflow file to `.github/workflows/opencode.yml` in your repo. Set the appropriate `model` and required API keys in `env`.
```yml
name: opencode
on:
issue_comment:
types: [created]
jobs:
opencode:
if: |
contains(github.event.comment.body, '/oc') ||
contains(github.event.comment.body, '/opencode')
runs-on: ubuntu-latest
permissions:
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run opencode
uses: sst/opencode/github@latest
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
with:
model: anthropic/claude-sonnet-4-20250514
```
3. Store the API keys in secrets. In your organization or project **settings**, expand **Secrets and variables** on the left and select **Actions**. Add the required API keys.
## Support
This is an early release. If you encounter issues or have feedback, please create an issue at https://github.com/sst/opencode/issues.
## Development
To test locally:
1. Navigate to a test repo (e.g. `hello-world`):
```bash
cd hello-world
```
2. Run:
```bash
MODEL=anthropic/claude-sonnet-4-20250514 \
ANTHROPIC_API_KEY=sk-ant-api03-1234567890 \
GITHUB_RUN_ID=dummy \
bun /path/to/opencode/packages/opencode/src/index.ts github run \
--token 'github_pat_1234567890' \
--event '{"eventName":"issue_comment",...}'
```
- `MODEL`: The model used by opencode. Same as the `MODEL` defined in the GitHub workflow.
- `ANTHROPIC_API_KEY`: Your model provider API key. Same as the keys defined in the GitHub workflow.
- `GITHUB_RUN_ID`: Dummy value to emulate GitHub action environment.
- `/path/to/opencode`: Path to your cloned opencode repo. `bun /path/to/opencode/packages/opencode/src/index.ts` runs your local version of `opencode`.
- `--token`: A GitHub persontal access token. This token is used to verify you have `admin` or `write` access to the test repo. Generate a token [here](https://github.com/settings/personal-access-tokens).
- `--event`: Mock GitHub event payload (see templates below).
### Issue comment event
```
--event '{"eventName":"issue_comment","repo":{"owner":"sst","repo":"hello-world"},"actor":"fwang","payload":{"issue":{"number":4},"comment":{"id":1,"body":"hey opencode, summarize thread"}}}'
```
Replace:
- `"owner":"sst"` with repo owner
- `"repo":"hello-world"` with repo name
- `"actor":"fwang"` with the GitHub username of commentor
- `"number":4` with the GitHub issue id
- `"body":"hey opencode, summarize thread"` with comment body
### Issue comment with image attachment.
```
--event '{"eventName":"issue_comment","repo":{"owner":"sst","repo":"hello-world"},"actor":"fwang","payload":{"issue":{"number":4},"comment":{"id":1,"body":"hey opencode, what is in my image ![Image](https://github.com/user-attachments/assets/xxxxxxxx)"}}}'
```
Replace the image URL `https://github.com/user-attachments/assets/xxxxxxxx` with a valid GitHub attachment (you can generate one by commenting with an image in any issue).
### PR comment event
```
--event '{"eventName":"issue_comment","repo":{"owner":"sst","repo":"hello-world"},"actor":"fwang","payload":{"issue":{"number":4,"pull_request":{}},"comment":{"id":1,"body":"hey opencode, summarize thread"}}}'
```
+29
View File
@@ -0,0 +1,29 @@
name: "opencode GitHub Action"
description: "Run opencode in GitHub Actions workflows"
branding:
icon: "code"
color: "orange"
inputs:
model:
description: "Model to use"
required: true
share:
description: "Share the opencode session (defaults to true for public repos)"
required: false
runs:
using: "composite"
steps:
- name: Install opencode
shell: bash
run: curl -fsSL https://opencode.ai/install | bash
- name: Run opencode
shell: bash
id: run_opencode
run: opencode github run
env:
MODEL: ${{ inputs.model }}
SHARE: ${{ inputs.share }}
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
# Get the latest Git tag
latest_tag=$(git tag --sort=committerdate | grep -E '^github-v[0-9]+\.[0-9]+\.[0-9]+$' | tail -1)
if [ -z "$latest_tag" ]; then
echo "No tags found"
exit 1
fi
echo "Latest tag: $latest_tag"
# Update latest tag
git tag -d latest
git push origin :refs/tags/latest
git tag -a latest $latest_tag -m "Update latest to $latest_tag"
git push origin latest
+4 -6
View File
@@ -9,12 +9,9 @@ while [ "$#" -gt 0 ]; do
esac
done
git fetch --force --tags
# Get the latest Git tag
latest_tag=$(git tag --sort=committerdate | grep -E '[0-9]' | tail -1)
# If there is no tag, exit the script
git fetch --force --tags
latest_tag=$(git tag --sort=committerdate | grep -E '^github-v[0-9]+\.[0-9]+\.[0-9]+$' | tail -1)
if [ -z "$latest_tag" ]; then
echo "No tags found"
exit 1
@@ -39,5 +36,6 @@ fi
echo "New version: $new_version"
# Tag
git tag $new_version
git push --tags
git push --tags
+17 -1
View File
@@ -4,6 +4,8 @@ export const domain = (() => {
return `${$app.stage}.dev.opencode.ai`
})()
const GITHUB_APP_ID = new sst.Secret("GITHUB_APP_ID")
const GITHUB_APP_PRIVATE_KEY = new sst.Secret("GITHUB_APP_PRIVATE_KEY")
const bucket = new sst.cloudflare.Bucket("Bucket")
export const api = new sst.cloudflare.Worker("Api", {
@@ -13,7 +15,7 @@ export const api = new sst.cloudflare.Worker("Api", {
WEB_DOMAIN: domain,
},
url: true,
link: [bucket],
link: [bucket, GITHUB_APP_ID, GITHUB_APP_PRIVATE_KEY],
transform: {
worker: (args) => {
args.logpush = true
@@ -39,6 +41,20 @@ new sst.cloudflare.x.Astro("Web", {
domain,
path: "packages/web",
environment: {
// For astro config
SST_STAGE: $app.stage,
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],
})
+1 -2
View File
@@ -48,7 +48,7 @@ if [ -z "$requested_version" ]; then
url="https://github.com/sst/opencode/releases/latest/download/$filename"
specific_version=$(curl -s https://api.github.com/repos/sst/opencode/releases/latest | awk -F'"' '/"tag_name": "/ {gsub(/^v/, "", $4); print $4}')
if [[ $? -ne 0 ]]; then
if [[ $? -ne 0 || -z "$specific_version" ]]; then
echo "${RED}Failed to fetch version information${NC}"
exit 1
fi
@@ -186,4 +186,3 @@ if [ -n "${GITHUB_ACTIONS-}" ] && [ "${GITHUB_ACTIONS}" == "true" ]; then
echo "$INSTALL_DIR" >> $GITHUB_PATH
print_message info "Added $INSTALL_DIR to \$GITHUB_PATH"
fi
+8 -14
View File
@@ -1,19 +1,13 @@
{
"$schema": "https://opencode.ai/config.json",
"experimental": {
"hook": {
"file_edited": {
".json": [
{
"command": ["bun", "run", "prettier", "$FILE"]
}
]
},
"session_completed": [
{
"command": ["touch", "./node_modules/foo"]
}
]
"mcp": {
"context7": {
"type": "remote",
"url": "https://mcp.context7.com/sse"
},
"weather": {
"type": "local",
"command": ["opencode", "x", "@h1deya/mcp-server-weather"]
}
}
}
+15 -13
View File
@@ -5,23 +5,29 @@
"type": "module",
"packageManager": "bun@1.2.14",
"scripts": {
"dev": "bun run --conditions=development packages/opencode/src/index.ts",
"typecheck": "bun run --filter='*' typecheck",
"postinstall": "./scripts/hooks"
"stainless": "./scripts/stainless",
"postinstall": "./script/hooks"
},
"workspaces": {
"packages": [
"packages/*"
"packages/*",
"packages/sdk/js"
],
"catalog": {
"typescript": "5.8.2",
"@types/node": "22.13.9",
"zod": "3.24.2",
"ai": "4.3.16"
"@tsconfig/node22": "22.0.2",
"ai": "5.0.0-beta.34",
"hono": "4.7.10",
"typescript": "5.8.2",
"zod": "3.25.49",
"remeda": "2.26.0"
}
},
"devDependencies": {
"prettier": "3.5.3",
"sst": "3.17.6"
"sst": "3.17.8"
},
"repository": {
"type": "git",
@@ -29,17 +35,13 @@
},
"license": "MIT",
"prettier": {
"semi": false
},
"overrides": {
"zod": "3.24.2"
"semi": false,
"printWidth": 120
},
"trustedDependencies": [
"esbuild",
"protobufjs",
"sharp"
],
"patchedDependencies": {
"ai@4.3.16": "patches/ai@4.3.16.patch"
}
"patchedDependencies": {}
}
+14 -3
View File
@@ -1,12 +1,23 @@
{
"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"
}
}
+205 -115
View File
@@ -1,5 +1,10 @@
import { Hono } from "hono"
import { DurableObject } from "cloudflare:workers"
import { randomUUID } from "node:crypto"
import { jwtVerify, createRemoteJWKSet } from "jose"
import { createAppAuth } from "@octokit/auth-app"
import { Octokit } from "@octokit/rest"
import { Resource } from "sst"
type Env = {
SYNC_SERVER: DurableObjectNamespace<SyncServer>
@@ -40,7 +45,8 @@ export class SyncServer extends DurableObject<Env> {
const sessionID = await this.getSessionID()
if (
!key.startsWith(`session/info/${sessionID}`) &&
!key.startsWith(`session/message/${sessionID}/`)
!key.startsWith(`session/message/${sessionID}/`) &&
!key.startsWith(`session/part/${sessionID}/`)
)
return new Response("Error: Invalid key", { status: 400 })
@@ -70,7 +76,7 @@ export class SyncServer extends DurableObject<Env> {
}
public async getData() {
const data = await this.ctx.storage.list()
const data = (await this.ctx.storage.list()) as Map<string, any>
return Array.from(data.entries())
.filter(([key, _]) => key.startsWith("session/"))
.map(([key, content]) => ({ key, content }))
@@ -106,122 +112,206 @@ export class SyncServer extends DurableObject<Env> {
}
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
const url = new URL(request.url)
const splits = url.pathname.split("/")
const method = splits[1]
if (request.method === "GET" && method === "") {
return new Response("Hello, world!", {
headers: { "Content-Type": "text/plain" },
})
export default new Hono<{ Bindings: Env }>()
.get("/", (c) => c.text("Hello, world!"))
.post("/share_create", async (c) => {
const body = await c.req.json<{ sessionID: string }>()
const sessionID = body.sessionID
const short = SyncServer.shortName(sessionID)
const id = c.env.SYNC_SERVER.idFromName(short)
const stub = c.env.SYNC_SERVER.get(id)
const secret = await stub.share(sessionID)
return c.json({
secret,
url: `https://${c.env.WEB_DOMAIN}/s/${short}`,
})
})
.post("/share_delete", async (c) => {
const body = await c.req.json<{ sessionID: string; secret: string }>()
const sessionID = body.sessionID
const secret = body.secret
const id = c.env.SYNC_SERVER.idFromName(SyncServer.shortName(sessionID))
const stub = c.env.SYNC_SERVER.get(id)
await stub.assertSecret(secret)
await stub.clear()
return c.json({})
})
.post("/share_delete_admin", async (c) => {
const id = c.env.SYNC_SERVER.idFromName("oVF8Rsiv")
const stub = c.env.SYNC_SERVER.get(id)
await stub.clear()
return c.json({})
})
.post("/share_sync", async (c) => {
const body = await c.req.json<{
sessionID: string
secret: string
key: string
content: any
}>()
const name = SyncServer.shortName(body.sessionID)
const id = c.env.SYNC_SERVER.idFromName(name)
const stub = c.env.SYNC_SERVER.get(id)
await stub.assertSecret(body.secret)
await stub.publish(body.key, body.content)
return c.json({})
})
.get("/share_poll", async (c) => {
const upgradeHeader = c.req.header("Upgrade")
if (!upgradeHeader || upgradeHeader !== "websocket") {
return c.text("Error: Upgrade header is required", { status: 426 })
}
const id = c.req.query("id")
console.log("share_poll", id)
if (!id) return c.text("Error: Share ID is required", { status: 400 })
const stub = c.env.SYNC_SERVER.get(c.env.SYNC_SERVER.idFromName(id))
return stub.fetch(c.req.raw)
})
.get("/share_data", async (c) => {
const id = c.req.query("id")
console.log("share_data", id)
if (!id) return c.text("Error: Share ID is required", { status: 400 })
const stub = c.env.SYNC_SERVER.get(c.env.SYNC_SERVER.idFromName(id))
const data = await stub.getData()
if (request.method === "POST" && method === "share_create") {
const body = await request.json<any>()
const sessionID = body.sessionID
const short = SyncServer.shortName(sessionID)
const id = env.SYNC_SERVER.idFromName(short)
const stub = env.SYNC_SERVER.get(id)
const secret = await stub.share(sessionID)
return new Response(
JSON.stringify({
secret,
url: `https://${env.WEB_DOMAIN}/s/${short}`,
}),
{
headers: { "Content-Type": "application/json" },
},
)
}
if (request.method === "POST" && method === "share_delete") {
const body = await request.json<any>()
const sessionID = body.sessionID
const secret = body.secret
const id = env.SYNC_SERVER.idFromName(SyncServer.shortName(sessionID))
const stub = env.SYNC_SERVER.get(id)
await stub.assertSecret(secret)
await stub.clear()
return new Response(JSON.stringify({}), {
headers: { "Content-Type": "application/json" },
})
}
if (request.method === "POST" && method === "share_delete_admin") {
const id = env.SYNC_SERVER.idFromName("oVF8Rsiv")
const stub = env.SYNC_SERVER.get(id)
await stub.clear()
return new Response(JSON.stringify({}), {
headers: { "Content-Type": "application/json" },
})
}
if (request.method === "POST" && method === "share_sync") {
const body = await request.json<{
sessionID: string
secret: string
key: string
content: any
}>()
const name = SyncServer.shortName(body.sessionID)
const id = env.SYNC_SERVER.idFromName(name)
const stub = env.SYNC_SERVER.get(id)
await stub.assertSecret(body.secret)
await stub.publish(body.key, body.content)
return new Response(JSON.stringify({}), {
headers: { "Content-Type": "application/json" },
})
}
if (request.method === "GET" && method === "share_poll") {
const upgradeHeader = request.headers.get("Upgrade")
if (!upgradeHeader || upgradeHeader !== "websocket") {
return new Response("Error: Upgrade header is required", {
status: 426,
})
let info
const messages: Record<string, any> = {}
data.forEach((d) => {
const [root, type, ...splits] = d.key.split("/")
if (root !== "session") return
if (type === "info") {
info = d.content
return
}
const id = url.searchParams.get("id")
console.log("share_poll", id)
if (!id)
return new Response("Error: Share ID is required", { status: 400 })
const stub = env.SYNC_SERVER.get(env.SYNC_SERVER.idFromName(id))
return stub.fetch(request)
}
if (request.method === "GET" && method === "share_data") {
const id = url.searchParams.get("id")
console.log("share_data", id)
if (!id)
return new Response("Error: Share ID is required", { status: 400 })
const stub = env.SYNC_SERVER.get(env.SYNC_SERVER.idFromName(id))
const data = await stub.getData()
let info
const messages: Record<string, any> = {}
data.forEach((d) => {
const [root, type, ...splits] = d.key.split("/")
if (root !== "session") return
if (type === "info") {
info = d.content
return
}
if (type === "message") {
const [, messageID] = splits
messages[messageID] = d.content
if (type === "message") {
messages[d.content.id] = {
parts: [],
...d.content,
}
}
if (type === "part") {
messages[d.content.messageID].parts.push(d.content)
}
})
return c.json({ info, messages })
})
/**
* Used by the GitHub action to get GitHub installation access token given the OIDC token
*/
.post("/exchange_github_app_token", async (c) => {
const EXPECTED_AUDIENCE = "opencode-github-action"
const GITHUB_ISSUER = "https://token.actions.githubusercontent.com"
const JWKS_URL = `${GITHUB_ISSUER}/.well-known/jwks`
// get Authorization header
const token = c.req.header("Authorization")?.replace(/^Bearer /, "")
if (!token) return c.json({ error: "Authorization header is required" }, { status: 401 })
// verify token
const JWKS = createRemoteJWKSet(new URL(JWKS_URL))
let owner, repo
try {
const { payload } = await jwtVerify(token, JWKS, {
issuer: GITHUB_ISSUER,
audience: EXPECTED_AUDIENCE,
})
return new Response(
JSON.stringify({
info,
messages,
}),
{
headers: { "Content-Type": "application/json" },
},
)
const sub = payload.sub // e.g. 'repo:my-org/my-repo:ref:refs/heads/main'
const parts = sub.split(":")[1].split("/")
owner = parts[0]
repo = parts[1]
} catch (err) {
console.error("Token verification failed:", err)
return c.json({ error: "Invalid or expired token" }, { status: 403 })
}
},
}
// Create app JWT token
const auth = createAppAuth({
appId: Resource.GITHUB_APP_ID.value,
privateKey: Resource.GITHUB_APP_PRIVATE_KEY.value,
})
const appAuth = await auth({ type: "app" })
// Lookup installation
const octokit = new Octokit({ auth: appAuth.token })
const { data: installation } = await octokit.apps.getRepoInstallation({ owner, repo })
// Get installation token
const installationAuth = await auth({ type: "installation", installationId: installation.id })
return c.json({ token: installationAuth.token })
})
/**
* Used by the GitHub action to get GitHub installation access token given user PAT token (used when testing `opencode github run` locally)
*/
.post("/exchange_github_app_token_with_pat", async (c) => {
const body = await c.req.json<{ owner: string; repo: string }>()
const owner = body.owner
const repo = body.repo
try {
// get Authorization header
const authHeader = c.req.header("Authorization")
const token = authHeader?.replace(/^Bearer /, "")
if (!token) throw new Error("Authorization header is required")
// Verify permissions
const userClient = new Octokit({ auth: token })
const { data: repoData } = await userClient.repos.get({ owner, repo })
if (!repoData.permissions.admin && !repoData.permissions.push && !repoData.permissions.maintain)
throw new Error("User does not have write permissions")
// Get installation token
const auth = createAppAuth({
appId: Resource.GITHUB_APP_ID.value,
privateKey: Resource.GITHUB_APP_PRIVATE_KEY.value,
})
const appAuth = await auth({ type: "app" })
// Lookup installation
const appClient = new Octokit({ auth: appAuth.token })
const { data: installation } = await appClient.apps.getRepoInstallation({ owner, repo })
// Get installation token
const installationAuth = await auth({ type: "installation", installationId: installation.id })
return c.json({ token: installationAuth.token })
} catch (e: any) {
let error = e
if (e instanceof Error) {
error = e.message
}
return c.json({ error }, { status: 401 })
}
})
/**
* Used by the opencode CLI to check if the GitHub app is installed
*/
.get("/get_github_app_installation", async (c) => {
const owner = c.req.query("owner")
const repo = c.req.query("repo")
const auth = createAppAuth({
appId: Resource.GITHUB_APP_ID.value,
privateKey: Resource.GITHUB_APP_PRIVATE_KEY.value,
})
const appAuth = await auth({ type: "app" })
// Lookup installation
const octokit = new Octokit({ auth: appAuth.token })
let installation
try {
const ret = await octokit.apps.getRepoInstallation({ owner, repo })
installation = ret.data
} catch (err) {
if (err instanceof Error && err.message.includes("Not Found")) {
// not installed
} else {
throw err
}
}
return c.json({ installation })
})
.all("*", (c) => c.text("Not Found"))
+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"))
+33 -8
View File
@@ -6,20 +6,45 @@
import "sst"
declare module "sst" {
export interface Resource {
Web: {
type: "sst.cloudflare.Astro"
url: string
"ANTHROPIC_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
"GITHUB_APP_ID": {
"type": "sst.sst.Secret"
"value": string
}
"GITHUB_APP_PRIVATE_KEY": {
"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
import * as cloudflare from "@cloudflare/workers-types"
// cloudflare
import * as cloudflare from "@cloudflare/workers-types";
declare module "sst" {
export interface Resource {
Api: cloudflare.Service
Bucket: cloudflare.R2Bucket
"Api": cloudflare.Service
"Bucket": cloudflare.R2Bucket
"GatewayApi": cloudflare.Service
}
}
import "sst"
export {}
export {}
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

-1
View File
@@ -1,4 +1,3 @@
node_modules
research
dist
gen
-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
-369
View File
@@ -1,369 +0,0 @@
{
"type": "object",
"properties": {
"$schema": {
"type": "string",
"description": "JSON schema reference for configuration validation"
},
"theme": {
"type": "string",
"description": "Theme name to use for the interface"
},
"keybinds": {
"type": "object",
"properties": {
"leader": {
"type": "string",
"description": "Leader key for keybind combinations"
},
"help": {
"type": "string",
"description": "Show help dialog"
},
"editor_open": {
"type": "string",
"description": "Open external editor"
},
"session_new": {
"type": "string",
"description": "Create a new session"
},
"session_list": {
"type": "string",
"description": "List all sessions"
},
"session_share": {
"type": "string",
"description": "Share current session"
},
"session_interrupt": {
"type": "string",
"description": "Interrupt current session"
},
"session_compact": {
"type": "string",
"description": "Toggle compact mode for session"
},
"tool_details": {
"type": "string",
"description": "Show tool details"
},
"model_list": {
"type": "string",
"description": "List available models"
},
"theme_list": {
"type": "string",
"description": "List available themes"
},
"project_init": {
"type": "string",
"description": "Initialize project configuration"
},
"input_clear": {
"type": "string",
"description": "Clear input field"
},
"input_paste": {
"type": "string",
"description": "Paste from clipboard"
},
"input_submit": {
"type": "string",
"description": "Submit input"
},
"input_newline": {
"type": "string",
"description": "Insert newline in input"
},
"history_previous": {
"type": "string",
"description": "Navigate to previous history item"
},
"history_next": {
"type": "string",
"description": "Navigate to next history item"
},
"messages_page_up": {
"type": "string",
"description": "Scroll messages up by one page"
},
"messages_page_down": {
"type": "string",
"description": "Scroll messages down by one page"
},
"messages_half_page_up": {
"type": "string",
"description": "Scroll messages up by half page"
},
"messages_half_page_down": {
"type": "string",
"description": "Scroll messages down by half page"
},
"messages_previous": {
"type": "string",
"description": "Navigate to previous message"
},
"messages_next": {
"type": "string",
"description": "Navigate to next message"
},
"messages_first": {
"type": "string",
"description": "Navigate to first message"
},
"messages_last": {
"type": "string",
"description": "Navigate to last message"
},
"app_exit": {
"type": "string",
"description": "Exit the application"
}
},
"additionalProperties": false,
"description": "Custom keybind configurations"
},
"autoshare": {
"type": "boolean",
"description": "Share newly created sessions automatically"
},
"autoupdate": {
"type": "boolean",
"description": "Automatically update to the latest version"
},
"disabled_providers": {
"type": "array",
"items": {
"type": "string"
},
"description": "Disable providers that are loaded automatically"
},
"model": {
"type": "string",
"description": "Model to use in the format of provider/model, eg anthropic/claude-2"
},
"provider": {
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"api": {
"type": "string"
},
"name": {
"type": "string"
},
"env": {
"type": "array",
"items": {
"type": "string"
}
},
"id": {
"type": "string"
},
"npm": {
"type": "string"
},
"models": {
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"attachment": {
"type": "boolean"
},
"reasoning": {
"type": "boolean"
},
"temperature": {
"type": "boolean"
},
"tool_call": {
"type": "boolean"
},
"cost": {
"type": "object",
"properties": {
"input": {
"type": "number"
},
"output": {
"type": "number"
},
"cache_read": {
"type": "number"
},
"cache_write": {
"type": "number"
}
},
"required": ["input", "output"],
"additionalProperties": false
},
"limit": {
"type": "object",
"properties": {
"context": {
"type": "number"
},
"output": {
"type": "number"
}
},
"required": ["context", "output"],
"additionalProperties": false
},
"id": {
"type": "string"
},
"options": {
"type": "object",
"additionalProperties": {}
}
},
"additionalProperties": false
}
},
"options": {
"type": "object",
"additionalProperties": {}
}
},
"required": ["models"],
"additionalProperties": false
},
"description": "Custom provider configurations and model overrides"
},
"mcp": {
"type": "object",
"additionalProperties": {
"anyOf": [
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "local",
"description": "Type of MCP server connection"
},
"command": {
"type": "array",
"items": {
"type": "string"
},
"description": "Command and arguments to run the MCP server"
},
"environment": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"description": "Environment variables to set when running the MCP server"
},
"enabled": {
"type": "boolean",
"description": "Enable or disable the MCP server on startup"
}
},
"required": ["type", "command"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "remote",
"description": "Type of MCP server connection"
},
"url": {
"type": "string",
"description": "URL of the remote MCP server"
},
"enabled": {
"type": "boolean",
"description": "Enable or disable the MCP server on startup"
}
},
"required": ["type", "url"],
"additionalProperties": false
}
]
},
"description": "MCP (Model Context Protocol) server configurations"
},
"instructions": {
"type": "array",
"items": {
"type": "string"
},
"description": "Additional instruction files or patterns to include"
},
"experimental": {
"type": "object",
"properties": {
"hook": {
"type": "object",
"properties": {
"file_edited": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"type": "object",
"properties": {
"command": {
"type": "array",
"items": {
"type": "string"
}
},
"environment": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"required": ["command"],
"additionalProperties": false
}
}
},
"session_completed": {
"type": "array",
"items": {
"type": "object",
"properties": {
"command": {
"type": "array",
"items": {
"type": "string"
}
},
"environment": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"required": ["command"],
"additionalProperties": false
}
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
},
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}
+30 -13
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"
@@ -17,37 +17,54 @@
"devDependencies": {
"@ai-sdk/amazon-bedrock": "2.2.10",
"@ai-sdk/anthropic": "1.2.12",
"@octokit/webhooks-types": "7.6.1",
"@standard-schema/spec": "1.0.0",
"@tsconfig/bun": "1.0.7",
"@types/bun": "latest",
"@types/turndown": "5.0.5",
"@types/yargs": "17.0.33",
"typescript": "catalog:",
"vscode-languageserver-types": "3.17.5",
"zod-to-json-schema": "3.24.5"
},
"dependencies": {
"@clack/prompts": "0.11.0",
"@flystorage/file-storage": "1.1.0",
"@flystorage/local-fs": "1.1.0",
"@hono/zod-validator": "0.5.0",
"@actions/core": "1.11.1",
"@actions/github": "6.0.1",
"@clack/prompts": "1.0.0-alpha.1",
"@hono/zod-validator": "0.4.2",
"@modelcontextprotocol/sdk": "1.15.1",
"@octokit/graphql": "9.0.1",
"@octokit/rest": "22.0.0",
"@openauthjs/openauth": "0.4.3",
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/sdk": "workspace:*",
"@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:",
"decimal.js": "10.5.0",
"diff": "8.0.2",
"env-paths": "3.0.0",
"hono": "4.7.10",
"gray-matter": "4.0.3",
"hono": "catalog:",
"hono-openapi": "0.4.8",
"isomorphic-git": "1.32.1",
"jsonc-parser": "3.3.1",
"minimatch": "10.0.3",
"open": "10.1.2",
"remeda": "2.22.3",
"ts-lsp-client": "1.0.3",
"remeda": "catalog:",
"tree-sitter": "0.22.4",
"tree-sitter-bash": "0.23.3",
"turndown": "7.2.0",
"vscode-jsonrpc": "8.2.1",
"vscode-languageclient": "8",
"xdg-basedir": "5.1.0",
"yargs": "18.0.0",
"zod": "catalog:",
"zod-openapi": "4.2.4",
"zod-validation-error": "3.5.2"
"zod-openapi": "4.1.0"
}
}
+44 -60
View File
@@ -1,35 +1,30 @@
#!/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 --exact-match HEAD`
.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}`)
const GOARCH: Record<string, string> = {
arm64: "arm64",
x64: "amd64",
"x64-baseline": "amd64",
}
const targets = [
["windows", "x64"],
["linux", "arm64"],
["linux", "x64"],
["linux", "x64-baseline"],
["darwin", "x64"],
["darwin", "x64-baseline"],
["darwin", "arm64"],
["windows", "x64"],
]
await $`rm -rf dist`
@@ -43,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(
@@ -57,8 +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
}
@@ -82,8 +76,7 @@ await Bun.file(`./dist/${pkg.name}/package.json`).write(
2,
),
)
if (!dry)
await $`cd ./dist/${pkg.name} && bun publish --access public --tag ${npmTag}`
if (!dry) await $`cd ./dist/${pkg.name} && bun publish --access public --tag ${npmTag}`
if (!snapshot) {
// Github Release
@@ -91,52 +84,44 @@ if (!snapshot) {
await $`cd dist/${key}/bin && zip -r ../../${key}.zip *`
}
const previous = await fetch(
"https://api.github.com/repos/sst/opencode/releases/latest",
)
.then((res) => res.json())
const previous = await fetch("https://api.github.com/repos/sst/opencode/releases/latest")
.then((res) => {
if (!res.ok) throw new Error(res.statusText)
return res.json()
})
.then((data) => data.tag_name)
const commits = await fetch(
`https://api.github.com/repos/sst/opencode/compare/${previous}...HEAD`,
)
console.log("finding commits between", previous, "and", "HEAD")
const commits = await fetch(`https://api.github.com/repos/sst/opencode/compare/${previous}...HEAD`)
.then((res) => res.json())
.then((data) => data.commits || [])
const notes = commits
.map((commit: any) => `- ${commit.commit.message.split("\n")[0]}`)
.filter((x: string) => {
const lower = x.toLowerCase()
return (
!lower.includes("ignore:") &&
!lower.includes("ci:") &&
!lower.includes("wip:") &&
!lower.includes("docs:") &&
!lower.includes("doc:")
)
})
.join("\n")
const raw = commits.map((commit: any) => `- ${commit.commit.message.split("\n").join(" ")}`)
console.log(raw)
if (!dry)
await $`gh release create v${version} --title "v${version}" --notes ${notes} ./dist/*.zip`
const notes =
raw
.filter((x: string) => {
const lower = x.toLowerCase()
return (
!lower.includes("release:") &&
!lower.includes("ignore:") &&
!lower.includes("chore:") &&
!lower.includes("ci:") &&
!lower.includes("wip:") &&
!lower.includes("docs:") &&
!lower.includes("doc:")
)
})
.join("\n") || "No notable changes"
if (!dry) await $`gh release create v${version} --title "v${version}" --notes ${notes} ./dist/*.zip`
// Calculate SHA values
const arm64Sha =
await $`sha256sum ./dist/opencode-linux-arm64.zip | cut -d' ' -f1`
.text()
.then((x) => x.trim())
const x64Sha =
await $`sha256sum ./dist/opencode-linux-x64.zip | cut -d' ' -f1`
.text()
.then((x) => x.trim())
const macX64Sha =
await $`sha256sum ./dist/opencode-darwin-x64.zip | cut -d' ' -f1`
.text()
.then((x) => x.trim())
const macArm64Sha =
await $`sha256sum ./dist/opencode-darwin-arm64.zip | cut -d' ' -f1`
.text()
.then((x) => x.trim())
const arm64Sha = await $`sha256sum ./dist/opencode-linux-arm64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
const x64Sha = await $`sha256sum ./dist/opencode-linux-x64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
const macX64Sha = await $`sha256sum ./dist/opencode-darwin-x64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
const macArm64Sha = await $`sha256sum ./dist/opencode-darwin-arm64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
// AUR package
const pkgbuild = [
@@ -170,9 +155,8 @@ if (!snapshot) {
for (const pkg of ["opencode", "opencode-bin"]) {
await $`rm -rf ./dist/aur-${pkg}`
await $`git clone ssh://aur@aur.archlinux.org/${pkg}.git ./dist/aur-${pkg}`
await Bun.file(`./dist/aur-${pkg}/PKGBUILD`).write(
pkgbuild.replace("${pkg}", pkg),
)
await $`cd ./dist/aur-${pkg} && git checkout master`
await Bun.file(`./dist/aur-${pkg}/PKGBUILD`).write(pkgbuild.replace("${pkg}", pkg))
await $`cd ./dist/aur-${pkg} && makepkg --printsrcinfo > .SRCINFO`
await $`cd ./dist/aur-${pkg} && git add PKGBUILD .SRCINFO`
await $`cd ./dist/aur-${pkg} && git commit -m "Update to v${version}"`
+29 -2
View File
@@ -4,5 +4,32 @@ import "zod-openapi/extend"
import { Config } from "../src/config/config"
import { zodToJsonSchema } from "zod-to-json-schema"
const result = zodToJsonSchema(Config.Info)
await Bun.write("config.schema.json", JSON.stringify(result, null, 2))
const file = process.argv[2]
const result = zodToJsonSchema(Config.Info, {
/**
* We'll use the `default` values of the field as the only value in `examples`.
* This will ensure no docs are needed to be read, as the configuration is
* self-documenting.
*
* See https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.9.5
*/
postProcess(jsonSchema) {
const schema = jsonSchema as typeof jsonSchema & {
examples?: unknown[]
}
if (schema && typeof schema === "object" && "type" in schema && schema.type === "string" && schema?.default) {
if (!schema.examples) {
schema.examples = [schema.default]
}
schema.description = [schema.description || "", `default: \`${schema.default}\``]
.filter(Boolean)
.join("\n\n")
.trim()
}
return jsonSchema
},
})
await Bun.write(file, JSON.stringify(result, null, 2))
+123
View File
@@ -0,0 +1,123 @@
import { App } from "../app/app"
import { Config } from "../config/config"
import z from "zod"
import { Provider } from "../provider/provider"
import { generateObject, type ModelMessage } from "ai"
import PROMPT_GENERATE from "./generate.txt"
import { SystemPrompt } from "../session/system"
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(),
prompt: z.string().optional(),
tools: z.record(z.boolean()),
})
.openapi({
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> = {
general: {
name: "general",
description:
"General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you.",
tools: {
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) {
delete result[key]
continue
}
let item = result[key]
if (!item)
item = result[key] = {
name: key,
mode: "all",
tools: {},
}
if (value.model) item.model = Provider.parseModel(value.model)
if (value.prompt) item.prompt = value.prompt
if (value.tools)
item.tools = {
...item.tools,
...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
})
export async function get(agent: string) {
return state().then((x) => x[agent])
}
export async function list() {
return state().then((x) => Object.values(x))
}
export async function generate(input: { description: string }) {
const defaultModel = await Provider.defaultModel()
const model = await Provider.getModel(defaultModel.providerID, defaultModel.modelID)
const system = SystemPrompt.header(defaultModel.providerID)
system.push(PROMPT_GENERATE)
const existing = await list()
const result = await generateObject({
temperature: 0.3,
prompt: [
...system.map(
(item): ModelMessage => ({
role: "system",
content: item,
}),
),
{
role: "user",
content: `Create an agent configuration based on this request: \"${input.description}\".\n\nIMPORTANT: The following identifiers already exist and must NOT be used: ${existing.map((i) => i.name).join(", ")}\n Return ONLY the JSON object, no other text, do not wrap in backticks`,
},
],
model: model.language,
schema: z.object({
identifier: z.string(),
whenToUse: z.string(),
systemPrompt: z.string(),
}),
})
return result.object
}
}
+75
View File
@@ -0,0 +1,75 @@
You are an elite AI agent architect specializing in crafting high-performance agent configurations. Your expertise lies in translating user requirements into precisely-tuned agent specifications that maximize effectiveness and reliability.
**Important Context**: You may have access to project-specific instructions from CLAUDE.md files and other context that may include coding standards, project structure, and custom requirements. Consider this context when creating agents to ensure they align with the project's established patterns and practices.
When a user describes what they want an agent to do, you will:
1. **Extract Core Intent**: Identify the fundamental purpose, key responsibilities, and success criteria for the agent. Look for both explicit requirements and implicit needs. Consider any project-specific context from CLAUDE.md files. For agents that are meant to review code, you should assume that the user is asking to review recently written code and not the whole codebase, unless the user has explicitly instructed you otherwise.
2. **Design Expert Persona**: Create a compelling expert identity that embodies deep domain knowledge relevant to the task. The persona should inspire confidence and guide the agent's decision-making approach.
3. **Architect Comprehensive Instructions**: Develop a system prompt that:
- Establishes clear behavioral boundaries and operational parameters
- Provides specific methodologies and best practices for task execution
- Anticipates edge cases and provides guidance for handling them
- Incorporates any specific requirements or preferences mentioned by the user
- Defines output format expectations when relevant
- Aligns with project-specific coding standards and patterns from CLAUDE.md
4. **Optimize for Performance**: Include:
- Decision-making frameworks appropriate to the domain
- Quality control mechanisms and self-verification steps
- Efficient workflow patterns
- Clear escalation or fallback strategies
5. **Create Identifier**: Design a concise, descriptive identifier that:
- Uses lowercase letters, numbers, and hyphens only
- Is typically 2-4 words joined by hyphens
- Clearly indicates the agent's primary function
- Is memorable and easy to type
- Avoids generic terms like "helper" or "assistant"
6 **Example agent descriptions**:
- in the 'whenToUse' field of the JSON object, you should include examples of when this agent should be used.
- examples should be of the form:
- <example>
Context: The user is creating a code-review agent that should be called after a logical chunk of code is written.
user: "Please write a function that checks if a number is prime"
assistant: "Here is the relevant function: "
<function call omitted for brevity only for this example>
<commentary>
Since the user is greeting, use the Task tool to launch the greeting-responder agent to respond with a friendly joke.
</commentary>
assistant: "Now let me use the code-reviewer agent to review the code"
</example>
- <example>
Context: User is creating an agent to respond to the word "hello" with a friendly jok.
user: "Hello"
assistant: "I'm going to use the Task tool to launch the greeting-responder agent to respond with a friendly joke"
<commentary>
Since the user is greeting, use the greeting-responder agent to respond with a friendly joke.
</commentary>
</example>
- If the user mentioned or implied that the agent should be used proactively, you should include examples of this.
- NOTE: Ensure that in the examples, you are making the assistant use the Agent tool and not simply respond directly to the task.
Your output must be a valid JSON object with exactly these fields:
{
"identifier": "A unique, descriptive identifier using lowercase letters, numbers, and hyphens (e.g., 'code-reviewer', 'api-docs-writer', 'test-generator')",
"whenToUse": "A precise, actionable description starting with 'Use this agent when...' that clearly defines the triggering conditions and use cases. Ensure you include examples as described above.",
"systemPrompt": "The complete system prompt that will govern the agent's behavior, written in second person ('You are...', 'You will...') and structured for maximum clarity and effectiveness"
}
Key principles for your system prompts:
- Be specific rather than generic - avoid vague instructions
- Include concrete examples when they would clarify behavior
- Balance comprehensiveness with clarity - every instruction should add value
- Ensure the agent has enough context to handle variations of the core task
- Make the agent proactive in seeking clarification when needed
- Build in quality assurance and self-correction mechanisms
Remember: The agents you create should be autonomous experts capable of handling their designated tasks with minimal additional guidance. Your system prompts are their complete operational manual.
+3 -14
View File
@@ -12,7 +12,6 @@ export namespace App {
export const Info = z
.object({
user: z.string(),
hostname: z.string(),
git: z.boolean(),
path: z.object({
@@ -45,23 +44,14 @@ export namespace App {
}
export const provideExisting = ctx.provide
export async function provide<T>(
input: Input,
cb: (app: App.Info) => Promise<T>,
) {
export async function provide<T>(input: Input, cb: (app: App.Info) => Promise<T>) {
log.info("creating", {
cwd: input.cwd,
})
const git = await Filesystem.findUp(".git", input.cwd).then(([x]) =>
x ? path.dirname(x) : undefined,
)
const git = await Filesystem.findUp(".git", input.cwd).then(([x]) => (x ? path.dirname(x) : undefined))
log.info("git", { git })
const data = path.join(
Global.Path.data,
"project",
git ? directory(git) : "global",
)
const data = path.join(Global.Path.data, "project", git ? directory(git) : "global")
const stateFile = Bun.file(path.join(data, APP_JSON))
const state = (await stateFile.json().catch(() => ({}))) as {
initialized: number
@@ -79,7 +69,6 @@ export namespace App {
const root = git ?? input.cwd
const info: Info = {
user: os.userInfo().username,
hostname: os.hostname(),
time: {
initialized: state.initialized,
+20 -26
View File
@@ -4,20 +4,18 @@ import { Auth } from "./index"
export namespace AuthAnthropic {
const CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
export async function authorize() {
export async function authorize(mode: "max" | "console") {
const pkce = await generatePKCE()
const url = new URL("https://claude.ai/oauth/authorize", import.meta.url)
const url = new URL(
`https://${mode === "console" ? "console.anthropic.com" : "claude.ai"}/oauth/authorize`,
import.meta.url,
)
url.searchParams.set("code", "true")
url.searchParams.set("client_id", CLIENT_ID)
url.searchParams.set("response_type", "code")
url.searchParams.set(
"redirect_uri",
"https://console.anthropic.com/oauth/code/callback",
)
url.searchParams.set(
"scope",
"org:create_api_key user:profile user:inference",
)
url.searchParams.set("redirect_uri", "https://console.anthropic.com/oauth/code/callback")
url.searchParams.set("scope", "org:create_api_key user:profile user:inference")
url.searchParams.set("code_challenge", pkce.challenge)
url.searchParams.set("code_challenge_method", "S256")
url.searchParams.set("state", pkce.verifier)
@@ -45,32 +43,28 @@ export namespace AuthAnthropic {
})
if (!result.ok) throw new ExchangeFailed()
const json = await result.json()
await Auth.set("anthropic", {
type: "oauth",
return {
refresh: json.refresh_token as string,
access: json.access_token as string,
expires: Date.now() + json.expires_in * 1000,
})
}
}
export async function access() {
const info = await Auth.get("anthropic")
if (!info || info.type !== "oauth") return
if (info.access && info.expires > Date.now()) return info.access
const response = await fetch(
"https://console.anthropic.com/v1/oauth/token",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
grant_type: "refresh_token",
refresh_token: info.refresh,
client_id: CLIENT_ID,
}),
const response = await fetch("https://console.anthropic.com/v1/oauth/token", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
)
body: JSON.stringify({
grant_type: "refresh_token",
refresh_token: info.refresh,
client_id: CLIENT_ID,
}),
})
if (!response.ok) return
const json = await response.json()
await Auth.set("anthropic", {
+3 -4
View File
@@ -4,13 +4,12 @@ import path from "path"
export const AuthCopilot = lazy(async () => {
const file = Bun.file(path.join(Global.Path.state, "plugin", "copilot.ts"))
const response = fetch(
"https://raw.githubusercontent.com/sst/opencode-github-copilot/refs/heads/main/auth.ts",
)
const exists = await file.exists()
const response = fetch("https://raw.githubusercontent.com/sst/opencode-github-copilot/refs/heads/main/auth.ts")
.then((x) => Bun.write(file, x))
.catch(() => {})
if (!file.exists()) {
if (!exists) {
const worked = await response
if (!worked) return
}
+1 -4
View File
@@ -122,10 +122,7 @@ export namespace AuthGithubCopilot {
return tokenData.token
}
export const DeviceCodeError = NamedError.create(
"DeviceCodeError",
z.object({}),
)
export const DeviceCodeError = NamedError.create("DeviceCodeError", z.object({}))
export const TokenExchangeError = NamedError.create(
"TokenExchangeError",
+7 -1
View File
@@ -16,7 +16,13 @@ export namespace Auth {
key: z.string(),
})
export const Info = z.discriminatedUnion("type", [Oauth, Api])
export const WellKnown = z.object({
type: z.literal("wellknown"),
key: z.string(),
token: z.string(),
})
export const Info = z.discriminatedUnion("type", [Oauth, Api, WellKnown])
export type Info = z.infer<typeof Info>
const filepath = path.join(Global.Path.data, "auth.json")
+30 -13
View File
@@ -8,10 +8,7 @@ import { readableStreamToText } from "bun"
export namespace BunProc {
const log = Log.create({ service: "bun" })
export async function run(
cmd: string[],
options?: Bun.SpawnOptions.OptionsObject<any, any, any>,
) {
export async function run(cmd: string[], options?: Bun.SpawnOptions.OptionsObject<any, any, any>) {
log.info("running", {
cmd: [which(), ...cmd],
...options,
@@ -26,9 +23,17 @@ export namespace BunProc {
BUN_BE_BUN: "1",
},
})
const code = await result.exited;
const stdout = result.stdout ? typeof result.stdout === "number" ? result.stdout : await readableStreamToText(result.stdout) : undefined
const stderr = result.stderr ? typeof result.stderr === "number" ? result.stderr : await readableStreamToText(result.stderr) : undefined
const code = await result.exited
const stdout = result.stdout
? typeof result.stdout === "number"
? result.stdout
: await readableStreamToText(result.stdout)
: undefined
const stderr = result.stderr
? typeof result.stderr === "number"
? result.stderr
: await readableStreamToText(result.stderr)
: undefined
log.info("done", {
code,
stdout,
@@ -55,13 +60,23 @@ export namespace BunProc {
export async function install(pkg: string, version = "latest") {
const mod = path.join(Global.Path.cache, "node_modules", pkg)
const pkgjson = Bun.file(path.join(Global.Path.cache, "package.json"))
const parsed = await pkgjson.json().catch(() => ({
dependencies: {},
}))
const parsed = await pkgjson.json().catch(async () => {
const result = { dependencies: {} }
await Bun.write(pkgjson.name!, JSON.stringify(result, null, 2))
return result
})
if (parsed.dependencies[pkg] === version) return mod
parsed.dependencies[pkg] = version
await Bun.write(pkgjson, JSON.stringify(parsed, null, 2))
await BunProc.run(["install", "--registry=https://registry.npmjs.org"], {
// Build command arguments
const args = ["add", "--force", "--exact", "--cwd", Global.Path.cache, pkg + "@" + version]
// 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, {
cwd: Global.Path.cache,
}).catch((e) => {
throw new InstallFailedError(
@@ -71,6 +86,8 @@ export namespace BunProc {
},
)
})
parsed.dependencies[pkg] = version
await Bun.write(pkgjson.name!, JSON.stringify(parsed, null, 2))
return mod
}
}
+2 -8
View File
@@ -18,10 +18,7 @@ export namespace Bus {
const registry = new Map<string, EventDefinition>()
export function event<Type extends string, Properties extends ZodType>(
type: Type,
properties: Properties,
) {
export function event<Type extends string, Properties extends ZodType>(type: Type, properties: Properties) {
const result = {
type,
properties,
@@ -72,10 +69,7 @@ export namespace Bus {
export function subscribe<Definition extends EventDefinition>(
def: Definition,
callback: (event: {
type: Definition["type"]
properties: z.infer<Definition["properties"]>
}) => void,
callback: (event: { type: Definition["type"]; properties: z.infer<Definition["properties"]> }) => void,
) {
return raw(def.type, callback)
}
+5 -6
View File
@@ -1,20 +1,19 @@
import { App } from "../app/app"
import { ConfigHooks } from "../config/hooks"
import { FileWatcher } from "../file/watch"
import { Format } from "../format"
import { LSP } from "../lsp"
import { Plugin } from "../plugin"
import { Share } from "../share/share"
import { Snapshot } from "../snapshot"
export async function bootstrap<T>(
input: App.Input,
cb: (app: App.Info) => Promise<T>,
) {
export async function bootstrap<T>(input: App.Input, cb: (app: App.Info) => Promise<T>) {
return App.provide(input, async (app) => {
Share.init()
Format.init()
Plugin.init()
ConfigHooks.init()
LSP.init()
FileWatcher.init()
Snapshot.init()
return cb(app)
})
+110
View File
@@ -0,0 +1,110 @@
import { cmd } from "./cmd"
import * as prompts from "@clack/prompts"
import { UI } from "../ui"
import { Global } from "../../global"
import { Agent } from "../../agent/agent"
import path from "path"
import matter from "gray-matter"
import { App } from "../../app/app"
const AgentCreateCommand = cmd({
command: "create",
describe: "create a new agent",
async handler() {
await App.provide({ cwd: process.cwd() }, async (app) => {
UI.empty()
prompts.intro("Create agent")
let scope: "global" | "project" = "global"
if (app.git) {
const scopeResult = await prompts.select({
message: "Location",
options: [
{
label: "Current project",
value: "project" as const,
hint: app.path.root,
},
{
label: "Global",
value: "global" as const,
hint: Global.Path.config,
},
],
})
if (prompts.isCancel(scopeResult)) throw new UI.CancelledError()
scope = scopeResult
}
const query = await prompts.text({
message: "Description",
placeholder: "What should this agent do?",
validate: (x) => x && (x.length > 0 ? undefined : "Required"),
})
if (prompts.isCancel(query)) throw new UI.CancelledError()
const spinner = prompts.spinner()
spinner.start("Generating agent configuration...")
const generated = await Agent.generate({ description: query })
spinner.stop(`Agent ${generated.identifier} generated`)
const availableTools = [
"bash",
"read",
"write",
"edit",
"list",
"glob",
"grep",
"webfetch",
"task",
"todowrite",
"todoread",
]
const selectedTools = await prompts.multiselect({
message: "Select tools to enable",
options: availableTools.map((tool) => ({
label: tool,
value: tool,
})),
initialValues: availableTools,
})
if (prompts.isCancel(selectedTools)) throw new UI.CancelledError()
const tools: Record<string, boolean> = {}
for (const tool of availableTools) {
if (!selectedTools.includes(tool)) {
tools[tool] = false
}
}
const frontmatter: any = {
description: generated.whenToUse,
}
if (Object.keys(tools).length > 0) {
frontmatter.tools = tools
}
const content = matter.stringify(generated.systemPrompt, frontmatter)
const filePath = path.join(
scope === "global" ? Global.Path.config : path.join(app.path.root, ".opencode"),
`agent`,
`${generated.identifier}.md`,
)
await Bun.write(filePath, content)
prompts.log.success(`Agent created: ${filePath}`)
prompts.outro("Done")
})
},
})
export const AgentCommand = cmd({
command: "agent",
describe: "manage agents",
builder: (yargs) => yargs.command(AgentCreateCommand).demandCommand(),
async handler() {},
})
+114 -38
View File
@@ -15,11 +15,7 @@ export const AuthCommand = cmd({
command: "auth",
describe: "manage credentials",
builder: (yargs) =>
yargs
.command(AuthLoginCommand)
.command(AuthLogoutCommand)
.command(AuthListCommand)
.demandCommand(),
yargs.command(AuthLoginCommand).command(AuthLogoutCommand).command(AuthListCommand).demandCommand(),
async handler() {},
})
@@ -31,9 +27,7 @@ export const AuthListCommand = cmd({
UI.empty()
const authPath = path.join(Global.Path.data, "auth.json")
const homedir = os.homedir()
const displayPath = authPath.startsWith(homedir)
? authPath.replace(homedir, "~")
: authPath
const displayPath = authPath.startsWith(homedir) ? authPath.replace(homedir, "~") : authPath
prompts.intro(`Credentials ${UI.Style.TEXT_DIM}${displayPath}`)
const results = await Auth.all().then((x) => Object.entries(x))
const database = await ModelsDev.get()
@@ -67,25 +61,56 @@ export const AuthListCommand = cmd({
prompts.log.info(`${provider} ${UI.Style.TEXT_DIM}${envVar}`)
}
prompts.outro(`${activeEnvVars.length} environment variables`)
prompts.outro(`${activeEnvVars.length} environment variable` + (activeEnvVars.length === 1 ? "" : "s"))
}
},
})
export const AuthLoginCommand = cmd({
command: "login",
command: "login [url]",
describe: "log in to a provider",
async handler() {
builder: (yargs) =>
yargs.positional("url", {
describe: "opencode auth provider",
type: "string",
}),
async handler(args) {
UI.empty()
prompts.intro("Add credential")
if (args.url) {
const wellknown = await fetch(`${args.url}/.well-known/opencode`).then((x) => x.json())
prompts.log.info(`Running \`${wellknown.auth.command.join(" ")}\``)
const proc = Bun.spawn({
cmd: wellknown.auth.command,
stdout: "pipe",
})
const exit = await proc.exited
if (exit !== 0) {
prompts.log.error("Failed")
prompts.outro("Done")
return
}
const token = await new Response(proc.stdout).text()
await Auth.set(args.url, {
type: "wellknown",
key: wellknown.auth.env,
token: token.trim(),
})
prompts.log.success("Logged into " + args.url)
prompts.outro("Done")
return
}
await ModelsDev.refresh().catch(() => {})
const providers = await ModelsDev.get()
const priority: Record<string, number> = {
anthropic: 0,
"github-copilot": 1,
openai: 2,
google: 3,
openrouter: 4,
vercel: 5,
}
let provider = await prompts.select({
let provider = await prompts.autocomplete({
message: "Select provider",
maxItems: 8,
options: [
@@ -114,8 +139,7 @@ export const AuthLoginCommand = cmd({
if (provider === "other") {
provider = await prompts.text({
message: "Enter provider id",
validate: (x) =>
x.match(/^[a-z-]+$/) ? undefined : "a-z 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\//, "")
@@ -127,7 +151,7 @@ export const AuthLoginCommand = cmd({
if (provider === "amazon-bedrock") {
prompts.log.info(
"Amazon bedrock can be configured with standard AWS environment variables like AWS_PROFILE or AWS_ACCESS_KEY_ID",
"Amazon bedrock can be configured with standard AWS environment variables like AWS_BEARER_TOKEN_BEDROCK, AWS_PROFILE or AWS_ACCESS_KEY_ID",
)
prompts.outro("Done")
return
@@ -139,20 +163,24 @@ export const AuthLoginCommand = cmd({
options: [
{
label: "Claude Pro/Max",
value: "oauth",
value: "max",
},
{
label: "API Key",
label: "Create API Key",
value: "console",
},
{
label: "Manually enter API Key",
value: "api",
},
],
})
if (prompts.isCancel(method)) throw new UI.CancelledError()
if (method === "oauth") {
if (method === "max") {
// some weird bug where program exits without this
await new Promise((resolve) => setTimeout(resolve, 10))
const { url, verifier } = await AuthAnthropic.authorize()
const { url, verifier } = await AuthAnthropic.authorize("max")
prompts.note("Trying to open browser...")
try {
await open(url)
@@ -165,17 +193,70 @@ 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()
await AuthAnthropic.exchange(code, verifier)
.then(() => {
prompts.log.success("Login successful")
try {
const credentials = await AuthAnthropic.exchange(code, verifier)
await Auth.set("anthropic", {
type: "oauth",
refresh: credentials.refresh,
access: credentials.access,
expires: credentials.expires,
})
.catch(() => {
prompts.log.error("Invalid code")
prompts.log.success("Login successful")
} catch {
prompts.log.error("Invalid code")
}
prompts.outro("Done")
return
}
if (method === "console") {
// some weird bug where program exits without this
await new Promise((resolve) => setTimeout(resolve, 10))
const { url, verifier } = await AuthAnthropic.authorize("console")
prompts.note("Trying to open browser...")
try {
await open(url)
} catch (e) {
prompts.log.error(
"Failed to open browser perhaps you are running without a display or X server, please open the following URL in your browser:",
)
}
prompts.log.info(url)
const code = await prompts.text({
message: "Paste the authorization code here: ",
validate: (x) => x && (x.length > 0 ? undefined : "Required"),
})
if (prompts.isCancel(code)) throw new UI.CancelledError()
try {
const credentials = await AuthAnthropic.exchange(code, verifier)
const accessToken = credentials.access
const response = await fetch("https://api.anthropic.com/api/oauth/claude_cli/create_api_key", {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json, text/plain, */*",
},
})
if (!response.ok) {
throw new Error("Failed to create API key")
}
const json = await response.json()
await Auth.set("anthropic", {
type: "api",
key: json.raw_key,
})
prompts.log.success("Login successful - API key created and saved")
} catch (error) {
prompts.log.error("Invalid code or failed to create API key")
}
prompts.outro("Done")
return
}
@@ -186,17 +267,13 @@ export const AuthLoginCommand = cmd({
await new Promise((resolve) => setTimeout(resolve, 10))
const deviceInfo = await copilot.authorize()
prompts.note(
`Please visit: ${deviceInfo.verification}\nEnter code: ${deviceInfo.user}`,
)
prompts.note(`Please visit: ${deviceInfo.verification}\nEnter code: ${deviceInfo.user}`)
const spinner = prompts.spinner()
spinner.start("Waiting for authorization...")
while (true) {
await new Promise((resolve) =>
setTimeout(resolve, deviceInfo.interval * 1000),
)
await new Promise((resolve) => setTimeout(resolve, deviceInfo.interval * 1000))
const response = await copilot.poll(deviceInfo.device)
if (response.status === "pending") continue
if (response.status === "success") {
@@ -219,9 +296,13 @@ export const AuthLoginCommand = cmd({
return
}
if (provider === "vercel") {
prompts.log.info("You can create an api key in the dashboard")
}
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, {
@@ -248,12 +329,7 @@ export const AuthLogoutCommand = cmd({
const providerID = await prompts.select({
message: "Select provider",
options: credentials.map(([key, value]) => ({
label:
(database[key]?.name || key) +
UI.Style.TEXT_DIM +
" (" +
value.type +
")",
label: (database[key]?.name || key) + UI.Style.TEXT_DIM + " (" + value.type + ")",
value: key,
})),
})
+1 -2
View File
@@ -31,7 +31,6 @@ const FileStatusCommand = cmd({
export const FileCommand = cmd({
command: "file",
builder: (yargs) =>
yargs.command(FileReadCommand).command(FileStatusCommand).demandCommand(),
builder: (yargs) => yargs.command(FileReadCommand).command(FileStatusCommand).demandCommand(),
async handler() {},
})
+14 -3
View File
@@ -1,8 +1,10 @@
import { Global } from "../../../global"
import { bootstrap } from "../../bootstrap"
import { cmd } from "../cmd"
import { FileCommand } from "./file"
import { LSPCommand } from "./lsp"
import { RipgrepCommand } from "./ripgrep"
import { ScrapCommand } from "./scrap"
import { SnapshotCommand } from "./snapshot"
export const DebugCommand = cmd({
@@ -12,17 +14,26 @@ export const DebugCommand = cmd({
.command(LSPCommand)
.command(RipgrepCommand)
.command(FileCommand)
.command(ScrapCommand)
.command(SnapshotCommand)
.command(PathsCommand)
.command({
command: "wait",
async handler() {
await bootstrap({ cwd: process.cwd() }, async () => {
await new Promise((resolve) =>
setTimeout(resolve, 1_000 * 60 * 60 * 24),
)
await new Promise((resolve) => setTimeout(resolve, 1_000 * 60 * 60 * 24))
})
},
})
.demandCommand(),
async handler() {},
})
const PathsCommand = cmd({
command: "paths",
handler() {
for (const [key, value] of Object.entries(Global.Path)) {
console.log(key.padEnd(10), value)
}
},
})
+15 -6
View File
@@ -6,14 +6,13 @@ import { Log } from "../../../util/log"
export const LSPCommand = cmd({
command: "lsp",
builder: (yargs) =>
yargs.command(DiagnosticsCommand).command(SymbolsCommand).demandCommand(),
yargs.command(DiagnosticsCommand).command(SymbolsCommand).command(DocumentSymbolsCommand).demandCommand(),
async handler() {},
})
const DiagnosticsCommand = cmd({
command: "diagnostics <file>",
builder: (yargs) =>
yargs.positional("file", { type: "string", demandOption: true }),
builder: (yargs) => yargs.positional("file", { type: "string", demandOption: true }),
async handler(args) {
await bootstrap({ cwd: process.cwd() }, async () => {
await LSP.touchFile(args.file, true)
@@ -24,14 +23,24 @@ const DiagnosticsCommand = cmd({
export const SymbolsCommand = cmd({
command: "symbols <query>",
builder: (yargs) =>
yargs.positional("query", { type: "string", demandOption: true }),
builder: (yargs) => yargs.positional("query", { type: "string", demandOption: true }),
async handler(args) {
await bootstrap({ cwd: process.cwd() }, async () => {
await LSP.touchFile("./src/index.ts", true)
using _ = Log.Default.time("symbols")
const results = await LSP.workspaceSymbol(args.query)
console.log(JSON.stringify(results, null, 2))
})
},
})
export const DocumentSymbolsCommand = cmd({
command: "document-symbols <uri>",
builder: (yargs) => yargs.positional("uri", { type: "string", demandOption: true }),
async handler(args) {
await bootstrap({ cwd: process.cwd() }, async () => {
using _ = Log.Default.time("document-symbols")
const results = await LSP.documentSymbol(args.uri)
console.log(JSON.stringify(results, null, 2))
})
},
})
@@ -5,12 +5,7 @@ import { cmd } from "../cmd"
export const RipgrepCommand = cmd({
command: "rg",
builder: (yargs) =>
yargs
.command(TreeCommand)
.command(FilesCommand)
.command(SearchCommand)
.demandCommand(),
builder: (yargs) => yargs.command(TreeCommand).command(FilesCommand).command(SearchCommand).demandCommand(),
async handler() {},
})
@@ -50,7 +45,7 @@ const FilesCommand = cmd({
const files = await Ripgrep.files({
cwd: app.path.cwd,
query: args.query,
glob: args.glob,
glob: args.glob ? [args.glob] : undefined,
limit: args.limit,
})
console.log(files.join("\n"))
@@ -0,0 +1,7 @@
import { cmd } from "../cmd"
export const ScrapCommand = cmd({
command: "scrap",
builder: (yargs) => yargs,
async handler() {},
})
+24 -15
View File
@@ -4,36 +4,45 @@ import { cmd } from "../cmd"
export const SnapshotCommand = cmd({
command: "snapshot",
builder: (yargs) =>
yargs
.command(SnapshotCreateCommand)
.command(SnapshotRestoreCommand)
.demandCommand(),
builder: (yargs) => yargs.command(TrackCommand).command(PatchCommand).command(DiffCommand).demandCommand(),
async handler() {},
})
export const SnapshotCreateCommand = cmd({
command: "create",
const TrackCommand = cmd({
command: "track",
async handler() {
await bootstrap({ cwd: process.cwd() }, async () => {
const result = await Snapshot.create("test")
console.log(result)
console.log(await Snapshot.track())
})
},
})
export const SnapshotRestoreCommand = cmd({
command: "restore <commit>",
const PatchCommand = cmd({
command: "patch <hash>",
builder: (yargs) =>
yargs.positional("commit", {
yargs.positional("hash", {
type: "string",
description: "commit",
description: "hash",
demandOption: true,
}),
async handler(args) {
await bootstrap({ cwd: process.cwd() }, async () => {
await Snapshot.restore("test", args.commit)
console.log("restored")
console.log(await Snapshot.patch(args.hash))
})
},
})
const DiffCommand = cmd({
command: "diff <hash>",
builder: (yargs) =>
yargs.positional("hash", {
type: "string",
description: "hash",
demandOption: true,
}),
async handler(args) {
await bootstrap({ cwd: process.cwd() }, async () => {
console.log(await Snapshot.diff(args.hash))
})
},
})
+1 -9
View File
@@ -1,18 +1,10 @@
import { Server } from "../../server/server"
import fs from "fs/promises"
import path from "path"
import type { CommandModule } from "yargs"
export const GenerateCommand = {
command: "generate",
handler: async () => {
const specs = await Server.openapi()
const dir = "gen"
await fs.rmdir(dir, { recursive: true }).catch(() => {})
await fs.mkdir(dir, { recursive: true })
await Bun.write(
path.join(dir, "openapi.json"),
JSON.stringify(specs, null, 2),
)
process.stdout.write(JSON.stringify(specs, null, 2))
},
} satisfies CommandModule
File diff suppressed because it is too large Load Diff
+80
View File
@@ -0,0 +1,80 @@
import { cmd } from "./cmd"
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
import * as prompts from "@clack/prompts"
import { UI } from "../ui"
export const McpCommand = cmd({
command: "mcp",
builder: (yargs) => yargs.command(McpAddCommand).demandCommand(),
async handler() {},
})
export const McpAddCommand = cmd({
command: "add",
describe: "add an MCP server",
async handler() {
UI.empty()
prompts.intro("Add MCP server")
const name = await prompts.text({
message: "Enter MCP server name",
validate: (x) => x && (x.length > 0 ? undefined : "Required"),
})
if (prompts.isCancel(name)) throw new UI.CancelledError()
const type = await prompts.select({
message: "Select MCP server type",
options: [
{
label: "Local",
value: "local",
hint: "Run a local command",
},
{
label: "Remote",
value: "remote",
hint: "Connect to a remote URL",
},
],
})
if (prompts.isCancel(type)) throw new UI.CancelledError()
if (type === "local") {
const command = await prompts.text({
message: "Enter command to run",
placeholder: "e.g., opencode x @modelcontextprotocol/server-filesystem",
validate: (x) => x && (x.length > 0 ? undefined : "Required"),
})
if (prompts.isCancel(command)) throw new UI.CancelledError()
prompts.log.info(`Local MCP server "${name}" configured with command: ${command}`)
prompts.outro("MCP server added successfully")
return
}
if (type === "remote") {
const url = await prompts.text({
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"
},
})
if (prompts.isCancel(url)) throw new UI.CancelledError()
const client = new Client({
name: "opencode",
version: "1.0.0",
})
const transport = new StreamableHTTPClientTransport(new URL(url))
await client.connect(transport)
prompts.log.info(`Remote MCP server "${name}" configured with URL: ${url}`)
}
prompts.outro("MCP server added successfully")
},
})
+80 -43
View File
@@ -2,12 +2,14 @@ import type { Argv } from "yargs"
import { Bus } from "../../bus"
import { Provider } from "../../provider/provider"
import { Session } from "../../session"
import { Message } from "../../session/message"
import { UI } from "../ui"
import { cmd } from "./cmd"
import { Flag } from "../../flag/flag"
import { Config } from "../../config/config"
import { bootstrap } from "../bootstrap"
import { MessageV2 } from "../../session/message-v2"
import { Identifier } from "../../id/id"
import { Agent } from "../../agent/agent"
const TOOL: Record<string, [string, string]> = {
todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
@@ -52,13 +54,22 @@ export const RunCommand = cmd({
alias: ["m"],
describe: "model to use in the format of provider/model",
})
.option("agent", {
type: "string",
describe: "agent to use",
})
},
handler: async (args) => {
const message = args.message.join(" ")
let message = args.message.join(" ")
if (!process.stdin.isTTY) message += "\n" + (await Bun.stdin.text())
await bootstrap({ cwd: process.cwd() }, async () => {
const session = await (async () => {
if (args.continue) {
const first = await Session.list().next()
const list = Session.list()
const first = await list.next()
await list.return()
if (first.done) return
return first.value
}
@@ -73,32 +84,39 @@ export const RunCommand = cmd({
return
}
const isPiped = !process.stdout.isTTY
UI.empty()
UI.println(UI.logo())
UI.empty()
UI.println(UI.Style.TEXT_NORMAL_BOLD + "> ", message)
UI.empty()
const cfg = await Config.get()
if (cfg.autoshare || Flag.OPENCODE_AUTO_SHARE || args.share) {
await Session.share(session.id)
UI.println(
UI.Style.TEXT_INFO_BOLD +
"~ https://opencode.ai/s/" +
session.id.slice(-8),
)
if (cfg.share === "auto" || Flag.OPENCODE_AUTO_SHARE || args.share) {
try {
await Session.share(session.id)
UI.println(UI.Style.TEXT_INFO_BOLD + "~ https://opencode.ai/s/" + session.id.slice(-8))
} catch (error) {
if (error instanceof Error && error.message.includes("disabled")) {
UI.println(UI.Style.TEXT_DANGER_BOLD + "! " + error.message)
} else {
throw error
}
}
}
UI.empty()
const { providerID, modelID } = args.model
? Provider.parseModel(args.model)
: await Provider.defaultModel()
UI.println(
UI.Style.TEXT_NORMAL_BOLD + "@ ",
UI.Style.TEXT_NORMAL + `${providerID}/${modelID}`,
)
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()
function printEvent(color: string, type: string, title: string) {
@@ -110,52 +128,71 @@ export const RunCommand = cmd({
)
}
Bus.subscribe(Message.Event.PartUpdated, async (evt) => {
if (evt.properties.sessionID !== session.id) return
let text = ""
Bus.subscribe(MessageV2.Event.PartUpdated, async (evt) => {
if (evt.properties.part.sessionID !== session.id) return
if (evt.properties.part.messageID === messageID) return
const part = evt.properties.part
const message = await Session.getMessage(
evt.properties.sessionID,
evt.properties.messageID,
)
if (
part.type === "tool-invocation" &&
part.toolInvocation.state === "result"
) {
const metadata = message.metadata.tool[part.toolInvocation.toolCallId]
const [tool, color] = TOOL[part.toolInvocation.toolName] ?? [
part.toolInvocation.toolName,
UI.Style.TEXT_INFO_BOLD,
]
printEvent(color, tool, metadata?.title || "Unknown")
if (part.type === "tool" && part.state.status === "completed") {
const [tool, color] = TOOL[part.tool] ?? [part.tool, UI.Style.TEXT_INFO_BOLD]
const title =
part.state.title || Object.keys(part.state.input).length > 0 ? JSON.stringify(part.state.input) : "Unknown"
printEvent(color, tool, title)
}
if (part.type === "text") {
if (part.text.includes("\n")) {
text = part.text
if (part.time?.end) {
UI.empty()
UI.println(part.text)
UI.println(UI.markdown(text))
UI.empty()
text = ""
return
}
printEvent(UI.Style.TEXT_NORMAL_BOLD, "Text", part.text)
}
})
let errorMsg: string | undefined
Bus.subscribe(Session.Event.Error, async (evt) => {
const { sessionID, error } = evt.properties
if (sessionID !== session.id || !error) return
let err = String(error.name)
if ("data" in error && error.data && "message" in error.data) {
err = error.data.message
}
errorMsg = errorMsg ? errorMsg + "\n" + err : err
UI.error(err)
})
const messageID = Identifier.ascending("message")
const result = await Session.chat({
sessionID: session.id,
providerID,
modelID,
messageID,
...(agent.model
? agent.model
: {
providerID,
modelID,
}),
agent: agent.name,
parts: [
{
id: Identifier.ascending("part"),
type: "text",
text: message,
},
],
})
const isPiped = !process.stdout.isTTY
if (isPiped) {
const match = result.parts.findLast((x) => x.type === "text")
if (match) process.stdout.write(match.text)
if (match) process.stdout.write(UI.markdown(match.text))
if (errorMsg) process.stdout.write(errorMsg)
}
UI.empty()
})
+1 -5
View File
@@ -1,6 +1,5 @@
import { Provider } from "../../provider/provider"
import { Server } from "../../server/server"
import { Share } from "../../share/share"
import { bootstrap } from "../bootstrap"
import { cmd } from "./cmd"
@@ -32,15 +31,12 @@ export const ServeCommand = cmd({
const hostname = args.hostname
const port = args.port
await Share.init()
const server = Server.listen({
port,
hostname,
})
console.log(
`opencode server listening on http://${server.hostname}:${server.port}`,
)
console.log(`opencode server listening on http://${server.hostname}:${server.port}`)
await new Promise(() => {})
+98
View File
@@ -0,0 +1,98 @@
import { cmd } from "./cmd"
interface SessionStats {
totalSessions: number
totalMessages: number
totalCost: number
totalTokens: {
input: number
output: number
reasoning: number
cache: {
read: number
write: number
}
}
toolUsage: Record<string, number>
dateRange: {
earliest: number
latest: number
}
days: number
costPerDay: number
}
export const StatsCommand = cmd({
command: "stats",
handler: async () => {},
})
export function displayStats(stats: SessionStats) {
const width = 56
function renderRow(label: string, value: string): string {
const availableWidth = width - 1
const paddingNeeded = availableWidth - label.length - value.length
const padding = Math.max(0, paddingNeeded)
return `${label}${" ".repeat(padding)}${value}`
}
// Overview section
console.log("┌────────────────────────────────────────────────────────┐")
console.log("│ OVERVIEW │")
console.log("├────────────────────────────────────────────────────────┤")
console.log(renderRow("Sessions", stats.totalSessions.toLocaleString()))
console.log(renderRow("Messages", stats.totalMessages.toLocaleString()))
console.log(renderRow("Days", stats.days.toString()))
console.log("└────────────────────────────────────────────────────────┘")
console.log()
// Cost & Tokens section
console.log("┌────────────────────────────────────────────────────────┐")
console.log("│ COST & TOKENS │")
console.log("├────────────────────────────────────────────────────────┤")
const cost = isNaN(stats.totalCost) ? 0 : stats.totalCost
const costPerDay = isNaN(stats.costPerDay) ? 0 : stats.costPerDay
console.log(renderRow("Total Cost", `$${cost.toFixed(2)}`))
console.log(renderRow("Cost/Day", `$${costPerDay.toFixed(2)}`))
console.log(renderRow("Input", formatNumber(stats.totalTokens.input)))
console.log(renderRow("Output", formatNumber(stats.totalTokens.output)))
console.log(renderRow("Cache Read", formatNumber(stats.totalTokens.cache.read)))
console.log(renderRow("Cache Write", formatNumber(stats.totalTokens.cache.write)))
console.log("└────────────────────────────────────────────────────────┘")
console.log()
// Tool Usage section
if (Object.keys(stats.toolUsage).length > 0) {
const sortedTools = Object.entries(stats.toolUsage)
.sort(([, a], [, b]) => b - a)
.slice(0, 10)
console.log("┌────────────────────────────────────────────────────────┐")
console.log("│ TOOL USAGE │")
console.log("├────────────────────────────────────────────────────────┤")
const maxCount = Math.max(...sortedTools.map(([, count]) => count))
const totalToolUsage = Object.values(stats.toolUsage).reduce((a, b) => a + b, 0)
for (const [tool, count] of sortedTools) {
const barLength = Math.max(1, Math.floor((count / maxCount) * 20))
const bar = "█".repeat(barLength)
const percentage = ((count / totalToolUsage) * 100).toFixed(1)
const content = ` ${tool.padEnd(10)} ${bar.padEnd(20)} ${count.toString().padStart(3)} (${percentage.padStart(4)}%)`
const padding = Math.max(0, width - content.length)
console.log(`${content}${" ".repeat(padding)}`)
}
console.log("└────────────────────────────────────────────────────────┘")
}
console.log()
}
function formatNumber(num: number): string {
if (num >= 1000000) {
return (num / 1000000).toFixed(1) + "M"
} else if (num >= 1000) {
return (num / 1000).toFixed(1) + "K"
}
return num.toString()
}
+73 -18
View File
@@ -9,15 +9,55 @@ import fs from "fs/promises"
import { Installation } from "../../installation"
import { Config } from "../../config/config"
import { Bus } from "../../bus"
import { Log } from "../../util/log"
import { FileWatcher } from "../../file/watch"
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]",
describe: "start opencode tui",
builder: (yargs) =>
yargs.positional("project", {
type: "string",
describe: "path to start opencode in",
}),
yargs
.positional("project", {
type: "string",
describe: "path to start opencode in",
})
.option("model", {
type: "string",
alias: ["m"],
describe: "model to use in the format of provider/model",
})
.option("prompt", {
alias: ["p"],
type: "string",
describe: "prompt to use",
})
.option("mode", {
type: "string",
describe: "mode to use",
})
.option("port", {
type: "number",
describe: "port to listen on",
default: 0,
})
.option("hostname", {
alias: ["h"],
type: "string",
describe: "hostname to listen on",
default: "127.0.0.1",
}),
handler: async (args) => {
while (true) {
const cwd = args.project ? path.resolve(args.project) : process.cwd()
@@ -28,45 +68,54 @@ export const TuiCommand = cmd({
return
}
const result = await bootstrap({ cwd }, async (app) => {
FileWatcher.init()
const providers = await Provider.list()
if (Object.keys(providers).length === 0) {
return "needs_provider"
}
const server = Server.listen({
port: 0,
hostname: "127.0.0.1",
port: args.port,
hostname: args.hostname,
})
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
let cwd = Bun.fileURLToPath(new URL("../../../../tui/cmd/opencode", import.meta.url))
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()
cmd = [binary]
}
Log.Default.info("tui", {
cmd,
})
const proc = Bun.spawn({
cmd: [...cmd, ...process.argv.slice(2)],
cmd: [
...cmd,
...(args.model ? ["--model", args.model] : []),
...(args.prompt ? ["--prompt", args.prompt] : []),
...(args.mode ? ["--mode", args.mode] : []),
],
cwd,
stdout: "inherit",
stderr: "inherit",
stdin: "inherit",
env: {
...process.env,
CGO_ENABLED: "0",
OPENCODE_SERVER: server.url.toString(),
OPENCODE_APP_INFO: JSON.stringify(app),
OPENCODE_AGENTS: JSON.stringify(await Agent.list()),
},
onExit: () => {
server.stop()
@@ -74,7 +123,7 @@ export const TuiCommand = cmd({
})
;(async () => {
if (Installation.VERSION === "dev") return
if (Installation.isDev()) return
if (Installation.isSnapshot()) return
const config = await Config.global()
if (config.autoupdate === false) return
@@ -84,9 +133,15 @@ export const TuiCommand = cmd({
const method = await Installation.method()
if (method === "unknown") return
await Installation.upgrade(method, latest)
.then(() => {
Bus.publish(Installation.Event.Updated, { version: latest })
})
.then(() => Bus.publish(Installation.Event.Updated, { version: latest }))
.catch(() => {})
})()
;(async () => {
if (Ide.alreadyInstalled()) return
const ide = Ide.ide()
if (ide === "unknown") return
await Ide.install(ide)
.then(() => Bus.publish(Ide.Event.Installed, { ide }))
.catch(() => {})
})()
+10 -6
View File
@@ -27,22 +27,26 @@ export const UpgradeCommand = {
const detectedMethod = await Installation.method()
const method = (args.method as Installation.Method) ?? detectedMethod
if (method === "unknown") {
prompts.log.error(
`opencode is installed to ${process.execPath} and seems to be managed by a package manager`,
)
prompts.log.error(`opencode is installed to ${process.execPath} and seems to be managed by a package manager`)
prompts.outro("Done")
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`)
prompts.outro("Done")
return
}
prompts.log.info(`From ${Installation.VERSION}${target}`)
const spinner = prompts.spinner()
spinner.start("Upgrading...")
const err = await Installation.upgrade(method, target).catch((err) => err)
if (err) {
spinner.stop("Upgrade failed")
if (err instanceof Installation.UpgradeFailedError)
prompts.log.error(err.data.stderr)
if (err instanceof Installation.UpgradeFailedError) prompts.log.error(err.data.stderr)
else if (err instanceof Error) prompts.log.error(err.message)
prompts.outro("Done")
return
+6 -5
View File
@@ -5,14 +5,15 @@ 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`,
...(input.data.issues?.map(
(issue) => "↳ " + issue.message + " " + issue.path.join("."),
) ?? []),
...(input.data.issues?.map((issue) => "↳ " + issue.message + " " + issue.path.join(".")) ?? []),
].join("\n")
if (UI.CancelledError.isInstance(input)) return ""
+4
View File
@@ -76,4 +76,8 @@ export namespace UI {
export function error(message: string) {
println(Style.TEXT_DANGER_BOLD + "Error: " + Style.TEXT_NORMAL + message)
}
export function markdown(text: string): string {
return text
}
}
+353 -118
View File
@@ -1,26 +1,134 @@
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"
import { ModelsDev } from "../provider/models"
import { mergeDeep } from "remeda"
import { mergeDeep, pipe } from "remeda"
import { Global } from "../global"
import fs from "fs/promises"
import { lazy } from "../util/lazy"
import { NamedError } from "../util/error"
import matter from "gray-matter"
import { Flag } from "../flag/flag"
import { Auth } from "../auth"
import { type ParseError as JsoncParseError, parse as parseJsonc, printParseErrorCode } from "jsonc-parser"
export namespace Config {
const log = Log.create({ service: "config" })
export const state = App.state("config", async (app) => {
const auth = await Auth.all()
let result = await global()
for (const file of ["opencode.jsonc", "opencode.json"]) {
const found = await Filesystem.findUp(file, app.path.cwd, app.path.root)
for (const resolved of found.toReversed()) {
result = mergeDeep(result, await load(resolved))
result = mergeDeep(result, await loadFile(resolved))
}
}
// Override with custom config if provided
if (Flag.OPENCODE_CONFIG) {
result = mergeDeep(result, await loadFile(Flag.OPENCODE_CONFIG))
log.debug("loaded custom config", { path: Flag.OPENCODE_CONFIG })
}
for (const [key, value] of Object.entries(auth)) {
if (value.type === "wellknown") {
process.env[value.key] = value.token
const wellknown = await fetch(`${key}/.well-known/opencode`).then((x) => x.json())
result = mergeDeep(result, await load(JSON.stringify(wellknown.config ?? {}), process.cwd()))
}
}
result.agent = result.agent || {}
const markdownAgents = [
...(await Filesystem.globUp("agent/*.md", Global.Path.config, Global.Path.config)),
...(await Filesystem.globUp(".opencode/agent/*.md", app.path.cwd, app.path.root)),
]
for (const item of markdownAgents) {
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.agent = mergeDeep(result.agent, {
[config.name]: parsed.data,
})
continue
}
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"
}
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")
result.username = os.userInfo().username
}
log.info("loaded", result)
return result
@@ -29,18 +137,12 @@ export namespace Config {
export const McpLocal = z
.object({
type: z.literal("local").describe("Type of MCP server connection"),
command: z
.string()
.array()
.describe("Command and arguments to run the MCP server"),
command: z.string().array().describe("Command and arguments to run the MCP server"),
environment: z
.record(z.string(), z.string())
.optional()
.describe("Environment variables to set when running the MCP server"),
enabled: z
.boolean()
.optional()
.describe("Enable or disable the MCP server on startup"),
enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"),
})
.strict()
.openapi({
@@ -51,10 +153,8 @@ export namespace Config {
.object({
type: z.literal("remote").describe("Type of MCP server connection"),
url: z.string().describe("URL of the remote MCP server"),
enabled: z
.boolean()
.optional()
.describe("Enable or disable the MCP server on startup"),
enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"),
headers: z.record(z.string(), z.string()).optional().describe("Headers to send with the request"),
})
.strict()
.openapi({
@@ -64,122 +164,185 @@ export namespace Config {
export const Mcp = z.discriminatedUnion("type", [McpLocal, McpRemote])
export type Mcp = z.infer<typeof Mcp>
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: "AgentConfig",
})
export type Agent = z.infer<typeof Agent>
export const Keybinds = z
.object({
leader: z
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("none").describe("@deprecated use switch_agent. Next mode"),
switch_mode_reverse: z
.string()
.optional()
.describe("Leader key for keybind combinations"),
help: z.string().optional().describe("Show help dialog"),
editor_open: z.string().optional().describe("Open external editor"),
session_new: z.string().optional().describe("Create a new session"),
session_list: z.string().optional().describe("List all sessions"),
session_share: z.string().optional().describe("Share current session"),
session_interrupt: z
.string()
.optional()
.describe("Interrupt current session"),
session_compact: z
.string()
.optional()
.describe("Toggle compact mode for session"),
tool_details: z.string().optional().describe("Show tool details"),
model_list: z.string().optional().describe("List available models"),
theme_list: z.string().optional().describe("List available themes"),
project_init: z
.string()
.optional()
.describe("Initialize project configuration"),
input_clear: z.string().optional().describe("Clear input field"),
input_paste: z.string().optional().describe("Paste from clipboard"),
input_submit: z.string().optional().describe("Submit input"),
input_newline: z.string().optional().describe("Insert newline in input"),
history_previous: z
.string()
.optional()
.describe("Navigate to previous history item"),
history_next: z
.string()
.optional()
.describe("Navigate to next history item"),
messages_page_up: z
.string()
.optional()
.describe("Scroll messages up by one page"),
messages_page_down: z
.string()
.optional()
.describe("Scroll messages down by one page"),
messages_half_page_up: z
.string()
.optional()
.describe("Scroll messages up by half page"),
.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"),
session_list: z.string().optional().default("<leader>l").describe("List all sessions"),
session_share: z.string().optional().default("<leader>s").describe("Share current session"),
session_unshare: z.string().optional().default("none").describe("Unshare current session"),
session_interrupt: z.string().optional().default("esc").describe("Interrupt current session"),
session_compact: z.string().optional().default("<leader>c").describe("Compact the session"),
tool_details: z.string().optional().default("<leader>d").describe("Toggle tool details"),
model_list: z.string().optional().default("<leader>m").describe("List available models"),
theme_list: z.string().optional().default("<leader>t").describe("List available themes"),
file_list: z.string().optional().default("<leader>f").describe("List files"),
file_close: z.string().optional().default("esc").describe("Close file"),
file_search: z.string().optional().default("<leader>/").describe("Search file"),
file_diff_toggle: z.string().optional().default("<leader>v").describe("Split/unified diff"),
project_init: z.string().optional().default("<leader>i").describe("Create/update AGENTS.md"),
input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"),
input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"),
input_submit: z.string().optional().default("enter").describe("Submit input"),
input_newline: z.string().optional().default("shift+enter,ctrl+j").describe("Insert newline in input"),
messages_page_up: z.string().optional().default("pgup").describe("Scroll messages up by one page"),
messages_page_down: z.string().optional().default("pgdown").describe("Scroll messages down by one page"),
messages_half_page_up: z.string().optional().default("ctrl+alt+u").describe("Scroll messages up by half page"),
messages_half_page_down: z
.string()
.optional()
.default("ctrl+alt+d")
.describe("Scroll messages down by half page"),
messages_previous: z
.string()
.optional()
.describe("Navigate to previous message"),
messages_next: z.string().optional().describe("Navigate to next message"),
messages_first: z
.string()
.optional()
.describe("Navigate to first message"),
messages_last: z.string().optional().describe("Navigate to last message"),
app_exit: z.string().optional().describe("Exit the application"),
messages_previous: z.string().optional().default("ctrl+up").describe("Navigate to previous message"),
messages_next: z.string().optional().default("ctrl+down").describe("Navigate to next message"),
messages_first: z.string().optional().default("ctrl+g").describe("Navigate to first message"),
messages_last: z.string().optional().default("ctrl+alt+g").describe("Navigate to last message"),
messages_layout_toggle: z.string().optional().default("<leader>p").describe("Toggle layout"),
messages_copy: z.string().optional().default("<leader>y").describe("Copy message"),
messages_revert: z.string().optional().default("none").describe("@deprecated use messages_undo. Revert message"),
messages_undo: z.string().optional().default("<leader>u").describe("Undo message"),
messages_redo: z.string().optional().default("<leader>r").describe("Redo message"),
app_exit: z.string().optional().default("ctrl+c,<leader>q").describe("Exit the application"),
})
.strict()
.openapi({
ref: "KeybindsConfig",
})
export const Layout = z.enum(["auto", "stretch"]).openapi({
ref: "LayoutConfig",
})
export type Layout = z.infer<typeof Layout>
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
.object({
$schema: z
.string()
.optional()
.describe("JSON schema reference for configuration validation"),
theme: z
.string()
.optional()
.describe("Theme name to use for the interface"),
$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()
.describe(
"Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing",
),
autoshare: z
.boolean()
.optional()
.describe("Share newly created sessions automatically"),
autoupdate: z
.boolean()
.optional()
.describe("Automatically update to the latest version"),
disabled_providers: z
.array(z.string())
.optional()
.describe("Disable providers that are loaded automatically"),
model: z
.describe("@deprecated Use 'share' field instead. Share newly created sessions automatically"),
autoupdate: z.boolean().optional().describe("Automatically update to the latest version"),
disabled_providers: z.array(z.string()).optional().describe("Disable providers that are loaded automatically"),
model: z.string().describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(),
small_model: z
.string()
.describe(
"Model to use in the format of provider/model, eg anthropic/claude-2",
)
.describe("Small model to use for tasks like title generation in the format of provider/model")
.optional(),
username: z
.string()
.optional()
.describe("Custom username to display in conversations instead of system username"),
mode: z
.object({
build: Agent.optional(),
plan: Agent.optional(),
})
.catchall(Agent)
.optional()
.describe("@deprecated Use `agent` field instead."),
agent: z
.object({
plan: Agent.optional(),
build: Agent.optional(),
general: Agent.optional(),
})
.catchall(Agent)
.optional()
.describe("Agent configuration, see https://opencode.ai/docs/agent"),
provider: z
.record(
ModelsDev.Provider.partial().extend({
models: z.record(ModelsDev.Model.partial()),
options: z.record(z.any()).optional(),
}),
ModelsDev.Provider.partial()
.extend({
models: z.record(ModelsDev.Model.partial()),
options: z
.object({
apiKey: z.string().optional(),
baseURL: z.string().optional(),
})
.catchall(z.any())
.optional(),
})
.strict(),
)
.optional()
.describe("Custom provider configurations and model overrides"),
mcp: z
.record(z.string(), Mcp)
.optional()
.describe("MCP (Model Context Protocol) server configurations"),
instructions: z
.array(z.string())
.optional()
.describe("Additional instruction files or patterns to include"),
mcp: z.record(z.string(), Mcp).optional().describe("MCP (Model Context Protocol) server configurations"),
formatter: z
.record(
z.string(),
z.object({
disabled: z.boolean().optional(),
command: z.array(z.string()).optional(),
environment: z.record(z.string(), z.string()).optional(),
extensions: z.array(z.string()).optional(),
}),
)
.optional(),
lsp: z
.record(
z.string(),
z.union([
z.object({
disabled: z.literal(true),
}),
z.object({
command: z.array(z.string()),
extensions: z.array(z.string()).optional(),
disabled: z.boolean().optional(),
env: z.record(z.string(), z.string()).optional(),
initialization: z.record(z.string(), z.any()).optional(),
}),
]),
)
.optional(),
instructions: z.array(z.string()).optional().describe("Additional instruction files or patterns to include"),
layout: Layout.optional().describe("@deprecated Always uses stretch layout."),
permission: z
.object({
edit: Permission.optional(),
bash: z.union([Permission, z.record(z.string(), Permission)]).optional(),
})
.optional(),
experimental: z
.object({
hook: z
@@ -215,7 +378,12 @@ export namespace Config {
export type Info = z.output<typeof Info>
export const global = lazy(async () => {
let result = await load(path.join(Global.Path.config, "config.json"))
let result: Info = pipe(
{},
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"), {
with: {
@@ -227,10 +395,7 @@ export namespace Config {
if (provider && model) result.model = `${provider}/${model}`
result["$schema"] = "https://opencode.ai/config.json"
result = mergeDeep(result, rest)
await Bun.write(
path.join(Global.Path.config, "config.json"),
JSON.stringify(result, null, 2),
)
await Bun.write(path.join(Global.Path.config, "config.json"), JSON.stringify(result, null, 2))
await fs.unlink(path.join(Global.Path.config, "config"))
})
.catch(() => {})
@@ -238,23 +403,93 @@ export namespace Config {
return result
})
async function load(path: string) {
const data = await Bun.file(path)
.json()
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 }, { cause: err })
if (err.code === "ENOENT") return
throw new JsonError({ path: filepath }, { cause: err })
})
const parsed = Info.safeParse(data)
if (parsed.success) return parsed.data
throw new InvalidError({ path, issues: parsed.error.issues })
if (!text) return {}
return load(text, filepath)
}
async function load(text: string, filepath: string) {
text = text.replace(/\{env:([^}]+)\}/g, (_, varName) => {
return process.env[varName] || ""
})
const fileMatches = text.match(/\{file:[^}]+\}/g)
if (fileMatches) {
const configDir = path.dirname(filepath)
const lines = text.split("\n")
for (const match of fileMatches) {
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()).trim()
// escape newlines/quotes, strip outer quotes
text = text.replace(match, JSON.stringify(fileContent).slice(1, -1))
}
}
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(filepath, JSON.stringify(parsed.data, null, 2))
}
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: filepath, issues: parsed.error.issues })
}
export const JsonError = NamedError.create(
"ConfigJsonError",
z.object({
path: z.string(),
message: z.string().optional(),
}),
)
+6 -4
View File
@@ -22,9 +22,7 @@ export namespace ConfigHooks {
command: item.command,
})
Bun.spawn({
cmd: item.command.map((x) =>
x.replace("$FILE", payload.properties.file),
),
cmd: item.command.map((x) => x.replace("$FILE", payload.properties.file)),
env: item.environment,
cwd: app.path.cwd,
stdout: "ignore",
@@ -33,9 +31,13 @@ export namespace ConfigHooks {
}
})
Bus.subscribe(Session.Event.Idle, async () => {
Bus.subscribe(Session.Event.Idle, async (payload) => {
const cfg = await Config.get()
if (cfg.experimental?.hook?.session_completed) {
const session = await Session.get(payload.properties.sessionID)
// Only fire hook for top-level sessions (not subagent sessions)
if (session.parentID) return
for (const item of cfg.experimental.hook.session_completed) {
log.info("session_completed", {
command: item.command,
+26 -13
View File
@@ -5,6 +5,7 @@ import { z } from "zod"
import { NamedError } from "../util/error"
import { lazy } from "../util/lazy"
import { Log } from "../util/log"
import { ZipReader, BlobReader, BlobWriter } from "@zip.js/zip.js"
export namespace Fzf {
const log = Log.create({ service: "fzf" })
@@ -86,20 +87,32 @@ export namespace Fzf {
})
}
if (config.extension === "zip") {
const proc = Bun.spawn(
["unzip", "-j", archivePath, "fzf.exe", "-d", Global.Path.bin],
{
cwd: Global.Path.bin,
stderr: "pipe",
stdout: "ignore",
},
)
await proc.exited
if (proc.exitCode !== 0)
const zipFileReader = new ZipReader(new BlobReader(new Blob([await Bun.file(archivePath).arrayBuffer()])));
const entries = await zipFileReader.getEntries();
let fzfEntry: any;
for (const entry of entries) {
if (entry.filename === "fzf.exe") {
fzfEntry = entry;
break;
}
}
if (!fzfEntry) {
throw new ExtractionFailedError({
filepath: archivePath,
stderr: await Bun.readableStreamToText(proc.stderr),
})
stderr: "fzf.exe not found in zip archive",
});
}
const fzfBlob = await fzfEntry.getData(new BlobWriter());
if (!fzfBlob) {
throw new ExtractionFailedError({
filepath: archivePath,
stderr: "Failed to extract fzf.exe from zip archive",
});
}
await Bun.write(filepath, await fzfBlob.arrayBuffer());
await zipFileReader.close();
}
await fs.unlink(archivePath)
if (process.platform !== "win32") await fs.chmod(filepath, 0o755)
@@ -114,4 +127,4 @@ export namespace Fzf {
const { filepath } = await state()
return filepath
}
}
}
+23 -28
View File
@@ -11,6 +11,19 @@ import { Log } from "../util/log"
export namespace File {
const log = Log.create({ service: "file" })
export const Info = z
.object({
path: z.string(),
added: z.number().int(),
removed: z.number().int(),
status: z.enum(["added", "deleted", "modified"]),
})
.openapi({
ref: "File",
})
export type Info = z.infer<typeof Info>
export const Event = {
Edited: Bus.event(
"file.edited",
@@ -24,20 +37,16 @@ export namespace File {
const app = App.info()
if (!app.git) return []
const diffOutput = await $`git diff --numstat HEAD`
.cwd(app.path.cwd)
.quiet()
.nothrow()
.text()
const diffOutput = await $`git diff --numstat HEAD`.cwd(app.path.cwd).quiet().nothrow().text()
const changedFiles = []
const changedFiles: Info[] = []
if (diffOutput.trim()) {
const lines = diffOutput.trim().split("\n")
for (const line of lines) {
const [added, removed, filepath] = line.split("\t")
changedFiles.push({
file: filepath,
path: filepath,
added: added === "-" ? 0 : parseInt(added, 10),
removed: removed === "-" ? 0 : parseInt(removed, 10),
status: "modified",
@@ -45,22 +54,16 @@ export namespace File {
}
}
const untrackedOutput = await $`git ls-files --others --exclude-standard`
.cwd(app.path.cwd)
.quiet()
.nothrow()
.text()
const untrackedOutput = await $`git ls-files --others --exclude-standard`.cwd(app.path.cwd).quiet().nothrow().text()
if (untrackedOutput.trim()) {
const untrackedFiles = untrackedOutput.trim().split("\n")
for (const filepath of untrackedFiles) {
try {
const content = await Bun.file(
path.join(app.path.root, filepath),
).text()
const content = await Bun.file(path.join(app.path.root, filepath)).text()
const lines = content.split("\n").length
changedFiles.push({
file: filepath,
path: filepath,
added: lines,
removed: 0,
status: "added",
@@ -72,17 +75,13 @@ export namespace File {
}
// Get deleted files
const deletedOutput = await $`git diff --name-only --diff-filter=D HEAD`
.cwd(app.path.cwd)
.quiet()
.nothrow()
.text()
const deletedOutput = await $`git diff --name-only --diff-filter=D HEAD`.cwd(app.path.cwd).quiet().nothrow().text()
if (deletedOutput.trim()) {
const deletedFiles = deletedOutput.trim().split("\n")
for (const filepath of deletedFiles) {
changedFiles.push({
file: filepath,
path: filepath,
added: 0,
removed: 0, // Could get original line count but would require another git command
status: "deleted",
@@ -92,7 +91,7 @@ export namespace File {
return changedFiles.map((x) => ({
...x,
file: path.relative(app.path.cwd, path.join(app.path.root, x.file)),
path: path.relative(app.path.cwd, path.join(app.path.root, x.path)),
}))
}
@@ -112,11 +111,7 @@ export namespace File {
filepath: rel,
})
if (diff !== "unmodified") {
const original = await $`git show HEAD:${rel}`
.cwd(app.path.root)
.quiet()
.nothrow()
.text()
const original = await $`git show HEAD:${rel}`.cwd(app.path.root).quiet().nothrow().text()
const patch = createPatch(file, original, content, "old", "new", {
context: Infinity,
})
+45 -45
View File
@@ -7,6 +7,7 @@ import { NamedError } from "../util/error"
import { lazy } from "../util/lazy"
import { $ } from "bun"
import { Fzf } from "./fzf"
import { ZipReader, BlobReader, BlobWriter } from "@zip.js/zip.js"
export namespace Ripgrep {
const Stats = z.object({
@@ -122,15 +123,11 @@ export namespace Ripgrep {
const state = lazy(async () => {
let filepath = Bun.which("rg")
if (filepath) return { filepath }
filepath = path.join(
Global.Path.bin,
"rg" + (process.platform === "win32" ? ".exe" : ""),
)
filepath = path.join(Global.Path.bin, "rg" + (process.platform === "win32" ? ".exe" : ""))
const file = Bun.file(filepath)
if (!(await file.exists())) {
const platformKey =
`${process.arch}-${process.platform}` as keyof typeof PLATFORM
const platformKey = `${process.arch}-${process.platform}` as keyof typeof PLATFORM
const config = PLATFORM[platformKey]
if (!config) throw new UnsupportedPlatformError({ platform: platformKey })
@@ -139,8 +136,7 @@ export namespace Ripgrep {
const url = `https://github.com/BurntSushi/ripgrep/releases/download/${version}/${filename}`
const response = await fetch(url)
if (!response.ok)
throw new DownloadFailedError({ url, status: response.status })
if (!response.ok) throw new DownloadFailedError({ url, status: response.status })
const buffer = await response.arrayBuffer()
const archivePath = path.join(Global.Path.bin, filename)
@@ -164,20 +160,34 @@ export namespace Ripgrep {
})
}
if (config.extension === "zip") {
const proc = Bun.spawn(
["unzip", "-j", archivePath, "*/rg.exe", "-d", Global.Path.bin],
{
cwd: Global.Path.bin,
stderr: "pipe",
stdout: "ignore",
},
)
await proc.exited
if (proc.exitCode !== 0)
throw new ExtractionFailedError({
filepath: archivePath,
stderr: await Bun.readableStreamToText(proc.stderr),
})
if (config.extension === "zip") {
const zipFileReader = new ZipReader(new BlobReader(new Blob([await Bun.file(archivePath).arrayBuffer()])))
const entries = await zipFileReader.getEntries()
let rgEntry: any
for (const entry of entries) {
if (entry.filename.endsWith("rg.exe")) {
rgEntry = entry
break
}
}
if (!rgEntry) {
throw new ExtractionFailedError({
filepath: archivePath,
stderr: "rg.exe not found in zip archive",
})
}
const rgBlob = await rgEntry.getData(new BlobWriter())
if (!rgBlob) {
throw new ExtractionFailedError({
filepath: archivePath,
stderr: "Failed to extract rg.exe from zip archive",
})
}
await Bun.write(filepath, await rgBlob.arrayBuffer())
await zipFileReader.close()
}
}
await fs.unlink(archivePath)
if (!platformKey.endsWith("-win32")) await fs.chmod(filepath, 0o755)
@@ -193,17 +203,16 @@ export namespace Ripgrep {
return filepath
}
export async function files(input: {
cwd: string
query?: string
glob?: string
limit?: number
}) {
const commands = [
`${await filepath()} --files --hidden --glob='!.git/*' ${input.glob ? `--glob='${input.glob}'` : ``}`,
]
if (input.query)
commands.push(`${await Fzf.filepath()} --filter=${input.query}`)
export async function files(input: { cwd: string; query?: string; glob?: string[]; limit?: number }) {
const commands = [`${$.escape(await filepath())} --files --follow --hidden --glob='!.git/*'`]
if (input.glob) {
for (const g of input.glob) {
commands[0] += ` --glob='${g}'`
}
}
if (input.query) commands.push(`${await Fzf.filepath()} --filter=${input.query}`)
if (input.limit) commands.push(`head -n ${input.limit}`)
const joined = commands.join(" | ")
const result = await $`${{ raw: joined }}`.cwd(input.cwd).nothrow().text()
@@ -240,6 +249,7 @@ export namespace Ripgrep {
children: [],
}
for (const file of files) {
if (file.includes(".opencode")) continue
const parts = file.split(path.sep)
getPath(root, parts, true)
}
@@ -310,18 +320,8 @@ export namespace Ripgrep {
return lines.join("\n")
}
export async function search(input: {
cwd: string
pattern: string
glob?: string[]
limit?: number
}) {
const args = [
`${await filepath()}`,
"--json",
"--hidden",
"--glob='!.git/*'",
]
export async function search(input: { cwd: string; pattern: string; glob?: string[]; limit?: number }) {
const args = [`${await filepath()}`, "--json", "--hidden", "--glob='!.git/*'"]
if (input.glob) {
for (const g of input.glob) {
+4 -4
View File
@@ -1,6 +1,8 @@
import { App } from "../app/app"
import { Log } from "../util/log"
export namespace FileTime {
const log = Log.create({ service: "file.time" })
export const state = App.state("tool.filetimes", () => {
const read: {
[sessionID: string]: {
@@ -13,6 +15,7 @@ export namespace FileTime {
})
export function read(sessionID: string, file: string) {
log.info("read", { sessionID, file })
const { read } = state()
read[sessionID] = read[sessionID] || {}
read[sessionID][file] = new Date()
@@ -24,10 +27,7 @@ export namespace FileTime {
export async function assert(sessionID: string, filepath: string) {
const time = get(sessionID, filepath)
if (!time)
throw new Error(
`You must read the file ${filepath} before overwriting it. Use the Read tool first`,
)
if (!time) throw new Error(`You must read the file ${filepath} before overwriting it. Use the Read tool first`)
const stats = await Bun.file(filepath).stat()
if (stats.mtime.getTime() > time.getTime()) {
throw new Error(
+30 -30
View File
@@ -3,6 +3,7 @@ import { Bus } from "../bus"
import fs from "fs"
import { App } from "../app/app"
import { Log } from "../util/log"
import { Flag } from "../flag/flag"
export namespace FileWatcher {
const log = Log.create({ service: "file.watcher" })
@@ -16,37 +17,36 @@ export namespace FileWatcher {
}),
),
}
const state = App.state(
"file.watcher",
() => {
const app = App.use()
if (!app.info.git) return {}
try {
const watcher = fs.watch(app.info.path.cwd, { recursive: true }, (event, file) => {
log.info("change", { file, event })
if (!file) return
// for some reason async local storage is lost here
// https://github.com/oven-sh/bun/issues/20754
App.provideExisting(app, async () => {
Bus.publish(Event.Updated, {
file,
event,
})
})
})
return { watcher }
} catch {
return {}
}
},
async (state) => {
state.watcher?.close()
},
)
export function init() {
App.state(
"file.watcher",
() => {
const app = App.use()
try {
const watcher = fs.watch(
app.info.path.cwd,
{ recursive: true },
(event, file) => {
log.info("change", { file, event })
if (!file) return
// for some reason async local storage is lost here
// https://github.com/oven-sh/bun/issues/20754
App.provideExisting(app, async () => {
Bus.publish(Event.Updated, {
file,
event,
})
})
},
)
return { watcher }
} catch {
return {}
}
},
async (state) => {
state.watcher?.close()
},
)()
if (Flag.OPENCODE_DISABLE_WATCHER || true) return
state()
}
}
+2
View File
@@ -1,5 +1,7 @@
export namespace Flag {
export const OPENCODE_AUTO_SHARE = truthy("OPENCODE_AUTO_SHARE")
export const OPENCODE_DISABLE_WATCHER = truthy("OPENCODE_DISABLE_WATCHER")
export const OPENCODE_CONFIG = process.env["OPENCODE_CONFIG"]
function truthy(key: string) {
const value = process.env[key]?.toLowerCase()
+77 -34
View File
@@ -1,5 +1,6 @@
import { App } from "../app/app"
import { BunProc } from "../bun"
import { Filesystem } from "../util/filesystem"
export interface Info {
name: string
@@ -29,7 +30,7 @@ export const mix: Info = {
export const prettier: Info = {
name: "prettier",
command: [BunProc.which(), "run", "prettier", "--write", "$FILE"],
command: [BunProc.which(), "x", "prettier", "--write", "$FILE"],
environment: {
BUN_BE_BUN: "1",
},
@@ -62,23 +63,55 @@ export const prettier: Info = {
".gql",
],
async enabled() {
// this is more complicated because we only want to use prettier if it's
// being used with the current project
try {
const proc = Bun.spawn({
cmd: [BunProc.which(), "run", "prettier", "--version"],
cwd: App.info().path.cwd,
env: {
BUN_BE_BUN: "1",
},
stdout: "ignore",
stderr: "ignore",
})
const exit = await proc.exited
return exit === 0
} catch {
return false
const app = App.info()
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
},
}
@@ -94,23 +127,11 @@ export const zig: Info = {
export const clang: Info = {
name: "clang-format",
command: ["clang-format", "-i", "$FILE"],
extensions: [
".c",
".cc",
".cpp",
".cxx",
".c++",
".h",
".hh",
".hpp",
".hxx",
".h++",
".ino",
".C",
".H",
],
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
},
}
@@ -128,7 +149,29 @@ export const ruff: Info = {
command: ["ruff", "format", "$FILE"],
extensions: [".py", ".pyi"],
async enabled() {
return Bun.which("ruff") !== null
if (!Bun.which("ruff")) return false
const app = App.info()
const configs = ["pyproject.toml", "ruff.toml", ".ruff.toml"]
for (const config of configs) {
const found = await Filesystem.findUp(config, app.path.cwd, app.path.root)
if (found.length > 0) {
if (config === "pyproject.toml") {
const content = await Bun.file(found[0]).text()
if (content.includes("[tool.ruff]")) return true
} else {
return true
}
}
}
const deps = ["requirements.txt", "pyproject.toml", "Pipfile"]
for (const dep of deps) {
const found = await Filesystem.findUp(dep, app.path.cwd, app.path.root)
if (found.length > 0) {
const content = await Bun.file(found[0]).text()
if (content.includes("ruff")) return true
}
}
return false
},
}
+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)
+13 -3
View File
@@ -13,7 +13,7 @@ export namespace Global {
export const Path = {
data,
bin: path.join(data, "bin"),
providers: path.join(config, "providers"),
log: path.join(data, "log"),
cache,
config,
state,
@@ -23,7 +23,17 @@ export namespace Global {
await Promise.all([
fs.mkdir(Global.Path.data, { recursive: true }),
fs.mkdir(Global.Path.config, { recursive: true }),
fs.mkdir(Global.Path.cache, { recursive: true }),
fs.mkdir(Global.Path.providers, { recursive: true }),
fs.mkdir(Global.Path.state, { recursive: true }),
fs.mkdir(Global.Path.log, { recursive: true }),
])
const CACHE_VERSION = "4"
const version = await Bun.file(path.join(Global.Path.cache, "version"))
.text()
.catch(() => "0")
if (version !== CACHE_VERSION) {
await fs.rm(Global.Path.cache, { recursive: true, force: true })
await Bun.file(path.join(Global.Path.cache, "version")).write(CACHE_VERSION)
}
+6 -17
View File
@@ -5,7 +5,9 @@ export namespace Identifier {
const prefixes = {
session: "ses",
message: "msg",
permission: "per",
user: "usr",
part: "prt",
} as const
export function schema(prefix: keyof typeof prefixes) {
@@ -26,11 +28,7 @@ export namespace Identifier {
return generateID(prefix, true, given)
}
function generateID(
prefix: keyof typeof prefixes,
descending: boolean,
given?: string,
): string {
function generateID(prefix: keyof typeof prefixes, descending: boolean, given?: string): string {
if (!given) {
return generateNewID(prefix, descending)
}
@@ -42,8 +40,7 @@ export namespace Identifier {
}
function randomBase62(length: number): string {
const chars =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
let result = ""
const bytes = randomBytes(length)
for (let i = 0; i < length; i++) {
@@ -52,10 +49,7 @@ export namespace Identifier {
return result
}
function generateNewID(
prefix: keyof typeof prefixes,
descending: boolean,
): string {
function generateNewID(prefix: keyof typeof prefixes, descending: boolean): string {
const currentTimestamp = Date.now()
if (currentTimestamp !== lastTimestamp) {
@@ -73,11 +67,6 @@ export namespace Identifier {
timeBytes[i] = Number((now >> BigInt(40 - 8 * i)) & BigInt(0xff))
}
return (
prefixes[prefix] +
"_" +
timeBytes.toString("hex") +
randomBase62(LENGTH - 12)
)
return prefixes[prefix] + "_" + timeBytes.toString("hex") + randomBase62(LENGTH - 12)
}
}
+74
View File
@@ -0,0 +1,74 @@
import { spawn } from "bun"
import { z } from "zod"
import { NamedError } from "../util/error"
import { Log } from "../util/log"
import { Bus } from "../bus"
const SUPPORTED_IDES = [
{ name: "Windsurf" as const, cmd: "windsurf" },
{ name: "Visual Studio Code" as const, cmd: "code" },
{ name: "Cursor" as const, cmd: "cursor" },
{ name: "VSCodium" as const, cmd: "codium" },
]
export namespace Ide {
const log = Log.create({ service: "ide" })
export const Event = {
Installed: Bus.event(
"ide.installed",
z.object({
ide: z.string(),
}),
),
}
export const AlreadyInstalledError = NamedError.create("AlreadyInstalledError", z.object({}))
export const InstallFailedError = NamedError.create(
"InstallFailedError",
z.object({
stderr: z.string(),
}),
)
export function ide() {
if (process.env["TERM_PROGRAM"] === "vscode") {
const v = process.env["GIT_ASKPASS"]
for (const ide of SUPPORTED_IDES) {
if (v?.includes(ide.name)) return ide.name
}
}
return "unknown"
}
export function alreadyInstalled() {
return process.env["OPENCODE_CALLER"] === "vscode"
}
export async function install(ide: (typeof SUPPORTED_IDES)[number]["name"]) {
const cmd = SUPPORTED_IDES.find((i) => i.name === ide)?.cmd
if (!cmd) throw new Error(`Unknown IDE: ${ide}`)
const p = spawn([cmd, "--install-extension", "sst-dev.opencode"], {
stdout: "pipe",
stderr: "pipe",
})
await p.exited
const stdout = await new Response(p.stdout).text()
const stderr = await new Response(p.stderr).text()
log.info("installed", {
ide,
stdout,
stderr,
})
if (p.exitCode !== 0) {
throw new InstallFailedError({ stderr })
}
if (stdout.includes("already installed")) {
throw new AlreadyInstalledError({})
}
}
}
+29 -10
View File
@@ -1,10 +1,13 @@
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"
import { GenerateCommand } from "./cli/cmd/generate"
import { Log } from "./util/log"
import { AuthCommand } from "./cli/cmd/auth"
import { AgentCommand } from "./cli/cmd/agent"
import { UpgradeCommand } from "./cli/cmd/upgrade"
import { ModelsCommand } from "./cli/cmd/models"
import { UI } from "./cli/ui"
@@ -14,6 +17,9 @@ import { FormatError } from "./cli/error"
import { ServeCommand } from "./cli/cmd/serve"
import { TuiCommand } from "./cli/cmd/tui"
import { DebugCommand } from "./cli/cmd/debug"
import { StatsCommand } from "./cli/cmd/stats"
import { McpCommand } from "./cli/cmd/mcp"
import { GithubCommand } from "./cli/cmd/github"
const cancel = new AbortController()
@@ -38,29 +44,45 @@ const cli = yargs(hideBin(process.argv))
describe: "print logs to stderr",
type: "boolean",
})
.middleware(async () => {
await Log.init({ print: process.argv.includes("--print-logs") })
.option("log-level", {
describe: "log level",
type: "string",
choices: ["DEBUG", "INFO", "WARN", "ERROR"],
})
.middleware(async (opts) => {
await Log.init({
print: process.argv.includes("--print-logs"),
dev: Installation.isDev(),
level: (() => {
if (opts.logLevel) return opts.logLevel as Log.Level
if (Installation.isDev()) return "DEBUG"
return "INFO"
})(),
})
Log.Default.info("opencode", {
version: Installation.VERSION,
args: process.argv.slice(2),
})
})
.usage("\n" + UI.logo())
.command(McpCommand)
.command(TuiCommand)
.command(RunCommand)
.command(GenerateCommand)
.command(DebugCommand)
.command(AuthCommand)
.command(AgentCommand)
.command(UpgradeCommand)
.command(ServeCommand)
.command(ModelsCommand)
.command(StatsCommand)
.command(GithubCommand)
.fail((msg) => {
if (
msg.startsWith("Unknown argument") ||
msg.startsWith("Not enough non-option arguments")
) {
if (msg.startsWith("Unknown argument") || msg.startsWith("Not enough non-option arguments")) {
cli.showHelp("log")
}
process.exit(1)
})
.strict()
@@ -97,10 +119,7 @@ try {
Log.Default.error("fatal", data)
const formatted = FormatError(e)
if (formatted) UI.error(formatted)
if (formatted === undefined)
UI.error(
"Unexpected error, check log file at " + Log.file() + " for more details",
)
if (formatted === undefined) UI.error("Unexpected error, check log file at " + Log.file() + " for more details")
process.exitCode = 1
}
+9 -3
View File
@@ -135,12 +135,18 @@ export namespace Installation {
})
}
export const VERSION =
typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "dev"
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")
.then((res) => res.json())
.then((data) => data.tag_name.slice(1) as string)
.then((data) => {
if (typeof data.tag_name !== "string") {
log.error("GitHub API error", data)
throw new Error("failed to fetch latest version")
}
return data.tag_name.slice(1) as string
})
}
}
+40 -33
View File
@@ -1,9 +1,5 @@
import path from "path"
import {
createMessageConnection,
StreamMessageReader,
StreamMessageWriter,
} from "vscode-jsonrpc/node"
import { createMessageConnection, StreamMessageReader, StreamMessageWriter } from "vscode-jsonrpc/node"
import type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types"
import { App } from "../app/app"
import { Log } from "../util/log"
@@ -38,45 +34,54 @@ export namespace LSPClient {
),
}
export async function create(serverID: string, server: LSPServer.Handle) {
export async function create(input: { serverID: string; server: LSPServer.Handle; root: string }) {
const app = App.info()
log.info("starting client", { id: serverID })
const l = log.clone().tag("serverID", input.serverID)
l.info("starting client")
const connection = createMessageConnection(
new StreamMessageReader(server.process.stdout),
new StreamMessageWriter(server.process.stdin),
new StreamMessageReader(input.server.process.stdout),
new StreamMessageWriter(input.server.process.stdin),
)
const diagnostics = new Map<string, Diagnostic[]>()
connection.onNotification("textDocument/publishDiagnostics", (params) => {
const path = new URL(params.uri).pathname
log.info("textDocument/publishDiagnostics", {
l.info("textDocument/publishDiagnostics", {
path,
})
const exists = diagnostics.has(path)
diagnostics.set(path, params.diagnostics)
if (!exists && serverID === "typescript") return
Bus.publish(Event.Diagnostics, { path, serverID })
if (!exists && input.serverID === "typescript") return
Bus.publish(Event.Diagnostics, { path, serverID: input.serverID })
})
connection.onRequest("window/workDoneProgress/create", (params) => {
l.info("window/workDoneProgress/create", params)
return null
})
connection.onRequest("workspace/configuration", async () => {
return [{}]
})
connection.listen()
log.info("sending initialize", { id: serverID })
l.info("sending initialize")
await withTimeout(
connection.sendRequest("initialize", {
processId: server.process.pid,
rootUri: "file://" + input.root,
processId: input.server.process.pid,
workspaceFolders: [
{
name: "workspace",
uri: "file://" + app.path.cwd,
uri: "file://" + input.root,
},
],
initializationOptions: {
...server.initialization,
...input.server.initialization,
},
capabilities: {
window: {
workDoneProgress: true,
},
workspace: {
configuration: true,
},
@@ -92,28 +97,33 @@ export namespace LSPClient {
},
}),
5_000,
).catch(() => {
throw new InitializeError({ serverID })
).catch((err) => {
l.error("initialize error", { error: err })
throw new InitializeError(
{ serverID: input.serverID },
{
cause: err,
},
)
})
await connection.sendNotification("initialized", {})
log.info("initialized")
const files: {
[path: string]: number
} = {}
const result = {
root: input.root,
get serverID() {
return serverID
return input.serverID
},
get connection() {
return connection
},
notify: {
async open(input: { path: string }) {
input.path = path.isAbsolute(input.path)
? input.path
: path.resolve(app.path.cwd, input.path)
input.path = path.isAbsolute(input.path) ? input.path : path.resolve(app.path.cwd, input.path)
const file = Bun.file(input.path)
const text = await file.text()
const version = files[input.path]
@@ -145,18 +155,13 @@ export namespace LSPClient {
return diagnostics
},
async waitForDiagnostics(input: { path: string }) {
input.path = path.isAbsolute(input.path)
? input.path
: path.resolve(app.path.cwd, input.path)
input.path = path.isAbsolute(input.path) ? input.path : path.resolve(app.path.cwd, input.path)
log.info("waiting for diagnostics", input)
let unsub: () => void
return await withTimeout(
new Promise<void>((resolve) => {
unsub = Bus.subscribe(Event.Diagnostics, (event) => {
if (
event.properties.path === input.path &&
event.properties.serverID === result.serverID
) {
if (event.properties.path === input.path && event.properties.serverID === result.serverID) {
log.info("got diagnostics", input)
unsub?.()
resolve()
@@ -171,14 +176,16 @@ export namespace LSPClient {
})
},
async shutdown() {
log.info("shutting down", { serverID })
l.info("shutting down")
connection.end()
connection.dispose()
server.process.kill("SIGTERM")
log.info("shutdown", { serverID })
input.server.process.kill()
l.info("shutdown")
},
}
l.info("initialized")
return result
}
}
+162 -52
View File
@@ -3,64 +3,94 @@ import { Log } from "../util/log"
import { LSPClient } from "./client"
import path from "path"
import { LSPServer } from "./server"
import { Ripgrep } from "../file/ripgrep"
import { z } from "zod"
import { Config } from "../config/config"
import { spawn } from "child_process"
export namespace LSP {
const log = Log.create({ service: "lsp" })
export const Range = z
.object({
start: z.object({
line: z.number(),
character: z.number(),
}),
end: z.object({
line: z.number(),
character: z.number(),
}),
})
.openapi({
ref: "Range",
})
export type Range = z.infer<typeof Range>
export const Symbol = z
.object({
name: z.string(),
kind: z.number(),
location: z.object({
uri: z.string(),
range: z.object({
start: z.object({
line: z.number(),
character: z.number(),
}),
end: z.object({
line: z.number(),
character: z.number(),
}),
}),
range: Range,
}),
})
.openapi({
ref: "LSP.Symbol",
ref: "Symbol",
})
export type Symbol = z.infer<typeof Symbol>
export const DocumentSymbol = z
.object({
name: z.string(),
detail: z.string().optional(),
kind: z.number(),
range: Range,
selectionRange: Range,
})
.openapi({
ref: "DocumentSymbol",
})
export type DocumentSymbol = z.infer<typeof DocumentSymbol>
const state = App.state(
"lsp",
async (app) => {
log.info("initializing")
const clients = new Map<string, LSPClient.Info>()
for (const server of Object.values(LSPServer)) {
for (const extension of server.extensions) {
const [file] = await Ripgrep.files({
cwd: app.path.cwd,
glob: "*" + extension,
})
if (!file) continue
const handle = await server.spawn(App.info())
if (!handle) break
const client = await LSPClient.create(server.id, handle).catch(
() => {},
)
if (!client) break
clients.set(server.id, client)
break
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,
}
},
}
}
log.info("initialized")
return {
broken: new Set<string>(),
servers,
clients,
}
},
async (state) => {
for (const client of state.clients.values()) {
for (const client of state.clients) {
await client.shutdown()
}
},
@@ -70,16 +100,44 @@ export namespace LSP {
return state()
}
async function getClients(file: string) {
const s = await state()
const extension = path.parse(file).ext
const result: LSPClient.Info[] = []
for (const server of Object.values(LSPServer)) {
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
const match = s.clients.find((x) => x.root === root && x.serverID === server.id)
if (match) {
result.push(match)
continue
}
const handle = await server.spawn(App.info(), root)
if (!handle) continue
const client = await LSPClient.create({
serverID: server.id,
server: handle,
root,
}).catch((err) => {
s.broken.add(root + server.id)
handle.process.kill()
log.error("", { error: err })
})
if (!client) continue
s.clients.push(client)
result.push(client)
}
return result
}
export async function touchFile(input: string, waitForDiagnostics?: boolean) {
const extension = path.parse(input).ext
const matches = Object.values(LSPServer)
.filter((x) => x.extensions.includes(extension))
.map((x) => x.id)
const clients = await getClients(input)
await run(async (client) => {
if (!matches.includes(client.serverID)) return
const wait = waitForDiagnostics
? client.waitForDiagnostics({ path: input })
: Promise.resolve()
if (!clients.includes(client)) return
const wait = waitForDiagnostics ? client.waitForDiagnostics({ path: input }) : Promise.resolve()
await client.notify.open({ path: input })
return wait
})
@@ -97,11 +155,7 @@ export namespace LSP {
return results
}
export async function hover(input: {
file: string
line: number
character: number
}) {
export async function hover(input: { file: string; line: number; character: number }) {
return run((client) => {
return client.connection.sendRequest("textDocument/hover", {
textDocument: {
@@ -115,18 +169,74 @@ export namespace LSP {
})
}
enum SymbolKind {
File = 1,
Module = 2,
Namespace = 3,
Package = 4,
Class = 5,
Method = 6,
Property = 7,
Field = 8,
Constructor = 9,
Enum = 10,
Interface = 11,
Function = 12,
Variable = 13,
Constant = 14,
String = 15,
Number = 16,
Boolean = 17,
Array = 18,
Object = 19,
Key = 20,
Null = 21,
EnumMember = 22,
Struct = 23,
Event = 24,
Operator = 25,
TypeParameter = 26,
}
const kinds = [
SymbolKind.Class,
SymbolKind.Function,
SymbolKind.Method,
SymbolKind.Interface,
SymbolKind.Variable,
SymbolKind.Constant,
SymbolKind.Struct,
SymbolKind.Enum,
]
export async function workspaceSymbol(query: string) {
return run((client) =>
client.connection.sendRequest("workspace/symbol", {
query,
}),
client.connection
.sendRequest("workspace/symbol", {
query,
})
.then((result: any) => result.filter((x: LSP.Symbol) => kinds.includes(x.kind)))
.then((result: any) => result.slice(0, 10))
.catch(() => []),
).then((result) => result.flat() as LSP.Symbol[])
}
async function run<T>(
input: (client: LSPClient.Info) => Promise<T>,
): Promise<T[]> {
const clients = await state().then((x) => [...x.clients.values()])
export async function documentSymbol(uri: string) {
return run((client) =>
client.connection
.sendRequest("textDocument/documentSymbol", {
textDocument: {
uri,
},
})
.catch(() => []),
)
.then((result) => result.flat() as (LSP.DocumentSymbol | LSP.Symbol)[])
.then((result) => result.filter(Boolean))
}
async function run<T>(input: (client: LSPClient.Info) => Promise<T>): Promise<T[]> {
const clients = await state().then((x) => x.clients)
const tasks = clients.map((x) => input(x))
return Promise.all(tasks)
}
+2
View File
@@ -94,4 +94,6 @@ export const LANGUAGE_EXTENSIONS: Record<string, string> = {
".yml": "yaml",
".mjs": "javascript",
".cjs": "javascript",
".zig": "zig",
".zon": "zig",
} as const
+254 -37
View File
@@ -4,6 +4,9 @@ import path from "path"
import { Global } from "../global"
import { Log } from "../util/log"
import { BunProc } from "../bun"
import { $ } from "bun"
import fs from "fs/promises"
import { Filesystem } from "../util/filesystem"
export namespace LSPServer {
const log = Log.create({ service: "lsp.server" })
@@ -13,31 +16,44 @@ export namespace LSPServer {
initialization?: Record<string, any>
}
type RootFunction = (file: string, app: App.Info) => Promise<string | undefined>
const NearestRoot = (patterns: string[]): RootFunction => {
return async (file, app) => {
const files = Filesystem.up({
targets: patterns,
start: path.dirname(file),
stop: app.path.root,
})
const first = await files.next()
await files.return()
if (!first.value) return app.path.root
return path.dirname(first.value)
}
}
export interface Info {
id: string
extensions: string[]
spawn(app: App.Info): Promise<Handle | undefined>
global?: boolean
root: RootFunction
spawn(app: App.Info, root: string): Promise<Handle | undefined>
}
export const Typescript: Info = {
id: "typescript",
root: NearestRoot(["tsconfig.json", "package.json", "jsconfig.json"]),
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
async spawn(app) {
const tsserver = await Bun.resolve(
"typescript/lib/tsserver.js",
app.path.cwd,
).catch(() => {})
async spawn(app, root) {
const tsserver = await Bun.resolve("typescript/lib/tsserver.js", app.path.cwd).catch(() => {})
if (!tsserver) return
const proc = spawn(
BunProc.which(),
["x", "typescript-language-server", "--stdio"],
{
env: {
...process.env,
BUN_BE_BUN: "1",
},
const proc = spawn(BunProc.which(), ["x", "typescript-language-server", "--stdio"], {
cwd: root,
env: {
...process.env,
BUN_BE_BUN: "1",
},
)
})
return {
process: proc,
initialization: {
@@ -51,8 +67,13 @@ export namespace LSPServer {
export const Gopls: Info = {
id: "golang",
root: async (file, app) => {
const work = await NearestRoot(["go.work"])(file, app)
if (work) return work
return NearestRoot(["go.mod", "go.sum"])(file, app)
},
extensions: [".go"],
async spawn() {
async spawn(_, root) {
let bin = Bun.which("gopls", {
PATH: process.env["PATH"] + ":" + Global.Path.bin,
})
@@ -71,24 +92,24 @@ export namespace LSPServer {
log.error("Failed to install gopls")
return
}
bin = path.join(
Global.Path.bin,
"gopls" + (process.platform === "win32" ? ".exe" : ""),
)
bin = path.join(Global.Path.bin, "gopls" + (process.platform === "win32" ? ".exe" : ""))
log.info(`installed gopls`, {
bin,
})
}
return {
process: spawn(bin!),
process: spawn(bin!, {
cwd: root,
}),
}
},
}
export const RubyLsp: Info = {
id: "ruby-lsp",
root: NearestRoot(["Gemfile"]),
extensions: [".rb", ".rake", ".gemspec", ".ru"],
async spawn() {
async spawn(_, root) {
let bin = Bun.which("ruby-lsp", {
PATH: process.env["PATH"] + ":" + Global.Path.bin,
})
@@ -111,16 +132,15 @@ export namespace LSPServer {
log.error("Failed to install ruby-lsp")
return
}
bin = path.join(
Global.Path.bin,
"ruby-lsp" + (process.platform === "win32" ? ".exe" : ""),
)
bin = path.join(Global.Path.bin, "ruby-lsp" + (process.platform === "win32" ? ".exe" : ""))
log.info(`installed ruby-lsp`, {
bin,
})
}
return {
process: spawn(bin!, ["--stdio"]),
process: spawn(bin!, ["--stdio"], {
cwd: root,
}),
}
},
}
@@ -128,20 +148,217 @@ export namespace LSPServer {
export const Pyright: Info = {
id: "pyright",
extensions: [".py", ".pyi"],
async spawn() {
const proc = spawn(
BunProc.which(),
["x", "pyright-langserver", "--stdio"],
{
env: {
...process.env,
BUN_BE_BUN: "1",
},
root: NearestRoot(["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile", "pyrightconfig.json"]),
async spawn(_, root) {
const proc = spawn(BunProc.which(), ["x", "pyright-langserver", "--stdio"], {
cwd: root,
env: {
...process.env,
BUN_BE_BUN: "1",
},
)
})
return {
process: proc,
}
},
}
export const ElixirLS: Info = {
id: "elixir-ls",
extensions: [".ex", ".exs"],
root: NearestRoot(["mix.exs", "mix.lock"]),
async spawn(_, root) {
let binary = Bun.which("elixir-ls")
if (!binary) {
const elixirLsPath = path.join(Global.Path.bin, "elixir-ls")
binary = path.join(
Global.Path.bin,
"elixir-ls-master",
"release",
process.platform === "win32" ? "language_server.bar" : "language_server.sh",
)
if (!(await Bun.file(binary).exists())) {
const elixir = Bun.which("elixir")
if (!elixir) {
log.error("elixir is required to run elixir-ls")
return
}
log.info("downloading elixir-ls from GitHub releases")
const response = await fetch("https://github.com/elixir-lsp/elixir-ls/archive/refs/heads/master.zip")
if (!response.ok) return
const zipPath = path.join(Global.Path.bin, "elixir-ls.zip")
await Bun.file(zipPath).write(response)
await $`unzip -o -q ${zipPath}`.cwd(Global.Path.bin).nothrow()
await fs.rm(zipPath, {
force: true,
recursive: true,
})
await $`mix deps.get && mix compile && mix elixir_ls.release2 -o release`
.quiet()
.cwd(path.join(Global.Path.bin, "elixir-ls-master"))
.env({ MIX_ENV: "prod", ...process.env })
log.info(`installed elixir-ls`, {
path: elixirLsPath,
})
}
}
return {
process: spawn(binary, {
cwd: root,
}),
}
},
}
export const Zls: Info = {
id: "zls",
extensions: [".zig", ".zon"],
root: NearestRoot(["build.zig"]),
async spawn(_, root) {
let bin = Bun.which("zls", {
PATH: process.env["PATH"] + ":" + Global.Path.bin,
})
if (!bin) {
const zig = Bun.which("zig")
if (!zig) {
log.error("Zig is required to use zls. Please install Zig first.")
return
}
log.info("downloading zls from GitHub releases")
const releaseResponse = await fetch("https://api.github.com/repos/zigtools/zls/releases/latest")
if (!releaseResponse.ok) {
log.error("Failed to fetch zls release info")
return
}
const release = await releaseResponse.json()
const platform = process.platform
const arch = process.arch
let assetName = ""
let zlsArch: string = arch
if (arch === "arm64") zlsArch = "aarch64"
else if (arch === "x64") zlsArch = "x86_64"
else if (arch === "ia32") zlsArch = "x86"
let zlsPlatform: string = platform
if (platform === "darwin") zlsPlatform = "macos"
else if (platform === "win32") zlsPlatform = "windows"
const ext = platform === "win32" ? "zip" : "tar.xz"
assetName = `zls-${zlsArch}-${zlsPlatform}.${ext}`
const supportedCombos = [
"zls-x86_64-linux.tar.xz",
"zls-x86_64-macos.tar.xz",
"zls-x86_64-windows.zip",
"zls-aarch64-linux.tar.xz",
"zls-aarch64-macos.tar.xz",
"zls-aarch64-windows.zip",
"zls-x86-linux.tar.xz",
"zls-x86-windows.zip",
]
if (!supportedCombos.includes(assetName)) {
log.error(`Platform ${platform} and architecture ${arch} is not supported by zls`)
return
}
const asset = release.assets.find((a: any) => a.name === assetName)
if (!asset) {
log.error(`Could not find asset ${assetName} in latest zls release`)
return
}
const downloadUrl = asset.browser_download_url
const downloadResponse = await fetch(downloadUrl)
if (!downloadResponse.ok) {
log.error("Failed to download zls")
return
}
const tempPath = path.join(Global.Path.bin, assetName)
await Bun.file(tempPath).write(downloadResponse)
if (ext === "zip") {
await $`unzip -o -q ${tempPath}`.cwd(Global.Path.bin).nothrow()
} else {
await $`tar -xf ${tempPath}`.cwd(Global.Path.bin).nothrow()
}
await fs.rm(tempPath, { force: true })
bin = path.join(Global.Path.bin, "zls" + (platform === "win32" ? ".exe" : ""))
if (!(await Bun.file(bin).exists())) {
log.error("Failed to extract zls binary")
return
}
if (platform !== "win32") {
await $`chmod +x ${bin}`.nothrow()
}
log.info(`installed zls`, { bin })
}
return {
process: spawn(bin, {
cwd: root,
}),
}
},
}
export const CSharp: Info = {
id: "csharp",
root: NearestRoot([".sln", ".csproj", "global.json"]),
extensions: [".cs"],
async spawn(_, root) {
let bin = Bun.which("csharp-ls", {
PATH: process.env["PATH"] + ":" + Global.Path.bin,
})
if (!bin) {
if (!Bun.which("dotnet")) {
log.error(".NET SDK is required to install csharp-ls")
return
}
log.info("installing csharp-ls via dotnet tool")
const proc = Bun.spawn({
cmd: ["dotnet", "tool", "install", "csharp-ls", "--tool-path", Global.Path.bin],
stdout: "pipe",
stderr: "pipe",
stdin: "pipe",
})
const exit = await proc.exited
if (exit !== 0) {
log.error("Failed to install csharp-ls")
return
}
bin = path.join(Global.Path.bin, "csharp-ls" + (process.platform === "win32" ? ".exe" : ""))
log.info(`installed csharp-ls`, { bin })
}
return {
process: spawn(bin, {
cwd: root,
}),
}
},
}
}
+28 -14
View File
@@ -1,5 +1,7 @@
import { experimental_createMCPClient, type Tool } from "ai"
import { Experimental_StdioMCPTransport } from "ai/mcp-stdio"
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
import { App } from "../app/app"
import { Config } from "../config/config"
import { Log } from "../util/log"
@@ -32,14 +34,28 @@ export namespace MCP {
}
log.info("found", { key, type: mcp.type })
if (mcp.type === "remote") {
const client = await experimental_createMCPClient({
name: key,
transport: {
type: "sse",
url: mcp.url,
},
}).catch(() => {})
if (!client) {
const transports = [
new StreamableHTTPClientTransport(new URL(mcp.url), {
requestInit: {
headers: mcp.headers,
},
}),
new SSEClientTransport(new URL(mcp.url), {
requestInit: {
headers: mcp.headers,
},
}),
]
for (const transport of transports) {
const client = await experimental_createMCPClient({
name: key,
transport,
}).catch(() => {})
if (!client) continue
clients[key] = client
break
}
if (!clients[key])
Bus.publish(Session.Event.Error, {
error: {
name: "UnknownError",
@@ -48,16 +64,13 @@ export namespace MCP {
},
},
})
continue
}
clients[key] = client
}
if (mcp.type === "local") {
const [cmd, ...args] = mcp.command
const client = await experimental_createMCPClient({
name: key,
transport: new Experimental_StdioMCPTransport({
transport: new StdioClientTransport({
stderr: "ignore",
command: cmd,
args,
@@ -102,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()
+87 -159
View File
@@ -5,23 +5,11 @@ import { mergeDeep, sortBy } from "remeda"
import { NoSuchModelError, type LanguageModel, type Provider as SDK } from "ai"
import { Log } from "../util/log"
import { BunProc } from "../bun"
import { BashTool } from "../tool/bash"
import { EditTool } from "../tool/edit"
import { WebFetchTool } from "../tool/webfetch"
import { GlobTool } from "../tool/glob"
import { GrepTool } from "../tool/grep"
import { ListTool } from "../tool/ls"
import { PatchTool } from "../tool/patch"
import { ReadTool } from "../tool/read"
import type { Tool } from "../tool/tool"
import { WriteTool } from "../tool/write"
import { TodoReadTool, TodoWriteTool } from "../tool/todo"
import { AuthAnthropic } from "../auth/anthropic"
import { AuthCopilot } from "../auth/copilot"
import { ModelsDev } from "./models"
import { NamedError } from "../util/error"
import { Auth } from "../auth"
// import { TaskTool } from "../tool/task"
export namespace Provider {
const log = Log.create({ service: "provider" })
@@ -91,8 +79,7 @@ export namespace Provider {
if (!info || info.type !== "oauth") return
if (!info.access || info.expires < Date.now()) {
const tokens = await copilot.access(info.refresh)
if (!tokens)
throw new Error("GitHub Copilot authentication expired")
if (!tokens) throw new Error("GitHub Copilot authentication expired")
await Auth.set("github-copilot", {
type: "oauth",
...tokens,
@@ -100,25 +87,27 @@ export namespace Provider {
info.access = tokens.access
}
let isAgentCall = false
let isVisionRequest = false
try {
const body =
typeof init.body === "string"
? JSON.parse(init.body)
: init.body
const body = typeof init.body === "string" ? JSON.parse(init.body) : init.body
if (body?.messages) {
isAgentCall = body.messages.some(
isAgentCall = body.messages.some((msg: any) => msg.role && ["tool", "assistant"].includes(msg.role))
isVisionRequest = body.messages.some(
(msg: any) =>
msg.role && ["tool", "assistant"].includes(msg.role),
Array.isArray(msg.content) && msg.content.some((part: any) => part.type === "image_url"),
)
}
} catch {}
const headers = {
const headers: Record<string, string> = {
...init.headers,
...copilot.HEADERS,
Authorization: `Bearer ${info.access}`,
"Openai-Intent": "conversation-edits",
"X-Initiator": isAgentCall ? "agent" : "user",
}
if (isVisionRequest) {
headers["Copilot-Vision-Request"] = "true"
}
delete headers["x-api-key"]
return fetch(input, {
...init,
@@ -137,15 +126,22 @@ 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"])
if (!process.env["AWS_PROFILE"] && !process.env["AWS_ACCESS_KEY_ID"] && !process.env["AWS_BEARER_TOKEN_BEDROCK"])
return { autoload: false }
const region = process.env["AWS_REGION"] ?? "us-east-1"
const { fromNodeProviderChain } = await import(
await BunProc.install("@aws-sdk/credential-providers")
)
const { fromNodeProviderChain } = await import(await BunProc.install("@aws-sdk/credential-providers"))
return {
autoload: true,
options: {
@@ -157,9 +153,7 @@ export namespace Provider {
switch (regionPrefix) {
case "us": {
const modelRequiresPrefix = ["claude", "deepseek"].some((m) =>
modelID.includes(m),
)
const modelRequiresPrefix = ["claude", "deepseek"].some((m) => modelID.includes(m))
if (modelRequiresPrefix) {
modelID = `${regionPrefix}.${modelID}`
}
@@ -174,25 +168,18 @@ export namespace Provider {
"eu-south-1",
"eu-south-2",
].some((r) => region.includes(r))
const modelRequiresPrefix = [
"claude",
"nova-lite",
"nova-micro",
"llama3",
"pixtral",
].some((m) => modelID.includes(m))
const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "llama3", "pixtral"].some((m) =>
modelID.includes(m),
)
if (regionRequiresPrefix && modelRequiresPrefix) {
modelID = `${regionPrefix}.${modelID}`
}
break
}
case "ap": {
const modelRequiresPrefix = [
"claude",
"nova-lite",
"nova-micro",
"nova-pro",
].some((m) => modelID.includes(m))
const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "nova-pro"].some((m) =>
modelID.includes(m),
)
if (modelRequiresPrefix) {
regionPrefix = "apac"
modelID = `${regionPrefix}.${modelID}`
@@ -216,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 () => {
@@ -230,10 +228,7 @@ export namespace Provider {
options: Record<string, any>
}
} = {}
const models = new Map<
string,
{ info: ModelsDev.Model; language: LanguageModel }
>()
const models = new Map<string, { info: ModelsDev.Model; language: LanguageModel }>()
const sdk = new Map<string, SDK>()
log.info("init")
@@ -248,7 +243,7 @@ export namespace Provider {
if (!provider) {
const info = database[id]
if (!info) return
if (info.api) options["baseURL"] = info.api
if (info.api && !options["baseURL"]) options["baseURL"] = info.api
providers[id] = {
source,
info,
@@ -285,14 +280,20 @@ export namespace Provider {
reasoning: model.reasoning ?? existing?.reasoning ?? false,
temperature: model.temperature ?? existing?.temperature ?? false,
tool_call: model.tool_call ?? existing?.tool_call ?? true,
cost: {
...existing?.cost,
...model.cost,
input: 0,
output: 0,
cache_read: 0,
cache_write: 0,
},
cost:
!model.cost && !existing?.cost
? {
input: 0,
output: 0,
cache_read: 0,
cache_write: 0,
}
: {
cache_read: 0,
cache_write: 0,
...existing?.cost,
...model.cost,
},
options: {
...existing?.options,
...model.options,
@@ -308,9 +309,7 @@ export namespace Provider {
database[providerID] = parsed
}
const disabled = await Config.get().then(
(cfg) => new Set(cfg.disabled_providers ?? []),
)
const disabled = await Config.get().then((cfg) => new Set(cfg.disabled_providers ?? []))
// load env
for (const [providerID, provider] of Object.entries(database)) {
if (disabled.has(providerID)) continue
@@ -337,12 +336,7 @@ export namespace Provider {
if (disabled.has(providerID)) continue
const result = await fn(database[providerID])
if (result && (result.autoload || providers[providerID])) {
mergeProvider(
providerID,
result.options ?? {},
"custom",
result.getModel,
)
mergeProvider(providerID, result.options ?? {}, "custom", result.getModel)
}
}
@@ -379,9 +373,12 @@ export namespace Provider {
const existing = s.sdk.get(provider.id)
if (existing) return existing
const pkg = provider.npm ?? provider.id
const mod = await import(await BunProc.install(pkg, "latest"))
const mod = await import(await BunProc.install(pkg, "beta"))
const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
const loaded = fn(s.providers[provider.id]?.options)
const loaded = fn({
name: provider.id,
...s.providers[provider.id]?.options,
})
s.sdk.set(provider.id, loaded)
return loaded as SDK
})().catch((e) => {
@@ -389,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()
@@ -406,9 +407,7 @@ export namespace Provider {
const sdk = await getSDK(provider.info)
try {
const language = provider.getModel
? await provider.getModel(sdk, modelID)
: sdk.languageModel(modelID)
const language = provider.getModel ? await provider.getModel(sdk, modelID) : sdk.languageModel(modelID)
log.info("found", { providerID, modelID })
s.models.set(key, {
info,
@@ -431,14 +430,29 @@ export namespace Provider {
}
}
export async function getSmallModel(providerID: string) {
const cfg = await Config.get()
if (cfg.small_model) {
const parsed = parseModel(cfg.small_model)
return getModel(parsed.providerID, parsed.modelID)
}
const provider = await state().then((state) => state.providers[providerID])
if (!provider) return
const priority = ["3-5-haiku", "3.5-haiku", "gemini-2.5-flash"]
for (const item of priority) {
for (const model of Object.keys(provider.info.models)) {
if (model.includes(item)) return getModel(providerID, model)
}
}
}
const priority = ["gemini-2.5-pro-preview", "codex-mini", "claude-sonnet-4"]
export function sort(models: ModelsDev.Model[]) {
return sortBy(
models,
[
(model) => priority.findIndex((filter) => model.id.includes(filter)),
"desc",
],
[(model) => priority.findIndex((filter) => model.id.includes(filter)), "desc"],
[(model) => (model.id.includes("latest") ? 0 : 1), "asc"],
[(model) => model.id, "desc"],
)
@@ -449,11 +463,7 @@ export namespace Provider {
if (cfg.model) return parseModel(cfg.model)
const provider = await list()
.then((val) => Object.values(val))
.then((x) =>
x.find(
(p) => !cfg.provider || Object.keys(cfg.provider).includes(p.info.id),
),
)
.then((x) => x.find((p) => !cfg.provider || Object.keys(cfg.provider).includes(p.info.id)))
if (!provider) throw new Error("no providers found")
const [model] = sort(Object.values(provider.info.models))
if (!model) throw new Error("no models found")
@@ -471,80 +481,6 @@ export namespace Provider {
}
}
const TOOLS = [
BashTool,
EditTool,
WebFetchTool,
GlobTool,
GrepTool,
ListTool,
// LspDiagnosticTool,
// LspHoverTool,
PatchTool,
ReadTool,
// MultiEditTool,
WriteTool,
TodoWriteTool,
TodoReadTool,
// TaskTool,
]
const TOOL_MAPPING: Record<string, Tool.Info[]> = {
anthropic: TOOLS.filter((t) => t.id !== "patch"),
openai: TOOLS.map((t) => ({
...t,
parameters: optionalToNullable(t.parameters),
})),
azure: TOOLS.map((t) => ({
...t,
parameters: optionalToNullable(t.parameters),
})),
google: TOOLS,
}
export async function tools(providerID: string) {
/*
const cfg = await Config.get()
if (cfg.tool?.provider?.[providerID])
return cfg.tool.provider[providerID].map(
(id) => TOOLS.find((t) => t.id === id)!,
)
*/
return TOOL_MAPPING[providerID] ?? TOOLS
}
function optionalToNullable(schema: z.ZodTypeAny): z.ZodTypeAny {
if (schema instanceof z.ZodObject) {
const shape = schema.shape
const newShape: Record<string, z.ZodTypeAny> = {}
for (const [key, value] of Object.entries(shape)) {
const zodValue = value as z.ZodTypeAny
if (zodValue instanceof z.ZodOptional) {
newShape[key] = zodValue.unwrap().nullable()
} else {
newShape[key] = optionalToNullable(zodValue)
}
}
return z.object(newShape)
}
if (schema instanceof z.ZodArray) {
return z.array(optionalToNullable(schema.element))
}
if (schema instanceof z.ZodUnion) {
return z.union(
schema.options.map((option: z.ZodTypeAny) =>
optionalToNullable(option),
) as [z.ZodTypeAny, z.ZodTypeAny, ...z.ZodTypeAny[]],
)
}
return schema
}
export const ModelNotFoundError = NamedError.create(
"ProviderModelNotFoundError",
z.object({
@@ -559,12 +495,4 @@ export namespace Provider {
providerID: z.string(),
}),
)
export const AuthError = NamedError.create(
"ProviderAuthError",
z.object({
providerID: z.string(),
message: z.string(),
}),
)
}
+72 -26
View File
@@ -1,38 +1,84 @@
import type { LanguageModelV1Prompt } from "ai"
import type { ModelMessage } from "ai"
import { unique } from "remeda"
export namespace ProviderTransform {
export function message(
msgs: LanguageModelV1Prompt,
providerID: string,
modelID: string,
) {
if (providerID === "anthropic" || modelID.includes("anthropic")) {
const system = msgs.filter((msg) => msg.role === "system").slice(0, 2)
const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
function normalizeToolCallIds(msgs: ModelMessage[]): ModelMessage[] {
return msgs.map((msg) => {
if ((msg.role === "assistant" || msg.role === "tool") && Array.isArray(msg.content)) {
msg.content = msg.content.map((part) => {
if ((part.type === "tool-call" || part.type === "tool-result") && "toolCallId" in part) {
return {
...part,
toolCallId: part.toolCallId.replace(/[^a-zA-Z0-9_-]/g, "_"),
}
}
return part
})
}
return msg
})
}
for (const msg of unique([...system, ...final])) {
msg.providerMetadata = {
...msg.providerMetadata,
anthropic: {
cacheControl: { 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
}
}
}
if (providerID === "amazon-bedrock" || modelID.includes("anthropic")) {
const system = msgs.filter((msg) => msg.role === "system").slice(0, 2)
const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
for (const msg of unique([...system, ...final])) {
msg.providerMetadata = {
...msg.providerMetadata,
bedrock: {
cachePoint: { type: "ephemeral" },
},
}
msg.providerOptions = {
...msg.providerOptions,
...providerOptions,
}
}
return msgs
}
export function message(msgs: ModelMessage[], providerID: string, modelID: string) {
if (modelID.includes("claude")) {
msgs = normalizeToolCallIds(msgs)
}
if (providerID === "anthropic" || modelID.includes("anthropic") || modelID.includes("claude")) {
msgs = applyCaching(msgs, providerID)
}
return msgs
}
export function temperature(_providerID: string, modelID: string) {
if (modelID.toLowerCase().includes("qwen")) return 0.55
return 0
}
export function topP(_providerID: string, modelID: string) {
if (modelID.toLowerCase().includes("qwen")) return 1
return undefined
}
}
+445 -45
View File
@@ -6,7 +6,6 @@ import { streamSSE } from "hono/streaming"
import { Session } from "../session"
import { resolver, validator as zValidator } from "hono-openapi/zod"
import { z } from "zod"
import { Message } from "../session/message"
import { Provider } from "../provider/provider"
import { App } from "../app/app"
import { mapValues } from "remeda"
@@ -16,6 +15,11 @@ import { Ripgrep } from "../file/ripgrep"
import { Config } from "../config/config"
import { File } from "../file"
import { LSP } from "../lsp"
import { MessageV2 } from "../session/message-v2"
import { callTui, TuiRoute } from "./tui"
import { Permission } from "../permission"
import { lazy } from "../util/lazy"
import { Agent } from "../agent/agent"
const ERRORS = {
400: {
@@ -41,7 +45,11 @@ export namespace Server {
export type Routes = ReturnType<typeof app>
function app() {
export const Event = {
Connected: Bus.event("server.connected", z.object({})),
}
export const app = lazy(() => {
const app = new Hono()
const result = app
@@ -51,23 +59,25 @@ export namespace Server {
status: 400,
})
}
return c.json(
new NamedError.Unknown({ message: err.toString() }).toObject(),
{
status: 400,
},
)
return c.json(new NamedError.Unknown({ message: err.toString() }).toObject(), {
status: 400,
})
})
.use(async (c, next) => {
log.info("request", {
method: c.req.method,
path: c.req.path,
})
const skipLogging = c.req.path === "/log"
if (!skipLogging) {
log.info("request", {
method: c.req.method,
path: c.req.path,
})
}
const start = Date.now()
await next()
log.info("response", {
duration: Date.now() - start,
})
if (!skipLogging) {
log.info("response", {
duration: Date.now() - start,
})
}
})
.get(
"/doc",
@@ -86,6 +96,7 @@ export namespace Server {
"/event",
describeRoute({
description: "Get events",
operationId: "event.subscribe",
responses: {
200: {
description: "Event stream",
@@ -105,7 +116,10 @@ export namespace Server {
log.info("event connected")
return streamSSE(c, async (stream) => {
stream.writeSSE({
data: JSON.stringify({}),
data: JSON.stringify({
type: "server.connected",
properties: {},
}),
})
const unsub = Bus.subscribeAll(async (event) => {
await stream.writeSSE({
@@ -126,6 +140,7 @@ export namespace Server {
"/app",
describeRoute({
description: "Get app info",
operationId: "app.get",
responses: {
200: {
description: "200",
@@ -145,6 +160,7 @@ export namespace Server {
"/app/init",
describeRoute({
description: "Initialize the app",
operationId: "app.init",
responses: {
200: {
description: "Initialize the app",
@@ -165,6 +181,7 @@ export namespace Server {
"/config",
describeRoute({
description: "Get config info",
operationId: "config.get",
responses: {
200: {
description: "Get config info",
@@ -184,6 +201,7 @@ export namespace Server {
"/session",
describeRoute({
description: "List all sessions",
operationId: "session.list",
responses: {
200: {
description: "List of sessions",
@@ -197,13 +215,43 @@ export namespace Server {
}),
async (c) => {
const sessions = await Array.fromAsync(Session.list())
sessions.sort((a, b) => b.time.updated - a.time.updated)
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: {
@@ -225,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",
@@ -251,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",
@@ -271,6 +321,7 @@ export namespace Server {
zValidator(
"json",
z.object({
messageID: z.string(),
providerID: z.string(),
modelID: z.string(),
}),
@@ -286,6 +337,7 @@ export namespace Server {
"/session/:id/abort",
describeRoute({
description: "Abort a session",
operationId: "session.abort",
responses: {
200: {
description: "Aborted session",
@@ -311,6 +363,7 @@ export namespace Server {
"/session/:id/share",
describeRoute({
description: "Share a session",
operationId: "session.share",
responses: {
200: {
description: "Successfully shared session",
@@ -339,6 +392,7 @@ export namespace Server {
"/session/:id/share",
describeRoute({
description: "Unshare the session",
operationId: "session.unshare",
responses: {
200: {
description: "Successfully unshared session",
@@ -367,6 +421,7 @@ export namespace Server {
"/session/:id/summarize",
describeRoute({
description: "Summarize the session",
operationId: "session.summarize",
responses: {
200: {
description: "Summarized session",
@@ -402,12 +457,20 @@ export namespace Server {
"/session/:id/message",
describeRoute({
description: "List messages for a session",
operationId: "session.messages",
responses: {
200: {
description: "List of messages",
content: {
"application/json": {
schema: resolver(Message.Info.array()),
schema: resolver(
z
.object({
info: MessageV2.Info,
parts: MessageV2.Part.array(),
})
.array(),
),
},
},
},
@@ -424,16 +487,51 @@ 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",
content: {
"application/json": {
schema: resolver(Message.Info),
schema: resolver(MessageV2.Assistant),
},
},
},
@@ -445,14 +543,7 @@ export namespace Server {
id: z.string().openapi({ description: "Session ID" }),
}),
),
zValidator(
"json",
z.object({
providerID: z.string(),
modelID: z.string(),
parts: Message.MessagePart.array(),
}),
),
zValidator("json", Session.ChatInput.omit({ sessionID: true })),
async (c) => {
const sessionID = c.req.valid("param").id
const body = c.req.valid("json")
@@ -460,10 +551,100 @@ export namespace Server {
return c.json(msg)
},
)
.post(
"/session/:id/revert",
describeRoute({
description: "Revert a message",
operationId: "session.revert",
responses: {
200: {
description: "Updated session",
content: {
"application/json": {
schema: resolver(Session.Info),
},
},
},
},
}),
zValidator(
"param",
z.object({
id: z.string(),
}),
),
zValidator("json", Session.RevertInput.omit({ sessionID: true })),
async (c) => {
const id = c.req.valid("param").id
log.info("revert", c.req.valid("json"))
const session = await Session.revert({ sessionID: id, ...c.req.valid("json") })
return c.json(session)
},
)
.post(
"/session/:id/unrevert",
describeRoute({
description: "Restore all reverted messages",
operationId: "session.unrevert",
responses: {
200: {
description: "Updated session",
content: {
"application/json": {
schema: resolver(Session.Info),
},
},
},
},
}),
zValidator(
"param",
z.object({
id: z.string(),
}),
),
async (c) => {
const id = c.req.valid("param").id
const session = await Session.unrevert({ sessionID: id })
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",
@@ -481,15 +662,10 @@ export namespace Server {
},
}),
async (c) => {
const providers = await Provider.list().then((x) =>
mapValues(x, (item) => item.info),
)
const providers = await Provider.list().then((x) => mapValues(x, (item) => item.info))
return c.json({
providers: Object.values(providers),
default: mapValues(
providers,
(item) => Provider.sort(Object.values(item.models))[0].id,
),
default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
})
},
)
@@ -497,6 +673,7 @@ export namespace Server {
"/find",
describeRoute({
description: "Find text in files",
operationId: "find.text",
responses: {
200: {
description: "Matches",
@@ -529,6 +706,7 @@ export namespace Server {
"/find/file",
describeRoute({
description: "Find files",
operationId: "find.files",
responses: {
200: {
description: "File paths",
@@ -561,12 +739,13 @@ export namespace Server {
"/find/symbol",
describeRoute({
description: "Find workspace symbols",
operationId: "find.symbols",
responses: {
200: {
description: "Symbols",
content: {
"application/json": {
schema: resolver(z.unknown().array()),
schema: resolver(LSP.Symbol.array()),
},
},
},
@@ -588,6 +767,7 @@ export namespace Server {
"/file",
describeRoute({
description: "Read a file",
operationId: "file.read",
responses: {
200: {
description: "File content",
@@ -624,21 +804,13 @@ export namespace Server {
"/file/status",
describeRoute({
description: "Get file status",
operationId: "file.status",
responses: {
200: {
description: "File status",
content: {
"application/json": {
schema: resolver(
z
.object({
file: z.string(),
added: z.number().int(),
removed: z.number().int(),
status: z.enum(["added", "deleted", "modified"]),
})
.array(),
),
schema: resolver(File.Info.array()),
},
},
},
@@ -649,9 +821,237 @@ export namespace Server {
return c.json(content)
},
)
.post(
"/log",
describeRoute({
description: "Write a log entry to the server logs",
operationId: "app.log",
responses: {
200: {
description: "Log entry written successfully",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
},
}),
zValidator(
"json",
z.object({
service: z.string().openapi({ description: "Service name for the log entry" }),
level: z.enum(["debug", "info", "error", "warn"]).openapi({ description: "Log level" }),
message: z.string().openapi({ description: "Log message" }),
extra: z
.record(z.string(), z.any())
.optional()
.openapi({ description: "Additional metadata for the log entry" }),
}),
),
async (c) => {
const { service, level, message, extra } = c.req.valid("json")
const logger = Log.create({ service })
switch (level) {
case "debug":
logger.debug(message, extra)
break
case "info":
logger.info(message, extra)
break
case "error":
logger.error(message, extra)
break
case "warn":
logger.warn(message, extra)
break
}
return c.json(true)
},
)
.get(
"/agent",
describeRoute({
description: "List all agents",
operationId: "app.agents",
responses: {
200: {
description: "List of agents",
content: {
"application/json": {
schema: resolver(Agent.Info.array()),
},
},
},
},
}),
async (c) => {
const modes = await Agent.list()
return c.json(modes)
},
)
.post(
"/tui/append-prompt",
describeRoute({
description: "Append prompt to the TUI",
operationId: "tui.appendPrompt",
responses: {
200: {
description: "Prompt processed successfully",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
},
}),
zValidator(
"json",
z.object({
text: z.string(),
}),
),
async (c) => c.json(await callTui(c)),
)
.post(
"/tui/open-help",
describeRoute({
description: "Open the help dialog",
operationId: "tui.openHelp",
responses: {
200: {
description: "Help dialog opened successfully",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
},
}),
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()
+30
View File
@@ -0,0 +1,30 @@
import { Hono, type Context } from "hono"
import { AsyncQueue } from "../util/queue"
interface Request {
path: string
body: any
}
const request = new AsyncQueue<Request>()
const response = new AsyncQueue<any>()
export async function callTui(ctx: Context) {
const body = await ctx.req.json()
request.push({
path: ctx.req.path,
body,
})
return response.next()
}
export const TuiRoute = new Hono()
.get("/next", async (c) => {
const req = await request.next()
return c.json(req)
})
.post("/response", async (c) => {
const body = await c.req.json()
response.push(body)
return c.json(true)
})
File diff suppressed because it is too large Load Diff
+539
View File
@@ -0,0 +1,539 @@
import z from "zod"
import { Bus } from "../bus"
import { NamedError } from "../util/error"
import { Message } from "./message"
import { convertToModelMessages, type ModelMessage, type UIMessage } from "ai"
import { Identifier } from "../id/id"
import { LSP } from "../lsp"
export namespace MessageV2 {
export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({}))
export const AbortedError = NamedError.create("MessageAbortedError", z.object({}))
export const AuthError = NamedError.create(
"ProviderAuthError",
z.object({
providerID: z.string(),
message: z.string(),
}),
)
export const ToolStatePending = z
.object({
status: z.literal("pending"),
})
.openapi({
ref: "ToolStatePending",
})
export type ToolStatePending = z.infer<typeof ToolStatePending>
export const ToolStateRunning = z
.object({
status: z.literal("running"),
input: z.any(),
title: z.string().optional(),
metadata: z.record(z.any()).optional(),
time: z.object({
start: z.number(),
}),
})
.openapi({
ref: "ToolStateRunning",
})
export type ToolStateRunning = z.infer<typeof ToolStateRunning>
export const ToolStateCompleted = z
.object({
status: z.literal("completed"),
input: z.record(z.any()),
output: z.string(),
title: z.string(),
metadata: z.record(z.any()),
time: z.object({
start: z.number(),
end: z.number(),
}),
})
.openapi({
ref: "ToolStateCompleted",
})
export type ToolStateCompleted = z.infer<typeof ToolStateCompleted>
export const ToolStateError = z
.object({
status: z.literal("error"),
input: z.record(z.any()),
error: z.string(),
time: z.object({
start: z.number(),
end: z.number(),
}),
})
.openapi({
ref: "ToolStateError",
})
export type ToolStateError = z.infer<typeof ToolStateError>
export const ToolState = z
.discriminatedUnion("status", [ToolStatePending, ToolStateRunning, ToolStateCompleted, ToolStateError])
.openapi({
ref: "ToolState",
})
const PartBase = z.object({
id: z.string(),
sessionID: z.string(),
messageID: z.string(),
})
export const SnapshotPart = PartBase.extend({
type: z.literal("snapshot"),
snapshot: z.string(),
}).openapi({
ref: "SnapshotPart",
})
export type SnapshotPart = z.infer<typeof SnapshotPart>
export const PatchPart = PartBase.extend({
type: z.literal("patch"),
hash: z.string(),
files: z.string().array(),
}).openapi({
ref: "PatchPart",
})
export type PatchPart = z.infer<typeof PatchPart>
export const TextPart = PartBase.extend({
type: z.literal("text"),
text: z.string(),
synthetic: z.boolean().optional(),
time: z
.object({
start: z.number(),
end: z.number().optional(),
})
.optional(),
}).openapi({
ref: "TextPart",
})
export type TextPart = z.infer<typeof TextPart>
export const ToolPart = PartBase.extend({
type: z.literal("tool"),
callID: z.string(),
tool: z.string(),
state: ToolState,
}).openapi({
ref: "ToolPart",
})
export type ToolPart = z.infer<typeof ToolPart>
const FilePartSourceBase = z.object({
text: z
.object({
value: z.string(),
start: z.number().int(),
end: z.number().int(),
})
.openapi({
ref: "FilePartSourceText",
}),
})
export const FileSource = FilePartSourceBase.extend({
type: z.literal("file"),
path: z.string(),
}).openapi({
ref: "FileSource",
})
export const SymbolSource = FilePartSourceBase.extend({
type: z.literal("symbol"),
path: z.string(),
range: LSP.Range,
name: z.string(),
kind: z.number().int(),
}).openapi({
ref: "SymbolSource",
})
export const FilePartSource = z.discriminatedUnion("type", [FileSource, SymbolSource]).openapi({
ref: "FilePartSource",
})
export const FilePart = PartBase.extend({
type: z.literal("file"),
mime: z.string(),
filename: z.string().optional(),
url: z.string(),
source: FilePartSource.optional(),
}).openapi({
ref: "FilePart",
})
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({
ref: "StepStartPart",
})
export type StepStartPart = z.infer<typeof StepStartPart>
export const StepFinishPart = PartBase.extend({
type: z.literal("step-finish"),
cost: z.number(),
tokens: z.object({
input: z.number(),
output: z.number(),
reasoning: z.number(),
cache: z.object({
read: z.number(),
write: z.number(),
}),
}),
}).openapi({
ref: "StepFinishPart",
})
export type StepFinishPart = z.infer<typeof StepFinishPart>
const Base = z.object({
id: z.string(),
sessionID: z.string(),
})
export const User = Base.extend({
role: z.literal("user"),
time: z.object({
created: z.number(),
}),
}).openapi({
ref: "UserMessage",
})
export type User = z.infer<typeof User>
export const Part = z
.discriminatedUnion("type", [
TextPart,
FilePart,
ToolPart,
StepStartPart,
StepFinishPart,
SnapshotPart,
PatchPart,
AgentPart,
])
.openapi({
ref: "Part",
})
export type Part = z.infer<typeof Part>
export const Assistant = Base.extend({
role: z.literal("assistant"),
time: z.object({
created: z.number(),
completed: z.number().optional(),
}),
error: z
.discriminatedUnion("name", [
AuthError.Schema,
NamedError.Unknown.Schema,
OutputLengthError.Schema,
AbortedError.Schema,
])
.optional(),
system: z.string().array(),
modelID: z.string(),
providerID: z.string(),
mode: z.string(),
path: z.object({
cwd: z.string(),
root: z.string(),
}),
summary: z.boolean().optional(),
cost: z.number(),
tokens: z.object({
input: z.number(),
output: z.number(),
reasoning: z.number(),
cache: z.object({
read: z.number(),
write: z.number(),
}),
}),
}).openapi({
ref: "AssistantMessage",
})
export type Assistant = z.infer<typeof Assistant>
export const Info = z.discriminatedUnion("role", [User, Assistant]).openapi({
ref: "Message",
})
export type Info = z.infer<typeof Info>
export const Event = {
Updated: Bus.event(
"message.updated",
z.object({
info: Info,
}),
),
Removed: Bus.event(
"message.removed",
z.object({
sessionID: z.string(),
messageID: z.string(),
}),
),
PartUpdated: Bus.event(
"message.part.updated",
z.object({
part: Part,
}),
),
PartRemoved: Bus.event(
"message.part.removed",
z.object({
sessionID: z.string(),
messageID: z.string(),
partID: z.string(),
}),
),
}
export function fromV1(v1: Message.Info) {
if (v1.role === "assistant") {
const info: Assistant = {
id: v1.id,
sessionID: v1.metadata.sessionID,
role: "assistant",
time: {
created: v1.metadata.time.created,
completed: v1.metadata.time.completed,
},
cost: v1.metadata.assistant!.cost,
path: v1.metadata.assistant!.path,
summary: v1.metadata.assistant!.summary,
tokens: v1.metadata.assistant!.tokens,
modelID: v1.metadata.assistant!.modelID,
providerID: v1.metadata.assistant!.providerID,
system: v1.metadata.assistant!.system,
mode: "build",
error: v1.metadata.error,
}
const parts = v1.parts.flatMap((part): Part[] => {
const base = {
id: Identifier.ascending("part"),
messageID: v1.id,
sessionID: v1.metadata.sessionID,
}
if (part.type === "text") {
return [
{
...base,
type: "text",
text: part.text,
},
]
}
if (part.type === "step-start") {
return [
{
...base,
type: "step-start",
},
]
}
if (part.type === "tool-invocation") {
return [
{
...base,
type: "tool",
callID: part.toolInvocation.toolCallId,
tool: part.toolInvocation.toolName,
state: (() => {
if (part.toolInvocation.state === "partial-call") {
return {
status: "pending",
}
}
const { title, time, ...metadata } = v1.metadata.tool[part.toolInvocation.toolCallId] ?? {}
if (part.toolInvocation.state === "call") {
return {
status: "running",
input: part.toolInvocation.args,
time: {
start: time?.start,
},
}
}
if (part.toolInvocation.state === "result") {
return {
status: "completed",
input: part.toolInvocation.args,
output: part.toolInvocation.result,
title,
time,
metadata,
}
}
throw new Error("unknown tool invocation state")
})(),
},
]
}
return []
})
return {
info,
parts,
}
}
if (v1.role === "user") {
const info: User = {
id: v1.id,
sessionID: v1.metadata.sessionID,
role: "user",
time: {
created: v1.metadata.time.created,
},
}
const parts = v1.parts.flatMap((part): Part[] => {
const base = {
id: Identifier.ascending("part"),
messageID: v1.id,
sessionID: v1.metadata.sessionID,
}
if (part.type === "text") {
return [
{
...base,
type: "text",
text: part.text,
},
]
}
if (part.type === "file") {
return [
{
...base,
type: "file",
mime: part.mediaType,
filename: part.filename,
url: part.url,
},
]
}
return []
})
return { info, parts }
}
throw new Error("unknown message type")
}
export function toModelMessage(
input: {
info: Info
parts: Part[]
}[],
): ModelMessage[] {
const result: UIMessage[] = []
for (const msg of input) {
if (msg.parts.length === 0) continue
if (msg.info.role === "user") {
result.push({
id: msg.info.id,
role: "user",
parts: msg.parts.flatMap((part): UIMessage["parts"] => {
if (part.type === "text")
return [
{
type: "text",
text: part.text,
},
]
// text/plain files are converted into text parts, ignore them
if (part.type === "file" && part.mime !== "text/plain")
return [
{
type: "file",
url: part.url,
mediaType: part.mime,
filename: part.filename,
},
]
return []
}),
})
}
if (msg.info.role === "assistant") {
result.push({
id: msg.info.id,
role: "assistant",
parts: msg.parts.flatMap((part): UIMessage["parts"] => {
if (part.type === "text")
return [
{
type: "text",
text: part.text,
},
]
if (part.type === "step-start")
return [
{
type: "step-start",
},
]
if (part.type === "tool") {
if (part.state.status === "completed")
return [
{
type: ("tool-" + part.tool) as `tool-${string}`,
state: "output-available",
toolCallId: part.callID,
input: part.state.input,
output: part.state.output,
},
]
if (part.state.status === "error")
return [
{
type: ("tool-" + part.tool) as `tool-${string}`,
state: "output-error",
toolCallId: part.callID,
input: part.state.input,
errorText: part.state.error,
},
]
}
return []
}),
})
}
}
return convertToModelMessages(result)
}
}
+12 -47
View File
@@ -1,12 +1,14 @@
import z from "zod"
import { Bus } from "../bus"
import { Provider } from "../provider/provider"
import { NamedError } from "../util/error"
export namespace Message {
export const OutputLengthError = NamedError.create(
"MessageOutputLengthError",
z.object({}),
export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({}))
export const AuthError = NamedError.create(
"ProviderAuthError",
z.object({
providerID: z.string(),
message: z.string(),
}),
)
export const ToolCall = z
@@ -49,11 +51,9 @@ export namespace Message {
})
export type ToolResult = z.infer<typeof ToolResult>
export const ToolInvocation = z
.discriminatedUnion("state", [ToolCall, ToolPartialCall, ToolResult])
.openapi({
ref: "ToolInvocation",
})
export const ToolInvocation = z.discriminatedUnion("state", [ToolCall, ToolPartialCall, ToolResult]).openapi({
ref: "ToolInvocation",
})
export type ToolInvocation = z.infer<typeof ToolInvocation>
export const TextPart = z
@@ -122,14 +122,7 @@ export namespace Message {
export type StepStartPart = z.infer<typeof StepStartPart>
export const MessagePart = z
.discriminatedUnion("type", [
TextPart,
ReasoningPart,
ToolInvocationPart,
SourceUrlPart,
FilePart,
StepStartPart,
])
.discriminatedUnion("type", [TextPart, ReasoningPart, ToolInvocationPart, SourceUrlPart, FilePart, StepStartPart])
.openapi({
ref: "MessagePart",
})
@@ -147,11 +140,7 @@ export namespace Message {
completed: z.number().optional(),
}),
error: z
.discriminatedUnion("name", [
Provider.AuthError.Schema,
NamedError.Unknown.Schema,
OutputLengthError.Schema,
])
.discriminatedUnion("name", [AuthError.Schema, NamedError.Unknown.Schema, OutputLengthError.Schema])
.optional(),
sessionID: z.string(),
tool: z.record(
@@ -197,28 +186,4 @@ export namespace Message {
ref: "Message",
})
export type Info = z.infer<typeof Info>
export const Event = {
Updated: Bus.event(
"message.updated",
z.object({
info: Info,
}),
),
Removed: Bus.event(
"message.removed",
z.object({
sessionID: z.string(),
messageID: z.string(),
}),
),
PartUpdated: Bus.event(
"message.part.updated",
z.object({
part: MessagePart,
sessionID: z.string(),
messageID: z.string(),
}),
),
}
}
@@ -0,0 +1,147 @@
You are opencode, an agent - please keep going until the users query is completely resolved, before ending your turn and yielding back to the user.
Your thinking should be thorough and so it's fine if it's very long. However, avoid unnecessary repetition and verbosity. You should be concise, but thorough.
You MUST iterate and keep going until the problem is solved.
You have everything you need to resolve this problem. I want you to fully solve this autonomously before coming back to me.
Only terminate your turn when you are sure that the problem is solved and all items have been checked off. Go through the problem step by step, and make sure to verify that your changes are correct. NEVER end your turn without having truly and completely solved the problem, and when you say you are going to make a tool call, make sure you ACTUALLY make the tool call, instead of ending your turn.
THE PROBLEM CAN NOT BE SOLVED WITHOUT EXTENSIVE INTERNET RESEARCH.
You must use the webfetch tool to recursively gather all information from URL's provided to you by the user, as well as any links you find in the content of those pages.
Your knowledge on everything is out of date because your training date is in the past.
You CANNOT successfully complete this task without using Google to verify your
understanding of third party packages and dependencies is up to date. You must use the webfetch tool to search google for how to properly use libraries, packages, frameworks, dependencies, etc. every single time you install or implement one. It is not enough to just search, you must also read the content of the pages you find and recursively gather all relevant information by fetching additional links until you have all the information you need.
Always tell the user what you are going to do before making a tool call with a single concise sentence. This will help them understand what you are doing and why.
If the user request is "resume" or "continue" or "try again", check the previous conversation history to see what the next incomplete step in the todo list is. Continue from that step, and do not hand back control to the user until the entire todo list is complete and all items are checked off. Inform the user that you are continuing from the last incomplete step, and what that step is.
Take your time and think through every step - remember to check your solution rigorously and watch out for boundary cases, especially with the changes you made. Use the sequential thinking tool if available. Your solution must be perfect. If not, continue working on it. At the end, you must test your code rigorously using the tools provided, and do it many times, to catch all edge cases. If it is not robust, iterate more and make it perfect. Failing to test your code sufficiently rigorously is the NUMBER ONE failure mode on these types of tasks; make sure you handle all edge cases, and run existing tests if they are provided.
You MUST plan extensively before each function call, and reflect extensively on the outcomes of the previous function calls. DO NOT do this entire process by making function calls only, as this can impair your ability to solve the problem and think insightfully.
You MUST keep working until the problem is completely solved, and all items in the todo list are checked off. Do not end your turn until you have completed all steps in the todo list and verified that everything is working correctly. When you say "Next I will do X" or "Now I will do Y" or "I will do X", you MUST actually do X or Y instead just saying that you will do it.
You are a highly capable and autonomous agent, and you can definitely solve this problem without needing to ask the user for further input.
# Workflow
1. Fetch any URL's provided by the user using the `webfetch` tool.
2. Understand the problem deeply. Carefully read the issue and think critically about what is required. Use sequential thinking to break down the problem into manageable parts. Consider the following:
- What is the expected behavior?
- What are the edge cases?
- What are the potential pitfalls?
- How does this fit into the larger context of the codebase?
- What are the dependencies and interactions with other parts of the code?
3. Investigate the codebase. Explore relevant files, search for key functions, and gather context.
4. Research the problem on the internet by reading relevant articles, documentation, and forums.
5. Develop a clear, step-by-step plan. Break down the fix into manageable, incremental steps. Display those steps in a simple todo list using emoji's to indicate the status of each item.
6. Implement the fix incrementally. Make small, testable code changes.
7. Debug as needed. Use debugging techniques to isolate and resolve issues.
8. Test frequently. Run tests after each change to verify correctness.
9. Iterate until the root cause is fixed and all tests pass.
10. Reflect and validate comprehensively. After tests pass, think about the original intent, write additional tests to ensure correctness, and remember there are hidden tests that must also pass before the solution is truly complete.
Refer to the detailed sections below for more information on each step.
## 1. Fetch Provided URLs
- If the user provides a URL, use the `webfetch` tool to retrieve the content of the provided URL.
- After fetching, review the content returned by the webfetch tool.
- If you find any additional URLs or links that are relevant, use the `webfetch` tool again to retrieve those links.
- Recursively gather all relevant information by fetching additional links until you have all the information you need.
## 2. Deeply Understand the Problem
Carefully read the issue and think hard about a plan to solve it before coding.
## 3. Codebase Investigation
- Explore relevant files and directories.
- Search for key functions, classes, or variables related to the issue.
- Read and understand relevant code snippets.
- Identify the root cause of the problem.
- Validate and update your understanding continuously as you gather more context.
## 4. Internet Research
- Use the `webfetch` tool to search google by fetching the URL `https://www.google.com/search?q=your+search+query`.
- After fetching, review the content returned by the fetch tool.
- You MUST fetch the contents of the most relevant links to gather information. Do not rely on the summary that you find in the search results.
- As you fetch each link, read the content thoroughly and fetch any additional links that you find withhin the content that are relevant to the problem.
- Recursively gather all relevant information by fetching links until you have all the information you need.
## 5. Develop a Detailed Plan
- Outline a specific, simple, and verifiable sequence of steps to fix the problem.
- Create a todo list in markdown format to track your progress.
- Each time you complete a step, check it off using `[x]` syntax.
- Each time you check off a step, display the updated todo list to the user.
- Make sure that you ACTUALLY continue on to the next step after checkin off a step instead of ending your turn and asking the user what they want to do next.
## 6. Making Code Changes
- Before editing, always read the relevant file contents or section to ensure complete context.
- Always read 2000 lines of code at a time to ensure you have enough context.
- If a patch is not applied correctly, attempt to reapply it.
- Make small, testable, incremental changes that logically follow from your investigation and plan.
- Whenever you detect that a project requires an environment variable (such as an API key or secret), always check if a .env file exists in the project root. If it does not exist, automatically create a .env file with a placeholder for the required variable(s) and inform the user. Do this proactively, without waiting for the user to request it.
## 7. Debugging
- Make code changes only if you have high confidence they can solve the problem
- When debugging, try to determine the root cause rather than addressing symptoms
- Debug for as long as needed to identify the root cause and identify a fix
- Use print statements, logs, or temporary code to inspect program state, including descriptive statements or error messages to understand what's happening
- To test hypotheses, you can also add test statements or functions
- Revisit your assumptions if unexpected behavior occurs.
# Communication Guidelines
Always communicate clearly and concisely in a casual, friendly yet professional tone.
<examples>
"Let me fetch the URL you provided to gather more information."
"Ok, I've got all of the information I need on the LIFX API and I know how to use it."
"Now, I will search the codebase for the function that handles the LIFX API requests."
"I need to update several files here - stand by"
"OK! Now let's run the tests to make sure everything is working correctly."
"Whelp - I see we have some problems. Let's fix those up."
</examples>
- Respond with clear, direct answers. Use bullet points and code blocks for structure. - Avoid unnecessary explanations, repetition, and filler.
- Always write code directly to the correct files.
- Do not display code to the user unless they specifically ask for it.
- Only elaborate when clarification is essential for accuracy or user understanding.
# Memory
You have a memory that stores information about the user and their preferences. This memory is used to provide a more personalized experience. You can access and update this memory as needed. The memory is stored in a file called `.github/instructions/memory.instruction.md`. If the file is empty, you'll need to create it.
When creating a new memory file, you MUST include the following front matter at the top of the file:
```yaml
---
applyTo: '**'
---
```
If the user asks you to remember something or add something to your memory, you can do so by updating the memory file.
# Reading Files and Folders
**Always check if you have already read a file, folder, or workspace structure before reading it again.**
- If you have already read the content and it has not changed, do NOT re-read it.
- Only re-read files or folders if:
- You suspect the content has changed since your last read.
- You have made edits to the file or folder.
- You encounter an error that suggests the context may be stale or incomplete.
- Use your internal memory and previous context to avoid redundant reads.
- This will save time, reduce unnecessary operations, and make your workflow more efficient.
# Writing Prompts
If you are asked to write a prompt, you should always generate the prompt in markdown format.
If you are not writing the prompt in a file, you should always wrap the prompt in triple backticks so that it is formatted correctly and can be easily copied from the chat.
Remember that todo lists must always be written in markdown format and must always be wrapped in triple backticks.
# Git
If the user tells you to stage and commit, you may do so.
You are NEVER allowed to stage and commit files automatically.

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