Compare commits

...

1083 Commits

Author SHA1 Message Date
Frank c32923723b wip: vscode extension
publish-github-action / publish (push) Waiting to run
publish-vscode / publish (push) Waiting to run
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) Waiting to run
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) Waiting to run
publish / publish (push) Waiting to run
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) Waiting to run
2025-07-19 22:38:36 -04:00
Dax Raad 079526743f fix bash tool rendering
publish / publish (push) Waiting to run
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) Waiting to run
2025-07-19 19:30:12 -04:00
Dax Raad b562f88cd8 ci: ignore
publish / publish (push) Waiting to run
2025-07-19 19:29:05 -04:00
Dax Raad 3f0f80297f Revert "fix: prevent sparse spacing in hyphenated words (#1102)"
publish / publish (push) Waiting to run
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) Waiting to run
2025-07-19 18:20:07 -05:00
Dax Raad 73554b6909 fix first run github copilot
publish / publish (push) Waiting to run
2025-07-19 19:19:38 -04:00
Dax Raad fda02482c7 ci: ignore
publish / publish (push) Waiting to run
2025-07-19 19:00:17 -04:00
Dax Raad b10f0868d6 ci: ignore
publish / publish (push) Waiting to run
2025-07-19 18:54:46 -04:00
Dax Raad 23d1f9380a ignore: ci
publish / publish (push) Waiting to run
2025-07-19 18:48:43 -04:00
Dax Raad 64c9908db4 perf: more performance improvements
publish / publish (push) Waiting to run
2025-07-19 18:41:21 -04:00
Dax Raad 85bbcce7db perf: scroll to bottom in thread
publish / publish (push) Waiting to run
2025-07-19 17:55:01 -04:00
Dax Raad b236673fa4 disable todo tools when running as task
publish / publish (push) Waiting to run
2025-07-19 15:54:11 -04:00
Dax Raad 44c91d8c55 ignore: create memo abstraction
publish / publish (push) Waiting to run
2025-07-19 15:26:26 -04:00
Dax Raad f5f0f57098 fix openrouter caching
publish / publish (push) Waiting to run
2025-07-19 15:11:21 -04:00
Dax Raad 86541ff922 shitty hack for terrible charm bubbletea performance
publish / publish (push) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
2025-07-15 14:00:52 -04:00
adamdotdevin 0ae563491b fix(tui): wider max width
publish / publish (push) Waiting to run
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) Waiting to run
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) Waiting to run
2025-07-15 00:35:02 -04:00
Dax Raad f72567716f bring back task tool
publish / publish (push) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
2025-07-13 23:06:31 -04:00
Dax Raad fd57a04d9c ci
publish / publish (push) Waiting to run
2025-07-13 22:58:33 -04:00
Dax Raad eb9cb012a2 fix missing tokens
publish / publish (push) Waiting to run
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) Waiting to run
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) Waiting to run
2025-07-11 21:09:39 -04:00
Dax Raad a799fdb96f switch global config to ~/.config/opencode/opencode.json
publish / publish (push) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
2025-07-10 15:58:45 -04:00
Dax Raad d06dd4bfbc config: escape file: string content
publish / publish (push) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
2025-07-09 22:54:47 -04:00
Dax Raad e179ade5d8 better detection of prettier formatter
publish / publish (push) Waiting to run
2025-07-09 22:37:31 -04:00
Dax Raad 1ba82bc7cb do not wait for LSP to be fully ready
publish / publish (push) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
2025-07-08 18:33:12 -04:00
Dax Raad 57cd48c53e properly load typescript lsp in subpaths
publish / publish (push) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
2025-07-07 17:39:52 -04:00
Dax Raad 5cd08e988a fix mcp tools corrupting session
publish / publish (push) Waiting to run
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) Waiting to run
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) Waiting to run
2025-07-07 16:12:47 -04:00
Dax 9cad702612 v2 message format and upgrade to ai sdk v5 (#743)
publish / publish (push) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
2025-07-03 10:53:43 -05:00
adamdottv b7bd7c28e2 fix(tui): redundant tool calls in each message in collapsed mode
publish / publish (push) Waiting to run
2025-07-03 10:42:27 -05:00
Dax Raad 89821f3f69 flag for disabling file watcher 2025-07-03 10:37:08 -04:00
Dax Raad 7a4aa43f22 increase max retries to 10
publish / publish (push) Waiting to run
2025-07-03 10:32:55 -04:00
adamdottv 384ab92fe4 fix(tui): include orphaned tool calls 2025-07-03 09:32:44 -05:00
adamdottv fdeff1c5b4 fix(tui): selected message visuals 2025-07-03 09:03:04 -05:00
Dax Raad 36e70ddf8a fix webdomain 2025-07-03 09:58:25 -04:00
GitHub Action 5924c5170e Update download stats 2025-07-03 2025-07-03 12:26:51 +00:00
adamdottv f733412538 feat(tui): unshare command
publish / publish (push) Waiting to run
2025-07-03 07:09:09 -05:00
adamdottv 85d21de2bc fix(tui): always show status bar 2025-07-03 06:53:05 -05:00
adamdottv b99f76142b fix(tui): better highlight visuals 2025-07-03 06:49:37 -05:00
Craig Andrews a7ae9e2c7e fix: simplify parallel map using channels (#582) 2025-07-03 05:43:10 -05:00
Lev 685969001c fix: properly handle utf-8 in diff highlighting (#585) 2025-07-03 05:42:40 -05:00
Dax Raad d918a64481 do not install gopls if go is not installed
publish / publish (push) Waiting to run
2025-07-02 23:59:08 -04:00
Jacob Hands 0470bb18a0 fix: use correct opencode bin path when running in development mode (#483)
publish / publish (push) Waiting to run
2025-07-02 23:37:48 -04:00
Timo Clasen c2bee00385 fix: typescript error (#618) 2025-07-02 23:27:43 -04:00
Adi Yeroslav df7b7b7234 Update prompt reference from CLAUDE.md to AGENTS.md (#623) 2025-07-02 23:27:22 -04:00
Aiden Cline bd1d1f4dc0 feat: configurable instructions (#624) 2025-07-02 23:27:04 -04:00
andrewxt 05a535d1c3 fix mouse scroll events being interpreted as keyboard input (#628) 2025-07-02 23:26:09 -04:00
Dax Raad b09f93b6ee remove need for glibc
publish / publish (push) Waiting to run
2025-07-02 22:53:04 -04:00
Dax Raad 196fc6b0e1 fix(tui): change messages layout toggle keybinding from <leader>m to <leader>p
publish / publish (push) Waiting to run
🤖 Generated with [opencode](https://opencode.ai)

Co-Authored-By: opencode <noreply@opencode.ai>
2025-07-02 20:06:30 -04:00
Aiden Cline 6f4eb70bb9 fix: unawaited promise causes opencode to use unenabled formatter (#625)
publish / publish (push) Waiting to run
2025-07-02 19:19:31 -04:00
Jay V 5da78a48d8 docs: tweak styles 2025-07-02 18:44:05 -04:00
adamdottv e9e786746f fix: typescript error 2025-07-02 16:08:41 -05:00
adamdottv cbae63697e feat(tui): file viewer, select messages 2025-07-02 16:08:11 -05:00
Ciaran McAleer 1dd3bcae9f Changed handling of OpenRouter requests to add some custom headers so that it can see the app (#613)
Co-authored-by: Dax Raad <d@ironbay.co>
2025-07-02 14:43:59 -04:00
Dax Raad 6a8e961119 wip: session revert/unrevert 2025-07-02 13:10:36 -04:00
Mark Huggins 70dbfa4d00 fix: Copilot Premium Requests (#595) 2025-07-02 12:04:53 -04:00
Prashant Choudhary af1ed486b0 fix: Ensure shared file previews use truncated content (#607)
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2025-07-02 12:04:10 -04:00
Timo Clasen 46faca1f62 fix(formatting): check for enabled formatters (#611) 2025-07-02 12:03:42 -04:00
Dax Raad 82c1c87be7 wip: symbols for lsp 2025-07-02 11:35:25 -04:00
Dax Raad eea253cd12 fix lazy loading 2025-07-02 11:18:25 -04:00
Dax Raad 7822227eec fix filewatcher not closing cleanly
publish / publish (push) Waiting to run
2025-07-02 11:15:12 -04:00
GitHub Action cd6936ce50 Update download stats 2025-07-02
publish / publish (push) Waiting to run
2025-07-02 12:26:24 +00:00
Timo Clasen 6ab9f3f277 fix(tui): no space between agent and user message (#598) 2025-07-02 05:12:49 -05:00
Jean du Plessis 746c00dd7e fix: Small typo in CLI --model flag description (#577) 2025-07-02 05:10:58 -05:00
Daniel Vélez f32f58248c chore: rename OpenCode to opencode (#579) 2025-07-02 05:09:51 -05:00
Dax Raad b181a35ca5 improve ripgrep download
publish / publish (push) Waiting to run
2025-07-01 22:39:17 -04:00
Dax Raad 838115e2cd ci: tweaks 2025-07-01 22:25:53 -04:00
Dax Raad c6166de726 ignore: read deleted files 2025-07-01 20:45:50 -04:00
Dax Raad 7b0edfa712 ignore: add file status command 2025-07-01 20:44:12 -04:00
Dax Raad 93e0e4212c ignore: add file status command 2025-07-01 20:39:43 -04:00
Dax Raad 9e03bd911b ignore: fix file read with diff 2025-07-01 20:08:42 -04:00
Aiden Cline 7d049da15c fix: handle null case if tool args are empty for todos (#588) 2025-07-01 18:25:23 -05:00
Frank c92e3b9a84 Temporarily add admin unshare api 2025-07-01 18:57:08 -04:00
Dax Raad a6c5cbc24e fix write tool timeout
publish / publish (push) Waiting to run
2025-07-01 13:50:57 -04:00
Dax Raad 63f9ee3f5a add file watcher 2025-07-01 13:45:25 -04:00
Dax Raad b47f1fb9fa snapshot functionality 2025-07-01 12:28:34 -04:00
adamdottv 01f00e1c5e fix(tui): better message rendering performance
publish / publish (push) Waiting to run
2025-07-01 07:57:45 -05:00
GitHub Action 89fcd16d9c Update download stats 2025-07-01 2025-07-01 12:27:09 +00:00
adamdottv 05ff77816e fix(tui): layout issues 2025-07-01 06:41:39 -05:00
Adam Spiers f40a780ec8 docs: use correct baseUrl for astro editLink (#507)
Co-authored-by: Adam Spiers <opencode@adamspiers.org>
2025-07-01 05:31:18 -05:00
Timo Clasen 07ef626992 feat: better model dialog with sorting by release date (#563) 2025-07-01 05:28:32 -05:00
Dax Raad 23bcc0defe fix lsp diagnostic accurancy
publish / publish (push) Waiting to run
2025-06-30 22:48:32 -04:00
Dax Raad e8c1c74b94 fix loading api key from env for openai compatible providers
publish / publish (push) Waiting to run
2025-06-30 19:07:51 -04:00
Max Rabin c3b02e205b Add pyright lsp for Python (#551)
Co-authored-by: Max Rabin <max.rabin@mobileye.com>
2025-06-30 18:17:47 -04:00
adamdottv de2142c9a3 chore: typescript error 2025-06-30 15:46:18 -05:00
adamdottv f83afc58c8 chore: include model release date 2025-06-30 15:46:18 -05:00
Dax Raad 605545a7a3 optimistically boot lsp servers 2025-06-30 16:45:26 -04:00
Jeremy Mack f8b5c4e698 chore: remove duplicate EditTool in TOOLS array (#556) 2025-06-30 15:32:15 -04:00
Dax Raad d438216a95 fix bash tool extra line 2025-06-30 15:31:30 -04:00
Dax Raad 98de39d0b8 fix formatting of bash tools
publish / publish (push) Waiting to run
2025-06-30 15:28:59 -04:00
adamdottv bda7396a92 fix(tui): layout issues
publish / publish (push) Waiting to run
2025-06-30 14:04:56 -05:00
Dax Raad 576656ce75 add back in file hierarchy in system prompt but limit to 200 items
publish / publish (push) Waiting to run
2025-06-30 14:46:46 -04:00
adamdottv 0215e11ac7 chore: rework layout primitives
publish / publish (push) Waiting to run
2025-06-30 12:29:29 -05:00
Timo Clasen da26642494 feat: update user and agent messages width and alignment (#515)
Co-authored-by: adamdottv <2363879+adamdottv@users.noreply.github.com>
2025-06-30 11:57:56 -05:00
Dmytro Yankovskyi f2162fade9 fix(#467): more granular bedrock modelID based on aws region (#482) 2025-06-30 11:12:30 -04:00
Dax Raad 123c5f0543 docs: readme 2025-06-30 10:56:38 -04:00
GitHub Action 24c575e464 Update download stats 2025-06-30 2025-06-30 12:26:30 +00:00
Dax Raad 84f7d88d84 add ruby formatter and lsp 2025-06-29 22:00:08 -04:00
Timo Clasen 609145d352 docs: how to disable mcp server (#543)
publish / publish (push) Waiting to run
Co-authored-by: GitHub Action <action@github.com>
2025-06-29 21:33:30 -04:00
Dax Raad 12baee0927 ignore: refactoring 2025-06-29 21:30:23 -04:00
Dax Raad 6e84fdab27 add zig, python, clang, and kotlin formatters
Co-authored-by: Suhas-Koheda <Suhas-Koheda@users.noreply.github.com>
Co-authored-by: Polo123456789 <Polo123456789@users.noreply.github.com>
Co-authored-by: theodore-s-beers <theodore-s-beers@users.noreply.github.com>
Co-authored-by: TylerHillery <TylerHillery@users.noreply.github.com>
2025-06-29 21:27:35 -04:00
Dax Raad 66d03b0a94 restructure formatters 2025-06-29 21:22:21 -04:00
Polo123456789 c6b7e173e1 Formatters as plugins (#487) 2025-06-29 21:13:32 -04:00
Adam Spiers c40d0b46cb chore: add .editorconfig (#536)
Co-authored-by: Adam Spiers <opencode@adamspiers.org>
2025-06-29 21:12:58 -04:00
Dax Raad 128cbe4652 fix issue with costs resetting once chat is completed
publish / publish (push) Waiting to run
2025-06-29 19:43:03 -04:00
Dax Raad b14e6702df better amazon bedrock caching with anthropic models
publish / publish (push) Waiting to run
2025-06-29 19:27:07 -04:00
Dax 44c405c7ab Update STATS.md 2025-06-29 13:04:44 -04:00
GitHub Action 0e439cbb77 Update download stats 2025-06-29 2025-06-29 12:23:40 +00:00
GitHub Action 1c3e0f6b70 Update download stats 2025-06-29 2025-06-29 02:00:18 +00:00
Dax Raad b20c0e47d3 ci: stats 2025-06-28 21:59:14 -04:00
Dax Raad bd20786c6b ci: daily stats script 2025-06-28 21:57:46 -04:00
Timo Clasen 62ee067c3b Add Option to Disable MCP Servers (#513) 2025-06-28 21:05:31 -04:00
Dax Raad 91fc0dd8a8 fix context display
publish / publish (push) Waiting to run
2025-06-28 21:01:53 -04:00
adamdottv f1cee9025d fix(tui): share command and error messages
publish / publish (push) Waiting to run
2025-06-28 17:51:28 -05:00
Dax Raad 7c7f7c9dfb patch for scroll dumping characters into input buffer
publish / publish (push) Waiting to run
2025-06-28 11:56:47 -04:00
Dax Raad 55d01efa1e ci: fix
publish / publish (push) Waiting to run
2025-06-28 09:16:29 -04:00
Gal Schlezinger 1121e24802 optimize edit-tool rendering (#463)
publish / publish (push) Waiting to run
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com>
2025-06-28 06:01:10 -05:00
Jay V d38ea2b059 docs: share handle slower code blocks 2025-06-27 20:21:28 -04:00
Jay V 869e066d11 docs: adding more share images 2025-06-27 20:03:17 -04:00
Jay V 2d14941574 docs: tweak logo 2025-06-27 19:18:54 -04:00
Jay V 06f034258b Merge branch 'jeremyosih-feat/scroll-to-bottom-button' into dev 2025-06-27 19:16:46 -04:00
Jay V d12ecb71e1 docs: share page format 2025-06-27 19:16:33 -04:00
Dax Raad 1aeb35debe smooth out initial onboarding flow 2025-06-27 19:10:42 -04:00
Jay V 1a23e403cd docs: share cleanup title 2025-06-27 19:10:42 -04:00
adamdottv 9acc472d87 chore: rework openapi spec and use stainless sdk 2025-06-27 19:10:42 -04:00
Dax Raad fb410ed8fa temporary patch for input lag on initial run 2025-06-27 19:10:42 -04:00
Polo123456789 6b4f339b93 feat: Add golang file formatting (#474) 2025-06-27 19:10:42 -04:00
Wendell Misiedjan a909016e1c fix: bunproc stdout / stderr parsing, error handling for bun ResolveMessage (#468) 2025-06-27 19:10:42 -04:00
Jay V b132ef1409 docs: update logo screenshot 2025-06-27 19:10:42 -04:00
Jay V 12b3557f00 docs: share page write tool bug 2025-06-27 19:10:42 -04:00
Dax Raad 9c915818f0 ci: ignore 2025-06-27 19:10:42 -04:00
Dax Raad cd6f21700b ci: aur 2025-06-27 19:10:42 -04:00
Dax Raad d0866dbaa7 ignore: cleanup 2025-06-27 19:10:42 -04:00
Dax Raad 6cc740e319 lazy load formatters 2025-06-27 19:10:42 -04:00
Ryan Winchester ed02dbb9a5 feat: add elixir file formatting (#458) 2025-06-27 19:10:42 -04:00
adamdottv d79fb8d885 fix(web): remove system prompts from share page 2025-06-27 19:10:42 -04:00
TheGoddessInari 624d89e363 scripts/hooks: Change shebang to universal /bin/sh (#453) 2025-06-27 19:10:41 -04:00
adamdottv db8a86e68b fix(tui): min width on user messages 2025-06-27 19:10:41 -04:00
Dax Raad 5810778ece fix formatting output going into tui 2025-06-27 19:10:41 -04:00
adamdottv 6f47532119 ignore: more metadata in app info 2025-06-27 19:10:41 -04:00
adamdottv 6b41ab2cb1 fix: don't use prettier for langs it doesn't format 2025-06-27 19:10:41 -04:00
Dax Raad bc35c5c8be ignore: run prettier 2025-06-27 19:10:41 -04:00
Dax Raad 58b0356501 add auto formatting and experimental hooks feature 2025-06-27 19:10:41 -04:00
Jay V 077d1ad801 docs: lander tweak 2025-06-27 19:10:41 -04:00
Jay V 314a230809 docs: tweak lander 2025-06-27 19:10:41 -04:00
Dax Raad 6abe3d1861 smooth out initial onboarding flow 2025-06-27 16:09:59 -04:00
Jay V 07f42ae5ed docs: share cleanup title 2025-06-27 15:31:21 -04:00
adamdottv fce49d6887 chore: rework openapi spec and use stainless sdk 2025-06-27 14:26:25 -05:00
Dax Raad 863a377ba0 temporary patch for input lag on initial run
publish / publish (push) Waiting to run
2025-06-27 14:36:03 -04:00
Polo123456789 1fcb9ce01b feat: Add golang file formatting (#474) 2025-06-27 14:11:09 -04:00
Wendell Misiedjan f5a998b425 fix: bunproc stdout / stderr parsing, error handling for bun ResolveMessage (#468) 2025-06-27 14:09:35 -04:00
Jay V d07aeb7281 docs: update logo screenshot 2025-06-27 14:04:09 -04:00
Jay V 66b00a2113 docs: share page write tool bug 2025-06-27 13:25:15 -04:00
Dax Raad 5a9cef2775 ci: ignore
publish / publish (push) Waiting to run
2025-06-27 12:37:57 -04:00
Dax Raad e0fff88a34 ci: aur
publish / publish (push) Waiting to run
2025-06-27 12:29:13 -04:00
Dax Raad a23f5b0605 ignore: cleanup
publish / publish (push) Waiting to run
2025-06-27 11:35:57 -04:00
Dax Raad cdbaf1c2d2 lazy load formatters 2025-06-27 11:33:37 -04:00
Ryan Winchester 56ceed0b45 feat: add elixir file formatting (#458) 2025-06-27 10:15:11 -04:00
adamdottv 18b5d647ba fix(web): remove system prompts from share page 2025-06-27 06:48:44 -05:00
TheGoddessInari fef2234408 scripts/hooks: Change shebang to universal /bin/sh (#453) 2025-06-27 07:40:22 -04:00
adamdottv 651673b545 fix(tui): min width on user messages 2025-06-27 06:31:13 -05:00
Dax Raad 9141f437ad fix formatting output going into tui
publish / publish (push) Waiting to run
2025-06-27 07:29:41 -04:00
adamdottv 1f05d9f9f6 ignore: more metadata in app info 2025-06-27 06:19:27 -05:00
adamdottv 4b518c22b0 fix: don't use prettier for langs it doesn't format
publish / publish (push) Waiting to run
2025-06-27 05:47:14 -05:00
Dax Raad a94c88bc5b ignore: run prettier
publish / publish (push) Waiting to run
2025-06-26 22:30:44 -04:00
Dax Raad 2142c34fdd add auto formatting and experimental hooks feature
publish / publish (push) Waiting to run
2025-06-26 22:17:08 -04:00
Jay V 07418353dc docs: lander tweak 2025-06-26 19:47:58 -04:00
Jay V f318800d1c docs: tweak lander 2025-06-26 19:02:44 -04:00
Jeremy Osih 7ddff18c60 Merge branch 'sst:dev' into feat/scroll-to-bottom-button 2025-06-27 00:41:20 +02:00
Jeremy Osih e2b2ab93e7 feat(web): add scroll to last message button
Add intelligent floating scroll button for long conversations that:
- Only appears when scrolling down (direction-aware)
- Auto-hides after 3 seconds of inactivity
- Stays visible on hover to prevent accidental disappearance
- Uses consistent design patterns with repo styling
- Includes proper accessibility features

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

Co-Authored-By: Jeremy Osih <osih.jeremy@gmail.com>
Co-Authored-By: opencode <noreply@opencode.ai>
2025-06-27 00:38:14 +02:00
Dax Raad bcc5b62ab1 disable task tool temporarily
publish / publish (push) Waiting to run
2025-06-26 18:27:49 -04:00
Jay V 6c8b7ef436 docs: edit theme doc 2025-06-26 17:56:31 -04:00
Dax Raad 7b3a84ec23 fix: improve JSON formatting and add piped output support for run command
🤖 Generated with [opencode](https://opencode.ai)

Co-Authored-By: opencode <noreply@opencode.ai>
2025-06-26 17:32:00 -04:00
Jay V 81ec62671c Merge branch 'rekram1-node-chore/update-config-docs' into dev 2025-06-26 17:24:03 -04:00
Jay V e445a8f1e3 docs: editing rules 2025-06-26 17:23:52 -04:00
adamdottv dd739a913c fix(tui): editor styles were off 2025-06-26 17:22:21 -04:00
adamdottv 0beabc75c5 feat(tui): more themes 2025-06-26 17:22:21 -04:00
adamdottv cdd24efe28 feat(tui): more themes 2025-06-26 17:22:21 -04:00
adamdottv 74980357f7 fix(tui): editor width issues 2025-06-26 17:22:21 -04:00
adamdottv e7dcb3a646 feat(tui): show lsp diagnostics for edit and write tools 2025-06-26 17:22:21 -04:00
adamdottv 82b8777f73 fix(tui): chat editor aesthetics 2025-06-26 17:22:21 -04:00
adamdottv 8c6ead66e6 fix(tui): visual issue with modal selected items in system theme 2025-06-26 17:22:21 -04:00
adamdottv e152ab1c3e chore: tui agents.md 2025-06-26 17:22:21 -04:00
Jay V 6b0bc17f55 docs: social share 2025-06-26 17:22:17 -04:00
adamdottv d0630ffbce fix(tui): editor spinner colors 2025-06-26 17:21:53 -04:00
Jay V f91e6e39a6 docs: share page responsive diff 2025-06-26 17:21:53 -04:00
adamdottv f4d718e26f docs: system theme 2025-06-26 17:21:53 -04:00
adamdottv f3b7b59a99 fix(tui): make opencode theme default 2025-06-26 17:21:53 -04:00
Adam 2267e4dda2 feat: default system theme (#419)
Co-authored-by: adamdottv <2363879+adamdottv@users.noreply.github.com>
2025-06-26 17:21:53 -04:00
Mike Wallio f038bf694a Fix undefined is not an object (evaluating 'G.title') (#395) 2025-06-26 17:21:53 -04:00
Juhani Pelli b36c9ef4f2 fix: guard against large output limit causing infinite summarize loop (#399) 2025-06-26 17:21:53 -04:00
Dax Raad 72cf101b3f more edit tool fixes 2025-06-26 17:21:53 -04:00
Dax Raad eed50647c4 integrate gemini-cli strategies for edit tool 2025-06-26 17:21:53 -04:00
Dax Raad 0c4ed45f75 make edit tool more robust 2025-06-26 17:21:53 -04:00
Dax Raad 09c1c5cd41 ignore: typo 2025-06-26 17:21:53 -04:00
Dax Raad 47853d656c add output length errors 2025-06-26 17:21:53 -04:00
Dax Raad 8c71b48b62 fix codex not working 2025-06-26 17:21:53 -04:00
Dax Raad 14033dcc3c redirect uncaught errors to log file 2025-06-26 17:21:53 -04:00
adamdottv c3b68acf50 fix(tui): editor styles were off
publish / publish (push) Waiting to run
2025-06-26 15:12:26 -05:00
adamdottv b9ad7efbaf feat(tui): more themes
publish / publish (push) Waiting to run
2025-06-26 15:03:30 -05:00
adamdottv 499c8753b7 feat(tui): more themes 2025-06-26 14:54:32 -05:00
adamdottv f6554bfd19 fix(tui): editor width issues
publish / publish (push) Waiting to run
2025-06-26 12:57:11 -05:00
adamdottv 912ef162a0 feat(tui): show lsp diagnostics for edit and write tools 2025-06-26 12:47:17 -05:00
adamdottv baeee448a8 fix(tui): chat editor aesthetics 2025-06-26 12:44:44 -05:00
adamdottv 6e84d1d671 fix(tui): visual issue with modal selected items in system theme
publish / publish (push) Waiting to run
2025-06-26 12:33:06 -05:00
adamdottv 95aaa6ded5 chore: tui agents.md 2025-06-26 12:28:29 -05:00
Jay V 44e85db4ec docs: social share 2025-06-26 13:21:42 -04:00
adamdottv c05b3e86fd fix(tui): editor spinner colors 2025-06-26 12:21:20 -05:00
Jay V dd9d55f07a docs: share page responsive diff 2025-06-26 13:06:41 -04:00
adamdottv 8a77348fd9 docs: system theme 2025-06-26 11:33:02 -05:00
adamdottv a07424a36e fix(tui): make opencode theme default 2025-06-26 11:32:25 -05:00
Adam 069d4b4c2c feat: default system theme (#419)
publish / publish (push) Waiting to run
Co-authored-by: adamdottv <2363879+adamdottv@users.noreply.github.com>
2025-06-26 10:16:07 -05:00
rekram1-node 7188551854 chore: document AGENTS.md 2025-06-26 08:28:06 -05:00
Mike Wallio 8ed884557e Fix undefined is not an object (evaluating 'G.title') (#395)
publish / publish (push) Waiting to run
2025-06-25 19:40:09 -04:00
Juhani Pelli 4220d272b2 fix: guard against large output limit causing infinite summarize loop (#399) 2025-06-25 19:39:51 -04:00
Dax Raad 19c4e09dd2 more edit tool fixes
publish / publish (push) Waiting to run
2025-06-25 19:22:54 -04:00
Dax Raad 04f770b57e integrate gemini-cli strategies for edit tool
publish / publish (push) Waiting to run
2025-06-25 17:56:14 -04:00
Dax Raad 1227651bbc make edit tool more robust
publish / publish (push) Waiting to run
2025-06-25 17:10:48 -04:00
Dax Raad e82ff0e159 ignore: typo 2025-06-25 11:02:57 -04:00
Dax Raad 56050bca9c add output length errors 2025-06-25 11:02:09 -04:00
Dax Raad e221442b6b fix codex not working
publish / publish (push) Waiting to run
2025-06-25 10:01:35 -04:00
Dax Raad bf39e0ac01 redirect uncaught errors to log file
publish / publish (push) Waiting to run
2025-06-25 08:41:10 -04:00
Dax Raad 53e90f2764 better error message when bad directory is specified to start in 2025-06-24 22:28:25 -04:00
Dax Raad d333addc69 Enhance auth command with environment variable display and add models command
publish / publish (push) Waiting to run
🤖 Generated with [opencode](https://opencode.ai)

Co-Authored-By: opencode <noreply@opencode.ai>
2025-06-24 22:24:55 -04:00
Dax Raad 2461ee70d8 do not print error on ctrl+c during prompts 2025-06-24 22:09:43 -04:00
Dax Raad 05182debe9 ci: bun lock 2025-06-24 21:14:32 -04:00
Dax Raad 605b2f891f ci: fix type issue 2025-06-24 21:12:32 -04:00
Dax Raad cbcef488f6 remove accidental opanai autoloader
publish / publish (push) Waiting to run
2025-06-24 21:11:11 -04:00
Dax Raad c841e0a1cc added opencode serve command
publish / publish (push) Waiting to run
2025-06-24 20:52:09 -04:00
Jay V 3a4969c8b1 docs: share page styles 2025-06-24 19:34:35 -04:00
Jay V 24a1399fa0 docs: share page bash tool output 2025-06-24 19:28:51 -04:00
Aiden Cline 245f14dfda fix: file completions replaced wrong text when paths overlap (#378) 2025-06-24 18:13:15 -05:00
Dax Raad 171289e420 docs: windows instructions
publish / publish (push) Waiting to run
2025-06-24 18:54:59 -04:00
Dax Raad bfa4c87379 ci: windows 2025-06-24 18:46:43 -04:00
Dax Raad eb6e0d92cc ci: windows 2025-06-24 18:40:36 -04:00
Dax Raad 3dea857589 ci: windows 2025-06-24 18:27:57 -04:00
Dax Raad f6862be398 ci: windows 2025-06-24 18:13:15 -04:00
Dax ad32e5975a windows fixes (#374)
Co-authored-by: Matthew Glazar <strager.nds@gmail.com>
2025-06-24 18:05:04 -04:00
Jay 440ce260df docs: Share page diff view improvements (#373) 2025-06-24 17:11:43 -04:00
Dax Raad f1fe3b6826 ignore: revert
publish / publish (push) Waiting to run
2025-06-24 14:59:27 -04:00
Dax Raad 0b96daf6b7 ci: upgrade bun 2025-06-24 14:42:25 -04:00
Dax Raad 5849b8b9d3 potential fix for failing to install provider package on first run 2025-06-24 14:33:35 -04:00
Jay V 609ca542b7 docs: share code blocks in markdown 2025-06-24 13:53:59 -04:00
Adam b0b9740de9 feat: delete sessions (#362)
publish / publish (push) Waiting to run
Co-authored-by: adamdottv <2363879+adamdottv@users.noreply.github.com>
2025-06-24 11:07:41 -05:00
Dax Raad 96e0f3ffc3 remove debug code writing to /tmp/message.json
publish / publish (push) Waiting to run
2025-06-24 11:16:17 -04:00
Dax Raad 44fa5e5efe ignore: fix type errors 2025-06-24 11:09:36 -04:00
Dax Raad e54520ba82 externalize github copilot code 2025-06-24 10:42:19 -04:00
Tom 777645ea9b feat(tui): add debounce logic to escape key interrupt (#169)
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: adamdottv <2363879+adamdottv@users.noreply.github.com>
2025-06-24 06:31:02 -05:00
Dax Raad d4e82228b4 add HOMEBREW_NO_AUTO_UPDATE to brew upgrades
publish / publish (push) Waiting to run
2025-06-23 20:36:08 -04:00
Dax Raad 17f7c8c131 force use npm registry
publish / publish (push) Waiting to run
2025-06-23 20:23:37 -04:00
GitMurf 72e44a4c84 fix: typescript error (any) from models (#347) 2025-06-23 18:44:57 -04:00
Dax Raad d7b0155b31 glob tool should respect .gitignore
publish / publish (push) Waiting to run
2025-06-23 17:37:32 -04:00
Dax Raad 1894d4b014 bundle models.dev at build time and ignore refresh errors
publish / publish (push) Waiting to run
2025-06-23 14:50:19 -04:00
Dax Raad 4f5d8bba8e ignore: remove opencode.json 2025-06-23 14:32:57 -04:00
Lucas Grzegorczyk 151861f640 Fix project folder name starting with "-" in data (#323). Note old session data will still be in the old format in ~/.local/share/opencode/projects - you can remove the leading dash to recover the, 2025-06-23 14:31:51 -04:00
adamdottv 4250f7803b feat(tui): optimistically render user messages
publish / publish (push) Waiting to run
2025-06-23 12:30:20 -05:00
Jay V c550376116 fix help copy 2025-06-23 13:00:24 -04:00
adamdottv 26fa010891 fix(tui): less incorrect escapingn of < and > 2025-06-23 11:32:32 -05:00
adamdottv fe57afac6b fix: overlay border color issues 2025-06-23 11:21:49 -05:00
adamdottv 2674e9f6dd fix: markdown wrapping issue 2025-06-23 11:20:44 -05:00
adamdottv 98dc5bd3d3 chore: unused import 2025-06-23 10:21:57 -05:00
adamdottv b94a248a0f docs: disabled_providers 2025-06-23 10:21:25 -05:00
GitMurf 9c280d0852 feat(copilot): edit headers for better rate limit avoidance (#321) 2025-06-23 10:44:19 -04:00
Dax Raad 64e72f1eec more graceful mcp failures
publish / publish (push) Waiting to run
2025-06-22 21:10:05 -04:00
Dax Raad 20d06c6173 aws autoload include more env vars 2025-06-22 20:16:10 -04:00
Dax Raad daa89f2e11 fix double entry of github copilot in auth login
publish / publish (push) Waiting to run
2025-06-22 19:13:25 -04:00
Martin Palma b172f79404 Add Github Copilot OAuth authentication flow (#305) 2025-06-22 19:11:37 -04:00
Dax Raad f7c32c6b06 docs: contributing 2025-06-22 17:55:10 -04:00
adamdottv b874ff0888 fix(tui): color contrast fixes for nord 2025-06-22 15:17:18 -05:00
adamdottv 5df7b580c7 fix(tui): help dialog visuals 2025-06-22 14:28:16 -05:00
Márk Magyar a90cb9f298 ignore: fix typos and formatting (#294) 2025-06-22 14:26:46 -04:00
Tom 81f31481e0 fix: resolve test failures by adding missing zod-openapi import (#301)
Co-authored-by: opencode <noreply@opencode.ai>
2025-06-22 14:25:02 -04:00
Tom aa58c7de3a combine stdout and stderr in bash tool output (#300)
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: Dax Raad <d@ironbay.co>
2025-06-22 14:24:35 -04:00
Adam b654ce47ea fix(tui): mouse wheel escape codes leaking into input 2025-06-22 10:26:44 -05:00
adamdottv a157edb81f fix(tui): cleanup help dialog
publish / publish (push) Waiting to run
2025-06-22 06:44:23 -05:00
phantomreactor 129469a108 fix: invisible html tags and compact long delay (#304) 2025-06-22 06:29:04 -05:00
adamdottv 31d643736c fix(tui): cleanup modal visuals
publish / publish (push) Waiting to run
2025-06-22 06:09:23 -05:00
adamdottv 96252dbadc fix(tui): theme dialog visuals 2025-06-22 05:34:22 -05:00
Márk Magyar 63499e156f fix: session title generation (#293) 2025-06-21 14:32:11 -05:00
tyrellshawn 3826151828 Created a Theme inspired by the matrix (#285) 2025-06-21 07:29:49 -05:00
Thomas Meire 0ff3821efa Add error handling on the calls to the server to debug issue #132 (#137) 2025-06-21 07:24:39 -05:00
Dax Raad 0ae21cb235 footer clarifies it's showing context usage, not input token usage 2025-06-20 22:52:51 -04:00
Dax Raad 5cc23eeffa huge optimization for token usage with anthropic
publish / publish (push) Waiting to run
2025-06-20 22:43:04 -04:00
Dax Raad b11e2a5f57 Add --method option to upgrade command for manual installation method selection
publish / publish (push) Waiting to run
🤖 Generated with [opencode](https://opencode.ai)

Co-Authored-By: opencode <noreply@opencode.ai>
2025-06-20 20:48:23 -04:00
Dax Raad 04896da98c handle brew upgrades better 2025-06-20 20:27:23 -04:00
Dax Raad 4a59893f32 make IDs more random
publish / publish (push) Waiting to run
2025-06-20 17:39:59 -04:00
Saatvik Arya 222e9d686e docs: add API client generation instructions to README and AGENTS.md (#273) 2025-06-20 17:27:58 -04:00
Saatvik Arya 9f590063f6 fix: return false for missing AWS_PROFILE in amazon-bedrock provider (#277) 2025-06-20 17:27:27 -04:00
adamdottv f4c59e865a fix(tui): visual tweaks to themes
publish / publish (push) Waiting to run
2025-06-20 15:49:51 -05:00
adamdottv c1396302f8 docs: cleanup casing
publish / publish (push) Waiting to run
2025-06-20 15:35:25 -05:00
adamdottv b24299267e docs: new themes docs 2025-06-20 15:31:38 -05:00
adamdottv ac66652df1 feat(tui): one dark theme 2025-06-20 15:14:23 -05:00
adamdottv 74b025a4ea feat(tui): kanagawa theme 2025-06-20 15:14:23 -05:00
adamdottv bd68b08aac feat(tui): gruvbox theme 2025-06-20 15:14:23 -05:00
adamdottv ef51cb8c70 feat(tui): catppuccin theme 2025-06-20 15:14:22 -05:00
adamdottv c5ea952100 feat(tui): nord theme 2025-06-20 15:14:22 -05:00
adamdottv 4e3dc82ea4 feat(tui): custom themes 2025-06-20 15:14:22 -05:00
Márk Magyar 843f831497 feat(tui): theme switcher with preview (#264) 2025-06-20 15:14:05 -05:00
Jay V f0af9b3296 docs: share bugs 2025-06-20 15:50:12 -04:00
Jay V 024d093b2f docs: share page bugs 2025-06-20 15:50:12 -04:00
Dax Raad 9632a32ecd fix issue with conversations hanging 2025-06-20 15:49:49 -04:00
Dax Raad 1e9fa8d022 docs: readme 2025-06-20 15:22:41 -04:00
Dax Raad 4ec02330f3 Consolidate session context handling and add global config support
publish / publish (push) Waiting to run
Refactored context file discovery by removing separate SessionContext module and integrating functionality into SystemPrompt.context(). Added support for finding AGENTS.md and CLAUDE.md files in global config directories.

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

Co-Authored-By: opencode <noreply@opencode.ai>
2025-06-20 15:14:12 -04:00
Josh a3c73c4322 feat: forward provider options from model config (#202)
publish / publish (push) Waiting to run
Co-authored-by: Dax Raad <d@ironbay.co>
2025-06-20 15:03:41 -04:00
Dmytro Yankovskyi 0419533097 fix(#243): claude on aws bedrock (#241)
publish / publish (push) Waiting to run
Co-authored-by: Dax Raad <d@ironbay.co>
2025-06-20 14:57:33 -04:00
niba b28abc7b6c chore: use client_id everywhere (#260) 2025-06-20 14:56:33 -04:00
adamdottv ef298f3e9e docs: theme schema update 2025-06-20 13:51:32 -05:00
adamdottv e1a186f869 docs: theme schema 2025-06-20 13:00:32 -05:00
adamdottv fdbdf1d281 chore(tui): rename theme colors 2025-06-20 13:00:31 -05:00
Jay V 2746ce1673 docs: add config 2025-06-20 13:22:31 -04:00
Jay V 91b4ee0c15 docs: share header 2025-06-20 13:12:35 -04:00
Jay V c0533a86ab docs: test 2025-06-20 13:02:05 -04:00
Jay V b86828ded7 docs: test 2025-06-20 12:59:32 -04:00
Jay V 7f3362a773 docs: test 2025-06-20 12:56:00 -04:00
adamdottv 65dcb33041 fix(tui): async load messages on theme/session switch 2025-06-20 11:25:21 -05:00
adamdottv 02ec438865 fix(tui): input latency optimization 2025-06-20 11:08:08 -05:00
adamdottv 8bb4f64b0b fix(tui): fetch tool more defensive
publish / publish (push) Waiting to run
2025-06-20 09:00:28 -05:00
adamdottv bddfa6557b fix(tui): typeahead open/close perf 2025-06-20 08:20:10 -05:00
adamdottv e57ef002ee chore: fix typescript issues 2025-06-20 07:48:42 -05:00
adamdottv c90a1a4467 fix(tui): insert newline correctly positioned 2025-06-20 07:42:04 -05:00
adamdottv 0b7358672e fix(tui): input feels laggy 2025-06-20 07:31:45 -05:00
Dax Raad 76cd9dc4b1 docs: remove opencode.json 2025-06-20 01:00:15 -04:00
Dax Raad cb7930165c config validation
publish / publish (push) Waiting to run
2025-06-20 00:57:28 -04:00
Rohan Godha d9d0d6725f fix: phantom input bug on wsl (#200) 2025-06-19 20:08:56 -05:00
Tom Watkins 13212960b8 docs: fix typo in config.mdx (#218) 2025-06-19 21:08:21 -04:00
Dax Raad 66114f4480 fix for azure models not liking tool definitions
publish / publish (push) Waiting to run
2025-06-19 18:28:42 -04:00
Jay V 0638941067 docs: tweak lander 2025-06-19 18:19:35 -04:00
Jay 78341ed135 docs: Update README.md 2025-06-19 18:19:06 -04:00
Dax Raad 2f35addf54 remove unused permission timeout
publish / publish (push) Waiting to run
2025-06-19 18:00:53 -04:00
Dax Raad 692ee660c2 temporarily disable project details in system prompt
publish / publish (push) Waiting to run
2025-06-19 17:37:23 -04:00
Frank 6c28afed0b Infra: fix DO tag 2025-06-19 17:20:13 -04:00
Dax Raad 58a19b9b7a fix gopls download spewing into terminal
publish / publish (push) Waiting to run
2025-06-19 17:08:58 -04:00
adamdottv bed7486889 fix(tui): upgrade notification
publish / publish (push) Waiting to run
2025-06-19 16:03:45 -05:00
Jay V 0e42178044 docs: share 2025-06-19 16:57:42 -04:00
adamdottv 8374977903 fix(tui): better help on home
publish / publish (push) Waiting to run
2025-06-19 15:56:28 -05:00
Jay V df3adc416b docs: tweak 2025-06-19 16:40:15 -04:00
Jay V 16f13a7ef6 docs: copy 2025-06-19 16:39:36 -04:00
Jay V ba24bbe1ea docs: styles 2025-06-19 16:38:37 -04:00
Jay V 943c6657a4 docs: adding to config 2025-06-19 16:36:17 -04:00
Dax Raad 7a073130d7 ignore: do migration 2025-06-19 16:32:32 -04:00
Jay V 16fceab8c6 docs: readme
publish / publish (push) Waiting to run
2025-06-19 16:29:20 -04:00
Jay V f72483cbcc docs: edits 2025-06-19 16:28:11 -04:00
Jay V b1f79ca730 Copy changes 2025-06-19 16:28:03 -04:00
adamdottv a0ffc4e7ae fix(tui): better tool titles 2025-06-19 15:11:53 -05:00
adamdottv a879a388a0 feat(tui): better task tool rendering
publish / publish (push) Waiting to run
2025-06-19 15:02:13 -05:00
adamdottv 5f25476682 fix(tui): sorted tool arg maps 2025-06-19 14:07:33 -05:00
adamdottv bcec113325 fix: typescript error 2025-06-19 13:57:25 -05:00
adamdottv 16cc70eda1 fix(tui): sorted tool arg maps 2025-06-19 13:56:09 -05:00
Dax Raad e103581f95 use production share url
publish / publish (push) Waiting to run
2025-06-19 14:21:00 -04:00
Dax Raad d7ae3c6033 ci: share 2025-06-19 13:26:15 -04:00
Dax Raad 9b49eee4a6 add schema descriptions to config fields
Enhance configuration schema with descriptive text for all fields to improve developer experience and auto-generated documentation.

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

Co-Authored-By: opencode <noreply@opencode.ai>
2025-06-19 13:12:13 -04:00
Dax Raad f5bc0a6de8 fix task agent performance issues
publish / publish (push) Waiting to run
2025-06-19 13:00:57 -04:00
Dax Raad fc079b67b2 allow selecting model and continuing previous session for opencode run 2025-06-19 13:00:57 -04:00
adamdottv 45f06f3068 fix(tui): vscode and mac terminal colors 2025-06-19 11:46:08 -05:00
adamdottv 9af0aa12ba fix(tui): overlay border backgrounds 2025-06-19 11:41:30 -05:00
Jay V b5e6f888ec ignore: share version 2025-06-19 12:20:30 -04:00
adamdottv 225a12c170 chore: cleanup logs 2025-06-19 10:52:45 -05:00
adamdottv 44919b1270 fix(tui): toast placement and overlay rendering 2025-06-19 10:45:10 -05:00
adamdottv 9ccb743875 feat(tui): more toast messages 2025-06-19 10:41:59 -05:00
adamdottv 0d6c436fc8 feat(tui): toast messages 2025-06-19 10:12:29 -05:00
Dax Raad 3d79b566f5 fix webfetch tool when returning html as text
publish / publish (push) Waiting to run
2025-06-19 10:43:54 -04:00
Dax Raad a44cc4a140 support token caching for anthropic via openrouter
publish / publish (push) Waiting to run
2025-06-19 10:32:14 -04:00
adamdottv 8bf54ee56d feat(tui): copy share url to clipboard
publish / publish (push) Waiting to run
2025-06-19 09:06:25 -05:00
Dax Raad b7319a304f remove opencode_ prefixes from tool names. unfortunately this will break
publish / publish (push) Waiting to run
all old sessions and share links. we'll be more backwards compatible in
the future once we're more stable.
2025-06-19 09:59:44 -04:00
adamdottv 8c6e559179 feat(tui): expand input to fit message
publish / publish (push) Waiting to run
2025-06-19 08:45:27 -05:00
Dax Raad b8d070cf5e ensure session.info is synced when shared
publish / publish (push) Waiting to run
2025-06-19 09:41:11 -04:00
adamdottv 553d5d3890 fix: include schema in converted toml config 2025-06-19 06:02:02 -05:00
Guillermo Antony Cava Nuñez 5aaffea356 Fixes tool tip layering (#199) 2025-06-19 00:23:29 -04:00
Dax Raad c596119a4f hooks 2025-06-19 00:20:03 -04:00
Frank 0b12b10fe6 Update SST 2025-06-18 23:38:06 -04:00
Josh 0a853034e5 fix: incorrect command on main screen for exiting application (#201)
publish / publish (push) Waiting to run
2025-06-18 23:19:43 -04:00
Dax Raad a0211c03d5 stop loading models.dev format from global config
publish / publish (push) Waiting to run
2025-06-18 23:08:51 -04:00
Dax Raad 3a9b34569d docs: update README 2025-06-18 23:03:54 -04:00
Dax Raad 71cd048840 BREAKING CONFIG CHANGE
publish / publish (push) Waiting to run
We have changed the config format yet again - but this should be the
final time. You can see the readme for more details but the summary is

- got rid of global providers config
- got rid of global toml
- global config is now in `~/.config/opencode/config.json`
- it will be merged with any project level config
2025-06-18 23:01:19 -04:00
Dax Raad 430bbb205c rework config 2025-06-18 23:01:19 -04:00
Frank f1fb0c5395 Render version 2025-06-18 22:26:51 -04:00
Jay V 7230d51718 ignore: share copy link 2025-06-18 20:18:10 -04:00
Dax Raad df777268e8 support global config 2025-06-18 18:56:52 -04:00
Jay V 7c4db19c75 ignore: share logo 2025-06-18 18:33:51 -04:00
Jay V 691c1ddeca ignore: share 2025-06-18 18:22:19 -04:00
Jay V 7006b1b080 ignore: share 2025-06-18 18:21:44 -04:00
Jay V 597a270b2e ignore: share favicon 2025-06-18 18:10:22 -04:00
Jay V 36a916f8b8 ignore: share 2025-06-18 18:01:34 -04:00
adamdottv 3854f6a63f feat(tui): show provider next to model 2025-06-18 16:09:49 -05:00
adamdottv aa99745a9d chore: tui style tweaks 2025-06-18 15:59:58 -05:00
Jay V a075a4e129 ignore: share 2025-06-18 16:54:33 -04:00
Jay V 652eff9d3f ignore: share 2025-06-18 16:54:33 -04:00
adamdottv 91107f2889 fix(tui): file completion quirks 2025-06-18 15:51:26 -05:00
Frank b62546f9ba Api: only return session messages 2025-06-18 16:20:34 -04:00
Dax Raad 5872e83fbe ignore: types 2025-06-18 16:20:03 -04:00
Jay V 3658e54047 ignore: share 2025-06-18 16:15:22 -04:00
Jay V 6121ef35d5 ignore: share 2025-06-18 16:15:11 -04:00
Dax Raad 3013a08eb4 ignore: share page stuff 2025-06-18 16:13:33 -04:00
adamdottv 14334dbdb2 fix(tui): modal visuals 2025-06-18 15:12:24 -05:00
Dax Raad 735f79badc share types 2025-06-18 15:34:13 -04:00
Jay V 0eb7cf5ce5 ignore: share 2025-06-18 15:32:40 -04:00
adamdottv 60b5b046ab chore: cleanup logs 2025-06-18 14:18:46 -05:00
Dax Raad 5f616b2367 docs: improve keybinds configuration format in README
publish / publish (push) Waiting to run
Update keybinds configuration example to use proper TOML table syntax instead of dot notation for better readability and standard TOML formatting.

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

Co-Authored-By: opencode <noreply@opencode.ai>
2025-06-18 15:10:46 -04:00
adamdottv 2fe9c6180c fix(tui): help dialog sorting 2025-06-18 14:06:20 -05:00
adamdottv 4e0821f7d9 feat(tui): configurable keybinds and mouse scroll 2025-06-18 13:56:51 -05:00
Dax Raad f97eacfbeb ctrl+c should gracefully clean up pending sessions
publish / publish (push) Waiting to run
2025-06-18 14:11:49 -04:00
Jay V b8d929d8f4 ignore: share error styles 2025-06-18 14:10:14 -04:00
Dax Raad dd599c496c fix panic when invalid config
publish / publish (push) Waiting to run
2025-06-18 14:03:16 -04:00
Dax Raad 3121d84ede track version on session info
publish / publish (push) Waiting to run
2025-06-18 13:40:36 -04:00
Jay V eee92921f4 ignore: share page styles 2025-06-18 12:53:48 -04:00
Dax Raad f5400000a0 implemented diff trimming 2025-06-18 11:20:40 -04:00
Dax Raad 7adba0e933 allow setting options in global provider store
publish / publish (push) Waiting to run
2025-06-18 11:06:16 -04:00
Dax Raad 4c0b88dca8 fix issue with provider cache
publish / publish (push) Waiting to run
2025-06-18 10:56:23 -04:00
Dax Raad 99ad27bbd8 fix deploys 2025-06-18 10:47:07 -04:00
Dax Raad aaec44f2d5 remove call to npm causing noticible delay when starting chat
publish / publish (push) Waiting to run
2025-06-18 10:35:41 -04:00
Dax Raad 7e6fb4e0c8 fix issue when running opencode in empty directory
publish / publish (push) Waiting to run
2025-06-18 10:29:09 -04:00
Dax Raad bc4bdfc1b1 ignore: sync
publish / publish (push) Waiting to run
2025-06-18 08:36:25 -04:00
SBSTN 40c1d2af24 Add Everforest Theme (#170) 2025-06-18 05:55:38 -05:00
Dax Raad cf8dd4a1f8 integrate cache read/write data
publish / publish (push) Waiting to run
2025-06-17 20:51:39 -04:00
Jay V 24dcfee294 ignore: share page polish 2025-06-17 20:26:12 -04:00
Dax Raad c0f019ff08 improve anthropic oauth token caching and authentication handling
publish / publish (push) Waiting to run
🤖 Generated with [opencode](https://opencode.ai)

Co-Authored-By: opencode <noreply@opencode.ai>
2025-06-17 13:23:15 -04:00
Dax Raad 1bd6809d36 ignore: fix 2025-06-17 13:04:26 -04:00
Dax Raad dbcd784a82 state 2025-06-17 12:29:28 -04:00
Dax Raad 42f1d9b0d8 support disabling providers from automatically being added
publish / publish (push) Waiting to run
2025-06-17 12:23:04 -04:00
Dax Raad 04ea736dc2 docs: readme 2025-06-17 12:17:45 -04:00
Dax Raad f4bc2c2c05 docs: update README
publish / publish (push) Waiting to run
2025-06-17 12:14:38 -04:00
Dax Raad a71568d84b docs: readme 2025-06-17 12:12:33 -04:00
Dax Raad ba5cc709d1 support global config for providers 2025-06-17 12:10:44 -04:00
Dax Raad 53c3aa88c0 fix issue with tool schemas and google
publish / publish (push) Waiting to run
2025-06-17 11:27:07 -04:00
Dax Raad 32a0bf3d15 fix agent getting caught in summary loop
publish / publish (push) Waiting to run
2025-06-17 10:50:03 -04:00
Dax Raad 2c19d39737 ci: fix
publish / publish (push) Waiting to run
2025-06-17 10:38:01 -04:00
Dax Raad 7ce22fa132 Fix TypeScript compilation errors and consolidate version handling
publish / publish (push) Waiting to run
🤖 Generated with [opencode](https://opencode.ai)

Co-Authored-By: opencode <noreply@opencode.ai>
2025-06-17 10:27:49 -04:00
adamdottv bbd7c065f4 feat: better collapsed tool call visuals
publish / publish (push) Waiting to run
2025-06-17 08:35:18 -05:00
adamdottv 60c10c7b68 chore: consolidate chat page into tui.go 2025-06-17 07:09:04 -05:00
Dax Raad 60fe0aa090 Add autoshare configuration and improve run command UI
Enables automatic session sharing via global config or flag, enhances UI with logo display and provider/model info positioning.

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

Co-Authored-By: opencode <noreply@opencode.ai>
2025-06-17 01:45:32 -04:00
Dax Raad 8084990a31 do not autoupgrade snapshot builds
publish / publish (push) Waiting to run
2025-06-17 01:18:32 -04:00
Dax Raad f544a01971 autoupgrade
publish / publish (push) Waiting to run
2025-06-17 01:05:05 -04:00
Dax e188879d8f Improve upgrade command with installation method detection (#158) 2025-06-17 00:07:17 -04:00
Jay ce1e42b7a1 docs: Update README.md 2025-06-16 21:01:38 -04:00
Jay V bade721218 docs: readme 2025-06-16 20:10:19 -04:00
adamdottv ffe9b6df7e feat: share and init commands
publish / publish (push) Waiting to run
2025-06-16 15:58:52 -05:00
Dax Raad 45d2fe2027 ci: fixed ci issue 2025-06-16 16:58:25 -04:00
Dax Raad b40da9923d docs: readme 2025-06-16 16:53:43 -04:00
Dax Raad 0349548abd ignore: logs 2025-06-16 16:02:45 -04:00
Dax Raad a21aa1a52a docs: update readme 2025-06-16 15:42:35 -04:00
Miles Till 7d023c0ce4 fix: incorrect lipgloss version (#131) 2025-06-16 14:35:46 -05:00
Dax Raad 2cf3ab55ce remove windows builds
publish / publish (push) Waiting to run
2025-06-16 15:11:14 -04:00
Dax Raad 115a71fd20 opencode auth login: validation on provider id and better error messages
publish / publish (push) Waiting to run
2025-06-16 15:09:49 -04:00
Dax Raad 15990d80a8 docs: readme 2025-06-16 15:04:32 -04:00
Dax Raad 0ecff22f98 BREAKING CHANGE: the config structure has changed, custom providers have an npm field now to specify which npm package to load. see examples in README.md 2025-06-16 15:02:25 -04:00
Dax Raad c0a3e5b0f7 limit to 4 system prompts cached
publish / publish (push) Waiting to run
2025-06-16 14:51:59 -04:00
adamdottv b0b5d2a5cc fix: include cached tokens in tui 2025-06-16 12:59:38 -05:00
adamdottv aa90b03da6 feat: believe it or not, even faster tui init
publish / publish (push) Waiting to run
2025-06-16 12:34:34 -05:00
adamdottv c482fe1ac8 feat: even faster tui init 2025-06-16 12:24:18 -05:00
adamdottv 6120b64792 feat: faster tui init
publish / publish (push) Waiting to run
2025-06-16 11:54:55 -05:00
Dax Raad b5723b3ace properly track cache token counts
publish / publish (push) Waiting to run
2025-06-16 12:43:22 -04:00
Dax Raad f3e32fe112 fix cache-control
publish / publish (push) Waiting to run
2025-06-16 12:07:01 -04:00
adamdottv d120eafd9e fix: default provider and model 2025-06-16 10:51:01 -05:00
adamdottv 34823c4bc5 chore: remove status service 2025-06-16 10:45:19 -05:00
Dax Raad 0ca81eb0d3 enable prompt caching for anthropic
publish / publish (push) Waiting to run
2025-06-16 11:41:54 -04:00
adamdottv 03a017e8c7 fix: spinner background color 2025-06-16 10:03:44 -05:00
Dax Raad 49439d1cd8 docs: readme 2025-06-16 10:44:48 -04:00
Thomas Meire e4f6c1e3ce ignore: remove log files and add them to gitignore (#138) 2025-06-16 09:30:07 -04:00
Jacob 3534e727bf fix: typo and literal wording in packages/opencode/AGENTS.md (#134) 2025-06-16 08:18:29 -05:00
Dax Raad c2379097e0 docs: update README 2025-06-15 21:43:20 -04:00
Dax Raad fd64aa4c25 docs: readme 2025-06-15 21:39:02 -04:00
Dax Raad a205b1e7f5 enhance ripgrep files function with query filtering and limit support
publish / publish (push) Waiting to run
🤖 Generated with [opencode](https://opencode.ai)

Co-Authored-By: opencode <noreply@opencode.ai>
2025-06-15 21:26:32 -04:00
Dax Raad 89c49bf253 first pass making system prompt less fast
publish / publish (push) Waiting to run
2025-06-15 20:25:04 -04:00
George Potoshin a8c4a6b3da Fix: Improve Help UI Readability (Issue #99) (#117) 2025-06-15 18:38:44 -05:00
adamdottv 742eed5ce4 fix: background color rendering issues
publish / publish (push) Waiting to run
2025-06-15 15:07:05 -05:00
Pierre B. 4c033aa9b2 fix: spelling, grammar and typos (#121) 2025-06-15 14:23:18 -05:00
Dax Raad 1fe40495d2 Improve LSP server initialization with timeout handling and skip failed servers
publish / publish (push) Waiting to run
🤖 Generated with [opencode](https://opencode.ai)

Co-Authored-By: opencode <noreply@opencode.ai>
2025-06-15 13:52:57 -04:00
Dax Raad d142ec3b1d ignore: add timer log helper 2025-06-15 13:33:24 -04:00
Dax Raad 9d8f080da1 fix postinstall script for node
publish / publish (push) Waiting to run
2025-06-15 13:11:11 -04:00
Pierre B. 3eece9d138 chore: ignore intellij, vscode (#122) 2025-06-15 10:40:34 -05:00
adamdottv 1a4f17a827 fix: background color rendering issues
publish / publish (push) Waiting to run
2025-06-15 05:57:15 -05:00
szymon bf231d5469 remove .DS_Store (#112) 2025-06-15 05:34:46 -05:00
Dax Raad 44cc41abb0 beginning of upgrade command 2025-06-14 22:05:41 -04:00
Dax Raad e140c098ed revert ctrl+d - conflicts with page down
publish / publish (push) Waiting to run
2025-06-14 21:29:02 -04:00
Dax Raad 9871d97af1 fix aborting issue
publish / publish (push) Waiting to run
2025-06-14 21:23:57 -04:00
Dax Raad 502098549b fix issue with follow up tool calls and cancelation
publish / publish (push) Waiting to run
2025-06-14 21:03:44 -04:00
Dax Raad 24f1399bcf fix issue continuing session after aborted 2025-06-14 20:24:50 -04:00
nitishxyz 1e0a50bf89 Add Ayu dark theme (#109)
publish / publish (push) Waiting to run
2025-06-14 20:08:31 -04:00
Berr 233e07c66f fix: improve browser opening error handling in AuthLoginCommand (#111) 2025-06-14 20:07:41 -04:00
Dax Raad 32f82e91d2 sync 2025-06-14 20:04:41 -04:00
Dax Raad 70405b2705 ci: ignore commits 2025-06-14 18:59:05 -04:00
Dax Raad d8085d6609 docs: typo 2025-06-14 18:58:27 -04:00
Rohan Godha c4f03b1039 tui: fix help dialog background (#110)
publish / publish (push) Waiting to run
2025-06-14 18:57:15 -04:00
Conor O'Brien 1ef5a6b321 nit: update commands displayed on home to match commands available (#108) 2025-06-14 18:56:44 -04:00
Gal Schlezinger 86b27f602d make ctrl+d quit too, just like shells (#105) 2025-06-14 18:56:34 -04:00
Dax Raad a760394536 docs: use ollama example 2025-06-14 18:55:39 -04:00
Dax Raad ec9555e957 downgrade to ai sdk v4.x
publish / publish (push) Waiting to run
2025-06-14 18:44:08 -04:00
Gal Schlezinger def232321f add focus tracking for tui so cursor will hide when not in focus (#103) 2025-06-14 14:53:43 -05:00
Dax Raad 158e0d6e1c docs: readme 2025-06-14 14:52:02 -04:00
Dax Raad 1ad42d91ec docs: provider config 2025-06-14 14:45:59 -04:00
Dax Raad 1c6cfee713 docs: faq 2025-06-14 14:39:13 -04:00
Mantena Rama Raju ed53b3e331 typo (#94) 2025-06-14 14:36:29 -04:00
Dax Raad 9ba49a23cd Enhance provider system with dynamic package resolution and improved logging
publish / publish (push) Waiting to run
- Add npm registry lookup for AI SDK packages with fallback support
- Enhance error logging with cause information
- Add timing deltas to log output for performance monitoring

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

Co-Authored-By: opencode <noreply@opencode.ai>
2025-06-14 14:35:33 -04:00
Albert Ilagan 6438647eef tui: remove quit dialog (#97) 2025-06-14 12:47:34 -05:00
Dax Raad b3068ac292 doc: remove dev script 2025-06-14 13:05:23 -04:00
Dax Raad 76ba37611b docs: contributing
publish / publish (push) Waiting to run
2025-06-14 12:45:26 -04:00
Dax Raad 5862327e26 sync 2025-06-14 12:32:41 -04:00
Dax Raad 81468d6ee4 docs: mcp 2025-06-14 12:25:26 -04:00
Dax Raad a1d5ac0c45 docs: config schema 2025-06-14 12:22:07 -04:00
Dax Raad 88bf2e3e77 ci: update publish script 2025-06-14 12:13:59 -04:00
Dax Raad 7f4d13b76d docs: readme 2025-06-14 12:13:46 -04:00
Dax Raad 7ae04a8797 naming fixes
publish / publish (push) Waiting to run
2025-06-14 01:54:28 -04:00
Dax Raad 986c354a39 Add flag system and auto-share functionality
publish / publish (push) Waiting to run
- Add Flag module for environment variable configuration
- Implement OPENCODE_AUTO_SHARE flag to automatically share new sessions
- Update session creation to conditionally auto-share based on flag

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

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-14 01:51:04 -04:00
Dax Raad a568f4058d Improve logging and simplify fzf implementation
- Refactor fzf search to use Bun's $ syntax for cleaner command execution
- Add request/response duration logging to server middleware
- Set default service name for logging to improve log clarity

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

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-14 01:51:04 -04:00
Alireza Bahrami 6f1d970962 fix(install): check if the path export command already exists (#28) 2025-06-13 23:28:33 -04:00
Dax Raad 7da025f4fb Clean up workflows and enhance file discovery tools to include dot files
publish / publish (push) Waiting to run
🤖 Generated with [OpenCode](https://opencode.ai)

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-13 23:24:46 -04:00
Dax Raad a86c9511c7 sync 2025-06-13 17:42:56 -04:00
Dax Raad 6c802425ee screenshot 2025-06-13 17:42:14 -04:00
Dax Raad 6c9b8b010f disable autoshare
publish / publish (push) Waiting to run
2025-06-13 17:30:17 -04:00
Dax Raad 048958d7f7 sync 2025-06-13 17:24:45 -04:00
Dax Raad f42e4b7443 sync
publish / publish (push) Waiting to run
2025-06-13 17:23:22 -04:00
adamdottv e57cf4a4ce wip: refactoring tui 2025-06-13 15:56:33 -05:00
Dax Raad e02b728a81 sync
publish / publish (push) Waiting to run
2025-06-13 16:47:15 -04:00
Dax Raad 0a7c4a5389 sync 2025-06-13 16:46:48 -04:00
Dax Raad a177d3fa37 sync 2025-06-13 16:45:58 -04:00
Dax Raad f386dd5789 sync 2025-06-13 16:43:53 -04:00
Dax Raad 574b4eda99 commit and push 2025-06-13 16:42:31 -04:00
Dax Raad 2af22d2ec8 sync 2025-06-13 16:37:58 -04:00
Dax Raad 93e3be05a3 sync
publish / publish (push) Waiting to run
2025-06-13 16:29:57 -04:00
Dax Raad 3f1700cc34 reset readme 2025-06-13 16:26:34 -04:00
Dax Raad b6c79abca2 ci
publish / publish (push) Waiting to run
2025-06-13 16:23:38 -04:00
Dax Raad b86c1c9151 homebrew
publish / publish (push) Waiting to run
2025-06-13 16:17:27 -04:00
adamdottv 50d6057777 wip: refactoring tui
publish / publish (push) Waiting to run
2025-06-13 11:27:05 -05:00
adamdottv 8cd554f84c wip: refactoring tui 2025-06-13 11:18:46 -05:00
adamdottv 9ad13b3197 wip: refactoring tui 2025-06-13 10:47:51 -05:00
adamdottv 453298c1c0 wip: refactoring tui 2025-06-13 09:57:54 -05:00
adamdottv 2d8ce02971 wip: refactoring tui 2025-06-13 09:44:09 -05:00
adamdottv 09492cfeb7 wip: refactoring tui 2025-06-13 09:19:51 -05:00
adamdottv eda1445856 wip: refactoring tui 2025-06-13 08:30:57 -05:00
adamdottv 161634d663 wip: refactoring tui 2025-06-13 08:30:56 -05:00
Dax Raad ba1d90b1b9 do not strip aur package
publish / publish (push) Waiting to run
2025-06-13 08:27:17 -04:00
adamdottv fe1d09b88d wip: refactoring tui
publish / publish (push) Waiting to run
2025-06-13 07:01:26 -05:00
adamdottv fae45d757f wip: refactoring tui 2025-06-13 06:56:12 -05:00
adamdottv 93db8564a5 wip: refactoring tui 2025-06-13 06:49:59 -05:00
adamdottv da824c82b6 wip: refactoring tui 2025-06-13 06:23:12 -05:00
Dax Raad 30176d32ba load API keys
publish / publish (push) Waiting to run
2025-06-13 00:53:46 -04:00
Dax Raad aa330249c4 Enhance auth command with dynamic provider selection
- Add support for dynamically loading providers from ModelsDev
- Prioritize anthropic as recommended provider
- Add "other" provider option for manual entry
- Include special handling for amazon-bedrock with AWS config guidance
- Expand provider selection UI to show up to 8 providers

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

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-13 00:33:54 -04:00
Dax Raad d0f3211c7f sync 2025-06-13 00:06:15 -04:00
Dax Raad 899e5f38b2 Refactor AI SDK provider loading to use BunProc.install
Simplifies provider installation by using BunProc.install() instead of manual path construction and file system checks.

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

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-12 23:50:26 -04:00
Dax Raad fba34a04fa bedrock loader
publish / publish (push) Waiting to run
2025-06-12 23:39:52 -04:00
Dax Raad 3c0a28dbed Update provider configuration and server handling
🤖 Generated with [OpenCode](https://opencode.ai)

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-12 23:10:03 -04:00
Thomas Meire c45233e3ff Update error message to say opencode instead of sst (#81)
publish / publish (push) Waiting to run
2025-06-12 18:38:59 -04:00
Dax Raad c3c3fcd3c8 ci
publish / publish (push) Waiting to run
2025-06-12 18:30:19 -04:00
Dax Raad 76e9b68e11 ci
publish / publish (push) Waiting to run
2025-06-12 18:21:08 -04:00
Dax Raad bf5435821e sync
publish / publish (push) Waiting to run
2025-06-12 18:14:04 -04:00
Dax Raad 833d5f312a Update CLI run command and session handling
publish / publish (push) Waiting to run
🤖 Generated with [OpenCode](https://opencode.ai)

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-12 18:07:31 -04:00
Dax Raad 073df4d75f subagent 2025-06-12 18:07:31 -04:00
Jay V 4f16fa6944 updating logo 2025-06-12 17:30:24 -04:00
adamdottv 40f193a630 wip: refactoring tui 2025-06-12 16:04:45 -05:00
adamdottv c1c488c545 wip: refactoring tui 2025-06-12 16:00:26 -05:00
adamdottv 4bac47ab25 wip: refactoring tui 2025-06-12 16:00:25 -05:00
adamdottv c590a9cec6 wip: refactoring tui 2025-06-12 16:00:25 -05:00
adamdottv e97bb2639c wip: refactoring tui 2025-06-12 16:00:25 -05:00
adamdottv 6bb6a8714c wip: refactoring tui 2025-06-12 16:00:25 -05:00
adamdottv fd76c78ebf wip: refactoring tui 2025-06-12 16:00:24 -05:00
adamdottv 1db78332b9 wip: refactoring tui 2025-06-12 16:00:24 -05:00
adamdottv 6e35dc52f9 wip: refactoring tui 2025-06-12 16:00:24 -05:00
Dax Raad 9e4f0f3393 ci 2025-06-12 14:15:44 -04:00
Dax Raad ceee72eb98 sync 2025-06-12 14:11:01 -04:00
Dax Raad 17919fd496 Update publish workflow to support snapshot releases on dontlook branch
🤖 Generated with [OpenCode](https://opencode.ai)

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-12 14:10:29 -04:00
Dax Raad 8caa6e5f52 sync
publish / publish (push) Waiting to run
2025-06-12 14:06:06 -04:00
Dax Raad bcff8a8292 Update index.ts
publish / publish (push) Waiting to run
🤖 Generated with [OpenCode](https://opencode.ai)

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-12 13:59:42 -04:00
Dax Raad eae59a759f sync
publish / publish (push) Waiting to run
2025-06-12 13:49:43 -04:00
Jay V df5c05934e fix share page edit 2025-06-12 13:42:10 -04:00
Dax Raad 550f417682 do not output bunproc
publish / publish (push) Waiting to run
2025-06-12 13:39:03 -04:00
Dax Raad 1b27c1529a Fix AUR SSH key path handling in publish script
publish / publish (push) Waiting to run
Quote and trim AUR_KEY environment variable to handle paths with spaces and multiline content properly.

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

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-12 13:37:12 -04:00
Dax Raad 02da0e1787 Simplify BunProc.which() to use process.execPath directly
publish / publish (push) Waiting to run
🤖 Generated with [OpenCode](https://opencode.ai)

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-12 13:32:31 -04:00
Dax Raad 73b81c241a update bun integration
publish / publish (push) Waiting to run
🤖 Generated with [OpenCode](https://opencode.ai)

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-12 13:29:14 -04:00
Dax Raad 4b4df65a4e sync
publish / publish (push) Waiting to run
2025-06-12 13:19:24 -04:00
Dax Raad 31e1709c52 token
publish / publish (push) Waiting to run
2025-06-12 13:17:06 -04:00
Dax Raad 3af67dd2f5 skip nil values
publish / publish (push) Waiting to run
2025-06-12 13:13:34 -04:00
Jay V 84160a39c2 share page handle undefined 2025-06-12 13:11:34 -04:00
Dax Raad db4babbd6d sync
publish / publish (push) Waiting to run
2025-06-12 13:04:01 -04:00
Dax Raad 882730c9be sync
release / goreleaser (push) Waiting to run
2025-06-12 12:48:38 -04:00
Dax Raad 57e567bb38 npm token
release / goreleaser (push) Waiting to run
2025-06-12 12:09:13 -04:00
Dax Raad d92611d67c sync 2025-06-12 11:58:41 -04:00
Dax Raad be627906c2 sync
release / goreleaser (push) Waiting to run
2025-06-12 11:57:57 -04:00
Dax Raad 7dac4db214 sync
release / goreleaser (push) Waiting to run
2025-06-12 11:56:52 -04:00
Dax Raad 42f966513d sync
release / goreleaser (push) Waiting to run
2025-06-12 11:56:06 -04:00
Dax Raad 61630059fa tweak
release / goreleaser (push) Waiting to run
2025-06-12 11:55:07 -04:00
Dax Raad 07077d34b7 sync
release / goreleaser (push) Waiting to run
2025-06-12 11:18:17 -04:00
Dax Raad 0ef68306a7 sync
release / goreleaser (push) Waiting to run
2025-06-12 11:02:51 -04:00
Dax Raad 2e3ad15d06 sync
release / goreleaser (push) Waiting to run
2025-06-12 11:00:37 -04:00
Dax Raad 287a0b519d Add postinstall script and update session/release configuration
release / goreleaser (push) Waiting to run
🤖 Generated with [OpenCode](https://opencode.ai)

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-12 00:50:49 -04:00
Dax Raad f3dc4bd937 Refactor external tools organization and add file search API endpoint
🤖 Generated with [OpenCode](https://opencode.ai)

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-11 23:59:51 -04:00
Dax Raad 1fec4b47e5 Add search function to fzf and move version constant to namespace level
🤖 Generated with [OpenCode](https://opencode.ai)

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-11 23:58:13 -04:00
Dax Raad 468ce2019b add title temp 2025-06-11 19:09:08 -04:00
Dax Raad c6f381ab3a sync 2025-06-11 19:00:09 -04:00
Dax Raad acb23eb995 sync 2025-06-11 18:45:18 -04:00
Dax Raad 8f43c59091 sync 2025-06-11 18:19:21 -04:00
adamdottv 70a3a19c88 wip: refactoring tui 2025-06-11 12:05:54 -05:00
adamdottv 17f6a60ef7 wip: refactoring tui 2025-06-11 11:58:25 -05:00
Dax Raad e52fb7ff3f sync 2025-06-11 12:58:06 -04:00
Dax Raad 60f3b52996 sync 2025-06-11 12:44:21 -04:00
adamdottv 775e5ecbf0 wip: refactoring tui 2025-06-11 11:43:28 -05:00
Dax Raad 70d291afdf sync 2025-06-11 12:35:20 -04:00
Dax Raad aac32a0a5e sync 2025-06-11 11:41:10 -04:00
Dax Raad d759b814c7 turn on tool call streaming 2025-06-11 11:36:12 -04:00
Jay V 7d9edc53db share page scroll anchor 2025-06-11 09:43:52 -05:00
Dax Raad 6fd8f1034b fix bash 2025-06-11 00:37:43 -04:00
Dax Raad c1a9c33a62 Standardize styling in auth commands by replacing Bun.color with UI.Style constants
🤖 Generated with [OpenCode](https://opencode.ai)

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-11 00:34:55 -04:00
Dax Raad c2c1f8e54b sync 2025-06-11 00:27:46 -04:00
Dax Raad d3197c6d15 Refactor authentication system to consolidate auth flow and remove provider-based commands
🤖 Generated with [OpenCode](https://opencode.ai)

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-11 00:21:46 -04:00
Dax Raad a26c53875c sync 2025-06-10 22:18:27 -04:00
Dax Raad b5f59b3bcc fix rendering 2025-06-10 19:37:25 -04:00
Dax Raad e553e64e77 Fix shutdown handling, error management, and process lifecycle issues
🤖 Generated with [OpenCode](https://opencode.ai)

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-10 18:58:47 -04:00
Dax Raad 567ad1e69a fix bunfile bug 2025-06-10 18:23:19 -04:00
Dax Raad 8975092d91 sync 2025-06-10 18:10:30 -04:00
Dax Raad 5a192393b8 add abort signals 2025-06-10 17:56:05 -04:00
Dax Raad 37cf5c07a6 webfetch spoof browser 2025-06-10 16:39:06 -04:00
Dax Raad 6edb6024db kill 2025-06-10 16:35:02 -04:00
Dax Raad 51c5048570 lsp 2025-06-10 16:29:35 -04:00
Dax Raad e054d5ecca merge deep config 2025-06-10 16:19:02 -04:00
Frank 3ac6381350 Share: render url on frontend 2025-06-10 15:50:42 -04:00
Dax Raad a269194d1e onboarding progress 2025-06-10 15:43:14 -04:00
Dax Raad e1cc298e95 fix share link 2025-06-10 13:36:47 -04:00
Dax Raad e7c8654e7c sync 2025-06-10 13:30:13 -04:00
Dax Raad 160913e061 Update LSP client/server and CLI scrap command functionality
🤖 Generated with [OpenCode](https://opencode.ai)

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-10 13:30:13 -04:00
Jay V 11ca89ea69 moving share urls 2025-06-10 11:52:02 -05:00
Dax Raad e64fe86149 Enhance ripgrep error handling and utility functions
🤖 Generated with [OpenCode](https://opencode.ai)

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-10 11:06:01 -04:00
Jay V 6c25bbe80d share page fix 2025-06-10 09:33:28 -05:00
Dax Raad b2d5a8a488 sync 2025-06-10 10:14:03 -04:00
Dax Raad bd41ad730a Improve ripgrep error handling with structured error types
🤖 Generated with [OpenCode](https://opencode.ai)

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-10 10:14:03 -04:00
Frank b65bc68367 Share: og image 2025-06-09 23:37:32 -04:00
Frank 93e8522fd6 ssr sync 2025-06-09 23:07:29 -04:00
Dax Raad cea7df2c5e aborted toolcalls should be cleaned up 2025-06-09 20:48:50 -04:00
Dax Raad 25b5de1fca properly support codex mini 2025-06-09 20:24:18 -04:00
Dax Raad de16cad6e4 notice 2025-06-09 19:58:08 -04:00
Jay V e46952a9e5 share og image 2025-06-09 16:42:13 -05:00
Jay V fee3ee9155 edits 2025-06-09 16:09:15 -05:00
Dax Raad b356744e69 file permissions for anthropic credentials 2025-06-09 17:07:52 -04:00
Jay V 495d105088 share page show lsp diag 2025-06-09 15:41:19 -05:00
Jay V 423bc72edb share page fix ai text 2025-06-09 15:02:46 -05:00
Jay V 213fac68c8 fix duplicates models 2025-06-09 15:02:46 -05:00
Dax Raad 10219b14d4 improve AGENTS.md 2025-06-09 15:28:06 -04:00
Dax Raad d9af524676 sync 2025-06-09 15:00:48 -04:00
Dax Raad 902ee2e1db Refactor logging system to centralize initialization and remove printLogs parameter
🤖 Generated with [OpenCode](https://opencode.ai)

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-09 14:52:30 -04:00
Dax Raad 5db2410770 optional 2025-06-09 14:02:58 -04:00
Dax Raad 8d6c253c16 sync 2025-06-09 14:01:11 -04:00
Dax Raad a080eab5c0 Add TypeScript server initialization config to LSP server
🤖 Generated with [OpenCode](https://opencode.ai)

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-08 13:22:07 -04:00
Dax Raad 100a8c5821 fix ts lsp 2025-06-08 12:57:33 -04:00
Dax Raad 9af89c5fc7 sync 2025-06-08 12:46:32 -04:00
Frank 66ccc67a4d Share: load server data on page load 2025-06-08 01:17:54 -04:00
Frank a7473daf7b Remove unused package 2025-06-07 23:47:45 -04:00
Frank 79f839215f Infra: use Astro component 2025-06-07 23:46:56 -04:00
Dax Raad 37929c6bc2 sync 2025-06-07 17:15:40 -04:00
Dax Raad 96218334eb logs 2025-06-06 23:58:09 -04:00
Dax Raad b683b4e51e add go version 2025-06-06 23:26:11 -04:00
Dax Raad a23908a136 autodownload lsp 2025-06-06 23:21:57 -04:00
Dax Raad c84fb15216 make lsp better 2025-06-06 17:20:08 -04:00
Jay V 7c2f97f0d8 share page styles 2025-06-06 12:18:02 -04:00
Dax Raad 482fe2b8c9 lsp progress 2025-06-05 23:42:04 -04:00
Dax Raad d9986aa6c2 update auth and provider configuration
🤖 Generated with [OpenCode](https://opencode.ai)

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-05 20:11:56 -04:00
Jay V c1b5af5ffa share page markdown 2025-06-05 19:14:35 -04:00
Jay V 3a755eadce share write tool 2025-06-05 16:41:44 -04:00
Jay V a50e389df6 share page 2025-06-05 16:35:23 -04:00
Dax Raad f8d2695e56 fix webfetch 2025-06-05 15:59:17 -04:00
Jay V 9fd5d25f1f share ssr 2025-06-05 15:37:23 -04:00
Dax Raad 61ee871243 fix cost 2025-06-05 15:06:42 -04:00
Dax Raad d9e4815d71 integrate with models.dev 2025-06-05 14:59:16 -04:00
Jay V f52d1a4bee share page fetch 2025-06-05 14:17:43 -04:00
adamdottv a79f270202 wip: refactoring tui 2025-06-05 13:10:20 -05:00
Jay V 8671607e1b share page todos 2025-06-05 13:52:44 -04:00
Dax Raad c1410091c7 sync 2025-06-05 13:51:18 -04:00
Dax Raad 06c1f06493 gopls 2025-06-05 13:51:18 -04:00
Dax Raad 47532b9f03 sync 2025-06-05 13:51:18 -04:00
adamdottv 580ff78958 wip: refactoring tui 2025-06-05 12:38:25 -05:00
adamdottv a6f8c4c5ce feat: opus 4 2025-06-05 11:39:13 -05:00
Dax Raad b2edf44898 sync 2025-06-05 12:06:21 -04:00
Dax Raad fc7d4c1e54 fixed 2025-06-05 11:58:04 -04:00
Dax Raad 2e15770085 claude oauth support 2025-06-05 11:51:06 -04:00
adamdottv 49209a9f78 wip: refactoring tui 2025-06-05 10:46:07 -05:00
adamdottv 1c87a48522 wip: refactoring tui 2025-06-05 05:43:33 -05:00
Dax Raad b6f9913d78 fix cost 2025-06-05 02:23:11 -04:00
Dax Raad f09d53ffa7 Add session share URL display to CLI output
🤖 Generated with [OpenCode](https://opencode.ai)

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-04 21:01:36 -04:00
Dax Raad f89481b727 sync 2025-06-04 20:49:33 -04:00
Jay V e7e30c8274 share page glob 2025-06-04 20:14:14 -04:00
Jay V 91418a5c44 share page glob 2025-06-04 19:34:15 -04:00
Jay V 09d34cc7cb read tool share page 2025-06-04 18:39:06 -04:00
Dax Raad 3aad1a9c7d sync 2025-06-04 18:32:10 -04:00
Dax Raad 3f26b0174c rework 2025-06-04 18:03:04 -04:00
Dax Raad da0795ef88 track errors 2025-06-04 17:55:14 -04:00
Jay V 79b89127fa share page tool args 2025-06-04 17:48:42 -04:00
Dax Raad 16c37de95d Refactor session module structure and improve error handling
- Rename session.ts to index.ts for cleaner module imports
- Update all imports to use new session module structure
- Add error metadata tracking to message schema
- Improve error handling in session stream processing

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

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-04 17:38:54 -04:00
Jay V bf2eeb14c1 styles share 2025-06-04 17:01:04 -04:00
Dax Raad f8317c2d38 implemented todo tool 2025-06-04 15:09:50 -04:00
adamdottv 5fe1f105c0 wip: refactoring tui 2025-06-04 13:52:23 -05:00
adamdottv 7f0dbb1a8d wip: refactoring tui 2025-06-04 12:57:17 -05:00
Jay V 12a78352f6 share collapse system prompt 2025-06-04 13:37:07 -04:00
Dax Raad 472e03be40 sync 2025-06-04 13:33:53 -04:00
Dax Raad 44ce3e9727 more tools 2025-06-04 13:33:25 -04:00
Dax Raad b3bebdabca sync 2025-06-04 13:12:55 -04:00
Dax Raad 1cb48d5c24 tool updates 2025-06-04 13:12:48 -04:00
adamdottv d71754a704 wip: refactoring tui 2025-06-04 11:48:01 -05:00
adamdottv 29377e8dcb wip: refactoring tui 2025-06-04 11:48:01 -05:00
Jay V a2cb7e93ba styles share 2025-06-04 12:44:36 -04:00
Jay V 34725efdf2 styles 2025-06-04 12:32:25 -04:00
adamdottv 6f1e4a1545 wip: refactoring tui 2025-06-04 09:20:48 -05:00
Dax Raad f296a67855 sync 2025-06-04 10:20:29 -04:00
Dax Raad 5f044228f9 fix tokens 2025-06-04 10:14:42 -04:00
Jay V 8c2ea63e34 share paage durations 2025-06-03 18:08:46 -04:00
Jay V 844adb645a share page bugs 2025-06-03 17:20:30 -04:00
adamdottv dda3d41aca wip: refactoring tui 2025-06-03 15:49:41 -05:00
Dax Raad 90f300438d fix port issue 2025-06-03 16:35:37 -04:00
Dax Raad 320f23a8dc mcp support 2025-06-03 15:57:48 -04:00
Jay V 844acfe572 styles 2025-06-03 15:57:00 -04:00
Jay V cb2cca75c7 Styling share 2025-06-03 15:55:34 -04:00
Jay V fb57778f01 lock 2025-06-03 15:55:34 -04:00
Dax Raad 88fc35a41f Fix bash tool stream handling by ensuring consistent stdout/stderr pipe configuration
🤖 Generated with opencode
Co-Authored-By: opencode <noreply@opencode.ai>
2025-06-03 14:48:05 -04:00
Dax Raad ca446d6ce4 Replace env-paths with xdg-basedir for better XDG compliance and cross-platform directory handling
🤖 Generated with opencode
Co-Authored-By: opencode <noreply@opencode.ai>
2025-06-03 14:46:28 -04:00
Dax Raad cf5ef05ddf change file name again 2025-06-03 14:28:50 -04:00
Dax Raad 98ea791f43 sync 2025-06-03 14:25:25 -04:00
Dax Raad fae6e568a0 initialized 2025-06-03 14:24:45 -04:00
Dax Raad 42a876cb2f max output tokens 2025-06-03 13:46:39 -04:00
adamdottv 6aefa320b9 wip: refactoring tui 2025-06-03 12:45:28 -05:00
Dax Raad c86d7fe6b9 do not list files if not git 2025-06-03 13:19:44 -04:00
Dax Raad ff6cef3da1 include env 2025-06-03 13:18:42 -04:00
Dax Raad a9dc21d782 sync 2025-06-03 13:08:47 -04:00
Dax Raad 58701d364a sync 2025-06-03 13:00:27 -04:00
Dax Raad be03feb20d initialzie 2025-06-03 12:38:48 -04:00
Dax Raad c6562cb167 sync
release / goreleaser (push) Waiting to run
2025-06-03 12:08:07 -04:00
Dax Raad bf8c451953 added app 2025-06-03 12:02:21 -04:00
Dax Raad 66685b4917 Refactor app context system to use Zod schemas and sync access pattern
🤖 Generated with opencode
Co-Authored-By: opencode <noreply@opencode.ai>
2025-06-03 11:59:03 -04:00
Dax Raad 375257a559 check in generated files 2025-06-02 22:14:12 -04:00
Dax Raad 19a42597af scope filetimes to session 2025-06-02 20:24:32 -04:00
Dax Raad e696710502 add permission system 2025-06-02 19:51:37 -04:00
adamdottv 0ae62d2d24 wip: refactoring tui 2025-06-02 14:14:13 -05:00
adamdottv b3f7ea6566 wip: refactoring tui 2025-06-02 13:33:05 -05:00
adamdottv 02ab71e91e wip: refactoring tui 2025-06-02 12:30:03 -05:00
adamdottv b24e7cf4e1 wip: refactoring tui 2025-06-02 12:00:21 -05:00
adamdottv 1b69195c4e wip: refactoring tui 2025-06-02 11:33:01 -05:00
Dax Raad c481538ed4 spawn from anywhere in dev mode 2025-06-02 11:40:28 -04:00
Dax Raad 7fd6392ca0 remove json 2025-06-01 15:03:25 -04:00
Dax Raad fcc44590e3 sync 2025-06-01 15:01:57 -04:00
Dax Raad 215a20b9a9 Fix TypeScript errors and clean up unused imports
- Remove unused fs import from app.ts
- Remove unused LLM import and missing module reference from server.ts
- Fix parameter naming inconsistency in fetch.ts execute function
- Add missing metadata property to fetch.ts return statement
- Update test file to use correct API signatures and parameter names
- Remove unused parameters from example.ts

🤖 Generated with opencode
Co-Authored-By: opencode <noreply@opencode.ai>
2025-06-01 14:46:04 -04:00
Dax Raad 059d76ce82 Refactor application path handling and data storage architecture
Replace simple directory-based path system with git-aware data management that uses global data directories and proper workspace detection.

🤖 Generated with opencode
Co-Authored-By: opencode <noreply@opencode.ai>
2025-06-01 14:40:44 -04:00
Dax Raad 68608753d9 fix tool name change 2025-05-31 20:33:21 -04:00
491 changed files with 59378 additions and 22474 deletions
+9
View File
@@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
insert_final_newline = true
end_of_line = lf
indent_style = space
indent_size = 2
max_line_length = 80
-37
View File
@@ -1,37 +0,0 @@
name: build
on:
workflow_dispatch:
push:
branches:
- dev
concurrency: ${{ github.workflow }}-${{ github.ref }}
permissions:
contents: write
packages: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- run: git fetch --force --tags
- uses: actions/setup-go@v5
with:
go-version: ">=1.23.2"
cache: true
cache-dependency-path: go.sum
- run: go mod download
- uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
version: latest
args: build --snapshot --clean
+4 -3
View File
@@ -3,7 +3,8 @@ name: deploy
on:
push:
branches:
- dontlook
- dev
- production
workflow_dispatch:
concurrency: ${{ github.workflow }}-${{ github.ref }}
@@ -16,10 +17,10 @@ jobs:
- uses: oven-sh/setup-bun@v1
with:
bun-version: latest
bun-version: 1.2.17
- run: bun install
- run: bun sst deploy --stage=dev
- run: bun sst deploy --stage=${{ github.ref_name }}
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+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 }}
+24
View File
@@ -0,0 +1,24 @@
name: opencode
on:
issue_comment:
types: [created]
jobs:
opencode:
if: startsWith(github.event.comment.body, 'hey 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/sdks/github@github-v1
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: ./sdks/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 }}
+65
View File
@@ -0,0 +1,65 @@
name: publish
on:
workflow_dispatch:
push:
branches:
- dev
tags:
- "*"
- "!vscode-v*"
- "!github-v*"
concurrency: ${{ github.workflow }}-${{ github.ref }}
permissions:
contents: write
packages: write
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- run: git fetch --force --tags
- uses: actions/setup-go@v5
with:
go-version: ">=1.24.0"
cache: true
cache-dependency-path: go.sum
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.2.17
- name: Install makepkg
run: |
sudo apt-get update
sudo apt-get install -y pacman-package-manager
- name: Setup SSH for AUR
run: |
mkdir -p ~/.ssh
echo "${{ secrets.AUR_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts
git config --global user.email "opencode@sst.dev"
git config --global user.name "opencode"
- 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
env:
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
AUR_KEY: ${{ secrets.AUR_KEY }}
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
-40
View File
@@ -1,40 +0,0 @@
name: release
on:
workflow_dispatch:
push:
tags:
- "*"
concurrency: ${{ github.workflow }}-${{ github.ref }}
permissions:
contents: write
packages: write
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- run: git fetch --force --tags
- uses: actions/setup-go@v5
with:
go-version: ">=1.23.2"
cache: true
cache-dependency-path: go.sum
- run: go mod download
- uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
AUR_KEY: ${{ secrets.AUR_KEY }}
+32
View File
@@ -0,0 +1,32 @@
name: stats
on:
schedule:
- cron: "0 12 * * *" # Run daily at 12:00 UTC
workflow_dispatch: # Allow manual trigger
jobs:
stats:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Run stats script
run: bun scripts/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 "ignore: update download stats $(date -I)"
git push
+4 -1
View File
@@ -1,5 +1,8 @@
.DS_Store
node_modules
.opencode
.sst
app.log
.env
.idea
.vscode
openapi.json
-3
View File
@@ -1,3 +0,0 @@
{
"$schema": "./opencode-schema.json"
}
+15
View File
@@ -0,0 +1,15 @@
# TUI Agent Guidelines
## Style
- prefer single word variable/function names
- avoid try catch where possible - prefer to let exceptions bubble up
- avoid else statements where possible
- do not make useless helper functions - inline functionality unless the
function is reusable or composable
- prefer Bun apis
## Workflow
- you can regenerate the golang sdk by calling ./scripts/stainless.ts
- we use bun for everything
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2025 OpenCode
Copyright (c) 2025 opencode
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+84 -702
View File
@@ -1,728 +1,110 @@
# ◍OpenCode
<p align="center">
<a href="https://opencode.ai">
<picture>
<source srcset="packages/web/src/assets/logo-ornate-dark.svg" media="(prefers-color-scheme: dark)">
<source srcset="packages/web/src/assets/logo-ornate-light.svg" media="(prefers-color-scheme: light)">
<img src="packages/web/src/assets/logo-ornate-light.svg" alt="opencode logo">
</picture>
</a>
</p>
<p align="center">AI coding agent, built for the terminal.</p>
<p align="center">
<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>
![OpenCode Terminal UI](screenshot.png)
[![opencode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
> **⚠️ Early Development Notice:** This project is in early development and is not yet ready for production use. Features may change, break, or be incomplete. Use at your own risk.
---
A powerful terminal-based AI assistant for developers, providing intelligent coding assistance directly in your terminal.
## Overview
OpenCode is a Go-based CLI application that brings AI assistance to your terminal. It provides a TUI (Terminal User Interface) for interacting with various AI models to help with coding tasks, debugging, and more.
## Features
- **Interactive TUI**: Built with [Bubble Tea](https://github.com/charmbracelet/bubbletea) for a smooth terminal experience
- **Multiple AI Providers**: Support for OpenAI, Anthropic Claude, Google Gemini, AWS Bedrock, Groq, Azure OpenAI, and OpenRouter
- **Session Management**: Save and manage multiple conversation sessions
- **Tool Integration**: AI can execute commands, search files, and modify code
- **Vim-like Editor**: Integrated editor with text input capabilities
- **Persistent Storage**: SQLite database for storing conversations and sessions
- **LSP Integration**: Language Server Protocol support for code intelligence
- **File Change Tracking**: Track and visualize file changes during sessions
- **External Editor Support**: Open your preferred editor for composing messages
- **Named Arguments for Custom Commands**: Create powerful custom commands with multiple named placeholders
## Installation
### Using the Install Script
### Installation
```bash
# Install the latest version
# YOLO
curl -fsSL https://opencode.ai/install | bash
# Install a specific version
curl -fsSL https://opencode.ai/install | VERSION=0.1.0 bash
# Package managers
npm i -g opencode-ai@latest # or bun/pnpm/yarn
brew install sst/tap/opencode # macOS
paru -S opencode-bin # Arch Linux
```
### Using Homebrew (macOS and Linux)
> [!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
brew install sst/tap/opencode
# 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
```
### Using AUR (Arch Linux)
### Documentation
For more info on how to configure opencode [**head over to our docs**](https://opencode.ai/docs).
### Contributing
opencode is an opinionated tool so any fundamental feature needs to go through a
design process with the core team.
> [!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.
- Bun
- Golang 1.24.x
And run.
```bash
# Using yay
yay -S opencode-bin
# Using paru
paru -S opencode-bin
$ bun install
$ bun run packages/opencode/src/index.ts
```
### Using Go
#### Development Notes
```bash
go install github.com/sst/opencode@latest
```
**API Client**: After making changes to the TypeScript API endpoints in `packages/opencode/src/server/server.ts`, you will need the opencode team to generate a new stainless sdk for the clients.
## Configuration
### FAQ
OpenCode looks for configuration in the following locations:
#### How is this different than Claude Code?
- `$HOME/.opencode.json`
- `$XDG_CONFIG_HOME/opencode/.opencode.json`
- `./.opencode.json` (local directory)
It's very similar to Claude Code in terms of capability. Here are the key differences:
### Environment Variables
- 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.
- 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.
You can configure OpenCode using environment variables:
#### What's the other repo?
| Environment Variable | Purpose |
| -------------------------- | ------------------------------------------------------ |
| `ANTHROPIC_API_KEY` | For Claude models |
| `OPENAI_API_KEY` | For OpenAI models |
| `GEMINI_API_KEY` | For Google Gemini models |
| `VERTEXAI_PROJECT` | For Google Cloud VertexAI (Gemini) |
| `VERTEXAI_LOCATION` | For Google Cloud VertexAI (Gemini) |
| `GROQ_API_KEY` | For Groq models |
| `AWS_ACCESS_KEY_ID` | For AWS Bedrock (Claude) |
| `AWS_SECRET_ACCESS_KEY` | For AWS Bedrock (Claude) |
| `AWS_REGION` | For AWS Bedrock (Claude) |
| `AZURE_OPENAI_ENDPOINT` | For Azure OpenAI models |
| `AZURE_OPENAI_API_KEY` | For Azure OpenAI models (optional when using Entra ID) |
| `AZURE_OPENAI_API_VERSION` | For Azure OpenAI models |
The other confusingly named repo has no relation to this one. You can [read the story behind it here](https://x.com/thdxr/status/1933561254481666466).
### Configuration File Structure
---
```json
{
"data": {
"directory": ".opencode"
},
"providers": {
"openai": {
"apiKey": "your-api-key",
"disabled": false
},
"anthropic": {
"apiKey": "your-api-key",
"disabled": false
},
"groq": {
"apiKey": "your-api-key",
"disabled": false
},
"openrouter": {
"apiKey": "your-api-key",
"disabled": false
}
},
"agents": {
"primary": {
"model": "claude-3.7-sonnet",
"maxTokens": 5000
},
"task": {
"model": "claude-3.7-sonnet",
"maxTokens": 5000
},
"title": {
"model": "claude-3.7-sonnet",
"maxTokens": 80
}
},
"mcpServers": {
"example": {
"type": "stdio",
"command": "path/to/mcp-server",
"env": [],
"args": []
}
},
"lsp": {
"go": {
"disabled": false,
"command": "gopls"
}
},
"shell": {
"path": "/bin/zsh",
"args": ["-l"]
},
"debug": false,
"debugLSP": false
}
```
## Supported AI Models
OpenCode supports a variety of AI models from different providers:
### OpenAI
- GPT-4.1 family (gpt-4.1, gpt-4.1-mini, gpt-4.1-nano)
- GPT-4.5 Preview
- GPT-4o family (gpt-4o, gpt-4o-mini)
- O1 family (o1, o1-pro, o1-mini)
- O3 family (o3, o3-mini)
- O4 Mini
### Anthropic
- Claude 3.5 Sonnet
- Claude 3.5 Haiku
- Claude 3.7 Sonnet
- Claude 3 Haiku
- Claude 3 Opus
### Google
- Gemini 2.5
- Gemini 2.5 Flash
- Gemini 2.0 Flash
- Gemini 2.0 Flash Lite
### AWS Bedrock
- Claude 3.7 Sonnet
### Groq
- Llama 4 Maverick (17b-128e-instruct)
- Llama 4 Scout (17b-16e-instruct)
- QWEN QWQ-32b
- Deepseek R1 distill Llama 70b
- Llama 3.3 70b Versatile
### Azure OpenAI
- GPT-4.1 family (gpt-4.1, gpt-4.1-mini, gpt-4.1-nano)
- GPT-4.5 Preview
- GPT-4o family (gpt-4o, gpt-4o-mini)
- O1 family (o1, o1-mini)
- O3 family (o3, o3-mini)
- O4 Mini
### Google Cloud VertexAI
- Gemini 2.5
- Gemini 2.5 Flash
## Using Bedrock Models
To use bedrock models with OpenCode you need three things.
1. Valid AWS credentials (the env vars: `AWS_SECRET_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and `AWS_REGION`)
2. Access to the corresponding model in AWS Bedrock in your region.
a. You can request access in the AWS console on the Bedrock -> "Model access" page.
3. A correct configuration file. You don't need the `providers` key. Instead you have to prefix your models per agent with `bedrock.` and then a valid model. For now only Claude 3.7 is supported.
```json
{
"agents": {
"primary": {
"model": "bedrock.claude-3.7-sonnet",
"maxTokens": 5000,
"reasoningEffort": ""
},
"task": {
"model": "bedrock.claude-3.7-sonnet",
"maxTokens": 5000,
"reasoningEffort": ""
},
"title": {
"model": "bedrock.claude-3.7-sonnet",
"maxTokens": 80,
"reasoningEffort": ""
}
}
}
```
## Interactive Mode Usage
```bash
# Start OpenCode
opencode
# Start with debug logging
opencode -d
# Start with a specific working directory
opencode -c /path/to/project
```
## Non-interactive Prompt Mode
You can run OpenCode in non-interactive mode by passing a prompt directly as a command-line argument or by piping text into the command. This is useful for scripting, automation, or when you want a quick answer without launching the full TUI.
```bash
# Run a single prompt and print the AI's response to the terminal
opencode -p "Explain the use of context in Go"
# Pipe input to OpenCode (equivalent to using -p flag)
echo "Explain the use of context in Go" | opencode
# Get response in JSON format
opencode -p "Explain the use of context in Go" -f json
# Or with piped input
echo "Explain the use of context in Go" | opencode -f json
# Run without showing the spinner
opencode -p "Explain the use of context in Go" -q
# Or with piped input
echo "Explain the use of context in Go" | opencode -q
# Enable verbose logging to stderr
opencode -p "Explain the use of context in Go" --verbose
# Or with piped input
echo "Explain the use of context in Go" | opencode --verbose
# Restrict the agent to only use specific tools
opencode -p "Explain the use of context in Go" --allowedTools=view,ls,glob
# Or with piped input
echo "Explain the use of context in Go" | opencode --allowedTools=view,ls,glob
# Prevent the agent from using specific tools
opencode -p "Explain the use of context in Go" --excludedTools=bash,edit
# Or with piped input
echo "Explain the use of context in Go" | opencode --excludedTools=bash,edit
```
In this mode, OpenCode will process your prompt, print the result to standard output, and then exit. All permissions are auto-approved for the session.
### Tool Restrictions
You can control which tools the AI assistant has access to in non-interactive mode:
- `--allowedTools`: Comma-separated list of tools that the agent is allowed to use. Only these tools will be available.
- `--excludedTools`: Comma-separated list of tools that the agent is not allowed to use. All other tools will be available.
These flags are mutually exclusive - you can use either `--allowedTools` or `--excludedTools`, but not both at the same time.
### Output Formats
OpenCode supports the following output formats in non-interactive mode:
| Format | Description |
| ------ | ------------------------------- |
| `text` | Plain text output (default) |
| `json` | Output wrapped in a JSON object |
The output format is implemented as a strongly-typed `OutputFormat` in the codebase, ensuring type safety and validation when processing outputs.
## Command-line Flags
| Flag | Short | Description |
| ----------------- | ----- | --------------------------------------------------- |
| `--help` | `-h` | Display help information |
| `--debug` | `-d` | Enable debug mode |
| `--cwd` | `-c` | Set current working directory |
| `--prompt` | `-p` | Run a single prompt in non-interactive mode |
| `--output-format` | `-f` | Output format for non-interactive mode (text, json) |
| `--quiet` | `-q` | Hide spinner in non-interactive mode |
| `--verbose` | | Display logs to stderr in non-interactive mode |
| `--allowedTools` | | Restrict the agent to only use specified tools |
| `--excludedTools` | | Prevent the agent from using specified tools |
## Keyboard Shortcuts
### Global Shortcuts
| Shortcut | Action |
| -------- | ------------------------------------------------------- |
| `Ctrl+C` | Quit application |
| `Ctrl+?` | Toggle help dialog |
| `?` | Toggle help dialog (when not in editing mode) |
| `Ctrl+L` | View logs |
| `Ctrl+A` | Switch session |
| `Ctrl+K` | Command dialog |
| `Ctrl+O` | Toggle model selection dialog |
| `Esc` | Close current overlay/dialog or return to previous mode |
### Chat Page Shortcuts
| Shortcut | Action |
| -------- | --------------------------------------- |
| `Ctrl+N` | Create new session |
| `Ctrl+X` | Cancel current operation/generation |
| `i` | Focus editor (when not in writing mode) |
| `Esc` | Exit writing mode and focus messages |
### Editor Shortcuts
| Shortcut | Action |
| ------------------- | ----------------------------------------- |
| `Ctrl+S` | Send message (when editor is focused) |
| `Enter` or `Ctrl+S` | Send message (when editor is not focused) |
| `Ctrl+E` | Open external editor |
| `Esc` | Blur editor and focus messages |
### Session Dialog Shortcuts
| Shortcut | Action |
| ---------- | ---------------- |
| `↑` or `k` | Previous session |
| `↓` or `j` | Next session |
| `Enter` | Select session |
| `Esc` | Close dialog |
### Model Dialog Shortcuts
| Shortcut | Action |
| ---------- | ----------------- |
| `↑` or `k` | Move up |
| `↓` or `j` | Move down |
| `←` or `h` | Previous provider |
| `→` or `l` | Next provider |
| `Esc` | Close dialog |
### Permission Dialog Shortcuts
| Shortcut | Action |
| ----------------------- | ---------------------------- |
| `←` or `left` | Switch options left |
| `→` or `right` or `tab` | Switch options right |
| `Enter` or `space` | Confirm selection |
| `a` | Allow permission |
| `A` | Allow permission for session |
| `d` | Deny permission |
### Logs Page Shortcuts
| Shortcut | Action |
| ------------------ | ------------------- |
| `Backspace` or `q` | Return to chat page |
## AI Assistant Tools
OpenCode's AI assistant has access to various tools to help with coding tasks:
### File and Code Tools
| Tool | Description | Parameters |
| ------------- | --------------------------- | ---------------------------------------------------------------------------------------- |
| `glob` | Find files by pattern | `pattern` (required), `path` (optional) |
| `grep` | Search file contents | `pattern` (required), `path` (optional), `include` (optional), `literal_text` (optional) |
| `ls` | List directory contents | `path` (optional), `ignore` (optional array of patterns) |
| `view` | View file contents | `file_path` (required), `offset` (optional), `limit` (optional) |
| `write` | Write to files | `file_path` (required), `content` (required) |
| `edit` | Edit files | Various parameters for file editing |
| `patch` | Apply patches to files | `file_path` (required), `diff` (required) |
| `diagnostics` | Get diagnostics information | `file_path` (optional) |
### Other Tools
| Tool | Description | Parameters |
| ------- | ------------------------------- | ----------------------------------------------------------- |
| `bash` | Execute shell commands | `command` (required), `timeout` (optional) |
| `fetch` | Fetch data from URLs | `url` (required), `format` (required), `timeout` (optional) |
| `agent` | Run sub-tasks with the AI agent | `prompt` (required) |
## Theming
OpenCode supports multiple themes for customizing the appearance of the terminal interface.
### Available Themes
The following predefined themes are available:
- `opencode` (default)
- `catppuccin`
- `dracula`
- `flexoki`
- `gruvbox`
- `monokai`
- `onedark`
- `tokyonight`
- `tron`
- `custom` (user-defined)
### Setting a Theme
You can set a theme in your `.opencode.json` configuration file:
```json
{
"tui": {
"theme": "monokai"
}
}
```
### Custom Themes
You can define your own custom theme by setting the `theme` to `"custom"` and providing color definitions in the `customTheme` map:
```json
{
"tui": {
"theme": "custom",
"customTheme": {
"primary": "#ffcc00",
"secondary": "#00ccff",
"accent": { "dark": "#aa00ff", "light": "#ddccff" },
"error": "#ff0000"
}
}
}
```
#### Color Definition Formats
Custom theme colors support two formats:
1. **Simple Hex String**: A single hex color string (e.g., `"#aabbcc"`) that will be used for both light and dark terminal backgrounds.
2. **Adaptive Object**: An object with `dark` and `light` keys, each holding a hex color string. This allows for adaptive colors based on the terminal's background.
#### Available Color Keys
You can define any of the following color keys in your `customTheme`:
- Base colors: `primary`, `secondary`, `accent`
- Status colors: `error`, `warning`, `success`, `info`
- Text colors: `text`, `textMuted`, `textEmphasized`
- Background colors: `background`, `backgroundSecondary`, `backgroundDarker`
- Border colors: `borderNormal`, `borderFocused`, `borderDim`
- Diff view colors: `diffAdded`, `diffRemoved`, `diffContext`, etc.
You don't need to define all colors. Any undefined colors will fall back to the default "opencode" theme colors.
### Shell Configuration
OpenCode allows you to configure the shell used by the `bash` tool. By default, it uses:
1. The shell specified in the config file (if provided)
2. The shell from the `$SHELL` environment variable (if available)
3. Falls back to `/bin/bash` if neither of the above is available
To configure a custom shell, add a `shell` section to your `.opencode.json` configuration file:
```json
{
"shell": {
"path": "/bin/zsh",
"args": ["-l"]
}
}
```
You can specify any shell executable and custom arguments:
```json
{
"shell": {
"path": "/usr/bin/fish",
"args": []
}
}
```
## Architecture
OpenCode is built with a modular architecture:
- **cmd**: Command-line interface using Cobra
- **internal/app**: Core application services
- **internal/config**: Configuration management
- **internal/db**: Database operations and migrations
- **internal/llm**: LLM providers and tools integration
- **internal/tui**: Terminal UI components and layouts
- **internal/logging**: Logging infrastructure
- **internal/message**: Message handling
- **internal/session**: Session management
- **internal/lsp**: Language Server Protocol integration
## Custom Commands
OpenCode supports custom commands that can be created by users to quickly send predefined prompts to the AI assistant.
### Creating Custom Commands
Custom commands are predefined prompts stored as Markdown files in one of three locations:
1. **User Commands** (prefixed with `user:`):
```
$XDG_CONFIG_HOME/opencode/commands/
```
(typically `~/.config/opencode/commands/` on Linux/macOS)
or
```
$HOME/.opencode/commands/
```
2. **Project Commands** (prefixed with `project:`):
```
<PROJECT DIR>/.opencode/commands/
```
Each `.md` file in these directories becomes a custom command. The file name (without extension) becomes the command ID.
For example, creating a file at `~/.config/opencode/commands/prime-context.md` with content:
```markdown
RUN git ls-files
READ README.md
```
This creates a command called `user:prime-context`.
### Command Arguments
OpenCode supports named arguments in custom commands using placeholders in the format `$NAME` (where NAME consists of uppercase letters, numbers, and underscores, and must start with a letter).
For example:
```markdown
# Fetch Context for Issue $ISSUE_NUMBER
RUN gh issue view $ISSUE_NUMBER --json title,body,comments
RUN git grep --author="$AUTHOR_NAME" -n .
RUN grep -R "$SEARCH_PATTERN" $DIRECTORY
```
When you run a command with arguments, OpenCode will prompt you to enter values for each unique placeholder. Named arguments provide several benefits:
- Clear identification of what each argument represents
- Ability to use the same argument multiple times
- Better organization for commands with multiple inputs
### Organizing Commands
You can organize commands in subdirectories:
```
~/.config/opencode/commands/git/commit.md
```
This creates a command with ID `user:git:commit`.
### Using Custom Commands
1. Press `Ctrl+K` to open the command dialog
2. Select your custom command (prefixed with either `user:` or `project:`)
3. Press Enter to execute the command
The content of the command file will be sent as a message to the AI assistant.
## MCP (Model Context Protocol)
OpenCode implements the Model Context Protocol (MCP) to extend its capabilities through external tools. MCP provides a standardized way for the AI assistant to interact with external services and tools.
### MCP Features
- **External Tool Integration**: Connect to external tools and services via a standardized protocol
- **Tool Discovery**: Automatically discover available tools from MCP servers
- **Multiple Connection Types**:
- **Stdio**: Communicate with tools via standard input/output
- **SSE**: Communicate with tools via Server-Sent Events
- **Security**: Permission system for controlling access to MCP tools
### Configuring MCP Servers
MCP servers are defined in the configuration file under the `mcpServers` section:
```json
{
"mcpServers": {
"example": {
"type": "stdio",
"command": "path/to/mcp-server",
"env": [],
"args": []
},
"web-example": {
"type": "sse",
"url": "https://example.com/mcp",
"headers": {
"Authorization": "Bearer token"
}
}
}
}
```
### MCP Tool Usage
Once configured, MCP tools are automatically available to the AI assistant alongside built-in tools. They follow the same permission model as other tools, requiring user approval before execution.
## LSP (Language Server Protocol)
OpenCode integrates with Language Server Protocol to provide code intelligence features across multiple programming languages.
### LSP Features
- **Multi-language Support**: Connect to language servers for different programming languages
- **Diagnostics**: Receive error checking and linting information
- **File Watching**: Automatically notify language servers of file changes
### Configuring LSP
Language servers are configured in the configuration file under the `lsp` section:
```json
{
"lsp": {
"go": {
"disabled": false,
"command": "gopls"
},
"typescript": {
"disabled": false,
"command": "typescript-language-server",
"args": ["--stdio"]
}
}
}
```
### LSP Integration with AI
The AI assistant can access LSP features through the `diagnostics` tool, allowing it to:
- Check for errors in your code
- Suggest fixes based on diagnostics
While the LSP client implementation supports the full LSP protocol (including completions, hover, definition, etc.), currently only diagnostics are exposed to the AI assistant.
## Development
### Prerequisites
- Go 1.24.0 or higher
### Building from Source
```bash
# Clone the repository
git clone https://github.com/sst/opencode.git
cd opencode
# Build
go build -o opencode
# Run
./opencode
```
## Acknowledgments
OpenCode gratefully acknowledges the contributions and support from these key individuals:
- [@isaacphi](https://github.com/isaacphi) - For the [mcp-language-server](https://github.com/isaacphi/mcp-language-server) project which provided the foundation for our LSP client implementation
- [@adamdottv](https://github.com/adamdottv) - For the design direction and UI/UX architecture
Special thanks to the broader open source community whose tools and libraries have made this project possible.
## License
OpenCode is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
## Contributing
Contributions are welcome! Here's how you can contribute:
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
Please make sure to update tests as appropriate and follow the existing code style.
**Join our community** [Discord](https://discord.gg/opencode) | [YouTube](https://www.youtube.com/c/sst-dev) | [X.com](https://x.com/SST_dev)
+25
View File
@@ -0,0 +1,25 @@
# Download Stats
| Date | GitHub Downloads | npm Downloads | Total |
| ---------- | ---------------- | ---------------- | ----------------- |
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
| 2025-07-10 | 43,796 (+5,744) | 71,402 (+6,934) | 115,198 (+12,678) |
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
| 2025-07-18 | 70,380 (+1) | 102,587 (+0) | 172,967 (+1) |
| 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) |
+497 -255
View File
File diff suppressed because it is too large Load Diff
+13 -9
View File
@@ -4,13 +4,18 @@ 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", {
domain: `api.${domain}`,
handler: "packages/function/src/api.ts",
environment: {
WEB_DOMAIN: domain,
},
url: true,
link: [bucket],
link: [bucket, GITHUB_APP_ID, GITHUB_APP_PRIVATE_KEY],
transform: {
worker: (args) => {
args.logpush = true
@@ -23,22 +28,21 @@ export const api = new sst.cloudflare.Worker("Api", {
},
])
args.migrations = {
oldTag: "v1",
newTag: "v1",
// Note: when releasing the next tag, make sure all stages use tag v2
oldTag: $app.stage === "production" ? "" : "v1",
newTag: $app.stage === "production" ? "" : "v1",
//newSqliteClasses: ["SyncServer"],
}
},
},
})
new sst.cloudflare.StaticSite("Web", {
path: "packages/web",
new sst.cloudflare.x.Astro("Web", {
domain,
path: "packages/web",
environment: {
// For astro config
SST_STAGE: $app.stage,
VITE_API_URL: api.url,
},
build: {
command: "bun run build",
output: "dist",
},
})
+18 -10
View File
@@ -12,23 +12,28 @@ requested_version=${VERSION:-}
os=$(uname -s | tr '[:upper:]' '[:lower:]')
if [[ "$os" == "darwin" ]]; then
os="mac"
os="darwin"
fi
arch=$(uname -m)
if [[ "$arch" == "aarch64" ]]; then
arch="arm64"
elif [[ "$arch" == "x86_64" ]]; then
arch="x64"
fi
filename="$APP-$os-$arch.tar.gz"
filename="$APP-$os-$arch.zip"
case "$filename" in
*"-linux-"*)
[[ "$arch" == "x86_64" || "$arch" == "arm64" || "$arch" == "i386" ]] || exit 1
[[ "$arch" == "x64" || "$arch" == "arm64" ]] || exit 1
;;
*"-mac-"*)
[[ "$arch" == "x86_64" || "$arch" == "arm64" ]] || exit 1
*"-darwin-"*)
[[ "$arch" == "x64" || "$arch" == "arm64" ]] || exit 1
;;
*"-windows-"*)
[[ "$arch" == "x64" ]] || exit 1
;;
*)
echo "${RED}Unsupported OS/Arch: $os/$arch${NC}"
@@ -43,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
@@ -88,8 +93,9 @@ check_version() {
download_and_install() {
print_message info "Downloading ${ORANGE}opencode ${GREEN}version: ${YELLOW}$specific_version ${GREEN}..."
mkdir -p opencodetmp && cd opencodetmp
curl -# -L $url | tar xz
mv opencode $INSTALL_DIR
curl -# -L -o "$filename" "$url"
unzip -q "$filename"
mv opencode "$INSTALL_DIR"
cd .. && rm -rf opencodetmp
}
@@ -101,7 +107,9 @@ add_to_path() {
local config_file=$1
local command=$2
if [[ -w $config_file ]]; then
if grep -Fxq "$command" "$config_file"; then
print_message info "Command already exists in $config_file, skipping write."
elif [[ -w $config_file ]]; then
echo -e "\n# opencode" >> "$config_file"
echo "$command" >> "$config_file"
print_message info "Successfully added ${ORANGE}opencode ${GREEN}to \$PATH in $config_file"
@@ -167,6 +175,7 @@ if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
;;
*)
export PATH=$INSTALL_DIR:$PATH
print_message warning "Manually add the directory to $config_file (or similar):"
print_message info " export PATH=$INSTALL_DIR:\$PATH"
;;
@@ -177,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
+23
View File
@@ -0,0 +1,23 @@
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"openrouter": {
"models": {
"moonshotai/kimi-k2": {
"options": {
"provider": {
"order": ["baseten"],
"allow_fallbacks": false
}
}
}
}
}
},
"mcp": {
"weather": {
"type": "local",
"command": ["opencode", "x", "@h1deya/mcp-server-weather"]
}
}
}
+11 -9
View File
@@ -5,7 +5,10 @@
"type": "module",
"packageManager": "bun@1.2.14",
"scripts": {
"dev": "sst dev"
"dev": "bun run packages/opencode/src/index.ts",
"typecheck": "bun run --filter='*' typecheck",
"stainless": "./scripts/stainless",
"postinstall": "./scripts/hooks"
},
"workspaces": {
"packages": [
@@ -14,13 +17,13 @@
"catalog": {
"typescript": "5.8.2",
"@types/node": "22.13.9",
"zod": "3.24.2",
"ai": "5.0.0-alpha.7"
"zod": "3.25.49",
"ai": "5.0.0-beta.21"
}
},
"devDependencies": {
"prettier": "3.5.3",
"sst": "3.16.0"
"sst": "3.17.8"
},
"repository": {
"type": "git",
@@ -28,14 +31,13 @@
},
"license": "MIT",
"prettier": {
"semi": false
},
"overrides": {
"zod": "3.24.2"
"semi": false,
"printWidth": 120
},
"trustedDependencies": [
"esbuild",
"protobufjs",
"sharp"
]
],
"patchedDependencies": {}
}
+5
View File
@@ -8,5 +8,10 @@
"@cloudflare/workers-types": "4.20250522.0",
"typescript": "catalog:",
"@types/node": "catalog:"
},
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "22.0.0",
"jose": "6.0.11"
}
}
+176 -18
View File
@@ -1,9 +1,14 @@
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>
Bucket: R2Bucket
WEB_DOMAIN: string
}
export class SyncServer extends DurableObject<Env> {
@@ -19,9 +24,9 @@ export class SyncServer extends DurableObject<Env> {
this.ctx.acceptWebSocket(server)
const data = await this.ctx.storage.list()
for (const [key, content] of data.entries()) {
server.send(JSON.stringify({ key, content }))
}
Array.from(data.entries())
.filter(([key, _]) => key.startsWith("session/"))
.map(([key, content]) => server.send(JSON.stringify({ key, content })))
return new Response(null, {
status: 101,
@@ -35,12 +40,12 @@ export class SyncServer extends DurableObject<Env> {
ws.close(code, "Durable Object is closing WebSocket")
}
async publish(secret: string, key: string, content: any) {
if (secret !== (await this.getSecret())) throw new Error("Invalid secret")
async publish(key: string, content: any) {
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 })
@@ -69,6 +74,17 @@ export class SyncServer extends DurableObject<Env> {
return secret
}
public async getData() {
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 }))
}
public async assertSecret(secret: string) {
if (secret !== (await this.getSecret())) throw new Error("Invalid secret")
}
private async getSecret() {
return this.ctx.storage.get<string>("secret")
}
@@ -77,22 +93,26 @@ export class SyncServer extends DurableObject<Env> {
return this.ctx.storage.get<string>("sessionID")
}
async clear(secret: string) {
await this.assertSecret(secret)
async clear() {
const sessionID = await this.getSessionID()
const list = await this.env.Bucket.list({
prefix: `session/message/${sessionID}/`,
limit: 1000,
})
for (const item of list.objects) {
await this.env.Bucket.delete(item.key)
}
await this.env.Bucket.delete(`session/info/${sessionID}`)
await this.ctx.storage.deleteAll()
}
private async assertSecret(secret: string) {
if (secret !== (await this.getSecret())) throw new Error("Invalid secret")
}
static shortName(id: string) {
return id.substring(id.length - 8)
}
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const url = new URL(request.url)
const splits = url.pathname.split("/")
const method = splits[1]
@@ -113,7 +133,7 @@ export default {
return new Response(
JSON.stringify({
secret,
url: "https://dev.opencode.ai/s?id=" + short,
url: `https://${env.WEB_DOMAIN}/s/${short}`,
}),
{
headers: { "Content-Type": "application/json" },
@@ -127,7 +147,17 @@ export default {
const secret = body.secret
const id = env.SYNC_SERVER.idFromName(SyncServer.shortName(sessionID))
const stub = env.SYNC_SERVER.get(id)
await stub.clear(secret)
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" },
})
@@ -143,7 +173,8 @@ export default {
const name = SyncServer.shortName(body.sessionID)
const id = env.SYNC_SERVER.idFromName(name)
const stub = env.SYNC_SERVER.get(id)
await stub.publish(body.secret, body.key, body.content)
await stub.assertSecret(body.secret)
await stub.publish(body.key, body.content)
return new Response(JSON.stringify({}), {
headers: { "Content-Type": "application/json" },
})
@@ -158,10 +189,137 @@ export default {
}
const id = url.searchParams.get("id")
console.log("share_poll", id)
if (!id)
return new Response("Error: Share ID is required", { status: 400 })
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") {
messages[d.content.id] = {
parts: [],
...d.content,
}
}
if (type === "part") {
messages[d.content.messageID].parts.push(d.content)
}
})
return new Response(
JSON.stringify({
info,
messages,
}),
{
headers: { "Content-Type": "application/json" },
},
)
}
/**
* Used by the GitHub action to get GitHub installation access token given the OIDC token
*/
if (request.method === "POST" && method === "exchange_github_app_token") {
const EXPECTED_AUDIENCE = "opencode-github-action"
const GITHUB_ISSUER = "https://token.actions.githubusercontent.com"
const JWKS_URL = `${GITHUB_ISSUER}/.well-known/jwks`
// get Authorization header
const authHeader = request.headers.get("Authorization")
const token = authHeader?.replace(/^Bearer /, "")
if (!token)
return new Response(JSON.stringify({ error: "Authorization header is required" }), {
status: 401,
headers: { "Content-Type": "application/json" },
})
// verify token
const JWKS = createRemoteJWKSet(new URL(JWKS_URL))
let owner, repo
try {
const { payload } = await jwtVerify(token, JWKS, {
issuer: GITHUB_ISSUER,
audience: EXPECTED_AUDIENCE,
})
const sub = payload.sub // e.g. 'repo:my-org/my-repo:ref:refs/heads/main'
const parts = sub.split(":")[1].split("/")
owner = parts[0]
repo = parts[1]
} catch (err) {
console.error("Token verification failed:", err)
return new Response(JSON.stringify({ error: "Invalid or expired token" }), {
status: 403,
headers: { "Content-Type": "application/json" },
})
}
// 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 new Response(JSON.stringify({ token: installationAuth.token }), {
headers: { "Content-Type": "application/json" },
})
}
/**
* Used by the opencode CLI to check if the GitHub app is installed
*/
if (request.method === "GET" && method === "get_github_app_installation") {
const owner = url.searchParams.get("owner")
const repo = url.searchParams.get("repo")
const auth = createAppAuth({
appId: Resource.GITHUB_APP_ID.value,
privateKey: Resource.GITHUB_APP_PRIVATE_KEY.value,
})
const appAuth = await auth({ type: "app" })
// Lookup installation
const octokit = new Octokit({ auth: appAuth.token })
let installation
try {
const ret = await octokit.apps.getRepoInstallation({ owner, repo })
installation = ret.data
} catch (err) {
if (err instanceof Error && err.message.includes("Not Found")) {
// not installed
} else {
throw err
}
}
return new Response(JSON.stringify({ installation }), {
headers: { "Content-Type": "application/json" },
})
}
return new Response("Not Found", { status: 404 })
},
}
+9 -1
View File
@@ -6,8 +6,16 @@
import "sst"
declare module "sst" {
export interface Resource {
"GITHUB_APP_ID": {
"type": "sst.sst.Secret"
"value": string
}
"GITHUB_APP_PRIVATE_KEY": {
"type": "sst.sst.Secret"
"value": string
}
"Web": {
"type": "sst.cloudflare.StaticSite"
"type": "sst.cloudflare.Astro"
"url": string
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
node_modules
research
dist
gen
app.log
+40
View File
@@ -0,0 +1,40 @@
# opencode agent guidelines
## Build/Test Commands
- **Install**: `bun install`
- **Run**: `bun run index.ts`
- **Typecheck**: `bun run typecheck` (npm run typecheck)
- **Test**: `bun test` (runs all tests)
- **Single test**: `bun test test/tool/tool.test.ts` (specific test file)
## Code Style
- **Runtime**: Bun with TypeScript ESM modules
- **Imports**: Use relative imports for local modules, named imports preferred
- **Types**: Zod schemas for validation, TypeScript interfaces for structure
- **Naming**: camelCase for variables/functions, PascalCase for classes/namespaces
- **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
- **Context**: Pass `sessionID` in tool context, use `App.provide()` for DI
- **Validation**: All inputs validated with Zod schemas
- **Logging**: Use `Log.create({ service: "name" })` pattern
- **Storage**: Use `Storage` namespace for persistence
- **API Client**: Go TUI communicates with TypeScript server via stainless SDK. When adding/modifying server endpoints in `packages/opencode/src/server/server.ts`, ask the user to generate a new client SDK to proceed with client-side changes.
+1 -1
View File
@@ -49,7 +49,7 @@ else
done
if [ -z "$resolved" ]; then
printf "It seems that your package manager failed to install the right version of the SST CLI for your platform. You can try manually installing the \"%s\" package\n" "$name" >&2
printf "It seems that your package manager failed to install the right version of the opencode CLI for your platform. You can try manually installing the \"%s\" package\n" "$name" >&2
exit 1
fi
fi
+56
View File
@@ -0,0 +1,56 @@
@echo off
setlocal enabledelayedexpansion
if defined OPENCODE_BIN_PATH (
set "resolved=%OPENCODE_BIN_PATH%"
goto :execute
)
rem Get the directory of this script
set "script_dir=%~dp0"
set "script_dir=%script_dir:~0,-1%"
rem Detect platform and architecture
set "platform=win32"
rem Detect architecture
if "%PROCESSOR_ARCHITECTURE%"=="AMD64" (
set "arch=x64"
) else if "%PROCESSOR_ARCHITECTURE%"=="ARM64" (
set "arch=arm64"
) else if "%PROCESSOR_ARCHITECTURE%"=="x86" (
set "arch=x86"
) else (
set "arch=x64"
)
set "name=opencode-!platform!-!arch!"
set "binary=opencode.exe"
rem Search for the binary starting from script location
set "resolved="
set "current_dir=%script_dir%"
:search_loop
set "candidate=%current_dir%\node_modules\%name%\bin\%binary%"
if exist "%candidate%" (
set "resolved=%candidate%"
goto :execute
)
rem Move up one directory
for %%i in ("%current_dir%") do set "parent_dir=%%~dpi"
set "parent_dir=%parent_dir:~0,-1%"
rem Check if we've reached the root
if "%current_dir%"=="%parent_dir%" goto :not_found
set "current_dir=%parent_dir%"
goto :search_loop
:not_found
echo It seems that your package manager failed to install the right version of the opencode CLI for your platform. You can try manually installing the "%name%" package >&2
exit /b 1
:execute
rem Execute the binary with all arguments
"%resolved%" %*
+25 -16
View File
@@ -1,40 +1,49 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "0.0.0",
"version": "0.0.5",
"name": "opencode",
"type": "module",
"private": true,
"scripts": {},
"scripts": {
"typecheck": "tsc --noEmit",
"dev": "bun run ./src/index.ts"
},
"bin": {
"opencode": "./bin/opencode"
},
"exports": {
"./*": [
"./src/*.ts",
"./src/*/index.ts"
]
"./*": "./src/*.ts"
},
"devDependencies": {
"@ai-sdk/amazon-bedrock": "2.2.10",
"@ai-sdk/anthropic": "1.2.12",
"@standard-schema/spec": "1.0.0",
"@tsconfig/bun": "1.0.7",
"@types/bun": "latest",
"@types/turndown": "5.0.5",
"typescript": "catalog:"
"@types/yargs": "17.0.33",
"typescript": "catalog:",
"vscode-languageserver-types": "3.17.5",
"zod-to-json-schema": "3.24.5"
},
"dependencies": {
"@flystorage/file-storage": "1.1.0",
"@flystorage/local-fs": "1.1.0",
"@hono/zod-validator": "0.5.0",
"@standard-schema/spec": "1.0.0",
"@clack/prompts": "0.11.0",
"@hono/zod-validator": "0.4.2",
"@modelcontextprotocol/sdk": "1.15.1",
"@openauthjs/openauth": "0.4.3",
"ai": "catalog:",
"cac": "6.7.14",
"decimal.js": "10.5.0",
"diff": "8.0.2",
"env-paths": "3.0.0",
"hono": "4.7.10",
"hono-openapi": "0.4.8",
"isomorphic-git": "1.32.1",
"open": "10.1.2",
"remeda": "2.22.3",
"ts-lsp-client": "1.0.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-openapi": "4.1.0"
}
}
+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env node
import fs from "fs"
import path from "path"
import os from "os"
import { fileURLToPath } from "url"
import { createRequire } from "module"
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const require = createRequire(import.meta.url)
function detectPlatformAndArch() {
// Map platform names
let platform
switch (os.platform()) {
case "darwin":
platform = "darwin"
break
case "linux":
platform = "linux"
break
case "win32":
platform = "win32"
break
default:
platform = os.platform()
break
}
// Map architecture names
let arch
switch (os.arch()) {
case "x64":
arch = "x64"
break
case "arm64":
arch = "arm64"
break
case "arm":
arch = "arm"
break
default:
arch = os.arch()
break
}
return { platform, arch }
}
function findBinary() {
const { platform, arch } = detectPlatformAndArch()
const packageName = `opencode-${platform}-${arch}`
const binary = platform === "win32" ? "opencode.exe" : "opencode"
try {
// Use require.resolve to find the package
const packageJsonPath = require.resolve(`${packageName}/package.json`)
const packageDir = path.dirname(packageJsonPath)
const binaryPath = path.join(packageDir, "bin", binary)
if (!fs.existsSync(binaryPath)) {
throw new Error(`Binary not found at ${binaryPath}`)
}
return binaryPath
} catch (error) {
throw new Error(`Could not find package ${packageName}: ${error.message}`)
}
}
function main() {
try {
const binaryPath = findBinary()
const binScript = path.join(__dirname, "bin", "opencode")
// Remove existing bin script if it exists
if (fs.existsSync(binScript)) {
fs.unlinkSync(binScript)
}
// Create symlink to the actual binary
fs.symlinkSync(binaryPath, binScript)
console.log(`opencode binary symlinked: ${binScript} -> ${binaryPath}`)
} catch (error) {
console.error("Failed to create opencode binary symlink:", error.message)
process.exit(1)
}
}
main()
+221
View File
@@ -0,0 +1,221 @@
#!/usr/bin/env bun
import { $ } from "bun"
import pkg from "../package.json"
const dry = process.argv.includes("--dry")
const snapshot = process.argv.includes("--snapshot")
const version = snapshot
? `0.0.0-${new Date().toISOString().slice(0, 16).replace(/[-:T]/g, "")}`
: await $`git describe --tags --abbrev=0`
.text()
.then((x) => x.substring(1).trim())
.catch(() => {
console.error("tag not found")
process.exit(1)
})
console.log(`publishing ${version}`)
const GOARCH: Record<string, string> = {
arm64: "arm64",
x64: "amd64",
}
const targets = [
["linux", "arm64"],
["linux", "x64"],
["darwin", "x64"],
["darwin", "arm64"],
["windows", "x64"],
]
await $`rm -rf dist`
const optionalDependencies: Record<string, string> = {}
const npmTag = snapshot ? "snapshot" : "latest"
for (const [os, arch] of targets) {
console.log(`building ${os}-${arch}`)
const name = `${pkg.name}-${os}-${arch}`
await $`mkdir -p dist/${name}/bin`
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 $`rm -rf ./dist/${name}/bin/tui`
await Bun.file(`dist/${name}/package.json`).write(
JSON.stringify(
{
name,
version,
os: [os === "windows" ? "win32" : os],
cpu: [arch],
},
null,
2,
),
)
if (!dry) await $`cd dist/${name} && bun publish --access public --tag ${npmTag}`
optionalDependencies[name] = version
}
await $`mkdir -p ./dist/${pkg.name}`
await $`cp -r ./bin ./dist/${pkg.name}/bin`
await $`cp ./script/postinstall.mjs ./dist/${pkg.name}/postinstall.mjs`
await Bun.file(`./dist/${pkg.name}/package.json`).write(
JSON.stringify(
{
name: pkg.name + "-ai",
bin: {
[pkg.name]: `./bin/${pkg.name}`,
},
scripts: {
postinstall: "node ./postinstall.mjs",
},
version,
optionalDependencies,
},
null,
2,
),
)
if (!dry) await $`cd ./dist/${pkg.name} && bun publish --access public --tag ${npmTag}`
if (!snapshot) {
// Github Release
for (const key of Object.keys(optionalDependencies)) {
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())
.then((data) => data.tag_name)
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("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())
// AUR package
const pkgbuild = [
"# Maintainer: dax",
"# Maintainer: adam",
"",
"pkgname='${pkg}'",
`pkgver=${version.split("-")[0]}`,
"options=('!debug' '!strip')",
"pkgrel=1",
"pkgdesc='The AI coding agent built for the terminal.'",
"url='https://github.com/sst/opencode'",
"arch=('aarch64' 'x86_64')",
"license=('MIT')",
"provides=('opencode')",
"conflicts=('opencode')",
"depends=('fzf' 'ripgrep')",
"",
`source_aarch64=("\${pkgname}_\${pkgver}_aarch64.zip::https://github.com/sst/opencode/releases/download/v${version}/opencode-linux-arm64.zip")`,
`sha256sums_aarch64=('${arm64Sha}')`,
"",
`source_x86_64=("\${pkgname}_\${pkgver}_x86_64.zip::https://github.com/sst/opencode/releases/download/v${version}/opencode-linux-x64.zip")`,
`sha256sums_x86_64=('${x64Sha}')`,
"",
"package() {",
' install -Dm755 ./opencode "${pkgdir}/usr/bin/opencode"',
"}",
"",
].join("\n")
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 $`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}"`
if (!dry) await $`cd ./dist/aur-${pkg} && git push`
}
// Homebrew formula
const homebrewFormula = [
"# typed: false",
"# frozen_string_literal: true",
"",
"# This file was generated by GoReleaser. DO NOT EDIT.",
"class Opencode < Formula",
` desc "The AI coding agent built for the terminal."`,
` homepage "https://github.com/sst/opencode"`,
` version "${version.split("-")[0]}"`,
"",
" on_macos do",
" if Hardware::CPU.intel?",
` url "https://github.com/sst/opencode/releases/download/v${version}/opencode-darwin-x64.zip"`,
` sha256 "${macX64Sha}"`,
"",
" def install",
' bin.install "opencode"',
" end",
" end",
" if Hardware::CPU.arm?",
` url "https://github.com/sst/opencode/releases/download/v${version}/opencode-darwin-arm64.zip"`,
` sha256 "${macArm64Sha}"`,
"",
" def install",
' bin.install "opencode"',
" end",
" end",
" end",
"",
" on_linux do",
" if Hardware::CPU.intel? and Hardware::CPU.is_64_bit?",
` url "https://github.com/sst/opencode/releases/download/v${version}/opencode-linux-x64.zip"`,
` sha256 "${x64Sha}"`,
" def install",
' bin.install "opencode"',
" end",
" end",
" if Hardware::CPU.arm? and Hardware::CPU.is_64_bit?",
` url "https://github.com/sst/opencode/releases/download/v${version}/opencode-linux-arm64.zip"`,
` sha256 "${arm64Sha}"`,
" def install",
' bin.install "opencode"',
" end",
" end",
" end",
"end",
"",
"",
].join("\n")
await $`rm -rf ./dist/homebrew-tap`
await $`git clone https://${process.env["GITHUB_TOKEN"]}@github.com/sst/homebrew-tap.git ./dist/homebrew-tap`
await Bun.file("./dist/homebrew-tap/opencode.rb").write(homebrewFormula)
await $`cd ./dist/homebrew-tap && git add opencode.rb`
await $`cd ./dist/homebrew-tap && git commit -m "Update to v${version}"`
if (!dry) await $`cd ./dist/homebrew-tap && git push`
}
-69
View File
@@ -1,69 +0,0 @@
#!/usr/bin/env bun
import { $ } from "bun"
import pkg from "../package.json"
const dry = process.argv.includes("--dry")
const version = `0.0.0-${Date.now()}`
const GOARCH: Record<string, string> = {
arm64: "arm64",
x64: "amd64",
}
const targets = [
["linux", "arm64"],
["linux", "x64"],
["darwin", "x64"],
["darwin", "arm64"],
["windows", "x64"],
]
await $`rm -rf dist`
const optionalDependencies: Record<string, string> = {}
for (const [os, arch] of targets) {
console.log(`building ${os}-${arch}`)
const name = `${pkg.name}-${os}-${arch}`
await $`mkdir -p dist/${name}/bin`
await $`GOOS=${os} GOARCH=${GOARCH[arch]} go build -ldflags="-s -w -X github.com/sst/opencode/internal/version.Version=${version}" -o ../opencode/dist/${name}/bin/tui ../tui/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 $`rm -rf ./dist/${name}/bin/tui`
await Bun.file(`dist/${name}/package.json`).write(
JSON.stringify(
{
name,
version,
os: [os === "windows" ? "win32" : os],
cpu: [arch],
},
null,
2,
),
)
if (!dry) await $`cd dist/${name} && npm publish --access public --tag latest`
optionalDependencies[name] = version
}
await $`mkdir -p ./dist/${pkg.name}`
await $`cp -r ./bin ./dist/${pkg.name}/bin`
await Bun.file(`./dist/${pkg.name}/package.json`).write(
JSON.stringify(
{
name: pkg.name + "-ai",
bin: {
[pkg.name]: `./bin/${pkg.name}`,
},
version,
optionalDependencies,
},
null,
2,
),
)
if (!dry)
await $`cd ./dist/${pkg.name} && npm publish --access public --tag latest`
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env bun
import "zod-openapi/extend"
import { Config } from "../src/config/config"
import { zodToJsonSchema } from "zod-to-json-schema"
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))
+99 -32
View File
@@ -1,21 +1,62 @@
import fs from "fs/promises"
import { AppPath } from "./path"
import "zod-openapi/extend"
import { Log } from "../util/log"
import { Context } from "../util/context"
import { Filesystem } from "../util/filesystem"
import { Global } from "../global"
import path from "path"
import os from "os"
import { z } from "zod"
export namespace App {
const log = Log.create({ service: "app" })
export type Info = Awaited<ReturnType<typeof create>>
export const Info = z
.object({
hostname: z.string(),
git: z.boolean(),
path: z.object({
config: z.string(),
data: z.string(),
root: z.string(),
cwd: z.string(),
state: z.string(),
}),
time: z.object({
initialized: z.number().optional(),
}),
})
.openapi({
ref: "App",
})
export type Info = z.infer<typeof Info>
const ctx = Context.create<Info>("app")
const ctx = Context.create<{
info: Info
services: Map<any, { state: any; shutdown?: (input: any) => Promise<void> }>
}>("app")
async function create(input: { directory: string }) {
const dataDir = AppPath.data(input.directory)
await fs.mkdir(dataDir, { recursive: true })
await Log.file(input.directory)
export const use = ctx.use
log.info("created", { path: dataDir })
const APP_JSON = "app.json"
export type Input = {
cwd: string
}
export const provideExisting = ctx.provide
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))
log.info("git", { git })
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
}
await stateFile.write(JSON.stringify(state))
const services = new Map<
any,
@@ -25,16 +66,39 @@ export namespace App {
}
>()
const result = {
get services() {
return services
const root = git ?? input.cwd
const info: Info = {
hostname: os.hostname(),
time: {
initialized: state.initialized,
},
get root() {
return input.directory
git: git !== undefined,
path: {
config: Global.Path.config,
state: Global.Path.state,
data,
root,
cwd: input.cwd,
},
}
const app = {
services,
info,
}
return result
return ctx.provide(app, async () => {
try {
const result = await cb(app.info)
return result
} finally {
for (const [key, entry] of app.services.entries()) {
if (!entry.shutdown) continue
log.info("shutdown", { name: key })
await entry.shutdown?.(await entry.state)
}
}
})
}
export function state<State>(
@@ -48,31 +112,34 @@ export namespace App {
if (!services.has(key)) {
log.info("registering service", { name: key })
services.set(key, {
state: init(app),
shutdown: shutdown,
state: init(app.info),
shutdown,
})
}
return services.get(key)?.state as State
}
}
export async function use() {
return ctx.use()
export function info() {
return ctx.use().info
}
export async function provide<T extends (app: Info) => any>(
input: { directory: string },
cb: T,
) {
const app = await create(input)
export async function initialize() {
const { info } = ctx.use()
info.time.initialized = Date.now()
await Bun.write(
path.join(info.path.data, APP_JSON),
JSON.stringify({
initialized: Date.now(),
}),
)
}
return ctx.provide(app, async () => {
const result = await cb(app)
for (const [key, entry] of app.services.entries()) {
log.info("shutdown", { name: key })
await entry.shutdown?.(await entry.state)
}
return result
})
function directory(input: string): string {
return input
.split(path.sep)
.filter(Boolean)
.join("-")
.replace(/[^A-Za-z0-9_]/g, "-")
}
}
-11
View File
@@ -1,11 +0,0 @@
import path from "path"
export namespace AppPath {
export function data(input: string) {
return path.join(input, ".opencode")
}
export function storage(input: string) {
return path.join(data(input), "storage")
}
}
+84
View File
@@ -0,0 +1,84 @@
import { generatePKCE } from "@openauthjs/openauth/pkce"
import { Auth } from "./index"
export namespace AuthAnthropic {
const CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
export async function authorize(mode: "max" | "console") {
const pkce = await generatePKCE()
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("code_challenge", pkce.challenge)
url.searchParams.set("code_challenge_method", "S256")
url.searchParams.set("state", pkce.verifier)
return {
url: url.toString(),
verifier: pkce.verifier,
}
}
export async function exchange(code: string, verifier: string) {
const splits = code.split("#")
const result = await fetch("https://console.anthropic.com/v1/oauth/token", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
code: splits[0],
state: splits[1],
grant_type: "authorization_code",
client_id: CLIENT_ID,
redirect_uri: "https://console.anthropic.com/oauth/code/callback",
code_verifier: verifier,
}),
})
if (!result.ok) throw new ExchangeFailed()
const json = await result.json()
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,
}),
})
if (!response.ok) return
const json = await response.json()
await Auth.set("anthropic", {
type: "oauth",
refresh: json.refresh_token as string,
access: json.access_token as string,
expires: Date.now() + json.expires_in * 1000,
})
return json.access_token as string
}
export class ExchangeFailed extends Error {
constructor() {
super("Exchange failed")
}
}
}
+19
View File
@@ -0,0 +1,19 @@
import { Global } from "../global"
import { lazy } from "../util/lazy"
import path from "path"
export const AuthCopilot = lazy(async () => {
const file = Bun.file(path.join(Global.Path.state, "plugin", "copilot.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 (!exists) {
const worked = await response
if (!worked) return
}
const result = await import(file.name!).catch(() => {})
if (!result) return
return result.AuthCopilot
})
@@ -0,0 +1,147 @@
import { z } from "zod"
import { Auth } from "./index"
import { NamedError } from "../util/error"
export namespace AuthGithubCopilot {
const CLIENT_ID = "Iv1.b507a08c87ecfe98"
const DEVICE_CODE_URL = "https://github.com/login/device/code"
const ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token"
const COPILOT_API_KEY_URL = "https://api.github.com/copilot_internal/v2/token"
interface DeviceCodeResponse {
device_code: string
user_code: string
verification_uri: string
expires_in: number
interval: number
}
interface AccessTokenResponse {
access_token?: string
error?: string
error_description?: string
}
interface CopilotTokenResponse {
token: string
expires_at: number
refresh_in: number
endpoints: {
api: string
}
}
export async function authorize() {
const deviceResponse = await fetch(DEVICE_CODE_URL, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"User-Agent": "GitHubCopilotChat/0.26.7",
},
body: JSON.stringify({
client_id: CLIENT_ID,
scope: "read:user",
}),
})
const deviceData: DeviceCodeResponse = await deviceResponse.json()
return {
device: deviceData.device_code,
user: deviceData.user_code,
verification: deviceData.verification_uri,
interval: deviceData.interval || 5,
expiry: deviceData.expires_in,
}
}
export async function poll(device_code: string) {
const response = await fetch(ACCESS_TOKEN_URL, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"User-Agent": "GitHubCopilotChat/0.26.7",
},
body: JSON.stringify({
client_id: CLIENT_ID,
device_code,
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
}),
})
if (!response.ok) return "failed"
const data: AccessTokenResponse = await response.json()
if (data.access_token) {
// Store the GitHub OAuth token
await Auth.set("github-copilot", {
type: "oauth",
refresh: data.access_token,
access: "",
expires: 0,
})
return "complete"
}
if (data.error === "authorization_pending") return "pending"
if (data.error) return "failed"
return "pending"
}
export async function access() {
const info = await Auth.get("github-copilot")
if (!info || info.type !== "oauth") return
if (info.access && info.expires > Date.now()) return info.access
// Get new Copilot API token
const response = await fetch(COPILOT_API_KEY_URL, {
headers: {
Accept: "application/json",
Authorization: `Bearer ${info.refresh}`,
"User-Agent": "GitHubCopilotChat/0.26.7",
"Editor-Version": "vscode/1.99.3",
"Editor-Plugin-Version": "copilot-chat/0.26.7",
},
})
if (!response.ok) return
const tokenData: CopilotTokenResponse = await response.json()
// Store the Copilot API token
await Auth.set("github-copilot", {
type: "oauth",
refresh: info.refresh,
access: tokenData.token,
expires: tokenData.expires_at * 1000,
})
return tokenData.token
}
export const DeviceCodeError = NamedError.create("DeviceCodeError", z.object({}))
export const TokenExchangeError = NamedError.create(
"TokenExchangeError",
z.object({
message: z.string(),
}),
)
export const AuthenticationError = NamedError.create(
"AuthenticationError",
z.object({
message: z.string(),
}),
)
export const CopilotTokenError = NamedError.create(
"CopilotTokenError",
z.object({
message: z.string(),
}),
)
}
+51
View File
@@ -0,0 +1,51 @@
import path from "path"
import { Global } from "../global"
import fs from "fs/promises"
import { z } from "zod"
export namespace Auth {
export const Oauth = z.object({
type: z.literal("oauth"),
refresh: z.string(),
access: z.string(),
expires: z.number(),
})
export const Api = z.object({
type: z.literal("api"),
key: z.string(),
})
export const Info = z.discriminatedUnion("type", [Oauth, Api])
export type Info = z.infer<typeof Info>
const filepath = path.join(Global.Path.data, "auth.json")
export async function get(providerID: string) {
const file = Bun.file(filepath)
return file
.json()
.catch(() => ({}))
.then((x) => x[providerID] as Info | undefined)
}
export async function all(): Promise<Record<string, Info>> {
const file = Bun.file(filepath)
return file.json().catch(() => ({}))
}
export async function set(key: string, info: Info) {
const file = Bun.file(filepath)
const data = await all()
await Bun.write(file, JSON.stringify({ ...data, [key]: info }, null, 2))
await fs.chmod(file.name!, 0o600)
}
export async function remove(key: string) {
const file = Bun.file(filepath)
const data = await all()
delete data[key]
await Bun.write(file, JSON.stringify(data, null, 2))
await fs.chmod(file.name!, 0o600)
}
}
+76 -16
View File
@@ -1,32 +1,92 @@
import path from "path"
import { z } from "zod"
import { Global } from "../global"
import { Log } from "../util/log"
import path from "path"
import { NamedError } from "../util/error"
import { readableStreamToText } from "bun"
export namespace BunProc {
const log = Log.create({ service: "bun" })
export function run(
cmd: string[],
options?: Bun.SpawnOptions.OptionsObject<any, any, any>,
) {
const root =
process.argv0 !== "bun"
? path.resolve(process.cwd(), process.argv0)
: process.argv0
export async function run(cmd: string[], options?: Bun.SpawnOptions.OptionsObject<any, any, any>) {
log.info("running", {
cmd: [root, ...cmd],
options,
})
const result = Bun.spawnSync([root, ...cmd], {
cmd: [which(), ...cmd],
...options,
argv0: "bun",
})
const result = Bun.spawn([which(), ...cmd], {
...options,
stdout: "pipe",
stderr: "pipe",
env: {
...process.env,
...options?.env,
BUN_BE_BUN: "1",
},
})
if (result.exitCode !== 0) {
console.error(result.stderr?.toString("utf8") ?? "")
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,
stderr,
})
if (code !== 0) {
throw new Error(`Command failed with exit code ${result.exitCode}`)
}
return result
}
export function which() {
return process.execPath
}
export const InstallFailedError = NamedError.create(
"BunInstallFailedError",
z.object({
pkg: z.string(),
version: z.string(),
}),
)
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(async () => {
const result = { dependencies: {} }
await Bun.write(pkgjson.name!, JSON.stringify(result, null, 2))
return result
})
if (parsed.dependencies[pkg] === version) return mod
// 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
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(
{ pkg, version },
{
cause: e,
},
)
})
parsed.dependencies[pkg] = version
await Bun.write(pkgjson.name!, JSON.stringify(parsed, null, 2))
return mod
}
}
+16 -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,
@@ -49,7 +46,7 @@ export namespace Bus {
)
}
export function publish<Definition extends EventDefinition>(
export async function publish<Definition extends EventDefinition>(
def: Definition,
properties: z.output<Definition["properties"]>,
) {
@@ -60,22 +57,33 @@ export namespace Bus {
log.info("publishing", {
type: def.type,
})
const pending = []
for (const key of [def.type, "*"]) {
const match = state().subscriptions.get(key)
for (const sub of match ?? []) {
sub(payload)
pending.push(sub(payload))
}
}
return Promise.all(pending)
}
export function subscribe<Definition extends EventDefinition>(
def: Definition,
callback: (event: { type: Definition["type"]; properties: z.infer<Definition["properties"]> }) => void,
) {
return raw(def.type, callback)
}
export function once<Definition extends EventDefinition>(
def: Definition,
callback: (event: {
type: Definition["type"]
properties: z.infer<Definition["properties"]>
}) => void,
}) => "done" | undefined,
) {
return raw(def.type, callback)
const unsub = subscribe(def, (event) => {
if (callback(event)) unsub()
})
}
export function subscribeAll(callback: (event: any) => void) {
+16
View File
@@ -0,0 +1,16 @@
import { App } from "../app/app"
import { ConfigHooks } from "../config/hooks"
import { Format } from "../format"
import { LSP } from "../lsp"
import { Share } from "../share/share"
export async function bootstrap<T>(input: App.Input, cb: (app: App.Info) => Promise<T>) {
return App.provide(input, async (app) => {
Share.init()
Format.init()
ConfigHooks.init()
LSP.init()
return cb(app)
})
}
+311
View File
@@ -0,0 +1,311 @@
import { AuthAnthropic } from "../../auth/anthropic"
import { AuthCopilot } from "../../auth/copilot"
import { Auth } from "../../auth"
import { cmd } from "./cmd"
import * as prompts from "@clack/prompts"
import open from "open"
import { UI } from "../ui"
import { ModelsDev } from "../../provider/models"
import { map, pipe, sortBy, values } from "remeda"
import path from "path"
import os from "os"
import { Global } from "../../global"
export const AuthCommand = cmd({
command: "auth",
describe: "manage credentials",
builder: (yargs) =>
yargs.command(AuthLoginCommand).command(AuthLogoutCommand).command(AuthListCommand).demandCommand(),
async handler() {},
})
export const AuthListCommand = cmd({
command: "list",
aliases: ["ls"],
describe: "list providers",
async handler() {
UI.empty()
const authPath = path.join(Global.Path.data, "auth.json")
const homedir = os.homedir()
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()
for (const [providerID, result] of results) {
const name = database[providerID]?.name || providerID
prompts.log.info(`${name} ${UI.Style.TEXT_DIM}${result.type}`)
}
prompts.outro(`${results.length} credentials`)
// Environment variables section
const activeEnvVars: Array<{ provider: string; envVar: string }> = []
for (const [providerID, provider] of Object.entries(database)) {
for (const envVar of provider.env) {
if (process.env[envVar]) {
activeEnvVars.push({
provider: provider.name || providerID,
envVar,
})
}
}
}
if (activeEnvVars.length > 0) {
UI.empty()
prompts.intro("Environment")
for (const { provider, envVar } of activeEnvVars) {
prompts.log.info(`${provider} ${UI.Style.TEXT_DIM}${envVar}`)
}
prompts.outro(`${activeEnvVars.length} environment variables`)
}
},
})
export const AuthLoginCommand = cmd({
command: "login",
describe: "log in to a provider",
async handler() {
UI.empty()
prompts.intro("Add credential")
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({
message: "Select provider",
maxItems: 8,
options: [
...pipe(
providers,
values(),
sortBy(
(x) => priority[x.id] ?? 99,
(x) => x.name ?? x.id,
),
map((x) => ({
label: x.name,
value: x.id,
hint: priority[x.id] === 0 ? "recommended" : undefined,
})),
),
{
value: "other",
label: "Other",
},
],
})
if (prompts.isCancel(provider)) throw new UI.CancelledError()
if (provider === "other") {
provider = await prompts.text({
message: "Enter provider id",
validate: (x) => (x.match(/^[0-9a-z-]+$/) ? undefined : "a-z, 0-9 and hyphens only"),
})
if (prompts.isCancel(provider)) throw new UI.CancelledError()
provider = provider.replace(/^@ai-sdk\//, "")
if (prompts.isCancel(provider)) throw new UI.CancelledError()
prompts.log.warn(
`This only stores a credential for ${provider} - you will need configure it in opencode.json, check the docs for examples.`,
)
}
if (provider === "amazon-bedrock") {
prompts.log.info(
"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
}
if (provider === "anthropic") {
const method = await prompts.select({
message: "Login method",
options: [
{
label: "Claude Pro/Max",
value: "max",
},
{
label: "Create API Key",
value: "console",
},
{
label: "Manually enter API Key",
value: "api",
},
],
})
if (prompts.isCancel(method)) throw new UI.CancelledError()
if (method === "max") {
// some weird bug where program exits without this
await new Promise((resolve) => setTimeout(resolve, 10))
const { url, verifier } = await AuthAnthropic.authorize("max")
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.length > 0 ? undefined : "Required"),
})
if (prompts.isCancel(code)) throw new UI.CancelledError()
try {
const credentials = await AuthAnthropic.exchange(code, verifier)
await Auth.set("anthropic", {
type: "oauth",
refresh: credentials.refresh,
access: credentials.access,
expires: credentials.expires,
})
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.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
}
}
const copilot = await AuthCopilot()
if (provider === "github-copilot" && copilot) {
await new Promise((resolve) => setTimeout(resolve, 10))
const deviceInfo = await copilot.authorize()
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))
const response = await copilot.poll(deviceInfo.device)
if (response.status === "pending") continue
if (response.status === "success") {
await Auth.set("github-copilot", {
type: "oauth",
refresh: response.refresh,
access: response.access,
expires: response.expires,
})
spinner.stop("Login successful")
break
}
if (response.status === "failed") {
spinner.stop("Failed to authorize", 1)
break
}
}
prompts.outro("Done")
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"),
})
if (prompts.isCancel(key)) throw new UI.CancelledError()
await Auth.set(provider, {
type: "api",
key,
})
prompts.outro("Done")
},
})
export const AuthLogoutCommand = cmd({
command: "logout",
describe: "log out from a configured provider",
async handler() {
UI.empty()
const credentials = await Auth.all().then((x) => Object.entries(x))
prompts.intro("Remove credential")
if (credentials.length === 0) {
prompts.log.error("No credentials found")
return
}
const database = await ModelsDev.get()
const providerID = await prompts.select({
message: "Select provider",
options: credentials.map(([key, value]) => ({
label: (database[key]?.name || key) + UI.Style.TEXT_DIM + " (" + value.type + ")",
value: key,
})),
})
if (prompts.isCancel(providerID)) throw new UI.CancelledError()
await Auth.remove(providerID)
prompts.outro("Logout successful")
},
})
+5
View File
@@ -0,0 +1,5 @@
import type { CommandModule } from "yargs"
export function cmd<T, U>(input: CommandModule<T, U>) {
return input
}
@@ -0,0 +1,36 @@
import { File } from "../../../file"
import { bootstrap } from "../../bootstrap"
import { cmd } from "../cmd"
const FileReadCommand = cmd({
command: "read <path>",
builder: (yargs) =>
yargs.positional("path", {
type: "string",
demandOption: true,
description: "File path to read",
}),
async handler(args) {
await bootstrap({ cwd: process.cwd() }, async () => {
const content = await File.read(args.path)
console.log(content)
})
},
})
const FileStatusCommand = cmd({
command: "status",
builder: (yargs) => yargs,
async handler() {
await bootstrap({ cwd: process.cwd() }, async () => {
const status = await File.status()
console.log(JSON.stringify(status, null, 2))
})
},
})
export const FileCommand = cmd({
command: "file",
builder: (yargs) => yargs.command(FileReadCommand).command(FileStatusCommand).demandCommand(),
async handler() {},
})
@@ -0,0 +1,28 @@
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({
command: "debug",
builder: (yargs) =>
yargs
.command(LSPCommand)
.command(RipgrepCommand)
.command(FileCommand)
.command(ScrapCommand)
.command(SnapshotCommand)
.command({
command: "wait",
async handler() {
await bootstrap({ cwd: process.cwd() }, async () => {
await new Promise((resolve) => setTimeout(resolve, 1_000 * 60 * 60 * 24))
})
},
})
.demandCommand(),
async handler() {},
})
@@ -0,0 +1,46 @@
import { LSP } from "../../../lsp"
import { bootstrap } from "../../bootstrap"
import { cmd } from "../cmd"
import { Log } from "../../../util/log"
export const LSPCommand = cmd({
command: "lsp",
builder: (yargs) =>
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 }),
async handler(args) {
await bootstrap({ cwd: process.cwd() }, async () => {
await LSP.touchFile(args.file, true)
console.log(await LSP.diagnostics())
})
},
})
export const SymbolsCommand = cmd({
command: "symbols <query>",
builder: (yargs) => yargs.positional("query", { type: "string", demandOption: true }),
async handler(args) {
await bootstrap({ cwd: process.cwd() }, async () => {
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))
})
},
})
@@ -0,0 +1,82 @@
import { App } from "../../../app/app"
import { Ripgrep } from "../../../file/ripgrep"
import { bootstrap } from "../../bootstrap"
import { cmd } from "../cmd"
export const RipgrepCommand = cmd({
command: "rg",
builder: (yargs) => yargs.command(TreeCommand).command(FilesCommand).command(SearchCommand).demandCommand(),
async handler() {},
})
const TreeCommand = cmd({
command: "tree",
builder: (yargs) =>
yargs.option("limit", {
type: "number",
}),
async handler(args) {
await bootstrap({ cwd: process.cwd() }, async () => {
const app = App.info()
console.log(await Ripgrep.tree({ cwd: app.path.cwd, limit: args.limit }))
})
},
})
const FilesCommand = cmd({
command: "files",
builder: (yargs) =>
yargs
.option("query", {
type: "string",
description: "Filter files by query",
})
.option("glob", {
type: "string",
description: "Glob pattern to match files",
})
.option("limit", {
type: "number",
description: "Limit number of results",
}),
async handler(args) {
await bootstrap({ cwd: process.cwd() }, async () => {
const app = App.info()
const files = await Ripgrep.files({
cwd: app.path.cwd,
query: args.query,
glob: args.glob ? [args.glob] : undefined,
limit: args.limit,
})
console.log(files.join("\n"))
})
},
})
const SearchCommand = cmd({
command: "search <pattern>",
builder: (yargs) =>
yargs
.positional("pattern", {
type: "string",
demandOption: true,
description: "Search pattern",
})
.option("glob", {
type: "array",
description: "File glob patterns",
})
.option("limit", {
type: "number",
description: "Limit number of results",
}),
async handler(args) {
const results = await Ripgrep.search({
cwd: process.cwd(),
pattern: args.pattern,
glob: args.glob as string[] | undefined,
limit: args.limit,
})
console.log(JSON.stringify(results, null, 2))
},
})
@@ -0,0 +1,7 @@
import { cmd } from "../cmd"
export const ScrapCommand = cmd({
command: "scrap",
builder: (yargs) => yargs,
async handler() {},
})
@@ -0,0 +1,52 @@
import { Snapshot } from "../../../snapshot"
import { bootstrap } from "../../bootstrap"
import { cmd } from "../cmd"
export const SnapshotCommand = cmd({
command: "snapshot",
builder: (yargs) => yargs.command(CreateCommand).command(RestoreCommand).command(DiffCommand).demandCommand(),
async handler() {},
})
const CreateCommand = cmd({
command: "create",
async handler() {
await bootstrap({ cwd: process.cwd() }, async () => {
const result = await Snapshot.create("test")
console.log(result)
})
},
})
const RestoreCommand = cmd({
command: "restore <commit>",
builder: (yargs) =>
yargs.positional("commit", {
type: "string",
description: "commit",
demandOption: true,
}),
async handler(args) {
await bootstrap({ cwd: process.cwd() }, async () => {
await Snapshot.restore("test", args.commit)
console.log("restored")
})
},
})
export const DiffCommand = cmd({
command: "diff <commit>",
describe: "diff",
builder: (yargs) =>
yargs.positional("commit", {
type: "string",
description: "commit",
demandOption: true,
}),
async handler(args) {
await bootstrap({ cwd: process.cwd() }, async () => {
const diff = await Snapshot.diff("test", args.commit)
console.log(diff)
})
},
})
+15
View File
@@ -0,0 +1,15 @@
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))
},
} satisfies CommandModule
@@ -0,0 +1,221 @@
import { $ } from "bun"
import path from "path"
import { exec } from "child_process"
import * as prompts from "@clack/prompts"
import { map, pipe, sortBy, values } from "remeda"
import { UI } from "../ui"
import { cmd } from "./cmd"
import { ModelsDev } from "../../provider/models"
import { App } from "../../app/app"
const WORKFLOW_FILE = ".github/workflows/opencode.yml"
export const InstallGithubCommand = cmd({
command: "install-github",
describe: "install the GitHub agent",
async handler() {
await App.provide({ cwd: process.cwd() }, async () => {
UI.empty()
prompts.intro("Install GitHub agent")
const app = await getAppInfo()
await installGitHubApp()
const providers = await ModelsDev.get()
const provider = await promptProvider()
const model = await promptModel()
//const key = await promptKey()
await addWorkflowFiles()
printNextSteps()
function printNextSteps() {
let step2
if (provider === "amazon-bedrock") {
step2 =
"Configure OIDC in AWS - https://docs.github.com/en/actions/how-tos/security-for-github-actions/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services"
} else {
const url = `https://github.com/organizations/${app.owner}/settings/secrets/actions`
const env = providers[provider].env
const envStr =
env.length === 1
? `\`${env[0]}\` secret`
: `\`${[env.slice(0, -1).join("\`, \`"), ...env.slice(-1)].join("\` and \`")}\` secrets`
step2 = `Add ${envStr} for ${providers[provider].name} - ${url}`
}
prompts.outro(
[
"Next steps:",
` 1. Commit "${WORKFLOW_FILE}" file and push`,
` 2. ${step2}`,
" 3. Learn how to use the GitHub agent - https://docs.opencode.ai/docs/github/getting-started",
].join("\n"),
)
}
async function getAppInfo() {
const app = App.info()
if (!app.git) {
prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
throw new UI.CancelledError()
}
// Get repo info
const info = await $`git remote get-url origin`.quiet().nothrow().text()
// match https or git pattern
// ie. https://github.com/sst/opencode.git
// ie. git@github.com:sst/opencode.git
const parsed = info.match(/git@github\.com:(.*)\.git/) ?? info.match(/github\.com\/(.*)\.git/)
if (!parsed) {
prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
throw new UI.CancelledError()
}
const [owner, repo] = parsed[1].split("/")
return { owner, repo, root: app.path.root }
}
async function promptProvider() {
const priority: Record<string, number> = {
anthropic: 0,
"github-copilot": 1,
openai: 2,
google: 3,
}
let provider = await prompts.select({
message: "Select provider",
maxItems: 8,
options: pipe(
providers,
values(),
sortBy(
(x) => priority[x.id] ?? 99,
(x) => x.name ?? x.id,
),
map((x) => ({
label: x.name,
value: x.id,
hint: priority[x.id] === 0 ? "recommended" : undefined,
})),
),
})
if (prompts.isCancel(provider)) throw new UI.CancelledError()
return provider
}
async function promptModel() {
const providerData = providers[provider]!
const model = await prompts.select({
message: "Select model",
maxItems: 8,
options: pipe(
providerData.models,
values(),
sortBy((x) => x.name ?? x.id),
map((x) => ({
label: x.name ?? x.id,
value: x.id,
})),
),
})
if (prompts.isCancel(model)) throw new UI.CancelledError()
return model
}
async function installGitHubApp() {
const s = prompts.spinner()
s.start("Installing GitHub app")
// Get installation
const installation = await getInstallation()
if (installation) return s.stop("GitHub app already installed")
// Open browser
const url = "https://github.com/apps/opencode-agent"
const command =
process.platform === "darwin"
? `open "${url}"`
: process.platform === "win32"
? `start "${url}"`
: `xdg-open "${url}"`
exec(command, (error) => {
if (error) {
prompts.log.warn(`Could not open browser. Please visit: ${url}`)
}
})
// Wait for installation
s.message("Waiting for GitHub app to be installed")
const MAX_RETRIES = 60
let retries = 0
do {
const installation = await getInstallation()
if (installation) break
if (retries > MAX_RETRIES) {
s.stop(
`Failed to detect GitHub app installation. Make sure to install the app for the \`${app.owner}/${app.repo}\` repository.`,
)
throw new UI.CancelledError()
}
retries++
await new Promise((resolve) => setTimeout(resolve, 1000))
} while (true)
s.stop("Installed GitHub app")
async function getInstallation() {
return await fetch(`https://api.opencode.ai/get_github_app_installation?owner=${app.owner}&repo=${app.repo}`)
.then((res) => res.json())
.then((data) => data.installation)
}
}
async function addWorkflowFiles() {
const envStr =
provider === "amazon-bedrock"
? ""
: `\n env:${providers[provider].env.map((e) => `\n ${e}: \${{ secrets.${e} }}`).join("")}`
await Bun.write(
path.join(app.root, WORKFLOW_FILE),
`
name: opencode
on:
issue_comment:
types: [created]
jobs:
opencode:
if: |
startsWith(github.event.comment.body, 'opencode') ||
startsWith(github.event.comment.body, 'hi opencode') ||
startsWith(github.event.comment.body, 'hey opencode') ||
contains(github.event.comment.body, '@opencode-agent')
runs-on: ubuntu-latest
permissions:
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run opencode
uses: sst/opencode/sdks/github@github-v1${envStr}
with:
model: ${provider}/${model}
`.trim(),
)
prompts.log.success(`Added workflow file: "${WORKFLOW_FILE}"`)
}
})
},
})
+79
View File
@@ -0,0 +1,79 @@
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.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.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.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")
},
})
+19
View File
@@ -0,0 +1,19 @@
import { App } from "../../app/app"
import { Provider } from "../../provider/provider"
import { cmd } from "./cmd"
export const ModelsCommand = cmd({
command: "models",
describe: "list all available models",
handler: async () => {
await App.provide({ cwd: process.cwd() }, async () => {
const providers = await Provider.list()
for (const [providerID, provider] of Object.entries(providers)) {
for (const modelID of Object.keys(provider.info.models)) {
console.log(`${providerID}/${modelID}`)
}
}
})
},
})
+190
View File
@@ -0,0 +1,190 @@
import type { Argv } from "yargs"
import { Bus } from "../../bus"
import { Provider } from "../../provider/provider"
import { Session } from "../../session"
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 { Mode } from "../../session/mode"
import { Identifier } from "../../id/id"
const TOOL: Record<string, [string, string]> = {
todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
todoread: ["Todo", UI.Style.TEXT_WARNING_BOLD],
bash: ["Bash", UI.Style.TEXT_DANGER_BOLD],
edit: ["Edit", UI.Style.TEXT_SUCCESS_BOLD],
glob: ["Glob", UI.Style.TEXT_INFO_BOLD],
grep: ["Grep", UI.Style.TEXT_INFO_BOLD],
list: ["List", UI.Style.TEXT_INFO_BOLD],
read: ["Read", UI.Style.TEXT_HIGHLIGHT_BOLD],
write: ["Write", UI.Style.TEXT_SUCCESS_BOLD],
websearch: ["Search", UI.Style.TEXT_DIM_BOLD],
}
export const RunCommand = cmd({
command: "run [message..]",
describe: "run opencode with a message",
builder: (yargs: Argv) => {
return yargs
.positional("message", {
describe: "message to send",
type: "string",
array: true,
default: [],
})
.option("continue", {
alias: ["c"],
describe: "continue the last session",
type: "boolean",
})
.option("session", {
alias: ["s"],
describe: "session id to continue",
type: "string",
})
.option("share", {
type: "boolean",
describe: "share the session",
})
.option("model", {
type: "string",
alias: ["m"],
describe: "model to use in the format of provider/model",
})
.option("mode", {
type: "string",
describe: "mode to use",
})
},
handler: async (args) => {
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 list = Session.list()
const first = await list.next()
await list.return()
if (first.done) return
return first.value
}
if (args.session) return Session.get(args.session)
return Session.create()
})()
if (!session) {
UI.error("Session not found")
return
}
UI.empty()
UI.println(UI.logo())
UI.empty()
const cfg = await Config.get()
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}`)
UI.empty()
function printEvent(color: string, type: string, title: string) {
UI.println(
color + `|`,
UI.Style.TEXT_NORMAL + UI.Style.TEXT_DIM + ` ${type.padEnd(7, " ")}`,
"",
UI.Style.TEXT_NORMAL + title,
)
}
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
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") {
text = part.text
if (part.time?.end) {
UI.empty()
UI.println(UI.markdown(text))
UI.empty()
text = ""
return
}
}
})
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 mode = args.mode ? await Mode.get(args.mode) : await Mode.list().then((x) => x[0])
const messageID = Identifier.ascending("message")
const result = await Session.chat({
sessionID: session.id,
messageID,
...(mode.model
? mode.model
: {
providerID,
modelID,
}),
mode: mode.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(UI.markdown(match.text))
if (errorMsg) process.stdout.write(errorMsg)
}
UI.empty()
})
},
})
+48
View File
@@ -0,0 +1,48 @@
import { Provider } from "../../provider/provider"
import { Server } from "../../server/server"
import { Share } from "../../share/share"
import { bootstrap } from "../bootstrap"
import { cmd } from "./cmd"
export const ServeCommand = cmd({
command: "serve",
builder: (yargs) =>
yargs
.option("port", {
alias: ["p"],
type: "number",
describe: "port to listen on",
default: 4096,
})
.option("hostname", {
alias: ["h"],
type: "string",
describe: "hostname to listen on",
default: "127.0.0.1",
}),
describe: "starts a headless opencode server",
handler: async (args) => {
const cwd = process.cwd()
await bootstrap({ cwd }, async () => {
const providers = await Provider.list()
if (Object.keys(providers).length === 0) {
return "needs_provider"
}
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}`)
await new Promise(() => {})
server.stop()
})
},
})
+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()
}
+163
View File
@@ -0,0 +1,163 @@
import { Global } from "../../global"
import { Provider } from "../../provider/provider"
import { Server } from "../../server/server"
import { bootstrap } from "../bootstrap"
import { UI } from "../ui"
import { cmd } from "./cmd"
import path from "path"
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 { Mode } from "../../session/mode"
export const TuiCommand = cmd({
command: "$0 [project]",
describe: "start opencode tui",
builder: (yargs) =>
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",
}),
handler: async (args) => {
while (true) {
const cwd = args.project ? path.resolve(args.project) : process.cwd()
try {
process.chdir(cwd)
} catch (e) {
UI.error("Failed to change directory to " + cwd)
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",
})
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
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 fs.chmod(binary, 0o755)
}
cwd = process.cwd()
cmd = [binary]
}
Log.Default.info("tui", {
cmd,
})
const proc = Bun.spawn({
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_MODES: JSON.stringify(await Mode.list()),
},
onExit: () => {
server.stop()
},
})
;(async () => {
if (Installation.VERSION === "dev") return
if (Installation.isSnapshot()) return
const config = await Config.global()
if (config.autoupdate === false) return
const latest = await Installation.latest().catch(() => {})
if (!latest) return
if (Installation.VERSION === latest) return
const method = await Installation.method()
if (method === "unknown") return
await Installation.upgrade(method, latest)
.then(() => {
Bus.publish(Installation.Event.Updated, { version: latest })
})
.catch(() => {})
})()
await proc.exited
server.stop()
return "done"
})
if (result === "done") break
if (result === "needs_provider") {
UI.empty()
UI.println(UI.logo(" "))
const result = await Bun.spawn({
cmd: [...getOpencodeCommand(), "auth", "login"],
cwd: process.cwd(),
stdout: "inherit",
stderr: "inherit",
stdin: "inherit",
}).exited
if (result !== 0) return
UI.empty()
}
}
},
})
/**
* Get the correct command to run opencode CLI
* In development: ["bun", "run", "packages/opencode/src/index.ts"]
* In production: ["/path/to/opencode"]
*/
function getOpencodeCommand(): string[] {
// Check if OPENCODE_BIN_PATH is set (used by shell wrapper scripts)
if (process.env["OPENCODE_BIN_PATH"]) {
return [process.env["OPENCODE_BIN_PATH"]]
}
const execPath = process.execPath.toLowerCase()
if (Installation.isDev()) {
// In development, use bun to run the TypeScript entry point
return [execPath, "run", process.argv[1]]
}
// In production, use the current executable path
return [process.execPath]
}
+57
View File
@@ -0,0 +1,57 @@
import type { Argv } from "yargs"
import { UI } from "../ui"
import * as prompts from "@clack/prompts"
import { Installation } from "../../installation"
export const UpgradeCommand = {
command: "upgrade [target]",
describe: "upgrade opencode to the latest or a specific version",
builder: (yargs: Argv) => {
return yargs
.positional("target", {
describe: "version to upgrade to, for ex '0.1.48' or 'v0.1.48'",
type: "string",
})
.option("method", {
alias: "m",
describe: "installation method to use",
type: "string",
choices: ["curl", "npm", "pnpm", "bun", "brew"],
})
},
handler: async (args: { target?: string; method?: string }) => {
UI.empty()
UI.println(UI.logo(" "))
UI.empty()
prompts.intro("Upgrade")
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.outro("Done")
return
}
prompts.log.info("Using method: " + method)
const target = args.target ?? (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)
else if (err instanceof Error) prompts.log.error(err.message)
prompts.outro("Done")
return
}
spinner.stop("Upgrade complete")
prompts.outro("Done")
},
}
+16
View File
@@ -0,0 +1,16 @@
import { Config } from "../config/config"
import { MCP } from "../mcp"
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.InvalidError.isInstance(input))
return [
`Config file at ${input.data.path} is invalid`,
...(input.data.issues?.map((issue) => "↳ " + issue.message + " " + issue.path.join(".")) ?? []),
].join("\n")
if (UI.CancelledError.isInstance(input)) return ""
}
+83
View File
@@ -0,0 +1,83 @@
import { z } from "zod"
import { EOL } from "os"
import { NamedError } from "../util/error"
export namespace UI {
const LOGO = [
[`█▀▀█ █▀▀█ █▀▀ █▀▀▄ `, `█▀▀ █▀▀█ █▀▀▄ █▀▀`],
[`█░░█ █░░█ █▀▀ █░░█ `, `█░░ █░░█ █░░█ █▀▀`],
[`▀▀▀▀ █▀▀▀ ▀▀▀ ▀ ▀ `, `▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀▀`],
]
export const CancelledError = NamedError.create("UICancelledError", z.void())
export const Style = {
TEXT_HIGHLIGHT: "\x1b[96m",
TEXT_HIGHLIGHT_BOLD: "\x1b[96m\x1b[1m",
TEXT_DIM: "\x1b[90m",
TEXT_DIM_BOLD: "\x1b[90m\x1b[1m",
TEXT_NORMAL: "\x1b[0m",
TEXT_NORMAL_BOLD: "\x1b[1m",
TEXT_WARNING: "\x1b[93m",
TEXT_WARNING_BOLD: "\x1b[93m\x1b[1m",
TEXT_DANGER: "\x1b[91m",
TEXT_DANGER_BOLD: "\x1b[91m\x1b[1m",
TEXT_SUCCESS: "\x1b[92m",
TEXT_SUCCESS_BOLD: "\x1b[92m\x1b[1m",
TEXT_INFO: "\x1b[94m",
TEXT_INFO_BOLD: "\x1b[94m\x1b[1m",
}
export function println(...message: string[]) {
print(...message)
Bun.stderr.write(EOL)
}
export function print(...message: string[]) {
blank = false
Bun.stderr.write(message.join(" "))
}
let blank = false
export function empty() {
if (blank) return
println("" + Style.TEXT_NORMAL)
blank = true
}
export function logo(pad?: string) {
const result = []
for (const row of LOGO) {
if (pad) result.push(pad)
result.push(Bun.color("gray", "ansi"))
result.push(row[0])
result.push("\x1b[0m")
result.push(row[1])
result.push(EOL)
}
return result.join("").trimEnd()
}
export async function input(prompt: string): Promise<string> {
const readline = require("readline")
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
})
return new Promise((resolve) => {
rl.question(prompt, (answer: string) => {
rl.close()
resolve(answer.trim())
})
})
}
export function error(message: string) {
println(Style.TEXT_DANGER_BOLD + "Error: " + Style.TEXT_NORMAL + message)
}
export function markdown(text: string): string {
return text
}
}
+275 -31
View File
@@ -1,56 +1,300 @@
import path from "path"
import { Log } from "../util/log"
import path from "path"
import { z } from "zod"
import { App } from "../app/app"
import { Provider } from "../provider/provider"
import { Filesystem } from "../util/filesystem"
import { ModelsDev } from "../provider/models"
import { mergeDeep, pipe } from "remeda"
import { Global } from "../global"
import fs from "fs/promises"
import { lazy } from "../util/lazy"
import { NamedError } from "../util/error"
export namespace Config {
const log = Log.create({ service: "config" })
export const state = App.state("config", async (app) => {
const result = await load(app.root)
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))
}
}
// Handle migration from autoshare to share field
if (result.autoshare === true && !result.share) {
result.share = "auto"
}
if (!result.username) {
const os = await import("os")
result.username = os.userInfo().username
}
log.info("loaded", result)
return result
})
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"),
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"),
})
.strict()
.openapi({
ref: "McpLocalConfig",
})
export const McpRemote = z
.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"),
headers: z.record(z.string(), z.string()).optional().describe("Headers to send with the request"),
})
.strict()
.openapi({
ref: "McpRemoteConfig",
})
export const Mcp = z.discriminatedUnion("type", [McpLocal, McpRemote])
export type Mcp = z.infer<typeof Mcp>
export const Mode = z
.object({
model: z.string().optional(),
prompt: z.string().optional(),
tools: z.record(z.string(), z.boolean()).optional(),
})
.openapi({
ref: "ModeConfig",
})
export type Mode = z.infer<typeof Mode>
export const Keybinds = z
.object({
leader: z.string().optional().default("ctrl+x").describe("Leader key for keybind combinations"),
app_help: z.string().optional().default("<leader>h").describe("Show help dialog"),
switch_mode: z.string().optional().default("tab").describe("Next mode"),
switch_mode_reverse: z.string().optional().default("shift+tab").describe("Previous Mode"),
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("<leader>u").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().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("<leader>r").describe("Revert 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 Info = z
.object({
provider: z.lazy(() => Provider.Info.array().optional()),
tool: z
$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"),
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("@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(
"Small model to use for tasks like summarization and 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({
provider: z.record(z.string(), z.string().array()).optional(),
build: Mode.optional(),
plan: Mode.optional(),
})
.catchall(Mode)
.optional()
.describe("Modes configuration, see https://opencode.ai/docs/modes"),
provider: z
.record(
ModelsDev.Provider.partial().extend({
models: z.record(ModelsDev.Model.partial()),
options: z.record(z.any()).optional(),
}),
)
.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"),
layout: Layout.optional().describe("@deprecated Always uses stretch layout."),
experimental: z
.object({
hook: z
.object({
file_edited: z
.record(
z.string(),
z
.object({
command: z.string().array(),
environment: z.record(z.string(), z.string()).optional(),
})
.array(),
)
.optional(),
session_completed: z
.object({
command: z.string().array(),
environment: z.record(z.string(), z.string()).optional(),
})
.array()
.optional(),
})
.optional(),
})
.optional(),
})
.strict()
.openapi({
ref: "Config",
})
export type Info = z.output<typeof Info>
export const global = lazy(async () => {
let result = pipe(
{},
mergeDeep(await load(path.join(Global.Path.config, "config.json"))),
mergeDeep(await load(path.join(Global.Path.config, "opencode.json"))),
)
await import(path.join(Global.Path.config, "config"), {
with: {
type: "toml",
},
})
.then(async (mod) => {
const { provider, model, ...rest } = mod.default
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 fs.unlink(path.join(Global.Path.config, "config"))
})
.catch(() => {})
return result
})
async function load(configPath: string) {
let text = await Bun.file(configPath)
.text()
.catch((err) => {
if (err.code === "ENOENT") return
throw new JsonError({ path: configPath }, { cause: err })
})
if (!text) return {}
text = text.replace(/\{env:([^}]+)\}/g, (_, varName) => {
return process.env[varName] || ""
})
const fileMatches = text.match(/"?\{file:([^}]+)\}"?/g)
if (fileMatches) {
const configDir = path.dirname(configPath)
for (const match of fileMatches) {
const filePath = match.replace(/^"?\{file:/, "").replace(/\}"?$/, "")
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(configDir, filePath)
const fileContent = await Bun.file(resolvedPath).text()
text = text.replace(match, JSON.stringify(fileContent))
}
}
let data: any
try {
data = JSON.parse(text)
} catch (err) {
throw new JsonError({ path: configPath }, { cause: err as Error })
}
const parsed = Info.safeParse(data)
if (parsed.success) {
if (!parsed.data.$schema) {
parsed.data.$schema = "https://opencode.ai/config.json"
await Bun.write(configPath, JSON.stringify(parsed.data, null, 2))
}
return parsed.data
}
throw new InvalidError({ path: configPath, issues: parsed.error.issues })
}
export const JsonError = NamedError.create(
"ConfigJsonError",
z.object({
path: z.string(),
}),
)
export const InvalidError = NamedError.create(
"ConfigInvalidError",
z.object({
path: z.string(),
issues: z.custom<z.ZodIssue[]>().optional(),
}),
)
export function get() {
return state()
}
async function load(directory: string) {
let result: Info = {}
for (const file of ["opencode.jsonc", "opencode.json"]) {
const resolved = path.join(directory, file)
log.info("searching", { path: resolved })
try {
result = await import(path.join(directory, file)).then((mod) =>
Info.parse(mod.default),
)
log.info("found", { path: resolved })
break
} catch (e) {
if (e instanceof z.ZodError) {
for (const issue of e.issues) {
log.info(issue.message)
}
throw e
}
continue
}
}
log.info("loaded", result)
return result
}
}
+56
View File
@@ -0,0 +1,56 @@
import { App } from "../app/app"
import { Bus } from "../bus"
import { File } from "../file"
import { Session } from "../session"
import { Log } from "../util/log"
import { Config } from "./config"
import path from "path"
export namespace ConfigHooks {
const log = Log.create({ service: "config.hooks" })
export function init() {
log.info("init")
const app = App.info()
Bus.subscribe(File.Event.Edited, async (payload) => {
const cfg = await Config.get()
const ext = path.extname(payload.properties.file)
for (const item of cfg.experimental?.hook?.file_edited?.[ext] ?? []) {
log.info("file_edited", {
file: payload.properties.file,
command: item.command,
})
Bun.spawn({
cmd: item.command.map((x) => x.replace("$FILE", payload.properties.file)),
env: item.environment,
cwd: app.path.cwd,
stdout: "ignore",
stderr: "ignore",
})
}
})
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,
})
Bun.spawn({
cmd: item.command,
cwd: App.info().path.cwd,
env: item.environment,
stdout: "ignore",
stderr: "ignore",
})
}
}
})
}
}
+108
View File
@@ -0,0 +1,108 @@
import path from "path"
import { Global } from "../global"
import fs from "fs/promises"
import { z } from "zod"
import { NamedError } from "../util/error"
import { lazy } from "../util/lazy"
import { Log } from "../util/log"
export namespace Fzf {
const log = Log.create({ service: "fzf" })
const VERSION = "0.62.0"
const PLATFORM = {
darwin: { extension: "tar.gz" },
linux: { extension: "tar.gz" },
win32: { extension: "zip" },
} as const
export const ExtractionFailedError = NamedError.create(
"FzfExtractionFailedError",
z.object({
filepath: z.string(),
stderr: z.string(),
}),
)
export const UnsupportedPlatformError = NamedError.create(
"FzfUnsupportedPlatformError",
z.object({
platform: z.string(),
}),
)
export const DownloadFailedError = NamedError.create(
"FzfDownloadFailedError",
z.object({
url: z.string(),
status: z.number(),
}),
)
const state = lazy(async () => {
let filepath = Bun.which("fzf")
if (filepath) {
log.info("found", { filepath })
return { filepath }
}
filepath = path.join(Global.Path.bin, "fzf" + (process.platform === "win32" ? ".exe" : ""))
const file = Bun.file(filepath)
if (!(await file.exists())) {
const archMap = { x64: "amd64", arm64: "arm64" } as const
const arch = archMap[process.arch as keyof typeof archMap] ?? "amd64"
const config = PLATFORM[process.platform as keyof typeof PLATFORM]
if (!config) throw new UnsupportedPlatformError({ platform: process.platform })
const version = VERSION
const platformName = process.platform === "win32" ? "windows" : process.platform
const filename = `fzf-${version}-${platformName}_${arch}.${config.extension}`
const url = `https://github.com/junegunn/fzf/releases/download/v${version}/${filename}`
const response = await fetch(url)
if (!response.ok) throw new DownloadFailedError({ url, status: response.status })
const buffer = await response.arrayBuffer()
const archivePath = path.join(Global.Path.bin, filename)
await Bun.write(archivePath, buffer)
if (config.extension === "tar.gz") {
const proc = Bun.spawn(["tar", "-xzf", archivePath, "fzf"], {
cwd: Global.Path.bin,
stderr: "pipe",
stdout: "pipe",
})
await proc.exited
if (proc.exitCode !== 0)
throw new ExtractionFailedError({
filepath,
stderr: await Bun.readableStreamToText(proc.stderr),
})
}
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)
throw new ExtractionFailedError({
filepath: archivePath,
stderr: await Bun.readableStreamToText(proc.stderr),
})
}
await fs.unlink(archivePath)
if (process.platform !== "win32") await fs.chmod(filepath, 0o755)
}
return {
filepath,
}
})
export async function filepath() {
const { filepath } = await state()
return filepath
}
}
+123
View File
@@ -0,0 +1,123 @@
import { z } from "zod"
import { Bus } from "../bus"
import { $ } from "bun"
import { createPatch } from "diff"
import path from "path"
import * as git from "isomorphic-git"
import { App } from "../app/app"
import fs from "fs"
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",
z.object({
file: z.string(),
}),
),
}
export async function status() {
const app = App.info()
if (!app.git) return []
const diffOutput = await $`git diff --numstat HEAD`.cwd(app.path.cwd).quiet().nothrow().text()
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({
path: filepath,
added: added === "-" ? 0 : parseInt(added, 10),
removed: removed === "-" ? 0 : parseInt(removed, 10),
status: "modified",
})
}
}
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 lines = content.split("\n").length
changedFiles.push({
path: filepath,
added: lines,
removed: 0,
status: "added",
})
} catch {
continue
}
}
}
// Get deleted files
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({
path: filepath,
added: 0,
removed: 0, // Could get original line count but would require another git command
status: "deleted",
})
}
}
return changedFiles.map((x) => ({
...x,
path: path.relative(app.path.cwd, path.join(app.path.root, x.path)),
}))
}
export async function read(file: string) {
using _ = log.time("read", { file })
const app = App.info()
const full = path.join(app.path.cwd, file)
const content = await Bun.file(full)
.text()
.catch(() => "")
.then((x) => x.trim())
if (app.git) {
const rel = path.relative(app.path.root, full)
const diff = await git.status({
fs,
dir: app.path.root,
filepath: rel,
})
if (diff !== "unmodified") {
const original = await $`git show HEAD:${rel}`.cwd(app.path.root).quiet().nothrow().text()
const patch = createPatch(file, original, content, "old", "new", {
context: Infinity,
})
return { type: "patch", content: patch }
}
}
return { type: "raw", content }
}
}
+336
View File
@@ -0,0 +1,336 @@
// Ripgrep utility functions
import path from "path"
import { Global } from "../global"
import fs from "fs/promises"
import { z } from "zod"
import { NamedError } from "../util/error"
import { lazy } from "../util/lazy"
import { $ } from "bun"
import { Fzf } from "./fzf"
export namespace Ripgrep {
const Stats = z.object({
elapsed: z.object({
secs: z.number(),
nanos: z.number(),
human: z.string(),
}),
searches: z.number(),
searches_with_match: z.number(),
bytes_searched: z.number(),
bytes_printed: z.number(),
matched_lines: z.number(),
matches: z.number(),
})
const Begin = z.object({
type: z.literal("begin"),
data: z.object({
path: z.object({
text: z.string(),
}),
}),
})
export const Match = z.object({
type: z.literal("match"),
data: z
.object({
path: z.object({
text: z.string(),
}),
lines: z.object({
text: z.string(),
}),
line_number: z.number(),
absolute_offset: z.number(),
submatches: z.array(
z.object({
match: z.object({
text: z.string(),
}),
start: z.number(),
end: z.number(),
}),
),
})
.openapi({ ref: "Match" }),
})
const End = z.object({
type: z.literal("end"),
data: z.object({
path: z.object({
text: z.string(),
}),
binary_offset: z.number().nullable(),
stats: Stats,
}),
})
const Summary = z.object({
type: z.literal("summary"),
data: z.object({
elapsed_total: z.object({
human: z.string(),
nanos: z.number(),
secs: z.number(),
}),
stats: Stats,
}),
})
const Result = z.union([Begin, Match, End, Summary])
export type Result = z.infer<typeof Result>
export type Match = z.infer<typeof Match>
export type Begin = z.infer<typeof Begin>
export type End = z.infer<typeof End>
export type Summary = z.infer<typeof Summary>
const PLATFORM = {
"arm64-darwin": { platform: "aarch64-apple-darwin", extension: "tar.gz" },
"arm64-linux": {
platform: "aarch64-unknown-linux-gnu",
extension: "tar.gz",
},
"x64-darwin": { platform: "x86_64-apple-darwin", extension: "tar.gz" },
"x64-linux": { platform: "x86_64-unknown-linux-musl", extension: "tar.gz" },
"x64-win32": { platform: "x86_64-pc-windows-msvc", extension: "zip" },
} as const
export const ExtractionFailedError = NamedError.create(
"RipgrepExtractionFailedError",
z.object({
filepath: z.string(),
stderr: z.string(),
}),
)
export const UnsupportedPlatformError = NamedError.create(
"RipgrepUnsupportedPlatformError",
z.object({
platform: z.string(),
}),
)
export const DownloadFailedError = NamedError.create(
"RipgrepDownloadFailedError",
z.object({
url: z.string(),
status: z.number(),
}),
)
const state = lazy(async () => {
let filepath = Bun.which("rg")
if (filepath) return { filepath }
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 config = PLATFORM[platformKey]
if (!config) throw new UnsupportedPlatformError({ platform: platformKey })
const version = "14.1.1"
const filename = `ripgrep-${version}-${config.platform}.${config.extension}`
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 })
const buffer = await response.arrayBuffer()
const archivePath = path.join(Global.Path.bin, filename)
await Bun.write(archivePath, buffer)
if (config.extension === "tar.gz") {
const args = ["tar", "-xzf", archivePath, "--strip-components=1"]
if (platformKey.endsWith("-darwin")) args.push("--include=*/rg")
if (platformKey.endsWith("-linux")) args.push("--wildcards", "*/rg")
const proc = Bun.spawn(args, {
cwd: Global.Path.bin,
stderr: "pipe",
stdout: "pipe",
})
await proc.exited
if (proc.exitCode !== 0)
throw new ExtractionFailedError({
filepath,
stderr: await Bun.readableStreamToText(proc.stderr),
})
}
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),
})
}
await fs.unlink(archivePath)
if (!platformKey.endsWith("-win32")) await fs.chmod(filepath, 0o755)
}
return {
filepath,
}
})
export async function filepath() {
const { filepath } = await state()
return filepath
}
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()
return result.split("\n").filter(Boolean)
}
export async function tree(input: { cwd: string; limit?: number }) {
const files = await Ripgrep.files({ cwd: input.cwd })
interface Node {
path: string[]
children: Node[]
}
function getPath(node: Node, parts: string[], create: boolean) {
if (parts.length === 0) return node
let current = node
for (const part of parts) {
let existing = current.children.find((x) => x.path.at(-1) === part)
if (!existing) {
if (!create) return
existing = {
path: current.path.concat(part),
children: [],
}
current.children.push(existing)
}
current = existing
}
return current
}
const root: Node = {
path: [],
children: [],
}
for (const file of files) {
const parts = file.split(path.sep)
getPath(root, parts, true)
}
function sort(node: Node) {
node.children.sort((a, b) => {
if (!a.children.length && b.children.length) return 1
if (!b.children.length && a.children.length) return -1
return a.path.at(-1)!.localeCompare(b.path.at(-1)!)
})
for (const child of node.children) {
sort(child)
}
}
sort(root)
let current = [root]
const result: Node = {
path: [],
children: [],
}
let processed = 0
const limit = input.limit ?? 50
while (current.length > 0) {
const next = []
for (const node of current) {
if (node.children.length) next.push(...node.children)
}
const max = Math.max(...current.map((x) => x.children.length))
for (let i = 0; i < max && processed < limit; i++) {
for (const node of current) {
const child = node.children[i]
if (!child) continue
getPath(result, child.path, true)
processed++
if (processed >= limit) break
}
}
if (processed >= limit) {
for (const node of [...current, ...next]) {
const compare = getPath(result, node.path, false)
if (!compare) continue
if (compare?.children.length !== node.children.length) {
const diff = node.children.length - compare.children.length
compare.children.push({
path: compare.path.concat(`[${diff} truncated]`),
children: [],
})
}
}
break
}
current = next
}
const lines: string[] = []
function render(node: Node, depth: number) {
const indent = "\t".repeat(depth)
lines.push(indent + node.path.at(-1) + (node.children.length ? "/" : ""))
for (const child of node.children) {
render(child, depth + 1)
}
}
result.children.map((x) => render(x, 0))
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/*'"]
if (input.glob) {
for (const g of input.glob) {
args.push(`--glob=${g}`)
}
}
if (input.limit) {
args.push(`--max-count=${input.limit}`)
}
args.push(input.pattern)
const command = args.join(" ")
const result = await $`${{ raw: command }}`.cwd(input.cwd).quiet().nothrow()
if (result.exitCode !== 0) {
return []
}
const lines = result.text().trim().split("\n").filter(Boolean)
// Parse JSON lines from ripgrep output
return lines
.map((line) => JSON.parse(line))
.map((parsed) => Result.parse(parsed))
.filter((r) => r.type === "match")
.map((r) => r.data)
}
}
+38
View File
@@ -0,0 +1,38 @@
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]: {
[path: string]: Date | undefined
}
} = {}
return {
read,
}
})
export function read(sessionID: string, file: string) {
log.info("read", { sessionID, file })
const { read } = state()
read[sessionID] = read[sessionID] || {}
read[sessionID][file] = new Date()
}
export function get(sessionID: string, file: string) {
return state().read[sessionID]?.[file]
}
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`)
const stats = await Bun.file(filepath).stat()
if (stats.mtime.getTime() > time.getTime()) {
throw new Error(
`File ${filepath} has been modified since it was last read.\nLast modification: ${stats.mtime.toISOString()}\nLast read: ${time.toISOString()}\n\nPlease read the file again before modifying it.`,
)
}
}
}
+52
View File
@@ -0,0 +1,52 @@
import { z } from "zod"
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" })
export const Event = {
Updated: Bus.event(
"file.watcher.updated",
z.object({
file: z.string(),
event: z.union([z.literal("rename"), z.literal("change")]),
}),
),
}
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() {
if (Flag.OPENCODE_DISABLE_WATCHER || true) return
state()
}
}
+9
View File
@@ -0,0 +1,9 @@
export namespace Flag {
export const OPENCODE_AUTO_SHARE = truthy("OPENCODE_AUTO_SHARE")
export const OPENCODE_DISABLE_WATCHER = truthy("OPENCODE_DISABLE_WATCHER")
function truthy(key: string) {
const value = process.env[key]?.toLowerCase()
return value === "true" || value === "1"
}
}
+159
View File
@@ -0,0 +1,159 @@
import { App } from "../app/app"
import { BunProc } from "../bun"
import { Filesystem } from "../util/filesystem"
import path from "path"
export interface Info {
name: string
command: string[]
environment?: Record<string, string>
extensions: string[]
enabled(): Promise<boolean>
}
export const gofmt: Info = {
name: "gofmt",
command: ["gofmt", "-w", "$FILE"],
extensions: [".go"],
async enabled() {
return Bun.which("gofmt") !== null
},
}
export const mix: Info = {
name: "mix",
command: ["mix", "format", "$FILE"],
extensions: [".ex", ".exs", ".eex", ".heex", ".leex", ".neex", ".sface"],
async enabled() {
return Bun.which("mix") !== null
},
}
export const prettier: Info = {
name: "prettier",
command: [BunProc.which(), "x", "prettier", "--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 nms = await Filesystem.findUp("node_modules", app.path.cwd, app.path.root)
for (const item of nms) {
if (await Bun.file(path.join(item, ".bin", "prettier")).exists()) return true
}
return false
},
}
export const zig: Info = {
name: "zig",
command: ["zig", "fmt", "$FILE"],
extensions: [".zig", ".zon"],
async enabled() {
return Bun.which("zig") !== null
},
}
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"],
async enabled() {
return Bun.which("clang-format") !== null
},
}
export const ktlint: Info = {
name: "ktlint",
command: ["ktlint", "-F", "$FILE"],
extensions: [".kt", ".kts"],
async enabled() {
return Bun.which("ktlint") !== null
},
}
export const ruff: Info = {
name: "ruff",
command: ["ruff", "format", "$FILE"],
extensions: [".py", ".pyi"],
async enabled() {
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
},
}
export const rubocop: Info = {
name: "rubocop",
command: ["rubocop", "--autocorrect", "$FILE"],
extensions: [".rb", ".rake", ".gemspec", ".ru"],
async enabled() {
return Bun.which("rubocop") !== null
},
}
export const standardrb: Info = {
name: "standardrb",
command: ["standardrb", "--fix", "$FILE"],
extensions: [".rb", ".rake", ".gemspec", ".ru"],
async enabled() {
return Bun.which("standardrb") !== null
},
}
export const htmlbeautifier: Info = {
name: "htmlbeautifier",
command: ["htmlbeautifier", "$FILE"],
extensions: [".erb", ".html.erb"],
async enabled() {
return Bun.which("htmlbeautifier") !== null
},
}
+65
View File
@@ -0,0 +1,65 @@
import { App } from "../app/app"
import { Bus } from "../bus"
import { File } from "../file"
import { Log } from "../util/log"
import path from "path"
import * as Formatter from "./formatter"
export namespace Format {
const log = Log.create({ service: "format" })
const state = App.state("format", () => {
const enabled: Record<string, boolean> = {}
return {
enabled,
}
})
async function isEnabled(item: Formatter.Info) {
const s = state()
let status = s.enabled[item.name]
if (status === undefined) {
status = await item.enabled()
s.enabled[item.name] = status
}
return status
}
async function getFormatter(ext: string) {
const result = []
for (const item of Object.values(Formatter)) {
if (!item.extensions.includes(ext)) continue
if (!(await isEnabled(item))) continue
result.push(item)
}
return result
}
export function init() {
log.info("init")
Bus.subscribe(File.Event.Edited, async (payload) => {
const file = payload.properties.file
log.info("formatting", { file })
const ext = path.extname(file)
for (const item of await getFormatter(ext)) {
log.info("running", { command: item.command })
const proc = Bun.spawn({
cmd: item.command.map((x) => x.replace("$FILE", file)),
cwd: App.info().path.cwd,
env: item.environment,
stdout: "ignore",
stderr: "ignore",
})
const exit = await proc.exited
if (exit !== 0)
log.error("failed", {
command: item.command,
...item.environment,
})
}
})
}
}
+34 -15
View File
@@ -1,20 +1,39 @@
import envpaths from "env-paths"
import fs from "fs/promises"
const paths = envpaths("opencode", {
suffix: "",
})
import { xdgData, xdgCache, xdgConfig, xdgState } from "xdg-basedir"
import path from "path"
await Promise.all([
fs.mkdir(paths.config, { recursive: true }),
fs.mkdir(paths.cache, { recursive: true }),
])
const app = "opencode"
const data = path.join(xdgData!, app)
const cache = path.join(xdgCache!, app)
const config = path.join(xdgConfig!, app)
const state = path.join(xdgState!, app)
export namespace Global {
export function config() {
return paths.config
}
export function cache() {
return paths.cache
}
export const Path = {
data,
bin: path.join(data, "bin"),
providers: path.join(config, "providers"),
cache,
config,
state,
} as const
}
await Promise.all([
fs.mkdir(Global.Path.data, { recursive: true }),
fs.mkdir(Global.Path.config, { recursive: true }),
fs.mkdir(Global.Path.providers, { recursive: true }),
fs.mkdir(Global.Path.state, { recursive: true }),
])
const CACHE_VERSION = "3"
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)
}
+15 -18
View File
@@ -5,6 +5,8 @@ export namespace Identifier {
const prefixes = {
session: "ses",
message: "msg",
user: "usr",
part: "prt",
} as const
export function schema(prefix: keyof typeof prefixes) {
@@ -25,11 +27,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)
}
@@ -40,10 +38,17 @@ export namespace Identifier {
return given
}
function generateNewID(
prefix: keyof typeof prefixes,
descending: boolean,
): string {
function randomBase62(length: number): string {
const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
let result = ""
const bytes = randomBytes(length)
for (let i = 0; i < length; i++) {
result += chars[bytes[i] % 62]
}
return result
}
function generateNewID(prefix: keyof typeof prefixes, descending: boolean): string {
const currentTimestamp = Date.now()
if (currentTimestamp !== lastTimestamp) {
@@ -61,14 +66,6 @@ export namespace Identifier {
timeBytes[i] = Number((now >> BigInt(40 - 8 * i)) & BigInt(0xff))
}
const randLength = (LENGTH - 12) / 2
const random = randomBytes(randLength)
return (
prefixes[prefix] +
"_" +
timeBytes.toString("hex") +
random.toString("hex")
)
return prefixes[prefix] + "_" + timeBytes.toString("hex") + randomBase62(LENGTH - 12)
}
}
+120 -115
View File
@@ -1,119 +1,124 @@
import "zod-openapi/extend"
import { App } from "./app/app"
import { Server } from "./server/server"
import fs from "fs/promises"
import path from "path"
import { Bus } from "./bus"
import { Session } from "./session/session"
import cac from "cac"
import { Share } from "./share/share"
import { Message } from "./session/message"
import { Global } from "./global"
import { Provider } from "./provider/provider"
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 { UpgradeCommand } from "./cli/cmd/upgrade"
import { ModelsCommand } from "./cli/cmd/models"
import { UI } from "./cli/ui"
import { Installation } from "./installation"
import { NamedError } from "./util/error"
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 { InstallGithubCommand } from "./cli/cmd/install-github"
import { Trace } from "./trace"
declare global {
const OPENCODE_VERSION: string
Trace.init()
const cancel = new AbortController()
process.on("unhandledRejection", (e) => {
Log.Default.error("rejection", {
e: e instanceof Error ? e.message : e,
})
})
process.on("uncaughtException", (e) => {
Log.Default.error("exception", {
e: e instanceof Error ? e.message : e,
})
})
const cli = yargs(hideBin(process.argv))
.scriptName("opencode")
.help("help", "show help")
.version("version", "show version number", Installation.VERSION)
.alias("version", "v")
.option("print-logs", {
describe: "print logs to stderr",
type: "boolean",
})
.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(UpgradeCommand)
.command(ServeCommand)
.command(ModelsCommand)
.command(StatsCommand)
.command(InstallGithubCommand)
.fail((msg) => {
if (msg.startsWith("Unknown argument") || msg.startsWith("Not enough non-option arguments")) {
cli.showHelp("log")
}
})
.strict()
try {
await cli.parse()
} catch (e) {
let data: Record<string, any> = {}
if (e instanceof NamedError) {
const obj = e.toObject()
Object.assign(data, {
...obj.data,
})
}
if (e instanceof Error) {
Object.assign(data, {
name: e.name,
message: e.message,
cause: e.cause?.toString(),
})
}
if (e instanceof ResolveMessage) {
Object.assign(data, {
name: e.name,
message: e.message,
code: e.code,
specifier: e.specifier,
referrer: e.referrer,
position: e.position,
importKind: e.importKind,
})
}
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")
process.exitCode = 1
}
const cli = cac("opencode")
cli.command("", "Start the opencode in interactive mode").action(async () => {
await App.provide({ directory: process.cwd() }, async () => {
await Share.init()
const server = Server.listen()
let cmd = ["go", "run", "./main.go"]
let cwd = "../tui"
if (Bun.embeddedFiles.length > 0) {
const blob = Bun.embeddedFiles[0] as File
const binary = path.join(Global.cache(), "tui", blob.name)
const file = Bun.file(binary)
if (!(await file.exists())) {
console.log("installing tui binary...")
await Bun.write(file, blob, { mode: 0o755 })
await fs.chmod(binary, 0o755)
}
cwd = process.cwd()
cmd = [binary]
}
const proc = Bun.spawn({
cmd,
cwd,
stdout: "inherit",
stderr: "inherit",
stdin: "inherit",
onExit: () => {
server.stop()
},
})
await proc.exited
await server.stop()
})
})
cli.command("generate", "Generate OpenAPI and event specs").action(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),
)
})
cli
.command("run [...message]", "Run a chat message")
.option("--session <id>", "Session ID")
.action(async (message: string[], options) => {
await App.provide({ directory: process.cwd() }, async () => {
await Share.init()
const session = options.session
? await Session.get(options.session)
: await Session.create()
console.log("Session:", session.id)
Bus.subscribe(Message.Event.Updated, async (message) => {
console.log("Thinking...")
})
const unsub = Bus.subscribe(Session.Event.Updated, async (message) => {
if (message.properties.info.share?.url)
console.log("Share:", message.properties.info.share.url)
unsub()
})
const [provider] = await Provider.active().then((val) =>
val.values().toArray(),
)
if (!provider) throw new Error("no providers found")
const model = provider.models[0]
if (!model) throw new Error("no models found")
console.log("using", provider.id, model.id)
const result = await Session.chat({
sessionID: session.id,
providerID: provider.id,
modelID: model.id,
parts: [
{
type: "text",
text: message.join(" "),
},
],
})
for (const part of result.parts) {
if (part.type === "text") {
console.log("opencode:", part.text)
}
}
console.log({
cost: result.metadata.assistant?.cost,
tokens: result.metadata.assistant?.tokens,
})
})
})
cli.version(typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "dev")
cli.help()
cli.parse()
cancel.abort()
+151
View File
@@ -0,0 +1,151 @@
import path from "path"
import { $ } from "bun"
import { z } from "zod"
import { NamedError } from "../util/error"
import { Bus } from "../bus"
import { Log } from "../util/log"
declare global {
const OPENCODE_VERSION: string
}
export namespace Installation {
const log = Log.create({ service: "installation" })
export type Method = Awaited<ReturnType<typeof method>>
export const Event = {
Updated: Bus.event(
"installation.updated",
z.object({
version: z.string(),
}),
),
}
export const Info = z
.object({
version: z.string(),
latest: z.string(),
})
.openapi({
ref: "InstallationInfo",
})
export type Info = z.infer<typeof Info>
export async function info() {
return {
version: VERSION,
latest: await latest(),
}
}
export function isSnapshot() {
return VERSION.startsWith("0.0.0")
}
export function isDev() {
return VERSION === "dev"
}
export async function method() {
if (process.execPath.includes(path.join(".opencode", "bin"))) return "curl"
const exec = process.execPath.toLowerCase()
const checks = [
{
name: "npm" as const,
command: () => $`npm list -g --depth=0`.throws(false).text(),
},
{
name: "yarn" as const,
command: () => $`yarn global list`.throws(false).text(),
},
{
name: "pnpm" as const,
command: () => $`pnpm list -g --depth=0`.throws(false).text(),
},
{
name: "bun" as const,
command: () => $`bun pm ls -g`.throws(false).text(),
},
{
name: "brew" as const,
command: () => $`brew list --formula opencode-ai`.throws(false).text(),
},
]
checks.sort((a, b) => {
const aMatches = exec.includes(a.name)
const bMatches = exec.includes(b.name)
if (aMatches && !bMatches) return -1
if (!aMatches && bMatches) return 1
return 0
})
for (const check of checks) {
const output = await check.command()
if (output.includes("opencode-ai")) {
return check.name
}
}
return "unknown"
}
export const UpgradeFailedError = NamedError.create(
"UpgradeFailedError",
z.object({
stderr: z.string(),
}),
)
export async function upgrade(method: Method, target: string) {
const cmd = (() => {
switch (method) {
case "curl":
return $`curl -fsSL https://opencode.ai/install | bash`.env({
...process.env,
VERSION: target,
})
case "npm":
return $`npm install -g opencode-ai@${target}`
case "pnpm":
return $`pnpm install -g opencode-ai@${target}`
case "bun":
return $`bun install -g opencode-ai@${target}`
case "brew":
return $`brew install sst/tap/opencode`.env({
HOMEBREW_NO_AUTO_UPDATE: "1",
})
default:
throw new Error(`Unknown method: ${method}`)
}
})()
const result = await cmd.quiet().throws(false)
log.info("upgraded", {
method,
target,
stdout: result.stdout.toString(),
stderr: result.stderr.toString(),
})
if (result.exitCode !== 0)
throw new UpgradeFailedError({
stderr: result.stderr.toString("utf8"),
})
}
export const VERSION = typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "dev"
export async function latest() {
return fetch("https://api.github.com/repos/sst/opencode/releases/latest")
.then((res) => res.json())
.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
})
}
}
+92 -109
View File
@@ -1,24 +1,29 @@
import { spawn } from "child_process"
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"
import { LANGUAGE_EXTENSIONS } from "./language"
import { Bus } from "../bus"
import z from "zod"
import type { LSPServer } from "./server"
import { NamedError } from "../util/error"
import { withTimeout } from "../util/timeout"
export namespace LSPClient {
const log = Log.create({ service: "lsp.client" })
export type Info = Awaited<ReturnType<typeof create>>
export type Info = NonNullable<Awaited<ReturnType<typeof create>>>
export type Diagnostic = VSCodeDiagnostic
export const InitializeError = NamedError.create(
"LSPInitializeError",
z.object({
serverID: z.string(),
}),
)
export const Event = {
Diagnostics: Bus.event(
"lsp.client.diagnostics",
@@ -29,101 +34,88 @@ export namespace LSPClient {
),
}
export async function create(input: { cmd: string[]; serverID: string }) {
log.info("starting client", input)
const app = await App.use()
const [command, ...args] = input.cmd
const server = spawn(command, args, {
stdio: ["pipe", "pipe", "pipe"],
cwd: app.root,
})
export async function create(input: { serverID: string; server: LSPServer.Handle; root: string }) {
const app = App.info()
const l = log.clone().tag("serverID", input.serverID)
l.info("starting client")
const connection = createMessageConnection(
new StreamMessageReader(server.stdout),
new StreamMessageWriter(server.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)
// servers seem to send one blank publishDiagnostics event before the first real one
if (!exists && !params.diagnostics.length) return
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()
await connection.sendRequest("initialize", {
processId: server.pid,
initializationOptions: {
l.info("sending initialize")
await withTimeout(
connection.sendRequest("initialize", {
rootUri: "file://" + input.root,
processId: input.server.process.pid,
workspaceFolders: [
{
name: "workspace",
uri: "file://" + app.root,
uri: "file://" + input.root,
},
],
tsserver: {
path: require.resolve("typescript/lib/tsserver.js"),
initializationOptions: {
...input.server.initialization,
},
},
capabilities: {
workspace: {
configuration: true,
didChangeConfiguration: {
dynamicRegistration: true,
capabilities: {
window: {
workDoneProgress: true,
},
didChangeWatchedFiles: {
dynamicRegistration: true,
relativePatternSupport: true,
workspace: {
configuration: true,
},
},
textDocument: {
synchronization: {
dynamicRegistration: true,
didSave: true,
},
completion: {
completionItem: {},
},
codeLens: {
dynamicRegistration: true,
},
documentSymbol: {},
codeAction: {
codeActionLiteralSupport: {
codeActionKind: {
valueSet: [],
},
textDocument: {
synchronization: {
didOpen: true,
didChange: true,
},
publishDiagnostics: {
versionSupport: true,
},
},
publishDiagnostics: {
versionSupport: true,
},
semanticTokens: {
requests: {
range: {},
full: {},
},
tokenTypes: [],
tokenModifiers: [],
formats: [],
},
},
window: {},
},
}),
5_000,
).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 = new Set<string>()
await connection.sendNotification("initialized", {})
const files: {
[path: string]: number
} = {}
const result = {
get clientID() {
root: input.root,
get serverID() {
return input.serverID
},
get connection() {
@@ -131,78 +123,69 @@ export namespace LSPClient {
},
notify: {
async open(input: { path: string }) {
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 opened = files.has(input.path)
if (!opened) {
log.info("textDocument/didOpen", input)
const version = files[input.path]
if (version !== undefined) {
diagnostics.delete(input.path)
const extension = path.extname(input.path)
const languageId = LANGUAGE_EXTENSIONS[extension] ?? "plaintext"
await connection.sendNotification("textDocument/didOpen", {
await connection.sendNotification("textDocument/didClose", {
textDocument: {
uri: `file://` + input.path,
languageId,
version: Date.now(),
text,
},
})
files.add(input.path)
return
}
log.info("textDocument/didChange", input)
log.info("textDocument/didOpen", input)
diagnostics.delete(input.path)
await connection.sendNotification("textDocument/didChange", {
const extension = path.extname(input.path)
const languageId = LANGUAGE_EXTENSIONS[extension] ?? "plaintext"
await connection.sendNotification("textDocument/didOpen", {
textDocument: {
uri: `file://` + input.path,
version: Date.now(),
languageId,
version: 0,
text,
},
contentChanges: [
{
text,
},
],
})
files[input.path] = 0
return
},
},
get diagnostics() {
return diagnostics
},
async waitForDiagnostics(input: { path: string }) {
input.path = path.isAbsolute(input.path) ? input.path : path.resolve(app.path.cwd, input.path)
log.info("waiting for diagnostics", input)
let unsub: () => void
let timeout: NodeJS.Timeout
return await Promise.race([
new Promise<void>(async (resolve) => {
return await withTimeout(
new Promise<void>((resolve) => {
unsub = Bus.subscribe(Event.Diagnostics, (event) => {
if (
event.properties.path === input.path &&
event.properties.serverID === result.clientID
) {
if (event.properties.path === input.path && event.properties.serverID === result.serverID) {
log.info("got diagnostics", input)
clearTimeout(timeout)
unsub?.()
resolve()
}
})
}),
new Promise<void>((resolve) => {
timeout = setTimeout(() => {
log.info("timed out refreshing diagnostics", input)
unsub?.()
resolve()
}, 5000)
}),
])
3000,
)
.catch(() => {})
.finally(() => {
unsub?.()
})
},
async shutdown() {
log.info("shutting down")
l.info("shutting down")
connection.end()
connection.dispose()
input.server.process.kill()
l.info("shutdown")
},
}
l.info("initialized")
return result
}
}
+154 -54
View File
@@ -2,42 +2,113 @@ import { App } from "../app/app"
import { Log } from "../util/log"
import { LSPClient } from "./client"
import path from "path"
import { LSPServer } from "./server"
import { z } from "zod"
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: Range,
}),
})
.openapi({
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 () => {
log.info("initializing")
const clients = new Map<string, LSPClient.Info>()
const clients: LSPClient.Info[] = []
return {
broken: new Set<string>(),
clients,
}
},
async (state) => {
for (const client of state.clients.values()) {
for (const client of state.clients) {
await client.shutdown()
}
},
)
export async function file(input: string) {
const extension = path.parse(input).ext
export async function init() {
return state()
}
async function getClients(file: string) {
const s = await state()
const matches = AUTO.filter((x) => x.extensions.includes(extension))
for (const match of matches) {
const existing = s.clients.get(match.id)
if (existing) continue
const extension = path.parse(file).ext
const result: LSPClient.Info[] = []
for (const server of Object.values(LSPServer)) {
if (!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({
cmd: match.command,
serverID: match.id,
serverID: server.id,
server: handle,
root,
}).catch((err) => {
s.broken.add(root + server.id)
handle.process.kill()
log.error("", { error: err })
})
s.clients.set(match.id, client)
if (!client) continue
s.clients.push(client)
result.push(client)
}
return result
}
export async function touchFile(input: string, waitForDiagnostics?: boolean) {
const clients = await getClients(input)
await run(async (client) => {
const wait = client.waitForDiagnostics({ path: input })
if (!clients.includes(client)) return
const wait = waitForDiagnostics ? client.waitForDiagnostics({ path: input }) : Promise.resolve()
await client.notify.open({ path: input })
return wait
})
@@ -55,11 +126,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: {
@@ -73,45 +140,78 @@ export namespace LSP {
})
}
async function run<T>(
input: (client: LSPClient.Info) => Promise<T>,
): Promise<T[]> {
const clients = await state().then((x) => [...x.clients.values()])
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,
})
.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[])
}
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)
}
const AUTO: {
id: string
command: string[]
extensions: string[]
install?: () => Promise<void>
}[] = [
{
id: "typescript",
command: ["bun", "x", "typescript-language-server", "--stdio"],
extensions: [
".ts",
".tsx",
".js",
".jsx",
".mjs",
".cjs",
".mts",
".cts",
".mtsx",
".ctsx",
],
},
/*
{
id: "golang",
command: ["gopls"],
extensions: [".go"],
},
*/
]
export namespace Diagnostic {
export function pretty(diagnostic: LSPClient.Diagnostic) {
const severityMap = {
+10
View File
@@ -63,6 +63,14 @@ export const LANGUAGE_EXTENSIONS: Record<string, string> = {
".cshtml": "razor",
".razor": "razor",
".rb": "ruby",
".rake": "ruby",
".gemspec": "ruby",
".ru": "ruby",
".erb": "erb",
".html.erb": "erb",
".js.erb": "erb",
".css.erb": "erb",
".json.erb": "erb",
".rs": "rust",
".scss": "scss",
".sass": "sass",
@@ -86,4 +94,6 @@ export const LANGUAGE_EXTENSIONS: Record<string, string> = {
".yml": "yaml",
".mjs": "javascript",
".cjs": "javascript",
".zig": "zig",
".zon": "zig",
} as const
+325
View File
@@ -0,0 +1,325 @@
import { spawn, type ChildProcessWithoutNullStreams } from "child_process"
import type { App } from "../app/app"
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" })
export interface Handle {
process: ChildProcessWithoutNullStreams
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[]
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, 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"], {
cwd: root,
env: {
...process.env,
BUN_BE_BUN: "1",
},
})
return {
process: proc,
initialization: {
tsserver: {
path: tsserver,
},
},
}
},
}
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(_, root) {
let bin = Bun.which("gopls", {
PATH: process.env["PATH"] + ":" + Global.Path.bin,
})
if (!bin) {
if (!Bun.which("go")) return
log.info("installing gopls")
const proc = Bun.spawn({
cmd: ["go", "install", "golang.org/x/tools/gopls@latest"],
env: { ...process.env, GOBIN: Global.Path.bin },
stdout: "pipe",
stderr: "pipe",
stdin: "pipe",
})
const exit = await proc.exited
if (exit !== 0) {
log.error("Failed to install gopls")
return
}
bin = path.join(Global.Path.bin, "gopls" + (process.platform === "win32" ? ".exe" : ""))
log.info(`installed gopls`, {
bin,
})
}
return {
process: spawn(bin!, {
cwd: root,
}),
}
},
}
export const RubyLsp: Info = {
id: "ruby-lsp",
root: NearestRoot(["Gemfile"]),
extensions: [".rb", ".rake", ".gemspec", ".ru"],
async spawn(_, root) {
let bin = Bun.which("ruby-lsp", {
PATH: process.env["PATH"] + ":" + Global.Path.bin,
})
if (!bin) {
const ruby = Bun.which("ruby")
const gem = Bun.which("gem")
if (!ruby || !gem) {
log.info("Ruby not found, please install Ruby first")
return
}
log.info("installing ruby-lsp")
const proc = Bun.spawn({
cmd: ["gem", "install", "ruby-lsp", "--bindir", Global.Path.bin],
stdout: "pipe",
stderr: "pipe",
stdin: "pipe",
})
const exit = await proc.exited
if (exit !== 0) {
log.error("Failed to install ruby-lsp")
return
}
bin = path.join(Global.Path.bin, "ruby-lsp" + (process.platform === "win32" ? ".exe" : ""))
log.info(`installed ruby-lsp`, {
bin,
})
}
return {
process: spawn(bin!, ["--stdio"], {
cwd: root,
}),
}
},
}
export const Pyright: Info = {
id: "pyright",
extensions: [".py", ".pyi"],
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,
}),
}
},
}
}
+111
View File
@@ -0,0 +1,111 @@
import { experimental_createMCPClient, type Tool } from "ai"
import { Experimental_StdioMCPTransport } from "ai/mcp-stdio"
import { App } from "../app/app"
import { Config } from "../config/config"
import { Log } from "../util/log"
import { NamedError } from "../util/error"
import { z } from "zod"
import { Session } from "../session"
import { Bus } from "../bus"
export namespace MCP {
const log = Log.create({ service: "mcp" })
export const Failed = NamedError.create(
"MCPFailed",
z.object({
name: z.string(),
}),
)
const state = App.state(
"mcp",
async () => {
const cfg = await Config.get()
const clients: {
[name: string]: Awaited<ReturnType<typeof experimental_createMCPClient>>
} = {}
for (const [key, mcp] of Object.entries(cfg.mcp ?? {})) {
if (mcp.enabled === false) {
log.info("mcp server disabled", { key })
continue
}
log.info("found", { key, type: mcp.type })
if (mcp.type === "remote") {
const client = await experimental_createMCPClient({
name: key,
transport: {
type: "sse",
url: mcp.url,
headers: mcp.headers,
},
}).catch(() => {})
if (!client) {
Bus.publish(Session.Event.Error, {
error: {
name: "UnknownError",
data: {
message: `MCP server ${key} failed to start`,
},
},
})
continue
}
clients[key] = client
}
if (mcp.type === "local") {
const [cmd, ...args] = mcp.command
const client = await experimental_createMCPClient({
name: key,
transport: new Experimental_StdioMCPTransport({
stderr: "ignore",
command: cmd,
args,
env: {
...process.env,
...(cmd === "opencode" ? { BUN_BE_BUN: "1" } : {}),
...mcp.environment,
},
}),
}).catch(() => {})
if (!client) {
Bus.publish(Session.Event.Error, {
error: {
name: "UnknownError",
data: {
message: `MCP server ${key} failed to start`,
},
},
})
continue
}
clients[key] = client
}
}
return {
clients,
}
},
async (state) => {
for (const client of Object.values(state.clients)) {
client.close()
}
},
)
export async function clients() {
return state().then((state) => state.clients)
}
export async function tools() {
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
}
}
return result
}
}
+136
View File
@@ -0,0 +1,136 @@
import { App } from "../app/app"
import { z } from "zod"
import { Bus } from "../bus"
import { Log } from "../util/log"
export namespace Permission {
const log = Log.create({ service: "permission" })
export const Info = z
.object({
id: z.string(),
sessionID: z.string(),
title: z.string(),
metadata: z.record(z.any()),
time: z.object({
created: z.number(),
}),
})
.openapi({
ref: "permission.info",
})
export type Info = z.infer<typeof Info>
export const Event = {
Updated: Bus.event("permission.updated", Info),
}
const state = App.state(
"permission",
() => {
const pending: {
[sessionID: string]: {
[permissionID: string]: {
info: Info
resolve: () => void
reject: (e: any) => void
}
}
} = {}
const approved: {
[sessionID: string]: {
[permissionID: string]: Info
}
} = {}
return {
pending,
approved,
}
},
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))
}
}
},
)
export function ask(input: {
id: Info["id"]
sessionID: Info["sessionID"]
title: Info["title"]
metadata: Info["metadata"]
}) {
return
const { pending, approved } = state()
log.info("asking", {
sessionID: input.sessionID,
permissionID: input.id,
})
if (approved[input.sessionID]?.[input.id]) {
log.info("previously approved", {
sessionID: input.sessionID,
permissionID: input.id,
})
return
}
const info: Info = {
id: input.id,
sessionID: input.sessionID,
title: input.title,
metadata: input.metadata,
time: {
created: Date.now(),
},
}
pending[input.sessionID] = pending[input.sessionID] || {}
return new Promise<void>((resolve, reject) => {
pending[input.sessionID][input.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"
}) {
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))
return
}
match.resolve()
if (input.response === "always") {
approved[input.sessionID] = approved[input.sessionID] || {}
approved[input.sessionID][input.permissionID] = match.info
}
}
export class RejectedError extends Error {
constructor(
public readonly sessionID: string,
public readonly permissionID: string,
) {
super(`The user rejected permission to use this functionality`)
}
}
}
@@ -1,62 +0,0 @@
import type { Provider } from "./provider"
export const PROVIDER_DATABASE: Provider.Info[] = [
{
id: "anthropic",
name: "Anthropic",
models: [
{
id: "claude-sonnet-4-20250514",
name: "Claude Sonnet 4",
cost: {
input: 3.0 / 1_000_000,
output: 15.0 / 1_000_000,
inputCached: 3.75 / 1_000_000,
outputCached: 0.3 / 1_000_000,
},
contextWindow: 200_000,
maxOutputTokens: 50_000,
attachment: true,
},
],
},
{
id: "openai",
name: "OpenAI",
models: [
{
id: "codex-mini-latest",
name: "Codex Mini",
cost: {
input: 1.5 / 1_000_000,
inputCached: 0.375 / 1_000_000,
output: 6.0 / 1_000_000,
outputCached: 0.0 / 1_000_000,
},
contextWindow: 200_000,
maxOutputTokens: 100_000,
attachment: true,
reasoning: true,
},
],
},
{
id: "google",
name: "Google",
models: [
{
id: "gemini-2.5-pro-preview-03-25",
name: "Gemini 2.5 Pro",
cost: {
input: 1.25 / 1_000_000,
inputCached: 0 / 1_000_000,
output: 10 / 1_000_000,
outputCached: 0 / 1_000_000,
},
contextWindow: 1_000_000,
maxOutputTokens: 50_000,
attachment: true,
},
],
},
]
@@ -0,0 +1,4 @@
export async function data() {
const json = await fetch("https://models.dev/api.json").then((x) => x.text())
return json
}
+70
View File
@@ -0,0 +1,70 @@
import { Global } from "../global"
import { Log } from "../util/log"
import path from "path"
import { z } from "zod"
import { data } from "./models-macro" with { type: "macro" }
export namespace ModelsDev {
const log = Log.create({ service: "models.dev" })
const filepath = path.join(Global.Path.cache, "models.json")
export const Model = z
.object({
id: z.string(),
name: z.string(),
release_date: z.string(),
attachment: z.boolean(),
reasoning: z.boolean(),
temperature: z.boolean(),
tool_call: z.boolean(),
cost: z.object({
input: z.number(),
output: z.number(),
cache_read: z.number().optional(),
cache_write: z.number().optional(),
}),
limit: z.object({
context: z.number(),
output: z.number(),
}),
options: z.record(z.any()),
})
.openapi({
ref: "Model",
})
export type Model = z.infer<typeof Model>
export const Provider = z
.object({
api: z.string().optional(),
name: z.string(),
env: z.array(z.string()),
id: z.string(),
npm: z.string().optional(),
models: z.record(Model),
})
.openapi({
ref: "Provider",
})
export type Provider = z.infer<typeof Provider>
export async function get() {
const file = Bun.file(filepath)
const result = await file.json().catch(() => {})
if (result) {
refresh()
return result as Record<string, Provider>
}
refresh()
const json = await data()
return JSON.parse(json) as Record<string, Provider>
}
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)
}
}
+529 -86
View File
@@ -1,78 +1,348 @@
import z from "zod"
import { App } from "../app/app"
import { Config } from "../config/config"
import { PROVIDER_DATABASE } from "./database"
import { mergeDeep, sortBy } from "remeda"
import { NoSuchModelError, type LanguageModel, type Provider as SDK } from "ai"
import { Log } from "../util/log"
import path from "path"
import { Global } from "../global"
import { BunProc } from "../bun"
import { BashTool } from "../tool/bash"
import { EditTool } from "../tool/edit"
import { FetchTool } from "../tool/fetch"
import { WebFetchTool } from "../tool/webfetch"
import { GlobTool } from "../tool/glob"
import { GrepTool } from "../tool/grep"
import { ListTool } from "../tool/ls"
import { LspDiagnosticTool } from "../tool/lsp-diagnostics"
import { LspHoverTool } from "../tool/lsp-hover"
import { PatchTool } from "../tool/patch"
import { ViewTool } from "../tool/view"
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" })
export const Model = z
.object({
id: z.string(),
name: z.string().optional(),
cost: z.object({
input: z.number(),
inputCached: z.number(),
output: z.number(),
outputCached: z.number(),
}),
contextWindow: z.number(),
maxOutputTokens: z.number().optional(),
attachment: z.boolean(),
reasoning: z.boolean().optional(),
})
.openapi({
ref: "Provider.Model",
})
export type Model = z.output<typeof Model>
type CustomLoader = (
provider: ModelsDev.Provider,
api?: string,
) => Promise<{
autoload: boolean
getModel?: (sdk: any, modelID: string) => Promise<any>
options?: Record<string, any>
}>
export const Info = z
.object({
id: z.string(),
name: z.string(),
options: z.record(z.string(), z.any()).optional(),
models: Model.array(),
})
.openapi({
ref: "Provider.Info",
})
export type Info = z.output<typeof Info>
type Source = "env" | "config" | "custom" | "api"
const AUTODETECT: Record<string, string[]> = {
anthropic: ["ANTHROPIC_API_KEY"],
openai: ["OPENAI_API_KEY"],
google: ["GOOGLE_GENERATIVE_AI_API_KEY", "GEMINI_API_KEY"],
const CUSTOM_LOADERS: Record<string, CustomLoader> = {
async anthropic(provider) {
const access = await AuthAnthropic.access()
if (!access) return { autoload: false }
for (const model of Object.values(provider.models)) {
model.cost = {
input: 0,
output: 0,
}
}
return {
autoload: true,
options: {
apiKey: "",
async fetch(input: any, init: any) {
const access = await AuthAnthropic.access()
const headers = {
...init.headers,
authorization: `Bearer ${access}`,
"anthropic-beta": "oauth-2025-04-20",
}
delete headers["x-api-key"]
return fetch(input, {
...init,
headers,
})
},
},
}
},
"github-copilot": async (provider) => {
const copilot = await AuthCopilot()
if (!copilot) return { autoload: false }
let info = await Auth.get("github-copilot")
if (!info || info.type !== "oauth") return { autoload: false }
if (provider && provider.models) {
for (const model of Object.values(provider.models)) {
model.cost = {
input: 0,
output: 0,
}
}
}
return {
autoload: true,
options: {
apiKey: "",
async fetch(input: any, init: any) {
const info = await Auth.get("github-copilot")
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")
await Auth.set("github-copilot", {
type: "oauth",
...tokens,
})
info.access = tokens.access
}
let isAgentCall = false
let isVisionRequest = false
try {
const body = typeof init.body === "string" ? JSON.parse(init.body) : init.body
if (body?.messages) {
isAgentCall = body.messages.some((msg: any) => msg.role && ["tool", "assistant"].includes(msg.role))
isVisionRequest = body.messages.some(
(msg: any) =>
Array.isArray(msg.content) && msg.content.some((part: any) => part.type === "image_url"),
)
}
} catch {}
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,
headers,
})
},
},
}
},
openai: async () => {
return {
autoload: false,
async getModel(sdk: any, modelID: string) {
return sdk.responses(modelID)
},
options: {},
}
},
"amazon-bedrock": async () => {
if (!process.env["AWS_PROFILE"] && !process.env["AWS_ACCESS_KEY_ID"] && !process.env["AWS_BEARER_TOKEN_BEDROCK"])
return { autoload: false }
const region = process.env["AWS_REGION"] ?? "us-east-1"
const { fromNodeProviderChain } = await import(await BunProc.install("@aws-sdk/credential-providers"))
return {
autoload: true,
options: {
region,
credentialProvider: fromNodeProviderChain(),
},
async getModel(sdk: any, modelID: string) {
let regionPrefix = region.split("-")[0]
switch (regionPrefix) {
case "us": {
const modelRequiresPrefix = ["claude", "deepseek"].some((m) => modelID.includes(m))
if (modelRequiresPrefix) {
modelID = `${regionPrefix}.${modelID}`
}
break
}
case "eu": {
const regionRequiresPrefix = [
"eu-west-1",
"eu-west-3",
"eu-north-1",
"eu-central-1",
"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),
)
if (regionRequiresPrefix && modelRequiresPrefix) {
modelID = `${regionPrefix}.${modelID}`
}
break
}
case "ap": {
const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "nova-pro"].some((m) =>
modelID.includes(m),
)
if (modelRequiresPrefix) {
regionPrefix = "apac"
modelID = `${regionPrefix}.${modelID}`
}
break
}
}
return sdk.languageModel(modelID)
},
}
},
openrouter: async () => {
return {
autoload: false,
options: {
headers: {
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
},
},
}
},
}
const state = App.state("provider", async () => {
const config = await Config.get()
const providers = new Map<string, Info>()
const models = new Map<string, { info: Model; language: LanguageModel }>()
const database = await ModelsDev.get()
const providers: {
[providerID: string]: {
source: Source
info: ModelsDev.Provider
getModel?: (sdk: any, modelID: string) => Promise<any>
options: Record<string, any>
}
} = {}
const models = new Map<string, { info: ModelsDev.Model; language: LanguageModel }>()
const sdk = new Map<string, SDK>()
for (const item of PROVIDER_DATABASE) {
if (!AUTODETECT[item.id].some((env) => process.env[env])) continue
providers.set(item.id, item)
log.info("init")
function mergeProvider(
id: string,
options: Record<string, any>,
source: Source,
getModel?: (sdk: any, modelID: string) => Promise<any>,
) {
const provider = providers[id]
if (!provider) {
const info = database[id]
if (!info) return
if (info.api && !options["baseURL"]) options["baseURL"] = info.api
providers[id] = {
source,
info,
options,
getModel,
}
return
}
provider.options = mergeDeep(provider.options, options)
provider.source = source
provider.getModel = getModel ?? provider.getModel
}
for (const item of config.provider ?? []) {
providers.set(item.id, item)
const configProviders = Object.entries(config.provider ?? {})
for (const [providerID, provider] of configProviders) {
const existing = database[providerID]
const parsed: ModelsDev.Provider = {
id: providerID,
npm: provider.npm ?? existing?.npm,
name: provider.name ?? existing?.name ?? providerID,
env: provider.env ?? existing?.env ?? [],
api: provider.api ?? existing?.api,
models: existing?.models ?? {},
}
for (const [modelID, model] of Object.entries(provider.models ?? {})) {
const existing = parsed.models[modelID]
const parsedModel: ModelsDev.Model = {
id: modelID,
name: model.name ?? existing?.name ?? modelID,
release_date: model.release_date ?? existing?.release_date,
attachment: model.attachment ?? existing?.attachment ?? false,
reasoning: model.reasoning ?? existing?.reasoning ?? false,
temperature: model.temperature ?? existing?.temperature ?? false,
tool_call: model.tool_call ?? existing?.tool_call ?? true,
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,
},
limit: model.limit ??
existing?.limit ?? {
context: 0,
output: 0,
},
}
parsed.models[modelID] = parsedModel
}
database[providerID] = parsed
}
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
const apiKey = provider.env.map((item) => process.env[item]).at(0)
if (!apiKey) continue
mergeProvider(
providerID,
// only include apiKey if there's only one potential option
provider.env.length === 1 ? { apiKey } : {},
"env",
)
}
// load apikeys
for (const [providerID, provider] of Object.entries(await Auth.all())) {
if (disabled.has(providerID)) continue
if (provider.type === "api") {
mergeProvider(providerID, { apiKey: provider.key }, "api")
}
}
// load custom
for (const [providerID, fn] of Object.entries(CUSTOM_LOADERS)) {
if (disabled.has(providerID)) continue
const result = await fn(database[providerID])
if (result && (result.autoload || providers[providerID])) {
mergeProvider(providerID, result.options ?? {}, "custom", result.getModel)
}
}
// load config
for (const [providerID, provider] of configProviders) {
mergeProvider(providerID, provider.options ?? {}, "config")
}
for (const [providerID, provider] of Object.entries(providers)) {
if (Object.keys(provider.info.models).length === 0) {
delete providers[providerID]
continue
}
log.info("found", { providerID })
}
return {
@@ -82,28 +352,30 @@ export namespace Provider {
}
})
export async function active() {
export async function list() {
return state().then((state) => state.providers)
}
async function getSDK(providerID: string) {
const s = await state()
if (s.sdk.has(providerID)) return s.sdk.get(providerID)!
const dir = path.join(Global.cache(), `node_modules`, `@ai-sdk`, providerID)
if (!(await Bun.file(path.join(dir, "package.json")).exists())) {
log.info("installing", {
providerID,
async function getSDK(provider: ModelsDev.Provider) {
return (async () => {
using _ = log.time("getSDK", {
providerID: provider.id,
})
BunProc.run(["add", `@ai-sdk/${providerID}@alpha`], {
cwd: Global.cache(),
const s = await state()
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, "beta"))
const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
const loaded = fn({
name: provider.id,
...s.providers[provider.id]?.options,
})
}
const mod = await import(path.join(dir))
const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
const loaded = fn(s.providers.get(providerID)?.options)
s.sdk.set(providerID, loaded)
return loaded as SDK
s.sdk.set(provider.id, loaded)
return loaded as SDK
})().catch((e) => {
throw new InitError({ providerID: provider.id }, { cause: e })
})
}
export async function getModel(providerID: string, modelID: string) {
@@ -111,21 +383,19 @@ export namespace Provider {
const s = await state()
if (s.models.has(key)) return s.models.get(key)!
log.info("loading", {
log.info("getModel", {
providerID,
modelID,
})
const provider = s.providers.get(providerID)
if (!provider) throw new ModelNotFoundError(modelID)
const info = provider.models.find((m) => m.id === modelID)
if (!info) throw new ModelNotFoundError(modelID)
const sdk = await getSDK(providerID)
if (!sdk) throw new ModelNotFoundError(modelID)
const provider = s.providers[providerID]
if (!provider) throw new ModelNotFoundError({ providerID, modelID })
const info = provider.info.models[modelID]
if (!info) throw new ModelNotFoundError({ providerID, modelID })
const sdk = await getSDK(provider.info)
try {
const language = 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,
@@ -136,41 +406,214 @@ export namespace Provider {
language,
}
} catch (e) {
if (e instanceof NoSuchModelError) throw new ModelNotFoundError(modelID)
if (e instanceof NoSuchModelError)
throw new ModelNotFoundError(
{
modelID: modelID,
providerID,
},
{ cause: e },
)
throw e
}
}
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) => (model.id.includes("latest") ? 0 : 1), "asc"],
[(model) => model.id, "desc"],
)
}
export async function defaultModel() {
const cfg = await Config.get()
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)))
if (!provider) throw new Error("no providers found")
const [model] = sort(Object.values(provider.info.models))
if (!model) throw new Error("no models found")
return {
providerID: provider.info.id,
modelID: model.id,
}
}
export function parseModel(model: string) {
const [providerID, ...rest] = model.split("/")
return {
providerID: providerID,
modelID: rest.join("/"),
}
}
const TOOLS = [
BashTool,
EditTool,
FetchTool,
WebFetchTool,
GlobTool,
GrepTool,
ListTool,
LspDiagnosticTool,
LspHoverTool,
// LspDiagnosticTool,
// LspHoverTool,
PatchTool,
ViewTool,
EditTool,
ReadTool,
// MultiEditTool,
WriteTool,
TodoWriteTool,
TodoReadTool,
TaskTool,
]
const TOOL_MAPPING: Record<string, Tool.Info[]> = {
anthropic: TOOLS,
openai: TOOLS,
google: TOOLS,
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.map((t) => ({
...t,
parameters: sanitizeGeminiParameters(t.parameters),
})),
}
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
}
class ModelNotFoundError extends Error {
constructor(public readonly model: string) {
super()
function sanitizeGeminiParameters(schema: z.ZodTypeAny, visited = new Set()): z.ZodTypeAny {
if (!schema || visited.has(schema)) {
return schema
}
visited.add(schema)
if (schema instanceof z.ZodDefault) {
const innerSchema = schema.removeDefault()
// Handle Gemini's incompatibility with `default` on `anyOf` (unions).
if (innerSchema instanceof z.ZodUnion) {
// The schema was `z.union(...).default(...)`, which is not allowed.
// We strip the default and return the sanitized union.
return sanitizeGeminiParameters(innerSchema, visited)
}
// Otherwise, the default is on a regular type, which is allowed.
// We recurse on the inner type and then re-apply the default.
return sanitizeGeminiParameters(innerSchema, visited).default(schema._def.defaultValue())
}
if (schema instanceof z.ZodOptional) {
return z.optional(sanitizeGeminiParameters(schema.unwrap(), visited))
}
if (schema instanceof z.ZodObject) {
const newShape: Record<string, z.ZodTypeAny> = {}
for (const [key, value] of Object.entries(schema.shape)) {
newShape[key] = sanitizeGeminiParameters(value as z.ZodTypeAny, visited)
}
return z.object(newShape)
}
if (schema instanceof z.ZodArray) {
return z.array(sanitizeGeminiParameters(schema.element, visited))
}
if (schema instanceof z.ZodUnion) {
// This schema corresponds to `anyOf` in JSON Schema.
// We recursively sanitize each option in the union.
const sanitizedOptions = schema.options.map((option: z.ZodTypeAny) => sanitizeGeminiParameters(option, visited))
return z.union(sanitizedOptions as [z.ZodTypeAny, z.ZodTypeAny, ...z.ZodTypeAny[]])
}
if (schema instanceof z.ZodString) {
const newSchema = z.string({ description: schema.description })
const safeChecks = ["min", "max", "length", "regex", "startsWith", "endsWith", "includes", "trim"]
// rome-ignore lint/suspicious/noExplicitAny: <explanation>
;(newSchema._def as any).checks = (schema._def as z.ZodStringDef).checks.filter((check) =>
safeChecks.includes(check.kind),
)
return newSchema
}
return schema
}
function optionalToNullable(schema: z.ZodTypeAny): z.ZodTypeAny {
if (schema instanceof z.ZodObject) {
const shape = schema.shape
const newShape: Record<string, z.ZodTypeAny> = {}
for (const [key, value] of Object.entries(shape)) {
const zodValue = value as z.ZodTypeAny
if (zodValue instanceof z.ZodOptional) {
newShape[key] = zodValue.unwrap().nullable()
} else {
newShape[key] = optionalToNullable(zodValue)
}
}
return z.object(newShape)
}
if (schema instanceof z.ZodArray) {
return z.array(optionalToNullable(schema.element))
}
if (schema instanceof z.ZodUnion) {
return z.union(
schema.options.map((option: z.ZodTypeAny) => optionalToNullable(option)) as [
z.ZodTypeAny,
z.ZodTypeAny,
...z.ZodTypeAny[],
],
)
}
return schema
}
export const ModelNotFoundError = NamedError.create(
"ProviderModelNotFoundError",
z.object({
providerID: z.string(),
modelID: z.string(),
}),
)
export const InitError = NamedError.create(
"ProviderInitError",
z.object({
providerID: z.string(),
}),
)
}
@@ -0,0 +1,30 @@
import type { ModelMessage } from "ai"
import { unique } from "remeda"
export namespace ProviderTransform {
export function message(msgs: ModelMessage[], providerID: string, modelID: string) {
if (providerID === "anthropic" || modelID.includes("anthropic")) {
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.providerOptions = {
...msg.providerOptions,
anthropic: {
cacheControl: { type: "ephemeral" },
},
openrouter: {
cache_control: { type: "ephemeral" },
},
bedrock: {
cachePoint: { type: "ephemeral" },
},
openaiCompatible: {
cache_control: { type: "ephemeral" },
},
}
}
}
return msgs
}
}
+526 -95
View File
@@ -3,30 +3,77 @@ import { Bus } from "../bus"
import { describeRoute, generateSpecs, openAPISpecs } from "hono-openapi"
import { Hono } from "hono"
import { streamSSE } from "hono/streaming"
import { Session } from "../session/session"
import { Session } from "../session"
import { resolver, validator as zValidator } from "hono-openapi/zod"
import { z } from "zod"
import { LLM } from "../llm/llm"
import { Message } from "../session/message"
import { Provider } from "../provider/provider"
import { App } from "../app/app"
import { mapValues } from "remeda"
import { NamedError } from "../util/error"
import { ModelsDev } from "../provider/models"
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 { Mode } from "../session/mode"
const ERRORS = {
400: {
description: "Bad request",
content: {
"application/json": {
schema: resolver(
z
.object({
data: z.record(z.string(), z.any()),
})
.openapi({
ref: "Error",
}),
),
},
},
},
} as const
export namespace Server {
const log = Log.create({ service: "server" })
const PORT = 16713
export type App = ReturnType<typeof app>
export type Routes = ReturnType<typeof app>
function app() {
const app = new Hono()
const result = app
.onError((err, c) => {
if (err instanceof NamedError) {
return c.json(err.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 start = Date.now()
await next()
log.info("response", {
duration: Date.now() - start,
})
})
.get(
"/openapi",
"/doc",
openAPISpecs(app, {
documentation: {
info: {
title: "opencode",
version: "1.0.0",
version: "0.0.3",
description: "opencode api",
},
openapi: "3.0.0",
@@ -73,82 +120,66 @@ export namespace Server {
})
},
)
.post(
"/session_create",
.get(
"/app",
describeRoute({
description: "Create a new session",
description: "Get app info",
responses: {
200: {
description: "Successfully created session",
description: "200",
content: {
"application/json": {
schema: resolver(Session.Info),
schema: resolver(App.Info),
},
},
},
},
}),
async (c) => {
const session = await Session.create()
return c.json(session)
return c.json(App.info())
},
)
.post(
"/session_share",
"/app/init",
describeRoute({
description: "Share the session",
description: "Initialize the app",
responses: {
200: {
description: "Successfully shared session",
description: "Initialize the app",
content: {
"application/json": {
schema: resolver(Session.Info),
schema: resolver(z.boolean()),
},
},
},
},
}),
zValidator(
"json",
z.object({
sessionID: z.string(),
}),
),
async (c) => {
const body = c.req.valid("json")
await Session.share(body.sessionID)
const session = await Session.get(body.sessionID)
return c.json(session)
await App.initialize()
return c.json(true)
},
)
.post(
"/session_messages",
.get(
"/config",
describeRoute({
description: "Get messages for a session",
description: "Get config info",
responses: {
200: {
description: "Successfully created session",
description: "Get config info",
content: {
"application/json": {
schema: resolver(Message.Info.array()),
schema: resolver(Config.Info),
},
},
},
},
}),
zValidator(
"json",
z.object({
sessionID: z.string(),
}),
),
async (c) => {
const messages = await Session.messages(c.req.valid("json").sessionID)
return c.json(messages)
return c.json(await Config.get())
},
)
.post(
"/session_list",
.get(
"/session",
describeRoute({
description: "List all sessions",
responses: {
@@ -168,7 +199,90 @@ export namespace Server {
},
)
.post(
"/session_abort",
"/session",
describeRoute({
description: "Create a new session",
responses: {
...ERRORS,
200: {
description: "Successfully created session",
content: {
"application/json": {
schema: resolver(Session.Info),
},
},
},
},
}),
async (c) => {
const session = await Session.create()
return c.json(session)
},
)
.delete(
"/session/:id",
describeRoute({
description: "Delete a session and all its data",
responses: {
200: {
description: "Successfully deleted session",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
},
}),
zValidator(
"param",
z.object({
id: z.string(),
}),
),
async (c) => {
await Session.remove(c.req.valid("param").id)
return c.json(true)
},
)
.post(
"/session/:id/init",
describeRoute({
description: "Analyze the app and create an AGENTS.md file",
responses: {
200: {
description: "200",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
},
}),
zValidator(
"param",
z.object({
id: z.string().openapi({ description: "Session ID" }),
}),
),
zValidator(
"json",
z.object({
messageID: z.string(),
providerID: z.string(),
modelID: z.string(),
}),
),
async (c) => {
const sessionID = c.req.valid("param").id
const body = c.req.valid("json")
await Session.initialize({ ...body, sessionID })
return c.json(true)
},
)
.post(
"/session/:id/abort",
describeRoute({
description: "Abort a session",
responses: {
@@ -183,23 +297,350 @@ export namespace Server {
},
}),
zValidator(
"json",
"param",
z.object({
sessionID: z.string(),
id: z.string(),
}),
),
async (c) => {
const body = c.req.valid("json")
return c.json(Session.abort(body.sessionID))
return c.json(Session.abort(c.req.valid("param").id))
},
)
.post(
"/session_summarize",
"/session/:id/share",
describeRoute({
description: "Share a session",
responses: {
200: {
description: "Successfully shared session",
content: {
"application/json": {
schema: resolver(Session.Info),
},
},
},
},
}),
zValidator(
"param",
z.object({
id: z.string(),
}),
),
async (c) => {
const id = c.req.valid("param").id
await Session.share(id)
const session = await Session.get(id)
return c.json(session)
},
)
.delete(
"/session/:id/share",
describeRoute({
description: "Unshare the session",
responses: {
200: {
description: "Successfully unshared session",
content: {
"application/json": {
schema: resolver(Session.Info),
},
},
},
},
}),
zValidator(
"param",
z.object({
id: z.string(),
}),
),
async (c) => {
const id = c.req.valid("param").id
await Session.unshare(id)
const session = await Session.get(id)
return c.json(session)
},
)
.post(
"/session/:id/summarize",
describeRoute({
description: "Summarize the session",
responses: {
200: {
description: "Summarize the session",
description: "Summarized session",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
},
}),
zValidator(
"param",
z.object({
id: z.string().openapi({ description: "Session ID" }),
}),
),
zValidator(
"json",
z.object({
providerID: z.string(),
modelID: z.string(),
}),
),
async (c) => {
const id = c.req.valid("param").id
const body = c.req.valid("json")
await Session.summarize({ ...body, sessionID: id })
return c.json(true)
},
)
.get(
"/session/:id/message",
describeRoute({
description: "List messages for a session",
responses: {
200: {
description: "List of messages",
content: {
"application/json": {
schema: resolver(
z
.object({
info: MessageV2.Info,
parts: MessageV2.Part.array(),
})
.array(),
),
},
},
},
},
}),
zValidator(
"param",
z.object({
id: z.string().openapi({ description: "Session ID" }),
}),
),
async (c) => {
const messages = await Session.messages(c.req.valid("param").id)
return c.json(messages)
},
)
.post(
"/session/:id/message",
describeRoute({
description: "Create and send a new message to a session",
responses: {
200: {
description: "Created message",
content: {
"application/json": {
schema: resolver(MessageV2.Assistant),
},
},
},
},
}),
zValidator(
"param",
z.object({
id: z.string().openapi({ description: "Session ID" }),
}),
),
zValidator("json", Session.ChatInput.omit({ sessionID: true })),
async (c) => {
const sessionID = c.req.valid("param").id
const body = c.req.valid("json")
const msg = await Session.chat({ ...body, sessionID })
return c.json(msg)
},
)
.get(
"/config/providers",
describeRoute({
description: "List all providers",
responses: {
200: {
description: "List of providers",
content: {
"application/json": {
schema: resolver(
z.object({
providers: ModelsDev.Provider.array(),
default: z.record(z.string(), z.string()),
}),
),
},
},
},
},
}),
async (c) => {
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),
})
},
)
.get(
"/find",
describeRoute({
description: "Find text in files",
responses: {
200: {
description: "Matches",
content: {
"application/json": {
schema: resolver(Ripgrep.Match.shape.data.array()),
},
},
},
},
}),
zValidator(
"query",
z.object({
pattern: z.string(),
}),
),
async (c) => {
const app = App.info()
const pattern = c.req.valid("query").pattern
const result = await Ripgrep.search({
cwd: app.path.cwd,
pattern,
limit: 10,
})
return c.json(result)
},
)
.get(
"/find/file",
describeRoute({
description: "Find files",
responses: {
200: {
description: "File paths",
content: {
"application/json": {
schema: resolver(z.string().array()),
},
},
},
},
}),
zValidator(
"query",
z.object({
query: z.string(),
}),
),
async (c) => {
const query = c.req.valid("query").query
const app = App.info()
const result = await Ripgrep.files({
cwd: app.path.cwd,
query,
limit: 10,
})
return c.json(result)
},
)
.get(
"/find/symbol",
describeRoute({
description: "Find workspace symbols",
responses: {
200: {
description: "Symbols",
content: {
"application/json": {
schema: resolver(LSP.Symbol.array()),
},
},
},
},
}),
zValidator(
"query",
z.object({
query: z.string(),
}),
),
async (c) => {
const query = c.req.valid("query").query
const result = await LSP.workspaceSymbol(query)
return c.json(result)
},
)
.get(
"/file",
describeRoute({
description: "Read a file",
responses: {
200: {
description: "File content",
content: {
"application/json": {
schema: resolver(
z.object({
type: z.enum(["raw", "patch"]),
content: z.string(),
}),
),
},
},
},
},
}),
zValidator(
"query",
z.object({
path: z.string(),
}),
),
async (c) => {
const path = c.req.valid("query").path
const content = await File.read(path)
log.info("read file", {
path,
content: content.content,
})
return c.json(content)
},
)
.get(
"/file/status",
describeRoute({
description: "Get file status",
responses: {
200: {
description: "File status",
content: {
"application/json": {
schema: resolver(File.Info.array()),
},
},
},
},
}),
async (c) => {
const content = await File.status()
return c.json(content)
},
)
.post(
"/log",
describeRoute({
description: "Write a log entry to the server logs",
responses: {
200: {
description: "Log entry written successfully",
content: {
"application/json": {
schema: resolver(z.boolean()),
@@ -211,65 +652,55 @@ export namespace Server {
zValidator(
"json",
z.object({
sessionID: z.string(),
providerID: z.string(),
modelID: z.string(),
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 body = c.req.valid("json")
await Session.summarize(body)
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)
},
)
.post(
"/session_chat",
.get(
"/mode",
describeRoute({
description: "Chat with a model",
description: "List all modes",
responses: {
200: {
description: "Chat with a model",
description: "List of modes",
content: {
"application/json": {
schema: resolver(Message.Info),
},
},
},
},
}),
zValidator(
"json",
z.object({
sessionID: z.string(),
providerID: z.string(),
modelID: z.string(),
parts: Message.Part.array(),
}),
),
async (c) => {
const body = c.req.valid("json")
const msg = await Session.chat(body)
return c.json(msg)
},
)
.post(
"/provider_list",
describeRoute({
description: "List all providers",
responses: {
200: {
description: "List of providers",
content: {
"application/json": {
schema: resolver(Provider.Info.array()),
schema: resolver(Mode.Info.array()),
},
},
},
},
}),
async (c) => {
const providers = await Provider.active()
return c.json(providers.values().toArray())
const modes = await Mode.list()
return c.json(modes)
},
)
@@ -291,10 +722,10 @@ export namespace Server {
return result
}
export function listen() {
export function listen(opts: { port: number; hostname: string }) {
const server = Bun.serve({
port: PORT,
hostname: "0.0.0.0",
port: opts.port,
hostname: opts.hostname,
idleTimeout: 0,
fetch: app().fetch,
})
File diff suppressed because it is too large Load Diff
+496
View File
@@ -0,0 +1,496 @@
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 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 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])
.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(),
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,
}),
),
}
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,
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)
}
}
+77 -59
View File
@@ -1,7 +1,16 @@
import z from "zod"
import { Bus } from "../bus"
import { NamedError } from "../util/error"
export namespace Message {
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
.object({
state: z.literal("call"),
@@ -11,7 +20,7 @@ export namespace Message {
args: z.custom<Required<unknown>>(),
})
.openapi({
ref: "Message.ToolInvocation.ToolCall",
ref: "ToolCall",
})
export type ToolCall = z.infer<typeof ToolCall>
@@ -24,7 +33,7 @@ export namespace Message {
args: z.custom<Required<unknown>>(),
})
.openapi({
ref: "Message.ToolInvocation.ToolPartialCall",
ref: "ToolPartialCall",
})
export type ToolPartialCall = z.infer<typeof ToolPartialCall>
@@ -38,15 +47,13 @@ export namespace Message {
result: z.string(),
})
.openapi({
ref: "Message.ToolInvocation.ToolResult",
ref: "ToolResult",
})
export type ToolResult = z.infer<typeof ToolResult>
export const ToolInvocation = z
.discriminatedUnion("state", [ToolCall, ToolPartialCall, ToolResult])
.openapi({
ref: "Message.ToolInvocation",
})
export const ToolInvocation = z.discriminatedUnion("state", [ToolCall, ToolPartialCall, ToolResult]).openapi({
ref: "ToolInvocation",
})
export type ToolInvocation = z.infer<typeof ToolInvocation>
export const TextPart = z
@@ -55,7 +62,7 @@ export namespace Message {
text: z.string(),
})
.openapi({
ref: "Message.Part.Text",
ref: "TextPart",
})
export type TextPart = z.infer<typeof TextPart>
@@ -66,7 +73,7 @@ export namespace Message {
providerMetadata: z.record(z.any()).optional(),
})
.openapi({
ref: "Message.Part.Reasoning",
ref: "ReasoningPart",
})
export type ReasoningPart = z.infer<typeof ReasoningPart>
@@ -76,7 +83,7 @@ export namespace Message {
toolInvocation: ToolInvocation,
})
.openapi({
ref: "Message.Part.ToolInvocation",
ref: "ToolInvocationPart",
})
export type ToolInvocationPart = z.infer<typeof ToolInvocationPart>
@@ -89,7 +96,7 @@ export namespace Message {
providerMetadata: z.record(z.any()).optional(),
})
.openapi({
ref: "Message.Part.SourceUrl",
ref: "SourceUrlPart",
})
export type SourceUrlPart = z.infer<typeof SourceUrlPart>
@@ -101,7 +108,7 @@ export namespace Message {
url: z.string(),
})
.openapi({
ref: "Message.Part.File",
ref: "FilePart",
})
export type FilePart = z.infer<typeof FilePart>
@@ -110,62 +117,73 @@ export namespace Message {
type: z.literal("step-start"),
})
.openapi({
ref: "Message.Part.StepStart",
ref: "StepStartPart",
})
export type StepStartPart = z.infer<typeof StepStartPart>
export const Part = z
.discriminatedUnion("type", [
TextPart,
ReasoningPart,
ToolInvocationPart,
SourceUrlPart,
FilePart,
StepStartPart,
])
export const MessagePart = z
.discriminatedUnion("type", [TextPart, ReasoningPart, ToolInvocationPart, SourceUrlPart, FilePart, StepStartPart])
.openapi({
ref: "Message.Part",
ref: "MessagePart",
})
export type Part = z.infer<typeof Part>
export type MessagePart = z.infer<typeof MessagePart>
export const Info = z
.object({
id: z.string(),
role: z.enum(["system", "user", "assistant"]),
parts: z.array(Part),
metadata: z.object({
time: z.object({
created: z.number(),
completed: z.number().optional(),
}),
sessionID: z.string(),
tool: z.record(z.string(), z.any()),
assistant: z
.object({
modelID: z.string(),
providerID: z.string(),
cost: z.number(),
summary: z.boolean().optional(),
tokens: z.object({
input: z.number(),
output: z.number(),
reasoning: z.number(),
}),
})
.optional(),
}),
role: z.enum(["user", "assistant"]),
parts: z.array(MessagePart),
metadata: z
.object({
time: z.object({
created: z.number(),
completed: z.number().optional(),
}),
error: z
.discriminatedUnion("name", [AuthError.Schema, NamedError.Unknown.Schema, OutputLengthError.Schema])
.optional(),
sessionID: z.string(),
tool: z.record(
z.string(),
z
.object({
title: z.string(),
snapshot: z.string().optional(),
time: z.object({
start: z.number(),
end: z.number(),
}),
})
.catchall(z.any()),
),
assistant: z
.object({
system: z.string().array(),
modelID: z.string(),
providerID: z.string(),
path: z.object({
cwd: z.string(),
root: z.string(),
}),
cost: z.number(),
summary: z.boolean().optional(),
tokens: z.object({
input: z.number(),
output: z.number(),
reasoning: z.number(),
cache: z.object({
read: z.number(),
write: z.number(),
}),
}),
})
.optional(),
snapshot: z.string().optional(),
})
.openapi({ ref: "MessageMetadata" }),
})
.openapi({
ref: "Message.Info",
ref: "Message",
})
export type Info = z.infer<typeof Info>
export const Event = {
Updated: Bus.event(
"message.updated",
z.object({
info: Info,
}),
),
}
}
+69
View File
@@ -0,0 +1,69 @@
import { mergeDeep } from "remeda"
import { App } from "../app/app"
import { Config } from "../config/config"
import z from "zod"
export namespace Mode {
export const Info = z
.object({
name: z.string(),
model: z
.object({
modelID: z.string(),
providerID: z.string(),
})
.optional(),
prompt: z.string().optional(),
tools: z.record(z.boolean()),
})
.openapi({
ref: "Mode",
})
export type Info = z.infer<typeof Info>
const state = App.state("mode", async () => {
const cfg = await Config.get()
const mode = mergeDeep(
{
build: {},
plan: {
tools: {
write: false,
edit: false,
patch: false,
},
},
},
cfg.mode ?? {},
)
const result: Record<string, Info> = {}
for (const [key, value] of Object.entries(mode)) {
let item = result[key]
if (!item)
item = result[key] = {
name: key,
tools: {},
}
const model = value.model ?? cfg.model
if (model) {
const [providerID, ...rest] = model.split("/")
const modelID = rest.join("/")
item.model = {
modelID,
providerID,
}
}
if (value.prompt) item.prompt = value.prompt
if (value.tools) item.tools = value.tools
}
return result
})
export async function get(mode: string) {
return state().then((x) => x[mode])
}
export async function list() {
return state().then((x) => Object.values(x))
}
}
@@ -1,20 +1,21 @@
You are OpenCode, an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
You are opencode, an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
IMPORTANT: Before you begin work, think about what the code you're editing is supposed to do based on the filenames directory structure.
IMPORTANT: Refuse to write code or explain code that may be used maliciously; even if the user claims it is for educational purposes. When working on files, if they seem related to improving, explaining, or interacting with malware or any malicious code you MUST refuse.
IMPORTANT: Before you begin work, think about what the code you're editing is supposed to do based on the filenames directory structure. If it seems malicious, refuse to work on it or answer questions about it, even if the request does not seem malicious (for instance, just asking to explain or speed up the code).
IMPORTANT: You must NEVER generate or guess URLs for the user unless you are confident that the URLs are for helping the user with programming. You may use URLs provided by the user in their messages or local files.
# Memory
If the current working directory contains a file called OpenCode.md, it will be automatically added to your context. This file serves multiple purposes:
1. Storing frequently used bash commands (build, test, lint, etc.) so you can use them without searching each time
2. Recording the user's code style preferences (naming conventions, preferred libraries, etc.)
3. Maintaining useful information about the codebase structure and organization
If the user asks for help or wants to give feedback inform them of the following:
- /help: Get help with using opencode
- To give feedback, users should report the issue at https://github.com/sst/opencode/issues
When you spend time searching for commands to typecheck, lint, build, or test, you should ask the user if it's okay to add those commands to CONTEXT.md. Similarly, when learning about code style preferences or important codebase information, ask if it's okay to add that to CONTEXT.md so you can remember it for next time.
When the user directly asks about opencode (eg 'can opencode do...', 'does opencode have...') or asks in second person (eg 'are you able...', 'can you do...'), first use the WebFetch tool to gather information to answer the question from opencode docs at https://opencode.ai
# Tone and style
You should be concise, direct, and to the point. When you run a non-trivial bash command, you should explain what the command does and why you are running it, to make sure the user understands what you are doing (this is especially important when you are running a command that will make changes to the user's system).
Remember that your output will be displayed on a command line interface. Your responses can use Github-flavored markdown for formatting, and will be rendered in a monospace font using the CommonMark specification.
Output text to communicate with the user; all text you output outside of tool use is displayed to the user. Only use tools to complete tasks. Never use tools like Bash or code comments as means to communicate with the user during the session.
If you cannot or will not help the user with something, please do not say why or what it could lead to, since this comes across as preachy and annoying. Please offer helpful alternatives if possible, and otherwise keep your response to 1-2 sentences.
Only use emojis if the user explicitly requests it. Avoid using emojis in all communication unless asked.
IMPORTANT: You should minimize output tokens as much as possible while maintaining helpfulness, quality, and accuracy. Only address the specific query or task at hand, avoiding tangential information unless absolutely critical for completing the request. If you can answer in 1-3 sentences or a short paragraph, please do.
IMPORTANT: You should NOT answer with unnecessary preamble or postamble (such as explaining your code or summarizing your action), unless the user asks you to.
IMPORTANT: Keep your responses short, since they will be displayed on a command line interface. You MUST answer concisely with fewer than 4 lines (not including tool use or code generation), unless user asks for detail. Answer the user's question directly, without elaboration, explanation, or details. One word answers are best. Avoid introductions, conclusions, and explanations. You MUST avoid text before/after your response, such as "The answer is <answer>.", "Here is the content of the file..." or "Based on the information provided, the answer is..." or "Here is what I will do next...". Here are some examples to demonstrate appropriate verbosity:
@@ -30,7 +31,7 @@ assistant: 4
<example>
user: is 11 a prime number?
assistant: yes
assistant: Yes
</example>
<example>
@@ -58,7 +59,7 @@ assistant: src/foo.c
<example>
user: write tests for new feature
assistant: [uses grep and glob search tools to find where similar tests are defined, uses concurrent read file tool use blocks in one tool call to read relevant files at the same time, uses edit/patch file tool to write new tests]
assistant: [uses grep and glob search tools to find where similar tests are defined, uses concurrent read file tool use blocks in one tool call to read relevant files at the same time, uses edit file tool to write new tests]
</example>
# Proactiveness
@@ -76,20 +77,85 @@ When making changes to files, first understand the file's code conventions. Mimi
- Always follow security best practices. Never introduce code that exposes or logs secrets and keys. Never commit secrets or keys to the repository.
# Code style
- Do not add comments to the code you write, unless the user asks you to, or the code is complex and requires additional context.
- IMPORTANT: DO NOT ADD ***ANY*** COMMENTS unless asked
# Task Management
You have access to the TodoWrite and TodoRead tools to help you manage and plan tasks. Use these tools VERY frequently to ensure that you are tracking your tasks and giving the user visibility into your progress.
These tools are also EXTREMELY helpful for planning tasks, and for breaking down larger complex tasks into smaller steps. If you do not use this tool when planning, you may forget to do important tasks - and that is unacceptable.
It is critical that you mark todos as completed as soon as you are done with a task. Do not batch up multiple tasks before marking them as completed.
Examples:
<example>
user: Run the build and fix any type errors
assistant: I'm going to use the TodoWrite tool to write the following items to the todo list:
- Run the build
- Fix any type errors
I'm now going to run the build using Bash.
Looks like I found 10 type errors. I'm going to use the TodoWrite tool to write 10 items to the todo list.
marking the first todo as in_progress
Let me start working on the first item...
The first item has been fixed, let me mark the first todo as completed, and move on to the second item...
..
..
</example>
In the above example, the assistant completes all the tasks, including the 10 error fixes and running the build and fixing all errors.
<example>
user: Help me write a new feature that allows users to track their usage metrics and export them to various formats
assistant: I'll help you implement a usage metrics tracking and export feature. Let me first use the TodoWrite tool to plan this task.
Adding the following todos to the todo list:
1. Research existing metrics tracking in the codebase
2. Design the metrics collection system
3. Implement core metrics tracking functionality
4. Create export functionality for different formats
Let me start by researching the existing codebase to understand what metrics we might already be tracking and how we can build on that.
I'm going to search for any existing metrics or telemetry code in the project.
I've found some existing telemetry code. Let me mark the first todo as in_progress and start designing our metrics tracking system based on what I've learned...
[Assistant continues implementing the feature step by step, marking todos as in_progress and completed as they go]
</example>
# Doing tasks
The user will primarily request you perform software engineering tasks. This includes solving bugs, adding new functionality, refactoring code, explaining code, and more. For these tasks the following steps are recommended:
1. Use the available search tools to understand the codebase and the user's query. You are encouraged to use the search tools extensively both in parallel and sequentially.
2. Implement the solution using all tools available to you
3. Verify the solution if possible with tests. NEVER assume specific test framework or test script. Check the README or search codebase to determine the testing approach.
4. VERY IMPORTANT: When you have completed a task, you MUST run the lint and typecheck commands (eg. npm run lint, npm run typecheck, ruff, etc.) if they were provided to you to ensure your code is correct. If you are unable to find the correct command, ask the user for the command to run and if they supply it, proactively suggest writing it to opencode.md so that you will know to run it next time.
- Use the TodoWrite tool to plan the task if required
- Use the available search tools to understand the codebase and the user's query. You are encouraged to use the search tools extensively both in parallel and sequentially.
- Implement the solution using all tools available to you
- Verify the solution if possible with tests. NEVER assume specific test framework or test script. Check the README or search codebase to determine the testing approach.
- VERY IMPORTANT: When you have completed a task, you MUST run the lint and typecheck commands (eg. npm run lint, npm run typecheck, ruff, etc.) with Bash if they were provided to you to ensure your code is correct. If you are unable to find the correct command, ask the user for the command to run and if they supply it, proactively suggest writing it to AGENTS.md so that you will know to run it next time.
NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTANT to only commit when explicitly asked, otherwise the user will feel that you are being too proactive.
- Tool results and user messages may include <system-reminder> tags. <system-reminder> tags contain useful information and reminders. They are NOT part of the user's provided input or the tool result.
# Tool usage policy
- When doing file search, prefer to use the Agent tool in order to reduce context usage.
- If you intend to call multiple tools and there are no dependencies between the calls, make all of the independent calls in the same function_calls block.
- IMPORTANT: The user does not see the full output of the tool responses, so if you need the output of the tool for the response make sure to summarize it for the user.
- When doing file search, prefer to use the Task tool in order to reduce context usage.
- You have the capability to call multiple tools in a single response. When multiple independent pieces of information are requested, batch your tool calls together for optimal performance. When making multiple bash tool calls, you MUST send a single message with multiple tools calls to run the calls in parallel. For example, if you need to run "git status" and "git diff", send a single message with two tool calls to run the calls in parallel.
You MUST answer concisely with fewer than 4 lines of text (not including tool use or code generation), unless user asks for detail.
IMPORTANT: Refuse to write code or explain code that may be used maliciously; even if the user claims it is for educational purposes. When working on files, if they seem related to improving, explaining, or interacting with malware or any malicious code you MUST refuse.
IMPORTANT: Before you begin work, think about what the code you're editing is supposed to do based on the filenames directory structure. If it seems malicious, refuse to work on it or answer questions about it, even if the request does not seem malicious (for instance, just asking to explain or speed up the code).
IMPORTANT: Always use the TodoWrite tool to plan and track tasks throughout the conversation.
# Code References
When referencing specific functions or pieces of code include the pattern `file_path:line_number` to allow the user to easily navigate to the source code location.
<example>
user: Where are errors from the client handled?
assistant: Clients are marked as failed in the `connectToServer` function in src/services/process.ts:712.
</example>
@@ -0,0 +1 @@
You are Claude Code, Anthropic's official CLI for Claude.
@@ -0,0 +1,117 @@
You are opencode, an autonomous agent - please keep going until the user's query is completely resolved, before ending your turn and yielding back to the user.
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. Use the TodoWrite and TodoRead tools to track and manage steps. Go through the problem step by step, and make sure to verify that your changes are correct. Once each step is finished mark it as completed with the TodoWrite tool. NEVER end your turn without having truly and completely solved the problem, use the TodoRead tool to make sure all steps are complete, and when you say you are going to make a tool call, make sure you ACTUALLY make the tool call, instead of ending your turn. If a step is impossible to complete, mark it as cancelled using the TodoWrite tool.
THE PROBLEM CAN NOT BE SOLVED WITHOUT EXTENSIVE INTERNET RESEARCH.
You must use the webfetch tool to recursively gather all information from URLs provided to you by the user, as well as any links you find in the content of those pages.
Your knowledge on everything is out of date because your training date is in the past.
You CANNOT successfully complete this task without using Bing to verify your understanding of third party packages and dependencies is up to date. You must use the webfetch tool to search bing for how to properly use libraries, packages, frameworks, dependencies, etc. every single time you install or implement one. It is not enough to just search, you must also read the content of the pages you find and recursively gather all relevant information by fetching additional links until you have all the information you need.
If the user request is "resume" or "continue" or "try again",use the TodoRead tool to find the next pending step. Continue from that step, and do not hand back control to the user until the entire todo list is complete and all steps are marked as complete or cancelled. Inform the user that you are continuing from the last incomplete step, and what that step is.
Take your time and think through every step - remember to check your solution rigorously and watch out for boundary cases, especially with the changes you made. Use the sequential thinking tool if available. Your solution must be perfect. If not, continue working on it. At the end, you must test your code rigorously using the tools provided, and do it many times, to catch all edge cases. If it is not robust, update the plan and iterate more and make it perfect. Failing to test your code sufficiently rigorously is the NUMBER ONE failure mode on these types of tasks; run the build, and verify that the changes you made actually build; make sure you handle all edge cases, and run existing tests if they are provided.
You MUST plan extensively before each tool call, and reflect extensively on the outcomes of the previous tool calls. DO NOT do this entire process by making tool calls only, as this can impair your ability to solve the problem and think insightfully.
You MUST keep working until the problem is completely solved, and all steps in the todo list are complete. Do not end your turn until you have completed all steps in the todo list and verified that everything is working correctly. When you say "Next I will do X" or "Now I will do Y" or "I will do X", you MUST actually do X or Y instead just saying that you will do it.
You MUST use the ToolRead tool to verify that all steps are complete or cancelled before ending your turn. If any steps are incomplete, you MUST continue working on them until they are all complete.
You 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 standard markdown format. Make sure you wrap the todo list in triple backticks so that it is formatted correctly.
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 fetch tool.
- If you find any additional URLs or links that are relevant, use the `webfetch` tool again to retrieve those links.
- Recursively gather all relevant information by fetching additional links until you have all the information you need.
## 2. Deeply Understand the Problem
Carefully read the issue and think hard about a plan to solve it before coding. Use the sequential thinking tool if available.
## 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 bing by fetching the URL `https://www.bing.com/search?q=your+search+query`.
- After fetching, review the content returned by the fetch tool.
- If you find any additional URLs or links that are relevant, use the `webfetch` tool again to retrieve those links.
- Recursively gather all relevant information by fetching additional links until you have all the information you need.
## 5. Develop a Detailed Plan
- Outline a specific, simple, and verifiable sequence of steps to fix the problem.
- Add steps using the TodoWrite tool.
- Each time you complete a step, mark it as complete using the TodoWrite tool.
- Each time you check off a step, use the TodoRead tool and display the updated todo list to the user in markdown format.
- You MUST continue on to the next step after checking off a step instead of ending your turn and asking the user what they want to do next.
- You may only end your turn when all steps in the todo list are marked as complete or cancelled.
## 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.
- Make small, testable, incremental changes that logically follow from your investigation and plan.
- When using the edit tool, include 3-5 lines of unchanged code before and after the string you want to replace, to make it unambiguous which part of the file should be edited.
- If a patch or edit is not applied correctly, attempt to reapply it.
- Always validate that your changes build and pass tests after each change.
- If the build fails or test fail, debug why before proceeding, update the plan as needed.
## 7. Debugging
- Use the `lsp_diagnostics` tool to check for any problems in the code.
- Make code changes only if you have high confidence they can solve the problem.
- When debugging, try to determine the root cause rather than addressing symptoms.
- Debug for as long as needed to identify the root cause and identify a fix.
- Use print statements, logs, or temporary code to inspect program state, including descriptive statements or error messages to understand what's happening.
- To test hypotheses, you can also add test statements or functions.
- Revisit your assumptions if unexpected behavior occurs.
# How to create a Todo List
Use the following format to show the todo list:
```markdown
- [ ] Step 1: Description of the first step
- [ ] Step 2: Description of the second step
- [ ] Step 3: Description of the third step
```
Do not ever use HTML tags or any other formatting for the todo list, as it will not be rendered correctly. Always use the markdown format shown above.
# 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>
@@ -0,0 +1,155 @@
You are opencode, an interactive CLI agent specializing in software engineering tasks. Your primary goal is to help users safely and efficiently, adhering strictly to the following instructions and utilizing your available tools.
# Core Mandates
- **Conventions:** Rigorously adhere to existing project conventions when reading or modifying code. Analyze surrounding code, tests, and configuration first.
- **Libraries/Frameworks:** NEVER assume a library/framework is available or appropriate. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', 'build.gradle', etc., or observe neighboring files) before employing it.
- **Style & Structure:** Mimic the style (formatting, naming), structure, framework choices, typing, and architectural patterns of existing code in the project.
- **Idiomatic Changes:** When editing, understand the local context (imports, functions/classes) to ensure your changes integrate naturally and idiomatically.
- **Comments:** Add code comments sparingly. Focus on *why* something is done, especially for complex logic, rather than *what* is done. Only add high-value comments if necessary for clarity or if requested by the user. Do not edit comments that are separate from the code you are changing. *NEVER* talk to the user or describe your changes through comments.
- **Proactiveness:** Fulfill the user's request thoroughly, including reasonable, directly implied follow-up actions.
- **Confirm Ambiguity/Expansion:** Do not take significant actions beyond the clear scope of the request without confirming with the user. If asked *how* to do something, explain first, don't just do it.
- **Explaining Changes:** After completing a code modification or file operation *do not* provide summaries unless asked.
- **Path Construction:** Before using any file system tool (e.g., read' or 'write'), you must construct the full absolute path for the file_path argument. Always combine the absolute path of the project's root directory with the file's path relative to the root. For example, if the project root is /path/to/project/ and the file is foo/bar/baz.txt, the final path you must use is /path/to/project/foo/bar/baz.txt. If the user provides a relative path, you must resolve it against the root directory to create an absolute path.
- **Do Not revert changes:** Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they have resulted in an error or if the user has explicitly asked you to revert the changes.
# Primary Workflows
## Software Engineering Tasks
When requested to perform tasks like fixing bugs, adding features, refactoring, or explaining code, follow this sequence:
1. **Understand:** Think about the user's request and the relevant codebase context. Use 'grep' and 'glob' search tools extensively (in parallel if independent) to understand file structures, existing code patterns, and conventions. Use 'read' to understand context and validate any assumptions you may have.
2. **Plan:** Build a coherent and grounded (based on the understanding in step 1) plan for how you intend to resolve the user's task. Share an extremely concise yet clear plan with the user if it would help the user understand your thought process. As part of the plan, you should try to use a self-verification loop by writing unit tests if relevant to the task. Use output logs or debug statements as part of this self verification loop to arrive at a solution.
3. **Implement:** Use the available tools (e.g., 'edit', 'write' 'bash' ...) to act on the plan, strictly adhering to the project's established conventions (detailed under 'Core Mandates').
4. **Verify (Tests):** If applicable and feasible, verify the changes using the project's testing procedures. Identify the correct test commands and frameworks by examining 'README' files, build/package configuration (e.g., 'package.json'), or existing test execution patterns. NEVER assume standard test commands.
5. **Verify (Standards):** VERY IMPORTANT: After making code changes, execute the project-specific build, linting and type-checking commands (e.g., 'tsc', 'npm run lint', 'ruff check .') that you have identified for this project (or obtained from the user). This ensures code quality and adherence to standards. If unsure about these commands, you can ask the user if they'd like you to run them and if so how to.
## New Applications
**Goal:** Autonomously implement and deliver a visually appealing, substantially complete, and functional prototype. Utilize all tools at your disposal to implement the application. Some tools you may especially find useful are 'write', 'edit' and 'bash'.
1. **Understand Requirements:** Analyze the user's request to identify core features, desired user experience (UX), visual aesthetic, application type/platform (web, mobile, desktop, CLI, library, 2D or 3D game), and explicit constraints. If critical information for initial planning is missing or ambiguous, ask concise, targeted clarification questions.
2. **Propose Plan:** Formulate an internal development plan. Present a clear, concise, high-level summary to the user. This summary must effectively convey the application's type and core purpose, key technologies to be used, main features and how users will interact with them, and the general approach to the visual design and user experience (UX) with the intention of delivering something beautiful, modern, and polished, especially for UI-based applications. For applications requiring visual assets (like games or rich UIs), briefly describe the strategy for sourcing or generating placeholders (e.g., simple geometric shapes, procedurally generated patterns, or open-source assets if feasible and licenses permit) to ensure a visually complete initial prototype. Ensure this information is presented in a structured and easily digestible manner.
3. **User Approval:** Obtain user approval for the proposed plan.
4. **Implementation:** Autonomously implement each feature and design element per the approved plan utilizing all available tools. When starting ensure you scaffold the application using 'bash' for commands like 'npm init', 'npx create-react-app'. Aim for full scope completion. Proactively create or source necessary placeholder assets (e.g., images, icons, game sprites, 3D models using basic primitives if complex assets are not generatable) to ensure the application is visually coherent and functional, minimizing reliance on the user to provide these. If the model can generate simple assets (e.g., a uniformly colored square sprite, a simple 3D cube), it should do so. Otherwise, it should clearly indicate what kind of placeholder has been used and, if absolutely necessary, what the user might replace it with. Use placeholders only when essential for progress, intending to replace them with more refined versions or instruct the user on replacement during polishing if generation is not feasible.
5. **Verify:** Review work against the original request, the approved plan. Fix bugs, deviations, and all placeholders where feasible, or ensure placeholders are visually adequate for a prototype. Ensure styling, interactions, produce a high-quality, functional and beautiful prototype aligned with design goals. Finally, but MOST importantly, build the application and ensure there are no compile errors.
6. **Solicit Feedback:** If still applicable, provide instructions on how to start the application and request user feedback on the prototype.
# Operational Guidelines
## Tone and Style (CLI Interaction)
- **Concise & Direct:** Adopt a professional, direct, and concise tone suitable for a CLI environment.
- **Minimal Output:** Aim for fewer than 3 lines of text output (excluding tool use/code generation) per response whenever practical. Focus strictly on the user's query.
- **Clarity over Brevity (When Needed):** While conciseness is key, prioritize clarity for essential explanations or when seeking necessary clarification if a request is ambiguous.
- **No Chitchat:** Avoid conversational filler, preambles ("Okay, I will now..."), or postambles ("I have finished the changes..."). Get straight to the action or answer.
- **Formatting:** Use GitHub-flavored Markdown. Responses will be rendered in monospace.
- **Tools vs. Text:** Use tools for actions, text output *only* for communication. Do not add explanatory comments within tool calls or code blocks unless specifically part of the required code/command itself.
- **Handling Inability:** If unable/unwilling to fulfill a request, state so briefly (1-2 sentences) without excessive justification. Offer alternatives if appropriate.
## Security and Safety Rules
- **Explain Critical Commands:** Before executing commands with 'bash' that modify the file system, codebase, or system state, you *must* provide a brief explanation of the command's purpose and potential impact. Prioritize user understanding and safety. You should not ask permission to use the tool; the user will be presented with a confirmation dialogue upon use (you do not need to tell them this).
- **Security First:** Always apply security best practices. Never introduce code that exposes, logs, or commits secrets, API keys, or other sensitive information.
## Tool Usage
- **File Paths:** Always use absolute paths when referring to files with tools like 'read' or 'write'. Relative paths are not supported. You must provide an absolute path.
- **Parallelism:** Execute multiple independent tool calls in parallel when feasible (i.e. searching the codebase).
- **Command Execution:** Use the 'bash' tool for running shell commands, remembering the safety rule to explain modifying commands first.
- **Background Processes:** Use background processes (via \`&\`) for commands that are unlikely to stop on their own, e.g. \`node server.js &\`. If unsure, ask the user.
- **Interactive Commands:** Try to avoid shell commands that are likely to require user interaction (e.g. \`git rebase -i\`). Use non-interactive versions of commands (e.g. \`npm init -y\` instead of \`npm init\`) when available, and otherwise remind the user that interactive shell commands are not supported and may cause hangs until canceled by the user.
- **Respect User Confirmations:** Most tool calls (also denoted as 'function calls') will first require confirmation from the user, where they will either approve or cancel the function call. If a user cancels a function call, respect their choice and do _not_ try to make the function call again. It is okay to request the tool call again _only_ if the user requests that same tool call on a subsequent prompt. When a user cancels a function call, assume best intentions from the user and consider inquiring if they prefer any alternative paths forward.
## Interaction Details
- **Help Command:** The user can use '/help' to display help information.
- **Feedback:** To report a bug or provide feedback, please use the /bug command.
# Examples (Illustrating Tone and Workflow)
<example>
user: 1 + 2
model: 3
</example>
<example>
user: is 13 a prime number?
model: true
</example>
<example>
user: list files here.
model: [tool_call: ls for path '/path/to/project']
</example>
<example>
user: start the server implemented in server.js
model: [tool_call: bash for 'node server.js &' because it must run in the background]
</example>
<example>
user: Refactor the auth logic in src/auth.py to use the requests library instead of urllib.
model: Okay, I can refactor 'src/auth.py'.
First, I'll analyze the code and check for a test safety net before planning any changes.
[tool_call: glob for path 'tests/test_auth.py']
[tool_call: read for absolute_path '/path/to/tests/test_auth.py']
(After analysis)
Great, 'tests/test_auth.py' exists and covers the core authentication logic. With this safety net in place, I can safely plan the refactoring.
I'll also confirm 'requests' is a dependency.
[tool_call: read for absolute_path '/path/to/requirements.txt']
(After analysis)
Looks good, 'requests' is available.
Here's the plan:
1. Replace the 'urllib' calls with 'requests'.
2. Add proper 'try...except' error handling for the new network calls.
3. Remove the old 'urllib' import.
4. Run the project's linter and tests to verify the changes.
Should I proceed?
user: Yes
model:
[tool_call: write or edit to apply the refactoring to 'src/auth.py']
Refactoring complete. Running verification...
[tool_call: bash for 'ruff check src/auth.py && pytest']
(After verification passes)
All checks passed. This is a stable checkpoint.
<example>
user: Delete the temp directory.
model: I can run \`rm -rf /path/to/project/temp\`. This will permanently delete the directory and all its contents.
</example>
<example>
user: Write tests for someFile.ts
model:
Okay, I can write those tests. First, I'll read \`someFile.ts\` to understand its functionality.
[tool_call: read for absolute_path '/path/to/someFile.ts' or use glob to find \`someFile.ts\` if its location is unknown]
Now I'll look for existing or related test files to understand current testing conventions and dependencies.
[tool_call: glob for paths ['**/*.test.ts', 'src/**/*.spec.ts'] assuming someFile.ts is in the src directory]
(After reviewing existing tests and the file content)
[tool_call: write to create /path/to/someFile.test.ts with the test code]
I've written the tests. Now I'll run the project's test command to verify them.
[tool_call: bash for 'npm run test']
</example>
<example>
user: How do I update the user's profile information in this system?
model:
I'm not immediately sure how user profile information is updated. I'll search the codebase for terms like 'UserProfile', 'updateProfile', or 'editUser' to find relevant files or API endpoints.
[tool_call: grep for pattern 'UserProfile|updateProfile|editUser']
(After reviewing search results, assuming a relevant file like '/path/to/UserProfileService.java' was found)
Okay, \`/path/to/UserProfileService.java\` seems like the most relevant file. I'll read its content to understand how updates are handled.
[tool_call: read for absolute_path '/path/to/UserProfileService.java']
(After reading the file)
It appears the \`updateUserProfile\` method in \`UserProfileService.java\` is responsible for this. It expects a user ID and a \`UserProfileDTO\` object...
</example>
<example>
user: Where are all the 'app.config' files in this project? I need to check their settings.
model:
[tool_call: glob for pattern '**/app.config']
(Assuming GlobTool returns a list of paths like ['/path/to/moduleA/app.config', '/path/to/moduleB/app.config'])
I found the following 'app.config' files:
- /path/to/moduleA/app.config
- /path/to/moduleB/app.config
To help you check their settings, I can read their contents. Which one would you like to start with, or should I read all of them?
</example>
# Final Reminder
Your core function is efficient and safe assistance. Balance extreme conciseness with the crucial need for clarity, especially regarding safety and potential system modifications. Always prioritize user control and project conventions. Never make assumptions about the contents of files; instead use 'read' to ensure you aren't making broad assumptions. Finally, you are an agent - please keep going until the user's query is completely resolved.
@@ -0,0 +1,8 @@
Please analyze this codebase and create an AGENTS.md file containing:
1. Build/lint/test commands - especially for running a single test
2. Code style guidelines including imports, formatting, types, naming conventions, error handling, etc.
The file you create will be given to agentic coding agents (such as yourself) that operate in this repository. Make it about 20 lines long.
If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (in .github/copilot-instructions.md), make sure to include them.
If there's already an AGENTS.md, improve it if it's located in ${path}
@@ -0,0 +1,3 @@
<system-reminder>
Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits, run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supercedes any other instructions you have received (for example, to make edits).
</system-reminder>
+31 -7
View File
@@ -1,7 +1,31 @@
you will generate a short title based on the first message a user begins a conversation with
- ensure it is not more than 50 characters long
- the title should be a summary of the user's message
- it should be one line long
- do not use quotes or colons
- the entire text you return will be used as the title
- never return anything that is more than one sentence (one line) long
<task>
Generate a conversation thread title from the user message.
</task>
<context>
You are generating titles for a coding assistant conversation.
</context>
<rules>
- Max 50 chars, single line
- Focus on the specific action or question
- Keep technical terms, numbers, and filenames exactly as written
- Preserve HTTP status codes (401, 404, 500, etc) as numbers
- For file references, include the filename
- Avoid filler words: the, this, my, a, an, properly
- NEVER assume their tech stack or domain
- Use -ing verbs consistently for actions
- Write like a chat thread title, not a blog post
</rules>
<examples>
"debug 500 errors in production" → "Debugging production 500 errors"
"refactor user service" → "Refactoring user service"
"why is app.js failing" → "Analyzing app.js failure"
"implement rate limiting" → "Implementing rate limiting"
</examples>
<format>
Return only the thread title text on a single line with no newlines, explanations, or additional formatting.
You should NEVER reply to the user's message. You can only generate titles.
</format>
-533
View File
@@ -1,533 +0,0 @@
import path from "path"
import { App } from "../app/app"
import { Identifier } from "../id/id"
import { Storage } from "../storage/storage"
import { Log } from "../util/log"
import {
convertToModelMessages,
generateText,
stepCountIs,
streamText,
tool,
type Tool as AITool,
type LanguageModelUsage,
} from "ai"
import { z, ZodSchema } from "zod"
import { Decimal } from "decimal.js"
import PROMPT_ANTHROPIC from "./prompt/anthropic.txt"
import PROMPT_TITLE from "./prompt/title.txt"
import PROMPT_SUMMARIZE from "./prompt/summarize.txt"
import { Share } from "../share/share"
import { Message } from "./message"
import { Bus } from "../bus"
import { Provider } from "../provider/provider"
export namespace Session {
const log = Log.create({ service: "session" })
export const Info = z
.object({
id: Identifier.schema("session"),
share: z
.object({
secret: z.string(),
url: z.string(),
})
.optional(),
title: z.string(),
time: z.object({
created: z.number(),
updated: z.number(),
}),
})
.openapi({
ref: "session.info",
})
export type Info = z.output<typeof Info>
export const Event = {
Updated: Bus.event(
"session.updated",
z.object({
info: Info,
}),
),
}
const state = App.state("session", () => {
const sessions = new Map<string, Info>()
const messages = new Map<string, Message.Info[]>()
return {
sessions,
messages,
}
})
export async function create() {
const result: Info = {
id: Identifier.descending("session"),
title: "New Session - " + new Date().toISOString(),
time: {
created: Date.now(),
updated: Date.now(),
},
}
log.info("created", result)
state().sessions.set(result.id, result)
await Storage.writeJSON("session/info/" + result.id, result)
share(result.id).then((share) => {
update(result.id, (draft) => {
draft.share = share
})
})
Bus.publish(Event.Updated, {
info: result,
})
return result
}
export async function get(id: string) {
const result = state().sessions.get(id)
if (result) {
return result
}
const read = await Storage.readJSON<Info>("session/info/" + id)
state().sessions.set(id, read)
return read as Info
}
export async function share(id: string) {
const session = await get(id)
if (session.share) return session.share
const share = await Share.create(id)
await update(id, (draft) => {
draft.share = share
})
for (const msg of await messages(id)) {
await Share.sync("session/message/" + id + "/" + msg.id, msg)
}
return share
}
export async function update(id: string, editor: (session: Info) => void) {
const { sessions } = state()
const session = await get(id)
if (!session) return
editor(session)
session.time.updated = Date.now()
sessions.set(id, session)
await Storage.writeJSON("session/info/" + id, session)
Bus.publish(Event.Updated, {
info: session,
})
return session
}
export async function messages(sessionID: string) {
const result = [] as Message.Info[]
const list = Storage.list("session/message/" + sessionID)
for await (const p of list) {
const read = await Storage.readJSON<Message.Info>(p).catch(() => {})
if (!read) continue
result.push(read)
}
result.sort((a, b) => (a.id > b.id ? 1 : -1))
return result
}
export async function* list() {
for await (const item of Storage.list("session/info")) {
const sessionID = path.basename(item, ".json")
yield get(sessionID)
}
}
export function abort(sessionID: string) {
const controller = pending.get(sessionID)
if (!controller) return false
controller.abort()
pending.delete(sessionID)
return true
}
async function updateMessage(msg: Message.Info) {
await Storage.writeJSON(
"session/message/" + msg.metadata.sessionID + "/" + msg.id,
msg,
)
Bus.publish(Message.Event.Updated, {
info: msg,
})
}
export async function chat(input: {
sessionID: string
providerID: string
modelID: string
parts: Message.Part[]
}) {
const l = log.clone().tag("session", input.sessionID)
l.info("chatting")
const model = await Provider.getModel(input.providerID, input.modelID)
let msgs = await messages(input.sessionID)
const previous = msgs.at(-1)
if (previous?.metadata.assistant) {
const tokens =
previous.metadata.assistant.tokens.input +
previous.metadata.assistant.tokens.output
if (
tokens >
(model.info.contextWindow - (model.info.maxOutputTokens ?? 0)) * 0.9
) {
await summarize({
sessionID: input.sessionID,
providerID: input.providerID,
modelID: input.modelID,
})
return chat(input)
}
}
using abort = lock(input.sessionID)
const lastSummary = msgs.findLast(
(msg) => msg.metadata.assistant?.summary === true,
)
if (lastSummary)
msgs = msgs.filter(
(msg) => msg.role === "system" || msg.id >= lastSummary.id,
)
const app = await App.use()
if (msgs.length === 0) {
const system: Message.Info = {
id: Identifier.ascending("message"),
role: "system",
parts: [
{
type: "text",
text: PROMPT_ANTHROPIC,
},
],
metadata: {
sessionID: input.sessionID,
time: {
created: Date.now(),
},
tool: {},
},
}
const contextFile = Bun.file(path.join(app.root, "CONTEXT.md"))
if (await contextFile.exists()) {
const context = await contextFile.text()
system.parts.push({
type: "text",
text: context,
})
}
msgs.push(system)
generateText({
messages: convertToModelMessages([
{
role: "system",
parts: [
{
type: "text",
text: PROMPT_TITLE,
},
],
},
{
role: "user",
parts: input.parts,
},
]),
model: model.language,
}).then((result) => {
return Session.update(input.sessionID, (draft) => {
draft.title = result.text
})
})
await updateMessage(system)
}
const msg: Message.Info = {
role: "user",
id: Identifier.ascending("message"),
parts: input.parts,
metadata: {
time: {
created: Date.now(),
},
sessionID: input.sessionID,
tool: {},
},
}
await updateMessage(msg)
msgs.push(msg)
const next: Message.Info = {
id: Identifier.ascending("message"),
role: "assistant",
parts: [],
metadata: {
assistant: {
cost: 0,
tokens: {
input: 0,
output: 0,
reasoning: 0,
},
modelID: input.modelID,
providerID: input.providerID,
},
time: {
created: Date.now(),
},
sessionID: input.sessionID,
tool: {},
},
}
await updateMessage(next)
const tools: Record<string, AITool> = {}
for (const item of await Provider.tools(input.providerID)) {
tools[item.id.replaceAll(".", "_")] = tool({
id: item.id as any,
description: item.description,
parameters: item.parameters as ZodSchema,
async execute(args, opts) {
const start = Date.now()
try {
const result = await item.execute(args)
next.metadata!.tool![opts.toolCallId] = {
...result.metadata,
time: {
start,
end: Date.now(),
},
}
return result.output
} catch (e: any) {
next.metadata!.tool![opts.toolCallId] = {
error: true,
message: e.toString(),
time: {
start,
end: Date.now(),
},
}
return e.toString()
}
},
})
}
const result = streamText({
onStepFinish: async (step) => {
const assistant = next.metadata!.assistant!
const usage = getUsage(step.usage, model.info)
assistant.cost = usage.cost
assistant.tokens = usage.tokens
await updateMessage(next)
},
abortSignal: abort.signal,
maxRetries: 6,
stopWhen: stepCountIs(1000),
messages: convertToModelMessages(msgs),
temperature: 0,
tools,
model: model.language,
})
let text: Message.TextPart | undefined
const reader = result.toUIMessageStream().getReader()
while (true) {
const result = await reader.read().catch((e) => {
if (e instanceof DOMException && e.name === "AbortError") {
return
}
throw e
})
if (!result) break
const { done, value } = result
if (done) break
l.info("part", {
type: value.type,
})
switch (value.type) {
case "start":
break
case "start-step":
text = undefined
next.parts.push({
type: "step-start",
})
break
case "text":
if (!text) {
text = value
next.parts.push(value)
break
}
text.text += value.text
break
case "tool-call":
next.parts.push({
type: "tool-invocation",
toolInvocation: {
state: "call",
...value,
// hack until zod v4
args: value.args as any,
},
})
break
case "tool-result":
const match = next.parts.find(
(p) =>
p.type === "tool-invocation" &&
p.toolInvocation.toolCallId === value.toolCallId,
)
if (match && match.type === "tool-invocation") {
match.toolInvocation = {
args: match.toolInvocation.args,
toolCallId: match.toolInvocation.toolCallId,
toolName: match.toolInvocation.toolName,
state: "result",
result: value.result as string,
}
}
break
case "finish":
break
case "finish-step":
break
case "error":
log.error("error", value)
break
default:
l.info("unhandled", {
type: value.type,
})
}
await updateMessage(next)
}
next.metadata!.time.completed = Date.now()
await updateMessage(next)
return next
}
export async function summarize(input: {
sessionID: string
providerID: string
modelID: string
}) {
using abort = lock(input.sessionID)
const msgs = await messages(input.sessionID)
const lastSummary = msgs.findLast(
(msg) => msg.metadata.assistant?.summary === true,
)?.id
const filtered = msgs.filter(
(msg) => msg.role !== "system" && (!lastSummary || msg.id >= lastSummary),
)
const model = await Provider.getModel(input.providerID, input.modelID)
const next: Message.Info = {
id: Identifier.ascending("message"),
role: "assistant",
parts: [],
metadata: {
tool: {},
sessionID: input.sessionID,
assistant: {
summary: true,
cost: 0,
modelID: input.modelID,
providerID: input.providerID,
tokens: {
input: 0,
output: 0,
reasoning: 0,
},
},
time: {
created: Date.now(),
},
},
}
await updateMessage(next)
const result = await generateText({
abortSignal: abort.signal,
model: model.language,
messages: convertToModelMessages([
{
role: "system",
parts: [
{
type: "text",
text: PROMPT_SUMMARIZE,
},
],
},
...filtered,
{
role: "user",
parts: [
{
type: "text",
text: "Provide a detailed but concise summary of our conversation above. Focus on information that would be helpful for continuing the conversation, including what we did, what we're doing, which files we're working on, and what we're going to do next.",
},
],
},
]),
})
next.parts.push({
type: "text",
text: result.text,
})
const assistant = next.metadata!.assistant!
const usage = getUsage(result.usage, model.info)
assistant.cost = usage.cost
assistant.tokens = usage.tokens
await updateMessage(next)
}
const pending = new Map<string, AbortController>()
function lock(sessionID: string) {
log.info("locking", { sessionID })
if (pending.has(sessionID)) throw new BusyError(sessionID)
const controller = new AbortController()
pending.set(sessionID, controller)
return {
signal: controller.signal,
[Symbol.dispose]() {
log.info("unlocking", { sessionID })
pending.delete(sessionID)
},
}
}
function getUsage(usage: LanguageModelUsage, model: Provider.Model) {
const tokens = {
input: usage.inputTokens ?? 0,
output: usage.outputTokens ?? 0,
reasoning: usage.reasoningTokens ?? 0,
}
return {
cost: new Decimal(0)
.add(new Decimal(tokens.input).mul(model.cost.input))
.add(new Decimal(tokens.output).mul(model.cost.output))
.toNumber(),
tokens,
}
}
export class BusyError extends Error {
constructor(public readonly sessionID: string) {
super(`Session ${sessionID} is busy`)
}
}
}
+104
View File
@@ -0,0 +1,104 @@
import { App } from "../app/app"
import { Ripgrep } from "../file/ripgrep"
import { Global } from "../global"
import { Filesystem } from "../util/filesystem"
import { Config } from "../config/config"
import path from "path"
import os from "os"
import PROMPT_ANTHROPIC from "./prompt/anthropic.txt"
import PROMPT_BEAST from "./prompt/beast.txt"
import PROMPT_GEMINI from "./prompt/gemini.txt"
import PROMPT_ANTHROPIC_SPOOF from "./prompt/anthropic_spoof.txt"
import PROMPT_SUMMARIZE from "./prompt/summarize.txt"
import PROMPT_TITLE from "./prompt/title.txt"
export namespace SystemPrompt {
export function provider(modelID: string) {
if (modelID.includes("gpt-") || modelID.includes("o1") || modelID.includes("o3")) return [PROMPT_BEAST]
if (modelID.includes("gemini-")) return [PROMPT_GEMINI]
return [PROMPT_ANTHROPIC]
}
export async function environment() {
const app = App.info()
return [
[
`Here is some useful information about the environment you are running in:`,
`<env>`,
` Working directory: ${app.path.cwd}`,
` Is directory a git repo: ${app.git ? "yes" : "no"}`,
` Platform: ${process.platform}`,
` Today's date: ${new Date().toDateString()}`,
`</env>`,
`<project>`,
` ${
app.git
? await Ripgrep.tree({
cwd: app.path.cwd,
limit: 200,
})
: ""
}`,
`</project>`,
].join("\n"),
]
}
const CUSTOM_FILES = [
"AGENTS.md",
"CLAUDE.md",
"CONTEXT.md", // deprecated
]
export async function custom() {
const { cwd, root } = App.info().path
const config = await Config.get()
const found = []
for (const item of CUSTOM_FILES) {
const matches = await Filesystem.findUp(item, cwd, root)
found.push(...matches.map((x) => Bun.file(x).text()))
}
found.push(
Bun.file(path.join(Global.Path.config, "AGENTS.md"))
.text()
.catch(() => ""),
)
found.push(
Bun.file(path.join(os.homedir(), ".claude", "CLAUDE.md"))
.text()
.catch(() => ""),
)
if (config.instructions) {
for (const instruction of config.instructions) {
try {
const matches = await Filesystem.globUp(instruction, cwd, root)
found.push(...matches.map((x) => Bun.file(x).text()))
} catch {
continue // Skip invalid glob patterns
}
}
}
return Promise.all(found).then((result) => result.filter(Boolean))
}
export function summarize(providerID: string) {
switch (providerID) {
case "anthropic":
return [PROMPT_ANTHROPIC_SPOOF.trim(), PROMPT_SUMMARIZE]
default:
return [PROMPT_SUMMARIZE]
}
}
export function title(providerID: string) {
switch (providerID) {
case "anthropic":
return [PROMPT_ANTHROPIC_SPOOF.trim(), PROMPT_TITLE]
default:
return [PROMPT_TITLE]
}
}
}

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