mirror of
https://fastgit.cc/https://github.com/anomalyco/opencode
synced 2026-05-04 07:40:39 +08:00
Compare commits
110 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
8
.github/workflows/opencode.yml
vendored
8
.github/workflows/opencode.yml
vendored
@@ -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
|
||||
|
||||
26
.github/workflows/publish.yml
vendored
26
.github/workflows/publish.yml
vendored
@@ -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,7 @@ jobs:
|
||||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.2.17
|
||||
bun-version: 1.2.19
|
||||
|
||||
- name: Install makepkg
|
||||
run: |
|
||||
@@ -53,12 +56,7 @@ jobs:
|
||||
- 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 }}
|
||||
|
||||
2
.github/workflows/stats.yml
vendored
2
.github/workflows/stats.yml
vendored
@@ -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: |
|
||||
|
||||
67
STATS.md
67
STATS.md
@@ -1,33 +1,38 @@
|
||||
# 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) |
|
||||
|
||||
@@ -1,24 +1,6 @@
|
||||
{
|
||||
"$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": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"plugin": ["./packages/plugin/src/example.ts"],
|
||||
"mcp": {
|
||||
"context7": {
|
||||
"type": "remote",
|
||||
|
||||
11
package.json
11
package.json
@@ -8,18 +8,21 @@
|
||||
"dev": "bun run 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": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/function",
|
||||
"version": "0.0.1",
|
||||
"version": "0.3.113",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "0.0.5",
|
||||
"version": "0.3.113",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
@@ -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:") &&
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -12,6 +12,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 +23,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 +37,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 +66,32 @@ 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 })
|
||||
}
|
||||
|
||||
// Handle migration from autoshare to share field
|
||||
if (result.autoshare === true && !result.share) {
|
||||
result.share = "auto"
|
||||
@@ -117,6 +144,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 +215,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 +280,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 +351,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 +376,90 @@ 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
|
||||
}
|
||||
const filePath = match.replace(/^\{file:/, "").replace(/\}$/, "")
|
||||
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]
|
||||
if (typeof plugin === "string") {
|
||||
data.plugin[i] = path.resolve(path.dirname(filepath), plugin)
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
@@ -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,34 @@ 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,
|
||||
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 +104,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`)
|
||||
}
|
||||
|
||||
85
packages/opencode/src/plugin/index.ts
Normal file
85
packages/opencode/src/plugin/index.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
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 { pathOr } from "remeda"
|
||||
|
||||
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 (const plugin of config.plugin ?? []) {
|
||||
log.info("loading plugin", { path: plugin })
|
||||
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,
|
||||
}
|
||||
})
|
||||
|
||||
type Path<T, Prefix extends string = ""> = T extends object
|
||||
? {
|
||||
[K in keyof T]: K extends string
|
||||
? T[K] extends Function | undefined
|
||||
? `${Prefix}${K}`
|
||||
: Path<T[K], `${Prefix}${K}.`>
|
||||
: never
|
||||
}[keyof T]
|
||||
: never
|
||||
|
||||
export type FunctionFromKey<T, P extends Path<T>> = P extends `${infer K}.${infer R}`
|
||||
? K extends keyof T
|
||||
? R extends Path<T[K]>
|
||||
? FunctionFromKey<T[K], R>
|
||||
: never
|
||||
: never
|
||||
: P extends keyof T
|
||||
? T[P]
|
||||
: never
|
||||
|
||||
export async function trigger<
|
||||
Name extends Path<Required<Hooks>>,
|
||||
Input = Parameters<FunctionFromKey<Required<Hooks>, Name>>[0],
|
||||
Output = Parameters<FunctionFromKey<Required<Hooks>, Name>>[1],
|
||||
>(fn: Name, input: Input, output: Output): Promise<Output> {
|
||||
if (!fn) return output
|
||||
const path = fn.split(".")
|
||||
for (const hook of await state().then((x) => x.hooks)) {
|
||||
// @ts-expect-error
|
||||
const fn = pathOr(hook, path, undefined)
|
||||
if (!fn) continue
|
||||
// @ts-expect-error
|
||||
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).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" })
|
||||
@@ -256,7 +257,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 +294,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 +381,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 +572,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 +605,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
|
||||
@@ -705,11 +724,23 @@ export namespace Session {
|
||||
description: item.description,
|
||||
inputSchema: item.parameters as ZodSchema,
|
||||
async execute(args, options) {
|
||||
await Plugin.trigger(
|
||||
"tool.execute.before",
|
||||
{
|
||||
tool: item.id,
|
||||
sessionID: input.sessionID,
|
||||
callID: options.toolCallId,
|
||||
},
|
||||
{
|
||||
args,
|
||||
},
|
||||
)
|
||||
await processor.track(options.toolCallId)
|
||||
const result = await item.execute(args, {
|
||||
sessionID: input.sessionID,
|
||||
abort: abort.signal,
|
||||
messageID: assistantMsg.id,
|
||||
callID: options.toolCallId,
|
||||
metadata: async (val) => {
|
||||
const match = processor.partFromToolCall(options.toolCallId)
|
||||
if (match && match.state.status === "running") {
|
||||
@@ -728,6 +759,15 @@ export namespace Session {
|
||||
}
|
||||
},
|
||||
})
|
||||
await Plugin.trigger(
|
||||
"tool.execute.after",
|
||||
{
|
||||
tool: item.id,
|
||||
sessionID: input.sessionID,
|
||||
callID: options.toolCallId,
|
||||
},
|
||||
result,
|
||||
)
|
||||
return result
|
||||
},
|
||||
toModelOutput(result) {
|
||||
@@ -764,8 +804,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 +871,15 @@ export namespace Session {
|
||||
messages,
|
||||
}
|
||||
},
|
||||
maxRetries: 10,
|
||||
maxRetries: 3,
|
||||
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 +889,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,
|
||||
@@ -1030,7 +1088,7 @@ export namespace Session {
|
||||
}
|
||||
break
|
||||
|
||||
case "text":
|
||||
case "text-delta":
|
||||
if (currentText) {
|
||||
currentText.text += value.text
|
||||
if (currentText.text) await updatePart(currentText)
|
||||
@@ -1099,7 +1157,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: {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -2,11 +2,28 @@ import { z } from "zod"
|
||||
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,10 +37,84 @@ 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
|
||||
})()
|
||||
|
||||
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}`.text().then((x) => x.trim())
|
||||
log.info("resolved path", { arg, resolved })
|
||||
if (!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 = Bun.spawn({
|
||||
cmd: ["bash", "-c", params.command],
|
||||
cwd: App.info().path.cwd,
|
||||
cwd: app.path.cwd,
|
||||
maxBuffer: MAX_OUTPUT_LENGTH,
|
||||
signal: ctx.abort,
|
||||
timeout: timeout,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -74,7 +74,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,
|
||||
|
||||
53
packages/opencode/src/tool/test.ts
Normal file
53
packages/opencode/src/tool/test.ts
Normal file
@@ -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)
|
||||
|
||||
14
packages/opencode/src/util/wildcard.ts
Normal file
14
packages/opencode/src/util/wildcard.ts
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
47
packages/opencode/test/tool/bash.test.ts
Normal file
47
packages/opencode/test/tool/bash.test.ts
Normal file
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
1
packages/plugin/.gitignore
vendored
Normal file
1
packages/plugin/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
dist
|
||||
21
packages/plugin/package.json
Normal file
21
packages/plugin/package.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "0.3.113",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"devDependencies": {
|
||||
"typescript": "catalog:",
|
||||
"@hey-api/openapi-ts": "0.80.1",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*"
|
||||
}
|
||||
}
|
||||
18
packages/plugin/script/publish.ts
Normal file
18
packages/plugin/script/publish.ts
Normal file
@@ -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`
|
||||
}
|
||||
7
packages/plugin/src/example.ts
Normal file
7
packages/plugin/src/example.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Plugin } from "./index"
|
||||
|
||||
export const ExamplePlugin: Plugin = async ({ app, client }) => {
|
||||
return {
|
||||
permission: {},
|
||||
}
|
||||
}
|
||||
56
packages/plugin/src/index.ts
Normal file
56
packages/plugin/src/index.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { Event, createOpencodeClient, App, Model, Provider, Permission, UserMessage, Part } from "@opencode-ai/sdk"
|
||||
import { $ } from "bun"
|
||||
|
||||
export type PluginInput = {
|
||||
client: ReturnType<typeof createOpencodeClient>
|
||||
app: App
|
||||
$: $
|
||||
}
|
||||
export type Plugin = (input: PluginInput) => Promise<Hooks>
|
||||
|
||||
export interface Hooks {
|
||||
event?: (input: { event: Event }) => Promise<void>
|
||||
chat?: {
|
||||
/**
|
||||
* Called when a new message is received
|
||||
*/
|
||||
message?: (input: {}, output: { message: UserMessage; parts: Part[] }) => Promise<void>
|
||||
/**
|
||||
* Modify parameters sent to LLM
|
||||
*/
|
||||
params?: (
|
||||
input: { model: Model; provider: Provider; message: UserMessage },
|
||||
output: { temperature: number; topP: number },
|
||||
) => Promise<void>
|
||||
}
|
||||
permission?: {
|
||||
/**
|
||||
* Called when a permission is asked
|
||||
*/
|
||||
ask?: (input: Permission, output: { status: "ask" | "deny" | "allow" }) => Promise<void>
|
||||
}
|
||||
tool?: {
|
||||
execute?: {
|
||||
/**
|
||||
* Called before a tool is executed
|
||||
*/
|
||||
before?: (
|
||||
input: { tool: string; sessionID: string; callID: string },
|
||||
output: {
|
||||
args: any
|
||||
},
|
||||
) => Promise<void>
|
||||
/**
|
||||
* Called after a tool is executed
|
||||
*/
|
||||
after?: (
|
||||
input: { tool: string; sessionID: string; callID: string },
|
||||
output: {
|
||||
title: string
|
||||
output: string
|
||||
metadata: any
|
||||
},
|
||||
) => Promise<void>
|
||||
}
|
||||
}
|
||||
}
|
||||
16
packages/plugin/tsconfig.json
Normal file
16
packages/plugin/tsconfig.json
Normal file
@@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
);
|
||||
7
packages/sdk/go/.devcontainer/devcontainer.json
Normal file
7
packages/sdk/go/.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,7 @@
|
||||
// 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/go:1.23-bookworm",
|
||||
"postCreateCommand": "go mod tidy"
|
||||
}
|
||||
49
packages/sdk/go/.github/workflows/ci.yml
vendored
Normal file
49
packages/sdk/go/.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
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-go' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
|
||||
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: ./go.mod
|
||||
|
||||
- name: Run lints
|
||||
run: ./scripts/lint
|
||||
test:
|
||||
timeout-minutes: 10
|
||||
name: test
|
||||
runs-on: ${{ github.repository == 'stainless-sdks/opencode-go' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
|
||||
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: ./go.mod
|
||||
|
||||
- name: Bootstrap
|
||||
run: ./scripts/bootstrap
|
||||
|
||||
- name: Run tests
|
||||
run: ./scripts/test
|
||||
4
packages/sdk/go/.gitignore
vendored
Normal file
4
packages/sdk/go/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
.prism.log
|
||||
codegen.log
|
||||
Brewfile.lock.json
|
||||
.idea/
|
||||
3
packages/sdk/go/.release-please-manifest.json
Normal file
3
packages/sdk/go/.release-please-manifest.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
".": "0.1.0-alpha.8"
|
||||
}
|
||||
4
packages/sdk/go/.stats.yml
Normal file
4
packages/sdk/go/.stats.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
configured_endpoints: 34
|
||||
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-2ebd9d5478864042a2e01b4995f42acbc39069fa7fcccd1c2e567366ee6c243d.yml
|
||||
openapi_spec_hash: 2a34451b288ea30af1cb61332c417c2a
|
||||
config_hash: 11a6f0803eb407367c3f677d3e524c37
|
||||
1
packages/sdk/go/Brewfile
Normal file
1
packages/sdk/go/Brewfile
Normal file
@@ -0,0 +1 @@
|
||||
brew "go"
|
||||
73
packages/sdk/go/CHANGELOG.md
Normal file
73
packages/sdk/go/CHANGELOG.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# Changelog
|
||||
|
||||
## 0.1.0-alpha.8 (2025-07-02)
|
||||
|
||||
Full Changelog: [v0.1.0-alpha.7...v0.1.0-alpha.8](https://github.com/sst/opencode-sdk-go/compare/v0.1.0-alpha.7...v0.1.0-alpha.8)
|
||||
|
||||
### Features
|
||||
|
||||
* **api:** update via SDK Studio ([651e937](https://github.com/sst/opencode-sdk-go/commit/651e937c334e1caba3b968e6cac865c219879519))
|
||||
|
||||
## 0.1.0-alpha.7 (2025-06-30)
|
||||
|
||||
Full Changelog: [v0.1.0-alpha.6...v0.1.0-alpha.7](https://github.com/sst/opencode-sdk-go/compare/v0.1.0-alpha.6...v0.1.0-alpha.7)
|
||||
|
||||
### Features
|
||||
|
||||
* **api:** update via SDK Studio ([13550a5](https://github.com/sst/opencode-sdk-go/commit/13550a5c65d77325e945ed99fe0799cd1107b775))
|
||||
* **api:** update via SDK Studio ([7b73730](https://github.com/sst/opencode-sdk-go/commit/7b73730c7fa62ba966dda3541c3e97b49be8d2bf))
|
||||
|
||||
|
||||
### Chores
|
||||
|
||||
* **ci:** only run for pushes and fork pull requests ([bea59b8](https://github.com/sst/opencode-sdk-go/commit/bea59b886800ef555f89c47a9256d6392ed2e53d))
|
||||
|
||||
## 0.1.0-alpha.6 (2025-06-28)
|
||||
|
||||
Full Changelog: [v0.1.0-alpha.5...v0.1.0-alpha.6](https://github.com/sst/opencode-sdk-go/compare/v0.1.0-alpha.5...v0.1.0-alpha.6)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* don't try to deserialize as json when ResponseBodyInto is []byte ([5988d04](https://github.com/sst/opencode-sdk-go/commit/5988d04839cb78b6613057280b91b72a60fef33d))
|
||||
|
||||
## 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-go/compare/v0.1.0-alpha.4...v0.1.0-alpha.5)
|
||||
|
||||
### Features
|
||||
|
||||
* **api:** update via SDK Studio ([9e39a59](https://github.com/sst/opencode-sdk-go/commit/9e39a59b3d5d1bd5e64633732521fb28362cc70e))
|
||||
|
||||
## 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-go/compare/v0.1.0-alpha.3...v0.1.0-alpha.4)
|
||||
|
||||
### Features
|
||||
|
||||
* **api:** update via SDK Studio ([9609d1b](https://github.com/sst/opencode-sdk-go/commit/9609d1b1db7806d00cb846c9914cb4935cdedf52))
|
||||
|
||||
## 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-go/compare/v0.1.0-alpha.2...v0.1.0-alpha.3)
|
||||
|
||||
### Features
|
||||
|
||||
* **api:** update via SDK Studio ([57f3230](https://github.com/sst/opencode-sdk-go/commit/57f32309023cc1f0f20c20d02a3907e390a71f61))
|
||||
|
||||
## 0.1.0-alpha.2 (2025-06-27)
|
||||
|
||||
Full Changelog: [v0.1.0-alpha.1...v0.1.0-alpha.2](https://github.com/sst/opencode-sdk-go/compare/v0.1.0-alpha.1...v0.1.0-alpha.2)
|
||||
|
||||
### Features
|
||||
|
||||
* **api:** update via SDK Studio ([a766f1c](https://github.com/sst/opencode-sdk-go/commit/a766f1c54f02bbc1380151b0e22d97cc2c5892e6))
|
||||
|
||||
## 0.1.0-alpha.1 (2025-06-27)
|
||||
|
||||
Full Changelog: [v0.0.1-alpha.0...v0.1.0-alpha.1](https://github.com/sst/opencode-sdk-go/compare/v0.0.1-alpha.0...v0.1.0-alpha.1)
|
||||
|
||||
### Features
|
||||
|
||||
* **api:** update via SDK Studio ([27b7376](https://github.com/sst/opencode-sdk-go/commit/27b7376310466ee17a63f2104f546b53a2b8361a))
|
||||
* **api:** update via SDK Studio ([0a73e04](https://github.com/sst/opencode-sdk-go/commit/0a73e04c23c90b2061611edaa8fd6282dc0ce397))
|
||||
* **api:** update via SDK Studio ([9b7883a](https://github.com/sst/opencode-sdk-go/commit/9b7883a144eeac526d9d04538e0876a9d18bb844))
|
||||
66
packages/sdk/go/CONTRIBUTING.md
Normal file
66
packages/sdk/go/CONTRIBUTING.md
Normal file
@@ -0,0 +1,66 @@
|
||||
## Setting up the environment
|
||||
|
||||
To set up the repository, run:
|
||||
|
||||
```sh
|
||||
$ ./scripts/bootstrap
|
||||
$ ./scripts/build
|
||||
```
|
||||
|
||||
This will install all the required dependencies and build the SDK.
|
||||
|
||||
You can also [install go 1.18+ manually](https://go.dev/doc/install).
|
||||
|
||||
## 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 `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.
|
||||
|
||||
```go
|
||||
# add an example to examples/<your-example>/main.go
|
||||
|
||||
package main
|
||||
|
||||
func main() {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
```sh
|
||||
$ go run ./examples/<your-example>
|
||||
```
|
||||
|
||||
## Using the repository from source
|
||||
|
||||
To use a local version of this library from source in another project, edit the `go.mod` with a replace
|
||||
directive. This can be done through the CLI with the following:
|
||||
|
||||
```sh
|
||||
$ go mod edit -replace github.com/sst/opencode-sdk-go=/path/to/opencode-sdk-go
|
||||
```
|
||||
|
||||
## 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
|
||||
# you will need npm installed
|
||||
$ npx prism mock path/to/your/openapi.yml
|
||||
```
|
||||
|
||||
```sh
|
||||
$ ./scripts/test
|
||||
```
|
||||
|
||||
## Formatting
|
||||
|
||||
This library uses the standard gofmt code formatter:
|
||||
|
||||
```sh
|
||||
$ ./scripts/format
|
||||
```
|
||||
354
packages/sdk/go/README.md
Normal file
354
packages/sdk/go/README.md
Normal file
@@ -0,0 +1,354 @@
|
||||
# Opencode Go API Library
|
||||
|
||||
<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go"><img src="https://pkg.go.dev/badge/github.com/sst/opencode-sdk-go.svg" alt="Go Reference"></a>
|
||||
|
||||
The Opencode Go library provides convenient access to the [Opencode REST API](https://opencode.ai/docs)
|
||||
from applications written in Go.
|
||||
|
||||
It is generated with [Stainless](https://www.stainless.com/).
|
||||
|
||||
## Installation
|
||||
|
||||
<!-- x-release-please-start-version -->
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/sst/opencode-sdk-go" // imported as opencode
|
||||
)
|
||||
```
|
||||
|
||||
<!-- x-release-please-end -->
|
||||
|
||||
Or to pin the version:
|
||||
|
||||
<!-- x-release-please-start-version -->
|
||||
|
||||
```sh
|
||||
go get -u 'github.com/sst/opencode-sdk-go@v0.1.0-alpha.8'
|
||||
```
|
||||
|
||||
<!-- x-release-please-end -->
|
||||
|
||||
## Requirements
|
||||
|
||||
This library requires Go 1.18+.
|
||||
|
||||
## Usage
|
||||
|
||||
The full API of this library can be found in [api.md](api.md).
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/sst/opencode-sdk-go"
|
||||
)
|
||||
|
||||
func main() {
|
||||
client := opencode.NewClient()
|
||||
sessions, err := client.Session.List(context.TODO())
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
fmt.Printf("%+v\n", sessions)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Request fields
|
||||
|
||||
All request parameters are wrapped in a generic `Field` type,
|
||||
which we use to distinguish zero values from null or omitted fields.
|
||||
|
||||
This prevents accidentally sending a zero value if you forget a required parameter,
|
||||
and enables explicitly sending `null`, `false`, `''`, or `0` on optional parameters.
|
||||
Any field not specified is not sent.
|
||||
|
||||
To construct fields with values, use the helpers `String()`, `Int()`, `Float()`, or most commonly, the generic `F[T]()`.
|
||||
To send a null, use `Null[T]()`, and to send a nonconforming value, use `Raw[T](any)`. For example:
|
||||
|
||||
```go
|
||||
params := FooParams{
|
||||
Name: opencode.F("hello"),
|
||||
|
||||
// Explicitly send `"description": null`
|
||||
Description: opencode.Null[string](),
|
||||
|
||||
Point: opencode.F(opencode.Point{
|
||||
X: opencode.Int(0),
|
||||
Y: opencode.Int(1),
|
||||
|
||||
// In cases where the API specifies a given type,
|
||||
// but you want to send something else, use `Raw`:
|
||||
Z: opencode.Raw[int64](0.01), // sends a float
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
### Response objects
|
||||
|
||||
All fields in response structs are value types (not pointers or wrappers).
|
||||
|
||||
If a given field is `null`, not present, or invalid, the corresponding field
|
||||
will simply be its zero value.
|
||||
|
||||
All response structs also include a special `JSON` field, containing more detailed
|
||||
information about each property, which you can use like so:
|
||||
|
||||
```go
|
||||
if res.Name == "" {
|
||||
// true if `"name"` is either not present or explicitly null
|
||||
res.JSON.Name.IsNull()
|
||||
|
||||
// true if the `"name"` key was not present in the response JSON at all
|
||||
res.JSON.Name.IsMissing()
|
||||
|
||||
// When the API returns data that cannot be coerced to the expected type:
|
||||
if res.JSON.Name.IsInvalid() {
|
||||
raw := res.JSON.Name.Raw()
|
||||
|
||||
legacyName := struct{
|
||||
First string `json:"first"`
|
||||
Last string `json:"last"`
|
||||
}{}
|
||||
json.Unmarshal([]byte(raw), &legacyName)
|
||||
name = legacyName.First + " " + legacyName.Last
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
These `.JSON` structs also include an `Extras` map containing
|
||||
any properties in the json response that were not specified
|
||||
in the struct. This can be useful for API features not yet
|
||||
present in the SDK.
|
||||
|
||||
```go
|
||||
body := res.JSON.ExtraFields["my_unexpected_field"].Raw()
|
||||
```
|
||||
|
||||
### RequestOptions
|
||||
|
||||
This library uses the functional options pattern. Functions defined in the
|
||||
`option` package return a `RequestOption`, which is a closure that mutates a
|
||||
`RequestConfig`. These options can be supplied to the client or at individual
|
||||
requests. For example:
|
||||
|
||||
```go
|
||||
client := opencode.NewClient(
|
||||
// Adds a header to every request made by the client
|
||||
option.WithHeader("X-Some-Header", "custom_header_info"),
|
||||
)
|
||||
|
||||
client.Session.List(context.TODO(), ...,
|
||||
// Override the header
|
||||
option.WithHeader("X-Some-Header", "some_other_custom_header_info"),
|
||||
// Add an undocumented field to the request body, using sjson syntax
|
||||
option.WithJSONSet("some.json.path", map[string]string{"my": "object"}),
|
||||
)
|
||||
```
|
||||
|
||||
See the [full list of request options](https://pkg.go.dev/github.com/sst/opencode-sdk-go/option).
|
||||
|
||||
### Pagination
|
||||
|
||||
This library provides some conveniences for working with paginated list endpoints.
|
||||
|
||||
You can use `.ListAutoPaging()` methods to iterate through items across all pages:
|
||||
|
||||
Or you can use simple `.List()` methods to fetch a single page and receive a standard response object
|
||||
with additional helper methods like `.GetNextPage()`, e.g.:
|
||||
|
||||
### Errors
|
||||
|
||||
When the API returns a non-success status code, we return an error with type
|
||||
`*opencode.Error`. This contains the `StatusCode`, `*http.Request`, and
|
||||
`*http.Response` values of the request, as well as the JSON of the error body
|
||||
(much like other response objects in the SDK).
|
||||
|
||||
To handle errors, we recommend that you use the `errors.As` pattern:
|
||||
|
||||
```go
|
||||
_, err := client.Session.List(context.TODO())
|
||||
if err != nil {
|
||||
var apierr *opencode.Error
|
||||
if errors.As(err, &apierr) {
|
||||
println(string(apierr.DumpRequest(true))) // Prints the serialized HTTP request
|
||||
println(string(apierr.DumpResponse(true))) // Prints the serialized HTTP response
|
||||
}
|
||||
panic(err.Error()) // GET "/session": 400 Bad Request { ... }
|
||||
}
|
||||
```
|
||||
|
||||
When other errors occur, they are returned unwrapped; for example,
|
||||
if HTTP transport fails, you might receive `*url.Error` wrapping `*net.OpError`.
|
||||
|
||||
### Timeouts
|
||||
|
||||
Requests do not time out by default; use context to configure a timeout for a request lifecycle.
|
||||
|
||||
Note that if a request is [retried](#retries), the context timeout does not start over.
|
||||
To set a per-retry timeout, use `option.WithRequestTimeout()`.
|
||||
|
||||
```go
|
||||
// This sets the timeout for the request, including all the retries.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
client.Session.List(
|
||||
ctx,
|
||||
// This sets the per-retry timeout
|
||||
option.WithRequestTimeout(20*time.Second),
|
||||
)
|
||||
```
|
||||
|
||||
### File uploads
|
||||
|
||||
Request parameters that correspond to file uploads in multipart requests are typed as
|
||||
`param.Field[io.Reader]`. The contents of the `io.Reader` will by default be sent as a multipart form
|
||||
part with the file name of "anonymous_file" and content-type of "application/octet-stream".
|
||||
|
||||
The file name and content-type can be customized by implementing `Name() string` or `ContentType()
|
||||
string` on the run-time type of `io.Reader`. Note that `os.File` implements `Name() string`, so a
|
||||
file returned by `os.Open` will be sent with the file name on disk.
|
||||
|
||||
We also provide a helper `opencode.FileParam(reader io.Reader, filename string, contentType string)`
|
||||
which can be used to wrap any `io.Reader` with the appropriate file name and content type.
|
||||
|
||||
### Retries
|
||||
|
||||
Certain errors will be automatically retried 2 times by default, with a short exponential backoff.
|
||||
We retry by default all connection errors, 408 Request Timeout, 409 Conflict, 429 Rate Limit,
|
||||
and >=500 Internal errors.
|
||||
|
||||
You can use the `WithMaxRetries` option to configure or disable this:
|
||||
|
||||
```go
|
||||
// Configure the default for all requests:
|
||||
client := opencode.NewClient(
|
||||
option.WithMaxRetries(0), // default is 2
|
||||
)
|
||||
|
||||
// Override per-request:
|
||||
client.Session.List(context.TODO(), option.WithMaxRetries(5))
|
||||
```
|
||||
|
||||
### Accessing raw response data (e.g. response headers)
|
||||
|
||||
You can access the raw HTTP response data by using the `option.WithResponseInto()` request option. This is useful when
|
||||
you need to examine response headers, status codes, or other details.
|
||||
|
||||
```go
|
||||
// Create a variable to store the HTTP response
|
||||
var response *http.Response
|
||||
sessions, err := client.Session.List(context.TODO(), option.WithResponseInto(&response))
|
||||
if err != nil {
|
||||
// handle error
|
||||
}
|
||||
fmt.Printf("%+v\n", sessions)
|
||||
|
||||
fmt.Printf("Status Code: %d\n", response.StatusCode)
|
||||
fmt.Printf("Headers: %+#v\n", response.Header)
|
||||
```
|
||||
|
||||
### 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.
|
||||
`RequestOptions` on the client, such as retries, will be respected when making these requests.
|
||||
|
||||
```go
|
||||
var (
|
||||
// params can be an io.Reader, a []byte, an encoding/json serializable object,
|
||||
// or a "…Params" struct defined in this library.
|
||||
params map[string]interface{}
|
||||
|
||||
// result can be an []byte, *http.Response, a encoding/json deserializable object,
|
||||
// or a model defined in this library.
|
||||
result *http.Response
|
||||
)
|
||||
err := client.Post(context.Background(), "/unspecified", params, &result)
|
||||
if err != nil {
|
||||
…
|
||||
}
|
||||
```
|
||||
|
||||
#### Undocumented request params
|
||||
|
||||
To make requests using undocumented parameters, you may use either the `option.WithQuerySet()`
|
||||
or the `option.WithJSONSet()` methods.
|
||||
|
||||
```go
|
||||
params := FooNewParams{
|
||||
ID: opencode.F("id_xxxx"),
|
||||
Data: opencode.F(FooNewParamsData{
|
||||
FirstName: opencode.F("John"),
|
||||
}),
|
||||
}
|
||||
client.Foo.New(context.Background(), params, option.WithJSONSet("data.last_name", "Doe"))
|
||||
```
|
||||
|
||||
#### Undocumented response properties
|
||||
|
||||
To access undocumented response properties, you may either access the raw JSON of the response as a string
|
||||
with `result.JSON.RawJSON()`, or get the raw JSON of a particular field on the result with
|
||||
`result.JSON.Foo.Raw()`.
|
||||
|
||||
Any fields that are not present on the response struct will be saved and can be accessed by `result.JSON.ExtraFields()` which returns the extra fields as a `map[string]Field`.
|
||||
|
||||
### Middleware
|
||||
|
||||
We provide `option.WithMiddleware` which applies the given
|
||||
middleware to requests.
|
||||
|
||||
```go
|
||||
func Logger(req *http.Request, next option.MiddlewareNext) (res *http.Response, err error) {
|
||||
// Before the request
|
||||
start := time.Now()
|
||||
LogReq(req)
|
||||
|
||||
// Forward the request to the next handler
|
||||
res, err = next(req)
|
||||
|
||||
// Handle stuff after the request
|
||||
end := time.Now()
|
||||
LogRes(res, err, start - end)
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
client := opencode.NewClient(
|
||||
option.WithMiddleware(Logger),
|
||||
)
|
||||
```
|
||||
|
||||
When multiple middlewares are provided as variadic arguments, the middlewares
|
||||
are applied left to right. If `option.WithMiddleware` is given
|
||||
multiple times, for example first in the client then the method, the
|
||||
middleware in the client will run first and the middleware given in the method
|
||||
will run next.
|
||||
|
||||
You may also replace the default `http.Client` with
|
||||
`option.WithHTTPClient(client)`. Only one http client is
|
||||
accepted (this overwrites any previous client) and receives requests after any
|
||||
middleware has been applied.
|
||||
|
||||
## 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 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.)_
|
||||
2. 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-go/issues) with questions, bugs, or suggestions.
|
||||
|
||||
## Contributing
|
||||
|
||||
See [the contributing documentation](./CONTRIBUTING.md).
|
||||
43
packages/sdk/go/aliases.go
Normal file
43
packages/sdk/go/aliases.go
Normal file
@@ -0,0 +1,43 @@
|
||||
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
package opencode
|
||||
|
||||
import (
|
||||
"github.com/sst/opencode-sdk-go/internal/apierror"
|
||||
"github.com/sst/opencode-sdk-go/shared"
|
||||
)
|
||||
|
||||
type Error = apierror.Error
|
||||
|
||||
// This is an alias to an internal type.
|
||||
type MessageAbortedError = shared.MessageAbortedError
|
||||
|
||||
// This is an alias to an internal type.
|
||||
type MessageAbortedErrorName = shared.MessageAbortedErrorName
|
||||
|
||||
// This is an alias to an internal value.
|
||||
const MessageAbortedErrorNameMessageAbortedError = shared.MessageAbortedErrorNameMessageAbortedError
|
||||
|
||||
// This is an alias to an internal type.
|
||||
type ProviderAuthError = shared.ProviderAuthError
|
||||
|
||||
// This is an alias to an internal type.
|
||||
type ProviderAuthErrorData = shared.ProviderAuthErrorData
|
||||
|
||||
// This is an alias to an internal type.
|
||||
type ProviderAuthErrorName = shared.ProviderAuthErrorName
|
||||
|
||||
// This is an alias to an internal value.
|
||||
const ProviderAuthErrorNameProviderAuthError = shared.ProviderAuthErrorNameProviderAuthError
|
||||
|
||||
// This is an alias to an internal type.
|
||||
type UnknownError = shared.UnknownError
|
||||
|
||||
// This is an alias to an internal type.
|
||||
type UnknownErrorData = shared.UnknownErrorData
|
||||
|
||||
// This is an alias to an internal type.
|
||||
type UnknownErrorName = shared.UnknownErrorName
|
||||
|
||||
// This is an alias to an internal value.
|
||||
const UnknownErrorNameUnknownError = shared.UnknownErrorNameUnknownError
|
||||
146
packages/sdk/go/api.md
Normal file
146
packages/sdk/go/api.md
Normal file
@@ -0,0 +1,146 @@
|
||||
# Shared Response Types
|
||||
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go/shared">shared</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go/shared#MessageAbortedError">MessageAbortedError</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go/shared">shared</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go/shared#ProviderAuthError">ProviderAuthError</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go/shared">shared</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go/shared#UnknownError">UnknownError</a>
|
||||
|
||||
# Event
|
||||
|
||||
Response Types:
|
||||
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#EventListResponse">EventListResponse</a>
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="get /event">client.Event.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#EventService.List">List</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#EventListResponse">EventListResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
|
||||
# App
|
||||
|
||||
Response Types:
|
||||
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#App">App</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Mode">Mode</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Model">Model</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Provider">Provider</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppProvidersResponse">AppProvidersResponse</a>
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="get /app">client.App.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppService.Get">Get</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#App">App</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="post /app/init">client.App.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppService.Init">Init</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="post /log">client.App.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppService.Log">Log</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppLogParams">AppLogParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="get /mode">client.App.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppService.Modes">Modes</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Mode">Mode</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="get /config/providers">client.App.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppService.Providers">Providers</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppProvidersResponse">AppProvidersResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
|
||||
# Find
|
||||
|
||||
Response Types:
|
||||
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Symbol">Symbol</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindTextResponse">FindTextResponse</a>
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="get /find/file">client.Find.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindService.Files">Files</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindFilesParams">FindFilesParams</a>) ([]<a href="https://pkg.go.dev/builtin#string">string</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="get /find/symbol">client.Find.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindService.Symbols">Symbols</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindSymbolsParams">FindSymbolsParams</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Symbol">Symbol</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="get /find">client.Find.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindService.Text">Text</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindTextParams">FindTextParams</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindTextResponse">FindTextResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
|
||||
# File
|
||||
|
||||
Response Types:
|
||||
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#File">File</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileReadResponse">FileReadResponse</a>
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="get /file">client.File.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileService.Read">Read</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileReadParams">FileReadParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileReadResponse">FileReadResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="get /file/status">client.File.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileService.Status">Status</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#File">File</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
|
||||
# Config
|
||||
|
||||
Response Types:
|
||||
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Config">Config</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#KeybindsConfig">KeybindsConfig</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#McpLocalConfig">McpLocalConfig</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#McpRemoteConfig">McpRemoteConfig</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ModeConfig">ModeConfig</a>
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="get /config">client.Config.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ConfigService.Get">Get</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Config">Config</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
|
||||
# Session
|
||||
|
||||
Params Types:
|
||||
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FilePartInputParam">FilePartInputParam</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FilePartSourceUnionParam">FilePartSourceUnionParam</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FilePartSourceTextParam">FilePartSourceTextParam</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileSourceParam">FileSourceParam</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SymbolSourceParam">SymbolSourceParam</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TextPartInputParam">TextPartInputParam</a>
|
||||
|
||||
Response Types:
|
||||
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AssistantMessage">AssistantMessage</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FilePart">FilePart</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FilePartSource">FilePartSource</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FilePartSourceText">FilePartSourceText</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileSource">FileSource</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Message">Message</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Part">Part</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SnapshotPart">SnapshotPart</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#StepFinishPart">StepFinishPart</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#StepStartPart">StepStartPart</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SymbolSource">SymbolSource</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TextPart">TextPart</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolPart">ToolPart</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolStateCompleted">ToolStateCompleted</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolStateError">ToolStateError</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolStatePending">ToolStatePending</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolStateRunning">ToolStateRunning</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#UserMessage">UserMessage</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionMessageResponse">SessionMessageResponse</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionMessagesResponse">SessionMessagesResponse</a>
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /session">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.New">New</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="get /session">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.List">List</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="delete /session/{id}">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Delete">Delete</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="post /session/{id}/abort">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Abort">Abort</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="post /session/{id}/message">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Chat">Chat</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionChatParams">SessionChatParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AssistantMessage">AssistantMessage</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="post /session/{id}/init">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Init">Init</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionInitParams">SessionInitParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="get /session/{id}/message/{messageID}">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Message">Message</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, messageID <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionMessageResponse">SessionMessageResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="get /session/{id}/message">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Messages">Messages</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionMessagesResponse">SessionMessagesResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="post /session/{id}/revert">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Revert">Revert</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionRevertParams">SessionRevertParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="post /session/{id}/share">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Share">Share</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="post /session/{id}/summarize">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Summarize">Summarize</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionSummarizeParams">SessionSummarizeParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="post /session/{id}/unrevert">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Unrevert">Unrevert</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="delete /session/{id}/share">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Unshare">Unshare</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
|
||||
## Permissions
|
||||
|
||||
Response Types:
|
||||
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Permission">Permission</a>
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /session/{id}/permissions/{permissionID}">client.Session.Permissions.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionPermissionService.Respond">Respond</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, permissionID <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionPermissionRespondParams">SessionPermissionRespondParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
|
||||
# Tui
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /tui/append-prompt">client.Tui.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiService.AppendPrompt">AppendPrompt</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiAppendPromptParams">TuiAppendPromptParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="post /tui/clear-prompt">client.Tui.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiService.ClearPrompt">ClearPrompt</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="post /tui/execute-command">client.Tui.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiService.ExecuteCommand">ExecuteCommand</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiExecuteCommandParams">TuiExecuteCommandParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="post /tui/open-help">client.Tui.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiService.OpenHelp">OpenHelp</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="post /tui/open-models">client.Tui.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiService.OpenModels">OpenModels</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="post /tui/open-sessions">client.Tui.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiService.OpenSessions">OpenSessions</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="post /tui/open-themes">client.Tui.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiService.OpenThemes">OpenThemes</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="post /tui/submit-prompt">client.Tui.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiService.SubmitPrompt">SubmitPrompt</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
368
packages/sdk/go/app.go
Normal file
368
packages/sdk/go/app.go
Normal file
@@ -0,0 +1,368 @@
|
||||
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
package opencode
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/sst/opencode-sdk-go/internal/apijson"
|
||||
"github.com/sst/opencode-sdk-go/internal/param"
|
||||
"github.com/sst/opencode-sdk-go/internal/requestconfig"
|
||||
"github.com/sst/opencode-sdk-go/option"
|
||||
)
|
||||
|
||||
// AppService contains methods and other services that help with interacting with
|
||||
// the opencode API.
|
||||
//
|
||||
// Note, unlike clients, this service does not read variables from the environment
|
||||
// automatically. You should not instantiate this service directly, and instead use
|
||||
// the [NewAppService] method instead.
|
||||
type AppService struct {
|
||||
Options []option.RequestOption
|
||||
}
|
||||
|
||||
// NewAppService generates a new service that applies the given options to each
|
||||
// request. These options are applied after the parent client's options (if there
|
||||
// is one), and before any request-specific options.
|
||||
func NewAppService(opts ...option.RequestOption) (r *AppService) {
|
||||
r = &AppService{}
|
||||
r.Options = opts
|
||||
return
|
||||
}
|
||||
|
||||
// Get app info
|
||||
func (r *AppService) Get(ctx context.Context, opts ...option.RequestOption) (res *App, err error) {
|
||||
opts = append(r.Options[:], opts...)
|
||||
path := "app"
|
||||
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
|
||||
return
|
||||
}
|
||||
|
||||
// Initialize the app
|
||||
func (r *AppService) Init(ctx context.Context, opts ...option.RequestOption) (res *bool, err error) {
|
||||
opts = append(r.Options[:], opts...)
|
||||
path := "app/init"
|
||||
err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, nil, &res, opts...)
|
||||
return
|
||||
}
|
||||
|
||||
// Write a log entry to the server logs
|
||||
func (r *AppService) Log(ctx context.Context, body AppLogParams, opts ...option.RequestOption) (res *bool, err error) {
|
||||
opts = append(r.Options[:], opts...)
|
||||
path := "log"
|
||||
err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...)
|
||||
return
|
||||
}
|
||||
|
||||
// List all modes
|
||||
func (r *AppService) Modes(ctx context.Context, opts ...option.RequestOption) (res *[]Mode, err error) {
|
||||
opts = append(r.Options[:], opts...)
|
||||
path := "mode"
|
||||
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
|
||||
return
|
||||
}
|
||||
|
||||
// List all providers
|
||||
func (r *AppService) Providers(ctx context.Context, opts ...option.RequestOption) (res *AppProvidersResponse, err error) {
|
||||
opts = append(r.Options[:], opts...)
|
||||
path := "config/providers"
|
||||
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
|
||||
return
|
||||
}
|
||||
|
||||
type App struct {
|
||||
Git bool `json:"git,required"`
|
||||
Hostname string `json:"hostname,required"`
|
||||
Path AppPath `json:"path,required"`
|
||||
Time AppTime `json:"time,required"`
|
||||
JSON appJSON `json:"-"`
|
||||
}
|
||||
|
||||
// appJSON contains the JSON metadata for the struct [App]
|
||||
type appJSON struct {
|
||||
Git apijson.Field
|
||||
Hostname apijson.Field
|
||||
Path apijson.Field
|
||||
Time apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *App) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r appJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type AppPath struct {
|
||||
Config string `json:"config,required"`
|
||||
Cwd string `json:"cwd,required"`
|
||||
Data string `json:"data,required"`
|
||||
Root string `json:"root,required"`
|
||||
State string `json:"state,required"`
|
||||
JSON appPathJSON `json:"-"`
|
||||
}
|
||||
|
||||
// appPathJSON contains the JSON metadata for the struct [AppPath]
|
||||
type appPathJSON struct {
|
||||
Config apijson.Field
|
||||
Cwd apijson.Field
|
||||
Data apijson.Field
|
||||
Root apijson.Field
|
||||
State apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *AppPath) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r appPathJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type AppTime struct {
|
||||
Initialized float64 `json:"initialized"`
|
||||
JSON appTimeJSON `json:"-"`
|
||||
}
|
||||
|
||||
// appTimeJSON contains the JSON metadata for the struct [AppTime]
|
||||
type appTimeJSON struct {
|
||||
Initialized apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *AppTime) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r appTimeJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type Mode struct {
|
||||
Name string `json:"name,required"`
|
||||
Tools map[string]bool `json:"tools,required"`
|
||||
Model ModeModel `json:"model"`
|
||||
Prompt string `json:"prompt"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
JSON modeJSON `json:"-"`
|
||||
}
|
||||
|
||||
// modeJSON contains the JSON metadata for the struct [Mode]
|
||||
type modeJSON struct {
|
||||
Name apijson.Field
|
||||
Tools apijson.Field
|
||||
Model apijson.Field
|
||||
Prompt apijson.Field
|
||||
Temperature apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *Mode) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r modeJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type ModeModel struct {
|
||||
ModelID string `json:"modelID,required"`
|
||||
ProviderID string `json:"providerID,required"`
|
||||
JSON modeModelJSON `json:"-"`
|
||||
}
|
||||
|
||||
// modeModelJSON contains the JSON metadata for the struct [ModeModel]
|
||||
type modeModelJSON struct {
|
||||
ModelID apijson.Field
|
||||
ProviderID apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *ModeModel) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r modeModelJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type Model struct {
|
||||
ID string `json:"id,required"`
|
||||
Attachment bool `json:"attachment,required"`
|
||||
Cost ModelCost `json:"cost,required"`
|
||||
Limit ModelLimit `json:"limit,required"`
|
||||
Name string `json:"name,required"`
|
||||
Options map[string]interface{} `json:"options,required"`
|
||||
Reasoning bool `json:"reasoning,required"`
|
||||
ReleaseDate string `json:"release_date,required"`
|
||||
Temperature bool `json:"temperature,required"`
|
||||
ToolCall bool `json:"tool_call,required"`
|
||||
JSON modelJSON `json:"-"`
|
||||
}
|
||||
|
||||
// modelJSON contains the JSON metadata for the struct [Model]
|
||||
type modelJSON struct {
|
||||
ID apijson.Field
|
||||
Attachment apijson.Field
|
||||
Cost apijson.Field
|
||||
Limit apijson.Field
|
||||
Name apijson.Field
|
||||
Options apijson.Field
|
||||
Reasoning apijson.Field
|
||||
ReleaseDate apijson.Field
|
||||
Temperature apijson.Field
|
||||
ToolCall apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *Model) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r modelJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type ModelCost struct {
|
||||
Input float64 `json:"input,required"`
|
||||
Output float64 `json:"output,required"`
|
||||
CacheRead float64 `json:"cache_read"`
|
||||
CacheWrite float64 `json:"cache_write"`
|
||||
JSON modelCostJSON `json:"-"`
|
||||
}
|
||||
|
||||
// modelCostJSON contains the JSON metadata for the struct [ModelCost]
|
||||
type modelCostJSON struct {
|
||||
Input apijson.Field
|
||||
Output apijson.Field
|
||||
CacheRead apijson.Field
|
||||
CacheWrite apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *ModelCost) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r modelCostJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type ModelLimit struct {
|
||||
Context float64 `json:"context,required"`
|
||||
Output float64 `json:"output,required"`
|
||||
JSON modelLimitJSON `json:"-"`
|
||||
}
|
||||
|
||||
// modelLimitJSON contains the JSON metadata for the struct [ModelLimit]
|
||||
type modelLimitJSON struct {
|
||||
Context apijson.Field
|
||||
Output apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *ModelLimit) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r modelLimitJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type Provider struct {
|
||||
ID string `json:"id,required"`
|
||||
Env []string `json:"env,required"`
|
||||
Models map[string]Model `json:"models,required"`
|
||||
Name string `json:"name,required"`
|
||||
API string `json:"api"`
|
||||
Npm string `json:"npm"`
|
||||
JSON providerJSON `json:"-"`
|
||||
}
|
||||
|
||||
// providerJSON contains the JSON metadata for the struct [Provider]
|
||||
type providerJSON struct {
|
||||
ID apijson.Field
|
||||
Env apijson.Field
|
||||
Models apijson.Field
|
||||
Name apijson.Field
|
||||
API apijson.Field
|
||||
Npm apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *Provider) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r providerJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type AppProvidersResponse struct {
|
||||
Default map[string]string `json:"default,required"`
|
||||
Providers []Provider `json:"providers,required"`
|
||||
JSON appProvidersResponseJSON `json:"-"`
|
||||
}
|
||||
|
||||
// appProvidersResponseJSON contains the JSON metadata for the struct
|
||||
// [AppProvidersResponse]
|
||||
type appProvidersResponseJSON struct {
|
||||
Default apijson.Field
|
||||
Providers apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *AppProvidersResponse) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r appProvidersResponseJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type AppLogParams struct {
|
||||
// Log level
|
||||
Level param.Field[AppLogParamsLevel] `json:"level,required"`
|
||||
// Log message
|
||||
Message param.Field[string] `json:"message,required"`
|
||||
// Service name for the log entry
|
||||
Service param.Field[string] `json:"service,required"`
|
||||
// Additional metadata for the log entry
|
||||
Extra param.Field[map[string]interface{}] `json:"extra"`
|
||||
}
|
||||
|
||||
func (r AppLogParams) MarshalJSON() (data []byte, err error) {
|
||||
return apijson.MarshalRoot(r)
|
||||
}
|
||||
|
||||
// Log level
|
||||
type AppLogParamsLevel string
|
||||
|
||||
const (
|
||||
AppLogParamsLevelDebug AppLogParamsLevel = "debug"
|
||||
AppLogParamsLevelInfo AppLogParamsLevel = "info"
|
||||
AppLogParamsLevelError AppLogParamsLevel = "error"
|
||||
AppLogParamsLevelWarn AppLogParamsLevel = "warn"
|
||||
)
|
||||
|
||||
func (r AppLogParamsLevel) IsKnown() bool {
|
||||
switch r {
|
||||
case AppLogParamsLevelDebug, AppLogParamsLevelInfo, AppLogParamsLevelError, AppLogParamsLevelWarn:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
131
packages/sdk/go/app_test.go
Normal file
131
packages/sdk/go/app_test.go
Normal file
@@ -0,0 +1,131 @@
|
||||
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
package opencode_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/sst/opencode-sdk-go"
|
||||
"github.com/sst/opencode-sdk-go/internal/testutil"
|
||||
"github.com/sst/opencode-sdk-go/option"
|
||||
)
|
||||
|
||||
func TestAppGet(t *testing.T) {
|
||||
t.Skip("skipped: tests are disabled for the time being")
|
||||
baseURL := "http://localhost:4010"
|
||||
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
|
||||
baseURL = envURL
|
||||
}
|
||||
if !testutil.CheckTestServer(t, baseURL) {
|
||||
return
|
||||
}
|
||||
client := opencode.NewClient(
|
||||
option.WithBaseURL(baseURL),
|
||||
)
|
||||
_, err := client.App.Get(context.TODO())
|
||||
if err != nil {
|
||||
var apierr *opencode.Error
|
||||
if errors.As(err, &apierr) {
|
||||
t.Log(string(apierr.DumpRequest(true)))
|
||||
}
|
||||
t.Fatalf("err should be nil: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppInit(t *testing.T) {
|
||||
t.Skip("skipped: tests are disabled for the time being")
|
||||
baseURL := "http://localhost:4010"
|
||||
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
|
||||
baseURL = envURL
|
||||
}
|
||||
if !testutil.CheckTestServer(t, baseURL) {
|
||||
return
|
||||
}
|
||||
client := opencode.NewClient(
|
||||
option.WithBaseURL(baseURL),
|
||||
)
|
||||
_, err := client.App.Init(context.TODO())
|
||||
if err != nil {
|
||||
var apierr *opencode.Error
|
||||
if errors.As(err, &apierr) {
|
||||
t.Log(string(apierr.DumpRequest(true)))
|
||||
}
|
||||
t.Fatalf("err should be nil: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppLogWithOptionalParams(t *testing.T) {
|
||||
t.Skip("skipped: tests are disabled for the time being")
|
||||
baseURL := "http://localhost:4010"
|
||||
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
|
||||
baseURL = envURL
|
||||
}
|
||||
if !testutil.CheckTestServer(t, baseURL) {
|
||||
return
|
||||
}
|
||||
client := opencode.NewClient(
|
||||
option.WithBaseURL(baseURL),
|
||||
)
|
||||
_, err := client.App.Log(context.TODO(), opencode.AppLogParams{
|
||||
Level: opencode.F(opencode.AppLogParamsLevelDebug),
|
||||
Message: opencode.F("message"),
|
||||
Service: opencode.F("service"),
|
||||
Extra: opencode.F(map[string]interface{}{
|
||||
"foo": "bar",
|
||||
}),
|
||||
})
|
||||
if err != nil {
|
||||
var apierr *opencode.Error
|
||||
if errors.As(err, &apierr) {
|
||||
t.Log(string(apierr.DumpRequest(true)))
|
||||
}
|
||||
t.Fatalf("err should be nil: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppModes(t *testing.T) {
|
||||
t.Skip("skipped: tests are disabled for the time being")
|
||||
baseURL := "http://localhost:4010"
|
||||
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
|
||||
baseURL = envURL
|
||||
}
|
||||
if !testutil.CheckTestServer(t, baseURL) {
|
||||
return
|
||||
}
|
||||
client := opencode.NewClient(
|
||||
option.WithBaseURL(baseURL),
|
||||
)
|
||||
_, err := client.App.Modes(context.TODO())
|
||||
if err != nil {
|
||||
var apierr *opencode.Error
|
||||
if errors.As(err, &apierr) {
|
||||
t.Log(string(apierr.DumpRequest(true)))
|
||||
}
|
||||
t.Fatalf("err should be nil: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppProviders(t *testing.T) {
|
||||
t.Skip("skipped: tests are disabled for the time being")
|
||||
baseURL := "http://localhost:4010"
|
||||
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
|
||||
baseURL = envURL
|
||||
}
|
||||
if !testutil.CheckTestServer(t, baseURL) {
|
||||
return
|
||||
}
|
||||
client := opencode.NewClient(
|
||||
option.WithBaseURL(baseURL),
|
||||
)
|
||||
_, err := client.App.Providers(context.TODO())
|
||||
if err != nil {
|
||||
var apierr *opencode.Error
|
||||
if errors.As(err, &apierr) {
|
||||
t.Log(string(apierr.DumpRequest(true)))
|
||||
}
|
||||
t.Fatalf("err should be nil: %s", err.Error())
|
||||
}
|
||||
}
|
||||
125
packages/sdk/go/client.go
Normal file
125
packages/sdk/go/client.go
Normal file
@@ -0,0 +1,125 @@
|
||||
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
package opencode
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/sst/opencode-sdk-go/internal/requestconfig"
|
||||
"github.com/sst/opencode-sdk-go/option"
|
||||
)
|
||||
|
||||
// Client creates a struct with services and top level methods that help with
|
||||
// interacting with the opencode API. You should not instantiate this client
|
||||
// directly, and instead use the [NewClient] method instead.
|
||||
type Client struct {
|
||||
Options []option.RequestOption
|
||||
Event *EventService
|
||||
App *AppService
|
||||
Find *FindService
|
||||
File *FileService
|
||||
Config *ConfigService
|
||||
Session *SessionService
|
||||
Tui *TuiService
|
||||
}
|
||||
|
||||
// DefaultClientOptions read from the environment (OPENCODE_BASE_URL). This should
|
||||
// be used to initialize new clients.
|
||||
func DefaultClientOptions() []option.RequestOption {
|
||||
defaults := []option.RequestOption{option.WithEnvironmentProduction()}
|
||||
if o, ok := os.LookupEnv("OPENCODE_BASE_URL"); ok {
|
||||
defaults = append(defaults, option.WithBaseURL(o))
|
||||
}
|
||||
return defaults
|
||||
}
|
||||
|
||||
// NewClient generates a new client with the default option read from the
|
||||
// environment (OPENCODE_BASE_URL). The option passed in as arguments are applied
|
||||
// after these default arguments, and all option will be passed down to the
|
||||
// services and requests that this client makes.
|
||||
func NewClient(opts ...option.RequestOption) (r *Client) {
|
||||
opts = append(DefaultClientOptions(), opts...)
|
||||
|
||||
r = &Client{Options: opts}
|
||||
|
||||
r.Event = NewEventService(opts...)
|
||||
r.App = NewAppService(opts...)
|
||||
r.Find = NewFindService(opts...)
|
||||
r.File = NewFileService(opts...)
|
||||
r.Config = NewConfigService(opts...)
|
||||
r.Session = NewSessionService(opts...)
|
||||
r.Tui = NewTuiService(opts...)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Execute makes a request with the given context, method, URL, request params,
|
||||
// response, and request options. This is useful for hitting undocumented endpoints
|
||||
// while retaining the base URL, auth, retries, and other options from the client.
|
||||
//
|
||||
// If a byte slice or an [io.Reader] is supplied to params, it will be used as-is
|
||||
// for the request body.
|
||||
//
|
||||
// The params is by default serialized into the body using [encoding/json]. If your
|
||||
// type implements a MarshalJSON function, it will be used instead to serialize the
|
||||
// request. If a URLQuery method is implemented, the returned [url.Values] will be
|
||||
// used as query strings to the url.
|
||||
//
|
||||
// If your params struct uses [param.Field], you must provide either [MarshalJSON],
|
||||
// [URLQuery], and/or [MarshalForm] functions. It is undefined behavior to use a
|
||||
// struct uses [param.Field] without specifying how it is serialized.
|
||||
//
|
||||
// Any "…Params" object defined in this library can be used as the request
|
||||
// argument. Note that 'path' arguments will not be forwarded into the url.
|
||||
//
|
||||
// The response body will be deserialized into the res variable, depending on its
|
||||
// type:
|
||||
//
|
||||
// - A pointer to a [*http.Response] is populated by the raw response.
|
||||
// - A pointer to a byte array will be populated with the contents of the request
|
||||
// body.
|
||||
// - A pointer to any other type uses this library's default JSON decoding, which
|
||||
// respects UnmarshalJSON if it is defined on the type.
|
||||
// - A nil value will not read the response body.
|
||||
//
|
||||
// For even greater flexibility, see [option.WithResponseInto] and
|
||||
// [option.WithResponseBodyInto].
|
||||
func (r *Client) Execute(ctx context.Context, method string, path string, params interface{}, res interface{}, opts ...option.RequestOption) error {
|
||||
opts = append(r.Options, opts...)
|
||||
return requestconfig.ExecuteNewRequest(ctx, method, path, params, res, opts...)
|
||||
}
|
||||
|
||||
// Get makes a GET request with the given URL, params, and optionally deserializes
|
||||
// to a response. See [Execute] documentation on the params and response.
|
||||
func (r *Client) Get(ctx context.Context, path string, params interface{}, res interface{}, opts ...option.RequestOption) error {
|
||||
return r.Execute(ctx, http.MethodGet, path, params, res, opts...)
|
||||
}
|
||||
|
||||
// Post makes a POST request with the given URL, params, and optionally
|
||||
// deserializes to a response. See [Execute] documentation on the params and
|
||||
// response.
|
||||
func (r *Client) Post(ctx context.Context, path string, params interface{}, res interface{}, opts ...option.RequestOption) error {
|
||||
return r.Execute(ctx, http.MethodPost, path, params, res, opts...)
|
||||
}
|
||||
|
||||
// Put makes a PUT request with the given URL, params, and optionally deserializes
|
||||
// to a response. See [Execute] documentation on the params and response.
|
||||
func (r *Client) Put(ctx context.Context, path string, params interface{}, res interface{}, opts ...option.RequestOption) error {
|
||||
return r.Execute(ctx, http.MethodPut, path, params, res, opts...)
|
||||
}
|
||||
|
||||
// Patch makes a PATCH request with the given URL, params, and optionally
|
||||
// deserializes to a response. See [Execute] documentation on the params and
|
||||
// response.
|
||||
func (r *Client) Patch(ctx context.Context, path string, params interface{}, res interface{}, opts ...option.RequestOption) error {
|
||||
return r.Execute(ctx, http.MethodPatch, path, params, res, opts...)
|
||||
}
|
||||
|
||||
// Delete makes a DELETE request with the given URL, params, and optionally
|
||||
// deserializes to a response. See [Execute] documentation on the params and
|
||||
// response.
|
||||
func (r *Client) Delete(ctx context.Context, path string, params interface{}, res interface{}, opts ...option.RequestOption) error {
|
||||
return r.Execute(ctx, http.MethodDelete, path, params, res, opts...)
|
||||
}
|
||||
332
packages/sdk/go/client_test.go
Normal file
332
packages/sdk/go/client_test.go
Normal file
@@ -0,0 +1,332 @@
|
||||
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
package opencode_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sst/opencode-sdk-go"
|
||||
"github.com/sst/opencode-sdk-go/internal"
|
||||
"github.com/sst/opencode-sdk-go/option"
|
||||
)
|
||||
|
||||
type closureTransport struct {
|
||||
fn func(req *http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
func (t *closureTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return t.fn(req)
|
||||
}
|
||||
|
||||
func TestUserAgentHeader(t *testing.T) {
|
||||
var userAgent string
|
||||
client := opencode.NewClient(
|
||||
option.WithHTTPClient(&http.Client{
|
||||
Transport: &closureTransport{
|
||||
fn: func(req *http.Request) (*http.Response, error) {
|
||||
userAgent = req.Header.Get("User-Agent")
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
client.Session.List(context.Background())
|
||||
if userAgent != fmt.Sprintf("Opencode/Go %s", internal.PackageVersion) {
|
||||
t.Errorf("Expected User-Agent to be correct, but got: %#v", userAgent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetryAfter(t *testing.T) {
|
||||
retryCountHeaders := make([]string, 0)
|
||||
client := opencode.NewClient(
|
||||
option.WithHTTPClient(&http.Client{
|
||||
Transport: &closureTransport{
|
||||
fn: func(req *http.Request) (*http.Response, error) {
|
||||
retryCountHeaders = append(retryCountHeaders, req.Header.Get("X-Stainless-Retry-Count"))
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusTooManyRequests,
|
||||
Header: http.Header{
|
||||
http.CanonicalHeaderKey("Retry-After"): []string{"0.1"},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
_, err := client.Session.List(context.Background())
|
||||
if err == nil {
|
||||
t.Error("Expected there to be a cancel error")
|
||||
}
|
||||
|
||||
attempts := len(retryCountHeaders)
|
||||
if attempts != 3 {
|
||||
t.Errorf("Expected %d attempts, got %d", 3, attempts)
|
||||
}
|
||||
|
||||
expectedRetryCountHeaders := []string{"0", "1", "2"}
|
||||
if !reflect.DeepEqual(retryCountHeaders, expectedRetryCountHeaders) {
|
||||
t.Errorf("Expected %v retry count headers, got %v", expectedRetryCountHeaders, retryCountHeaders)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteRetryCountHeader(t *testing.T) {
|
||||
retryCountHeaders := make([]string, 0)
|
||||
client := opencode.NewClient(
|
||||
option.WithHTTPClient(&http.Client{
|
||||
Transport: &closureTransport{
|
||||
fn: func(req *http.Request) (*http.Response, error) {
|
||||
retryCountHeaders = append(retryCountHeaders, req.Header.Get("X-Stainless-Retry-Count"))
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusTooManyRequests,
|
||||
Header: http.Header{
|
||||
http.CanonicalHeaderKey("Retry-After"): []string{"0.1"},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
}),
|
||||
option.WithHeaderDel("X-Stainless-Retry-Count"),
|
||||
)
|
||||
_, err := client.Session.List(context.Background())
|
||||
if err == nil {
|
||||
t.Error("Expected there to be a cancel error")
|
||||
}
|
||||
|
||||
expectedRetryCountHeaders := []string{"", "", ""}
|
||||
if !reflect.DeepEqual(retryCountHeaders, expectedRetryCountHeaders) {
|
||||
t.Errorf("Expected %v retry count headers, got %v", expectedRetryCountHeaders, retryCountHeaders)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOverwriteRetryCountHeader(t *testing.T) {
|
||||
retryCountHeaders := make([]string, 0)
|
||||
client := opencode.NewClient(
|
||||
option.WithHTTPClient(&http.Client{
|
||||
Transport: &closureTransport{
|
||||
fn: func(req *http.Request) (*http.Response, error) {
|
||||
retryCountHeaders = append(retryCountHeaders, req.Header.Get("X-Stainless-Retry-Count"))
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusTooManyRequests,
|
||||
Header: http.Header{
|
||||
http.CanonicalHeaderKey("Retry-After"): []string{"0.1"},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
}),
|
||||
option.WithHeader("X-Stainless-Retry-Count", "42"),
|
||||
)
|
||||
_, err := client.Session.List(context.Background())
|
||||
if err == nil {
|
||||
t.Error("Expected there to be a cancel error")
|
||||
}
|
||||
|
||||
expectedRetryCountHeaders := []string{"42", "42", "42"}
|
||||
if !reflect.DeepEqual(retryCountHeaders, expectedRetryCountHeaders) {
|
||||
t.Errorf("Expected %v retry count headers, got %v", expectedRetryCountHeaders, retryCountHeaders)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetryAfterMs(t *testing.T) {
|
||||
attempts := 0
|
||||
client := opencode.NewClient(
|
||||
option.WithHTTPClient(&http.Client{
|
||||
Transport: &closureTransport{
|
||||
fn: func(req *http.Request) (*http.Response, error) {
|
||||
attempts++
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusTooManyRequests,
|
||||
Header: http.Header{
|
||||
http.CanonicalHeaderKey("Retry-After-Ms"): []string{"100"},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
_, err := client.Session.List(context.Background())
|
||||
if err == nil {
|
||||
t.Error("Expected there to be a cancel error")
|
||||
}
|
||||
if want := 3; attempts != want {
|
||||
t.Errorf("Expected %d attempts, got %d", want, attempts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextCancel(t *testing.T) {
|
||||
client := opencode.NewClient(
|
||||
option.WithHTTPClient(&http.Client{
|
||||
Transport: &closureTransport{
|
||||
fn: func(req *http.Request) (*http.Response, error) {
|
||||
<-req.Context().Done()
|
||||
return nil, req.Context().Err()
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
cancelCtx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
_, err := client.Session.List(cancelCtx)
|
||||
if err == nil {
|
||||
t.Error("Expected there to be a cancel error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextCancelDelay(t *testing.T) {
|
||||
client := opencode.NewClient(
|
||||
option.WithHTTPClient(&http.Client{
|
||||
Transport: &closureTransport{
|
||||
fn: func(req *http.Request) (*http.Response, error) {
|
||||
<-req.Context().Done()
|
||||
return nil, req.Context().Err()
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
cancelCtx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
|
||||
defer cancel()
|
||||
_, err := client.Session.List(cancelCtx)
|
||||
if err == nil {
|
||||
t.Error("expected there to be a cancel error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextDeadline(t *testing.T) {
|
||||
testTimeout := time.After(3 * time.Second)
|
||||
testDone := make(chan struct{})
|
||||
|
||||
deadline := time.Now().Add(100 * time.Millisecond)
|
||||
deadlineCtx, cancel := context.WithDeadline(context.Background(), deadline)
|
||||
defer cancel()
|
||||
|
||||
go func() {
|
||||
client := opencode.NewClient(
|
||||
option.WithHTTPClient(&http.Client{
|
||||
Transport: &closureTransport{
|
||||
fn: func(req *http.Request) (*http.Response, error) {
|
||||
<-req.Context().Done()
|
||||
return nil, req.Context().Err()
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
_, err := client.Session.List(deadlineCtx)
|
||||
if err == nil {
|
||||
t.Error("expected there to be a deadline error")
|
||||
}
|
||||
close(testDone)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-testTimeout:
|
||||
t.Fatal("client didn't finish in time")
|
||||
case <-testDone:
|
||||
if diff := time.Since(deadline); diff < -30*time.Millisecond || 30*time.Millisecond < diff {
|
||||
t.Fatalf("client did not return within 30ms of context deadline, got %s", diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextDeadlineStreaming(t *testing.T) {
|
||||
testTimeout := time.After(3 * time.Second)
|
||||
testDone := make(chan struct{})
|
||||
|
||||
deadline := time.Now().Add(100 * time.Millisecond)
|
||||
deadlineCtx, cancel := context.WithDeadline(context.Background(), deadline)
|
||||
defer cancel()
|
||||
|
||||
go func() {
|
||||
client := opencode.NewClient(
|
||||
option.WithHTTPClient(&http.Client{
|
||||
Transport: &closureTransport{
|
||||
fn: func(req *http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Status: "200 OK",
|
||||
Body: io.NopCloser(
|
||||
io.Reader(readerFunc(func([]byte) (int, error) {
|
||||
<-req.Context().Done()
|
||||
return 0, req.Context().Err()
|
||||
})),
|
||||
),
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
stream := client.Event.ListStreaming(deadlineCtx)
|
||||
for stream.Next() {
|
||||
_ = stream.Current()
|
||||
}
|
||||
if stream.Err() == nil {
|
||||
t.Error("expected there to be a deadline error")
|
||||
}
|
||||
close(testDone)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-testTimeout:
|
||||
t.Fatal("client didn't finish in time")
|
||||
case <-testDone:
|
||||
if diff := time.Since(deadline); diff < -30*time.Millisecond || 30*time.Millisecond < diff {
|
||||
t.Fatalf("client did not return within 30ms of context deadline, got %s", diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextDeadlineStreamingWithRequestTimeout(t *testing.T) {
|
||||
testTimeout := time.After(3 * time.Second)
|
||||
testDone := make(chan struct{})
|
||||
deadline := time.Now().Add(100 * time.Millisecond)
|
||||
|
||||
go func() {
|
||||
client := opencode.NewClient(
|
||||
option.WithHTTPClient(&http.Client{
|
||||
Transport: &closureTransport{
|
||||
fn: func(req *http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Status: "200 OK",
|
||||
Body: io.NopCloser(
|
||||
io.Reader(readerFunc(func([]byte) (int, error) {
|
||||
<-req.Context().Done()
|
||||
return 0, req.Context().Err()
|
||||
})),
|
||||
),
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
stream := client.Event.ListStreaming(context.Background(), option.WithRequestTimeout((100 * time.Millisecond)))
|
||||
for stream.Next() {
|
||||
_ = stream.Current()
|
||||
}
|
||||
if stream.Err() == nil {
|
||||
t.Error("expected there to be a deadline error")
|
||||
}
|
||||
close(testDone)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-testTimeout:
|
||||
t.Fatal("client didn't finish in time")
|
||||
case <-testDone:
|
||||
if diff := time.Since(deadline); diff < -30*time.Millisecond || 30*time.Millisecond < diff {
|
||||
t.Fatalf("client did not return within 30ms of context deadline, got %s", diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type readerFunc func([]byte) (int, error)
|
||||
|
||||
func (f readerFunc) Read(p []byte) (int, error) { return f(p) }
|
||||
func (f readerFunc) Close() error { return nil }
|
||||
887
packages/sdk/go/config.go
Normal file
887
packages/sdk/go/config.go
Normal file
@@ -0,0 +1,887 @@
|
||||
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
package opencode
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"reflect"
|
||||
|
||||
"github.com/sst/opencode-sdk-go/internal/apijson"
|
||||
"github.com/sst/opencode-sdk-go/internal/requestconfig"
|
||||
"github.com/sst/opencode-sdk-go/option"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// ConfigService contains methods and other services that help with interacting
|
||||
// with the opencode API.
|
||||
//
|
||||
// Note, unlike clients, this service does not read variables from the environment
|
||||
// automatically. You should not instantiate this service directly, and instead use
|
||||
// the [NewConfigService] method instead.
|
||||
type ConfigService struct {
|
||||
Options []option.RequestOption
|
||||
}
|
||||
|
||||
// NewConfigService generates a new service that applies the given options to each
|
||||
// request. These options are applied after the parent client's options (if there
|
||||
// is one), and before any request-specific options.
|
||||
func NewConfigService(opts ...option.RequestOption) (r *ConfigService) {
|
||||
r = &ConfigService{}
|
||||
r.Options = opts
|
||||
return
|
||||
}
|
||||
|
||||
// Get config info
|
||||
func (r *ConfigService) Get(ctx context.Context, opts ...option.RequestOption) (res *Config, err error) {
|
||||
opts = append(r.Options[:], opts...)
|
||||
path := "config"
|
||||
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
|
||||
return
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
// JSON schema reference for configuration validation
|
||||
Schema string `json:"$schema"`
|
||||
// Modes configuration, see https://opencode.ai/docs/modes
|
||||
Agent ConfigAgent `json:"agent"`
|
||||
// @deprecated Use 'share' field instead. Share newly created sessions
|
||||
// automatically
|
||||
Autoshare bool `json:"autoshare"`
|
||||
// Automatically update to the latest version
|
||||
Autoupdate bool `json:"autoupdate"`
|
||||
// Disable providers that are loaded automatically
|
||||
DisabledProviders []string `json:"disabled_providers"`
|
||||
Experimental ConfigExperimental `json:"experimental"`
|
||||
// Additional instruction files or patterns to include
|
||||
Instructions []string `json:"instructions"`
|
||||
// Custom keybind configurations
|
||||
Keybinds KeybindsConfig `json:"keybinds"`
|
||||
// @deprecated Always uses stretch layout.
|
||||
Layout ConfigLayout `json:"layout"`
|
||||
// MCP (Model Context Protocol) server configurations
|
||||
Mcp map[string]ConfigMcp `json:"mcp"`
|
||||
// Modes configuration, see https://opencode.ai/docs/modes
|
||||
Mode ConfigMode `json:"mode"`
|
||||
// Model to use in the format of provider/model, eg anthropic/claude-2
|
||||
Model string `json:"model"`
|
||||
Permission ConfigPermission `json:"permission"`
|
||||
// Custom provider configurations and model overrides
|
||||
Provider map[string]ConfigProvider `json:"provider"`
|
||||
// Control sharing behavior:'manual' allows manual sharing via commands, 'auto'
|
||||
// enables automatic sharing, 'disabled' disables all sharing
|
||||
Share ConfigShare `json:"share"`
|
||||
// Small model to use for tasks like summarization and title generation in the
|
||||
// format of provider/model
|
||||
SmallModel string `json:"small_model"`
|
||||
// Theme name to use for the interface
|
||||
Theme string `json:"theme"`
|
||||
// Custom username to display in conversations instead of system username
|
||||
Username string `json:"username"`
|
||||
JSON configJSON `json:"-"`
|
||||
}
|
||||
|
||||
// configJSON contains the JSON metadata for the struct [Config]
|
||||
type configJSON struct {
|
||||
Schema apijson.Field
|
||||
Agent apijson.Field
|
||||
Autoshare apijson.Field
|
||||
Autoupdate apijson.Field
|
||||
DisabledProviders apijson.Field
|
||||
Experimental apijson.Field
|
||||
Instructions apijson.Field
|
||||
Keybinds apijson.Field
|
||||
Layout apijson.Field
|
||||
Mcp apijson.Field
|
||||
Mode apijson.Field
|
||||
Model apijson.Field
|
||||
Permission apijson.Field
|
||||
Provider apijson.Field
|
||||
Share apijson.Field
|
||||
SmallModel apijson.Field
|
||||
Theme apijson.Field
|
||||
Username apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *Config) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r configJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
// Modes configuration, see https://opencode.ai/docs/modes
|
||||
type ConfigAgent struct {
|
||||
General ConfigAgentGeneral `json:"general"`
|
||||
ExtraFields map[string]ConfigAgent `json:"-,extras"`
|
||||
JSON configAgentJSON `json:"-"`
|
||||
}
|
||||
|
||||
// configAgentJSON contains the JSON metadata for the struct [ConfigAgent]
|
||||
type configAgentJSON struct {
|
||||
General apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *ConfigAgent) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r configAgentJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type ConfigAgentGeneral struct {
|
||||
Description string `json:"description,required"`
|
||||
JSON configAgentGeneralJSON `json:"-"`
|
||||
ModeConfig
|
||||
}
|
||||
|
||||
// configAgentGeneralJSON contains the JSON metadata for the struct
|
||||
// [ConfigAgentGeneral]
|
||||
type configAgentGeneralJSON struct {
|
||||
Description apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *ConfigAgentGeneral) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r configAgentGeneralJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type ConfigExperimental struct {
|
||||
Hook ConfigExperimentalHook `json:"hook"`
|
||||
JSON configExperimentalJSON `json:"-"`
|
||||
}
|
||||
|
||||
// configExperimentalJSON contains the JSON metadata for the struct
|
||||
// [ConfigExperimental]
|
||||
type configExperimentalJSON struct {
|
||||
Hook apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *ConfigExperimental) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r configExperimentalJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type ConfigExperimentalHook struct {
|
||||
FileEdited map[string][]ConfigExperimentalHookFileEdited `json:"file_edited"`
|
||||
SessionCompleted []ConfigExperimentalHookSessionCompleted `json:"session_completed"`
|
||||
JSON configExperimentalHookJSON `json:"-"`
|
||||
}
|
||||
|
||||
// configExperimentalHookJSON contains the JSON metadata for the struct
|
||||
// [ConfigExperimentalHook]
|
||||
type configExperimentalHookJSON struct {
|
||||
FileEdited apijson.Field
|
||||
SessionCompleted apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *ConfigExperimentalHook) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r configExperimentalHookJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type ConfigExperimentalHookFileEdited struct {
|
||||
Command []string `json:"command,required"`
|
||||
Environment map[string]string `json:"environment"`
|
||||
JSON configExperimentalHookFileEditedJSON `json:"-"`
|
||||
}
|
||||
|
||||
// configExperimentalHookFileEditedJSON contains the JSON metadata for the struct
|
||||
// [ConfigExperimentalHookFileEdited]
|
||||
type configExperimentalHookFileEditedJSON struct {
|
||||
Command apijson.Field
|
||||
Environment apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *ConfigExperimentalHookFileEdited) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r configExperimentalHookFileEditedJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type ConfigExperimentalHookSessionCompleted struct {
|
||||
Command []string `json:"command,required"`
|
||||
Environment map[string]string `json:"environment"`
|
||||
JSON configExperimentalHookSessionCompletedJSON `json:"-"`
|
||||
}
|
||||
|
||||
// configExperimentalHookSessionCompletedJSON contains the JSON metadata for the
|
||||
// struct [ConfigExperimentalHookSessionCompleted]
|
||||
type configExperimentalHookSessionCompletedJSON struct {
|
||||
Command apijson.Field
|
||||
Environment apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *ConfigExperimentalHookSessionCompleted) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r configExperimentalHookSessionCompletedJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
// @deprecated Always uses stretch layout.
|
||||
type ConfigLayout string
|
||||
|
||||
const (
|
||||
ConfigLayoutAuto ConfigLayout = "auto"
|
||||
ConfigLayoutStretch ConfigLayout = "stretch"
|
||||
)
|
||||
|
||||
func (r ConfigLayout) IsKnown() bool {
|
||||
switch r {
|
||||
case ConfigLayoutAuto, ConfigLayoutStretch:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type ConfigMcp struct {
|
||||
// Type of MCP server connection
|
||||
Type ConfigMcpType `json:"type,required"`
|
||||
// This field can have the runtime type of [[]string].
|
||||
Command interface{} `json:"command"`
|
||||
// Enable or disable the MCP server on startup
|
||||
Enabled bool `json:"enabled"`
|
||||
// This field can have the runtime type of [map[string]string].
|
||||
Environment interface{} `json:"environment"`
|
||||
// This field can have the runtime type of [map[string]string].
|
||||
Headers interface{} `json:"headers"`
|
||||
// URL of the remote MCP server
|
||||
URL string `json:"url"`
|
||||
JSON configMcpJSON `json:"-"`
|
||||
union ConfigMcpUnion
|
||||
}
|
||||
|
||||
// configMcpJSON contains the JSON metadata for the struct [ConfigMcp]
|
||||
type configMcpJSON struct {
|
||||
Type apijson.Field
|
||||
Command apijson.Field
|
||||
Enabled apijson.Field
|
||||
Environment apijson.Field
|
||||
Headers apijson.Field
|
||||
URL apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r configMcpJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
func (r *ConfigMcp) UnmarshalJSON(data []byte) (err error) {
|
||||
*r = ConfigMcp{}
|
||||
err = apijson.UnmarshalRoot(data, &r.union)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return apijson.Port(r.union, &r)
|
||||
}
|
||||
|
||||
// AsUnion returns a [ConfigMcpUnion] interface which you can cast to the specific
|
||||
// types for more type safety.
|
||||
//
|
||||
// Possible runtime types of the union are [McpLocalConfig], [McpRemoteConfig].
|
||||
func (r ConfigMcp) AsUnion() ConfigMcpUnion {
|
||||
return r.union
|
||||
}
|
||||
|
||||
// Union satisfied by [McpLocalConfig] or [McpRemoteConfig].
|
||||
type ConfigMcpUnion interface {
|
||||
implementsConfigMcp()
|
||||
}
|
||||
|
||||
func init() {
|
||||
apijson.RegisterUnion(
|
||||
reflect.TypeOf((*ConfigMcpUnion)(nil)).Elem(),
|
||||
"type",
|
||||
apijson.UnionVariant{
|
||||
TypeFilter: gjson.JSON,
|
||||
Type: reflect.TypeOf(McpLocalConfig{}),
|
||||
DiscriminatorValue: "local",
|
||||
},
|
||||
apijson.UnionVariant{
|
||||
TypeFilter: gjson.JSON,
|
||||
Type: reflect.TypeOf(McpRemoteConfig{}),
|
||||
DiscriminatorValue: "remote",
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Type of MCP server connection
|
||||
type ConfigMcpType string
|
||||
|
||||
const (
|
||||
ConfigMcpTypeLocal ConfigMcpType = "local"
|
||||
ConfigMcpTypeRemote ConfigMcpType = "remote"
|
||||
)
|
||||
|
||||
func (r ConfigMcpType) IsKnown() bool {
|
||||
switch r {
|
||||
case ConfigMcpTypeLocal, ConfigMcpTypeRemote:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Modes configuration, see https://opencode.ai/docs/modes
|
||||
type ConfigMode struct {
|
||||
Build ModeConfig `json:"build"`
|
||||
Plan ModeConfig `json:"plan"`
|
||||
ExtraFields map[string]ModeConfig `json:"-,extras"`
|
||||
JSON configModeJSON `json:"-"`
|
||||
}
|
||||
|
||||
// configModeJSON contains the JSON metadata for the struct [ConfigMode]
|
||||
type configModeJSON struct {
|
||||
Build apijson.Field
|
||||
Plan apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *ConfigMode) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r configModeJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type ConfigPermission struct {
|
||||
Bash ConfigPermissionBashUnion `json:"bash"`
|
||||
Edit ConfigPermissionEdit `json:"edit"`
|
||||
JSON configPermissionJSON `json:"-"`
|
||||
}
|
||||
|
||||
// configPermissionJSON contains the JSON metadata for the struct
|
||||
// [ConfigPermission]
|
||||
type configPermissionJSON struct {
|
||||
Bash apijson.Field
|
||||
Edit apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *ConfigPermission) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r configPermissionJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
// Union satisfied by [ConfigPermissionBashString] or [ConfigPermissionBashMap].
|
||||
type ConfigPermissionBashUnion interface {
|
||||
implementsConfigPermissionBashUnion()
|
||||
}
|
||||
|
||||
func init() {
|
||||
apijson.RegisterUnion(
|
||||
reflect.TypeOf((*ConfigPermissionBashUnion)(nil)).Elem(),
|
||||
"",
|
||||
apijson.UnionVariant{
|
||||
TypeFilter: gjson.String,
|
||||
Type: reflect.TypeOf(ConfigPermissionBashString("")),
|
||||
},
|
||||
apijson.UnionVariant{
|
||||
TypeFilter: gjson.JSON,
|
||||
Type: reflect.TypeOf(ConfigPermissionBashMap{}),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
type ConfigPermissionBashString string
|
||||
|
||||
const (
|
||||
ConfigPermissionBashStringAsk ConfigPermissionBashString = "ask"
|
||||
ConfigPermissionBashStringAllow ConfigPermissionBashString = "allow"
|
||||
)
|
||||
|
||||
func (r ConfigPermissionBashString) IsKnown() bool {
|
||||
switch r {
|
||||
case ConfigPermissionBashStringAsk, ConfigPermissionBashStringAllow:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (r ConfigPermissionBashString) implementsConfigPermissionBashUnion() {}
|
||||
|
||||
type ConfigPermissionBashMap map[string]ConfigPermissionBashMapItem
|
||||
|
||||
func (r ConfigPermissionBashMap) implementsConfigPermissionBashUnion() {}
|
||||
|
||||
type ConfigPermissionBashMapItem string
|
||||
|
||||
const (
|
||||
ConfigPermissionBashMapAsk ConfigPermissionBashMapItem = "ask"
|
||||
ConfigPermissionBashMapAllow ConfigPermissionBashMapItem = "allow"
|
||||
)
|
||||
|
||||
func (r ConfigPermissionBashMapItem) IsKnown() bool {
|
||||
switch r {
|
||||
case ConfigPermissionBashMapAsk, ConfigPermissionBashMapAllow:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type ConfigPermissionEdit string
|
||||
|
||||
const (
|
||||
ConfigPermissionEditAsk ConfigPermissionEdit = "ask"
|
||||
ConfigPermissionEditAllow ConfigPermissionEdit = "allow"
|
||||
)
|
||||
|
||||
func (r ConfigPermissionEdit) IsKnown() bool {
|
||||
switch r {
|
||||
case ConfigPermissionEditAsk, ConfigPermissionEditAllow:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type ConfigProvider struct {
|
||||
Models map[string]ConfigProviderModel `json:"models,required"`
|
||||
ID string `json:"id"`
|
||||
API string `json:"api"`
|
||||
Env []string `json:"env"`
|
||||
Name string `json:"name"`
|
||||
Npm string `json:"npm"`
|
||||
Options ConfigProviderOptions `json:"options"`
|
||||
JSON configProviderJSON `json:"-"`
|
||||
}
|
||||
|
||||
// configProviderJSON contains the JSON metadata for the struct [ConfigProvider]
|
||||
type configProviderJSON struct {
|
||||
Models apijson.Field
|
||||
ID apijson.Field
|
||||
API apijson.Field
|
||||
Env apijson.Field
|
||||
Name apijson.Field
|
||||
Npm apijson.Field
|
||||
Options apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *ConfigProvider) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r configProviderJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type ConfigProviderModel struct {
|
||||
ID string `json:"id"`
|
||||
Attachment bool `json:"attachment"`
|
||||
Cost ConfigProviderModelsCost `json:"cost"`
|
||||
Limit ConfigProviderModelsLimit `json:"limit"`
|
||||
Name string `json:"name"`
|
||||
Options map[string]interface{} `json:"options"`
|
||||
Reasoning bool `json:"reasoning"`
|
||||
ReleaseDate string `json:"release_date"`
|
||||
Temperature bool `json:"temperature"`
|
||||
ToolCall bool `json:"tool_call"`
|
||||
JSON configProviderModelJSON `json:"-"`
|
||||
}
|
||||
|
||||
// configProviderModelJSON contains the JSON metadata for the struct
|
||||
// [ConfigProviderModel]
|
||||
type configProviderModelJSON struct {
|
||||
ID apijson.Field
|
||||
Attachment apijson.Field
|
||||
Cost apijson.Field
|
||||
Limit apijson.Field
|
||||
Name apijson.Field
|
||||
Options apijson.Field
|
||||
Reasoning apijson.Field
|
||||
ReleaseDate apijson.Field
|
||||
Temperature apijson.Field
|
||||
ToolCall apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *ConfigProviderModel) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r configProviderModelJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type ConfigProviderModelsCost struct {
|
||||
Input float64 `json:"input,required"`
|
||||
Output float64 `json:"output,required"`
|
||||
CacheRead float64 `json:"cache_read"`
|
||||
CacheWrite float64 `json:"cache_write"`
|
||||
JSON configProviderModelsCostJSON `json:"-"`
|
||||
}
|
||||
|
||||
// configProviderModelsCostJSON contains the JSON metadata for the struct
|
||||
// [ConfigProviderModelsCost]
|
||||
type configProviderModelsCostJSON struct {
|
||||
Input apijson.Field
|
||||
Output apijson.Field
|
||||
CacheRead apijson.Field
|
||||
CacheWrite apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *ConfigProviderModelsCost) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r configProviderModelsCostJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type ConfigProviderModelsLimit struct {
|
||||
Context float64 `json:"context,required"`
|
||||
Output float64 `json:"output,required"`
|
||||
JSON configProviderModelsLimitJSON `json:"-"`
|
||||
}
|
||||
|
||||
// configProviderModelsLimitJSON contains the JSON metadata for the struct
|
||||
// [ConfigProviderModelsLimit]
|
||||
type configProviderModelsLimitJSON struct {
|
||||
Context apijson.Field
|
||||
Output apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *ConfigProviderModelsLimit) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r configProviderModelsLimitJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type ConfigProviderOptions struct {
|
||||
APIKey string `json:"apiKey"`
|
||||
BaseURL string `json:"baseURL"`
|
||||
ExtraFields map[string]interface{} `json:"-,extras"`
|
||||
JSON configProviderOptionsJSON `json:"-"`
|
||||
}
|
||||
|
||||
// configProviderOptionsJSON contains the JSON metadata for the struct
|
||||
// [ConfigProviderOptions]
|
||||
type configProviderOptionsJSON struct {
|
||||
APIKey apijson.Field
|
||||
BaseURL apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *ConfigProviderOptions) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r configProviderOptionsJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
// Control sharing behavior:'manual' allows manual sharing via commands, 'auto'
|
||||
// enables automatic sharing, 'disabled' disables all sharing
|
||||
type ConfigShare string
|
||||
|
||||
const (
|
||||
ConfigShareManual ConfigShare = "manual"
|
||||
ConfigShareAuto ConfigShare = "auto"
|
||||
ConfigShareDisabled ConfigShare = "disabled"
|
||||
)
|
||||
|
||||
func (r ConfigShare) IsKnown() bool {
|
||||
switch r {
|
||||
case ConfigShareManual, ConfigShareAuto, ConfigShareDisabled:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type KeybindsConfig struct {
|
||||
// Exit the application
|
||||
AppExit string `json:"app_exit,required"`
|
||||
// Show help dialog
|
||||
AppHelp string `json:"app_help,required"`
|
||||
// Open external editor
|
||||
EditorOpen string `json:"editor_open,required"`
|
||||
// Close file
|
||||
FileClose string `json:"file_close,required"`
|
||||
// Split/unified diff
|
||||
FileDiffToggle string `json:"file_diff_toggle,required"`
|
||||
// List files
|
||||
FileList string `json:"file_list,required"`
|
||||
// Search file
|
||||
FileSearch string `json:"file_search,required"`
|
||||
// Clear input field
|
||||
InputClear string `json:"input_clear,required"`
|
||||
// Insert newline in input
|
||||
InputNewline string `json:"input_newline,required"`
|
||||
// Paste from clipboard
|
||||
InputPaste string `json:"input_paste,required"`
|
||||
// Submit input
|
||||
InputSubmit string `json:"input_submit,required"`
|
||||
// Leader key for keybind combinations
|
||||
Leader string `json:"leader,required"`
|
||||
// Copy message
|
||||
MessagesCopy string `json:"messages_copy,required"`
|
||||
// Navigate to first message
|
||||
MessagesFirst string `json:"messages_first,required"`
|
||||
// Scroll messages down by half page
|
||||
MessagesHalfPageDown string `json:"messages_half_page_down,required"`
|
||||
// Scroll messages up by half page
|
||||
MessagesHalfPageUp string `json:"messages_half_page_up,required"`
|
||||
// Navigate to last message
|
||||
MessagesLast string `json:"messages_last,required"`
|
||||
// Toggle layout
|
||||
MessagesLayoutToggle string `json:"messages_layout_toggle,required"`
|
||||
// Navigate to next message
|
||||
MessagesNext string `json:"messages_next,required"`
|
||||
// Scroll messages down by one page
|
||||
MessagesPageDown string `json:"messages_page_down,required"`
|
||||
// Scroll messages up by one page
|
||||
MessagesPageUp string `json:"messages_page_up,required"`
|
||||
// Navigate to previous message
|
||||
MessagesPrevious string `json:"messages_previous,required"`
|
||||
// Redo message
|
||||
MessagesRedo string `json:"messages_redo,required"`
|
||||
// @deprecated use messages_undo. Revert message
|
||||
MessagesRevert string `json:"messages_revert,required"`
|
||||
// Undo message
|
||||
MessagesUndo string `json:"messages_undo,required"`
|
||||
// List available models
|
||||
ModelList string `json:"model_list,required"`
|
||||
// Create/update AGENTS.md
|
||||
ProjectInit string `json:"project_init,required"`
|
||||
// Compact the session
|
||||
SessionCompact string `json:"session_compact,required"`
|
||||
// Export session to editor
|
||||
SessionExport string `json:"session_export,required"`
|
||||
// Interrupt current session
|
||||
SessionInterrupt string `json:"session_interrupt,required"`
|
||||
// List all sessions
|
||||
SessionList string `json:"session_list,required"`
|
||||
// Create a new session
|
||||
SessionNew string `json:"session_new,required"`
|
||||
// Share current session
|
||||
SessionShare string `json:"session_share,required"`
|
||||
// Unshare current session
|
||||
SessionUnshare string `json:"session_unshare,required"`
|
||||
// Next mode
|
||||
SwitchMode string `json:"switch_mode,required"`
|
||||
// Previous Mode
|
||||
SwitchModeReverse string `json:"switch_mode_reverse,required"`
|
||||
// List available themes
|
||||
ThemeList string `json:"theme_list,required"`
|
||||
// Toggle tool details
|
||||
ToolDetails string `json:"tool_details,required"`
|
||||
JSON keybindsConfigJSON `json:"-"`
|
||||
}
|
||||
|
||||
// keybindsConfigJSON contains the JSON metadata for the struct [KeybindsConfig]
|
||||
type keybindsConfigJSON struct {
|
||||
AppExit apijson.Field
|
||||
AppHelp apijson.Field
|
||||
EditorOpen apijson.Field
|
||||
FileClose apijson.Field
|
||||
FileDiffToggle apijson.Field
|
||||
FileList apijson.Field
|
||||
FileSearch apijson.Field
|
||||
InputClear apijson.Field
|
||||
InputNewline apijson.Field
|
||||
InputPaste apijson.Field
|
||||
InputSubmit apijson.Field
|
||||
Leader apijson.Field
|
||||
MessagesCopy apijson.Field
|
||||
MessagesFirst apijson.Field
|
||||
MessagesHalfPageDown apijson.Field
|
||||
MessagesHalfPageUp apijson.Field
|
||||
MessagesLast apijson.Field
|
||||
MessagesLayoutToggle apijson.Field
|
||||
MessagesNext apijson.Field
|
||||
MessagesPageDown apijson.Field
|
||||
MessagesPageUp apijson.Field
|
||||
MessagesPrevious apijson.Field
|
||||
MessagesRedo apijson.Field
|
||||
MessagesRevert apijson.Field
|
||||
MessagesUndo apijson.Field
|
||||
ModelList apijson.Field
|
||||
ProjectInit apijson.Field
|
||||
SessionCompact apijson.Field
|
||||
SessionExport apijson.Field
|
||||
SessionInterrupt apijson.Field
|
||||
SessionList apijson.Field
|
||||
SessionNew apijson.Field
|
||||
SessionShare apijson.Field
|
||||
SessionUnshare apijson.Field
|
||||
SwitchMode apijson.Field
|
||||
SwitchModeReverse apijson.Field
|
||||
ThemeList apijson.Field
|
||||
ToolDetails apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *KeybindsConfig) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r keybindsConfigJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type McpLocalConfig struct {
|
||||
// Command and arguments to run the MCP server
|
||||
Command []string `json:"command,required"`
|
||||
// Type of MCP server connection
|
||||
Type McpLocalConfigType `json:"type,required"`
|
||||
// Enable or disable the MCP server on startup
|
||||
Enabled bool `json:"enabled"`
|
||||
// Environment variables to set when running the MCP server
|
||||
Environment map[string]string `json:"environment"`
|
||||
JSON mcpLocalConfigJSON `json:"-"`
|
||||
}
|
||||
|
||||
// mcpLocalConfigJSON contains the JSON metadata for the struct [McpLocalConfig]
|
||||
type mcpLocalConfigJSON struct {
|
||||
Command apijson.Field
|
||||
Type apijson.Field
|
||||
Enabled apijson.Field
|
||||
Environment apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *McpLocalConfig) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r mcpLocalConfigJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
func (r McpLocalConfig) implementsConfigMcp() {}
|
||||
|
||||
// Type of MCP server connection
|
||||
type McpLocalConfigType string
|
||||
|
||||
const (
|
||||
McpLocalConfigTypeLocal McpLocalConfigType = "local"
|
||||
)
|
||||
|
||||
func (r McpLocalConfigType) IsKnown() bool {
|
||||
switch r {
|
||||
case McpLocalConfigTypeLocal:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type McpRemoteConfig struct {
|
||||
// Type of MCP server connection
|
||||
Type McpRemoteConfigType `json:"type,required"`
|
||||
// URL of the remote MCP server
|
||||
URL string `json:"url,required"`
|
||||
// Enable or disable the MCP server on startup
|
||||
Enabled bool `json:"enabled"`
|
||||
// Headers to send with the request
|
||||
Headers map[string]string `json:"headers"`
|
||||
JSON mcpRemoteConfigJSON `json:"-"`
|
||||
}
|
||||
|
||||
// mcpRemoteConfigJSON contains the JSON metadata for the struct [McpRemoteConfig]
|
||||
type mcpRemoteConfigJSON struct {
|
||||
Type apijson.Field
|
||||
URL apijson.Field
|
||||
Enabled apijson.Field
|
||||
Headers apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *McpRemoteConfig) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r mcpRemoteConfigJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
func (r McpRemoteConfig) implementsConfigMcp() {}
|
||||
|
||||
// Type of MCP server connection
|
||||
type McpRemoteConfigType string
|
||||
|
||||
const (
|
||||
McpRemoteConfigTypeRemote McpRemoteConfigType = "remote"
|
||||
)
|
||||
|
||||
func (r McpRemoteConfigType) IsKnown() bool {
|
||||
switch r {
|
||||
case McpRemoteConfigTypeRemote:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type ModeConfig struct {
|
||||
Disable bool `json:"disable"`
|
||||
Model string `json:"model"`
|
||||
Prompt string `json:"prompt"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
Tools map[string]bool `json:"tools"`
|
||||
JSON modeConfigJSON `json:"-"`
|
||||
}
|
||||
|
||||
// modeConfigJSON contains the JSON metadata for the struct [ModeConfig]
|
||||
type modeConfigJSON struct {
|
||||
Disable apijson.Field
|
||||
Model apijson.Field
|
||||
Prompt apijson.Field
|
||||
Temperature apijson.Field
|
||||
Tools apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *ModeConfig) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r modeConfigJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
36
packages/sdk/go/config_test.go
Normal file
36
packages/sdk/go/config_test.go
Normal file
@@ -0,0 +1,36 @@
|
||||
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
package opencode_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/sst/opencode-sdk-go"
|
||||
"github.com/sst/opencode-sdk-go/internal/testutil"
|
||||
"github.com/sst/opencode-sdk-go/option"
|
||||
)
|
||||
|
||||
func TestConfigGet(t *testing.T) {
|
||||
t.Skip("skipped: tests are disabled for the time being")
|
||||
baseURL := "http://localhost:4010"
|
||||
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
|
||||
baseURL = envURL
|
||||
}
|
||||
if !testutil.CheckTestServer(t, baseURL) {
|
||||
return
|
||||
}
|
||||
client := opencode.NewClient(
|
||||
option.WithBaseURL(baseURL),
|
||||
)
|
||||
_, err := client.Config.Get(context.TODO())
|
||||
if err != nil {
|
||||
var apierr *opencode.Error
|
||||
if errors.As(err, &apierr) {
|
||||
t.Log(string(apierr.DumpRequest(true)))
|
||||
}
|
||||
t.Fatalf("err should be nil: %s", err.Error())
|
||||
}
|
||||
}
|
||||
1395
packages/sdk/go/event.go
Normal file
1395
packages/sdk/go/event.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
File generated from our OpenAPI spec by Stainless.
|
||||
|
||||
This directory can be used to store example files demonstrating usage of this SDK.
|
||||
It is ignored by Stainless code generation and its content (other than this keep file) won't be touched.
|
||||
It is ignored by Stainless code generation and its content (other than this keep file) won't be touched.
|
||||
50
packages/sdk/go/field.go
Normal file
50
packages/sdk/go/field.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package opencode
|
||||
|
||||
import (
|
||||
"github.com/sst/opencode-sdk-go/internal/param"
|
||||
"io"
|
||||
)
|
||||
|
||||
// F is a param field helper used to initialize a [param.Field] generic struct.
|
||||
// This helps specify null, zero values, and overrides, as well as normal values.
|
||||
// You can read more about this in our [README].
|
||||
//
|
||||
// [README]: https://pkg.go.dev/github.com/sst/opencode-sdk-go#readme-request-fields
|
||||
func F[T any](value T) param.Field[T] { return param.Field[T]{Value: value, Present: true} }
|
||||
|
||||
// Null is a param field helper which explicitly sends null to the API.
|
||||
func Null[T any]() param.Field[T] { return param.Field[T]{Null: true, Present: true} }
|
||||
|
||||
// Raw is a param field helper for specifying values for fields when the
|
||||
// type you are looking to send is different from the type that is specified in
|
||||
// the SDK. For example, if the type of the field is an integer, but you want
|
||||
// to send a float, you could do that by setting the corresponding field with
|
||||
// Raw[int](0.5).
|
||||
func Raw[T any](value any) param.Field[T] { return param.Field[T]{Raw: value, Present: true} }
|
||||
|
||||
// Int is a param field helper which helps specify integers. This is
|
||||
// particularly helpful when specifying integer constants for fields.
|
||||
func Int(value int64) param.Field[int64] { return F(value) }
|
||||
|
||||
// String is a param field helper which helps specify strings.
|
||||
func String(value string) param.Field[string] { return F(value) }
|
||||
|
||||
// Float is a param field helper which helps specify floats.
|
||||
func Float(value float64) param.Field[float64] { return F(value) }
|
||||
|
||||
// Bool is a param field helper which helps specify bools.
|
||||
func Bool(value bool) param.Field[bool] { return F(value) }
|
||||
|
||||
// FileParam is a param field helper which helps files with a mime content-type.
|
||||
func FileParam(reader io.Reader, filename string, contentType string) param.Field[io.Reader] {
|
||||
return F[io.Reader](&file{reader, filename, contentType})
|
||||
}
|
||||
|
||||
type file struct {
|
||||
io.Reader
|
||||
name string
|
||||
contentType string
|
||||
}
|
||||
|
||||
func (f *file) ContentType() string { return f.contentType }
|
||||
func (f *file) Filename() string { return f.name }
|
||||
142
packages/sdk/go/file.go
Normal file
142
packages/sdk/go/file.go
Normal file
@@ -0,0 +1,142 @@
|
||||
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
package opencode
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/sst/opencode-sdk-go/internal/apijson"
|
||||
"github.com/sst/opencode-sdk-go/internal/apiquery"
|
||||
"github.com/sst/opencode-sdk-go/internal/param"
|
||||
"github.com/sst/opencode-sdk-go/internal/requestconfig"
|
||||
"github.com/sst/opencode-sdk-go/option"
|
||||
)
|
||||
|
||||
// FileService contains methods and other services that help with interacting with
|
||||
// the opencode API.
|
||||
//
|
||||
// Note, unlike clients, this service does not read variables from the environment
|
||||
// automatically. You should not instantiate this service directly, and instead use
|
||||
// the [NewFileService] method instead.
|
||||
type FileService struct {
|
||||
Options []option.RequestOption
|
||||
}
|
||||
|
||||
// NewFileService generates a new service that applies the given options to each
|
||||
// request. These options are applied after the parent client's options (if there
|
||||
// is one), and before any request-specific options.
|
||||
func NewFileService(opts ...option.RequestOption) (r *FileService) {
|
||||
r = &FileService{}
|
||||
r.Options = opts
|
||||
return
|
||||
}
|
||||
|
||||
// Read a file
|
||||
func (r *FileService) Read(ctx context.Context, query FileReadParams, opts ...option.RequestOption) (res *FileReadResponse, err error) {
|
||||
opts = append(r.Options[:], opts...)
|
||||
path := "file"
|
||||
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...)
|
||||
return
|
||||
}
|
||||
|
||||
// Get file status
|
||||
func (r *FileService) Status(ctx context.Context, opts ...option.RequestOption) (res *[]File, err error) {
|
||||
opts = append(r.Options[:], opts...)
|
||||
path := "file/status"
|
||||
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
|
||||
return
|
||||
}
|
||||
|
||||
type File struct {
|
||||
Added int64 `json:"added,required"`
|
||||
Path string `json:"path,required"`
|
||||
Removed int64 `json:"removed,required"`
|
||||
Status FileStatus `json:"status,required"`
|
||||
JSON fileJSON `json:"-"`
|
||||
}
|
||||
|
||||
// fileJSON contains the JSON metadata for the struct [File]
|
||||
type fileJSON struct {
|
||||
Added apijson.Field
|
||||
Path apijson.Field
|
||||
Removed apijson.Field
|
||||
Status apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *File) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r fileJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type FileStatus string
|
||||
|
||||
const (
|
||||
FileStatusAdded FileStatus = "added"
|
||||
FileStatusDeleted FileStatus = "deleted"
|
||||
FileStatusModified FileStatus = "modified"
|
||||
)
|
||||
|
||||
func (r FileStatus) IsKnown() bool {
|
||||
switch r {
|
||||
case FileStatusAdded, FileStatusDeleted, FileStatusModified:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type FileReadResponse struct {
|
||||
Content string `json:"content,required"`
|
||||
Type FileReadResponseType `json:"type,required"`
|
||||
JSON fileReadResponseJSON `json:"-"`
|
||||
}
|
||||
|
||||
// fileReadResponseJSON contains the JSON metadata for the struct
|
||||
// [FileReadResponse]
|
||||
type fileReadResponseJSON struct {
|
||||
Content apijson.Field
|
||||
Type apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *FileReadResponse) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r fileReadResponseJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type FileReadResponseType string
|
||||
|
||||
const (
|
||||
FileReadResponseTypeRaw FileReadResponseType = "raw"
|
||||
FileReadResponseTypePatch FileReadResponseType = "patch"
|
||||
)
|
||||
|
||||
func (r FileReadResponseType) IsKnown() bool {
|
||||
switch r {
|
||||
case FileReadResponseTypeRaw, FileReadResponseTypePatch:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type FileReadParams struct {
|
||||
Path param.Field[string] `query:"path,required"`
|
||||
}
|
||||
|
||||
// URLQuery serializes [FileReadParams]'s query parameters as `url.Values`.
|
||||
func (r FileReadParams) URLQuery() (v url.Values) {
|
||||
return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{
|
||||
ArrayFormat: apiquery.ArrayQueryFormatComma,
|
||||
NestedFormat: apiquery.NestedQueryFormatBrackets,
|
||||
})
|
||||
}
|
||||
60
packages/sdk/go/file_test.go
Normal file
60
packages/sdk/go/file_test.go
Normal file
@@ -0,0 +1,60 @@
|
||||
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
package opencode_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/sst/opencode-sdk-go"
|
||||
"github.com/sst/opencode-sdk-go/internal/testutil"
|
||||
"github.com/sst/opencode-sdk-go/option"
|
||||
)
|
||||
|
||||
func TestFileRead(t *testing.T) {
|
||||
t.Skip("skipped: tests are disabled for the time being")
|
||||
baseURL := "http://localhost:4010"
|
||||
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
|
||||
baseURL = envURL
|
||||
}
|
||||
if !testutil.CheckTestServer(t, baseURL) {
|
||||
return
|
||||
}
|
||||
client := opencode.NewClient(
|
||||
option.WithBaseURL(baseURL),
|
||||
)
|
||||
_, err := client.File.Read(context.TODO(), opencode.FileReadParams{
|
||||
Path: opencode.F("path"),
|
||||
})
|
||||
if err != nil {
|
||||
var apierr *opencode.Error
|
||||
if errors.As(err, &apierr) {
|
||||
t.Log(string(apierr.DumpRequest(true)))
|
||||
}
|
||||
t.Fatalf("err should be nil: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileStatus(t *testing.T) {
|
||||
t.Skip("skipped: tests are disabled for the time being")
|
||||
baseURL := "http://localhost:4010"
|
||||
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
|
||||
baseURL = envURL
|
||||
}
|
||||
if !testutil.CheckTestServer(t, baseURL) {
|
||||
return
|
||||
}
|
||||
client := opencode.NewClient(
|
||||
option.WithBaseURL(baseURL),
|
||||
)
|
||||
_, err := client.File.Status(context.TODO())
|
||||
if err != nil {
|
||||
var apierr *opencode.Error
|
||||
if errors.As(err, &apierr) {
|
||||
t.Log(string(apierr.DumpRequest(true)))
|
||||
}
|
||||
t.Fatalf("err should be nil: %s", err.Error())
|
||||
}
|
||||
}
|
||||
326
packages/sdk/go/find.go
Normal file
326
packages/sdk/go/find.go
Normal file
@@ -0,0 +1,326 @@
|
||||
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
package opencode
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/sst/opencode-sdk-go/internal/apijson"
|
||||
"github.com/sst/opencode-sdk-go/internal/apiquery"
|
||||
"github.com/sst/opencode-sdk-go/internal/param"
|
||||
"github.com/sst/opencode-sdk-go/internal/requestconfig"
|
||||
"github.com/sst/opencode-sdk-go/option"
|
||||
)
|
||||
|
||||
// FindService contains methods and other services that help with interacting with
|
||||
// the opencode API.
|
||||
//
|
||||
// Note, unlike clients, this service does not read variables from the environment
|
||||
// automatically. You should not instantiate this service directly, and instead use
|
||||
// the [NewFindService] method instead.
|
||||
type FindService struct {
|
||||
Options []option.RequestOption
|
||||
}
|
||||
|
||||
// NewFindService generates a new service that applies the given options to each
|
||||
// request. These options are applied after the parent client's options (if there
|
||||
// is one), and before any request-specific options.
|
||||
func NewFindService(opts ...option.RequestOption) (r *FindService) {
|
||||
r = &FindService{}
|
||||
r.Options = opts
|
||||
return
|
||||
}
|
||||
|
||||
// Find files
|
||||
func (r *FindService) Files(ctx context.Context, query FindFilesParams, opts ...option.RequestOption) (res *[]string, err error) {
|
||||
opts = append(r.Options[:], opts...)
|
||||
path := "find/file"
|
||||
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...)
|
||||
return
|
||||
}
|
||||
|
||||
// Find workspace symbols
|
||||
func (r *FindService) Symbols(ctx context.Context, query FindSymbolsParams, opts ...option.RequestOption) (res *[]Symbol, err error) {
|
||||
opts = append(r.Options[:], opts...)
|
||||
path := "find/symbol"
|
||||
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...)
|
||||
return
|
||||
}
|
||||
|
||||
// Find text in files
|
||||
func (r *FindService) Text(ctx context.Context, query FindTextParams, opts ...option.RequestOption) (res *[]FindTextResponse, err error) {
|
||||
opts = append(r.Options[:], opts...)
|
||||
path := "find"
|
||||
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...)
|
||||
return
|
||||
}
|
||||
|
||||
type Symbol struct {
|
||||
Kind float64 `json:"kind,required"`
|
||||
Location SymbolLocation `json:"location,required"`
|
||||
Name string `json:"name,required"`
|
||||
JSON symbolJSON `json:"-"`
|
||||
}
|
||||
|
||||
// symbolJSON contains the JSON metadata for the struct [Symbol]
|
||||
type symbolJSON struct {
|
||||
Kind apijson.Field
|
||||
Location apijson.Field
|
||||
Name apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *Symbol) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r symbolJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type SymbolLocation struct {
|
||||
Range SymbolLocationRange `json:"range,required"`
|
||||
Uri string `json:"uri,required"`
|
||||
JSON symbolLocationJSON `json:"-"`
|
||||
}
|
||||
|
||||
// symbolLocationJSON contains the JSON metadata for the struct [SymbolLocation]
|
||||
type symbolLocationJSON struct {
|
||||
Range apijson.Field
|
||||
Uri apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *SymbolLocation) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r symbolLocationJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type SymbolLocationRange struct {
|
||||
End SymbolLocationRangeEnd `json:"end,required"`
|
||||
Start SymbolLocationRangeStart `json:"start,required"`
|
||||
JSON symbolLocationRangeJSON `json:"-"`
|
||||
}
|
||||
|
||||
// symbolLocationRangeJSON contains the JSON metadata for the struct
|
||||
// [SymbolLocationRange]
|
||||
type symbolLocationRangeJSON struct {
|
||||
End apijson.Field
|
||||
Start apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *SymbolLocationRange) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r symbolLocationRangeJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type SymbolLocationRangeEnd struct {
|
||||
Character float64 `json:"character,required"`
|
||||
Line float64 `json:"line,required"`
|
||||
JSON symbolLocationRangeEndJSON `json:"-"`
|
||||
}
|
||||
|
||||
// symbolLocationRangeEndJSON contains the JSON metadata for the struct
|
||||
// [SymbolLocationRangeEnd]
|
||||
type symbolLocationRangeEndJSON struct {
|
||||
Character apijson.Field
|
||||
Line apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *SymbolLocationRangeEnd) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r symbolLocationRangeEndJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type SymbolLocationRangeStart struct {
|
||||
Character float64 `json:"character,required"`
|
||||
Line float64 `json:"line,required"`
|
||||
JSON symbolLocationRangeStartJSON `json:"-"`
|
||||
}
|
||||
|
||||
// symbolLocationRangeStartJSON contains the JSON metadata for the struct
|
||||
// [SymbolLocationRangeStart]
|
||||
type symbolLocationRangeStartJSON struct {
|
||||
Character apijson.Field
|
||||
Line apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *SymbolLocationRangeStart) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r symbolLocationRangeStartJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type FindTextResponse struct {
|
||||
AbsoluteOffset float64 `json:"absolute_offset,required"`
|
||||
LineNumber float64 `json:"line_number,required"`
|
||||
Lines FindTextResponseLines `json:"lines,required"`
|
||||
Path FindTextResponsePath `json:"path,required"`
|
||||
Submatches []FindTextResponseSubmatch `json:"submatches,required"`
|
||||
JSON findTextResponseJSON `json:"-"`
|
||||
}
|
||||
|
||||
// findTextResponseJSON contains the JSON metadata for the struct
|
||||
// [FindTextResponse]
|
||||
type findTextResponseJSON struct {
|
||||
AbsoluteOffset apijson.Field
|
||||
LineNumber apijson.Field
|
||||
Lines apijson.Field
|
||||
Path apijson.Field
|
||||
Submatches apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *FindTextResponse) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r findTextResponseJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type FindTextResponseLines struct {
|
||||
Text string `json:"text,required"`
|
||||
JSON findTextResponseLinesJSON `json:"-"`
|
||||
}
|
||||
|
||||
// findTextResponseLinesJSON contains the JSON metadata for the struct
|
||||
// [FindTextResponseLines]
|
||||
type findTextResponseLinesJSON struct {
|
||||
Text apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *FindTextResponseLines) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r findTextResponseLinesJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type FindTextResponsePath struct {
|
||||
Text string `json:"text,required"`
|
||||
JSON findTextResponsePathJSON `json:"-"`
|
||||
}
|
||||
|
||||
// findTextResponsePathJSON contains the JSON metadata for the struct
|
||||
// [FindTextResponsePath]
|
||||
type findTextResponsePathJSON struct {
|
||||
Text apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *FindTextResponsePath) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r findTextResponsePathJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type FindTextResponseSubmatch struct {
|
||||
End float64 `json:"end,required"`
|
||||
Match FindTextResponseSubmatchesMatch `json:"match,required"`
|
||||
Start float64 `json:"start,required"`
|
||||
JSON findTextResponseSubmatchJSON `json:"-"`
|
||||
}
|
||||
|
||||
// findTextResponseSubmatchJSON contains the JSON metadata for the struct
|
||||
// [FindTextResponseSubmatch]
|
||||
type findTextResponseSubmatchJSON struct {
|
||||
End apijson.Field
|
||||
Match apijson.Field
|
||||
Start apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *FindTextResponseSubmatch) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r findTextResponseSubmatchJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type FindTextResponseSubmatchesMatch struct {
|
||||
Text string `json:"text,required"`
|
||||
JSON findTextResponseSubmatchesMatchJSON `json:"-"`
|
||||
}
|
||||
|
||||
// findTextResponseSubmatchesMatchJSON contains the JSON metadata for the struct
|
||||
// [FindTextResponseSubmatchesMatch]
|
||||
type findTextResponseSubmatchesMatchJSON struct {
|
||||
Text apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *FindTextResponseSubmatchesMatch) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r findTextResponseSubmatchesMatchJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type FindFilesParams struct {
|
||||
Query param.Field[string] `query:"query,required"`
|
||||
}
|
||||
|
||||
// URLQuery serializes [FindFilesParams]'s query parameters as `url.Values`.
|
||||
func (r FindFilesParams) URLQuery() (v url.Values) {
|
||||
return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{
|
||||
ArrayFormat: apiquery.ArrayQueryFormatComma,
|
||||
NestedFormat: apiquery.NestedQueryFormatBrackets,
|
||||
})
|
||||
}
|
||||
|
||||
type FindSymbolsParams struct {
|
||||
Query param.Field[string] `query:"query,required"`
|
||||
}
|
||||
|
||||
// URLQuery serializes [FindSymbolsParams]'s query parameters as `url.Values`.
|
||||
func (r FindSymbolsParams) URLQuery() (v url.Values) {
|
||||
return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{
|
||||
ArrayFormat: apiquery.ArrayQueryFormatComma,
|
||||
NestedFormat: apiquery.NestedQueryFormatBrackets,
|
||||
})
|
||||
}
|
||||
|
||||
type FindTextParams struct {
|
||||
Pattern param.Field[string] `query:"pattern,required"`
|
||||
}
|
||||
|
||||
// URLQuery serializes [FindTextParams]'s query parameters as `url.Values`.
|
||||
func (r FindTextParams) URLQuery() (v url.Values) {
|
||||
return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{
|
||||
ArrayFormat: apiquery.ArrayQueryFormatComma,
|
||||
NestedFormat: apiquery.NestedQueryFormatBrackets,
|
||||
})
|
||||
}
|
||||
86
packages/sdk/go/find_test.go
Normal file
86
packages/sdk/go/find_test.go
Normal file
@@ -0,0 +1,86 @@
|
||||
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
package opencode_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/sst/opencode-sdk-go"
|
||||
"github.com/sst/opencode-sdk-go/internal/testutil"
|
||||
"github.com/sst/opencode-sdk-go/option"
|
||||
)
|
||||
|
||||
func TestFindFiles(t *testing.T) {
|
||||
t.Skip("skipped: tests are disabled for the time being")
|
||||
baseURL := "http://localhost:4010"
|
||||
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
|
||||
baseURL = envURL
|
||||
}
|
||||
if !testutil.CheckTestServer(t, baseURL) {
|
||||
return
|
||||
}
|
||||
client := opencode.NewClient(
|
||||
option.WithBaseURL(baseURL),
|
||||
)
|
||||
_, err := client.Find.Files(context.TODO(), opencode.FindFilesParams{
|
||||
Query: opencode.F("query"),
|
||||
})
|
||||
if err != nil {
|
||||
var apierr *opencode.Error
|
||||
if errors.As(err, &apierr) {
|
||||
t.Log(string(apierr.DumpRequest(true)))
|
||||
}
|
||||
t.Fatalf("err should be nil: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindSymbols(t *testing.T) {
|
||||
t.Skip("skipped: tests are disabled for the time being")
|
||||
baseURL := "http://localhost:4010"
|
||||
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
|
||||
baseURL = envURL
|
||||
}
|
||||
if !testutil.CheckTestServer(t, baseURL) {
|
||||
return
|
||||
}
|
||||
client := opencode.NewClient(
|
||||
option.WithBaseURL(baseURL),
|
||||
)
|
||||
_, err := client.Find.Symbols(context.TODO(), opencode.FindSymbolsParams{
|
||||
Query: opencode.F("query"),
|
||||
})
|
||||
if err != nil {
|
||||
var apierr *opencode.Error
|
||||
if errors.As(err, &apierr) {
|
||||
t.Log(string(apierr.DumpRequest(true)))
|
||||
}
|
||||
t.Fatalf("err should be nil: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindText(t *testing.T) {
|
||||
t.Skip("skipped: tests are disabled for the time being")
|
||||
baseURL := "http://localhost:4010"
|
||||
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
|
||||
baseURL = envURL
|
||||
}
|
||||
if !testutil.CheckTestServer(t, baseURL) {
|
||||
return
|
||||
}
|
||||
client := opencode.NewClient(
|
||||
option.WithBaseURL(baseURL),
|
||||
)
|
||||
_, err := client.Find.Text(context.TODO(), opencode.FindTextParams{
|
||||
Pattern: opencode.F("pattern"),
|
||||
})
|
||||
if err != nil {
|
||||
var apierr *opencode.Error
|
||||
if errors.As(err, &apierr) {
|
||||
t.Log(string(apierr.DumpRequest(true)))
|
||||
}
|
||||
t.Fatalf("err should be nil: %s", err.Error())
|
||||
}
|
||||
}
|
||||
13
packages/sdk/go/go.mod
Normal file
13
packages/sdk/go/go.mod
Normal file
@@ -0,0 +1,13 @@
|
||||
module github.com/sst/opencode-sdk-go
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/tidwall/gjson v1.14.4
|
||||
github.com/tidwall/sjson v1.2.5
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
)
|
||||
10
packages/sdk/go/go.sum
Normal file
10
packages/sdk/go/go.sum
Normal file
@@ -0,0 +1,10 @@
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
|
||||
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
53
packages/sdk/go/internal/apierror/apierror.go
Normal file
53
packages/sdk/go/internal/apierror/apierror.go
Normal file
@@ -0,0 +1,53 @@
|
||||
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||
|
||||
package apierror
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
|
||||
"github.com/sst/opencode-sdk-go/internal/apijson"
|
||||
)
|
||||
|
||||
// Error represents an error that originates from the API, i.e. when a request is
|
||||
// made and the API returns a response with a HTTP status code. Other errors are
|
||||
// not wrapped by this SDK.
|
||||
type Error struct {
|
||||
JSON errorJSON `json:"-"`
|
||||
StatusCode int
|
||||
Request *http.Request
|
||||
Response *http.Response
|
||||
}
|
||||
|
||||
// errorJSON contains the JSON metadata for the struct [Error]
|
||||
type errorJSON struct {
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *Error) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r errorJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
func (r *Error) Error() string {
|
||||
// Attempt to re-populate the response body
|
||||
return fmt.Sprintf("%s \"%s\": %d %s %s", r.Request.Method, r.Request.URL, r.Response.StatusCode, http.StatusText(r.Response.StatusCode), r.JSON.RawJSON())
|
||||
}
|
||||
|
||||
func (r *Error) DumpRequest(body bool) []byte {
|
||||
if r.Request.GetBody != nil {
|
||||
r.Request.Body, _ = r.Request.GetBody()
|
||||
}
|
||||
out, _ := httputil.DumpRequestOut(r.Request, body)
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *Error) DumpResponse(body bool) []byte {
|
||||
out, _ := httputil.DumpResponse(r.Response, body)
|
||||
return out
|
||||
}
|
||||
383
packages/sdk/go/internal/apiform/encoder.go
Normal file
383
packages/sdk/go/internal/apiform/encoder.go
Normal file
@@ -0,0 +1,383 @@
|
||||
package apiform
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/textproto"
|
||||
"path"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sst/opencode-sdk-go/internal/param"
|
||||
)
|
||||
|
||||
var encoders sync.Map // map[encoderEntry]encoderFunc
|
||||
|
||||
func Marshal(value interface{}, writer *multipart.Writer) error {
|
||||
e := &encoder{dateFormat: time.RFC3339}
|
||||
return e.marshal(value, writer)
|
||||
}
|
||||
|
||||
func MarshalRoot(value interface{}, writer *multipart.Writer) error {
|
||||
e := &encoder{root: true, dateFormat: time.RFC3339}
|
||||
return e.marshal(value, writer)
|
||||
}
|
||||
|
||||
type encoder struct {
|
||||
dateFormat string
|
||||
root bool
|
||||
}
|
||||
|
||||
type encoderFunc func(key string, value reflect.Value, writer *multipart.Writer) error
|
||||
|
||||
type encoderField struct {
|
||||
tag parsedStructTag
|
||||
fn encoderFunc
|
||||
idx []int
|
||||
}
|
||||
|
||||
type encoderEntry struct {
|
||||
reflect.Type
|
||||
dateFormat string
|
||||
root bool
|
||||
}
|
||||
|
||||
func (e *encoder) marshal(value interface{}, writer *multipart.Writer) error {
|
||||
val := reflect.ValueOf(value)
|
||||
if !val.IsValid() {
|
||||
return nil
|
||||
}
|
||||
typ := val.Type()
|
||||
enc := e.typeEncoder(typ)
|
||||
return enc("", val, writer)
|
||||
}
|
||||
|
||||
func (e *encoder) typeEncoder(t reflect.Type) encoderFunc {
|
||||
entry := encoderEntry{
|
||||
Type: t,
|
||||
dateFormat: e.dateFormat,
|
||||
root: e.root,
|
||||
}
|
||||
|
||||
if fi, ok := encoders.Load(entry); ok {
|
||||
return fi.(encoderFunc)
|
||||
}
|
||||
|
||||
// To deal with recursive types, populate the map with an
|
||||
// indirect func before we build it. This type waits on the
|
||||
// real func (f) to be ready and then calls it. This indirect
|
||||
// func is only used for recursive types.
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
f encoderFunc
|
||||
)
|
||||
wg.Add(1)
|
||||
fi, loaded := encoders.LoadOrStore(entry, encoderFunc(func(key string, v reflect.Value, writer *multipart.Writer) error {
|
||||
wg.Wait()
|
||||
return f(key, v, writer)
|
||||
}))
|
||||
if loaded {
|
||||
return fi.(encoderFunc)
|
||||
}
|
||||
|
||||
// Compute the real encoder and replace the indirect func with it.
|
||||
f = e.newTypeEncoder(t)
|
||||
wg.Done()
|
||||
encoders.Store(entry, f)
|
||||
return f
|
||||
}
|
||||
|
||||
func (e *encoder) newTypeEncoder(t reflect.Type) encoderFunc {
|
||||
if t.ConvertibleTo(reflect.TypeOf(time.Time{})) {
|
||||
return e.newTimeTypeEncoder()
|
||||
}
|
||||
if t.ConvertibleTo(reflect.TypeOf((*io.Reader)(nil)).Elem()) {
|
||||
return e.newReaderTypeEncoder()
|
||||
}
|
||||
e.root = false
|
||||
switch t.Kind() {
|
||||
case reflect.Pointer:
|
||||
inner := t.Elem()
|
||||
|
||||
innerEncoder := e.typeEncoder(inner)
|
||||
return func(key string, v reflect.Value, writer *multipart.Writer) error {
|
||||
if !v.IsValid() || v.IsNil() {
|
||||
return nil
|
||||
}
|
||||
return innerEncoder(key, v.Elem(), writer)
|
||||
}
|
||||
case reflect.Struct:
|
||||
return e.newStructTypeEncoder(t)
|
||||
case reflect.Slice, reflect.Array:
|
||||
return e.newArrayTypeEncoder(t)
|
||||
case reflect.Map:
|
||||
return e.newMapEncoder(t)
|
||||
case reflect.Interface:
|
||||
return e.newInterfaceEncoder()
|
||||
default:
|
||||
return e.newPrimitiveTypeEncoder(t)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *encoder) newPrimitiveTypeEncoder(t reflect.Type) encoderFunc {
|
||||
switch t.Kind() {
|
||||
// Note that we could use `gjson` to encode these types but it would complicate our
|
||||
// code more and this current code shouldn't cause any issues
|
||||
case reflect.String:
|
||||
return func(key string, v reflect.Value, writer *multipart.Writer) error {
|
||||
return writer.WriteField(key, v.String())
|
||||
}
|
||||
case reflect.Bool:
|
||||
return func(key string, v reflect.Value, writer *multipart.Writer) error {
|
||||
if v.Bool() {
|
||||
return writer.WriteField(key, "true")
|
||||
}
|
||||
return writer.WriteField(key, "false")
|
||||
}
|
||||
case reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return func(key string, v reflect.Value, writer *multipart.Writer) error {
|
||||
return writer.WriteField(key, strconv.FormatInt(v.Int(), 10))
|
||||
}
|
||||
case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return func(key string, v reflect.Value, writer *multipart.Writer) error {
|
||||
return writer.WriteField(key, strconv.FormatUint(v.Uint(), 10))
|
||||
}
|
||||
case reflect.Float32:
|
||||
return func(key string, v reflect.Value, writer *multipart.Writer) error {
|
||||
return writer.WriteField(key, strconv.FormatFloat(v.Float(), 'f', -1, 32))
|
||||
}
|
||||
case reflect.Float64:
|
||||
return func(key string, v reflect.Value, writer *multipart.Writer) error {
|
||||
return writer.WriteField(key, strconv.FormatFloat(v.Float(), 'f', -1, 64))
|
||||
}
|
||||
default:
|
||||
return func(key string, v reflect.Value, writer *multipart.Writer) error {
|
||||
return fmt.Errorf("unknown type received at primitive encoder: %s", t.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *encoder) newArrayTypeEncoder(t reflect.Type) encoderFunc {
|
||||
itemEncoder := e.typeEncoder(t.Elem())
|
||||
|
||||
return func(key string, v reflect.Value, writer *multipart.Writer) error {
|
||||
if key != "" {
|
||||
key = key + "."
|
||||
}
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
err := itemEncoder(key+strconv.Itoa(i), v.Index(i), writer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc {
|
||||
if t.Implements(reflect.TypeOf((*param.FieldLike)(nil)).Elem()) {
|
||||
return e.newFieldTypeEncoder(t)
|
||||
}
|
||||
|
||||
encoderFields := []encoderField{}
|
||||
extraEncoder := (*encoderField)(nil)
|
||||
|
||||
// This helper allows us to recursively collect field encoders into a flat
|
||||
// array. The parameter `index` keeps track of the access patterns necessary
|
||||
// to get to some field.
|
||||
var collectEncoderFields func(r reflect.Type, index []int)
|
||||
collectEncoderFields = func(r reflect.Type, index []int) {
|
||||
for i := 0; i < r.NumField(); i++ {
|
||||
idx := append(index, i)
|
||||
field := t.FieldByIndex(idx)
|
||||
if !field.IsExported() {
|
||||
continue
|
||||
}
|
||||
// If this is an embedded struct, traverse one level deeper to extract
|
||||
// the field and get their encoders as well.
|
||||
if field.Anonymous {
|
||||
collectEncoderFields(field.Type, idx)
|
||||
continue
|
||||
}
|
||||
// If json tag is not present, then we skip, which is intentionally
|
||||
// different behavior from the stdlib.
|
||||
ptag, ok := parseFormStructTag(field)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
// We only want to support unexported field if they're tagged with
|
||||
// `extras` because that field shouldn't be part of the public API. We
|
||||
// also want to only keep the top level extras
|
||||
if ptag.extras && len(index) == 0 {
|
||||
extraEncoder = &encoderField{ptag, e.typeEncoder(field.Type.Elem()), idx}
|
||||
continue
|
||||
}
|
||||
if ptag.name == "-" {
|
||||
continue
|
||||
}
|
||||
|
||||
dateFormat, ok := parseFormatStructTag(field)
|
||||
oldFormat := e.dateFormat
|
||||
if ok {
|
||||
switch dateFormat {
|
||||
case "date-time":
|
||||
e.dateFormat = time.RFC3339
|
||||
case "date":
|
||||
e.dateFormat = "2006-01-02"
|
||||
}
|
||||
}
|
||||
encoderFields = append(encoderFields, encoderField{ptag, e.typeEncoder(field.Type), idx})
|
||||
e.dateFormat = oldFormat
|
||||
}
|
||||
}
|
||||
collectEncoderFields(t, []int{})
|
||||
|
||||
// Ensure deterministic output by sorting by lexicographic order
|
||||
sort.Slice(encoderFields, func(i, j int) bool {
|
||||
return encoderFields[i].tag.name < encoderFields[j].tag.name
|
||||
})
|
||||
|
||||
return func(key string, value reflect.Value, writer *multipart.Writer) error {
|
||||
if key != "" {
|
||||
key = key + "."
|
||||
}
|
||||
|
||||
for _, ef := range encoderFields {
|
||||
field := value.FieldByIndex(ef.idx)
|
||||
err := ef.fn(key+ef.tag.name, field, writer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if extraEncoder != nil {
|
||||
err := e.encodeMapEntries(key, value.FieldByIndex(extraEncoder.idx), writer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (e *encoder) newFieldTypeEncoder(t reflect.Type) encoderFunc {
|
||||
f, _ := t.FieldByName("Value")
|
||||
enc := e.typeEncoder(f.Type)
|
||||
|
||||
return func(key string, value reflect.Value, writer *multipart.Writer) error {
|
||||
present := value.FieldByName("Present")
|
||||
if !present.Bool() {
|
||||
return nil
|
||||
}
|
||||
null := value.FieldByName("Null")
|
||||
if null.Bool() {
|
||||
return nil
|
||||
}
|
||||
raw := value.FieldByName("Raw")
|
||||
if !raw.IsNil() {
|
||||
return e.typeEncoder(raw.Type())(key, raw, writer)
|
||||
}
|
||||
return enc(key, value.FieldByName("Value"), writer)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *encoder) newTimeTypeEncoder() encoderFunc {
|
||||
format := e.dateFormat
|
||||
return func(key string, value reflect.Value, writer *multipart.Writer) error {
|
||||
return writer.WriteField(key, value.Convert(reflect.TypeOf(time.Time{})).Interface().(time.Time).Format(format))
|
||||
}
|
||||
}
|
||||
|
||||
func (e encoder) newInterfaceEncoder() encoderFunc {
|
||||
return func(key string, value reflect.Value, writer *multipart.Writer) error {
|
||||
value = value.Elem()
|
||||
if !value.IsValid() {
|
||||
return nil
|
||||
}
|
||||
return e.typeEncoder(value.Type())(key, value, writer)
|
||||
}
|
||||
}
|
||||
|
||||
var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
|
||||
|
||||
func escapeQuotes(s string) string {
|
||||
return quoteEscaper.Replace(s)
|
||||
}
|
||||
|
||||
func (e *encoder) newReaderTypeEncoder() encoderFunc {
|
||||
return func(key string, value reflect.Value, writer *multipart.Writer) error {
|
||||
reader := value.Convert(reflect.TypeOf((*io.Reader)(nil)).Elem()).Interface().(io.Reader)
|
||||
filename := "anonymous_file"
|
||||
contentType := "application/octet-stream"
|
||||
if named, ok := reader.(interface{ Filename() string }); ok {
|
||||
filename = named.Filename()
|
||||
} else if named, ok := reader.(interface{ Name() string }); ok {
|
||||
filename = path.Base(named.Name())
|
||||
}
|
||||
if typed, ok := reader.(interface{ ContentType() string }); ok {
|
||||
contentType = typed.ContentType()
|
||||
}
|
||||
|
||||
// Below is taken almost 1-for-1 from [multipart.CreateFormFile]
|
||||
h := make(textproto.MIMEHeader)
|
||||
h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, escapeQuotes(key), escapeQuotes(filename)))
|
||||
h.Set("Content-Type", contentType)
|
||||
filewriter, err := writer.CreatePart(h)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(filewriter, reader)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Given a []byte of json (may either be an empty object or an object that already contains entries)
|
||||
// encode all of the entries in the map to the json byte array.
|
||||
func (e *encoder) encodeMapEntries(key string, v reflect.Value, writer *multipart.Writer) error {
|
||||
type mapPair struct {
|
||||
key string
|
||||
value reflect.Value
|
||||
}
|
||||
|
||||
if key != "" {
|
||||
key = key + "."
|
||||
}
|
||||
|
||||
pairs := []mapPair{}
|
||||
|
||||
iter := v.MapRange()
|
||||
for iter.Next() {
|
||||
if iter.Key().Type().Kind() == reflect.String {
|
||||
pairs = append(pairs, mapPair{key: iter.Key().String(), value: iter.Value()})
|
||||
} else {
|
||||
return fmt.Errorf("cannot encode a map with a non string key")
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure deterministic output
|
||||
sort.Slice(pairs, func(i, j int) bool {
|
||||
return pairs[i].key < pairs[j].key
|
||||
})
|
||||
|
||||
elementEncoder := e.typeEncoder(v.Type().Elem())
|
||||
for _, p := range pairs {
|
||||
err := elementEncoder(key+string(p.key), p.value, writer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *encoder) newMapEncoder(t reflect.Type) encoderFunc {
|
||||
return func(key string, value reflect.Value, writer *multipart.Writer) error {
|
||||
return e.encodeMapEntries(key, value, writer)
|
||||
}
|
||||
}
|
||||
5
packages/sdk/go/internal/apiform/form.go
Normal file
5
packages/sdk/go/internal/apiform/form.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package apiform
|
||||
|
||||
type Marshaler interface {
|
||||
MarshalMultipart() ([]byte, string, error)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user