Compare commits

..

1220 Commits

Author SHA1 Message Date
Dax Raad 0d50c867ff fix mcp tools corrupting session
publish / publish (push) Has been cancelled
2025-07-07 17:05:16 -04:00
Dax Raad 27f7e02f12 run: truncate prompt 2025-07-07 16:41:42 -04:00
Jay V 0f93ecd564 docs: canonical url 2025-07-07 16:37:00 -04:00
Dax Raad da909d9684 append piped stdin to prompt
publish / publish (push) Has been cancelled
2025-07-07 16:33:21 -04:00
Jay V facd851b11 docs: dynamic domain 2025-07-07 16:31:15 -04:00
Dax Raad c51de945a5 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 9253a3ca9e docs: debug 2025-07-07 16:26:23 -04:00
Dax Raad 7cfa297a78 wip: model and prompt flags for tui 2025-07-07 16:24:37 -04:00
Jay V 661b74def6 docs: debug info 2025-07-07 16:13:26 -04:00
Dax Raad b478e5655c fix interrupt
publish / publish (push) Has been cancelled
2025-07-07 16:12:47 -04:00
Dax f884766445 v2 message format and upgrade to ai sdk v5 (#743)
publish / publish (push) Has been cancelled
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Liang-Shih Lin <liangshihlin@proton.me>
Co-authored-by: Dominik Engelhardt <dominikengelhardt@ymail.com>
Co-authored-by: Jay V <air@live.ca>
Co-authored-by: adamdottv <2363879+adamdottv@users.noreply.github.com>
2025-07-07 15:53:43 -04:00
Jay V 76b2e4539c docs: discord
publish / publish (push) Has been cancelled
2025-07-07 14:44:37 -04:00
Dominik Engelhardt d87922c0eb Fix Elixir LSP startup (#726) 2025-07-06 23:37:46 -04:00
Liang-Shih Lin 2446483df5 fix: Skip opencode upgrade if same version (#720) 2025-07-06 23:36:59 -04:00
GitHub Action f4c453155d Update download stats 2025-07-06 2025-07-06 12:03:56 +00:00
Dax Raad 969ad80ed2 fix openrouter caching with anthropic, should be a lot cheaper 2025-07-05 11:39:54 -04:00
GitHub Action af064b41d7 Update download stats 2025-07-05 2025-07-05 12:03:56 +00:00
Dax Raad ea6bfef21a use full filepath
publish / publish (push) Has been cancelled
2025-07-04 17:58:03 -04:00
Jay V 107363b1d9 docs: fix show more in share page 2025-07-04 17:57:12 -04:00
Dax Raad 85214d7c59 fix input bar not rendering capital letters
publish / publish (push) Has been cancelled
2025-07-04 17:21:51 -04:00
Timo Clasen 997cb2d945 fix(tui): optimistic rendering (#692) 2025-07-04 16:06:57 -05:00
Dax Raad 45b139390c make file attachments work good like
publish / publish (push) Has been cancelled
2025-07-04 16:21:26 -04:00
Jay V 994368de15 docs: share fix scrolling again 2025-07-04 13:53:25 -04:00
Jay V 143fd8e076 docs: share improve markdown rendering of ai responses 2025-07-04 13:53:25 -04:00
Dax Raad 06dba28bd6 wip: fix media type 2025-07-04 12:50:52 -04:00
adamdottv b8d276a049 fix(tui): full paths for attachments 2025-07-04 11:42:22 -05:00
Dax Raad ee01f01271 file attachments 2025-07-04 12:24:01 -04:00
adamdottv 32d5db4f0a fix(tui): markdown wrapping off sometimes 2025-07-04 11:16:38 -05:00
adamdottv f6108b7be8 fix(tui): handle pdf and image @ files 2025-07-04 11:13:09 -05:00
adamdottv 94ef341c9d feat(tui): render attachments 2025-07-04 10:55:02 -05:00
adamdottv f9abc7c84f feat(tui): file attachments 2025-07-04 10:55:02 -05:00
adamdottv 891ed6ebc0 fix(tui): slower startup due to file.status 2025-07-04 10:55:01 -05:00
Dax Raad 163e23a68b removed banned command concept 2025-07-04 11:32:12 -04:00
Vladimir f13b0af491 docs: Fix invalid json in the mcp example config (#645) 2025-07-04 11:24:13 -04:00
Aiden Cline 4a0be45d3d chore: document instructions configuration option (#670) 2025-07-04 11:22:45 -04:00
Dax Raad 23788674c8 disable snapshots temporarily
publish / publish (push) Has been cancelled
2025-07-04 08:45:18 -04:00
GitHub Action 121eb24e73 Update download stats 2025-07-04 2025-07-04 12:26:16 +00:00
Dax Raad 571d60182a improve snapshotting speed further
publish / publish (push) Has been cancelled
2025-07-03 21:36:09 -04:00
Jay V 167a9dcaf3 docs: share fix scroll to anchor 2025-07-03 20:30:21 -04:00
Dax Raad 37327259cb ci: ignore 2025-07-03 20:30:02 -04:00
Dax Raad cdb25656d5 improve snapshot speed
publish / publish (push) Has been cancelled
2025-07-03 20:16:25 -04:00
Jay V 25c876caa2 docs: share fix last message not expandable 2025-07-03 19:33:55 -04:00
Dax Raad cf83e31f23 add elixir lsp support
publish / publish (push) Has been cancelled
2025-07-03 19:29:51 -04:00
Dax Raad 3bc238b58b wip: logs 2025-07-03 19:29:51 -04:00
Jay V b8de69dced docs: fix share page scroll performance 2025-07-03 19:15:38 -04:00
Jay V e7fcb692a4 docs: tweak page title 2025-07-03 16:23:08 -04:00
Timo Clasen dae38574ab chore: add dev script (#666) 2025-07-03 14:43:25 -05:00
Dax Raad ed4f862b49 fix /unshare
publish / publish (push) Has been cancelled
2025-07-03 15:34:04 -04:00
adamdottv fce59db94a chore: simplify completions 2025-07-03 12:48:22 -05:00
Jay V 3e2a0c7281 docs: share handle slow loading pages 2025-07-03 13:15:21 -04:00
adamdottv 5a0910ea79 chore: better local dev with stainless script 2025-07-03 11:49:15 -05:00
adamdottv 1dffabcfda fix(tui): panic on completions failure
publish / publish (push) Has been cancelled
2025-07-03 10:53:43 -05:00
adamdottv c389e0ed43 fix(tui): redundant tool calls in each message in collapsed mode
publish / publish (push) Has been cancelled
2025-07-03 10:42:27 -05:00
Dax Raad 204801052a flag for disabling file watcher 2025-07-03 10:37:08 -04:00
Dax Raad 2528d8cb88 increase max retries to 10
publish / publish (push) Has been cancelled
2025-07-03 10:32:55 -04:00
adamdottv 6b73ffd1c1 fix(tui): include orphaned tool calls 2025-07-03 09:32:44 -05:00
adamdottv 0eadc50a33 fix(tui): selected message visuals 2025-07-03 09:03:04 -05:00
Dax Raad aeea84a877 fix webdomain 2025-07-03 09:58:25 -04:00
GitHub Action a54c5c6298 Update download stats 2025-07-03 2025-07-03 12:26:51 +00:00
adamdottv 8825cd3811 feat(tui): unshare command
publish / publish (push) Has been cancelled
2025-07-03 07:09:09 -05:00
adamdottv 3d9a5d9970 fix(tui): always show status bar 2025-07-03 06:53:05 -05:00
adamdottv 1f9e195fa6 fix(tui): better highlight visuals 2025-07-03 06:49:37 -05:00
Craig Andrews 73c012c76c fix: simplify parallel map using channels (#582) 2025-07-03 05:43:10 -05:00
Lev 2ace57404b fix: properly handle utf-8 in diff highlighting (#585) 2025-07-03 05:42:40 -05:00
Dax Raad 8c4b5e088b do not install gopls if go is not installed
publish / publish (push) Has been cancelled
2025-07-02 23:59:08 -04:00
Jacob Hands 69920a73d7 fix: use correct opencode bin path when running in development mode (#483)
publish / publish (push) Has been cancelled
2025-07-02 23:37:48 -04:00
Timo Clasen ae76a3467a fix: typescript error (#618) 2025-07-02 23:27:43 -04:00
Adi Yeroslav 701107cda4 Update prompt reference from CLAUDE.md to AGENTS.md (#623) 2025-07-02 23:27:22 -04:00
Aiden Cline b99565959b feat: configurable instructions (#624) 2025-07-02 23:27:04 -04:00
andrewxt 67aa7ce04d fix mouse scroll events being interpreted as keyboard input (#628) 2025-07-02 23:26:09 -04:00
Dax Raad c663fbc3ee remove need for glibc
publish / publish (push) Has been cancelled
2025-07-02 22:53:04 -04:00
Dax Raad 2090bab537 fix(tui): change messages layout toggle keybinding from <leader>m to <leader>p
publish / publish (push) Has been cancelled
🤖 Generated with [opencode](https://opencode.ai)

Co-Authored-By: opencode <noreply@opencode.ai>
2025-07-02 20:06:30 -04:00
Aiden Cline 64d5fff9a3 fix: unawaited promise causes opencode to use unenabled formatter (#625)
publish / publish (push) Has been cancelled
2025-07-02 19:19:31 -04:00
Jay V 925f695503 docs: tweak styles 2025-07-02 18:44:05 -04:00
adamdottv f1c925795d fix: typescript error 2025-07-02 16:08:41 -05:00
adamdottv c82a060eca feat(tui): file viewer, select messages 2025-07-02 16:08:11 -05:00
Ciaran McAleer 63e783ef79 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 35d6273fb3 wip: session revert/unrevert 2025-07-02 13:10:36 -04:00
Mark Huggins b89d4a16fd fix: Copilot Premium Requests (#595) 2025-07-02 12:04:53 -04:00
Prashant Choudhary 2799a96032 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 8f4b79227c fix(formatting): check for enabled formatters (#611) 2025-07-02 12:03:42 -04:00
Dax Raad c810b6d206 wip: symbols for lsp 2025-07-02 11:35:25 -04:00
Dax Raad fa35407572 fix lazy loading 2025-07-02 11:18:25 -04:00
Dax Raad 8bbbc07aff fix filewatcher not closing cleanly
publish / publish (push) Has been cancelled
2025-07-02 11:15:12 -04:00
GitHub Action 75a21ba3ce Update download stats 2025-07-02
publish / publish (push) Has been cancelled
2025-07-02 12:26:24 +00:00
Timo Clasen 0d6fb68a88 fix(tui): no space between agent and user message (#598) 2025-07-02 05:12:49 -05:00
Jean du Plessis 242b886434 fix: Small typo in CLI --model flag description (#577) 2025-07-02 05:10:58 -05:00
Daniel Vélez caf465a9da chore: rename OpenCode to opencode (#579) 2025-07-02 05:09:51 -05:00
Dax Raad bbf77c6139 improve ripgrep download
publish / publish (push) Has been cancelled
2025-07-01 22:39:17 -04:00
Dax Raad 53b7e04b86 ci: tweaks 2025-07-01 22:25:53 -04:00
Dax Raad 9e75e3ed18 ignore: read deleted files 2025-07-01 20:45:50 -04:00
Dax Raad 6389858d41 ignore: add file status command 2025-07-01 20:44:12 -04:00
Dax Raad 7e5941e14b ignore: add file status command 2025-07-01 20:39:43 -04:00
Dax Raad c68aeed8d9 ignore: fix file read with diff 2025-07-01 20:08:42 -04:00
Aiden Cline b199a609a8 fix: handle null case if tool args are empty for todos (#588) 2025-07-01 18:25:23 -05:00
Frank 4a5a93b3f8 Temporarily add admin unshare api 2025-07-01 18:57:08 -04:00
Dax Raad e99bdcefac fix write tool timeout
publish / publish (push) Has been cancelled
2025-07-01 13:50:57 -04:00
Dax Raad 26dcb85de1 add file watcher 2025-07-01 13:45:25 -04:00
Dax Raad 11d042be25 snapshot functionality 2025-07-01 12:28:34 -04:00
adamdottv 33b5fe236a fix(tui): better message rendering performance
publish / publish (push) Has been cancelled
2025-07-01 07:57:45 -05:00
GitHub Action d56991006c Update download stats 2025-07-01 2025-07-01 12:27:09 +00:00
adamdottv 739a9f71c3 fix(tui): layout issues 2025-07-01 06:41:39 -05:00
Adam Spiers aef81fce0b 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 8f3d7b4038 feat: better model dialog with sorting by release date (#563) 2025-07-01 05:28:32 -05:00
Dax Raad de15e67834 fix lsp diagnostic accurancy
publish / publish (push) Has been cancelled
2025-06-30 22:48:32 -04:00
Dax Raad fea56d8de6 fix loading api key from env for openai compatible providers
publish / publish (push) Has been cancelled
2025-06-30 19:07:51 -04:00
Max Rabin 3d71be2b45 Add pyright lsp for Python (#551)
Co-authored-by: Max Rabin <max.rabin@mobileye.com>
2025-06-30 18:17:47 -04:00
adamdottv 58baca2a5b chore: typescript error 2025-06-30 15:46:18 -05:00
adamdottv ef73926db6 chore: include model release date 2025-06-30 15:46:18 -05:00
Dax Raad 9ad1687f04 optimistically boot lsp servers 2025-06-30 16:45:26 -04:00
Jeremy Mack c573270e66 chore: remove duplicate EditTool in TOOLS array (#556) 2025-06-30 15:32:15 -04:00
Dax Raad 9ebad68274 fix bash tool extra line 2025-06-30 15:31:30 -04:00
Dax Raad 03664ba588 fix formatting of bash tools
publish / publish (push) Has been cancelled
2025-06-30 15:28:59 -04:00
adamdottv 5a107b275c fix(tui): layout issues
publish / publish (push) Has been cancelled
2025-06-30 14:04:56 -05:00
Dax Raad dd5736fe5f add back in file hierarchy in system prompt but limit to 200 items
publish / publish (push) Has been cancelled
2025-06-30 14:46:46 -04:00
adamdottv 9f3ba03965 chore: rework layout primitives
publish / publish (push) Has been cancelled
2025-06-30 12:29:29 -05:00
Timo Clasen d090c08ef0 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 68e82e4d94 fix(#467): more granular bedrock modelID based on aws region (#482) 2025-06-30 11:12:30 -04:00
Dax Raad a4aa0e6f8d docs: readme 2025-06-30 10:56:38 -04:00
GitHub Action 8c1ae2717c Update download stats 2025-06-30 2025-06-30 12:26:30 +00:00
Dax Raad 72d48759d7 add ruby formatter and lsp 2025-06-29 22:00:08 -04:00
Timo Clasen 986144b377 docs: how to disable mcp server (#543)
publish / publish (push) Has been cancelled
Co-authored-by: GitHub Action <action@github.com>
2025-06-29 21:33:30 -04:00
Dax Raad 1fdb326aa7 ignore: refactoring 2025-06-29 21:30:23 -04:00
Dax Raad 463257e7e4 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 0f41e60bd6 restructure formatters 2025-06-29 21:22:21 -04:00
Polo123456789 7df81f7b3e Formatters as plugins (#487) 2025-06-29 21:13:32 -04:00
Adam Spiers dd22cb2bb0 chore: add .editorconfig (#536)
Co-authored-by: Adam Spiers <opencode@adamspiers.org>
2025-06-29 21:12:58 -04:00
Dax Raad 248325925f fix issue with costs resetting once chat is completed
publish / publish (push) Has been cancelled
2025-06-29 19:43:03 -04:00
Dax Raad ca48a4f0fb better amazon bedrock caching with anthropic models
publish / publish (push) Has been cancelled
2025-06-29 19:27:07 -04:00
Dax 98ee5a3d87 Update STATS.md 2025-06-29 13:04:44 -04:00
GitHub Action 67480e5a1c Update download stats 2025-06-29 2025-06-29 12:23:40 +00:00
GitHub Action 2581a9b54c Update download stats 2025-06-29 2025-06-29 02:00:18 +00:00
Dax Raad 14a293e124 ci: stats 2025-06-28 21:59:14 -04:00
Dax Raad 780419ecae ci: daily stats script 2025-06-28 21:57:46 -04:00
Timo Clasen f0962e2d9c Add Option to Disable MCP Servers (#513) 2025-06-28 21:05:31 -04:00
Dax Raad 3a9584a419 fix context display
publish / publish (push) Has been cancelled
2025-06-28 21:01:53 -04:00
adamdottv 196f42cbff fix(tui): share command and error messages
publish / publish (push) Has been cancelled
2025-06-28 17:51:28 -05:00
Dax Raad 322385f6b1 patch for scroll dumping characters into input buffer
publish / publish (push) Has been cancelled
2025-06-28 11:56:47 -04:00
Dax Raad b7446cd7b9 ci: fix
publish / publish (push) Has been cancelled
2025-06-28 09:16:29 -04:00
Gal Schlezinger f618e569ab optimize edit-tool rendering (#463)
publish / publish (push) Has been cancelled
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 7b394b91e2 docs: share handle slower code blocks 2025-06-27 20:21:28 -04:00
Jay V 6a7983a4ea docs: adding more share images 2025-06-27 20:03:17 -04:00
Jay V 737146fca1 docs: tweak logo 2025-06-27 19:18:54 -04:00
Jay V 688f3fd12f Merge branch 'jeremyosih-feat/scroll-to-bottom-button' into dev 2025-06-27 19:16:46 -04:00
Jay V 145df08444 docs: share page format 2025-06-27 19:16:33 -04:00
Dax Raad 8b400515ea smooth out initial onboarding flow 2025-06-27 19:10:42 -04:00
Jay V 289797f56d docs: share cleanup title 2025-06-27 19:10:42 -04:00
adamdottv be0811ecc3 chore: rework openapi spec and use stainless sdk 2025-06-27 19:10:42 -04:00
Dax Raad 0676bcd4fd temporary patch for input lag on initial run 2025-06-27 19:10:42 -04:00
Polo123456789 d076def561 feat: Add golang file formatting (#474) 2025-06-27 19:10:42 -04:00
Wendell Misiedjan e0807d7317 fix: bunproc stdout / stderr parsing, error handling for bun ResolveMessage (#468) 2025-06-27 19:10:42 -04:00
Jay V fa2723f2d0 docs: update logo screenshot 2025-06-27 19:10:42 -04:00
Jay V 87d62514db docs: share page write tool bug 2025-06-27 19:10:42 -04:00
Dax Raad 2f8cf9146b ci: ignore 2025-06-27 19:10:42 -04:00
Dax Raad 8e0ec6b037 ci: aur 2025-06-27 19:10:42 -04:00
Dax Raad 6dc434cb83 ignore: cleanup 2025-06-27 19:10:42 -04:00
Dax Raad d972c27f03 lazy load formatters 2025-06-27 19:10:42 -04:00
Ryan Winchester 9e2bb63688 feat: add elixir file formatting (#458) 2025-06-27 19:10:42 -04:00
adamdottv 49053b66a9 fix(web): remove system prompts from share page 2025-06-27 19:10:42 -04:00
TheGoddessInari 47497aef07 scripts/hooks: Change shebang to universal /bin/sh (#453) 2025-06-27 19:10:41 -04:00
adamdottv 8455029de1 fix(tui): min width on user messages 2025-06-27 19:10:41 -04:00
Dax Raad 9f07f89384 fix formatting output going into tui 2025-06-27 19:10:41 -04:00
adamdottv d840d43e8f ignore: more metadata in app info 2025-06-27 19:10:41 -04:00
adamdottv 9ead2f3dfb fix: don't use prettier for langs it doesn't format 2025-06-27 19:10:41 -04:00
Dax Raad f3742ddbb8 ignore: run prettier 2025-06-27 19:10:41 -04:00
Dax Raad b61a841aa8 add auto formatting and experimental hooks feature 2025-06-27 19:10:41 -04:00
Jay V ebcf11e574 docs: lander tweak 2025-06-27 19:10:41 -04:00
Jay V 065f0aaddf docs: tweak lander 2025-06-27 19:10:41 -04:00
Dax Raad c0773dc7c5 smooth out initial onboarding flow 2025-06-27 16:09:59 -04:00
Jay V 1c3c74bd36 docs: share cleanup title 2025-06-27 15:31:21 -04:00
adamdottv 79bbf90b72 chore: rework openapi spec and use stainless sdk 2025-06-27 14:26:25 -05:00
Dax Raad 226a4a7f36 temporary patch for input lag on initial run
publish / publish (push) Has been cancelled
2025-06-27 14:36:03 -04:00
Polo123456789 df3b424830 feat: Add golang file formatting (#474) 2025-06-27 14:11:09 -04:00
Wendell Misiedjan 3cfd9d80bc fix: bunproc stdout / stderr parsing, error handling for bun ResolveMessage (#468) 2025-06-27 14:09:35 -04:00
Jay V e0553b8d2c docs: update logo screenshot 2025-06-27 14:04:09 -04:00
Jay V 391c837b37 docs: share page write tool bug 2025-06-27 13:25:15 -04:00
Dax Raad 5773d9d1a3 ci: ignore
publish / publish (push) Has been cancelled
2025-06-27 12:37:57 -04:00
Dax Raad ce611963c3 ci: aur
publish / publish (push) Has been cancelled
2025-06-27 12:29:13 -04:00
Dax Raad f865cacfb8 ignore: cleanup
publish / publish (push) Has been cancelled
2025-06-27 11:35:57 -04:00
Dax Raad 2ec0611f42 lazy load formatters 2025-06-27 11:33:37 -04:00
Ryan Winchester 334161a30e feat: add elixir file formatting (#458) 2025-06-27 10:15:11 -04:00
adamdottv dbb6e55226 fix(web): remove system prompts from share page 2025-06-27 06:48:44 -05:00
TheGoddessInari d0f9260559 scripts/hooks: Change shebang to universal /bin/sh (#453) 2025-06-27 07:40:22 -04:00
adamdottv d2176064e1 fix(tui): min width on user messages 2025-06-27 06:31:13 -05:00
Dax Raad ed8d277e49 fix formatting output going into tui
publish / publish (push) Has been cancelled
2025-06-27 07:29:41 -04:00
adamdottv 59b3268c64 ignore: more metadata in app info 2025-06-27 06:19:27 -05:00
adamdottv d043f67761 fix: don't use prettier for langs it doesn't format
publish / publish (push) Has been cancelled
2025-06-27 05:47:14 -05:00
Dax Raad 51bf193889 ignore: run prettier
publish / publish (push) Has been cancelled
2025-06-26 22:30:44 -04:00
Dax Raad f8b78f08b4 add auto formatting and experimental hooks feature
publish / publish (push) Has been cancelled
2025-06-26 22:17:08 -04:00
Jay V a4f32d602b docs: lander tweak 2025-06-26 19:47:58 -04:00
Jay V dc3dd21cf3 docs: tweak lander 2025-06-26 19:02:44 -04:00
Jeremy Osih b4c2fcccf5 Merge branch 'sst:dev' into feat/scroll-to-bottom-button 2025-06-27 00:41:20 +02:00
Jeremy Osih e950ad5306 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 8ca713b737 disable task tool temporarily
publish / publish (push) Has been cancelled
2025-06-26 18:27:49 -04:00
Jay V 5b54554fd5 docs: edit theme doc 2025-06-26 17:56:31 -04:00
Dax Raad 4bc651f958 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 3b6976a9c8 Merge branch 'rekram1-node-chore/update-config-docs' into dev 2025-06-26 17:24:03 -04:00
Jay V 863d5c1e8e docs: editing rules 2025-06-26 17:23:52 -04:00
adamdottv 97e19e9677 fix(tui): editor styles were off 2025-06-26 17:22:21 -04:00
adamdottv b27851461f feat(tui): more themes 2025-06-26 17:22:21 -04:00
adamdottv 209687377a feat(tui): more themes 2025-06-26 17:22:21 -04:00
adamdottv 90face1c09 fix(tui): editor width issues 2025-06-26 17:22:21 -04:00
adamdottv 936e2ce48b feat(tui): show lsp diagnostics for edit and write tools 2025-06-26 17:22:21 -04:00
adamdottv 16ee8ee379 fix(tui): chat editor aesthetics 2025-06-26 17:22:21 -04:00
adamdottv ac39308dad fix(tui): visual issue with modal selected items in system theme 2025-06-26 17:22:21 -04:00
adamdottv 346b49219d chore: tui agents.md 2025-06-26 17:22:21 -04:00
Jay V d84c1f20c7 docs: social share 2025-06-26 17:22:17 -04:00
adamdottv dfb8777555 fix(tui): editor spinner colors 2025-06-26 17:21:53 -04:00
Jay V 008af18156 docs: share page responsive diff 2025-06-26 17:21:53 -04:00
adamdottv ab23167f80 docs: system theme 2025-06-26 17:21:53 -04:00
adamdottv b17ec46463 fix(tui): make opencode theme default 2025-06-26 17:21:53 -04:00
Adam 2e26b58d16 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 31b56e5a05 Fix undefined is not an object (evaluating 'G.title') (#395) 2025-06-26 17:21:53 -04:00
Juhani Pelli 47c401cf25 fix: guard against large output limit causing infinite summarize loop (#399) 2025-06-26 17:21:53 -04:00
Dax Raad fab8dc9e6f more edit tool fixes 2025-06-26 17:21:53 -04:00
Dax Raad f39a2b1f16 integrate gemini-cli strategies for edit tool 2025-06-26 17:21:53 -04:00
Dax Raad 66830ced4e make edit tool more robust 2025-06-26 17:21:53 -04:00
Dax Raad 9d3fad754d ignore: typo 2025-06-26 17:21:53 -04:00
Dax Raad dcd3131f58 add output length errors 2025-06-26 17:21:53 -04:00
Dax Raad 3d02e07161 fix codex not working 2025-06-26 17:21:53 -04:00
Dax Raad 4dbc6a43a6 redirect uncaught errors to log file 2025-06-26 17:21:53 -04:00
adamdottv 5394b5188b fix(tui): editor styles were off
publish / publish (push) Has been cancelled
2025-06-26 15:12:26 -05:00
adamdottv 8e680b3957 feat(tui): more themes
publish / publish (push) Has been cancelled
2025-06-26 15:03:30 -05:00
adamdottv 1b8cd796d6 feat(tui): more themes 2025-06-26 14:54:32 -05:00
adamdottv 35fba793d0 fix(tui): editor width issues
publish / publish (push) Has been cancelled
2025-06-26 12:57:11 -05:00
adamdottv 5358d43b74 feat(tui): show lsp diagnostics for edit and write tools 2025-06-26 12:47:17 -05:00
adamdottv f777347bac fix(tui): chat editor aesthetics 2025-06-26 12:44:44 -05:00
adamdottv 17c8b914df fix(tui): visual issue with modal selected items in system theme
publish / publish (push) Has been cancelled
2025-06-26 12:33:06 -05:00
adamdottv 43b467dd12 chore: tui agents.md 2025-06-26 12:28:29 -05:00
Jay V 0e0770921e docs: social share 2025-06-26 13:21:42 -04:00
adamdottv 8edbb74352 fix(tui): editor spinner colors 2025-06-26 12:21:20 -05:00
Jay V e6bfa95758 docs: share page responsive diff 2025-06-26 13:06:41 -04:00
adamdottv e4120b6287 docs: system theme 2025-06-26 11:33:02 -05:00
adamdottv ccbc9e00f2 fix(tui): make opencode theme default 2025-06-26 11:32:25 -05:00
Adam 7d13baadc8 feat: default system theme (#419)
publish / publish (push) Has been cancelled
Co-authored-by: adamdottv <2363879+adamdottv@users.noreply.github.com>
2025-06-26 10:16:07 -05:00
rekram1-node 9acc83697f chore: document AGENTS.md 2025-06-26 08:28:06 -05:00
Mike Wallio db24bf87c0 Fix undefined is not an object (evaluating 'G.title') (#395)
publish / publish (push) Has been cancelled
2025-06-25 19:40:09 -04:00
Juhani Pelli f4c0d2d2fd fix: guard against large output limit causing infinite summarize loop (#399) 2025-06-25 19:39:51 -04:00
Dax Raad d240f4c676 more edit tool fixes
publish / publish (push) Has been cancelled
2025-06-25 19:22:54 -04:00
Dax Raad 9c90cdbe08 integrate gemini-cli strategies for edit tool
publish / publish (push) Has been cancelled
2025-06-25 17:56:14 -04:00
Dax Raad fc7af31fe5 make edit tool more robust
publish / publish (push) Has been cancelled
2025-06-25 17:10:48 -04:00
Dax Raad 2f8d23ec66 ignore: typo 2025-06-25 11:02:57 -04:00
Dax Raad 77ae3fb9b9 add output length errors 2025-06-25 11:02:09 -04:00
Dax Raad 4e7f6c47fd fix codex not working
publish / publish (push) Has been cancelled
2025-06-25 10:01:35 -04:00
Dax Raad 50469ed750 redirect uncaught errors to log file
publish / publish (push) Has been cancelled
2025-06-25 08:41:10 -04:00
Dax Raad aaab785493 better error message when bad directory is specified to start in 2025-06-24 22:28:25 -04:00
Dax Raad 9751937894 Enhance auth command with environment variable display and add models command
publish / publish (push) Has been cancelled
🤖 Generated with [opencode](https://opencode.ai)

Co-Authored-By: opencode <noreply@opencode.ai>
2025-06-24 22:24:55 -04:00
Dax Raad 0fc8dfc77e do not print error on ctrl+c during prompts 2025-06-24 22:09:43 -04:00
Dax Raad 81b7df61ec ci: bun lock 2025-06-24 21:14:32 -04:00
Dax Raad 8217b96d4a ci: fix type issue 2025-06-24 21:12:32 -04:00
Dax Raad 7dd0918d32 remove accidental opanai autoloader
publish / publish (push) Has been cancelled
2025-06-24 21:11:11 -04:00
Dax Raad 4b26b43855 added opencode serve command
publish / publish (push) Has been cancelled
2025-06-24 20:52:09 -04:00
Jay V 9d7cfda9fe docs: share page styles 2025-06-24 19:34:35 -04:00
Jay V a3cf18c905 docs: share page bash tool output 2025-06-24 19:28:51 -04:00
Aiden Cline 0b1a8ae699 fix: file completions replaced wrong text when paths overlap (#378) 2025-06-24 18:13:15 -05:00
Dax Raad eb70b1e5c8 docs: windows instructions
publish / publish (push) Has been cancelled
2025-06-24 18:54:59 -04:00
Dax Raad 00a3d818b6 ci: windows 2025-06-24 18:46:43 -04:00
Dax Raad 2384c7e734 ci: windows 2025-06-24 18:40:36 -04:00
Dax Raad 1bad3d9894 ci: windows 2025-06-24 18:27:57 -04:00
Dax Raad 4f715e66dc ci: windows 2025-06-24 18:13:15 -04:00
Dax ec001ca02f windows fixes (#374)
Co-authored-by: Matthew Glazar <strager.nds@gmail.com>
2025-06-24 18:05:04 -04:00
Jay a2d3b9f0c8 docs: Share page diff view improvements (#373) 2025-06-24 17:11:43 -04:00
Dax Raad 9cfb6ff964 ignore: revert
publish / publish (push) Has been cancelled
2025-06-24 14:59:27 -04:00
Dax Raad 6ed661c140 ci: upgrade bun 2025-06-24 14:42:25 -04:00
Dax Raad 9dc00edfc9 potential fix for failing to install provider package on first run 2025-06-24 14:33:35 -04:00
Jay V e063bf888e docs: share code blocks in markdown 2025-06-24 13:53:59 -04:00
Adam 6f18475428 feat: delete sessions (#362)
publish / publish (push) Has been cancelled
Co-authored-by: adamdottv <2363879+adamdottv@users.noreply.github.com>
2025-06-24 11:07:41 -05:00
Dax Raad 3664b09812 remove debug code writing to /tmp/message.json
publish / publish (push) Has been cancelled
2025-06-24 11:16:17 -04:00
Dax Raad 7050cc0ac3 ignore: fix type errors 2025-06-24 11:09:36 -04:00
Dax Raad 4d3d63294d externalize github copilot code 2025-06-24 10:42:19 -04:00
Tom 6bc61cbc2d 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 01d351bebe add HOMEBREW_NO_AUTO_UPDATE to brew upgrades
publish / publish (push) Has been cancelled
2025-06-23 20:36:08 -04:00
Dax Raad dbba4a97aa force use npm registry
publish / publish (push) Has been cancelled
2025-06-23 20:23:37 -04:00
GitMurf 0dc586faef fix: typescript error (any) from models (#347) 2025-06-23 18:44:57 -04:00
Dax Raad f19c6b05f2 glob tool should respect .gitignore
publish / publish (push) Has been cancelled
2025-06-23 17:37:32 -04:00
Dax Raad bc34f08333 bundle models.dev at build time and ignore refresh errors
publish / publish (push) Has been cancelled
2025-06-23 14:50:19 -04:00
Dax Raad b7ee16aabd ignore: remove opencode.json 2025-06-23 14:32:57 -04:00
Lucas Grzegorczyk ed1b0d97bf 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 8d3b2fb821 feat(tui): optimistically render user messages
publish / publish (push) Has been cancelled
2025-06-23 12:30:20 -05:00
Jay V fa991920bc fix help copy 2025-06-23 13:00:24 -04:00
adamdottv 5e79e3d7a5 fix(tui): less incorrect escapingn of < and > 2025-06-23 11:32:32 -05:00
adamdottv 966015c9ae fix: overlay border color issues 2025-06-23 11:21:49 -05:00
adamdottv 61f057337a fix: markdown wrapping issue 2025-06-23 11:20:44 -05:00
adamdottv 0b261054a2 chore: unused import 2025-06-23 10:21:57 -05:00
adamdottv e2e481cbb5 docs: disabled_providers 2025-06-23 10:21:25 -05:00
GitMurf 5140e83012 feat(copilot): edit headers for better rate limit avoidance (#321) 2025-06-23 10:44:19 -04:00
Dax Raad 100d6212be more graceful mcp failures
publish / publish (push) Has been cancelled
2025-06-22 21:10:05 -04:00
Dax Raad f0e19a6542 aws autoload include more env vars 2025-06-22 20:16:10 -04:00
Dax Raad 00c4d4f9f8 fix double entry of github copilot in auth login
publish / publish (push) Has been cancelled
2025-06-22 19:13:25 -04:00
Martin Palma 6e6fe6e013 Add Github Copilot OAuth authentication flow (#305) 2025-06-22 19:11:37 -04:00
Dax Raad d05b60291e docs: contributing 2025-06-22 17:55:10 -04:00
adamdottv 5162361372 fix(tui): color contrast fixes for nord 2025-06-22 15:17:18 -05:00
adamdottv d271b9f75b fix(tui): help dialog visuals 2025-06-22 14:28:16 -05:00
Márk Magyar 333569bed3 ignore: fix typos and formatting (#294) 2025-06-22 14:26:46 -04:00
Tom 09b89fdb23 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 0e8c3359d1 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 37e0a7050f fix(tui): mouse wheel escape codes leaking into input 2025-06-22 10:26:44 -05:00
adamdottv 774dcb6980 fix(tui): cleanup help dialog
publish / publish (push) Has been cancelled
2025-06-22 06:44:23 -05:00
phantomreactor 28bc49ad17 fix: invisible html tags and compact long delay (#304) 2025-06-22 06:29:04 -05:00
adamdottv dc1947838c fix(tui): cleanup modal visuals
publish / publish (push) Has been cancelled
2025-06-22 06:09:23 -05:00
adamdottv 3ea2daaa4c fix(tui): theme dialog visuals 2025-06-22 05:34:22 -05:00
Márk Magyar 137e964131 fix: session title generation (#293) 2025-06-21 14:32:11 -05:00
tyrellshawn 8efbe497fd Created a Theme inspired by the matrix (#285) 2025-06-21 07:29:49 -05:00
Thomas Meire 119d2d966c Add error handling on the calls to the server to debug issue #132 (#137) 2025-06-21 07:24:39 -05:00
Dax Raad 194415e785 footer clarifies it's showing context usage, not input token usage 2025-06-20 22:52:51 -04:00
Dax Raad 1684042fb6 huge optimization for token usage with anthropic
publish / publish (push) Has been cancelled
2025-06-20 22:43:04 -04:00
Dax Raad 59f0004d34 Add --method option to upgrade command for manual installation method selection
publish / publish (push) Has been cancelled
🤖 Generated with [opencode](https://opencode.ai)

Co-Authored-By: opencode <noreply@opencode.ai>
2025-06-20 20:48:23 -04:00
Dax Raad da35a64fa1 handle brew upgrades better 2025-06-20 20:27:23 -04:00
Dax Raad 460338ca53 make IDs more random
publish / publish (push) Has been cancelled
2025-06-20 17:39:59 -04:00
Saatvik Arya 53c18a64b4 docs: add API client generation instructions to README and AGENTS.md (#273) 2025-06-20 17:27:58 -04:00
Saatvik Arya b8144c5654 fix: return false for missing AWS_PROFILE in amazon-bedrock provider (#277) 2025-06-20 17:27:27 -04:00
adamdottv 9081e17fcc fix(tui): visual tweaks to themes
publish / publish (push) Has been cancelled
2025-06-20 15:49:51 -05:00
adamdottv ef3fd5900f docs: cleanup casing
publish / publish (push) Has been cancelled
2025-06-20 15:35:25 -05:00
adamdottv 453d690c11 docs: new themes docs 2025-06-20 15:31:38 -05:00
adamdottv c45be6a645 feat(tui): one dark theme 2025-06-20 15:14:23 -05:00
adamdottv 7b9b177088 feat(tui): kanagawa theme 2025-06-20 15:14:23 -05:00
adamdottv 3cee5b0470 feat(tui): gruvbox theme 2025-06-20 15:14:23 -05:00
adamdottv 9246d1c901 feat(tui): catppuccin theme 2025-06-20 15:14:22 -05:00
adamdottv cc12abc83e feat(tui): nord theme 2025-06-20 15:14:22 -05:00
adamdottv 4f7e4a9436 feat(tui): custom themes 2025-06-20 15:14:22 -05:00
Márk Magyar eee396f903 feat(tui): theme switcher with preview (#264) 2025-06-20 15:14:05 -05:00
Jay V 0d2f8e175a docs: share bugs 2025-06-20 15:50:12 -04:00
Jay V 4df40e0d9b docs: share page bugs 2025-06-20 15:50:12 -04:00
Dax Raad b72e17a8b7 fix issue with conversations hanging 2025-06-20 15:49:49 -04:00
Dax Raad 61160dc220 docs: readme 2025-06-20 15:22:41 -04:00
Dax Raad 98734ff28c Consolidate session context handling and add global config support
publish / publish (push) Has been cancelled
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 9991352663 feat: forward provider options from model config (#202)
publish / publish (push) Has been cancelled
Co-authored-by: Dax Raad <d@ironbay.co>
2025-06-20 15:03:41 -04:00
Dmytro Yankovskyi 91c4da5dbd fix(#243): claude on aws bedrock (#241)
publish / publish (push) Has been cancelled
Co-authored-by: Dax Raad <d@ironbay.co>
2025-06-20 14:57:33 -04:00
niba 2fd0e7dd6b chore: use client_id everywhere (#260) 2025-06-20 14:56:33 -04:00
adamdottv d50b7ad481 docs: theme schema update 2025-06-20 13:51:32 -05:00
adamdottv df95c49401 docs: theme schema 2025-06-20 13:00:32 -05:00
adamdottv 8b73c52f00 chore(tui): rename theme colors 2025-06-20 13:00:31 -05:00
Jay V 5603098d17 docs: add config 2025-06-20 13:22:31 -04:00
Jay V f436a50125 docs: share header 2025-06-20 13:12:35 -04:00
Jay V e19e977591 docs: test 2025-06-20 13:02:05 -04:00
Jay V addbe295b1 docs: test 2025-06-20 12:59:32 -04:00
Jay V 9a573dedc6 docs: test 2025-06-20 12:56:00 -04:00
adamdottv 9ea0d71e8d fix(tui): async load messages on theme/session switch 2025-06-20 11:25:21 -05:00
adamdottv b1a3599017 fix(tui): input latency optimization 2025-06-20 11:08:08 -05:00
adamdottv 7b0329f67f fix(tui): fetch tool more defensive
publish / publish (push) Has been cancelled
2025-06-20 09:00:28 -05:00
adamdottv 311b9c74dd fix(tui): typeahead open/close perf 2025-06-20 08:20:10 -05:00
adamdottv f7e8dd2ff8 chore: fix typescript issues 2025-06-20 07:48:42 -05:00
adamdottv 40b1dd7ef2 fix(tui): insert newline correctly positioned 2025-06-20 07:42:04 -05:00
adamdottv 261e76e0a3 fix(tui): input feels laggy 2025-06-20 07:31:45 -05:00
Dax Raad a300bfaccb docs: remove opencode.json 2025-06-20 01:00:15 -04:00
Dax Raad 41dba0db08 config validation
publish / publish (push) Has been cancelled
2025-06-20 00:57:28 -04:00
Rohan Godha 6674c6083a fix: phantom input bug on wsl (#200) 2025-06-19 20:08:56 -05:00
Tom Watkins f6afa2c6bb docs: fix typo in config.mdx (#218) 2025-06-19 21:08:21 -04:00
Dax Raad b2fb0508ea fix for azure models not liking tool definitions
publish / publish (push) Has been cancelled
2025-06-19 18:28:42 -04:00
Jay V 93f4252bb1 docs: tweak lander 2025-06-19 18:19:35 -04:00
Jay 46ab9c16dd docs: Update README.md 2025-06-19 18:19:06 -04:00
Dax Raad d869df4fee remove unused permission timeout
publish / publish (push) Has been cancelled
2025-06-19 18:00:53 -04:00
Dax Raad b99d4650ec temporarily disable project details in system prompt
publish / publish (push) Has been cancelled
2025-06-19 17:37:23 -04:00
Frank 261bb7f110 Infra: fix DO tag 2025-06-19 17:20:13 -04:00
Dax Raad 0515fbb260 fix gopls download spewing into terminal
publish / publish (push) Has been cancelled
2025-06-19 17:08:58 -04:00
adamdottv 88211d8c5b fix(tui): upgrade notification
publish / publish (push) Has been cancelled
2025-06-19 16:03:45 -05:00
Jay V a812f95b9d docs: share 2025-06-19 16:57:42 -04:00
adamdottv 3728a12bee fix(tui): better help on home
publish / publish (push) Has been cancelled
2025-06-19 15:56:28 -05:00
Jay V af07e51213 docs: tweak 2025-06-19 16:40:15 -04:00
Jay V 3113788c92 docs: copy 2025-06-19 16:39:36 -04:00
Jay V efb5fe6d4e docs: styles 2025-06-19 16:38:37 -04:00
Jay V 54dd6c644d docs: adding to config 2025-06-19 16:36:17 -04:00
Dax Raad 39ad8f2667 ignore: do migration 2025-06-19 16:32:32 -04:00
Jay V c4a2c84e53 docs: readme
publish / publish (push) Has been cancelled
2025-06-19 16:29:20 -04:00
Jay V 44fe012812 docs: edits 2025-06-19 16:28:11 -04:00
Jay V f5e7f079ea Copy changes 2025-06-19 16:28:03 -04:00
adamdottv 15a8936806 fix(tui): better tool titles 2025-06-19 15:11:53 -05:00
adamdottv 4e4cff49c0 feat(tui): better task tool rendering
publish / publish (push) Has been cancelled
2025-06-19 15:02:13 -05:00
adamdottv 5540503bee fix(tui): sorted tool arg maps 2025-06-19 14:07:33 -05:00
adamdottv 193718034b fix: typescript error 2025-06-19 13:57:25 -05:00
adamdottv 72108c0296 fix(tui): sorted tool arg maps 2025-06-19 13:56:09 -05:00
Dax Raad ec1c9f8cd1 use production share url
publish / publish (push) Has been cancelled
2025-06-19 14:21:00 -04:00
Dax Raad a85b0a370e ci: share 2025-06-19 13:26:15 -04:00
Dax Raad e7784d2864 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 97c4815444 fix task agent performance issues
publish / publish (push) Has been cancelled
2025-06-19 13:00:57 -04:00
Dax Raad 7d1a1663c8 allow selecting model and continuing previous session for opencode run 2025-06-19 13:00:57 -04:00
adamdottv 24c0ce6e53 fix(tui): vscode and mac terminal colors 2025-06-19 11:46:08 -05:00
adamdottv 4cdc86612c fix(tui): overlay border backgrounds 2025-06-19 11:41:30 -05:00
Jay V f1f3f8d12c ignore: share version 2025-06-19 12:20:30 -04:00
adamdottv e78d3b54bf chore: cleanup logs 2025-06-19 10:52:45 -05:00
adamdottv f8a7cd372d fix(tui): toast placement and overlay rendering 2025-06-19 10:45:10 -05:00
adamdottv f48eac638d feat(tui): more toast messages 2025-06-19 10:41:59 -05:00
adamdottv e1f12f93eb feat(tui): toast messages 2025-06-19 10:12:29 -05:00
Dax Raad 7ca8334a8b fix webfetch tool when returning html as text
publish / publish (push) Has been cancelled
2025-06-19 10:43:54 -04:00
Dax Raad f1a2b2eba4 support token caching for anthropic via openrouter
publish / publish (push) Has been cancelled
2025-06-19 10:32:14 -04:00
adamdottv 4b132656df feat(tui): copy share url to clipboard
publish / publish (push) Has been cancelled
2025-06-19 09:06:25 -05:00
Dax Raad 26bab00dab remove opencode_ prefixes from tool names. unfortunately this will break
publish / publish (push) Has been cancelled
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 568c04753e feat(tui): expand input to fit message
publish / publish (push) Has been cancelled
2025-06-19 08:45:27 -05:00
Dax Raad 4a06e164d2 ensure session.info is synced when shared
publish / publish (push) Has been cancelled
2025-06-19 09:41:11 -04:00
adamdottv c57b52c300 fix: include schema in converted toml config 2025-06-19 06:02:02 -05:00
Guillermo Antony Cava Nuñez 0b8f48f17f Fixes tool tip layering (#199) 2025-06-19 00:23:29 -04:00
Dax Raad 3862184ccb hooks 2025-06-19 00:20:03 -04:00
Frank 8619c50976 Update SST 2025-06-18 23:38:06 -04:00
Josh bb6b56b72a fix: incorrect command on main screen for exiting application (#201)
publish / publish (push) Has been cancelled
2025-06-18 23:19:43 -04:00
Dax Raad 1252b65166 stop loading models.dev format from global config
publish / publish (push) Has been cancelled
2025-06-18 23:08:51 -04:00
Dax Raad 6840276dad docs: update README 2025-06-18 23:03:54 -04:00
Dax Raad bd8c3cd0f1 BREAKING CONFIG CHANGE
publish / publish (push) Has been cancelled
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 e5e9b3e3c0 rework config 2025-06-18 23:01:19 -04:00
Frank 1e8a681de9 Render version 2025-06-18 22:26:51 -04:00
Jay V a834bedc17 ignore: share copy link 2025-06-18 20:18:10 -04:00
Dax Raad 6a3392385e support global config 2025-06-18 18:56:52 -04:00
Jay V 6a00e063c4 ignore: share logo 2025-06-18 18:33:51 -04:00
Jay V 73a0ce2b7d ignore: share 2025-06-18 18:22:19 -04:00
Jay V 4d1afd01fa ignore: share 2025-06-18 18:21:44 -04:00
Jay V 801d5f47bd ignore: share favicon 2025-06-18 18:10:22 -04:00
Jay V b6caae9708 ignore: share 2025-06-18 18:01:34 -04:00
adamdottv 183ca64ef9 feat(tui): show provider next to model 2025-06-18 16:09:49 -05:00
adamdottv 8c32cfe829 chore: tui style tweaks 2025-06-18 15:59:58 -05:00
Jay V 73dcc88da1 ignore: share 2025-06-18 16:54:33 -04:00
Jay V 14bded65dc ignore: share 2025-06-18 16:54:33 -04:00
adamdottv 87d1d3fb62 fix(tui): file completion quirks 2025-06-18 15:51:26 -05:00
Frank e054454109 Api: only return session messages 2025-06-18 16:20:34 -04:00
Dax Raad a6142cf975 ignore: types 2025-06-18 16:20:03 -04:00
Jay V 69332e5fa3 ignore: share 2025-06-18 16:15:22 -04:00
Jay V 20201ba3c4 ignore: share 2025-06-18 16:15:11 -04:00
Dax Raad 658067186a ignore: share page stuff 2025-06-18 16:13:33 -04:00
adamdottv ac777b77cf fix(tui): modal visuals 2025-06-18 15:12:24 -05:00
Dax Raad 5944ae2023 share types 2025-06-18 15:34:13 -04:00
Jay V 2f10961ba8 ignore: share 2025-06-18 15:32:40 -04:00
adamdottv fae97978a3 chore: cleanup logs 2025-06-18 14:18:46 -05:00
Dax Raad 3423415e49 docs: improve keybinds configuration format in README
publish / publish (push) Has been cancelled
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 1d0bfc2b2a fix(tui): help dialog sorting 2025-06-18 14:06:20 -05:00
adamdottv bd46cf0f86 feat(tui): configurable keybinds and mouse scroll 2025-06-18 13:56:51 -05:00
Dax Raad d4157d9a96 ctrl+c should gracefully clean up pending sessions
publish / publish (push) Has been cancelled
2025-06-18 14:11:49 -04:00
Jay V 6e4ef585d8 ignore: share error styles 2025-06-18 14:10:14 -04:00
Dax Raad e05c3b7a76 fix panic when invalid config
publish / publish (push) Has been cancelled
2025-06-18 14:03:16 -04:00
Dax Raad f99904bc1c track version on session info
publish / publish (push) Has been cancelled
2025-06-18 13:40:36 -04:00
Jay V b796d6763f ignore: share page styles 2025-06-18 12:53:48 -04:00
Dax Raad c1250abdf8 implemented diff trimming 2025-06-18 11:20:40 -04:00
Dax Raad ebe51534a1 allow setting options in global provider store
publish / publish (push) Has been cancelled
2025-06-18 11:06:16 -04:00
Dax Raad b8bbee4718 fix issue with provider cache
publish / publish (push) Has been cancelled
2025-06-18 10:56:23 -04:00
Dax Raad 8f852b396f fix deploys 2025-06-18 10:47:07 -04:00
Dax Raad ae4d089c06 remove call to npm causing noticible delay when starting chat
publish / publish (push) Has been cancelled
2025-06-18 10:35:41 -04:00
Dax Raad 5110fbdaf9 fix issue when running opencode in empty directory
publish / publish (push) Has been cancelled
2025-06-18 10:29:09 -04:00
Dax Raad e6ddb474fc ignore: sync
publish / publish (push) Has been cancelled
2025-06-18 08:36:25 -04:00
SBSTN 0dc71774ce Add Everforest Theme (#170) 2025-06-18 05:55:38 -05:00
Dax Raad b470466e30 integrate cache read/write data
publish / publish (push) Has been cancelled
2025-06-17 20:51:39 -04:00
Jay V d1f9311931 ignore: share page polish 2025-06-17 20:26:12 -04:00
Dax Raad 1c58023df9 improve anthropic oauth token caching and authentication handling
publish / publish (push) Has been cancelled
🤖 Generated with [opencode](https://opencode.ai)

Co-Authored-By: opencode <noreply@opencode.ai>
2025-06-17 13:23:15 -04:00
Dax Raad 4e0aa58b7e ignore: fix 2025-06-17 13:04:26 -04:00
Dax Raad 23ee34b35f state 2025-06-17 12:29:28 -04:00
Dax Raad 674c9a5220 support disabling providers from automatically being added
publish / publish (push) Has been cancelled
2025-06-17 12:23:04 -04:00
Dax Raad 54c86ed43a docs: readme 2025-06-17 12:17:45 -04:00
Dax Raad 676d75ee75 docs: update README
publish / publish (push) Has been cancelled
2025-06-17 12:14:38 -04:00
Dax Raad 70dc0a12f2 docs: readme 2025-06-17 12:12:33 -04:00
Dax Raad d579c5e8aa support global config for providers 2025-06-17 12:10:44 -04:00
Dax Raad ee91f31313 fix issue with tool schemas and google
publish / publish (push) Has been cancelled
2025-06-17 11:27:07 -04:00
Dax Raad 57b3051024 fix agent getting caught in summary loop
publish / publish (push) Has been cancelled
2025-06-17 10:50:03 -04:00
Dax Raad ae5cf3cc23 ci: fix
publish / publish (push) Has been cancelled
2025-06-17 10:38:01 -04:00
Dax Raad 68e1b3c46c Fix TypeScript compilation errors and consolidate version handling
publish / publish (push) Has been cancelled
🤖 Generated with [opencode](https://opencode.ai)

Co-Authored-By: opencode <noreply@opencode.ai>
2025-06-17 10:27:49 -04:00
adamdottv 2d68814abc feat: better collapsed tool call visuals
publish / publish (push) Has been cancelled
2025-06-17 08:35:18 -05:00
adamdottv a5da5127fa chore: consolidate chat page into tui.go 2025-06-17 07:09:04 -05:00
Dax Raad b5a4439704 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 9c5616521d do not autoupgrade snapshot builds
publish / publish (push) Has been cancelled
2025-06-17 01:18:32 -04:00
Dax Raad 3fe163416d autoupgrade
publish / publish (push) Has been cancelled
2025-06-17 01:05:05 -04:00
Dax d054f88130 Improve upgrade command with installation method detection (#158) 2025-06-17 00:07:17 -04:00
Jay b929b4f4b9 docs: Update README.md 2025-06-16 21:01:38 -04:00
Jay V 4c0c83b02d docs: readme 2025-06-16 20:10:19 -04:00
adamdottv d6d45bdc63 feat: share and init commands
publish / publish (push) Has been cancelled
2025-06-16 15:58:52 -05:00
Dax Raad 13a83721b0 ci: fixed ci issue 2025-06-16 16:58:25 -04:00
Dax Raad f0edffbae9 docs: readme 2025-06-16 16:53:43 -04:00
Dax Raad 8131bee49a ignore: logs 2025-06-16 16:02:45 -04:00
Dax Raad b5f44ae13f docs: update readme 2025-06-16 15:42:35 -04:00
Miles Till 0d23f2a7fd fix: incorrect lipgloss version (#131) 2025-06-16 14:35:46 -05:00
Dax Raad ac096d84ad remove windows builds
publish / publish (push) Has been cancelled
2025-06-16 15:11:14 -04:00
Dax Raad fcaf0e6dbf opencode auth login: validation on provider id and better error messages
publish / publish (push) Has been cancelled
2025-06-16 15:09:49 -04:00
Dax Raad 19e259d90d docs: readme 2025-06-16 15:04:32 -04:00
Dax Raad 2c9fd1e776 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 63996c4189 limit to 4 system prompts cached
publish / publish (push) Has been cancelled
2025-06-16 14:51:59 -04:00
adamdottv c7bb7ce4de fix: include cached tokens in tui 2025-06-16 12:59:38 -05:00
adamdottv c8eb1b24c3 feat: believe it or not, even faster tui init
publish / publish (push) Has been cancelled
2025-06-16 12:34:34 -05:00
adamdottv b9f894f1e9 feat: even faster tui init 2025-06-16 12:24:18 -05:00
adamdottv 7c0d10a4ce feat: faster tui init
publish / publish (push) Has been cancelled
2025-06-16 11:54:55 -05:00
Dax Raad 06af406146 properly track cache token counts
publish / publish (push) Has been cancelled
2025-06-16 12:43:22 -04:00
Dax Raad 0e3458b112 fix cache-control
publish / publish (push) Has been cancelled
2025-06-16 12:07:01 -04:00
adamdottv 2d15c683e0 fix: default provider and model 2025-06-16 10:51:01 -05:00
adamdottv 3c94d26570 chore: remove status service 2025-06-16 10:45:19 -05:00
Dax Raad 1a553e525f enable prompt caching for anthropic
publish / publish (push) Has been cancelled
2025-06-16 11:41:54 -04:00
adamdottv 3c4e966216 fix: spinner background color 2025-06-16 10:03:44 -05:00
Dax Raad 0721620ed8 docs: readme 2025-06-16 10:44:48 -04:00
Thomas Meire 9fc6734f32 ignore: remove log files and add them to gitignore (#138) 2025-06-16 09:30:07 -04:00
Jacob e1733a423d fix: typo and literal wording in packages/opencode/AGENTS.md (#134) 2025-06-16 08:18:29 -05:00
Dax Raad d42e3db7e0 docs: update README 2025-06-15 21:43:20 -04:00
Dax Raad cdb26f6d83 docs: readme 2025-06-15 21:39:02 -04:00
Dax Raad fe05edaa79 enhance ripgrep files function with query filtering and limit support
publish / publish (push) Has been cancelled
🤖 Generated with [opencode](https://opencode.ai)

Co-Authored-By: opencode <noreply@opencode.ai>
2025-06-15 21:26:32 -04:00
Dax Raad 7d174767b0 first pass making system prompt less fast
publish / publish (push) Has been cancelled
2025-06-15 20:25:04 -04:00
George Potoshin c5eefd1752 Fix: Improve Help UI Readability (Issue #99) (#117) 2025-06-15 18:38:44 -05:00
adamdottv 77a6b3bdd6 fix: background color rendering issues
publish / publish (push) Has been cancelled
2025-06-15 15:07:05 -05:00
Pierre B. 7effff56c0 fix: spelling, grammar and typos (#121) 2025-06-15 14:23:18 -05:00
Dax Raad e30fba0d3c Improve LSP server initialization with timeout handling and skip failed servers
publish / publish (push) Has been cancelled
🤖 Generated with [opencode](https://opencode.ai)

Co-Authored-By: opencode <noreply@opencode.ai>
2025-06-15 13:52:57 -04:00
Dax Raad 7fbb2ca9a6 ignore: add timer log helper 2025-06-15 13:33:24 -04:00
Dax Raad 230d0a1510 fix postinstall script for node
publish / publish (push) Has been cancelled
2025-06-15 13:11:11 -04:00
Pierre B. 46ff2c0ae0 chore: ignore intellij, vscode (#122) 2025-06-15 10:40:34 -05:00
adamdottv b8a89dab0f fix: background color rendering issues
publish / publish (push) Has been cancelled
2025-06-15 05:57:15 -05:00
szymon 7351e12886 remove .DS_Store (#112) 2025-06-15 05:34:46 -05:00
Dax Raad 38879dee2d beginning of upgrade command 2025-06-14 22:05:41 -04:00
Dax Raad c4ff8dd205 revert ctrl+d - conflicts with page down
publish / publish (push) Has been cancelled
2025-06-14 21:29:02 -04:00
Dax Raad 0e035b3115 fix aborting issue
publish / publish (push) Has been cancelled
2025-06-14 21:23:57 -04:00
Dax Raad b855511d9a fix issue with follow up tool calls and cancelation
publish / publish (push) Has been cancelled
2025-06-14 21:03:44 -04:00
Dax Raad 783faf554d fix issue continuing session after aborted 2025-06-14 20:24:50 -04:00
nitishxyz bfd4269d7d Add Ayu dark theme (#109)
publish / publish (push) Has been cancelled
2025-06-14 20:08:31 -04:00
Berr 25f78b053b fix: improve browser opening error handling in AuthLoginCommand (#111) 2025-06-14 20:07:41 -04:00
Dax Raad 87f260ee17 sync 2025-06-14 20:04:41 -04:00
Dax Raad 12931a869d ci: ignore commits 2025-06-14 18:59:05 -04:00
Dax Raad f759e1804d docs: typo 2025-06-14 18:58:27 -04:00
Rohan Godha c9b4564d36 tui: fix help dialog background (#110)
publish / publish (push) Has been cancelled
2025-06-14 18:57:15 -04:00
Conor O'Brien d097c546db nit: update commands displayed on home to match commands available (#108) 2025-06-14 18:56:44 -04:00
Gal Schlezinger adb54521b4 make ctrl+d quit too, just like shells (#105) 2025-06-14 18:56:34 -04:00
Dax Raad 2ea0399aa7 docs: use ollama example 2025-06-14 18:55:39 -04:00
Dax Raad fa1266263d downgrade to ai sdk v4.x
publish / publish (push) Has been cancelled
2025-06-14 18:44:08 -04:00
Gal Schlezinger fe109c921e add focus tracking for tui so cursor will hide when not in focus (#103) 2025-06-14 14:53:43 -05:00
Dax Raad 37bb8895fe docs: readme 2025-06-14 14:52:02 -04:00
Dax Raad 89b95be4de docs: provider config 2025-06-14 14:45:59 -04:00
Dax Raad eaf295bac7 docs: faq 2025-06-14 14:39:13 -04:00
Mantena Rama Raju 27d3cec477 typo (#94) 2025-06-14 14:36:29 -04:00
Dax Raad 574d494c3c Enhance provider system with dynamic package resolution and improved logging
publish / publish (push) Has been cancelled
- 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 0239761f31 tui: remove quit dialog (#97) 2025-06-14 12:47:34 -05:00
Dax Raad a53f9165e9 doc: remove dev script 2025-06-14 13:05:23 -04:00
Dax Raad ffc231bd8b docs: contributing
publish / publish (push) Has been cancelled
2025-06-14 12:45:26 -04:00
Dax Raad 3cf4ef56fb sync 2025-06-14 12:32:41 -04:00
Dax Raad c738e26438 docs: mcp 2025-06-14 12:25:26 -04:00
Dax Raad 9c6aa82ac1 docs: config schema 2025-06-14 12:22:07 -04:00
Dax Raad ef74d97491 ci: update publish script 2025-06-14 12:13:59 -04:00
Dax Raad af892e5432 docs: readme 2025-06-14 12:13:46 -04:00
Dax Raad d7aca6230d naming fixes
publish / publish (push) Has been cancelled
2025-06-14 01:54:28 -04:00
Dax Raad 0f9c2c5c27 Add flag system and auto-share functionality
publish / publish (push) Has been cancelled
- 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 6a261dedb4 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 ec928d88b5 fix(install): check if the path export command already exists (#28) 2025-06-13 23:28:33 -04:00
Dax Raad 59a5f120c0 Clean up workflows and enhance file discovery tools to include dot files
publish / publish (push) Has been cancelled
🤖 Generated with [OpenCode](https://opencode.ai)

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-13 23:24:46 -04:00
Dax Raad ce07f80b19 sync 2025-06-13 17:42:56 -04:00
Dax Raad 168fd9b2e3 screenshot 2025-06-13 17:42:14 -04:00
Dax Raad df13b155f9 disable autoshare
publish / publish (push) Has been cancelled
2025-06-13 17:30:17 -04:00
Dax Raad eeed5b8718 sync 2025-06-13 17:24:45 -04:00
Dax Raad 148ef90210 sync
publish / publish (push) Has been cancelled
2025-06-13 17:23:22 -04:00
adamdottv 67023bb007 wip: refactoring tui 2025-06-13 15:56:33 -05:00
Dax Raad a316aed4fe sync
publish / publish (push) Has been cancelled
2025-06-13 16:47:15 -04:00
Dax Raad 9f7c0bd599 sync 2025-06-13 16:46:48 -04:00
Dax Raad c7e1068f90 sync 2025-06-13 16:45:58 -04:00
Dax Raad e2052d790b sync 2025-06-13 16:43:53 -04:00
Dax Raad d3b2763c14 commit and push 2025-06-13 16:42:31 -04:00
Dax Raad c6492de7ac sync 2025-06-13 16:37:58 -04:00
Dax Raad d8fa0fb50c sync
publish / publish (push) Has been cancelled
2025-06-13 16:29:57 -04:00
Dax Raad 18ab8faa1d reset readme 2025-06-13 16:26:34 -04:00
Dax Raad f35ce180e2 ci
publish / publish (push) Has been cancelled
2025-06-13 16:23:38 -04:00
Dax Raad 2bee48a9bc homebrew
publish / publish (push) Has been cancelled
2025-06-13 16:17:27 -04:00
adamdottv 10ddd654cf wip: refactoring tui
publish / publish (push) Has been cancelled
2025-06-13 11:27:05 -05:00
adamdottv 61396b93ed wip: refactoring tui 2025-06-13 11:18:46 -05:00
adamdottv 62b9a30a9c wip: refactoring tui 2025-06-13 10:47:51 -05:00
adamdottv 5706c6ad3a wip: refactoring tui 2025-06-13 09:57:54 -05:00
adamdottv e8e03c895a wip: refactoring tui 2025-06-13 09:44:09 -05:00
adamdottv 38667682a7 wip: refactoring tui 2025-06-13 09:19:51 -05:00
adamdottv d7d5fc39fb wip: refactoring tui 2025-06-13 08:30:57 -05:00
adamdottv 0caf25adee wip: refactoring tui 2025-06-13 08:30:56 -05:00
Dax Raad 37febc6873 do not strip aur package
publish / publish (push) Has been cancelled
2025-06-13 08:27:17 -04:00
adamdottv 4169f0c412 wip: refactoring tui
publish / publish (push) Has been cancelled
2025-06-13 07:01:26 -05:00
adamdottv b7f06bbc1f wip: refactoring tui 2025-06-13 06:56:12 -05:00
adamdottv 1b8cfe9e99 wip: refactoring tui 2025-06-13 06:49:59 -05:00
adamdottv 97837d2d23 wip: refactoring tui 2025-06-13 06:23:12 -05:00
Dax Raad 9abc2a0cf8 load API keys
publish / publish (push) Has been cancelled
2025-06-13 00:53:46 -04:00
Dax Raad 9fb47bc855 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 73e9fb53d5 sync 2025-06-13 00:06:15 -04:00
Dax Raad f03637b1fc 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 2c376c5abc bedrock loader
publish / publish (push) Has been cancelled
2025-06-12 23:39:52 -04:00
Dax Raad 442e1b52ad 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 e8c3abc369 Update error message to say opencode instead of sst (#81)
publish / publish (push) Has been cancelled
2025-06-12 18:38:59 -04:00
Dax Raad c8648baba2 ci
publish / publish (push) Has been cancelled
2025-06-12 18:30:19 -04:00
Dax Raad 7b3a799856 ci
publish / publish (push) Has been cancelled
2025-06-12 18:21:08 -04:00
Dax Raad 9356b6c35a sync
publish / publish (push) Has been cancelled
2025-06-12 18:14:04 -04:00
Dax Raad 29a6603a89 Update CLI run command and session handling
publish / publish (push) Has been cancelled
🤖 Generated with [OpenCode](https://opencode.ai)

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-12 18:07:31 -04:00
Dax Raad a454ba8895 subagent 2025-06-12 18:07:31 -04:00
Jay V 5eae7aef0e updating logo 2025-06-12 17:30:24 -04:00
adamdottv 1031bceef7 wip: refactoring tui 2025-06-12 16:04:45 -05:00
adamdottv 653965ef59 wip: refactoring tui 2025-06-12 16:00:26 -05:00
adamdottv ca0ea3f94d wip: refactoring tui 2025-06-12 16:00:25 -05:00
adamdottv 98bd5109c2 wip: refactoring tui 2025-06-12 16:00:25 -05:00
adamdottv 78f65e4789 wip: refactoring tui 2025-06-12 16:00:25 -05:00
adamdottv 75dd2f75aa wip: refactoring tui 2025-06-12 16:00:25 -05:00
adamdottv fe86e58bbb wip: refactoring tui 2025-06-12 16:00:24 -05:00
adamdottv ae339015fc wip: refactoring tui 2025-06-12 16:00:24 -05:00
adamdottv cce2e4ad75 wip: refactoring tui 2025-06-12 16:00:24 -05:00
Dax Raad a1ce35c208 ci 2025-06-12 14:15:44 -04:00
Dax Raad 69d6709a19 sync 2025-06-12 14:11:01 -04:00
Dax Raad 52ec134b2d 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 db88bede05 sync
publish / publish (push) Has been cancelled
2025-06-12 14:06:06 -04:00
Dax Raad d4d218d7d6 Update index.ts
publish / publish (push) Has been cancelled
🤖 Generated with [OpenCode](https://opencode.ai)

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-12 13:59:42 -04:00
Dax Raad 3e086e3ab9 sync
publish / publish (push) Has been cancelled
2025-06-12 13:49:43 -04:00
Jay V 2f5faae34b fix share page edit 2025-06-12 13:42:10 -04:00
Dax Raad e3ad6a0698 do not output bunproc
publish / publish (push) Has been cancelled
2025-06-12 13:39:03 -04:00
Dax Raad b536b45536 Fix AUR SSH key path handling in publish script
publish / publish (push) Has been cancelled
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 81c245035f Simplify BunProc.which() to use process.execPath directly
publish / publish (push) Has been cancelled
🤖 Generated with [OpenCode](https://opencode.ai)

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

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-12 13:29:14 -04:00
Dax Raad 0cca75ef48 sync
publish / publish (push) Has been cancelled
2025-06-12 13:19:24 -04:00
Dax Raad ee1f55dbe2 token
publish / publish (push) Has been cancelled
2025-06-12 13:17:06 -04:00
Dax Raad 2fa50190e5 skip nil values
publish / publish (push) Has been cancelled
2025-06-12 13:13:34 -04:00
Jay V 662b6b1258 share page handle undefined 2025-06-12 13:11:34 -04:00
Dax Raad f0dbe40522 sync
publish / publish (push) Has been cancelled
2025-06-12 13:04:01 -04:00
Dax Raad 41c54f629c sync
release / goreleaser (push) Has been cancelled
2025-06-12 12:48:38 -04:00
Dax Raad 4503201b15 npm token
release / goreleaser (push) Has been cancelled
2025-06-12 12:09:13 -04:00
Dax Raad 120151ee38 sync 2025-06-12 11:58:41 -04:00
Dax Raad 4d2e556713 sync
release / goreleaser (push) Has been cancelled
2025-06-12 11:57:57 -04:00
Dax Raad 54a5d3a9eb sync
release / goreleaser (push) Has been cancelled
2025-06-12 11:56:52 -04:00
Dax Raad 22dc6b6ec9 sync
release / goreleaser (push) Has been cancelled
2025-06-12 11:56:06 -04:00
Dax Raad b5c6ddcd04 tweak
release / goreleaser (push) Has been cancelled
2025-06-12 11:55:07 -04:00
Dax Raad e03ad6c42e sync
release / goreleaser (push) Has been cancelled
2025-06-12 11:18:17 -04:00
Dax Raad 33457d8472 sync
release / goreleaser (push) Has been cancelled
2025-06-12 11:02:51 -04:00
Dax Raad 888105e60f sync
release / goreleaser (push) Has been cancelled
2025-06-12 11:00:37 -04:00
Dax Raad b7b490f67c Add postinstall script and update session/release configuration
release / goreleaser (push) Has been cancelled
🤖 Generated with [OpenCode](https://opencode.ai)

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-12 00:50:49 -04:00
Dax Raad f6ed59bf45 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 83991bee88 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 29142eb940 add title temp 2025-06-11 19:09:08 -04:00
Dax Raad aab47714c9 sync 2025-06-11 19:00:09 -04:00
Dax Raad 9b2b610920 sync 2025-06-11 18:45:18 -04:00
Dax Raad 468cec545a sync 2025-06-11 18:19:21 -04:00
adamdottv 3c82fb6818 wip: refactoring tui 2025-06-11 12:05:54 -05:00
adamdottv 8a2f370eda wip: refactoring tui 2025-06-11 11:58:25 -05:00
Dax Raad 636133e6cb sync 2025-06-11 12:58:06 -04:00
Dax Raad 6cf8784ecf sync 2025-06-11 12:44:21 -04:00
adamdottv 95d5e1f231 wip: refactoring tui 2025-06-11 11:43:28 -05:00
Dax Raad 979bad3e64 sync 2025-06-11 12:35:20 -04:00
Dax Raad 300d0474a3 sync 2025-06-11 11:41:10 -04:00
Dax Raad d4379c8c93 turn on tool call streaming 2025-06-11 11:36:12 -04:00
Jay V a9b230f419 share page scroll anchor 2025-06-11 09:43:52 -05:00
Dax Raad 07cffebc8f fix bash 2025-06-11 00:37:43 -04:00
Dax Raad f1de1634d6 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 8d8663399d sync 2025-06-11 00:27:46 -04:00
Dax Raad 83eb61fd5f 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 b8e7d06356 sync 2025-06-10 22:18:27 -04:00
Dax Raad 4543765e3a fix rendering 2025-06-10 19:37:25 -04:00
Dax Raad 28f5cbbfe9 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 ca3c22dc12 fix bunfile bug 2025-06-10 18:23:19 -04:00
Dax Raad 49110f7412 sync 2025-06-10 18:10:30 -04:00
Dax Raad 14dcf43246 add abort signals 2025-06-10 17:56:05 -04:00
Dax Raad 84e4afc0bd webfetch spoof browser 2025-06-10 16:39:06 -04:00
Dax Raad 1cc8e9a36d kill 2025-06-10 16:35:02 -04:00
Dax Raad 43b429db93 lsp 2025-06-10 16:29:35 -04:00
Dax Raad 6248c1e720 merge deep config 2025-06-10 16:19:02 -04:00
Frank 772e1851c0 Share: render url on frontend 2025-06-10 15:50:42 -04:00
Dax Raad 5ab2ff9589 onboarding progress 2025-06-10 15:43:14 -04:00
Dax Raad a0062d4661 fix share link 2025-06-10 13:36:47 -04:00
Dax Raad ef7f1f0761 sync 2025-06-10 13:30:13 -04:00
Dax Raad 96b5a079ff 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 0e58f488df moving share urls 2025-06-10 11:52:02 -05:00
Dax Raad fa7416687b 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 c3ab370344 share page fix 2025-06-10 09:33:28 -05:00
Dax Raad bb60aa3060 sync 2025-06-10 10:14:03 -04:00
Dax Raad 34fa8cadd6 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 edd459ec00 Share: og image 2025-06-09 23:37:32 -04:00
Frank 177875f624 ssr sync 2025-06-09 23:07:29 -04:00
Dax Raad fdaa7f287c aborted toolcalls should be cleaned up 2025-06-09 20:48:50 -04:00
Dax Raad fed659c582 properly support codex mini 2025-06-09 20:24:18 -04:00
Dax Raad bffc612a4e notice 2025-06-09 19:58:08 -04:00
Jay V b97b15e0fe share og image 2025-06-09 16:42:13 -05:00
Jay V fcb972de19 edits 2025-06-09 16:09:15 -05:00
Dax Raad 4478195ea8 file permissions for anthropic credentials 2025-06-09 17:07:52 -04:00
Jay V 54c4a783b3 share page show lsp diag 2025-06-09 15:41:19 -05:00
Jay V c091cbb624 share page fix ai text 2025-06-09 15:02:46 -05:00
Jay V d4f8fd867a fix duplicates models 2025-06-09 15:02:46 -05:00
Dax Raad a2884b08cc improve AGENTS.md 2025-06-09 15:28:06 -04:00
Dax Raad 60faa26a15 sync 2025-06-09 15:00:48 -04:00
Dax Raad d8510ab452 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 3c23b92bea optional 2025-06-09 14:02:58 -04:00
Dax Raad 021fd3fcb5 sync 2025-06-09 14:01:11 -04:00
Dax Raad fa3253d1b6 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 a1cc8f6cdb fix ts lsp 2025-06-08 12:57:33 -04:00
Dax Raad 1bcc02442a sync 2025-06-08 12:46:32 -04:00
Frank 1d782dc19a Share: load server data on page load 2025-06-08 01:17:54 -04:00
Frank 879d02f86c Remove unused package 2025-06-07 23:47:45 -04:00
Frank 028d589ea0 Infra: use Astro component 2025-06-07 23:46:56 -04:00
Dax Raad cdbdb96218 sync 2025-06-07 17:15:40 -04:00
Dax Raad f22c93ba1b logs 2025-06-06 23:58:09 -04:00
Dax Raad b34d5c959b add go version 2025-06-06 23:26:11 -04:00
Dax Raad 32e6a552c0 autodownload lsp 2025-06-06 23:21:57 -04:00
Dax Raad d6afebf22a make lsp better 2025-06-06 17:20:08 -04:00
Jay V b32cb2b932 share page styles 2025-06-06 12:18:02 -04:00
Dax Raad 265f427d2a lsp progress 2025-06-05 23:42:04 -04:00
Dax Raad 16520261f4 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 65b2cf73d7 share page markdown 2025-06-05 19:14:35 -04:00
Jay V 95069af03f share write tool 2025-06-05 16:41:44 -04:00
Jay V 3e4ebb6e5d share page 2025-06-05 16:35:23 -04:00
Dax Raad 9147108675 fix webfetch 2025-06-05 15:59:17 -04:00
Jay V ea9dd4e9e2 share ssr 2025-06-05 15:37:23 -04:00
Dax Raad 4a6e36a404 fix cost 2025-06-05 15:06:42 -04:00
Dax Raad db2bb32bcf integrate with models.dev 2025-06-05 14:59:16 -04:00
Jay V 1384a5e3e6 share page fetch 2025-06-05 14:17:43 -04:00
adamdottv 167aea6aaf wip: refactoring tui 2025-06-05 13:10:20 -05:00
Jay V 142056e9af share page todos 2025-06-05 13:52:44 -04:00
Dax Raad 241c366164 sync 2025-06-05 13:51:18 -04:00
Dax Raad 3cd7ae0807 gopls 2025-06-05 13:51:18 -04:00
Dax Raad 299a74061a sync 2025-06-05 13:51:18 -04:00
adamdottv e3d5af2855 wip: refactoring tui 2025-06-05 12:38:25 -05:00
adamdottv bbfa72552a feat: opus 4 2025-06-05 11:39:13 -05:00
Dax Raad 8dfdd3927e sync 2025-06-05 12:06:21 -04:00
Dax Raad 02e326f87f fixed 2025-06-05 11:58:04 -04:00
Dax Raad 35b03e4cb3 claude oauth support 2025-06-05 11:51:06 -04:00
adamdottv b3555cda30 wip: refactoring tui 2025-06-05 10:46:07 -05:00
adamdottv 04bd98cf4d wip: refactoring tui 2025-06-05 05:43:33 -05:00
Dax Raad f3e31130ba fix cost 2025-06-05 02:23:11 -04:00
Dax Raad 671e91f201 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 d334ead84a sync 2025-06-04 20:49:33 -04:00
Jay V 160428d2d4 share page glob 2025-06-04 20:14:14 -04:00
Jay V 0a1f9accd7 share page glob 2025-06-04 19:34:15 -04:00
Jay V a74f27e59a read tool share page 2025-06-04 18:39:06 -04:00
Dax Raad f76cdfff9b sync 2025-06-04 18:32:10 -04:00
Dax Raad 05974ea109 rework 2025-06-04 18:03:04 -04:00
Dax Raad 6cfce1e4da track errors 2025-06-04 17:55:14 -04:00
Jay V e20093678f share page tool args 2025-06-04 17:48:42 -04:00
Dax Raad 7f8f46f9fe 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 2dbdecb0f7 styles share 2025-06-04 17:01:04 -04:00
Dax Raad 53b0a25085 implemented todo tool 2025-06-04 15:09:50 -04:00
adamdottv 093e64eb54 wip: refactoring tui 2025-06-04 13:52:23 -05:00
adamdottv f34a3b6f67 wip: refactoring tui 2025-06-04 12:57:17 -05:00
Jay V 484c90ed00 share collapse system prompt 2025-06-04 13:37:07 -04:00
Dax Raad f49694a543 sync 2025-06-04 13:33:53 -04:00
Dax Raad fb88705bdc more tools 2025-06-04 13:33:25 -04:00
Dax Raad 90d85e6393 sync 2025-06-04 13:12:55 -04:00
Dax Raad d13822d26e tool updates 2025-06-04 13:12:48 -04:00
adamdottv a890288900 wip: refactoring tui 2025-06-04 11:48:01 -05:00
adamdottv 31d6e303a6 wip: refactoring tui 2025-06-04 11:48:01 -05:00
Jay V 199c42f726 styles share 2025-06-04 12:44:36 -04:00
Jay V 3211594821 styles 2025-06-04 12:32:25 -04:00
adamdottv 01050a430f wip: refactoring tui 2025-06-04 09:20:48 -05:00
Dax Raad 0b565b18c4 sync 2025-06-04 10:20:29 -04:00
Dax Raad 0791b077d7 fix tokens 2025-06-04 10:14:42 -04:00
Jay V 2fb59fee8e share paage durations 2025-06-03 18:08:46 -04:00
Jay V 8206da4d9e share page bugs 2025-06-03 17:20:30 -04:00
adamdottv 0c6bda8255 wip: refactoring tui 2025-06-03 15:49:41 -05:00
Dax Raad 1e063e7937 fix port issue 2025-06-03 16:35:37 -04:00
Dax Raad 37c34fd39c mcp support 2025-06-03 15:57:48 -04:00
Jay V 3a60ae98f3 styles 2025-06-03 15:57:00 -04:00
Jay V feeb49a42b Styling share 2025-06-03 15:55:34 -04:00
Jay V ad84355ebc lock 2025-06-03 15:55:34 -04:00
Dax Raad e2397a343d 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 8e769dcac0 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 9ad0477af6 change file name again 2025-06-03 14:28:50 -04:00
Dax Raad c5eafd5722 sync 2025-06-03 14:25:25 -04:00
Dax Raad caf9fdc893 initialized 2025-06-03 14:24:45 -04:00
Dax Raad be4155a838 max output tokens 2025-06-03 13:46:39 -04:00
adamdottv b00326a75a wip: refactoring tui 2025-06-03 12:45:28 -05:00
Dax Raad 4cf0aebb2e do not list files if not git 2025-06-03 13:19:44 -04:00
Dax Raad a5a39dada7 include env 2025-06-03 13:18:42 -04:00
Dax Raad 866f22b077 sync 2025-06-03 13:08:47 -04:00
Dax Raad ec7ab16ce4 sync 2025-06-03 13:00:27 -04:00
Dax Raad e5b06a2d95 initialzie 2025-06-03 12:38:48 -04:00
Dax Raad fae49aaf88 sync
release / goreleaser (push) Has been cancelled
2025-06-03 12:08:07 -04:00
Dax Raad 274c8baa34 added app 2025-06-03 12:02:21 -04:00
Dax Raad bfb36a8566 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 8487346d3f check in generated files 2025-06-02 22:14:12 -04:00
Dax Raad 54b99cd88a scope filetimes to session 2025-06-02 20:24:32 -04:00
Dax Raad 786db364d2 add permission system 2025-06-02 19:51:37 -04:00
adamdottv 863e7a093e wip: refactoring tui 2025-06-02 14:14:13 -05:00
adamdottv ca87b2806f wip: refactoring tui 2025-06-02 13:33:05 -05:00
adamdottv 2958c6b53c wip: refactoring tui 2025-06-02 12:30:03 -05:00
adamdottv 8b5394e031 wip: refactoring tui 2025-06-02 12:00:21 -05:00
adamdottv da92ee5f09 wip: refactoring tui 2025-06-02 11:33:01 -05:00
Dax Raad 80de5d489f spawn from anywhere in dev mode 2025-06-02 11:40:28 -04:00
Dax Raad ab17578516 remove json 2025-06-01 15:03:25 -04:00
Dax Raad e764525578 sync 2025-06-01 15:01:57 -04:00
Dax Raad a1c4f345a8 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 526a8ea19a 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 4be9f7ab9c fix tool name change 2025-05-31 20:33:21 -04:00
Dax Raad 8e40160934 sync
release / goreleaser (push) Has been cancelled
2025-05-31 18:45:36 -04:00
Dax Raad e97ed735d9 sync 2025-05-31 18:42:43 -04:00
Dax Raad 6d21525e71 sync 2025-05-31 18:10:49 -04:00
Dax Raad b4f809559e tool rework
release / goreleaser (push) Has been cancelled
2025-05-31 17:12:16 -04:00
Dax Raad 33109bac4d fix generate 2025-05-31 16:06:51 -04:00
Dax Raad f072ab3276 lazy load LLMs even harder 2025-05-31 16:05:12 -04:00
Dax Raad 3b746162d2 run formatter 2025-05-31 14:41:00 -04:00
Dax Raad 6df19f1828 Standardize code style to no semicolons and remove unused @types/node dependency
🤖 Generated with opencode
Co-Authored-By: opencode <noreply@opencode.ai>
2025-05-30 22:14:09 -04:00
Dax Raad fba56d6871 Update package dependencies and lock file
Synchronized package.json and bun.lock with latest dependency versions and optimized package structure.

🤖 Generated with opencode
Co-Authored-By: opencode <noreply@opencode.ai>
2025-05-30 22:02:22 -04:00
Dax Raad 1472efcbfe Optimize package management with catalog and exact versions
- Add catalog feature to centralize common dependency versions
- Convert all package versions to exact (remove ^ prefixes)
- Move prettier to root package only to reduce duplication
- Configure bunfig.toml for exact dependency installs
- Improve version consistency across workspace packages

🤖 Generated with opencode
Co-Authored-By: opencode <noreply@opencode.ai>
2025-05-30 21:56:37 -04:00
Dax Raad 56a5d58945 sync 2025-05-30 21:42:03 -04:00
Dax Raad f50a57041f embed go 2025-05-30 21:26:53 -04:00
Dax Raad f3da73553c sync 2025-05-30 20:48:36 -04:00
Jay V 9a26b3058f listing themes explicitly 2025-05-30 17:06:03 -04:00
Jay V a09be7cf74 tweaking styles 2025-05-30 17:00:18 -04:00
Dax Raad 91a9e455e2 sync 2025-05-30 16:43:42 -04:00
Jay V c391c6d3f3 tweaking share edit 2025-05-30 16:42:59 -04:00
adamdottv ca562266b7 wip: refactoring tui 2025-05-30 15:37:04 -05:00
adamdottv c69c9327da wip: refactoring tui 2025-05-30 15:34:25 -05:00
Dax Raad f5e2c596d4 sync 2025-05-30 14:45:10 -04:00
Dax Raad e9bad39a7e sync 2025-05-30 14:40:59 -04:00
Dax Raad 42c7880858 sync 2025-05-30 14:40:59 -04:00
Jay V 017a440a70 refactor 2025-05-30 14:08:21 -04:00
Jay V 1ab9547bb2 import 2025-05-30 14:02:15 -04:00
Jay V a4e46e6e18 share page diff 2025-05-30 13:58:34 -04:00
Jay V 680d52016c adding diff 2025-05-30 13:58:34 -04:00
adamdottv 6ebbcb3179 wip: refactoring tui 2025-05-30 12:29:27 -05:00
adamdottv 437de4ee36 wip: refactoring tui 2025-05-30 10:37:09 -05:00
adamdottv a5b28b5cef wip: refactoring tui 2025-05-30 08:35:59 -05:00
adamdottv 9bf024f8be wip: refactoring tui 2025-05-30 08:29:37 -05:00
adamdottv 189d0e5fb2 wip: refactoring tui 2025-05-30 06:47:44 -05:00
adamdottv b1b402faa7 wip: refactoring tui 2025-05-30 06:03:34 -05:00
adamdottv c5413c8c8d wip: refactoring tui 2025-05-30 05:32:42 -05:00
adamdottv da1e8484a9 wip: refactoring tui 2025-05-29 15:42:18 -05:00
adamdottv 4818bc5426 wip: refactoring tui 2025-05-29 15:41:13 -05:00
adamdottv 8a8c6b14af wip: refactoring tui 2025-05-29 15:40:37 -05:00
adamdottv 0e31bbcd93 wip: refactoring tui 2025-05-29 15:37:06 -05:00
adamdottv 913b3434d8 wip: refactoring tui 2025-05-29 15:30:39 -05:00
adamdottv 1c01ee4834 wip: refactoring tui 2025-05-29 15:22:25 -05:00
adamdottv 005d6e0bde wip: refactoring tui 2025-05-29 15:18:47 -05:00
adamdottv 37c0c1f358 wip: refactoring tui 2025-05-29 15:10:44 -05:00
adamdottv 2a132f86d6 wip: refactoring tui 2025-05-29 15:02:22 -05:00
adamdottv 50ba0b380b wip: refactoring tui 2025-05-29 14:39:37 -05:00
adamdottv 6cccbdccd3 wip: refactoring tui 2025-05-29 14:20:55 -05:00
Dax Raad d0ad09d798 Improve async share handling and enhance diagnostic output with filenames
🤖 Generated with opencode
Co-Authored-By: opencode <noreply@opencode.ai>
2025-05-29 15:16:42 -04:00
Dax Raad 4fa4246c10 Fix working indicator to only show when message is incomplete
🤖 Generated with opencode
Co-Authored-By: opencode <noreply@opencode.ai>
2025-05-29 15:10:23 -04:00
Dax Raad 0fe72864f2 remov elog 2025-05-29 15:05:44 -04:00
adamdottv ce5b3126d3 wip: refactoring tui 2025-05-29 14:04:49 -05:00
adamdottv 26606ccbf7 wip: refactoring tui 2025-05-29 14:04:49 -05:00
adamdottv fce9e79d38 wip: refactoring tui 2025-05-29 14:04:48 -05:00
adamdottv 6759674c0f wip: refactoring tui 2025-05-29 14:04:48 -05:00
Dax Raad a9799136fe support continuation 2025-05-29 14:29:08 -04:00
Jay V 7a29af4e30 styling tool calls 2025-05-29 14:24:25 -04:00
Dax Raad d398001f96 Remove React/Ink dependencies and add context window management
- Remove unused React and Ink CLI dependencies to simplify package
- Update provider schema to use maxOutputTokens for clarity
- Add automatic summarization when approaching context window limits
- Simplify message event handling and add cost/token metadata display

🤖 Generated with opencode
Co-Authored-By: opencode <noreply@opencode.ai>
2025-05-29 14:16:15 -04:00
Dax Raad e68747a64a add summarize 2025-05-29 13:17:56 -04:00
Dax Raad d62ce482da fix race 2025-05-29 13:15:58 -04:00
Dax Raad f9f41e205d add summarize 2025-05-29 13:02:48 -04:00
Dax Raad 80597cd3fd type error fix 2025-05-29 11:58:40 -04:00
Dax Raad 48f81fe4d3 sync 2025-05-29 11:38:55 -04:00
Dax Raad a96c2ce65c sync 2025-05-29 11:35:56 -04:00
Dax Raad 6f604bd0f9 remove secondary codegen 2025-05-29 11:32:55 -04:00
Dax Raad 42c1cd6a85 event 2025-05-29 11:17:34 -04:00
Dax Raad 33a831d2be rework types 2025-05-29 10:22:07 -04:00
adamdottv d70201cd93 wip: refactoring tui 2025-05-29 06:45:34 -05:00
adamdottv 9f1a75e938 wip: refactoring tui 2025-05-29 06:42:58 -05:00
adamdottv 2b77a7f714 wip: refactoring tui 2025-05-29 06:32:44 -05:00
Jay V 5974a53071 styling share 2025-05-28 20:04:58 -04:00
Jay V 3d61cc5d2b styling share 2025-05-28 18:32:40 -04:00
Dax Raad a22a2f0f37 autoshare 2025-05-28 17:34:37 -04:00
Dax Raad be65ed6f88 sync 2025-05-28 17:25:00 -04:00
Dax Raad e88264075a sync 2025-05-28 17:24:21 -04:00
Jay V 041a080a13 refactor share 2025-05-28 16:42:44 -04:00
adamdottv 9d7c5efb9b wip: refactoring tui 2025-05-28 15:36:36 -05:00
adamdottv 8863a499a9 wip: refactoring tui 2025-05-28 15:36:35 -05:00
adamdottv 15d21bf04a wip: refactoring tui 2025-05-28 15:36:35 -05:00
adamdottv 5e738ce7d3 wip: refactoring tui 2025-05-28 15:36:35 -05:00
adamdottv 641e9ff664 chore: cleanup unused imports 2025-05-28 15:36:35 -05:00
adamdottv d249766777 chore: remove dead code 2025-05-28 15:36:35 -05:00
Dax Raad 6cf4b7f00b sync 2025-05-28 15:40:35 -04:00
Dax Raad 6183398543 sync 2025-05-28 15:39:51 -04:00
Dax Raad ff786d9139 abort 2025-05-28 15:07:51 -04:00
Dax Raad 4767276a0e more 2025-05-28 14:17:02 -04:00
Jay V 71bab45065 styling share 2025-05-28 14:10:20 -04:00
Dax Raad cb48813c95 sync 2025-05-28 13:58:55 -04:00
Dax Raad 520cd02dd5 added cost field 2025-05-28 13:57:24 -04:00
Dax Raad afe741b63e add cost 2025-05-28 13:57:02 -04:00
Dax Raad f3b224090c better discriminator 2025-05-28 13:35:05 -04:00
Dax Raad 3b7b7f4bea add runtime 2025-05-28 13:24:35 -04:00
Dax Raad 3a4d3b249f generate message type 2025-05-28 13:22:48 -04:00
Dax Raad 55a6fcdd3f add provider_list 2025-05-28 12:53:22 -04:00
Dax Raad 4132fcc1b2 return tool errors properly 2025-05-27 21:53:21 -04:00
Jay V 37082b2176 styling share 2025-05-27 20:38:29 -04:00
Dax Raad b9f009c529 title 2025-05-27 17:22:29 -04:00
Dax Raad 601f610eb7 sync 2025-05-27 16:56:12 -04:00
Dax Raad 2e2bdd46b4 sync 2025-05-27 15:41:44 -04:00
Dax Raad 3a28ce9b0a sync 2025-05-27 15:37:46 -04:00
Dax Raad bb6fc2a1fd regen client 2025-05-27 15:35:48 -04:00
Dax Raad ad76fa8616 fix 2025-05-27 15:35:31 -04:00
Dax Raad bdac7d10dd add session list 2025-05-27 15:34:46 -04:00
Dax Raad 0ecfdd7501 fix type errors 2025-05-27 15:28:58 -04:00
Dax Raad a9758e0db5 vomit 2025-05-27 15:26:49 -04:00
Dax Raad e98f915fd5 stripped 2025-05-27 15:20:43 -04:00
Dax Raad 07f0fea4bf trust sharp 2025-05-27 14:36:23 -04:00
Dax Raad 6a43afc4e7 sync 2025-05-27 14:31:53 -04:00
Dax Raad c01eefc729 Add manual trigger option to deployment workflow
🤖 Generated with opencode
Co-Authored-By: opencode <noreply@opencode.ai>
2025-05-27 14:30:52 -04:00
Dax Raad 5d4ccc8883 Add GitHub Actions deployment workflow to automate deployments
🤖 Generated with opencode
Co-Authored-By: opencode <noreply@opencode.ai>
2025-05-27 14:29:36 -04:00
Dax Raad 98b5390a22 Refactor grep tool output generation and fix ls directory traversal bug
🤖 Generated with opencode
Co-Authored-By: opencode <noreply@opencode.ai>
2025-05-27 02:26:53 -04:00
Dax Raad c040baae11 Refactor LSP tools and add hover functionality
- Split diagnostics tool into separate lsp-diagnostics.ts file
- Add new lsp-hover.ts tool for LSP hover information
- Update tool exports and session integration
- Remove old diagnostics.ts file

🤖 Generated with opencode
Co-Authored-By: opencode <noreply@opencode.ai>
2025-05-27 02:17:35 -04:00
Dax Raad 754cc66741 Add automatic CONTEXT.md loading and improve share sync reliability
🤖 Generated with opencode
Co-Authored-By: opencode <noreply@opencode.ai>
2025-05-26 23:04:40 -04:00
Dax Raad 6ef0b991ec Fix race condition in share sync by implementing request queue
🤖 Generated with opencode
Co-Authored-By: opencode <noreply@opencode.ai>
2025-05-26 22:56:58 -04:00
Dax Raad f6ca06b8ea track tokens progressively 2025-05-26 22:35:30 -04:00
Dax Raad 4c198940d5 Remove debug console.log statements and improve LSP diagnostic handling
🤖 Generated with opencode
Co-Authored-By: opencode <noreply@opencode.ai>
2025-05-26 22:25:17 -04:00
Dax Raad 2e938d9da1 Standardize API parameters to camelCase and improve LSP client reliability
- Convert tool parameters from snake_case to camelCase for consistency
- Add file existence check in LSP client before opening files
- Fix version increment timing in LSP textDocument operations
- Optimize session token tracking using onStepFinish callback
- Add debugging logs for diagnostics troubleshooting

🤖 Generated with opencode
Co-Authored-By: opencode <noreply@opencode.ai>
2025-05-26 22:08:50 -04:00
Dax Raad b840a40759 sync 2025-05-26 21:44:55 -04:00
Dax Raad a1d40f8f28 add context.md 2025-05-26 21:38:17 -04:00
Dax Raad 575d76fa06 Fix session update to use proper getter and correct operation order
🤖 Generated with opencode
Co-Authored-By: opencode <noreply@opencode.ai>
2025-05-26 21:25:20 -04:00
Dax Raad b75456f5dd Clean up code formatting and remove unused imports for better consistency
🤖 Generated with opencode
Co-Authored-By: opencode <noreply@opencode.ai>
2025-05-26 21:22:38 -04:00
Dax Raad eb69cc3943 sync 2025-05-26 21:13:46 -04:00
Dax Raad e524209352 title 2025-05-26 21:08:22 -04:00
Jay V e1c897c1ae styles 2025-05-26 20:33:21 -04:00
Jay V 39f54e83e1 styles 2025-05-26 19:05:17 -04:00
Jay V d34c974996 adding styles 2025-05-26 19:01:50 -04:00
Frank c203891b84 Share: sync 2025-05-26 18:54:11 -04:00
Dax Raad 591bd2a4e3 sync 2025-05-26 18:14:36 -04:00
Dax Raad 94f35130f7 sync 2025-05-26 18:10:10 -04:00
Dax Raad f26873f5de sync 2025-05-26 18:06:47 -04:00
Jay V 66b18959eb Merging docs and share app 2025-05-26 17:25:12 -04:00
Jay V deacf5991a Adding share page 2025-05-26 17:25:12 -04:00
Dax Raad 25623d1f84 sync 2025-05-26 17:22:31 -04:00
Dax Raad de9f144858 sync 2025-05-26 17:15:47 -04:00
Dax Raad 0ad8738933 sync 2025-05-26 17:15:47 -04:00
Frank db5744bbc4 Share: sync 2025-05-26 16:35:03 -04:00
Dax Raad b87ba57819 shutdown 2025-05-26 14:52:38 -04:00
Dax Raad 802389a90e fixed id generation 2025-05-26 14:36:45 -04:00
Dax Raad 4880b08b8a sync 2025-05-26 14:18:07 -04:00
Dax Raad 80555f13e0 more tools 2025-05-26 14:09:17 -04:00
Dax Raad 113c49457f ls tool 2025-05-26 13:44:59 -04:00
Dax Raad e1ec815d1b sync 2025-05-26 13:39:16 -04:00
Dax Raad 2ed17f4877 exit properly 2025-05-26 13:21:15 -04:00
Frank 80118212da Share: sync 2025-05-26 12:40:17 -04:00
Frank 9b3760247a Share: sync 2025-05-26 12:40:17 -04:00
Dax Raad a2d652b13d sync 2025-05-26 12:40:17 -04:00
Dax Raad 5c491758f5 sync 2025-05-26 12:40:17 -04:00
Dax Raad 5f750b7368 share 2025-05-26 12:40:17 -04:00
Frank e2dc5a8faf Share: sync 2025-05-26 12:40:17 -04:00
Frank 72d10a0823 Share: fix types 2025-05-26 12:40:17 -04:00
Frank 7623b33f31 Share: sync 2025-05-26 12:40:17 -04:00
Frank 9b331a917e Share: sync 2025-05-26 12:40:17 -04:00
Frank d51b4263ab Share: sync 2025-05-26 12:40:17 -04:00
Frank 34a2dcb80a Share: sync 2025-05-26 12:40:17 -04:00
Frank 8cbd59296e Share: syc 2025-05-26 12:40:17 -04:00
Frank 83974e0c95 Share: sync 2025-05-26 12:40:17 -04:00
Dax Raad 59d43fa5da add diagnostics meta 2025-05-26 12:40:17 -04:00
Dax Raad e01afb407c add tool tests 2025-05-26 12:40:17 -04:00
Dax Raad f0f55bc75f sync 2025-05-26 12:40:17 -04:00
Dax Raad 2860a2bb1a sync 2025-05-26 12:40:17 -04:00
Dax Raad 9b564f0b73 tool meta 2025-05-26 12:40:17 -04:00
Dax Raad 2437ce3f8b toolz 2025-05-26 12:40:17 -04:00
Dax Raad fa8a46326a codegen 2025-05-26 12:40:17 -04:00
Dax Raad 652429377b sync 2025-05-26 12:40:17 -04:00
Dax Raad 99af6146d5 openapi 2025-05-26 12:40:17 -04:00
Dax Raad 020e0ca039 sync 2025-05-26 12:40:17 -04:00
Dax Raad 0439072420 fix 2025-05-26 12:40:17 -04:00
Dax Raad 49ad2efef6 sync 2025-05-26 12:40:17 -04:00
Dax Raad 0e303e6508 sync 2025-05-26 12:40:17 -04:00
Dax Raad bcd2fd68b7 sync 2025-05-26 12:40:17 -04:00
Dax Raad d0d67029f4 process 2025-05-26 12:40:17 -04:00
Dax Raad a34d020bc6 sync 2025-05-26 12:40:17 -04:00
Martin Palma 96fbc37f01 feat: ayu theme (#60) 2025-05-26 06:45:06 -05:00
Ed Zynda 89e3a72ae1 feat: add support for piped input to CLI (#51) 2025-05-23 06:54:09 -05:00
Ed Zynda b9ebcea82c really disable history nav when completions dialog is open (#50) 2025-05-23 06:43:30 -05:00
Jay f31f92119d Merge pull request #52 from sst/docs
Adding website
2025-05-22 15:23:09 -04:00
Jay V da9b2a18b9 editing docs 2025-05-22 15:10:08 -04:00
Dax Raad 2b258b1473 switch default to claude sonnet 4
release / goreleaser (push) Has been cancelled
2025-05-22 13:17:32 -04:00
Jay V 6f894950a6 adding other chapters 2025-05-21 19:34:50 -04:00
Jay V 9049295cc9 Merge branch 'dev' into docs 2025-05-21 15:01:25 -04:00
Jay V 4526b14b17 Moving readme to docs 2025-05-21 15:01:12 -04:00
Jay V f768313c4f Moving readme to docs 2025-05-21 14:59:09 -04:00
Jay V d9c1b2cc90 adding docs site 2025-05-21 14:58:30 -04:00
Jay V 6cfcf51752 adding docs site 2025-05-21 14:24:53 -04:00
Alvin Johansson dff8e77eb6 docs: add clearer bedrock instructions (#40) 2025-05-20 08:23:51 -05:00
Ed Zynda 6e854a4df4 fix: disable history navigation when filepicker is open (#39) 2025-05-18 18:41:34 -05:00
phantomreactor 2f8984fadb Codex mini (#34) 2025-05-18 08:29:53 -05:00
Ed Zynda c84918cb47 feat: Add message history navigation with arrow keys (#30) 2025-05-17 13:35:49 -05:00
Pierre B. 05bb065d00 fix: log pane text wrapping (#32) 2025-05-17 13:33:47 -05:00
Pierre Berube 3742997889 fix: typo 2025-05-17 08:50:30 -05:00
Pierre Berube daf0305203 fix: split out bedrock models 2025-05-17 08:48:51 -05:00
Ed Zynda 307982a099 feat: Add tool restriction flags for non-interactive mode (#29)
release / goreleaser (push) Has been cancelled
2025-05-17 08:23:13 -05:00
phantomreactor ba416e787b paste images with ctrl+v (#26) 2025-05-16 14:31:50 -05:00
Ed Zynda b71cae63f1 feat: Add tools dialog accessible via F9 (#24)
* Add tools dialog

* Remove sorting and double items

* Update key handling
2025-05-16 10:57:35 -05:00
Ed Zynda c92f7c6630 fix: Show correct file paths in permission window (#25)
* Fix paths in permission window to show relative paths

* Fix paths in permission window to show actual file paths
2025-05-16 10:56:39 -05:00
Ed Zynda 4a444e9c9b feat: Make shell configurable via config file (#23) 2025-05-16 06:27:28 -05:00
Ed Zynda 623d132772 feat: Add non-interactive mode (#18) 2025-05-16 06:06:28 -05:00
adamdottv d127a1c4eb feat: 0-255 color support in custom themes 2025-05-15 19:02:27 -05:00
adamdottv c9cca48d08 fix: layout
release / goreleaser (push) Has been cancelled
2025-05-15 15:57:15 -05:00
adamdottv 3944930fc0 chore: cleanup 2025-05-15 15:45:22 -05:00
adamdottv 825c0b64af fix: build 2025-05-15 14:49:30 -05:00
adamdottv d7af7dd3fe fix: redundant status msg 2025-05-15 14:42:10 -05:00
adamdottv b112216241 fix: build 2025-05-15 13:36:58 -05:00
mineo 87237b6462 feat: support VertexAI provider (#153)
* support: vertexai

fix

fix

set default for vertexai

added comment

fix

fix

* create schema

* fix README.md

* fix order

* added pupularity

* set tools if tools is exists

restore commentout

* fix comment

* set summarizer model
2025-05-15 13:35:06 -05:00
phantomreactor 5f5f9dad87 add support to preview webp images and paste file paths
release / goreleaser (push) Has been cancelled
2025-05-15 12:55:36 -05:00
adamdottv aa8b3ce1ee fix: init ordering 2025-05-15 12:52:42 -05:00
adamdottv a65e593ab4 feat: batch tool 2025-05-15 12:44:16 -05:00
adamdottv 5d9058eb74 fix: tui height 2025-05-15 12:18:28 -05:00
adamdottv a850320fad fix: don't limit session logs 2025-05-15 12:05:26 -05:00
adamdottv ddbb217d0d feat: better status bar 2025-05-15 12:04:15 -05:00
Ed Zynda ab150be7c3 feat: Support named arguments in custom commands (#158)
* Allow multiple named args

* fix: Fix styling in multi-arguments dialog

* Remove old unused modal

* Focus on only one input at a time
2025-05-15 09:43:33 -05:00
Adictya a203fb8ccc fix(complete-module): change completion start key to slash
release / goreleaser (push) Has been cancelled
2025-05-15 08:29:54 -05:00
Adictya acc084c9ea chore(complete-module): lint 2025-05-15 08:29:54 -05:00
Adictya 3ee213081e fix(complete-module): logging 2025-05-15 08:29:54 -05:00
Adictya 15bf40bc10 feat(complete-module): add completions logic, dialog and providers 2025-05-15 08:29:54 -05:00
adamdottv a33e3e25b6 chore: cleanup dead code 2025-05-15 07:19:17 -05:00
adamdottv 658faab2bf chore: update icon in readme 2025-05-15 07:03:58 -05:00
rekram1-node 797045ee29 feat: add configuration persistence for model selections 2025-05-14 16:32:05 -05:00
adamdottv c8f8d67a88 feat: codeAction tool
release / goreleaser (push) Has been cancelled
2025-05-14 14:57:47 -05:00
Dax Raad 182e32e4f7 add screenshot 2025-05-14 15:50:25 -04:00
adamdottv 5ea989fb74 feat: docSymbols and workspaceSymbols tools
release / goreleaser (push) Has been cancelled
2025-05-14 14:40:45 -05:00
adamdottv 45c778b90d feat: definition and references tools 2025-05-14 14:25:15 -05:00
Dax Raad 47cbb650a0 ci
release / goreleaser (push) Has been cancelled
2025-05-14 14:30:47 -04:00
Dax Raad e91371c6a5 ci 2025-05-14 14:27:18 -04:00
adamdottv 9d17314309 fix: tweak 2025-05-14 13:15:39 -05:00
adamdottv 3982be4310 feat: session specific logs 2025-05-14 13:06:09 -05:00
adamdottv 4c998d4f4f chore: remove sourcegraph tool 2025-05-14 10:42:16 -05:00
adamdottv f7849c2d59 fix: log display 2025-05-14 07:47:15 -05:00
adamdottv 463002185b fix: log perf 2025-05-14 07:38:21 -05:00
adamdottv 53a80eac1e chore: cleanup 2025-05-14 06:00:34 -05:00
adamdottv 01b6bf5bb7 chore: refactor db 2025-05-13 13:08:43 -05:00
adamdottv d8f3b60625 chore: refactoring 2025-05-13 11:34:48 -05:00
adamdottv cf8e16018d chore: refactoring 2025-05-13 11:15:14 -05:00
adamdottv 674797bd48 chore: refactoring 2025-05-13 11:07:34 -05:00
adamdottv 1f9610e266 chore: refactoring 2025-05-13 10:45:58 -05:00
adamdottv ae86ef519c chore: refactoring 2025-05-13 10:27:09 -05:00
adamdottv 2391e338b4 chore: rename 2025-05-13 10:02:39 -05:00
adamdottv 1e9399fbee chore: cleanup 2025-05-13 09:26:54 -05:00
adamdottv e9f74b867f chore: cleanup 2025-05-13 07:15:58 -05:00
adamdottv 5079556896 chore: cleanup 2025-05-13 07:04:27 -05:00
adamdottv 7f0e68b933 chore: cleanup 2025-05-13 06:55:18 -05:00
adamdottv 0c21ca5318 chore: cleanup 2025-05-13 06:51:28 -05:00
adamdottv 0117c72a2c chore: refactor diff 2025-05-12 15:17:50 -05:00
adamdottv e3eb9e5435 chore: refactor diff 2025-05-12 14:48:13 -05:00
adamdottv d941be3f1f chore: refactor agent.go 2025-05-12 14:43:12 -05:00
adamdottv 36e5ae804e chore: rename coder -> primary 2025-05-12 14:32:27 -05:00
adamdottv c9b90dd184 fix: show context % 2025-05-12 14:08:57 -05:00
adamdottv 8270a1e4b1 chore: cleanup 2025-05-12 13:01:59 -05:00
adamdottv 7f9c992993 fix: log status messages 2025-05-12 12:53:20 -05:00
adamdottv b6524c0982 chore: cleanup 2025-05-12 11:38:07 -05:00
adamdottv 425c0f1bab fix: timestamp formatting 2025-05-12 11:33:52 -05:00
adamdottv d20d0c5a95 chore: cleanup 2025-05-12 11:22:19 -05:00
adamdottv 5af3c05d41 chore: cleanup 2025-05-12 10:53:13 -05:00
adamdottv df4a9295c0 chore: cleanup 2025-05-12 10:46:14 -05:00
adamdottv 8cbfc581b5 chore: cleanup 2025-05-12 10:45:03 -05:00
Nicholas Hamilton 4bb350a09b Fix filepicker manual input (#146)
* fix: allows to type i while manual inputting filepath

* fix: file selection in filepicker focus mode

* remove duplicate code
2025-05-12 10:01:56 -05:00
adamdottv 17c5b9c12c fix: build 2025-05-12 09:59:57 -05:00
Ed Zynda 1f8580553c feat: custom commands (#133)
* Implement custom commands

* Add User: prefix

* Reuse var

* Check if the agent is busy and if so report a warning

* Update README

* fix typo

* Implement user and project scoped custom commands

* Allow for $ARGUMENTS

* UI tweaks

* Update internal/tui/components/dialog/arguments.go

Co-authored-by: Kujtim Hoxha <kujtimii.h@gmail.com>

* Also search in $HOME/.opencode/commands

---------

Co-authored-by: Kujtim Hoxha <kujtimii.h@gmail.com>
2025-05-12 09:58:59 -05:00
mineo f92b2b76dc replace github.com/google/generative-ai-go with github.com/googleapis/go-genai (#138)
* replace to github.com/googleapis/go-genai

* fix history logic

* small fixes

---------

Co-authored-by: Kujtim Hoxha <kujtimii.h@gmail.com>
2025-05-12 09:56:30 -05:00
adamdottv 1d1a1ddcbf fix: visual tweaks 2025-05-12 09:54:20 -05:00
adamdottv dfe5fd8d97 wip: refactoring 2025-05-12 09:44:56 -05:00
adamdottv ed9fba99c9 wip: refactoring 2025-05-12 08:43:34 -05:00
adamdottv f100777199 wip: logging improvements 2025-05-09 13:37:13 -05:00
adamdottv f41b7bbd0a chore: refactoring status updates 2025-05-08 12:03:59 -05:00
adamdottv e35ea2d448 fix: log page nav 2025-05-08 07:59:15 -05:00
adamdottv bab17d7520 feat: session manager 2025-05-08 07:58:37 -05:00
adamdottv 051d7d7936 chore: logging token usage 2025-05-06 14:40:00 -05:00
adamdottv b638dafe5f feat: better logs page 2025-05-06 14:22:37 -05:00
adamdottv e387b1f16c fix: openrouter require_parameters 2025-05-06 11:17:32 -05:00
adamdottv 71a68dd56d feat: add qwen3 models 2025-05-06 10:19:07 -05:00
adamdottv 3ee8ebd3d3 fix: auto-compact logic 2025-05-06 10:03:21 -05:00
adamdottv ef298b2f18 fix: hide empty messages 2025-05-05 14:38:33 -05:00
adamdottv 3cc08494a5 fix: pubsub leak and shutdown seq 2025-05-05 14:23:29 -05:00
adamdottv afcdabd095 fix: anthropic non-empty blocks 2025-05-05 12:00:09 -05:00
adamdottv efaba6c5b8 feat: hide tool calls 2025-05-05 11:25:34 -05:00
adamdottv 874715838a feat: show sender name and timestamp 2025-05-05 11:02:02 -05:00
adamdottv 167eb9ddfa fix: diagnostics visual in status bar 2025-05-05 07:04:33 -05:00
Joshua LaMorey-Salzmann fba344718f Config fix correcting loose viper string check, default model now set correctly (#147) 2025-05-05 06:56:10 -05:00
adamdottv cdd906e32e fix: bedrock supports attachments 2025-05-02 15:35:24 -05:00
phantomreactor ff0ef3bb43 feat: add support for images 2025-05-02 15:29:46 -05:00
adamdottv 0095832be3 chore: cleanup and logging 2025-05-02 15:24:47 -05:00
adamdottv 406ccf9b87 fix: diagnostics tool init 2025-05-02 15:24:47 -05:00
adamdottv f90d6238ed fix: bedrock context window 2025-05-02 15:24:47 -05:00
adamdottv f004a0b8c3 fix: anthropic non-empty blocks 2025-05-02 15:24:47 -05:00
adamdottv 49423da081 feat: compact command with auto-compact 2025-05-02 15:24:47 -05:00
adamdottv 364cf5b429 feat: write to context.md by default 2025-05-02 15:24:47 -05:00
adamdottv b2f24e38ed feat: better diagnostic visuals in status bar 2025-05-02 15:24:47 -05:00
adamdottv 49037e7b28 feat: better logs page 2025-05-02 15:24:47 -05:00
adamdottv c66832d299 fix: wording 2025-05-01 11:54:35 -05:00
adamdottv 7398b4ce70 fix: don't truncate task tool output 2025-05-01 11:35:54 -05:00
Kujtim Hoxha a61b2026eb add xai support (#135) 2025-05-01 11:08:26 -05:00
Aiden Cline 69ade34c2c fix: tweak the logic in config to ensure that env vs file configurations merge properly (#115) 2025-05-01 11:08:17 -05:00
Garrett Ladley fbca5441f6 feat: test for getContextFromPaths (#105)
* feat: test for getContextFromPaths

* fix: use testify
2025-05-01 11:08:06 -05:00
Kujtim Hoxha e4680caebb some small fixes 2025-05-01 11:07:47 -05:00
adamdottv e760d28c5a feat: show hunk headers 2025-05-01 09:02:14 -05:00
adamdottv 7d5f0f9d18 fix: pass input to EDITOR 2025-05-01 07:58:27 -05:00
adamdottv 515f4e8642 fix: visual tweaks 2025-05-01 07:32:04 -05:00
adamdottv f2b36b9234 fix: remove lsp tool 2025-05-01 07:28:37 -05:00
adamdottv f224978bbc fix: remove manual lsp definition 2025-05-01 07:24:53 -05:00
adamdottv 8819a37a05 fix: logo 2025-05-01 06:43:59 -05:00
adamdottv 769dff00ba fix: don't mark as init 2025-05-01 06:43:30 -05:00
adamdottv d1be7a984e fix: logo 2025-05-01 06:36:30 -05:00
adamdottv 3e30607a6d fix: minor prompt fix 2025-05-01 06:35:47 -05:00
adamdottv d08e58279d feat: lsp discovery 2025-05-01 06:26:20 -05:00
adamdottv 7bc542abff fix: better diagnostics visual 2025-04-30 15:23:19 -05:00
adamdottv ed50c36789 fix: lsp issues with tmp and deleted files 2025-04-30 12:20:51 -05:00
adamdottv 98cf65b425 fix: more intuitive keybinds 2025-04-30 11:34:21 -05:00
adamdottv 5406083850 fix: minor icon tweak 2025-04-30 11:33:30 -05:00
adamdottv 91ae9b33d3 feat: custom themes 2025-04-30 11:05:59 -05:00
adamdottv a42175c067 fix: info and hint icons 2025-04-30 07:47:14 -05:00
adamdottv 8497145db2 fix: status sizing 2025-04-30 07:47:10 -05:00
adamdottv 89544fad61 feat: tron theme 2025-04-30 07:46:35 -05:00
adamdottv 1151accf4b feat: tokyonight theme 2025-04-30 07:46:35 -05:00
adamdottv 1ae3f1830b feat: dracula theme 2025-04-30 07:46:35 -05:00
adamdottv 1e958b62ad feat: opencode theme (default) 2025-04-30 07:46:35 -05:00
adamdottv fdf5367f4f feat: monokai pro theme 2025-04-30 07:46:34 -05:00
adamdottv 0e8842a007 feat: onedark theme 2025-04-30 07:46:34 -05:00
adamdottv 060994f393 feat: flexoki theme 2025-04-30 07:46:34 -05:00
adamdottv 61b605e724 feat: themes 2025-04-30 07:46:34 -05:00
Adam 61d9dc9511 fix: allow text selection (#127) 2025-04-30 12:52:30 +02:00
Hunter Casten 76275e533e fix(openrouter): set api key from env (#129) 2025-04-30 12:50:57 +02:00
Isaac Scarrott 98e2910e82 feat: Add support for OpenRouter (#92)
release / goreleaser (push) Has been cancelled
* Add support for OpenRouter as a new model provider

- Introduced `ProviderOpenRouter` in the `models` package.
- Added OpenRouter-specific models, including `GPT41`, `GPT41Mini`, `GPT4o`, and others, with their configurations and costs.
- Updated `generateSchema` to include OpenRouter as a provider.
- Added OpenRouter-specific environment variable handling (`OPENROUTER_API_KEY`) in `config.go`.
- Implemented default model settings for OpenRouter agents in `setDefaultModelForAgent`.
- Updated `getProviderAPIKey` to retrieve the OpenRouter API key.
- Extended `SupportedModels` to include OpenRouter models.
- Added OpenRouter client initialization in the `provider` package.
- Modified `processGeneration` to handle `FinishReasonUnknown` in addition to `FinishReasonToolUse`.

* [feature/openrouter-provider] Add new models and provider to schema

- Added "deepseek-chat-free" and "deepseek-r1-free" to the list of supported models in `opencode-schema.json`.

* [feature/openrouter-provider] Add OpenRouter provider support and integrate new models

- Updated README.md to include OpenRouter as a supported provider and its configuration details.
- Added `OPENROUTER_API_KEY` to environment variable configuration.
- Introduced OpenRouter-specific models in `internal/llm/models/openrouter.go` with mappings to existing cost and token configurations.
- Updated `internal/config/config.go` to set default models for OpenRouter agents.
- Extended `opencode-schema.json` to include OpenRouter models in the schema definitions.
- Refactored model IDs and names to align with OpenRouter naming conventions.

* [feature/openrouter-provider] Refactor finish reason handling and tool call logic in agent and OpenAI provider

- Simplified finish reason check in `agent.go` by removing redundant variable assignment.
- Updated `openai.go` to override the finish reason to `FinishReasonToolUse` when tool calls are present.
- Ensured consistent finish reason handling in both `send` and `stream` methods of the OpenAI provider.

[feature/openrouter-provider] Refactor finish reason handling and tool call logic in agent and OpenAI provider

- Simplified finish reason check in `agent.go` by removing redundant variable assignment.
- Updated `openai.go` to override the finish reason to `FinishReasonToolUse` when tool calls are present.
- Ensured consistent finish reason handling in both `send` and `stream` methods of the OpenAI provider.

* **[feature/openrouter-provider] Add support for custom headers in OpenAI client configuration**

- Introduced a new `extraHeaders` field in the `openaiOptions` struct to allow specifying additional HTTP headers.
- Added logic in `newOpenAIClient` to apply `extraHeaders` to the OpenAI client configuration.
- Implemented a new option function `WithOpenAIExtraHeaders` to set custom headers in `openaiOptions`.
- Updated the OpenRouter provider configuration in `NewProvider` to include default headers (`HTTP-Referer` and `X-Title`) for OpenRouter API requests.

* Update OpenRouter model config and remove unsupported models

* [feature/openrouter-provider] Update OpenRouter models and default configurations

- Added new OpenRouter models: `claude-3.5-sonnet`, `claude-3-haiku`, `claude-3.7-sonnet`, `claude-3.5-haiku`, and `claude-3-opus` in `openrouter.go`.
- Updated default agent models in `config.go`:
  - `agents.coder.model` now uses `claude-3.7-sonnet`.
  - `agents.task.model` now uses `claude-3.7-sonnet`.
  - `agents.title.model` now uses `claude-3.5-haiku`.
- Updated `opencode-schema.json` to include the new models in the allowed list for schema validation.
- Adjusted logic in `setDefaultModelForAgent` to reflect the new default models.

* [feature/openrouter-provider] Remove unused ProviderEvent emission in stream function

The changes remove the emission of a `ProviderEvent` with type `EventContentStop` in the `stream` function of the `openaiClient` implementation. This event was sent upon successful stream completion but is no longer used.
2025-04-29 13:56:49 +02:00
Kujtim Hoxha 2941137416 fix diagnostics for deleted files 2025-04-28 19:37:42 +02:00
Aiden Cline b3c0285db3 feat: model selection for given provider (#57)
release / goreleaser (push) Has been cancelled
* feat: model selection for given provider

* tweak: adjust cfg validation func, remove duplicated logic, consolidate agent updating into agent.go

* tweak: make the model dialog scrollable, adjust padding slightly for modal"

* feat: add provider selection, add hints, simplify some logic, add horizontal scrolling support, additional scroll indicators"

* remove nav help

* update docs

* increase number of visible models, make horizontal scroll "wrap"

* add provider popularity rankings
2025-04-28 19:25:06 +02:00
YJG 805aeff83c feat: add azure openai models (#74) 2025-04-28 15:42:57 +02:00
Kujtim Hoxha bce2ec5c10 fix duplicate context
release / goreleaser (push) Has been cancelled
2025-04-27 20:43:27 +02:00
Kujtim Hoxha 292e9d90ca remove unnecessary var 2025-04-27 20:34:20 +02:00
Kujtim Hoxha 2b4441a0d1 fix context
release / goreleaser (push) Has been cancelled
2025-04-27 20:31:53 +02:00
Garrett Ladley 8f3a94df92 feat: configure context paths (#86)
release / goreleaser (push) Has been cancelled
2025-04-27 20:11:09 +02:00
Kujtim Hoxha 4415220555 fix minor issue
release / goreleaser (push) Has been cancelled
2025-04-27 19:24:46 +02:00
Kujtim Hoxha a3a04d8a54 fix gemini provider
release / goreleaser (push) Has been cancelled
2025-04-27 19:12:02 +02:00
Lukáš Loukota 792e2b164b fix: gemini tool calling 2025-04-27 19:12:02 +02:00
Kujtim Hoxha 5859dcdc00 small glob fixes 2025-04-27 18:01:31 +02:00
isaac-scarrott 3c2b0f4dd0 [feature/ripgrep-glob] Add ripgrep-based file globbing to improve performance
- Introduced `globWithRipgrep` function to perform file globbing using the `rg` (ripgrep) command.
- Updated `globFiles` to prioritize ripgrep-based globbing and fall back to doublestar-based globbing if ripgrep fails.
- Added logic to handle ripgrep command execution, output parsing, and filtering of hidden files.
- Ensured results are sorted by path length and limited to the specified maximum number of matches.
- Modified imports to include `os/exec` and `bytes` for ripgrep integration.
2025-04-27 18:01:31 +02:00
Kujtim Hoxha 9738886620 fix provider config
release / goreleaser (push) Has been cancelled
2025-04-27 14:44:40 +02:00
Sam Ottenhoff f3dccad54b Handle new Cursor rules format
1. Check if a path ends with a slash (/)
2. If it does, treat it as a directory and read all files within it
3. For directories like .cursor/rules/, it will scan all files and include their content in the prompt
4. Each file from a directory will be prefixed with "# From filename" for clarity
2025-04-27 14:17:06 +02:00
Kujtim Hoxha b3a8dbd0d9 fix retry warning
release / goreleaser (push) Has been cancelled
2025-04-27 14:08:09 +02:00
Garrett Mitchell Ladley d93694a979 feat: simpler diff implementation 2025-04-27 13:56:57 +02:00
Fuad 8a4d4152ce use workingDir if shellInstance is nil otherwise use cwd if shellInstance is not nil 2025-04-27 13:46:59 +02:00
Fuad f12386e558 use provided workingg dir 2025-04-27 13:46:59 +02:00
Fuad 94aeb7b7fe Fix nil pointer dereference in GetPersistentShell
Added nil check in GetPersistentShell before accessing
shellInstance.isAlive
to prevent panic when newPersistentShell returns nil due to shell
startup
errors. This resolves the "invalid memory address or nil pointer
dereference"
error that was occurring in the shell tool.
2025-04-27 13:46:59 +02:00
Kujtim Hoxha a35466cdb3 fix acc error
release / goreleaser (push) Has been cancelled
2025-04-25 21:58:14 +02:00
Kujtim Hoxha 170c7ad67a small fixes 2025-04-25 14:42:47 +02:00
Hunter Casten 7a62ab7675 feat(groq): add support for Groq using the OpenAI provider 2025-04-25 11:11:52 +02:00
Kujtim Hoxha 1586d757dc remove tool timeout 2025-04-24 22:35:17 +02:00
Dax Raad d043526200 add more installation options 2025-04-24 16:34:57 -04:00
Kujtim Hoxha aaf0bc14ba try fix 2025-04-24 22:27:51 +02:00
Kujtim Hoxha f2d9bb7ee3 try fix 2025-04-24 22:27:51 +02:00
Kujtim Hoxha de41703e20 change db driver 2025-04-24 22:27:51 +02:00
Kujtim Hoxha 2c24bfb7b3 fix kitty issues 2025-04-24 19:57:04 +02:00
Dax Raad 47a37b7dd6 back to disablign cgo 2025-04-24 12:46:11 -04:00
Dax Raad bdbf31f0b9 force CGO
release / goreleaser (push) Has been cancelled
2025-04-24 12:34:55 -04:00
Dax Raad 4e6560efb9 turn on cgo
release / goreleaser (push) Has been cancelled
2025-04-24 12:30:33 -04:00
Dax Raad f2f6efdd35 fix version ldflags
release / goreleaser (push) Has been cancelled
2025-04-24 12:26:42 -04:00
Kujtim Hoxha b106787a50 change package name 2025-04-24 18:26:16 +02:00
Kujtim Hoxha e1b2ce483f change additions/removals 2025-04-24 16:40:36 +02:00
Kujtim Hoxha c42d94c465 small fixes 2025-04-24 16:40:36 +02:00
Kujtim Hoxha f879a94c95 update mainter email 2025-04-24 16:40:36 +02:00
Kujtim Hoxha 6d05d5a7c3 small changes 2025-04-24 16:40:36 +02:00
Kujtim Hoxha 2c5003e3fc remove edit/normal mode 2025-04-24 16:40:36 +02:00
Dax Raad 1b22acbc58 sync 2025-04-22 21:29:32 -04:00
Dax Raad 7648a2d790 remove cache
release / goreleaser (push) Has been cancelled
2025-04-22 21:23:20 -04:00
Dax Raad 789ff3c59b sync
release / goreleaser (push) Has been cancelled
2025-04-22 21:21:53 -04:00
Dax Raad 259858df5f attempt cache builds 2025-04-22 21:20:35 -04:00
Dax 289f6af3de CI (#43)
release / goreleaser (push) Has been cancelled
* sync

* ci test

* ci test

* tag

* sync

* sync

* ci

* sync

* enable cgo

* sync

* add back brew

* switch to main
2025-04-22 21:16:14 -04:00
Kujtim Hoxha cded7ee9fd Merge pull request #38 from rgodha24/fix-provider-defaults
fix: set provider defaults correctly in AWS projects
2025-04-22 12:15:27 +02:00
Rohan Godha c02721ce6b fix: set provider defaults correctly in AWS projects 2025-04-22 00:53:05 -04:00
Kujtim Hoxha 5d7a9a98b3 Merge pull request #34 from shreyassanthu77/main 2025-04-22 00:06:03 +02:00
Shreyas Mididoddi ce8bf37f15 look for $HOME/.config for config in addition to $XDG_CONFIG_HOME/ 2025-04-22 00:49:23 +05:30
Kujtim Hoxha f33dff8772 Merge pull request #27 from kujtimiihoxha/opencode
OpenCode - Initial Implementation
2025-04-21 19:59:35 +02:00
Kujtim Hoxha 3a6a26981a init command 2025-04-21 19:53:55 +02:00
Kujtim Hoxha d7569d79c6 prevent the editor when agent busy 2025-04-21 16:50:10 +02:00
Kujtim Hoxha ed3518d075 small things 2025-04-21 16:24:38 +02:00
Kujtim Hoxha d03a73a8d3 update readme 2025-04-21 16:22:52 +02:00
Kujtim Hoxha 1e11805efc add description 2025-04-21 15:52:32 +02:00
Kujtim Hoxha a8d5787e8e config validation 2025-04-21 14:29:28 +02:00
Kujtim Hoxha 9ae6af8856 remove old logs 2025-04-21 13:46:32 +02:00
Kujtim Hoxha e7bb99baab fix the memory bug 2025-04-21 13:42:29 +02:00
Kujtim Hoxha 1da298e755 fix anthropic 2025-04-21 13:42:29 +02:00
Kujtim Hoxha c40e68496d add context to the prompt 2025-04-21 13:42:29 +02:00
Kujtim Hoxha 8e160488ff improve cache 2025-04-21 13:42:29 +02:00
Kujtim Hoxha 2de5127417 initial tool call stream 2025-04-21 13:42:29 +02:00
Kujtim Hoxha 2b5a33e476 lsp improvements 2025-04-21 13:42:29 +02:00
Kujtim Hoxha bf8cd3bd12 add bedrock model 2025-04-21 13:42:29 +02:00
Kujtim Hoxha 72afeb9f54 small fixes 2025-04-21 13:42:29 +02:00
Kujtim Hoxha 333ea6ec4b implement patch, update ui, improve rendering 2025-04-21 13:42:27 +02:00
Kujtim Hoxha 05d0e86f10 update logs 2025-04-21 13:42:02 +02:00
Kujtim Hoxha e3a62736db add license 2025-04-21 13:42:02 +02:00
Kujtim Hoxha c24e3c18e0 small improvements 2025-04-21 13:42:02 +02:00
Kujtim Hoxha caea293759 small fixes 2025-04-21 13:42:02 +02:00
Kujtim Hoxha 36172979b4 Update agent prompt, improve TUI patch UI, remove obsolete tool tests
- Replace and expand agent coder prompt for clarity and safety
- Add patch tool and TUI dialog support for patch diffs
- Sort sidebar modified files by name
- Remove Bash/Edit/Sourcegraph/Write tool tests

🤖 Generated with opencode
Co-Authored-By: opencode <noreply@opencode.ai>
2025-04-21 13:42:02 +02:00
Kujtim Hoxha cc07f7a186 rename to opencode 2025-04-21 13:42:02 +02:00
Kujtim Hoxha bbfa60c787 reimplement agent,provider and add file history 2025-04-21 13:42:00 +02:00
Kujtim Hoxha 76b4065f17 update gitignore 2025-04-21 13:41:27 +02:00
Kujtim Hoxha 1cdd24fbc7 minor fixes 2025-04-21 13:41:27 +02:00
Kujtim Hoxha f6be348bf7 fix segment diff and add new theme 2025-04-21 13:41:27 +02:00
Kujtim Hoxha 013694832f fix diff 2025-04-21 13:41:27 +02:00
Kujtim Hoxha 0130bde1ed remove node dependency and implement diff format 2025-04-21 13:41:27 +02:00
Kujtim Hoxha 0b3e5f5bd4 handle errors correctly in the other tools 2025-04-21 13:41:27 +02:00
Kujtim Hoxha 921f5ee5bd handle errors correctly in the edit tool 2025-04-21 13:41:27 +02:00
Kujtim Hoxha 9ae05fea12 handle errros correctly in the bash tool 2025-04-21 13:41:27 +02:00
Kujtim Hoxha 80cd75c4fb handle errors correctly in the agent tool 2025-04-21 13:41:27 +02:00
Kujtim Hoxha cdc5f209dc cleanup diff, cleanup agent 2025-04-21 13:41:27 +02:00
Kujtim Hoxha 3ad983db0f cleanup app, config and root 2025-04-21 13:41:25 +02:00
Kujtim Hoxha 5601466fe1 cleanup config 2025-04-21 13:40:35 +02:00
Kujtim Hoxha d63d0c4c44 wip files 2025-04-21 13:38:42 +02:00
Kujtim Hoxha bd2ec29b65 add initial git support 2025-04-21 13:38:42 +02:00
Kujtim Hoxha 0697dcc1d9 implement nested tool calls and initial setup for result metadata 2025-04-21 13:38:42 +02:00
Kujtim Hoxha 8d874b839d add initial message handling 2025-04-21 13:38:42 +02:00
Kujtim Hoxha 08bd75bb6e add initial mock sidebar 2025-04-21 13:38:42 +02:00
Kujtim Hoxha 23e7a95083 intiial layout 2025-04-21 13:38:42 +02:00
Kujtim Hoxha 6b1c64bcc7 Merge pull request #30 from jdlms/update/config-GPT4.1
OpenAI model config changed to GPT-4.1
2025-04-15 11:54:38 +02:00
jdlms 633d1d286a OpenAI model config changed to GPT-4.1 2025-04-15 09:36:47 +02:00
Kujtim Hoxha 9004b91bf9 Merge pull request #28 from naxodev/refactor/improve-process-management
refactor: use context for automatic lsp process cleanup
2025-04-13 12:41:55 +02:00
Nacho Vazquez bd2cb4b5b9 refactor: use context for automatic lsp process cleanup 2025-04-12 12:43:23 -03:00
Kujtim Hoxha 6fe85c66e7 small change 2025-04-11 13:21:41 +02:00
Kujtim Hoxha c08f5a7902 fix cancell 2025-04-10 15:27:42 +02:00
Kujtim Hoxha 36f201d5d3 fix logs and add cancellation 2025-04-10 13:29:44 +02:00
Kujtim Hoxha 0b007b9c77 add logs border title 2025-04-09 20:10:40 +02:00
Kujtim Hoxha 21cfe9a243 default the region to us 2025-04-09 20:05:22 +02:00
Kujtim Hoxha 65055c7687 minor fixes 2025-04-09 20:04:44 +02:00
Kujtim Hoxha 2af1bbb828 Merge pull request #25 from kujtimiihoxha/cleanup-logs-status
Cleanup Logs and Status
2025-04-09 19:19:45 +02:00
Kujtim Hoxha 635324d386 small fixes 2025-04-09 19:18:51 +02:00
Kujtim Hoxha d39d52d95d finish logs page 2025-04-09 19:07:39 +02:00
Kujtim Hoxha b12ca55594 remove logs 2025-04-09 17:51:00 +02:00
Kujtim Hoxha e090223b13 Merge pull request #26 from kujtimiihoxha/bedrock
Add initial support for bedrock
2025-04-09 17:49:24 +02:00
Kujtim Hoxha 939ae03f42 add bedrock support 2025-04-09 17:45:41 +02:00
Kujtim Hoxha 0d8d324ac6 cleanup retry 2025-04-08 20:46:40 +02:00
Kujtim Hoxha fde04bbf85 Merge pull request #22 from adamdottv/adam/retries
fix(anthropic): better 429/529 handling
2025-04-08 20:32:57 +02:00
Kujtim Hoxha 124bd57c50 Merge pull request #24 from kujtimiihoxha/cleanup-tools
Cleanup tools
2025-04-08 19:42:59 +02:00
Kujtim Hoxha c571283ac2 fix typo 2025-04-08 19:41:21 +02:00
Kujtim Hoxha 94923948e1 structure tools the same
- add some tests
- fix some tests
- change how we handle permissions
2025-04-08 19:15:23 +02:00
adamdottv 4385fb3219 fix: status messages 2025-04-08 11:45:26 -05:00
Kujtim Hoxha f463ce6694 improve status message handling 2025-04-07 19:43:31 +02:00
adamdottv 57a2210d8d fix: handle anthropic 429s 2025-04-07 10:43:54 -05:00
Kujtim Hoxha 5acf0cba60 update readme 2025-04-07 15:37:47 +02:00
Kujtim Hoxha a6db4f652c remove test code 2025-04-07 12:56:54 +02:00
adamdottv 50d8fbbb40 fix(anthropic): increase max retries 2025-04-05 15:07:43 -05:00
Kujtim Hoxha b8383f5590 Merge pull request #21 from adamdottv/adam/anthropic-empty-messages 2025-04-05 09:32:07 +02:00
adamdottv 781b296180 fix(anthropic): skip empty messages 2025-04-04 14:33:55 -05:00
Kujtim Hoxha 6419973667 Enhance bash tool security and improve permission dialog UI
- Expand safe command list with common dev tools (git, go, node, python, etc.)
- Improve multi-word command detection for better security checks
- Add scrollable viewport to permission dialog for better diff viewing
- Fix command batching in TUI update to properly handle multiple commands

🤖 Generated with termai
Co-Authored-By: termai <noreply@termai.io>
2025-04-04 15:41:25 +02:00
Kujtim Hoxha 8f8b403bbc Make tool error results visually distinct with red border
- Change tool result border color to red when displaying errors
- Keep green border for successful results
- Improve visual distinction between error and success states

🤖 Generated with termai
Co-Authored-By: termai <noreply@termai.io>
2025-04-04 15:08:32 +02:00
Kujtim Hoxha fa5840cf75 Modernize min/max usage in permission dialog
- Replace if statements with direct min/max function calls
- Fix linter hint about using modern min/max functions
- Simplify code for better readability

🤖 Generated with termai
Co-Authored-By: termai <noreply@termai.io>
2025-04-04 15:05:48 +02:00
Kujtim Hoxha 1d467dfda3 Fix status message clearing issue
- Add missing case for ClearStatusMsg in main TUI update function
- Ensures status messages are properly cleared after timeout

🤖 Generated with termai
Co-Authored-By: termai <noreply@termai.io>
2025-04-04 15:04:58 +02:00
Kujtim Hoxha 3aef4dedcf Improve LSP diagnostics handling for file operations
- Split LSP file notification into separate functions
- Add waitForLspDiagnostics function to wait for diagnostics after file changes
- Move LSP diagnostics to after file operations in edit and write tools
- Fix string splitting in diff generation
- Reduce diagnostics timeout from 10 to 5 seconds

🤖 Generated with termai
Co-Authored-By: termai <noreply@termai.io>
2025-04-04 15:03:50 +02:00
Kujtim Hoxha a1f6147506 Add auto-removal of status bar messages after timeout
🤖 Generated with termai
Co-Authored-By: termai <noreply@termai.io>
2025-04-04 14:52:30 +02:00
Kujtim Hoxha b28c1e0c03 Improve diff display with colored formatting in permission dialogs
- Add colored formatting for diff display (green for additions, red for removals)
- Create a dedicated formatDiff function to handle diff styling
- Restructure permission dialog render function for better organization
- Apply custom styling to different line types in diffs

🤖 Generated with termai
Co-Authored-By: termai <noreply@termai.io>
2025-04-04 14:45:02 +02:00
Kujtim Hoxha ef15976f5d Make permission dialog height dynamic based on content
- Adjust dialog height for bash commands based on content length
- Reduce default dialog size for bash commands to be more compact
- Add strings import for string splitting

🤖 Generated with termai
Co-Authored-By: termai <noreply@termai.io>
2025-04-04 14:40:41 +02:00
Kujtim Hoxha c185dc84d6 Enhance UI feedback and improve file diff visualization
- Improve diff display in permission dialogs with better formatting
- Add visual indicators for focus changes in permission dialogs
- Increase diagnostics timeout from 5 to 10 seconds
- Fix write tool to show proper diffs for existing files
- Update status component to properly handle messages

🤖 Generated with termai
Co-Authored-By: termai <noreply@termai.io>
2025-04-04 14:36:57 +02:00
Kujtim Hoxha 6bb1c84f7f Improve Sourcegraph tool with context window and fix diagnostics
- Add context_window parameter to control code context display
- Fix LSP diagnostics notification handling with proper async waiting
- Switch to keyword search pattern for better results
- Add Sourcegraph tool to task agent

🤖 Generated with termai
Co-Authored-By: termai <noreply@termai.io>
2025-04-04 14:23:08 +02:00
Kujtim Hoxha eb9877ee20 add sourcegraph tool 2025-04-04 13:31:21 +02:00
Kujtim Hoxha f8e05a0d9a small improvements to the prompt 2025-04-03 20:30:30 +02:00
Kujtim Hoxha 49b593d9dd small fixes 2025-04-03 19:50:31 +02:00
Kujtim Hoxha 230917bbbf add termai config 2025-04-03 17:40:05 +02:00
Kujtim Hoxha 795b369219 small fixes 2025-04-03 17:38:28 +02:00
Kujtim Hoxha cfdd687216 add initial lsp support 2025-04-03 17:23:41 +02:00
Kujtim Hoxha afd9ad0560 rework llm 2025-04-01 13:38:54 +02:00
Kujtim Hoxha 904061c243 additional tools 2025-03-26 01:12:30 +01:00
Kujtim Hoxha 005b8ac167 initial working agent 2025-03-24 11:47:39 +01:00
Kujtim Hoxha e7258e38ae initial agent setup 2025-03-23 22:25:31 +01:00
Kujtim Hoxha 8daa6e774a add initial stuff 2025-03-23 19:19:08 +01:00
Kujtim Hoxha 796bbf4d66 small size fix and colors added 2025-03-23 15:17:51 +01:00
Kujtim Hoxha 7844cacb25 add help 2025-03-23 15:00:12 +01:00
Kujtim Hoxha 4b0ea68d7a initial 2025-03-21 18:20:28 +01:00
338 changed files with 53394 additions and 0 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
+26
View File
@@ -0,0 +1,26 @@
name: deploy
on:
push:
branches:
- dev
- production
workflow_dispatch:
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: oven-sh/setup-bun@v1
with:
bun-version: 1.2.17
- run: bun install
- run: bun sst deploy --stage=${{ github.ref_name }}
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+63
View File
@@ -0,0 +1,63 @@
name: publish
on:
workflow_dispatch:
push:
branches:
- dev
tags:
- "*"
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 }}
+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
+8
View File
@@ -0,0 +1,8 @@
.DS_Store
node_modules
.opencode
.sst
.env
.idea
.vscode
openapi.json
+21
View File
@@ -0,0 +1,21 @@
MIT License
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
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+79
View File
@@ -0,0 +1,79 @@
<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://discord.gg/opencode"><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](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
---
### Installation
```bash
# YOLO
curl -fsSL https://opencode.ai/install | 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
```
> **Note:** Remove versions older than 0.1.x before installing
### Documentation
For more info on how to configure opencode [**head over to our docs**](https://opencode.ai/docs).
### Contributing
For any new features we'd appreciate it if you could open an issue first to discuss what you'd like to implement. We're pretty responsive there and it'll save you from working on something that we don't end up using. No need to do this for simpler fixes.
> **Note**: Please talk to us via github issues before spending time working on
> a new feature
To run opencode locally you need.
- Bun
- Golang 1.24.x
And run.
```bash
$ bun install
$ bun run packages/opencode/src/index.ts
```
#### Development Notes
**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.
### FAQ
#### How is this different than Claude Code?
It's very similar to Claude Code in terms of capability. Here are the key differences:
- 100% open source
- Not coupled to any provider. Although Anthropic is recommended, opencode can be used with OpenAI, Google or even local models. As models evolve the gaps between them will close and pricing will drop so being provider agnostic is important.
- 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.
#### What's the other repo?
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).
---
**Join our community** [Discord](https://discord.gg/opencode) | [YouTube](https://www.youtube.com/c/sst-dev) | [X.com](https://x.com/SST_dev)
+12
View File
@@ -0,0 +1,12 @@
# 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) |
View File
View File
+1912
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -0,0 +1,2 @@
[install]
exact = true
View File
View File
+46
View File
@@ -0,0 +1,46 @@
export const domain = (() => {
if ($app.stage === "production") return "opencode.ai"
if ($app.stage === "dev") return "dev.opencode.ai"
return `${$app.stage}.dev.opencode.ai`
})()
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],
transform: {
worker: (args) => {
args.logpush = true
args.bindings = $resolve(args.bindings).apply((bindings) => [
...bindings,
{
name: "SYNC_SERVER",
type: "durable_object_namespace",
className: "SyncServer",
},
])
args.migrations = {
// 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.x.Astro("Web", {
domain,
path: "packages/web",
environment: {
// For astro config
SST_STAGE: $app.stage,
VITE_API_URL: api.url,
},
})
Executable
+189
View File
@@ -0,0 +1,189 @@
#!/usr/bin/env bash
set -euo pipefail
APP=opencode
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
ORANGE='\033[38;2;255;140;0m'
NC='\033[0m' # No Color
requested_version=${VERSION:-}
os=$(uname -s | tr '[:upper:]' '[:lower:]')
if [[ "$os" == "darwin" ]]; then
os="darwin"
fi
arch=$(uname -m)
if [[ "$arch" == "aarch64" ]]; then
arch="arm64"
elif [[ "$arch" == "x86_64" ]]; then
arch="x64"
fi
filename="$APP-$os-$arch.zip"
case "$filename" in
*"-linux-"*)
[[ "$arch" == "x64" || "$arch" == "arm64" ]] || exit 1
;;
*"-darwin-"*)
[[ "$arch" == "x64" || "$arch" == "arm64" ]] || exit 1
;;
*"-windows-"*)
[[ "$arch" == "x64" ]] || exit 1
;;
*)
echo "${RED}Unsupported OS/Arch: $os/$arch${NC}"
exit 1
;;
esac
INSTALL_DIR=$HOME/.opencode/bin
mkdir -p "$INSTALL_DIR"
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
echo "${RED}Failed to fetch version information${NC}"
exit 1
fi
else
url="https://github.com/sst/opencode/releases/download/v${requested_version}/$filename"
specific_version=$requested_version
fi
print_message() {
local level=$1
local message=$2
local color=""
case $level in
info) color="${GREEN}" ;;
warning) color="${YELLOW}" ;;
error) color="${RED}" ;;
esac
echo -e "${color}${message}${NC}"
}
check_version() {
if command -v opencode >/dev/null 2>&1; then
opencode_path=$(which opencode)
## TODO: check if version is installed
# installed_version=$(opencode version)
installed_version="0.0.1"
installed_version=$(echo $installed_version | awk '{print $2}')
if [[ "$installed_version" != "$specific_version" ]]; then
print_message info "Installed version: ${YELLOW}$installed_version."
else
print_message info "Version ${YELLOW}$specific_version${GREEN} already installed"
exit 0
fi
fi
}
download_and_install() {
print_message info "Downloading ${ORANGE}opencode ${GREEN}version: ${YELLOW}$specific_version ${GREEN}..."
mkdir -p opencodetmp && cd opencodetmp
curl -# -L -o "$filename" "$url"
unzip -q "$filename"
mv opencode "$INSTALL_DIR"
cd .. && rm -rf opencodetmp
}
check_version
download_and_install
add_to_path() {
local config_file=$1
local command=$2
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"
else
print_message warning "Manually add the directory to $config_file (or similar):"
print_message info " $command"
fi
}
XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-$HOME/.config}
current_shell=$(basename "$SHELL")
case $current_shell in
fish)
config_files="$HOME/.config/fish/config.fish"
;;
zsh)
config_files="$HOME/.zshrc $HOME/.zshenv $XDG_CONFIG_HOME/zsh/.zshrc $XDG_CONFIG_HOME/zsh/.zshenv"
;;
bash)
config_files="$HOME/.bashrc $HOME/.bash_profile $HOME/.profile $XDG_CONFIG_HOME/bash/.bashrc $XDG_CONFIG_HOME/bash/.bash_profile"
;;
ash)
config_files="$HOME/.ashrc $HOME/.profile /etc/profile"
;;
sh)
config_files="$HOME/.ashrc $HOME/.profile /etc/profile"
;;
*)
# Default case if none of the above matches
config_files="$HOME/.bashrc $HOME/.bash_profile $XDG_CONFIG_HOME/bash/.bashrc $XDG_CONFIG_HOME/bash/.bash_profile"
;;
esac
config_file=""
for file in $config_files; do
if [[ -f $file ]]; then
config_file=$file
break
fi
done
if [[ -z $config_file ]]; then
print_message error "No config file found for $current_shell. Checked files: ${config_files[@]}"
exit 1
fi
if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then
case $current_shell in
fish)
add_to_path "$config_file" "fish_add_path $INSTALL_DIR"
;;
zsh)
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
;;
bash)
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
;;
ash)
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
;;
sh)
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"
;;
esac
fi
if [ -n "${GITHUB_ACTIONS-}" ] && [ "${GITHUB_ACTIONS}" == "true" ]; then
echo "$INSTALL_DIR" >> $GITHUB_PATH
print_message info "Added $INSTALL_DIR to \$GITHUB_PATH"
fi
+25
View File
@@ -0,0 +1,25 @@
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"weather": {
"type": "local",
"command": ["opencode", "x", "@h1deya/mcp-server-weather"]
}
},
"experimental": {
"hook": {
"file_edited": {
".json": [
{
"command": ["bun", "run", "prettier", "$FILE"]
}
]
},
"session_completed": [
{
"command": ["touch", "./node_modules/foo"]
}
]
}
}
}
+45
View File
@@ -0,0 +1,45 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "opencode",
"private": true,
"type": "module",
"packageManager": "bun@1.2.14",
"scripts": {
"dev": "bun run packages/opencode/src/index.ts",
"typecheck": "bun run --filter='*' typecheck",
"stainless": "bun run ./packages/opencode/src/index.ts serve ",
"postinstall": "./scripts/hooks"
},
"workspaces": {
"packages": [
"packages/*"
],
"catalog": {
"typescript": "5.8.2",
"@types/node": "22.13.9",
"zod": "3.25.49",
"ai": "5.0.0-beta.7"
}
},
"devDependencies": {
"prettier": "3.5.3",
"sst": "3.17.8"
},
"repository": {
"type": "git",
"url": "https://github.com/sst/opencode"
},
"license": "MIT",
"prettier": {
"semi": false,
"printWidth": 120
},
"trustedDependencies": [
"esbuild",
"protobufjs",
"sharp"
],
"patchedDependencies": {
"ai@4.3.16": "patches/ai@4.3.16.patch"
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"name": "@opencode/function",
"version": "0.0.1",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
"devDependencies": {
"@cloudflare/workers-types": "4.20250522.0",
"typescript": "catalog:",
"@types/node": "catalog:"
}
}
+222
View File
@@ -0,0 +1,222 @@
import { DurableObject } from "cloudflare:workers"
import { randomUUID } from "node:crypto"
type Env = {
SYNC_SERVER: DurableObjectNamespace<SyncServer>
Bucket: R2Bucket
WEB_DOMAIN: string
}
export class SyncServer extends DurableObject<Env> {
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env)
}
async fetch() {
console.log("SyncServer subscribe")
const webSocketPair = new WebSocketPair()
const [client, server] = Object.values(webSocketPair)
this.ctx.acceptWebSocket(server)
const data = await this.ctx.storage.list()
Array.from(data.entries())
.filter(([key, _]) => key.startsWith("session/"))
.map(([key, content]) => server.send(JSON.stringify({ key, content })))
return new Response(null, {
status: 101,
webSocket: client,
})
}
async webSocketMessage(ws, message) {}
async webSocketClose(ws, code, reason, wasClean) {
ws.close(code, "Durable Object is closing WebSocket")
}
async publish(key: string, content: any) {
const sessionID = await this.getSessionID()
if (!key.startsWith(`session/info/${sessionID}`) && !key.startsWith(`session/message/${sessionID}/`))
return new Response("Error: Invalid key", { status: 400 })
// store message
await this.env.Bucket.put(`share/${key}.json`, JSON.stringify(content), {
httpMetadata: {
contentType: "application/json",
},
})
await this.ctx.storage.put(key, content)
const clients = this.ctx.getWebSockets()
console.log("SyncServer publish", key, "to", clients.length, "subscribers")
for (const client of clients) {
client.send(JSON.stringify({ key, content }))
}
}
public async share(sessionID: string) {
let secret = await this.getSecret()
if (secret) return secret
secret = randomUUID()
await this.ctx.storage.put("secret", secret)
await this.ctx.storage.put("sessionID", sessionID)
return secret
}
public async getData() {
const data = await this.ctx.storage.list()
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")
}
private async getSessionID() {
return this.ctx.storage.get<string>("sessionID")
}
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()
}
static shortName(id: string) {
return id.substring(id.length - 8)
}
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
const url = new URL(request.url)
const splits = url.pathname.split("/")
const method = splits[1]
if (request.method === "GET" && method === "") {
return new Response("Hello, world!", {
headers: { "Content-Type": "text/plain" },
})
}
if (request.method === "POST" && method === "share_create") {
const body = await request.json<any>()
const sessionID = body.sessionID
const short = SyncServer.shortName(sessionID)
const id = env.SYNC_SERVER.idFromName(short)
const stub = env.SYNC_SERVER.get(id)
const secret = await stub.share(sessionID)
return new Response(
JSON.stringify({
secret,
url: `https://${env.WEB_DOMAIN}/s/${short}`,
}),
{
headers: { "Content-Type": "application/json" },
},
)
}
if (request.method === "POST" && method === "share_delete") {
const body = await request.json<any>()
const sessionID = body.sessionID
const secret = body.secret
const id = env.SYNC_SERVER.idFromName(SyncServer.shortName(sessionID))
const stub = env.SYNC_SERVER.get(id)
await stub.assertSecret(secret)
await stub.clear()
return new Response(JSON.stringify({}), {
headers: { "Content-Type": "application/json" },
})
}
if (request.method === "POST" && method === "share_delete_admin") {
const id = env.SYNC_SERVER.idFromName("oVF8Rsiv")
const stub = env.SYNC_SERVER.get(id)
await stub.clear()
return new Response(JSON.stringify({}), {
headers: { "Content-Type": "application/json" },
})
}
if (request.method === "POST" && method === "share_sync") {
const body = await request.json<{
sessionID: string
secret: string
key: string
content: any
}>()
const name = SyncServer.shortName(body.sessionID)
const id = env.SYNC_SERVER.idFromName(name)
const stub = env.SYNC_SERVER.get(id)
await stub.assertSecret(body.secret)
await stub.publish(body.key, body.content)
return new Response(JSON.stringify({}), {
headers: { "Content-Type": "application/json" },
})
}
if (request.method === "GET" && method === "share_poll") {
const upgradeHeader = request.headers.get("Upgrade")
if (!upgradeHeader || upgradeHeader !== "websocket") {
return new Response("Error: Upgrade header is required", {
status: 426,
})
}
const id = url.searchParams.get("id")
console.log("share_poll", id)
if (!id) return new Response("Error: Share ID is required", { status: 400 })
const stub = env.SYNC_SERVER.get(env.SYNC_SERVER.idFromName(id))
return stub.fetch(request)
}
if (request.method === "GET" && method === "share_data") {
const id = url.searchParams.get("id")
console.log("share_data", id)
if (!id) return new Response("Error: Share ID is required", { status: 400 })
const stub = env.SYNC_SERVER.get(env.SYNC_SERVER.idFromName(id))
const data = await stub.getData()
let info
const messages: Record<string, any> = {}
data.forEach((d) => {
const [root, type, ...splits] = d.key.split("/")
if (root !== "session") return
if (type === "info") {
info = d.content
return
}
if (type === "message") {
const [, messageID] = splits
messages[messageID] = d.content
}
})
return new Response(
JSON.stringify({
info,
messages,
}),
{
headers: { "Content-Type": "application/json" },
},
)
}
},
}
+25
View File
@@ -0,0 +1,25 @@
/* This file is auto-generated by SST. Do not edit. */
/* tslint:disable */
/* eslint-disable */
/* deno-fmt-ignore-file */
import "sst"
declare module "sst" {
export interface Resource {
Web: {
type: "sst.cloudflare.Astro"
url: string
}
}
}
// cloudflare
import * as cloudflare from "@cloudflare/workers-types"
declare module "sst" {
export interface Resource {
Api: cloudflare.Service
Bucket: cloudflare.R2Bucket
}
}
import "sst"
export {}
+9
View File
@@ -0,0 +1,9 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@tsconfig/node22/tsconfig.json",
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "bundler",
"types": ["@cloudflare/workers-types", "node"]
}
}
+4
View File
@@ -0,0 +1,4 @@
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.
+15
View File
@@ -0,0 +1,15 @@
# js
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run index.ts
```
This project was created using `bun init` in bun v1.2.12. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
+61
View File
@@ -0,0 +1,61 @@
#!/bin/sh
set -e
if [ -n "$OPENCODE_BIN_PATH" ]; then
resolved="$OPENCODE_BIN_PATH"
else
# Get the real path of this script, resolving any symlinks
script_path="$0"
while [ -L "$script_path" ]; do
link_target="$(readlink "$script_path")"
case "$link_target" in
/*) script_path="$link_target" ;;
*) script_path="$(dirname "$script_path")/$link_target" ;;
esac
done
script_dir="$(dirname "$script_path")"
script_dir="$(cd "$script_dir" && pwd)"
# Map platform names
case "$(uname -s)" in
Darwin) platform="darwin" ;;
Linux) platform="linux" ;;
MINGW*|CYGWIN*|MSYS*) platform="win32" ;;
*) platform="$(uname -s | tr '[:upper:]' '[:lower:]')" ;;
esac
# Map architecture names
case "$(uname -m)" in
x86_64|amd64) arch="x64" ;;
aarch64) arch="arm64" ;;
armv7l) arch="arm" ;;
*) arch="$(uname -m)" ;;
esac
name="opencode-${platform}-${arch}"
binary="opencode"
[ "$platform" = "win32" ] && binary="opencode.exe"
# Search for the binary starting from real script location
resolved=""
current_dir="$script_dir"
while [ "$current_dir" != "/" ]; do
candidate="$current_dir/node_modules/$name/bin/$binary"
if [ -f "$candidate" ]; then
resolved="$candidate"
break
fi
current_dir="$(dirname "$current_dir")"
done
if [ -z "$resolved" ]; then
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
# Handle SIGINT gracefully
trap '' INT
# Execute the binary with all arguments
exec "$resolved" "$@"
+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%" %*
+369
View File
@@ -0,0 +1,369 @@
{
"type": "object",
"properties": {
"$schema": {
"type": "string",
"description": "JSON schema reference for configuration validation"
},
"theme": {
"type": "string",
"description": "Theme name to use for the interface"
},
"keybinds": {
"type": "object",
"properties": {
"leader": {
"type": "string",
"description": "Leader key for keybind combinations"
},
"help": {
"type": "string",
"description": "Show help dialog"
},
"editor_open": {
"type": "string",
"description": "Open external editor"
},
"session_new": {
"type": "string",
"description": "Create a new session"
},
"session_list": {
"type": "string",
"description": "List all sessions"
},
"session_share": {
"type": "string",
"description": "Share current session"
},
"session_interrupt": {
"type": "string",
"description": "Interrupt current session"
},
"session_compact": {
"type": "string",
"description": "Toggle compact mode for session"
},
"tool_details": {
"type": "string",
"description": "Show tool details"
},
"model_list": {
"type": "string",
"description": "List available models"
},
"theme_list": {
"type": "string",
"description": "List available themes"
},
"project_init": {
"type": "string",
"description": "Initialize project configuration"
},
"input_clear": {
"type": "string",
"description": "Clear input field"
},
"input_paste": {
"type": "string",
"description": "Paste from clipboard"
},
"input_submit": {
"type": "string",
"description": "Submit input"
},
"input_newline": {
"type": "string",
"description": "Insert newline in input"
},
"history_previous": {
"type": "string",
"description": "Navigate to previous history item"
},
"history_next": {
"type": "string",
"description": "Navigate to next history item"
},
"messages_page_up": {
"type": "string",
"description": "Scroll messages up by one page"
},
"messages_page_down": {
"type": "string",
"description": "Scroll messages down by one page"
},
"messages_half_page_up": {
"type": "string",
"description": "Scroll messages up by half page"
},
"messages_half_page_down": {
"type": "string",
"description": "Scroll messages down by half page"
},
"messages_previous": {
"type": "string",
"description": "Navigate to previous message"
},
"messages_next": {
"type": "string",
"description": "Navigate to next message"
},
"messages_first": {
"type": "string",
"description": "Navigate to first message"
},
"messages_last": {
"type": "string",
"description": "Navigate to last message"
},
"app_exit": {
"type": "string",
"description": "Exit the application"
}
},
"additionalProperties": false,
"description": "Custom keybind configurations"
},
"autoshare": {
"type": "boolean",
"description": "Share newly created sessions automatically"
},
"autoupdate": {
"type": "boolean",
"description": "Automatically update to the latest version"
},
"disabled_providers": {
"type": "array",
"items": {
"type": "string"
},
"description": "Disable providers that are loaded automatically"
},
"model": {
"type": "string",
"description": "Model to use in the format of provider/model, eg anthropic/claude-2"
},
"provider": {
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"api": {
"type": "string"
},
"name": {
"type": "string"
},
"env": {
"type": "array",
"items": {
"type": "string"
}
},
"id": {
"type": "string"
},
"npm": {
"type": "string"
},
"models": {
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"attachment": {
"type": "boolean"
},
"reasoning": {
"type": "boolean"
},
"temperature": {
"type": "boolean"
},
"tool_call": {
"type": "boolean"
},
"cost": {
"type": "object",
"properties": {
"input": {
"type": "number"
},
"output": {
"type": "number"
},
"cache_read": {
"type": "number"
},
"cache_write": {
"type": "number"
}
},
"required": ["input", "output"],
"additionalProperties": false
},
"limit": {
"type": "object",
"properties": {
"context": {
"type": "number"
},
"output": {
"type": "number"
}
},
"required": ["context", "output"],
"additionalProperties": false
},
"id": {
"type": "string"
},
"options": {
"type": "object",
"additionalProperties": {}
}
},
"additionalProperties": false
}
},
"options": {
"type": "object",
"additionalProperties": {}
}
},
"required": ["models"],
"additionalProperties": false
},
"description": "Custom provider configurations and model overrides"
},
"mcp": {
"type": "object",
"additionalProperties": {
"anyOf": [
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "local",
"description": "Type of MCP server connection"
},
"command": {
"type": "array",
"items": {
"type": "string"
},
"description": "Command and arguments to run the MCP server"
},
"environment": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"description": "Environment variables to set when running the MCP server"
},
"enabled": {
"type": "boolean",
"description": "Enable or disable the MCP server on startup"
}
},
"required": ["type", "command"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "remote",
"description": "Type of MCP server connection"
},
"url": {
"type": "string",
"description": "URL of the remote MCP server"
},
"enabled": {
"type": "boolean",
"description": "Enable or disable the MCP server on startup"
}
},
"required": ["type", "url"],
"additionalProperties": false
}
]
},
"description": "MCP (Model Context Protocol) server configurations"
},
"instructions": {
"type": "array",
"items": {
"type": "string"
},
"description": "Additional instruction files or patterns to include"
},
"experimental": {
"type": "object",
"properties": {
"hook": {
"type": "object",
"properties": {
"file_edited": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"type": "object",
"properties": {
"command": {
"type": "array",
"items": {
"type": "string"
}
},
"environment": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"required": ["command"],
"additionalProperties": false
}
}
},
"session_completed": {
"type": "array",
"items": {
"type": "object",
"properties": {
"command": {
"type": "array",
"items": {
"type": "string"
}
},
"environment": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"required": ["command"],
"additionalProperties": false
}
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
},
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}
+53
View File
@@ -0,0 +1,53 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "0.0.5",
"name": "opencode",
"type": "module",
"private": true,
"scripts": {
"typecheck": "tsc --noEmit",
"dev": "bun run ./src/index.ts"
},
"bin": {
"opencode": "./bin/opencode"
},
"exports": {
"./*": "./src/*.ts"
},
"devDependencies": {
"@ai-sdk/amazon-bedrock": "2.2.10",
"@ai-sdk/anthropic": "1.2.12",
"@tsconfig/bun": "1.0.7",
"@types/bun": "latest",
"@types/turndown": "5.0.5",
"@types/yargs": "17.0.33",
"typescript": "catalog:",
"zod-to-json-schema": "3.24.5"
},
"dependencies": {
"@clack/prompts": "0.11.0",
"@flystorage/file-storage": "1.1.0",
"@flystorage/local-fs": "1.1.0",
"@hono/zod-validator": "0.5.0",
"@openauthjs/openauth": "0.4.3",
"@standard-schema/spec": "1.0.0",
"ai": "catalog:",
"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-validation-error": "3.5.2"
}
}
+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()
+218
View File
@@ -0,0 +1,218 @@
#!/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 --exact-match HEAD`
.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("ci:") &&
!lower.includes("wip:") &&
!lower.includes("docs:") &&
!lower.includes("doc:")
)
})
.join("\n")
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 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`
}
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env bun
import "zod-openapi/extend"
import { Config } from "../src/config/config"
import { zodToJsonSchema } from "zod-to-json-schema"
const result = zodToJsonSchema(Config.Info)
await Bun.write("config.schema.json", JSON.stringify(result, null, 2))
+147
View File
@@ -0,0 +1,147 @@
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 const Info = z
.object({
user: z.string(),
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: Info
services: Map<any, { state: any; shutdown?: (input: any) => Promise<void> }>
}>("app")
export const use = ctx.use
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,
{
state: any
shutdown?: (input: any) => Promise<void>
}
>()
const root = git ?? input.cwd
const info: Info = {
user: os.userInfo().username,
hostname: os.hostname(),
time: {
initialized: state.initialized,
},
git: git !== undefined,
path: {
config: Global.Path.config,
state: Global.Path.state,
data,
root,
cwd: input.cwd,
},
}
const app = {
services,
info,
}
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>(
key: any,
init: (app: Info) => State,
shutdown?: (state: Awaited<State>) => Promise<void>,
) {
return () => {
const app = ctx.use()
const services = app.services
if (!services.has(key)) {
log.info("registering service", { name: key })
services.set(key, {
state: init(app.info),
shutdown,
})
}
return services.get(key)?.state as State
}
}
export function info() {
return ctx.use().info
}
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(),
}),
)
}
function directory(input: string): string {
return input
.split(path.sep)
.filter(Boolean)
.join("-")
.replace(/[^A-Za-z0-9_]/g, "-")
}
}
+81
View File
@@ -0,0 +1,81 @@
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() {
const pkce = await generatePKCE()
const url = new URL("https://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()
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,
})
}
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")
}
}
}
+18
View File
@@ -0,0 +1,18 @@
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 response = fetch("https://raw.githubusercontent.com/sst/opencode-github-copilot/refs/heads/main/auth.ts")
.then((x) => Bun.write(file, x))
.catch(() => {})
if (!file.exists()) {
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)
}
}
+81
View File
@@ -0,0 +1,81 @@
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 async function run(cmd: string[], options?: Bun.SpawnOptions.OptionsObject<any, any, any>) {
log.info("running", {
cmd: [which(), ...cmd],
...options,
})
const result = Bun.spawn([which(), ...cmd], {
...options,
stdout: "pipe",
stderr: "pipe",
env: {
...process.env,
...options?.env,
BUN_BE_BUN: "1",
},
})
const code = await result.exited
const stdout = result.stdout
? typeof result.stdout === "number"
? result.stdout
: await readableStreamToText(result.stdout)
: undefined
const stderr = result.stderr
? typeof result.stderr === "number"
? result.stderr
: await readableStreamToText(result.stderr)
: undefined
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(() => ({
dependencies: {},
}))
if (parsed.dependencies[pkg] === version) return mod
parsed.dependencies[pkg] = version
await Bun.write(pkgjson, JSON.stringify(parsed, null, 2))
await BunProc.run(["install", "--cwd", Global.Path.cache, "--registry=https://registry.npmjs.org"], {
cwd: Global.Path.cache,
}).catch((e) => {
throw new InstallFailedError(
{ pkg, version },
{
cause: e,
},
)
})
return mod
}
}
+109
View File
@@ -0,0 +1,109 @@
import { z, type ZodType } from "zod"
import { App } from "../app/app"
import { Log } from "../util/log"
export namespace Bus {
const log = Log.create({ service: "bus" })
type Subscription = (event: any) => void
const state = App.state("bus", () => {
const subscriptions = new Map<any, Subscription[]>()
return {
subscriptions,
}
})
export type EventDefinition = ReturnType<typeof event>
const registry = new Map<string, EventDefinition>()
export function event<Type extends string, Properties extends ZodType>(type: Type, properties: Properties) {
const result = {
type,
properties,
}
registry.set(type, result)
return result
}
export function payloads() {
return z.discriminatedUnion(
"type",
registry
.entries()
.map(([type, def]) =>
z
.object({
type: z.literal(type),
properties: def.properties,
})
.openapi({
ref: "Event" + "." + def.type,
}),
)
.toArray() as any,
)
}
export async function publish<Definition extends EventDefinition>(
def: Definition,
properties: z.output<Definition["properties"]>,
) {
const payload = {
type: def.type,
properties,
}
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 ?? []) {
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"]>
}) => "done" | undefined,
) {
const unsub = subscribe(def, (event) => {
if (callback(event)) unsub()
})
}
export function subscribeAll(callback: (event: any) => void) {
return raw("*", callback)
}
function raw(type: string, callback: (event: any) => void) {
log.info("subscribing", { type })
const subscriptions = state().subscriptions
let match = subscriptions.get(type) ?? []
match.push(callback)
subscriptions.set(type, match)
return () => {
log.info("unsubscribing", { type })
const match = subscriptions.get(type)
if (!match) return
const index = match.indexOf(callback)
if (index === -1) return
match.splice(index, 1)
}
}
}
+18
View File
@@ -0,0 +1,18 @@
import { App } from "../app/app"
import { ConfigHooks } from "../config/hooks"
import { FileWatcher } from "../file/watch"
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()
FileWatcher.init()
return cb(app)
})
}
+248
View File
@@ -0,0 +1,248 @@
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,
}
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(/^[a-z-]+$/) ? undefined : "a-z 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_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: "oauth",
},
{
label: "API Key",
value: "api",
},
],
})
if (prompts.isCancel(method)) throw new UI.CancelledError()
if (method === "oauth") {
// some weird bug where program exits without this
await new Promise((resolve) => setTimeout(resolve, 10))
const { url, verifier } = await AuthAnthropic.authorize()
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()
await AuthAnthropic.exchange(code, verifier)
.then(() => {
prompts.log.success("Login successful")
})
.catch(() => {
prompts.log.error("Invalid code")
})
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
}
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,26 @@
import { bootstrap } from "../../bootstrap"
import { cmd } from "../cmd"
import { FileCommand } from "./file"
import { LSPCommand } from "./lsp"
import { RipgrepCommand } from "./ripgrep"
import { SnapshotCommand } from "./snapshot"
export const DebugCommand = cmd({
command: "debug",
builder: (yargs) =>
yargs
.command(LSPCommand)
.command(RipgrepCommand)
.command(FileCommand)
.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,34 @@
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).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 () => {
await LSP.touchFile("./src/index.ts", true)
using _ = Log.Default.time("symbols")
const results = await LSP.workspaceSymbol(args.query)
console.log(JSON.stringify(results, null, 2))
})
},
})
@@ -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,
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,35 @@
import { Snapshot } from "../../../snapshot"
import { bootstrap } from "../../bootstrap"
import { cmd } from "../cmd"
export const SnapshotCommand = cmd({
command: "snapshot",
builder: (yargs) => yargs.command(SnapshotCreateCommand).command(SnapshotRestoreCommand).demandCommand(),
async handler() {},
})
export const SnapshotCreateCommand = cmd({
command: "create",
async handler() {
await bootstrap({ cwd: process.cwd() }, async () => {
const result = await Snapshot.create("test")
console.log(result)
})
},
})
export const SnapshotRestoreCommand = 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")
})
},
})
+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
+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}`)
}
}
})
},
})
+147
View File
@@ -0,0 +1,147 @@
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"
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",
})
},
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 first = await Session.list().next()
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
}
const isPiped = !process.stdout.isTTY
UI.empty()
UI.println(UI.logo())
UI.empty()
const displayMessage = message.length > 300 ? message.slice(0, 300) + "..." : message
UI.println(UI.Style.TEXT_NORMAL_BOLD + "> ", displayMessage)
UI.empty()
const cfg = await Config.get()
if (cfg.autoshare || Flag.OPENCODE_AUTO_SHARE || args.share) {
await Session.share(session.id)
UI.println(UI.Style.TEXT_INFO_BOLD + "~ https://opencode.ai/s/" + session.id.slice(-8))
}
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,
)
}
Bus.subscribe(MessageV2.Event.PartUpdated, async (evt) => {
if (evt.properties.sessionID !== session.id) 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]
printEvent(color, tool, part.state.title || "Unknown")
}
if (part.type === "text") {
if (part.text.includes("\n")) {
UI.empty()
UI.println(part.text)
UI.empty()
return
}
printEvent(UI.Style.TEXT_NORMAL_BOLD, "Text", part.text)
}
})
const result = await Session.chat({
sessionID: session.id,
providerID,
modelID,
parts: [
{
type: "text",
text: message,
},
],
})
if (isPiped) {
const match = result.parts.findLast((x) => x.type === "text")
if (match) process.stdout.write(match.text)
}
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()
})
},
})
+153
View File
@@ -0,0 +1,153 @@
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"
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",
}),
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) => {
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] : []),
],
cwd,
stdout: "inherit",
stderr: "inherit",
stdin: "inherit",
env: {
...process.env,
OPENCODE_SERVER: server.url.toString(),
OPENCODE_APP_INFO: JSON.stringify(app),
},
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 ""
}
+79
View File
@@ -0,0 +1,79 @@
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)
}
}
+198
View File
@@ -0,0 +1,198 @@
import { Log } from "../util/log"
import path from "path"
import { z } from "zod"
import { App } from "../app/app"
import { Filesystem } from "../util/filesystem"
import { ModelsDev } from "../provider/models"
import { mergeDeep } 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) => {
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))
}
}
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"),
})
.strict()
.openapi({
ref: "McpRemoteConfig",
})
export const Mcp = z.discriminatedUnion("type", [McpLocal, McpRemote])
export type Mcp = z.infer<typeof Mcp>
export const Keybinds = z
.object({
leader: z.string().optional().describe("Leader key for keybind combinations"),
help: z.string().optional().describe("Show help dialog"),
editor_open: z.string().optional().describe("Open external editor"),
session_new: z.string().optional().describe("Create a new session"),
session_list: z.string().optional().describe("List all sessions"),
session_share: z.string().optional().describe("Share current session"),
session_interrupt: z.string().optional().describe("Interrupt current session"),
session_compact: z.string().optional().describe("Toggle compact mode for session"),
tool_details: z.string().optional().describe("Show tool details"),
model_list: z.string().optional().describe("List available models"),
theme_list: z.string().optional().describe("List available themes"),
project_init: z.string().optional().describe("Initialize project configuration"),
input_clear: z.string().optional().describe("Clear input field"),
input_paste: z.string().optional().describe("Paste from clipboard"),
input_submit: z.string().optional().describe("Submit input"),
input_newline: z.string().optional().describe("Insert newline in input"),
history_previous: z.string().optional().describe("Navigate to previous history item"),
history_next: z.string().optional().describe("Navigate to next history item"),
messages_page_up: z.string().optional().describe("Scroll messages up by one page"),
messages_page_down: z.string().optional().describe("Scroll messages down by one page"),
messages_half_page_up: z.string().optional().describe("Scroll messages up by half page"),
messages_half_page_down: z.string().optional().describe("Scroll messages down by half page"),
messages_previous: z.string().optional().describe("Navigate to previous message"),
messages_next: z.string().optional().describe("Navigate to next message"),
messages_first: z.string().optional().describe("Navigate to first message"),
messages_last: z.string().optional().describe("Navigate to last message"),
app_exit: z.string().optional().describe("Exit the application"),
})
.strict()
.openapi({
ref: "KeybindsConfig",
})
export const Info = z
.object({
$schema: z.string().optional().describe("JSON schema reference for configuration validation"),
theme: z.string().optional().describe("Theme name to use for the interface"),
keybinds: Keybinds.optional().describe("Custom keybind configurations"),
autoshare: z.boolean().optional().describe("Share newly created sessions automatically"),
autoupdate: z.boolean().optional().describe("Automatically update to the latest version"),
disabled_providers: z.array(z.string()).optional().describe("Disable providers that are loaded automatically"),
model: z.string().describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(),
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"),
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 = await load(path.join(Global.Path.config, "config.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(path: string) {
const data = await Bun.file(path)
.json()
.catch((err) => {
if (err.code === "ENOENT") return {}
throw new JsonError({ path }, { cause: err })
})
const parsed = Info.safeParse(data)
if (parsed.success) return parsed.data
throw new InvalidError({ path, 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()
}
}
+52
View File
@@ -0,0 +1,52 @@
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 () => {
const cfg = await Config.get()
if (cfg.experimental?.hook?.session_completed) {
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
}
}
+110
View File
@@ -0,0 +1,110 @@
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 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 = []
if (diffOutput.trim()) {
const lines = diffOutput.trim().split("\n")
for (const line of lines) {
const [added, removed, filepath] = line.split("\t")
changedFiles.push({
file: filepath,
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({
file: 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({
file: filepath,
added: 0,
removed: 0, // Could get original line count but would require another git command
status: "deleted",
})
}
}
return changedFiles.map((x) => ({
...x,
file: path.relative(app.path.cwd, path.join(app.path.root, x.file)),
}))
}
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 }
}
}
+329
View File
@@ -0,0 +1,329 @@
// 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(),
}),
),
}),
})
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 = [
`${await filepath()} --files --hidden --glob='!.git/*' ${input.glob ? `--glob='${input.glob}'` : ``}`,
]
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) 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"
}
}
+146
View File
@@ -0,0 +1,146 @@
import { App } from "../app/app"
import { BunProc } from "../bun"
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(), "run", "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() {
// this is more complicated because we only want to use prettier if it's
// being used with the current project
try {
const proc = Bun.spawn({
cmd: [BunProc.which(), "run", "prettier", "--version"],
cwd: App.info().path.cwd,
env: {
BUN_BE_BUN: "1",
},
stdout: "ignore",
stderr: "ignore",
})
const exit = await proc.exited
return exit === 0
} catch {
return false
}
},
}
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() {
return Bun.which("ruff") !== null
},
}
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,
})
}
})
}
}
+29
View File
@@ -0,0 +1,29 @@
import fs from "fs/promises"
import { xdgData, xdgCache, xdgConfig, xdgState } from "xdg-basedir"
import path from "path"
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 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.cache, { recursive: true }),
fs.mkdir(Global.Path.providers, { recursive: true }),
fs.mkdir(Global.Path.state, { recursive: true }),
])
+70
View File
@@ -0,0 +1,70 @@
import { z } from "zod"
import { randomBytes } from "crypto"
export namespace Identifier {
const prefixes = {
session: "ses",
message: "msg",
user: "usr",
} as const
export function schema(prefix: keyof typeof prefixes) {
return z.string().startsWith(prefixes[prefix])
}
const LENGTH = 26
// State for monotonic ID generation
let lastTimestamp = 0
let counter = 0
export function ascending(prefix: keyof typeof prefixes, given?: string) {
return generateID(prefix, false, given)
}
export function descending(prefix: keyof typeof prefixes, given?: string) {
return generateID(prefix, true, given)
}
function generateID(prefix: keyof typeof prefixes, descending: boolean, given?: string): string {
if (!given) {
return generateNewID(prefix, descending)
}
if (!given.startsWith(prefixes[prefix])) {
throw new Error(`ID ${given} does not start with ${prefixes[prefix]}`)
}
return given
}
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) {
lastTimestamp = currentTimestamp
counter = 0
}
counter++
let now = BigInt(currentTimestamp) * BigInt(0x1000) + BigInt(counter)
now = descending ? ~now : now
const timeBytes = Buffer.alloc(6)
for (let i = 0; i < 6; i++) {
timeBytes[i] = Number((now >> BigInt(40 - 8 * i)) & BigInt(0xff))
}
return prefixes[prefix] + "_" + timeBytes.toString("hex") + randomBase62(LENGTH - 12)
}
}
+101
View File
@@ -0,0 +1,101 @@
import "zod-openapi/extend"
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"
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",
})
.middleware(async () => {
await Log.init({ print: process.argv.includes("--print-logs") })
Log.Default.info("opencode", {
version: Installation.VERSION,
args: process.argv.slice(2),
})
})
.usage("\n" + UI.logo())
.command(TuiCommand)
.command(RunCommand)
.command(GenerateCommand)
.command(DebugCommand)
.command(AuthCommand)
.command(UpgradeCommand)
.command(ServeCommand)
.command(ModelsCommand)
.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
}
cancel.abort()
+145
View File
@@ -0,0 +1,145 @@
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) => data.tag_name.slice(1) as string)
}
}
+182
View File
@@ -0,0 +1,182 @@
import path from "path"
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 = 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",
z.object({
serverID: z.string(),
path: z.string(),
}),
),
}
export async function create(serverID: string, server: LSPServer.Handle) {
const app = App.info()
log.info("starting client", { id: serverID })
const connection = createMessageConnection(
new StreamMessageReader(server.process.stdout),
new StreamMessageWriter(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", {
path,
})
const exists = diagnostics.has(path)
diagnostics.set(path, params.diagnostics)
if (!exists && serverID === "typescript") return
Bus.publish(Event.Diagnostics, { path, serverID })
})
connection.onRequest("workspace/configuration", async () => {
return [{}]
})
connection.listen()
log.info("sending initialize", { id: serverID })
await withTimeout(
connection.sendRequest("initialize", {
rootUri: "file://" + app.path.cwd,
processId: server.process.pid,
workspaceFolders: [
{
name: "workspace",
uri: "file://" + app.path.cwd,
},
],
initializationOptions: {
...server.initialization,
},
capabilities: {
workspace: {
configuration: true,
},
textDocument: {
synchronization: {
didOpen: true,
didChange: true,
},
publishDiagnostics: {
versionSupport: true,
},
},
},
}),
5_000,
).catch((err) => {
log.error("initialize error", { error: err })
throw new InitializeError(
{ serverID },
{
cause: err,
},
)
})
await connection.sendNotification("initialized", {})
log.info("initialized", {
serverID,
})
const files: {
[path: string]: number
} = {}
const result = {
get serverID() {
return serverID
},
get connection() {
return connection
},
notify: {
async open(input: { path: string }) {
input.path = path.isAbsolute(input.path) ? input.path : path.resolve(app.path.cwd, input.path)
const file = Bun.file(input.path)
const text = await file.text()
const version = files[input.path]
if (version !== undefined) {
diagnostics.delete(input.path)
await connection.sendNotification("textDocument/didClose", {
textDocument: {
uri: `file://` + input.path,
},
})
}
log.info("textDocument/didOpen", input)
diagnostics.delete(input.path)
const extension = path.extname(input.path)
const languageId = LANGUAGE_EXTENSIONS[extension] ?? "plaintext"
await connection.sendNotification("textDocument/didOpen", {
textDocument: {
uri: `file://` + input.path,
languageId,
version: 0,
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
return await withTimeout(
new Promise<void>((resolve) => {
unsub = Bus.subscribe(Event.Diagnostics, (event) => {
if (event.properties.path === input.path && event.properties.serverID === result.serverID) {
log.info("got diagnostics", input)
unsub?.()
resolve()
}
})
}),
3000,
)
.catch(() => {})
.finally(() => {
unsub?.()
})
},
async shutdown() {
log.info("shutting down", { serverID })
connection.end()
connection.dispose()
log.info("shutdown", { serverID })
},
}
return result
}
}
+140
View File
@@ -0,0 +1,140 @@
import { App } from "../app/app"
import { Log } from "../util/log"
import { LSPClient } from "./client"
import path from "path"
import { LSPServer } from "./server"
import { Ripgrep } from "../file/ripgrep"
import { z } from "zod"
export namespace LSP {
const log = Log.create({ service: "lsp" })
export const Symbol = z
.object({
name: z.string(),
kind: z.number(),
location: z.object({
uri: z.string(),
range: z.object({
start: z.object({
line: z.number(),
character: z.number(),
}),
end: z.object({
line: z.number(),
character: z.number(),
}),
}),
}),
})
.openapi({
ref: "LSP.Symbol",
})
export type Symbol = z.infer<typeof Symbol>
const state = App.state(
"lsp",
async (app) => {
log.info("initializing")
const clients = new Map<string, LSPClient.Info>()
for (const server of Object.values(LSPServer)) {
for (const extension of server.extensions) {
const [file] = await Ripgrep.files({
cwd: app.path.cwd,
glob: "*" + extension,
})
if (!file) continue
const handle = await server.spawn(App.info())
if (!handle) break
const client = await LSPClient.create(server.id, handle).catch((err) => log.error("", { error: err }))
if (!client) break
clients.set(server.id, client)
break
}
}
log.info("initialized")
return {
clients,
}
},
async (state) => {
for (const client of state.clients.values()) {
await client.shutdown()
}
},
)
export async function init() {
return state()
}
export async function touchFile(input: string, waitForDiagnostics?: boolean) {
const extension = path.parse(input).ext
const matches = Object.values(LSPServer)
.filter((x) => x.extensions.includes(extension))
.map((x) => x.id)
await run(async (client) => {
if (!matches.includes(client.serverID)) return
const wait = waitForDiagnostics ? client.waitForDiagnostics({ path: input }) : Promise.resolve()
await client.notify.open({ path: input })
return wait
})
}
export async function diagnostics() {
const results: Record<string, LSPClient.Diagnostic[]> = {}
for (const result of await run(async (client) => client.diagnostics)) {
for (const [path, diagnostics] of result.entries()) {
const arr = results[path] || []
arr.push(...diagnostics)
results[path] = arr
}
}
return results
}
export async function hover(input: { file: string; line: number; character: number }) {
return run((client) => {
return client.connection.sendRequest("textDocument/hover", {
textDocument: {
uri: `file://${input.file}`,
},
position: {
line: input.line,
character: input.character,
},
})
})
}
export async function workspaceSymbol(query: string) {
return run((client) =>
client.connection.sendRequest("workspace/symbol", {
query,
}),
).then((result) => result.flat() as LSP.Symbol[])
}
async function run<T>(input: (client: LSPClient.Info) => Promise<T>): Promise<T[]> {
const clients = await state().then((x) => [...x.clients.values()])
const tasks = clients.map((x) => input(x))
return Promise.all(tasks)
}
export namespace Diagnostic {
export function pretty(diagnostic: LSPClient.Diagnostic) {
const severityMap = {
1: "ERROR",
2: "WARN",
3: "INFO",
4: "HINT",
}
const severity = severityMap[diagnostic.severity || 1]
const line = diagnostic.range.start.line + 1
const col = diagnostic.range.start.character + 1
return `${severity} [${line}:${col}] ${diagnostic.message}`
}
}
}
+97
View File
@@ -0,0 +1,97 @@
export const LANGUAGE_EXTENSIONS: Record<string, string> = {
".abap": "abap",
".bat": "bat",
".bib": "bibtex",
".bibtex": "bibtex",
".clj": "clojure",
".coffee": "coffeescript",
".c": "c",
".cpp": "cpp",
".cxx": "cpp",
".cc": "cpp",
".c++": "cpp",
".cs": "csharp",
".css": "css",
".d": "d",
".pas": "pascal",
".pascal": "pascal",
".diff": "diff",
".patch": "diff",
".dart": "dart",
".dockerfile": "dockerfile",
".ex": "elixir",
".exs": "elixir",
".erl": "erlang",
".hrl": "erlang",
".fs": "fsharp",
".fsi": "fsharp",
".fsx": "fsharp",
".fsscript": "fsharp",
".gitcommit": "git-commit",
".gitrebase": "git-rebase",
".go": "go",
".groovy": "groovy",
".hbs": "handlebars",
".handlebars": "handlebars",
".hs": "haskell",
".html": "html",
".htm": "html",
".ini": "ini",
".java": "java",
".js": "javascript",
".jsx": "javascriptreact",
".json": "json",
".tex": "latex",
".latex": "latex",
".less": "less",
".lua": "lua",
".makefile": "makefile",
makefile: "makefile",
".md": "markdown",
".markdown": "markdown",
".m": "objective-c",
".mm": "objective-cpp",
".pl": "perl",
".pm": "perl6",
".php": "php",
".ps1": "powershell",
".psm1": "powershell",
".pug": "jade",
".jade": "jade",
".py": "python",
".r": "r",
".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",
".scala": "scala",
".shader": "shaderlab",
".sh": "shellscript",
".bash": "shellscript",
".zsh": "shellscript",
".ksh": "shellscript",
".sql": "sql",
".swift": "swift",
".ts": "typescript",
".tsx": "typescriptreact",
".mts": "typescript",
".cts": "typescript",
".mtsx": "typescriptreact",
".ctsx": "typescriptreact",
".xml": "xml",
".xsl": "xsl",
".yaml": "yaml",
".yml": "yaml",
".mjs": "javascript",
".cjs": "javascript",
} as const
+184
View File
@@ -0,0 +1,184 @@
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"
export namespace LSPServer {
const log = Log.create({ service: "lsp.server" })
export interface Handle {
process: ChildProcessWithoutNullStreams
initialization?: Record<string, any>
}
export interface Info {
id: string
extensions: string[]
spawn(app: App.Info): Promise<Handle | undefined>
}
export const Typescript: Info = {
id: "typescript",
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
async spawn(app) {
const tsserver = await Bun.resolve("typescript/lib/tsserver.js", app.path.cwd).catch(() => {})
if (!tsserver) return
const proc = spawn(BunProc.which(), ["x", "typescript-language-server", "--stdio"], {
env: {
...process.env,
BUN_BE_BUN: "1",
},
})
return {
process: proc,
initialization: {
tsserver: {
path: tsserver,
},
},
}
},
}
export const Gopls: Info = {
id: "golang",
extensions: [".go"],
async spawn() {
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!),
}
},
}
export const RubyLsp: Info = {
id: "ruby-lsp",
extensions: [".rb", ".rake", ".gemspec", ".ru"],
async spawn() {
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"]),
}
},
}
export const Pyright: Info = {
id: "pyright",
extensions: [".py", ".pyi"],
async spawn() {
const proc = spawn(BunProc.which(), ["x", "pyright-langserver", "--stdio"], {
env: {
...process.env,
BUN_BE_BUN: "1",
},
})
return {
process: proc,
}
},
}
export const ElixirLS: Info = {
id: "elixir-ls",
extensions: [".ex", ".exs"],
async spawn() {
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),
}
},
}
}
+110
View File
@@ -0,0 +1,110 @@
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,
},
}).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`)
}
}
}
@@ -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)
}
}
+534
View File
@@ -0,0 +1,534 @@
import z from "zod"
import { App } from "../app/app"
import { Config } from "../config/config"
import { mergeDeep, sortBy } from "remeda"
import { NoSuchModelError, type LanguageModel, type Provider as SDK } from "ai"
import { Log } from "../util/log"
import { BunProc } from "../bun"
import { BashTool } from "../tool/bash"
import { EditTool } from "../tool/edit"
import { WebFetchTool } from "../tool/webfetch"
import { GlobTool } from "../tool/glob"
import { GrepTool } from "../tool/grep"
import { ListTool } from "../tool/ls"
import { PatchTool } from "../tool/patch"
import { ReadTool } from "../tool/read"
import type { Tool } from "../tool/tool"
import { WriteTool } from "../tool/write"
import { TodoReadTool, TodoWriteTool } from "../tool/todo"
import { AuthAnthropic } from "../auth/anthropic"
import { AuthCopilot } from "../auth/copilot"
import { ModelsDev } from "./models"
import { NamedError } from "../util/error"
import { Auth } from "../auth"
// import { TaskTool } from "../tool/task"
export namespace Provider {
const log = Log.create({ service: "provider" })
type CustomLoader = (
provider: ModelsDev.Provider,
api?: string,
) => Promise<{
autoload: boolean
getModel?: (sdk: any, modelID: string) => Promise<any>
options?: Record<string, any>
}>
type Source = "env" | "config" | "custom" | "api"
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
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))
}
} catch {}
const headers = {
...init.headers,
...copilot.HEADERS,
Authorization: `Bearer ${info.access}`,
"Openai-Intent": "conversation-edits",
"X-Initiator": isAgentCall ? "agent" : "user",
}
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"]) 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 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>()
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"] = info.api
providers[id] = {
source,
info,
options,
getModel,
}
return
}
provider.options = mergeDeep(provider.options, options)
provider.source = source
provider.getModel = getModel ?? provider.getModel
}
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: {
...existing?.cost,
...model.cost,
input: 0,
output: 0,
cache_read: 0,
cache_write: 0,
},
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 {
models,
providers,
sdk,
}
})
export async function list() {
return state().then((state) => state.providers)
}
async function getSDK(provider: ModelsDev.Provider) {
return (async () => {
using _ = log.time("getSDK", {
providerID: provider.id,
})
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(s.providers[provider.id]?.options)
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) {
const key = `${providerID}/${modelID}`
const s = await state()
if (s.models.has(key)) return s.models.get(key)!
log.info("getModel", {
providerID,
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 = provider.getModel ? await provider.getModel(sdk, modelID) : sdk.languageModel(modelID)
log.info("found", { providerID, modelID })
s.models.set(key, {
info,
language,
})
return {
info,
language,
}
} catch (e) {
if (e instanceof NoSuchModelError)
throw new ModelNotFoundError(
{
modelID: modelID,
providerID,
},
{ cause: e },
)
throw e
}
}
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,
WebFetchTool,
GlobTool,
GrepTool,
ListTool,
// LspDiagnosticTool,
// LspHoverTool,
PatchTool,
ReadTool,
// MultiEditTool,
WriteTool,
TodoWriteTool,
TodoReadTool,
// TaskTool,
]
const TOOL_MAPPING: Record<string, Tool.Info[]> = {
anthropic: TOOLS.filter((t) => t.id !== "patch"),
openai: TOOLS.map((t) => ({
...t,
parameters: optionalToNullable(t.parameters),
})),
azure: TOOLS.map((t) => ({
...t,
parameters: optionalToNullable(t.parameters),
})),
google: TOOLS,
}
export async function tools(providerID: string) {
/*
const cfg = await Config.get()
if (cfg.tool?.provider?.[providerID])
return cfg.tool.provider[providerID].map(
(id) => TOOLS.find((t) => t.id === id)!,
)
*/
return TOOL_MAPPING[providerID] ?? TOOLS
}
function optionalToNullable(schema: z.ZodTypeAny): z.ZodTypeAny {
if (schema instanceof z.ZodObject) {
const shape = schema.shape
const newShape: Record<string, z.ZodTypeAny> = {}
for (const [key, value] of Object.entries(shape)) {
const zodValue = value as z.ZodTypeAny
if (zodValue instanceof z.ZodOptional) {
newShape[key] = zodValue.unwrap().nullable()
} else {
newShape[key] = optionalToNullable(zodValue)
}
}
return z.object(newShape)
}
if (schema instanceof z.ZodArray) {
return z.array(optionalToNullable(schema.element))
}
if (schema instanceof z.ZodUnion) {
return z.union(
schema.options.map((option: z.ZodTypeAny) => optionalToNullable(option)) as [
z.ZodTypeAny,
z.ZodTypeAny,
...z.ZodTypeAny[],
],
)
}
return schema
}
export const ModelNotFoundError = NamedError.create(
"ProviderModelNotFoundError",
z.object({
providerID: z.string(),
modelID: z.string(),
}),
)
export const InitError = NamedError.create(
"ProviderInitError",
z.object({
providerID: z.string(),
}),
)
export const AuthError = NamedError.create(
"ProviderAuthError",
z.object({
providerID: z.string(),
message: z.string(),
}),
)
}
@@ -0,0 +1,37 @@
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" },
},
openaiCompatible: {
cache_control: { type: "ephemeral" },
},
}
}
}
if (providerID === "amazon-bedrock" || modelID.includes("anthropic")) {
const system = msgs.filter((msg) => msg.role === "system").slice(0, 2)
const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
for (const msg of unique([...system, ...final])) {
msg.providerOptions = {
...msg.providerOptions,
bedrock: {
cachePoint: { type: "ephemeral" },
},
}
}
}
return msgs
}
}
+672
View File
@@ -0,0 +1,672 @@
import { Log } from "../util/log"
import { Bus } from "../bus"
import { describeRoute, generateSpecs, openAPISpecs } from "hono-openapi"
import { Hono } from "hono"
import { streamSSE } from "hono/streaming"
import { Session } from "../session"
import { resolver, validator as zValidator } from "hono-openapi/zod"
import { z } from "zod"
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"
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" })
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(
"/doc",
openAPISpecs(app, {
documentation: {
info: {
title: "opencode",
version: "0.0.3",
description: "opencode api",
},
openapi: "3.0.0",
},
}),
)
.get(
"/event",
describeRoute({
description: "Get events",
responses: {
200: {
description: "Event stream",
content: {
"application/json": {
schema: resolver(
Bus.payloads().openapi({
ref: "Event",
}),
),
},
},
},
},
}),
async (c) => {
log.info("event connected")
return streamSSE(c, async (stream) => {
stream.writeSSE({
data: JSON.stringify({}),
})
const unsub = Bus.subscribeAll(async (event) => {
await stream.writeSSE({
data: JSON.stringify(event),
})
})
await new Promise<void>((resolve) => {
stream.onAbort(() => {
unsub()
resolve()
log.info("event disconnected")
})
})
})
},
)
.get(
"/app",
describeRoute({
description: "Get app info",
responses: {
200: {
description: "200",
content: {
"application/json": {
schema: resolver(App.Info),
},
},
},
},
}),
async (c) => {
return c.json(App.info())
},
)
.post(
"/app/init",
describeRoute({
description: "Initialize the app",
responses: {
200: {
description: "Initialize the app",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
},
}),
async (c) => {
await App.initialize()
return c.json(true)
},
)
.get(
"/config",
describeRoute({
description: "Get config info",
responses: {
200: {
description: "Get config info",
content: {
"application/json": {
schema: resolver(Config.Info),
},
},
},
},
}),
async (c) => {
return c.json(await Config.get())
},
)
.get(
"/session",
describeRoute({
description: "List all sessions",
responses: {
200: {
description: "List of sessions",
content: {
"application/json": {
schema: resolver(Session.Info.array()),
},
},
},
},
}),
async (c) => {
const sessions = await Array.fromAsync(Session.list())
return c.json(sessions)
},
)
.post(
"/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({
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: {
200: {
description: "Aborted session",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
},
}),
zValidator(
"param",
z.object({
id: z.string(),
}),
),
async (c) => {
return c.json(Session.abort(c.req.valid("param").id))
},
)
.post(
"/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: "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(MessageV2.Info.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",
z.object({
providerID: z.string(),
modelID: z.string(),
parts: MessageV2.UserPart.array(),
}),
),
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(z.unknown().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(
z
.object({
file: z.string(),
added: z.number().int(),
removed: z.number().int(),
status: z.enum(["added", "deleted", "modified"]),
})
.array(),
),
},
},
},
},
}),
async (c) => {
const content = await File.status()
return c.json(content)
},
)
return result
}
export async function openapi() {
const a = app()
const result = await generateSpecs(a, {
documentation: {
info: {
title: "opencode",
version: "1.0.0",
description: "opencode api",
},
openapi: "3.0.0",
},
})
return result
}
export function listen(opts: { port: number; hostname: string }) {
const server = Bun.serve({
port: opts.port,
hostname: opts.hostname,
idleTimeout: 0,
fetch: app().fetch,
})
return server
}
}
+923
View File
@@ -0,0 +1,923 @@
import path from "path"
import { Decimal } from "decimal.js"
import { z, ZodSchema } from "zod"
import {
generateText,
LoadAPIKeyError,
streamText,
tool,
wrapLanguageModel,
type Tool as AITool,
type LanguageModelUsage,
type ProviderMetadata,
type ModelMessage,
stepCountIs,
} from "ai"
import PROMPT_INITIALIZE from "../session/prompt/initialize.txt"
import { App } from "../app/app"
import { Bus } from "../bus"
import { Config } from "../config/config"
import { Flag } from "../flag/flag"
import { Identifier } from "../id/id"
import { Installation } from "../installation"
import { MCP } from "../mcp"
import { Provider } from "../provider/provider"
import { ProviderTransform } from "../provider/transform"
import type { ModelsDev } from "../provider/models"
import { Share } from "../share/share"
import { Snapshot } from "../snapshot"
import { Storage } from "../storage/storage"
import type { Tool } from "../tool/tool"
import { Log } from "../util/log"
import { NamedError } from "../util/error"
import { SystemPrompt } from "./system"
import { FileTime } from "../file/time"
import { MessageV2 } from "./message-v2"
export namespace Session {
const log = Log.create({ service: "session" })
export const Info = z
.object({
id: Identifier.schema("session"),
parentID: Identifier.schema("session").optional(),
share: z
.object({
url: z.string(),
})
.optional(),
title: z.string(),
version: z.string(),
time: z.object({
created: z.number(),
updated: z.number(),
}),
revert: z
.object({
messageID: z.string(),
part: z.number(),
snapshot: z.string().optional(),
})
.optional(),
})
.openapi({
ref: "Session",
})
export type Info = z.output<typeof Info>
export const ShareInfo = z
.object({
secret: z.string(),
url: z.string(),
})
.openapi({
ref: "SessionShare",
})
export type ShareInfo = z.output<typeof ShareInfo>
export const Event = {
Updated: Bus.event(
"session.updated",
z.object({
info: Info,
}),
),
Deleted: Bus.event(
"session.deleted",
z.object({
info: Info,
}),
),
Idle: Bus.event(
"session.idle",
z.object({
sessionID: z.string(),
}),
),
Error: Bus.event(
"session.error",
z.object({
error: MessageV2.Assistant.shape.error,
}),
),
}
const state = App.state(
"session",
() => {
const sessions = new Map<string, Info>()
const messages = new Map<string, MessageV2.Info[]>()
const pending = new Map<string, AbortController>()
return {
sessions,
messages,
pending,
}
},
async (state) => {
for (const [_, controller] of state.pending) {
controller.abort()
}
},
)
export async function create(parentID?: string) {
const result: Info = {
id: Identifier.descending("session"),
version: Installation.VERSION,
parentID,
title: (parentID ? "Child session - " : "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)
const cfg = await Config.get()
if (!result.parentID && (Flag.OPENCODE_AUTO_SHARE || cfg.autoshare))
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 getShare(id: string) {
return Storage.readJSON<ShareInfo>("session/share/" + id)
}
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 = {
url: share.url,
}
})
await Storage.writeJSON<ShareInfo>("session/share/" + id, share)
await Share.sync("session/info/" + id, session)
for (const msg of await messages(id)) {
await Share.sync("session/message/" + id + "/" + msg.id, msg)
}
return share
}
export async function unshare(id: string) {
const share = await getShare(id)
if (!share) return
await Storage.remove("session/share/" + id)
await update(id, (draft) => {
draft.share = undefined
})
await Share.remove(id, share.secret)
}
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 MessageV2.Info[]
const list = Storage.list("session/message/" + sessionID)
for await (const p of list) {
const read = await Storage.readJSON<MessageV2.Info>(p)
result.push(read)
}
result.sort((a, b) => (a.id > b.id ? 1 : -1))
return result
}
export async function getMessage(sessionID: string, messageID: string) {
return Storage.readJSON<MessageV2.Info>("session/message/" + sessionID + "/" + messageID)
}
export async function* list() {
for await (const item of Storage.list("session/info")) {
const sessionID = path.basename(item, ".json")
yield get(sessionID)
}
}
export async function children(parentID: string) {
const result = [] as Session.Info[]
for await (const item of Storage.list("session/info")) {
const sessionID = path.basename(item, ".json")
const session = await get(sessionID)
if (session.parentID !== parentID) continue
result.push(session)
}
return result
}
export function abort(sessionID: string) {
const controller = state().pending.get(sessionID)
if (!controller) return false
controller.abort()
state().pending.delete(sessionID)
return true
}
export async function remove(sessionID: string, emitEvent = true) {
try {
abort(sessionID)
const session = await get(sessionID)
for (const child of await children(sessionID)) {
await remove(child.id, false)
}
await unshare(sessionID).catch(() => {})
await Storage.remove(`session/info/${sessionID}`).catch(() => {})
await Storage.removeDir(`session/message/${sessionID}/`).catch(() => {})
state().sessions.delete(sessionID)
state().messages.delete(sessionID)
if (emitEvent) {
Bus.publish(Event.Deleted, {
info: session,
})
}
} catch (e) {
log.error(e)
}
}
async function updateMessage(msg: MessageV2.Info) {
await Storage.writeJSON("session/message/" + msg.sessionID + "/" + msg.id, msg)
Bus.publish(MessageV2.Event.Updated, {
info: msg,
})
}
export async function chat(input: {
sessionID: string
providerID: string
modelID: string
parts: MessageV2.UserPart[]
system?: string[]
tools?: Tool.Info[]
}) {
using abort = lock(input.sessionID)
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 session = await get(input.sessionID)
if (session.revert) {
const trimmed = []
for (const msg of msgs) {
if (msg.id > session.revert.messageID || (msg.id === session.revert.messageID && session.revert.part === 0)) {
await Storage.remove("session/message/" + input.sessionID + "/" + msg.id)
await Bus.publish(MessageV2.Event.Removed, {
sessionID: input.sessionID,
messageID: msg.id,
})
continue
}
if (msg.id === session.revert.messageID) {
if (session.revert.part === 0) break
msg.parts = msg.parts.slice(0, session.revert.part)
}
trimmed.push(msg)
}
msgs = trimmed
await update(input.sessionID, (draft) => {
draft.revert = undefined
})
}
const previous = msgs.at(-1) as MessageV2.Assistant
// auto summarize if too long
if (previous) {
const tokens =
previous.tokens.input + previous.tokens.cache.read + previous.tokens.cache.write + previous.tokens.output
if (
model.info.limit.context &&
tokens > Math.max((model.info.limit.context - (model.info.limit.output ?? 0)) * 0.9, 0)
) {
await summarize({
sessionID: input.sessionID,
providerID: input.providerID,
modelID: input.modelID,
})
return chat(input)
}
}
const lastSummary = msgs.findLast((msg) => msg.role === "assistant" && msg.summary === true)
if (lastSummary) msgs = msgs.filter((msg) => msg.id >= lastSummary.id)
const app = App.info()
input.parts = await Promise.all(
input.parts.map(async (part): Promise<MessageV2.UserPart[]> => {
if (part.type === "file") {
const url = new URL(part.url)
switch (url.protocol) {
case "file:":
const filepath = path.join(app.path.cwd, url.pathname)
let file = Bun.file(filepath)
if (part.mime === "text/plain") {
let text = await file.text()
const range = {
start: url.searchParams.get("start"),
end: url.searchParams.get("end"),
}
if (range.start != null && part.mime === "text/plain") {
const lines = text.split("\n")
const start = parseInt(range.start)
const end = range.end ? parseInt(range.end) : lines.length
text = lines.slice(start, end).join("\n")
}
FileTime.read(input.sessionID, filepath)
return [
{
type: "text",
text: ["Called the Read tool on " + url.pathname, "<results>", text, "</results>"].join("\n"),
},
]
}
return [
{
type: "text",
text: ["Called the Read tool on " + url.pathname].join("\n"),
},
{
type: "file",
url: `data:${part.mime};base64,` + Buffer.from(await file.bytes()).toString("base64url"),
mime: part.mime,
filename: part.filename!,
},
]
}
}
return [part]
}),
).then((x) => x.flat())
if (msgs.length === 0 && !session.parentID) {
generateText({
maxOutputTokens: input.providerID === "google" ? 1024 : 20,
providerOptions: model.info.options,
messages: [
...SystemPrompt.title(input.providerID).map(
(x): ModelMessage => ({
role: "system",
content: x,
}),
),
...MessageV2.toModelMessage([
{
id: Identifier.ascending("message"),
role: "user",
sessionID: input.sessionID,
parts: input.parts,
time: {
created: Date.now(),
},
},
]),
],
model: model.language,
})
.then((result) => {
if (result.text)
return Session.update(input.sessionID, (draft) => {
draft.title = result.text
})
})
.catch(() => {})
}
const msg: MessageV2.Info = {
id: Identifier.ascending("message"),
role: "user",
sessionID: input.sessionID,
parts: input.parts,
time: {
created: Date.now(),
},
}
await updateMessage(msg)
msgs.push(msg)
const system = input.system ?? SystemPrompt.provider(input.providerID)
system.push(...(await SystemPrompt.environment()))
system.push(...(await SystemPrompt.custom()))
const next: MessageV2.Info = {
id: Identifier.ascending("message"),
role: "assistant",
parts: [],
system,
path: {
cwd: app.path.cwd,
root: app.path.root,
},
cost: 0,
tokens: {
input: 0,
output: 0,
reasoning: 0,
cache: { read: 0, write: 0 },
},
modelID: input.modelID,
providerID: input.providerID,
time: {
created: Date.now(),
},
sessionID: input.sessionID,
}
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,
inputSchema: item.parameters as ZodSchema,
async execute(args, opts) {
const result = await item.execute(args, {
sessionID: input.sessionID,
abort: abort.signal,
messageID: next.id,
metadata: async (val) => {
const match = next.parts.find(
(p): p is MessageV2.ToolPart => p.type === "tool" && p.id === opts.toolCallId,
)
if (match && match.state.status === "running") {
match.state.title = val.title
match.state.metadata = val.metadata
}
await updateMessage(next)
},
})
return result
},
toModelOutput(result) {
return {
type: "text",
value: result.output,
}
},
})
}
for (const [key, item] of Object.entries(await MCP.tools())) {
const execute = item.execute
if (!execute) continue
item.execute = async (args, opts) => {
const result = await execute(args, opts)
const output = result.content
.filter((x: any) => x.type === "text")
.map((x: any) => x.text)
.join("\n\n")
return {
output,
}
}
item.toModelOutput = (result) => {
return {
type: "text",
value: result.output,
}
}
tools[key] = item
}
let text: MessageV2.TextPart = {
type: "text",
text: "",
}
const result = streamText({
onError() {},
maxRetries: 10,
maxOutputTokens: Math.max(0, model.info.limit.output) || undefined,
abortSignal: abort.signal,
stopWhen: stepCountIs(1000),
providerOptions: model.info.options,
messages: [
...system.map(
(x): ModelMessage => ({
role: "system",
content: x,
}),
),
...MessageV2.toModelMessage(msgs),
],
temperature: model.info.temperature ? 0 : undefined,
tools: model.info.tool_call === false ? undefined : tools,
model: wrapLanguageModel({
model: model.language,
middleware: [
{
async transformParams(args) {
if (args.type === "stream") {
// @ts-expect-error
args.params.prompt = ProviderTransform.message(args.params.prompt, input.providerID, input.modelID)
}
return args.params
},
},
],
}),
})
for await (const value of result.fullStream) {
l.info("part", {
type: value.type,
})
switch (value.type) {
case "start":
break
case "tool-input-start":
next.parts.push({
type: "tool",
tool: value.toolName,
id: value.id,
state: {
status: "pending",
},
})
Bus.publish(MessageV2.Event.PartUpdated, {
part: next.parts[next.parts.length - 1],
sessionID: next.sessionID,
messageID: next.id,
})
break
case "tool-input-delta":
break
case "tool-call": {
const match = next.parts.find((p): p is MessageV2.ToolPart => p.type === "tool" && p.id === value.toolCallId)
if (match) {
match.state = {
status: "running",
input: value.input,
time: {
start: Date.now(),
},
}
Bus.publish(MessageV2.Event.PartUpdated, {
part: match,
sessionID: next.sessionID,
messageID: next.id,
})
}
break
}
case "tool-result": {
const match = next.parts.find((p): p is MessageV2.ToolPart => p.type === "tool" && p.id === value.toolCallId)
if (match && match.state.status === "running") {
match.state = {
status: "completed",
input: value.input,
output: value.output.output,
metadata: value.output.metadata,
title: value.output.title,
time: {
start: match.state.time.start,
end: Date.now(),
},
}
Bus.publish(MessageV2.Event.PartUpdated, {
part: match,
sessionID: next.sessionID,
messageID: next.id,
})
}
break
}
case "tool-error": {
const match = next.parts.find((p): p is MessageV2.ToolPart => p.type === "tool" && p.id === value.toolCallId)
if (match && match.state.status === "running") {
match.state = {
status: "error",
input: value.input,
error: (value.error as any).toString(),
time: {
start: match.state.time.start,
end: Date.now(),
},
}
Bus.publish(MessageV2.Event.PartUpdated, {
part: match,
sessionID: next.sessionID,
messageID: next.id,
})
}
break
}
case "error":
const e = value.error
log.error("", {
error: e,
})
switch (true) {
case MessageV2.OutputLengthError.isInstance(e):
next.error = e
break
case LoadAPIKeyError.isInstance(e):
next.error = new Provider.AuthError(
{
providerID: input.providerID,
message: e.message,
},
{ cause: e },
).toObject()
break
case e instanceof Error:
next.error = new NamedError.Unknown({ message: e.toString() }, { cause: e }).toObject()
break
default:
next.error = new NamedError.Unknown({ message: JSON.stringify(e) }, { cause: e })
}
Bus.publish(Event.Error, {
error: next.error,
})
break
case "start-step":
next.parts.push({
type: "step-start",
})
break
case "finish-step":
const usage = getUsage(model.info, value.usage, value.providerMetadata)
next.cost += usage.cost
next.tokens = usage.tokens
break
case "text-start":
text = {
type: "text",
text: "",
}
break
case "text":
if (text.text === "") next.parts.push(text)
text.text += value.text
break
case "text-end":
Bus.publish(MessageV2.Event.PartUpdated, {
part: text,
sessionID: next.sessionID,
messageID: next.id,
})
break
case "finish":
next.time.completed = Date.now()
break
default:
l.info("unhandled", {
...value,
})
continue
}
await updateMessage(next)
}
next.time.completed = Date.now()
await updateMessage(next)
return next
}
export async function revert(_input: { sessionID: string; messageID: string; part: number }) {
// TODO
/*
const message = await getMessage(input.sessionID, input.messageID)
if (!message) return
const part = message.parts[input.part]
if (!part) return
const session = await get(input.sessionID)
const snapshot =
session.revert?.snapshot ?? (await Snapshot.create(input.sessionID))
const old = (() => {
if (message.role === "assistant") {
const lastTool = message.parts.findLast(
(part, index) =>
part.type === "tool-invocation" && index < input.part,
)
if (lastTool && lastTool.type === "tool-invocation")
return message.metadata.tool[lastTool.toolInvocation.toolCallId]
.snapshot
}
return message.metadata.snapshot
})()
if (old) await Snapshot.restore(input.sessionID, old)
await update(input.sessionID, (draft) => {
draft.revert = {
messageID: input.messageID,
part: input.part,
snapshot,
}
})
*/
}
export async function unrevert(sessionID: string) {
const session = await get(sessionID)
if (!session) return
if (!session.revert) return
if (session.revert.snapshot) await Snapshot.restore(sessionID, session.revert.snapshot)
update(sessionID, (draft) => {
draft.revert = undefined
})
}
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.role === "assistant" && msg.summary === true)?.id
const filtered = msgs.filter((msg) => !lastSummary || msg.id >= lastSummary)
const model = await Provider.getModel(input.providerID, input.modelID)
const app = App.info()
const system = SystemPrompt.summarize(input.providerID)
const next: MessageV2.Info = {
id: Identifier.ascending("message"),
role: "assistant",
parts: [],
sessionID: input.sessionID,
system,
path: {
cwd: app.path.cwd,
root: app.path.root,
},
summary: true,
cost: 0,
modelID: input.modelID,
providerID: input.providerID,
tokens: {
input: 0,
output: 0,
reasoning: 0,
cache: { read: 0, write: 0 },
},
time: {
created: Date.now(),
},
}
await updateMessage(next)
let text: MessageV2.TextPart | undefined
const result = streamText({
abortSignal: abort.signal,
model: model.language,
messages: [
...system.map(
(x): ModelMessage => ({
role: "system",
content: x,
}),
),
...MessageV2.toModelMessage(filtered),
{
role: "user",
content: [
{
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.",
},
],
},
],
onStepFinish: async (step) => {
const usage = getUsage(model.info, step.usage, step.providerMetadata)
next.cost += usage.cost
next.tokens = usage.tokens
await updateMessage(next)
if (text) {
Bus.publish(MessageV2.Event.PartUpdated, {
part: text,
messageID: next.id,
sessionID: next.sessionID,
})
}
text = undefined
},
async onFinish(input) {
const usage = getUsage(model.info, input.usage, input.providerMetadata)
next.cost += usage.cost
next.tokens = usage.tokens
next.time.completed = Date.now()
await updateMessage(next)
},
})
for await (const value of result.fullStream) {
switch (value.type) {
case "text":
if (!text) {
text = {
type: "text",
text: value.text,
}
next.parts.push(text)
} else text.text += value.text
await updateMessage(next)
break
}
}
}
function lock(sessionID: string) {
log.info("locking", { sessionID })
if (state().pending.has(sessionID)) throw new BusyError(sessionID)
const controller = new AbortController()
state().pending.set(sessionID, controller)
return {
signal: controller.signal,
[Symbol.dispose]() {
log.info("unlocking", { sessionID })
state().pending.delete(sessionID)
Bus.publish(Event.Idle, {
sessionID,
})
},
}
}
function getUsage(model: ModelsDev.Model, usage: LanguageModelUsage, metadata?: ProviderMetadata) {
const tokens = {
input: usage.inputTokens ?? 0,
output: usage.outputTokens ?? 0,
reasoning: 0,
cache: {
write: (metadata?.["anthropic"]?.["cacheCreationInputTokens"] ??
// @ts-expect-error
metadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ??
0) as number,
read: usage.cachedInputTokens ?? 0,
},
}
return {
cost: new Decimal(0)
.add(new Decimal(tokens.input).mul(model.cost.input).div(1_000_000))
.add(new Decimal(tokens.output).mul(model.cost.output).div(1_000_000))
.add(new Decimal(tokens.cache.read).mul(model.cost.cache_read ?? 0).div(1_000_000))
.add(new Decimal(tokens.cache.write).mul(model.cost.cache_write ?? 0).div(1_000_000))
.toNumber(),
tokens,
}
}
export class BusyError extends Error {
constructor(public readonly sessionID: string) {
super(`Session ${sessionID} is busy`)
}
}
export async function initialize(input: { sessionID: string; modelID: string; providerID: string }) {
const app = App.info()
await Session.chat({
sessionID: input.sessionID,
providerID: input.providerID,
modelID: input.modelID,
parts: [
{
type: "text",
text: PROMPT_INITIALIZE.replace("${path}", app.path.root),
},
],
})
await App.initialize()
}
}
+398
View File
@@ -0,0 +1,398 @@
import z from "zod"
import { Bus } from "../bus"
import { Provider } from "../provider/provider"
import { NamedError } from "../util/error"
import { Message } from "./message"
import { convertToModelMessages, type ModelMessage, type UIMessage } from "ai"
export namespace MessageV2 {
export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({}))
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",
})
export const TextPart = z
.object({
type: z.literal("text"),
text: z.string(),
})
.openapi({
ref: "TextPart",
})
export type TextPart = z.infer<typeof TextPart>
export const ToolPart = z
.object({
type: z.literal("tool"),
id: z.string(),
tool: z.string(),
state: ToolState,
})
.openapi({
ref: "ToolPart",
})
export type ToolPart = z.infer<typeof ToolPart>
export const FilePart = z
.object({
type: z.literal("file"),
mime: z.string(),
filename: z.string().optional(),
url: z.string(),
})
.openapi({
ref: "FilePart",
})
export type FilePart = z.infer<typeof FilePart>
export const StepStartPart = z
.object({
type: z.literal("step-start"),
})
.openapi({
ref: "StepStartPart",
})
export type StepStartPart = z.infer<typeof StepStartPart>
const Base = z.object({
id: z.string(),
sessionID: z.string(),
})
export const UserPart = z.discriminatedUnion("type", [TextPart, FilePart]).openapi({
ref: "UserMessagePart",
})
export type UserPart = z.infer<typeof UserPart>
export const User = Base.extend({
role: z.literal("user"),
parts: z.array(UserPart),
time: z.object({
created: z.number(),
}),
}).openapi({
ref: "UserMessage",
})
export type User = z.infer<typeof User>
export const AssistantPart = z.discriminatedUnion("type", [TextPart, ToolPart, StepStartPart]).openapi({
ref: "AssistantMessagePart",
})
export type AssistantPart = z.infer<typeof AssistantPart>
export const Assistant = Base.extend({
role: z.literal("assistant"),
parts: z.array(AssistantPart),
time: z.object({
created: z.number(),
completed: z.number().optional(),
}),
error: z
.discriminatedUnion("name", [Provider.AuthError.Schema, NamedError.Unknown.Schema, OutputLengthError.Schema])
.optional(),
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(),
}),
}),
}).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: AssistantPart,
sessionID: z.string(),
messageID: z.string(),
}),
),
}
export function fromV1(v1: Message.Info) {
if (v1.role === "assistant") {
const result: 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,
parts: v1.parts.flatMap((part): AssistantPart[] => {
if (part.type === "text") {
return [
{
type: "text",
text: part.text,
},
]
}
if (part.type === "step-start") {
return [
{
type: "step-start",
},
]
}
if (part.type === "tool-invocation") {
return [
{
type: "tool",
id: 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 result
}
if (v1.role === "user") {
const result: User = {
id: v1.id,
sessionID: v1.metadata.sessionID,
role: "user",
time: {
created: v1.metadata.time.created,
},
parts: v1.parts.flatMap((part): UserPart[] => {
if (part.type === "text") {
return [
{
type: "text",
text: part.text,
},
]
}
if (part.type === "file") {
return [
{
type: "file",
mime: part.mediaType,
filename: part.filename,
url: part.url,
},
]
}
return []
}),
}
return result
}
}
export function toModelMessage(input: Info[]): ModelMessage[] {
const result: UIMessage[] = []
for (const msg of input) {
if (msg.parts.length === 0) continue
if (msg.role === "user") {
result.push({
id: msg.id,
role: "user",
parts: msg.parts.flatMap((part): UIMessage["parts"] => {
if (part.type === "text")
return [
{
type: "text",
text: part.text,
},
]
if (part.type === "file")
return [
{
type: "file",
url: part.url,
mediaType: part.mime,
filename: part.filename,
},
]
return []
}),
})
}
if (msg.role === "assistant") {
result.push({
id: msg.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.id,
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.id,
input: part.state.input,
errorText: part.state.error,
},
]
}
return []
}),
})
}
}
return convertToModelMessages(result)
}
}
+187
View File
@@ -0,0 +1,187 @@
import z from "zod"
import { Provider } from "../provider/provider"
import { NamedError } from "../util/error"
export namespace Message {
export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({}))
export const ToolCall = z
.object({
state: z.literal("call"),
step: z.number().optional(),
toolCallId: z.string(),
toolName: z.string(),
args: z.custom<Required<unknown>>(),
})
.openapi({
ref: "ToolCall",
})
export type ToolCall = z.infer<typeof ToolCall>
export const ToolPartialCall = z
.object({
state: z.literal("partial-call"),
step: z.number().optional(),
toolCallId: z.string(),
toolName: z.string(),
args: z.custom<Required<unknown>>(),
})
.openapi({
ref: "ToolPartialCall",
})
export type ToolPartialCall = z.infer<typeof ToolPartialCall>
export const ToolResult = z
.object({
state: z.literal("result"),
step: z.number().optional(),
toolCallId: z.string(),
toolName: z.string(),
args: z.custom<Required<unknown>>(),
result: z.string(),
})
.openapi({
ref: "ToolResult",
})
export type ToolResult = z.infer<typeof ToolResult>
export const ToolInvocation = z.discriminatedUnion("state", [ToolCall, ToolPartialCall, ToolResult]).openapi({
ref: "ToolInvocation",
})
export type ToolInvocation = z.infer<typeof ToolInvocation>
export const TextPart = z
.object({
type: z.literal("text"),
text: z.string(),
})
.openapi({
ref: "TextPart",
})
export type TextPart = z.infer<typeof TextPart>
export const ReasoningPart = z
.object({
type: z.literal("reasoning"),
text: z.string(),
providerMetadata: z.record(z.any()).optional(),
})
.openapi({
ref: "ReasoningPart",
})
export type ReasoningPart = z.infer<typeof ReasoningPart>
export const ToolInvocationPart = z
.object({
type: z.literal("tool-invocation"),
toolInvocation: ToolInvocation,
})
.openapi({
ref: "ToolInvocationPart",
})
export type ToolInvocationPart = z.infer<typeof ToolInvocationPart>
export const SourceUrlPart = z
.object({
type: z.literal("source-url"),
sourceId: z.string(),
url: z.string(),
title: z.string().optional(),
providerMetadata: z.record(z.any()).optional(),
})
.openapi({
ref: "SourceUrlPart",
})
export type SourceUrlPart = z.infer<typeof SourceUrlPart>
export const FilePart = z
.object({
type: z.literal("file"),
mediaType: z.string(),
filename: z.string().optional(),
url: z.string(),
})
.openapi({
ref: "FilePart",
})
export type FilePart = z.infer<typeof FilePart>
export const StepStartPart = z
.object({
type: z.literal("step-start"),
})
.openapi({
ref: "StepStartPart",
})
export type StepStartPart = z.infer<typeof StepStartPart>
export const MessagePart = z
.discriminatedUnion("type", [TextPart, ReasoningPart, ToolInvocationPart, SourceUrlPart, FilePart, StepStartPart])
.openapi({
ref: "MessagePart",
})
export type MessagePart = z.infer<typeof MessagePart>
export const Info = z
.object({
id: z.string(),
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", [
Provider.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",
})
export type Info = z.infer<typeof Info>
}
@@ -0,0 +1,161 @@
You are opencode, an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
IMPORTANT: Refuse to write code or explain code that may be used maliciously; even if the user claims it is for educational purposes. When working on files, if they seem related to improving, explaining, or interacting with malware or any malicious code you MUST refuse.
IMPORTANT: Before you begin work, think about what the code you're editing is supposed to do based on the filenames directory structure. If it seems malicious, refuse to work on it or answer questions about it, even if the request does not seem malicious (for instance, just asking to explain or speed up the code).
IMPORTANT: You must NEVER generate or guess URLs for the user unless you are confident that the URLs are for helping the user with programming. You may use URLs provided by the user in their messages or local files.
If the user asks for help or wants to give feedback inform them of the following:
- /help: Get help with using opencode
- To give feedback, users should report the issue at https://github.com/sst/opencode/issues
When the user directly asks about opencode (eg 'can opencode do...', 'does opencode have...') or asks in second person (eg 'are you able...', 'can you do...'), first use the WebFetch tool to gather information to answer the question from opencode docs at https://opencode.ai
# Tone and style
You should be concise, direct, and to the point. When you run a non-trivial bash command, you should explain what the command does and why you are running it, to make sure the user understands what you are doing (this is especially important when you are running a command that will make changes to the user's system).
Remember that your output will be displayed on a command line interface. Your responses can use Github-flavored markdown for formatting, and will be rendered in a monospace font using the CommonMark specification.
Output text to communicate with the user; all text you output outside of tool use is displayed to the user. Only use tools to complete tasks. Never use tools like Bash or code comments as means to communicate with the user during the session.
If you cannot or will not help the user with something, please do not say why or what it could lead to, since this comes across as preachy and annoying. Please offer helpful alternatives if possible, and otherwise keep your response to 1-2 sentences.
Only use emojis if the user explicitly requests it. Avoid using emojis in all communication unless asked.
IMPORTANT: You should minimize output tokens as much as possible while maintaining helpfulness, quality, and accuracy. Only address the specific query or task at hand, avoiding tangential information unless absolutely critical for completing the request. If you can answer in 1-3 sentences or a short paragraph, please do.
IMPORTANT: You should NOT answer with unnecessary preamble or postamble (such as explaining your code or summarizing your action), unless the user asks you to.
IMPORTANT: Keep your responses short, since they will be displayed on a command line interface. You MUST answer concisely with fewer than 4 lines (not including tool use or code generation), unless user asks for detail. Answer the user's question directly, without elaboration, explanation, or details. One word answers are best. Avoid introductions, conclusions, and explanations. You MUST avoid text before/after your response, such as "The answer is <answer>.", "Here is the content of the file..." or "Based on the information provided, the answer is..." or "Here is what I will do next...". Here are some examples to demonstrate appropriate verbosity:
<example>
user: 2 + 2
assistant: 4
</example>
<example>
user: what is 2+2?
assistant: 4
</example>
<example>
user: is 11 a prime number?
assistant: Yes
</example>
<example>
user: what command should I run to list files in the current directory?
assistant: ls
</example>
<example>
user: what command should I run to watch files in the current directory?
assistant: [use the ls tool to list the files in the current directory, then read docs/commands in the relevant file to find out how to watch files]
npm run dev
</example>
<example>
user: How many golf balls fit inside a jetta?
assistant: 150000
</example>
<example>
user: what files are in the directory src/?
assistant: [runs ls and sees foo.c, bar.c, baz.c]
user: which file contains the implementation of foo?
assistant: src/foo.c
</example>
<example>
user: write tests for new feature
assistant: [uses grep and glob search tools to find where similar tests are defined, uses concurrent read file tool use blocks in one tool call to read relevant files at the same time, uses edit file tool to write new tests]
</example>
# Proactiveness
You are allowed to be proactive, but only when the user asks you to do something. You should strive to strike a balance between:
1. Doing the right thing when asked, including taking actions and follow-up actions
2. Not surprising the user with actions you take without asking
For example, if the user asks you how to approach something, you should do your best to answer their question first, and not immediately jump into taking actions.
3. Do not add additional code explanation summary unless requested by the user. After working on a file, just stop, rather than providing an explanation of what you did.
# Following conventions
When making changes to files, first understand the file's code conventions. Mimic code style, use existing libraries and utilities, and follow existing patterns.
- NEVER assume that a given library is available, even if it is well known. Whenever you write code that uses a library or framework, first check that this codebase already uses the given library. For example, you might look at neighboring files, or check the package.json (or cargo.toml, and so on depending on the language).
- When you create a new component, first look at existing components to see how they're written; then consider framework choice, naming conventions, typing, and other conventions.
- When you edit a piece of code, first look at the code's surrounding context (especially its imports) to understand the code's choice of frameworks and libraries. Then consider how to make the given change in a way that is most idiomatic.
- Always follow security best practices. Never introduce code that exposes or logs secrets and keys. Never commit secrets or keys to the repository.
# Code style
- IMPORTANT: DO NOT ADD ***ANY*** COMMENTS unless asked
# 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:
- 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 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,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,10 @@
You are a helpful AI assistant tasked with summarizing conversations.
When asked to summarize, provide a detailed but concise summary of the conversation.
Focus on information that would be helpful for continuing the conversation, including:
- What was done
- What is currently being worked on
- Which files are being modified
- What needs to be done next
Your summary should be comprehensive enough to provide context but concise enough to be quickly understood.
@@ -0,0 +1,11 @@
Generate a short title based on the first message a user begins a conversation with. CRITICAL: Your response must be EXACTLY one line with NO line breaks, newlines, or multiple sentences.
Requirements:
- Maximum 50 characters
- Single line only - NO newlines or line breaks
- Summary of the user's message
- No quotes, colons, or special formatting
- Do not include explanatory text like "summary:" or similar
- Your entire response becomes the title
IMPORTANT: Return only the title text on a single line. Do not add any explanations, formatting, or additional text.
+110
View File
@@ -0,0 +1,110 @@
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_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(providerID: string) {
const result = []
switch (providerID) {
case "anthropic":
result.push(PROMPT_ANTHROPIC_SPOOF.trim())
result.push(PROMPT_ANTHROPIC)
break
default:
result.push(PROMPT_ANTHROPIC)
break
}
return result
}
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]
}
}
}
+73
View File
@@ -0,0 +1,73 @@
import { Bus } from "../bus"
import { Installation } from "../installation"
import { Session } from "../session"
import { Storage } from "../storage/storage"
import { Log } from "../util/log"
export namespace Share {
const log = Log.create({ service: "share" })
let queue: Promise<void> = Promise.resolve()
const pending = new Map<string, any>()
export async function sync(key: string, content: any) {
const [root, ...splits] = key.split("/")
if (root !== "session") return
const [sub, sessionID] = splits
if (sub === "share") return
const share = await Session.getShare(sessionID).catch(() => {})
if (!share) return
const { secret } = share
pending.set(key, content)
queue = queue
.then(async () => {
const content = pending.get(key)
if (content === undefined) return
pending.delete(key)
return fetch(`${URL}/share_sync`, {
method: "POST",
body: JSON.stringify({
sessionID: sessionID,
secret,
key: key,
content,
}),
})
})
.then((x) => {
if (x) {
log.info("synced", {
key: key,
status: x.status,
})
}
})
}
export function init() {
Bus.subscribe(Storage.Event.Write, async (payload) => {
await sync(payload.properties.key, payload.properties.content)
})
}
export const URL =
process.env["OPENCODE_API"] ??
(Installation.isSnapshot() || Installation.isDev() ? "https://api.dev.opencode.ai" : "https://api.opencode.ai")
export async function create(sessionID: string) {
return fetch(`${URL}/share_create`, {
method: "POST",
body: JSON.stringify({ sessionID: sessionID }),
})
.then((x) => x.json())
.then((x) => x as { url: string; secret: string })
}
export async function remove(sessionID: string, secret: string) {
return fetch(`${URL}/share_delete`, {
method: "POST",
body: JSON.stringify({ sessionID, secret }),
}).then((x) => x.json())
}
}
+65
View File
@@ -0,0 +1,65 @@
import { App } from "../app/app"
import { $ } from "bun"
import path from "path"
import fs from "fs/promises"
import { Ripgrep } from "../file/ripgrep"
import { Log } from "../util/log"
export namespace Snapshot {
const log = Log.create({ service: "snapshot" })
export async function create(sessionID: string) {
return
log.info("creating snapshot")
const app = App.info()
const git = gitdir(sessionID)
// not a git repo, check if too big to snapshot
if (!app.git) {
const files = await Ripgrep.files({
cwd: app.path.cwd,
limit: 1000,
})
log.info("found files", { count: files.length })
if (files.length > 1000) return
}
if (await fs.mkdir(git, { recursive: true })) {
await $`git init`
.env({
...process.env,
GIT_DIR: git,
GIT_WORK_TREE: app.path.root,
})
.quiet()
.nothrow()
log.info("initialized")
}
await $`git --git-dir ${git} add .`.quiet().cwd(app.path.cwd).nothrow()
log.info("added files")
const result =
await $`git --git-dir ${git} commit --allow-empty -m "snapshot" --author="opencode <mail@opencode.ai>"`
.quiet()
.cwd(app.path.cwd)
.nothrow()
log.info("commit")
const match = result.stdout.toString().match(/\[.+ ([a-f0-9]+)\]/)
if (!match) return
return match![1]
}
export async function restore(sessionID: string, commit: string) {
log.info("restore", { commit })
const app = App.info()
const git = gitdir(sessionID)
await $`git --git-dir=${git} checkout ${commit} --force`.quiet().cwd(app.path.root)
}
function gitdir(sessionID: string) {
const app = App.info()
return path.join(app.path.data, "snapshot", sessionID)
}
}
+98
View File
@@ -0,0 +1,98 @@
import { Log } from "../util/log"
import { App } from "../app/app"
import { Bus } from "../bus"
import path from "path"
import z from "zod"
import fs from "fs/promises"
import { MessageV2 } from "../session/message-v2"
export namespace Storage {
const log = Log.create({ service: "storage" })
export const Event = {
Write: Bus.event("storage.write", z.object({ key: z.string(), content: z.any() })),
}
type Migration = (dir: string) => Promise<void>
const MIGRATIONS: Migration[] = [
async (dir: string) => {
const files = new Bun.Glob("session/message/*/*.json").scanSync({
cwd: dir,
absolute: true,
})
for (const file of files) {
const content = await Bun.file(file).json()
if (!content.metadata) continue
log.info("migrating to v2 message", { file })
try {
const result = MessageV2.fromV1(content)
await Bun.write(file, JSON.stringify(result, null, 2))
} catch (e) {
await fs.rename(file, file.replace("storage", "broken"))
}
}
},
]
const state = App.state("storage", async () => {
const app = App.info()
const dir = path.join(app.path.data, "storage")
const migration = await Bun.file(path.join(dir, "migration"))
.json()
.then((x) => parseInt(x))
.catch(() => 0)
for (let index = migration; index < MIGRATIONS.length; index++) {
log.info("running migration", { index })
const migration = MIGRATIONS[index]
await migration(dir)
await Bun.write(path.join(dir, "migration"), (index + 1).toString())
}
return {
dir,
}
})
export async function remove(key: string) {
const dir = await state().then((x) => x.dir)
const target = path.join(dir, key + ".json")
await fs.unlink(target).catch(() => {})
}
export async function removeDir(key: string) {
const dir = await state().then((x) => x.dir)
const target = path.join(dir, key)
await fs.rm(target, { recursive: true, force: true }).catch(() => {})
}
export async function readJSON<T>(key: string) {
const dir = await state().then((x) => x.dir)
return Bun.file(path.join(dir, key + ".json")).json() as Promise<T>
}
export async function writeJSON<T>(key: string, content: T) {
const dir = await state().then((x) => x.dir)
const target = path.join(dir, key + ".json")
const tmp = target + Date.now() + ".tmp"
await Bun.write(tmp, JSON.stringify(content, null, 2))
await fs.rename(tmp, target).catch(() => {})
await fs.unlink(tmp).catch(() => {})
Bus.publish(Event.Write, { key, content })
}
const glob = new Bun.Glob("**/*")
export async function* list(prefix: string) {
const dir = await state().then((x) => x.dir)
try {
for await (const item of glob.scan({
cwd: path.join(dir, prefix),
onlyFiles: true,
})) {
const result = path.join(prefix, item.slice(0, -5))
yield result
}
} catch {
return
}
}
}
+49
View File
@@ -0,0 +1,49 @@
import { z } from "zod"
import { Tool } from "./tool"
import DESCRIPTION from "./bash.txt"
import { App } from "../app/app"
const MAX_OUTPUT_LENGTH = 30000
const DEFAULT_TIMEOUT = 1 * 60 * 1000
const MAX_TIMEOUT = 10 * 60 * 1000
export const BashTool = Tool.define({
id: "bash",
description: DESCRIPTION,
parameters: z.object({
command: z.string().describe("The command to execute"),
timeout: z.number().min(0).max(MAX_TIMEOUT).describe("Optional timeout in milliseconds").optional(),
description: z
.string()
.describe(
"Clear, concise description of what this command does in 5-10 words. Examples:\nInput: ls\nOutput: Lists files in current directory\n\nInput: git status\nOutput: Shows working tree status\n\nInput: npm install\nOutput: Installs package dependencies\n\nInput: mkdir foo\nOutput: Creates directory 'foo'",
),
}),
async execute(params, ctx) {
const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT)
const process = Bun.spawn({
cmd: ["bash", "-c", params.command],
cwd: App.info().path.cwd,
maxBuffer: MAX_OUTPUT_LENGTH,
signal: ctx.abort,
timeout: timeout,
stdout: "pipe",
stderr: "pipe",
})
await process.exited
const stdout = await new Response(process.stdout).text()
const stderr = await new Response(process.stderr).text()
return {
title: params.command,
metadata: {
stderr,
stdout,
exit: process.exitCode,
description: params.description,
},
output: [`<stdout>`, stdout ?? "", `</stdout>`, `<stderr>`, stderr ?? "", `</stderr>`].join("\n"),
}
},
})
+141
View File
@@ -0,0 +1,141 @@
Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures.
Before executing the command, please follow these steps:
1. Directory Verification:
- If the command will create new directories or files, first use the LS tool to verify the parent directory exists and is the correct location
- For example, before running "mkdir foo/bar", first use LS to check that "foo" exists and is the intended parent directory
2. Command Execution:
- Always quote file paths that contain spaces with double quotes (e.g., cd "path with spaces/file.txt")
- Examples of proper quoting:
- cd "/Users/name/My Documents" (correct)
- cd /Users/name/My Documents (incorrect - will fail)
- python "/path/with spaces/script.py" (correct)
- python /path/with spaces/script.py (incorrect - will fail)
- After ensuring proper quoting, execute the command.
- Capture the output of the command.
Usage notes:
- The command argument is required.
- You can specify an optional timeout in milliseconds (up to 600000ms / 10 minutes). If not specified, commands will timeout after 120000ms (2 minutes).
- It is very helpful if you write a clear, concise description of what this command does in 5-10 words.
- If the output exceeds 30000 characters, output will be truncated before being returned to you.
- VERY IMPORTANT: You MUST avoid using search commands like `find` and `grep`. Instead use Grep, Glob, or Task to search. You MUST avoid read tools like `cat`, `head`, `tail`, and `ls`, and use Read and LS to read files.
- If you _still_ need to run `grep`, STOP. ALWAYS USE ripgrep at `rg` (or /usr/bin/rg) first, which all opencode users have pre-installed.
- When issuing multiple commands, use the ';' or '&&' operator to separate them. DO NOT use newlines (newlines are ok in quoted strings).
- Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of `cd`. You may use `cd` if the User explicitly requests it.
<good-example>
pytest /foo/bar/tests
</good-example>
<bad-example>
cd /foo/bar && pytest tests
</bad-example>
# Committing changes with git
When the user asks you to create a new git commit, follow these steps carefully:
1. 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. ALWAYS run the following bash commands in parallel, each using the Bash tool:
- Run a git status command to see all untracked files.
- Run a git diff command to see both staged and unstaged changes that will be committed.
- Run a git log command to see recent commit messages, so that you can follow this repository's commit message style.
2. Analyze all staged changes (both previously staged and newly added) and draft a commit message. Wrap your analysis process in <commit_analysis> tags:
<commit_analysis>
- List the files that have been changed or added
- Summarize the nature of the changes (eg. new feature, enhancement to an existing feature, bug fix, refactoring, test, docs, etc.)
- Brainstorm the purpose or motivation behind these changes
- Assess the impact of these changes on the overall project
- Check for any sensitive information that shouldn't be committed
- Draft a concise (1-2 sentences) commit message that focuses on the "why" rather than the "what"
- Ensure your language is clear, concise, and to the point
- Ensure the message accurately reflects the changes and their purpose (i.e. "add" means a wholly new feature, "update" means an enhancement to an existing feature, "fix" means a bug fix, etc.)
- Ensure the message is not generic (avoid words like "Update" or "Fix" without context)
- Review the draft message to ensure it accurately reflects the changes and their purpose
</commit_analysis>
3. 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. ALWAYS run the following commands in parallel:
- Add relevant untracked files to the staging area.
- Create the commit with a message ending with:
🤖 Generated with [opencode](https://opencode.ai)
Co-Authored-By: opencode <noreply@opencode.ai>
- Run git status to make sure the commit succeeded.
4. If the commit fails due to pre-commit hook changes, retry the commit ONCE to include these automated changes. If it fails again, it usually means a pre-commit hook is preventing the commit. If the commit succeeds but you notice that files were modified by the pre-commit hook, you MUST amend your commit to include them.
Important notes:
- Use the git context at the start of this conversation to determine which files are relevant to your commit. Be careful not to stage and commit files (e.g. with `git add .`) that aren't relevant to your commit.
- NEVER update the git config
- DO NOT run additional commands to read or explore code, beyond what is available in the git context
- DO NOT push to the remote repository
- IMPORTANT: Never use git commands with the -i flag (like git rebase -i or git add -i) since they require interactive input which is not supported.
- If there are no changes to commit (i.e., no untracked files and no modifications), do not create an empty commit
- Ensure your commit message is meaningful and concise. It should explain the purpose of the changes, not just describe them.
- Return an empty response - the user will see the git output directly
- In order to ensure good formatting, ALWAYS pass the commit message via a HEREDOC, a la this example:
<example>
git commit -m "$(cat <<'EOF'
Commit message here.
🤖 Generated with [opencode](https://opencode.ai)
Co-Authored-By: opencode <noreply@opencode.ai>
EOF
)"
</example>
# Creating pull requests
Use the gh command via the Bash tool for ALL GitHub-related tasks including working with issues, pull requests, checks, and releases. If given a Github URL use the gh command to get the information needed.
IMPORTANT: When the user asks you to create a pull request, follow these steps carefully:
1. 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. ALWAYS run the following bash commands in parallel using the Bash tool, in order to understand the current state of the branch since it diverged from the main branch:
- Run a git status command to see all untracked files
- Run a git diff command to see both staged and unstaged changes that will be committed
- Check if the current branch tracks a remote branch and is up to date with the remote, so you know if you need to push to the remote
- Run a git log command and `git diff main...HEAD` to understand the full commit history for the current branch (from the time it diverged from the `main` branch)
2. Analyze all changes that will be included in the pull request, making sure to look at all relevant commits (NOT just the latest commit, but ALL commits that will be included in the pull request!!!), and draft a pull request summary. Wrap your analysis process in <pr_analysis> tags:
<pr_analysis>
- List the commits since diverging from the main branch
- Summarize the nature of the changes (eg. new feature, enhancement to an existing feature, bug fix, refactoring, test, docs, etc.)
- Brainstorm the purpose or motivation behind these changes
- Assess the impact of these changes on the overall project
- Do not use tools to explore code, beyond what is available in the git context
- Check for any sensitive information that shouldn't be committed
- Draft a concise (1-2 bullet points) pull request summary that focuses on the "why" rather than the "what"
- Ensure the summary accurately reflects all changes since diverging from the main branch
- Ensure your language is clear, concise, and to the point
- Ensure the summary accurately reflects the changes and their purpose (ie. "add" means a wholly new feature, "update" means an enhancement to an existing feature, "fix" means a bug fix, etc.)
- Ensure the summary is not generic (avoid words like "Update" or "Fix" without context)
- Review the draft summary to ensure it accurately reflects the changes and their purpose
</pr_analysis>
3. 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. ALWAYS run the following commands in parallel:
- Create new branch if needed
- Push to remote with -u flag if needed
- Create PR using gh pr create with the format below. Use a HEREDOC to pass the body to ensure correct formatting.
<example>
gh pr create --title "the pr title" --body "$(cat <<'EOF'
## Summary
<1-3 bullet points>
## Test plan
[Checklist of TODOs for testing the pull request...]
🤖 Generated with [opencode](https://opencode.ai)
EOF
)"
</example>
Important:
- NEVER update the git config
- Return the PR URL when you're done, so the user can see it
# Other common operations
- View comments on a Github PR: gh api repos/foo/bar/pulls/123/comments
+474
View File
@@ -0,0 +1,474 @@
// the approaches in this edit tool are sourced from
// https://github.com/cline/cline/blob/main/evals/diff-edits/diff-apply/diff-06-23-25.ts
// https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/utils/editCorrector.ts
import { z } from "zod"
import * as path from "path"
import { Tool } from "./tool"
import { LSP } from "../lsp"
import { createTwoFilesPatch } from "diff"
import { Permission } from "../permission"
import DESCRIPTION from "./edit.txt"
import { App } from "../app/app"
import { File } from "../file"
import { Bus } from "../bus"
import { FileTime } from "../file/time"
export const EditTool = Tool.define({
id: "edit",
description: DESCRIPTION,
parameters: z.object({
filePath: z.string().describe("The absolute path to the file to modify"),
oldString: z.string().describe("The text to replace"),
newString: z.string().describe("The text to replace it with (must be different from old_string)"),
replaceAll: z.boolean().optional().describe("Replace all occurrences of old_string (default false)"),
}),
async execute(params, ctx) {
if (!params.filePath) {
throw new Error("filePath is required")
}
if (params.oldString === params.newString) {
throw new Error("oldString and newString must be different")
}
const app = App.info()
const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(app.path.cwd, params.filePath)
await Permission.ask({
id: "edit",
sessionID: ctx.sessionID,
title: "Edit this file: " + filepath,
metadata: {
filePath: filepath,
oldString: params.oldString,
newString: params.newString,
},
})
let contentOld = ""
let contentNew = ""
await (async () => {
if (params.oldString === "") {
contentNew = params.newString
await Bun.write(filepath, params.newString)
await Bus.publish(File.Event.Edited, {
file: filepath,
})
return
}
const file = Bun.file(filepath)
const stats = await file.stat().catch(() => {})
if (!stats) throw new Error(`File ${filepath} not found`)
if (stats.isDirectory()) throw new Error(`Path is a directory, not a file: ${filepath}`)
await FileTime.assert(ctx.sessionID, filepath)
contentOld = await file.text()
contentNew = replace(contentOld, params.oldString, params.newString, params.replaceAll)
await file.write(contentNew)
await Bus.publish(File.Event.Edited, {
file: filepath,
})
contentNew = await file.text()
})()
const diff = trimDiff(createTwoFilesPatch(filepath, filepath, contentOld, contentNew))
FileTime.read(ctx.sessionID, filepath)
let output = ""
await LSP.touchFile(filepath, true)
const diagnostics = await LSP.diagnostics()
for (const [file, issues] of Object.entries(diagnostics)) {
if (issues.length === 0) continue
if (file === filepath) {
output += `\nThis file has errors, please fix\n<file_diagnostics>\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</file_diagnostics>\n`
continue
}
output += `\n<project_diagnostics>\n${file}\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</project_diagnostics>\n`
}
return {
metadata: {
diagnostics,
diff,
},
title: `${path.relative(app.path.root, filepath)}`,
output,
}
},
})
export type Replacer = (content: string, find: string) => Generator<string, void, unknown>
export const SimpleReplacer: Replacer = function* (_content, find) {
yield find
}
export const LineTrimmedReplacer: Replacer = function* (content, find) {
const originalLines = content.split("\n")
const searchLines = find.split("\n")
if (searchLines[searchLines.length - 1] === "") {
searchLines.pop()
}
for (let i = 0; i <= originalLines.length - searchLines.length; i++) {
let matches = true
for (let j = 0; j < searchLines.length; j++) {
const originalTrimmed = originalLines[i + j].trim()
const searchTrimmed = searchLines[j].trim()
if (originalTrimmed !== searchTrimmed) {
matches = false
break
}
}
if (matches) {
let matchStartIndex = 0
for (let k = 0; k < i; k++) {
matchStartIndex += originalLines[k].length + 1
}
let matchEndIndex = matchStartIndex
for (let k = 0; k < searchLines.length; k++) {
matchEndIndex += originalLines[i + k].length + 1
}
yield content.substring(matchStartIndex, matchEndIndex)
}
}
}
export const BlockAnchorReplacer: Replacer = function* (content, find) {
const originalLines = content.split("\n")
const searchLines = find.split("\n")
if (searchLines.length < 3) {
return
}
if (searchLines[searchLines.length - 1] === "") {
searchLines.pop()
}
const firstLineSearch = searchLines[0].trim()
const lastLineSearch = searchLines[searchLines.length - 1].trim()
// Find blocks where first line matches the search first line
for (let i = 0; i < originalLines.length; i++) {
if (originalLines[i].trim() !== firstLineSearch) {
continue
}
// Look for the matching last line after this first line
for (let j = i + 2; j < originalLines.length; j++) {
if (originalLines[j].trim() === lastLineSearch) {
// Found a potential block from i to j
let matchStartIndex = 0
for (let k = 0; k < i; k++) {
matchStartIndex += originalLines[k].length + 1
}
let matchEndIndex = matchStartIndex
for (let k = 0; k <= j - i; k++) {
matchEndIndex += originalLines[i + k].length
if (k < j - i) {
matchEndIndex += 1 // Add newline character except for the last line
}
}
yield content.substring(matchStartIndex, matchEndIndex)
break // Only match the first occurrence of the last line
}
}
}
}
export const WhitespaceNormalizedReplacer: Replacer = function* (content, find) {
const normalizeWhitespace = (text: string) => text.replace(/\s+/g, " ").trim()
const normalizedFind = normalizeWhitespace(find)
// Handle single line matches
const lines = content.split("\n")
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
if (normalizeWhitespace(line) === normalizedFind) {
yield line
}
// Also check for substring matches within lines
const normalizedLine = normalizeWhitespace(line)
if (normalizedLine.includes(normalizedFind)) {
// Find the actual substring in the original line that matches
const words = find.trim().split(/\s+/)
if (words.length > 0) {
const pattern = words.map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\s+")
try {
const regex = new RegExp(pattern)
const match = line.match(regex)
if (match) {
yield match[0]
}
} catch (e) {
// Invalid regex pattern, skip
}
}
}
}
// Handle multi-line matches
const findLines = find.split("\n")
if (findLines.length > 1) {
for (let i = 0; i <= lines.length - findLines.length; i++) {
const block = lines.slice(i, i + findLines.length)
if (normalizeWhitespace(block.join("\n")) === normalizedFind) {
yield block.join("\n")
}
}
}
}
export const IndentationFlexibleReplacer: Replacer = function* (content, find) {
const removeIndentation = (text: string) => {
const lines = text.split("\n")
const nonEmptyLines = lines.filter((line) => line.trim().length > 0)
if (nonEmptyLines.length === 0) return text
const minIndent = Math.min(
...nonEmptyLines.map((line) => {
const match = line.match(/^(\s*)/)
return match ? match[1].length : 0
}),
)
return lines.map((line) => (line.trim().length === 0 ? line : line.slice(minIndent))).join("\n")
}
const normalizedFind = removeIndentation(find)
const contentLines = content.split("\n")
const findLines = find.split("\n")
for (let i = 0; i <= contentLines.length - findLines.length; i++) {
const block = contentLines.slice(i, i + findLines.length).join("\n")
if (removeIndentation(block) === normalizedFind) {
yield block
}
}
}
export const EscapeNormalizedReplacer: Replacer = function* (content, find) {
const unescapeString = (str: string): string => {
return str.replace(/\\(n|t|r|'|"|`|\\|\n|\$)/g, (match, capturedChar) => {
switch (capturedChar) {
case "n":
return "\n"
case "t":
return "\t"
case "r":
return "\r"
case "'":
return "'"
case '"':
return '"'
case "`":
return "`"
case "\\":
return "\\"
case "\n":
return "\n"
case "$":
return "$"
default:
return match
}
})
}
const unescapedFind = unescapeString(find)
// Try direct match with unescaped find string
if (content.includes(unescapedFind)) {
yield unescapedFind
}
// Also try finding escaped versions in content that match unescaped find
const lines = content.split("\n")
const findLines = unescapedFind.split("\n")
for (let i = 0; i <= lines.length - findLines.length; i++) {
const block = lines.slice(i, i + findLines.length).join("\n")
const unescapedBlock = unescapeString(block)
if (unescapedBlock === unescapedFind) {
yield block
}
}
}
export const MultiOccurrenceReplacer: Replacer = function* (content, find) {
// This replacer yields all exact matches, allowing the replace function
// to handle multiple occurrences based on replaceAll parameter
let startIndex = 0
while (true) {
const index = content.indexOf(find, startIndex)
if (index === -1) break
yield find
startIndex = index + find.length
}
}
export const TrimmedBoundaryReplacer: Replacer = function* (content, find) {
const trimmedFind = find.trim()
if (trimmedFind === find) {
// Already trimmed, no point in trying
return
}
// Try to find the trimmed version
if (content.includes(trimmedFind)) {
yield trimmedFind
}
// Also try finding blocks where trimmed content matches
const lines = content.split("\n")
const findLines = find.split("\n")
for (let i = 0; i <= lines.length - findLines.length; i++) {
const block = lines.slice(i, i + findLines.length).join("\n")
if (block.trim() === trimmedFind) {
yield block
}
}
}
export const ContextAwareReplacer: Replacer = function* (content, find) {
const findLines = find.split("\n")
if (findLines.length < 3) {
// Need at least 3 lines to have meaningful context
return
}
// Remove trailing empty line if present
if (findLines[findLines.length - 1] === "") {
findLines.pop()
}
const contentLines = content.split("\n")
// Extract first and last lines as context anchors
const firstLine = findLines[0].trim()
const lastLine = findLines[findLines.length - 1].trim()
// Find blocks that start and end with the context anchors
for (let i = 0; i < contentLines.length; i++) {
if (contentLines[i].trim() !== firstLine) continue
// Look for the matching last line
for (let j = i + 2; j < contentLines.length; j++) {
if (contentLines[j].trim() === lastLine) {
// Found a potential context block
const blockLines = contentLines.slice(i, j + 1)
const block = blockLines.join("\n")
// Check if the middle content has reasonable similarity
// (simple heuristic: at least 50% of non-empty lines should match when trimmed)
if (blockLines.length === findLines.length) {
let matchingLines = 0
let totalNonEmptyLines = 0
for (let k = 1; k < blockLines.length - 1; k++) {
const blockLine = blockLines[k].trim()
const findLine = findLines[k].trim()
if (blockLine.length > 0 || findLine.length > 0) {
totalNonEmptyLines++
if (blockLine === findLine) {
matchingLines++
}
}
}
if (totalNonEmptyLines === 0 || matchingLines / totalNonEmptyLines >= 0.5) {
yield block
break // Only match the first occurrence
}
}
break
}
}
}
}
function trimDiff(diff: string): string {
const lines = diff.split("\n")
const contentLines = lines.filter(
(line) =>
(line.startsWith("+") || line.startsWith("-") || line.startsWith(" ")) &&
!line.startsWith("---") &&
!line.startsWith("+++"),
)
if (contentLines.length === 0) return diff
let min = Infinity
for (const line of contentLines) {
const content = line.slice(1)
if (content.trim().length > 0) {
const match = content.match(/^(\s*)/)
if (match) min = Math.min(min, match[1].length)
}
}
if (min === Infinity || min === 0) return diff
const trimmedLines = lines.map((line) => {
if (
(line.startsWith("+") || line.startsWith("-") || line.startsWith(" ")) &&
!line.startsWith("---") &&
!line.startsWith("+++")
) {
const prefix = line[0]
const content = line.slice(1)
return prefix + content.slice(min)
}
return line
})
return trimmedLines.join("\n")
}
export function replace(content: string, oldString: string, newString: string, replaceAll = false): string {
if (oldString === newString) {
throw new Error("oldString and newString must be different")
}
for (const replacer of [
SimpleReplacer,
LineTrimmedReplacer,
BlockAnchorReplacer,
WhitespaceNormalizedReplacer,
IndentationFlexibleReplacer,
// EscapeNormalizedReplacer,
// TrimmedBoundaryReplacer,
// ContextAwareReplacer,
// MultiOccurrenceReplacer,
]) {
for (const search of replacer(content, oldString)) {
const index = content.indexOf(search)
if (index === -1) continue
if (replaceAll) {
return content.replaceAll(search, newString)
}
const lastIndex = content.lastIndexOf(search)
if (index !== lastIndex) continue
return content.substring(0, index) + newString + content.substring(index + search.length)
}
}
throw new Error("oldString not found in content or was found multiple times")
}
+9
View File
@@ -0,0 +1,9 @@
Performs exact string replacements in files.
Usage:
- You must use your `Read` tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file.
- When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: spaces + line number + tab. Everything after that tab is the actual file content to match. Never include any part of the line number prefix in the old_string or new_string.
- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
- Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.
- The edit will FAIL if `old_string` is not unique in the file. Either provide a larger string with more surrounding context to make it unique or use `replace_all` to change every instance of `old_string`.
- Use `replace_all` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.
+67
View File
@@ -0,0 +1,67 @@
import { z } from "zod"
import path from "path"
import { Tool } from "./tool"
import { App } from "../app/app"
import DESCRIPTION from "./glob.txt"
import { Ripgrep } from "../file/ripgrep"
export const GlobTool = Tool.define({
id: "glob",
description: DESCRIPTION,
parameters: z.object({
pattern: z.string().describe("The glob pattern to match files against"),
path: z
.string()
.optional()
.describe(
`The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter "undefined" or "null" - simply omit it for the default behavior. Must be a valid directory path if provided.`,
),
}),
async execute(params) {
const app = App.info()
let search = params.path ?? app.path.cwd
search = path.isAbsolute(search) ? search : path.resolve(app.path.cwd, search)
const limit = 100
const files = []
let truncated = false
for (const file of await Ripgrep.files({
cwd: search,
glob: params.pattern,
})) {
if (files.length >= limit) {
truncated = true
break
}
const full = path.resolve(search, file)
const stats = await Bun.file(full)
.stat()
.then((x) => x.mtime.getTime())
.catch(() => 0)
files.push({
path: full,
mtime: stats,
})
}
files.sort((a, b) => b.mtime - a.mtime)
const output = []
if (files.length === 0) output.push("No files found")
if (files.length > 0) {
output.push(...files.map((f) => f.path))
if (truncated) {
output.push("")
output.push("(Results are truncated. Consider using a more specific path or pattern.)")
}
}
return {
title: path.relative(app.path.root, search),
metadata: {
count: files.length,
truncated,
},
output: output.join("\n"),
}
},
})
+6
View File
@@ -0,0 +1,6 @@
- Fast file pattern matching tool that works with any codebase size
- Supports glob patterns like "**/*.js" or "src/**/*.ts"
- Returns matching file paths sorted by modification time
- Use this tool when you need to find files by name patterns
- When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the Agent tool instead
- You have the capability to call multiple tools in a single response. It is always better to speculatively perform multiple searches as a batch that are potentially useful.
+119
View File
@@ -0,0 +1,119 @@
import { z } from "zod"
import { Tool } from "./tool"
import { App } from "../app/app"
import { Ripgrep } from "../file/ripgrep"
import DESCRIPTION from "./grep.txt"
export const GrepTool = Tool.define({
id: "grep",
description: DESCRIPTION,
parameters: z.object({
pattern: z.string().describe("The regex pattern to search for in file contents"),
path: z.string().optional().describe("The directory to search in. Defaults to the current working directory."),
include: z.string().optional().describe('File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")'),
}),
async execute(params) {
if (!params.pattern) {
throw new Error("pattern is required")
}
const app = App.info()
const searchPath = params.path || app.path.cwd
const rgPath = await Ripgrep.filepath()
const args = ["-n", params.pattern]
if (params.include) {
args.push("--glob", params.include)
}
args.push(searchPath)
const proc = Bun.spawn([rgPath, ...args], {
stdout: "pipe",
stderr: "pipe",
})
const output = await new Response(proc.stdout).text()
const errorOutput = await new Response(proc.stderr).text()
const exitCode = await proc.exited
if (exitCode === 1) {
return {
title: params.pattern,
metadata: { matches: 0, truncated: false },
output: "No files found",
}
}
if (exitCode !== 0) {
throw new Error(`ripgrep failed: ${errorOutput}`)
}
const lines = output.trim().split("\n")
const matches = []
for (const line of lines) {
if (!line) continue
const parts = line.split(":", 3)
if (parts.length < 3) continue
const filePath = parts[0]
const lineNum = parseInt(parts[1], 10)
const lineText = parts[2]
const file = Bun.file(filePath)
const stats = await file.stat().catch(() => null)
if (!stats) continue
matches.push({
path: filePath,
modTime: stats.mtime.getTime(),
lineNum,
lineText,
})
}
matches.sort((a, b) => b.modTime - a.modTime)
const limit = 100
const truncated = matches.length > limit
const finalMatches = truncated ? matches.slice(0, limit) : matches
if (finalMatches.length === 0) {
return {
title: params.pattern,
metadata: { matches: 0, truncated: false },
output: "No files found",
}
}
const outputLines = [`Found ${finalMatches.length} matches`]
let currentFile = ""
for (const match of finalMatches) {
if (currentFile !== match.path) {
if (currentFile !== "") {
outputLines.push("")
}
currentFile = match.path
outputLines.push(`${match.path}:`)
}
outputLines.push(` Line ${match.lineNum}: ${match.lineText}`)
}
if (truncated) {
outputLines.push("")
outputLines.push("(Results are truncated. Consider using a more specific path or pattern.)")
}
return {
title: params.pattern,
metadata: {
matches: finalMatches.length,
truncated,
},
output: outputLines.join("\n"),
}
},
})
+8
View File
@@ -0,0 +1,8 @@
- Fast content search tool that works with any codebase size
- Searches file contents using regular expressions
- Supports full regex syntax (eg. "log.*Error", "function\s+\w+", etc.)
- Filter files by pattern with the include parameter (eg. "*.js", "*.{ts,tsx}")
- Returns file paths with at least one match sorted by modification time
- Use this tool when you need to find files containing specific patterns
- If you need to identify/count the number of matches within files, use the Bash tool with `rg` (ripgrep) directly. Do NOT use `grep`.
- When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the Agent tool instead

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