mirror of
https://fastgit.cc/https://github.com/anomalyco/opencode
synced 2026-05-04 15:50:44 +08:00
Compare commits
181 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
fc45c0c944 | ||
|
|
9d869f784c | ||
|
|
bd244f73af | ||
|
|
dd34556e9c | ||
|
|
f7dd48e60d | ||
|
|
93c779cf48 | ||
|
|
360c04c542 |
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: ./sdks/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) |
|
||||
|
||||
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
|
||||
*/
|
||||
|
||||
@@ -31,9 +31,12 @@
|
||||
"@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
|
||||
|
||||
@@ -84,40 +84,22 @@ export const InstallGithubCommand = cmd({
|
||||
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,
|
||||
})),
|
||||
options: pipe(
|
||||
providers,
|
||||
values(),
|
||||
sortBy(
|
||||
(x) => priority[x.id] ?? 99,
|
||||
(x) => x.name ?? x.id,
|
||||
),
|
||||
{
|
||||
value: "other",
|
||||
label: "Other",
|
||||
},
|
||||
],
|
||||
map((x) => ({
|
||||
label: x.name,
|
||||
value: x.id,
|
||||
hint: priority[x.id] === 0 ? "recommended" : undefined,
|
||||
})),
|
||||
),
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
@@ -211,7 +193,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
|
||||
|
||||
@@ -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,10 +23,38 @@ 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")
|
||||
@@ -57,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({
|
||||
@@ -71,12 +101,19 @@ export namespace Config {
|
||||
model: z.string().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"),
|
||||
@@ -88,7 +125,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"),
|
||||
@@ -117,7 +154,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()
|
||||
@@ -136,9 +175,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()
|
||||
@@ -146,6 +187,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()
|
||||
@@ -158,19 +205,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"
|
||||
@@ -17,6 +18,9 @@ import { DebugCommand } from "./cli/cmd/debug"
|
||||
import { StatsCommand } from "./cli/cmd/stats"
|
||||
import { McpCommand } from "./cli/cmd/mcp"
|
||||
import { InstallGithubCommand } from "./cli/cmd/install-github"
|
||||
import { Trace } from "./trace"
|
||||
|
||||
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,6 +73,7 @@ const cli = yargs(hideBin(process.argv))
|
||||
.command(GenerateCommand)
|
||||
.command(DebugCommand)
|
||||
.command(AuthCommand)
|
||||
.command(AgentCommand)
|
||||
.command(UpgradeCommand)
|
||||
.command(ServeCommand)
|
||||
.command(ModelsCommand)
|
||||
|
||||
@@ -37,6 +37,7 @@ export namespace MCP {
|
||||
transport: {
|
||||
type: "sse",
|
||||
url: mcp.url,
|
||||
headers: mcp.headers,
|
||||
},
|
||||
}).catch(() => {})
|
||||
if (!client) {
|
||||
|
||||
@@ -13,7 +13,6 @@ 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"
|
||||
@@ -139,7 +138,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 +271,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 +366,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 +418,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"]
|
||||
@@ -470,30 +486,85 @@ export namespace Provider {
|
||||
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
|
||||
const result = await Promise.all(TOOLS.map((t) => t()))
|
||||
switch (providerID) {
|
||||
case "anthropic":
|
||||
return result.filter((t) => t.id !== "patch")
|
||||
case "openai":
|
||||
return result.map((t) => ({
|
||||
...t,
|
||||
parameters: optionalToNullable(t.parameters),
|
||||
}))
|
||||
case "azure":
|
||||
return result.map((t) => ({
|
||||
...t,
|
||||
parameters: optionalToNullable(t.parameters),
|
||||
}))
|
||||
case "google":
|
||||
return result.map((t) => ({
|
||||
...t,
|
||||
parameters: sanitizeGeminiParameters(t.parameters),
|
||||
}))
|
||||
default:
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -3,7 +3,7 @@ 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)
|
||||
|
||||
@@ -13,22 +13,15 @@ export namespace ProviderTransform {
|
||||
anthropic: {
|
||||
cacheControl: { type: "ephemeral" },
|
||||
},
|
||||
openaiCompatible: {
|
||||
openrouter: {
|
||||
cache_control: { type: "ephemeral" },
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
if (providerID === "amazon-bedrock" || modelID.includes("anthropic")) {
|
||||
const system = msgs.filter((msg) => msg.role === "system").slice(0, 2)
|
||||
const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
|
||||
|
||||
for (const msg of unique([...system, ...final])) {
|
||||
msg.providerOptions = {
|
||||
...msg.providerOptions,
|
||||
bedrock: {
|
||||
cachePoint: { type: "ephemeral" },
|
||||
},
|
||||
openaiCompatible: {
|
||||
cache_control: { type: "ephemeral" },
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,7 @@ import { MessageV2 } from "./message-v2"
|
||||
import { Mode } from "./mode"
|
||||
import { LSP } from "../lsp"
|
||||
import { ReadTool } from "../tool/read"
|
||||
import { splitWhen } from "remeda"
|
||||
|
||||
export namespace Session {
|
||||
const log = Log.create({ service: "session" })
|
||||
@@ -64,7 +64,7 @@ export namespace Session {
|
||||
revert: z
|
||||
.object({
|
||||
messageID: z.string(),
|
||||
part: z.number(),
|
||||
partID: z.string().optional(),
|
||||
snapshot: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
@@ -118,11 +118,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 +246,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 +257,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 +330,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: {
|
||||
@@ -441,12 +430,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().then((t) =>
|
||||
t.execute(args, {
|
||||
sessionID: input.sessionID,
|
||||
abort: new AbortController().signal,
|
||||
messageID: userMsg.id,
|
||||
metadata: async () => {},
|
||||
}),
|
||||
)
|
||||
return [
|
||||
{
|
||||
id: Identifier.ascending("part"),
|
||||
@@ -464,6 +455,12 @@ export namespace Session {
|
||||
synthetic: true,
|
||||
text: result.output,
|
||||
},
|
||||
{
|
||||
...part,
|
||||
id: part.id ?? Identifier.ascending("part"),
|
||||
messageID: userMsg.id,
|
||||
sessionID: input.sessionID,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -479,22 +476,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 +508,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 +616,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 +636,7 @@ export namespace Session {
|
||||
id: Identifier.ascending("message"),
|
||||
role: "assistant",
|
||||
system,
|
||||
mode: inputMode,
|
||||
path: {
|
||||
cwd: app.path.cwd,
|
||||
root: app.path.root,
|
||||
@@ -584,12 +662,14 @@ export namespace Session {
|
||||
|
||||
for (const item of await Provider.tools(input.providerID)) {
|
||||
if (mode.tools[item.id] === false) continue
|
||||
if (input.tools?.[item.id] === false) continue
|
||||
if (session.parentID && item.id === "task") continue
|
||||
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 +708,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 +730,58 @@ 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,
|
||||
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 => ({
|
||||
@@ -681,12 +809,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 +843,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 +863,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 +901,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 +935,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
|
||||
}
|
||||
@@ -912,7 +1057,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 +1081,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 +1149,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 +1210,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 +1276,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,7 +1,7 @@
|
||||
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
|
||||
@@ -22,38 +22,39 @@ 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.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 () => {
|
||||
|
||||
@@ -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"
|
||||
@@ -86,7 +86,10 @@ export const EditTool = Tool.define({
|
||||
output += `\nThis file has errors, please fix\n<file_diagnostics>\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</file_diagnostics>\n`
|
||||
continue
|
||||
}
|
||||
output += `\n<project_diagnostics>\n${file}\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</project_diagnostics>\n`
|
||||
output += `\n<project_diagnostics>\n${file}\n${issues
|
||||
.filter((item) => item.severity === 1)
|
||||
.map(LSP.Diagnostic.pretty)
|
||||
.join("\n")}\n</project_diagnostics>\n`
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -102,6 +105,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
|
||||
}
|
||||
@@ -157,8 +185,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
|
||||
@@ -167,25 +197,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) {
|
||||
@@ -198,23 +316,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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -454,7 +572,7 @@ export function replace(content: string, oldString: string, newString: string, r
|
||||
BlockAnchorReplacer,
|
||||
WhitespaceNormalizedReplacer,
|
||||
IndentationFlexibleReplacer,
|
||||
// EscapeNormalizedReplacer,
|
||||
EscapeNormalizedReplacer,
|
||||
// TrimmedBoundaryReplacer,
|
||||
// ContextAwareReplacer,
|
||||
// MultiOccurrenceReplacer,
|
||||
|
||||
@@ -10,12 +10,22 @@ export const MultiEditTool = Tool.define({
|
||||
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()
|
||||
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,
|
||||
|
||||
@@ -5,59 +5,71 @@ 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(async () => {
|
||||
const agents = await Agent.list()
|
||||
const description = DESCRIPTION.replace("{agents}", agents.map((a) => `- ${a.name}: ${a.description}`).join("\n"))
|
||||
return {
|
||||
id: "task",
|
||||
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,
|
||||
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>
|
||||
|
||||
@@ -25,8 +25,8 @@ export namespace Tool {
|
||||
}
|
||||
|
||||
export function define<Parameters extends StandardSchemaV1, Result extends Metadata>(
|
||||
input: Info<Parameters, Result>,
|
||||
): Info<Parameters, Result> {
|
||||
return input
|
||||
input: Info<Parameters, Result> | (() => Promise<Info<Parameters, Result>>),
|
||||
): () => Promise<Info<Parameters, Result>> {
|
||||
return input instanceof Function ? input : async () => input
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
const list = await ListTool()
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
64
packages/sdk/release-please-config.json
Normal file
64
packages/sdk/release-please-config.json
Normal file
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"packages": {
|
||||
".": {}
|
||||
},
|
||||
"$schema": "https://raw.githubusercontent.com/stainless-api/release-please/main/schemas/config.json",
|
||||
"include-v-in-tag": true,
|
||||
"include-component-in-tag": false,
|
||||
"versioning": "prerelease",
|
||||
"prerelease": true,
|
||||
"bump-minor-pre-major": true,
|
||||
"bump-patch-for-minor-pre-major": false,
|
||||
"pull-request-header": "Automated Release PR",
|
||||
"pull-request-title-pattern": "release: ${version}",
|
||||
"changelog-sections": [
|
||||
{
|
||||
"type": "feat",
|
||||
"section": "Features"
|
||||
},
|
||||
{
|
||||
"type": "fix",
|
||||
"section": "Bug Fixes"
|
||||
},
|
||||
{
|
||||
"type": "perf",
|
||||
"section": "Performance Improvements"
|
||||
},
|
||||
{
|
||||
"type": "revert",
|
||||
"section": "Reverts"
|
||||
},
|
||||
{
|
||||
"type": "chore",
|
||||
"section": "Chores"
|
||||
},
|
||||
{
|
||||
"type": "docs",
|
||||
"section": "Documentation"
|
||||
},
|
||||
{
|
||||
"type": "style",
|
||||
"section": "Styles"
|
||||
},
|
||||
{
|
||||
"type": "refactor",
|
||||
"section": "Refactors"
|
||||
},
|
||||
{
|
||||
"type": "test",
|
||||
"section": "Tests",
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"type": "build",
|
||||
"section": "Build System"
|
||||
},
|
||||
{
|
||||
"type": "ci",
|
||||
"section": "Continuous Integration",
|
||||
"hidden": true
|
||||
}
|
||||
],
|
||||
"release-type": "node",
|
||||
"extra-files": ["src/version.ts", "README.md"]
|
||||
}
|
||||
18
packages/sdk/scripts/bootstrap
Executable file
18
packages/sdk/scripts/bootstrap
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ]; then
|
||||
brew bundle check >/dev/null 2>&1 || {
|
||||
echo "==> Installing Homebrew dependencies…"
|
||||
brew bundle
|
||||
}
|
||||
fi
|
||||
|
||||
echo "==> Installing Node dependencies…"
|
||||
|
||||
PACKAGE_MANAGER=$(command -v yarn >/dev/null 2>&1 && echo "yarn" || echo "npm")
|
||||
|
||||
$PACKAGE_MANAGER install
|
||||
51
packages/sdk/scripts/build
Executable file
51
packages/sdk/scripts/build
Executable file
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -exuo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
node scripts/utils/check-version.cjs
|
||||
|
||||
# Build into dist and will publish the package from there,
|
||||
# so that src/resources/foo.ts becomes <package root>/resources/foo.js
|
||||
# This way importing from `"@opencode-ai/sdk/resources/foo"` works
|
||||
# even with `"moduleResolution": "node"`
|
||||
|
||||
rm -rf dist; mkdir dist
|
||||
# Copy src to dist/src and build from dist/src into dist, so that
|
||||
# the source map for index.js.map will refer to ./src/index.ts etc
|
||||
cp -rp src README.md dist
|
||||
for file in LICENSE CHANGELOG.md; do
|
||||
if [ -e "${file}" ]; then cp "${file}" dist; fi
|
||||
done
|
||||
if [ -e "bin/cli" ]; then
|
||||
mkdir -p dist/bin
|
||||
cp -p "bin/cli" dist/bin/;
|
||||
fi
|
||||
if [ -e "bin/migration-config.json" ]; then
|
||||
mkdir -p dist/bin
|
||||
cp -p "bin/migration-config.json" dist/bin/;
|
||||
fi
|
||||
# this converts the export map paths for the dist directory
|
||||
# and does a few other minor things
|
||||
node scripts/utils/make-dist-package-json.cjs > dist/package.json
|
||||
|
||||
# build to .js/.mjs/.d.ts files
|
||||
./node_modules/.bin/tsc-multi
|
||||
# we need to patch index.js so that `new module.exports()` works for cjs backwards
|
||||
# compat. No way to get that from index.ts because it would cause compile errors
|
||||
# when building .mjs
|
||||
node scripts/utils/fix-index-exports.cjs
|
||||
cp tsconfig.dist-src.json dist/src/tsconfig.json
|
||||
|
||||
node scripts/utils/postprocess-files.cjs
|
||||
|
||||
# make sure that nothing crashes when we require the output CJS or
|
||||
# import the output ESM
|
||||
(cd dist && node -e 'require("@opencode-ai/sdk")')
|
||||
(cd dist && node -e 'import("@opencode-ai/sdk")' --input-type=module)
|
||||
|
||||
if [ -e ./scripts/build-deno ]
|
||||
then
|
||||
./scripts/build-deno
|
||||
fi
|
||||
12
packages/sdk/scripts/format
Executable file
12
packages/sdk/scripts/format
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
echo "==> Running eslint --fix"
|
||||
./node_modules/.bin/eslint --fix .
|
||||
|
||||
echo "==> Running prettier --write"
|
||||
# format things eslint didn't
|
||||
./node_modules/.bin/prettier --write --cache --cache-strategy metadata . '!**/dist' '!**/*.ts' '!**/*.mts' '!**/*.cts' '!**/*.js' '!**/*.mjs' '!**/*.cjs'
|
||||
21
packages/sdk/scripts/lint
Executable file
21
packages/sdk/scripts/lint
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
echo "==> Running eslint"
|
||||
./node_modules/.bin/eslint .
|
||||
|
||||
echo "==> Building"
|
||||
./scripts/build
|
||||
|
||||
echo "==> Checking types"
|
||||
./node_modules/typescript/bin/tsc
|
||||
|
||||
echo "==> Running Are The Types Wrong?"
|
||||
./node_modules/.bin/attw --pack dist -f json >.attw.json || true
|
||||
node scripts/utils/attw-report.cjs
|
||||
|
||||
echo "==> Running publint"
|
||||
./node_modules/.bin/publint dist
|
||||
41
packages/sdk/scripts/mock
Executable file
41
packages/sdk/scripts/mock
Executable file
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
if [[ -n "$1" && "$1" != '--'* ]]; then
|
||||
URL="$1"
|
||||
shift
|
||||
else
|
||||
URL="$(grep 'openapi_spec_url' .stats.yml | cut -d' ' -f2)"
|
||||
fi
|
||||
|
||||
# Check if the URL is empty
|
||||
if [ -z "$URL" ]; then
|
||||
echo "Error: No OpenAPI spec path/url provided or found in .stats.yml"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> Starting mock server with URL ${URL}"
|
||||
|
||||
# Run prism mock on the given spec
|
||||
if [ "$1" == "--daemon" ]; then
|
||||
npm exec --package=@stainless-api/prism-cli@5.8.5 -- prism mock "$URL" &> .prism.log &
|
||||
|
||||
# Wait for server to come online
|
||||
echo -n "Waiting for server"
|
||||
while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do
|
||||
echo -n "."
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
if grep -q "✖ fatal" ".prism.log"; then
|
||||
cat .prism.log
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo
|
||||
else
|
||||
npm exec --package=@stainless-api/prism-cli@5.8.5 -- prism mock "$URL"
|
||||
fi
|
||||
56
packages/sdk/scripts/test
Executable file
56
packages/sdk/scripts/test
Executable file
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
function prism_is_running() {
|
||||
curl --silent "http://localhost:4010" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
kill_server_on_port() {
|
||||
pids=$(lsof -t -i tcp:"$1" || echo "")
|
||||
if [ "$pids" != "" ]; then
|
||||
kill "$pids"
|
||||
echo "Stopped $pids."
|
||||
fi
|
||||
}
|
||||
|
||||
function is_overriding_api_base_url() {
|
||||
[ -n "$TEST_API_BASE_URL" ]
|
||||
}
|
||||
|
||||
if ! is_overriding_api_base_url && ! prism_is_running ; then
|
||||
# When we exit this script, make sure to kill the background mock server process
|
||||
trap 'kill_server_on_port 4010' EXIT
|
||||
|
||||
# Start the dev server
|
||||
./scripts/mock --daemon
|
||||
fi
|
||||
|
||||
if is_overriding_api_base_url ; then
|
||||
echo -e "${GREEN}✔ Running tests against ${TEST_API_BASE_URL}${NC}"
|
||||
echo
|
||||
elif ! prism_is_running ; then
|
||||
echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Prism server"
|
||||
echo -e "running against your OpenAPI spec."
|
||||
echo
|
||||
echo -e "To run the server, pass in the path or url of your OpenAPI"
|
||||
echo -e "spec to the prism command:"
|
||||
echo
|
||||
echo -e " \$ ${YELLOW}npm exec --package=@stoplight/prism-cli@~5.3.2 -- prism mock path/to/your.openapi.yml${NC}"
|
||||
echo
|
||||
|
||||
exit 1
|
||||
else
|
||||
echo -e "${GREEN}✔ Mock prism server is running with your OpenAPI spec${NC}"
|
||||
echo
|
||||
fi
|
||||
|
||||
echo "==> Running tests"
|
||||
./node_modules/.bin/jest "$@"
|
||||
24
packages/sdk/scripts/utils/attw-report.cjs
Normal file
24
packages/sdk/scripts/utils/attw-report.cjs
Normal file
@@ -0,0 +1,24 @@
|
||||
const fs = require('fs');
|
||||
const problems = Object.values(JSON.parse(fs.readFileSync('.attw.json', 'utf-8')).problems)
|
||||
.flat()
|
||||
.filter(
|
||||
(problem) =>
|
||||
!(
|
||||
// This is intentional, if the user specifies .mjs they get ESM.
|
||||
(
|
||||
(problem.kind === 'CJSResolvesToESM' && problem.entrypoint.endsWith('.mjs')) ||
|
||||
// This is intentional for backwards compat reasons.
|
||||
(problem.kind === 'MissingExportEquals' && problem.implementationFileName.endsWith('/index.js')) ||
|
||||
// this is intentional, we deliberately attempt to import types that may not exist from parent node_modules
|
||||
// folders to better support various runtimes without triggering automatic type acquisition.
|
||||
(problem.kind === 'InternalResolutionError' && problem.moduleSpecifier.includes('node_modules'))
|
||||
)
|
||||
),
|
||||
);
|
||||
fs.unlinkSync('.attw.json');
|
||||
if (problems.length) {
|
||||
process.stdout.write('The types are wrong!\n' + JSON.stringify(problems, null, 2) + '\n');
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
process.stdout.write('Types ok!\n');
|
||||
}
|
||||
9
packages/sdk/scripts/utils/check-is-in-git-install.sh
Executable file
9
packages/sdk/scripts/utils/check-is-in-git-install.sh
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
# Check if you happen to call prepare for a repository that's already in node_modules.
|
||||
[ "$(basename "$(dirname "$PWD")")" = 'node_modules' ] ||
|
||||
# The name of the containing directory that 'npm` uses, which looks like
|
||||
# $HOME/.npm/_cacache/git-cloneXXXXXX
|
||||
[ "$(basename "$(dirname "$PWD")")" = 'tmp' ] ||
|
||||
# The name of the containing directory that 'yarn` uses, which looks like
|
||||
# $(yarn cache dir)/.tmp/XXXXX
|
||||
[ "$(basename "$(dirname "$PWD")")" = '.tmp' ]
|
||||
20
packages/sdk/scripts/utils/check-version.cjs
Normal file
20
packages/sdk/scripts/utils/check-version.cjs
Normal file
@@ -0,0 +1,20 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const main = () => {
|
||||
const pkg = require('../../package.json');
|
||||
const version = pkg['version'];
|
||||
if (!version) throw 'The version property is not set in the package.json file';
|
||||
if (typeof version !== 'string') {
|
||||
throw `Unexpected type for the package.json version field; got ${typeof version}, expected string`;
|
||||
}
|
||||
|
||||
const versionFile = path.resolve(__dirname, '..', '..', 'src', 'version.ts');
|
||||
const contents = fs.readFileSync(versionFile, 'utf8');
|
||||
const output = contents.replace(/(export const VERSION = ')(.*)(')/g, `$1${version}$3`);
|
||||
fs.writeFileSync(versionFile, output);
|
||||
};
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
17
packages/sdk/scripts/utils/fix-index-exports.cjs
Normal file
17
packages/sdk/scripts/utils/fix-index-exports.cjs
Normal file
@@ -0,0 +1,17 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const indexJs =
|
||||
process.env['DIST_PATH'] ?
|
||||
path.resolve(process.env['DIST_PATH'], 'index.js')
|
||||
: path.resolve(__dirname, '..', '..', 'dist', 'index.js');
|
||||
|
||||
let before = fs.readFileSync(indexJs, 'utf8');
|
||||
let after = before.replace(
|
||||
/^(\s*Object\.defineProperty\s*\(exports,\s*["']__esModule["'].+)$/m,
|
||||
`exports = module.exports = function (...args) {
|
||||
return new exports.default(...args)
|
||||
}
|
||||
$1`.replace(/^ /gm, ''),
|
||||
);
|
||||
fs.writeFileSync(indexJs, after, 'utf8');
|
||||
13
packages/sdk/scripts/utils/git-swap.sh
Executable file
13
packages/sdk/scripts/utils/git-swap.sh
Executable file
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
set -exuo pipefail
|
||||
# the package is published to NPM from ./dist
|
||||
# we want the final file structure for git installs to match the npm installs, so we
|
||||
|
||||
# delete everything except ./dist and ./node_modules
|
||||
find . -maxdepth 1 -mindepth 1 ! -name 'dist' ! -name 'node_modules' -exec rm -rf '{}' +
|
||||
|
||||
# move everything from ./dist to .
|
||||
mv dist/* .
|
||||
|
||||
# delete the now-empty ./dist
|
||||
rmdir dist
|
||||
21
packages/sdk/scripts/utils/make-dist-package-json.cjs
Normal file
21
packages/sdk/scripts/utils/make-dist-package-json.cjs
Normal file
@@ -0,0 +1,21 @@
|
||||
const pkgJson = require(process.env['PKG_JSON_PATH'] || '../../package.json');
|
||||
|
||||
function processExportMap(m) {
|
||||
for (const key in m) {
|
||||
const value = m[key];
|
||||
if (typeof value === 'string') m[key] = value.replace(/^\.\/dist\//, './');
|
||||
else processExportMap(value);
|
||||
}
|
||||
}
|
||||
processExportMap(pkgJson.exports);
|
||||
|
||||
for (const key of ['types', 'main', 'module']) {
|
||||
if (typeof pkgJson[key] === 'string') pkgJson[key] = pkgJson[key].replace(/^(\.\/)?dist\//, './');
|
||||
}
|
||||
|
||||
delete pkgJson.devDependencies;
|
||||
delete pkgJson.scripts.prepack;
|
||||
delete pkgJson.scripts.prepublishOnly;
|
||||
delete pkgJson.scripts.prepare;
|
||||
|
||||
console.log(JSON.stringify(pkgJson, null, 2));
|
||||
94
packages/sdk/scripts/utils/postprocess-files.cjs
Normal file
94
packages/sdk/scripts/utils/postprocess-files.cjs
Normal file
@@ -0,0 +1,94 @@
|
||||
// @ts-check
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const distDir =
|
||||
process.env['DIST_PATH'] ?
|
||||
path.resolve(process.env['DIST_PATH'])
|
||||
: path.resolve(__dirname, '..', '..', 'dist');
|
||||
|
||||
async function* walk(dir) {
|
||||
for await (const d of await fs.promises.opendir(dir)) {
|
||||
const entry = path.join(dir, d.name);
|
||||
if (d.isDirectory()) yield* walk(entry);
|
||||
else if (d.isFile()) yield entry;
|
||||
}
|
||||
}
|
||||
|
||||
async function postprocess() {
|
||||
for await (const file of walk(distDir)) {
|
||||
if (!/(\.d)?[cm]?ts$/.test(file)) continue;
|
||||
|
||||
const code = await fs.promises.readFile(file, 'utf8');
|
||||
|
||||
// strip out lib="dom", types="node", and types="react" references; these
|
||||
// are needed at build time, but would pollute the user's TS environment
|
||||
const transformed = code.replace(
|
||||
/^ *\/\/\/ *<reference +(lib="dom"|types="(node|react)").*?\n/gm,
|
||||
// replace with same number of characters to avoid breaking source maps
|
||||
(match) => ' '.repeat(match.length - 1) + '\n',
|
||||
);
|
||||
|
||||
if (transformed !== code) {
|
||||
console.error(`wrote ${path.relative(process.cwd(), file)}`);
|
||||
await fs.promises.writeFile(file, transformed, 'utf8');
|
||||
}
|
||||
}
|
||||
|
||||
const newExports = {
|
||||
'.': {
|
||||
require: {
|
||||
types: './index.d.ts',
|
||||
default: './index.js',
|
||||
},
|
||||
types: './index.d.mts',
|
||||
default: './index.mjs',
|
||||
},
|
||||
};
|
||||
|
||||
for (const entry of await fs.promises.readdir(distDir, { withFileTypes: true })) {
|
||||
if (entry.isDirectory() && entry.name !== 'src' && entry.name !== 'internal' && entry.name !== 'bin') {
|
||||
const subpath = './' + entry.name;
|
||||
newExports[subpath + '/*.mjs'] = {
|
||||
default: subpath + '/*.mjs',
|
||||
};
|
||||
newExports[subpath + '/*.js'] = {
|
||||
default: subpath + '/*.js',
|
||||
};
|
||||
newExports[subpath + '/*'] = {
|
||||
import: subpath + '/*.mjs',
|
||||
require: subpath + '/*.js',
|
||||
};
|
||||
} else if (entry.isFile() && /\.[cm]?js$/.test(entry.name)) {
|
||||
const { name, ext } = path.parse(entry.name);
|
||||
const subpathWithoutExt = './' + name;
|
||||
const subpath = './' + entry.name;
|
||||
newExports[subpathWithoutExt] ||= { import: undefined, require: undefined };
|
||||
const isModule = ext[1] === 'm';
|
||||
if (isModule) {
|
||||
newExports[subpathWithoutExt].import = subpath;
|
||||
} else {
|
||||
newExports[subpathWithoutExt].require = subpath;
|
||||
}
|
||||
newExports[subpath] = {
|
||||
default: subpath,
|
||||
};
|
||||
}
|
||||
}
|
||||
await fs.promises.writeFile(
|
||||
'dist/package.json',
|
||||
JSON.stringify(
|
||||
Object.assign(
|
||||
/** @type {Record<String, unknown>} */ (
|
||||
JSON.parse(await fs.promises.readFile('dist/package.json', 'utf-8'))
|
||||
),
|
||||
{
|
||||
exports: newExports,
|
||||
},
|
||||
),
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
}
|
||||
postprocess();
|
||||
25
packages/sdk/scripts/utils/upload-artifact.sh
Executable file
25
packages/sdk/scripts/utils/upload-artifact.sh
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
set -exuo pipefail
|
||||
|
||||
RESPONSE=$(curl -X POST "$URL" \
|
||||
-H "Authorization: Bearer $AUTH" \
|
||||
-H "Content-Type: application/json")
|
||||
|
||||
SIGNED_URL=$(echo "$RESPONSE" | jq -r '.url')
|
||||
|
||||
if [[ "$SIGNED_URL" == "null" ]]; then
|
||||
echo -e "\033[31mFailed to get signed URL.\033[0m"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
UPLOAD_RESPONSE=$(tar -cz dist | curl -v -X PUT \
|
||||
-H "Content-Type: application/gzip" \
|
||||
--data-binary @- "$SIGNED_URL" 2>&1)
|
||||
|
||||
if echo "$UPLOAD_RESPONSE" | grep -q "HTTP/[0-9.]* 200"; then
|
||||
echo -e "\033[32mUploaded build to Stainless storage.\033[0m"
|
||||
echo -e "\033[32mInstallation: npm install 'https://pkg.stainless.com/s/opencode-typescript/$SHA'\033[0m"
|
||||
else
|
||||
echo -e "\033[31mFailed to upload artifact.\033[0m"
|
||||
exit 1
|
||||
fi
|
||||
2
packages/sdk/src/api-promise.ts
Normal file
2
packages/sdk/src/api-promise.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
/** @deprecated Import from ./core/api-promise instead */
|
||||
export * from './core/api-promise';
|
||||
864
packages/sdk/src/client.ts
Normal file
864
packages/sdk/src/client.ts
Normal file
@@ -0,0 +1,864 @@
|
||||
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
import type { RequestInit, RequestInfo, BodyInit } from './internal/builtin-types';
|
||||
import type { HTTPMethod, PromiseOrValue, MergedRequestInit, FinalizedRequestInit } from './internal/types';
|
||||
import { uuid4 } from './internal/utils/uuid';
|
||||
import { validatePositiveInteger, isAbsoluteURL, safeJSON } from './internal/utils/values';
|
||||
import { sleep } from './internal/utils/sleep';
|
||||
export type { Logger, LogLevel } from './internal/utils/log';
|
||||
import { castToError, isAbortError } from './internal/errors';
|
||||
import type { APIResponseProps } from './internal/parse';
|
||||
import { getPlatformHeaders } from './internal/detect-platform';
|
||||
import * as Shims from './internal/shims';
|
||||
import * as Opts from './internal/request-options';
|
||||
import { VERSION } from './version';
|
||||
import * as Errors from './core/error';
|
||||
import * as Uploads from './core/uploads';
|
||||
import * as API from './resources/index';
|
||||
import { APIPromise } from './core/api-promise';
|
||||
import {
|
||||
App,
|
||||
AppInitResponse,
|
||||
AppLogParams,
|
||||
AppLogResponse,
|
||||
AppModesResponse,
|
||||
AppProvidersResponse,
|
||||
AppResource,
|
||||
Mode,
|
||||
Model,
|
||||
Provider,
|
||||
} from './resources/app';
|
||||
import {
|
||||
Config,
|
||||
ConfigResource,
|
||||
KeybindsConfig,
|
||||
McpLocalConfig,
|
||||
McpRemoteConfig,
|
||||
ModeConfig,
|
||||
} from './resources/config';
|
||||
import { Event, EventListResponse } from './resources/event';
|
||||
import { File, FileReadParams, FileReadResponse, FileResource, FileStatusResponse } from './resources/file';
|
||||
import {
|
||||
Find,
|
||||
FindFilesParams,
|
||||
FindFilesResponse,
|
||||
FindSymbolsParams,
|
||||
FindSymbolsResponse,
|
||||
FindTextParams,
|
||||
FindTextResponse,
|
||||
Match,
|
||||
Symbol,
|
||||
} from './resources/find';
|
||||
import {
|
||||
AssistantMessage,
|
||||
FilePart,
|
||||
FilePartInput,
|
||||
FilePartSource,
|
||||
FilePartSourceText,
|
||||
FileSource,
|
||||
Message,
|
||||
Part,
|
||||
Session,
|
||||
SessionAbortResponse,
|
||||
SessionChatParams,
|
||||
SessionDeleteResponse,
|
||||
SessionInitParams,
|
||||
SessionInitResponse,
|
||||
SessionListResponse,
|
||||
SessionMessagesResponse,
|
||||
SessionResource,
|
||||
SessionRevertParams,
|
||||
SessionSummarizeParams,
|
||||
SessionSummarizeResponse,
|
||||
SnapshotPart,
|
||||
StepFinishPart,
|
||||
StepStartPart,
|
||||
SymbolSource,
|
||||
TextPart,
|
||||
TextPartInput,
|
||||
ToolPart,
|
||||
ToolStateCompleted,
|
||||
ToolStateError,
|
||||
ToolStatePending,
|
||||
ToolStateRunning,
|
||||
UserMessage,
|
||||
} from './resources/session';
|
||||
import { Tui, TuiAppendPromptParams, TuiAppendPromptResponse, TuiOpenHelpResponse } from './resources/tui';
|
||||
import { type Fetch } from './internal/builtin-types';
|
||||
import { HeadersLike, NullableHeaders, buildHeaders } from './internal/headers';
|
||||
import { FinalRequestOptions, RequestOptions } from './internal/request-options';
|
||||
import { readEnv } from './internal/utils/env';
|
||||
import {
|
||||
type LogLevel,
|
||||
type Logger,
|
||||
formatRequestDetails,
|
||||
loggerFor,
|
||||
parseLogLevel,
|
||||
} from './internal/utils/log';
|
||||
import { isEmptyObj } from './internal/utils/values';
|
||||
|
||||
export interface ClientOptions {
|
||||
/**
|
||||
* Override the default base URL for the API, e.g., "https://api.example.com/v2/"
|
||||
*
|
||||
* Defaults to process.env['OPENCODE_BASE_URL'].
|
||||
*/
|
||||
baseURL?: string | null | undefined;
|
||||
|
||||
/**
|
||||
* The maximum amount of time (in milliseconds) that the client should wait for a response
|
||||
* from the server before timing out a single request.
|
||||
*
|
||||
* Note that request timeouts are retried by default, so in a worst-case scenario you may wait
|
||||
* much longer than this timeout before the promise succeeds or fails.
|
||||
*
|
||||
* @unit milliseconds
|
||||
*/
|
||||
timeout?: number | undefined;
|
||||
/**
|
||||
* Additional `RequestInit` options to be passed to `fetch` calls.
|
||||
* Properties will be overridden by per-request `fetchOptions`.
|
||||
*/
|
||||
fetchOptions?: MergedRequestInit | undefined;
|
||||
|
||||
/**
|
||||
* Specify a custom `fetch` function implementation.
|
||||
*
|
||||
* If not provided, we expect that `fetch` is defined globally.
|
||||
*/
|
||||
fetch?: Fetch | undefined;
|
||||
|
||||
/**
|
||||
* The maximum number of times that the client will retry a request in case of a
|
||||
* temporary failure, like a network error or a 5XX error from the server.
|
||||
*
|
||||
* @default 2
|
||||
*/
|
||||
maxRetries?: number | undefined;
|
||||
|
||||
/**
|
||||
* Default headers to include with every request to the API.
|
||||
*
|
||||
* These can be removed in individual requests by explicitly setting the
|
||||
* header to `null` in request options.
|
||||
*/
|
||||
defaultHeaders?: HeadersLike | undefined;
|
||||
|
||||
/**
|
||||
* Default query parameters to include with every request to the API.
|
||||
*
|
||||
* These can be removed in individual requests by explicitly setting the
|
||||
* param to `undefined` in request options.
|
||||
*/
|
||||
defaultQuery?: Record<string, string | undefined> | undefined;
|
||||
|
||||
/**
|
||||
* Set the log level.
|
||||
*
|
||||
* Defaults to process.env['OPENCODE_LOG'] or 'warn' if it isn't set.
|
||||
*/
|
||||
logLevel?: LogLevel | undefined;
|
||||
|
||||
/**
|
||||
* Set the logger.
|
||||
*
|
||||
* Defaults to globalThis.console.
|
||||
*/
|
||||
logger?: Logger | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* API Client for interfacing with the Opencode API.
|
||||
*/
|
||||
export class Opencode {
|
||||
baseURL: string;
|
||||
maxRetries: number;
|
||||
timeout: number;
|
||||
logger: Logger | undefined;
|
||||
logLevel: LogLevel | undefined;
|
||||
fetchOptions: MergedRequestInit | undefined;
|
||||
|
||||
private fetch: Fetch;
|
||||
#encoder: Opts.RequestEncoder;
|
||||
protected idempotencyHeader?: string;
|
||||
private _options: ClientOptions;
|
||||
|
||||
/**
|
||||
* API Client for interfacing with the Opencode API.
|
||||
*
|
||||
* @param {string} [opts.baseURL=process.env['OPENCODE_BASE_URL'] ?? http://localhost:54321] - Override the default base URL for the API.
|
||||
* @param {number} [opts.timeout=1 minute] - The maximum amount of time (in milliseconds) the client will wait for a response before timing out.
|
||||
* @param {MergedRequestInit} [opts.fetchOptions] - Additional `RequestInit` options to be passed to `fetch` calls.
|
||||
* @param {Fetch} [opts.fetch] - Specify a custom `fetch` function implementation.
|
||||
* @param {number} [opts.maxRetries=2] - The maximum number of times the client will retry a request.
|
||||
* @param {HeadersLike} opts.defaultHeaders - Default headers to include with every request to the API.
|
||||
* @param {Record<string, string | undefined>} opts.defaultQuery - Default query parameters to include with every request to the API.
|
||||
*/
|
||||
constructor({ baseURL = readEnv('OPENCODE_BASE_URL'), ...opts }: ClientOptions = {}) {
|
||||
const options: ClientOptions = {
|
||||
...opts,
|
||||
baseURL: baseURL || `http://localhost:54321`,
|
||||
};
|
||||
|
||||
this.baseURL = options.baseURL!;
|
||||
this.timeout = options.timeout ?? Opencode.DEFAULT_TIMEOUT /* 1 minute */;
|
||||
this.logger = options.logger ?? console;
|
||||
const defaultLogLevel = 'warn';
|
||||
// Set default logLevel early so that we can log a warning in parseLogLevel.
|
||||
this.logLevel = defaultLogLevel;
|
||||
this.logLevel =
|
||||
parseLogLevel(options.logLevel, 'ClientOptions.logLevel', this) ??
|
||||
parseLogLevel(readEnv('OPENCODE_LOG'), "process.env['OPENCODE_LOG']", this) ??
|
||||
defaultLogLevel;
|
||||
this.fetchOptions = options.fetchOptions;
|
||||
this.maxRetries = options.maxRetries ?? 2;
|
||||
this.fetch = options.fetch ?? Shims.getDefaultFetch();
|
||||
this.#encoder = Opts.FallbackEncoder;
|
||||
|
||||
this._options = options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new client instance re-using the same options given to the current client with optional overriding.
|
||||
*/
|
||||
withOptions(options: Partial<ClientOptions>): this {
|
||||
const client = new (this.constructor as any as new (props: ClientOptions) => typeof this)({
|
||||
...this._options,
|
||||
baseURL: this.baseURL,
|
||||
maxRetries: this.maxRetries,
|
||||
timeout: this.timeout,
|
||||
logger: this.logger,
|
||||
logLevel: this.logLevel,
|
||||
fetch: this.fetch,
|
||||
fetchOptions: this.fetchOptions,
|
||||
...options,
|
||||
});
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the base URL is set to its default.
|
||||
*/
|
||||
#baseURLOverridden(): boolean {
|
||||
return this.baseURL !== 'http://localhost:54321';
|
||||
}
|
||||
|
||||
protected defaultQuery(): Record<string, string | undefined> | undefined {
|
||||
return this._options.defaultQuery;
|
||||
}
|
||||
|
||||
protected validateHeaders({ values, nulls }: NullableHeaders) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic re-implementation of `qs.stringify` for primitive types.
|
||||
*/
|
||||
protected stringifyQuery(query: Record<string, unknown>): string {
|
||||
return Object.entries(query)
|
||||
.filter(([_, value]) => typeof value !== 'undefined')
|
||||
.map(([key, value]) => {
|
||||
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
||||
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
||||
}
|
||||
if (value === null) {
|
||||
return `${encodeURIComponent(key)}=`;
|
||||
}
|
||||
throw new Errors.OpencodeError(
|
||||
`Cannot stringify type ${typeof value}; Expected string, number, boolean, or null. If you need to pass nested query parameters, you can manually encode them, e.g. { query: { 'foo[key1]': value1, 'foo[key2]': value2 } }, and please open a GitHub issue requesting better support for your use case.`,
|
||||
);
|
||||
})
|
||||
.join('&');
|
||||
}
|
||||
|
||||
private getUserAgent(): string {
|
||||
return `${this.constructor.name}/JS ${VERSION}`;
|
||||
}
|
||||
|
||||
protected defaultIdempotencyKey(): string {
|
||||
return `stainless-node-retry-${uuid4()}`;
|
||||
}
|
||||
|
||||
protected makeStatusError(
|
||||
status: number,
|
||||
error: Object,
|
||||
message: string | undefined,
|
||||
headers: Headers,
|
||||
): Errors.APIError {
|
||||
return Errors.APIError.generate(status, error, message, headers);
|
||||
}
|
||||
|
||||
buildURL(
|
||||
path: string,
|
||||
query: Record<string, unknown> | null | undefined,
|
||||
defaultBaseURL?: string | undefined,
|
||||
): string {
|
||||
const baseURL = (!this.#baseURLOverridden() && defaultBaseURL) || this.baseURL;
|
||||
const url =
|
||||
isAbsoluteURL(path) ?
|
||||
new URL(path)
|
||||
: new URL(baseURL + (baseURL.endsWith('/') && path.startsWith('/') ? path.slice(1) : path));
|
||||
|
||||
const defaultQuery = this.defaultQuery();
|
||||
if (!isEmptyObj(defaultQuery)) {
|
||||
query = { ...defaultQuery, ...query };
|
||||
}
|
||||
|
||||
if (typeof query === 'object' && query && !Array.isArray(query)) {
|
||||
url.search = this.stringifyQuery(query as Record<string, unknown>);
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Used as a callback for mutating the given `FinalRequestOptions` object.
|
||||
*/
|
||||
protected async prepareOptions(options: FinalRequestOptions): Promise<void> {}
|
||||
|
||||
/**
|
||||
* Used as a callback for mutating the given `RequestInit` object.
|
||||
*
|
||||
* This is useful for cases where you want to add certain headers based off of
|
||||
* the request properties, e.g. `method` or `url`.
|
||||
*/
|
||||
protected async prepareRequest(
|
||||
request: RequestInit,
|
||||
{ url, options }: { url: string; options: FinalRequestOptions },
|
||||
): Promise<void> {}
|
||||
|
||||
get<Rsp>(path: string, opts?: PromiseOrValue<RequestOptions>): APIPromise<Rsp> {
|
||||
return this.methodRequest('get', path, opts);
|
||||
}
|
||||
|
||||
post<Rsp>(path: string, opts?: PromiseOrValue<RequestOptions>): APIPromise<Rsp> {
|
||||
return this.methodRequest('post', path, opts);
|
||||
}
|
||||
|
||||
patch<Rsp>(path: string, opts?: PromiseOrValue<RequestOptions>): APIPromise<Rsp> {
|
||||
return this.methodRequest('patch', path, opts);
|
||||
}
|
||||
|
||||
put<Rsp>(path: string, opts?: PromiseOrValue<RequestOptions>): APIPromise<Rsp> {
|
||||
return this.methodRequest('put', path, opts);
|
||||
}
|
||||
|
||||
delete<Rsp>(path: string, opts?: PromiseOrValue<RequestOptions>): APIPromise<Rsp> {
|
||||
return this.methodRequest('delete', path, opts);
|
||||
}
|
||||
|
||||
private methodRequest<Rsp>(
|
||||
method: HTTPMethod,
|
||||
path: string,
|
||||
opts?: PromiseOrValue<RequestOptions>,
|
||||
): APIPromise<Rsp> {
|
||||
return this.request(
|
||||
Promise.resolve(opts).then((opts) => {
|
||||
return { method, path, ...opts };
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
request<Rsp>(
|
||||
options: PromiseOrValue<FinalRequestOptions>,
|
||||
remainingRetries: number | null = null,
|
||||
): APIPromise<Rsp> {
|
||||
return new APIPromise(this, this.makeRequest(options, remainingRetries, undefined));
|
||||
}
|
||||
|
||||
private async makeRequest(
|
||||
optionsInput: PromiseOrValue<FinalRequestOptions>,
|
||||
retriesRemaining: number | null,
|
||||
retryOfRequestLogID: string | undefined,
|
||||
): Promise<APIResponseProps> {
|
||||
const options = await optionsInput;
|
||||
const maxRetries = options.maxRetries ?? this.maxRetries;
|
||||
if (retriesRemaining == null) {
|
||||
retriesRemaining = maxRetries;
|
||||
}
|
||||
|
||||
await this.prepareOptions(options);
|
||||
|
||||
const { req, url, timeout } = await this.buildRequest(options, {
|
||||
retryCount: maxRetries - retriesRemaining,
|
||||
});
|
||||
|
||||
await this.prepareRequest(req, { url, options });
|
||||
|
||||
/** Not an API request ID, just for correlating local log entries. */
|
||||
const requestLogID = 'log_' + ((Math.random() * (1 << 24)) | 0).toString(16).padStart(6, '0');
|
||||
const retryLogStr = retryOfRequestLogID === undefined ? '' : `, retryOf: ${retryOfRequestLogID}`;
|
||||
const startTime = Date.now();
|
||||
|
||||
loggerFor(this).debug(
|
||||
`[${requestLogID}] sending request`,
|
||||
formatRequestDetails({
|
||||
retryOfRequestLogID,
|
||||
method: options.method,
|
||||
url,
|
||||
options,
|
||||
headers: req.headers,
|
||||
}),
|
||||
);
|
||||
|
||||
if (options.signal?.aborted) {
|
||||
throw new Errors.APIUserAbortError();
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const response = await this.fetchWithTimeout(url, req, timeout, controller).catch(castToError);
|
||||
const headersTime = Date.now();
|
||||
|
||||
if (response instanceof Error) {
|
||||
const retryMessage = `retrying, ${retriesRemaining} attempts remaining`;
|
||||
if (options.signal?.aborted) {
|
||||
throw new Errors.APIUserAbortError();
|
||||
}
|
||||
// detect native connection timeout errors
|
||||
// deno throws "TypeError: error sending request for url (https://example/): client error (Connect): tcp connect error: Operation timed out (os error 60): Operation timed out (os error 60)"
|
||||
// undici throws "TypeError: fetch failed" with cause "ConnectTimeoutError: Connect Timeout Error (attempted address: example:443, timeout: 1ms)"
|
||||
// others do not provide enough information to distinguish timeouts from other connection errors
|
||||
const isTimeout =
|
||||
isAbortError(response) ||
|
||||
/timed? ?out/i.test(String(response) + ('cause' in response ? String(response.cause) : ''));
|
||||
if (retriesRemaining) {
|
||||
loggerFor(this).info(
|
||||
`[${requestLogID}] connection ${isTimeout ? 'timed out' : 'failed'} - ${retryMessage}`,
|
||||
);
|
||||
loggerFor(this).debug(
|
||||
`[${requestLogID}] connection ${isTimeout ? 'timed out' : 'failed'} (${retryMessage})`,
|
||||
formatRequestDetails({
|
||||
retryOfRequestLogID,
|
||||
url,
|
||||
durationMs: headersTime - startTime,
|
||||
message: response.message,
|
||||
}),
|
||||
);
|
||||
return this.retryRequest(options, retriesRemaining, retryOfRequestLogID ?? requestLogID);
|
||||
}
|
||||
loggerFor(this).info(
|
||||
`[${requestLogID}] connection ${isTimeout ? 'timed out' : 'failed'} - error; no more retries left`,
|
||||
);
|
||||
loggerFor(this).debug(
|
||||
`[${requestLogID}] connection ${isTimeout ? 'timed out' : 'failed'} (error; no more retries left)`,
|
||||
formatRequestDetails({
|
||||
retryOfRequestLogID,
|
||||
url,
|
||||
durationMs: headersTime - startTime,
|
||||
message: response.message,
|
||||
}),
|
||||
);
|
||||
if (isTimeout) {
|
||||
throw new Errors.APIConnectionTimeoutError();
|
||||
}
|
||||
throw new Errors.APIConnectionError({ cause: response });
|
||||
}
|
||||
|
||||
const responseInfo = `[${requestLogID}${retryLogStr}] ${req.method} ${url} ${
|
||||
response.ok ? 'succeeded' : 'failed'
|
||||
} with status ${response.status} in ${headersTime - startTime}ms`;
|
||||
|
||||
if (!response.ok) {
|
||||
const shouldRetry = await this.shouldRetry(response);
|
||||
if (retriesRemaining && shouldRetry) {
|
||||
const retryMessage = `retrying, ${retriesRemaining} attempts remaining`;
|
||||
|
||||
// We don't need the body of this response.
|
||||
await Shims.CancelReadableStream(response.body);
|
||||
loggerFor(this).info(`${responseInfo} - ${retryMessage}`);
|
||||
loggerFor(this).debug(
|
||||
`[${requestLogID}] response error (${retryMessage})`,
|
||||
formatRequestDetails({
|
||||
retryOfRequestLogID,
|
||||
url: response.url,
|
||||
status: response.status,
|
||||
headers: response.headers,
|
||||
durationMs: headersTime - startTime,
|
||||
}),
|
||||
);
|
||||
return this.retryRequest(
|
||||
options,
|
||||
retriesRemaining,
|
||||
retryOfRequestLogID ?? requestLogID,
|
||||
response.headers,
|
||||
);
|
||||
}
|
||||
|
||||
const retryMessage = shouldRetry ? `error; no more retries left` : `error; not retryable`;
|
||||
|
||||
loggerFor(this).info(`${responseInfo} - ${retryMessage}`);
|
||||
|
||||
const errText = await response.text().catch((err: any) => castToError(err).message);
|
||||
const errJSON = safeJSON(errText);
|
||||
const errMessage = errJSON ? undefined : errText;
|
||||
|
||||
loggerFor(this).debug(
|
||||
`[${requestLogID}] response error (${retryMessage})`,
|
||||
formatRequestDetails({
|
||||
retryOfRequestLogID,
|
||||
url: response.url,
|
||||
status: response.status,
|
||||
headers: response.headers,
|
||||
message: errMessage,
|
||||
durationMs: Date.now() - startTime,
|
||||
}),
|
||||
);
|
||||
|
||||
const err = this.makeStatusError(response.status, errJSON, errMessage, response.headers);
|
||||
throw err;
|
||||
}
|
||||
|
||||
loggerFor(this).info(responseInfo);
|
||||
loggerFor(this).debug(
|
||||
`[${requestLogID}] response start`,
|
||||
formatRequestDetails({
|
||||
retryOfRequestLogID,
|
||||
url: response.url,
|
||||
status: response.status,
|
||||
headers: response.headers,
|
||||
durationMs: headersTime - startTime,
|
||||
}),
|
||||
);
|
||||
|
||||
return { response, options, controller, requestLogID, retryOfRequestLogID, startTime };
|
||||
}
|
||||
|
||||
async fetchWithTimeout(
|
||||
url: RequestInfo,
|
||||
init: RequestInit | undefined,
|
||||
ms: number,
|
||||
controller: AbortController,
|
||||
): Promise<Response> {
|
||||
const { signal, method, ...options } = init || {};
|
||||
if (signal) signal.addEventListener('abort', () => controller.abort());
|
||||
|
||||
const timeout = setTimeout(() => controller.abort(), ms);
|
||||
|
||||
const isReadableBody =
|
||||
((globalThis as any).ReadableStream && options.body instanceof (globalThis as any).ReadableStream) ||
|
||||
(typeof options.body === 'object' && options.body !== null && Symbol.asyncIterator in options.body);
|
||||
|
||||
const fetchOptions: RequestInit = {
|
||||
signal: controller.signal as any,
|
||||
...(isReadableBody ? { duplex: 'half' } : {}),
|
||||
method: 'GET',
|
||||
...options,
|
||||
};
|
||||
if (method) {
|
||||
// Custom methods like 'patch' need to be uppercased
|
||||
// See https://github.com/nodejs/undici/issues/2294
|
||||
fetchOptions.method = method.toUpperCase();
|
||||
}
|
||||
|
||||
try {
|
||||
// use undefined this binding; fetch errors if bound to something else in browser/cloudflare
|
||||
return await this.fetch.call(undefined, url, fetchOptions);
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
private async shouldRetry(response: Response): Promise<boolean> {
|
||||
// Note this is not a standard header.
|
||||
const shouldRetryHeader = response.headers.get('x-should-retry');
|
||||
|
||||
// If the server explicitly says whether or not to retry, obey.
|
||||
if (shouldRetryHeader === 'true') return true;
|
||||
if (shouldRetryHeader === 'false') return false;
|
||||
|
||||
// Retry on request timeouts.
|
||||
if (response.status === 408) return true;
|
||||
|
||||
// Retry on lock timeouts.
|
||||
if (response.status === 409) return true;
|
||||
|
||||
// Retry on rate limits.
|
||||
if (response.status === 429) return true;
|
||||
|
||||
// Retry internal errors.
|
||||
if (response.status >= 500) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async retryRequest(
|
||||
options: FinalRequestOptions,
|
||||
retriesRemaining: number,
|
||||
requestLogID: string,
|
||||
responseHeaders?: Headers | undefined,
|
||||
): Promise<APIResponseProps> {
|
||||
let timeoutMillis: number | undefined;
|
||||
|
||||
// Note the `retry-after-ms` header may not be standard, but is a good idea and we'd like proactive support for it.
|
||||
const retryAfterMillisHeader = responseHeaders?.get('retry-after-ms');
|
||||
if (retryAfterMillisHeader) {
|
||||
const timeoutMs = parseFloat(retryAfterMillisHeader);
|
||||
if (!Number.isNaN(timeoutMs)) {
|
||||
timeoutMillis = timeoutMs;
|
||||
}
|
||||
}
|
||||
|
||||
// About the Retry-After header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
|
||||
const retryAfterHeader = responseHeaders?.get('retry-after');
|
||||
if (retryAfterHeader && !timeoutMillis) {
|
||||
const timeoutSeconds = parseFloat(retryAfterHeader);
|
||||
if (!Number.isNaN(timeoutSeconds)) {
|
||||
timeoutMillis = timeoutSeconds * 1000;
|
||||
} else {
|
||||
timeoutMillis = Date.parse(retryAfterHeader) - Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
// If the API asks us to wait a certain amount of time (and it's a reasonable amount),
|
||||
// just do what it says, but otherwise calculate a default
|
||||
if (!(timeoutMillis && 0 <= timeoutMillis && timeoutMillis < 60 * 1000)) {
|
||||
const maxRetries = options.maxRetries ?? this.maxRetries;
|
||||
timeoutMillis = this.calculateDefaultRetryTimeoutMillis(retriesRemaining, maxRetries);
|
||||
}
|
||||
await sleep(timeoutMillis);
|
||||
|
||||
return this.makeRequest(options, retriesRemaining - 1, requestLogID);
|
||||
}
|
||||
|
||||
private calculateDefaultRetryTimeoutMillis(retriesRemaining: number, maxRetries: number): number {
|
||||
const initialRetryDelay = 0.5;
|
||||
const maxRetryDelay = 8.0;
|
||||
|
||||
const numRetries = maxRetries - retriesRemaining;
|
||||
|
||||
// Apply exponential backoff, but not more than the max.
|
||||
const sleepSeconds = Math.min(initialRetryDelay * Math.pow(2, numRetries), maxRetryDelay);
|
||||
|
||||
// Apply some jitter, take up to at most 25 percent of the retry time.
|
||||
const jitter = 1 - Math.random() * 0.25;
|
||||
|
||||
return sleepSeconds * jitter * 1000;
|
||||
}
|
||||
|
||||
async buildRequest(
|
||||
inputOptions: FinalRequestOptions,
|
||||
{ retryCount = 0 }: { retryCount?: number } = {},
|
||||
): Promise<{ req: FinalizedRequestInit; url: string; timeout: number }> {
|
||||
const options = { ...inputOptions };
|
||||
const { method, path, query, defaultBaseURL } = options;
|
||||
|
||||
const url = this.buildURL(path!, query as Record<string, unknown>, defaultBaseURL);
|
||||
if ('timeout' in options) validatePositiveInteger('timeout', options.timeout);
|
||||
options.timeout = options.timeout ?? this.timeout;
|
||||
const { bodyHeaders, body } = this.buildBody({ options });
|
||||
const reqHeaders = await this.buildHeaders({ options: inputOptions, method, bodyHeaders, retryCount });
|
||||
|
||||
const req: FinalizedRequestInit = {
|
||||
method,
|
||||
headers: reqHeaders,
|
||||
...(options.signal && { signal: options.signal }),
|
||||
...((globalThis as any).ReadableStream &&
|
||||
body instanceof (globalThis as any).ReadableStream && { duplex: 'half' }),
|
||||
...(body && { body }),
|
||||
...((this.fetchOptions as any) ?? {}),
|
||||
...((options.fetchOptions as any) ?? {}),
|
||||
};
|
||||
|
||||
return { req, url, timeout: options.timeout };
|
||||
}
|
||||
|
||||
private async buildHeaders({
|
||||
options,
|
||||
method,
|
||||
bodyHeaders,
|
||||
retryCount,
|
||||
}: {
|
||||
options: FinalRequestOptions;
|
||||
method: HTTPMethod;
|
||||
bodyHeaders: HeadersLike;
|
||||
retryCount: number;
|
||||
}): Promise<Headers> {
|
||||
let idempotencyHeaders: HeadersLike = {};
|
||||
if (this.idempotencyHeader && method !== 'get') {
|
||||
if (!options.idempotencyKey) options.idempotencyKey = this.defaultIdempotencyKey();
|
||||
idempotencyHeaders[this.idempotencyHeader] = options.idempotencyKey;
|
||||
}
|
||||
|
||||
const headers = buildHeaders([
|
||||
idempotencyHeaders,
|
||||
{
|
||||
Accept: 'application/json',
|
||||
'User-Agent': this.getUserAgent(),
|
||||
'X-Stainless-Retry-Count': String(retryCount),
|
||||
...(options.timeout ? { 'X-Stainless-Timeout': String(Math.trunc(options.timeout / 1000)) } : {}),
|
||||
...getPlatformHeaders(),
|
||||
},
|
||||
this._options.defaultHeaders,
|
||||
bodyHeaders,
|
||||
options.headers,
|
||||
]);
|
||||
|
||||
this.validateHeaders(headers);
|
||||
|
||||
return headers.values;
|
||||
}
|
||||
|
||||
private buildBody({ options: { body, headers: rawHeaders } }: { options: FinalRequestOptions }): {
|
||||
bodyHeaders: HeadersLike;
|
||||
body: BodyInit | undefined;
|
||||
} {
|
||||
if (!body) {
|
||||
return { bodyHeaders: undefined, body: undefined };
|
||||
}
|
||||
const headers = buildHeaders([rawHeaders]);
|
||||
if (
|
||||
// Pass raw type verbatim
|
||||
ArrayBuffer.isView(body) ||
|
||||
body instanceof ArrayBuffer ||
|
||||
body instanceof DataView ||
|
||||
(typeof body === 'string' &&
|
||||
// Preserve legacy string encoding behavior for now
|
||||
headers.values.has('content-type')) ||
|
||||
// `Blob` is superset of `File`
|
||||
body instanceof Blob ||
|
||||
// `FormData` -> `multipart/form-data`
|
||||
body instanceof FormData ||
|
||||
// `URLSearchParams` -> `application/x-www-form-urlencoded`
|
||||
body instanceof URLSearchParams ||
|
||||
// Send chunked stream (each chunk has own `length`)
|
||||
((globalThis as any).ReadableStream && body instanceof (globalThis as any).ReadableStream)
|
||||
) {
|
||||
return { bodyHeaders: undefined, body: body as BodyInit };
|
||||
} else if (
|
||||
typeof body === 'object' &&
|
||||
(Symbol.asyncIterator in body ||
|
||||
(Symbol.iterator in body && 'next' in body && typeof body.next === 'function'))
|
||||
) {
|
||||
return { bodyHeaders: undefined, body: Shims.ReadableStreamFrom(body as AsyncIterable<Uint8Array>) };
|
||||
} else {
|
||||
return this.#encoder({ body, headers });
|
||||
}
|
||||
}
|
||||
|
||||
static Opencode = this;
|
||||
static DEFAULT_TIMEOUT = 60000; // 1 minute
|
||||
|
||||
static OpencodeError = Errors.OpencodeError;
|
||||
static APIError = Errors.APIError;
|
||||
static APIConnectionError = Errors.APIConnectionError;
|
||||
static APIConnectionTimeoutError = Errors.APIConnectionTimeoutError;
|
||||
static APIUserAbortError = Errors.APIUserAbortError;
|
||||
static NotFoundError = Errors.NotFoundError;
|
||||
static ConflictError = Errors.ConflictError;
|
||||
static RateLimitError = Errors.RateLimitError;
|
||||
static BadRequestError = Errors.BadRequestError;
|
||||
static AuthenticationError = Errors.AuthenticationError;
|
||||
static InternalServerError = Errors.InternalServerError;
|
||||
static PermissionDeniedError = Errors.PermissionDeniedError;
|
||||
static UnprocessableEntityError = Errors.UnprocessableEntityError;
|
||||
|
||||
static toFile = Uploads.toFile;
|
||||
|
||||
event: API.Event = new API.Event(this);
|
||||
app: API.AppResource = new API.AppResource(this);
|
||||
find: API.Find = new API.Find(this);
|
||||
file: API.FileResource = new API.FileResource(this);
|
||||
config: API.ConfigResource = new API.ConfigResource(this);
|
||||
session: API.SessionResource = new API.SessionResource(this);
|
||||
tui: API.Tui = new API.Tui(this);
|
||||
}
|
||||
Opencode.Event = Event;
|
||||
Opencode.AppResource = AppResource;
|
||||
Opencode.Find = Find;
|
||||
Opencode.FileResource = FileResource;
|
||||
Opencode.ConfigResource = ConfigResource;
|
||||
Opencode.SessionResource = SessionResource;
|
||||
Opencode.Tui = Tui;
|
||||
export declare namespace Opencode {
|
||||
export type RequestOptions = Opts.RequestOptions;
|
||||
|
||||
export { Event as Event, type EventListResponse as EventListResponse };
|
||||
|
||||
export {
|
||||
AppResource as AppResource,
|
||||
type App as App,
|
||||
type Mode as Mode,
|
||||
type Model as Model,
|
||||
type Provider as Provider,
|
||||
type AppInitResponse as AppInitResponse,
|
||||
type AppLogResponse as AppLogResponse,
|
||||
type AppModesResponse as AppModesResponse,
|
||||
type AppProvidersResponse as AppProvidersResponse,
|
||||
type AppLogParams as AppLogParams,
|
||||
};
|
||||
|
||||
export {
|
||||
Find as Find,
|
||||
type Match as Match,
|
||||
type Symbol as Symbol,
|
||||
type FindFilesResponse as FindFilesResponse,
|
||||
type FindSymbolsResponse as FindSymbolsResponse,
|
||||
type FindTextResponse as FindTextResponse,
|
||||
type FindFilesParams as FindFilesParams,
|
||||
type FindSymbolsParams as FindSymbolsParams,
|
||||
type FindTextParams as FindTextParams,
|
||||
};
|
||||
|
||||
export {
|
||||
FileResource as FileResource,
|
||||
type File as File,
|
||||
type FileReadResponse as FileReadResponse,
|
||||
type FileStatusResponse as FileStatusResponse,
|
||||
type FileReadParams as FileReadParams,
|
||||
};
|
||||
|
||||
export {
|
||||
ConfigResource as ConfigResource,
|
||||
type Config as Config,
|
||||
type KeybindsConfig as KeybindsConfig,
|
||||
type McpLocalConfig as McpLocalConfig,
|
||||
type McpRemoteConfig as McpRemoteConfig,
|
||||
type ModeConfig as ModeConfig,
|
||||
};
|
||||
|
||||
export {
|
||||
SessionResource as SessionResource,
|
||||
type AssistantMessage as AssistantMessage,
|
||||
type FilePart as FilePart,
|
||||
type FilePartInput as FilePartInput,
|
||||
type FilePartSource as FilePartSource,
|
||||
type FilePartSourceText as FilePartSourceText,
|
||||
type FileSource as FileSource,
|
||||
type Message as Message,
|
||||
type Part as Part,
|
||||
type Session as Session,
|
||||
type SnapshotPart as SnapshotPart,
|
||||
type StepFinishPart as StepFinishPart,
|
||||
type StepStartPart as StepStartPart,
|
||||
type SymbolSource as SymbolSource,
|
||||
type TextPart as TextPart,
|
||||
type TextPartInput as TextPartInput,
|
||||
type ToolPart as ToolPart,
|
||||
type ToolStateCompleted as ToolStateCompleted,
|
||||
type ToolStateError as ToolStateError,
|
||||
type ToolStatePending as ToolStatePending,
|
||||
type ToolStateRunning as ToolStateRunning,
|
||||
type UserMessage as UserMessage,
|
||||
type SessionListResponse as SessionListResponse,
|
||||
type SessionDeleteResponse as SessionDeleteResponse,
|
||||
type SessionAbortResponse as SessionAbortResponse,
|
||||
type SessionInitResponse as SessionInitResponse,
|
||||
type SessionMessagesResponse as SessionMessagesResponse,
|
||||
type SessionSummarizeResponse as SessionSummarizeResponse,
|
||||
type SessionChatParams as SessionChatParams,
|
||||
type SessionInitParams as SessionInitParams,
|
||||
type SessionRevertParams as SessionRevertParams,
|
||||
type SessionSummarizeParams as SessionSummarizeParams,
|
||||
};
|
||||
|
||||
export {
|
||||
Tui as Tui,
|
||||
type TuiAppendPromptResponse as TuiAppendPromptResponse,
|
||||
type TuiOpenHelpResponse as TuiOpenHelpResponse,
|
||||
type TuiAppendPromptParams as TuiAppendPromptParams,
|
||||
};
|
||||
|
||||
export type MessageAbortedError = API.MessageAbortedError;
|
||||
export type ProviderAuthError = API.ProviderAuthError;
|
||||
export type UnknownError = API.UnknownError;
|
||||
}
|
||||
3
packages/sdk/src/core/README.md
Normal file
3
packages/sdk/src/core/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# `core`
|
||||
|
||||
This directory holds public modules implementing non-resource-specific SDK functionality.
|
||||
92
packages/sdk/src/core/api-promise.ts
Normal file
92
packages/sdk/src/core/api-promise.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
import { type Opencode } from '../client';
|
||||
|
||||
import { type PromiseOrValue } from '../internal/types';
|
||||
import { APIResponseProps, defaultParseResponse } from '../internal/parse';
|
||||
|
||||
/**
|
||||
* A subclass of `Promise` providing additional helper methods
|
||||
* for interacting with the SDK.
|
||||
*/
|
||||
export class APIPromise<T> extends Promise<T> {
|
||||
private parsedPromise: Promise<T> | undefined;
|
||||
#client: Opencode;
|
||||
|
||||
constructor(
|
||||
client: Opencode,
|
||||
private responsePromise: Promise<APIResponseProps>,
|
||||
private parseResponse: (
|
||||
client: Opencode,
|
||||
props: APIResponseProps,
|
||||
) => PromiseOrValue<T> = defaultParseResponse,
|
||||
) {
|
||||
super((resolve) => {
|
||||
// this is maybe a bit weird but this has to be a no-op to not implicitly
|
||||
// parse the response body; instead .then, .catch, .finally are overridden
|
||||
// to parse the response
|
||||
resolve(null as any);
|
||||
});
|
||||
this.#client = client;
|
||||
}
|
||||
|
||||
_thenUnwrap<U>(transform: (data: T, props: APIResponseProps) => U): APIPromise<U> {
|
||||
return new APIPromise(this.#client, this.responsePromise, async (client, props) =>
|
||||
transform(await this.parseResponse(client, props), props),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the raw `Response` instance instead of parsing the response
|
||||
* data.
|
||||
*
|
||||
* If you want to parse the response body but still get the `Response`
|
||||
* instance, you can use {@link withResponse()}.
|
||||
*
|
||||
* 👋 Getting the wrong TypeScript type for `Response`?
|
||||
* Try setting `"moduleResolution": "NodeNext"` or add `"lib": ["DOM"]`
|
||||
* to your `tsconfig.json`.
|
||||
*/
|
||||
asResponse(): Promise<Response> {
|
||||
return this.responsePromise.then((p) => p.response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the parsed response data and the raw `Response` instance.
|
||||
*
|
||||
* If you just want to get the raw `Response` instance without parsing it,
|
||||
* you can use {@link asResponse()}.
|
||||
*
|
||||
* 👋 Getting the wrong TypeScript type for `Response`?
|
||||
* Try setting `"moduleResolution": "NodeNext"` or add `"lib": ["DOM"]`
|
||||
* to your `tsconfig.json`.
|
||||
*/
|
||||
async withResponse(): Promise<{ data: T; response: Response }> {
|
||||
const [data, response] = await Promise.all([this.parse(), this.asResponse()]);
|
||||
return { data, response };
|
||||
}
|
||||
|
||||
private parse(): Promise<T> {
|
||||
if (!this.parsedPromise) {
|
||||
this.parsedPromise = this.responsePromise.then((data) => this.parseResponse(this.#client, data));
|
||||
}
|
||||
return this.parsedPromise;
|
||||
}
|
||||
|
||||
override then<TResult1 = T, TResult2 = never>(
|
||||
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
|
||||
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null,
|
||||
): Promise<TResult1 | TResult2> {
|
||||
return this.parse().then(onfulfilled, onrejected);
|
||||
}
|
||||
|
||||
override catch<TResult = never>(
|
||||
onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null,
|
||||
): Promise<T | TResult> {
|
||||
return this.parse().catch(onrejected);
|
||||
}
|
||||
|
||||
override finally(onfinally?: (() => void) | undefined | null): Promise<T> {
|
||||
return this.parse().finally(onfinally);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user