mirror of
https://fastgit.cc/https://github.com/anomalyco/opencode
synced 2026-05-02 23:04:07 +08:00
Compare commits
189 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a07f37073b | ||
|
|
4d760a1984 | ||
|
|
6b7058fe1c | ||
|
|
1149b984d9 | ||
|
|
81fb1b313e | ||
|
|
3a7a2a838e | ||
|
|
10ae43a121 | ||
|
|
c85b970903 | ||
|
|
7044662cfa | ||
|
|
92656fdf29 | ||
|
|
c65e7aff86 | ||
|
|
e97613ef9f | ||
|
|
827469c725 | ||
|
|
613b5fbe48 | ||
|
|
7ed05962db | ||
|
|
250a86ec52 | ||
|
|
0795a577e0 | ||
|
|
8e5607f9c0 | ||
|
|
d6b3bb0807 | ||
|
|
f307a5ce0b | ||
|
|
151c7ed5a2 | ||
|
|
fc13d057f8 | ||
|
|
fc73d3c523 | ||
|
|
5d871b2075 | ||
|
|
529a171d51 | ||
|
|
8dcd39f5b7 | ||
|
|
88477b3ee7 | ||
|
|
0c7e529e6d | ||
|
|
01f75839a9 | ||
|
|
4306f1a339 | ||
|
|
aa2a5057ac | ||
|
|
284c01018e | ||
|
|
22c9e2942b | ||
|
|
d50ae8e4d4 | ||
|
|
e9074e60cf | ||
|
|
541a7a39d3 | ||
|
|
72e464ac3e | ||
|
|
20bf27feda | ||
|
|
d288d21330 | ||
|
|
34f6ffe1d7 | ||
|
|
a11999137f | ||
|
|
a16554d445 | ||
|
|
2553137395 | ||
|
|
6b6b81556f | ||
|
|
ff23f67ad5 | ||
|
|
8f0644e35b | ||
|
|
3fdd23df16 | ||
|
|
2c82ee592c | ||
|
|
1ad529db59 | ||
|
|
96866e52ce | ||
|
|
507c975e92 | ||
|
|
3e69d5276b | ||
|
|
289a4d9b18 | ||
|
|
12bf5f641d | ||
|
|
2051e85e96 | ||
|
|
12b86829d9 | ||
|
|
6c9ec54129 | ||
|
|
b7b0cdbd7c | ||
|
|
fd98c3189a | ||
|
|
1278353616 | ||
|
|
638ec7bc50 | ||
|
|
38ae7d60aa | ||
|
|
2d1f9fc321 | ||
|
|
ee0c8132db | ||
|
|
c2208fa1f9 | ||
|
|
bf42d8b011 | ||
|
|
0deb85fa45 | ||
|
|
da19b10703 | ||
|
|
80b17dab44 | ||
|
|
6d2ffa82de | ||
|
|
7998c3b5ce | ||
|
|
13def91e9a | ||
|
|
26a40610dd | ||
|
|
db2fbed691 | ||
|
|
3d4c1425d9 | ||
|
|
10c8b49590 | ||
|
|
500cea5ce7 | ||
|
|
5aafab118f | ||
|
|
01f8d3b05d | ||
|
|
99d6a28249 | ||
|
|
5eaf7ab586 | ||
|
|
e4f754eee7 | ||
|
|
f20ef61bc7 | ||
|
|
5611ef8b28 | ||
|
|
bec796e3c3 | ||
|
|
0bd8b2c72f | ||
|
|
5550ce47e1 | ||
|
|
2d84dadc0c | ||
|
|
45c0578b22 | ||
|
|
1ded535175 | ||
|
|
d957ab849b | ||
|
|
4b2e52c834 | ||
|
|
6867658c0f | ||
|
|
b8620395cb | ||
|
|
90d37c98f8 | ||
|
|
c9a40917c2 | ||
|
|
0aa0e740cd | ||
|
|
bb17d14665 | ||
|
|
cd0b2ae032 | ||
|
|
8e8796507d | ||
|
|
cef5c29583 | ||
|
|
acaed1f270 | ||
|
|
cda0dbc195 | ||
|
|
758425a8e4 | ||
|
|
93446df335 | ||
|
|
adc8b90e0f | ||
|
|
733c9903ec | ||
|
|
7306e20361 | ||
|
|
b4c7042c17 | ||
|
|
6965787b33 | ||
|
|
ce064b8b0e | ||
|
|
0fc546fc6b | ||
|
|
77ac9e5ec2 | ||
|
|
af2c0b3695 | ||
|
|
811b22367d | ||
|
|
933d50e25a | ||
|
|
800bee2722 | ||
|
|
5b4fb96c2e | ||
|
|
1d20bf343d | ||
|
|
79d9bf57f7 | ||
|
|
7b63db6a13 | ||
|
|
0e1565449e | ||
|
|
f9a47fe5a3 | ||
|
|
2bf9d5d4ec | ||
|
|
c18f9ece69 | ||
|
|
4e3c73c4f5 | ||
|
|
8bf2eeccd0 | ||
|
|
6232e0fc58 | ||
|
|
a8b4aed446 | ||
|
|
03de0c406d | ||
|
|
faf8da8743 | ||
|
|
3386908fd6 | ||
|
|
5a8847952a | ||
|
|
87d21ebf2b | ||
|
|
a524fc545c | ||
|
|
4316edaf43 | ||
|
|
d845924e8b | ||
|
|
a29b322bdd | ||
|
|
9723ffa7a6 | ||
|
|
f06cd88773 | ||
|
|
9af92b6914 | ||
|
|
8f64c4b312 | ||
|
|
a32877e908 | ||
|
|
6465c9c44a | ||
|
|
4699739814 | ||
|
|
c1d87c32a2 | ||
|
|
9c5d9be33a | ||
|
|
97d9c851e6 | ||
|
|
76bd702992 | ||
|
|
50c453e577 | ||
|
|
86d5b25d18 | ||
|
|
2b44dbdbf1 | ||
|
|
4bbbbac5f6 | ||
|
|
3c3a997d2a | ||
|
|
1676f8b5dd | ||
|
|
c87a7469a0 | ||
|
|
132e26ddbf | ||
|
|
f1da70b1de | ||
|
|
5c9d1910af | ||
|
|
18abcab208 | ||
|
|
01e7dc2d02 | ||
|
|
611854e4b6 | ||
|
|
d56dec4ba7 | ||
|
|
c952e9ae3d | ||
|
|
6470243095 | ||
|
|
c8321cfbd9 | ||
|
|
46c246e01f | ||
|
|
9964d8e6c0 | ||
|
|
df33143396 | ||
|
|
571aeaaea2 | ||
|
|
edfea03917 | ||
|
|
81c88cc742 | ||
|
|
99b9390d80 | ||
|
|
23c30521d8 | ||
|
|
e681d610de | ||
|
|
a1fdeded3e | ||
|
|
2051312d12 | ||
|
|
20cb7a76af | ||
|
|
a493aec174 | ||
|
|
3ce3ac8e61 | ||
|
|
91ad64feda | ||
|
|
60b55f9d92 | ||
|
|
3c6c2bf13b | ||
|
|
d4f9375548 | ||
|
|
28b39f547e | ||
|
|
7520f5efa8 | ||
|
|
eb4cdf4b20 | ||
|
|
9f6fc1c3c5 | ||
|
|
dfede9ae6e |
8
.github/workflows/opencode.yml
vendored
8
.github/workflows/opencode.yml
vendored
@@ -6,7 +6,11 @@ on:
|
||||
|
||||
jobs:
|
||||
opencode:
|
||||
if: startsWith(github.event.comment.body, 'hey 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
|
||||
@@ -17,7 +21,7 @@ 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:
|
||||
|
||||
11
.github/workflows/publish-github-action.yml
vendored
11
.github/workflows/publish-github-action.yml
vendored
@@ -2,6 +2,10 @@ name: publish-github-action
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- "github-v*.*.*"
|
||||
- "!github-v1"
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
@@ -18,12 +22,9 @@ jobs:
|
||||
|
||||
- run: git fetch --force --tags
|
||||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.2.17
|
||||
|
||||
- name: Publish
|
||||
run: |
|
||||
git config --global user.email "opencode@sst.dev"
|
||||
git config --global user.name "opencode"
|
||||
./scripts/publish-github-action.ts
|
||||
./script/publish
|
||||
working-directory: ./github
|
||||
|
||||
36
.github/workflows/publish-vscode.yml
vendored
Normal file
36
.github/workflows/publish-vscode.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: publish-vscode
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- "vscode-v*.*.*"
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.2.17
|
||||
|
||||
- run: git fetch --force --tags
|
||||
- run: bun install -g @vscode/vsce
|
||||
|
||||
- name: Publish
|
||||
run: |
|
||||
bun install
|
||||
./script/publish
|
||||
working-directory: ./sdks/vscode
|
||||
env:
|
||||
VSCE_PAT: ${{ secrets.VSCE_PAT }}
|
||||
OPENVSX_TOKEN: ${{ secrets.OPENVSX_TOKEN }}
|
||||
2
.github/workflows/publish.yml
vendored
2
.github/workflows/publish.yml
vendored
@@ -7,6 +7,8 @@ on:
|
||||
- dev
|
||||
tags:
|
||||
- "*"
|
||||
- "!vscode-v*"
|
||||
- "!github-v*"
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,8 +1,8 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
.opencode
|
||||
.sst
|
||||
.env
|
||||
.idea
|
||||
.vscode
|
||||
openapi.json
|
||||
scratch
|
||||
|
||||
44
.opencode/agent/example-driven-docs-writer.md
Normal file
44
.opencode/agent/example-driven-docs-writer.md
Normal file
@@ -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.
|
||||
@@ -1,5 +1,3 @@
|
||||
# TUI Agent Guidelines
|
||||
|
||||
## Style
|
||||
|
||||
- prefer single word variable/function names
|
||||
|
||||
15
README.md
15
README.md
@@ -33,6 +33,21 @@ paru -S opencode-bin # Arch Linux
|
||||
> [!TIP]
|
||||
> Remove versions older than 0.1.x before installing.
|
||||
|
||||
#### Installation Directory
|
||||
|
||||
The install script respects the following priority order for the installation path:
|
||||
|
||||
1. `$OPENCODE_INSTALL_DIR` - Custom installation directory
|
||||
2. `$XDG_BIN_DIR` - XDG Base Directory Specification compliant path
|
||||
3. `$HOME/bin` - Standard user binary directory (if exists or can be created)
|
||||
4. `$HOME/.opencode/bin` - Default fallback
|
||||
|
||||
```bash
|
||||
# Examples
|
||||
OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash
|
||||
XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash
|
||||
```
|
||||
|
||||
### Documentation
|
||||
|
||||
For more info on how to configure opencode [**head over to our docs**](https://opencode.ai/docs).
|
||||
|
||||
45
STATS.md
45
STATS.md
@@ -1,20 +1,29 @@
|
||||
# Download Stats
|
||||
|
||||
| Date | GitHub Downloads | npm Downloads | Total |
|
||||
| ---------- | ---------------- | --------------- | ----------------- |
|
||||
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
|
||||
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
|
||||
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
|
||||
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
|
||||
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
|
||||
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
|
||||
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
|
||||
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
|
||||
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
|
||||
| 2025-07-10 | 43,796 (+5,744) | 71,402 (+6,934) | 115,198 (+12,678) |
|
||||
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
|
||||
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
|
||||
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
|
||||
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
|
||||
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
|
||||
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
|
||||
| Date | GitHub Downloads | npm Downloads | Total |
|
||||
| ---------- | ---------------- | ---------------- | ----------------- |
|
||||
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
|
||||
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
|
||||
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
|
||||
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
|
||||
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
|
||||
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
|
||||
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
|
||||
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
|
||||
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
|
||||
| 2025-07-10 | 43,796 (+5,744) | 71,402 (+6,934) | 115,198 (+12,678) |
|
||||
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
|
||||
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
|
||||
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
|
||||
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
|
||||
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
|
||||
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
|
||||
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
|
||||
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
|
||||
| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
|
||||
| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
|
||||
| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
|
||||
| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
|
||||
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
|
||||
| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
|
||||
| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
|
||||
|
||||
133
github/README.md
Normal file
133
github/README.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# opencode GitHub Action
|
||||
|
||||
A GitHub Action that integrates [opencode](https://opencode.ai) directly into your GitHub workflow.
|
||||
|
||||
Start your comment with `hey opencode`, and opencode will take action via your GitHub Actions runner.
|
||||
|
||||
## Features
|
||||
|
||||
#### Triage and explain issues
|
||||
|
||||
```
|
||||
hey opencode, explain this issue
|
||||
```
|
||||
|
||||
#### Fix or implement issues - opencode will create a PR with the changes.
|
||||
|
||||
```
|
||||
hi opencode, fix this
|
||||
```
|
||||
|
||||
- Review PRs and make changes
|
||||
|
||||
```
|
||||
Delete the attachment from S3 when the note is removed @opencode-agent
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
Run the following command in the terminal from your GitHub repo:
|
||||
|
||||
```
|
||||
opencode github install
|
||||
```
|
||||
|
||||
This will walk you through installing the GitHub app, configuring 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: |
|
||||
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/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`):
|
||||
|
||||
```
|
||||
cd hello-world
|
||||
```
|
||||
|
||||
2. Run:
|
||||
|
||||
```
|
||||
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 "}}}'
|
||||
```
|
||||
|
||||
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
github/action.yml
Normal file
29
github/action.yml
Normal 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: false
|
||||
|
||||
share:
|
||||
description: "Share the opencode session (defaults to true for public repos)"
|
||||
required: false
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install opencode
|
||||
shell: bash
|
||||
run: curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
- name: Run opencode
|
||||
shell: bash
|
||||
id: run_opencode
|
||||
run: opencode github run
|
||||
env:
|
||||
MODEL: ${{ inputs.model }}
|
||||
SHARE: ${{ inputs.share }}
|
||||
15
github/script/publish
Executable file
15
github/script/publish
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Get the latest Git tag
|
||||
latest_tag=$(git tag --sort=committerdate | grep -E '^github-v[0-9]+\.[0-9]+\.[0-9]+$' | tail -1)
|
||||
if [ -z "$latest_tag" ]; then
|
||||
echo "No tags found"
|
||||
exit 1
|
||||
fi
|
||||
echo "Latest tag: $latest_tag"
|
||||
|
||||
# Update latest tag
|
||||
git tag -d latest
|
||||
git push origin :refs/tags/latest
|
||||
git tag -a latest $latest_tag -m "Update latest to $latest_tag"
|
||||
git push origin latest
|
||||
41
github/script/release
Executable file
41
github/script/release
Executable file
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Parse command line arguments
|
||||
minor=false
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--minor) minor=true; shift 1;;
|
||||
*) echo "Unknown parameter: $1"; exit 1;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Get the latest Git tag
|
||||
git fetch --force --tags
|
||||
latest_tag=$(git tag --sort=committerdate | grep -E '^github-v[0-9]+\.[0-9]+\.[0-9]+$' | tail -1)
|
||||
if [ -z "$latest_tag" ]; then
|
||||
echo "No tags found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Latest tag: $latest_tag"
|
||||
|
||||
# Split the tag into major, minor, and patch numbers
|
||||
IFS='.' read -ra VERSION <<< "$latest_tag"
|
||||
|
||||
if [ "$minor" = true ]; then
|
||||
# Increment the minor version and reset patch to 0
|
||||
minor_number=${VERSION[1]}
|
||||
let "minor_number++"
|
||||
new_version="${VERSION[0]}.$minor_number.0"
|
||||
else
|
||||
# Increment the patch version
|
||||
patch_number=${VERSION[2]}
|
||||
let "patch_number++"
|
||||
new_version="${VERSION[0]}.${VERSION[1]}.$patch_number"
|
||||
fi
|
||||
|
||||
echo "New version: $new_version"
|
||||
|
||||
# Tag
|
||||
git tag $new_version
|
||||
git push --tags
|
||||
1
install
1
install
@@ -186,4 +186,3 @@ if [ -n "${GITHUB_ACTIONS-}" ] && [ "${GITHUB_ACTIONS}" == "true" ]; then
|
||||
echo "$INSTALL_DIR" >> $GITHUB_PATH
|
||||
print_message info "Added $INSTALL_DIR to \$GITHUB_PATH"
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,5 +1,24 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"provider": {
|
||||
"openrouter": {
|
||||
"models": {
|
||||
"moonshotai/kimi-k2": {
|
||||
"options": {
|
||||
"provider": {
|
||||
"order": ["baseten"],
|
||||
"allow_fallbacks": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"huggingface": {
|
||||
"models": {
|
||||
"Qwen/Qwen3-235B-A22B-Instruct-2507:fireworks-ai": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"mcp": {
|
||||
"weather": {
|
||||
"type": "local",
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"typescript": "5.8.2",
|
||||
"@types/node": "22.13.9",
|
||||
"zod": "3.25.49",
|
||||
"ai": "5.0.0-beta.18"
|
||||
"ai": "5.0.0-beta.21"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -288,6 +288,56 @@ export default {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by the GitHub action to get GitHub installation access token given user PAT token (used when testing `opencode github run` locally)
|
||||
*/
|
||||
if (request.method === "POST" && method === "exchange_github_app_token_with_pat") {
|
||||
const body = await request.json<any>()
|
||||
const owner = body.owner
|
||||
const repo = body.repo
|
||||
|
||||
try {
|
||||
// get Authorization header
|
||||
const authHeader = request.headers.get("Authorization")
|
||||
const token = authHeader?.replace(/^Bearer /, "")
|
||||
if (!token) throw new Error("Authorization header is required")
|
||||
|
||||
// Verify permissions
|
||||
const userClient = new Octokit({ auth: token })
|
||||
const { data: repoData } = await userClient.repos.get({ owner, repo })
|
||||
if (!repoData.permissions.admin && !repoData.permissions.push && !repoData.permissions.maintain)
|
||||
throw new Error("User does not have write permissions")
|
||||
|
||||
// Get installation token
|
||||
const auth = createAppAuth({
|
||||
appId: Resource.GITHUB_APP_ID.value,
|
||||
privateKey: Resource.GITHUB_APP_PRIVATE_KEY.value,
|
||||
})
|
||||
const appAuth = await auth({ type: "app" })
|
||||
|
||||
// Lookup installation
|
||||
const appClient = new Octokit({ auth: appAuth.token })
|
||||
const { data: installation } = await appClient.apps.getRepoInstallation({ owner, repo })
|
||||
|
||||
// Get installation token
|
||||
const installationAuth = await auth({ type: "installation", installationId: installation.id })
|
||||
|
||||
return new Response(JSON.stringify({ token: installationAuth.token }), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
} catch (e: any) {
|
||||
let error = e
|
||||
if (e instanceof Error) {
|
||||
error = e.message
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ error }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by the opencode CLI to check if the GitHub app is installed
|
||||
*/
|
||||
|
||||
@@ -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,13 +28,18 @@
|
||||
"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",
|
||||
"@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",
|
||||
"gray-matter": "4.0.3",
|
||||
"hono": "4.7.10",
|
||||
"hono-openapi": "0.4.8",
|
||||
"isomorphic-git": "1.32.1",
|
||||
|
||||
@@ -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,27 +92,34 @@ 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 notes = commits
|
||||
.map((commit: any) => `- ${commit.commit.message.split("\n")[0]}`)
|
||||
.filter((x: string) => {
|
||||
const lower = x.toLowerCase()
|
||||
return (
|
||||
!lower.includes("ignore:") &&
|
||||
!lower.includes("chore:") &&
|
||||
!lower.includes("ci:") &&
|
||||
!lower.includes("wip:") &&
|
||||
!lower.includes("docs:") &&
|
||||
!lower.includes("doc:")
|
||||
)
|
||||
})
|
||||
.join("\n")
|
||||
const raw = commits.map((commit: any) => `- ${commit.commit.message.split("\n").join(" ")}`)
|
||||
console.log(raw)
|
||||
|
||||
const notes =
|
||||
raw
|
||||
.filter((x: string) => {
|
||||
const lower = x.toLowerCase()
|
||||
return (
|
||||
!lower.includes("ignore:") &&
|
||||
!lower.includes("chore:") &&
|
||||
!lower.includes("ci:") &&
|
||||
!lower.includes("wip:") &&
|
||||
!lower.includes("docs:") &&
|
||||
!lower.includes("doc:")
|
||||
)
|
||||
})
|
||||
.join("\n") || "No notable changes"
|
||||
|
||||
if (!dry) await $`gh release create v${version} --title "v${version}" --notes ${notes} ./dist/*.zip`
|
||||
|
||||
@@ -152,6 +161,7 @@ if (!snapshot) {
|
||||
for (const pkg of ["opencode", "opencode-bin"]) {
|
||||
await $`rm -rf ./dist/aur-${pkg}`
|
||||
await $`git clone ssh://aur@aur.archlinux.org/${pkg}.git ./dist/aur-${pkg}`
|
||||
await $`cd ./dist/aur-${pkg} && git checkout master`
|
||||
await Bun.file(`./dist/aur-${pkg}/PKGBUILD`).write(pkgbuild.replace("${pkg}", pkg))
|
||||
await $`cd ./dist/aur-${pkg} && makepkg --printsrcinfo > .SRCINFO`
|
||||
await $`cd ./dist/aur-${pkg} && git add PKGBUILD .SRCINFO`
|
||||
|
||||
102
packages/opencode/src/agent/agent.ts
Normal file
102
packages/opencode/src/agent/agent.ts
Normal 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
packages/opencode/src/agent/generate.txt
Normal file
75
packages/opencode/src/agent/generate.txt
Normal 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.
|
||||
@@ -4,11 +4,12 @@ import path from "path"
|
||||
|
||||
export const AuthCopilot = lazy(async () => {
|
||||
const file = Bun.file(path.join(Global.Path.state, "plugin", "copilot.ts"))
|
||||
const exists = await file.exists()
|
||||
const response = fetch("https://raw.githubusercontent.com/sst/opencode-github-copilot/refs/heads/main/auth.ts")
|
||||
.then((x) => Bun.write(file, x))
|
||||
.catch(() => {})
|
||||
|
||||
if (!file.exists()) {
|
||||
if (!exists) {
|
||||
const worked = await response
|
||||
if (!worked) return
|
||||
}
|
||||
|
||||
@@ -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
packages/opencode/src/cli/cmd/agent.ts
Normal file
110
packages/opencode/src/cli/cmd/agent.ts
Normal 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() {},
|
||||
})
|
||||
@@ -78,6 +78,8 @@ export const AuthLoginCommand = cmd({
|
||||
"github-copilot": 1,
|
||||
openai: 2,
|
||||
google: 3,
|
||||
openrouter: 4,
|
||||
vercel: 5,
|
||||
}
|
||||
let provider = await prompts.select({
|
||||
message: "Select provider",
|
||||
@@ -108,7 +110,7 @@ export const AuthLoginCommand = cmd({
|
||||
if (provider === "other") {
|
||||
provider = await prompts.text({
|
||||
message: "Enter provider id",
|
||||
validate: (x) => (x.match(/^[a-z-]+$/) ? undefined : "a-z and hyphens only"),
|
||||
validate: (x) => (x.match(/^[0-9a-z-]+$/) ? undefined : "a-z, 0-9 and hyphens only"),
|
||||
})
|
||||
if (prompts.isCancel(provider)) throw new UI.CancelledError()
|
||||
provider = provider.replace(/^@ai-sdk\//, "")
|
||||
@@ -120,7 +122,7 @@ export const AuthLoginCommand = cmd({
|
||||
|
||||
if (provider === "amazon-bedrock") {
|
||||
prompts.log.info(
|
||||
"Amazon bedrock can be configured with standard AWS environment variables like AWS_PROFILE or AWS_ACCESS_KEY_ID",
|
||||
"Amazon bedrock can be configured with standard AWS environment variables like AWS_BEARER_TOKEN_BEDROCK, AWS_PROFILE or AWS_ACCESS_KEY_ID",
|
||||
)
|
||||
prompts.outro("Done")
|
||||
return
|
||||
@@ -265,6 +267,10 @@ export const AuthLoginCommand = cmd({
|
||||
return
|
||||
}
|
||||
|
||||
if (provider === "vercel") {
|
||||
prompts.log.info("You can create an api key in the dashboard")
|
||||
}
|
||||
|
||||
const key = await prompts.password({
|
||||
message: "Enter your API key",
|
||||
validate: (x) => (x.length > 0 ? undefined : "Required"),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -4,49 +4,30 @@ 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).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")
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const DiffCommand = cmd({
|
||||
command: "diff <commit>",
|
||||
describe: "diff",
|
||||
builder: (yargs) =>
|
||||
yargs.positional("commit", {
|
||||
type: "string",
|
||||
description: "commit",
|
||||
demandOption: true,
|
||||
}),
|
||||
async handler(args) {
|
||||
await bootstrap({ cwd: process.cwd() }, async () => {
|
||||
const diff = await Snapshot.diff("test", args.commit)
|
||||
console.log(diff)
|
||||
console.log(await Snapshot.patch(args.hash))
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Server } from "../../server/server"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import type { CommandModule } from "yargs"
|
||||
|
||||
export const GenerateCommand = {
|
||||
@@ -10,6 +9,6 @@ export const GenerateCommand = {
|
||||
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
|
||||
|
||||
1112
packages/opencode/src/cli/cmd/github.ts
Normal file
1112
packages/opencode/src/cli/cmd/github.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,235 +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,
|
||||
})),
|
||||
),
|
||||
{
|
||||
value: "other",
|
||||
label: "Other",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
if (prompts.isCancel(provider)) throw new UI.CancelledError()
|
||||
if (provider === "other") {
|
||||
provider = await prompts.text({
|
||||
message: "Enter provider id",
|
||||
validate: (x) => (x.match(/^[a-z-]+$/) ? undefined : "a-z and hyphens only"),
|
||||
})
|
||||
if (prompts.isCancel(provider)) throw new UI.CancelledError()
|
||||
provider = provider.replace(/^@ai-sdk\//, "")
|
||||
if (prompts.isCancel(provider)) throw new UI.CancelledError()
|
||||
prompts.log.warn(
|
||||
`This only stores a credential for ${provider} - you will need configure it in opencode.json, check the docs for examples.`,
|
||||
)
|
||||
}
|
||||
|
||||
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, 'hey opencode')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run opencode
|
||||
uses: sst/opencode/sdks/github@github-v1${envStr}
|
||||
with:
|
||||
model: ${provider}/${model}
|
||||
`.trim(),
|
||||
)
|
||||
|
||||
prompts.log.success(`Added workflow file: "${WORKFLOW_FILE}"`)
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -172,8 +172,6 @@ export const RunCommand = cmd({
|
||||
parts: [
|
||||
{
|
||||
id: Identifier.ascending("part"),
|
||||
sessionID: session.id,
|
||||
messageID: messageID,
|
||||
type: "text",
|
||||
text: message,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"]
|
||||
@@ -116,6 +128,16 @@ export const TuiCommand = cmd({
|
||||
})
|
||||
.catch(() => {})
|
||||
})()
|
||||
;(async () => {
|
||||
if (Ide.alreadyInstalled()) return
|
||||
const ide = await Ide.ide()
|
||||
if (ide === "unknown") return
|
||||
await Ide.install(ide)
|
||||
.then(() => {
|
||||
Bus.publish(Ide.Event.Installed, { ide })
|
||||
})
|
||||
.catch(() => {})
|
||||
})()
|
||||
|
||||
await proc.exited
|
||||
server.stop()
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Global } from "../global"
|
||||
import fs from "fs/promises"
|
||||
import { lazy } from "../util/lazy"
|
||||
import { NamedError } from "../util/error"
|
||||
import matter from "gray-matter"
|
||||
|
||||
export namespace Config {
|
||||
const log = Log.create({ service: "config" })
|
||||
@@ -22,18 +23,43 @@ export namespace Config {
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
result.username = os.userInfo().username
|
||||
}
|
||||
if (!result.layout) {
|
||||
result.layout = "auto"
|
||||
}
|
||||
|
||||
log.info("loaded", result)
|
||||
|
||||
@@ -60,6 +86,7 @@ export namespace Config {
|
||||
type: z.literal("remote").describe("Type of MCP server connection"),
|
||||
url: z.string().describe("URL of the remote MCP server"),
|
||||
enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"),
|
||||
headers: z.record(z.string(), z.string()).optional().describe("Headers to send with the request"),
|
||||
})
|
||||
.strict()
|
||||
.openapi({
|
||||
@@ -72,14 +99,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"),
|
||||
@@ -91,7 +126,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"),
|
||||
@@ -120,7 +155,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()
|
||||
@@ -139,9 +176,11 @@ export namespace Config {
|
||||
theme: z.string().optional().describe("Theme name to use for the interface"),
|
||||
keybinds: Keybinds.optional().describe("Custom keybind configurations"),
|
||||
share: z
|
||||
.enum(["auto", "disabled"])
|
||||
.enum(["manual", "auto", "disabled"])
|
||||
.optional()
|
||||
.describe("Control sharing behavior: 'auto' enables automatic sharing, 'disabled' disables all sharing"),
|
||||
.describe(
|
||||
"Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing",
|
||||
),
|
||||
autoshare: z
|
||||
.boolean()
|
||||
.optional()
|
||||
@@ -149,6 +188,12 @@ export namespace Config {
|
||||
autoupdate: z.boolean().optional().describe("Automatically update to the latest version"),
|
||||
disabled_providers: z.array(z.string()).optional().describe("Disable providers that are loaded automatically"),
|
||||
model: z.string().describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(),
|
||||
small_model: z
|
||||
.string()
|
||||
.describe(
|
||||
"Small model to use for tasks like summarization and title generation in the format of provider/model",
|
||||
)
|
||||
.optional(),
|
||||
username: z
|
||||
.string()
|
||||
.optional()
|
||||
@@ -161,19 +206,33 @@ export namespace Config {
|
||||
.catchall(Mode)
|
||||
.optional()
|
||||
.describe("Modes configuration, see https://opencode.ai/docs/modes"),
|
||||
log_level: Log.Level.optional().describe("Minimum log level to write to log files"),
|
||||
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"),
|
||||
mcp: z.record(z.string(), Mcp).optional().describe("MCP (Model Context Protocol) server configurations"),
|
||||
instructions: z.array(z.string()).optional().describe("Additional instruction files or patterns to include"),
|
||||
layout: Layout.optional().describe("Layout to use for the TUI"),
|
||||
layout: Layout.optional().describe("@deprecated Always uses stretch layout."),
|
||||
experimental: z
|
||||
.object({
|
||||
hook: z
|
||||
|
||||
@@ -31,9 +31,13 @@ export namespace ConfigHooks {
|
||||
}
|
||||
})
|
||||
|
||||
Bus.subscribe(Session.Event.Idle, async () => {
|
||||
Bus.subscribe(Session.Event.Idle, async (payload) => {
|
||||
const cfg = await Config.get()
|
||||
if (cfg.experimental?.hook?.session_completed) {
|
||||
const session = await Session.get(payload.properties.sessionID)
|
||||
// Only fire hook for top-level sessions (not subagent sessions)
|
||||
if (session.parentID) return
|
||||
|
||||
for (const item of cfg.experimental.hook.session_completed) {
|
||||
log.info("session_completed", {
|
||||
command: item.command,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -105,7 +105,29 @@ export const ruff: Info = {
|
||||
command: ["ruff", "format", "$FILE"],
|
||||
extensions: [".py", ".pyi"],
|
||||
async enabled() {
|
||||
return Bun.which("ruff") !== null
|
||||
if (!Bun.which("ruff")) return false
|
||||
const app = App.info()
|
||||
const configs = ["pyproject.toml", "ruff.toml", ".ruff.toml"]
|
||||
for (const config of configs) {
|
||||
const found = await Filesystem.findUp(config, app.path.cwd, app.path.root)
|
||||
if (found.length > 0) {
|
||||
if (config === "pyproject.toml") {
|
||||
const content = await Bun.file(found[0]).text()
|
||||
if (content.includes("[tool.ruff]")) return true
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
const deps = ["requirements.txt", "pyproject.toml", "Pipfile"]
|
||||
for (const dep of deps) {
|
||||
const found = await Filesystem.findUp(dep, app.path.cwd, app.path.root)
|
||||
if (found.length > 0) {
|
||||
const content = await Bun.file(found[0]).text()
|
||||
if (content.includes("ruff")) return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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,11 +22,10 @@ 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 }),
|
||||
])
|
||||
|
||||
const CACHE_VERSION = "2"
|
||||
const CACHE_VERSION = "3"
|
||||
|
||||
const version = await Bun.file(path.join(Global.Path.cache, "version"))
|
||||
.text()
|
||||
|
||||
74
packages/opencode/src/ide/index.ts
Normal file
74
packages/opencode/src/ide/index.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { $ } from "bun"
|
||||
import { z } from "zod"
|
||||
import { NamedError } from "../util/error"
|
||||
import { Log } from "../util/log"
|
||||
import { Bus } from "../bus"
|
||||
|
||||
const SUPPORTED_IDES = ["Windsurf", "Visual Studio Code", "Cursor", "VSCodium"] as const
|
||||
|
||||
export namespace Ide {
|
||||
const log = Log.create({ service: "ide" })
|
||||
|
||||
export const Event = {
|
||||
Installed: Bus.event(
|
||||
"ide.installed",
|
||||
z.object({
|
||||
ide: z.string(),
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
export type Ide = Awaited<ReturnType<typeof ide>>
|
||||
|
||||
export const AlreadyInstalledError = NamedError.create("AlreadyInstalledError", z.object({}))
|
||||
|
||||
export const InstallFailedError = NamedError.create(
|
||||
"InstallFailedError",
|
||||
z.object({
|
||||
stderr: z.string(),
|
||||
}),
|
||||
)
|
||||
|
||||
export async function ide() {
|
||||
if (process.env["TERM_PROGRAM"] === "vscode") {
|
||||
const v = process.env["GIT_ASKPASS"]
|
||||
for (const ide of SUPPORTED_IDES) {
|
||||
if (v?.includes(ide)) return ide
|
||||
}
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
export function alreadyInstalled() {
|
||||
return process.env["OPENCODE_CALLER"] === "vscode"
|
||||
}
|
||||
|
||||
export async function install(ide: Ide) {
|
||||
const cmd = (() => {
|
||||
switch (ide) {
|
||||
case "Windsurf":
|
||||
return $`windsurf --install-extension sst-dev.opencode`
|
||||
case "Visual Studio Code":
|
||||
return $`code --install-extension sst-dev.opencode`
|
||||
case "Cursor":
|
||||
return $`cursor --install-extension sst-dev.opencode`
|
||||
case "VSCodium":
|
||||
return $`codium --install-extension sst-dev.opencode`
|
||||
default:
|
||||
throw new Error(`Unknown IDE: ${ide}`)
|
||||
}
|
||||
})()
|
||||
// TODO: check OPENCODE_CALLER
|
||||
const result = await cmd.quiet().throws(false)
|
||||
log.info("installed", {
|
||||
ide,
|
||||
stdout: result.stdout.toString(),
|
||||
stderr: result.stderr.toString(),
|
||||
})
|
||||
if (result.exitCode !== 0)
|
||||
throw new InstallFailedError({
|
||||
stderr: result.stderr.toString("utf8"),
|
||||
})
|
||||
if (result.stdout.toString().includes("already installed")) throw new AlreadyInstalledError({})
|
||||
}
|
||||
}
|
||||
@@ -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,10 @@ 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()
|
||||
|
||||
const cancel = new AbortController()
|
||||
|
||||
@@ -41,25 +45,21 @@ const cli = yargs(hideBin(process.argv))
|
||||
describe: "print logs to stderr",
|
||||
type: "boolean",
|
||||
})
|
||||
.middleware(async () => {
|
||||
await Log.init({ print: process.argv.includes("--print-logs") })
|
||||
|
||||
try {
|
||||
const { Config } = await import("./config/config")
|
||||
const { App } = await import("./app/app")
|
||||
|
||||
App.provide({ cwd: process.cwd() }, async () => {
|
||||
const cfg = await Config.get()
|
||||
if (cfg.log_level) {
|
||||
Log.setLevel(cfg.log_level as Log.Level)
|
||||
} else {
|
||||
const defaultLevel = Installation.isDev() ? "DEBUG" : "INFO"
|
||||
Log.setLevel(defaultLevel)
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
Log.Default.error("failed to load config", { error: e })
|
||||
}
|
||||
.option("log-level", {
|
||||
describe: "log level",
|
||||
type: "string",
|
||||
choices: ["DEBUG", "INFO", "WARN", "ERROR"],
|
||||
})
|
||||
.middleware(async (opts) => {
|
||||
await Log.init({
|
||||
print: process.argv.includes("--print-logs"),
|
||||
dev: Installation.isDev(),
|
||||
level: (() => {
|
||||
if (opts.logLevel) return opts.logLevel as Log.Level
|
||||
if (Installation.isDev()) return "DEBUG"
|
||||
return "INFO"
|
||||
})(),
|
||||
})
|
||||
|
||||
Log.Default.info("opencode", {
|
||||
version: Installation.VERSION,
|
||||
@@ -73,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")
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ export namespace MCP {
|
||||
transport: {
|
||||
type: "sse",
|
||||
url: mcp.url,
|
||||
headers: mcp.headers,
|
||||
},
|
||||
}).catch(() => {})
|
||||
if (!client) {
|
||||
|
||||
@@ -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" })
|
||||
@@ -139,7 +127,8 @@ export namespace Provider {
|
||||
}
|
||||
},
|
||||
"amazon-bedrock": async () => {
|
||||
if (!process.env["AWS_PROFILE"] && !process.env["AWS_ACCESS_KEY_ID"]) return { autoload: false }
|
||||
if (!process.env["AWS_PROFILE"] && !process.env["AWS_ACCESS_KEY_ID"] && !process.env["AWS_BEARER_TOKEN_BEDROCK"])
|
||||
return { autoload: false }
|
||||
|
||||
const region = process.env["AWS_REGION"] ?? "us-east-1"
|
||||
|
||||
@@ -271,14 +260,20 @@ export namespace Provider {
|
||||
reasoning: model.reasoning ?? existing?.reasoning ?? false,
|
||||
temperature: model.temperature ?? existing?.temperature ?? false,
|
||||
tool_call: model.tool_call ?? existing?.tool_call ?? true,
|
||||
cost: {
|
||||
...existing?.cost,
|
||||
...model.cost,
|
||||
input: 0,
|
||||
output: 0,
|
||||
cache_read: 0,
|
||||
cache_write: 0,
|
||||
},
|
||||
cost:
|
||||
!model.cost && !existing?.cost
|
||||
? {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cache_read: 0,
|
||||
cache_write: 0,
|
||||
}
|
||||
: {
|
||||
cache_read: 0,
|
||||
cache_write: 0,
|
||||
...existing?.cost,
|
||||
...model.cost,
|
||||
},
|
||||
options: {
|
||||
...existing?.options,
|
||||
...model.options,
|
||||
@@ -360,7 +355,10 @@ export namespace Provider {
|
||||
const pkg = provider.npm ?? provider.id
|
||||
const mod = await import(await BunProc.install(pkg, "beta"))
|
||||
const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
|
||||
const loaded = fn(s.providers[provider.id]?.options)
|
||||
const loaded = fn({
|
||||
name: provider.id,
|
||||
...s.providers[provider.id]?.options,
|
||||
})
|
||||
s.sdk.set(provider.id, loaded)
|
||||
return loaded as SDK
|
||||
})().catch((e) => {
|
||||
@@ -409,6 +407,13 @@ export namespace Provider {
|
||||
}
|
||||
|
||||
export async function getSmallModel(providerID: string) {
|
||||
const cfg = await Config.get()
|
||||
|
||||
if (cfg.small_model) {
|
||||
const parsed = parseModel(cfg.small_model)
|
||||
return getModel(parsed.providerID, parsed.modelID)
|
||||
}
|
||||
|
||||
const provider = await state().then((state) => state.providers[providerID])
|
||||
if (!provider) return
|
||||
const priority = ["3-5-haiku", "3.5-haiku", "gemini-2.5-flash"]
|
||||
@@ -452,82 +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,
|
||||
}
|
||||
|
||||
export async function tools(providerID: string) {
|
||||
/*
|
||||
const cfg = await Config.get()
|
||||
if (cfg.tool?.provider?.[providerID])
|
||||
return cfg.tool.provider[providerID].map(
|
||||
(id) => TOOLS.find((t) => t.id === id)!,
|
||||
)
|
||||
*/
|
||||
return TOOL_MAPPING[providerID] ?? TOOLS
|
||||
}
|
||||
|
||||
function optionalToNullable(schema: z.ZodTypeAny): z.ZodTypeAny {
|
||||
if (schema instanceof z.ZodObject) {
|
||||
const shape = schema.shape
|
||||
const newShape: Record<string, z.ZodTypeAny> = {}
|
||||
|
||||
for (const [key, value] of Object.entries(shape)) {
|
||||
const zodValue = value as z.ZodTypeAny
|
||||
if (zodValue instanceof z.ZodOptional) {
|
||||
newShape[key] = zodValue.unwrap().nullable()
|
||||
} else {
|
||||
newShape[key] = optionalToNullable(zodValue)
|
||||
}
|
||||
}
|
||||
|
||||
return z.object(newShape)
|
||||
}
|
||||
|
||||
if (schema instanceof z.ZodArray) {
|
||||
return z.array(optionalToNullable(schema.element))
|
||||
}
|
||||
|
||||
if (schema instanceof z.ZodUnion) {
|
||||
return z.union(
|
||||
schema.options.map((option: z.ZodTypeAny) => optionalToNullable(option)) as [
|
||||
z.ZodTypeAny,
|
||||
z.ZodTypeAny,
|
||||
...z.ZodTypeAny[],
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
return schema
|
||||
}
|
||||
|
||||
export const ModelNotFoundError = NamedError.create(
|
||||
"ProviderModelNotFoundError",
|
||||
z.object({
|
||||
|
||||
@@ -3,35 +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)
|
||||
|
||||
for (const msg of unique([...system, ...final])) {
|
||||
msg.providerOptions = {
|
||||
...msg.providerOptions,
|
||||
anthropic: {
|
||||
cacheControl: { type: "ephemeral" },
|
||||
},
|
||||
openaiCompatible: {
|
||||
cache_control: { type: "ephemeral" },
|
||||
},
|
||||
}
|
||||
const providerOptions = {
|
||||
anthropic: {
|
||||
cacheControl: { type: "ephemeral" },
|
||||
},
|
||||
openrouter: {
|
||||
cache_control: { type: "ephemeral" },
|
||||
},
|
||||
bedrock: {
|
||||
cachePoint: { type: "ephemeral" },
|
||||
},
|
||||
openaiCompatible: {
|
||||
cache_control: { type: "ephemeral" },
|
||||
},
|
||||
}
|
||||
}
|
||||
if (providerID === "amazon-bedrock" || modelID.includes("anthropic")) {
|
||||
const system = msgs.filter((msg) => msg.role === "system").slice(0, 2)
|
||||
const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
|
||||
|
||||
for (const msg of unique([...system, ...final])) {
|
||||
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,
|
||||
bedrock: {
|
||||
cachePoint: { type: "ephemeral" },
|
||||
},
|
||||
...providerOptions,
|
||||
}
|
||||
}
|
||||
}
|
||||
return msgs
|
||||
}
|
||||
|
||||
export function temperature(_providerID: string, modelID: string) {
|
||||
if (modelID.toLowerCase().includes("qwen")) return 0.55
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
@@ -57,15 +58,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",
|
||||
@@ -195,6 +201,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)
|
||||
},
|
||||
)
|
||||
@@ -451,16 +458,7 @@ export namespace Server {
|
||||
id: z.string().openapi({ description: "Session ID" }),
|
||||
}),
|
||||
),
|
||||
zValidator(
|
||||
"json",
|
||||
z.object({
|
||||
messageID: z.string(),
|
||||
providerID: z.string(),
|
||||
modelID: z.string(),
|
||||
mode: z.string(),
|
||||
parts: z.union([MessageV2.FilePart, MessageV2.TextPart]).array(),
|
||||
}),
|
||||
),
|
||||
zValidator("json", Session.ChatInput.omit({ sessionID: true })),
|
||||
async (c) => {
|
||||
const sessionID = c.req.valid("param").id
|
||||
const body = c.req.valid("json")
|
||||
@@ -468,6 +466,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({
|
||||
@@ -712,6 +766,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
packages/opencode/src/server/tui.ts
Normal file
30
packages/opencode/src/server/tui.ts
Normal 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)
|
||||
})
|
||||
@@ -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,7 +65,7 @@ export namespace Session {
|
||||
revert: z
|
||||
.object({
|
||||
messageID: z.string(),
|
||||
part: z.number(),
|
||||
partID: z.string().optional(),
|
||||
snapshot: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
@@ -118,11 +119,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 +247,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 +258,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)
|
||||
@@ -319,72 +331,50 @@ export namespace Session {
|
||||
return part
|
||||
}
|
||||
|
||||
export async function chat(input: {
|
||||
sessionID: string
|
||||
messageID: string
|
||||
providerID: string
|
||||
modelID: string
|
||||
mode?: string
|
||||
parts: (MessageV2.TextPart | MessageV2.FilePart)[]
|
||||
}) {
|
||||
export const ChatInput = z.object({
|
||||
sessionID: Identifier.schema("session"),
|
||||
messageID: Identifier.schema("message").optional(),
|
||||
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", [
|
||||
MessageV2.TextPart.omit({
|
||||
messageID: true,
|
||||
sessionID: true,
|
||||
})
|
||||
.partial({
|
||||
id: true,
|
||||
})
|
||||
.openapi({
|
||||
ref: "TextPartInput",
|
||||
}),
|
||||
MessageV2.FilePart.omit({
|
||||
messageID: true,
|
||||
sessionID: true,
|
||||
})
|
||||
.partial({
|
||||
id: true,
|
||||
})
|
||||
.openapi({
|
||||
ref: "FilePartInput",
|
||||
}),
|
||||
]),
|
||||
),
|
||||
})
|
||||
export type ChatInput = 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,
|
||||
id: input.messageID ?? Identifier.ascending("message"),
|
||||
role: "user",
|
||||
sessionID: input.sessionID,
|
||||
time: {
|
||||
@@ -398,6 +388,34 @@ 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
|
||||
@@ -441,12 +459,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"),
|
||||
@@ -464,6 +484,12 @@ export namespace Session {
|
||||
synthetic: true,
|
||||
text: result.output,
|
||||
},
|
||||
{
|
||||
...part,
|
||||
id: part.id ?? Identifier.ascending("part"),
|
||||
messageID: userMsg.id,
|
||||
sessionID: input.sessionID,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -479,22 +505,29 @@ export namespace Session {
|
||||
synthetic: true,
|
||||
},
|
||||
{
|
||||
id: Identifier.ascending("part"),
|
||||
id: part.id ?? Identifier.ascending("part"),
|
||||
messageID: userMsg.id,
|
||||
sessionID: input.sessionID,
|
||||
type: "file",
|
||||
url: `data:${part.mime};base64,` + Buffer.from(await file.bytes()).toString("base64"),
|
||||
mime: part.mime,
|
||||
filename: part.filename!,
|
||||
source: part.source,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
return [part]
|
||||
return [
|
||||
{
|
||||
id: Identifier.ascending("part"),
|
||||
...part,
|
||||
messageID: userMsg.id,
|
||||
sessionID: input.sessionID,
|
||||
},
|
||||
]
|
||||
}),
|
||||
).then((x) => x.flat())
|
||||
|
||||
if (input.mode === "plan")
|
||||
if (inputMode === "plan")
|
||||
userParts.push({
|
||||
id: Identifier.ascending("part"),
|
||||
messageID: userMsg.id,
|
||||
@@ -504,11 +537,83 @@ export namespace Session {
|
||||
synthetic: true,
|
||||
})
|
||||
|
||||
if (msgs.length === 0 && !session.parentID) {
|
||||
await updateMessage(userMsg)
|
||||
for (const part of userParts) {
|
||||
await updatePart(part)
|
||||
}
|
||||
// mark session as updated since a message has been added to it
|
||||
await update(input.sessionID, (_draft) => {})
|
||||
|
||||
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) {
|
||||
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, {
|
||||
messageID: last.info.id,
|
||||
partID: part.id,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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: input.providerID === "google" ? 1024 : 20,
|
||||
providerOptions: small.info.options,
|
||||
maxOutputTokens: small.info.reasoning ? 1024 : 20,
|
||||
providerOptions: {
|
||||
[input.providerID]: small.info.options,
|
||||
},
|
||||
messages: [
|
||||
...SystemPrompt.title(input.providerID).map(
|
||||
(x): ModelMessage => ({
|
||||
@@ -540,15 +645,16 @@ export namespace Session {
|
||||
})
|
||||
.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
|
||||
@@ -559,6 +665,7 @@ export namespace Session {
|
||||
id: Identifier.ascending("message"),
|
||||
role: "assistant",
|
||||
system,
|
||||
mode: inputMode,
|
||||
path: {
|
||||
cwd: app.path.cwd,
|
||||
root: app.path.root,
|
||||
@@ -582,14 +689,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 (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,
|
||||
@@ -628,6 +740,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")
|
||||
@@ -649,11 +762,59 @@ export namespace Session {
|
||||
|
||||
const stream = streamText({
|
||||
onError() {},
|
||||
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: 10,
|
||||
maxOutputTokens: outputLimit,
|
||||
abortSignal: abort.signal,
|
||||
stopWhen: stepCountIs(1000),
|
||||
providerOptions: model.info.options,
|
||||
providerOptions: {
|
||||
[input.providerID]: model.info.options,
|
||||
},
|
||||
messages: [
|
||||
...system.map(
|
||||
(x): ModelMessage => ({
|
||||
@@ -663,7 +824,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,
|
||||
@@ -681,12 +844,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]
|
||||
},
|
||||
@@ -700,15 +878,6 @@ 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":
|
||||
@@ -729,6 +898,9 @@ export namespace Session {
|
||||
case "tool-input-delta":
|
||||
break
|
||||
|
||||
case "tool-input-end":
|
||||
break
|
||||
|
||||
case "tool-call": {
|
||||
const match = toolCalls[value.toolCallId]
|
||||
if (match) {
|
||||
@@ -764,15 +936,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
|
||||
}
|
||||
@@ -793,15 +970,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
|
||||
}
|
||||
@@ -859,6 +1039,7 @@ export namespace Session {
|
||||
start: Date.now(),
|
||||
end: Date.now(),
|
||||
}
|
||||
currentText.text = currentText.text.trimEnd()
|
||||
await updatePart(currentText)
|
||||
}
|
||||
currentText = undefined
|
||||
@@ -912,7 +1093,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({
|
||||
@@ -936,47 +1117,65 @@ 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)
|
||||
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 }) {
|
||||
@@ -986,13 +1185,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,
|
||||
@@ -1042,6 +1246,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)
|
||||
@@ -1104,8 +1312,6 @@ export namespace Session {
|
||||
parts: [
|
||||
{
|
||||
id: Identifier.ascending("part"),
|
||||
sessionID: input.sessionID,
|
||||
messageID: input.messageID,
|
||||
type: "text",
|
||||
text: PROMPT_INITIALIZE.replace("${path}", app.path.root),
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@ import { NamedError } from "../util/error"
|
||||
import { Message } from "./message"
|
||||
import { convertToModelMessages, type ModelMessage, type UIMessage } from "ai"
|
||||
import { Identifier } from "../id/id"
|
||||
import { LSP } from "../lsp"
|
||||
|
||||
export namespace MessageV2 {
|
||||
export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({}))
|
||||
@@ -93,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(),
|
||||
@@ -118,11 +128,45 @@ export namespace MessageV2 {
|
||||
})
|
||||
export type ToolPart = z.infer<typeof ToolPart>
|
||||
|
||||
const FilePartSourceBase = z.object({
|
||||
text: z
|
||||
.object({
|
||||
value: z.string(),
|
||||
start: z.number().int(),
|
||||
end: z.number().int(),
|
||||
})
|
||||
.openapi({
|
||||
ref: "FilePartSourceText",
|
||||
}),
|
||||
})
|
||||
|
||||
export const FileSource = FilePartSourceBase.extend({
|
||||
type: z.literal("file"),
|
||||
path: z.string(),
|
||||
}).openapi({
|
||||
ref: "FileSource",
|
||||
})
|
||||
|
||||
export const SymbolSource = FilePartSourceBase.extend({
|
||||
type: z.literal("symbol"),
|
||||
path: z.string(),
|
||||
range: LSP.Range,
|
||||
name: z.string(),
|
||||
kind: z.number().int(),
|
||||
}).openapi({
|
||||
ref: "SymbolSource",
|
||||
})
|
||||
|
||||
export const FilePartSource = z.discriminatedUnion("type", [FileSource, SymbolSource]).openapi({
|
||||
ref: "FilePartSource",
|
||||
})
|
||||
|
||||
export const FilePart = PartBase.extend({
|
||||
type: z.literal("file"),
|
||||
mime: z.string(),
|
||||
filename: z.string().optional(),
|
||||
url: z.string(),
|
||||
source: FilePartSource.optional(),
|
||||
}).openapi({
|
||||
ref: "FilePart",
|
||||
})
|
||||
@@ -168,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",
|
||||
})
|
||||
@@ -191,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(),
|
||||
@@ -236,6 +281,13 @@ export namespace MessageV2 {
|
||||
part: Part,
|
||||
}),
|
||||
),
|
||||
PartRemoved: Bus.event(
|
||||
"message.part.removed",
|
||||
z.object({
|
||||
messageID: z.string(),
|
||||
partID: z.string(),
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
export function fromV1(v1: Message.Info) {
|
||||
@@ -255,6 +307,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[] => {
|
||||
@@ -394,7 +447,8 @@ export namespace MessageV2 {
|
||||
text: part.text,
|
||||
},
|
||||
]
|
||||
if (part.type === "file")
|
||||
// text/plain files are converted into text parts, ignore them
|
||||
if (part.type === "file" && part.mime !== "text/plain")
|
||||
return [
|
||||
{
|
||||
type: "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
|
||||
|
||||
@@ -1,95 +1,117 @@
|
||||
You are an agent known as opencode - please keep going until the user’s query is completely resolved, before ending your turn and yielding back to the user.
|
||||
You are opencode, an autonomous agent - please keep going until the user's query is completely resolved, before ending your turn and yielding back to the user.
|
||||
|
||||
Your thinking should be thorough and so it's fine if it's very long. However, avoid unnecessary repetition and verbosity. You should be concise, but thorough.
|
||||
|
||||
You MUST iterate and keep going until the problem is solved.
|
||||
|
||||
I want you to fully solve this autonomously before coming back to me.
|
||||
You have everything you need to resolve this problem. I want you to fully solve this autonomously before coming back to me.
|
||||
|
||||
Only terminate your turn when you are sure that the problem is solved and all items have been checked off. Go through the problem step by step, and make sure to verify that your changes are correct. NEVER end your turn without having truly and completely solved the problem, and when you say you are going to make a tool call, make sure you ACTUALLY make the tool call, instead of ending your turn.
|
||||
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.
|
||||
|
||||
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.
|
||||
THE PROBLEM CAN NOT BE SOLVED WITHOUT EXTENSIVE INTERNET RESEARCH.
|
||||
|
||||
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 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.
|
||||
|
||||
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. 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.
|
||||
Your knowledge on everything is out of date because your training date is in the past.
|
||||
|
||||
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 CANNOT successfully complete this task without using Bing to verify your understanding of third party packages and dependencies is up to date. You must use the webfetch tool to search bing for how to properly use libraries, packages, frameworks, dependencies, etc. every single time you install or implement one. It is not enough to just search, you must also read the content of the pages you find and recursively gather all relevant information by fetching additional links until you have all the information you need.
|
||||
|
||||
If the user request is "resume" or "continue" or "try again",use the TodoRead tool to find the next pending step. Continue from that step, and do not hand back control to the user until the entire todo list is complete and all steps are marked as complete or cancelled. Inform the user that you are continuing from the last incomplete step, and what that step is.
|
||||
|
||||
Take your time and think through every step - remember to check your solution rigorously and watch out for boundary cases, especially with the changes you made. Use the sequential thinking tool if available. Your solution must be perfect. If not, continue working on it. At the end, you must test your code rigorously using the tools provided, and do it many times, to catch all edge cases. If it is not robust, update the plan and iterate more and make it perfect. Failing to test your code sufficiently rigorously is the NUMBER ONE failure mode on these types of tasks; run the build, and verify that the changes you made actually build; make sure you handle all edge cases, and run existing tests if they are provided.
|
||||
|
||||
You MUST plan extensively before each tool call, and reflect extensively on the outcomes of the previous tool calls. DO NOT do this entire process by making tool calls only, as this can impair your ability to solve the problem and think insightfully.
|
||||
|
||||
You MUST keep working until the problem is completely solved, and all steps in the todo list are complete. Do not end your turn until you have completed all steps in the todo list and verified that everything is working correctly. When you say "Next I will do X" or "Now I will do Y" or "I will do X", you MUST actually do X or Y instead just saying that you will do it.
|
||||
|
||||
You MUST use the ToolRead tool to verify that all steps are complete or cancelled before ending your turn. If any steps are incomplete, you MUST continue working on them until they are all complete.
|
||||
|
||||
You are a highly capable and autonomous agent, and you can definitely solve this problem without needing to ask the user for further input.
|
||||
|
||||
# Workflow
|
||||
|
||||
1. Understand the problem deeply. Carefully read the issue and think critically about what is required.
|
||||
2. Investigate the codebase. Explore relevant files, search for key functions, and gather context.
|
||||
3. 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.
|
||||
4. Implement the fix incrementally. Make small, testable code changes.
|
||||
5. Debug as needed. Use debugging techniques to isolate and resolve issues.
|
||||
6. Test frequently. Run tests after each change to verify correctness.
|
||||
7. Iterate until the root cause is fixed and all tests pass.
|
||||
8. Reflect and validate comprehensively. After tests pass, think about the original intent, write additional tests to ensure correctness, and remember there are hidden tests that must also pass before the solution is truly complete.
|
||||
1. Fetch any URL's provided by the user using the `webfetch` tool.
|
||||
2. Understand the problem deeply. Carefully read the issue and think critically about what is required. Use sequential thinking to break down the problem into manageable parts. Consider the following:
|
||||
- What is the expected behavior?
|
||||
- What are the edge cases?
|
||||
- What are the potential pitfalls?
|
||||
- How does this fit into the larger context of the codebase?
|
||||
- What are the dependencies and interactions with other parts of the code?
|
||||
3. Investigate the codebase. Explore relevant files, search for key functions, and gather context.
|
||||
4. Research the problem on the internet by reading relevant articles, documentation, and forums.
|
||||
5. Develop a clear, step-by-step plan. Break down the fix into manageable, incremental steps. Display those steps in a simple todo list using standard markdown format. Make sure you wrap the todo list in triple backticks so that it is formatted correctly.
|
||||
6. Implement the fix incrementally. Make small, testable code changes.
|
||||
7. Debug as needed. Use debugging techniques to isolate and resolve issues.
|
||||
8. Test frequently. Run tests after each change to verify correctness.
|
||||
9. Iterate until the root cause is fixed and all tests pass.
|
||||
10. Reflect and validate comprehensively. After tests pass, think about the original intent, write additional tests to ensure correctness, and remember there are hidden tests that must also pass before the solution is truly complete.
|
||||
|
||||
Refer to the detailed sections below for more information on each step.
|
||||
|
||||
## 1. Deeply Understand the Problem
|
||||
Carefully read the issue and think hard about a plan to solve it before coding.
|
||||
## 1. Fetch Provided URLs
|
||||
- If the user provides a URL, use the `webfetch` tool to retrieve the content of the provided URL.
|
||||
- After fetching, review the content returned by the fetch tool.
|
||||
- If you find any additional URLs or links that are relevant, use the `webfetch` tool again to retrieve those links.
|
||||
- Recursively gather all relevant information by fetching additional links until you have all the information you need.
|
||||
|
||||
## 2. Codebase Investigation
|
||||
## 2. Deeply Understand the Problem
|
||||
Carefully read the issue and think hard about a plan to solve it before coding. Use the sequential thinking tool if available.
|
||||
|
||||
## 3. Codebase Investigation
|
||||
- Explore relevant files and directories.
|
||||
- Search for key functions, classes, or variables related to the issue.
|
||||
- Read and understand relevant code snippets.
|
||||
- Identify the root cause of the problem.
|
||||
- Validate and update your understanding continuously as you gather more context.
|
||||
|
||||
## 3. Fetch Provided URLs
|
||||
- If the user provides a URL, use the `functions.fetch_webpage` tool to retrieve the content of the provided URL.
|
||||
## 4. Internet Research
|
||||
- Use the `webfetch` tool to search bing by fetching the URL `https://www.bing.com/search?q=your+search+query`.
|
||||
- After fetching, review the content returned by the fetch tool.
|
||||
- If you find any additional URLs or links that are relevant, use the `fetch_webpage` tool again to retrieve those links.
|
||||
- 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.
|
||||
|
||||
## 4. Develop a Detailed Plan
|
||||
## 5. Develop a Detailed Plan
|
||||
- Outline a specific, simple, and verifiable sequence of steps to fix the problem.
|
||||
- Create a todo list in markdown format to track your progress.
|
||||
- Each time you complete a step, check it off using `[x]` syntax.
|
||||
- Each time you check off a step, display the updated todo list to the user.
|
||||
- Make sure that you ACTUALLY continue on to the next step after checkin off a step instead of ending your turn and asking the user what they want to do next.
|
||||
- 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.
|
||||
|
||||
## 5. Making Code Changes
|
||||
## 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.
|
||||
|
||||
## 6. Debugging
|
||||
- Make code changes only if you have high confidence they can solve the problem
|
||||
- When debugging, try to determine the root cause rather than addressing symptoms
|
||||
- Debug for as long as needed to identify the root cause and identify a fix
|
||||
- Use the #problems tool to check for any problems in the code
|
||||
- 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
|
||||
## 7. Debugging
|
||||
- Use the `lsp_diagnostics` tool to check for any problems in the code.
|
||||
- Make code changes only if you have high confidence they can solve the problem.
|
||||
- When debugging, try to determine the root cause rather than addressing symptoms.
|
||||
- Debug for as long as needed to identify the root cause and identify a fix.
|
||||
- Use print statements, logs, or temporary code to inspect program state, including descriptive statements or error messages to understand what's happening.
|
||||
- To test hypotheses, you can also add test statements or functions.
|
||||
- Revisit your assumptions if unexpected behavior occurs.
|
||||
|
||||
# Fetch Webpage
|
||||
Use the `webfetch` tool when the user provides a URL. Follow these steps exactly.
|
||||
|
||||
1. Use the `webfetch` tool to retrieve the content of the provided URL.
|
||||
2. After fetching, review the content returned by the fetch tool.
|
||||
3. If you find any additional URLs or links that are relevant, use the `webfetch` tool again to retrieve those links.
|
||||
4. Go back to step 2 and repeat until you have all the information you need.
|
||||
|
||||
IMPORTANT: Recursively fetching links is crucial. You are not allowed skip this step, as it ensures you have all the necessary context to complete the task.
|
||||
|
||||
# How to create a Todo List
|
||||
Use the following format to create a todo list:
|
||||
```markdown
|
||||
- [ ] Step 1: Description of the first step
|
||||
- [ ] Step 2: Description of the second step
|
||||
- [ ] Step 3: Description of the third step
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
# Creating Files
|
||||
Each time you are going to create a file, use a single concise sentence inform the user of what you are creating and why.
|
||||
# Communication Guidelines
|
||||
Always communicate clearly and concisely in a casual, friendly yet professional tone.
|
||||
|
||||
# Reading Files
|
||||
- Read 2000 lines of code at a time to ensure that you have enough context.
|
||||
- Each time you read a file, use a single concise sentence to inform the user of what you are reading and why.
|
||||
<examples>
|
||||
"Let me fetch the URL you provided to gather more information."
|
||||
"Ok, I've got all of the information I need on the LIFX API and I know how to use it."
|
||||
"Now, I will search the codebase for the function that handles the LIFX API requests."
|
||||
"I need to update several files here - stand by"
|
||||
"OK! Now let's run the tests to make sure everything is working correctly."
|
||||
"Whelp - I see we have some problems. Let's fix those up."
|
||||
</examples>
|
||||
|
||||
@@ -14,6 +14,10 @@ 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]
|
||||
|
||||
@@ -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,64 @@ 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) {
|
||||
function gitdir() {
|
||||
const app = App.info()
|
||||
return path.join(app.path.data, "snapshot", sessionID)
|
||||
return path.join(app.path.data, "snapshots")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -7,8 +7,7 @@ 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"),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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."),
|
||||
|
||||
@@ -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."),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -10,8 +10,7 @@ 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"),
|
||||
|
||||
170
packages/opencode/src/tool/registry.ts
Normal file
170
packages/opencode/src/tool/registry.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
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")) {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -5,59 +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,
|
||||
parts: [
|
||||
{
|
||||
id: Identifier.ascending("part"),
|
||||
messageID,
|
||||
sessionID: session.id,
|
||||
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,
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,7 @@ 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"),
|
||||
|
||||
@@ -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)"),
|
||||
|
||||
53
packages/opencode/src/trace/index.ts
Normal file
53
packages/opencode/src/trace/index.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { Global } from "../global"
|
||||
import { Installation } from "../installation"
|
||||
import path from "path"
|
||||
|
||||
export namespace Trace {
|
||||
export function init() {
|
||||
if (!Installation.isDev()) return
|
||||
const writer = Bun.file(path.join(Global.Path.data, "log", "fetch.log")).writer()
|
||||
|
||||
const originalFetch = globalThis.fetch
|
||||
// @ts-expect-error
|
||||
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url
|
||||
const method = init?.method || "GET"
|
||||
|
||||
const urlObj = new URL(url)
|
||||
|
||||
writer.write(`\n${method} ${urlObj.pathname}${urlObj.search} HTTP/1.1\n`)
|
||||
writer.write(`Host: ${urlObj.host}\n`)
|
||||
|
||||
if (init?.headers) {
|
||||
if (init.headers instanceof Headers) {
|
||||
init.headers.forEach((value, key) => {
|
||||
writer.write(`${key}: ${value}\n`)
|
||||
})
|
||||
} else {
|
||||
for (const [key, value] of Object.entries(init.headers)) {
|
||||
writer.write(`${key}: ${value}\n`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (init?.body) {
|
||||
writer.write(`\n${init.body}`)
|
||||
}
|
||||
writer.flush()
|
||||
const response = await originalFetch(input, init)
|
||||
const clonedResponse = response.clone()
|
||||
writer.write(`\nHTTP/1.1 ${response.status} ${response.statusText}\n`)
|
||||
response.headers.forEach((value, key) => {
|
||||
writer.write(`${key}: ${value}\n`)
|
||||
})
|
||||
if (clonedResponse.body) {
|
||||
clonedResponse.text().then(async (x) => {
|
||||
writer.write(`\n${x}\n`)
|
||||
})
|
||||
}
|
||||
writer.flush()
|
||||
|
||||
return response
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -14,18 +14,10 @@ export namespace Log {
|
||||
ERROR: 3,
|
||||
}
|
||||
|
||||
let currentLevel: Level = "INFO"
|
||||
let level: Level = "INFO"
|
||||
|
||||
export function setLevel(level: Level) {
|
||||
currentLevel = level
|
||||
}
|
||||
|
||||
export function getLevel(): Level {
|
||||
return currentLevel
|
||||
}
|
||||
|
||||
function shouldLog(level: Level): boolean {
|
||||
return levelPriority[level] >= levelPriority[currentLevel]
|
||||
function shouldLog(input: Level): boolean {
|
||||
return levelPriority[input] >= levelPriority[level]
|
||||
}
|
||||
|
||||
export type Logger = {
|
||||
@@ -50,6 +42,7 @@ export namespace Log {
|
||||
|
||||
export interface Options {
|
||||
print: boolean
|
||||
dev?: boolean
|
||||
level?: Level
|
||||
}
|
||||
|
||||
@@ -59,11 +52,15 @@ export namespace Log {
|
||||
}
|
||||
|
||||
export async function init(options: Options) {
|
||||
if (options.level) level = options.level
|
||||
const dir = path.join(Global.Path.data, "log")
|
||||
await fs.mkdir(dir, { recursive: true })
|
||||
cleanup(dir)
|
||||
if (options.print) return
|
||||
logpath = path.join(dir, new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log")
|
||||
logpath = path.join(
|
||||
dir,
|
||||
options.dev ? "dev.log" : new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log",
|
||||
)
|
||||
const logfile = Bun.file(logpath)
|
||||
await fs.truncate(logpath).catch(() => {})
|
||||
const writer = logfile.writer()
|
||||
@@ -75,15 +72,16 @@ export namespace Log {
|
||||
}
|
||||
|
||||
async function cleanup(dir: string) {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true })
|
||||
const files = entries
|
||||
.filter((entry) => entry.isFile() && entry.name.endsWith(".log"))
|
||||
.map((entry) => path.join(dir, entry.name))
|
||||
|
||||
const glob = new Bun.Glob("????-??-??T??????.log")
|
||||
const files = await Array.fromAsync(
|
||||
glob.scan({
|
||||
cwd: dir,
|
||||
absolute: true,
|
||||
}),
|
||||
)
|
||||
if (files.length <= 5) return
|
||||
|
||||
const filesToDelete = files.slice(0, -10)
|
||||
|
||||
await Promise.all(filesToDelete.map((file) => fs.unlink(file).catch(() => {})))
|
||||
}
|
||||
|
||||
@@ -105,7 +103,7 @@ export namespace Log {
|
||||
...extra,
|
||||
})
|
||||
.filter(([_, value]) => value !== undefined && value !== null)
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.map(([key, value]) => `${key}=${typeof value === "object" ? JSON.stringify(value) : value}`)
|
||||
.join(" ")
|
||||
const next = new Date()
|
||||
const diff = next.getTime() - last
|
||||
|
||||
19
packages/opencode/src/util/queue.ts
Normal file
19
packages/opencode/src/util/queue.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -9,10 +9,13 @@ const ctx = {
|
||||
abort: AbortSignal.any([]),
|
||||
metadata: () => {},
|
||||
}
|
||||
const glob = await GlobTool.init()
|
||||
const list = await ListTool.init()
|
||||
|
||||
describe("tool.glob", () => {
|
||||
test("truncate", async () => {
|
||||
await App.provide({ cwd: process.cwd() }, async () => {
|
||||
let result = await GlobTool.execute(
|
||||
let result = await glob.execute(
|
||||
{
|
||||
pattern: "../../node_modules/**/*",
|
||||
path: undefined,
|
||||
@@ -24,7 +27,7 @@ describe("tool.glob", () => {
|
||||
})
|
||||
test("basic", async () => {
|
||||
await App.provide({ cwd: process.cwd() }, async () => {
|
||||
let result = await GlobTool.execute(
|
||||
let result = await glob.execute(
|
||||
{
|
||||
pattern: "*.json",
|
||||
path: undefined,
|
||||
@@ -42,7 +45,7 @@ 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)
|
||||
return await list.execute({ path: "./example", ignore: [".git"] }, ctx)
|
||||
})
|
||||
expect(result.output).toMatchSnapshot()
|
||||
})
|
||||
|
||||
15
packages/sdk/.devcontainer/devcontainer.json
Normal file
15
packages/sdk/.devcontainer/devcontainer.json
Normal file
@@ -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
packages/sdk/.github/workflows/ci.yml
vendored
Normal file
88
packages/sdk/.github/workflows/ci.yml
vendored
Normal 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
packages/sdk/.github/workflows/publish-npm.yml
vendored
Normal file
32
packages/sdk/.github/workflows/publish-npm.yml
vendored
Normal 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
packages/sdk/.github/workflows/release-doctor.yml
vendored
Normal file
21
packages/sdk/.github/workflows/release-doctor.yml
vendored
Normal 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
packages/sdk/.gitignore
vendored
Normal file
10
packages/sdk/.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
.prism.log
|
||||
node_modules
|
||||
yarn-error.log
|
||||
codegen.log
|
||||
Brewfile.lock.json
|
||||
dist
|
||||
dist-deno
|
||||
/*.tgz
|
||||
.idea/
|
||||
|
||||
7
packages/sdk/.prettierignore
Normal file
7
packages/sdk/.prettierignore
Normal file
@@ -0,0 +1,7 @@
|
||||
CHANGELOG.md
|
||||
/ecosystem-tests/*/**
|
||||
/node_modules
|
||||
/deno
|
||||
|
||||
# don't format tsc output, will break source maps
|
||||
/dist
|
||||
7
packages/sdk/.prettierrc.json
Normal file
7
packages/sdk/.prettierrc.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"arrowParens": "always",
|
||||
"experimentalTernaries": true,
|
||||
"printWidth": 110,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
3
packages/sdk/.release-please-manifest.json
Normal file
3
packages/sdk/.release-please-manifest.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
".": "0.1.0-alpha.20"
|
||||
}
|
||||
4
packages/sdk/.stats.yml
Normal file
4
packages/sdk/.stats.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
configured_endpoints: 26
|
||||
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-5748199af356c3243a46a466e73b5d0bab7eaa0c56895e1d0f903d637f61d0bb.yml
|
||||
openapi_spec_hash: c04f6b6be54b05d9b1283c24e870163b
|
||||
config_hash: 1ae82c93499b9f0b9ba828b8919f9cb3
|
||||
1
packages/sdk/Brewfile
Normal file
1
packages/sdk/Brewfile
Normal file
@@ -0,0 +1 @@
|
||||
brew "node"
|
||||
196
packages/sdk/CHANGELOG.md
Normal file
196
packages/sdk/CHANGELOG.md
Normal 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
packages/sdk/CONTRIBUTING.md
Normal file
107
packages/sdk/CONTRIBUTING.md
Normal 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 you’d 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
packages/sdk/LICENSE
Normal file
7
packages/sdk/LICENSE
Normal 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
packages/sdk/README.md
Normal file
370
packages/sdk/README.md
Normal file
@@ -0,0 +1,370 @@
|
||||
# Opencode TypeScript API Library
|
||||
|
||||
[>)](https://npmjs.org/package/@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
packages/sdk/SECURITY.md
Normal file
27
packages/sdk/SECURITY.md
Normal 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.
|
||||
140
packages/sdk/api.md
Normal file
140
packages/sdk/api.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# 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">Match</a></code>
|
||||
- <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>
|
||||
22
packages/sdk/bin/check-release-environment
Normal file
22
packages/sdk/bin/check-release-environment
Normal file
@@ -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
packages/sdk/bin/publish-npm
Normal file
61
packages/sdk/bin/publish-npm
Normal 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
packages/sdk/eslint.config.mjs
Normal file
42
packages/sdk/eslint.config.mjs
Normal 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
packages/sdk/examples/.keep
Normal file
4
packages/sdk/examples/.keep
Normal 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
packages/sdk/jest.config.ts
Normal file
23
packages/sdk/jest.config.ts
Normal 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;
|
||||
69
packages/sdk/package.json
Normal file
69
packages/sdk/package.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "0.1.0-alpha.20",
|
||||
"description": "The official TypeScript library for the Opencode API",
|
||||
"author": "Opencode <support@sst.dev>",
|
||||
"types": "dist/index.d.ts",
|
||||
"main": "dist/index.js",
|
||||
"type": "commonjs",
|
||||
"repository": "github:sst/opencode-sdk-js",
|
||||
"license": "MIT",
|
||||
"packageManager": "yarn@1.22.22",
|
||||
"files": [
|
||||
"**/*"
|
||||
],
|
||||
"private": false,
|
||||
"scripts": {
|
||||
"test": "./scripts/test",
|
||||
"build": "./scripts/build",
|
||||
"prepublishOnly": "echo 'to publish, run yarn build && (cd dist; yarn publish)' && exit 1",
|
||||
"format": "./scripts/format",
|
||||
"prepare": "if ./scripts/utils/check-is-in-git-install.sh; then ./scripts/build && ./scripts/utils/git-swap.sh; fi",
|
||||
"tsn": "ts-node -r tsconfig-paths/register",
|
||||
"lint": "./scripts/lint",
|
||||
"fix": "./scripts/format"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@arethetypeswrong/cli": "^0.17.0",
|
||||
"@swc/core": "^1.3.102",
|
||||
"@swc/jest": "^0.2.29",
|
||||
"@types/jest": "^29.4.0",
|
||||
"@types/node": "^20.17.6",
|
||||
"@typescript-eslint/eslint-plugin": "8.31.1",
|
||||
"@typescript-eslint/parser": "8.31.1",
|
||||
"eslint": "^9.20.1",
|
||||
"eslint-plugin-prettier": "^5.4.1",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"jest": "^29.4.0",
|
||||
"prettier": "^3.0.0",
|
||||
"publint": "^0.2.12",
|
||||
"ts-jest": "^29.1.0",
|
||||
"ts-node": "^10.5.0",
|
||||
"tsc-multi": "https://github.com/stainless-api/tsc-multi/releases/download/v1.1.8/tsc-multi.tgz",
|
||||
"tsconfig-paths": "^4.0.0",
|
||||
"typescript": "5.8.3",
|
||||
"typescript-eslint": "8.31.1"
|
||||
},
|
||||
"imports": {
|
||||
"@opencode-ai/sdk": ".",
|
||||
"@opencode-ai/sdk/*": "./src/*"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.mjs",
|
||||
"require": "./dist/index.js"
|
||||
},
|
||||
"./*.mjs": {
|
||||
"default": "./dist/*.mjs"
|
||||
},
|
||||
"./*.js": {
|
||||
"default": "./dist/*.js"
|
||||
},
|
||||
"./*": {
|
||||
"import": "./dist/*.mjs",
|
||||
"require": "./dist/*.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user