Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
857a3cd522 | ||
|
|
6ed774ef62 | ||
|
|
5e825a4b6a | ||
|
|
3db8e7c2b6 | ||
|
|
b459055757 | ||
|
|
2b195e82ee | ||
|
|
58e889796c | ||
|
|
51498c8de4 | ||
|
|
7a495faa49 | ||
|
|
4957fca718 | ||
|
|
8168626cd3 | ||
|
|
b824809605 | ||
|
|
5536b14347 | ||
|
|
01efe236ef | ||
|
|
7a1f96399d | ||
|
|
40036abb9d | ||
|
|
2970ba6416 | ||
|
|
81412b6197 | ||
|
|
5bf7691ea6 | ||
|
|
b1055a74d3 | ||
|
|
ffcb27fa9a | ||
|
|
38819e89b8 | ||
|
|
0a42068fbb | ||
|
|
b05decc572 | ||
|
|
c2f487906a | ||
|
|
ae78ec7a0c | ||
|
|
e8c03f13dd | ||
|
|
f85d30c484 | ||
|
|
1bac46612c | ||
|
|
9ab3462821 | ||
|
|
3b36822696 | ||
|
|
5b731479d5 | ||
|
|
a50bef6913 | ||
|
|
ed397c5057 | ||
|
|
c9187a9f3a | ||
|
|
2c67b26b5d | ||
|
|
170b94a99e | ||
|
|
cd58f10e3c | ||
|
|
ea85fdf3cd | ||
|
|
edda26ab33 | ||
|
|
ea4e1913c0 | ||
|
|
5eebc8ab51 | ||
|
|
21c52fd5cb | ||
|
|
5e8634afaf | ||
|
|
d4bac5cdbd | ||
|
|
263b266476 | ||
|
|
06830327e7 | ||
|
|
4b204fee58 | ||
|
|
99d3a0bb24 | ||
|
|
0930f6ac55 | ||
|
|
24515162fa | ||
|
|
53aa899e45 | ||
|
|
7e763e1c06 | ||
|
|
b0f2cc0c22 | ||
|
|
f90aa62784 | ||
|
|
852191f6cb | ||
|
|
c5e9dc081c | ||
|
|
49c8889228 | ||
|
|
f739e1a958 | ||
|
|
841f1907bb | ||
|
|
9255c507d6 | ||
|
|
2711047166 | ||
|
|
908048baef | ||
|
|
a9fbe07408 | ||
|
|
0ae213ee0e | ||
|
|
ca031278ca | ||
|
|
ae6e47bb42 | ||
|
|
42a5fcead4 | ||
|
|
8ad83f71a9 | ||
|
|
fa95c09cdc | ||
|
|
0b132c032a | ||
|
|
44d7103a42 | ||
|
|
8f45a0e227 | ||
|
|
6581741318 | ||
|
|
80d68d01f4 | ||
|
|
fa9db3c167 | ||
|
|
5a727c0794 | ||
|
|
71cd84dbbb | ||
|
|
e1b7e25f4d | ||
|
|
98b6bb218b | ||
|
|
5592ce8eaf | ||
|
|
510fe8a72a | ||
|
|
04a1ab3893 | ||
|
|
e74b4d098b | ||
|
|
50e4b3e6a7 | ||
|
|
6ebd828aa5 | ||
|
|
022c979d28 | ||
|
|
4172e3ad28 | ||
|
|
90d1698aed | ||
|
|
b0c38ce56b | ||
|
|
9b37d0e191 | ||
|
|
ea794a4bf6 | ||
|
|
52f9b37576 | ||
|
|
a0d2e53bde | ||
|
|
851e900982 | ||
|
|
3aa6eeb426 | ||
|
|
b6ee8e92f9 | ||
|
|
44211e1526 | ||
|
|
12f84f198f | ||
|
|
e6db1cf29d | ||
|
|
f07f04d969 | ||
|
|
33d613a470 | ||
|
|
0bbd7ea17b | ||
|
|
87f3166437 | ||
|
|
7665bd9439 | ||
|
|
30e10127f2 | ||
|
|
5e66fc2318 | ||
|
|
c1c99c7e0f | ||
|
|
04e3e83db3 | ||
|
|
4273714a62 | ||
|
|
a21e237706 | ||
|
|
aa9105649d | ||
|
|
53be288040 | ||
|
|
13dbf912ca | ||
|
|
69966c73f8 | ||
|
|
a00de2df08 | ||
|
|
5e72f50554 | ||
|
|
d558f15c91 | ||
|
|
614a23698f | ||
|
|
a2191ce6fb | ||
|
|
168350c981 | ||
|
|
f5f55062f1 | ||
|
|
360194e219 | ||
|
|
5ee994c31f | ||
|
|
fc73d4b1f9 | ||
|
|
936f4cb0c6 | ||
|
|
a5b20f973f | ||
|
|
872b1e068f | ||
|
|
e4e0b8fd34 | ||
|
|
c5368e7412 | ||
|
|
1d682544b9 | ||
|
|
d9210af98c | ||
|
|
ef633fe92e | ||
|
|
5500698734 | ||
|
|
e7631763f3 | ||
|
|
18a572b079 | ||
|
|
060a62ecfb | ||
|
|
ac3813549a | ||
|
|
b14da5fb1f | ||
|
|
416f2235fc | ||
|
|
4fabca426a | ||
|
|
7e9050edb9 | ||
|
|
3c49a9b7dd | ||
|
|
ad66b97463 | ||
|
|
10a0b7f60c | ||
|
|
ac8709ac7a | ||
|
|
2d9ed06367 | ||
|
|
50be2aee39 | ||
|
|
0bdbe6261a | ||
|
|
33cef075d2 | ||
|
|
b09ebf4645 | ||
|
|
3268c61813 | ||
|
|
4a221868da | ||
|
|
31b8e3d5ab | ||
|
|
1a78d833a8 | ||
|
|
18888351e9 | ||
|
|
3b7085ca28 | ||
|
|
160923dcf0 | ||
|
|
c38b091895 | ||
|
|
eecfd6d0ca | ||
|
|
6ef4cfa2fa | ||
|
|
190dee080c | ||
|
|
09074dc639 | ||
|
|
1b3d58e791 | ||
|
|
772c83c1d5 | ||
|
|
54dc937fa1 | ||
|
|
b5219f7585 | ||
|
|
0bd0453866 |
@@ -7,8 +7,10 @@ on:
|
||||
jobs:
|
||||
opencode:
|
||||
if: |
|
||||
contains(github.event.comment.body, '/oc') ||
|
||||
contains(github.event.comment.body, '/opencode')
|
||||
contains(github.event.comment.body, ' /oc') ||
|
||||
startsWith(github.event.comment.body, '/oc') ||
|
||||
contains(github.event.comment.body, ' /opencode') ||
|
||||
startsWith(github.event.comment.body, '/opencode')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -24,4 +26,4 @@ jobs:
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
with:
|
||||
model: anthropic/claude-sonnet-4-20250514
|
||||
model: anthropic/claude-sonnet-4-20250514
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
name: publish
|
||||
run-name: "${{ format('v{0}', inputs.version) }}"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
tags:
|
||||
- "*"
|
||||
- "!vscode-v*"
|
||||
- "!github-v*"
|
||||
inputs:
|
||||
version:
|
||||
description: "Version to publish"
|
||||
required: true
|
||||
type: string
|
||||
title:
|
||||
description: "Custom title for this run"
|
||||
required: false
|
||||
type: string
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
@@ -34,7 +37,16 @@ jobs:
|
||||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.2.17
|
||||
bun-version: 1.2.19
|
||||
|
||||
- name: Cache ~/.bun
|
||||
id: cache-bun
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.bun
|
||||
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-bun-
|
||||
|
||||
- name: Install makepkg
|
||||
run: |
|
||||
@@ -50,15 +62,12 @@ jobs:
|
||||
git config --global user.email "opencode@sst.dev"
|
||||
git config --global user.name "opencode"
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
- name: Publish
|
||||
run: |
|
||||
bun install
|
||||
if [ "${{ startsWith(github.ref, 'refs/tags/') }}" = "true" ]; then
|
||||
./script/publish.ts
|
||||
else
|
||||
./script/publish.ts --snapshot
|
||||
fi
|
||||
working-directory: ./packages/opencode
|
||||
OPENCODE_VERSION=${{ inputs.version }} ./script/publish.ts
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
|
||||
AUR_KEY: ${{ secrets.AUR_KEY }}
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
bun-version: latest
|
||||
|
||||
- name: Run stats script
|
||||
run: bun scripts/stats.ts
|
||||
run: bun script/stats.ts
|
||||
|
||||
- name: Commit stats
|
||||
run: |
|
||||
@@ -30,3 +30,5 @@ jobs:
|
||||
git add STATS.md
|
||||
git diff --staged --quiet || git commit -m "ignore: update download stats $(date -I)"
|
||||
git push
|
||||
env:
|
||||
POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
name: Typecheck
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [dev]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: 1.2.19
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
- name: Run typecheck
|
||||
run: bun typecheck
|
||||
+1
-1
@@ -5,4 +5,4 @@ node_modules
|
||||
.idea
|
||||
.vscode
|
||||
openapi.json
|
||||
scratch
|
||||
playground
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Plugin } from "./index"
|
||||
|
||||
export const ExamplePlugin: Plugin = async ({ app, client, $ }) => {
|
||||
return {
|
||||
permission: {},
|
||||
async "chat.params"(input, output) {
|
||||
output.topP = 1
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
## Style
|
||||
## IMPORTANT
|
||||
|
||||
- prefer single word variable/function names
|
||||
- avoid try catch where possible - prefer to let exceptions bubble up
|
||||
- avoid else statements where possible
|
||||
- do not make useless helper functions - inline functionality unless the
|
||||
function is reusable or composable
|
||||
- prefer Bun apis
|
||||
|
||||
## Workflow
|
||||
|
||||
- you can regenerate the golang sdk by calling ./scripts/stainless.ts
|
||||
- we use bun for everything
|
||||
- Try to keep things in one function unless composable or reusable
|
||||
- DO NOT do unnecessary destructuring of variables
|
||||
- DO NOT use `else` statements unless necessary
|
||||
- DO NOT use `try`/`catch` if it can be avoided
|
||||
- AVOID `try`/`catch` where possible
|
||||
- AVOID `else` statements
|
||||
- AVOID using `any` type
|
||||
- AVOID `let` statements
|
||||
- PREFER single word variable names where possible
|
||||
- Use as many bun apis as possible like Bun.file()
|
||||
|
||||
@@ -1,33 +1,41 @@
|
||||
# Download Stats
|
||||
|
||||
| Date | GitHub Downloads | npm Downloads | Total |
|
||||
| ---------- | ---------------- | ---------------- | ----------------- |
|
||||
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
|
||||
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
|
||||
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
|
||||
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
|
||||
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
|
||||
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
|
||||
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
|
||||
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
|
||||
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
|
||||
| 2025-07-10 | 43,796 (+5,744) | 71,402 (+6,934) | 115,198 (+12,678) |
|
||||
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
|
||||
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
|
||||
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
|
||||
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
|
||||
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
|
||||
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
|
||||
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
|
||||
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
|
||||
| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
|
||||
| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
|
||||
| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
|
||||
| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
|
||||
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
|
||||
| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
|
||||
| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
|
||||
| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
|
||||
| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
|
||||
| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
|
||||
| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) |
|
||||
| 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-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) |
|
||||
| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) |
|
||||
| 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) |
|
||||
| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
|
||||
| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
|
||||
| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
|
||||
| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) |
|
||||
| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) |
|
||||
| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) |
|
||||
| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) |
|
||||
| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) |
|
||||
| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) |
|
||||
| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) |
|
||||
| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) |
|
||||
|
||||
@@ -46,3 +46,15 @@ new sst.cloudflare.x.Astro("Web", {
|
||||
VITE_API_URL: api.url,
|
||||
},
|
||||
})
|
||||
|
||||
const OPENCODE_API_KEY = new sst.Secret("OPENCODE_API_KEY")
|
||||
const ANTHROPIC_API_KEY = new sst.Secret("ANTHROPIC_API_KEY")
|
||||
const OPENAI_API_KEY = new sst.Secret("OPENAI_API_KEY")
|
||||
const ZHIPU_API_KEY = new sst.Secret("ZHIPU_API_KEY")
|
||||
|
||||
export const gateway = new sst.cloudflare.Worker("GatewayApi", {
|
||||
domain: `api.gateway.${domain}`,
|
||||
handler: "packages/function/src/gateway.ts",
|
||||
url: true,
|
||||
link: [OPENCODE_API_KEY, ANTHROPIC_API_KEY, OPENAI_API_KEY, ZHIPU_API_KEY],
|
||||
})
|
||||
|
||||
@@ -1,24 +1,5 @@
|
||||
{
|
||||
"$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": {
|
||||
"context7": {
|
||||
"type": "remote",
|
||||
|
||||
+11
-6
@@ -5,21 +5,24 @@
|
||||
"type": "module",
|
||||
"packageManager": "bun@1.2.14",
|
||||
"scripts": {
|
||||
"dev": "bun run packages/opencode/src/index.ts",
|
||||
"dev": "bun run --conditions=development packages/opencode/src/index.ts",
|
||||
"typecheck": "bun run --filter='*' typecheck",
|
||||
"stainless": "./scripts/stainless",
|
||||
"postinstall": "./scripts/hooks"
|
||||
"postinstall": "./script/hooks"
|
||||
},
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
"packages/*"
|
||||
"packages/*",
|
||||
"packages/sdk/js"
|
||||
],
|
||||
"catalog": {
|
||||
"@types/node": "22.13.9",
|
||||
"ai": "5.0.0-beta.21",
|
||||
"@tsconfig/node22": "22.0.2",
|
||||
"ai": "5.0.0-beta.34",
|
||||
"hono": "4.7.10",
|
||||
"typescript": "5.8.2",
|
||||
"zod": "3.25.49"
|
||||
"zod": "3.25.49",
|
||||
"remeda": "2.26.0"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -40,5 +43,7 @@
|
||||
"protobufjs",
|
||||
"sharp"
|
||||
],
|
||||
"patchedDependencies": {}
|
||||
"patchedDependencies": {
|
||||
"marked-shiki@1.2.0": "patches/marked-shiki@1.2.0.patch"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
{
|
||||
"name": "@opencode/function",
|
||||
"version": "0.0.1",
|
||||
"version": "0.3.130",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "4.20250522.0",
|
||||
"typescript": "catalog:",
|
||||
"@types/node": "catalog:"
|
||||
"@types/node": "catalog:",
|
||||
"openai": "5.11.0",
|
||||
"typescript": "catalog:"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "2.0.0",
|
||||
"@ai-sdk/openai": "2.0.2",
|
||||
"@ai-sdk/openai-compatible": "1.0.1",
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"ai": "catalog:",
|
||||
"hono": "catalog:",
|
||||
"jose": "6.0.11"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,499 @@
|
||||
import { Hono, Context, Next } from "hono"
|
||||
import { Resource } from "sst"
|
||||
import { generateText, streamText } from "ai"
|
||||
import { createAnthropic } from "@ai-sdk/anthropic"
|
||||
import { createOpenAI } from "@ai-sdk/openai"
|
||||
import { createOpenAICompatible } from "@ai-sdk/openai-compatible"
|
||||
import { type LanguageModelV2Prompt } from "@ai-sdk/provider"
|
||||
import { type ChatCompletionCreateParamsBase } from "openai/resources/chat/completions"
|
||||
|
||||
type Env = {}
|
||||
|
||||
const auth = async (c: Context, next: Next) => {
|
||||
const authHeader = c.req.header("authorization")
|
||||
|
||||
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
||||
return c.json(
|
||||
{
|
||||
error: {
|
||||
message: "Missing API key.",
|
||||
type: "invalid_request_error",
|
||||
param: null,
|
||||
code: "unauthorized",
|
||||
},
|
||||
},
|
||||
401,
|
||||
)
|
||||
}
|
||||
|
||||
const apiKey = authHeader.split(" ")[1]
|
||||
|
||||
// Replace with your validation logic
|
||||
if (apiKey !== Resource.OPENCODE_API_KEY.value) {
|
||||
return c.json(
|
||||
{
|
||||
error: {
|
||||
message: "Invalid API key.",
|
||||
type: "invalid_request_error",
|
||||
param: null,
|
||||
code: "unauthorized",
|
||||
},
|
||||
},
|
||||
401,
|
||||
)
|
||||
}
|
||||
|
||||
await next()
|
||||
}
|
||||
export default new Hono<{ Bindings: Env }>()
|
||||
.get("/", (c) => c.text("Hello, world!"))
|
||||
.post("/v1/chat/completions", auth, async (c) => {
|
||||
try {
|
||||
const body = await c.req.json<ChatCompletionCreateParamsBase>()
|
||||
|
||||
console.log(body)
|
||||
|
||||
const model = (() => {
|
||||
const [provider, ...parts] = body.model.split("/")
|
||||
const model = parts.join("/")
|
||||
if (provider === "anthropic" && model === "claude-sonnet-4") {
|
||||
return createAnthropic({
|
||||
apiKey: Resource.ANTHROPIC_API_KEY.value,
|
||||
})("claude-sonnet-4-20250514")
|
||||
}
|
||||
if (provider === "openai" && model === "gpt-4.1") {
|
||||
return createOpenAI({
|
||||
apiKey: Resource.OPENAI_API_KEY.value,
|
||||
})("gpt-4.1")
|
||||
}
|
||||
if (provider === "zhipuai" && model === "glm-4.5-flash") {
|
||||
return createOpenAICompatible({
|
||||
name: "Zhipu AI",
|
||||
baseURL: "https://api.z.ai/api/paas/v4",
|
||||
apiKey: Resource.ZHIPU_API_KEY.value,
|
||||
})("glm-4.5-flash")
|
||||
}
|
||||
throw new Error(`Unsupported provider: ${provider}`)
|
||||
})()
|
||||
|
||||
const requestBody = transformOpenAIRequestToAiSDK()
|
||||
|
||||
return body.stream ? await handleStream() : await handleGenerate()
|
||||
|
||||
async function handleStream() {
|
||||
const result = await streamText({
|
||||
model,
|
||||
...requestBody,
|
||||
})
|
||||
|
||||
const encoder = new TextEncoder()
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
const id = `chatcmpl-${Date.now()}`
|
||||
const created = Math.floor(Date.now() / 1000)
|
||||
|
||||
try {
|
||||
for await (const chunk of result.fullStream) {
|
||||
// TODO
|
||||
//console.log("!!! CHUCK !!!", chunk);
|
||||
switch (chunk.type) {
|
||||
case "text-delta": {
|
||||
const data = {
|
||||
id,
|
||||
object: "chat.completion.chunk",
|
||||
created,
|
||||
model: body.model,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: {
|
||||
content: chunk.text,
|
||||
},
|
||||
finish_reason: null,
|
||||
},
|
||||
],
|
||||
}
|
||||
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
|
||||
break
|
||||
}
|
||||
|
||||
case "reasoning-delta": {
|
||||
const data = {
|
||||
id,
|
||||
object: "chat.completion.chunk",
|
||||
created,
|
||||
model: body.model,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: {
|
||||
reasoning_content: chunk.text,
|
||||
},
|
||||
finish_reason: null,
|
||||
},
|
||||
],
|
||||
}
|
||||
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
|
||||
break
|
||||
}
|
||||
|
||||
case "tool-call": {
|
||||
const data = {
|
||||
id,
|
||||
object: "chat.completion.chunk",
|
||||
created,
|
||||
model: body.model,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: {
|
||||
tool_calls: [
|
||||
{
|
||||
id: chunk.toolCallId,
|
||||
type: "function",
|
||||
function: {
|
||||
name: chunk.toolName,
|
||||
arguments: JSON.stringify(chunk.input),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
finish_reason: null,
|
||||
},
|
||||
],
|
||||
}
|
||||
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
|
||||
break
|
||||
}
|
||||
|
||||
case "error": {
|
||||
const data = {
|
||||
id,
|
||||
object: "chat.completion.chunk",
|
||||
created,
|
||||
model: body.model,
|
||||
error: {
|
||||
message: chunk.error,
|
||||
type: "server_error",
|
||||
},
|
||||
}
|
||||
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
|
||||
controller.enqueue(encoder.encode("data: [DONE]\n\n"))
|
||||
controller.close()
|
||||
break
|
||||
}
|
||||
|
||||
case "finish": {
|
||||
const finishReason =
|
||||
{
|
||||
stop: "stop",
|
||||
length: "length",
|
||||
"content-filter": "content_filter",
|
||||
"tool-calls": "tool_calls",
|
||||
error: "stop",
|
||||
other: "stop",
|
||||
unknown: "stop",
|
||||
}[chunk.finishReason] || "stop"
|
||||
|
||||
const data = {
|
||||
id,
|
||||
object: "chat.completion.chunk",
|
||||
created,
|
||||
model: body.model,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: {},
|
||||
finish_reason: finishReason,
|
||||
},
|
||||
],
|
||||
usage: {
|
||||
prompt_tokens: chunk.totalUsage.inputTokens,
|
||||
completion_tokens: chunk.totalUsage.outputTokens,
|
||||
total_tokens: chunk.totalUsage.totalTokens,
|
||||
completion_tokens_details: {
|
||||
reasoning_tokens: chunk.totalUsage.reasoningTokens,
|
||||
},
|
||||
prompt_tokens_details: {
|
||||
cached_tokens: chunk.totalUsage.cachedInputTokens,
|
||||
},
|
||||
},
|
||||
}
|
||||
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
|
||||
controller.enqueue(encoder.encode("data: [DONE]\n\n"))
|
||||
controller.close()
|
||||
break
|
||||
}
|
||||
|
||||
//case "stream-start":
|
||||
//case "response-metadata":
|
||||
case "start-step":
|
||||
case "finish-step":
|
||||
case "text-start":
|
||||
case "text-end":
|
||||
case "reasoning-start":
|
||||
case "reasoning-end":
|
||||
case "tool-input-start":
|
||||
case "tool-input-delta":
|
||||
case "tool-input-end":
|
||||
case "raw":
|
||||
default:
|
||||
// Log unknown chunk types for debugging
|
||||
console.warn(`Unknown chunk type: ${(chunk as any).type}`)
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
controller.error(error)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
"Content-Type": "text/plain; charset=utf-8",
|
||||
"Cache-Control": "no-cache",
|
||||
Connection: "keep-alive",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function handleGenerate() {
|
||||
const response = await generateText({
|
||||
model,
|
||||
...requestBody,
|
||||
})
|
||||
return c.json({
|
||||
id: `chatcmpl-${Date.now()}`,
|
||||
object: "chat.completion" as const,
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: body.model,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
message: {
|
||||
role: "assistant" as const,
|
||||
content: response.content?.find((c) => c.type === "text")?.text ?? "",
|
||||
reasoning_content: response.content?.find((c) => c.type === "reasoning")?.text,
|
||||
tool_calls: response.content
|
||||
?.filter((c) => c.type === "tool-call")
|
||||
.map((toolCall) => ({
|
||||
id: toolCall.toolCallId,
|
||||
type: "function" as const,
|
||||
function: {
|
||||
name: toolCall.toolName,
|
||||
arguments: toolCall.input,
|
||||
},
|
||||
})),
|
||||
},
|
||||
finish_reason:
|
||||
(
|
||||
{
|
||||
stop: "stop",
|
||||
length: "length",
|
||||
"content-filter": "content_filter",
|
||||
"tool-calls": "tool_calls",
|
||||
error: "stop",
|
||||
other: "stop",
|
||||
unknown: "stop",
|
||||
} as const
|
||||
)[response.finishReason] || "stop",
|
||||
},
|
||||
],
|
||||
usage: {
|
||||
prompt_tokens: response.usage?.inputTokens,
|
||||
completion_tokens: response.usage?.outputTokens,
|
||||
total_tokens: response.usage?.totalTokens,
|
||||
completion_tokens_details: {
|
||||
reasoning_tokens: response.usage?.reasoningTokens,
|
||||
},
|
||||
prompt_tokens_details: {
|
||||
cached_tokens: response.usage?.cachedInputTokens,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function transformOpenAIRequestToAiSDK() {
|
||||
const prompt = transformMessages()
|
||||
|
||||
return {
|
||||
prompt,
|
||||
maxOutputTokens: body.max_tokens ?? body.max_completion_tokens ?? undefined,
|
||||
temperature: body.temperature ?? undefined,
|
||||
topP: body.top_p ?? undefined,
|
||||
frequencyPenalty: body.frequency_penalty ?? undefined,
|
||||
presencePenalty: body.presence_penalty ?? undefined,
|
||||
providerOptions: body.reasoning_effort
|
||||
? {
|
||||
anthropic: {
|
||||
reasoningEffort: body.reasoning_effort,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
stopSequences: (typeof body.stop === "string" ? [body.stop] : body.stop) ?? undefined,
|
||||
responseFormat: (() => {
|
||||
if (!body.response_format) return { type: "text" }
|
||||
if (body.response_format.type === "json_schema")
|
||||
return {
|
||||
type: "json",
|
||||
schema: body.response_format.json_schema.schema,
|
||||
name: body.response_format.json_schema.name,
|
||||
description: body.response_format.json_schema.description,
|
||||
}
|
||||
if (body.response_format.type === "json_object") return { type: "json" }
|
||||
throw new Error("Unsupported response format")
|
||||
})(),
|
||||
seed: body.seed ?? undefined,
|
||||
}
|
||||
|
||||
function transformTools() {
|
||||
const { tools, tool_choice } = body
|
||||
|
||||
if (!tools || tools.length === 0) {
|
||||
return { tools: undefined, toolChoice: undefined }
|
||||
}
|
||||
|
||||
const aiSdkTools = tools.reduce(
|
||||
(acc, tool) => {
|
||||
acc[tool.function.name] = {
|
||||
type: "function" as const,
|
||||
name: tool.function.name,
|
||||
description: tool.function.description,
|
||||
inputSchema: tool.function.parameters,
|
||||
}
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, any>,
|
||||
)
|
||||
|
||||
let aiSdkToolChoice
|
||||
if (tool_choice == null) {
|
||||
aiSdkToolChoice = undefined
|
||||
} else if (tool_choice === "auto") {
|
||||
aiSdkToolChoice = "auto"
|
||||
} else if (tool_choice === "none") {
|
||||
aiSdkToolChoice = "none"
|
||||
} else if (tool_choice === "required") {
|
||||
aiSdkToolChoice = "required"
|
||||
} else if (tool_choice.type === "function") {
|
||||
aiSdkToolChoice = {
|
||||
type: "tool",
|
||||
toolName: tool_choice.function.name,
|
||||
}
|
||||
}
|
||||
|
||||
return { tools: aiSdkTools, toolChoice: aiSdkToolChoice }
|
||||
}
|
||||
|
||||
function transformMessages() {
|
||||
const { messages } = body
|
||||
const prompt: LanguageModelV2Prompt = []
|
||||
|
||||
for (const message of messages) {
|
||||
switch (message.role) {
|
||||
case "system": {
|
||||
prompt.push({
|
||||
role: "system",
|
||||
content: message.content as string,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case "user": {
|
||||
if (typeof message.content === "string") {
|
||||
prompt.push({
|
||||
role: "user",
|
||||
content: [{ type: "text", text: message.content }],
|
||||
})
|
||||
} else {
|
||||
const content = message.content.map((part) => {
|
||||
switch (part.type) {
|
||||
case "text":
|
||||
return { type: "text" as const, text: part.text }
|
||||
case "image_url":
|
||||
return {
|
||||
type: "file" as const,
|
||||
mediaType: "image/jpeg" as const,
|
||||
data: part.image_url.url,
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unsupported content part type: ${(part as any).type}`)
|
||||
}
|
||||
})
|
||||
prompt.push({
|
||||
role: "user",
|
||||
content,
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case "assistant": {
|
||||
const content: Array<
|
||||
| { type: "text"; text: string }
|
||||
| {
|
||||
type: "tool-call"
|
||||
toolCallId: string
|
||||
toolName: string
|
||||
input: any
|
||||
}
|
||||
> = []
|
||||
|
||||
if (message.content) {
|
||||
content.push({
|
||||
type: "text",
|
||||
text: message.content as string,
|
||||
})
|
||||
}
|
||||
|
||||
if (message.tool_calls) {
|
||||
for (const toolCall of message.tool_calls) {
|
||||
content.push({
|
||||
type: "tool-call",
|
||||
toolCallId: toolCall.id,
|
||||
toolName: toolCall.function.name,
|
||||
input: JSON.parse(toolCall.function.arguments),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
prompt.push({
|
||||
role: "assistant",
|
||||
content,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case "tool": {
|
||||
prompt.push({
|
||||
role: "tool",
|
||||
content: [
|
||||
{
|
||||
type: "tool-result",
|
||||
toolName: "placeholder",
|
||||
toolCallId: message.tool_call_id,
|
||||
output: {
|
||||
type: "text",
|
||||
value: message.content as string,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
throw new Error(`Unsupported message role: ${message.role}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return prompt
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
return c.json({ error: { message: error.message } }, 500)
|
||||
}
|
||||
})
|
||||
.all("*", (c) => c.text("Not Found"))
|
||||
Vendored
+17
@@ -6,6 +6,10 @@
|
||||
import "sst"
|
||||
declare module "sst" {
|
||||
export interface Resource {
|
||||
"ANTHROPIC_API_KEY": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"GITHUB_APP_ID": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
@@ -14,10 +18,22 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"OPENAI_API_KEY": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"OPENCODE_API_KEY": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"Web": {
|
||||
"type": "sst.cloudflare.Astro"
|
||||
"url": string
|
||||
}
|
||||
"ZHIPU_API_KEY": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
}
|
||||
}
|
||||
// cloudflare
|
||||
@@ -26,6 +42,7 @@ declare module "sst" {
|
||||
export interface Resource {
|
||||
"Api": cloudflare.Service
|
||||
"Bucket": cloudflare.R2Bucket
|
||||
"GatewayApi": cloudflare.Service
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,19 +17,6 @@
|
||||
- **Error handling**: Use Result patterns, avoid throwing exceptions in tools
|
||||
- **File structure**: Namespace-based organization (e.g., `Tool.define()`, `Session.create()`)
|
||||
|
||||
## IMPORTANT
|
||||
|
||||
- Try to keep things in one function unless composable or reusable
|
||||
- DO NOT do unnecessary destructuring of variables
|
||||
- DO NOT use `else` statements unless necessary
|
||||
- DO NOT use `try`/`catch` if it can be avoided
|
||||
- AVOID `try`/`catch` where possible
|
||||
- AVOID `else` statements
|
||||
- AVOID using `any` type
|
||||
- AVOID `let` statements
|
||||
- PREFER single word variable names where possible
|
||||
- Use as many bun apis as possible like Bun.file()
|
||||
|
||||
## Architecture
|
||||
|
||||
- **Tools**: Implement `Tool.Info` interface with `execute()` method
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "0.0.5",
|
||||
"version": "0.3.130",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit",
|
||||
"dev": "bun run ./src/index.ts"
|
||||
"dev": "bun run --conditions=development ./src/index.ts"
|
||||
},
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode"
|
||||
@@ -30,10 +30,14 @@
|
||||
"dependencies": {
|
||||
"@actions/core": "1.11.1",
|
||||
"@actions/github": "6.0.1",
|
||||
"@clack/prompts": "0.11.0",
|
||||
"@clack/prompts": "1.0.0-alpha.1",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"@modelcontextprotocol/sdk": "1.15.1",
|
||||
"@octokit/graphql": "9.0.1",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@openauthjs/openauth": "0.4.3",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@standard-schema/spec": "1.0.0",
|
||||
"@zip.js/zip.js": "2.7.62",
|
||||
"ai": "catalog:",
|
||||
@@ -43,8 +47,12 @@
|
||||
"hono": "catalog:",
|
||||
"hono-openapi": "0.4.8",
|
||||
"isomorphic-git": "1.32.1",
|
||||
"jsonc-parser": "3.3.1",
|
||||
"minimatch": "10.0.3",
|
||||
"open": "10.1.2",
|
||||
"remeda": "2.22.3",
|
||||
"remeda": "catalog:",
|
||||
"tree-sitter": "0.22.4",
|
||||
"tree-sitter-bash": "0.23.3",
|
||||
"turndown": "7.2.0",
|
||||
"vscode-jsonrpc": "8.2.1",
|
||||
"xdg-basedir": "5.1.0",
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
const dir = new URL("..", import.meta.url).pathname
|
||||
process.chdir(dir)
|
||||
import { $ } from "bun"
|
||||
|
||||
import pkg from "../package.json"
|
||||
|
||||
const dry = process.argv.includes("--dry")
|
||||
const snapshot = process.argv.includes("--snapshot")
|
||||
|
||||
const version = snapshot
|
||||
? `0.0.0-${new Date().toISOString().slice(0, 16).replace(/[-:T]/g, "")}`
|
||||
: await $`git describe --tags --abbrev=0`
|
||||
.text()
|
||||
.then((x) => x.substring(1).trim())
|
||||
.catch(() => {
|
||||
console.error("tag not found")
|
||||
process.exit(1)
|
||||
})
|
||||
const dry = process.env["OPENCODE_DRY"] === "true"
|
||||
const version = process.env["OPENCODE_VERSION"]!
|
||||
const snapshot = process.env["OPENCODE_SNAPSHOT"] === "true"
|
||||
|
||||
console.log(`publishing ${version}`)
|
||||
|
||||
@@ -26,12 +18,13 @@ const GOARCH: Record<string, string> = {
|
||||
}
|
||||
|
||||
const targets = [
|
||||
["windows", "x64"],
|
||||
["linux", "arm64"],
|
||||
["linux", "x64"],
|
||||
["linux", "x64-baseline"],
|
||||
["darwin", "x64"],
|
||||
["darwin", "x64-baseline"],
|
||||
["darwin", "arm64"],
|
||||
["windows", "x64"],
|
||||
]
|
||||
|
||||
await $`rm -rf dist`
|
||||
@@ -45,7 +38,7 @@ for (const [os, arch] of targets) {
|
||||
await $`CGO_ENABLED=0 GOOS=${os} GOARCH=${GOARCH[arch]} go build -ldflags="-s -w -X main.Version=${version}" -o ../opencode/dist/${name}/bin/tui ../tui/cmd/opencode/main.go`.cwd(
|
||||
"../tui",
|
||||
)
|
||||
await $`bun build --define OPENCODE_VERSION="'${version}'" --compile --minify --target=bun-${os}-${arch} --outfile=dist/${name}/bin/opencode ./src/index.ts ./dist/${name}/bin/tui`
|
||||
await $`bun build --define OPENCODE_TUI_PATH="'../../../dist/${name}/bin/tui'" --define OPENCODE_VERSION="'${version}'" --compile --target=bun-${os}-${arch} --outfile=dist/${name}/bin/opencode ./src/index.ts`
|
||||
await $`rm -rf ./dist/${name}/bin/tui`
|
||||
await Bun.file(`dist/${name}/package.json`).write(
|
||||
JSON.stringify(
|
||||
@@ -59,7 +52,7 @@ for (const [os, arch] of targets) {
|
||||
2,
|
||||
),
|
||||
)
|
||||
if (!dry) await $`cd dist/${name} && bun publish --access public --tag ${npmTag}`
|
||||
if (!dry) await $`cd dist/${name} && chmod 777 -R . && bun publish --access public --tag ${npmTag}`
|
||||
optionalDependencies[name] = version
|
||||
}
|
||||
|
||||
@@ -111,6 +104,7 @@ if (!snapshot) {
|
||||
.filter((x: string) => {
|
||||
const lower = x.toLowerCase()
|
||||
return (
|
||||
!lower.includes("release:") &&
|
||||
!lower.includes("ignore:") &&
|
||||
!lower.includes("chore:") &&
|
||||
!lower.includes("ci:") &&
|
||||
|
||||
@@ -38,7 +38,10 @@ export namespace Agent {
|
||||
},
|
||||
}
|
||||
for (const [key, value] of Object.entries(cfg.agent ?? {})) {
|
||||
if (value.disable) continue
|
||||
if (value.disable) {
|
||||
delete result[key]
|
||||
continue
|
||||
}
|
||||
let item = result[key]
|
||||
if (!item)
|
||||
item = result[key] = {
|
||||
|
||||
@@ -73,6 +73,7 @@ export namespace BunProc {
|
||||
// Let Bun handle registry resolution:
|
||||
// - If .npmrc files exist, Bun will use them automatically
|
||||
// - If no .npmrc files exist, Bun will default to https://registry.npmjs.org
|
||||
// - No need to pass --registry flag
|
||||
log.info("installing package using Bun's default registry resolution", { pkg, version })
|
||||
|
||||
await BunProc.run(args, {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { App } from "../app/app"
|
||||
import { ConfigHooks } from "../config/hooks"
|
||||
import { Format } from "../format"
|
||||
import { LSP } from "../lsp"
|
||||
import { Plugin } from "../plugin"
|
||||
import { Share } from "../share/share"
|
||||
import { Snapshot } from "../snapshot"
|
||||
|
||||
@@ -9,6 +10,7 @@ export async function bootstrap<T>(input: App.Input, cb: (app: App.Info) => Prom
|
||||
return App.provide(input, async (app) => {
|
||||
Share.init()
|
||||
Format.init()
|
||||
Plugin.init()
|
||||
ConfigHooks.init()
|
||||
LSP.init()
|
||||
Snapshot.init()
|
||||
|
||||
@@ -39,7 +39,7 @@ const AgentCreateCommand = cmd({
|
||||
const query = await prompts.text({
|
||||
message: "Description",
|
||||
placeholder: "What should this agent do?",
|
||||
validate: (x) => (x.length > 0 ? undefined : "Required"),
|
||||
validate: (x) => x && (x.length > 0 ? undefined : "Required"),
|
||||
})
|
||||
if (prompts.isCancel(query)) throw new UI.CancelledError()
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ export const AuthLoginCommand = cmd({
|
||||
type: "string",
|
||||
}),
|
||||
async handler(args) {
|
||||
UI.empty()
|
||||
prompts.intro("Add credential")
|
||||
if (args.url) {
|
||||
const wellknown = await fetch(`${args.url}/.well-known/opencode`).then((x) => x.json())
|
||||
@@ -99,7 +100,7 @@ export const AuthLoginCommand = cmd({
|
||||
prompts.outro("Done")
|
||||
return
|
||||
}
|
||||
UI.empty()
|
||||
await ModelsDev.refresh().catch(() => {})
|
||||
const providers = await ModelsDev.get()
|
||||
const priority: Record<string, number> = {
|
||||
anthropic: 0,
|
||||
@@ -109,7 +110,7 @@ export const AuthLoginCommand = cmd({
|
||||
openrouter: 4,
|
||||
vercel: 5,
|
||||
}
|
||||
let provider = await prompts.select({
|
||||
let provider = await prompts.autocomplete({
|
||||
message: "Select provider",
|
||||
maxItems: 8,
|
||||
options: [
|
||||
@@ -138,7 +139,7 @@ export const AuthLoginCommand = cmd({
|
||||
if (provider === "other") {
|
||||
provider = await prompts.text({
|
||||
message: "Enter provider id",
|
||||
validate: (x) => (x.match(/^[0-9a-z-]+$/) ? undefined : "a-z, 0-9 and hyphens only"),
|
||||
validate: (x) => 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\//, "")
|
||||
@@ -192,7 +193,7 @@ export const AuthLoginCommand = cmd({
|
||||
|
||||
const code = await prompts.text({
|
||||
message: "Paste the authorization code here: ",
|
||||
validate: (x) => (x.length > 0 ? undefined : "Required"),
|
||||
validate: (x) => x && (x.length > 0 ? undefined : "Required"),
|
||||
})
|
||||
if (prompts.isCancel(code)) throw new UI.CancelledError()
|
||||
|
||||
@@ -228,7 +229,7 @@ export const AuthLoginCommand = cmd({
|
||||
|
||||
const code = await prompts.text({
|
||||
message: "Paste the authorization code here: ",
|
||||
validate: (x) => (x.length > 0 ? undefined : "Required"),
|
||||
validate: (x) => x && (x.length > 0 ? undefined : "Required"),
|
||||
})
|
||||
if (prompts.isCancel(code)) throw new UI.CancelledError()
|
||||
|
||||
@@ -301,7 +302,7 @@ export const AuthLoginCommand = cmd({
|
||||
|
||||
const key = await prompts.password({
|
||||
message: "Enter your API key",
|
||||
validate: (x) => (x.length > 0 ? undefined : "Required"),
|
||||
validate: (x) => x && (x.length > 0 ? undefined : "Required"),
|
||||
})
|
||||
if (prompts.isCancel(key)) throw new UI.CancelledError()
|
||||
await Auth.set(provider, {
|
||||
|
||||
@@ -318,8 +318,10 @@ on:
|
||||
jobs:
|
||||
opencode:
|
||||
if: |
|
||||
contains(github.event.comment.body, '/oc') ||
|
||||
contains(github.event.comment.body, '/opencode')
|
||||
contains(github.event.comment.body, ' /oc') ||
|
||||
startsWith(github.event.comment.body, '/oc') ||
|
||||
contains(github.event.comment.body, ' /opencode') ||
|
||||
startsWith(github.event.comment.body, '/opencode')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -784,7 +786,7 @@ export const GithubRunCommand = cmd({
|
||||
console.log("Pushing to new branch...")
|
||||
await $`git add .`
|
||||
await $`git commit -m "${summary}
|
||||
|
||||
|
||||
Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
|
||||
await $`git push -u origin ${branch}`
|
||||
}
|
||||
@@ -793,7 +795,7 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
|
||||
console.log("Pushing to local branch...")
|
||||
await $`git add .`
|
||||
await $`git commit -m "${summary}
|
||||
|
||||
|
||||
Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
|
||||
await $`git push`
|
||||
}
|
||||
@@ -805,7 +807,7 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
|
||||
|
||||
await $`git add .`
|
||||
await $`git commit -m "${summary}
|
||||
|
||||
|
||||
Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
|
||||
await $`git push fork HEAD:${remoteBranch}`
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export const McpAddCommand = cmd({
|
||||
|
||||
const name = await prompts.text({
|
||||
message: "Enter MCP server name",
|
||||
validate: (x) => (x.length > 0 ? undefined : "Required"),
|
||||
validate: (x) => x && (x.length > 0 ? undefined : "Required"),
|
||||
})
|
||||
if (prompts.isCancel(name)) throw new UI.CancelledError()
|
||||
|
||||
@@ -44,7 +44,7 @@ export const McpAddCommand = cmd({
|
||||
const command = await prompts.text({
|
||||
message: "Enter command to run",
|
||||
placeholder: "e.g., opencode x @modelcontextprotocol/server-filesystem",
|
||||
validate: (x) => (x.length > 0 ? undefined : "Required"),
|
||||
validate: (x) => x && (x.length > 0 ? undefined : "Required"),
|
||||
})
|
||||
if (prompts.isCancel(command)) throw new UI.CancelledError()
|
||||
|
||||
@@ -58,6 +58,7 @@ export const McpAddCommand = cmd({
|
||||
message: "Enter MCP server URL",
|
||||
placeholder: "e.g., https://example.com/mcp",
|
||||
validate: (x) => {
|
||||
if (!x) return "Required"
|
||||
if (x.length === 0) return "Required"
|
||||
const isValid = URL.canParse(x)
|
||||
return isValid ? undefined : "Invalid URL"
|
||||
|
||||
@@ -14,6 +14,16 @@ import { FileWatcher } from "../../file/watch"
|
||||
import { Mode } from "../../session/mode"
|
||||
import { Ide } from "../../ide"
|
||||
|
||||
declare global {
|
||||
const OPENCODE_TUI_PATH: string
|
||||
}
|
||||
|
||||
if (typeof OPENCODE_TUI_PATH !== "undefined") {
|
||||
await import(OPENCODE_TUI_PATH as string, {
|
||||
with: { type: "file" },
|
||||
})
|
||||
}
|
||||
|
||||
export const TuiCommand = cmd({
|
||||
command: "$0 [project]",
|
||||
describe: "start opencode tui",
|
||||
@@ -71,16 +81,16 @@ export const TuiCommand = cmd({
|
||||
|
||||
let cmd = ["go", "run", "./main.go"]
|
||||
let cwd = Bun.fileURLToPath(new URL("../../../../tui/cmd/opencode", import.meta.url))
|
||||
if (Bun.embeddedFiles.length > 0) {
|
||||
const blob = Bun.embeddedFiles[0] as File
|
||||
let binaryName = blob.name
|
||||
const tui = Bun.embeddedFiles.find((item) => (item as File).name.includes("tui")) as File
|
||||
if (tui) {
|
||||
let binaryName = tui.name
|
||||
if (process.platform === "win32" && !binaryName.endsWith(".exe")) {
|
||||
binaryName += ".exe"
|
||||
}
|
||||
const binary = path.join(Global.Path.cache, "tui", binaryName)
|
||||
const file = Bun.file(binary)
|
||||
if (!(await file.exists())) {
|
||||
await Bun.write(file, blob, { mode: 0o755 })
|
||||
await Bun.write(file, tui, { mode: 0o755 })
|
||||
await fs.chmod(binary, 0o755)
|
||||
}
|
||||
cwd = process.cwd()
|
||||
|
||||
@@ -32,7 +32,7 @@ export const UpgradeCommand = {
|
||||
return
|
||||
}
|
||||
prompts.log.info("Using method: " + method)
|
||||
const target = args.target ?? (await Installation.latest())
|
||||
const target = args.target ? args.target.replace(/^v/, "") : await Installation.latest()
|
||||
|
||||
if (Installation.VERSION === target) {
|
||||
prompts.log.warn(`opencode upgrade skipped: ${target} is already installed`)
|
||||
|
||||
@@ -5,7 +5,11 @@ import { UI } from "./ui"
|
||||
export function FormatError(input: unknown) {
|
||||
if (MCP.Failed.isInstance(input))
|
||||
return `MCP server "${input.data.name}" failed. Note, opencode does not support MCP authentication yet.`
|
||||
if (Config.JsonError.isInstance(input)) return `Config file at ${input.data.path} is not valid JSON`
|
||||
if (Config.JsonError.isInstance(input)) {
|
||||
return (
|
||||
`Config file at ${input.data.path} is not valid JSON(C)` + (input.data.message ? `: ${input.data.message}` : "")
|
||||
)
|
||||
}
|
||||
if (Config.InvalidError.isInstance(input))
|
||||
return [
|
||||
`Config file at ${input.data.path} is invalid`,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Log } from "../util/log"
|
||||
import path from "path"
|
||||
import os from "os"
|
||||
import { z } from "zod"
|
||||
import { App } from "../app/app"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
@@ -12,6 +13,7 @@ import { NamedError } from "../util/error"
|
||||
import matter from "gray-matter"
|
||||
import { Flag } from "../flag/flag"
|
||||
import { Auth } from "../auth"
|
||||
import { type ParseError as JsoncParseError, parse as parseJsonc, printParseErrorCode } from "jsonc-parser"
|
||||
|
||||
export namespace Config {
|
||||
const log = Log.create({ service: "config" })
|
||||
@@ -22,13 +24,13 @@ export namespace Config {
|
||||
for (const file of ["opencode.jsonc", "opencode.json"]) {
|
||||
const found = await Filesystem.findUp(file, app.path.cwd, app.path.root)
|
||||
for (const resolved of found.toReversed()) {
|
||||
result = mergeDeep(result, await load(resolved))
|
||||
result = mergeDeep(result, await loadFile(resolved))
|
||||
}
|
||||
}
|
||||
|
||||
// Override with custom config if provided
|
||||
if (Flag.OPENCODE_CONFIG) {
|
||||
result = mergeDeep(result, await load(Flag.OPENCODE_CONFIG))
|
||||
result = mergeDeep(result, await loadFile(Flag.OPENCODE_CONFIG))
|
||||
log.debug("loaded custom config", { path: Flag.OPENCODE_CONFIG })
|
||||
}
|
||||
|
||||
@@ -36,7 +38,7 @@ export namespace Config {
|
||||
if (value.type === "wellknown") {
|
||||
process.env[value.key] = value.token
|
||||
const wellknown = await fetch(`${key}/.well-known/opencode`).then((x) => x.json())
|
||||
result = mergeDeep(result, await loadRaw(JSON.stringify(wellknown.config ?? {}), process.cwd()))
|
||||
result = mergeDeep(result, await load(JSON.stringify(wellknown.config ?? {}), process.cwd()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +67,40 @@ export namespace Config {
|
||||
throw new InvalidError({ path: item }, { cause: parsed.error })
|
||||
}
|
||||
|
||||
// Load mode markdown files
|
||||
result.mode = result.mode || {}
|
||||
const markdownModes = [
|
||||
...(await Filesystem.globUp("mode/*.md", Global.Path.config, Global.Path.config)),
|
||||
...(await Filesystem.globUp(".opencode/mode/*.md", app.path.cwd, app.path.root)),
|
||||
]
|
||||
for (const item of markdownModes) {
|
||||
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 = Mode.safeParse(config)
|
||||
if (parsed.success) {
|
||||
result.mode = mergeDeep(result.mode, {
|
||||
[config.name]: parsed.data,
|
||||
})
|
||||
continue
|
||||
}
|
||||
throw new InvalidError({ path: item }, { cause: parsed.error })
|
||||
}
|
||||
|
||||
result.plugin = result.plugin || []
|
||||
result.plugin.push(
|
||||
...[
|
||||
...(await Filesystem.globUp("plugin/*.ts", Global.Path.config, Global.Path.config)),
|
||||
...(await Filesystem.globUp(".opencode/plugin/*.ts", app.path.cwd, app.path.root)),
|
||||
].map((x) => "file://" + x),
|
||||
)
|
||||
|
||||
// Handle migration from autoshare to share field
|
||||
if (result.autoshare === true && !result.share) {
|
||||
result.share = "auto"
|
||||
@@ -117,6 +153,7 @@ export namespace Config {
|
||||
.object({
|
||||
model: z.string().optional(),
|
||||
temperature: z.number().optional(),
|
||||
top_p: z.number().optional(),
|
||||
prompt: z.string().optional(),
|
||||
tools: z.record(z.string(), z.boolean()).optional(),
|
||||
disable: z.boolean().optional(),
|
||||
@@ -187,11 +224,15 @@ export namespace Config {
|
||||
})
|
||||
export type Layout = z.infer<typeof Layout>
|
||||
|
||||
export const Permission = z.union([z.literal("ask"), z.literal("allow")])
|
||||
export type Permission = z.infer<typeof Permission>
|
||||
|
||||
export const Info = z
|
||||
.object({
|
||||
$schema: z.string().optional().describe("JSON schema reference for configuration validation"),
|
||||
theme: z.string().optional().describe("Theme name to use for the interface"),
|
||||
keybinds: Keybinds.optional().describe("Custom keybind configurations"),
|
||||
plugin: z.string().array().optional(),
|
||||
share: z
|
||||
.enum(["manual", "auto", "disabled"])
|
||||
.optional()
|
||||
@@ -248,8 +289,42 @@ export namespace Config {
|
||||
.optional()
|
||||
.describe("Custom provider configurations and model overrides"),
|
||||
mcp: z.record(z.string(), Mcp).optional().describe("MCP (Model Context Protocol) server configurations"),
|
||||
formatter: z
|
||||
.record(
|
||||
z.string(),
|
||||
z.object({
|
||||
disabled: z.boolean().optional(),
|
||||
command: z.array(z.string()).optional(),
|
||||
environment: z.record(z.string(), z.string()).optional(),
|
||||
extensions: z.array(z.string()).optional(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
lsp: z
|
||||
.record(
|
||||
z.string(),
|
||||
z.union([
|
||||
z.object({
|
||||
disabled: z.literal(true),
|
||||
}),
|
||||
z.object({
|
||||
command: z.array(z.string()),
|
||||
extensions: z.array(z.string()).optional(),
|
||||
disabled: z.boolean().optional(),
|
||||
env: z.record(z.string(), z.string()).optional(),
|
||||
initialization: z.record(z.string(), z.any()).optional(),
|
||||
}),
|
||||
]),
|
||||
)
|
||||
.optional(),
|
||||
instructions: z.array(z.string()).optional().describe("Additional instruction files or patterns to include"),
|
||||
layout: Layout.optional().describe("@deprecated Always uses stretch layout."),
|
||||
permission: z
|
||||
.object({
|
||||
edit: Permission.optional(),
|
||||
bash: z.union([Permission, z.record(z.string(), Permission)]).optional(),
|
||||
})
|
||||
.optional(),
|
||||
experimental: z
|
||||
.object({
|
||||
hook: z
|
||||
@@ -285,10 +360,11 @@ export namespace Config {
|
||||
export type Info = z.output<typeof Info>
|
||||
|
||||
export const global = lazy(async () => {
|
||||
let result = pipe(
|
||||
let result: Info = pipe(
|
||||
{},
|
||||
mergeDeep(await load(path.join(Global.Path.config, "config.json"))),
|
||||
mergeDeep(await load(path.join(Global.Path.config, "opencode.json"))),
|
||||
mergeDeep(await loadFile(path.join(Global.Path.config, "config.json"))),
|
||||
mergeDeep(await loadFile(path.join(Global.Path.config, "opencode.json"))),
|
||||
mergeDeep(await loadFile(path.join(Global.Path.config, "opencode.jsonc"))),
|
||||
)
|
||||
|
||||
await import(path.join(Global.Path.config, "config"), {
|
||||
@@ -309,54 +385,93 @@ export namespace Config {
|
||||
return result
|
||||
})
|
||||
|
||||
async function load(configPath: string) {
|
||||
let text = await Bun.file(configPath)
|
||||
async function loadFile(filepath: string): Promise<Info> {
|
||||
log.info("loading", { path: filepath })
|
||||
let text = await Bun.file(filepath)
|
||||
.text()
|
||||
.catch((err) => {
|
||||
if (err.code === "ENOENT") return
|
||||
throw new JsonError({ path: configPath }, { cause: err })
|
||||
throw new JsonError({ path: filepath }, { cause: err })
|
||||
})
|
||||
if (!text) return {}
|
||||
return loadRaw(text, configPath)
|
||||
return load(text, filepath)
|
||||
}
|
||||
|
||||
async function loadRaw(text: string, configPath: string) {
|
||||
async function load(text: string, filepath: string) {
|
||||
text = text.replace(/\{env:([^}]+)\}/g, (_, varName) => {
|
||||
return process.env[varName] || ""
|
||||
})
|
||||
|
||||
const fileMatches = text.match(/"?\{file:([^}]+)\}"?/g)
|
||||
const fileMatches = text.match(/\{file:[^}]+\}/g)
|
||||
if (fileMatches) {
|
||||
const configDir = path.dirname(configPath)
|
||||
const configDir = path.dirname(filepath)
|
||||
const lines = text.split("\n")
|
||||
|
||||
for (const match of fileMatches) {
|
||||
const filePath = match.replace(/^"?\{file:/, "").replace(/\}"?$/, "")
|
||||
const lineIndex = lines.findIndex((line) => line.includes(match))
|
||||
if (lineIndex !== -1 && lines[lineIndex].trim().startsWith("//")) {
|
||||
continue // Skip if line is commented
|
||||
}
|
||||
let filePath = match.replace(/^\{file:/, "").replace(/\}$/, "")
|
||||
if (filePath.startsWith("~/")) {
|
||||
filePath = path.join(os.homedir(), filePath.slice(2))
|
||||
}
|
||||
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(configDir, filePath)
|
||||
const fileContent = await Bun.file(resolvedPath).text()
|
||||
text = text.replace(match, JSON.stringify(fileContent))
|
||||
const fileContent = (await Bun.file(resolvedPath).text()).trim()
|
||||
// escape newlines/quotes, strip outer quotes
|
||||
text = text.replace(match, JSON.stringify(fileContent).slice(1, -1))
|
||||
}
|
||||
}
|
||||
|
||||
let data: any
|
||||
try {
|
||||
data = JSON.parse(text)
|
||||
} catch (err) {
|
||||
throw new JsonError({ path: configPath }, { cause: err as Error })
|
||||
const errors: JsoncParseError[] = []
|
||||
const data = parseJsonc(text, errors, { allowTrailingComma: true })
|
||||
if (errors.length) {
|
||||
const lines = text.split("\n")
|
||||
const errorDetails = errors
|
||||
.map((e) => {
|
||||
const beforeOffset = text.substring(0, e.offset).split("\n")
|
||||
const line = beforeOffset.length
|
||||
const column = beforeOffset[beforeOffset.length - 1].length + 1
|
||||
const problemLine = lines[line - 1]
|
||||
|
||||
const error = `${printParseErrorCode(e.error)} at line ${line}, column ${column}`
|
||||
if (!problemLine) return error
|
||||
|
||||
return `${error}\n Line ${line}: ${problemLine}\n${"".padStart(column + 9)}^`
|
||||
})
|
||||
.join("\n")
|
||||
|
||||
throw new JsonError({
|
||||
path: filepath,
|
||||
message: `\n--- JSONC Input ---\n${text}\n--- Errors ---\n${errorDetails}\n--- End ---`,
|
||||
})
|
||||
}
|
||||
|
||||
const parsed = Info.safeParse(data)
|
||||
if (parsed.success) {
|
||||
if (!parsed.data.$schema) {
|
||||
parsed.data.$schema = "https://opencode.ai/config.json"
|
||||
await Bun.write(configPath, JSON.stringify(parsed.data, null, 2))
|
||||
await Bun.write(filepath, JSON.stringify(parsed.data, null, 2))
|
||||
}
|
||||
return parsed.data
|
||||
const data = parsed.data
|
||||
if (data.plugin) {
|
||||
for (let i = 0; i < data.plugin?.length; i++) {
|
||||
const plugin = data.plugin[i]
|
||||
try {
|
||||
data.plugin[i] = import.meta.resolve(plugin, filepath)
|
||||
} catch (err) {}
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
throw new InvalidError({ path: configPath, issues: parsed.error.issues })
|
||||
|
||||
throw new InvalidError({ path: filepath, issues: parsed.error.issues })
|
||||
}
|
||||
export const JsonError = NamedError.create(
|
||||
"ConfigJsonError",
|
||||
z.object({
|
||||
path: z.string(),
|
||||
message: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { App } from "../app/app"
|
||||
import { BunProc } from "../bun"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
import path from "path"
|
||||
|
||||
export interface Info {
|
||||
name: string
|
||||
@@ -65,14 +64,57 @@ export const prettier: Info = {
|
||||
],
|
||||
async enabled() {
|
||||
const app = App.info()
|
||||
const nms = await Filesystem.findUp("node_modules", app.path.cwd, app.path.root)
|
||||
for (const item of nms) {
|
||||
if (await Bun.file(path.join(item, ".bin", "prettier")).exists()) return true
|
||||
const items = await Filesystem.findUp("package.json", app.path.cwd, app.path.root)
|
||||
for (const item of items) {
|
||||
const json = await Bun.file(item).json()
|
||||
if (json.dependencies?.prettier) return true
|
||||
if (json.devDependencies?.prettier) return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
}
|
||||
|
||||
export const biome: Info = {
|
||||
name: "biome",
|
||||
command: [BunProc.which(), "x", "biome", "format", "--write", "$FILE"],
|
||||
environment: {
|
||||
BUN_BE_BUN: "1",
|
||||
},
|
||||
extensions: [
|
||||
".js",
|
||||
".jsx",
|
||||
".mjs",
|
||||
".cjs",
|
||||
".ts",
|
||||
".tsx",
|
||||
".mts",
|
||||
".cts",
|
||||
".html",
|
||||
".htm",
|
||||
".css",
|
||||
".scss",
|
||||
".sass",
|
||||
".less",
|
||||
".vue",
|
||||
".svelte",
|
||||
".json",
|
||||
".jsonc",
|
||||
".yaml",
|
||||
".yml",
|
||||
".toml",
|
||||
".xml",
|
||||
".md",
|
||||
".mdx",
|
||||
".graphql",
|
||||
".gql",
|
||||
],
|
||||
async enabled() {
|
||||
const app = App.info()
|
||||
const items = await Filesystem.findUp("biome.json", app.path.cwd, app.path.root)
|
||||
return items.length > 0
|
||||
},
|
||||
}
|
||||
|
||||
export const zig: Info = {
|
||||
name: "zig",
|
||||
command: ["zig", "fmt", "$FILE"],
|
||||
@@ -87,7 +129,9 @@ export const clang: Info = {
|
||||
command: ["clang-format", "-i", "$FILE"],
|
||||
extensions: [".c", ".cc", ".cpp", ".cxx", ".c++", ".h", ".hh", ".hpp", ".hxx", ".h++", ".ino", ".C", ".H"],
|
||||
async enabled() {
|
||||
return Bun.which("clang-format") !== null
|
||||
const app = App.info()
|
||||
const items = await Filesystem.findUp(".clang-format", app.path.cwd, app.path.root)
|
||||
return items.length > 0
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -5,20 +5,40 @@ import { Log } from "../util/log"
|
||||
import path from "path"
|
||||
|
||||
import * as Formatter from "./formatter"
|
||||
import { Config } from "../config/config"
|
||||
import { mergeDeep } from "remeda"
|
||||
|
||||
export namespace Format {
|
||||
const log = Log.create({ service: "format" })
|
||||
|
||||
const state = App.state("format", () => {
|
||||
const state = App.state("format", async () => {
|
||||
const enabled: Record<string, boolean> = {}
|
||||
const cfg = await Config.get()
|
||||
|
||||
const formatters = { ...Formatter } as Record<string, Formatter.Info>
|
||||
for (const [name, item] of Object.entries(cfg.formatter ?? {})) {
|
||||
if (item.disabled) {
|
||||
delete formatters[name]
|
||||
continue
|
||||
}
|
||||
const result: Formatter.Info = mergeDeep(formatters[name] ?? {}, {
|
||||
command: [],
|
||||
extensions: [],
|
||||
...item,
|
||||
})
|
||||
result.enabled = async () => true
|
||||
result.name = name
|
||||
formatters[name] = result
|
||||
}
|
||||
|
||||
return {
|
||||
enabled,
|
||||
formatters,
|
||||
}
|
||||
})
|
||||
|
||||
async function isEnabled(item: Formatter.Info) {
|
||||
const s = state()
|
||||
const s = await state()
|
||||
let status = s.enabled[item.name]
|
||||
if (status === undefined) {
|
||||
status = await item.enabled()
|
||||
@@ -28,8 +48,10 @@ export namespace Format {
|
||||
}
|
||||
|
||||
async function getFormatter(ext: string) {
|
||||
const formatters = await state().then((x) => x.formatters)
|
||||
const result = []
|
||||
for (const item of Object.values(Formatter)) {
|
||||
for (const item of Object.values(formatters)) {
|
||||
log.info("checking", { name: item.name, ext })
|
||||
if (!item.extensions.includes(ext)) continue
|
||||
if (!(await isEnabled(item))) continue
|
||||
result.push(item)
|
||||
|
||||
@@ -13,6 +13,7 @@ export namespace Global {
|
||||
export const Path = {
|
||||
data,
|
||||
bin: path.join(data, "bin"),
|
||||
log: path.join(data, "log"),
|
||||
cache,
|
||||
config,
|
||||
state,
|
||||
@@ -23,9 +24,10 @@ await Promise.all([
|
||||
fs.mkdir(Global.Path.data, { recursive: true }),
|
||||
fs.mkdir(Global.Path.config, { recursive: true }),
|
||||
fs.mkdir(Global.Path.state, { recursive: true }),
|
||||
fs.mkdir(Global.Path.log, { recursive: true }),
|
||||
])
|
||||
|
||||
const CACHE_VERSION = "3"
|
||||
const CACHE_VERSION = "4"
|
||||
|
||||
const version = await Bun.file(path.join(Global.Path.cache, "version"))
|
||||
.text()
|
||||
|
||||
@@ -5,6 +5,7 @@ export namespace Identifier {
|
||||
const prefixes = {
|
||||
session: "ses",
|
||||
message: "msg",
|
||||
permission: "per",
|
||||
user: "usr",
|
||||
part: "prt",
|
||||
} as const
|
||||
|
||||
@@ -83,6 +83,7 @@ const cli = yargs(hideBin(process.argv))
|
||||
if (msg.startsWith("Unknown argument") || msg.startsWith("Not enough non-option arguments")) {
|
||||
cli.showHelp("log")
|
||||
}
|
||||
process.exit(1)
|
||||
})
|
||||
.strict()
|
||||
|
||||
|
||||
@@ -136,6 +136,7 @@ export namespace Installation {
|
||||
}
|
||||
|
||||
export const VERSION = typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "dev"
|
||||
export const USER_AGENT = `opencode/${VERSION}`
|
||||
|
||||
export async function latest() {
|
||||
return fetch("https://api.github.com/repos/sst/opencode/releases/latest")
|
||||
|
||||
@@ -4,6 +4,8 @@ import { LSPClient } from "./client"
|
||||
import path from "path"
|
||||
import { LSPServer } from "./server"
|
||||
import { z } from "zod"
|
||||
import { Config } from "../config/config"
|
||||
import { spawn } from "child_process"
|
||||
|
||||
export namespace LSP {
|
||||
const log = Log.create({ service: "lsp" })
|
||||
@@ -55,8 +57,35 @@ export namespace LSP {
|
||||
"lsp",
|
||||
async () => {
|
||||
const clients: LSPClient.Info[] = []
|
||||
const servers: Record<string, LSPServer.Info> = LSPServer
|
||||
const cfg = await Config.get()
|
||||
for (const [name, item] of Object.entries(cfg.lsp ?? {})) {
|
||||
const existing = servers[name]
|
||||
if (item.disabled) {
|
||||
delete servers[name]
|
||||
continue
|
||||
}
|
||||
servers[name] = {
|
||||
...existing,
|
||||
root: existing?.root ?? (async (_file, app) => app.path.root),
|
||||
extensions: item.extensions ?? existing.extensions,
|
||||
spawn: async (_app, root) => {
|
||||
return {
|
||||
process: spawn(item.command[0], item.command.slice(1), {
|
||||
cwd: root,
|
||||
env: {
|
||||
...process.env,
|
||||
...item.env,
|
||||
},
|
||||
}),
|
||||
initialization: item.initialization,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
return {
|
||||
broken: new Set<string>(),
|
||||
servers,
|
||||
clients,
|
||||
}
|
||||
},
|
||||
@@ -76,7 +105,7 @@ export namespace LSP {
|
||||
const extension = path.parse(file).ext
|
||||
const result: LSPClient.Info[] = []
|
||||
for (const server of Object.values(LSPServer)) {
|
||||
if (!server.extensions.includes(extension)) continue
|
||||
if (server.extensions.length && !server.extensions.includes(extension)) continue
|
||||
const root = await server.root(file, App.info())
|
||||
if (!root) continue
|
||||
if (s.broken.has(root + server.id)) continue
|
||||
|
||||
@@ -115,7 +115,8 @@ export namespace MCP {
|
||||
const result: Record<string, Tool> = {}
|
||||
for (const [clientName, client] of Object.entries(await clients())) {
|
||||
for (const [toolName, tool] of Object.entries(await client.tools())) {
|
||||
result[clientName + "_" + toolName] = tool
|
||||
const sanitizedClientName = clientName.replace(/\s+/g, "_")
|
||||
result[sanitizedClientName + "_" + toolName] = tool
|
||||
}
|
||||
}
|
||||
return result
|
||||
|
||||
@@ -2,6 +2,8 @@ import { App } from "../app/app"
|
||||
import { z } from "zod"
|
||||
import { Bus } from "../bus"
|
||||
import { Log } from "../util/log"
|
||||
import { Identifier } from "../id/id"
|
||||
import { Plugin } from "../plugin"
|
||||
|
||||
export namespace Permission {
|
||||
const log = Log.create({ service: "permission" })
|
||||
@@ -9,7 +11,11 @@ export namespace Permission {
|
||||
export const Info = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
type: z.string(),
|
||||
pattern: z.string().optional(),
|
||||
sessionID: z.string(),
|
||||
messageID: z.string(),
|
||||
callID: z.string().optional(),
|
||||
title: z.string(),
|
||||
metadata: z.record(z.any()),
|
||||
time: z.object({
|
||||
@@ -17,12 +23,16 @@ export namespace Permission {
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
ref: "permission.info",
|
||||
ref: "Permission",
|
||||
})
|
||||
export type Info = z.infer<typeof Info>
|
||||
|
||||
export const Event = {
|
||||
Updated: Bus.event("permission.updated", Info),
|
||||
Replied: Bus.event(
|
||||
"permission.replied",
|
||||
z.object({ sessionID: z.string(), permissionID: z.string(), response: z.string() }),
|
||||
),
|
||||
}
|
||||
|
||||
const state = App.state(
|
||||
@@ -40,7 +50,7 @@ export namespace Permission {
|
||||
|
||||
const approved: {
|
||||
[sessionID: string]: {
|
||||
[permissionID: string]: Info
|
||||
[permissionID: string]: boolean
|
||||
}
|
||||
} = {}
|
||||
|
||||
@@ -52,76 +62,90 @@ export namespace Permission {
|
||||
async (state) => {
|
||||
for (const pending of Object.values(state.pending)) {
|
||||
for (const item of Object.values(pending)) {
|
||||
item.reject(new RejectedError(item.info.sessionID, item.info.id))
|
||||
item.reject(new RejectedError(item.info.sessionID, item.info.id, item.info.callID))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export function ask(input: {
|
||||
id: Info["id"]
|
||||
sessionID: Info["sessionID"]
|
||||
export async function ask(input: {
|
||||
type: Info["type"]
|
||||
title: Info["title"]
|
||||
pattern?: Info["pattern"]
|
||||
callID?: Info["callID"]
|
||||
sessionID: Info["sessionID"]
|
||||
messageID: Info["messageID"]
|
||||
metadata: Info["metadata"]
|
||||
}) {
|
||||
return
|
||||
const { pending, approved } = state()
|
||||
log.info("asking", {
|
||||
sessionID: input.sessionID,
|
||||
permissionID: input.id,
|
||||
messageID: input.messageID,
|
||||
toolCallID: input.callID,
|
||||
})
|
||||
if (approved[input.sessionID]?.[input.id]) {
|
||||
log.info("previously approved", {
|
||||
sessionID: input.sessionID,
|
||||
permissionID: input.id,
|
||||
})
|
||||
return
|
||||
}
|
||||
if (approved[input.sessionID]?.[input.pattern ?? input.type]) return
|
||||
const info: Info = {
|
||||
id: input.id,
|
||||
id: Identifier.ascending("permission"),
|
||||
type: input.type,
|
||||
sessionID: input.sessionID,
|
||||
messageID: input.messageID,
|
||||
callID: input.callID,
|
||||
title: input.title,
|
||||
metadata: input.metadata,
|
||||
time: {
|
||||
created: Date.now(),
|
||||
},
|
||||
}
|
||||
|
||||
switch (
|
||||
await Plugin.trigger("permission.ask", info, {
|
||||
status: "ask",
|
||||
}).then((x) => x.status)
|
||||
) {
|
||||
case "deny":
|
||||
throw new RejectedError(info.sessionID, info.id, info.callID)
|
||||
case "allow":
|
||||
return
|
||||
}
|
||||
|
||||
pending[input.sessionID] = pending[input.sessionID] || {}
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
pending[input.sessionID][input.id] = {
|
||||
pending[input.sessionID][info.id] = {
|
||||
info,
|
||||
resolve,
|
||||
reject,
|
||||
}
|
||||
setTimeout(() => {
|
||||
respond({
|
||||
sessionID: input.sessionID,
|
||||
permissionID: input.id,
|
||||
response: "always",
|
||||
})
|
||||
}, 1000)
|
||||
Bus.publish(Event.Updated, info)
|
||||
})
|
||||
}
|
||||
|
||||
export function respond(input: {
|
||||
sessionID: Info["sessionID"]
|
||||
permissionID: Info["id"]
|
||||
response: "once" | "always" | "reject"
|
||||
}) {
|
||||
export const Response = z.enum(["once", "always", "reject"])
|
||||
export type Response = z.infer<typeof Response>
|
||||
|
||||
export function respond(input: { sessionID: Info["sessionID"]; permissionID: Info["id"]; response: Response }) {
|
||||
log.info("response", input)
|
||||
const { pending, approved } = state()
|
||||
const match = pending[input.sessionID]?.[input.permissionID]
|
||||
if (!match) return
|
||||
delete pending[input.sessionID][input.permissionID]
|
||||
if (input.response === "reject") {
|
||||
match.reject(new RejectedError(input.sessionID, input.permissionID))
|
||||
match.reject(new RejectedError(input.sessionID, input.permissionID, match.info.callID))
|
||||
return
|
||||
}
|
||||
match.resolve()
|
||||
Bus.publish(Event.Replied, {
|
||||
sessionID: input.sessionID,
|
||||
permissionID: input.permissionID,
|
||||
response: input.response,
|
||||
})
|
||||
if (input.response === "always") {
|
||||
approved[input.sessionID] = approved[input.sessionID] || {}
|
||||
approved[input.sessionID][input.permissionID] = match.info
|
||||
approved[input.sessionID][match.info.pattern ?? match.info.type] = true
|
||||
for (const item of Object.values(pending[input.sessionID])) {
|
||||
if ((item.info.pattern ?? item.info.type) === (match.info.pattern ?? match.info.type)) {
|
||||
respond({ sessionID: item.info.sessionID, permissionID: item.info.id, response: input.response })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,6 +153,7 @@ export namespace Permission {
|
||||
constructor(
|
||||
public readonly sessionID: string,
|
||||
public readonly permissionID: string,
|
||||
public readonly toolCallID?: string,
|
||||
) {
|
||||
super(`The user rejected permission to use this functionality`)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import type { Hooks, Plugin as PluginInstance } from "@opencode-ai/plugin"
|
||||
import { App } from "../app/app"
|
||||
import { Config } from "../config/config"
|
||||
import { Bus } from "../bus"
|
||||
import { Log } from "../util/log"
|
||||
import { createOpencodeClient } from "@opencode-ai/sdk"
|
||||
import { Server } from "../server/server"
|
||||
import { BunProc } from "../bun"
|
||||
|
||||
export namespace Plugin {
|
||||
const log = Log.create({ service: "plugin" })
|
||||
|
||||
const state = App.state("plugin", async (app) => {
|
||||
const client = createOpencodeClient({
|
||||
baseUrl: "http://localhost:4096",
|
||||
fetch: async (...args) => Server.app().fetch(...args),
|
||||
})
|
||||
const config = await Config.get()
|
||||
const hooks = []
|
||||
for (let plugin of config.plugin ?? []) {
|
||||
log.info("loading plugin", { path: plugin })
|
||||
if (!plugin.startsWith("file://")) {
|
||||
const [pkg, version] = plugin.split("@")
|
||||
plugin = await BunProc.install(pkg, version ?? "latest")
|
||||
}
|
||||
const mod = await import(plugin)
|
||||
for (const [_name, fn] of Object.entries<PluginInstance>(mod)) {
|
||||
const init = await fn({
|
||||
client,
|
||||
app,
|
||||
$: Bun.$,
|
||||
})
|
||||
hooks.push(init)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
hooks,
|
||||
}
|
||||
})
|
||||
|
||||
export async function trigger<
|
||||
Name extends keyof Required<Hooks>,
|
||||
Input = Parameters<Required<Hooks>[Name]>[0],
|
||||
Output = Parameters<Required<Hooks>[Name]>[1],
|
||||
>(name: Name, input: Input, output: Output): Promise<Output> {
|
||||
if (!name) return output
|
||||
for (const hook of await state().then((x) => x.hooks)) {
|
||||
const fn = hook[name]
|
||||
if (!fn) continue
|
||||
// @ts-expect-error if you feel adventurous, please fix the typing, make sure to bump the try-counter if you
|
||||
// give up.
|
||||
// try-counter: 2
|
||||
await fn(input, output)
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
export function init() {
|
||||
Bus.subscribeAll(async (input) => {
|
||||
const hooks = await state().then((x) => x.hooks)
|
||||
for (const hook of hooks) {
|
||||
hook["event"]?.({
|
||||
event: input,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { Log } from "../util/log"
|
||||
import path from "path"
|
||||
import { z } from "zod"
|
||||
import { data } from "./models-macro" with { type: "macro" }
|
||||
import { Installation } from "../installation"
|
||||
|
||||
export namespace ModelsDev {
|
||||
const log = Log.create({ service: "models.dev" })
|
||||
@@ -50,21 +51,30 @@ export namespace ModelsDev {
|
||||
export type Provider = z.infer<typeof Provider>
|
||||
|
||||
export async function get() {
|
||||
refresh()
|
||||
const file = Bun.file(filepath)
|
||||
const result = await file.json().catch(() => {})
|
||||
if (result) {
|
||||
refresh()
|
||||
return result as Record<string, Provider>
|
||||
}
|
||||
refresh()
|
||||
if (result) return result as Record<string, Provider>
|
||||
const json = await data()
|
||||
return JSON.parse(json) as Record<string, Provider>
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
export async function refresh() {
|
||||
const file = Bun.file(filepath)
|
||||
log.info("refreshing")
|
||||
const result = await fetch("https://models.dev/api.json").catch(() => {})
|
||||
if (result && result.ok) await Bun.write(file, result)
|
||||
log.info("refreshing", {
|
||||
file,
|
||||
})
|
||||
const result = await fetch("https://models.dev/api.json", {
|
||||
headers: {
|
||||
"User-Agent": Installation.USER_AGENT,
|
||||
},
|
||||
}).catch((e) => {
|
||||
log.error("Failed to fetch models.dev", {
|
||||
error: e,
|
||||
})
|
||||
})
|
||||
if (result && result.ok) await Bun.write(file, await result.text())
|
||||
}
|
||||
}
|
||||
|
||||
setInterval(() => ModelsDev.refresh(), 60 * 1000 * 60).unref()
|
||||
|
||||
@@ -126,6 +126,15 @@ export namespace Provider {
|
||||
options: {},
|
||||
}
|
||||
},
|
||||
azure: async () => {
|
||||
return {
|
||||
autoload: false,
|
||||
async getModel(sdk: any, modelID: string) {
|
||||
return sdk.responses(modelID)
|
||||
},
|
||||
options: {},
|
||||
}
|
||||
},
|
||||
"amazon-bedrock": async () => {
|
||||
if (!process.env["AWS_PROFILE"] && !process.env["AWS_ACCESS_KEY_ID"] && !process.env["AWS_BEARER_TOKEN_BEDROCK"])
|
||||
return { autoload: false }
|
||||
@@ -194,6 +203,17 @@ export namespace Provider {
|
||||
},
|
||||
}
|
||||
},
|
||||
vercel: async () => {
|
||||
return {
|
||||
autoload: false,
|
||||
options: {
|
||||
headers: {
|
||||
"http-referer": "https://opencode.ai/",
|
||||
"x-title": "opencode",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const state = App.state("provider", async () => {
|
||||
@@ -366,6 +386,10 @@ export namespace Provider {
|
||||
})
|
||||
}
|
||||
|
||||
export async function getProvider(providerID: string) {
|
||||
return state().then((s) => s.providers[providerID])
|
||||
}
|
||||
|
||||
export async function getModel(providerID: string, modelID: string) {
|
||||
const key = `${providerID}/${modelID}`
|
||||
const s = await state()
|
||||
|
||||
@@ -2,47 +2,73 @@ import type { ModelMessage } from "ai"
|
||||
import { unique } from "remeda"
|
||||
|
||||
export namespace ProviderTransform {
|
||||
export function message(msgs: ModelMessage[], providerID: string, modelID: string) {
|
||||
if (providerID === "anthropic" || modelID.includes("anthropic") || modelID.includes("claude")) {
|
||||
const system = msgs.filter((msg) => msg.role === "system").slice(0, 2)
|
||||
const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
|
||||
function normalizeToolCallIds(msgs: ModelMessage[]): ModelMessage[] {
|
||||
return msgs.map((msg) => {
|
||||
if ((msg.role === "assistant" || msg.role === "tool") && Array.isArray(msg.content)) {
|
||||
msg.content = msg.content.map((part) => {
|
||||
if ((part.type === "tool-call" || part.type === "tool-result") && "toolCallId" in part) {
|
||||
return {
|
||||
...part,
|
||||
toolCallId: part.toolCallId.replace(/[^a-zA-Z0-9_-]/g, "_"),
|
||||
}
|
||||
}
|
||||
return part
|
||||
})
|
||||
}
|
||||
return msg
|
||||
})
|
||||
}
|
||||
|
||||
const providerOptions = {
|
||||
anthropic: {
|
||||
cacheControl: { type: "ephemeral" },
|
||||
},
|
||||
openrouter: {
|
||||
cache_control: { type: "ephemeral" },
|
||||
},
|
||||
bedrock: {
|
||||
cachePoint: { type: "ephemeral" },
|
||||
},
|
||||
openaiCompatible: {
|
||||
cache_control: { type: "ephemeral" },
|
||||
},
|
||||
function applyCaching(msgs: ModelMessage[], providerID: string): ModelMessage[] {
|
||||
const system = msgs.filter((msg) => msg.role === "system").slice(0, 2)
|
||||
const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
|
||||
|
||||
const providerOptions = {
|
||||
anthropic: {
|
||||
cacheControl: { type: "ephemeral" },
|
||||
},
|
||||
openrouter: {
|
||||
cache_control: { type: "ephemeral" },
|
||||
},
|
||||
bedrock: {
|
||||
cachePoint: { type: "ephemeral" },
|
||||
},
|
||||
openaiCompatible: {
|
||||
cache_control: { type: "ephemeral" },
|
||||
},
|
||||
}
|
||||
|
||||
for (const msg of unique([...system, ...final])) {
|
||||
const shouldUseContentOptions = providerID !== "anthropic" && Array.isArray(msg.content) && msg.content.length > 0
|
||||
|
||||
if (shouldUseContentOptions) {
|
||||
const lastContent = msg.content[msg.content.length - 1]
|
||||
if (lastContent && typeof lastContent === "object") {
|
||||
lastContent.providerOptions = {
|
||||
...lastContent.providerOptions,
|
||||
...providerOptions,
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
for (const msg of unique([...system, ...final])) {
|
||||
const shouldUseContentOptions =
|
||||
providerID !== "anthropic" && Array.isArray(msg.content) && msg.content.length > 0
|
||||
|
||||
if (shouldUseContentOptions) {
|
||||
const lastContent = msg.content[msg.content.length - 1]
|
||||
if (lastContent && typeof lastContent === "object") {
|
||||
lastContent.providerOptions = {
|
||||
...lastContent.providerOptions,
|
||||
...providerOptions,
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
msg.providerOptions = {
|
||||
...msg.providerOptions,
|
||||
...providerOptions,
|
||||
}
|
||||
msg.providerOptions = {
|
||||
...msg.providerOptions,
|
||||
...providerOptions,
|
||||
}
|
||||
}
|
||||
|
||||
return msgs
|
||||
}
|
||||
|
||||
export function message(msgs: ModelMessage[], providerID: string, modelID: string) {
|
||||
if (modelID.includes("claude")) {
|
||||
msgs = normalizeToolCallIds(msgs)
|
||||
}
|
||||
if (providerID === "anthropic" || modelID.includes("anthropic") || modelID.includes("claude")) {
|
||||
msgs = applyCaching(msgs, providerID)
|
||||
}
|
||||
|
||||
return msgs
|
||||
}
|
||||
|
||||
@@ -50,4 +76,9 @@ export namespace ProviderTransform {
|
||||
if (modelID.toLowerCase().includes("qwen")) return 0.55
|
||||
return 0
|
||||
}
|
||||
|
||||
export function topP(_providerID: string, modelID: string) {
|
||||
if (modelID.toLowerCase().includes("qwen")) return 1
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ import { LSP } from "../lsp"
|
||||
import { MessageV2 } from "../session/message-v2"
|
||||
import { Mode } from "../session/mode"
|
||||
import { callTui, TuiRoute } from "./tui"
|
||||
import { Permission } from "../permission"
|
||||
import { lazy } from "../util/lazy"
|
||||
|
||||
const ERRORS = {
|
||||
400: {
|
||||
@@ -47,7 +49,7 @@ export namespace Server {
|
||||
Connected: Bus.event("server.connected", z.object({})),
|
||||
}
|
||||
|
||||
function app() {
|
||||
export const app = lazy(() => {
|
||||
const app = new Hono()
|
||||
|
||||
const result = app
|
||||
@@ -94,6 +96,7 @@ export namespace Server {
|
||||
"/event",
|
||||
describeRoute({
|
||||
description: "Get events",
|
||||
operationId: "event.subscribe",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Event stream",
|
||||
@@ -137,6 +140,7 @@ export namespace Server {
|
||||
"/app",
|
||||
describeRoute({
|
||||
description: "Get app info",
|
||||
operationId: "app.get",
|
||||
responses: {
|
||||
200: {
|
||||
description: "200",
|
||||
@@ -156,6 +160,7 @@ export namespace Server {
|
||||
"/app/init",
|
||||
describeRoute({
|
||||
description: "Initialize the app",
|
||||
operationId: "app.init",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Initialize the app",
|
||||
@@ -176,6 +181,7 @@ export namespace Server {
|
||||
"/config",
|
||||
describeRoute({
|
||||
description: "Get config info",
|
||||
operationId: "config.get",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Get config info",
|
||||
@@ -195,6 +201,7 @@ export namespace Server {
|
||||
"/session",
|
||||
describeRoute({
|
||||
description: "List all sessions",
|
||||
operationId: "session.list",
|
||||
responses: {
|
||||
200: {
|
||||
description: "List of sessions",
|
||||
@@ -216,6 +223,7 @@ export namespace Server {
|
||||
"/session",
|
||||
describeRoute({
|
||||
description: "Create a new session",
|
||||
operationId: "session.create",
|
||||
responses: {
|
||||
...ERRORS,
|
||||
200: {
|
||||
@@ -237,6 +245,7 @@ export namespace Server {
|
||||
"/session/:id",
|
||||
describeRoute({
|
||||
description: "Delete a session and all its data",
|
||||
operationId: "session.delete",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successfully deleted session",
|
||||
@@ -263,6 +272,7 @@ export namespace Server {
|
||||
"/session/:id/init",
|
||||
describeRoute({
|
||||
description: "Analyze the app and create an AGENTS.md file",
|
||||
operationId: "session.init",
|
||||
responses: {
|
||||
200: {
|
||||
description: "200",
|
||||
@@ -299,6 +309,7 @@ export namespace Server {
|
||||
"/session/:id/abort",
|
||||
describeRoute({
|
||||
description: "Abort a session",
|
||||
operationId: "session.abort",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Aborted session",
|
||||
@@ -324,6 +335,7 @@ export namespace Server {
|
||||
"/session/:id/share",
|
||||
describeRoute({
|
||||
description: "Share a session",
|
||||
operationId: "session.share",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successfully shared session",
|
||||
@@ -352,6 +364,7 @@ export namespace Server {
|
||||
"/session/:id/share",
|
||||
describeRoute({
|
||||
description: "Unshare the session",
|
||||
operationId: "session.unshare",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successfully unshared session",
|
||||
@@ -380,6 +393,7 @@ export namespace Server {
|
||||
"/session/:id/summarize",
|
||||
describeRoute({
|
||||
description: "Summarize the session",
|
||||
operationId: "session.summarize",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Summarized session",
|
||||
@@ -415,6 +429,7 @@ export namespace Server {
|
||||
"/session/:id/message",
|
||||
describeRoute({
|
||||
description: "List messages for a session",
|
||||
operationId: "session.messages",
|
||||
responses: {
|
||||
200: {
|
||||
description: "List of messages",
|
||||
@@ -444,10 +459,45 @@ export namespace Server {
|
||||
return c.json(messages)
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/session/:id/message/:messageID",
|
||||
describeRoute({
|
||||
description: "Get a message from a session",
|
||||
operationId: "session.message",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Message",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(
|
||||
z.object({
|
||||
info: MessageV2.Info,
|
||||
parts: MessageV2.Part.array(),
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
zValidator(
|
||||
"param",
|
||||
z.object({
|
||||
id: z.string().openapi({ description: "Session ID" }),
|
||||
messageID: z.string().openapi({ description: "Message ID" }),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const params = c.req.valid("param")
|
||||
const message = await Session.getMessage(params.id, params.messageID)
|
||||
return c.json(message)
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/session/:id/message",
|
||||
describeRoute({
|
||||
description: "Create and send a new message to a session",
|
||||
operationId: "session.chat",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Created message",
|
||||
@@ -477,6 +527,7 @@ export namespace Server {
|
||||
"/session/:id/revert",
|
||||
describeRoute({
|
||||
description: "Revert a message",
|
||||
operationId: "session.revert",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Updated session",
|
||||
@@ -506,6 +557,7 @@ export namespace Server {
|
||||
"/session/:id/unrevert",
|
||||
describeRoute({
|
||||
description: "Restore all reverted messages",
|
||||
operationId: "session.unrevert",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Updated session",
|
||||
@@ -529,10 +581,42 @@ export namespace Server {
|
||||
return c.json(session)
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/session/:id/permissions/:permissionID",
|
||||
describeRoute({
|
||||
description: "Respond to a permission request",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Permission processed successfully",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.boolean()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
zValidator(
|
||||
"param",
|
||||
z.object({
|
||||
id: z.string(),
|
||||
permissionID: z.string(),
|
||||
}),
|
||||
),
|
||||
zValidator("json", z.object({ response: Permission.Response })),
|
||||
async (c) => {
|
||||
const params = c.req.valid("param")
|
||||
const id = params.id
|
||||
const permissionID = params.permissionID
|
||||
Permission.respond({ sessionID: id, permissionID, response: c.req.valid("json").response })
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/config/providers",
|
||||
describeRoute({
|
||||
description: "List all providers",
|
||||
operationId: "config.providers",
|
||||
responses: {
|
||||
200: {
|
||||
description: "List of providers",
|
||||
@@ -561,6 +645,7 @@ export namespace Server {
|
||||
"/find",
|
||||
describeRoute({
|
||||
description: "Find text in files",
|
||||
operationId: "find.text",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Matches",
|
||||
@@ -593,6 +678,7 @@ export namespace Server {
|
||||
"/find/file",
|
||||
describeRoute({
|
||||
description: "Find files",
|
||||
operationId: "find.files",
|
||||
responses: {
|
||||
200: {
|
||||
description: "File paths",
|
||||
@@ -625,6 +711,7 @@ export namespace Server {
|
||||
"/find/symbol",
|
||||
describeRoute({
|
||||
description: "Find workspace symbols",
|
||||
operationId: "find.symbols",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Symbols",
|
||||
@@ -652,6 +739,7 @@ export namespace Server {
|
||||
"/file",
|
||||
describeRoute({
|
||||
description: "Read a file",
|
||||
operationId: "file.read",
|
||||
responses: {
|
||||
200: {
|
||||
description: "File content",
|
||||
@@ -688,6 +776,7 @@ export namespace Server {
|
||||
"/file/status",
|
||||
describeRoute({
|
||||
description: "Get file status",
|
||||
operationId: "file.status",
|
||||
responses: {
|
||||
200: {
|
||||
description: "File status",
|
||||
@@ -708,6 +797,7 @@ export namespace Server {
|
||||
"/log",
|
||||
describeRoute({
|
||||
description: "Write a log entry to the server logs",
|
||||
operationId: "app.log",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Log entry written successfully",
|
||||
@@ -757,6 +847,7 @@ export namespace Server {
|
||||
"/mode",
|
||||
describeRoute({
|
||||
description: "List all modes",
|
||||
operationId: "app.modes",
|
||||
responses: {
|
||||
200: {
|
||||
description: "List of modes",
|
||||
@@ -777,6 +868,7 @@ export namespace Server {
|
||||
"/tui/append-prompt",
|
||||
describeRoute({
|
||||
description: "Append prompt to the TUI",
|
||||
operationId: "tui.appendPrompt",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Prompt processed successfully",
|
||||
@@ -800,6 +892,7 @@ export namespace Server {
|
||||
"/tui/open-help",
|
||||
describeRoute({
|
||||
description: "Open the help dialog",
|
||||
operationId: "tui.openHelp",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Help dialog opened successfully",
|
||||
@@ -813,10 +906,124 @@ export namespace Server {
|
||||
}),
|
||||
async (c) => c.json(await callTui(c)),
|
||||
)
|
||||
.post(
|
||||
"/tui/open-sessions",
|
||||
describeRoute({
|
||||
description: "Open the session dialog",
|
||||
operationId: "tui.openSessions",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Session dialog opened successfully",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.boolean()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
async (c) => c.json(await callTui(c)),
|
||||
)
|
||||
.post(
|
||||
"/tui/open-themes",
|
||||
describeRoute({
|
||||
description: "Open the theme dialog",
|
||||
operationId: "tui.openThemes",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Theme dialog opened successfully",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.boolean()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
async (c) => c.json(await callTui(c)),
|
||||
)
|
||||
.post(
|
||||
"/tui/open-models",
|
||||
describeRoute({
|
||||
description: "Open the model dialog",
|
||||
operationId: "tui.openModels",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Model dialog opened successfully",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.boolean()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
async (c) => c.json(await callTui(c)),
|
||||
)
|
||||
.post(
|
||||
"/tui/submit-prompt",
|
||||
describeRoute({
|
||||
description: "Submit the prompt",
|
||||
operationId: "tui.submitPrompt",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Prompt submitted successfully",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.boolean()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
async (c) => c.json(await callTui(c)),
|
||||
)
|
||||
.post(
|
||||
"/tui/clear-prompt",
|
||||
describeRoute({
|
||||
description: "Clear the prompt",
|
||||
operationId: "tui.clearPrompt",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Prompt cleared successfully",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.boolean()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
async (c) => c.json(await callTui(c)),
|
||||
)
|
||||
.post(
|
||||
"/tui/execute-command",
|
||||
describeRoute({
|
||||
description: "Execute a TUI command (e.g. switch_mode)",
|
||||
operationId: "tui.executeCommand",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Command executed successfully",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.boolean()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
zValidator(
|
||||
"json",
|
||||
z.object({
|
||||
command: z.string(),
|
||||
}),
|
||||
),
|
||||
async (c) => c.json(await callTui(c)),
|
||||
)
|
||||
.route("/tui/control", TuiRoute)
|
||||
|
||||
return result
|
||||
}
|
||||
})
|
||||
|
||||
export async function openapi() {
|
||||
const a = app()
|
||||
|
||||
@@ -41,6 +41,7 @@ import { LSP } from "../lsp"
|
||||
import { ReadTool } from "../tool/read"
|
||||
import { mergeDeep, pipe, splitWhen } from "remeda"
|
||||
import { ToolRegistry } from "../tool/registry"
|
||||
import { Plugin } from "../plugin"
|
||||
|
||||
export namespace Session {
|
||||
const log = Log.create({ service: "session" })
|
||||
@@ -120,6 +121,7 @@ export namespace Session {
|
||||
const sessions = new Map<string, Info>()
|
||||
const messages = new Map<string, MessageV2.Info[]>()
|
||||
const pending = new Map<string, AbortController>()
|
||||
const autoCompacting = new Map<string, boolean>()
|
||||
const queued = new Map<
|
||||
string,
|
||||
{
|
||||
@@ -135,6 +137,7 @@ export namespace Session {
|
||||
sessions,
|
||||
messages,
|
||||
pending,
|
||||
autoCompacting,
|
||||
queued,
|
||||
}
|
||||
},
|
||||
@@ -256,7 +259,10 @@ export namespace Session {
|
||||
}
|
||||
|
||||
export async function getMessage(sessionID: string, messageID: string) {
|
||||
return Storage.readJSON<MessageV2.Info>("session/message/" + sessionID + "/" + messageID)
|
||||
return {
|
||||
info: await Storage.readJSON<MessageV2.Info>("session/message/" + sessionID + "/" + messageID),
|
||||
parts: await getParts(sessionID, messageID),
|
||||
}
|
||||
}
|
||||
|
||||
export async function getParts(sessionID: string, messageID: string) {
|
||||
@@ -290,6 +296,9 @@ export namespace Session {
|
||||
export function abort(sessionID: string) {
|
||||
const controller = state().pending.get(sessionID)
|
||||
if (!controller) return false
|
||||
log.info("aborting", {
|
||||
sessionID,
|
||||
})
|
||||
controller.abort()
|
||||
state().pending.delete(sessionID)
|
||||
return true
|
||||
@@ -374,6 +383,36 @@ export namespace Session {
|
||||
l.info("chatting")
|
||||
|
||||
const inputMode = input.mode ?? "build"
|
||||
|
||||
// Process revert cleanup first, before creating new messages
|
||||
const session = await get(input.sessionID)
|
||||
if (session.revert) {
|
||||
let msgs = await messages(input.sessionID)
|
||||
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, {
|
||||
sessionID: input.sessionID,
|
||||
messageID: last.info.id,
|
||||
partID: part.id,
|
||||
})
|
||||
}
|
||||
}
|
||||
await update(input.sessionID, (draft) => {
|
||||
draft.revert = undefined
|
||||
})
|
||||
}
|
||||
const userMsg: MessageV2.Info = {
|
||||
id: input.messageID ?? Identifier.ascending("message"),
|
||||
role: "user",
|
||||
@@ -535,12 +574,23 @@ export namespace Session {
|
||||
text: PROMPT_PLAN,
|
||||
synthetic: true,
|
||||
})
|
||||
|
||||
await Plugin.trigger(
|
||||
"chat.message",
|
||||
{},
|
||||
{
|
||||
message: userMsg,
|
||||
parts: userParts,
|
||||
},
|
||||
)
|
||||
await updateMessage(userMsg)
|
||||
for (const part of userParts) {
|
||||
await updatePart(part)
|
||||
}
|
||||
|
||||
// mark session as updated
|
||||
// used for session list sorting (indicates when session was most recently interacted with)
|
||||
await update(input.sessionID, (_draft) => {})
|
||||
|
||||
if (isLocked(input.sessionID)) {
|
||||
return new Promise((resolve) => {
|
||||
const queue = state().queued.get(input.sessionID) ?? []
|
||||
@@ -557,35 +607,6 @@ export namespace Session {
|
||||
|
||||
const model = await Provider.getModel(input.providerID, input.modelID)
|
||||
let msgs = await messages(input.sessionID)
|
||||
const session = await get(input.sessionID)
|
||||
|
||||
if (session.revert) {
|
||||
const messageID = session.revert.messageID
|
||||
const [preserve, remove] = splitWhen(msgs, (x) => x.info.id === messageID)
|
||||
msgs = preserve
|
||||
for (const msg of remove) {
|
||||
if (msg.info.id === userMsg.id) continue
|
||||
await Storage.remove(`session/message/${input.sessionID}/${msg.info.id}`)
|
||||
await Bus.publish(MessageV2.Event.Removed, { sessionID: input.sessionID, messageID: msg.info.id })
|
||||
}
|
||||
const last = preserve.at(-1)
|
||||
if (session.revert.partID && last) {
|
||||
const partID = session.revert.partID
|
||||
const [preserveParts, removeParts] = splitWhen(last.parts, (x) => x.id === partID)
|
||||
last.parts = preserveParts
|
||||
for (const part of removeParts) {
|
||||
await Storage.remove(`session/part/${input.sessionID}/${last.info.id}/${part.id}`)
|
||||
await Bus.publish(MessageV2.Event.PartRemoved, {
|
||||
sessionID: input.sessionID,
|
||||
messageID: last.info.id,
|
||||
partID: part.id,
|
||||
})
|
||||
}
|
||||
}
|
||||
await update(input.sessionID, (draft) => {
|
||||
draft.revert = undefined
|
||||
})
|
||||
}
|
||||
|
||||
const previous = msgs.filter((x) => x.info.role === "assistant").at(-1)?.info as MessageV2.Assistant
|
||||
const outputLimit = Math.min(model.info.limit.output, OUTPUT_TOKEN_MAX) || OUTPUT_TOKEN_MAX
|
||||
@@ -595,6 +616,8 @@ export namespace Session {
|
||||
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)) {
|
||||
state().autoCompacting.set(input.sessionID, true)
|
||||
|
||||
await summarize({
|
||||
sessionID: input.sessionID,
|
||||
providerID: input.providerID,
|
||||
@@ -603,7 +626,6 @@ export namespace Session {
|
||||
return chat(input)
|
||||
}
|
||||
}
|
||||
|
||||
using abort = lock(input.sessionID)
|
||||
|
||||
const lastSummary = msgs.findLast((msg) => msg.info.role === "assistant" && msg.info.summary === true)
|
||||
@@ -705,11 +727,22 @@ export namespace Session {
|
||||
description: item.description,
|
||||
inputSchema: item.parameters as ZodSchema,
|
||||
async execute(args, options) {
|
||||
await processor.track(options.toolCallId)
|
||||
await Plugin.trigger(
|
||||
"tool.execute.before",
|
||||
{
|
||||
tool: item.id,
|
||||
sessionID: input.sessionID,
|
||||
callID: options.toolCallId,
|
||||
},
|
||||
{
|
||||
args,
|
||||
},
|
||||
)
|
||||
const result = await item.execute(args, {
|
||||
sessionID: input.sessionID,
|
||||
abort: abort.signal,
|
||||
abort: options.abortSignal!,
|
||||
messageID: assistantMsg.id,
|
||||
callID: options.toolCallId,
|
||||
metadata: async (val) => {
|
||||
const match = processor.partFromToolCall(options.toolCallId)
|
||||
if (match && match.state.status === "running") {
|
||||
@@ -728,6 +761,15 @@ export namespace Session {
|
||||
}
|
||||
},
|
||||
})
|
||||
await Plugin.trigger(
|
||||
"tool.execute.after",
|
||||
{
|
||||
tool: item.id,
|
||||
sessionID: input.sessionID,
|
||||
callID: options.toolCallId,
|
||||
},
|
||||
result,
|
||||
)
|
||||
return result
|
||||
},
|
||||
toModelOutput(result) {
|
||||
@@ -740,11 +782,10 @@ export namespace Session {
|
||||
}
|
||||
|
||||
for (const [key, item] of Object.entries(await MCP.tools())) {
|
||||
if (mode.tools[key] === false) continue
|
||||
if (enabledTools[key] === false) continue
|
||||
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")
|
||||
@@ -764,8 +805,27 @@ export namespace Session {
|
||||
tools[key] = item
|
||||
}
|
||||
|
||||
const params = {
|
||||
temperature: model.info.temperature
|
||||
? (mode.temperature ?? ProviderTransform.temperature(input.providerID, input.modelID))
|
||||
: undefined,
|
||||
topP: mode.topP ?? ProviderTransform.topP(input.providerID, input.modelID),
|
||||
}
|
||||
await Plugin.trigger(
|
||||
"chat.params",
|
||||
{
|
||||
model: model.info,
|
||||
provider: await Provider.getProvider(input.providerID),
|
||||
message: userMsg,
|
||||
},
|
||||
params,
|
||||
)
|
||||
const stream = streamText({
|
||||
onError() {},
|
||||
onError(e) {
|
||||
log.error("streamText error", {
|
||||
error: e,
|
||||
})
|
||||
},
|
||||
async prepareStep({ messages }) {
|
||||
const queue = (state().queued.get(input.sessionID) ?? []).filter((x) => !x.processed)
|
||||
if (queue.length) {
|
||||
@@ -812,13 +872,26 @@ export namespace Session {
|
||||
messages,
|
||||
}
|
||||
},
|
||||
maxRetries: 10,
|
||||
async experimental_repairToolCall(input) {
|
||||
return {
|
||||
...input.toolCall,
|
||||
input: JSON.stringify({
|
||||
tool: input.toolCall.toolName,
|
||||
error: input.error.message,
|
||||
}),
|
||||
toolName: "invalid",
|
||||
}
|
||||
},
|
||||
maxRetries: 3,
|
||||
activeTools: Object.keys(tools).filter((x) => x !== "invalid"),
|
||||
maxOutputTokens: outputLimit,
|
||||
abortSignal: abort.signal,
|
||||
stopWhen: stepCountIs(1000),
|
||||
providerOptions: {
|
||||
[input.providerID]: model.info.options,
|
||||
},
|
||||
temperature: params.temperature,
|
||||
topP: params.topP,
|
||||
messages: [
|
||||
...system.map(
|
||||
(x): ModelMessage => ({
|
||||
@@ -828,9 +901,6 @@ export namespace Session {
|
||||
),
|
||||
...MessageV2.toModelMessage(msgs),
|
||||
],
|
||||
temperature: model.info.temperature
|
||||
? (mode.temperature ?? ProviderTransform.temperature(input.providerID, input.modelID))
|
||||
: undefined,
|
||||
tools: model.info.tool_call === false ? undefined : tools,
|
||||
model: wrapLanguageModel({
|
||||
model: model.language,
|
||||
@@ -862,15 +932,11 @@ export namespace Session {
|
||||
}
|
||||
|
||||
function createProcessor(assistantMsg: MessageV2.Assistant, model: ModelsDev.Model) {
|
||||
const toolCalls: Record<string, MessageV2.ToolPart> = {}
|
||||
const snapshots: Record<string, string> = {}
|
||||
const toolcalls: Record<string, MessageV2.ToolPart> = {}
|
||||
let snapshot: string | undefined
|
||||
return {
|
||||
async track(toolCallID: string) {
|
||||
const hash = await Snapshot.track()
|
||||
if (hash) snapshots[toolCallID] = hash
|
||||
},
|
||||
partFromToolCall(toolCallID: string) {
|
||||
return toolCalls[toolCallID]
|
||||
return toolcalls[toolCallID]
|
||||
},
|
||||
async process(stream: StreamTextResult<Record<string, AITool>, never>) {
|
||||
try {
|
||||
@@ -886,7 +952,7 @@ export namespace Session {
|
||||
|
||||
case "tool-input-start":
|
||||
const part = await updatePart({
|
||||
id: toolCalls[value.id]?.id ?? Identifier.ascending("part"),
|
||||
id: toolcalls[value.id]?.id ?? Identifier.ascending("part"),
|
||||
messageID: assistantMsg.id,
|
||||
sessionID: assistantMsg.sessionID,
|
||||
type: "tool",
|
||||
@@ -896,7 +962,7 @@ export namespace Session {
|
||||
status: "pending",
|
||||
},
|
||||
})
|
||||
toolCalls[value.id] = part as MessageV2.ToolPart
|
||||
toolcalls[value.id] = part as MessageV2.ToolPart
|
||||
break
|
||||
|
||||
case "tool-input-delta":
|
||||
@@ -906,10 +972,11 @@ export namespace Session {
|
||||
break
|
||||
|
||||
case "tool-call": {
|
||||
const match = toolCalls[value.toolCallId]
|
||||
const match = toolcalls[value.toolCallId]
|
||||
if (match) {
|
||||
const part = await updatePart({
|
||||
...match,
|
||||
tool: value.toolName,
|
||||
state: {
|
||||
status: "running",
|
||||
input: value.input,
|
||||
@@ -918,12 +985,12 @@ export namespace Session {
|
||||
},
|
||||
},
|
||||
})
|
||||
toolCalls[value.toolCallId] = part as MessageV2.ToolPart
|
||||
toolcalls[value.toolCallId] = part as MessageV2.ToolPart
|
||||
}
|
||||
break
|
||||
}
|
||||
case "tool-result": {
|
||||
const match = toolCalls[value.toolCallId]
|
||||
const match = toolcalls[value.toolCallId]
|
||||
if (match && match.state.status === "running") {
|
||||
await updatePart({
|
||||
...match,
|
||||
@@ -939,27 +1006,13 @@ export namespace Session {
|
||||
},
|
||||
},
|
||||
})
|
||||
delete toolCalls[value.toolCallId]
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
delete toolcalls[value.toolCallId]
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case "tool-error": {
|
||||
const match = toolCalls[value.toolCallId]
|
||||
const match = toolcalls[value.toolCallId]
|
||||
if (match && match.state.status === "running") {
|
||||
await updatePart({
|
||||
...match,
|
||||
@@ -973,19 +1026,7 @@ export namespace Session {
|
||||
},
|
||||
},
|
||||
})
|
||||
delete toolCalls[value.toolCallId]
|
||||
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: "patch",
|
||||
hash: patch.hash,
|
||||
files: patch.files,
|
||||
})
|
||||
}
|
||||
delete toolcalls[value.toolCallId]
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -1000,6 +1041,7 @@ export namespace Session {
|
||||
sessionID: assistantMsg.sessionID,
|
||||
type: "step-start",
|
||||
})
|
||||
snapshot = await Snapshot.track()
|
||||
break
|
||||
|
||||
case "finish-step":
|
||||
@@ -1015,6 +1057,20 @@ export namespace Session {
|
||||
cost: usage.cost,
|
||||
})
|
||||
await updateMessage(assistantMsg)
|
||||
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,
|
||||
})
|
||||
}
|
||||
snapshot = undefined
|
||||
}
|
||||
break
|
||||
|
||||
case "text-start":
|
||||
@@ -1030,7 +1086,7 @@ export namespace Session {
|
||||
}
|
||||
break
|
||||
|
||||
case "text":
|
||||
case "text-delta":
|
||||
if (currentText) {
|
||||
currentText.text += value.text
|
||||
if (currentText.text) await updatePart(currentText)
|
||||
@@ -1099,7 +1155,7 @@ export namespace Session {
|
||||
}
|
||||
const p = await getParts(assistantMsg.sessionID, assistantMsg.id)
|
||||
for (const part of p) {
|
||||
if (part.type === "tool" && part.state.status !== "completed") {
|
||||
if (part.type === "tool" && part.state.status !== "completed" && part.state.status !== "error") {
|
||||
updatePart({
|
||||
...part,
|
||||
state: {
|
||||
@@ -1262,9 +1318,19 @@ export namespace Session {
|
||||
state().pending.set(sessionID, controller)
|
||||
return {
|
||||
signal: controller.signal,
|
||||
[Symbol.dispose]() {
|
||||
async [Symbol.dispose]() {
|
||||
log.info("unlocking", { sessionID })
|
||||
state().pending.delete(sessionID)
|
||||
|
||||
const isAutoCompacting = state().autoCompacting.get(sessionID) ?? false
|
||||
if (isAutoCompacting) {
|
||||
state().autoCompacting.delete(sessionID)
|
||||
return
|
||||
}
|
||||
|
||||
const session = await get(sessionID)
|
||||
if (session.parentID) return
|
||||
|
||||
Bus.publish(Event.Idle, {
|
||||
sessionID,
|
||||
})
|
||||
@@ -1287,10 +1353,10 @@ export namespace Session {
|
||||
}
|
||||
return {
|
||||
cost: new Decimal(0)
|
||||
.add(new Decimal(tokens.input).mul(model.cost.input).div(1_000_000))
|
||||
.add(new Decimal(tokens.output).mul(model.cost.output).div(1_000_000))
|
||||
.add(new Decimal(tokens.cache.read).mul(model.cost.cache_read ?? 0).div(1_000_000))
|
||||
.add(new Decimal(tokens.cache.write).mul(model.cost.cache_write ?? 0).div(1_000_000))
|
||||
.add(new Decimal(tokens.input).mul(model.cost?.input ?? 0).div(1_000_000))
|
||||
.add(new Decimal(tokens.output).mul(model.cost?.output ?? 0).div(1_000_000))
|
||||
.add(new Decimal(tokens.cache.read).mul(model.cost?.cache_read ?? 0).div(1_000_000))
|
||||
.add(new Decimal(tokens.cache.write).mul(model.cost?.cache_write ?? 0).div(1_000_000))
|
||||
.toNumber(),
|
||||
tokens,
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ export namespace Mode {
|
||||
.object({
|
||||
name: z.string(),
|
||||
temperature: z.number().optional(),
|
||||
topP: z.number().optional(),
|
||||
model: z
|
||||
.object({
|
||||
modelID: z.string(),
|
||||
@@ -51,7 +52,8 @@ export namespace Mode {
|
||||
item.name = key
|
||||
if (value.model) item.model = Provider.parseModel(value.model)
|
||||
if (value.prompt) item.prompt = value.prompt
|
||||
if (value.temperature) item.temperature = value.temperature
|
||||
if (value.temperature != undefined) item.temperature = value.temperature
|
||||
if (value.top_p != undefined) item.topP = value.top_p
|
||||
if (value.tools)
|
||||
item.tools = {
|
||||
...value.tools,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# Beast Mode 3.1
|
||||
|
||||
You are opencode, an 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.
|
||||
|
||||
@@ -60,33 +60,28 @@ export namespace SystemPrompt {
|
||||
export async function custom() {
|
||||
const { cwd, root } = App.info().path
|
||||
const config = await Config.get()
|
||||
const found = []
|
||||
const paths = new Set<string>()
|
||||
|
||||
for (const item of CUSTOM_FILES) {
|
||||
const matches = await Filesystem.findUp(item, cwd, root)
|
||||
found.push(...matches.map((x) => Bun.file(x).text()))
|
||||
matches.forEach((path) => paths.add(path))
|
||||
}
|
||||
found.push(
|
||||
Bun.file(path.join(Global.Path.config, "AGENTS.md"))
|
||||
.text()
|
||||
.catch(() => ""),
|
||||
)
|
||||
found.push(
|
||||
Bun.file(path.join(os.homedir(), ".claude", "CLAUDE.md"))
|
||||
.text()
|
||||
.catch(() => ""),
|
||||
)
|
||||
|
||||
paths.add(path.join(Global.Path.config, "AGENTS.md"))
|
||||
paths.add(path.join(os.homedir(), ".claude", "CLAUDE.md"))
|
||||
|
||||
if (config.instructions) {
|
||||
for (const instruction of config.instructions) {
|
||||
try {
|
||||
const matches = await Filesystem.globUp(instruction, cwd, root)
|
||||
found.push(...matches.map((x) => Bun.file(x).text()))
|
||||
} catch {
|
||||
continue // Skip invalid glob patterns
|
||||
}
|
||||
const matches = await Filesystem.globUp(instruction, cwd, root).catch(() => [])
|
||||
matches.forEach((path) => paths.add(path))
|
||||
}
|
||||
}
|
||||
|
||||
const found = Array.from(paths).map((p) =>
|
||||
Bun.file(p)
|
||||
.text()
|
||||
.catch(() => ""),
|
||||
)
|
||||
return Promise.all(found).then((result) => result.filter(Boolean))
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ export namespace Snapshot {
|
||||
log.info("initialized")
|
||||
}
|
||||
await $`git --git-dir ${git} add .`.quiet().cwd(app.path.cwd).nothrow()
|
||||
const hash = await $`git --git-dir ${git} write-tree`.quiet().cwd(app.path.cwd).text()
|
||||
const hash = await $`git --git-dir ${git} write-tree`.quiet().cwd(app.path.cwd).nothrow().text()
|
||||
return hash.trim()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,31 @@
|
||||
import { z } from "zod"
|
||||
import { exec } from "child_process"
|
||||
import { text } from "stream/consumers"
|
||||
import { Tool } from "./tool"
|
||||
import DESCRIPTION from "./bash.txt"
|
||||
import { App } from "../app/app"
|
||||
import { Permission } from "../permission"
|
||||
import { Config } from "../config/config"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
import { lazy } from "../util/lazy"
|
||||
import { Log } from "../util/log"
|
||||
import { Wildcard } from "../util/wildcard"
|
||||
import { $ } from "bun"
|
||||
|
||||
const MAX_OUTPUT_LENGTH = 30000
|
||||
const DEFAULT_TIMEOUT = 1 * 60 * 1000
|
||||
const MAX_TIMEOUT = 10 * 60 * 1000
|
||||
|
||||
const log = Log.create({ service: "bash-tool" })
|
||||
|
||||
const parser = lazy(async () => {
|
||||
const { default: Parser } = await import("tree-sitter")
|
||||
const Bash = await import("tree-sitter-bash")
|
||||
const p = new Parser()
|
||||
p.setLanguage(Bash.language as any)
|
||||
return p
|
||||
})
|
||||
|
||||
export const BashTool = Tool.define("bash", {
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
@@ -20,19 +39,103 @@ export const BashTool = Tool.define("bash", {
|
||||
}),
|
||||
async execute(params, ctx) {
|
||||
const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT)
|
||||
const app = App.info()
|
||||
const cfg = await Config.get()
|
||||
const tree = await parser().then((p) => p.parse(params.command))
|
||||
const permissions = (() => {
|
||||
const value = cfg.permission?.bash
|
||||
if (!value)
|
||||
return {
|
||||
"*": "allow",
|
||||
}
|
||||
if (typeof value === "string")
|
||||
return {
|
||||
"*": value,
|
||||
}
|
||||
return value
|
||||
})()
|
||||
|
||||
const process = Bun.spawn({
|
||||
cmd: ["bash", "-c", params.command],
|
||||
cwd: App.info().path.cwd,
|
||||
maxBuffer: MAX_OUTPUT_LENGTH,
|
||||
let needsAsk = false
|
||||
for (const node of tree.rootNode.descendantsOfType("command")) {
|
||||
const command = []
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const child = node.child(i)
|
||||
if (!child) continue
|
||||
if (
|
||||
child.type !== "command_name" &&
|
||||
child.type !== "word" &&
|
||||
child.type !== "string" &&
|
||||
child.type !== "raw_string" &&
|
||||
child.type !== "concatenation"
|
||||
) {
|
||||
continue
|
||||
}
|
||||
command.push(child.text)
|
||||
}
|
||||
|
||||
// not an exhaustive list, but covers most common cases
|
||||
if (["cd", "rm", "cp", "mv", "mkdir", "touch", "chmod", "chown"].includes(command[0])) {
|
||||
for (const arg of command.slice(1)) {
|
||||
if (arg.startsWith("-") || (command[0] === "chmod" && arg.startsWith("+"))) continue
|
||||
const resolved = await $`realpath ${arg}`
|
||||
.quiet()
|
||||
.nothrow()
|
||||
.text()
|
||||
.then((x) => x.trim())
|
||||
log.info("resolved path", { arg, resolved })
|
||||
if (resolved && !Filesystem.contains(app.path.cwd, resolved)) {
|
||||
throw new Error(
|
||||
`This command references paths outside of ${app.path.cwd} so it is not allowed to be executed.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// always allow cd if it passes above check
|
||||
if (!needsAsk && command[0] !== "cd") {
|
||||
const ask = (() => {
|
||||
for (const [pattern, value] of Object.entries(permissions)) {
|
||||
const match = Wildcard.match(node.text, pattern)
|
||||
log.info("checking", { text: node.text.trim(), pattern, match })
|
||||
if (match) return value
|
||||
}
|
||||
return "ask"
|
||||
})()
|
||||
if (ask === "ask") needsAsk = true
|
||||
}
|
||||
}
|
||||
|
||||
if (needsAsk) {
|
||||
await Permission.ask({
|
||||
type: "bash",
|
||||
sessionID: ctx.sessionID,
|
||||
messageID: ctx.messageID,
|
||||
callID: ctx.callID,
|
||||
title: params.command,
|
||||
metadata: {
|
||||
command: params.command,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const process = exec(params.command, {
|
||||
cwd: app.path.cwd,
|
||||
signal: ctx.abort,
|
||||
timeout: timeout,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
maxBuffer: MAX_OUTPUT_LENGTH,
|
||||
timeout,
|
||||
})
|
||||
await process.exited
|
||||
const stdout = await new Response(process.stdout).text()
|
||||
const stderr = await new Response(process.stderr).text()
|
||||
|
||||
const stdoutPromise = text(process.stdout!)
|
||||
const stderrPromise = text(process.stderr!)
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
process.on("close", () => {
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
|
||||
const stdout = await stdoutPromise
|
||||
const stderr = await stderrPromise
|
||||
|
||||
return {
|
||||
title: params.command,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// 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"
|
||||
@@ -13,6 +14,8 @@ import { App } from "../app/app"
|
||||
import { File } from "../file"
|
||||
import { Bus } from "../bus"
|
||||
import { FileTime } from "../file/time"
|
||||
import { Config } from "../config/config"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
|
||||
export const EditTool = Tool.define("edit", {
|
||||
description: DESCRIPTION,
|
||||
@@ -32,56 +35,78 @@ export const EditTool = Tool.define("edit", {
|
||||
}
|
||||
|
||||
const app = App.info()
|
||||
const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(app.path.cwd, params.filePath)
|
||||
|
||||
await Permission.ask({
|
||||
id: "edit",
|
||||
sessionID: ctx.sessionID,
|
||||
title: "Edit this file: " + filepath,
|
||||
metadata: {
|
||||
filePath: filepath,
|
||||
oldString: params.oldString,
|
||||
newString: params.newString,
|
||||
},
|
||||
})
|
||||
const filePath = path.isAbsolute(params.filePath) ? params.filePath : path.join(app.path.cwd, params.filePath)
|
||||
if (!Filesystem.contains(app.path.cwd, filePath)) {
|
||||
throw new Error(`File ${filePath} is not in the current working directory`)
|
||||
}
|
||||
|
||||
const cfg = await Config.get()
|
||||
let diff = ""
|
||||
let contentOld = ""
|
||||
let contentNew = ""
|
||||
await (async () => {
|
||||
if (params.oldString === "") {
|
||||
contentNew = params.newString
|
||||
await Bun.write(filepath, params.newString)
|
||||
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
|
||||
if (cfg.permission?.edit === "ask") {
|
||||
await Permission.ask({
|
||||
type: "edit",
|
||||
sessionID: ctx.sessionID,
|
||||
messageID: ctx.messageID,
|
||||
callID: ctx.callID,
|
||||
title: "Edit this file: " + filePath,
|
||||
metadata: {
|
||||
filePath,
|
||||
diff,
|
||||
},
|
||||
})
|
||||
}
|
||||
await Bun.write(filePath, params.newString)
|
||||
await Bus.publish(File.Event.Edited, {
|
||||
file: filepath,
|
||||
file: filePath,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const file = Bun.file(filepath)
|
||||
const file = Bun.file(filePath)
|
||||
const stats = await file.stat().catch(() => {})
|
||||
if (!stats) throw new Error(`File ${filepath} not found`)
|
||||
if (stats.isDirectory()) throw new Error(`Path is a directory, not a file: ${filepath}`)
|
||||
await FileTime.assert(ctx.sessionID, filepath)
|
||||
if (!stats) throw new Error(`File ${filePath} not found`)
|
||||
if (stats.isDirectory()) throw new Error(`Path is a directory, not a file: ${filePath}`)
|
||||
await FileTime.assert(ctx.sessionID, filePath)
|
||||
contentOld = await file.text()
|
||||
|
||||
contentNew = replace(contentOld, params.oldString, params.newString, params.replaceAll)
|
||||
|
||||
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
|
||||
if (cfg.permission?.edit === "ask") {
|
||||
await Permission.ask({
|
||||
type: "edit",
|
||||
sessionID: ctx.sessionID,
|
||||
messageID: ctx.messageID,
|
||||
callID: ctx.callID,
|
||||
title: "Edit this file: " + filePath,
|
||||
metadata: {
|
||||
filePath,
|
||||
diff,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
await file.write(contentNew)
|
||||
await Bus.publish(File.Event.Edited, {
|
||||
file: filepath,
|
||||
file: filePath,
|
||||
})
|
||||
contentNew = await file.text()
|
||||
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
|
||||
})()
|
||||
|
||||
const diff = trimDiff(createTwoFilesPatch(filepath, filepath, contentOld, contentNew))
|
||||
|
||||
FileTime.read(ctx.sessionID, filepath)
|
||||
FileTime.read(ctx.sessionID, filePath)
|
||||
|
||||
let output = ""
|
||||
await LSP.touchFile(filepath, true)
|
||||
await LSP.touchFile(filePath, true)
|
||||
const diagnostics = await LSP.diagnostics()
|
||||
for (const [file, issues] of Object.entries(diagnostics)) {
|
||||
if (issues.length === 0) continue
|
||||
if (file === filepath) {
|
||||
if (file === filePath) {
|
||||
output += `\nThis file has errors, please fix\n<file_diagnostics>\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</file_diagnostics>\n`
|
||||
continue
|
||||
}
|
||||
@@ -96,7 +121,7 @@ export const EditTool = Tool.define("edit", {
|
||||
diagnostics,
|
||||
diff,
|
||||
},
|
||||
title: `${path.relative(app.path.root, filepath)}`,
|
||||
title: `${path.relative(app.path.root, filePath)}`,
|
||||
output,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { z } from "zod"
|
||||
import { Tool } from "./tool"
|
||||
|
||||
export const InvalidTool = Tool.define("invalid", {
|
||||
description: "Do not use",
|
||||
parameters: z.object({
|
||||
tool: z.string(),
|
||||
error: z.string(),
|
||||
}),
|
||||
async execute(params) {
|
||||
return {
|
||||
title: "Invalid Tool",
|
||||
output: `The arguments provided to the tool are invalid: ${params.error}`,
|
||||
metadata: {},
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -6,6 +6,7 @@ import { LSP } from "../lsp"
|
||||
import { FileTime } from "../file/time"
|
||||
import DESCRIPTION from "./read.txt"
|
||||
import { App } from "../app/app"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
|
||||
const DEFAULT_READ_LIMIT = 2000
|
||||
const MAX_LINE_LENGTH = 2000
|
||||
@@ -18,15 +19,19 @@ export const ReadTool = Tool.define("read", {
|
||||
limit: z.coerce.number().describe("The number of lines to read (defaults to 2000)").optional(),
|
||||
}),
|
||||
async execute(params, ctx) {
|
||||
let filePath = params.filePath
|
||||
if (!path.isAbsolute(filePath)) {
|
||||
filePath = path.join(process.cwd(), filePath)
|
||||
let filepath = params.filePath
|
||||
if (!path.isAbsolute(filepath)) {
|
||||
filepath = path.join(process.cwd(), filepath)
|
||||
}
|
||||
const app = App.info()
|
||||
if (!Filesystem.contains(app.path.cwd, filepath)) {
|
||||
throw new Error(`File ${filepath} is not in the current working directory`)
|
||||
}
|
||||
|
||||
const file = Bun.file(filePath)
|
||||
const file = Bun.file(filepath)
|
||||
if (!(await file.exists())) {
|
||||
const dir = path.dirname(filePath)
|
||||
const base = path.basename(filePath)
|
||||
const dir = path.dirname(filepath)
|
||||
const base = path.basename(filepath)
|
||||
|
||||
const dirEntries = fs.readdirSync(dir)
|
||||
const suggestions = dirEntries
|
||||
@@ -38,16 +43,18 @@ export const ReadTool = Tool.define("read", {
|
||||
.slice(0, 3)
|
||||
|
||||
if (suggestions.length > 0) {
|
||||
throw new Error(`File not found: ${filePath}\n\nDid you mean one of these?\n${suggestions.join("\n")}`)
|
||||
throw new Error(`File not found: ${filepath}\n\nDid you mean one of these?\n${suggestions.join("\n")}`)
|
||||
}
|
||||
|
||||
throw new Error(`File not found: ${filePath}`)
|
||||
throw new Error(`File not found: ${filepath}`)
|
||||
}
|
||||
|
||||
const limit = params.limit ?? DEFAULT_READ_LIMIT
|
||||
const offset = params.offset || 0
|
||||
const isImage = isImageFile(filePath)
|
||||
const isImage = isImageFile(filepath)
|
||||
if (isImage) throw new Error(`This is an image file of type: ${isImage}\nUse a different tool to process images`)
|
||||
const isBinary = await isBinaryFile(file)
|
||||
if (isBinary) throw new Error(`Cannot read binary file: ${filepath}`)
|
||||
const lines = await file.text().then((text) => text.split("\n"))
|
||||
const raw = lines.slice(offset, offset + limit).map((line) => {
|
||||
return line.length > MAX_LINE_LENGTH ? line.substring(0, MAX_LINE_LENGTH) + "..." : line
|
||||
@@ -66,11 +73,11 @@ export const ReadTool = Tool.define("read", {
|
||||
output += "\n</file>"
|
||||
|
||||
// just warms the lsp client
|
||||
LSP.touchFile(filePath, false)
|
||||
FileTime.read(ctx.sessionID, filePath)
|
||||
LSP.touchFile(filepath, false)
|
||||
FileTime.read(ctx.sessionID, filepath)
|
||||
|
||||
return {
|
||||
title: path.relative(App.info().path.root, filePath),
|
||||
title: path.relative(App.info().path.root, filepath),
|
||||
output,
|
||||
metadata: {
|
||||
preview,
|
||||
@@ -99,3 +106,14 @@ function isImageFile(filePath: string): string | false {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function isBinaryFile(file: Bun.BunFile): Promise<boolean> {
|
||||
const buffer = await file.arrayBuffer()
|
||||
const bytes = new Uint8Array(buffer.slice(0, 512)) // Check first 512 bytes
|
||||
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
if (bytes[i] === 0) return true // Null byte indicates binary
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -10,9 +10,11 @@ import { TaskTool } from "./task"
|
||||
import { TodoWriteTool, TodoReadTool } from "./todo"
|
||||
import { WebFetchTool } from "./webfetch"
|
||||
import { WriteTool } from "./write"
|
||||
import { InvalidTool } from "./invalid"
|
||||
|
||||
export namespace ToolRegistry {
|
||||
const ALL = [
|
||||
InvalidTool,
|
||||
BashTool,
|
||||
EditTool,
|
||||
WebFetchTool,
|
||||
@@ -74,7 +76,8 @@ export namespace ToolRegistry {
|
||||
modelID.toLowerCase().includes("qwen") ||
|
||||
modelID.includes("gpt-") ||
|
||||
modelID.includes("o1") ||
|
||||
modelID.includes("o3")
|
||||
modelID.includes("o3") ||
|
||||
modelID.includes("codex")
|
||||
) {
|
||||
return {
|
||||
patch: false,
|
||||
|
||||
@@ -20,7 +20,7 @@ export const TaskTool = Tool.define("task", async () => {
|
||||
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")
|
||||
if (msg.info.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> = {}
|
||||
@@ -38,8 +38,8 @@ export const TaskTool = Tool.define("task", async () => {
|
||||
})
|
||||
|
||||
const model = agent.model ?? {
|
||||
modelID: msg.modelID,
|
||||
providerID: msg.providerID,
|
||||
modelID: msg.info.modelID,
|
||||
providerID: msg.info.providerID,
|
||||
}
|
||||
|
||||
ctx.abort.addEventListener("abort", () => {
|
||||
@@ -50,7 +50,7 @@ export const TaskTool = Tool.define("task", async () => {
|
||||
sessionID: session.id,
|
||||
modelID: model.modelID,
|
||||
providerID: model.providerID,
|
||||
mode: msg.mode,
|
||||
mode: msg.info.mode,
|
||||
system: agent.prompt,
|
||||
tools: {
|
||||
...agent.tools,
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import Parser from "tree-sitter";
|
||||
import Bash from "tree-sitter-bash";
|
||||
|
||||
const parser = new Parser();
|
||||
parser.setLanguage(Bash.language as any);
|
||||
|
||||
const sourceCode = `cd --foo foo/bar && echo "hello" && cd ../baz`;
|
||||
|
||||
const tree = parser.parse(sourceCode);
|
||||
|
||||
// Function to extract commands and arguments
|
||||
function extractCommands(
|
||||
node: any,
|
||||
): Array<{ command: string; args: string[] }> {
|
||||
const commands: Array<{ command: string; args: string[] }> = [];
|
||||
|
||||
function traverse(node: any) {
|
||||
if (node.type === "command") {
|
||||
const commandNode = node.child(0);
|
||||
if (commandNode) {
|
||||
const command = commandNode.text;
|
||||
const args: string[] = [];
|
||||
|
||||
// Extract arguments
|
||||
for (let i = 1; i < node.childCount; i++) {
|
||||
const child = node.child(i);
|
||||
if (child && child.type === "word") {
|
||||
args.push(child.text);
|
||||
}
|
||||
}
|
||||
|
||||
commands.push({ command, args });
|
||||
}
|
||||
}
|
||||
|
||||
// Traverse children
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
traverse(node.child(i));
|
||||
}
|
||||
}
|
||||
|
||||
traverse(node);
|
||||
return commands;
|
||||
}
|
||||
|
||||
// Extract and display commands
|
||||
console.log("Source code: " + sourceCode);
|
||||
const commands = extractCommands(tree.rootNode);
|
||||
console.log("Extracted commands:");
|
||||
commands.forEach((cmd, index) => {
|
||||
console.log(`${index + 1}. Command: ${cmd.command}`);
|
||||
console.log(` Args: [${cmd.args.join(", ")}]`);
|
||||
});
|
||||
@@ -7,6 +7,7 @@ export namespace Tool {
|
||||
export type Context<M extends Metadata = Metadata> = {
|
||||
sessionID: string
|
||||
messageID: string
|
||||
callID?: string
|
||||
abort: AbortSignal
|
||||
metadata(input: { title?: string; metadata?: M }): void
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import { App } from "../app/app"
|
||||
import { Bus } from "../bus"
|
||||
import { File } from "../file"
|
||||
import { FileTime } from "../file/time"
|
||||
import { Config } from "../config/config"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
|
||||
export const WriteTool = Tool.define("write", {
|
||||
description: DESCRIPTION,
|
||||
@@ -18,21 +20,28 @@ export const WriteTool = Tool.define("write", {
|
||||
async execute(params, ctx) {
|
||||
const app = App.info()
|
||||
const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(app.path.cwd, params.filePath)
|
||||
if (!Filesystem.contains(app.path.cwd, filepath)) {
|
||||
throw new Error(`File ${filepath} is not in the current working directory`)
|
||||
}
|
||||
|
||||
const file = Bun.file(filepath)
|
||||
const exists = await file.exists()
|
||||
if (exists) await FileTime.assert(ctx.sessionID, filepath)
|
||||
|
||||
await Permission.ask({
|
||||
id: "write",
|
||||
sessionID: ctx.sessionID,
|
||||
title: exists ? "Overwrite this file: " + filepath : "Create new file: " + filepath,
|
||||
metadata: {
|
||||
filePath: filepath,
|
||||
content: params.content,
|
||||
exists,
|
||||
},
|
||||
})
|
||||
const cfg = await Config.get()
|
||||
if (cfg.permission?.edit === "ask")
|
||||
await Permission.ask({
|
||||
type: "write",
|
||||
sessionID: ctx.sessionID,
|
||||
messageID: ctx.messageID,
|
||||
callID: ctx.callID,
|
||||
title: exists ? "Overwrite this file: " + filepath : "Create new file: " + filepath,
|
||||
metadata: {
|
||||
filePath: filepath,
|
||||
content: params.content,
|
||||
exists,
|
||||
},
|
||||
})
|
||||
|
||||
await Bun.write(filepath, params.content)
|
||||
await Bus.publish(File.Event.Edited, {
|
||||
|
||||
@@ -9,7 +9,7 @@ export namespace Filesystem {
|
||||
}
|
||||
|
||||
export function contains(parent: string, child: string) {
|
||||
return relative(parent, child).startsWith("..")
|
||||
return !relative(parent, child).startsWith("..")
|
||||
}
|
||||
|
||||
export async function findUp(target: string, start: string, stop?: string) {
|
||||
|
||||
@@ -53,12 +53,10 @@ 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)
|
||||
cleanup(Global.Path.log)
|
||||
if (options.print) return
|
||||
logpath = path.join(
|
||||
dir,
|
||||
Global.Path.log,
|
||||
options.dev ? "dev.log" : new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log",
|
||||
)
|
||||
const logfile = Bun.file(logpath)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
export namespace Wildcard {
|
||||
export function match(str: string, pattern: string) {
|
||||
const regex = new RegExp(
|
||||
"^" +
|
||||
pattern
|
||||
.replace(/[.+^${}()|[\]\\]/g, "\\$&") // escape special regex chars
|
||||
.replace(/\*/g, ".*") // * becomes .*
|
||||
.replace(/\?/g, ".") + // ? becomes .
|
||||
"$",
|
||||
"s", // s flag enables multiline matching
|
||||
)
|
||||
return regex.test(str)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { App } from "../../src/app/app"
|
||||
import path from "path"
|
||||
import { BashTool } from "../../src/tool/bash"
|
||||
import { Log } from "../../src/util/log"
|
||||
|
||||
const ctx = {
|
||||
sessionID: "test",
|
||||
messageID: "",
|
||||
toolCallID: "",
|
||||
abort: AbortSignal.any([]),
|
||||
metadata: () => {},
|
||||
}
|
||||
|
||||
const bash = await BashTool.init()
|
||||
const projectRoot = path.join(__dirname, "../..")
|
||||
Log.init({ print: false })
|
||||
|
||||
describe("tool.bash", () => {
|
||||
test("basic", async () => {
|
||||
await App.provide({ cwd: projectRoot }, async () => {
|
||||
const result = await bash.execute(
|
||||
{
|
||||
command: "echo 'test'",
|
||||
description: "Echo test message",
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
expect(result.metadata.exit).toBe(0)
|
||||
expect(result.metadata.stdout).toContain("test")
|
||||
})
|
||||
})
|
||||
|
||||
test("cd ../ should fail outside of project root", async () => {
|
||||
await App.provide({ cwd: projectRoot }, async () => {
|
||||
await expect(
|
||||
bash.execute(
|
||||
{
|
||||
command: "cd ../",
|
||||
description: "Try to cd to parent directory",
|
||||
},
|
||||
ctx,
|
||||
),
|
||||
).rejects.toThrow("This command references paths outside of")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -7,6 +7,7 @@ import path from "path"
|
||||
const ctx = {
|
||||
sessionID: "test",
|
||||
messageID: "",
|
||||
toolCallID: "",
|
||||
abort: AbortSignal.any([]),
|
||||
metadata: () => {},
|
||||
}
|
||||
@@ -21,8 +22,8 @@ describe("tool.glob", () => {
|
||||
await App.provide({ cwd: projectRoot }, async () => {
|
||||
let result = await glob.execute(
|
||||
{
|
||||
pattern: "../../node_modules/**/*",
|
||||
path: undefined,
|
||||
pattern: "**/*",
|
||||
path: "../../node_modules",
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
@@ -40,7 +41,7 @@ describe("tool.glob", () => {
|
||||
)
|
||||
expect(result.metadata).toMatchObject({
|
||||
truncated: false,
|
||||
count: 3,
|
||||
count: 2,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@tsconfig/bun/tsconfig.json",
|
||||
"compilerOptions": {}
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"ESNext",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"customConditions": [
|
||||
"development"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
dist
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "0.3.130",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"development": "./src/index.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "catalog:",
|
||||
"@hey-api/openapi-ts": "0.80.1",
|
||||
"@tsconfig/node22": "catalog:"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
const dir = new URL("..", import.meta.url).pathname
|
||||
process.chdir(dir)
|
||||
|
||||
import { $ } from "bun"
|
||||
|
||||
const snapshot = process.env["OPENCODE_SNAPSHOT"] === "true"
|
||||
|
||||
await $`bun tsc`
|
||||
|
||||
if (snapshot) {
|
||||
await $`bun publish --tag snapshot --access public`
|
||||
await $`git checkout package.json`
|
||||
}
|
||||
if (!snapshot) {
|
||||
await $`bun publish --access public`
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Plugin } from "./index"
|
||||
|
||||
export const ExamplePlugin: Plugin = async ({ app, client, $ }) => {
|
||||
return {
|
||||
permission: {},
|
||||
async "chat.params"(input, output) {
|
||||
output.topP = 1
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { Event, createOpencodeClient, App, Model, Provider, Permission, UserMessage, Part } from "@opencode-ai/sdk"
|
||||
import type { BunShell } from "./shell"
|
||||
|
||||
export type PluginInput = {
|
||||
client: ReturnType<typeof createOpencodeClient>
|
||||
app: App
|
||||
$: BunShell
|
||||
}
|
||||
export type Plugin = (input: PluginInput) => Promise<Hooks>
|
||||
|
||||
export interface Hooks {
|
||||
event?: (input: { event: Event }) => Promise<void>
|
||||
/**
|
||||
* Called when a new message is received
|
||||
*/
|
||||
"chat.message"?: (input: {}, output: { message: UserMessage; parts: Part[] }) => Promise<void>
|
||||
/**
|
||||
* Modify parameters sent to LLM
|
||||
*/
|
||||
"chat.params"?: (
|
||||
input: { model: Model; provider: Provider; message: UserMessage },
|
||||
output: { temperature: number; topP: number },
|
||||
) => Promise<void>
|
||||
"permission.ask"?: (input: Permission, output: { status: "ask" | "deny" | "allow" }) => Promise<void>
|
||||
"tool.execute.before"?: (
|
||||
input: { tool: string; sessionID: string; callID: string },
|
||||
output: { args: any },
|
||||
) => Promise<void>
|
||||
"tool.execute.after"?: (
|
||||
input: { tool: string; sessionID: string; callID: string },
|
||||
output: {
|
||||
title: string
|
||||
output: string
|
||||
metadata: any
|
||||
},
|
||||
) => Promise<void>
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
export type ShellFunction = (input: Uint8Array) => Uint8Array
|
||||
|
||||
export type ShellExpression =
|
||||
| { toString(): string }
|
||||
| Array<ShellExpression>
|
||||
| string
|
||||
| { raw: string }
|
||||
| ReadableStream
|
||||
|
||||
export interface BunShell {
|
||||
(strings: TemplateStringsArray, ...expressions: ShellExpression[]): BunShellPromise
|
||||
|
||||
/**
|
||||
* Perform bash-like brace expansion on the given pattern.
|
||||
* @param pattern - Brace pattern to expand
|
||||
*/
|
||||
braces(pattern: string): string[]
|
||||
|
||||
/**
|
||||
* Escape strings for input into shell commands.
|
||||
*/
|
||||
escape(input: string): string
|
||||
|
||||
/**
|
||||
* Change the default environment variables for shells created by this instance.
|
||||
*/
|
||||
env(newEnv?: Record<string, string | undefined>): BunShell
|
||||
|
||||
/**
|
||||
* Default working directory to use for shells created by this instance.
|
||||
*/
|
||||
cwd(newCwd?: string): BunShell
|
||||
|
||||
/**
|
||||
* Configure the shell to not throw an exception on non-zero exit codes.
|
||||
*/
|
||||
nothrow(): BunShell
|
||||
|
||||
/**
|
||||
* Configure whether or not the shell should throw an exception on non-zero exit codes.
|
||||
*/
|
||||
throws(shouldThrow: boolean): BunShell
|
||||
}
|
||||
|
||||
export interface BunShellPromise extends Promise<BunShellOutput> {
|
||||
readonly stdin: WritableStream
|
||||
|
||||
/**
|
||||
* Change the current working directory of the shell.
|
||||
*/
|
||||
cwd(newCwd: string): this
|
||||
|
||||
/**
|
||||
* Set environment variables for the shell.
|
||||
*/
|
||||
env(newEnv: Record<string, string> | undefined): this
|
||||
|
||||
/**
|
||||
* By default, the shell will write to the current process's stdout and stderr, as well as buffering that output.
|
||||
* This configures the shell to only buffer the output.
|
||||
*/
|
||||
quiet(): this
|
||||
|
||||
/**
|
||||
* Read from stdout as a string, line by line
|
||||
* Automatically calls quiet() to disable echoing to stdout.
|
||||
*/
|
||||
lines(): AsyncIterable<string>
|
||||
|
||||
/**
|
||||
* Read from stdout as a string.
|
||||
* Automatically calls quiet() to disable echoing to stdout.
|
||||
*/
|
||||
text(encoding?: BufferEncoding): Promise<string>
|
||||
|
||||
/**
|
||||
* Read from stdout as a JSON object
|
||||
* Automatically calls quiet()
|
||||
*/
|
||||
json(): Promise<any>
|
||||
|
||||
/**
|
||||
* Read from stdout as an ArrayBuffer
|
||||
* Automatically calls quiet()
|
||||
*/
|
||||
arrayBuffer(): Promise<ArrayBuffer>
|
||||
|
||||
/**
|
||||
* Read from stdout as a Blob
|
||||
* Automatically calls quiet()
|
||||
*/
|
||||
blob(): Promise<Blob>
|
||||
|
||||
/**
|
||||
* Configure the shell to not throw an exception on non-zero exit codes.
|
||||
*/
|
||||
nothrow(): this
|
||||
|
||||
/**
|
||||
* Configure whether or not the shell should throw an exception on non-zero exit codes.
|
||||
*/
|
||||
throws(shouldThrow: boolean): this
|
||||
}
|
||||
|
||||
export interface BunShellOutput {
|
||||
readonly stdout: Buffer
|
||||
readonly stderr: Buffer
|
||||
readonly exitCode: number
|
||||
|
||||
/**
|
||||
* Read from stdout as a string
|
||||
*/
|
||||
text(encoding?: BufferEncoding): string
|
||||
|
||||
/**
|
||||
* Read from stdout as a JSON object
|
||||
*/
|
||||
json(): any
|
||||
|
||||
/**
|
||||
* Read from stdout as an ArrayBuffer
|
||||
*/
|
||||
arrayBuffer(): ArrayBuffer
|
||||
|
||||
/**
|
||||
* Read from stdout as an Uint8Array
|
||||
*/
|
||||
bytes(): Uint8Array
|
||||
|
||||
/**
|
||||
* Read from stdout as a Blob
|
||||
*/
|
||||
blob(): Blob
|
||||
}
|
||||
|
||||
export type BunShellError = Error & BunShellOutput
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig.json",
|
||||
"extends": "@tsconfig/node22/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"module": "preserve",
|
||||
"declaration": true,
|
||||
"moduleResolution": "bundler",
|
||||
"customConditions": [
|
||||
"development"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
// 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
@@ -1,88 +0,0 @@
|
||||
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
@@ -1,32 +0,0 @@
|
||||
# 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 }}
|
||||
@@ -1,21 +0,0 @@
|
||||
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 }}
|
||||
@@ -1,7 +0,0 @@
|
||||
CHANGELOG.md
|
||||
/ecosystem-tests/*/**
|
||||
/node_modules
|
||||
/deno
|
||||
|
||||
# don't format tsc output, will break source maps
|
||||
/dist
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"arrowParens": "always",
|
||||
"experimentalTernaries": true,
|
||||
"printWidth": 110,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
".": "0.1.0-alpha.20"
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
configured_endpoints: 26
|
||||
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-62d8fccba4eb8dc3a80434e0849eab3352e49fb96a718bb7b6d17ed8e582b716.yml
|
||||
openapi_spec_hash: 4ff9376cf9634e91731e63fe482ea532
|
||||
config_hash: 1ae82c93499b9f0b9ba828b8919f9cb3
|
||||
@@ -1 +0,0 @@
|
||||
brew "node"
|
||||
@@ -1,196 +0,0 @@
|
||||
# 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))
|
||||
@@ -1,107 +0,0 @@
|
||||
## 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.
|
||||
@@ -1,370 +0,0 @@
|
||||
# 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).
|
||||
@@ -1,139 +0,0 @@
|
||||
# Shared
|
||||
|
||||
Types:
|
||||
|
||||
- <code><a href="./src/resources/shared.ts">MessageAbortedError</a></code>
|
||||
- <code><a href="./src/resources/shared.ts">ProviderAuthError</a></code>
|
||||
- <code><a href="./src/resources/shared.ts">UnknownError</a></code>
|
||||
|
||||
# Event
|
||||
|
||||
Types:
|
||||
|
||||
- <code><a href="./src/resources/event.ts">EventListResponse</a></code>
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="get /event">client.event.<a href="./src/resources/event.ts">list</a>() -> EventListResponse</code>
|
||||
|
||||
# App
|
||||
|
||||
Types:
|
||||
|
||||
- <code><a href="./src/resources/app.ts">App</a></code>
|
||||
- <code><a href="./src/resources/app.ts">Mode</a></code>
|
||||
- <code><a href="./src/resources/app.ts">Model</a></code>
|
||||
- <code><a href="./src/resources/app.ts">Provider</a></code>
|
||||
- <code><a href="./src/resources/app.ts">AppInitResponse</a></code>
|
||||
- <code><a href="./src/resources/app.ts">AppLogResponse</a></code>
|
||||
- <code><a href="./src/resources/app.ts">AppModesResponse</a></code>
|
||||
- <code><a href="./src/resources/app.ts">AppProvidersResponse</a></code>
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="get /app">client.app.<a href="./src/resources/app.ts">get</a>() -> App</code>
|
||||
- <code title="post /app/init">client.app.<a href="./src/resources/app.ts">init</a>() -> AppInitResponse</code>
|
||||
- <code title="post /log">client.app.<a href="./src/resources/app.ts">log</a>({ ...params }) -> AppLogResponse</code>
|
||||
- <code title="get /mode">client.app.<a href="./src/resources/app.ts">modes</a>() -> AppModesResponse</code>
|
||||
- <code title="get /config/providers">client.app.<a href="./src/resources/app.ts">providers</a>() -> AppProvidersResponse</code>
|
||||
|
||||
# Find
|
||||
|
||||
Types:
|
||||
|
||||
- <code><a href="./src/resources/find.ts">Symbol</a></code>
|
||||
- <code><a href="./src/resources/find.ts">FindFilesResponse</a></code>
|
||||
- <code><a href="./src/resources/find.ts">FindSymbolsResponse</a></code>
|
||||
- <code><a href="./src/resources/find.ts">FindTextResponse</a></code>
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="get /find/file">client.find.<a href="./src/resources/find.ts">files</a>({ ...params }) -> FindFilesResponse</code>
|
||||
- <code title="get /find/symbol">client.find.<a href="./src/resources/find.ts">symbols</a>({ ...params }) -> FindSymbolsResponse</code>
|
||||
- <code title="get /find">client.find.<a href="./src/resources/find.ts">text</a>({ ...params }) -> FindTextResponse</code>
|
||||
|
||||
# File
|
||||
|
||||
Types:
|
||||
|
||||
- <code><a href="./src/resources/file.ts">File</a></code>
|
||||
- <code><a href="./src/resources/file.ts">FileReadResponse</a></code>
|
||||
- <code><a href="./src/resources/file.ts">FileStatusResponse</a></code>
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="get /file">client.file.<a href="./src/resources/file.ts">read</a>({ ...params }) -> FileReadResponse</code>
|
||||
- <code title="get /file/status">client.file.<a href="./src/resources/file.ts">status</a>() -> FileStatusResponse</code>
|
||||
|
||||
# Config
|
||||
|
||||
Types:
|
||||
|
||||
- <code><a href="./src/resources/config.ts">Config</a></code>
|
||||
- <code><a href="./src/resources/config.ts">KeybindsConfig</a></code>
|
||||
- <code><a href="./src/resources/config.ts">McpLocalConfig</a></code>
|
||||
- <code><a href="./src/resources/config.ts">McpRemoteConfig</a></code>
|
||||
- <code><a href="./src/resources/config.ts">ModeConfig</a></code>
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="get /config">client.config.<a href="./src/resources/config.ts">get</a>() -> Config</code>
|
||||
|
||||
# Session
|
||||
|
||||
Types:
|
||||
|
||||
- <code><a href="./src/resources/session.ts">AssistantMessage</a></code>
|
||||
- <code><a href="./src/resources/session.ts">FilePart</a></code>
|
||||
- <code><a href="./src/resources/session.ts">FilePartInput</a></code>
|
||||
- <code><a href="./src/resources/session.ts">FilePartSource</a></code>
|
||||
- <code><a href="./src/resources/session.ts">FilePartSourceText</a></code>
|
||||
- <code><a href="./src/resources/session.ts">FileSource</a></code>
|
||||
- <code><a href="./src/resources/session.ts">Message</a></code>
|
||||
- <code><a href="./src/resources/session.ts">Part</a></code>
|
||||
- <code><a href="./src/resources/session.ts">Session</a></code>
|
||||
- <code><a href="./src/resources/session.ts">SnapshotPart</a></code>
|
||||
- <code><a href="./src/resources/session.ts">StepFinishPart</a></code>
|
||||
- <code><a href="./src/resources/session.ts">StepStartPart</a></code>
|
||||
- <code><a href="./src/resources/session.ts">SymbolSource</a></code>
|
||||
- <code><a href="./src/resources/session.ts">TextPart</a></code>
|
||||
- <code><a href="./src/resources/session.ts">TextPartInput</a></code>
|
||||
- <code><a href="./src/resources/session.ts">ToolPart</a></code>
|
||||
- <code><a href="./src/resources/session.ts">ToolStateCompleted</a></code>
|
||||
- <code><a href="./src/resources/session.ts">ToolStateError</a></code>
|
||||
- <code><a href="./src/resources/session.ts">ToolStatePending</a></code>
|
||||
- <code><a href="./src/resources/session.ts">ToolStateRunning</a></code>
|
||||
- <code><a href="./src/resources/session.ts">UserMessage</a></code>
|
||||
- <code><a href="./src/resources/session.ts">SessionListResponse</a></code>
|
||||
- <code><a href="./src/resources/session.ts">SessionDeleteResponse</a></code>
|
||||
- <code><a href="./src/resources/session.ts">SessionAbortResponse</a></code>
|
||||
- <code><a href="./src/resources/session.ts">SessionInitResponse</a></code>
|
||||
- <code><a href="./src/resources/session.ts">SessionMessagesResponse</a></code>
|
||||
- <code><a href="./src/resources/session.ts">SessionSummarizeResponse</a></code>
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /session">client.session.<a href="./src/resources/session.ts">create</a>() -> Session</code>
|
||||
- <code title="get /session">client.session.<a href="./src/resources/session.ts">list</a>() -> SessionListResponse</code>
|
||||
- <code title="delete /session/{id}">client.session.<a href="./src/resources/session.ts">delete</a>(id) -> SessionDeleteResponse</code>
|
||||
- <code title="post /session/{id}/abort">client.session.<a href="./src/resources/session.ts">abort</a>(id) -> SessionAbortResponse</code>
|
||||
- <code title="post /session/{id}/message">client.session.<a href="./src/resources/session.ts">chat</a>(id, { ...params }) -> AssistantMessage</code>
|
||||
- <code title="post /session/{id}/init">client.session.<a href="./src/resources/session.ts">init</a>(id, { ...params }) -> SessionInitResponse</code>
|
||||
- <code title="get /session/{id}/message">client.session.<a href="./src/resources/session.ts">messages</a>(id) -> SessionMessagesResponse</code>
|
||||
- <code title="post /session/{id}/revert">client.session.<a href="./src/resources/session.ts">revert</a>(id, { ...params }) -> Session</code>
|
||||
- <code title="post /session/{id}/share">client.session.<a href="./src/resources/session.ts">share</a>(id) -> Session</code>
|
||||
- <code title="post /session/{id}/summarize">client.session.<a href="./src/resources/session.ts">summarize</a>(id, { ...params }) -> SessionSummarizeResponse</code>
|
||||
- <code title="post /session/{id}/unrevert">client.session.<a href="./src/resources/session.ts">unrevert</a>(id) -> Session</code>
|
||||
- <code title="delete /session/{id}/share">client.session.<a href="./src/resources/session.ts">unshare</a>(id) -> Session</code>
|
||||
|
||||
# Tui
|
||||
|
||||
Types:
|
||||
|
||||
- <code><a href="./src/resources/tui.ts">TuiAppendPromptResponse</a></code>
|
||||
- <code><a href="./src/resources/tui.ts">TuiOpenHelpResponse</a></code>
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /tui/append-prompt">client.tui.<a href="./src/resources/tui.ts">appendPrompt</a>({ ...params }) -> TuiAppendPromptResponse</code>
|
||||
- <code title="post /tui/open-help">client.tui.<a href="./src/resources/tui.ts">openHelp</a>() -> TuiOpenHelpResponse</code>
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/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!"
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
#!/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"
|
||||
@@ -1,42 +0,0 @@
|
||||
// @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',
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -1,4 +0,0 @@
|
||||
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.
|
||||
@@ -0,0 +1,4 @@
|
||||
configured_endpoints: 34
|
||||
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-52fd0b61e84fdc1cdd31ec12e1600510e9dd2f9d4fb20c2315b4975cb763ee98.yml
|
||||
openapi_spec_hash: e851b8d5a2412f5fc9be82ab88ebdfde
|
||||
config_hash: 11a6f0803eb407367c3f677d3e524c37
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user