Compare commits

...

155 Commits

Author SHA1 Message Date
Dax Raad f181fbf3f0 lower max retries to 3 - ai sdk currently cannot abort during a retry delay so things appear to be frozen
publish / publish (push) Waiting to run
2025-07-30 15:08:25 -04:00
Dax Raad 4a53957e9d release undo/redo 2025-07-30 13:09:18 -04:00
Aiden Cline a33870ea66 fix: attachment highlighting (#1427) 2025-07-30 11:43:34 -05:00
Aiden Cline d4e91d3d5f fix: prevent read tool from opening binary files and corrupting session (#1425) 2025-07-30 11:00:23 -05:00
GitHub Action 66891b148b ignore: update download stats 2025-07-30 2025-07-30 12:04:23 +00:00
Sam Huckaby 3bd652b22e fix: quick grammar and spelling check (#1402) 2025-07-30 05:54:47 -05:00
Aiden Cline 02d838c68e tweak: adjust astro css to render mixed nested lists (#1411) 2025-07-30 05:51:52 -05:00
municorn 8618f0d88b build: add @octokit/rest to opencode dependencies (#1396)
Co-authored-by: Frank <frank@sst.dev>
2025-07-29 22:33:25 -04:00
Dax Raad e9af4d7216 update beast prompt for openai models
publish / publish (push) Waiting to run
2025-07-29 22:15:13 -04:00
Dax Raad 8528f471c7 experimental well-known auth support
publish / publish (push) Waiting to run
2025-07-29 19:30:51 -04:00
Yordis Prieto 9790340493 chore: fix test to have deterministic testing (#1401) 2025-07-29 17:54:22 -05:00
Dax Raad c9b9c844db remove min/max in tool schemas
publish / publish (push) Waiting to run
2025-07-29 17:39:47 -04:00
Frank f30b5c0ec4 convert share backend to hono app 2025-07-29 16:39:48 -04:00
adamdotdevin c3685d1efe feat: show git diff in reverted messages 2025-07-29 13:11:38 -05:00
Yordis Prieto f294293e2f fix: update file name extraction in uploads test to use __filename (#1395) 2025-07-29 12:28:44 -05:00
Yordis Prieto 83ebda87d2 fix: improve handling of global File object in uploads tests (#1394) 2025-07-29 11:30:39 -05:00
Yordis Prieto 1588863883 chore: fix null handling in multipartFormRequestOptions test (#1385) 2025-07-29 11:17:03 -05:00
Andrea Grandi 173da3caaf feat: show current git branch in status bar, and make it responsive (#1339)
Co-authored-by: adamdotdevin <2363879+adamdottv@users.noreply.github.com>
2025-07-29 11:15:04 -05:00
Dax Raad 5a98110ab0 strip todo tool instructions from non anthropic models 2025-07-29 11:56:53 -04:00
Jacob Hands 9e0edaa5d6 feat: add OPENCODE_CONFIG env var for specifying a custom config file (#1370) 2025-07-29 11:03:11 -04:00
GitHub Action 9e6f0de575 ignore: update download stats 2025-07-29 2025-07-29 12:04:40 +00:00
Aiden Cline d9278e4dcc fix: task type error (#1384) 2025-07-29 06:18:34 -05:00
Dax Raad 2160f0815c trim opencode title 2025-07-28 23:24:38 -04:00
Dax Raad 53ecda4ef1 wip: undo properly remove messages from UI 2025-07-28 22:58:31 -04:00
Dax Raad 4f62117758 wip: snapshot 2025-07-28 22:58:31 -04:00
Jay V 26335aeb01 docs: share page hide patch part 2025-07-28 20:04:00 -04:00
Jay V 1931d70e77 docs: adding 2025-07-28 20:00:30 -04:00
Jay V 3ae8e63ce5 Tweak auth cli copy 2025-07-28 20:00:30 -04:00
Dax Raad fa6fad2bac ignore: tweak
publish / publish (push) Waiting to run
2025-07-28 12:20:37 -04:00
Dax Raad ff611ae792 fix phantom tool call failed messages and empty text parts with some models 2025-07-28 12:19:38 -04:00
GitHub Action 90d8c47c95 ignore: update download stats 2025-07-28 2025-07-28 12:04:21 +00:00
Frank 2b9ab27a6f vscode: add cmd+shift+esc keybinding
publish-vscode / publish (push) Waiting to run
2025-07-27 15:54:45 -04:00
Frank 5a024afa87 vscode: bring oc terminal to front if already opened 2025-07-27 14:57:45 -04:00
Frank f56daab456 show opencode button in vscode when focused on terminal 2025-07-27 14:44:14 -04:00
Aiden Cline f9d45b7d3b fix: attachment bugs (#1335) 2025-07-27 12:21:31 -05:00
Frank adfcd1a9eb fix input format affected by installing vscode extension
publish / publish (push) Waiting to run
2025-07-27 11:56:18 -04:00
Frank ebc7c8dfd6 simplify github action 2025-07-27 09:56:09 -04:00
GitHub Action 487275dce8 ignore: update download stats 2025-07-27 2025-07-27 12:04:15 +00:00
Aiden Cline 377a66d415 docs: document small_model cfg option (#1347) 2025-07-26 16:56:38 -05:00
Dax Raad 1cd12ca740 ci: fix opencode github 2025-07-26 10:02:31 -04:00
Aiden Cline aa30428e76 fix: strip thinking blocks from title (#1325) 2025-07-26 08:29:04 -05:00
GitHub Action 2ff7883f78 ignore: update download stats 2025-07-26 2025-07-26 12:03:59 +00:00
Frank 6692d1c5dc wip: github actions
publish / publish (push) Waiting to run
2025-07-26 02:49:05 -04:00
Frank 6182b4a0c1 wip: github action 2025-07-26 01:03:23 -04:00
Frank 6b7cfde20a wip: github actions
publish / publish (push) Waiting to run
2025-07-25 20:27:42 -04:00
Didier Durand 03e9ee7293 fixing various typos in text. (#1185) 2025-07-25 20:20:01 -04:00
Dax Raad 4fc32f7c9a better mcp support - should fix hanging when streamable http server is added
publish / publish (push) Waiting to run
2025-07-25 19:19:47 -04:00
Frank 39e0ae70fe wip: github actions
publish / publish (push) Waiting to run
2025-07-25 19:05:55 -04:00
Frank a67106b700 wip: github action
publish-github-action / publish (push) Waiting to run
2025-07-25 18:33:45 -04:00
Dax Raad 5a3aed4e88 qwen optimizations it works good now 2025-07-25 18:31:08 -04:00
Frank a3bd92ee60 wip: github actions
publish-github-action / publish (push) Waiting to run
publish / publish (push) Waiting to run
2025-07-25 18:29:53 -04:00
Michael Hanson 7e3fe04b06 Fix a broken example in the MCP documentation and add more clarity (#1322) 2025-07-25 17:47:01 -04:00
Frank 35a914ad99 wip: github actions
publish / publish (push) Waiting to run
2025-07-25 17:34:47 -04:00
Dax Raad 01198a5fdc wip: sync
publish / publish (push) Waiting to run
2025-07-25 15:52:27 -04:00
Dax Raad a765b14ad5 wip: drop 2025-07-25 15:51:02 -04:00
Dax Raad 2767f4f9c3 handle uploaded text/plain 2025-07-25 15:48:42 -04:00
kehanzhang a155713a52 fix(headless): respect mode passed to /message endpoint (#1300) 2025-07-25 15:26:49 -04:00
Dax Raad d655601c47 docs: mode temperature 2025-07-25 13:45:04 -04:00
Dax Raad 4b94f390ce allow temperature to be configured per mode
publish / publish (push) Waiting to run
2025-07-25 13:29:44 -04:00
Dominik Engelhardt 8e036cf17b fix: apply content-level caching for non-anthropic providers (#1305) 2025-07-25 12:19:44 -04:00
Yihui Khuu 0254c612ee feat: add csharp lsp (#1312) 2025-07-25 12:17:06 -04:00
Dax Raad 5febb0ff94 fix issue with trailing whitespace error in assistant message 2025-07-25 10:56:16 -04:00
Dax Raad 79edca063e fix reading model from config
publish / publish (push) Waiting to run
2025-07-25 10:53:37 -04:00
Yihui Khuu 58c47c20a2 fix: header width to display header in one line when sharing disabled (#1310) 2025-07-25 09:32:06 -05:00
Dax Raad 4f6a568d80 fix double system prompt 2025-07-25 10:28:42 -04:00
Dax Raad 1823d20d26 disable todo tools by default in agent
publish / publish (push) Waiting to run
2025-07-25 10:23:23 -04:00
Dax Raad 4ecfa69a07 fix symlinked agents
publish / publish (push) Waiting to run
2025-07-25 10:20:16 -04:00
GitHub Action 34bdc43a7f ignore: update download stats 2025-07-25 2025-07-25 12:04:21 +00:00
Dax Raad 82e28e2717 agents better display when spawning
publish / publish (push) Waiting to run
2025-07-24 23:08:03 -04:00
Dax Raad 31febec1c5 docs: agents
publish / publish (push) Waiting to run
2025-07-24 22:18:49 -04:00
Dax Raad 149bfd5e36 docs: agents 2025-07-24 22:16:16 -04:00
Dax Raad 61dc0a7b3c docs: agents 2025-07-24 22:07:30 -04:00
Dax Raad 0de957a2f0 real life totally configurabl ai subasians
publish / publish (push) Waiting to run
2025-07-24 21:21:02 -04:00
Frank 0caf100598 wip: github actions 2025-07-24 19:03:10 -04:00
Jay V 9f51d713ce docs: add to quick start 2025-07-24 18:57:54 -04:00
Filip c903c82704 Fix: added environment() to summarize() (#1290) 2025-07-24 18:24:54 -04:00
Dax Raad 621e7f8697 wip: handle deleting file 2025-07-24 17:49:23 -04:00
Dax Raad 877cbfc45b wip: fix type errors 2025-07-24 17:38:11 -04:00
Dax Raad 0d12a8c4d9 wip: more snapshot stuff 2025-07-24 17:38:11 -04:00
Aiden Cline 0463894731 (tui) tweak: add setting for scroll speed (#1288) 2025-07-24 16:34:59 -05:00
Clay Warren 9b94c9c57f feat: Replace unzip with @zip.js/zip.js for Windows compatibility (#662) 2025-07-24 16:49:04 -04:00
Filip 0ef0b3404b fix: add custom() to system prompt on summarize (#1289) 2025-07-24 16:48:17 -04:00
Filip 771c714b10 fix: edit tool (#1287) 2025-07-24 16:18:04 -04:00
Dax Raad 03cd495c20 ci: tweak
publish / publish (push) Waiting to run
2025-07-24 15:55:45 -04:00
Dax Raad 85c4cd83bc ci: tweak
publish / publish (push) Waiting to run
2025-07-24 15:51:33 -04:00
Dax Raad a480809965 includ baseline builds
publish / publish (push) Waiting to run
2025-07-24 14:37:38 -04:00
Jesse van der Pluijm 5c1d6d3c8d Check if modelID includes "claude" for antropic/claude prompt caching (#1284) 2025-07-24 11:31:28 -04:00
Dax Raad 99f1c60633 disable snapshots
publish / publish (push) Waiting to run
2025-07-24 11:08:20 -04:00
Aiden Cline cbffb6baad fix: slog error log serialization (#1276) 2025-07-24 07:19:00 -05:00
danielfyhr c1ce8564d2 add aura theme (#1280) 2025-07-24 07:17:27 -05:00
GitHub Action a6e69ae533 ignore: update download stats 2025-07-24 2025-07-24 12:04:18 +00:00
Dax Raad da84388d6c disable undo/redo for now 2025-07-23 21:02:13 -04:00
Rico Sta. Cruz cd758913cc fix: update max visible height in list tests (#1269) 2025-07-23 20:49:15 -04:00
Dax Raad 1d0162ebae fix header width 2025-07-23 20:48:35 -04:00
Dax Raad 0475fb5137 wip: always force create snapshot 2025-07-23 20:46:43 -04:00
Dax Raad cbf56d2cbd wip: fix redoing 2025-07-23 20:42:02 -04:00
Dax 11d236609d basic undo feature (#1268)
Co-authored-by: adamdotdevin <2363879+adamdottv@users.noreply.github.com>
Co-authored-by: Jay V <air@live.ca>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Andrew Joslin <andrew@ajoslin.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Tobias Walle <9933601+tobias-walle@users.noreply.github.com>
2025-07-23 20:30:46 -04:00
Yihui Khuu 9e80e6d855 feat: pass mode into task tool (#1248) 2025-07-23 20:29:59 -04:00
Aiden Cline 2e376833f6 docs: remove deprecated 'log_level' reference in docs (#1258) 2025-07-23 18:53:58 -04:00
Aiden Cline 4caae581b8 tweak: handle pasted attachment references (#1257) 2025-07-23 15:41:17 -05:00
Tobias Walle 1b848a00a5 fix "working" spinner animation (#1054) (#1259) 2025-07-23 15:40:34 -05:00
Dax Raad c2c65c796a remove providers path
publish / publish (push) Waiting to run
2025-07-23 12:15:31 -04:00
Dax Raad 38b4d1e03d add debug paths command 2025-07-23 12:14:54 -04:00
GitHub Action 117d54e948 ignore: update download stats 2025-07-23 2025-07-23 12:04:18 +00:00
Aiden Cline 7f3c98aa1f tweak: ensure most recently interacted with session appears at the top (#1239) 2025-07-22 22:37:36 -05:00
Dax Raad 1acf196e98 config: improve config schema
publish / publish (push) Waiting to run
2025-07-22 20:35:40 -04:00
Jay V 36abbe1d7b docs: edit ide 2025-07-22 19:02:30 -04:00
Andrew Joslin 7ac284c977 Allow multiline prompts for github agent (#1225) 2025-07-22 18:30:51 -04:00
Aiden Cline 774678077f feat(tui): support pipe into tui (#1230) 2025-07-22 17:19:20 -05:00
Jay V 7ed687975e docs: add tutorial closes #740 2025-07-22 17:54:53 -04:00
Frank a382ad9b1c wip: vscode extension 2025-07-22 17:13:58 -04:00
Dax Raad 4b52e5bd49 ci: error github api fail 2025-07-22 17:06:06 -04:00
Frank c6eed8c6fe wip: vscode extension 2025-07-22 16:50:56 -04:00
Frank 824da2a300 wip: vscode extension
publish / publish (push) Waiting to run
2025-07-22 16:46:44 -04:00
Frank 06bee6d089 wip: vscode extension 2025-07-22 16:46:44 -04:00
Frank 76fb049963 wip: vscode extension 2025-07-22 16:46:44 -04:00
Dax Raad 0eebf55d07 ignore: lock changes 2025-07-22 15:49:36 -04:00
Dax Raad 09e8d33f5f wip: tui api 2025-07-22 15:49:24 -04:00
Frank 23aa869162 wip: vscode extension
publish-vscode / publish (push) Waiting to run
2025-07-22 15:36:55 -04:00
Frank 64d43e06ec wip: vscode extension 2025-07-22 15:28:09 -04:00
Frank c7027ea017 wip: vscode extension 2025-07-22 13:21:49 -04:00
Aiden Cline 6eb987cbb9 tweak: cleanup cancelled markdown (#1222) 2025-07-22 12:08:03 -05:00
adamdotdevin 0b2d6c20ff chore: generate sdk into packages/sdk 2025-07-22 11:50:51 -05:00
Dax Raad f4ecb5e90b wip: append-prompt is better 2025-07-22 12:27:02 -04:00
Dax Raad 9dd85ea2c7 wip: tui api 2025-07-22 12:15:50 -04:00
Frank eca975848a wip: vscode extension 2025-07-22 11:21:29 -04:00
adamdotdevin a05547f189 fix(tui): more defensive attachment conversion
publish / publish (push) Waiting to run
2025-07-22 09:28:13 -05:00
GitHub Action 51aba439ec ignore: update download stats 2025-07-22 2025-07-22 12:04:22 +00:00
Aiden Cline 7d6d060f7d fix: mouse text selection bug (#1206) 2025-07-21 19:15:36 -05:00
Dax Raad 26e93c4d11 wip: api for tui 2025-07-21 19:53:58 -04:00
Frank cfe5f3bbbc wip: vscode extension 2025-07-21 19:10:57 -04:00
Timo Clasen bb7aec2213 feat(tui): add ctrl+p and ctrl-n to history navigation (#1199) 2025-07-21 15:10:50 -05:00
Frank cf3006d133 wip: vscode extension
publish-vscode / publish (push) Waiting to run
2025-07-21 15:48:46 -04:00
Dax Raad a6cd7c220a ci: tweaks 2025-07-21 15:45:44 -04:00
Dax Raad 4d6e7f818d fix broken attachments
publish / publish (push) Waiting to run
2025-07-21 15:38:41 -04:00
Dax Raad adf56be05f fix title generation bug
publish / publish (push) Waiting to run
2025-07-21 15:23:47 -04:00
Dax 531d035547 message queuing (#1200)
publish / publish (push) Waiting to run
2025-07-21 15:14:54 -04:00
adamdotdevin 0aec866a8c fix(tui): up/down arrow handling
publish / publish (push) Waiting to run
2025-07-21 10:44:21 -05:00
plyght fc18dbaa4c feat(tui): paste minimizing (#784)
Co-authored-by: adamdotdevin <2363879+adamdottv@users.noreply.github.com>
2025-07-21 10:31:29 -05:00
Dax Raad 6f15934a6b do not copy empty strings 2025-07-21 11:27:15 -04:00
Dax Raad d705b484c8 include newline between messages when copying
publish / publish (push) Waiting to run
2025-07-21 11:22:51 -04:00
Dax Raad adcc58a2b0 add toast for copy
publish / publish (push) Waiting to run
2025-07-21 11:19:54 -04:00
adamelmore 76a98df85f feat(tui): disable keybinds 2025-07-21 10:08:25 -05:00
adamelmore f356058fb4 docs: cleanup 2025-07-21 10:02:58 -05:00
adamelmore db24514d3e feat(tui): theme override with OPENCODE_THEME 2025-07-21 10:02:57 -05:00
adamdotdevin 4c5bbcf74e fix(tui): restore spinner ticks
publish / publish (push) Waiting to run
2025-07-21 05:58:24 -05:00
adamdotdevin b6f723f092 feat(tui): message history select with up/down arrows
publish / publish (push) Waiting to run
2025-07-21 05:52:11 -05:00
Aiden Cline e4f4915136 fix: pasting issue (#1182) 2025-07-21 04:09:16 -05:00
Aiden Cline f5180998dd fix: export cmd (#1184) 2025-07-21 04:08:26 -05:00
Dax 9b672c6b44 Update STATS.md 2025-07-20 20:36:23 -04:00
Dax Raad 22c807f5ee trimmed selection ui
publish / publish (push) Waiting to run
2025-07-20 19:36:56 -04:00
Dax Raad bce6822bcc ignore: remove log 2025-07-20 19:08:19 -04:00
Dax Raad 004f6504e2 implement copy paste much wow can you believe we went this long without it so stupid i blame adam
publish / publish (push) Waiting to run
2025-07-20 19:05:38 -04:00
Dax Raad 9278f64588 do not snapshot nongit projects for now
publish / publish (push) Waiting to run
2025-07-20 13:59:30 -04:00
Frank 4bdb686e1b wip: vscode extension
publish-vscode / publish (push) Waiting to run
2025-07-20 13:31:16 -04:00
255 changed files with 21364 additions and 3153 deletions
+6 -3
View File
@@ -6,9 +6,12 @@ on:
jobs:
opencode:
if: startsWith(github.event.comment.body, 'hey opencode')
if: |
contains(github.event.comment.body, '/oc') ||
contains(github.event.comment.body, '/opencode')
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- name: Checkout repository
@@ -17,8 +20,8 @@ jobs:
fetch-depth: 1
- name: Run opencode
uses: sst/opencode/sdks/github@github-v1
uses: sst/opencode/github@latest
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
with:
model: anthropic/claude-sonnet-4-20250514
model: anthropic/claude-sonnet-4-20250514
+1 -1
View File
@@ -27,4 +27,4 @@ jobs:
git config --global user.email "opencode@sst.dev"
git config --global user.name "opencode"
./script/publish
working-directory: ./sdks/github
working-directory: ./github
+1 -1
View File
@@ -1,8 +1,8 @@
.DS_Store
node_modules
.opencode
.sst
.env
.idea
.vscode
openapi.json
scratch
@@ -0,0 +1,44 @@
---
description: >-
Use this agent when you need to create or improve documentation that requires
concrete examples to illustrate every concept. Examples include:
<example>Context: User has written a new API endpoint and needs documentation.
user: 'I just created a POST /users endpoint that accepts name and email
fields. Can you document this?' assistant: 'I'll use the
example-driven-docs-writer agent to create documentation with practical
examples for your API endpoint.' <commentary>Since the user needs
documentation with examples, use the example-driven-docs-writer agent to
create comprehensive docs with code samples.</commentary></example>
<example>Context: User has a complex configuration file that needs
documentation. user: 'This config file has multiple sections and I need docs
that show how each option works' assistant: 'Let me use the
example-driven-docs-writer agent to create documentation that breaks down each
configuration option with practical examples.' <commentary>The user needs
documentation that demonstrates configuration options, perfect for the
example-driven-docs-writer agent.</commentary></example>
---
You are an expert technical documentation writer who specializes in creating clear, example-rich documentation that never leaves readers guessing. Your core principle is that every concept must be immediately illustrated with concrete examples, code samples, or practical demonstrations.
Your documentation approach:
- Never write more than one sentence in any section without providing an example, code snippet, diagram, or practical illustration
- Break up longer explanations with multiple examples showing different scenarios or use cases
- Use concrete, realistic examples rather than abstract or placeholder content
- Include both basic and advanced examples when covering complex topics
- Show expected inputs, outputs, and results for all examples
- Use code blocks, bullet points, tables, or other formatting to visually separate examples from explanatory text
Structural requirements:
- Start each section with a brief one-sentence explanation followed immediately by an example
- For multi-step processes, provide an example after each step
- Include error examples and edge cases alongside success scenarios
- Use consistent formatting and naming conventions throughout examples
- Ensure examples are copy-pasteable and functional when applicable
Quality standards:
- Verify that no paragraph exceeds one sentence without an accompanying example
- Test that examples are accurate and would work in real scenarios
- Ensure examples progress logically from simple to complex
- Include context for when and why to use different approaches shown in examples
- Provide troubleshooting examples for common issues
When you receive a documentation request, immediately identify what needs examples and plan to illustrate every single concept, feature, or instruction with concrete demonstrations. Ask for clarification if you need more context to create realistic, useful examples.
-2
View File
@@ -1,5 +1,3 @@
# TUI Agent Guidelines
## Style
- prefer single word variable/function names
+1 -1
View File
@@ -97,7 +97,7 @@ $ bun run packages/opencode/src/index.ts
It's very similar to Claude Code in terms of capability. Here are the key differences:
- 100% open source
- Not coupled to any provider. Although Anthropic is recommended, opencode can be used with OpenAI, Google or even local models. As models evolve the gaps between them will close and pricing will drop so being provider agnostic is important.
- Not coupled to any provider. Although Anthropic is recommended, opencode can be used with OpenAI, Google or even local models. As models evolve the gaps between them will close and pricing will drop so being provider-agnostic is important.
- A focus on TUI. opencode is built by neovim users and the creators of [terminal.shop](https://terminal.shop); we are going to push the limits of what's possible in the terminal.
- A client/server architecture. This for example can allow opencode to run on your computer, while you can drive it remotely from a mobile app. Meaning that the TUI frontend is just one of the possible clients.
+10 -1
View File
@@ -20,6 +20,15 @@
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
| 2025-07-18 | 70,380 (+1) | 102,587 (+0) | 172,967 (+1) |
| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) |
| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) |
+1025 -19
View File
File diff suppressed because it is too large Load Diff
+137
View File
@@ -0,0 +1,137 @@
# opencode GitHub Action
A GitHub Action that integrates [opencode](https://opencode.ai) directly into your GitHub workflow.
Mention `/opencode` in your comment, and opencode will execute tasks within your GitHub Actions runner.
## Features
#### Explain an issues
Leave the following comment on a GitHub issue. `opencode` will read the entire thread, including all comments, and reply with a clear explanation.
```
/opencode explain this issue
```
#### Fix an issues
Leave the following comment on a GitHub issue. opencode will create a new branch, implement the changes, and open a PR with the changes.
```
/opencode fix this
```
#### Review PRs and make changes
Leave the following comment on a GitHub PR. opencode will implement the requested change and commit it to the same PR.
```
Delete the attachment from S3 when the note is removed /oc
```
## Installation
Run the following command in the terminal from your GitHub repo:
```bash
opencode github install
```
This will walk you through installing the GitHub app, creating the workflow, and setting up secrets.
### Manual Setup
1. Install the GitHub app https://github.com/apps/opencode-agent. Make sure it is installed on the target repository.
2. Add the following workflow file to `.github/workflows/opencode.yml` in your repo. Set the appropriate `model` and required API keys in `env`.
```yml
name: opencode
on:
issue_comment:
types: [created]
jobs:
opencode:
if: |
contains(github.event.comment.body, '/oc') ||
contains(github.event.comment.body, '/opencode')
runs-on: ubuntu-latest
permissions:
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run opencode
uses: sst/opencode/github@latest
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
with:
model: anthropic/claude-sonnet-4-20250514
```
3. Store the API keys in secrets. In your organization or project **settings**, expand **Secrets and variables** on the left and select **Actions**. Add the required API keys.
## Support
This is an early release. If you encounter issues or have feedback, please create an issue at https://github.com/sst/opencode/issues.
## Development
To test locally:
1. Navigate to a test repo (e.g. `hello-world`):
```bash
cd hello-world
```
2. Run:
```bash
MODEL=anthropic/claude-sonnet-4-20250514 \
ANTHROPIC_API_KEY=sk-ant-api03-1234567890 \
GITHUB_RUN_ID=dummy \
bun /path/to/opencode/packages/opencode/src/index.ts github run \
--token 'github_pat_1234567890' \
--event '{"eventName":"issue_comment",...}'
```
- `MODEL`: The model used by opencode. Same as the `MODEL` defined in the GitHub workflow.
- `ANTHROPIC_API_KEY`: Your model provider API key. Same as the keys defined in the GitHub workflow.
- `GITHUB_RUN_ID`: Dummy value to emulate GitHub action environment.
- `/path/to/opencode`: Path to your cloned opencode repo. `bun /path/to/opencode/packages/opencode/src/index.ts` runs your local version of `opencode`.
- `--token`: A GitHub persontal access token. This token is used to verify you have `admin` or `write` access to the test repo. Generate a token [here](https://github.com/settings/personal-access-tokens).
- `--event`: Mock GitHub event payload (see templates below).
### Issue comment event
```
--event '{"eventName":"issue_comment","repo":{"owner":"sst","repo":"hello-world"},"actor":"fwang","payload":{"issue":{"number":4},"comment":{"id":1,"body":"hey opencode, summarize thread"}}}'
```
Replace:
- `"owner":"sst"` with repo owner
- `"repo":"hello-world"` with repo name
- `"actor":"fwang"` with the GitHub username of commentor
- `"number":4` with the GitHub issue id
- `"body":"hey opencode, summarize thread"` with comment body
### Issue comment with image attachment.
```
--event '{"eventName":"issue_comment","repo":{"owner":"sst","repo":"hello-world"},"actor":"fwang","payload":{"issue":{"number":4},"comment":{"id":1,"body":"hey opencode, what is in my image ![Image](https://github.com/user-attachments/assets/xxxxxxxx)"}}}'
```
Replace the image URL `https://github.com/user-attachments/assets/xxxxxxxx` with a valid GitHub attachment (you can generate one by commenting with an image in any issue).
### PR comment event
```
--event '{"eventName":"issue_comment","repo":{"owner":"sst","repo":"hello-world"},"actor":"fwang","payload":{"issue":{"number":4,"pull_request":{}},"comment":{"id":1,"body":"hey opencode, summarize thread"}}}'
```
+29
View File
@@ -0,0 +1,29 @@
name: "opencode GitHub Action"
description: "Run opencode in GitHub Actions workflows"
branding:
icon: "code"
color: "orange"
inputs:
model:
description: "Model to use"
required: true
share:
description: "Share the opencode session (defaults to true for public repos)"
required: false
runs:
using: "composite"
steps:
- name: Install opencode
shell: bash
run: curl -fsSL https://opencode.ai/install | bash
- name: Run opencode
shell: bash
id: run_opencode
run: opencode github run
env:
MODEL: ${{ inputs.model }}
SHARE: ${{ inputs.share }}
@@ -8,8 +8,8 @@ if [ -z "$latest_tag" ]; then
fi
echo "Latest tag: $latest_tag"
# Update github-v1 to latest
git tag -d github-v1
git push origin :refs/tags/github-v1
git tag -a github-v1 $latest_tag -m "Update github-v1 to $latest_tag"
git push origin github-v1
# Update latest tag
git tag -d latest
git push origin :refs/tags/latest
git tag -a latest $latest_tag -m "Update latest to $latest_tag"
git push origin latest
+9
View File
@@ -12,9 +12,18 @@
}
}
}
},
"huggingface": {
"models": {
"Qwen/Qwen3-235B-A22B-Instruct-2507:fireworks-ai": {}
}
}
},
"mcp": {
"context7": {
"type": "remote",
"url": "https://mcp.context7.com/sse"
},
"weather": {
"type": "local",
"command": ["opencode", "x", "@h1deya/mcp-server-weather"]
+4 -3
View File
@@ -15,10 +15,11 @@
"packages/*"
],
"catalog": {
"typescript": "5.8.2",
"@types/node": "22.13.9",
"zod": "3.25.49",
"ai": "5.0.0-beta.21"
"ai": "5.0.0-beta.21",
"hono": "4.7.10",
"typescript": "5.8.2",
"zod": "3.25.49"
}
},
"devDependencies": {
+1
View File
@@ -12,6 +12,7 @@
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "22.0.0",
"hono": "catalog:",
"jose": "6.0.11"
}
}
+176 -184
View File
@@ -1,3 +1,4 @@
import { Hono } from "hono"
import { DurableObject } from "cloudflare:workers"
import { randomUUID } from "node:crypto"
import { jwtVerify, createRemoteJWKSet } from "jose"
@@ -111,165 +112,156 @@ export class SyncServer extends DurableObject<Env> {
}
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const url = new URL(request.url)
const splits = url.pathname.split("/")
const method = splits[1]
if (request.method === "GET" && method === "") {
return new Response("Hello, world!", {
headers: { "Content-Type": "text/plain" },
})
export default new Hono<{ Bindings: Env }>()
.get("/", (c) => c.text("Hello, world!"))
.post("/share_create", async (c) => {
const body = await c.req.json<{ sessionID: string }>()
const sessionID = body.sessionID
const short = SyncServer.shortName(sessionID)
const id = c.env.SYNC_SERVER.idFromName(short)
const stub = c.env.SYNC_SERVER.get(id)
const secret = await stub.share(sessionID)
return c.json({
secret,
url: `https://${c.env.WEB_DOMAIN}/s/${short}`,
})
})
.post("/share_delete", async (c) => {
const body = await c.req.json<{ sessionID: string; secret: string }>()
const sessionID = body.sessionID
const secret = body.secret
const id = c.env.SYNC_SERVER.idFromName(SyncServer.shortName(sessionID))
const stub = c.env.SYNC_SERVER.get(id)
await stub.assertSecret(secret)
await stub.clear()
return c.json({})
})
.post("/share_delete_admin", async (c) => {
const id = c.env.SYNC_SERVER.idFromName("oVF8Rsiv")
const stub = c.env.SYNC_SERVER.get(id)
await stub.clear()
return c.json({})
})
.post("/share_sync", async (c) => {
const body = await c.req.json<{
sessionID: string
secret: string
key: string
content: any
}>()
const name = SyncServer.shortName(body.sessionID)
const id = c.env.SYNC_SERVER.idFromName(name)
const stub = c.env.SYNC_SERVER.get(id)
await stub.assertSecret(body.secret)
await stub.publish(body.key, body.content)
return c.json({})
})
.get("/share_poll", async (c) => {
const upgradeHeader = c.req.header("Upgrade")
if (!upgradeHeader || upgradeHeader !== "websocket") {
return c.text("Error: Upgrade header is required", { status: 426 })
}
const id = c.req.query("id")
console.log("share_poll", id)
if (!id) return c.text("Error: Share ID is required", { status: 400 })
const stub = c.env.SYNC_SERVER.get(c.env.SYNC_SERVER.idFromName(id))
return stub.fetch(c.req.raw)
})
.get("/share_data", async (c) => {
const id = c.req.query("id")
console.log("share_data", id)
if (!id) return c.text("Error: Share ID is required", { status: 400 })
const stub = c.env.SYNC_SERVER.get(c.env.SYNC_SERVER.idFromName(id))
const data = await stub.getData()
if (request.method === "POST" && method === "share_create") {
const body = await request.json<any>()
const sessionID = body.sessionID
const short = SyncServer.shortName(sessionID)
const id = env.SYNC_SERVER.idFromName(short)
const stub = env.SYNC_SERVER.get(id)
const secret = await stub.share(sessionID)
return new Response(
JSON.stringify({
secret,
url: `https://${env.WEB_DOMAIN}/s/${short}`,
}),
{
headers: { "Content-Type": "application/json" },
},
)
}
if (request.method === "POST" && method === "share_delete") {
const body = await request.json<any>()
const sessionID = body.sessionID
const secret = body.secret
const id = env.SYNC_SERVER.idFromName(SyncServer.shortName(sessionID))
const stub = env.SYNC_SERVER.get(id)
await stub.assertSecret(secret)
await stub.clear()
return new Response(JSON.stringify({}), {
headers: { "Content-Type": "application/json" },
})
}
if (request.method === "POST" && method === "share_delete_admin") {
const id = env.SYNC_SERVER.idFromName("oVF8Rsiv")
const stub = env.SYNC_SERVER.get(id)
await stub.clear()
return new Response(JSON.stringify({}), {
headers: { "Content-Type": "application/json" },
})
}
if (request.method === "POST" && method === "share_sync") {
const body = await request.json<{
sessionID: string
secret: string
key: string
content: any
}>()
const name = SyncServer.shortName(body.sessionID)
const id = env.SYNC_SERVER.idFromName(name)
const stub = env.SYNC_SERVER.get(id)
await stub.assertSecret(body.secret)
await stub.publish(body.key, body.content)
return new Response(JSON.stringify({}), {
headers: { "Content-Type": "application/json" },
})
}
if (request.method === "GET" && method === "share_poll") {
const upgradeHeader = request.headers.get("Upgrade")
if (!upgradeHeader || upgradeHeader !== "websocket") {
return new Response("Error: Upgrade header is required", {
status: 426,
})
let info
const messages: Record<string, any> = {}
data.forEach((d) => {
const [root, type, ...splits] = d.key.split("/")
if (root !== "session") return
if (type === "info") {
info = d.content
return
}
const id = url.searchParams.get("id")
console.log("share_poll", id)
if (!id) return new Response("Error: Share ID is required", { status: 400 })
const stub = env.SYNC_SERVER.get(env.SYNC_SERVER.idFromName(id))
return stub.fetch(request)
}
if (type === "message") {
messages[d.content.id] = {
parts: [],
...d.content,
}
}
if (type === "part") {
messages[d.content.messageID].parts.push(d.content)
}
})
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()
return c.json({ info, messages })
})
/**
* Used by the GitHub action to get GitHub installation access token given the OIDC token
*/
.post("/exchange_github_app_token", async (c) => {
const EXPECTED_AUDIENCE = "opencode-github-action"
const GITHUB_ISSUER = "https://token.actions.githubusercontent.com"
const JWKS_URL = `${GITHUB_ISSUER}/.well-known/jwks`
let info
const messages: Record<string, any> = {}
data.forEach((d) => {
const [root, type, ...splits] = d.key.split("/")
if (root !== "session") return
if (type === "info") {
info = d.content
return
}
if (type === "message") {
messages[d.content.id] = {
parts: [],
...d.content,
}
}
if (type === "part") {
messages[d.content.messageID].parts.push(d.content)
}
// get Authorization header
const token = c.req.header("Authorization")?.replace(/^Bearer /, "")
if (!token) return c.json({ error: "Authorization header is required" }, { status: 401 })
// verify token
const JWKS = createRemoteJWKSet(new URL(JWKS_URL))
let owner, repo
try {
const { payload } = await jwtVerify(token, JWKS, {
issuer: GITHUB_ISSUER,
audience: EXPECTED_AUDIENCE,
})
return new Response(
JSON.stringify({
info,
messages,
}),
{
headers: { "Content-Type": "application/json" },
},
)
const sub = payload.sub // e.g. 'repo:my-org/my-repo:ref:refs/heads/main'
const parts = sub.split(":")[1].split("/")
owner = parts[0]
repo = parts[1]
} catch (err) {
console.error("Token verification failed:", err)
return c.json({ error: "Invalid or expired token" }, { status: 403 })
}
/**
* Used by the GitHub action to get GitHub installation access token given the OIDC token
*/
if (request.method === "POST" && method === "exchange_github_app_token") {
const EXPECTED_AUDIENCE = "opencode-github-action"
const GITHUB_ISSUER = "https://token.actions.githubusercontent.com"
const JWKS_URL = `${GITHUB_ISSUER}/.well-known/jwks`
// Create app JWT token
const auth = createAppAuth({
appId: Resource.GITHUB_APP_ID.value,
privateKey: Resource.GITHUB_APP_PRIVATE_KEY.value,
})
const appAuth = await auth({ type: "app" })
// Lookup installation
const octokit = new Octokit({ auth: appAuth.token })
const { data: installation } = await octokit.apps.getRepoInstallation({ owner, repo })
// Get installation token
const installationAuth = await auth({ type: "installation", installationId: installation.id })
return c.json({ token: installationAuth.token })
})
/**
* Used by the GitHub action to get GitHub installation access token given user PAT token (used when testing `opencode github run` locally)
*/
.post("/exchange_github_app_token_with_pat", async (c) => {
const body = await c.req.json<{ owner: string; repo: string }>()
const owner = body.owner
const repo = body.repo
try {
// get Authorization header
const authHeader = request.headers.get("Authorization")
const authHeader = c.req.header("Authorization")
const token = authHeader?.replace(/^Bearer /, "")
if (!token)
return new Response(JSON.stringify({ error: "Authorization header is required" }), {
status: 401,
headers: { "Content-Type": "application/json" },
})
if (!token) throw new Error("Authorization header is required")
// verify token
const JWKS = createRemoteJWKSet(new URL(JWKS_URL))
let owner, repo
try {
const { payload } = await jwtVerify(token, JWKS, {
issuer: GITHUB_ISSUER,
audience: EXPECTED_AUDIENCE,
})
const sub = payload.sub // e.g. 'repo:my-org/my-repo:ref:refs/heads/main'
const parts = sub.split(":")[1].split("/")
owner = parts[0]
repo = parts[1]
} catch (err) {
console.error("Token verification failed:", err)
return new Response(JSON.stringify({ error: "Invalid or expired token" }), {
status: 403,
headers: { "Content-Type": "application/json" },
})
}
// Verify permissions
const userClient = new Octokit({ auth: token })
const { data: repoData } = await userClient.repos.get({ owner, repo })
if (!repoData.permissions.admin && !repoData.permissions.push && !repoData.permissions.maintain)
throw new Error("User does not have write permissions")
// Create app JWT token
// Get installation token
const auth = createAppAuth({
appId: Resource.GITHUB_APP_ID.value,
privateKey: Resource.GITHUB_APP_PRIVATE_KEY.value,
@@ -277,49 +269,49 @@ export default {
const appAuth = await auth({ type: "app" })
// Lookup installation
const octokit = new Octokit({ auth: appAuth.token })
const { data: installation } = await octokit.apps.getRepoInstallation({ owner, repo })
const appClient = new Octokit({ auth: appAuth.token })
const { data: installation } = await appClient.apps.getRepoInstallation({ owner, repo })
// Get installation token
const installationAuth = await auth({ type: "installation", installationId: installation.id })
return new Response(JSON.stringify({ token: installationAuth.token }), {
headers: { "Content-Type": "application/json" },
})
}
/**
* Used by the opencode CLI to check if the GitHub app is installed
*/
if (request.method === "GET" && method === "get_github_app_installation") {
const owner = url.searchParams.get("owner")
const repo = url.searchParams.get("repo")
const auth = createAppAuth({
appId: Resource.GITHUB_APP_ID.value,
privateKey: Resource.GITHUB_APP_PRIVATE_KEY.value,
})
const appAuth = await auth({ type: "app" })
// Lookup installation
const octokit = new Octokit({ auth: appAuth.token })
let installation
try {
const ret = await octokit.apps.getRepoInstallation({ owner, repo })
installation = ret.data
} catch (err) {
if (err instanceof Error && err.message.includes("Not Found")) {
// not installed
} else {
throw err
}
return c.json({ token: installationAuth.token })
} catch (e: any) {
let error = e
if (e instanceof Error) {
error = e.message
}
return new Response(JSON.stringify({ installation }), {
headers: { "Content-Type": "application/json" },
})
return c.json({ error }, { status: 401 })
}
})
/**
* Used by the opencode CLI to check if the GitHub app is installed
*/
.get("/get_github_app_installation", async (c) => {
const owner = c.req.query("owner")
const repo = c.req.query("repo")
const auth = createAppAuth({
appId: Resource.GITHUB_APP_ID.value,
privateKey: Resource.GITHUB_APP_PRIVATE_KEY.value,
})
const appAuth = await auth({ type: "app" })
// Lookup installation
const octokit = new Octokit({ auth: appAuth.token })
let installation
try {
const ret = await octokit.apps.getRepoInstallation({ owner, repo })
installation = ret.data
} catch (err) {
if (err instanceof Error && err.message.includes("Not Found")) {
// not installed
} else {
throw err
}
}
return new Response("Not Found", { status: 404 })
},
}
return c.json({ installation })
})
.all("*", (c) => c.text("Not Found"))
+9 -1
View File
@@ -17,6 +17,7 @@
"devDependencies": {
"@ai-sdk/amazon-bedrock": "2.2.10",
"@ai-sdk/anthropic": "1.2.12",
"@octokit/webhooks-types": "7.6.1",
"@standard-schema/spec": "1.0.0",
"@tsconfig/bun": "1.0.7",
"@types/bun": "latest",
@@ -27,14 +28,21 @@
"zod-to-json-schema": "3.24.5"
},
"dependencies": {
"@actions/core": "1.11.1",
"@actions/github": "6.0.1",
"@clack/prompts": "0.11.0",
"@hono/zod-validator": "0.4.2",
"@modelcontextprotocol/sdk": "1.15.1",
"@octokit/graphql": "9.0.1",
"@octokit/rest": "22.0.0",
"@openauthjs/openauth": "0.4.3",
"@standard-schema/spec": "1.0.0",
"@zip.js/zip.js": "2.7.62",
"ai": "catalog:",
"decimal.js": "10.5.0",
"diff": "8.0.2",
"hono": "4.7.10",
"gray-matter": "4.0.3",
"hono": "catalog:",
"hono-openapi": "0.4.8",
"isomorphic-git": "1.32.1",
"open": "10.1.2",
+11 -3
View File
@@ -22,11 +22,13 @@ console.log(`publishing ${version}`)
const GOARCH: Record<string, string> = {
arm64: "arm64",
x64: "amd64",
"x64-baseline": "amd64",
}
const targets = [
["linux", "arm64"],
["linux", "x64"],
["linux", "x64-baseline"],
["darwin", "x64"],
["darwin", "arm64"],
["windows", "x64"],
@@ -90,16 +92,22 @@ if (!snapshot) {
}
const previous = await fetch("https://api.github.com/repos/sst/opencode/releases/latest")
.then((res) => res.json())
.then((res) => {
if (!res.ok) throw new Error(res.statusText)
return res.json()
})
.then((data) => data.tag_name)
console.log("finding commits between", previous, "and", "HEAD")
const commits = await fetch(`https://api.github.com/repos/sst/opencode/compare/${previous}...HEAD`)
.then((res) => res.json())
.then((data) => data.commits || [])
const raw = commits.map((commit: any) => `- ${commit.commit.message.split("\n").join(" ")}`)
console.log(raw)
const notes =
commits
.map((commit: any) => `- ${commit.commit.message.split("\n")[0]}`)
raw
.filter((x: string) => {
const lower = x.toLowerCase()
return (
+102
View File
@@ -0,0 +1,102 @@
import { App } from "../app/app"
import { Config } from "../config/config"
import z from "zod"
import { Provider } from "../provider/provider"
import { generateObject, type ModelMessage } from "ai"
import PROMPT_GENERATE from "./generate.txt"
import { SystemPrompt } from "../session/system"
export namespace Agent {
export const Info = z
.object({
name: z.string(),
model: z
.object({
modelID: z.string(),
providerID: z.string(),
})
.optional(),
description: z.string(),
prompt: z.string().optional(),
tools: z.record(z.boolean()),
})
.openapi({
ref: "Agent",
})
export type Info = z.infer<typeof Info>
const state = App.state("agent", async () => {
const cfg = await Config.get()
const result: Record<string, Info> = {
general: {
name: "general",
description:
"General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you.",
tools: {
todoread: false,
todowrite: false,
},
},
}
for (const [key, value] of Object.entries(cfg.agent ?? {})) {
if (value.disable) continue
let item = result[key]
if (!item)
item = result[key] = {
name: key,
description: "",
tools: {
todowrite: false,
todoread: false,
},
}
const model = value.model ?? cfg.model
if (model) item.model = Provider.parseModel(model)
if (value.prompt) item.prompt = value.prompt
if (value.tools)
item.tools = {
...item.tools,
...value.tools,
}
if (value.description) item.description = value.description
}
return result
})
export async function get(agent: string) {
return state().then((x) => x[agent])
}
export async function list() {
return state().then((x) => Object.values(x))
}
export async function generate(input: { description: string }) {
const defaultModel = await Provider.defaultModel()
const model = await Provider.getModel(defaultModel.providerID, defaultModel.modelID)
const system = SystemPrompt.header(defaultModel.providerID)
system.push(PROMPT_GENERATE)
const existing = await list()
const result = await generateObject({
temperature: 0.3,
prompt: [
...system.map(
(item): ModelMessage => ({
role: "system",
content: item,
}),
),
{
role: "user",
content: `Create an agent configuration based on this request: \"${input.description}\".\n\nIMPORTANT: The following identifiers already exist and must NOT be used: ${existing.map((i) => i.name).join(", ")}\n Return ONLY the JSON object, no other text, do not wrap in backticks`,
},
],
model: model.language,
schema: z.object({
identifier: z.string(),
whenToUse: z.string(),
systemPrompt: z.string(),
}),
})
return result.object
}
}
+75
View File
@@ -0,0 +1,75 @@
You are an elite AI agent architect specializing in crafting high-performance agent configurations. Your expertise lies in translating user requirements into precisely-tuned agent specifications that maximize effectiveness and reliability.
**Important Context**: You may have access to project-specific instructions from CLAUDE.md files and other context that may include coding standards, project structure, and custom requirements. Consider this context when creating agents to ensure they align with the project's established patterns and practices.
When a user describes what they want an agent to do, you will:
1. **Extract Core Intent**: Identify the fundamental purpose, key responsibilities, and success criteria for the agent. Look for both explicit requirements and implicit needs. Consider any project-specific context from CLAUDE.md files. For agents that are meant to review code, you should assume that the user is asking to review recently written code and not the whole codebase, unless the user has explicitly instructed you otherwise.
2. **Design Expert Persona**: Create a compelling expert identity that embodies deep domain knowledge relevant to the task. The persona should inspire confidence and guide the agent's decision-making approach.
3. **Architect Comprehensive Instructions**: Develop a system prompt that:
- Establishes clear behavioral boundaries and operational parameters
- Provides specific methodologies and best practices for task execution
- Anticipates edge cases and provides guidance for handling them
- Incorporates any specific requirements or preferences mentioned by the user
- Defines output format expectations when relevant
- Aligns with project-specific coding standards and patterns from CLAUDE.md
4. **Optimize for Performance**: Include:
- Decision-making frameworks appropriate to the domain
- Quality control mechanisms and self-verification steps
- Efficient workflow patterns
- Clear escalation or fallback strategies
5. **Create Identifier**: Design a concise, descriptive identifier that:
- Uses lowercase letters, numbers, and hyphens only
- Is typically 2-4 words joined by hyphens
- Clearly indicates the agent's primary function
- Is memorable and easy to type
- Avoids generic terms like "helper" or "assistant"
6 **Example agent descriptions**:
- in the 'whenToUse' field of the JSON object, you should include examples of when this agent should be used.
- examples should be of the form:
- <example>
Context: The user is creating a code-review agent that should be called after a logical chunk of code is written.
user: "Please write a function that checks if a number is prime"
assistant: "Here is the relevant function: "
<function call omitted for brevity only for this example>
<commentary>
Since the user is greeting, use the Task tool to launch the greeting-responder agent to respond with a friendly joke.
</commentary>
assistant: "Now let me use the code-reviewer agent to review the code"
</example>
- <example>
Context: User is creating an agent to respond to the word "hello" with a friendly jok.
user: "Hello"
assistant: "I'm going to use the Task tool to launch the greeting-responder agent to respond with a friendly joke"
<commentary>
Since the user is greeting, use the greeting-responder agent to respond with a friendly joke.
</commentary>
</example>
- If the user mentioned or implied that the agent should be used proactively, you should include examples of this.
- NOTE: Ensure that in the examples, you are making the assistant use the Agent tool and not simply respond directly to the task.
Your output must be a valid JSON object with exactly these fields:
{
"identifier": "A unique, descriptive identifier using lowercase letters, numbers, and hyphens (e.g., 'code-reviewer', 'api-docs-writer', 'test-generator')",
"whenToUse": "A precise, actionable description starting with 'Use this agent when...' that clearly defines the triggering conditions and use cases. Ensure you include examples as described above.",
"systemPrompt": "The complete system prompt that will govern the agent's behavior, written in second person ('You are...', 'You will...') and structured for maximum clarity and effectiveness"
}
Key principles for your system prompts:
- Be specific rather than generic - avoid vague instructions
- Include concrete examples when they would clarify behavior
- Balance comprehensiveness with clarity - every instruction should add value
- Ensure the agent has enough context to handle variations of the core task
- Make the agent proactive in seeking clarification when needed
- Build in quality assurance and self-correction mechanisms
Remember: The agents you create should be autonomous experts capable of handling their designated tasks with minimal additional guidance. Your system prompts are their complete operational manual.
+7 -1
View File
@@ -16,7 +16,13 @@ export namespace Auth {
key: z.string(),
})
export const Info = z.discriminatedUnion("type", [Oauth, Api])
export const WellKnown = z.object({
type: z.literal("wellknown"),
key: z.string(),
token: z.string(),
})
export const Info = z.discriminatedUnion("type", [Oauth, Api, WellKnown])
export type Info = z.infer<typeof Info>
const filepath = path.join(Global.Path.data, "auth.json")
+2
View File
@@ -3,6 +3,7 @@ import { ConfigHooks } from "../config/hooks"
import { Format } from "../format"
import { LSP } from "../lsp"
import { Share } from "../share/share"
import { Snapshot } from "../snapshot"
export async function bootstrap<T>(input: App.Input, cb: (app: App.Info) => Promise<T>) {
return App.provide(input, async (app) => {
@@ -10,6 +11,7 @@ export async function bootstrap<T>(input: App.Input, cb: (app: App.Info) => Prom
Format.init()
ConfigHooks.init()
LSP.init()
Snapshot.init()
return cb(app)
})
+110
View File
@@ -0,0 +1,110 @@
import { cmd } from "./cmd"
import * as prompts from "@clack/prompts"
import { UI } from "../ui"
import { Global } from "../../global"
import { Agent } from "../../agent/agent"
import path from "path"
import matter from "gray-matter"
import { App } from "../../app/app"
const AgentCreateCommand = cmd({
command: "create",
describe: "create a new agent",
async handler() {
await App.provide({ cwd: process.cwd() }, async (app) => {
UI.empty()
prompts.intro("Create agent")
let scope: "global" | "project" = "global"
if (app.git) {
const scopeResult = await prompts.select({
message: "Location",
options: [
{
label: "Current project",
value: "project" as const,
hint: app.path.root,
},
{
label: "Global",
value: "global" as const,
hint: Global.Path.config,
},
],
})
if (prompts.isCancel(scopeResult)) throw new UI.CancelledError()
scope = scopeResult
}
const query = await prompts.text({
message: "Description",
placeholder: "What should this agent do?",
validate: (x) => (x.length > 0 ? undefined : "Required"),
})
if (prompts.isCancel(query)) throw new UI.CancelledError()
const spinner = prompts.spinner()
spinner.start("Generating agent configuration...")
const generated = await Agent.generate({ description: query })
spinner.stop(`Agent ${generated.identifier} generated`)
const availableTools = [
"bash",
"read",
"write",
"edit",
"list",
"glob",
"grep",
"webfetch",
"task",
"todowrite",
"todoread",
]
const selectedTools = await prompts.multiselect({
message: "Select tools to enable",
options: availableTools.map((tool) => ({
label: tool,
value: tool,
})),
initialValues: availableTools,
})
if (prompts.isCancel(selectedTools)) throw new UI.CancelledError()
const tools: Record<string, boolean> = {}
for (const tool of availableTools) {
if (!selectedTools.includes(tool)) {
tools[tool] = false
}
}
const frontmatter: any = {
description: generated.whenToUse,
}
if (Object.keys(tools).length > 0) {
frontmatter.tools = tools
}
const content = matter.stringify(generated.systemPrompt, frontmatter)
const filePath = path.join(
scope === "global" ? Global.Path.config : path.join(app.path.root, ".opencode"),
`agent`,
`${generated.identifier}.md`,
)
await Bun.write(filePath, content)
prompts.log.success(`Agent created: ${filePath}`)
prompts.outro("Done")
})
},
})
export const AgentCommand = cmd({
command: "agent",
describe: "manage agents",
builder: (yargs) => yargs.command(AgentCreateCommand).demandCommand(),
async handler() {},
})
+32 -4
View File
@@ -61,17 +61,45 @@ export const AuthListCommand = cmd({
prompts.log.info(`${provider} ${UI.Style.TEXT_DIM}${envVar}`)
}
prompts.outro(`${activeEnvVars.length} environment variables`)
prompts.outro(`${activeEnvVars.length} environment variable` + (activeEnvVars.length === 1 ? "" : "s"))
}
},
})
export const AuthLoginCommand = cmd({
command: "login",
command: "login [url]",
describe: "log in to a provider",
async handler() {
UI.empty()
builder: (yargs) =>
yargs.positional("url", {
describe: "opencode auth provider",
type: "string",
}),
async handler(args) {
prompts.intro("Add credential")
if (args.url) {
const wellknown = await fetch(`${args.url}/.well-known/opencode`).then((x) => x.json())
prompts.log.info(`Running \`${wellknown.auth.command.join(" ")}\``)
const proc = Bun.spawn({
cmd: wellknown.auth.command,
stdout: "pipe",
})
const exit = await proc.exited
if (exit !== 0) {
prompts.log.error("Failed")
prompts.outro("Done")
return
}
const token = await new Response(proc.stdout).text()
await Auth.set(args.url, {
type: "wellknown",
key: wellknown.auth.env,
token: token.trim(),
})
prompts.log.success("Logged into " + args.url)
prompts.outro("Done")
return
}
UI.empty()
const providers = await ModelsDev.get()
const priority: Record<string, number> = {
anthropic: 0,
@@ -1,3 +1,4 @@
import { Global } from "../../../global"
import { bootstrap } from "../../bootstrap"
import { cmd } from "../cmd"
import { FileCommand } from "./file"
@@ -15,6 +16,7 @@ export const DebugCommand = cmd({
.command(FileCommand)
.command(ScrapCommand)
.command(SnapshotCommand)
.command(PathsCommand)
.command({
command: "wait",
async handler() {
@@ -26,3 +28,12 @@ export const DebugCommand = cmd({
.demandCommand(),
async handler() {},
})
const PathsCommand = cmd({
command: "paths",
handler() {
for (const [key, value] of Object.entries(Global.Path)) {
console.log(key.padEnd(10), value)
}
},
})
+14 -18
View File
@@ -4,49 +4,45 @@ import { cmd } from "../cmd"
export const SnapshotCommand = cmd({
command: "snapshot",
builder: (yargs) => yargs.command(CreateCommand).command(RestoreCommand).command(DiffCommand).demandCommand(),
builder: (yargs) => yargs.command(TrackCommand).command(PatchCommand).command(DiffCommand).demandCommand(),
async handler() {},
})
const CreateCommand = cmd({
command: "create",
const TrackCommand = cmd({
command: "track",
async handler() {
await bootstrap({ cwd: process.cwd() }, async () => {
const result = await Snapshot.create("test")
console.log(result)
console.log(await Snapshot.track())
})
},
})
const RestoreCommand = cmd({
command: "restore <commit>",
const PatchCommand = cmd({
command: "patch <hash>",
builder: (yargs) =>
yargs.positional("commit", {
yargs.positional("hash", {
type: "string",
description: "commit",
description: "hash",
demandOption: true,
}),
async handler(args) {
await bootstrap({ cwd: process.cwd() }, async () => {
await Snapshot.restore("test", args.commit)
console.log("restored")
console.log(await Snapshot.patch(args.hash))
})
},
})
export const DiffCommand = cmd({
command: "diff <commit>",
describe: "diff",
const DiffCommand = cmd({
command: "diff <hash>",
builder: (yargs) =>
yargs.positional("commit", {
yargs.positional("hash", {
type: "string",
description: "commit",
description: "hash",
demandOption: true,
}),
async handler(args) {
await bootstrap({ cwd: process.cwd() }, async () => {
const diff = await Snapshot.diff("test", args.commit)
console.log(diff)
console.log(await Snapshot.diff(args.hash))
})
},
})
+1 -6
View File
@@ -1,15 +1,10 @@
import { Server } from "../../server/server"
import fs from "fs/promises"
import path from "path"
import type { CommandModule } from "yargs"
export const GenerateCommand = {
command: "generate",
handler: async () => {
const specs = await Server.openapi()
const dir = "gen"
await fs.rmdir(dir, { recursive: true }).catch(() => {})
await fs.mkdir(dir, { recursive: true })
await Bun.write(path.join(dir, "openapi.json"), JSON.stringify(specs, null, 2))
process.stdout.write(JSON.stringify(specs, null, 2))
},
} satisfies CommandModule
File diff suppressed because it is too large Load Diff
@@ -1,221 +0,0 @@
import { $ } from "bun"
import path from "path"
import { exec } from "child_process"
import * as prompts from "@clack/prompts"
import { map, pipe, sortBy, values } from "remeda"
import { UI } from "../ui"
import { cmd } from "./cmd"
import { ModelsDev } from "../../provider/models"
import { App } from "../../app/app"
const WORKFLOW_FILE = ".github/workflows/opencode.yml"
export const InstallGithubCommand = cmd({
command: "install-github",
describe: "install the GitHub agent",
async handler() {
await App.provide({ cwd: process.cwd() }, async () => {
UI.empty()
prompts.intro("Install GitHub agent")
const app = await getAppInfo()
await installGitHubApp()
const providers = await ModelsDev.get()
const provider = await promptProvider()
const model = await promptModel()
//const key = await promptKey()
await addWorkflowFiles()
printNextSteps()
function printNextSteps() {
let step2
if (provider === "amazon-bedrock") {
step2 =
"Configure OIDC in AWS - https://docs.github.com/en/actions/how-tos/security-for-github-actions/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services"
} else {
const url = `https://github.com/organizations/${app.owner}/settings/secrets/actions`
const env = providers[provider].env
const envStr =
env.length === 1
? `\`${env[0]}\` secret`
: `\`${[env.slice(0, -1).join("\`, \`"), ...env.slice(-1)].join("\` and \`")}\` secrets`
step2 = `Add ${envStr} for ${providers[provider].name} - ${url}`
}
prompts.outro(
[
"Next steps:",
` 1. Commit "${WORKFLOW_FILE}" file and push`,
` 2. ${step2}`,
" 3. Learn how to use the GitHub agent - https://docs.opencode.ai/docs/github/getting-started",
].join("\n"),
)
}
async function getAppInfo() {
const app = App.info()
if (!app.git) {
prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
throw new UI.CancelledError()
}
// Get repo info
const info = await $`git remote get-url origin`.quiet().nothrow().text()
// match https or git pattern
// ie. https://github.com/sst/opencode.git
// ie. git@github.com:sst/opencode.git
const parsed = info.match(/git@github\.com:(.*)\.git/) ?? info.match(/github\.com\/(.*)\.git/)
if (!parsed) {
prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
throw new UI.CancelledError()
}
const [owner, repo] = parsed[1].split("/")
return { owner, repo, root: app.path.root }
}
async function promptProvider() {
const priority: Record<string, number> = {
anthropic: 0,
"github-copilot": 1,
openai: 2,
google: 3,
}
let provider = await prompts.select({
message: "Select provider",
maxItems: 8,
options: pipe(
providers,
values(),
sortBy(
(x) => priority[x.id] ?? 99,
(x) => x.name ?? x.id,
),
map((x) => ({
label: x.name,
value: x.id,
hint: priority[x.id] === 0 ? "recommended" : undefined,
})),
),
})
if (prompts.isCancel(provider)) throw new UI.CancelledError()
return provider
}
async function promptModel() {
const providerData = providers[provider]!
const model = await prompts.select({
message: "Select model",
maxItems: 8,
options: pipe(
providerData.models,
values(),
sortBy((x) => x.name ?? x.id),
map((x) => ({
label: x.name ?? x.id,
value: x.id,
})),
),
})
if (prompts.isCancel(model)) throw new UI.CancelledError()
return model
}
async function installGitHubApp() {
const s = prompts.spinner()
s.start("Installing GitHub app")
// Get installation
const installation = await getInstallation()
if (installation) return s.stop("GitHub app already installed")
// Open browser
const url = "https://github.com/apps/opencode-agent"
const command =
process.platform === "darwin"
? `open "${url}"`
: process.platform === "win32"
? `start "${url}"`
: `xdg-open "${url}"`
exec(command, (error) => {
if (error) {
prompts.log.warn(`Could not open browser. Please visit: ${url}`)
}
})
// Wait for installation
s.message("Waiting for GitHub app to be installed")
const MAX_RETRIES = 60
let retries = 0
do {
const installation = await getInstallation()
if (installation) break
if (retries > MAX_RETRIES) {
s.stop(
`Failed to detect GitHub app installation. Make sure to install the app for the \`${app.owner}/${app.repo}\` repository.`,
)
throw new UI.CancelledError()
}
retries++
await new Promise((resolve) => setTimeout(resolve, 1000))
} while (true)
s.stop("Installed GitHub app")
async function getInstallation() {
return await fetch(`https://api.opencode.ai/get_github_app_installation?owner=${app.owner}&repo=${app.repo}`)
.then((res) => res.json())
.then((data) => data.installation)
}
}
async function addWorkflowFiles() {
const envStr =
provider === "amazon-bedrock"
? ""
: `\n env:${providers[provider].env.map((e) => `\n ${e}: \${{ secrets.${e} }}`).join("")}`
await Bun.write(
path.join(app.root, WORKFLOW_FILE),
`
name: opencode
on:
issue_comment:
types: [created]
jobs:
opencode:
if: |
startsWith(github.event.comment.body, 'opencode') ||
startsWith(github.event.comment.body, 'hi opencode') ||
startsWith(github.event.comment.body, 'hey opencode') ||
contains(github.event.comment.body, '@opencode-agent')
runs-on: ubuntu-latest
permissions:
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run opencode
uses: sst/opencode/sdks/github@github-v1${envStr}
with:
model: ${provider}/${model}
`.trim(),
)
prompts.log.success(`Added workflow file: "${WORKFLOW_FILE}"`)
}
})
},
})
-2
View File
@@ -1,6 +1,5 @@
import { Provider } from "../../provider/provider"
import { Server } from "../../server/server"
import { Share } from "../../share/share"
import { bootstrap } from "../bootstrap"
import { cmd } from "./cmd"
@@ -32,7 +31,6 @@ export const ServeCommand = cmd({
const hostname = args.hostname
const port = args.port
await Share.init()
const server = Server.listen({
port,
hostname,
+24 -6
View File
@@ -12,6 +12,7 @@ import { Bus } from "../../bus"
import { Log } from "../../util/log"
import { FileWatcher } from "../../file/watch"
import { Mode } from "../../session/mode"
import { Ide } from "../../ide"
export const TuiCommand = cmd({
command: "$0 [project]",
@@ -35,6 +36,17 @@ export const TuiCommand = cmd({
.option("mode", {
type: "string",
describe: "mode to use",
})
.option("port", {
type: "number",
describe: "port to listen on",
default: 0,
})
.option("hostname", {
alias: ["h"],
type: "string",
describe: "hostname to listen on",
default: "127.0.0.1",
}),
handler: async (args) => {
while (true) {
@@ -53,8 +65,8 @@ export const TuiCommand = cmd({
}
const server = Server.listen({
port: 0,
hostname: "127.0.0.1",
port: args.port,
hostname: args.hostname,
})
let cmd = ["go", "run", "./main.go"]
@@ -101,7 +113,7 @@ export const TuiCommand = cmd({
})
;(async () => {
if (Installation.VERSION === "dev") return
if (Installation.isDev()) return
if (Installation.isSnapshot()) return
const config = await Config.global()
if (config.autoupdate === false) return
@@ -111,9 +123,15 @@ export const TuiCommand = cmd({
const method = await Installation.method()
if (method === "unknown") return
await Installation.upgrade(method, latest)
.then(() => {
Bus.publish(Installation.Event.Updated, { version: latest })
})
.then(() => Bus.publish(Installation.Event.Updated, { version: latest }))
.catch(() => {})
})()
;(async () => {
if (Ide.alreadyInstalled()) return
const ide = Ide.ide()
if (ide === "unknown") return
await Ide.install(ide)
.then(() => Bus.publish(Ide.Event.Installed, { ide }))
.catch(() => {})
})()
+80 -6
View File
@@ -9,11 +9,15 @@ import { Global } from "../global"
import fs from "fs/promises"
import { lazy } from "../util/lazy"
import { NamedError } from "../util/error"
import matter from "gray-matter"
import { Flag } from "../flag/flag"
import { Auth } from "../auth"
export namespace Config {
const log = Log.create({ service: "config" })
export const state = App.state("config", async (app) => {
const auth = await Auth.all()
let result = await global()
for (const file of ["opencode.jsonc", "opencode.json"]) {
const found = await Filesystem.findUp(file, app.path.cwd, app.path.root)
@@ -22,10 +26,52 @@ export namespace Config {
}
}
// Override with custom config if provided
if (Flag.OPENCODE_CONFIG) {
result = mergeDeep(result, await load(Flag.OPENCODE_CONFIG))
log.debug("loaded custom config", { path: Flag.OPENCODE_CONFIG })
}
for (const [key, value] of Object.entries(auth)) {
if (value.type === "wellknown") {
process.env[value.key] = value.token
const wellknown = await fetch(`${key}/.well-known/opencode`).then((x) => x.json())
result = mergeDeep(result, await loadRaw(JSON.stringify(wellknown.config ?? {}), process.cwd()))
}
}
result.agent = result.agent || {}
const markdownAgents = [
...(await Filesystem.globUp("agent/*.md", Global.Path.config, Global.Path.config)),
...(await Filesystem.globUp(".opencode/agent/*.md", app.path.cwd, app.path.root)),
]
for (const item of markdownAgents) {
const content = await Bun.file(item).text()
const md = matter(content)
if (!md.data) continue
const config = {
name: path.basename(item, ".md"),
...md.data,
prompt: md.content.trim(),
}
const parsed = Agent.safeParse(config)
if (parsed.success) {
result.agent = mergeDeep(result.agent, {
[config.name]: parsed.data,
})
continue
}
throw new InvalidError({ path: item }, { cause: parsed.error })
}
// Handle migration from autoshare to share field
if (result.autoshare === true && !result.share) {
result.share = "auto"
}
if (result.keybinds?.messages_revert && !result.keybinds.messages_undo) {
result.keybinds.messages_undo = result.keybinds.messages_revert
}
if (!result.username) {
const os = await import("os")
@@ -70,14 +116,22 @@ export namespace Config {
export const Mode = z
.object({
model: z.string().optional(),
temperature: z.number().optional(),
prompt: z.string().optional(),
tools: z.record(z.string(), z.boolean()).optional(),
disable: z.boolean().optional(),
})
.openapi({
ref: "ModeConfig",
})
export type Mode = z.infer<typeof Mode>
export const Agent = Mode.extend({
description: z.string(),
}).openapi({
ref: "AgentConfig",
})
export const Keybinds = z
.object({
leader: z.string().optional().default("ctrl+x").describe("Leader key for keybind combinations"),
@@ -89,7 +143,7 @@ export namespace Config {
session_new: z.string().optional().default("<leader>n").describe("Create a new session"),
session_list: z.string().optional().default("<leader>l").describe("List all sessions"),
session_share: z.string().optional().default("<leader>s").describe("Share current session"),
session_unshare: z.string().optional().default("<leader>u").describe("Unshare current session"),
session_unshare: z.string().optional().default("none").describe("Unshare current session"),
session_interrupt: z.string().optional().default("esc").describe("Interrupt current session"),
session_compact: z.string().optional().default("<leader>c").describe("Compact the session"),
tool_details: z.string().optional().default("<leader>d").describe("Toggle tool details"),
@@ -118,7 +172,9 @@ export namespace Config {
messages_last: z.string().optional().default("ctrl+alt+g").describe("Navigate to last message"),
messages_layout_toggle: z.string().optional().default("<leader>p").describe("Toggle layout"),
messages_copy: z.string().optional().default("<leader>y").describe("Copy message"),
messages_revert: z.string().optional().default("<leader>r").describe("Revert message"),
messages_revert: z.string().optional().default("none").describe("@deprecated use messages_undo. Revert message"),
messages_undo: z.string().optional().default("<leader>u").describe("Undo message"),
messages_redo: z.string().optional().default("<leader>r").describe("Redo message"),
app_exit: z.string().optional().default("ctrl+c,<leader>q").describe("Exit the application"),
})
.strict()
@@ -167,12 +223,27 @@ export namespace Config {
.catchall(Mode)
.optional()
.describe("Modes configuration, see https://opencode.ai/docs/modes"),
agent: z
.object({
general: Agent.optional(),
})
.catchall(Agent)
.optional()
.describe("Modes configuration, see https://opencode.ai/docs/modes"),
provider: z
.record(
ModelsDev.Provider.partial().extend({
models: z.record(ModelsDev.Model.partial()),
options: z.record(z.any()).optional(),
}),
ModelsDev.Provider.partial()
.extend({
models: z.record(ModelsDev.Model.partial()),
options: z
.object({
apiKey: z.string().optional(),
baseURL: z.string().optional(),
})
.catchall(z.any())
.optional(),
})
.strict(),
)
.optional()
.describe("Custom provider configurations and model overrides"),
@@ -246,7 +317,10 @@ export namespace Config {
throw new JsonError({ path: configPath }, { cause: err })
})
if (!text) return {}
return loadRaw(text, configPath)
}
async function loadRaw(text: string, configPath: string) {
text = text.replace(/\{env:([^}]+)\}/g, (_, varName) => {
return process.env[varName] || ""
})
+36 -14
View File
@@ -5,6 +5,7 @@ import { z } from "zod"
import { NamedError } from "../util/error"
import { lazy } from "../util/lazy"
import { Log } from "../util/log"
import { ZipReader, BlobReader, BlobWriter } from "@zip.js/zip.js"
export namespace Fzf {
const log = Log.create({ service: "fzf" })
@@ -45,7 +46,10 @@ export namespace Fzf {
log.info("found", { filepath })
return { filepath }
}
filepath = path.join(Global.Path.bin, "fzf" + (process.platform === "win32" ? ".exe" : ""))
filepath = path.join(
Global.Path.bin,
"fzf" + (process.platform === "win32" ? ".exe" : ""),
)
const file = Bun.file(filepath)
if (!(await file.exists())) {
@@ -53,15 +57,18 @@ export namespace Fzf {
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 })
if (!config)
throw new UnsupportedPlatformError({ platform: process.platform })
const version = VERSION
const platformName = process.platform === "win32" ? "windows" : process.platform
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 })
if (!response.ok)
throw new DownloadFailedError({ url, status: response.status })
const buffer = await response.arrayBuffer()
const archivePath = path.join(Global.Path.bin, filename)
@@ -80,17 +87,32 @@ export namespace Fzf {
})
}
if (config.extension === "zip") {
const proc = Bun.spawn(["unzip", "-j", archivePath, "fzf.exe", "-d", Global.Path.bin], {
cwd: Global.Path.bin,
stderr: "pipe",
stdout: "ignore",
})
await proc.exited
if (proc.exitCode !== 0)
const zipFileReader = new ZipReader(new BlobReader(new Blob([await Bun.file(archivePath).arrayBuffer()])));
const entries = await zipFileReader.getEntries();
let fzfEntry: any;
for (const entry of entries) {
if (entry.filename === "fzf.exe") {
fzfEntry = entry;
break;
}
}
if (!fzfEntry) {
throw new ExtractionFailedError({
filepath: archivePath,
stderr: await Bun.readableStreamToText(proc.stderr),
})
stderr: "fzf.exe not found in zip archive",
});
}
const fzfBlob = await fzfEntry.getData(new BlobWriter());
if (!fzfBlob) {
throw new ExtractionFailedError({
filepath: archivePath,
stderr: "Failed to extract fzf.exe from zip archive",
});
}
await Bun.write(filepath, await fzfBlob.arrayBuffer());
await zipFileReader.close();
}
await fs.unlink(archivePath)
if (process.platform !== "win32") await fs.chmod(filepath, 0o755)
@@ -105,4 +127,4 @@ export namespace Fzf {
const { filepath } = await state()
return filepath
}
}
}
+48 -31
View File
@@ -7,6 +7,7 @@ import { NamedError } from "../util/error"
import { lazy } from "../util/lazy"
import { $ } from "bun"
import { Fzf } from "./fzf"
import { ZipReader, BlobReader, BlobWriter } from "@zip.js/zip.js"
export namespace Ripgrep {
const Stats = z.object({
@@ -34,27 +35,25 @@ export namespace Ripgrep {
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(),
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(),
}),
),
})
.openapi({ ref: "Match" }),
start: z.number(),
end: z.number(),
}),
),
}),
})
const End = z.object({
@@ -161,17 +160,34 @@ export namespace Ripgrep {
})
}
if (config.extension === "zip") {
const proc = Bun.spawn(["unzip", "-j", archivePath, "*/rg.exe", "-d", Global.Path.bin], {
cwd: Global.Path.bin,
stderr: "pipe",
stdout: "ignore",
})
await proc.exited
if (proc.exitCode !== 0)
throw new ExtractionFailedError({
filepath: archivePath,
stderr: await Bun.readableStreamToText(proc.stderr),
})
if (config.extension === "zip") {
const zipFileReader = new ZipReader(new BlobReader(new Blob([await Bun.file(archivePath).arrayBuffer()])))
const entries = await zipFileReader.getEntries()
let rgEntry: any
for (const entry of entries) {
if (entry.filename.endsWith("rg.exe")) {
rgEntry = entry
break
}
}
if (!rgEntry) {
throw new ExtractionFailedError({
filepath: archivePath,
stderr: "rg.exe not found in zip archive",
})
}
const rgBlob = await rgEntry.getData(new BlobWriter())
if (!rgBlob) {
throw new ExtractionFailedError({
filepath: archivePath,
stderr: "Failed to extract rg.exe from zip archive",
})
}
await Bun.write(filepath, await rgBlob.arrayBuffer())
await zipFileReader.close()
}
}
await fs.unlink(archivePath)
if (!platformKey.endsWith("-win32")) await fs.chmod(filepath, 0o755)
@@ -233,6 +249,7 @@ export namespace Ripgrep {
children: [],
}
for (const file of files) {
if (file.includes(".opencode")) continue
const parts = file.split(path.sep)
getPath(root, parts, true)
}
+1
View File
@@ -1,6 +1,7 @@
export namespace Flag {
export const OPENCODE_AUTO_SHARE = truthy("OPENCODE_AUTO_SHARE")
export const OPENCODE_DISABLE_WATCHER = truthy("OPENCODE_DISABLE_WATCHER")
export const OPENCODE_CONFIG = process.env["OPENCODE_CONFIG"]
function truthy(key: string) {
const value = process.env[key]?.toLowerCase()
-2
View File
@@ -13,7 +13,6 @@ export namespace Global {
export const Path = {
data,
bin: path.join(data, "bin"),
providers: path.join(config, "providers"),
cache,
config,
state,
@@ -23,7 +22,6 @@ export namespace Global {
await Promise.all([
fs.mkdir(Global.Path.data, { recursive: true }),
fs.mkdir(Global.Path.config, { recursive: true }),
fs.mkdir(Global.Path.providers, { recursive: true }),
fs.mkdir(Global.Path.state, { recursive: true }),
])
+74
View File
@@ -0,0 +1,74 @@
import { spawn } from "bun"
import { z } from "zod"
import { NamedError } from "../util/error"
import { Log } from "../util/log"
import { Bus } from "../bus"
const SUPPORTED_IDES = [
{ name: "Windsurf" as const, cmd: "windsurf" },
{ name: "Visual Studio Code" as const, cmd: "code" },
{ name: "Cursor" as const, cmd: "cursor" },
{ name: "VSCodium" as const, cmd: "codium" },
]
export namespace Ide {
const log = Log.create({ service: "ide" })
export const Event = {
Installed: Bus.event(
"ide.installed",
z.object({
ide: z.string(),
}),
),
}
export const AlreadyInstalledError = NamedError.create("AlreadyInstalledError", z.object({}))
export const InstallFailedError = NamedError.create(
"InstallFailedError",
z.object({
stderr: z.string(),
}),
)
export function ide() {
if (process.env["TERM_PROGRAM"] === "vscode") {
const v = process.env["GIT_ASKPASS"]
for (const ide of SUPPORTED_IDES) {
if (v?.includes(ide.name)) return ide.name
}
}
return "unknown"
}
export function alreadyInstalled() {
return process.env["OPENCODE_CALLER"] === "vscode"
}
export async function install(ide: (typeof SUPPORTED_IDES)[number]["name"]) {
const cmd = SUPPORTED_IDES.find((i) => i.name === ide)?.cmd
if (!cmd) throw new Error(`Unknown IDE: ${ide}`)
const p = spawn([cmd, "--install-extension", "sst-dev.opencode"], {
stdout: "pipe",
stderr: "pipe",
})
await p.exited
const stdout = await new Response(p.stdout).text()
const stderr = await new Response(p.stderr).text()
log.info("installed", {
ide,
stdout,
stderr,
})
if (p.exitCode !== 0) {
throw new InstallFailedError({ stderr })
}
if (stdout.includes("already installed")) {
throw new AlreadyInstalledError({})
}
}
}
+4 -2
View File
@@ -5,6 +5,7 @@ import { RunCommand } from "./cli/cmd/run"
import { GenerateCommand } from "./cli/cmd/generate"
import { Log } from "./util/log"
import { AuthCommand } from "./cli/cmd/auth"
import { AgentCommand } from "./cli/cmd/agent"
import { UpgradeCommand } from "./cli/cmd/upgrade"
import { ModelsCommand } from "./cli/cmd/models"
import { UI } from "./cli/ui"
@@ -16,7 +17,7 @@ import { TuiCommand } from "./cli/cmd/tui"
import { DebugCommand } from "./cli/cmd/debug"
import { StatsCommand } from "./cli/cmd/stats"
import { McpCommand } from "./cli/cmd/mcp"
import { InstallGithubCommand } from "./cli/cmd/install-github"
import { GithubCommand } from "./cli/cmd/github"
import { Trace } from "./trace"
Trace.init()
@@ -72,11 +73,12 @@ const cli = yargs(hideBin(process.argv))
.command(GenerateCommand)
.command(DebugCommand)
.command(AuthCommand)
.command(AgentCommand)
.command(UpgradeCommand)
.command(ServeCommand)
.command(ModelsCommand)
.command(StatsCommand)
.command(InstallGithubCommand)
.command(GithubCommand)
.fail((msg) => {
if (msg.startsWith("Unknown argument") || msg.startsWith("Not enough non-option arguments")) {
cli.showHelp("log")
+39
View File
@@ -322,4 +322,43 @@ export namespace LSPServer {
}
},
}
export const CSharp: Info = {
id: "csharp",
root: NearestRoot([".sln", ".csproj", "global.json"]),
extensions: [".cs"],
async spawn(_, root) {
let bin = Bun.which("csharp-ls", {
PATH: process.env["PATH"] + ":" + Global.Path.bin,
})
if (!bin) {
if (!Bun.which("dotnet")) {
log.error(".NET SDK is required to install csharp-ls")
return
}
log.info("installing csharp-ls via dotnet tool")
const proc = Bun.spawn({
cmd: ["dotnet", "tool", "install", "csharp-ls", "--tool-path", Global.Path.bin],
stdout: "pipe",
stderr: "pipe",
stdin: "pipe",
})
const exit = await proc.exited
if (exit !== 0) {
log.error("Failed to install csharp-ls")
return
}
bin = path.join(Global.Path.bin, "csharp-ls" + (process.platform === "win32" ? ".exe" : ""))
log.info(`installed csharp-ls`, { bin })
}
return {
process: spawn(bin, {
cwd: root,
}),
}
},
}
}
+26 -14
View File
@@ -1,5 +1,7 @@
import { experimental_createMCPClient, type Tool } from "ai"
import { Experimental_StdioMCPTransport } from "ai/mcp-stdio"
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
import { App } from "../app/app"
import { Config } from "../config/config"
import { Log } from "../util/log"
@@ -32,15 +34,28 @@ export namespace MCP {
}
log.info("found", { key, type: mcp.type })
if (mcp.type === "remote") {
const client = await experimental_createMCPClient({
name: key,
transport: {
type: "sse",
url: mcp.url,
headers: mcp.headers,
},
}).catch(() => {})
if (!client) {
const transports = [
new StreamableHTTPClientTransport(new URL(mcp.url), {
requestInit: {
headers: mcp.headers,
},
}),
new SSEClientTransport(new URL(mcp.url), {
requestInit: {
headers: mcp.headers,
},
}),
]
for (const transport of transports) {
const client = await experimental_createMCPClient({
name: key,
transport,
}).catch(() => {})
if (!client) continue
clients[key] = client
break
}
if (!clients[key])
Bus.publish(Session.Event.Error, {
error: {
name: "UnknownError",
@@ -49,16 +64,13 @@ export namespace MCP {
},
},
})
continue
}
clients[key] = client
}
if (mcp.type === "local") {
const [cmd, ...args] = mcp.command
const client = await experimental_createMCPClient({
name: key,
transport: new Experimental_StdioMCPTransport({
transport: new StdioClientTransport({
stderr: "ignore",
command: cmd,
args,
-145
View File
@@ -5,23 +5,11 @@ import { mergeDeep, sortBy } from "remeda"
import { NoSuchModelError, type LanguageModel, type Provider as SDK } from "ai"
import { Log } from "../util/log"
import { BunProc } from "../bun"
import { BashTool } from "../tool/bash"
import { EditTool } from "../tool/edit"
import { WebFetchTool } from "../tool/webfetch"
import { GlobTool } from "../tool/glob"
import { GrepTool } from "../tool/grep"
import { ListTool } from "../tool/ls"
import { PatchTool } from "../tool/patch"
import { ReadTool } from "../tool/read"
import type { Tool } from "../tool/tool"
import { WriteTool } from "../tool/write"
import { TodoReadTool, TodoWriteTool } from "../tool/todo"
import { AuthAnthropic } from "../auth/anthropic"
import { AuthCopilot } from "../auth/copilot"
import { ModelsDev } from "./models"
import { NamedError } from "../util/error"
import { Auth } from "../auth"
import { TaskTool } from "../tool/task"
export namespace Provider {
const log = Log.create({ service: "provider" })
@@ -469,139 +457,6 @@ export namespace Provider {
}
}
const TOOLS = [
BashTool,
EditTool,
WebFetchTool,
GlobTool,
GrepTool,
ListTool,
// LspDiagnosticTool,
// LspHoverTool,
PatchTool,
ReadTool,
// MultiEditTool,
WriteTool,
TodoWriteTool,
TodoReadTool,
TaskTool,
]
const TOOL_MAPPING: Record<string, Tool.Info[]> = {
anthropic: TOOLS.filter((t) => t.id !== "patch"),
openai: TOOLS.map((t) => ({
...t,
parameters: optionalToNullable(t.parameters),
})),
azure: TOOLS.map((t) => ({
...t,
parameters: optionalToNullable(t.parameters),
})),
google: TOOLS.map((t) => ({
...t,
parameters: sanitizeGeminiParameters(t.parameters),
})),
}
export async function tools(providerID: string) {
/*
const cfg = await Config.get()
if (cfg.tool?.provider?.[providerID])
return cfg.tool.provider[providerID].map(
(id) => TOOLS.find((t) => t.id === id)!,
)
*/
return TOOL_MAPPING[providerID] ?? TOOLS
}
function sanitizeGeminiParameters(schema: z.ZodTypeAny, visited = new Set()): z.ZodTypeAny {
if (!schema || visited.has(schema)) {
return schema
}
visited.add(schema)
if (schema instanceof z.ZodDefault) {
const innerSchema = schema.removeDefault()
// Handle Gemini's incompatibility with `default` on `anyOf` (unions).
if (innerSchema instanceof z.ZodUnion) {
// The schema was `z.union(...).default(...)`, which is not allowed.
// We strip the default and return the sanitized union.
return sanitizeGeminiParameters(innerSchema, visited)
}
// Otherwise, the default is on a regular type, which is allowed.
// We recurse on the inner type and then re-apply the default.
return sanitizeGeminiParameters(innerSchema, visited).default(schema._def.defaultValue())
}
if (schema instanceof z.ZodOptional) {
return z.optional(sanitizeGeminiParameters(schema.unwrap(), visited))
}
if (schema instanceof z.ZodObject) {
const newShape: Record<string, z.ZodTypeAny> = {}
for (const [key, value] of Object.entries(schema.shape)) {
newShape[key] = sanitizeGeminiParameters(value as z.ZodTypeAny, visited)
}
return z.object(newShape)
}
if (schema instanceof z.ZodArray) {
return z.array(sanitizeGeminiParameters(schema.element, visited))
}
if (schema instanceof z.ZodUnion) {
// This schema corresponds to `anyOf` in JSON Schema.
// We recursively sanitize each option in the union.
const sanitizedOptions = schema.options.map((option: z.ZodTypeAny) => sanitizeGeminiParameters(option, visited))
return z.union(sanitizedOptions as [z.ZodTypeAny, z.ZodTypeAny, ...z.ZodTypeAny[]])
}
if (schema instanceof z.ZodString) {
const newSchema = z.string({ description: schema.description })
const safeChecks = ["min", "max", "length", "regex", "startsWith", "endsWith", "includes", "trim"]
// rome-ignore lint/suspicious/noExplicitAny: <explanation>
;(newSchema._def as any).checks = (schema._def as z.ZodStringDef).checks.filter((check) =>
safeChecks.includes(check.kind),
)
return newSchema
}
return schema
}
function optionalToNullable(schema: z.ZodTypeAny): z.ZodTypeAny {
if (schema instanceof z.ZodObject) {
const shape = schema.shape
const newShape: Record<string, z.ZodTypeAny> = {}
for (const [key, value] of Object.entries(shape)) {
const zodValue = value as z.ZodTypeAny
if (zodValue instanceof z.ZodOptional) {
newShape[key] = zodValue.unwrap().nullable()
} else {
newShape[key] = optionalToNullable(zodValue)
}
}
return z.object(newShape)
}
if (schema instanceof z.ZodArray) {
return z.array(optionalToNullable(schema.element))
}
if (schema instanceof z.ZodUnion) {
return z.union(
schema.options.map((option: z.ZodTypeAny) => optionalToNullable(option)) as [
z.ZodTypeAny,
z.ZodTypeAny,
...z.ZodTypeAny[],
],
)
}
return schema
}
export const ModelNotFoundError = NamedError.create(
"ProviderModelNotFoundError",
z.object({
+36 -13
View File
@@ -3,28 +3,51 @@ import { unique } from "remeda"
export namespace ProviderTransform {
export function message(msgs: ModelMessage[], providerID: string, modelID: string) {
if (providerID === "anthropic" || modelID.includes("anthropic")) {
if (providerID === "anthropic" || modelID.includes("anthropic") || modelID.includes("claude")) {
const system = msgs.filter((msg) => msg.role === "system").slice(0, 2)
const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
const providerOptions = {
anthropic: {
cacheControl: { type: "ephemeral" },
},
openrouter: {
cache_control: { type: "ephemeral" },
},
bedrock: {
cachePoint: { type: "ephemeral" },
},
openaiCompatible: {
cache_control: { type: "ephemeral" },
},
}
for (const msg of unique([...system, ...final])) {
const shouldUseContentOptions =
providerID !== "anthropic" && Array.isArray(msg.content) && msg.content.length > 0
if (shouldUseContentOptions) {
const lastContent = msg.content[msg.content.length - 1]
if (lastContent && typeof lastContent === "object") {
lastContent.providerOptions = {
...lastContent.providerOptions,
...providerOptions,
}
continue
}
}
msg.providerOptions = {
...msg.providerOptions,
anthropic: {
cacheControl: { type: "ephemeral" },
},
openrouter: {
cache_control: { type: "ephemeral" },
},
bedrock: {
cachePoint: { type: "ephemeral" },
},
openaiCompatible: {
cache_control: { type: "ephemeral" },
},
...providerOptions,
}
}
}
return msgs
}
export function temperature(_providerID: string, modelID: string) {
if (modelID.toLowerCase().includes("qwen")) return 0.55
return 0
}
}
+119 -8
View File
@@ -17,6 +17,7 @@ import { File } from "../file"
import { LSP } from "../lsp"
import { MessageV2 } from "../session/message-v2"
import { Mode } from "../session/mode"
import { callTui, TuiRoute } from "./tui"
const ERRORS = {
400: {
@@ -42,6 +43,10 @@ export namespace Server {
export type Routes = ReturnType<typeof app>
export const Event = {
Connected: Bus.event("server.connected", z.object({})),
}
function app() {
const app = new Hono()
@@ -57,15 +62,20 @@ export namespace Server {
})
})
.use(async (c, next) => {
log.info("request", {
method: c.req.method,
path: c.req.path,
})
const skipLogging = c.req.path === "/log"
if (!skipLogging) {
log.info("request", {
method: c.req.method,
path: c.req.path,
})
}
const start = Date.now()
await next()
log.info("response", {
duration: Date.now() - start,
})
if (!skipLogging) {
log.info("response", {
duration: Date.now() - start,
})
}
})
.get(
"/doc",
@@ -103,7 +113,10 @@ export namespace Server {
log.info("event connected")
return streamSSE(c, async (stream) => {
stream.writeSSE({
data: JSON.stringify({}),
data: JSON.stringify({
type: "server.connected",
properties: {},
}),
})
const unsub = Bus.subscribeAll(async (event) => {
await stream.writeSSE({
@@ -195,6 +208,7 @@ export namespace Server {
}),
async (c) => {
const sessions = await Array.fromAsync(Session.list())
sessions.sort((a, b) => b.time.updated - a.time.updated)
return c.json(sessions)
},
)
@@ -459,6 +473,62 @@ export namespace Server {
return c.json(msg)
},
)
.post(
"/session/:id/revert",
describeRoute({
description: "Revert a message",
responses: {
200: {
description: "Updated session",
content: {
"application/json": {
schema: resolver(Session.Info),
},
},
},
},
}),
zValidator(
"param",
z.object({
id: z.string(),
}),
),
zValidator("json", Session.RevertInput.omit({ sessionID: true })),
async (c) => {
const id = c.req.valid("param").id
log.info("revert", c.req.valid("json"))
const session = await Session.revert({ sessionID: id, ...c.req.valid("json") })
return c.json(session)
},
)
.post(
"/session/:id/unrevert",
describeRoute({
description: "Restore all reverted messages",
responses: {
200: {
description: "Updated session",
content: {
"application/json": {
schema: resolver(Session.Info),
},
},
},
},
}),
zValidator(
"param",
z.object({
id: z.string(),
}),
),
async (c) => {
const id = c.req.valid("param").id
const session = await Session.unrevert({ sessionID: id })
return c.json(session)
},
)
.get(
"/config/providers",
describeRoute({
@@ -703,6 +773,47 @@ export namespace Server {
return c.json(modes)
},
)
.post(
"/tui/append-prompt",
describeRoute({
description: "Append prompt to the TUI",
responses: {
200: {
description: "Prompt processed successfully",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
},
}),
zValidator(
"json",
z.object({
text: z.string(),
}),
),
async (c) => c.json(await callTui(c)),
)
.post(
"/tui/open-help",
describeRoute({
description: "Open the help dialog",
responses: {
200: {
description: "Help dialog opened successfully",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
},
}),
async (c) => c.json(await callTui(c)),
)
.route("/tui/control", TuiRoute)
return result
}
+30
View File
@@ -0,0 +1,30 @@
import { Hono, type Context } from "hono"
import { AsyncQueue } from "../util/queue"
interface Request {
path: string
body: any
}
const request = new AsyncQueue<Request>()
const response = new AsyncQueue<any>()
export async function callTui(ctx: Context) {
const body = await ctx.req.json()
request.push({
path: ctx.req.path,
body,
})
return response.next()
}
export const TuiRoute = new Hono()
.get("/next", async (c) => {
const req = await request.next()
return c.json(req)
})
.post("/response", async (c) => {
const body = await c.req.json()
response.push(body)
return c.json(true)
})
+316 -148
View File
@@ -17,7 +17,6 @@ import {
import PROMPT_INITIALIZE from "../session/prompt/initialize.txt"
import PROMPT_PLAN from "../session/prompt/plan.txt"
import PROMPT_ANTHROPIC_SPOOF from "../session/prompt/anthropic_spoof.txt"
import { App } from "../app/app"
import { Bus } from "../bus"
@@ -40,6 +39,8 @@ import { MessageV2 } from "./message-v2"
import { Mode } from "./mode"
import { LSP } from "../lsp"
import { ReadTool } from "../tool/read"
import { mergeDeep, pipe, splitWhen } from "remeda"
import { ToolRegistry } from "../tool/registry"
export namespace Session {
const log = Log.create({ service: "session" })
@@ -64,8 +65,9 @@ export namespace Session {
revert: z
.object({
messageID: z.string(),
part: z.number(),
partID: z.string().optional(),
snapshot: z.string().optional(),
diff: z.string().optional(),
})
.optional(),
})
@@ -118,11 +120,22 @@ export namespace Session {
const sessions = new Map<string, Info>()
const messages = new Map<string, MessageV2.Info[]>()
const pending = new Map<string, AbortController>()
const queued = new Map<
string,
{
input: ChatInput
message: MessageV2.User
parts: MessageV2.Part[]
processed: boolean
callback: (input: { info: MessageV2.Assistant; parts: MessageV2.Part[] }) => void
}[]
>()
return {
sessions,
messages,
pending,
queued,
}
},
async (state) => {
@@ -235,7 +248,7 @@ export namespace Session {
const read = await Storage.readJSON<MessageV2.Info>(p)
result.push({
info: read,
parts: await parts(sessionID, read.id),
parts: await getParts(sessionID, read.id),
})
}
result.sort((a, b) => (a.info.id > b.info.id ? 1 : -1))
@@ -246,7 +259,7 @@ export namespace Session {
return Storage.readJSON<MessageV2.Info>("session/message/" + sessionID + "/" + messageID)
}
export async function parts(sessionID: string, messageID: string) {
export async function getParts(sessionID: string, messageID: string) {
const result = [] as MessageV2.Part[]
for (const item of await Storage.list("session/part/" + sessionID + "/" + messageID)) {
const read = await Storage.readJSON<MessageV2.Part>(item)
@@ -325,6 +338,7 @@ export namespace Session {
providerID: z.string(),
modelID: z.string(),
mode: z.string().optional(),
system: z.string().optional(),
tools: z.record(z.boolean()).optional(),
parts: z.array(
z.discriminatedUnion("type", [
@@ -351,64 +365,15 @@ export namespace Session {
]),
),
})
export type ChatInput = z.infer<typeof ChatInput>
export async function chat(input: z.infer<typeof ChatInput>) {
export async function chat(
input: z.infer<typeof ChatInput>,
): Promise<{ info: MessageV2.Assistant; parts: MessageV2.Part[] }> {
const l = log.clone().tag("session", input.sessionID)
l.info("chatting")
const model = await Provider.getModel(input.providerID, input.modelID)
let msgs = await messages(input.sessionID)
const session = await get(input.sessionID)
if (session.revert) {
const trimmed = []
for (const msg of msgs) {
if (
msg.info.id > session.revert.messageID ||
(msg.info.id === session.revert.messageID && session.revert.part === 0)
) {
await Storage.remove("session/message/" + input.sessionID + "/" + msg.info.id)
await Bus.publish(MessageV2.Event.Removed, {
sessionID: input.sessionID,
messageID: msg.info.id,
})
continue
}
if (msg.info.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.filter((x) => x.info.role === "assistant").at(-1)?.info as MessageV2.Assistant
const outputLimit = Math.min(model.info.limit.output, OUTPUT_TOKEN_MAX) || OUTPUT_TOKEN_MAX
// auto summarize if too long
if (previous && previous.tokens) {
const tokens =
previous.tokens.input + previous.tokens.cache.read + previous.tokens.cache.write + previous.tokens.output
if (model.info.limit.context && tokens > Math.max((model.info.limit.context - outputLimit) * 0.9, 0)) {
await summarize({
sessionID: input.sessionID,
providerID: input.providerID,
modelID: input.modelID,
})
return chat(input)
}
}
using abort = lock(input.sessionID)
const lastSummary = msgs.findLast((msg) => msg.info.role === "assistant" && msg.info.summary === true)
if (lastSummary) msgs = msgs.filter((msg) => msg.info.id >= lastSummary.info.id)
const inputMode = input.mode ?? "build"
const userMsg: MessageV2.Info = {
id: input.messageID ?? Identifier.ascending("message"),
role: "user",
@@ -424,12 +389,38 @@ export namespace Session {
if (part.type === "file") {
const url = new URL(part.url)
switch (url.protocol) {
case "data:":
if (part.mime === "text/plain") {
return [
{
id: Identifier.ascending("part"),
messageID: userMsg.id,
sessionID: input.sessionID,
type: "text",
synthetic: true,
text: `Called the Read tool with the following input: ${JSON.stringify({ filePath: part.filename })}`,
},
{
id: Identifier.ascending("part"),
messageID: userMsg.id,
sessionID: input.sessionID,
type: "text",
synthetic: true,
text: Buffer.from(part.url, "base64url").toString(),
},
{
...part,
id: part.id ?? Identifier.ascending("part"),
messageID: userMsg.id,
sessionID: input.sessionID,
},
]
}
break
case "file:":
// have to normalize, symbol search returns absolute paths
// Decode the pathname since URL constructor doesn't automatically decode it
const pathname = decodeURIComponent(url.pathname)
const relativePath = pathname.replace(app.path.cwd, ".")
const filePath = path.join(app.path.cwd, relativePath)
const filePath = decodeURIComponent(url.pathname)
if (part.mime === "text/plain") {
let offset: number | undefined = undefined
@@ -467,12 +458,14 @@ export namespace Session {
}
}
const args = { filePath, offset, limit }
const result = await ReadTool.execute(args, {
sessionID: input.sessionID,
abort: abort.signal,
messageID: userMsg.id,
metadata: async () => {},
})
const result = await ReadTool.init().then((t) =>
t.execute(args, {
sessionID: input.sessionID,
abort: new AbortController().signal,
messageID: userMsg.id,
metadata: async () => {},
}),
)
return [
{
id: Identifier.ascending("part"),
@@ -507,7 +500,7 @@ export namespace Session {
messageID: userMsg.id,
sessionID: input.sessionID,
type: "text",
text: `Called the Read tool with the following input: {\"filePath\":\"${pathname}\"}`,
text: `Called the Read tool with the following input: {\"filePath\":\"${filePath}\"}`,
synthetic: true,
},
{
@@ -533,8 +526,7 @@ export namespace Session {
]
}),
).then((x) => x.flat())
if (input.mode === "plan")
if (inputMode === "plan")
userParts.push({
id: Identifier.ascending("part"),
messageID: userMsg.id,
@@ -544,7 +536,80 @@ export namespace Session {
synthetic: true,
})
if (msgs.length === 0 && !session.parentID) {
await updateMessage(userMsg)
for (const part of userParts) {
await updatePart(part)
}
if (isLocked(input.sessionID)) {
return new Promise((resolve) => {
const queue = state().queued.get(input.sessionID) ?? []
queue.push({
input: input,
message: userMsg,
parts: userParts,
processed: false,
callback: resolve,
})
state().queued.set(input.sessionID, queue)
})
}
const model = await Provider.getModel(input.providerID, input.modelID)
let msgs = await messages(input.sessionID)
const session = await get(input.sessionID)
if (session.revert) {
const messageID = session.revert.messageID
const [preserve, remove] = splitWhen(msgs, (x) => x.info.id === messageID)
msgs = preserve
for (const msg of remove) {
if (msg.info.id === userMsg.id) continue
await Storage.remove(`session/message/${input.sessionID}/${msg.info.id}`)
await Bus.publish(MessageV2.Event.Removed, { sessionID: input.sessionID, messageID: msg.info.id })
}
const last = preserve.at(-1)
if (session.revert.partID && last) {
const partID = session.revert.partID
const [preserveParts, removeParts] = splitWhen(last.parts, (x) => x.id === partID)
last.parts = preserveParts
for (const part of removeParts) {
await Storage.remove(`session/part/${input.sessionID}/${last.info.id}/${part.id}`)
await Bus.publish(MessageV2.Event.PartRemoved, {
sessionID: input.sessionID,
messageID: last.info.id,
partID: part.id,
})
}
}
await update(input.sessionID, (draft) => {
draft.revert = undefined
})
}
const previous = msgs.filter((x) => x.info.role === "assistant").at(-1)?.info as MessageV2.Assistant
const outputLimit = Math.min(model.info.limit.output, OUTPUT_TOKEN_MAX) || OUTPUT_TOKEN_MAX
// auto summarize if too long
if (previous && previous.tokens) {
const tokens =
previous.tokens.input + previous.tokens.cache.read + previous.tokens.cache.write + previous.tokens.output
if (model.info.limit.context && tokens > Math.max((model.info.limit.context - outputLimit) * 0.9, 0)) {
await summarize({
sessionID: input.sessionID,
providerID: input.providerID,
modelID: input.modelID,
})
return chat(input)
}
}
using abort = lock(input.sessionID)
const lastSummary = msgs.findLast((msg) => msg.info.role === "assistant" && msg.info.summary === true)
if (lastSummary) msgs = msgs.filter((msg) => msg.info.id >= lastSummary.info.id)
if (msgs.length === 1 && !session.parentID) {
const small = (await Provider.getSmallModel(input.providerID)) ?? model
generateText({
maxOutputTokens: small.info.reasoning ? 1024 : 20,
@@ -577,20 +642,23 @@ export namespace Session {
.then((result) => {
if (result.text)
return Session.update(input.sessionID, (draft) => {
draft.title = result.text
const cleaned = result.text.replace(/<think>[\s\S]*?<\/think>\s*/g, "")
const title = cleaned.length > 100 ? cleaned.substring(0, 97) + "..." : cleaned
draft.title = title.trim()
})
})
.catch(() => {})
}
await updateMessage(userMsg)
for (const part of userParts) {
await updatePart(part)
}
msgs.push({ info: userMsg, parts: userParts })
const mode = await Mode.get(input.mode ?? "build")
let system = input.providerID === "anthropic" ? [PROMPT_ANTHROPIC_SPOOF.trim()] : []
system.push(...(mode.prompt ? [mode.prompt] : SystemPrompt.provider(input.modelID)))
const mode = await Mode.get(inputMode)
let system = SystemPrompt.header(input.providerID)
system.push(
...(() => {
if (input.system) return [input.system]
if (mode.prompt) return [mode.prompt]
return SystemPrompt.provider(input.modelID)
})(),
)
system.push(...(await SystemPrompt.environment()))
system.push(...(await SystemPrompt.custom()))
// max 2 system prompt messages for caching purposes
@@ -601,6 +669,7 @@ export namespace Session {
id: Identifier.ascending("message"),
role: "assistant",
system,
mode: inputMode,
path: {
cwd: app.path.cwd,
root: app.path.root,
@@ -624,15 +693,19 @@ export namespace Session {
const processor = createProcessor(assistantMsg, model.info)
for (const item of await Provider.tools(input.providerID)) {
if (mode.tools[item.id] === false) continue
if (input.tools?.[item.id] === false) continue
if (session.parentID && item.id === "task") continue
const enabledTools = pipe(
mode.tools,
mergeDeep(ToolRegistry.enabled(input.providerID, input.modelID)),
mergeDeep(input.tools ?? {}),
)
for (const item of await ToolRegistry.tools(input.providerID, input.modelID)) {
if (enabledTools[item.id] === false) continue
tools[item.id] = tool({
id: item.id as any,
description: item.description,
inputSchema: item.parameters as ZodSchema,
async execute(args, options) {
await processor.track(options.toolCallId)
const result = await item.execute(args, {
sessionID: input.sessionID,
abort: abort.signal,
@@ -671,6 +744,7 @@ export namespace Session {
const execute = item.execute
if (!execute) continue
item.execute = async (args, opts) => {
await processor.track(opts.toolCallId)
const result = await execute(args, opts)
const output = result.content
.filter((x: any) => x.type === "text")
@@ -692,7 +766,53 @@ export namespace Session {
const stream = streamText({
onError() {},
maxRetries: 10,
async prepareStep({ messages }) {
const queue = (state().queued.get(input.sessionID) ?? []).filter((x) => !x.processed)
if (queue.length) {
for (const item of queue) {
if (item.processed) continue
messages.push(
...MessageV2.toModelMessage([
{
info: item.message,
parts: item.parts,
},
]),
)
item.processed = true
}
assistantMsg.time.completed = Date.now()
await updateMessage(assistantMsg)
Object.assign(assistantMsg, {
id: Identifier.ascending("message"),
role: "assistant",
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,
mode: inputMode,
time: {
created: Date.now(),
},
sessionID: input.sessionID,
})
await updateMessage(assistantMsg)
}
return {
messages,
}
},
maxRetries: 3,
maxOutputTokens: outputLimit,
abortSignal: abort.signal,
stopWhen: stepCountIs(1000),
@@ -708,7 +828,9 @@ export namespace Session {
),
...MessageV2.toModelMessage(msgs),
],
temperature: model.info.temperature ? 0 : undefined,
temperature: model.info.temperature
? (mode.temperature ?? ProviderTransform.temperature(input.providerID, input.modelID))
: undefined,
tools: model.info.tool_call === false ? undefined : tools,
model: wrapLanguageModel({
model: model.language,
@@ -726,12 +848,27 @@ export namespace Session {
}),
})
const result = await processor.process(stream)
const queued = state().queued.get(input.sessionID) ?? []
const unprocessed = queued.find((x) => !x.processed)
if (unprocessed) {
unprocessed.processed = true
return chat(unprocessed.input)
}
for (const item of queued) {
item.callback(result)
}
state().queued.delete(input.sessionID)
return result
}
function createProcessor(assistantMsg: MessageV2.Assistant, model: ModelsDev.Model) {
const toolCalls: Record<string, MessageV2.ToolPart> = {}
const snapshots: Record<string, string> = {}
return {
async track(toolCallID: string) {
const hash = await Snapshot.track()
if (hash) snapshots[toolCallID] = hash
},
partFromToolCall(toolCallID: string) {
return toolCalls[toolCallID]
},
@@ -745,20 +882,11 @@ export namespace Session {
})
switch (value.type) {
case "start":
const snapshot = await Snapshot.create(assistantMsg.sessionID)
if (snapshot)
await updatePart({
id: Identifier.ascending("part"),
messageID: assistantMsg.id,
sessionID: assistantMsg.sessionID,
type: "snapshot",
snapshot,
})
break
case "tool-input-start":
const part = await updatePart({
id: Identifier.ascending("part"),
id: toolCalls[value.id]?.id ?? Identifier.ascending("part"),
messageID: assistantMsg.id,
sessionID: assistantMsg.sessionID,
type: "tool",
@@ -774,6 +902,9 @@ export namespace Session {
case "tool-input-delta":
break
case "tool-input-end":
break
case "tool-call": {
const match = toolCalls[value.toolCallId]
if (match) {
@@ -809,15 +940,20 @@ export namespace Session {
},
})
delete toolCalls[value.toolCallId]
const snapshot = await Snapshot.create(assistantMsg.sessionID)
if (snapshot)
await updatePart({
id: Identifier.ascending("part"),
messageID: assistantMsg.id,
sessionID: assistantMsg.sessionID,
type: "snapshot",
snapshot,
})
const snapshot = snapshots[value.toolCallId]
if (snapshot) {
const patch = await Snapshot.patch(snapshot)
if (patch.files.length) {
await updatePart({
id: Identifier.ascending("part"),
messageID: assistantMsg.id,
sessionID: assistantMsg.sessionID,
type: "patch",
hash: patch.hash,
files: patch.files,
})
}
}
}
break
}
@@ -838,15 +974,18 @@ export namespace Session {
},
})
delete toolCalls[value.toolCallId]
const snapshot = await Snapshot.create(assistantMsg.sessionID)
if (snapshot)
const snapshot = snapshots[value.toolCallId]
if (snapshot) {
const patch = await Snapshot.patch(snapshot)
await updatePart({
id: Identifier.ascending("part"),
messageID: assistantMsg.id,
sessionID: assistantMsg.sessionID,
type: "snapshot",
snapshot,
type: "patch",
hash: patch.hash,
files: patch.files,
})
}
}
break
}
@@ -894,12 +1033,13 @@ export namespace Session {
case "text":
if (currentText) {
currentText.text += value.text
await updatePart(currentText)
if (currentText.text) await updatePart(currentText)
}
break
case "text-end":
if (currentText && currentText.text) {
if (currentText) {
currentText.text = currentText.text.trimEnd()
currentText.time = {
start: Date.now(),
end: Date.now(),
@@ -957,7 +1097,7 @@ export namespace Session {
error: assistantMsg.error,
})
}
const p = await parts(assistantMsg.sessionID, assistantMsg.id)
const p = await getParts(assistantMsg.sessionID, assistantMsg.id)
for (const part of p) {
if (part.type === "tool" && part.state.status !== "completed") {
updatePart({
@@ -981,47 +1121,66 @@ export namespace Session {
}
}
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
export const RevertInput = z.object({
sessionID: Identifier.schema("session"),
messageID: Identifier.schema("message"),
partID: Identifier.schema("part").optional(),
})
export type RevertInput = z.infer<typeof RevertInput>
export async function revert(input: RevertInput) {
const all = await messages(input.sessionID)
let lastUser: MessageV2.User | undefined
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
let revert: Info["revert"]
const patches: Snapshot.Patch[] = []
for (const msg of all) {
if (msg.info.role === "user") lastUser = msg.info
const remaining = []
for (const part of msg.parts) {
if (revert) {
if (part.type === "patch") {
patches.push(part)
}
continue
}
if (!revert) {
if ((msg.info.id === input.messageID && !input.partID) || part.id === input.partID) {
// if no useful parts left in message, same as reverting whole message
const partID = remaining.some((item) => ["text", "tool"].includes(item.type)) ? input.partID : undefined
revert = {
messageID: !partID && lastUser ? lastUser.id : msg.info.id,
partID,
}
}
remaining.push(part)
}
}
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,
}
})
*/
}
if (revert) {
const session = await get(input.sessionID)
revert.snapshot = session.revert?.snapshot ?? (await Snapshot.track())
await Snapshot.revert(patches)
if (revert.snapshot) revert.diff = await Snapshot.diff(revert.snapshot)
return update(input.sessionID, (draft) => {
draft.revert = revert
})
}
return session
}
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) => {
export async function unrevert(input: { sessionID: string }) {
log.info("unreverting", input)
const session = await get(input.sessionID)
if (!session.revert) return session
if (session.revert.snapshot) await Snapshot.restore(session.revert.snapshot)
const next = await update(input.sessionID, (draft) => {
draft.revert = undefined
})
return next
}
export async function summarize(input: { sessionID: string; providerID: string; modelID: string }) {
@@ -1031,13 +1190,18 @@ export namespace Session {
const filtered = msgs.filter((msg) => !lastSummary || msg.info.id >= lastSummary.info.id)
const model = await Provider.getModel(input.providerID, input.modelID)
const app = App.info()
const system = SystemPrompt.summarize(input.providerID)
const system = [
...SystemPrompt.summarize(input.providerID),
...(await SystemPrompt.environment()),
...(await SystemPrompt.custom()),
]
const next: MessageV2.Info = {
id: Identifier.ascending("message"),
role: "assistant",
sessionID: input.sessionID,
system,
mode: "build",
path: {
cwd: app.path.cwd,
root: app.path.root,
@@ -1087,6 +1251,10 @@ export namespace Session {
return result
}
function isLocked(sessionID: string) {
return state().pending.has(sessionID)
}
function lock(sessionID: string) {
log.info("locking", { sessionID })
if (state().pending.has(sessionID)) throw new BusyError(sessionID)
+20 -1
View File
@@ -94,6 +94,15 @@ export namespace MessageV2 {
})
export type SnapshotPart = z.infer<typeof SnapshotPart>
export const PatchPart = PartBase.extend({
type: z.literal("patch"),
hash: z.string(),
files: z.string().array(),
}).openapi({
ref: "PatchPart",
})
export type PatchPart = z.infer<typeof PatchPart>
export const TextPart = PartBase.extend({
type: z.literal("text"),
text: z.string(),
@@ -203,7 +212,7 @@ export namespace MessageV2 {
export type User = z.infer<typeof User>
export const Part = z
.discriminatedUnion("type", [TextPart, FilePart, ToolPart, StepStartPart, StepFinishPart, SnapshotPart])
.discriminatedUnion("type", [TextPart, FilePart, ToolPart, StepStartPart, StepFinishPart, SnapshotPart, PatchPart])
.openapi({
ref: "Part",
})
@@ -226,6 +235,7 @@ export namespace MessageV2 {
system: z.string().array(),
modelID: z.string(),
providerID: z.string(),
mode: z.string(),
path: z.object({
cwd: z.string(),
root: z.string(),
@@ -271,6 +281,14 @@ export namespace MessageV2 {
part: Part,
}),
),
PartRemoved: Bus.event(
"message.part.removed",
z.object({
sessionID: z.string(),
messageID: z.string(),
partID: z.string(),
}),
),
}
export function fromV1(v1: Message.Info) {
@@ -290,6 +308,7 @@ export namespace MessageV2 {
modelID: v1.metadata.assistant!.modelID,
providerID: v1.metadata.assistant!.providerID,
system: v1.metadata.assistant!.system,
mode: "build",
error: v1.metadata.error,
}
const parts = v1.parts.flatMap((part): Part[] => {
+27 -24
View File
@@ -1,12 +1,13 @@
import { mergeDeep } from "remeda"
import { App } from "../app/app"
import { Config } from "../config/config"
import z from "zod"
import { Provider } from "../provider/provider"
export namespace Mode {
export const Info = z
.object({
name: z.string(),
temperature: z.number().optional(),
model: z
.object({
modelID: z.string(),
@@ -22,38 +23,40 @@ export namespace Mode {
export type Info = z.infer<typeof Info>
const state = App.state("mode", async () => {
const cfg = await Config.get()
const mode = mergeDeep(
{
build: {},
plan: {
tools: {
write: false,
edit: false,
patch: false,
},
const model = cfg.model ? Provider.parseModel(cfg.model) : undefined
const result: Record<string, Info> = {
build: {
model,
name: "build",
tools: {},
},
plan: {
name: "plan",
model,
tools: {
write: false,
edit: false,
patch: false,
},
},
cfg.mode ?? {},
)
const result: Record<string, Info> = {}
for (const [key, value] of Object.entries(mode)) {
}
for (const [key, value] of Object.entries(cfg.mode ?? {})) {
if (value.disable) continue
let item = result[key]
if (!item)
item = result[key] = {
name: key,
tools: {},
}
const model = value.model ?? cfg.model
if (model) {
const [providerID, ...rest] = model.split("/")
const modelID = rest.join("/")
item.model = {
modelID,
providerID,
}
}
item.name = key
if (value.model) item.model = Provider.parseModel(value.model)
if (value.prompt) item.prompt = value.prompt
if (value.tools) item.tools = value.tools
if (value.temperature) item.temperature = value.temperature
if (value.tools)
item.tools = {
...value.tools,
...item.tools,
}
}
return result
+85 -41
View File
@@ -1,4 +1,6 @@
You are opencode, an autonomous agent - please keep going until the user's query is completely resolved, before ending your turn and yielding back to the user.
# Beast Mode 3.1
You are opencode, an agent - please keep going until the users query is completely resolved, before ending your turn and yielding back to the user.
Your thinking should be thorough and so it's fine if it's very long. However, avoid unnecessary repetition and verbosity. You should be concise, but thorough.
@@ -6,25 +8,26 @@ You MUST iterate and keep going until the problem is solved.
You have everything you need to resolve this problem. I want you to fully solve this autonomously before coming back to me.
Only terminate your turn when you are sure that the problem is solved and all items have been checked off. Use the TodoWrite and TodoRead tools to track and manage steps. Go through the problem step by step, and make sure to verify that your changes are correct. Once each step is finished mark it as completed with the TodoWrite tool. NEVER end your turn without having truly and completely solved the problem, use the TodoRead tool to make sure all steps are complete, and when you say you are going to make a tool call, make sure you ACTUALLY make the tool call, instead of ending your turn. If a step is impossible to complete, mark it as cancelled using the TodoWrite tool.
Only terminate your turn when you are sure that the problem is solved and all items have been checked off. Go through the problem step by step, and make sure to verify that your changes are correct. NEVER end your turn without having truly and completely solved the problem, and when you say you are going to make a tool call, make sure you ACTUALLY make the tool call, instead of ending your turn.
THE PROBLEM CAN NOT BE SOLVED WITHOUT EXTENSIVE INTERNET RESEARCH.
You must use the webfetch tool to recursively gather all information from URLs provided to you by the user, as well as any links you find in the content of those pages.
You must use the webfetch tool to recursively gather all information from URL's provided to you by the user, as well as any links you find in the content of those pages.
Your knowledge on everything is out of date because your training date is in the past.
Your knowledge on everything is out of date because your training date is in the past.
You CANNOT successfully complete this task without using Bing to verify your understanding of third party packages and dependencies is up to date. You must use the webfetch tool to search bing for how to properly use libraries, packages, frameworks, dependencies, etc. every single time you install or implement one. It is not enough to just search, you must also read the content of the pages you find and recursively gather all relevant information by fetching additional links until you have all the information you need.
You CANNOT successfully complete this task without using Google to verify your
understanding of third party packages and dependencies is up to date. You must use the webfetch tool to search google for how to properly use libraries, packages, frameworks, dependencies, etc. every single time you install or implement one. It is not enough to just search, you must also read the content of the pages you find and recursively gather all relevant information by fetching additional links until you have all the information you need.
If the user request is "resume" or "continue" or "try again",use the TodoRead tool to find the next pending step. Continue from that step, and do not hand back control to the user until the entire todo list is complete and all steps are marked as complete or cancelled. Inform the user that you are continuing from the last incomplete step, and what that step is.
Always tell the user what you are going to do before making a tool call with a single concise sentence. This will help them understand what you are doing and why.
Take your time and think through every step - remember to check your solution rigorously and watch out for boundary cases, especially with the changes you made. Use the sequential thinking tool if available. Your solution must be perfect. If not, continue working on it. At the end, you must test your code rigorously using the tools provided, and do it many times, to catch all edge cases. If it is not robust, update the plan and iterate more and make it perfect. Failing to test your code sufficiently rigorously is the NUMBER ONE failure mode on these types of tasks; run the build, and verify that the changes you made actually build; make sure you handle all edge cases, and run existing tests if they are provided.
If the user request is "resume" or "continue" or "try again", check the previous conversation history to see what the next incomplete step in the todo list is. Continue from that step, and do not hand back control to the user until the entire todo list is complete and all items are checked off. Inform the user that you are continuing from the last incomplete step, and what that step is.
You MUST plan extensively before each tool call, and reflect extensively on the outcomes of the previous tool calls. DO NOT do this entire process by making tool calls only, as this can impair your ability to solve the problem and think insightfully.
Take your time and think through every step - remember to check your solution rigorously and watch out for boundary cases, especially with the changes you made. Use the sequential thinking tool if available. Your solution must be perfect. If not, continue working on it. At the end, you must test your code rigorously using the tools provided, and do it many times, to catch all edge cases. If it is not robust, iterate more and make it perfect. Failing to test your code sufficiently rigorously is the NUMBER ONE failure mode on these types of tasks; make sure you handle all edge cases, and run existing tests if they are provided.
You MUST keep working until the problem is completely solved, and all steps in the todo list are complete. Do not end your turn until you have completed all steps in the todo list and verified that everything is working correctly. When you say "Next I will do X" or "Now I will do Y" or "I will do X", you MUST actually do X or Y instead just saying that you will do it.
You MUST plan extensively before each function call, and reflect extensively on the outcomes of the previous function calls. DO NOT do this entire process by making function calls only, as this can impair your ability to solve the problem and think insightfully.
You MUST use the ToolRead tool to verify that all steps are complete or cancelled before ending your turn. If any steps are incomplete, you MUST continue working on them until they are all complete.
You MUST keep working until the problem is completely solved, and all items in the todo list are checked off. Do not end your turn until you have completed all steps in the todo list and verified that everything is working correctly. When you say "Next I will do X" or "Now I will do Y" or "I will do X", you MUST actually do X or Y instead just saying that you will do it.
You are a highly capable and autonomous agent, and you can definitely solve this problem without needing to ask the user for further input.
@@ -38,7 +41,7 @@ You are a highly capable and autonomous agent, and you can definitely solve this
- What are the dependencies and interactions with other parts of the code?
3. Investigate the codebase. Explore relevant files, search for key functions, and gather context.
4. Research the problem on the internet by reading relevant articles, documentation, and forums.
5. Develop a clear, step-by-step plan. Break down the fix into manageable, incremental steps. Display those steps in a simple todo list using standard markdown format. Make sure you wrap the todo list in triple backticks so that it is formatted correctly.
5. Develop a clear, step-by-step plan. Break down the fix into manageable, incremental steps. Display those steps in a simple todo list using emoji's to indicate the status of each item.
6. Implement the fix incrementally. Make small, testable code changes.
7. Debug as needed. Use debugging techniques to isolate and resolve issues.
8. Test frequently. Run tests after each change to verify correctness.
@@ -49,12 +52,12 @@ Refer to the detailed sections below for more information on each step.
## 1. Fetch Provided URLs
- If the user provides a URL, use the `webfetch` tool to retrieve the content of the provided URL.
- After fetching, review the content returned by the fetch tool.
- After fetching, review the content returned by the webfetch tool.
- If you find any additional URLs or links that are relevant, use the `webfetch` tool again to retrieve those links.
- Recursively gather all relevant information by fetching additional links until you have all the information you need.
## 2. Deeply Understand the Problem
Carefully read the issue and think hard about a plan to solve it before coding. Use the sequential thinking tool if available.
Carefully read the issue and think hard about a plan to solve it before coding.
## 3. Codebase Investigation
- Explore relevant files and directories.
@@ -64,49 +67,49 @@ Carefully read the issue and think hard about a plan to solve it before coding.
- Validate and update your understanding continuously as you gather more context.
## 4. Internet Research
- Use the `webfetch` tool to search bing by fetching the URL `https://www.bing.com/search?q=your+search+query`.
- Use the `webfetch` tool to search google by fetching the URL `https://www.google.com/search?q=your+search+query`.
- After fetching, review the content returned by the fetch tool.
- If you find any additional URLs or links that are relevant, use the `webfetch` tool again to retrieve those links.
- Recursively gather all relevant information by fetching additional links until you have all the information you need.
- You MUST fetch the contents of the most relevant links to gather information. Do not rely on the summary that you find in the search results.
- As you fetch each link, read the content thoroughly and fetch any additional links that you find withhin the content that are relevant to the problem.
- Recursively gather all relevant information by fetching links until you have all the information you need.
## 5. Develop a Detailed Plan
## 5. Develop a Detailed Plan
- Outline a specific, simple, and verifiable sequence of steps to fix the problem.
- Add steps using the TodoWrite tool.
- Each time you complete a step, mark it as complete using the TodoWrite tool.
- Each time you check off a step, use the TodoRead tool and display the updated todo list to the user in markdown format.
- You MUST continue on to the next step after checking off a step instead of ending your turn and asking the user what they want to do next.
- You may only end your turn when all steps in the todo list are marked as complete or cancelled.
- Create a todo list in markdown format to track your progress.
- Each time you complete a step, check it off using `[x]` syntax.
- Each time you check off a step, display the updated todo list to the user.
- Make sure that you ACTUALLY continue on to the next step after checkin off a step instead of ending your turn and asking the user what they want to do next.
## 6. Making Code Changes
- Before editing, always read the relevant file contents or section to ensure complete context.
- Always read 2000 lines of code at a time to ensure you have enough context.
- If a patch is not applied correctly, attempt to reapply it.
- Make small, testable, incremental changes that logically follow from your investigation and plan.
- When using the edit tool, include 3-5 lines of unchanged code before and after the string you want to replace, to make it unambiguous which part of the file should be edited.
- If a patch or edit is not applied correctly, attempt to reapply it.
- Always validate that your changes build and pass tests after each change.
- If the build fails or test fail, debug why before proceeding, update the plan as needed.
- Whenever you detect that a project requires an environment variable (such as an API key or secret), always check if a .env file exists in the project root. If it does not exist, automatically create a .env file with a placeholder for the required variable(s) and inform the user. Do this proactively, without waiting for the user to request it.
## 7. Debugging
- Use the `lsp_diagnostics` tool to check for any problems in the code.
- Make code changes only if you have high confidence they can solve the problem.
- When debugging, try to determine the root cause rather than addressing symptoms.
- Debug for as long as needed to identify the root cause and identify a fix.
- Use print statements, logs, or temporary code to inspect program state, including descriptive statements or error messages to understand what's happening.
- To test hypotheses, you can also add test statements or functions.
- Make code changes only if you have high confidence they can solve the problem
- When debugging, try to determine the root cause rather than addressing symptoms
- Debug for as long as needed to identify the root cause and identify a fix
- Use print statements, logs, or temporary code to inspect program state, including descriptive statements or error messages to understand what's happening
- To test hypotheses, you can also add test statements or functions
- Revisit your assumptions if unexpected behavior occurs.
# How to create a Todo List
Use the following format to show the todo list:
```markdown
- [ ] Step 1: Description of the first step
- [ ] Step 2: Description of the second step
- [ ] Step 3: Description of the third step
```
Do not ever use HTML tags or any other formatting for the todo list, as it will not be rendered correctly. Always use the markdown format shown above.
Use the following format to create a todo list:
```markdown
- [ ] Step 1: Description of the first step
- [ ] Step 2: Description of the second step
- [ ] Step 3: Description of the third step
```
Do not ever use HTML tags or any other formatting for the todo list, as it will not be rendered correctly. Always use the markdown format shown above. Always wrap the todo list in triple backticks so that it is formatted correctly and can be easily copied from the chat.
Always show the completed todo list to the user as the last item in your message, so that they can see that you have addressed all of the steps.
# Communication Guidelines
Always communicate clearly and concisely in a casual, friendly yet professional tone.
Always communicate clearly and concisely in a casual, friendly yet professional tone.
<examples>
"Let me fetch the URL you provided to gather more information."
"Ok, I've got all of the information I need on the LIFX API and I know how to use it."
@@ -115,3 +118,44 @@ Always communicate clearly and concisely in a casual, friendly yet professional
"OK! Now let's run the tests to make sure everything is working correctly."
"Whelp - I see we have some problems. Let's fix those up."
</examples>
- Respond with clear, direct answers. Use bullet points and code blocks for structure. - Avoid unnecessary explanations, repetition, and filler.
- Always write code directly to the correct files.
- Do not display code to the user unless they specifically ask for it.
- Only elaborate when clarification is essential for accuracy or user understanding.
# Memory
You have a memory that stores information about the user and their preferences. This memory is used to provide a more personalized experience. You can access and update this memory as needed. The memory is stored in a file called `.github/instructions/memory.instruction.md`. If the file is empty, you'll need to create it.
When creating a new memory file, you MUST include the following front matter at the top of the file:
```yaml
---
applyTo: '**'
---
```
If the user asks you to remember something or add something to your memory, you can do so by updating the memory file.
# Reading Files and Folders
**Always check if you have already read a file, folder, or workspace structure before reading it again.**
- If you have already read the content and it has not changed, do NOT re-read it.
- Only re-read files or folders if:
- You suspect the content has changed since your last read.
- You have made edits to the file or folder.
- You encounter an error that suggests the context may be stale or incomplete.
- Use your internal memory and previous context to avoid redundant reads.
- This will save time, reduce unnecessary operations, and make your workflow more efficient.
# Writing Prompts
If you are asked to write a prompt, you should always generate the prompt in markdown format.
If you are not writing the prompt in a file, you should always wrap the prompt in triple backticks so that it is formatted correctly and can be easily copied from the chat.
Remember that todo lists must always be written in markdown format and must always be wrapped in triple backticks.
# Git
If the user tells you to stage and commit, you may do so.
You are NEVER allowed to stage and commit files automatically.
@@ -1,3 +1,3 @@
<system-reminder>
Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits, run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supercedes any other instructions you have received (for example, to make edits).
Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits, run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supersedes any other instructions you have received (for example, to make edits).
</system-reminder>
@@ -0,0 +1,109 @@
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
# 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 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).
# 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>
@@ -10,7 +10,7 @@ You are generating titles for a coding assistant conversation.
- Max 50 chars, single line
- Focus on the specific action or question
- Keep technical terms, numbers, and filenames exactly as written
- Preserve HTTP status codes (401, 404, 500, etc) as numbers
- Preserve HTTP status codes (401, 404, 500, etc.) as numbers
- For file references, include the filename
- Avoid filler words: the, this, my, a, an, properly
- NEVER assume their tech stack or domain
+7 -1
View File
@@ -7,6 +7,7 @@ import path from "path"
import os from "os"
import PROMPT_ANTHROPIC from "./prompt/anthropic.txt"
import PROMPT_ANTHROPIC_WITHOUT_TODO from "./prompt/qwen.txt"
import PROMPT_BEAST from "./prompt/beast.txt"
import PROMPT_GEMINI from "./prompt/gemini.txt"
import PROMPT_ANTHROPIC_SPOOF from "./prompt/anthropic_spoof.txt"
@@ -14,10 +15,15 @@ import PROMPT_SUMMARIZE from "./prompt/summarize.txt"
import PROMPT_TITLE from "./prompt/title.txt"
export namespace SystemPrompt {
export function header(providerID: string) {
if (providerID.includes("anthropic")) return [PROMPT_ANTHROPIC_SPOOF.trim()]
return []
}
export function provider(modelID: string) {
if (modelID.includes("gpt-") || modelID.includes("o1") || modelID.includes("o3")) return [PROMPT_BEAST]
if (modelID.includes("gemini-")) return [PROMPT_GEMINI]
return [PROMPT_ANTHROPIC]
if (modelID.includes("claude")) return [PROMPT_ANTHROPIC]
return [PROMPT_ANTHROPIC_WITHOUT_TODO]
}
export async function environment() {
+75 -36
View File
@@ -2,27 +2,31 @@ 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"
import { Global } from "../global"
import { z } from "zod"
export namespace Snapshot {
const log = Log.create({ service: "snapshot" })
export async function create(sessionID: string) {
log.info("creating snapshot")
export function init() {
Array.fromAsync(
new Bun.Glob("**/snapshot").scan({
absolute: true,
onlyFiles: false,
cwd: Global.Path.data,
}),
).then((files) => {
for (const file of files) {
fs.rmdir(file, { recursive: true })
}
})
}
export async function track() {
const app = App.info()
// 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
}
const git = gitdir(sessionID)
if (!app.git) return
const git = gitdir()
if (await fs.mkdir(git, { recursive: true })) {
await $`git init`
.env({
@@ -34,36 +38,71 @@ export namespace Snapshot {
.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 -m "snapshot" --no-gpg-sign --author="opencode <mail@opencode.ai>"`
.quiet()
.cwd(app.path.cwd)
.nothrow()
const match = result.stdout.toString().match(/\[.+ ([a-f0-9]+)\]/)
if (!match) return
return match![1]
const hash = await $`git --git-dir ${git} write-tree`.quiet().cwd(app.path.cwd).text()
return hash.trim()
}
export async function restore(sessionID: string, snapshot: string) {
export const Patch = z.object({
hash: z.string(),
files: z.string().array(),
})
export type Patch = z.infer<typeof Patch>
export async function patch(hash: string): Promise<Patch> {
const app = App.info()
const git = gitdir()
await $`git --git-dir ${git} add .`.quiet().cwd(app.path.cwd).nothrow()
const files = await $`git --git-dir ${git} diff --name-only ${hash} -- .`.cwd(app.path.cwd).text()
return {
hash,
files: files
.trim()
.split("\n")
.map((x) => x.trim())
.filter(Boolean)
.map((x) => path.join(app.path.cwd, x)),
}
}
export async function restore(snapshot: string) {
log.info("restore", { commit: snapshot })
const app = App.info()
const git = gitdir(sessionID)
await $`git --git-dir=${git} checkout ${snapshot} --force`.quiet().cwd(app.path.root)
const git = gitdir()
await $`git --git-dir=${git} read-tree ${snapshot} && git --git-dir=${git} checkout-index -a -f`
.quiet()
.cwd(app.path.root)
}
export async function diff(sessionID: string, commit: string) {
const git = gitdir(sessionID)
const result = await $`git --git-dir=${git} diff -R ${commit}`.quiet().cwd(App.info().path.root)
return result.stdout.toString("utf8")
export async function revert(patches: Patch[]) {
const files = new Set<string>()
const git = gitdir()
for (const item of patches) {
for (const file of item.files) {
if (files.has(file)) continue
log.info("reverting", { file, hash: item.hash })
const result = await $`git --git-dir=${git} checkout ${item.hash} -- ${file}`
.quiet()
.cwd(App.info().path.root)
.nothrow()
if (result.exitCode !== 0) {
log.info("file not found in history, deleting", { file })
await fs.unlink(file).catch(() => {})
}
files.add(file)
}
}
}
function gitdir(sessionID: string) {
export async function diff(hash: string) {
const app = App.info()
return path.join(app.path.data, "snapshot", sessionID)
const git = gitdir()
const result = await $`git --git-dir=${git} diff ${hash} -- .`.quiet().cwd(app.path.root).text()
return result.trim()
}
function gitdir() {
const app = App.info()
return path.join(app.path.data, "snapshots")
}
}
+16
View File
@@ -72,6 +72,22 @@ export namespace Storage {
} catch (e) {}
}
},
async (dir: string) => {
const files = new Bun.Glob("session/message/*/*.json").scanSync({
cwd: dir,
absolute: true,
})
for (const file of files) {
try {
const content = await Bun.file(file).json()
if (content.role === "assistant" && !content.mode) {
log.info("adding mode field to message", { file })
content.mode = "build"
await Bun.write(file, JSON.stringify(content, null, 2))
}
} catch (e) {}
}
},
]
const state = App.state("storage", async () => {
+2 -3
View File
@@ -7,12 +7,11 @@ const MAX_OUTPUT_LENGTH = 30000
const DEFAULT_TIMEOUT = 1 * 60 * 1000
const MAX_TIMEOUT = 10 * 60 * 1000
export const BashTool = Tool.define({
id: "bash",
export const BashTool = Tool.define("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(),
timeout: z.number().describe("Optional timeout in milliseconds").optional(),
description: z
.string()
.describe(
+150 -36
View File
@@ -1,7 +1,7 @@
// 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
// https://github.com/cline/cline/blob/main/evals/diff-edits/diff-apply/diff-06-26-25.ts
import { z } from "zod"
import * as path from "path"
import { Tool } from "./tool"
@@ -14,8 +14,7 @@ import { File } from "../file"
import { Bus } from "../bus"
import { FileTime } from "../file/time"
export const EditTool = Tool.define({
id: "edit",
export const EditTool = Tool.define("edit", {
description: DESCRIPTION,
parameters: z.object({
filePath: z.string().describe("The absolute path to the file to modify"),
@@ -105,6 +104,31 @@ export const EditTool = Tool.define({
export type Replacer = (content: string, find: string) => Generator<string, void, unknown>
// Similarity thresholds for block anchor fallback matching
const SINGLE_CANDIDATE_SIMILARITY_THRESHOLD = 0.0
const MULTIPLE_CANDIDATES_SIMILARITY_THRESHOLD = 0.3
/**
* Levenshtein distance algorithm implementation
*/
function levenshtein(a: string, b: string): number {
// Handle empty strings
if (a === "" || b === "") {
return Math.max(a.length, b.length)
}
const matrix = Array.from({ length: a.length + 1 }, (_, i) =>
Array.from({ length: b.length + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)),
)
for (let i = 1; i <= a.length; i++) {
for (let j = 1; j <= b.length; j++) {
const cost = a[i - 1] === b[j - 1] ? 0 : 1
matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost)
}
}
return matrix[a.length][b.length]
}
export const SimpleReplacer: Replacer = function* (_content, find) {
yield find
}
@@ -160,8 +184,10 @@ export const BlockAnchorReplacer: Replacer = function* (content, find) {
const firstLineSearch = searchLines[0].trim()
const lastLineSearch = searchLines[searchLines.length - 1].trim()
const searchBlockSize = searchLines.length
// Find blocks where first line matches the search first line
// Collect all candidate positions where both anchors match
const candidates: Array<{ startLine: number; endLine: number }> = []
for (let i = 0; i < originalLines.length; i++) {
if (originalLines[i].trim() !== firstLineSearch) {
continue
@@ -170,25 +196,113 @@ export const BlockAnchorReplacer: Replacer = function* (content, find) {
// 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)
candidates.push({ startLine: i, endLine: j })
break // Only match the first occurrence of the last line
}
}
}
// Return immediately if no candidates
if (candidates.length === 0) {
return
}
// Handle single candidate scenario (using relaxed threshold)
if (candidates.length === 1) {
const { startLine, endLine } = candidates[0]
const actualBlockSize = endLine - startLine + 1
let similarity = 0
let linesToCheck = Math.min(searchBlockSize - 2, actualBlockSize - 2) // Middle lines only
if (linesToCheck > 0) {
for (let j = 1; j < searchBlockSize - 1 && j < actualBlockSize - 1; j++) {
const originalLine = originalLines[startLine + j].trim()
const searchLine = searchLines[j].trim()
const maxLen = Math.max(originalLine.length, searchLine.length)
if (maxLen === 0) {
continue
}
const distance = levenshtein(originalLine, searchLine)
similarity += (1 - distance / maxLen) / linesToCheck
// Exit early when threshold is reached
if (similarity >= SINGLE_CANDIDATE_SIMILARITY_THRESHOLD) {
break
}
}
} else {
// No middle lines to compare, just accept based on anchors
similarity = 1.0
}
if (similarity >= SINGLE_CANDIDATE_SIMILARITY_THRESHOLD) {
let matchStartIndex = 0
for (let k = 0; k < startLine; k++) {
matchStartIndex += originalLines[k].length + 1
}
let matchEndIndex = matchStartIndex
for (let k = startLine; k <= endLine; k++) {
matchEndIndex += originalLines[k].length
if (k < endLine) {
matchEndIndex += 1 // Add newline character except for the last line
}
}
yield content.substring(matchStartIndex, matchEndIndex)
}
return
}
// Calculate similarity for multiple candidates
let bestMatch: { startLine: number; endLine: number } | null = null
let maxSimilarity = -1
for (const candidate of candidates) {
const { startLine, endLine } = candidate
const actualBlockSize = endLine - startLine + 1
let similarity = 0
let linesToCheck = Math.min(searchBlockSize - 2, actualBlockSize - 2) // Middle lines only
if (linesToCheck > 0) {
for (let j = 1; j < searchBlockSize - 1 && j < actualBlockSize - 1; j++) {
const originalLine = originalLines[startLine + j].trim()
const searchLine = searchLines[j].trim()
const maxLen = Math.max(originalLine.length, searchLine.length)
if (maxLen === 0) {
continue
}
const distance = levenshtein(originalLine, searchLine)
similarity += 1 - distance / maxLen
}
similarity /= linesToCheck // Average similarity
} else {
// No middle lines to compare, just accept based on anchors
similarity = 1.0
}
if (similarity > maxSimilarity) {
maxSimilarity = similarity
bestMatch = candidate
}
}
// Threshold judgment
if (maxSimilarity >= MULTIPLE_CANDIDATES_SIMILARITY_THRESHOLD && bestMatch) {
const { startLine, endLine } = bestMatch
let matchStartIndex = 0
for (let k = 0; k < startLine; k++) {
matchStartIndex += originalLines[k].length + 1
}
let matchEndIndex = matchStartIndex
for (let k = startLine; k <= endLine; k++) {
matchEndIndex += originalLines[k].length
if (k < endLine) {
matchEndIndex += 1
}
}
yield content.substring(matchStartIndex, matchEndIndex)
}
}
export const WhitespaceNormalizedReplacer: Replacer = function* (content, find) {
@@ -201,23 +315,23 @@ export const WhitespaceNormalizedReplacer: Replacer = function* (content, find)
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]
} else {
// Only check for substring matches if the full line doesn't match
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
}
} catch (e) {
// Invalid regex pattern, skip
}
}
}
@@ -457,7 +571,7 @@ export function replace(content: string, oldString: string, newString: string, r
BlockAnchorReplacer,
WhitespaceNormalizedReplacer,
IndentationFlexibleReplacer,
// EscapeNormalizedReplacer,
EscapeNormalizedReplacer,
// TrimmedBoundaryReplacer,
// ContextAwareReplacer,
// MultiOccurrenceReplacer,
+1 -2
View File
@@ -5,8 +5,7 @@ import { App } from "../app/app"
import DESCRIPTION from "./glob.txt"
import { Ripgrep } from "../file/ripgrep"
export const GlobTool = Tool.define({
id: "glob",
export const GlobTool = Tool.define("glob", {
description: DESCRIPTION,
parameters: z.object({
pattern: z.string().describe("The glob pattern to match files against"),
+1 -2
View File
@@ -5,8 +5,7 @@ import { Ripgrep } from "../file/ripgrep"
import DESCRIPTION from "./grep.txt"
export const GrepTool = Tool.define({
id: "grep",
export const GrepTool = Tool.define("grep", {
description: DESCRIPTION,
parameters: z.object({
pattern: z.string().describe("The regex pattern to search for in file contents"),
+1 -2
View File
@@ -33,8 +33,7 @@ export const IGNORE_PATTERNS = [
const LIMIT = 100
export const ListTool = Tool.define({
id: "list",
export const ListTool = Tool.define("list", {
description: DESCRIPTION,
parameters: z.object({
path: z.string().describe("The absolute path to the directory to list (must be absolute, not relative)").optional(),
@@ -5,8 +5,7 @@ import { LSP } from "../lsp"
import { App } from "../app/app"
import DESCRIPTION from "./lsp-diagnostics.txt"
export const LspDiagnosticTool = Tool.define({
id: "lsp_diagnostics",
export const LspDiagnosticTool = Tool.define("lsp_diagnostics", {
description: DESCRIPTION,
parameters: z.object({
path: z.string().describe("The path to the file to get diagnostics."),
+1 -2
View File
@@ -5,8 +5,7 @@ import { LSP } from "../lsp"
import { App } from "../app/app"
import DESCRIPTION from "./lsp-hover.txt"
export const LspHoverTool = Tool.define({
id: "lsp_hover",
export const LspHoverTool = Tool.define("lsp_hover", {
description: DESCRIPTION,
parameters: z.object({
file: z.string().describe("The path to the file to get diagnostics."),
+13 -4
View File
@@ -5,17 +5,26 @@ import DESCRIPTION from "./multiedit.txt"
import path from "path"
import { App } from "../app/app"
export const MultiEditTool = Tool.define({
id: "multiedit",
export const MultiEditTool = Tool.define("multiedit", {
description: DESCRIPTION,
parameters: z.object({
filePath: z.string().describe("The absolute path to the file to modify"),
edits: z.array(EditTool.parameters).describe("Array of edit operations to perform sequentially on the file"),
edits: z
.array(
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 oldString)"),
replaceAll: z.boolean().optional().describe("Replace all occurrences of oldString (default false)"),
}),
)
.describe("Array of edit operations to perform sequentially on the file"),
}),
async execute(params, ctx) {
const tool = await EditTool.init()
const results = []
for (const [, edit] of params.edits.entries()) {
const result = await EditTool.execute(
const result = await tool.execute(
{
filePath: params.filePath,
oldString: edit.oldString,
+1 -2
View File
@@ -210,8 +210,7 @@ async function applyCommit(
}
}
export const PatchTool = Tool.define({
id: "patch",
export const PatchTool = Tool.define("patch", {
description: DESCRIPTION,
parameters: PatchParams,
execute: async (params, ctx) => {
+16 -4
View File
@@ -10,13 +10,12 @@ import { App } from "../app/app"
const DEFAULT_READ_LIMIT = 2000
const MAX_LINE_LENGTH = 2000
export const ReadTool = Tool.define({
id: "read",
export const ReadTool = Tool.define("read", {
description: DESCRIPTION,
parameters: z.object({
filePath: z.string().describe("The path to the file to read"),
offset: z.number().describe("The line number to start reading from (0-based)").optional(),
limit: z.number().describe("The number of lines to read (defaults to 2000)").optional(),
offset: z.coerce.number().describe("The line number to start reading from (0-based)").optional(),
limit: z.coerce.number().describe("The number of lines to read (defaults to 2000)").optional(),
}),
async execute(params, ctx) {
let filePath = params.filePath
@@ -49,6 +48,8 @@ export const ReadTool = Tool.define({
const offset = params.offset || 0
const isImage = isImageFile(filePath)
if (isImage) throw new Error(`This is an image file of type: ${isImage}\nUse a different tool to process images`)
const isBinary = await isBinaryFile(file)
if (isBinary) throw new Error(`Cannot read binary file: ${filePath}`)
const lines = await file.text().then((text) => text.split("\n"))
const raw = lines.slice(offset, offset + limit).map((line) => {
return line.length > MAX_LINE_LENGTH ? line.substring(0, MAX_LINE_LENGTH) + "..." : line
@@ -100,3 +101,14 @@ function isImageFile(filePath: string): string | false {
return false
}
}
async function isBinaryFile(file: Bun.BunFile): Promise<boolean> {
const buffer = await file.arrayBuffer()
const bytes = new Uint8Array(buffer.slice(0, 512)) // Check first 512 bytes
for (let i = 0; i < bytes.length; i++) {
if (bytes[i] === 0) return true // Null byte indicates binary
}
return false
}
+176
View File
@@ -0,0 +1,176 @@
import z from "zod"
import { BashTool } from "./bash"
import { EditTool } from "./edit"
import { GlobTool } from "./glob"
import { GrepTool } from "./grep"
import { ListTool } from "./ls"
import { PatchTool } from "./patch"
import { ReadTool } from "./read"
import { TaskTool } from "./task"
import { TodoWriteTool, TodoReadTool } from "./todo"
import { WebFetchTool } from "./webfetch"
import { WriteTool } from "./write"
export namespace ToolRegistry {
const ALL = [
BashTool,
EditTool,
WebFetchTool,
GlobTool,
GrepTool,
ListTool,
PatchTool,
ReadTool,
WriteTool,
TodoWriteTool,
TodoReadTool,
TaskTool,
]
export function ids() {
return ALL.map((t) => t.id)
}
export async function tools(providerID: string, _modelID: string) {
const result = await Promise.all(
ALL.map(async (t) => ({
id: t.id,
...(await t.init()),
})),
)
if (providerID === "openai") {
return result.map((t) => ({
...t,
parameters: optionalToNullable(t.parameters),
}))
}
if (providerID === "azure") {
return result.map((t) => ({
...t,
parameters: optionalToNullable(t.parameters),
}))
}
if (providerID === "google") {
return result.map((t) => ({
...t,
parameters: sanitizeGeminiParameters(t.parameters),
}))
}
return result
}
export function enabled(_providerID: string, modelID: string): Record<string, boolean> {
if (modelID.toLowerCase().includes("claude")) {
return {
patch: false,
}
}
if (
modelID.toLowerCase().includes("qwen") ||
modelID.includes("gpt-") ||
modelID.includes("o1") ||
modelID.includes("o3")
) {
return {
patch: false,
todowrite: false,
todoread: false,
}
}
return {}
}
function sanitizeGeminiParameters(schema: z.ZodTypeAny, visited = new Set()): z.ZodTypeAny {
if (!schema || visited.has(schema)) {
return schema
}
visited.add(schema)
if (schema instanceof z.ZodDefault) {
const innerSchema = schema.removeDefault()
// Handle Gemini's incompatibility with `default` on `anyOf` (unions).
if (innerSchema instanceof z.ZodUnion) {
// The schema was `z.union(...).default(...)`, which is not allowed.
// We strip the default and return the sanitized union.
return sanitizeGeminiParameters(innerSchema, visited)
}
// Otherwise, the default is on a regular type, which is allowed.
// We recurse on the inner type and then re-apply the default.
return sanitizeGeminiParameters(innerSchema, visited).default(schema._def.defaultValue())
}
if (schema instanceof z.ZodOptional) {
return z.optional(sanitizeGeminiParameters(schema.unwrap(), visited))
}
if (schema instanceof z.ZodObject) {
const newShape: Record<string, z.ZodTypeAny> = {}
for (const [key, value] of Object.entries(schema.shape)) {
newShape[key] = sanitizeGeminiParameters(value as z.ZodTypeAny, visited)
}
return z.object(newShape)
}
if (schema instanceof z.ZodArray) {
return z.array(sanitizeGeminiParameters(schema.element, visited))
}
if (schema instanceof z.ZodUnion) {
// This schema corresponds to `anyOf` in JSON Schema.
// We recursively sanitize each option in the union.
const sanitizedOptions = schema.options.map((option: z.ZodTypeAny) => sanitizeGeminiParameters(option, visited))
return z.union(sanitizedOptions as [z.ZodTypeAny, z.ZodTypeAny, ...z.ZodTypeAny[]])
}
if (schema instanceof z.ZodString) {
const newSchema = z.string({ description: schema.description })
const safeChecks = ["min", "max", "length", "regex", "startsWith", "endsWith", "includes", "trim"]
// rome-ignore lint/suspicious/noExplicitAny: <explanation>
;(newSchema._def as any).checks = (schema._def as z.ZodStringDef).checks.filter((check) =>
safeChecks.includes(check.kind),
)
return newSchema
}
return schema
}
function optionalToNullable(schema: z.ZodTypeAny): z.ZodTypeAny {
if (schema instanceof z.ZodObject) {
const shape = schema.shape
const newShape: Record<string, z.ZodTypeAny> = {}
for (const [key, value] of Object.entries(shape)) {
const zodValue = value as z.ZodTypeAny
if (zodValue instanceof z.ZodOptional) {
newShape[key] = zodValue.unwrap().nullable()
} else {
newShape[key] = optionalToNullable(zodValue)
}
}
return z.object(newShape)
}
if (schema instanceof z.ZodArray) {
return z.array(optionalToNullable(schema.element))
}
if (schema instanceof z.ZodUnion) {
return z.union(
schema.options.map((option: z.ZodTypeAny) => optionalToNullable(option)) as [
z.ZodTypeAny,
z.ZodTypeAny,
...z.ZodTypeAny[],
],
)
}
return schema
}
}
+64 -52
View File
@@ -5,61 +5,73 @@ import { Session } from "../session"
import { Bus } from "../bus"
import { MessageV2 } from "../session/message-v2"
import { Identifier } from "../id/id"
import { Agent } from "../agent/agent"
export const TaskTool = Tool.define({
id: "task",
description: DESCRIPTION,
parameters: z.object({
description: z.string().describe("A short (3-5 words) description of the task"),
prompt: z.string().describe("The task for the agent to perform"),
}),
async execute(params, ctx) {
const session = await Session.create(ctx.sessionID)
const msg = await Session.getMessage(ctx.sessionID, ctx.messageID)
if (msg.role !== "assistant") throw new Error("Not an assistant message")
export const TaskTool = Tool.define("task", async () => {
const agents = await Agent.list()
const description = DESCRIPTION.replace("{agents}", agents.map((a) => `- ${a.name}: ${a.description}`).join("\n"))
return {
description,
parameters: z.object({
description: z.string().describe("A short (3-5 words) description of the task"),
prompt: z.string().describe("The task for the agent to perform"),
subagent_type: z.string().describe("The type of specialized agent to use for this task"),
}),
async execute(params, ctx) {
const session = await Session.create(ctx.sessionID)
const msg = await Session.getMessage(ctx.sessionID, ctx.messageID)
if (msg.role !== "assistant") throw new Error("Not an assistant message")
const agent = await Agent.get(params.subagent_type)
const messageID = Identifier.ascending("message")
const parts: Record<string, MessageV2.ToolPart> = {}
const unsub = Bus.subscribe(MessageV2.Event.PartUpdated, async (evt) => {
if (evt.properties.part.sessionID !== session.id) return
if (evt.properties.part.messageID === messageID) return
if (evt.properties.part.type !== "tool") return
parts[evt.properties.part.id] = evt.properties.part
ctx.metadata({
title: params.description,
metadata: {
summary: Object.values(parts).sort((a, b) => a.id?.localeCompare(b.id)),
},
})
})
const messageID = Identifier.ascending("message")
const parts: Record<string, MessageV2.ToolPart> = {}
const unsub = Bus.subscribe(MessageV2.Event.PartUpdated, async (evt) => {
if (evt.properties.part.sessionID !== session.id) return
if (evt.properties.part.messageID === messageID) return
if (evt.properties.part.type !== "tool") return
parts[evt.properties.part.id] = evt.properties.part
ctx.metadata({
const model = agent.model ?? {
modelID: msg.modelID,
providerID: msg.providerID,
}
ctx.abort.addEventListener("abort", () => {
Session.abort(session.id)
})
const result = await Session.chat({
messageID,
sessionID: session.id,
modelID: model.modelID,
providerID: model.providerID,
mode: msg.mode,
system: agent.prompt,
tools: {
...agent.tools,
task: false,
},
parts: [
{
id: Identifier.ascending("part"),
type: "text",
text: params.prompt,
},
],
})
unsub()
return {
title: params.description,
metadata: {
summary: Object.values(parts).sort((a, b) => a.id?.localeCompare(b.id)),
summary: result.parts.filter((x) => x.type === "tool"),
},
})
})
ctx.abort.addEventListener("abort", () => {
Session.abort(session.id)
})
const result = await Session.chat({
messageID,
sessionID: session.id,
modelID: msg.modelID,
providerID: msg.providerID,
tools: {
todoread: false,
todowrite: false,
},
parts: [
{
id: Identifier.ascending("part"),
type: "text",
text: params.prompt,
},
],
})
unsub()
return {
title: params.description,
metadata: {
summary: result.parts.filter((x) => x.type === "tool"),
},
output: result.parts.findLast((x) => x.type === "text")!.text,
}
},
output: result.parts.findLast((x) => x.type === "text")?.text ?? "",
}
},
}
})
+46 -2
View File
@@ -1,12 +1,19 @@
Launch a new agent that has access to the following tools: Bash, Glob, Grep, LS, Read, Edit, MultiEdit, Write, NotebookRead, NotebookEdit, WebFetch, TodoRead, TodoWrite, WebSearch. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries, use the Agent tool to perform the search for you.
Launch a new agent to handle complex, multi-step tasks autonomously.
Available agent types and the tools they have access to:
{agents}
When using the Task tool, you must specify a subagent_type parameter to select which agent type to use.
When to use the Agent tool:
- If you are searching for a keyword like "config" or "logger", or for questions like "which file does X?", the Agent tool is strongly recommended
- When you are instructed to execute custom slash commands. Use the Agent tool with the slash command invocation as the entire prompt. The slash command can take arguments. For example: Task(description="Check the file", prompt="/check-file path/to/file.py")
When NOT to use the Agent tool:
- If you want to read a specific file path, use the Read or Glob tool instead of the Agent tool, to find the match more quickly
- If you are searching for a specific class definition like "class Foo", use the Glob tool instead, to find the match more quickly
- If you are searching for code within a specific file or set of 2-3 files, use the Read tool instead of the Agent tool, to find the match more quickly
- Other tasks that are not related to the agent descriptions above
Usage notes:
1. Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses
@@ -14,3 +21,40 @@ Usage notes:
3. Each agent invocation is stateless. You will not be able to send additional messages to the agent, nor will the agent be able to communicate with you outside of its final report. Therefore, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you.
4. The agent's outputs should generally be trusted
5. Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent
6. If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement.
Example usage:
<example_agent_descriptions>
"code-reviewer": use this agent after you are done writing a signficant piece of code
"greeting-responder": use this agent when to respond to user greetings with a friendly joke
</example_agent_description>
<example>
user: "Please write a function that checks if a number is prime"
assistant: Sure let me write a function that checks if a number is prime
assistant: First let me use the Write tool to write a function that checks if a number is prime
assistant: I'm going to use the Write tool to write the following code:
<code>
function isPrime(n) {
if (n <= 1) return false
for (let i = 2; i * i <= n; i++) {
if (n % i === 0) return false
}
return true
}
</code>
<commentary>
Since a signficant piece of code was written and the task was completed, now use the code-reviewer agent to review the code
</commentary>
assistant: Now let me use the code-reviewer agent to review the code
assistant: Uses the Task tool to launch the with the code-reviewer agent
</example>
<example>
user: "Hello"
<commentary>
Since the user is greeting, use the greeting-responder agent to respond with a friendly joke
</commentary>
assistant: "I'm going to use the Task tool to launch the with the greeting-responder agent"
</example>
+3 -5
View File
@@ -4,7 +4,7 @@ import DESCRIPTION_WRITE from "./todowrite.txt"
import { App } from "../app/app"
const TodoInfo = z.object({
content: z.string().min(1).describe("Brief description of the task"),
content: z.string().describe("Brief description of the task"),
status: z.enum(["pending", "in_progress", "completed", "cancelled"]).describe("Current status of the task"),
priority: z.enum(["high", "medium", "low"]).describe("Priority level of the task"),
id: z.string().describe("Unique identifier for the todo item"),
@@ -18,8 +18,7 @@ const state = App.state("todo-tool", () => {
return todos
})
export const TodoWriteTool = Tool.define({
id: "todowrite",
export const TodoWriteTool = Tool.define("todowrite", {
description: DESCRIPTION_WRITE,
parameters: z.object({
todos: z.array(TodoInfo).describe("The updated todo list"),
@@ -37,8 +36,7 @@ export const TodoWriteTool = Tool.define({
},
})
export const TodoReadTool = Tool.define({
id: "todoread",
export const TodoReadTool = Tool.define("todoread", {
description: "Use this tool to read your todo list",
parameters: z.object({}),
async execute(_params, opts) {
+20 -11
View File
@@ -12,21 +12,30 @@ export namespace Tool {
}
export interface Info<Parameters extends StandardSchemaV1 = StandardSchemaV1, M extends Metadata = Metadata> {
id: string
description: string
parameters: Parameters
execute(
args: StandardSchemaV1.InferOutput<Parameters>,
ctx: Context,
): Promise<{
title: string
metadata: M
output: string
init: () => Promise<{
description: string
parameters: Parameters
execute(
args: StandardSchemaV1.InferOutput<Parameters>,
ctx: Context,
): Promise<{
title: string
metadata: M
output: string
}>
}>
}
export function define<Parameters extends StandardSchemaV1, Result extends Metadata>(
input: Info<Parameters, Result>,
id: string,
init: Info<Parameters, Result>["init"] | Awaited<ReturnType<Info<Parameters, Result>["init"]>>,
): Info<Parameters, Result> {
return input
return {
id,
init: async () => {
if (init instanceof Function) return init()
return init
},
}
}
}
+2 -8
View File
@@ -7,20 +7,14 @@ const MAX_RESPONSE_SIZE = 5 * 1024 * 1024 // 5MB
const DEFAULT_TIMEOUT = 30 * 1000 // 30 seconds
const MAX_TIMEOUT = 120 * 1000 // 2 minutes
export const WebFetchTool = Tool.define({
id: "webfetch",
export const WebFetchTool = Tool.define("webfetch", {
description: DESCRIPTION,
parameters: z.object({
url: z.string().describe("The URL to fetch content from"),
format: z
.enum(["text", "markdown", "html"])
.describe("The format to return the content in (text, markdown, or html)"),
timeout: z
.number()
.min(0)
.max(MAX_TIMEOUT / 1000)
.describe("Optional timeout in seconds (max 120)")
.optional(),
timeout: z.number().describe("Optional timeout in seconds (max 120)").optional(),
}),
async execute(params, ctx) {
// Validate URL
+1 -2
View File
@@ -9,8 +9,7 @@ import { Bus } from "../bus"
import { File } from "../file"
import { FileTime } from "../file/time"
export const WriteTool = Tool.define({
id: "write",
export const WriteTool = Tool.define("write", {
description: DESCRIPTION,
parameters: z.object({
filePath: z.string().describe("The absolute path to the file to write (must be absolute, not relative)"),
+3 -1
View File
@@ -49,10 +49,12 @@ export namespace Filesystem {
const glob = new Bun.Glob(pattern)
for await (const match of glob.scan({
cwd: current,
absolute: true,
onlyFiles: true,
followSymlinks: true,
dot: true,
})) {
result.push(join(current, match))
result.push(match)
}
} catch {
// Skip invalid glob patterns
+19
View File
@@ -0,0 +1,19 @@
export class AsyncQueue<T> implements AsyncIterable<T> {
private queue: T[] = []
private resolvers: ((value: T) => void)[] = []
push(item: T) {
const resolve = this.resolvers.shift()
if (resolve) resolve(item)
else this.queue.push(item)
}
async next(): Promise<T> {
if (this.queue.length > 0) return this.queue.shift()!
return new Promise((resolve) => this.resolvers.push(resolve))
}
async *[Symbol.asyncIterator]() {
while (true) yield await this.next()
}
}
+1
View File
@@ -0,0 +1 @@
// Test fixture for ListTool
+1
View File
@@ -0,0 +1 @@
// Test fixture for ListTool
+1
View File
@@ -0,0 +1 @@
// Test fixture for ListTool
@@ -1,17 +1,9 @@
// Bun Snapshot v1, https://goo.gl/fbAQLP
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
exports[`tool.ls basic 1`] = `
"- /home/thdxr/dev/projects/sst/opencode/js/example/
- home/
- thdxr/
- dev/
- projects/
- sst/
- opencode/
- js/
- example/
- ink.tsx
- broken.ts
- cli.ts
"packages/opencode/test/fixtures/example/
broken.ts
cli.ts
ink.tsx
"
`;
+82
View File
@@ -326,6 +326,88 @@ const testCases: TestCase[] = [
find: "const msg = `Hello\\tWorld`;",
replace: "const msg = `Hi\\tWorld`;",
},
// Test case that reproduces the greedy matching bug - now should fail due to low similarity
{
content: [
"func main() {",
" if condition {",
" doSomething()",
" }",
" processData()",
" if anotherCondition {",
" doOtherThing()",
" }",
" return mainLayout",
"}",
"",
"func helper() {",
" }",
" return mainLayout", // This should NOT be matched due to low similarity
"}",
].join("\n"),
find: [" }", " return mainLayout"].join("\n"),
replace: [" }", " // Add some code here", " return mainLayout"].join("\n"),
fail: true, // This should fail because the pattern has low similarity score
},
// Test case for the fix - more specific pattern should work
{
content: [
"function renderLayout() {",
" const header = createHeader()",
" const body = createBody()",
" return mainLayout",
"}",
].join("\n"),
find: ["function renderLayout() {", " // different content", " return mainLayout", "}"].join("\n"),
replace: [
"function renderLayout() {",
" const header = createHeader()",
" const body = createBody()",
" // Add minimap overlay",
" return mainLayout",
"}",
].join("\n"),
},
// Test that large blocks without arbitrary size limits can work
{
content: Array.from({ length: 100 }, (_, i) => `line ${i}`).join("\n"),
find: Array.from({ length: 50 }, (_, i) => `line ${i + 25}`).join("\n"),
replace: Array.from({ length: 50 }, (_, i) => `updated line ${i + 25}`).join("\n"),
},
// Test case for the fix - more specific pattern should work
{
content: [
"function renderLayout() {",
" const header = createHeader()",
" const body = createBody()",
" return mainLayout",
"}",
].join("\n"),
find: ["function renderLayout() {", " // different content", " return mainLayout", "}"].join("\n"),
replace: [
"function renderLayout() {",
" const header = createHeader()",
" const body = createBody()",
" // Add minimap overlay",
" return mainLayout",
"}",
].join("\n"),
},
// Test BlockAnchorReplacer with overly large blocks (should fail)
{
content:
Array.from({ length: 100 }, (_, i) => `line ${i}`).join("\n") +
"\nfunction test() {\n" +
Array.from({ length: 60 }, (_, i) => ` content ${i}`).join("\n") +
"\n return result\n}",
find: ["function test() {", " // different content", " return result", "}"].join("\n"),
replace: ["function test() {", " return 42", "}"].join("\n"),
},
]
describe("EditTool Replacers", () => {
+17 -7
View File
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"
import { App } from "../../src/app/app"
import { GlobTool } from "../../src/tool/glob"
import { ListTool } from "../../src/tool/ls"
import path from "path"
const ctx = {
sessionID: "test",
@@ -9,10 +10,16 @@ const ctx = {
abort: AbortSignal.any([]),
metadata: () => {},
}
const glob = await GlobTool.init()
const list = await ListTool.init()
const projectRoot = path.join(__dirname, "../..")
const fixturePath = path.join(__dirname, "../fixtures/example")
describe("tool.glob", () => {
test("truncate", async () => {
await App.provide({ cwd: process.cwd() }, async () => {
let result = await GlobTool.execute(
await App.provide({ cwd: projectRoot }, async () => {
let result = await glob.execute(
{
pattern: "../../node_modules/**/*",
path: undefined,
@@ -23,8 +30,8 @@ describe("tool.glob", () => {
})
})
test("basic", async () => {
await App.provide({ cwd: process.cwd() }, async () => {
let result = await GlobTool.execute(
await App.provide({ cwd: projectRoot }, async () => {
let result = await glob.execute(
{
pattern: "*.json",
path: undefined,
@@ -41,9 +48,12 @@ describe("tool.glob", () => {
describe("tool.ls", () => {
test("basic", async () => {
const result = await App.provide({ cwd: process.cwd() }, async () => {
return await ListTool.execute({ path: "./example", ignore: [".git"] }, ctx)
const result = await App.provide({ cwd: projectRoot }, async () => {
return await list.execute({ path: fixturePath, ignore: [".git"] }, ctx)
})
expect(result.output).toMatchSnapshot()
// Normalize absolute path to relative for consistent snapshots
const normalizedOutput = result.output.replace(fixturePath, "packages/opencode/test/fixtures/example")
expect(normalizedOutput).toMatchSnapshot()
})
})
@@ -0,0 +1,15 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/debian
{
"name": "Development",
"image": "mcr.microsoft.com/devcontainers/typescript-node:latest",
"features": {
"ghcr.io/devcontainers/features/node:1": {}
},
"postCreateCommand": "yarn install",
"customizations": {
"vscode": {
"extensions": ["esbenp.prettier-vscode"]
}
}
}
+88
View File
@@ -0,0 +1,88 @@
name: CI
on:
push:
branches-ignore:
- 'generated'
- 'codegen/**'
- 'integrated/**'
- 'stl-preview-head/**'
- 'stl-preview-base/**'
pull_request:
branches-ignore:
- 'stl-preview-head/**'
- 'stl-preview-base/**'
jobs:
lint:
timeout-minutes: 10
name: lint
runs-on: ${{ github.repository == 'stainless-sdks/opencode-typescript' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Bootstrap
run: ./scripts/bootstrap
- name: Check types
run: ./scripts/lint
build:
timeout-minutes: 5
name: build
runs-on: ${{ github.repository == 'stainless-sdks/opencode-typescript' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Bootstrap
run: ./scripts/bootstrap
- name: Check build
run: ./scripts/build
- name: Get GitHub OIDC Token
if: github.repository == 'stainless-sdks/opencode-typescript'
id: github-oidc
uses: actions/github-script@v6
with:
script: core.setOutput('github_token', await core.getIDToken());
- name: Upload tarball
if: github.repository == 'stainless-sdks/opencode-typescript'
env:
URL: https://pkg.stainless.com/s
AUTH: ${{ steps.github-oidc.outputs.github_token }}
SHA: ${{ github.sha }}
run: ./scripts/utils/upload-artifact.sh
test:
timeout-minutes: 10
name: test
runs-on: ${{ github.repository == 'stainless-sdks/opencode-typescript' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Bootstrap
run: ./scripts/bootstrap
- name: Run tests
run: ./scripts/test
+32
View File
@@ -0,0 +1,32 @@
# This workflow is triggered when a GitHub release is created.
# It can also be run manually to re-publish to NPM in case it failed for some reason.
# You can run this workflow by navigating to https://www.github.com/sst/opencode-sdk-js/actions/workflows/publish-npm.yml
name: Publish NPM
on:
workflow_dispatch:
release:
types: [published]
jobs:
publish:
name: publish
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v3
with:
node-version: '20'
- name: Install dependencies
run: |
yarn install
- name: Publish to NPM
run: |
bash ./bin/publish-npm
env:
NPM_TOKEN: ${{ secrets.OPENCODE_NPM_TOKEN || secrets.NPM_TOKEN }}
+21
View File
@@ -0,0 +1,21 @@
name: Release Doctor
on:
pull_request:
branches:
- main
workflow_dispatch:
jobs:
release_doctor:
name: release doctor
runs-on: ubuntu-latest
if: github.repository == 'sst/opencode-sdk-js' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next')
steps:
- uses: actions/checkout@v4
- name: Check release environment
run: |
bash ./bin/check-release-environment
env:
NPM_TOKEN: ${{ secrets.OPENCODE_NPM_TOKEN || secrets.NPM_TOKEN }}
+10
View File
@@ -0,0 +1,10 @@
.prism.log
node_modules
yarn-error.log
codegen.log
Brewfile.lock.json
dist
dist-deno
/*.tgz
.idea/
+7
View File
@@ -0,0 +1,7 @@
CHANGELOG.md
/ecosystem-tests/*/**
/node_modules
/deno
# don't format tsc output, will break source maps
/dist
+7
View File
@@ -0,0 +1,7 @@
{
"arrowParens": "always",
"experimentalTernaries": true,
"printWidth": 110,
"singleQuote": true,
"trailingComma": "all"
}
@@ -0,0 +1,3 @@
{
".": "0.1.0-alpha.20"
}
+4
View File
@@ -0,0 +1,4 @@
configured_endpoints: 26
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-62d8fccba4eb8dc3a80434e0849eab3352e49fb96a718bb7b6d17ed8e582b716.yml
openapi_spec_hash: 4ff9376cf9634e91731e63fe482ea532
config_hash: 1ae82c93499b9f0b9ba828b8919f9cb3
+1
View File
@@ -0,0 +1 @@
brew "node"
+196
View File
@@ -0,0 +1,196 @@
# Changelog
## 0.1.0-alpha.20 (2025-07-16)
Full Changelog: [v0.1.0-alpha.19...v0.1.0-alpha.20](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.19...v0.1.0-alpha.20)
### Features
* **api:** api update ([d296473](https://github.com/sst/opencode-sdk-js/commit/d296473db58378932b85d1afaa60942ac5599c49))
* **api:** api update ([af2b587](https://github.com/sst/opencode-sdk-js/commit/af2b5875534a4782fac186542ecb9b04393c9b0a))
## 0.1.0-alpha.19 (2025-07-16)
Full Changelog: [v0.1.0-alpha.18...v0.1.0-alpha.19](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.18...v0.1.0-alpha.19)
### Features
* **api:** api update ([2e505ef](https://github.com/sst/opencode-sdk-js/commit/2e505ef451fdcf49358189c5f76bdc42fb821352))
## 0.1.0-alpha.18 (2025-07-15)
Full Changelog: [v0.1.0-alpha.17...v0.1.0-alpha.18](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.17...v0.1.0-alpha.18)
### Features
* **api:** api update ([25a23e5](https://github.com/sst/opencode-sdk-js/commit/25a23e599f1180754910961df65f0cc044aa2935))
## 0.1.0-alpha.17 (2025-07-15)
Full Changelog: [v0.1.0-alpha.16...v0.1.0-alpha.17](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.16...v0.1.0-alpha.17)
### Features
* **api:** api update ([8b5d592](https://github.com/sst/opencode-sdk-js/commit/8b5d59243a0212f98269412f4483e729e2367a77))
* **api:** api update ([ebd8986](https://github.com/sst/opencode-sdk-js/commit/ebd89862c48be2742eda727c83c01430413e00c0))
## 0.1.0-alpha.16 (2025-07-15)
Full Changelog: [v0.1.0-alpha.15...v0.1.0-alpha.16](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.15...v0.1.0-alpha.16)
### Features
* **api:** api update ([f26379d](https://github.com/sst/opencode-sdk-js/commit/f26379d83ae7094d6ba91c6705a97a3fbd88a55a))
### Chores
* make some internal functions async ([36b1db9](https://github.com/sst/opencode-sdk-js/commit/36b1db9ca9d47d9199e2eab5f0b454b7cd31f58f))
## 0.1.0-alpha.15 (2025-07-05)
Full Changelog: [v0.1.0-alpha.14...v0.1.0-alpha.15](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.14...v0.1.0-alpha.15)
### Features
* **api:** manual updates ([f6ee467](https://github.com/sst/opencode-sdk-js/commit/f6ee46752d0c174c8b934894cf2b140864864208))
### Chores
* **internal:** codegen related update ([47a1a97](https://github.com/sst/opencode-sdk-js/commit/47a1a972e755735d6b5472c61f726ab2face9e62))
## 0.1.0-alpha.14 (2025-07-03)
Full Changelog: [v0.1.0-alpha.13...v0.1.0-alpha.14](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.13...v0.1.0-alpha.14)
### Features
* **api:** api update ([a1d7cf9](https://github.com/sst/opencode-sdk-js/commit/a1d7cf948a2ff47ce4e98b4a52d0e4d213b87bf6))
### Chores
* **internal:** version bump ([f8ad145](https://github.com/sst/opencode-sdk-js/commit/f8ad145b9af0c4a465642630043e59236d5f4e8d))
## 0.1.0-alpha.13 (2025-07-03)
Full Changelog: [v0.1.0-alpha.12...v0.1.0-alpha.13](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.12...v0.1.0-alpha.13)
### Bug Fixes
* avoid console usage ([f96ac97](https://github.com/sst/opencode-sdk-js/commit/f96ac97fbaf7417efda306d8727654d1a4138386))
### Chores
* add docs to RequestOptions type ([1ca6677](https://github.com/sst/opencode-sdk-js/commit/1ca667765c22b706732c61ea3d9d2823aeda0a8e))
## 0.1.0-alpha.12 (2025-07-02)
Full Changelog: [v0.1.0-alpha.11...v0.1.0-alpha.12](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.11...v0.1.0-alpha.12)
### Features
* **api:** update via SDK Studio ([7739340](https://github.com/sst/opencode-sdk-js/commit/77393403648067fe937637c39e80067c347a8c5b))
## 0.1.0-alpha.11 (2025-06-30)
Full Changelog: [v0.1.0-alpha.10...v0.1.0-alpha.11](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.10...v0.1.0-alpha.11)
### Features
* **api:** update via SDK Studio ([2ce98e5](https://github.com/sst/opencode-sdk-js/commit/2ce98e55bf330cca0c38f60f011ffd9063b34ea0))
## 0.1.0-alpha.10 (2025-06-30)
Full Changelog: [v0.1.0-alpha.9...v0.1.0-alpha.10](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.9...v0.1.0-alpha.10)
### Features
* **api:** update via SDK Studio ([fa7c91c](https://github.com/sst/opencode-sdk-js/commit/fa7c91cc2fe52d42be7365ca2c4ce3e48c2e76ac))
### Chores
* **ci:** only run for pushes and fork pull requests ([0e850e5](https://github.com/sst/opencode-sdk-js/commit/0e850e51daac413dcf2d5e30c0ea7a1cd5346c4b))
* **client:** improve path param validation ([bc3ff0e](https://github.com/sst/opencode-sdk-js/commit/bc3ff0ee2de9af8be42deae87d12f003fb5f7aa5))
## 0.1.0-alpha.9 (2025-06-27)
Full Changelog: [v0.1.0-alpha.8...v0.1.0-alpha.9](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.8...v0.1.0-alpha.9)
### Features
* **api:** update via SDK Studio ([7009d10](https://github.com/sst/opencode-sdk-js/commit/7009d10aab99be7102371cee49013ab3edae4450))
* **api:** update via SDK Studio ([e60aa00](https://github.com/sst/opencode-sdk-js/commit/e60aa0024079671e3725ee6f6bfbf8c2dad78da2))
## 0.1.0-alpha.8 (2025-06-27)
Full Changelog: [v0.1.0-alpha.7...v0.1.0-alpha.8](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.7...v0.1.0-alpha.8)
### Features
* **api:** update via SDK Studio ([171e3d5](https://github.com/sst/opencode-sdk-js/commit/171e3d5f3ba69ff9ba8547dac90d85b1a0a137c1))
## 0.1.0-alpha.7 (2025-06-27)
Full Changelog: [v0.1.0-alpha.6...v0.1.0-alpha.7](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.6...v0.1.0-alpha.7)
### Features
* **api:** update via SDK Studio ([14d2d04](https://github.com/sst/opencode-sdk-js/commit/14d2d04d80c1d5880940c9c70a5c1ea200df2ebc))
## 0.1.0-alpha.6 (2025-06-27)
Full Changelog: [v0.1.0-alpha.5...v0.1.0-alpha.6](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.5...v0.1.0-alpha.6)
### Features
* **api:** update via SDK Studio ([45e78b2](https://github.com/sst/opencode-sdk-js/commit/45e78b2f0fca18f537de9986e358aa876fb0b686))
## 0.1.0-alpha.5 (2025-06-27)
Full Changelog: [v0.1.0-alpha.4...v0.1.0-alpha.5](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.4...v0.1.0-alpha.5)
### Features
* **api:** update via SDK Studio ([10a5be9](https://github.com/sst/opencode-sdk-js/commit/10a5be9261c4ba8aeece7bb6921752f5fa6d9f28))
## 0.1.0-alpha.4 (2025-06-27)
Full Changelog: [v0.1.0-alpha.3...v0.1.0-alpha.4](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.3...v0.1.0-alpha.4)
### Features
* **api:** update via SDK Studio ([20dcd17](https://github.com/sst/opencode-sdk-js/commit/20dcd171405b05801e5a56f1b40fd635259b6a94))
## 0.1.0-alpha.3 (2025-06-27)
Full Changelog: [v0.1.0-alpha.2...v0.1.0-alpha.3](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.2...v0.1.0-alpha.3)
### Bug Fixes
* **ci:** release-doctor — report correct token name ([128884f](https://github.com/sst/opencode-sdk-js/commit/128884f4bc64e618177a0b090cd6d52b122a059a))
## 0.1.0-alpha.2 (2025-06-24)
Full Changelog: [v0.1.0-alpha.1...v0.1.0-alpha.2](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.1...v0.1.0-alpha.2)
### Features
* **api:** update via SDK Studio ([2320f32](https://github.com/sst/opencode-sdk-js/commit/2320f32190ab58d15d00d7c3328f9fba2421536c))
## 0.1.0-alpha.1 (2025-06-24)
Full Changelog: [v0.0.1-alpha.0...v0.1.0-alpha.1](https://github.com/sst/opencode-sdk-js/compare/v0.0.1-alpha.0...v0.1.0-alpha.1)
### Features
* **api:** update via SDK Studio ([e448306](https://github.com/sst/opencode-sdk-js/commit/e4483068738cbb10233fca5a9d9d44a9c9815c8b))
* **api:** update via SDK Studio ([b222c96](https://github.com/sst/opencode-sdk-js/commit/b222c96a679a8aeecb60bcf92c247fef90c75b3d))
### Chores
* update SDK settings ([c1481ea](https://github.com/sst/opencode-sdk-js/commit/c1481ea7949c1422bedaeac278600b4ec3f58038))
+107
View File
@@ -0,0 +1,107 @@
## Setting up the environment
This repository uses [`yarn@v1`](https://classic.yarnpkg.com/lang/en/docs/install).
Other package managers may work but are not officially supported for development.
To set up the repository, run:
```sh
$ yarn
$ yarn build
```
This will install all the required dependencies and build output files to `dist/`.
## Modifying/Adding code
Most of the SDK is generated code. Modifications to code will be persisted between generations, but may
result in merge conflicts between manual patches and changes from the generator. The generator will never
modify the contents of the `src/lib/` and `examples/` directories.
## Adding and running examples
All files in the `examples/` directory are not modified by the generator and can be freely edited or added to.
```ts
// add an example to examples/<your-example>.ts
#!/usr/bin/env -S npm run tsn -T
```
```sh
$ chmod +x examples/<your-example>.ts
# run the example against your api
$ yarn tsn -T examples/<your-example>.ts
```
## Using the repository from source
If youd like to use the repository from source, you can either install from git or link to a cloned repository:
To install via git:
```sh
$ npm install git+ssh://git@github.com:sst/opencode-sdk-js.git
```
Alternatively, to link a local copy of the repo:
```sh
# Clone
$ git clone https://www.github.com/sst/opencode-sdk-js
$ cd opencode-sdk-js
# With yarn
$ yarn link
$ cd ../my-package
$ yarn link @opencode-ai/sdk
# With pnpm
$ pnpm link --global
$ cd ../my-package
$ pnpm link -—global @opencode-ai/sdk
```
## Running tests
Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests.
```sh
$ npx prism mock path/to/your/openapi.yml
```
```sh
$ yarn run test
```
## Linting and formatting
This repository uses [prettier](https://www.npmjs.com/package/prettier) and
[eslint](https://www.npmjs.com/package/eslint) to format the code in the repository.
To lint:
```sh
$ yarn lint
```
To format and fix all lint issues automatically:
```sh
$ yarn fix
```
## Publishing and releases
Changes made to this repository via the automated release PR pipeline should publish to npm automatically. If
the changes aren't made through the automated pipeline, you may want to make releases manually.
### Publish with a GitHub workflow
You can release to package managers by using [the `Publish NPM` GitHub action](https://www.github.com/sst/opencode-sdk-js/actions/workflows/publish-npm.yml). This requires a setup organization or repository secret to be set up.
### Publish manually
If you need to manually release a package, you can run the `bin/publish-npm` script with an `NPM_TOKEN` set on
the environment.
+7
View File
@@ -0,0 +1,7 @@
Copyright 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.
+370
View File
@@ -0,0 +1,370 @@
# Opencode TypeScript API Library
[![NPM version](<https://img.shields.io/npm/v/@opencode-ai/sdk.svg?label=npm%20(stable)>)](https://npmjs.org/package/@opencode-ai/sdk) ![npm bundle size](https://img.shields.io/bundlephobia/minzip/@opencode-ai/sdk)
This library provides convenient access to the Opencode REST API from server-side TypeScript or JavaScript.
The REST API documentation can be found on [opencode.ai](https://opencode.ai/docs). The full API of this library can be found in [api.md](api.md).
It is generated with [Stainless](https://www.stainless.com/).
## Installation
```sh
npm install @opencode-ai/sdk
```
## Usage
The full API of this library can be found in [api.md](api.md).
<!-- prettier-ignore -->
```js
import Opencode from '@opencode-ai/sdk';
const client = new Opencode();
const sessions = await client.session.list();
```
## Streaming responses
We provide support for streaming responses using Server Sent Events (SSE).
```ts
import Opencode from '@opencode-ai/sdk';
const client = new Opencode();
const stream = await client.event.list();
for await (const eventListResponse of stream) {
console.log(eventListResponse);
}
```
If you need to cancel a stream, you can `break` from the loop
or call `stream.controller.abort()`.
### Request & Response types
This library includes TypeScript definitions for all request params and response fields. You may import and use them like so:
<!-- prettier-ignore -->
```ts
import Opencode from '@opencode-ai/sdk';
const client = new Opencode();
const sessions: Opencode.SessionListResponse = await client.session.list();
```
Documentation for each method, request param, and response field are available in docstrings and will appear on hover in most modern editors.
## Handling errors
When the library is unable to connect to the API,
or if the API returns a non-success status code (i.e., 4xx or 5xx response),
a subclass of `APIError` will be thrown:
<!-- prettier-ignore -->
```ts
const sessions = await client.session.list().catch(async (err) => {
if (err instanceof Opencode.APIError) {
console.log(err.status); // 400
console.log(err.name); // BadRequestError
console.log(err.headers); // {server: 'nginx', ...}
} else {
throw err;
}
});
```
Error codes are as follows:
| Status Code | Error Type |
| ----------- | -------------------------- |
| 400 | `BadRequestError` |
| 401 | `AuthenticationError` |
| 403 | `PermissionDeniedError` |
| 404 | `NotFoundError` |
| 422 | `UnprocessableEntityError` |
| 429 | `RateLimitError` |
| >=500 | `InternalServerError` |
| N/A | `APIConnectionError` |
### Retries
Certain errors will be automatically retried 2 times by default, with a short exponential backoff.
Connection errors (for example, due to a network connectivity problem), 408 Request Timeout, 409 Conflict,
429 Rate Limit, and >=500 Internal errors will all be retried by default.
You can use the `maxRetries` option to configure or disable this:
<!-- prettier-ignore -->
```js
// Configure the default for all requests:
const client = new Opencode({
maxRetries: 0, // default is 2
});
// Or, configure per-request:
await client.session.list({
maxRetries: 5,
});
```
### Timeouts
Requests time out after 1 minute by default. You can configure this with a `timeout` option:
<!-- prettier-ignore -->
```ts
// Configure the default for all requests:
const client = new Opencode({
timeout: 20 * 1000, // 20 seconds (default is 1 minute)
});
// Override per-request:
await client.session.list({
timeout: 5 * 1000,
});
```
On timeout, an `APIConnectionTimeoutError` is thrown.
Note that requests which time out will be [retried twice by default](#retries).
## Advanced Usage
### Accessing raw Response data (e.g., headers)
The "raw" `Response` returned by `fetch()` can be accessed through the `.asResponse()` method on the `APIPromise` type that all methods return.
This method returns as soon as the headers for a successful response are received and does not consume the response body, so you are free to write custom parsing or streaming logic.
You can also use the `.withResponse()` method to get the raw `Response` along with the parsed data.
Unlike `.asResponse()` this method consumes the body, returning once it is parsed.
<!-- prettier-ignore -->
```ts
const client = new Opencode();
const response = await client.session.list().asResponse();
console.log(response.headers.get('X-My-Header'));
console.log(response.statusText); // access the underlying Response object
const { data: sessions, response: raw } = await client.session.list().withResponse();
console.log(raw.headers.get('X-My-Header'));
console.log(sessions);
```
### Logging
> [!IMPORTANT]
> All log messages are intended for debugging only. The format and content of log messages
> may change between releases.
#### Log levels
The log level can be configured in two ways:
1. Via the `OPENCODE_LOG` environment variable
2. Using the `logLevel` client option (overrides the environment variable if set)
```ts
import Opencode from '@opencode-ai/sdk';
const client = new Opencode({
logLevel: 'debug', // Show all log messages
});
```
Available log levels, from most to least verbose:
- `'debug'` - Show debug messages, info, warnings, and errors
- `'info'` - Show info messages, warnings, and errors
- `'warn'` - Show warnings and errors (default)
- `'error'` - Show only errors
- `'off'` - Disable all logging
At the `'debug'` level, all HTTP requests and responses are logged, including headers and bodies.
Some authentication-related headers are redacted, but sensitive data in request and response bodies
may still be visible.
#### Custom logger
By default, this library logs to `globalThis.console`. You can also provide a custom logger.
Most logging libraries are supported, including [pino](https://www.npmjs.com/package/pino), [winston](https://www.npmjs.com/package/winston), [bunyan](https://www.npmjs.com/package/bunyan), [consola](https://www.npmjs.com/package/consola), [signale](https://www.npmjs.com/package/signale), and [@std/log](https://jsr.io/@std/log). If your logger doesn't work, please open an issue.
When providing a custom logger, the `logLevel` option still controls which messages are emitted, messages
below the configured level will not be sent to your logger.
```ts
import Opencode from '@opencode-ai/sdk';
import pino from 'pino';
const logger = pino();
const client = new Opencode({
logger: logger.child({ name: 'Opencode' }),
logLevel: 'debug', // Send all messages to pino, allowing it to filter
});
```
### Making custom/undocumented requests
This library is typed for convenient access to the documented API. If you need to access undocumented
endpoints, params, or response properties, the library can still be used.
#### Undocumented endpoints
To make requests to undocumented endpoints, you can use `client.get`, `client.post`, and other HTTP verbs.
Options on the client, such as retries, will be respected when making these requests.
```ts
await client.post('/some/path', {
body: { some_prop: 'foo' },
query: { some_query_arg: 'bar' },
});
```
#### Undocumented request params
To make requests using undocumented parameters, you may use `// @ts-expect-error` on the undocumented
parameter. This library doesn't validate at runtime that the request matches the type, so any extra values you
send will be sent as-is.
```ts
client.session.list({
// ...
// @ts-expect-error baz is not yet public
baz: 'undocumented option',
});
```
For requests with the `GET` verb, any extra params will be in the query, all other requests will send the
extra param in the body.
If you want to explicitly send an extra argument, you can do so with the `query`, `body`, and `headers` request
options.
#### Undocumented response properties
To access undocumented response properties, you may access the response object with `// @ts-expect-error` on
the response object, or cast the response object to the requisite type. Like the request params, we do not
validate or strip extra properties from the response from the API.
### Customizing the fetch client
By default, this library expects a global `fetch` function is defined.
If you want to use a different `fetch` function, you can either polyfill the global:
```ts
import fetch from 'my-fetch';
globalThis.fetch = fetch;
```
Or pass it to the client:
```ts
import Opencode from '@opencode-ai/sdk';
import fetch from 'my-fetch';
const client = new Opencode({ fetch });
```
### Fetch options
If you want to set custom `fetch` options without overriding the `fetch` function, you can provide a `fetchOptions` object when instantiating the client or making a request. (Request-specific options override client options.)
```ts
import Opencode from '@opencode-ai/sdk';
const client = new Opencode({
fetchOptions: {
// `RequestInit` options
},
});
```
#### Configuring proxies
To modify proxy behavior, you can provide custom `fetchOptions` that add runtime-specific proxy
options to requests:
<img src="https://raw.githubusercontent.com/stainless-api/sdk-assets/refs/heads/main/node.svg" align="top" width="18" height="21"> **Node** <sup>[[docs](https://github.com/nodejs/undici/blob/main/docs/docs/api/ProxyAgent.md#example---proxyagent-with-fetch)]</sup>
```ts
import Opencode from '@opencode-ai/sdk';
import * as undici from 'undici';
const proxyAgent = new undici.ProxyAgent('http://localhost:8888');
const client = new Opencode({
fetchOptions: {
dispatcher: proxyAgent,
},
});
```
<img src="https://raw.githubusercontent.com/stainless-api/sdk-assets/refs/heads/main/bun.svg" align="top" width="18" height="21"> **Bun** <sup>[[docs](https://bun.sh/guides/http/proxy)]</sup>
```ts
import Opencode from '@opencode-ai/sdk';
const client = new Opencode({
fetchOptions: {
proxy: 'http://localhost:8888',
},
});
```
<img src="https://raw.githubusercontent.com/stainless-api/sdk-assets/refs/heads/main/deno.svg" align="top" width="18" height="21"> **Deno** <sup>[[docs](https://docs.deno.com/api/deno/~/Deno.createHttpClient)]</sup>
```ts
import Opencode from 'npm:@opencode-ai/sdk';
const httpClient = Deno.createHttpClient({ proxy: { url: 'http://localhost:8888' } });
const client = new Opencode({
fetchOptions: {
client: httpClient,
},
});
```
## Frequently Asked Questions
## Semantic versioning
This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions:
1. Changes that only affect static types, without breaking runtime behavior.
2. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals.)_
3. Changes that we do not expect to impact the vast majority of users in practice.
We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience.
We are keen for your feedback; please open an [issue](https://www.github.com/sst/opencode-sdk-js/issues) with questions, bugs, or suggestions.
## Requirements
TypeScript >= 4.9 is supported.
The following runtimes are supported:
- Web browsers (Up-to-date Chrome, Firefox, Safari, Edge, and more)
- Node.js 20 LTS or later ([non-EOL](https://endoflife.date/nodejs)) versions.
- Deno v1.28.0 or higher.
- Bun 1.0 or later.
- Cloudflare Workers.
- Vercel Edge Runtime.
- Jest 28 or greater with the `"node"` environment (`"jsdom"` is not supported at this time).
- Nitro v2.6 or greater.
Note that React Native is not supported at this time.
If you are interested in other runtime environments, please open or upvote an issue on GitHub.
## Contributing
See [the contributing documentation](./CONTRIBUTING.md).
+27
View File
@@ -0,0 +1,27 @@
# Security Policy
## Reporting Security Issues
This SDK is generated by [Stainless Software Inc](http://stainless.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken.
To report a security issue, please contact the Stainless team at security@stainless.com.
## Responsible Disclosure
We appreciate the efforts of security researchers and individuals who help us maintain the security of
SDKs we generate. If you believe you have found a security vulnerability, please adhere to responsible
disclosure practices by allowing us a reasonable amount of time to investigate and address the issue
before making any information public.
## Reporting Non-SDK Related Security Issues
If you encounter security issues that are not directly related to SDKs but pertain to the services
or products provided by Opencode, please follow the respective company's security reporting guidelines.
### Opencode Terms and Policies
Please contact support@sst.dev for any questions or concerns regarding the security of our services.
---
Thank you for helping us keep the SDKs and systems they interact with secure.
+139
View File
@@ -0,0 +1,139 @@
# Shared
Types:
- <code><a href="./src/resources/shared.ts">MessageAbortedError</a></code>
- <code><a href="./src/resources/shared.ts">ProviderAuthError</a></code>
- <code><a href="./src/resources/shared.ts">UnknownError</a></code>
# Event
Types:
- <code><a href="./src/resources/event.ts">EventListResponse</a></code>
Methods:
- <code title="get /event">client.event.<a href="./src/resources/event.ts">list</a>() -> EventListResponse</code>
# App
Types:
- <code><a href="./src/resources/app.ts">App</a></code>
- <code><a href="./src/resources/app.ts">Mode</a></code>
- <code><a href="./src/resources/app.ts">Model</a></code>
- <code><a href="./src/resources/app.ts">Provider</a></code>
- <code><a href="./src/resources/app.ts">AppInitResponse</a></code>
- <code><a href="./src/resources/app.ts">AppLogResponse</a></code>
- <code><a href="./src/resources/app.ts">AppModesResponse</a></code>
- <code><a href="./src/resources/app.ts">AppProvidersResponse</a></code>
Methods:
- <code title="get /app">client.app.<a href="./src/resources/app.ts">get</a>() -> App</code>
- <code title="post /app/init">client.app.<a href="./src/resources/app.ts">init</a>() -> AppInitResponse</code>
- <code title="post /log">client.app.<a href="./src/resources/app.ts">log</a>({ ...params }) -> AppLogResponse</code>
- <code title="get /mode">client.app.<a href="./src/resources/app.ts">modes</a>() -> AppModesResponse</code>
- <code title="get /config/providers">client.app.<a href="./src/resources/app.ts">providers</a>() -> AppProvidersResponse</code>
# Find
Types:
- <code><a href="./src/resources/find.ts">Symbol</a></code>
- <code><a href="./src/resources/find.ts">FindFilesResponse</a></code>
- <code><a href="./src/resources/find.ts">FindSymbolsResponse</a></code>
- <code><a href="./src/resources/find.ts">FindTextResponse</a></code>
Methods:
- <code title="get /find/file">client.find.<a href="./src/resources/find.ts">files</a>({ ...params }) -> FindFilesResponse</code>
- <code title="get /find/symbol">client.find.<a href="./src/resources/find.ts">symbols</a>({ ...params }) -> FindSymbolsResponse</code>
- <code title="get /find">client.find.<a href="./src/resources/find.ts">text</a>({ ...params }) -> FindTextResponse</code>
# File
Types:
- <code><a href="./src/resources/file.ts">File</a></code>
- <code><a href="./src/resources/file.ts">FileReadResponse</a></code>
- <code><a href="./src/resources/file.ts">FileStatusResponse</a></code>
Methods:
- <code title="get /file">client.file.<a href="./src/resources/file.ts">read</a>({ ...params }) -> FileReadResponse</code>
- <code title="get /file/status">client.file.<a href="./src/resources/file.ts">status</a>() -> FileStatusResponse</code>
# Config
Types:
- <code><a href="./src/resources/config.ts">Config</a></code>
- <code><a href="./src/resources/config.ts">KeybindsConfig</a></code>
- <code><a href="./src/resources/config.ts">McpLocalConfig</a></code>
- <code><a href="./src/resources/config.ts">McpRemoteConfig</a></code>
- <code><a href="./src/resources/config.ts">ModeConfig</a></code>
Methods:
- <code title="get /config">client.config.<a href="./src/resources/config.ts">get</a>() -> Config</code>
# Session
Types:
- <code><a href="./src/resources/session.ts">AssistantMessage</a></code>
- <code><a href="./src/resources/session.ts">FilePart</a></code>
- <code><a href="./src/resources/session.ts">FilePartInput</a></code>
- <code><a href="./src/resources/session.ts">FilePartSource</a></code>
- <code><a href="./src/resources/session.ts">FilePartSourceText</a></code>
- <code><a href="./src/resources/session.ts">FileSource</a></code>
- <code><a href="./src/resources/session.ts">Message</a></code>
- <code><a href="./src/resources/session.ts">Part</a></code>
- <code><a href="./src/resources/session.ts">Session</a></code>
- <code><a href="./src/resources/session.ts">SnapshotPart</a></code>
- <code><a href="./src/resources/session.ts">StepFinishPart</a></code>
- <code><a href="./src/resources/session.ts">StepStartPart</a></code>
- <code><a href="./src/resources/session.ts">SymbolSource</a></code>
- <code><a href="./src/resources/session.ts">TextPart</a></code>
- <code><a href="./src/resources/session.ts">TextPartInput</a></code>
- <code><a href="./src/resources/session.ts">ToolPart</a></code>
- <code><a href="./src/resources/session.ts">ToolStateCompleted</a></code>
- <code><a href="./src/resources/session.ts">ToolStateError</a></code>
- <code><a href="./src/resources/session.ts">ToolStatePending</a></code>
- <code><a href="./src/resources/session.ts">ToolStateRunning</a></code>
- <code><a href="./src/resources/session.ts">UserMessage</a></code>
- <code><a href="./src/resources/session.ts">SessionListResponse</a></code>
- <code><a href="./src/resources/session.ts">SessionDeleteResponse</a></code>
- <code><a href="./src/resources/session.ts">SessionAbortResponse</a></code>
- <code><a href="./src/resources/session.ts">SessionInitResponse</a></code>
- <code><a href="./src/resources/session.ts">SessionMessagesResponse</a></code>
- <code><a href="./src/resources/session.ts">SessionSummarizeResponse</a></code>
Methods:
- <code title="post /session">client.session.<a href="./src/resources/session.ts">create</a>() -> Session</code>
- <code title="get /session">client.session.<a href="./src/resources/session.ts">list</a>() -> SessionListResponse</code>
- <code title="delete /session/{id}">client.session.<a href="./src/resources/session.ts">delete</a>(id) -> SessionDeleteResponse</code>
- <code title="post /session/{id}/abort">client.session.<a href="./src/resources/session.ts">abort</a>(id) -> SessionAbortResponse</code>
- <code title="post /session/{id}/message">client.session.<a href="./src/resources/session.ts">chat</a>(id, { ...params }) -> AssistantMessage</code>
- <code title="post /session/{id}/init">client.session.<a href="./src/resources/session.ts">init</a>(id, { ...params }) -> SessionInitResponse</code>
- <code title="get /session/{id}/message">client.session.<a href="./src/resources/session.ts">messages</a>(id) -> SessionMessagesResponse</code>
- <code title="post /session/{id}/revert">client.session.<a href="./src/resources/session.ts">revert</a>(id, { ...params }) -> Session</code>
- <code title="post /session/{id}/share">client.session.<a href="./src/resources/session.ts">share</a>(id) -> Session</code>
- <code title="post /session/{id}/summarize">client.session.<a href="./src/resources/session.ts">summarize</a>(id, { ...params }) -> SessionSummarizeResponse</code>
- <code title="post /session/{id}/unrevert">client.session.<a href="./src/resources/session.ts">unrevert</a>(id) -> Session</code>
- <code title="delete /session/{id}/share">client.session.<a href="./src/resources/session.ts">unshare</a>(id) -> Session</code>
# Tui
Types:
- <code><a href="./src/resources/tui.ts">TuiAppendPromptResponse</a></code>
- <code><a href="./src/resources/tui.ts">TuiOpenHelpResponse</a></code>
Methods:
- <code title="post /tui/append-prompt">client.tui.<a href="./src/resources/tui.ts">appendPrompt</a>({ ...params }) -> TuiAppendPromptResponse</code>
- <code title="post /tui/open-help">client.tui.<a href="./src/resources/tui.ts">openHelp</a>() -> TuiOpenHelpResponse</code>
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
errors=()
if [ -z "${NPM_TOKEN}" ]; then
errors+=("The NPM_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets")
fi
lenErrors=${#errors[@]}
if [[ lenErrors -gt 0 ]]; then
echo -e "Found the following errors in the release environment:\n"
for error in "${errors[@]}"; do
echo -e "- $error\n"
done
exit 1
fi
echo "The environment is ready to push releases!"
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env bash
set -eux
npm config set '//registry.npmjs.org/:_authToken' "$NPM_TOKEN"
yarn build
cd dist
# Get package name and version from package.json
PACKAGE_NAME="$(jq -r -e '.name' ./package.json)"
VERSION="$(jq -r -e '.version' ./package.json)"
# Get latest version from npm
#
# If the package doesn't exist, npm will return:
# {
# "error": {
# "code": "E404",
# "summary": "Unpublished on 2025-06-05T09:54:53.528Z",
# "detail": "'the_package' is not in this registry..."
# }
# }
NPM_INFO="$(npm view "$PACKAGE_NAME" version --json 2>/dev/null || true)"
# Check if we got an E404 error
if echo "$NPM_INFO" | jq -e '.error.code == "E404"' > /dev/null 2>&1; then
# Package doesn't exist yet, no last version
LAST_VERSION=""
elif echo "$NPM_INFO" | jq -e '.error' > /dev/null 2>&1; then
# Report other errors
echo "ERROR: npm returned unexpected data:"
echo "$NPM_INFO"
exit 1
else
# Success - get the version
LAST_VERSION=$(echo "$NPM_INFO" | jq -r '.') # strip quotes
fi
# Check if current version is pre-release (e.g. alpha / beta / rc)
CURRENT_IS_PRERELEASE=false
if [[ "$VERSION" =~ -([a-zA-Z]+) ]]; then
CURRENT_IS_PRERELEASE=true
CURRENT_TAG="${BASH_REMATCH[1]}"
fi
# Check if last version is a stable release
LAST_IS_STABLE_RELEASE=true
if [[ -z "$LAST_VERSION" || "$LAST_VERSION" =~ -([a-zA-Z]+) ]]; then
LAST_IS_STABLE_RELEASE=false
fi
# Use a corresponding alpha/beta tag if there already is a stable release and we're publishing a prerelease.
if $CURRENT_IS_PRERELEASE && $LAST_IS_STABLE_RELEASE; then
TAG="$CURRENT_TAG"
else
TAG="latest"
fi
# Publish with the appropriate tag
yarn publish --access public --tag "$TAG"
+42
View File
@@ -0,0 +1,42 @@
// @ts-check
import tseslint from 'typescript-eslint';
import unusedImports from 'eslint-plugin-unused-imports';
import prettier from 'eslint-plugin-prettier';
export default tseslint.config(
{
languageOptions: {
parser: tseslint.parser,
parserOptions: { sourceType: 'module' },
},
files: ['**/*.ts', '**/*.mts', '**/*.cts', '**/*.js', '**/*.mjs', '**/*.cjs'],
ignores: ['dist/'],
plugins: {
'@typescript-eslint': tseslint.plugin,
'unused-imports': unusedImports,
prettier,
},
rules: {
'no-unused-vars': 'off',
'prettier/prettier': 'error',
'unused-imports/no-unused-imports': 'error',
'no-restricted-imports': [
'error',
{
patterns: [
{
regex: '^@opencode-ai/sdk(/.*)?',
message: 'Use a relative import, not a package import.',
},
],
},
],
},
},
{
files: ['tests/**', 'examples/**'],
rules: {
'no-restricted-imports': 'off',
},
},
);
+4
View File
@@ -0,0 +1,4 @@
File generated from our OpenAPI spec by Stainless.
This directory can be used to store example files demonstrating usage of this SDK.
It is ignored by Stainless code generation and its content (other than this keep file) won't be touched.
+23
View File
@@ -0,0 +1,23 @@
import type { JestConfigWithTsJest } from 'ts-jest';
const config: JestConfigWithTsJest = {
preset: 'ts-jest/presets/default-esm',
testEnvironment: 'node',
transform: {
'^.+\\.(t|j)sx?$': ['@swc/jest', { sourceMaps: 'inline' }],
},
moduleNameMapper: {
'^@opencode-ai/sdk$': '<rootDir>/src/index.ts',
'^@opencode-ai/sdk/(.*)$': '<rootDir>/src/$1',
},
modulePathIgnorePatterns: [
'<rootDir>/ecosystem-tests/',
'<rootDir>/dist/',
'<rootDir>/deno/',
'<rootDir>/deno_tests/',
'<rootDir>/packages/',
],
testPathIgnorePatterns: ['scripts'],
};
export default config;

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