Compare commits

...
Author SHA1 Message Date
Dax Raad b824809605 re-export shell $ for plugin 2025-08-04 23:20:04 -04:00
opencode 5536b14347 release: v0.3.129 2025-08-05 01:18:50 +00:00
Dax Raad 01efe236ef fix @opencode-ai/plugin exports 2025-08-04 21:12:18 -04:00
Frank 7a1f96399d sync 2025-08-04 21:09:44 -04:00
Frank 40036abb9d wip: gateway 2025-08-04 21:08:29 -04:00
Jay V 2970ba6416 docs: lock 2025-08-04 19:53:50 -04:00
Jay V 81412b6197 docs: edit new docs 2025-08-04 19:52:03 -04:00
Mahamed-BelkheirandGitHub 5bf7691ea6 fix: default value for models with no cost object (#1601) 2025-08-04 16:45:35 -05:00
Min Chun FuandGitHub b1055a74d3 added vesper theme (#1602) 2025-08-04 16:45:00 -05:00
Aiden ClineandGitHub ffcb27fa9a docs: make plugins page exposed (#1603) 2025-08-04 16:44:28 -05:00
opencode 38819e89b8 release: v0.3.128 2025-08-04 16:20:39 +00:00
Dax Raad 0a42068fbb hack to return tool call errors back to model 2025-08-04 12:15:24 -04:00
opencode b05decc572 release: v0.3.127 2025-08-04 16:06:13 +00:00
GitHub Action c2f487906a ignore: update download stats 2025-08-04 2025-08-04 12:04:34 +00:00
Aiden ClineandGitHub ae78ec7a0c fix double help printing (#1580) 2025-08-04 05:03:27 -05:00
Frank e8c03f13dd fix docs 2025-08-04 00:23:02 -04:00
Dax Raad f85d30c484 wip: plugins 2025-08-03 21:43:58 -04:00
Dax Raad 1bac46612c wip: plugin load from package 2025-08-03 21:19:03 -04:00
Dax Raad 9ab3462821 Add workflow_dispatch trigger to typecheck workflow 2025-08-03 17:16:20 -04:00
Aiden ClineandGitHub 3b36822696 fix: patch marked-shiki (#1569) 2025-08-03 16:13:35 -05:00
Dax Raad 5b731479d5 Add typecheck workflow 2025-08-03 17:12:23 -04:00
Dax Raad a50bef6913 ignore: cleanup 2025-08-03 17:09:30 -04:00
Yordis PrietoandGitHub ed397c5057 chore: add ts-expected-error (#1575) 2025-08-03 17:09:19 -04:00
Yordis PrietoandGitHub c9187a9f3a chore: remove unnecessary TypeScript error suppression (#1571) 2025-08-03 15:50:08 -04:00
opencode 2c67b26b5d release: v0.3.126 2025-08-03 19:45:14 +00:00
Dax Raad 170b94a99e ci: ignore 2025-08-03 15:39:34 -04:00
Dax Raad cd58f10e3c ci: ignore 2025-08-03 15:38:39 -04:00
Dax Raad ea85fdf3cd fix bash tool not showing stderr 2025-08-03 15:34:52 -04:00
Aiden ClineandGitHub edda26ab33 tweak: filter out duplicate instructions (#1567) 2025-08-03 15:10:21 -04:00
Dax Raad ea4e1913c0 increase models.dev polling interval to hourly 2025-08-03 14:58:35 -04:00
Aiden ClineandGitHub 5eebc8ab51 docs: fix mixed up documentation (#1564) 2025-08-03 13:01:09 -05:00
Dax Raad 21c52fd5cb fix bash tool getting stuck on interactive commands 2025-08-03 13:52:50 -04:00
opencode 5e8634afaf release: v0.3.123 2025-08-03 17:13:33 +00:00
Dax Raad d4bac5cdbd ci: ignore 2025-08-03 13:12:35 -04:00
opencode 263b266476 release: v0.3.122 2025-08-03 16:19:09 +00:00
Dax Raad 06830327e7 more efficient snapshots in parallel toolcalls 2025-08-03 12:12:28 -04:00
Giuseppe RotaandGitHub 4b204fee58 fix(docs): move disabled providers paragraph to its proper section (#1547) 2025-08-03 11:28:57 -04:00
Dax Raad 99d3a0bb24 more fixes for shell 128 error 2025-08-03 11:25:58 -04:00
opencode 0930f6ac55 release: v0.3.120 2025-08-03 14:59:03 +00:00
Dax Raad 24515162fa ci: ignore 2025-08-03 10:52:35 -04:00
Dax Raad 53aa899e45 ci: ignore 2025-08-03 10:42:52 -04:00
Dax Raad 7e763e1c06 fix shell error 128 2025-08-03 10:30:23 -04:00
GitHub Action b0f2cc0c22 ignore: update download stats 2025-08-03 2025-08-03 12:04:04 +00:00
Aiden ClineandGitHub f90aa62784 fix: expand tilde for file: references (#1553) 2025-08-03 06:15:06 -05:00
Dax Raad 852191f6cb ci: ignore 2025-08-03 03:54:17 -04:00
Dax Raad c5e9dc081c ci: bun cache 2025-08-03 03:53:31 -04:00
Dax Raad 49c8889228 ci: ignore 2025-08-03 03:45:05 -04:00
Dax Raad f739e1a958 ci: ignore 2025-08-03 03:37:53 -04:00
Dax Raad 841f1907bb ci: ignore 2025-08-03 03:35:17 -04:00
The PangolierandGitHub 9255c507d6 Share link hotfix (#1513) 2025-08-03 03:02:24 -04:00
Yordis PrietoandGitHub 2711047166 remove: delete extension test file (#1554) 2025-08-03 02:58:10 -04:00
Frank 908048baef sync 2025-08-02 21:28:03 -04:00
Frank a9fbe07408 Add Zhipu AI provider 2025-08-02 21:20:44 -04:00
Dax Raad 0ae213ee0e ci: ignore 2025-08-02 18:56:34 -04:00
Dax Raad ca031278ca wip: plugins 2025-08-02 18:50:19 -04:00
84 changed files with 4518 additions and 3752 deletions
+12 -1
View File
@@ -39,6 +39,15 @@ jobs:
with:
bun-version: 1.2.19
- name: Cache ~/.bun
id: cache-bun
uses: actions/cache@v3
with:
path: ~/.bun
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-
- name: Install makepkg
run: |
sudo apt-get update
@@ -53,9 +62,11 @@ jobs:
git config --global user.email "opencode@sst.dev"
git config --global user.name "opencode"
- name: Install dependencies
run: bun install
- name: Publish
run: |
bun install
OPENCODE_VERSION=${{ inputs.version }} ./script/publish.ts
env:
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
+24
View File
@@ -0,0 +1,24 @@
name: Typecheck
on:
pull_request:
branches: [dev]
workflow_dispatch:
jobs:
typecheck:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.2.19
- name: Install dependencies
run: bun install
- name: Run typecheck
run: bun typecheck
+1 -1
View File
@@ -5,4 +5,4 @@ node_modules
.idea
.vscode
openapi.json
scratch
playground
+10
View File
@@ -0,0 +1,10 @@
import { Plugin } from "./index"
export const ExamplePlugin: Plugin = async ({ app, client, $ }) => {
return {
permission: {},
async "chat.params"(input, output) {
output.topP = 1
},
}
}
+2
View File
@@ -36,3 +36,5 @@
| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) |
| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) |
| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) |
| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) |
| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) |
+49 -9
View File
@@ -10,22 +10,27 @@
},
"packages/function": {
"name": "@opencode/function",
"version": "0.0.1",
"version": "0.3.129",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
"@ai-sdk/openai-compatible": "1.0.1",
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "22.0.0",
"ai": "catalog:",
"hono": "catalog:",
"jose": "6.0.11",
},
"devDependencies": {
"@cloudflare/workers-types": "4.20250522.0",
"@types/node": "catalog:",
"openai": "5.11.0",
"typescript": "catalog:",
},
},
"packages/opencode": {
"name": "opencode",
"version": "0.0.0",
"version": "0.3.129",
"bin": {
"opencode": "./bin/opencode",
},
@@ -38,6 +43,8 @@
"@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:",
@@ -74,9 +81,21 @@
"zod-to-json-schema": "3.24.5",
},
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "0.3.129",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
},
"devDependencies": {
"@hey-api/openapi-ts": "0.80.1",
"@tsconfig/node22": "catalog:",
"typescript": "catalog:",
},
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "0.0.0",
"version": "0.3.129",
"devDependencies": {
"@hey-api/openapi-ts": "0.80.1",
"@tsconfig/node22": "catalog:",
@@ -85,7 +104,7 @@
},
"packages/web": {
"name": "@opencode/web",
"version": "0.0.1",
"version": "0.3.129",
"dependencies": {
"@astrojs/cloudflare": "^12.5.4",
"@astrojs/markdown-remark": "6.3.1",
@@ -120,6 +139,9 @@
"sharp",
"esbuild",
],
"patchedDependencies": {
"marked-shiki@1.2.0": "patches/marked-shiki@1.2.0.patch",
},
"catalog": {
"@tsconfig/node22": "22.0.2",
"@types/node": "22.13.9",
@@ -142,13 +164,17 @@
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@2.2.10", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-icLGO7Q0NinnHIPgT+y1QjHVwH4HwV+brWbvM+FfCG2Afpa89PyKa3Ret91kGjZpBgM/xnj1B7K5eM+rRlsXQA=="],
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@1.2.12", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-YSzjlko7JvuiyQFmI9RN1tNZdEiZxc+6xld/0tq/VkJaHpEzGAb1yiNxxvmYVcjvfu/PcvCxAAYXmTYQQ63IHQ=="],
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-uyyaO4KhxoIKZztREqLPh+6/K3ZJx/rp72JKoUEL9/kC+vfQTThUfPnY/bUryUpcnawx8IY/tSoYNOi/8PCv7w=="],
"@ai-sdk/gateway": ["@ai-sdk/gateway@1.0.0-beta.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-beta.2", "@ai-sdk/provider-utils": "3.0.0-beta.10" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-felWPMuECZRGx8xnmvH5dW3jywKTkGnw/tXN8szphGzEDr/BfxywuXijfPBG2WBUS6frPXsvSLDRdCm5W38PXA=="],
"@ai-sdk/provider": ["@ai-sdk/provider@2.0.0-beta.2", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-vqhtZA7R24q1XnmfmIb1fZSmHMIaJH1BVQ+0kFnNJgqWsc+V8i+yfetZ37gUc4fXATFmBuS/6O7+RPoHsZ2Fqg=="],
"@ai-sdk/openai": ["@ai-sdk/openai@2.0.2", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-D4zYz2uR90aooKQvX1XnS00Z7PkbrcY+snUvPfm5bCabTG7bzLrVtD56nJ5bSaZG8lmuOMfXpyiEEArYLyWPpw=="],
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0-beta.10", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-beta.2", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-e6WSsgM01au04/1L/v5daXHn00eKjPBQXl3jq3BfvQbQ1jo8Rls2pvrdkyVc25jBW4TV4Zm+tw+v6NAh5NPXMA=="],
"@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.1", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-luHVcU+yKzwv3ekKgbP3v+elUVxb2Rt+8c6w9qi7g2NYG2/pEL21oIrnaEnc6UtTZLLZX9EFBcpq2N1FQKDIMw=="],
"@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="],
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="],
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
@@ -416,6 +442,8 @@
"@openauthjs/openauth": ["@openauthjs/openauth@0.4.3", "", { "dependencies": { "@standard-schema/spec": "1.0.0-beta.3", "aws4fetch": "1.0.20", "jose": "5.9.6" }, "peerDependencies": { "arctic": "^2.2.2", "hono": "^4.0.0" } }, "sha512-RlnjqvHzqcbFVymEwhlUEuac4utA5h4nhSK/i2szZuQmxTIqbGUxZ+nM+avM+VV4Ing+/ZaNLKILoXS3yrkOOw=="],
"@opencode-ai/plugin": ["@opencode-ai/plugin@workspace:packages/plugin"],
"@opencode-ai/sdk": ["@opencode-ai/sdk@workspace:packages/sdk/js"],
"@opencode/function": ["@opencode/function@workspace:packages/function"],
@@ -1350,6 +1378,8 @@
"open": ["open@10.1.2", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "is-wsl": "^3.1.0" } }, "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw=="],
"openai": ["openai@5.11.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-+AuTc5pVjlnTuA9zvn8rA/k+1RluPIx9AD4eDcnutv6JNwHHZxIhkFy+tmMKCvmMFDQzfA/r1ujvPWB19DQkYg=="],
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
"opencode": ["opencode@workspace:packages/opencode"],
@@ -1824,9 +1854,9 @@
"@ai-sdk/amazon-bedrock/aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="],
"@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="],
"@ai-sdk/gateway/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0-beta.2", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-vqhtZA7R24q1XnmfmIb1fZSmHMIaJH1BVQ+0kFnNJgqWsc+V8i+yfetZ37gUc4fXATFmBuS/6O7+RPoHsZ2Fqg=="],
"@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.2.8", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA=="],
"@ai-sdk/gateway/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0-beta.10", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-beta.2", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-e6WSsgM01au04/1L/v5daXHn00eKjPBQXl3jq3BfvQbQ1jo8Rls2pvrdkyVc25jBW4TV4Zm+tw+v6NAh5NPXMA=="],
"@ampproject/remapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="],
@@ -1858,6 +1888,10 @@
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"ai/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0-beta.2", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-vqhtZA7R24q1XnmfmIb1fZSmHMIaJH1BVQ+0kFnNJgqWsc+V8i+yfetZ37gUc4fXATFmBuS/6O7+RPoHsZ2Fqg=="],
"ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0-beta.10", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-beta.2", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-e6WSsgM01au04/1L/v5daXHn00eKjPBQXl3jq3BfvQbQ1jo8Rls2pvrdkyVc25jBW4TV4Zm+tw+v6NAh5NPXMA=="],
"ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
@@ -1898,6 +1932,8 @@
"nypm/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@1.2.12", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-YSzjlko7JvuiyQFmI9RN1tNZdEiZxc+6xld/0tq/VkJaHpEzGAb1yiNxxvmYVcjvfu/PcvCxAAYXmTYQQ63IHQ=="],
"opencontrol/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.6.1", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="],
"opencontrol/hono": ["hono@4.7.4", "", {}, "sha512-Pst8FuGqz3L7tFF+u9Pu70eI0xa5S3LPUmrNd5Jm8nTHze9FxLTK9Kaj5g/k4UcwuJSXTP65SyHOPLrffpcAJg=="],
@@ -1984,6 +2020,10 @@
"gray-matter/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
"opencode/@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="],
"opencode/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.2.8", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA=="],
"opencontrol/@modelcontextprotocol/sdk/pkce-challenge": ["pkce-challenge@4.1.0", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="],
"opencontrol/@modelcontextprotocol/sdk/zod": ["zod@3.25.49", "", {}, "sha512-JMMPMy9ZBk3XFEdbM3iL1brx4NUSejd6xr3ELrrGEfGb355gjhiAWtG3K5o+AViV/3ZfkIrCzXsZn6SbLwTR8Q=="],
+12
View File
@@ -46,3 +46,15 @@ new sst.cloudflare.x.Astro("Web", {
VITE_API_URL: api.url,
},
})
const OPENCODE_API_KEY = new sst.Secret("OPENCODE_API_KEY")
const ANTHROPIC_API_KEY = new sst.Secret("ANTHROPIC_API_KEY")
const OPENAI_API_KEY = new sst.Secret("OPENAI_API_KEY")
const ZHIPU_API_KEY = new sst.Secret("ZHIPU_API_KEY")
export const gateway = new sst.cloudflare.Worker("GatewayApi", {
domain: `api.gateway.${domain}`,
handler: "packages/function/src/gateway.ts",
url: true,
link: [OPENCODE_API_KEY, ANTHROPIC_API_KEY, OPENAI_API_KEY, ZHIPU_API_KEY],
})
-11
View File
@@ -1,16 +1,5 @@
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"cerebras": {
"options": {
"apiKey": "csk-m33xrvt43whkypn8r4ph9xc8fenhx2f68c3pj22ext45v5k9"
},
"npm": "@ai-sdk/cerebras",
"models": {
"qwen-3-coder-480b": {}
}
}
},
"mcp": {
"context7": {
"type": "remote",
+4 -2
View File
@@ -5,7 +5,7 @@
"type": "module",
"packageManager": "bun@1.2.14",
"scripts": {
"dev": "bun run packages/opencode/src/index.ts",
"dev": "bun run --conditions=development packages/opencode/src/index.ts",
"typecheck": "bun run --filter='*' typecheck",
"stainless": "./scripts/stainless",
"postinstall": "./script/hooks"
@@ -43,5 +43,7 @@
"protobufjs",
"sharp"
],
"patchedDependencies": {}
"patchedDependencies": {
"marked-shiki@1.2.0": "patches/marked-shiki@1.2.0.patch"
}
}
+8 -3
View File
@@ -1,17 +1,22 @@
{
"name": "@opencode/function",
"version": "0.0.1",
"version": "0.3.129",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
"devDependencies": {
"@cloudflare/workers-types": "4.20250522.0",
"typescript": "catalog:",
"@types/node": "catalog:"
"@types/node": "catalog:",
"openai": "5.11.0",
"typescript": "catalog:"
},
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
"@ai-sdk/openai-compatible": "1.0.1",
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "22.0.0",
"ai": "catalog:",
"hono": "catalog:",
"jose": "6.0.11"
}
+499
View File
@@ -0,0 +1,499 @@
import { Hono, Context, Next } from "hono"
import { Resource } from "sst"
import { generateText, streamText } from "ai"
import { createAnthropic } from "@ai-sdk/anthropic"
import { createOpenAI } from "@ai-sdk/openai"
import { createOpenAICompatible } from "@ai-sdk/openai-compatible"
import { type LanguageModelV2Prompt } from "@ai-sdk/provider"
import { type ChatCompletionCreateParamsBase } from "openai/resources/chat/completions"
type Env = {}
const auth = async (c: Context, next: Next) => {
const authHeader = c.req.header("authorization")
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return c.json(
{
error: {
message: "Missing API key.",
type: "invalid_request_error",
param: null,
code: "unauthorized",
},
},
401,
)
}
const apiKey = authHeader.split(" ")[1]
// Replace with your validation logic
if (apiKey !== Resource.OPENCODE_API_KEY.value) {
return c.json(
{
error: {
message: "Invalid API key.",
type: "invalid_request_error",
param: null,
code: "unauthorized",
},
},
401,
)
}
await next()
}
export default new Hono<{ Bindings: Env }>()
.get("/", (c) => c.text("Hello, world!"))
.post("/v1/chat/completions", auth, async (c) => {
try {
const body = await c.req.json<ChatCompletionCreateParamsBase>()
console.log(body)
const model = (() => {
const [provider, ...parts] = body.model.split("/")
const model = parts.join("/")
if (provider === "anthropic" && model === "claude-sonnet-4") {
return createAnthropic({
apiKey: Resource.ANTHROPIC_API_KEY.value,
})("claude-sonnet-4-20250514")
}
if (provider === "openai" && model === "gpt-4.1") {
return createOpenAI({
apiKey: Resource.OPENAI_API_KEY.value,
})("gpt-4.1")
}
if (provider === "zhipuai" && model === "glm-4.5-flash") {
return createOpenAICompatible({
name: "Zhipu AI",
baseURL: "https://api.z.ai/api/paas/v4",
apiKey: Resource.ZHIPU_API_KEY.value,
})("glm-4.5-flash")
}
throw new Error(`Unsupported provider: ${provider}`)
})()
const requestBody = transformOpenAIRequestToAiSDK()
return body.stream ? await handleStream() : await handleGenerate()
async function handleStream() {
const result = await streamText({
model,
...requestBody,
})
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
const id = `chatcmpl-${Date.now()}`
const created = Math.floor(Date.now() / 1000)
try {
for await (const chunk of result.fullStream) {
// TODO
//console.log("!!! CHUCK !!!", chunk);
switch (chunk.type) {
case "text-delta": {
const data = {
id,
object: "chat.completion.chunk",
created,
model: body.model,
choices: [
{
index: 0,
delta: {
content: chunk.text,
},
finish_reason: null,
},
],
}
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
break
}
case "reasoning-delta": {
const data = {
id,
object: "chat.completion.chunk",
created,
model: body.model,
choices: [
{
index: 0,
delta: {
reasoning_content: chunk.text,
},
finish_reason: null,
},
],
}
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
break
}
case "tool-call": {
const data = {
id,
object: "chat.completion.chunk",
created,
model: body.model,
choices: [
{
index: 0,
delta: {
tool_calls: [
{
id: chunk.toolCallId,
type: "function",
function: {
name: chunk.toolName,
arguments: JSON.stringify(chunk.input),
},
},
],
},
finish_reason: null,
},
],
}
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
break
}
case "error": {
const data = {
id,
object: "chat.completion.chunk",
created,
model: body.model,
error: {
message: chunk.error,
type: "server_error",
},
}
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
controller.enqueue(encoder.encode("data: [DONE]\n\n"))
controller.close()
break
}
case "finish": {
const finishReason =
{
stop: "stop",
length: "length",
"content-filter": "content_filter",
"tool-calls": "tool_calls",
error: "stop",
other: "stop",
unknown: "stop",
}[chunk.finishReason] || "stop"
const data = {
id,
object: "chat.completion.chunk",
created,
model: body.model,
choices: [
{
index: 0,
delta: {},
finish_reason: finishReason,
},
],
usage: {
prompt_tokens: chunk.totalUsage.inputTokens,
completion_tokens: chunk.totalUsage.outputTokens,
total_tokens: chunk.totalUsage.totalTokens,
completion_tokens_details: {
reasoning_tokens: chunk.totalUsage.reasoningTokens,
},
prompt_tokens_details: {
cached_tokens: chunk.totalUsage.cachedInputTokens,
},
},
}
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
controller.enqueue(encoder.encode("data: [DONE]\n\n"))
controller.close()
break
}
//case "stream-start":
//case "response-metadata":
case "start-step":
case "finish-step":
case "text-start":
case "text-end":
case "reasoning-start":
case "reasoning-end":
case "tool-input-start":
case "tool-input-delta":
case "tool-input-end":
case "raw":
default:
// Log unknown chunk types for debugging
console.warn(`Unknown chunk type: ${(chunk as any).type}`)
break
}
}
} catch (error) {
controller.error(error)
}
},
})
return new Response(stream, {
headers: {
"Content-Type": "text/plain; charset=utf-8",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
})
}
async function handleGenerate() {
const response = await generateText({
model,
...requestBody,
})
return c.json({
id: `chatcmpl-${Date.now()}`,
object: "chat.completion" as const,
created: Math.floor(Date.now() / 1000),
model: body.model,
choices: [
{
index: 0,
message: {
role: "assistant" as const,
content: response.content?.find((c) => c.type === "text")?.text ?? "",
reasoning_content: response.content?.find((c) => c.type === "reasoning")?.text,
tool_calls: response.content
?.filter((c) => c.type === "tool-call")
.map((toolCall) => ({
id: toolCall.toolCallId,
type: "function" as const,
function: {
name: toolCall.toolName,
arguments: toolCall.input,
},
})),
},
finish_reason:
(
{
stop: "stop",
length: "length",
"content-filter": "content_filter",
"tool-calls": "tool_calls",
error: "stop",
other: "stop",
unknown: "stop",
} as const
)[response.finishReason] || "stop",
},
],
usage: {
prompt_tokens: response.usage?.inputTokens,
completion_tokens: response.usage?.outputTokens,
total_tokens: response.usage?.totalTokens,
completion_tokens_details: {
reasoning_tokens: response.usage?.reasoningTokens,
},
prompt_tokens_details: {
cached_tokens: response.usage?.cachedInputTokens,
},
},
})
}
function transformOpenAIRequestToAiSDK() {
const prompt = transformMessages()
return {
prompt,
maxOutputTokens: body.max_tokens ?? body.max_completion_tokens ?? undefined,
temperature: body.temperature ?? undefined,
topP: body.top_p ?? undefined,
frequencyPenalty: body.frequency_penalty ?? undefined,
presencePenalty: body.presence_penalty ?? undefined,
providerOptions: body.reasoning_effort
? {
anthropic: {
reasoningEffort: body.reasoning_effort,
},
}
: undefined,
stopSequences: (typeof body.stop === "string" ? [body.stop] : body.stop) ?? undefined,
responseFormat: (() => {
if (!body.response_format) return { type: "text" }
if (body.response_format.type === "json_schema")
return {
type: "json",
schema: body.response_format.json_schema.schema,
name: body.response_format.json_schema.name,
description: body.response_format.json_schema.description,
}
if (body.response_format.type === "json_object") return { type: "json" }
throw new Error("Unsupported response format")
})(),
seed: body.seed ?? undefined,
}
function transformTools() {
const { tools, tool_choice } = body
if (!tools || tools.length === 0) {
return { tools: undefined, toolChoice: undefined }
}
const aiSdkTools = tools.reduce(
(acc, tool) => {
acc[tool.function.name] = {
type: "function" as const,
name: tool.function.name,
description: tool.function.description,
inputSchema: tool.function.parameters,
}
return acc
},
{} as Record<string, any>,
)
let aiSdkToolChoice
if (tool_choice == null) {
aiSdkToolChoice = undefined
} else if (tool_choice === "auto") {
aiSdkToolChoice = "auto"
} else if (tool_choice === "none") {
aiSdkToolChoice = "none"
} else if (tool_choice === "required") {
aiSdkToolChoice = "required"
} else if (tool_choice.type === "function") {
aiSdkToolChoice = {
type: "tool",
toolName: tool_choice.function.name,
}
}
return { tools: aiSdkTools, toolChoice: aiSdkToolChoice }
}
function transformMessages() {
const { messages } = body
const prompt: LanguageModelV2Prompt = []
for (const message of messages) {
switch (message.role) {
case "system": {
prompt.push({
role: "system",
content: message.content as string,
})
break
}
case "user": {
if (typeof message.content === "string") {
prompt.push({
role: "user",
content: [{ type: "text", text: message.content }],
})
} else {
const content = message.content.map((part) => {
switch (part.type) {
case "text":
return { type: "text" as const, text: part.text }
case "image_url":
return {
type: "file" as const,
mediaType: "image/jpeg" as const,
data: part.image_url.url,
}
default:
throw new Error(`Unsupported content part type: ${(part as any).type}`)
}
})
prompt.push({
role: "user",
content,
})
}
break
}
case "assistant": {
const content: Array<
| { type: "text"; text: string }
| {
type: "tool-call"
toolCallId: string
toolName: string
input: any
}
> = []
if (message.content) {
content.push({
type: "text",
text: message.content as string,
})
}
if (message.tool_calls) {
for (const toolCall of message.tool_calls) {
content.push({
type: "tool-call",
toolCallId: toolCall.id,
toolName: toolCall.function.name,
input: JSON.parse(toolCall.function.arguments),
})
}
}
prompt.push({
role: "assistant",
content,
})
break
}
case "tool": {
prompt.push({
role: "tool",
content: [
{
type: "tool-result",
toolName: "placeholder",
toolCallId: message.tool_call_id,
output: {
type: "text",
value: message.content as string,
},
},
],
})
break
}
default: {
throw new Error(`Unsupported message role: ${message.role}`)
}
}
}
return prompt
}
}
} catch (error: any) {
return c.json({ error: { message: error.message } }, 500)
}
})
.all("*", (c) => c.text("Not Found"))
+17
View File
@@ -6,6 +6,10 @@
import "sst"
declare module "sst" {
export interface Resource {
"ANTHROPIC_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
"GITHUB_APP_ID": {
"type": "sst.sst.Secret"
"value": string
@@ -14,10 +18,22 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
"OPENAI_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
"OPENCODE_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
"Web": {
"type": "sst.cloudflare.Astro"
"url": string
}
"ZHIPU_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
}
}
// cloudflare
@@ -26,6 +42,7 @@ declare module "sst" {
export interface Resource {
"Api": cloudflare.Service
"Bucket": cloudflare.R2Bucket
"GatewayApi": cloudflare.Service
}
}
+4 -2
View File
@@ -1,12 +1,12 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "0.0.0",
"version": "0.3.129",
"name": "opencode",
"type": "module",
"private": true,
"scripts": {
"typecheck": "tsc --noEmit",
"dev": "bun run ./src/index.ts"
"dev": "bun run --conditions=development ./src/index.ts"
},
"bin": {
"opencode": "./bin/opencode"
@@ -36,6 +36,8 @@
"@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:",
+2
View File
@@ -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()
+41 -17
View File
@@ -1,5 +1,6 @@
import { Log } from "../util/log"
import path from "path"
import os from "os"
import { z } from "zod"
import { App } from "../app/app"
import { Filesystem } from "../util/filesystem"
@@ -23,13 +24,13 @@ export namespace Config {
for (const file of ["opencode.jsonc", "opencode.json"]) {
const found = await Filesystem.findUp(file, app.path.cwd, app.path.root)
for (const resolved of found.toReversed()) {
result = mergeDeep(result, await load(resolved))
result = mergeDeep(result, await loadFile(resolved))
}
}
// Override with custom config if provided
if (Flag.OPENCODE_CONFIG) {
result = mergeDeep(result, await load(Flag.OPENCODE_CONFIG))
result = mergeDeep(result, await loadFile(Flag.OPENCODE_CONFIG))
log.debug("loaded custom config", { path: Flag.OPENCODE_CONFIG })
}
@@ -37,7 +38,7 @@ export namespace Config {
if (value.type === "wellknown") {
process.env[value.key] = value.token
const wellknown = await fetch(`${key}/.well-known/opencode`).then((x) => x.json())
result = mergeDeep(result, await loadRaw(JSON.stringify(wellknown.config ?? {}), process.cwd()))
result = mergeDeep(result, await load(JSON.stringify(wellknown.config ?? {}), process.cwd()))
}
}
@@ -92,6 +93,14 @@ export namespace Config {
throw new InvalidError({ path: item }, { cause: parsed.error })
}
result.plugin = result.plugin || []
result.plugin.push(
...[
...(await Filesystem.globUp("plugin/*.ts", Global.Path.config, Global.Path.config)),
...(await Filesystem.globUp(".opencode/plugin/*.ts", app.path.cwd, app.path.root)),
].map((x) => "file://" + x),
)
// Handle migration from autoshare to share field
if (result.autoshare === true && !result.share) {
result.share = "auto"
@@ -223,6 +232,7 @@ export namespace Config {
$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()
@@ -352,9 +362,9 @@ export namespace Config {
export const global = lazy(async () => {
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 load(path.join(Global.Path.config, "opencode.jsonc"))),
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"), {
@@ -375,25 +385,26 @@ export namespace Config {
return result
})
async function load(configPath: string): Promise<Info> {
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)
if (fileMatches) {
const configDir = path.dirname(configPath)
const configDir = path.dirname(filepath)
const lines = text.split("\n")
for (const match of fileMatches) {
@@ -401,7 +412,10 @@ export namespace Config {
if (lineIndex !== -1 && lines[lineIndex].trim().startsWith("//")) {
continue // Skip if line is commented
}
const filePath = match.replace(/^\{file:/, "").replace(/\}$/, "")
let filePath = match.replace(/^\{file:/, "").replace(/\}$/, "")
if (filePath.startsWith("~/")) {
filePath = path.join(os.homedir(), filePath.slice(2))
}
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(configDir, filePath)
const fileContent = (await Bun.file(resolvedPath).text()).trim()
// escape newlines/quotes, strip outer quotes
@@ -428,7 +442,7 @@ export namespace Config {
.join("\n")
throw new JsonError({
path: configPath,
path: filepath,
message: `\n--- JSONC Input ---\n${text}\n--- Errors ---\n${errorDetails}\n--- End ---`,
})
}
@@ -437,11 +451,21 @@ export namespace Config {
if (parsed.success) {
if (!parsed.data.$schema) {
parsed.data.$schema = "https://opencode.ai/config.json"
await Bun.write(configPath, JSON.stringify(parsed.data, null, 2))
await Bun.write(filepath, JSON.stringify(parsed.data, null, 2))
}
return parsed.data
const data = parsed.data
if (data.plugin) {
for (let i = 0; i < data.plugin?.length; i++) {
const plugin = data.plugin[i]
try {
data.plugin[i] = import.meta.resolve(plugin, filepath)
} catch (err) {}
}
}
return data
}
throw new InvalidError({ path: configPath, issues: parsed.error.issues })
throw new InvalidError({ path: filepath, issues: parsed.error.issues })
}
export const JsonError = NamedError.create(
"ConfigJsonError",
+1
View File
@@ -83,6 +83,7 @@ const cli = yargs(hideBin(process.argv))
if (msg.startsWith("Unknown argument") || msg.startsWith("Not enough non-option arguments")) {
cli.showHelp("log")
}
process.exit(1)
})
.strict()
+14 -1
View File
@@ -3,6 +3,7 @@ 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" })
@@ -67,7 +68,7 @@ export namespace Permission {
},
)
export function ask(input: {
export async function ask(input: {
type: Info["type"]
title: Info["title"]
pattern?: Info["pattern"]
@@ -95,6 +96,18 @@ export namespace Permission {
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][info.id] = {
+69
View File
@@ -0,0 +1,69 @@
import type { Hooks, Plugin as PluginInstance } from "@opencode-ai/plugin"
import { App } from "../app/app"
import { Config } from "../config/config"
import { Bus } from "../bus"
import { Log } from "../util/log"
import { createOpencodeClient } from "@opencode-ai/sdk"
import { Server } from "../server/server"
import { BunProc } from "../bun"
export namespace Plugin {
const log = Log.create({ service: "plugin" })
const state = App.state("plugin", async (app) => {
const client = createOpencodeClient({
baseUrl: "http://localhost:4096",
fetch: async (...args) => Server.app().fetch(...args),
})
const config = await Config.get()
const hooks = []
for (let plugin of config.plugin ?? []) {
log.info("loading plugin", { path: plugin })
if (!plugin.startsWith("file://")) {
const [pkg, version] = plugin.split("@")
plugin = await BunProc.install(pkg, version ?? "latest")
}
const mod = await import(plugin)
for (const [_name, fn] of Object.entries<PluginInstance>(mod)) {
const init = await fn({
client,
app,
$: Bun.$,
})
hooks.push(init)
}
}
return {
hooks,
}
})
export async function trigger<
Name extends keyof Required<Hooks>,
Input = Parameters<Required<Hooks>[Name]>[0],
Output = Parameters<Required<Hooks>[Name]>[1],
>(name: Name, input: Input, output: Output): Promise<Output> {
if (!name) return output
for (const hook of await state().then((x) => x.hooks)) {
const fn = hook[name]
if (!fn) continue
// @ts-expect-error if you feel adventurous, please fix the typing, make sure to bump the try-counter if you
// give up.
// try-counter: 2
await fn(input, output)
}
return output
}
export function init() {
Bus.subscribeAll(async (input) => {
const hooks = await state().then((x) => x.hooks)
for (const hook of hooks) {
hook["event"]?.({
event: input,
})
}
})
}
}
+1 -1
View File
@@ -77,4 +77,4 @@ export namespace ModelsDev {
}
}
setInterval(() => ModelsDev.refresh(), 60 * 1000).unref()
setInterval(() => ModelsDev.refresh(), 60 * 1000 * 60).unref()
+18 -14
View File
@@ -97,7 +97,7 @@ export namespace Provider {
Array.isArray(msg.content) && msg.content.some((part: any) => part.type === "image_url"),
)
}
} catch { }
} catch {}
const headers: Record<string, string> = {
...init.headers,
...copilot.HEADERS,
@@ -283,26 +283,26 @@ export namespace Provider {
cost:
!model.cost && !existing?.cost
? {
input: 0,
output: 0,
cache_read: 0,
cache_write: 0,
}
input: 0,
output: 0,
cache_read: 0,
cache_write: 0,
}
: {
cache_read: 0,
cache_write: 0,
...existing?.cost,
...model.cost,
},
cache_read: 0,
cache_write: 0,
...existing?.cost,
...model.cost,
},
options: {
...existing?.options,
...model.options,
},
limit: model.limit ??
existing?.limit ?? {
context: 0,
output: 0,
},
context: 0,
output: 0,
},
}
parsed.models[modelID] = parsedModel
}
@@ -386,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()
+3 -2
View File
@@ -19,6 +19,7 @@ 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: {
@@ -48,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
@@ -1022,7 +1023,7 @@ export namespace Server {
.route("/tui/control", TuiRoute)
return result
}
})
export async function openapi() {
const a = app()
+94 -54
View File
@@ -13,6 +13,7 @@ import {
type ModelMessage,
stepCountIs,
type StreamTextResult,
InvalidToolInputError,
} from "ai"
import PROMPT_INITIALIZE from "../session/prompt/initialize.txt"
@@ -41,6 +42,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" })
@@ -571,7 +573,14 @@ 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)
@@ -716,10 +725,20 @@ export namespace Session {
description: item.description,
inputSchema: item.parameters as ZodSchema,
async execute(args, options) {
await processor.track(options.toolCallId)
await Plugin.trigger(
"tool.execute.before",
{
tool: item.id,
sessionID: input.sessionID,
callID: options.toolCallId,
},
{
args,
},
)
const result = await item.execute(args, {
sessionID: input.sessionID,
abort: abort.signal,
abort: options.abortSignal!,
messageID: assistantMsg.id,
callID: options.toolCallId,
metadata: async (val) => {
@@ -740,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) {
@@ -752,11 +780,10 @@ export namespace Session {
}
for (const [key, item] of Object.entries(await MCP.tools())) {
if (mode.tools[key] === false) continue
if (enabledTools[key] === false) continue
const execute = item.execute
if (!execute) continue
item.execute = async (args, opts) => {
await processor.track(opts.toolCallId)
const result = await execute(args, opts)
const output = result.content
.filter((x: any) => x.type === "text")
@@ -776,6 +803,21 @@ 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(e) {
log.error("streamText error", {
@@ -828,13 +870,29 @@ export namespace Session {
messages,
}
},
async experimental_repairToolCall(input) {
if (InvalidToolInputError.isInstance(input.error)) {
return {
...input.toolCall,
input: JSON.stringify({
tool: input.toolCall.toolName,
error: input.error.message,
}),
toolName: "invalid",
}
}
return null
},
maxRetries: 3,
activeTools: Object.keys(tools).filter((x) => x !== "invalid"),
maxOutputTokens: outputLimit,
abortSignal: abort.signal,
stopWhen: stepCountIs(1000),
providerOptions: {
[input.providerID]: model.info.options,
},
temperature: params.temperature,
topP: params.topP,
messages: [
...system.map(
(x): ModelMessage => ({
@@ -844,10 +902,6 @@ export namespace Session {
),
...MessageV2.toModelMessage(msgs),
],
temperature: model.info.temperature
? (mode.temperature ?? ProviderTransform.temperature(input.providerID, input.modelID))
: undefined,
topP: mode.topP ?? ProviderTransform.topP(input.providerID, input.modelID),
tools: model.info.tool_call === false ? undefined : tools,
model: wrapLanguageModel({
model: model.language,
@@ -879,15 +933,11 @@ export namespace Session {
}
function createProcessor(assistantMsg: MessageV2.Assistant, model: ModelsDev.Model) {
const toolCalls: Record<string, MessageV2.ToolPart> = {}
const snapshots: Record<string, string> = {}
const toolcalls: Record<string, MessageV2.ToolPart> = {}
let snapshot: string | undefined
return {
async track(toolCallID: string) {
const hash = await Snapshot.track()
if (hash) snapshots[toolCallID] = hash
},
partFromToolCall(toolCallID: string) {
return toolCalls[toolCallID]
return toolcalls[toolCallID]
},
async process(stream: StreamTextResult<Record<string, AITool>, never>) {
try {
@@ -903,7 +953,7 @@ export namespace Session {
case "tool-input-start":
const part = await updatePart({
id: toolCalls[value.id]?.id ?? Identifier.ascending("part"),
id: toolcalls[value.id]?.id ?? Identifier.ascending("part"),
messageID: assistantMsg.id,
sessionID: assistantMsg.sessionID,
type: "tool",
@@ -913,7 +963,7 @@ export namespace Session {
status: "pending",
},
})
toolCalls[value.id] = part as MessageV2.ToolPart
toolcalls[value.id] = part as MessageV2.ToolPart
break
case "tool-input-delta":
@@ -923,10 +973,11 @@ export namespace Session {
break
case "tool-call": {
const match = toolCalls[value.toolCallId]
const match = toolcalls[value.toolCallId]
if (match) {
const part = await updatePart({
...match,
tool: value.toolName,
state: {
status: "running",
input: value.input,
@@ -935,12 +986,12 @@ export namespace Session {
},
},
})
toolCalls[value.toolCallId] = part as MessageV2.ToolPart
toolcalls[value.toolCallId] = part as MessageV2.ToolPart
}
break
}
case "tool-result": {
const match = toolCalls[value.toolCallId]
const match = toolcalls[value.toolCallId]
if (match && match.state.status === "running") {
await updatePart({
...match,
@@ -956,27 +1007,13 @@ export namespace Session {
},
},
})
delete toolCalls[value.toolCallId]
const snapshot = snapshots[value.toolCallId]
if (snapshot) {
const patch = await Snapshot.patch(snapshot)
if (patch.files.length) {
await updatePart({
id: Identifier.ascending("part"),
messageID: assistantMsg.id,
sessionID: assistantMsg.sessionID,
type: "patch",
hash: patch.hash,
files: patch.files,
})
}
}
delete toolcalls[value.toolCallId]
}
break
}
case "tool-error": {
const match = toolCalls[value.toolCallId]
const match = toolcalls[value.toolCallId]
if (match && match.state.status === "running") {
await updatePart({
...match,
@@ -990,19 +1027,7 @@ export namespace Session {
},
},
})
delete toolCalls[value.toolCallId]
const snapshot = snapshots[value.toolCallId]
if (snapshot) {
const patch = await Snapshot.patch(snapshot)
await updatePart({
id: Identifier.ascending("part"),
messageID: assistantMsg.id,
sessionID: assistantMsg.sessionID,
type: "patch",
hash: patch.hash,
files: patch.files,
})
}
delete toolcalls[value.toolCallId]
}
break
}
@@ -1017,6 +1042,7 @@ export namespace Session {
sessionID: assistantMsg.sessionID,
type: "step-start",
})
snapshot = await Snapshot.track()
break
case "finish-step":
@@ -1032,6 +1058,20 @@ export namespace Session {
cost: usage.cost,
})
await updateMessage(assistantMsg)
if (snapshot) {
const patch = await Snapshot.patch(snapshot)
if (patch.files.length) {
await updatePart({
id: Identifier.ascending("part"),
messageID: assistantMsg.id,
sessionID: assistantMsg.sessionID,
type: "patch",
hash: patch.hash,
files: patch.files,
})
}
snapshot = undefined
}
break
case "text-start":
@@ -1304,10 +1344,10 @@ export namespace Session {
}
return {
cost: new Decimal(0)
.add(new Decimal(tokens.input).mul(model.cost.input).div(1_000_000))
.add(new Decimal(tokens.output).mul(model.cost.output).div(1_000_000))
.add(new Decimal(tokens.cache.read).mul(model.cost.cache_read ?? 0).div(1_000_000))
.add(new Decimal(tokens.cache.write).mul(model.cost.cache_write ?? 0).div(1_000_000))
.add(new Decimal(tokens.input).mul(model.cost?.input?? 0).div(1_000_000))
.add(new Decimal(tokens.output).mul(model.cost?.output?? 0).div(1_000_000))
.add(new Decimal(tokens.cache.read).mul(model.cost?.cache_read ?? 0).div(1_000_000))
.add(new Decimal(tokens.cache.write).mul(model.cost?.cache_write ?? 0).div(1_000_000))
.toNumber(),
tokens,
}
+13 -18
View File
@@ -60,33 +60,28 @@ export namespace SystemPrompt {
export async function custom() {
const { cwd, root } = App.info().path
const config = await Config.get()
const found = []
const paths = new Set<string>()
for (const item of CUSTOM_FILES) {
const matches = await Filesystem.findUp(item, cwd, root)
found.push(...matches.map((x) => Bun.file(x).text()))
matches.forEach((path) => paths.add(path))
}
found.push(
Bun.file(path.join(Global.Path.config, "AGENTS.md"))
.text()
.catch(() => ""),
)
found.push(
Bun.file(path.join(os.homedir(), ".claude", "CLAUDE.md"))
.text()
.catch(() => ""),
)
paths.add(path.join(Global.Path.config, "AGENTS.md"))
paths.add(path.join(os.homedir(), ".claude", "CLAUDE.md"))
if (config.instructions) {
for (const instruction of config.instructions) {
try {
const matches = await Filesystem.globUp(instruction, cwd, root)
found.push(...matches.map((x) => Bun.file(x).text()))
} catch {
continue // Skip invalid glob patterns
}
const matches = await Filesystem.globUp(instruction, cwd, root).catch(() => [])
matches.forEach((path) => paths.add(path))
}
}
const found = Array.from(paths).map((p) =>
Bun.file(p)
.text()
.catch(() => ""),
)
return Promise.all(found).then((result) => result.filter(Boolean))
}
+1 -1
View File
@@ -39,7 +39,7 @@ export namespace Snapshot {
log.info("initialized")
}
await $`git --git-dir ${git} add .`.quiet().cwd(app.path.cwd).nothrow()
const hash = await $`git --git-dir ${git} write-tree`.quiet().cwd(app.path.cwd).text()
const hash = await $`git --git-dir ${git} write-tree`.quiet().cwd(app.path.cwd).nothrow().text()
return hash.trim()
}
+23 -11
View File
@@ -1,4 +1,6 @@
import { z } from "zod"
import { exec } from "child_process"
import { text } from "stream/consumers"
import { Tool } from "./tool"
import DESCRIPTION from "./bash.txt"
import { App } from "../app/app"
@@ -75,9 +77,13 @@ export const BashTool = Tool.define("bash", {
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())
const resolved = await $`realpath ${arg}`
.quiet()
.nothrow()
.text()
.then((x) => x.trim())
log.info("resolved path", { arg, resolved })
if (!Filesystem.contains(app.path.cwd, resolved)) {
if (resolved && !Filesystem.contains(app.path.cwd, resolved)) {
throw new Error(
`This command references paths outside of ${app.path.cwd} so it is not allowed to be executed.`,
)
@@ -112,18 +118,24 @@ export const BashTool = Tool.define("bash", {
})
}
const process = Bun.spawn({
cmd: ["bash", "-c", params.command],
const process = exec(params.command, {
cwd: app.path.cwd,
maxBuffer: MAX_OUTPUT_LENGTH,
signal: ctx.abort,
timeout: timeout,
stdout: "pipe",
stderr: "pipe",
maxBuffer: MAX_OUTPUT_LENGTH,
timeout,
})
await process.exited
const stdout = await new Response(process.stdout).text()
const stderr = await new Response(process.stderr).text()
const stdoutPromise = text(process.stdout!)
const stderrPromise = text(process.stderr!)
await new Promise<void>((resolve) => {
process.on("close", () => {
resolve()
})
})
const stdout = await stdoutPromise
const stderr = await stderrPromise
return {
title: params.command,
+17
View File
@@ -0,0 +1,17 @@
import { z } from "zod"
import { Tool } from "./tool"
export const InvalidTool = Tool.define("invalid", {
description: "Do not use",
parameters: z.object({
tool: z.string(),
error: z.string(),
}),
async execute(params) {
return {
title: "Invalid Tool",
output: `The arguments provided to the tool are invalid: ${params.error}`,
metadata: {},
}
},
})
+2
View File
@@ -10,9 +10,11 @@ import { TaskTool } from "./task"
import { TodoWriteTool, TodoReadTool } from "./todo"
import { WebFetchTool } from "./webfetch"
import { WriteTool } from "./write"
import { InvalidTool } from "./invalid"
export namespace ToolRegistry {
const ALL = [
InvalidTool,
BashTool,
EditTool,
WebFetchTool,
+10 -1
View File
@@ -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
View File
@@ -0,0 +1 @@
dist
+26
View File
@@ -0,0 +1,26 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "0.3.129",
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit"
},
"exports": {
".": {
"development": "./src/index.ts",
"import": "./dist/index.js"
}
},
"files": [
"dist"
],
"dependencies": {
"@opencode-ai/sdk": "workspace:*"
},
"devDependencies": {
"typescript": "catalog:",
"@hey-api/openapi-ts": "0.80.1",
"@tsconfig/node22": "catalog:"
}
}
+18
View 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`
}
+10
View File
@@ -0,0 +1,10 @@
import { Plugin } from "./index"
export const ExamplePlugin: Plugin = async ({ app, client, $ }) => {
return {
permission: {},
async "chat.params"(input, output) {
output.topP = 1
},
}
}
+37
View File
@@ -0,0 +1,37 @@
import type { Event, createOpencodeClient, App, Model, Provider, Permission, UserMessage, Part } from "@opencode-ai/sdk"
import type { BunShell } from "./shell"
export type PluginInput = {
client: ReturnType<typeof createOpencodeClient>
app: App
$: BunShell
}
export type Plugin = (input: PluginInput) => Promise<Hooks>
export interface Hooks {
event?: (input: { event: Event }) => Promise<void>
/**
* Called when a new message is received
*/
"chat.message"?: (input: {}, output: { message: UserMessage; parts: Part[] }) => Promise<void>
/**
* Modify parameters sent to LLM
*/
"chat.params"?: (
input: { model: Model; provider: Provider; message: UserMessage },
output: { temperature: number; topP: number },
) => Promise<void>
"permission.ask"?: (input: Permission, output: { status: "ask" | "deny" | "allow" }) => Promise<void>
"tool.execute.before"?: (
input: { tool: string; sessionID: string; callID: string },
output: { args: any },
) => Promise<void>
"tool.execute.after"?: (
input: { tool: string; sessionID: string; callID: string },
output: {
title: string
output: string
metadata: any
},
) => Promise<void>
}
+136
View File
@@ -0,0 +1,136 @@
export type ShellFunction = (input: Uint8Array) => Uint8Array
export type ShellExpression =
| { toString(): string }
| Array<ShellExpression>
| string
| { raw: string }
| ReadableStream
export interface BunShell {
(strings: TemplateStringsArray, ...expressions: ShellExpression[]): BunShellPromise
/**
* Perform bash-like brace expansion on the given pattern.
* @param pattern - Brace pattern to expand
*/
braces(pattern: string): string[]
/**
* Escape strings for input into shell commands.
*/
escape(input: string): string
/**
* Change the default environment variables for shells created by this instance.
*/
env(newEnv?: Record<string, string | undefined>): BunShell
/**
* Default working directory to use for shells created by this instance.
*/
cwd(newCwd?: string): BunShell
/**
* Configure the shell to not throw an exception on non-zero exit codes.
*/
nothrow(): BunShell
/**
* Configure whether or not the shell should throw an exception on non-zero exit codes.
*/
throws(shouldThrow: boolean): BunShell
}
export interface BunShellPromise extends Promise<BunShellOutput> {
readonly stdin: WritableStream
/**
* Change the current working directory of the shell.
*/
cwd(newCwd: string): this
/**
* Set environment variables for the shell.
*/
env(newEnv: Record<string, string> | undefined): this
/**
* By default, the shell will write to the current process's stdout and stderr, as well as buffering that output.
* This configures the shell to only buffer the output.
*/
quiet(): this
/**
* Read from stdout as a string, line by line
* Automatically calls quiet() to disable echoing to stdout.
*/
lines(): AsyncIterable<string>
/**
* Read from stdout as a string.
* Automatically calls quiet() to disable echoing to stdout.
*/
text(encoding?: BufferEncoding): Promise<string>
/**
* Read from stdout as a JSON object
* Automatically calls quiet()
*/
json(): Promise<any>
/**
* Read from stdout as an ArrayBuffer
* Automatically calls quiet()
*/
arrayBuffer(): Promise<ArrayBuffer>
/**
* Read from stdout as a Blob
* Automatically calls quiet()
*/
blob(): Promise<Blob>
/**
* Configure the shell to not throw an exception on non-zero exit codes.
*/
nothrow(): this
/**
* Configure whether or not the shell should throw an exception on non-zero exit codes.
*/
throws(shouldThrow: boolean): this
}
export interface BunShellOutput {
readonly stdout: Buffer
readonly stderr: Buffer
readonly exitCode: number
/**
* Read from stdout as a string
*/
text(encoding?: BufferEncoding): string
/**
* Read from stdout as a JSON object
*/
json(): any
/**
* Read from stdout as an ArrayBuffer
*/
arrayBuffer(): ArrayBuffer
/**
* Read from stdout as an Uint8Array
*/
bytes(): Uint8Array
/**
* Read from stdout as a Blob
*/
blob(): Blob
}
export type BunShellError = Error & BunShellOutput
+9
View File
@@ -0,0 +1,9 @@
/* This file is auto-generated by SST. Do not edit. */
/* tslint:disable */
/* eslint-disable */
/* deno-fmt-ignore-file */
/// <reference path="../../sst-env.d.ts" />
import "sst"
export {}
+16
View 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"
]
}
-88
View File
@@ -1,88 +0,0 @@
name: CI
on:
push:
branches-ignore:
- 'generated'
- 'codegen/**'
- 'integrated/**'
- 'stl-preview-head/**'
- 'stl-preview-base/**'
pull_request:
branches-ignore:
- 'stl-preview-head/**'
- 'stl-preview-base/**'
jobs:
lint:
timeout-minutes: 10
name: lint
runs-on: ${{ github.repository == 'stainless-sdks/opencode-typescript' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Bootstrap
run: ./scripts/bootstrap
- name: Check types
run: ./scripts/lint
build:
timeout-minutes: 5
name: build
runs-on: ${{ github.repository == 'stainless-sdks/opencode-typescript' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Bootstrap
run: ./scripts/bootstrap
- name: Check build
run: ./scripts/build
- name: Get GitHub OIDC Token
if: github.repository == 'stainless-sdks/opencode-typescript'
id: github-oidc
uses: actions/github-script@v6
with:
script: core.setOutput('github_token', await core.getIDToken());
- name: Upload tarball
if: github.repository == 'stainless-sdks/opencode-typescript'
env:
URL: https://pkg.stainless.com/s
AUTH: ${{ steps.github-oidc.outputs.github_token }}
SHA: ${{ github.sha }}
run: ./scripts/utils/upload-artifact.sh
test:
timeout-minutes: 10
name: test
runs-on: ${{ github.repository == 'stainless-sdks/opencode-typescript' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Bootstrap
run: ./scripts/bootstrap
- name: Run tests
run: ./scripts/test
-32
View File
@@ -1,32 +0,0 @@
# This workflow is triggered when a GitHub release is created.
# It can also be run manually to re-publish to NPM in case it failed for some reason.
# You can run this workflow by navigating to https://www.github.com/sst/opencode-sdk-js/actions/workflows/publish-npm.yml
name: Publish NPM
on:
workflow_dispatch:
release:
types: [published]
jobs:
publish:
name: publish
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v3
with:
node-version: '20'
- name: Install dependencies
run: |
yarn install
- name: Publish to NPM
run: |
bash ./bin/publish-npm
env:
NPM_TOKEN: ${{ secrets.OPENCODE_NPM_TOKEN || secrets.NPM_TOKEN }}
-21
View File
@@ -1,21 +0,0 @@
name: Release Doctor
on:
pull_request:
branches:
- main
workflow_dispatch:
jobs:
release_doctor:
name: release doctor
runs-on: ubuntu-latest
if: github.repository == 'sst/opencode-sdk-js' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next')
steps:
- uses: actions/checkout@v4
- name: Check release environment
run: |
bash ./bin/check-release-environment
env:
NPM_TOKEN: ${{ secrets.OPENCODE_NPM_TOKEN || secrets.NPM_TOKEN }}
+2 -2
View File
@@ -1,4 +1,4 @@
configured_endpoints: 34
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-2ebd9d5478864042a2e01b4995f42acbc39069fa7fcccd1c2e567366ee6c243d.yml
openapi_spec_hash: 2a34451b288ea30af1cb61332c417c2a
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-52fd0b61e84fdc1cdd31ec12e1600510e9dd2f9d4fb20c2315b4975cb763ee98.yml
openapi_spec_hash: e851b8d5a2412f5fc9be82ab88ebdfde
config_hash: 11a6f0803eb407367c3f677d3e524c37
+2
View File
@@ -151,6 +151,7 @@ type Mode struct {
Model ModeModel `json:"model"`
Prompt string `json:"prompt"`
Temperature float64 `json:"temperature"`
TopP float64 `json:"topP"`
JSON modeJSON `json:"-"`
}
@@ -161,6 +162,7 @@ type modeJSON struct {
Model apijson.Field
Prompt apijson.Field
Temperature apijson.Field
TopP apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
+170 -3
View File
@@ -51,14 +51,16 @@ type Config struct {
// 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"`
DisabledProviders []string `json:"disabled_providers"`
Experimental ConfigExperimental `json:"experimental"`
Formatter map[string]ConfigFormatter `json:"formatter"`
// 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"`
Layout ConfigLayout `json:"layout"`
Lsp map[string]ConfigLsp `json:"lsp"`
// MCP (Model Context Protocol) server configurations
Mcp map[string]ConfigMcp `json:"mcp"`
// Modes configuration, see https://opencode.ai/docs/modes
@@ -66,6 +68,7 @@ type Config struct {
// Model to use in the format of provider/model, eg anthropic/claude-2
Model string `json:"model"`
Permission ConfigPermission `json:"permission"`
Plugin []string `json:"plugin"`
// Custom provider configurations and model overrides
Provider map[string]ConfigProvider `json:"provider"`
// Control sharing behavior:'manual' allows manual sharing via commands, 'auto'
@@ -89,13 +92,16 @@ type configJSON struct {
Autoupdate apijson.Field
DisabledProviders apijson.Field
Experimental apijson.Field
Formatter apijson.Field
Instructions apijson.Field
Keybinds apijson.Field
Layout apijson.Field
Lsp apijson.Field
Mcp apijson.Field
Mode apijson.Field
Model apijson.Field
Permission apijson.Field
Plugin apijson.Field
Provider apijson.Field
Share apijson.Field
SmallModel apijson.Field
@@ -247,6 +253,32 @@ func (r configExperimentalHookSessionCompletedJSON) RawJSON() string {
return r.raw
}
type ConfigFormatter struct {
Command []string `json:"command"`
Disabled bool `json:"disabled"`
Environment map[string]string `json:"environment"`
Extensions []string `json:"extensions"`
JSON configFormatterJSON `json:"-"`
}
// configFormatterJSON contains the JSON metadata for the struct [ConfigFormatter]
type configFormatterJSON struct {
Command apijson.Field
Disabled apijson.Field
Environment apijson.Field
Extensions apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *ConfigFormatter) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r configFormatterJSON) RawJSON() string {
return r.raw
}
// @deprecated Always uses stretch layout.
type ConfigLayout string
@@ -263,6 +295,139 @@ func (r ConfigLayout) IsKnown() bool {
return false
}
type ConfigLsp struct {
// This field can have the runtime type of [[]string].
Command interface{} `json:"command"`
Disabled bool `json:"disabled"`
// This field can have the runtime type of [map[string]string].
Env interface{} `json:"env"`
// This field can have the runtime type of [[]string].
Extensions interface{} `json:"extensions"`
// This field can have the runtime type of [map[string]interface{}].
Initialization interface{} `json:"initialization"`
JSON configLspJSON `json:"-"`
union ConfigLspUnion
}
// configLspJSON contains the JSON metadata for the struct [ConfigLsp]
type configLspJSON struct {
Command apijson.Field
Disabled apijson.Field
Env apijson.Field
Extensions apijson.Field
Initialization apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r configLspJSON) RawJSON() string {
return r.raw
}
func (r *ConfigLsp) UnmarshalJSON(data []byte) (err error) {
*r = ConfigLsp{}
err = apijson.UnmarshalRoot(data, &r.union)
if err != nil {
return err
}
return apijson.Port(r.union, &r)
}
// AsUnion returns a [ConfigLspUnion] interface which you can cast to the specific
// types for more type safety.
//
// Possible runtime types of the union are [ConfigLspDisabled], [ConfigLspObject].
func (r ConfigLsp) AsUnion() ConfigLspUnion {
return r.union
}
// Union satisfied by [ConfigLspDisabled] or [ConfigLspObject].
type ConfigLspUnion interface {
implementsConfigLsp()
}
func init() {
apijson.RegisterUnion(
reflect.TypeOf((*ConfigLspUnion)(nil)).Elem(),
"",
apijson.UnionVariant{
TypeFilter: gjson.JSON,
Type: reflect.TypeOf(ConfigLspDisabled{}),
},
apijson.UnionVariant{
TypeFilter: gjson.JSON,
Type: reflect.TypeOf(ConfigLspObject{}),
},
)
}
type ConfigLspDisabled struct {
Disabled ConfigLspDisabledDisabled `json:"disabled,required"`
JSON configLspDisabledJSON `json:"-"`
}
// configLspDisabledJSON contains the JSON metadata for the struct
// [ConfigLspDisabled]
type configLspDisabledJSON struct {
Disabled apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *ConfigLspDisabled) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r configLspDisabledJSON) RawJSON() string {
return r.raw
}
func (r ConfigLspDisabled) implementsConfigLsp() {}
type ConfigLspDisabledDisabled bool
const (
ConfigLspDisabledDisabledTrue ConfigLspDisabledDisabled = true
)
func (r ConfigLspDisabledDisabled) IsKnown() bool {
switch r {
case ConfigLspDisabledDisabledTrue:
return true
}
return false
}
type ConfigLspObject struct {
Command []string `json:"command,required"`
Disabled bool `json:"disabled"`
Env map[string]string `json:"env"`
Extensions []string `json:"extensions"`
Initialization map[string]interface{} `json:"initialization"`
JSON configLspObjectJSON `json:"-"`
}
// configLspObjectJSON contains the JSON metadata for the struct [ConfigLspObject]
type configLspObjectJSON struct {
Command apijson.Field
Disabled apijson.Field
Env apijson.Field
Extensions apijson.Field
Initialization apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *ConfigLspObject) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r configLspObjectJSON) RawJSON() string {
return r.raw
}
func (r ConfigLspObject) implementsConfigLsp() {}
type ConfigMcp struct {
// Type of MCP server connection
Type ConfigMcpType `json:"type,required"`
@@ -864,6 +1029,7 @@ type ModeConfig struct {
Prompt string `json:"prompt"`
Temperature float64 `json:"temperature"`
Tools map[string]bool `json:"tools"`
TopP float64 `json:"top_p"`
JSON modeConfigJSON `json:"-"`
}
@@ -874,6 +1040,7 @@ type modeConfigJSON struct {
Prompt apijson.Field
Temperature apijson.Field
Tools apijson.Field
TopP apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
+123 -121
View File
@@ -54,13 +54,13 @@ type EventListResponse struct {
// [EventListResponseEventMessageRemovedProperties],
// [EventListResponseEventMessagePartUpdatedProperties],
// [EventListResponseEventMessagePartRemovedProperties],
// [EventListResponseEventStorageWriteProperties], [Permission],
// [EventListResponseEventStorageWriteProperties],
// [EventListResponseEventFileEditedProperties], [interface{}], [Permission],
// [EventListResponseEventPermissionRepliedProperties],
// [EventListResponseEventFileEditedProperties],
// [EventListResponseEventSessionUpdatedProperties],
// [EventListResponseEventSessionDeletedProperties],
// [EventListResponseEventSessionIdleProperties],
// [EventListResponseEventSessionErrorProperties], [interface{}],
// [EventListResponseEventSessionErrorProperties],
// [EventListResponseEventFileWatcherUpdatedProperties],
// [EventListResponseEventIdeInstalledProperties].
Properties interface{} `json:"properties,required"`
@@ -100,11 +100,12 @@ func (r *EventListResponse) UnmarshalJSON(data []byte) (err error) {
// [EventListResponseEventMessageUpdated], [EventListResponseEventMessageRemoved],
// [EventListResponseEventMessagePartUpdated],
// [EventListResponseEventMessagePartRemoved],
// [EventListResponseEventStorageWrite], [EventListResponseEventPermissionUpdated],
// [EventListResponseEventPermissionReplied], [EventListResponseEventFileEdited],
// [EventListResponseEventStorageWrite], [EventListResponseEventFileEdited],
// [EventListResponseEventServerConnected],
// [EventListResponseEventPermissionUpdated],
// [EventListResponseEventPermissionReplied],
// [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted],
// [EventListResponseEventSessionIdle], [EventListResponseEventSessionError],
// [EventListResponseEventServerConnected],
// [EventListResponseEventFileWatcherUpdated],
// [EventListResponseEventIdeInstalled].
func (r EventListResponse) AsUnion() EventListResponseUnion {
@@ -116,11 +117,12 @@ func (r EventListResponse) AsUnion() EventListResponseUnion {
// [EventListResponseEventMessageUpdated], [EventListResponseEventMessageRemoved],
// [EventListResponseEventMessagePartUpdated],
// [EventListResponseEventMessagePartRemoved],
// [EventListResponseEventStorageWrite], [EventListResponseEventPermissionUpdated],
// [EventListResponseEventPermissionReplied], [EventListResponseEventFileEdited],
// [EventListResponseEventStorageWrite], [EventListResponseEventFileEdited],
// [EventListResponseEventServerConnected],
// [EventListResponseEventPermissionUpdated],
// [EventListResponseEventPermissionReplied],
// [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted],
// [EventListResponseEventSessionIdle], [EventListResponseEventSessionError],
// [EventListResponseEventServerConnected],
// [EventListResponseEventFileWatcherUpdated] or
// [EventListResponseEventIdeInstalled].
type EventListResponseUnion interface {
@@ -166,6 +168,16 @@ func init() {
Type: reflect.TypeOf(EventListResponseEventStorageWrite{}),
DiscriminatorValue: "storage.write",
},
apijson.UnionVariant{
TypeFilter: gjson.JSON,
Type: reflect.TypeOf(EventListResponseEventFileEdited{}),
DiscriminatorValue: "file.edited",
},
apijson.UnionVariant{
TypeFilter: gjson.JSON,
Type: reflect.TypeOf(EventListResponseEventServerConnected{}),
DiscriminatorValue: "server.connected",
},
apijson.UnionVariant{
TypeFilter: gjson.JSON,
Type: reflect.TypeOf(EventListResponseEventPermissionUpdated{}),
@@ -176,11 +188,6 @@ func init() {
Type: reflect.TypeOf(EventListResponseEventPermissionReplied{}),
DiscriminatorValue: "permission.replied",
},
apijson.UnionVariant{
TypeFilter: gjson.JSON,
Type: reflect.TypeOf(EventListResponseEventFileEdited{}),
DiscriminatorValue: "file.edited",
},
apijson.UnionVariant{
TypeFilter: gjson.JSON,
Type: reflect.TypeOf(EventListResponseEventSessionUpdated{}),
@@ -201,11 +208,6 @@ func init() {
Type: reflect.TypeOf(EventListResponseEventSessionError{}),
DiscriminatorValue: "session.error",
},
apijson.UnionVariant{
TypeFilter: gjson.JSON,
Type: reflect.TypeOf(EventListResponseEventServerConnected{}),
DiscriminatorValue: "server.connected",
},
apijson.UnionVariant{
TypeFilter: gjson.JSON,
Type: reflect.TypeOf(EventListResponseEventFileWatcherUpdated{}),
@@ -649,6 +651,105 @@ func (r EventListResponseEventStorageWriteType) IsKnown() bool {
return false
}
type EventListResponseEventFileEdited struct {
Properties EventListResponseEventFileEditedProperties `json:"properties,required"`
Type EventListResponseEventFileEditedType `json:"type,required"`
JSON eventListResponseEventFileEditedJSON `json:"-"`
}
// eventListResponseEventFileEditedJSON contains the JSON metadata for the struct
// [EventListResponseEventFileEdited]
type eventListResponseEventFileEditedJSON struct {
Properties apijson.Field
Type apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *EventListResponseEventFileEdited) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r eventListResponseEventFileEditedJSON) RawJSON() string {
return r.raw
}
func (r EventListResponseEventFileEdited) implementsEventListResponse() {}
type EventListResponseEventFileEditedProperties struct {
File string `json:"file,required"`
JSON eventListResponseEventFileEditedPropertiesJSON `json:"-"`
}
// eventListResponseEventFileEditedPropertiesJSON contains the JSON metadata for
// the struct [EventListResponseEventFileEditedProperties]
type eventListResponseEventFileEditedPropertiesJSON struct {
File apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *EventListResponseEventFileEditedProperties) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r eventListResponseEventFileEditedPropertiesJSON) RawJSON() string {
return r.raw
}
type EventListResponseEventFileEditedType string
const (
EventListResponseEventFileEditedTypeFileEdited EventListResponseEventFileEditedType = "file.edited"
)
func (r EventListResponseEventFileEditedType) IsKnown() bool {
switch r {
case EventListResponseEventFileEditedTypeFileEdited:
return true
}
return false
}
type EventListResponseEventServerConnected struct {
Properties interface{} `json:"properties,required"`
Type EventListResponseEventServerConnectedType `json:"type,required"`
JSON eventListResponseEventServerConnectedJSON `json:"-"`
}
// eventListResponseEventServerConnectedJSON contains the JSON metadata for the
// struct [EventListResponseEventServerConnected]
type eventListResponseEventServerConnectedJSON struct {
Properties apijson.Field
Type apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *EventListResponseEventServerConnected) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r eventListResponseEventServerConnectedJSON) RawJSON() string {
return r.raw
}
func (r EventListResponseEventServerConnected) implementsEventListResponse() {}
type EventListResponseEventServerConnectedType string
const (
EventListResponseEventServerConnectedTypeServerConnected EventListResponseEventServerConnectedType = "server.connected"
)
func (r EventListResponseEventServerConnectedType) IsKnown() bool {
switch r {
case EventListResponseEventServerConnectedTypeServerConnected:
return true
}
return false
}
type EventListResponseEventPermissionUpdated struct {
Properties Permission `json:"properties,required"`
Type EventListResponseEventPermissionUpdatedType `json:"type,required"`
@@ -752,66 +853,6 @@ func (r EventListResponseEventPermissionRepliedType) IsKnown() bool {
return false
}
type EventListResponseEventFileEdited struct {
Properties EventListResponseEventFileEditedProperties `json:"properties,required"`
Type EventListResponseEventFileEditedType `json:"type,required"`
JSON eventListResponseEventFileEditedJSON `json:"-"`
}
// eventListResponseEventFileEditedJSON contains the JSON metadata for the struct
// [EventListResponseEventFileEdited]
type eventListResponseEventFileEditedJSON struct {
Properties apijson.Field
Type apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *EventListResponseEventFileEdited) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r eventListResponseEventFileEditedJSON) RawJSON() string {
return r.raw
}
func (r EventListResponseEventFileEdited) implementsEventListResponse() {}
type EventListResponseEventFileEditedProperties struct {
File string `json:"file,required"`
JSON eventListResponseEventFileEditedPropertiesJSON `json:"-"`
}
// eventListResponseEventFileEditedPropertiesJSON contains the JSON metadata for
// the struct [EventListResponseEventFileEditedProperties]
type eventListResponseEventFileEditedPropertiesJSON struct {
File apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *EventListResponseEventFileEditedProperties) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r eventListResponseEventFileEditedPropertiesJSON) RawJSON() string {
return r.raw
}
type EventListResponseEventFileEditedType string
const (
EventListResponseEventFileEditedTypeFileEdited EventListResponseEventFileEditedType = "file.edited"
)
func (r EventListResponseEventFileEditedType) IsKnown() bool {
switch r {
case EventListResponseEventFileEditedTypeFileEdited:
return true
}
return false
}
type EventListResponseEventSessionUpdated struct {
Properties EventListResponseEventSessionUpdatedProperties `json:"properties,required"`
Type EventListResponseEventSessionUpdatedType `json:"type,required"`
@@ -1188,45 +1229,6 @@ func (r EventListResponseEventSessionErrorType) IsKnown() bool {
return false
}
type EventListResponseEventServerConnected struct {
Properties interface{} `json:"properties,required"`
Type EventListResponseEventServerConnectedType `json:"type,required"`
JSON eventListResponseEventServerConnectedJSON `json:"-"`
}
// eventListResponseEventServerConnectedJSON contains the JSON metadata for the
// struct [EventListResponseEventServerConnected]
type eventListResponseEventServerConnectedJSON struct {
Properties apijson.Field
Type apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *EventListResponseEventServerConnected) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r eventListResponseEventServerConnectedJSON) RawJSON() string {
return r.raw
}
func (r EventListResponseEventServerConnected) implementsEventListResponse() {}
type EventListResponseEventServerConnectedType string
const (
EventListResponseEventServerConnectedTypeServerConnected EventListResponseEventServerConnectedType = "server.connected"
)
func (r EventListResponseEventServerConnectedType) IsKnown() bool {
switch r {
case EventListResponseEventServerConnectedTypeServerConnected:
return true
}
return false
}
type EventListResponseEventFileWatcherUpdated struct {
Properties EventListResponseEventFileWatcherUpdatedProperties `json:"properties,required"`
Type EventListResponseEventFileWatcherUpdatedType `json:"type,required"`
@@ -1374,21 +1376,21 @@ const (
EventListResponseTypeMessagePartUpdated EventListResponseType = "message.part.updated"
EventListResponseTypeMessagePartRemoved EventListResponseType = "message.part.removed"
EventListResponseTypeStorageWrite EventListResponseType = "storage.write"
EventListResponseTypeFileEdited EventListResponseType = "file.edited"
EventListResponseTypeServerConnected EventListResponseType = "server.connected"
EventListResponseTypePermissionUpdated EventListResponseType = "permission.updated"
EventListResponseTypePermissionReplied EventListResponseType = "permission.replied"
EventListResponseTypeFileEdited EventListResponseType = "file.edited"
EventListResponseTypeSessionUpdated EventListResponseType = "session.updated"
EventListResponseTypeSessionDeleted EventListResponseType = "session.deleted"
EventListResponseTypeSessionIdle EventListResponseType = "session.idle"
EventListResponseTypeSessionError EventListResponseType = "session.error"
EventListResponseTypeServerConnected EventListResponseType = "server.connected"
EventListResponseTypeFileWatcherUpdated EventListResponseType = "file.watcher.updated"
EventListResponseTypeIdeInstalled EventListResponseType = "ide.installed"
)
func (r EventListResponseType) IsKnown() bool {
switch r {
case EventListResponseTypeInstallationUpdated, EventListResponseTypeLspClientDiagnostics, EventListResponseTypeMessageUpdated, EventListResponseTypeMessageRemoved, EventListResponseTypeMessagePartUpdated, EventListResponseTypeMessagePartRemoved, EventListResponseTypeStorageWrite, EventListResponseTypePermissionUpdated, EventListResponseTypePermissionReplied, EventListResponseTypeFileEdited, EventListResponseTypeSessionUpdated, EventListResponseTypeSessionDeleted, EventListResponseTypeSessionIdle, EventListResponseTypeSessionError, EventListResponseTypeServerConnected, EventListResponseTypeFileWatcherUpdated, EventListResponseTypeIdeInstalled:
case EventListResponseTypeInstallationUpdated, EventListResponseTypeLspClientDiagnostics, EventListResponseTypeMessageUpdated, EventListResponseTypeMessageRemoved, EventListResponseTypeMessagePartUpdated, EventListResponseTypeMessagePartRemoved, EventListResponseTypeStorageWrite, EventListResponseTypeFileEdited, EventListResponseTypeServerConnected, EventListResponseTypePermissionUpdated, EventListResponseTypePermissionReplied, EventListResponseTypeSessionUpdated, EventListResponseTypeSessionDeleted, EventListResponseTypeSessionIdle, EventListResponseTypeSessionError, EventListResponseTypeFileWatcherUpdated, EventListResponseTypeIdeInstalled:
return true
}
return false
+8 -2
View File
@@ -1,10 +1,16 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "0.0.0",
"version": "0.3.129",
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit"
},
"exports": {
".": "./dist/index.js"
".": {
"development": "./src/index.ts",
"import": "./dist/index.js"
}
},
"files": [
"dist"
+2 -9
View File
@@ -4,16 +4,11 @@ const dir = new URL("..", import.meta.url).pathname
process.chdir(dir)
import { $ } from "bun"
import fs from "fs/promises"
import path from "path"
console.log("=== Generating JS SDK ===")
console.log()
import { createClient } from "@hey-api/openapi-ts"
await fs.rm(path.join(dir, "src/gen"), { recursive: true, force: true })
await $`bun run ../../opencode/src/index.ts generate > openapi.json`
await $`bun dev generate > ${dir}/openapi.json`.cwd(path.resolve(dir, "../../opencode"))
await createClient({
input: "./openapi.json",
@@ -36,6 +31,4 @@ await createClient({
},
],
})
await $`rm -rf dist`
await $`bun tsc`
await $`bun prettier --write src/gen`
+2 -7
View File
@@ -5,20 +5,15 @@ process.chdir(dir)
import { $ } from "bun"
const version = process.env["OPENCODE_VERSION"]
if (!version) {
throw new Error("OPENCODE_VERSION is required")
}
await import("./generate")
await $`rm -rf dist`
await $`bun tsc`
const snapshot = process.env["OPENCODE_SNAPSHOT"] === "true"
await $`bun pm version --allow-same-version --no-git-tag-version ${version}`
if (snapshot) {
await $`bun publish --tag snapshot`
}
if (!snapshot) {
await $`bun publish`
}
await $`bun pm version 0.0.0 --no-git-tag-version`
+10 -6
View File
@@ -1,7 +1,7 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { ClientOptions } from './types.gen';
import { type Config, type ClientOptions as DefaultClientOptions, createClient, createConfig } from './client';
import type { ClientOptions } from "./types.gen"
import { type Config, type ClientOptions as DefaultClientOptions, createClient, createConfig } from "./client"
/**
* The `createClientConfig()` function will be called on client initialization
@@ -11,8 +11,12 @@ import { type Config, type ClientOptions as DefaultClientOptions, createClient,
* `setConfig()`. This is useful for example if you're using Next.js
* to ensure your client always has the correct values.
*/
export type CreateClientConfig<T extends DefaultClientOptions = ClientOptions> = (override?: Config<DefaultClientOptions & T>) => Config<Required<DefaultClientOptions> & T>;
export type CreateClientConfig<T extends DefaultClientOptions = ClientOptions> = (
override?: Config<DefaultClientOptions & T>,
) => Config<Required<DefaultClientOptions> & T>
export const client = createClient(createConfig<ClientOptions>({
baseUrl: 'http://localhost:4096'
}));
export const client = createClient(
createConfig<ClientOptions>({
baseUrl: "http://localhost:4096",
}),
)
+70 -80
View File
@@ -1,4 +1,4 @@
import type { Client, Config, RequestOptions } from './types';
import type { Client, Config, RequestOptions } from "./types"
import {
buildUrl,
createConfig,
@@ -7,189 +7,179 @@ import {
mergeConfigs,
mergeHeaders,
setAuthParams,
} from './utils';
} from "./utils"
type ReqInit = Omit<RequestInit, 'body' | 'headers'> & {
body?: any;
headers: ReturnType<typeof mergeHeaders>;
};
type ReqInit = Omit<RequestInit, "body" | "headers"> & {
body?: any
headers: ReturnType<typeof mergeHeaders>
}
export const createClient = (config: Config = {}): Client => {
let _config = mergeConfigs(createConfig(), config);
let _config = mergeConfigs(createConfig(), config)
const getConfig = (): Config => ({ ..._config });
const getConfig = (): Config => ({ ..._config })
const setConfig = (config: Config): Config => {
_config = mergeConfigs(_config, config);
return getConfig();
};
_config = mergeConfigs(_config, config)
return getConfig()
}
const interceptors = createInterceptors<
Request,
Response,
unknown,
RequestOptions
>();
const interceptors = createInterceptors<Request, Response, unknown, RequestOptions>()
const request: Client['request'] = async (options) => {
const request: Client["request"] = async (options) => {
const opts = {
..._config,
...options,
fetch: options.fetch ?? _config.fetch ?? globalThis.fetch,
headers: mergeHeaders(_config.headers, options.headers),
};
}
if (opts.security) {
await setAuthParams({
...opts,
security: opts.security,
});
})
}
if (opts.requestValidator) {
await opts.requestValidator(opts);
await opts.requestValidator(opts)
}
if (opts.body && opts.bodySerializer) {
opts.body = opts.bodySerializer(opts.body);
opts.body = opts.bodySerializer(opts.body)
}
// remove Content-Type header if body is empty to avoid sending invalid requests
if (opts.body === undefined || opts.body === '') {
opts.headers.delete('Content-Type');
if (opts.body === undefined || opts.body === "") {
opts.headers.delete("Content-Type")
}
const url = buildUrl(opts);
const url = buildUrl(opts)
const requestInit: ReqInit = {
redirect: 'follow',
redirect: "follow",
...opts,
};
}
let request = new Request(url, requestInit);
let request = new Request(url, requestInit)
for (const fn of interceptors.request._fns) {
if (fn) {
request = await fn(request, opts);
request = await fn(request, opts)
}
}
// fetch must be assigned here, otherwise it would throw the error:
// TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
const _fetch = opts.fetch!;
let response = await _fetch(request);
const _fetch = opts.fetch!
let response = await _fetch(request)
for (const fn of interceptors.response._fns) {
if (fn) {
response = await fn(response, request, opts);
response = await fn(response, request, opts)
}
}
const result = {
request,
response,
};
}
if (response.ok) {
if (
response.status === 204 ||
response.headers.get('Content-Length') === '0'
) {
return opts.responseStyle === 'data'
if (response.status === 204 || response.headers.get("Content-Length") === "0") {
return opts.responseStyle === "data"
? {}
: {
data: {},
...result,
};
}
}
const parseAs =
(opts.parseAs === 'auto'
? getParseAs(response.headers.get('Content-Type'))
: opts.parseAs) ?? 'json';
(opts.parseAs === "auto" ? getParseAs(response.headers.get("Content-Type")) : opts.parseAs) ?? "json"
let data: any;
let data: any
switch (parseAs) {
case 'arrayBuffer':
case 'blob':
case 'formData':
case 'json':
case 'text':
data = await response[parseAs]();
break;
case 'stream':
return opts.responseStyle === 'data'
case "arrayBuffer":
case "blob":
case "formData":
case "json":
case "text":
data = await response[parseAs]()
break
case "stream":
return opts.responseStyle === "data"
? response.body
: {
data: response.body,
...result,
};
}
}
if (parseAs === 'json') {
if (parseAs === "json") {
if (opts.responseValidator) {
await opts.responseValidator(data);
await opts.responseValidator(data)
}
if (opts.responseTransformer) {
data = await opts.responseTransformer(data);
data = await opts.responseTransformer(data)
}
}
return opts.responseStyle === 'data'
return opts.responseStyle === "data"
? data
: {
data,
...result,
};
}
}
const textError = await response.text();
let jsonError: unknown;
const textError = await response.text()
let jsonError: unknown
try {
jsonError = JSON.parse(textError);
jsonError = JSON.parse(textError)
} catch {
// noop
}
const error = jsonError ?? textError;
let finalError = error;
const error = jsonError ?? textError
let finalError = error
for (const fn of interceptors.error._fns) {
if (fn) {
finalError = (await fn(error, response, request, opts)) as string;
finalError = (await fn(error, response, request, opts)) as string
}
}
finalError = finalError || ({} as string);
finalError = finalError || ({} as string)
if (opts.throwOnError) {
throw finalError;
throw finalError
}
// TODO: we probably want to return error and improve types
return opts.responseStyle === 'data'
return opts.responseStyle === "data"
? undefined
: {
error: finalError,
...result,
};
};
}
}
return {
buildUrl,
connect: (options) => request({ ...options, method: 'CONNECT' }),
delete: (options) => request({ ...options, method: 'DELETE' }),
get: (options) => request({ ...options, method: 'GET' }),
connect: (options) => request({ ...options, method: "CONNECT" }),
delete: (options) => request({ ...options, method: "DELETE" }),
get: (options) => request({ ...options, method: "GET" }),
getConfig,
head: (options) => request({ ...options, method: 'HEAD' }),
head: (options) => request({ ...options, method: "HEAD" }),
interceptors,
options: (options) => request({ ...options, method: 'OPTIONS' }),
patch: (options) => request({ ...options, method: 'PATCH' }),
post: (options) => request({ ...options, method: 'POST' }),
put: (options) => request({ ...options, method: 'PUT' }),
options: (options) => request({ ...options, method: "OPTIONS" }),
patch: (options) => request({ ...options, method: "PATCH" }),
post: (options) => request({ ...options, method: "POST" }),
put: (options) => request({ ...options, method: "PUT" }),
request,
setConfig,
trace: (options) => request({ ...options, method: 'TRACE' }),
};
};
trace: (options) => request({ ...options, method: "TRACE" }),
}
}
+7 -11
View File
@@ -1,12 +1,8 @@
export type { Auth } from '../core/auth';
export type { QuerySerializerOptions } from '../core/bodySerializer';
export {
formDataBodySerializer,
jsonBodySerializer,
urlSearchParamsBodySerializer,
} from '../core/bodySerializer';
export { buildClientParams } from '../core/params';
export { createClient } from './client';
export type { Auth } from "../core/auth"
export type { QuerySerializerOptions } from "../core/bodySerializer"
export { formDataBodySerializer, jsonBodySerializer, urlSearchParamsBodySerializer } from "../core/bodySerializer"
export { buildClientParams } from "../core/params"
export { createClient } from "./client"
export type {
Client,
ClientOptions,
@@ -18,5 +14,5 @@ export type {
RequestResult,
ResponseStyle,
TDataShape,
} from './types';
export { createConfig, mergeHeaders } from './utils';
} from "./types"
export { createConfig, mergeHeaders } from "./utils"
+67 -98
View File
@@ -1,33 +1,30 @@
import type { Auth } from '../core/auth';
import type {
Client as CoreClient,
Config as CoreConfig,
} from '../core/types';
import type { Middleware } from './utils';
import type { Auth } from "../core/auth"
import type { Client as CoreClient, Config as CoreConfig } from "../core/types"
import type { Middleware } from "./utils"
export type ResponseStyle = 'data' | 'fields';
export type ResponseStyle = "data" | "fields"
export interface Config<T extends ClientOptions = ClientOptions>
extends Omit<RequestInit, 'body' | 'headers' | 'method'>,
extends Omit<RequestInit, "body" | "headers" | "method">,
CoreConfig {
/**
* Base URL for all requests made by this client.
*/
baseUrl?: T['baseUrl'];
baseUrl?: T["baseUrl"]
/**
* Fetch API implementation. You can use this option to provide a custom
* fetch instance.
*
* @default globalThis.fetch
*/
fetch?: (request: Request) => ReturnType<typeof fetch>;
fetch?: (request: Request) => ReturnType<typeof fetch>
/**
* Please don't use the Fetch client for Next.js applications. The `next`
* options won't have any effect.
*
* Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead.
*/
next?: never;
next?: never
/**
* Return the response data parsed in a specified format. By default, `auto`
* will infer the appropriate method from the `Content-Type` response header.
@@ -36,135 +33,118 @@ export interface Config<T extends ClientOptions = ClientOptions>
*
* @default 'auto'
*/
parseAs?:
| 'arrayBuffer'
| 'auto'
| 'blob'
| 'formData'
| 'json'
| 'stream'
| 'text';
parseAs?: "arrayBuffer" | "auto" | "blob" | "formData" | "json" | "stream" | "text"
/**
* Should we return only data or multiple fields (data, error, response, etc.)?
*
* @default 'fields'
*/
responseStyle?: ResponseStyle;
responseStyle?: ResponseStyle
/**
* Throw an error instead of returning it in the response?
*
* @default false
*/
throwOnError?: T['throwOnError'];
throwOnError?: T["throwOnError"]
}
export interface RequestOptions<
TResponseStyle extends ResponseStyle = 'fields',
TResponseStyle extends ResponseStyle = "fields",
ThrowOnError extends boolean = boolean,
Url extends string = string,
> extends Config<{
responseStyle: TResponseStyle;
throwOnError: ThrowOnError;
responseStyle: TResponseStyle
throwOnError: ThrowOnError
}> {
/**
* Any body that you want to add to your request.
*
* {@link https://developer.mozilla.org/docs/Web/API/fetch#body}
*/
body?: unknown;
path?: Record<string, unknown>;
query?: Record<string, unknown>;
body?: unknown
path?: Record<string, unknown>
query?: Record<string, unknown>
/**
* Security mechanism(s) to use for the request.
*/
security?: ReadonlyArray<Auth>;
url: Url;
security?: ReadonlyArray<Auth>
url: Url
}
export type RequestResult<
TData = unknown,
TError = unknown,
ThrowOnError extends boolean = boolean,
TResponseStyle extends ResponseStyle = 'fields',
TResponseStyle extends ResponseStyle = "fields",
> = ThrowOnError extends true
? Promise<
TResponseStyle extends 'data'
TResponseStyle extends "data"
? TData extends Record<string, unknown>
? TData[keyof TData]
: TData
: {
data: TData extends Record<string, unknown>
? TData[keyof TData]
: TData;
request: Request;
response: Response;
data: TData extends Record<string, unknown> ? TData[keyof TData] : TData
request: Request
response: Response
}
>
: Promise<
TResponseStyle extends 'data'
?
| (TData extends Record<string, unknown>
? TData[keyof TData]
: TData)
| undefined
TResponseStyle extends "data"
? (TData extends Record<string, unknown> ? TData[keyof TData] : TData) | undefined
: (
| {
data: TData extends Record<string, unknown>
? TData[keyof TData]
: TData;
error: undefined;
data: TData extends Record<string, unknown> ? TData[keyof TData] : TData
error: undefined
}
| {
data: undefined;
error: TError extends Record<string, unknown>
? TError[keyof TError]
: TError;
data: undefined
error: TError extends Record<string, unknown> ? TError[keyof TError] : TError
}
) & {
request: Request;
response: Response;
request: Request
response: Response
}
>;
>
export interface ClientOptions {
baseUrl?: string;
responseStyle?: ResponseStyle;
throwOnError?: boolean;
baseUrl?: string
responseStyle?: ResponseStyle
throwOnError?: boolean
}
type MethodFn = <
TData = unknown,
TError = unknown,
ThrowOnError extends boolean = false,
TResponseStyle extends ResponseStyle = 'fields',
TResponseStyle extends ResponseStyle = "fields",
>(
options: Omit<RequestOptions<TResponseStyle, ThrowOnError>, 'method'>,
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
options: Omit<RequestOptions<TResponseStyle, ThrowOnError>, "method">,
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>
type RequestFn = <
TData = unknown,
TError = unknown,
ThrowOnError extends boolean = false,
TResponseStyle extends ResponseStyle = 'fields',
TResponseStyle extends ResponseStyle = "fields",
>(
options: Omit<RequestOptions<TResponseStyle, ThrowOnError>, 'method'> &
Pick<Required<RequestOptions<TResponseStyle, ThrowOnError>>, 'method'>,
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
options: Omit<RequestOptions<TResponseStyle, ThrowOnError>, "method"> &
Pick<Required<RequestOptions<TResponseStyle, ThrowOnError>>, "method">,
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>
type BuildUrlFn = <
TData extends {
body?: unknown;
path?: Record<string, unknown>;
query?: Record<string, unknown>;
url: string;
body?: unknown
path?: Record<string, unknown>
query?: Record<string, unknown>
url: string
},
>(
options: Pick<TData, 'url'> & Options<TData>,
) => string;
options: Pick<TData, "url"> & Options<TData>,
) => string
export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn> & {
interceptors: Middleware<Request, Response, unknown, RequestOptions>;
};
interceptors: Middleware<Request, Response, unknown, RequestOptions>
}
/**
* The `createClientConfig()` function will be called on client initialization
@@ -176,47 +156,36 @@ export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn> & {
*/
export type CreateClientConfig<T extends ClientOptions = ClientOptions> = (
override?: Config<ClientOptions & T>,
) => Config<Required<ClientOptions> & T>;
) => Config<Required<ClientOptions> & T>
export interface TDataShape {
body?: unknown;
headers?: unknown;
path?: unknown;
query?: unknown;
url: string;
body?: unknown
headers?: unknown
path?: unknown
query?: unknown
url: string
}
type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>;
type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>
export type Options<
TData extends TDataShape = TDataShape,
ThrowOnError extends boolean = boolean,
TResponseStyle extends ResponseStyle = 'fields',
> = OmitKeys<
RequestOptions<TResponseStyle, ThrowOnError>,
'body' | 'path' | 'query' | 'url'
> &
Omit<TData, 'url'>;
TResponseStyle extends ResponseStyle = "fields",
> = OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, "body" | "path" | "query" | "url"> & Omit<TData, "url">
export type OptionsLegacyParser<
TData = unknown,
ThrowOnError extends boolean = boolean,
TResponseStyle extends ResponseStyle = 'fields',
TResponseStyle extends ResponseStyle = "fields",
> = TData extends { body?: any }
? TData extends { headers?: any }
? OmitKeys<
RequestOptions<TResponseStyle, ThrowOnError>,
'body' | 'headers' | 'url'
> &
TData
: OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, 'body' | 'url'> &
? OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, "body" | "headers" | "url"> & TData
: OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, "body" | "url"> &
TData &
Pick<RequestOptions<TResponseStyle, ThrowOnError>, 'headers'>
Pick<RequestOptions<TResponseStyle, ThrowOnError>, "headers">
: TData extends { headers?: any }
? OmitKeys<
RequestOptions<TResponseStyle, ThrowOnError>,
'headers' | 'url'
> &
? OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, "headers" | "url"> &
TData &
Pick<RequestOptions<TResponseStyle, ThrowOnError>, 'body'>
: OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, 'url'> & TData;
Pick<RequestOptions<TResponseStyle, ThrowOnError>, "body">
: OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, "url"> & TData
+154 -198
View File
@@ -1,64 +1,54 @@
import { getAuthToken } from '../core/auth';
import type {
QuerySerializer,
QuerySerializerOptions,
} from '../core/bodySerializer';
import { jsonBodySerializer } from '../core/bodySerializer';
import {
serializeArrayParam,
serializeObjectParam,
serializePrimitiveParam,
} from '../core/pathSerializer';
import type { Client, ClientOptions, Config, RequestOptions } from './types';
import { getAuthToken } from "../core/auth"
import type { QuerySerializer, QuerySerializerOptions } from "../core/bodySerializer"
import { jsonBodySerializer } from "../core/bodySerializer"
import { serializeArrayParam, serializeObjectParam, serializePrimitiveParam } from "../core/pathSerializer"
import type { Client, ClientOptions, Config, RequestOptions } from "./types"
interface PathSerializer {
path: Record<string, unknown>;
url: string;
path: Record<string, unknown>
url: string
}
const PATH_PARAM_RE = /\{[^{}]+\}/g;
const PATH_PARAM_RE = /\{[^{}]+\}/g
type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited';
type MatrixStyle = 'label' | 'matrix' | 'simple';
type ArraySeparatorStyle = ArrayStyle | MatrixStyle;
type ArrayStyle = "form" | "spaceDelimited" | "pipeDelimited"
type MatrixStyle = "label" | "matrix" | "simple"
type ArraySeparatorStyle = ArrayStyle | MatrixStyle
const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
let url = _url;
const matches = _url.match(PATH_PARAM_RE);
let url = _url
const matches = _url.match(PATH_PARAM_RE)
if (matches) {
for (const match of matches) {
let explode = false;
let name = match.substring(1, match.length - 1);
let style: ArraySeparatorStyle = 'simple';
let explode = false
let name = match.substring(1, match.length - 1)
let style: ArraySeparatorStyle = "simple"
if (name.endsWith('*')) {
explode = true;
name = name.substring(0, name.length - 1);
if (name.endsWith("*")) {
explode = true
name = name.substring(0, name.length - 1)
}
if (name.startsWith('.')) {
name = name.substring(1);
style = 'label';
} else if (name.startsWith(';')) {
name = name.substring(1);
style = 'matrix';
if (name.startsWith(".")) {
name = name.substring(1)
style = "label"
} else if (name.startsWith(";")) {
name = name.substring(1)
style = "matrix"
}
const value = path[name];
const value = path[name]
if (value === undefined || value === null) {
continue;
continue
}
if (Array.isArray(value)) {
url = url.replace(
match,
serializeArrayParam({ explode, name, style, value }),
);
continue;
url = url.replace(match, serializeArrayParam({ explode, name, style, value }))
continue
}
if (typeof value === 'object') {
if (typeof value === "object") {
url = url.replace(
match,
serializeObjectParam({
@@ -68,43 +58,37 @@ const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
value: value as Record<string, unknown>,
valueOnly: true,
}),
);
continue;
)
continue
}
if (style === 'matrix') {
if (style === "matrix") {
url = url.replace(
match,
`;${serializePrimitiveParam({
name,
value: value as string,
})}`,
);
continue;
)
continue
}
const replaceValue = encodeURIComponent(
style === 'label' ? `.${value as string}` : (value as string),
);
url = url.replace(match, replaceValue);
const replaceValue = encodeURIComponent(style === "label" ? `.${value as string}` : (value as string))
url = url.replace(match, replaceValue)
}
}
return url;
};
return url
}
export const createQuerySerializer = <T = unknown>({
allowReserved,
array,
object,
}: QuerySerializerOptions = {}) => {
export const createQuerySerializer = <T = unknown>({ allowReserved, array, object }: QuerySerializerOptions = {}) => {
const querySerializer = (queryParams: T) => {
const search: string[] = [];
if (queryParams && typeof queryParams === 'object') {
const search: string[] = []
if (queryParams && typeof queryParams === "object") {
for (const name in queryParams) {
const value = queryParams[name];
const value = queryParams[name]
if (value === undefined || value === null) {
continue;
continue
}
if (Array.isArray(value)) {
@@ -112,129 +96,120 @@ export const createQuerySerializer = <T = unknown>({
allowReserved,
explode: true,
name,
style: 'form',
style: "form",
value,
...array,
});
if (serializedArray) search.push(serializedArray);
} else if (typeof value === 'object') {
})
if (serializedArray) search.push(serializedArray)
} else if (typeof value === "object") {
const serializedObject = serializeObjectParam({
allowReserved,
explode: true,
name,
style: 'deepObject',
style: "deepObject",
value: value as Record<string, unknown>,
...object,
});
if (serializedObject) search.push(serializedObject);
})
if (serializedObject) search.push(serializedObject)
} else {
const serializedPrimitive = serializePrimitiveParam({
allowReserved,
name,
value: value as string,
});
if (serializedPrimitive) search.push(serializedPrimitive);
})
if (serializedPrimitive) search.push(serializedPrimitive)
}
}
}
return search.join('&');
};
return querySerializer;
};
return search.join("&")
}
return querySerializer
}
/**
* Infers parseAs value from provided Content-Type header.
*/
export const getParseAs = (
contentType: string | null,
): Exclude<Config['parseAs'], 'auto'> => {
export const getParseAs = (contentType: string | null): Exclude<Config["parseAs"], "auto"> => {
if (!contentType) {
// If no Content-Type header is provided, the best we can do is return the raw response body,
// which is effectively the same as the 'stream' option.
return 'stream';
return "stream"
}
const cleanContent = contentType.split(';')[0]?.trim();
const cleanContent = contentType.split(";")[0]?.trim()
if (!cleanContent) {
return;
return
}
if (
cleanContent.startsWith('application/json') ||
cleanContent.endsWith('+json')
) {
return 'json';
if (cleanContent.startsWith("application/json") || cleanContent.endsWith("+json")) {
return "json"
}
if (cleanContent === 'multipart/form-data') {
return 'formData';
if (cleanContent === "multipart/form-data") {
return "formData"
}
if (
['application/', 'audio/', 'image/', 'video/'].some((type) =>
cleanContent.startsWith(type),
)
) {
return 'blob';
if (["application/", "audio/", "image/", "video/"].some((type) => cleanContent.startsWith(type))) {
return "blob"
}
if (cleanContent.startsWith('text/')) {
return 'text';
if (cleanContent.startsWith("text/")) {
return "text"
}
return;
};
return
}
export const setAuthParams = async ({
security,
...options
}: Pick<Required<RequestOptions>, 'security'> &
Pick<RequestOptions, 'auth' | 'query'> & {
headers: Headers;
}: Pick<Required<RequestOptions>, "security"> &
Pick<RequestOptions, "auth" | "query"> & {
headers: Headers
}) => {
for (const auth of security) {
const token = await getAuthToken(auth, options.auth);
const token = await getAuthToken(auth, options.auth)
if (!token) {
continue;
continue
}
const name = auth.name ?? 'Authorization';
const name = auth.name ?? "Authorization"
switch (auth.in) {
case 'query':
case "query":
if (!options.query) {
options.query = {};
options.query = {}
}
options.query[name] = token;
break;
case 'cookie':
options.headers.append('Cookie', `${name}=${token}`);
break;
case 'header':
options.query[name] = token
break
case "cookie":
options.headers.append("Cookie", `${name}=${token}`)
break
case "header":
default:
options.headers.set(name, token);
break;
options.headers.set(name, token)
break
}
return;
return
}
};
}
export const buildUrl: Client['buildUrl'] = (options) => {
export const buildUrl: Client["buildUrl"] = (options) => {
const url = getUrl({
baseUrl: options.baseUrl as string,
path: options.path,
query: options.query,
querySerializer:
typeof options.querySerializer === 'function'
typeof options.querySerializer === "function"
? options.querySerializer
: createQuerySerializer(options.querySerializer),
url: options.url,
});
return url;
};
})
return url
}
export const getUrl = ({
baseUrl,
@@ -243,144 +218,125 @@ export const getUrl = ({
querySerializer,
url: _url,
}: {
baseUrl?: string;
path?: Record<string, unknown>;
query?: Record<string, unknown>;
querySerializer: QuerySerializer;
url: string;
baseUrl?: string
path?: Record<string, unknown>
query?: Record<string, unknown>
querySerializer: QuerySerializer
url: string
}) => {
const pathUrl = _url.startsWith('/') ? _url : `/${_url}`;
let url = (baseUrl ?? '') + pathUrl;
const pathUrl = _url.startsWith("/") ? _url : `/${_url}`
let url = (baseUrl ?? "") + pathUrl
if (path) {
url = defaultPathSerializer({ path, url });
url = defaultPathSerializer({ path, url })
}
let search = query ? querySerializer(query) : '';
if (search.startsWith('?')) {
search = search.substring(1);
let search = query ? querySerializer(query) : ""
if (search.startsWith("?")) {
search = search.substring(1)
}
if (search) {
url += `?${search}`;
url += `?${search}`
}
return url;
};
return url
}
export const mergeConfigs = (a: Config, b: Config): Config => {
const config = { ...a, ...b };
if (config.baseUrl?.endsWith('/')) {
config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1);
const config = { ...a, ...b }
if (config.baseUrl?.endsWith("/")) {
config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1)
}
config.headers = mergeHeaders(a.headers, b.headers);
return config;
};
config.headers = mergeHeaders(a.headers, b.headers)
return config
}
export const mergeHeaders = (
...headers: Array<Required<Config>['headers'] | undefined>
): Headers => {
const mergedHeaders = new Headers();
export const mergeHeaders = (...headers: Array<Required<Config>["headers"] | undefined>): Headers => {
const mergedHeaders = new Headers()
for (const header of headers) {
if (!header || typeof header !== 'object') {
continue;
if (!header || typeof header !== "object") {
continue
}
const iterator =
header instanceof Headers ? header.entries() : Object.entries(header);
const iterator = header instanceof Headers ? header.entries() : Object.entries(header)
for (const [key, value] of iterator) {
if (value === null) {
mergedHeaders.delete(key);
mergedHeaders.delete(key)
} else if (Array.isArray(value)) {
for (const v of value) {
mergedHeaders.append(key, v as string);
mergedHeaders.append(key, v as string)
}
} else if (value !== undefined) {
// assume object headers are meant to be JSON stringified, i.e. their
// content value in OpenAPI specification is 'application/json'
mergedHeaders.set(
key,
typeof value === 'object' ? JSON.stringify(value) : (value as string),
);
mergedHeaders.set(key, typeof value === "object" ? JSON.stringify(value) : (value as string))
}
}
}
return mergedHeaders;
};
return mergedHeaders
}
type ErrInterceptor<Err, Res, Req, Options> = (
error: Err,
response: Res,
request: Req,
options: Options,
) => Err | Promise<Err>;
) => Err | Promise<Err>
type ReqInterceptor<Req, Options> = (
request: Req,
options: Options,
) => Req | Promise<Req>;
type ReqInterceptor<Req, Options> = (request: Req, options: Options) => Req | Promise<Req>
type ResInterceptor<Res, Req, Options> = (
response: Res,
request: Req,
options: Options,
) => Res | Promise<Res>;
type ResInterceptor<Res, Req, Options> = (response: Res, request: Req, options: Options) => Res | Promise<Res>
class Interceptors<Interceptor> {
_fns: (Interceptor | null)[];
_fns: (Interceptor | null)[]
constructor() {
this._fns = [];
this._fns = []
}
clear() {
this._fns = [];
this._fns = []
}
getInterceptorIndex(id: number | Interceptor): number {
if (typeof id === 'number') {
return this._fns[id] ? id : -1;
if (typeof id === "number") {
return this._fns[id] ? id : -1
} else {
return this._fns.indexOf(id);
return this._fns.indexOf(id)
}
}
exists(id: number | Interceptor) {
const index = this.getInterceptorIndex(id);
return !!this._fns[index];
const index = this.getInterceptorIndex(id)
return !!this._fns[index]
}
eject(id: number | Interceptor) {
const index = this.getInterceptorIndex(id);
const index = this.getInterceptorIndex(id)
if (this._fns[index]) {
this._fns[index] = null;
this._fns[index] = null
}
}
update(id: number | Interceptor, fn: Interceptor) {
const index = this.getInterceptorIndex(id);
const index = this.getInterceptorIndex(id)
if (this._fns[index]) {
this._fns[index] = fn;
return id;
this._fns[index] = fn
return id
} else {
return false;
return false
}
}
use(fn: Interceptor) {
this._fns = [...this._fns, fn];
return this._fns.length - 1;
this._fns = [...this._fns, fn]
return this._fns.length - 1
}
}
// `createInterceptors()` response, meant for external use as it does not
// expose internals
export interface Middleware<Req, Res, Err, Options> {
error: Pick<
Interceptors<ErrInterceptor<Err, Res, Req, Options>>,
'eject' | 'use'
>;
request: Pick<Interceptors<ReqInterceptor<Req, Options>>, 'eject' | 'use'>;
response: Pick<
Interceptors<ResInterceptor<Res, Req, Options>>,
'eject' | 'use'
>;
error: Pick<Interceptors<ErrInterceptor<Err, Res, Req, Options>>, "eject" | "use">
request: Pick<Interceptors<ReqInterceptor<Req, Options>>, "eject" | "use">
response: Pick<Interceptors<ResInterceptor<Res, Req, Options>>, "eject" | "use">
}
// do not add `Middleware` as return type so we can use _fns internally
@@ -388,30 +344,30 @@ export const createInterceptors = <Req, Res, Err, Options>() => ({
error: new Interceptors<ErrInterceptor<Err, Res, Req, Options>>(),
request: new Interceptors<ReqInterceptor<Req, Options>>(),
response: new Interceptors<ResInterceptor<Res, Req, Options>>(),
});
})
const defaultQuerySerializer = createQuerySerializer({
allowReserved: false,
array: {
explode: true,
style: 'form',
style: "form",
},
object: {
explode: true,
style: 'deepObject',
style: "deepObject",
},
});
})
const defaultHeaders = {
'Content-Type': 'application/json',
};
"Content-Type": "application/json",
}
export const createConfig = <T extends ClientOptions = ClientOptions>(
override: Config<Omit<ClientOptions, keyof T> & T> = {},
): Config<Omit<ClientOptions, keyof T> & T> => ({
...jsonBodySerializer,
headers: defaultHeaders,
parseAs: 'auto',
parseAs: "auto",
querySerializer: defaultQuerySerializer,
...override,
});
})
+13 -14
View File
@@ -1,4 +1,4 @@
export type AuthToken = string | undefined;
export type AuthToken = string | undefined
export interface Auth {
/**
@@ -6,35 +6,34 @@ export interface Auth {
*
* @default 'header'
*/
in?: 'header' | 'query' | 'cookie';
in?: "header" | "query" | "cookie"
/**
* Header or query parameter name.
*
* @default 'Authorization'
*/
name?: string;
scheme?: 'basic' | 'bearer';
type: 'apiKey' | 'http';
name?: string
scheme?: "basic" | "bearer"
type: "apiKey" | "http"
}
export const getAuthToken = async (
auth: Auth,
callback: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken,
): Promise<string | undefined> => {
const token =
typeof callback === 'function' ? await callback(auth) : callback;
const token = typeof callback === "function" ? await callback(auth) : callback
if (!token) {
return;
return
}
if (auth.scheme === 'bearer') {
return `Bearer ${token}`;
if (auth.scheme === "bearer") {
return `Bearer ${token}`
}
if (auth.scheme === 'basic') {
return `Basic ${btoa(token)}`;
if (auth.scheme === "basic") {
return `Basic ${btoa(token)}`
}
return token;
};
return token
}
+34 -52
View File
@@ -1,88 +1,70 @@
import type {
ArrayStyle,
ObjectStyle,
SerializerOptions,
} from './pathSerializer';
import type { ArrayStyle, ObjectStyle, SerializerOptions } from "./pathSerializer"
export type QuerySerializer = (query: Record<string, unknown>) => string;
export type QuerySerializer = (query: Record<string, unknown>) => string
export type BodySerializer = (body: any) => any;
export type BodySerializer = (body: any) => any
export interface QuerySerializerOptions {
allowReserved?: boolean;
array?: SerializerOptions<ArrayStyle>;
object?: SerializerOptions<ObjectStyle>;
allowReserved?: boolean
array?: SerializerOptions<ArrayStyle>
object?: SerializerOptions<ObjectStyle>
}
const serializeFormDataPair = (
data: FormData,
key: string,
value: unknown,
): void => {
if (typeof value === 'string' || value instanceof Blob) {
data.append(key, value);
const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => {
if (typeof value === "string" || value instanceof Blob) {
data.append(key, value)
} else {
data.append(key, JSON.stringify(value));
data.append(key, JSON.stringify(value))
}
};
}
const serializeUrlSearchParamsPair = (
data: URLSearchParams,
key: string,
value: unknown,
): void => {
if (typeof value === 'string') {
data.append(key, value);
const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => {
if (typeof value === "string") {
data.append(key, value)
} else {
data.append(key, JSON.stringify(value));
data.append(key, JSON.stringify(value))
}
};
}
export const formDataBodySerializer = {
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(
body: T,
): FormData => {
const data = new FormData();
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(body: T): FormData => {
const data = new FormData()
Object.entries(body).forEach(([key, value]) => {
if (value === undefined || value === null) {
return;
return
}
if (Array.isArray(value)) {
value.forEach((v) => serializeFormDataPair(data, key, v));
value.forEach((v) => serializeFormDataPair(data, key, v))
} else {
serializeFormDataPair(data, key, value);
serializeFormDataPair(data, key, value)
}
});
})
return data;
return data
},
};
}
export const jsonBodySerializer = {
bodySerializer: <T>(body: T): string =>
JSON.stringify(body, (_key, value) =>
typeof value === 'bigint' ? value.toString() : value,
),
};
JSON.stringify(body, (_key, value) => (typeof value === "bigint" ? value.toString() : value)),
}
export const urlSearchParamsBodySerializer = {
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(
body: T,
): string => {
const data = new URLSearchParams();
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(body: T): string => {
const data = new URLSearchParams()
Object.entries(body).forEach(([key, value]) => {
if (value === undefined || value === null) {
return;
return
}
if (Array.isArray(value)) {
value.forEach((v) => serializeUrlSearchParamsPair(data, key, v));
value.forEach((v) => serializeUrlSearchParamsPair(data, key, v))
} else {
serializeUrlSearchParamsPair(data, key, value);
serializeUrlSearchParamsPair(data, key, value)
}
});
})
return data.toString();
return data.toString()
},
};
}
+56 -65
View File
@@ -1,142 +1,133 @@
type Slot = 'body' | 'headers' | 'path' | 'query';
type Slot = "body" | "headers" | "path" | "query"
export type Field =
| {
in: Exclude<Slot, 'body'>;
in: Exclude<Slot, "body">
/**
* Field name. This is the name we want the user to see and use.
*/
key: string;
key: string
/**
* Field mapped name. This is the name we want to use in the request.
* If omitted, we use the same value as `key`.
*/
map?: string;
map?: string
}
| {
in: Extract<Slot, 'body'>;
in: Extract<Slot, "body">
/**
* Key isn't required for bodies.
*/
key?: string;
map?: string;
};
key?: string
map?: string
}
export interface Fields {
allowExtra?: Partial<Record<Slot, boolean>>;
args?: ReadonlyArray<Field>;
allowExtra?: Partial<Record<Slot, boolean>>
args?: ReadonlyArray<Field>
}
export type FieldsConfig = ReadonlyArray<Field | Fields>;
export type FieldsConfig = ReadonlyArray<Field | Fields>
const extraPrefixesMap: Record<string, Slot> = {
$body_: 'body',
$headers_: 'headers',
$path_: 'path',
$query_: 'query',
};
const extraPrefixes = Object.entries(extraPrefixesMap);
$body_: "body",
$headers_: "headers",
$path_: "path",
$query_: "query",
}
const extraPrefixes = Object.entries(extraPrefixesMap)
type KeyMap = Map<
string,
{
in: Slot;
map?: string;
in: Slot
map?: string
}
>;
>
const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => {
if (!map) {
map = new Map();
map = new Map()
}
for (const config of fields) {
if ('in' in config) {
if ("in" in config) {
if (config.key) {
map.set(config.key, {
in: config.in,
map: config.map,
});
})
}
} else if (config.args) {
buildKeyMap(config.args, map);
buildKeyMap(config.args, map)
}
}
return map;
};
return map
}
interface Params {
body: unknown;
headers: Record<string, unknown>;
path: Record<string, unknown>;
query: Record<string, unknown>;
body: unknown
headers: Record<string, unknown>
path: Record<string, unknown>
query: Record<string, unknown>
}
const stripEmptySlots = (params: Params) => {
for (const [slot, value] of Object.entries(params)) {
if (value && typeof value === 'object' && !Object.keys(value).length) {
delete params[slot as Slot];
if (value && typeof value === "object" && !Object.keys(value).length) {
delete params[slot as Slot]
}
}
};
}
export const buildClientParams = (
args: ReadonlyArray<unknown>,
fields: FieldsConfig,
) => {
export const buildClientParams = (args: ReadonlyArray<unknown>, fields: FieldsConfig) => {
const params: Params = {
body: {},
headers: {},
path: {},
query: {},
};
}
const map = buildKeyMap(fields);
const map = buildKeyMap(fields)
let config: FieldsConfig[number] | undefined;
let config: FieldsConfig[number] | undefined
for (const [index, arg] of args.entries()) {
if (fields[index]) {
config = fields[index];
config = fields[index]
}
if (!config) {
continue;
continue
}
if ('in' in config) {
if ("in" in config) {
if (config.key) {
const field = map.get(config.key)!;
const name = field.map || config.key;
(params[field.in] as Record<string, unknown>)[name] = arg;
const field = map.get(config.key)!
const name = field.map || config.key
;(params[field.in] as Record<string, unknown>)[name] = arg
} else {
params.body = arg;
params.body = arg
}
} else {
for (const [key, value] of Object.entries(arg ?? {})) {
const field = map.get(key);
const field = map.get(key)
if (field) {
const name = field.map || key;
(params[field.in] as Record<string, unknown>)[name] = value;
const name = field.map || key
;(params[field.in] as Record<string, unknown>)[name] = value
} else {
const extra = extraPrefixes.find(([prefix]) =>
key.startsWith(prefix),
);
const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix))
if (extra) {
const [prefix, slot] = extra;
(params[slot] as Record<string, unknown>)[
key.slice(prefix.length)
] = value;
const [prefix, slot] = extra
;(params[slot] as Record<string, unknown>)[key.slice(prefix.length)] = value
} else {
for (const [slot, allowed] of Object.entries(
config.allowExtra ?? {},
)) {
for (const [slot, allowed] of Object.entries(config.allowExtra ?? {})) {
if (allowed) {
(params[slot as Slot] as Record<string, unknown>)[key] = value;
break;
;(params[slot as Slot] as Record<string, unknown>)[key] = value
break
}
}
}
@@ -145,7 +136,7 @@ export const buildClientParams = (
}
}
stripEmptySlots(params);
stripEmptySlots(params)
return params;
};
return params
}
+80 -94
View File
@@ -1,68 +1,66 @@
interface SerializeOptions<T>
extends SerializePrimitiveOptions,
SerializerOptions<T> {}
interface SerializeOptions<T> extends SerializePrimitiveOptions, SerializerOptions<T> {}
interface SerializePrimitiveOptions {
allowReserved?: boolean;
name: string;
allowReserved?: boolean
name: string
}
export interface SerializerOptions<T> {
/**
* @default true
*/
explode: boolean;
style: T;
explode: boolean
style: T
}
export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited';
export type ArraySeparatorStyle = ArrayStyle | MatrixStyle;
type MatrixStyle = 'label' | 'matrix' | 'simple';
export type ObjectStyle = 'form' | 'deepObject';
type ObjectSeparatorStyle = ObjectStyle | MatrixStyle;
export type ArrayStyle = "form" | "spaceDelimited" | "pipeDelimited"
export type ArraySeparatorStyle = ArrayStyle | MatrixStyle
type MatrixStyle = "label" | "matrix" | "simple"
export type ObjectStyle = "form" | "deepObject"
type ObjectSeparatorStyle = ObjectStyle | MatrixStyle
interface SerializePrimitiveParam extends SerializePrimitiveOptions {
value: string;
value: string
}
export const separatorArrayExplode = (style: ArraySeparatorStyle) => {
switch (style) {
case 'label':
return '.';
case 'matrix':
return ';';
case 'simple':
return ',';
case "label":
return "."
case "matrix":
return ";"
case "simple":
return ","
default:
return '&';
return "&"
}
};
}
export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => {
switch (style) {
case 'form':
return ',';
case 'pipeDelimited':
return '|';
case 'spaceDelimited':
return '%20';
case "form":
return ","
case "pipeDelimited":
return "|"
case "spaceDelimited":
return "%20"
default:
return ',';
return ","
}
};
}
export const separatorObjectExplode = (style: ObjectSeparatorStyle) => {
switch (style) {
case 'label':
return '.';
case 'matrix':
return ';';
case 'simple':
return ',';
case "label":
return "."
case "matrix":
return ";"
case "simple":
return ","
default:
return '&';
return "&"
}
};
}
export const serializeArrayParam = ({
allowReserved,
@@ -71,60 +69,54 @@ export const serializeArrayParam = ({
style,
value,
}: SerializeOptions<ArraySeparatorStyle> & {
value: unknown[];
value: unknown[]
}) => {
if (!explode) {
const joinedValues = (
allowReserved ? value : value.map((v) => encodeURIComponent(v as string))
).join(separatorArrayNoExplode(style));
const joinedValues = (allowReserved ? value : value.map((v) => encodeURIComponent(v as string))).join(
separatorArrayNoExplode(style),
)
switch (style) {
case 'label':
return `.${joinedValues}`;
case 'matrix':
return `;${name}=${joinedValues}`;
case 'simple':
return joinedValues;
case "label":
return `.${joinedValues}`
case "matrix":
return `;${name}=${joinedValues}`
case "simple":
return joinedValues
default:
return `${name}=${joinedValues}`;
return `${name}=${joinedValues}`
}
}
const separator = separatorArrayExplode(style);
const separator = separatorArrayExplode(style)
const joinedValues = value
.map((v) => {
if (style === 'label' || style === 'simple') {
return allowReserved ? v : encodeURIComponent(v as string);
if (style === "label" || style === "simple") {
return allowReserved ? v : encodeURIComponent(v as string)
}
return serializePrimitiveParam({
allowReserved,
name,
value: v as string,
});
})
})
.join(separator);
return style === 'label' || style === 'matrix'
? separator + joinedValues
: joinedValues;
};
.join(separator)
return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues
}
export const serializePrimitiveParam = ({
allowReserved,
name,
value,
}: SerializePrimitiveParam) => {
export const serializePrimitiveParam = ({ allowReserved, name, value }: SerializePrimitiveParam) => {
if (value === undefined || value === null) {
return '';
return ""
}
if (typeof value === 'object') {
if (typeof value === "object") {
throw new Error(
'Deeply-nested arrays/objects arent supported. Provide your own `querySerializer()` to handle these.',
);
"Deeply-nested arrays/objects arent supported. Provide your own `querySerializer()` to handle these.",
)
}
return `${name}=${allowReserved ? value : encodeURIComponent(value)}`;
};
return `${name}=${allowReserved ? value : encodeURIComponent(value)}`
}
export const serializeObjectParam = ({
allowReserved,
@@ -134,46 +126,40 @@ export const serializeObjectParam = ({
value,
valueOnly,
}: SerializeOptions<ObjectSeparatorStyle> & {
value: Record<string, unknown> | Date;
valueOnly?: boolean;
value: Record<string, unknown> | Date
valueOnly?: boolean
}) => {
if (value instanceof Date) {
return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`;
return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`
}
if (style !== 'deepObject' && !explode) {
let values: string[] = [];
if (style !== "deepObject" && !explode) {
let values: string[] = []
Object.entries(value).forEach(([key, v]) => {
values = [
...values,
key,
allowReserved ? (v as string) : encodeURIComponent(v as string),
];
});
const joinedValues = values.join(',');
values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)]
})
const joinedValues = values.join(",")
switch (style) {
case 'form':
return `${name}=${joinedValues}`;
case 'label':
return `.${joinedValues}`;
case 'matrix':
return `;${name}=${joinedValues}`;
case "form":
return `${name}=${joinedValues}`
case "label":
return `.${joinedValues}`
case "matrix":
return `;${name}=${joinedValues}`
default:
return joinedValues;
return joinedValues
}
}
const separator = separatorObjectExplode(style);
const separator = separatorObjectExplode(style)
const joinedValues = Object.entries(value)
.map(([key, v]) =>
serializePrimitiveParam({
allowReserved,
name: style === 'deepObject' ? `${name}[${key}]` : key,
name: style === "deepObject" ? `${name}[${key}]` : key,
value: v as string,
}),
)
.join(separator);
return style === 'label' || style === 'matrix'
? separator + joinedValues
: joinedValues;
};
.join(separator)
return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues
}
+28 -57
View File
@@ -1,32 +1,23 @@
import type { Auth, AuthToken } from './auth';
import type {
BodySerializer,
QuerySerializer,
QuerySerializerOptions,
} from './bodySerializer';
import type { Auth, AuthToken } from "./auth"
import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from "./bodySerializer"
export interface Client<
RequestFn = never,
Config = unknown,
MethodFn = never,
BuildUrlFn = never,
> {
export interface Client<RequestFn = never, Config = unknown, MethodFn = never, BuildUrlFn = never> {
/**
* Returns the final request URL.
*/
buildUrl: BuildUrlFn;
connect: MethodFn;
delete: MethodFn;
get: MethodFn;
getConfig: () => Config;
head: MethodFn;
options: MethodFn;
patch: MethodFn;
post: MethodFn;
put: MethodFn;
request: RequestFn;
setConfig: (config: Config) => Config;
trace: MethodFn;
buildUrl: BuildUrlFn
connect: MethodFn
delete: MethodFn
get: MethodFn
getConfig: () => Config
head: MethodFn
options: MethodFn
patch: MethodFn
post: MethodFn
put: MethodFn
request: RequestFn
setConfig: (config: Config) => Config
trace: MethodFn
}
export interface Config {
@@ -34,12 +25,12 @@ export interface Config {
* Auth token or a function returning auth token. The resolved value will be
* added to the request payload as defined by its `security` array.
*/
auth?: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken;
auth?: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken
/**
* A function for serializing request body parameter. By default,
* {@link JSON.stringify()} will be used.
*/
bodySerializer?: BodySerializer | null;
bodySerializer?: BodySerializer | null
/**
* An object containing any HTTP headers that you want to pre-populate your
* `Headers` object with.
@@ -47,32 +38,14 @@ export interface Config {
* {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more}
*/
headers?:
| RequestInit['headers']
| Record<
string,
| string
| number
| boolean
| (string | number | boolean)[]
| null
| undefined
| unknown
>;
| RequestInit["headers"]
| Record<string, string | number | boolean | (string | number | boolean)[] | null | undefined | unknown>
/**
* The request method.
*
* {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more}
*/
method?:
| 'CONNECT'
| 'DELETE'
| 'GET'
| 'HEAD'
| 'OPTIONS'
| 'PATCH'
| 'POST'
| 'PUT'
| 'TRACE';
method?: "CONNECT" | "DELETE" | "GET" | "HEAD" | "OPTIONS" | "PATCH" | "POST" | "PUT" | "TRACE"
/**
* A function for serializing request query parameters. By default, arrays
* will be exploded in form style, objects will be exploded in deepObject
@@ -83,24 +56,24 @@ export interface Config {
*
* {@link https://swagger.io/docs/specification/serialization/#query View examples}
*/
querySerializer?: QuerySerializer | QuerySerializerOptions;
querySerializer?: QuerySerializer | QuerySerializerOptions
/**
* A function validating request data. This is useful if you want to ensure
* the request conforms to the desired shape, so it can be safely sent to
* the server.
*/
requestValidator?: (data: unknown) => Promise<unknown>;
requestValidator?: (data: unknown) => Promise<unknown>
/**
* A function transforming response data before it's returned. This is useful
* for post-processing data, e.g. converting ISO strings into Date objects.
*/
responseTransformer?: (data: unknown) => Promise<unknown>;
responseTransformer?: (data: unknown) => Promise<unknown>
/**
* A function validating response data. This is useful if you want to ensure
* the response conforms to the desired shape, so it can be safely passed to
* the transformers and returned to the user.
*/
responseValidator?: (data: unknown) => Promise<unknown>;
responseValidator?: (data: unknown) => Promise<unknown>
}
type IsExactlyNeverOrNeverUndefined<T> = [T] extends [never]
@@ -109,10 +82,8 @@ type IsExactlyNeverOrNeverUndefined<T> = [T] extends [never]
? [undefined] extends [T]
? false
: true
: false;
: false
export type OmitNever<T extends Record<string, unknown>> = {
[K in keyof T as IsExactlyNeverOrNeverUndefined<T[K]> extends true
? never
: K]: T[K];
};
[K in keyof T as IsExactlyNeverOrNeverUndefined<T[K]> extends true ? never : K]: T[K]
}
+473 -396
View File
@@ -1,426 +1,503 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { Options as ClientOptions, TDataShape, Client } from './client';
import type { EventSubscribeData, EventSubscribeResponses, AppGetData, AppGetResponses, AppInitData, AppInitResponses, ConfigGetData, ConfigGetResponses, SessionListData, SessionListResponses, SessionCreateData, SessionCreateResponses, SessionCreateErrors, SessionDeleteData, SessionDeleteResponses, SessionInitData, SessionInitResponses, SessionAbortData, SessionAbortResponses, SessionUnshareData, SessionUnshareResponses, SessionShareData, SessionShareResponses, SessionSummarizeData, SessionSummarizeResponses, SessionMessagesData, SessionMessagesResponses, SessionChatData, SessionChatResponses, SessionMessageData, SessionMessageResponses, SessionRevertData, SessionRevertResponses, SessionUnrevertData, SessionUnrevertResponses, PostSessionByIdPermissionsByPermissionIdData, PostSessionByIdPermissionsByPermissionIdResponses, ConfigProvidersData, ConfigProvidersResponses, FindTextData, FindTextResponses, FindFilesData, FindFilesResponses, FindSymbolsData, FindSymbolsResponses, FileReadData, FileReadResponses, FileStatusData, FileStatusResponses, AppLogData, AppLogResponses, AppModesData, AppModesResponses, TuiAppendPromptData, TuiAppendPromptResponses, TuiOpenHelpData, TuiOpenHelpResponses, TuiOpenSessionsData, TuiOpenSessionsResponses, TuiOpenThemesData, TuiOpenThemesResponses, TuiOpenModelsData, TuiOpenModelsResponses, TuiSubmitPromptData, TuiSubmitPromptResponses, TuiClearPromptData, TuiClearPromptResponses, TuiExecuteCommandData, TuiExecuteCommandResponses } from './types.gen';
import { client as _heyApiClient } from './client.gen';
import type { Options as ClientOptions, TDataShape, Client } from "./client"
import type {
EventSubscribeData,
EventSubscribeResponses,
AppGetData,
AppGetResponses,
AppInitData,
AppInitResponses,
ConfigGetData,
ConfigGetResponses,
SessionListData,
SessionListResponses,
SessionCreateData,
SessionCreateResponses,
SessionCreateErrors,
SessionDeleteData,
SessionDeleteResponses,
SessionInitData,
SessionInitResponses,
SessionAbortData,
SessionAbortResponses,
SessionUnshareData,
SessionUnshareResponses,
SessionShareData,
SessionShareResponses,
SessionSummarizeData,
SessionSummarizeResponses,
SessionMessagesData,
SessionMessagesResponses,
SessionChatData,
SessionChatResponses,
SessionMessageData,
SessionMessageResponses,
SessionRevertData,
SessionRevertResponses,
SessionUnrevertData,
SessionUnrevertResponses,
PostSessionByIdPermissionsByPermissionIdData,
PostSessionByIdPermissionsByPermissionIdResponses,
ConfigProvidersData,
ConfigProvidersResponses,
FindTextData,
FindTextResponses,
FindFilesData,
FindFilesResponses,
FindSymbolsData,
FindSymbolsResponses,
FileReadData,
FileReadResponses,
FileStatusData,
FileStatusResponses,
AppLogData,
AppLogResponses,
AppModesData,
AppModesResponses,
TuiAppendPromptData,
TuiAppendPromptResponses,
TuiOpenHelpData,
TuiOpenHelpResponses,
TuiOpenSessionsData,
TuiOpenSessionsResponses,
TuiOpenThemesData,
TuiOpenThemesResponses,
TuiOpenModelsData,
TuiOpenModelsResponses,
TuiSubmitPromptData,
TuiSubmitPromptResponses,
TuiClearPromptData,
TuiClearPromptResponses,
TuiExecuteCommandData,
TuiExecuteCommandResponses,
} from "./types.gen"
import { client as _heyApiClient } from "./client.gen"
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = ClientOptions<TData, ThrowOnError> & {
/**
* You can provide a client instance returned by `createClient()` instead of
* individual options. This might be also useful if you want to implement a
* custom client.
*/
client?: Client;
/**
* You can pass arbitrary values through the `meta` object. This can be
* used to access values that aren't defined as part of the SDK function.
*/
meta?: Record<string, unknown>;
};
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = ClientOptions<
TData,
ThrowOnError
> & {
/**
* You can provide a client instance returned by `createClient()` instead of
* individual options. This might be also useful if you want to implement a
* custom client.
*/
client?: Client
/**
* You can pass arbitrary values through the `meta` object. This can be
* used to access values that aren't defined as part of the SDK function.
*/
meta?: Record<string, unknown>
}
class _HeyApiClient {
protected _client: Client = _heyApiClient;
constructor(args?: {
client?: Client;
}) {
if (args?.client) {
this._client = args.client;
}
protected _client: Client = _heyApiClient
constructor(args?: { client?: Client }) {
if (args?.client) {
this._client = args.client
}
}
}
class Event extends _HeyApiClient {
/**
* Get events
*/
public subscribe<ThrowOnError extends boolean = false>(options?: Options<EventSubscribeData, ThrowOnError>) {
return (options?.client ?? this._client).get<EventSubscribeResponses, unknown, ThrowOnError>({
url: '/event',
...options
});
}
/**
* Get events
*/
public subscribe<ThrowOnError extends boolean = false>(options?: Options<EventSubscribeData, ThrowOnError>) {
return (options?.client ?? this._client).get<EventSubscribeResponses, unknown, ThrowOnError>({
url: "/event",
...options,
})
}
}
class App extends _HeyApiClient {
/**
* Get app info
*/
public get<ThrowOnError extends boolean = false>(options?: Options<AppGetData, ThrowOnError>) {
return (options?.client ?? this._client).get<AppGetResponses, unknown, ThrowOnError>({
url: '/app',
...options
});
}
/**
* Initialize the app
*/
public init<ThrowOnError extends boolean = false>(options?: Options<AppInitData, ThrowOnError>) {
return (options?.client ?? this._client).post<AppInitResponses, unknown, ThrowOnError>({
url: '/app/init',
...options
});
}
/**
* Write a log entry to the server logs
*/
public log<ThrowOnError extends boolean = false>(options?: Options<AppLogData, ThrowOnError>) {
return (options?.client ?? this._client).post<AppLogResponses, unknown, ThrowOnError>({
url: '/log',
...options,
headers: {
'Content-Type': 'application/json',
...options?.headers
}
});
}
/**
* List all modes
*/
public modes<ThrowOnError extends boolean = false>(options?: Options<AppModesData, ThrowOnError>) {
return (options?.client ?? this._client).get<AppModesResponses, unknown, ThrowOnError>({
url: '/mode',
...options
});
}
/**
* Get app info
*/
public get<ThrowOnError extends boolean = false>(options?: Options<AppGetData, ThrowOnError>) {
return (options?.client ?? this._client).get<AppGetResponses, unknown, ThrowOnError>({
url: "/app",
...options,
})
}
/**
* Initialize the app
*/
public init<ThrowOnError extends boolean = false>(options?: Options<AppInitData, ThrowOnError>) {
return (options?.client ?? this._client).post<AppInitResponses, unknown, ThrowOnError>({
url: "/app/init",
...options,
})
}
/**
* Write a log entry to the server logs
*/
public log<ThrowOnError extends boolean = false>(options?: Options<AppLogData, ThrowOnError>) {
return (options?.client ?? this._client).post<AppLogResponses, unknown, ThrowOnError>({
url: "/log",
...options,
headers: {
"Content-Type": "application/json",
...options?.headers,
},
})
}
/**
* List all modes
*/
public modes<ThrowOnError extends boolean = false>(options?: Options<AppModesData, ThrowOnError>) {
return (options?.client ?? this._client).get<AppModesResponses, unknown, ThrowOnError>({
url: "/mode",
...options,
})
}
}
class Config extends _HeyApiClient {
/**
* Get config info
*/
public get<ThrowOnError extends boolean = false>(options?: Options<ConfigGetData, ThrowOnError>) {
return (options?.client ?? this._client).get<ConfigGetResponses, unknown, ThrowOnError>({
url: '/config',
...options
});
}
/**
* List all providers
*/
public providers<ThrowOnError extends boolean = false>(options?: Options<ConfigProvidersData, ThrowOnError>) {
return (options?.client ?? this._client).get<ConfigProvidersResponses, unknown, ThrowOnError>({
url: '/config/providers',
...options
});
}
/**
* Get config info
*/
public get<ThrowOnError extends boolean = false>(options?: Options<ConfigGetData, ThrowOnError>) {
return (options?.client ?? this._client).get<ConfigGetResponses, unknown, ThrowOnError>({
url: "/config",
...options,
})
}
/**
* List all providers
*/
public providers<ThrowOnError extends boolean = false>(options?: Options<ConfigProvidersData, ThrowOnError>) {
return (options?.client ?? this._client).get<ConfigProvidersResponses, unknown, ThrowOnError>({
url: "/config/providers",
...options,
})
}
}
class Session extends _HeyApiClient {
/**
* List all sessions
*/
public list<ThrowOnError extends boolean = false>(options?: Options<SessionListData, ThrowOnError>) {
return (options?.client ?? this._client).get<SessionListResponses, unknown, ThrowOnError>({
url: '/session',
...options
});
}
/**
* Create a new session
*/
public create<ThrowOnError extends boolean = false>(options?: Options<SessionCreateData, ThrowOnError>) {
return (options?.client ?? this._client).post<SessionCreateResponses, SessionCreateErrors, ThrowOnError>({
url: '/session',
...options
});
}
/**
* Delete a session and all its data
*/
public delete<ThrowOnError extends boolean = false>(options: Options<SessionDeleteData, ThrowOnError>) {
return (options.client ?? this._client).delete<SessionDeleteResponses, unknown, ThrowOnError>({
url: '/session/{id}',
...options
});
}
/**
* Analyze the app and create an AGENTS.md file
*/
public init<ThrowOnError extends boolean = false>(options: Options<SessionInitData, ThrowOnError>) {
return (options.client ?? this._client).post<SessionInitResponses, unknown, ThrowOnError>({
url: '/session/{id}/init',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
}
/**
* Abort a session
*/
public abort<ThrowOnError extends boolean = false>(options: Options<SessionAbortData, ThrowOnError>) {
return (options.client ?? this._client).post<SessionAbortResponses, unknown, ThrowOnError>({
url: '/session/{id}/abort',
...options
});
}
/**
* Unshare the session
*/
public unshare<ThrowOnError extends boolean = false>(options: Options<SessionUnshareData, ThrowOnError>) {
return (options.client ?? this._client).delete<SessionUnshareResponses, unknown, ThrowOnError>({
url: '/session/{id}/share',
...options
});
}
/**
* Share a session
*/
public share<ThrowOnError extends boolean = false>(options: Options<SessionShareData, ThrowOnError>) {
return (options.client ?? this._client).post<SessionShareResponses, unknown, ThrowOnError>({
url: '/session/{id}/share',
...options
});
}
/**
* Summarize the session
*/
public summarize<ThrowOnError extends boolean = false>(options: Options<SessionSummarizeData, ThrowOnError>) {
return (options.client ?? this._client).post<SessionSummarizeResponses, unknown, ThrowOnError>({
url: '/session/{id}/summarize',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
}
/**
* List messages for a session
*/
public messages<ThrowOnError extends boolean = false>(options: Options<SessionMessagesData, ThrowOnError>) {
return (options.client ?? this._client).get<SessionMessagesResponses, unknown, ThrowOnError>({
url: '/session/{id}/message',
...options
});
}
/**
* Create and send a new message to a session
*/
public chat<ThrowOnError extends boolean = false>(options: Options<SessionChatData, ThrowOnError>) {
return (options.client ?? this._client).post<SessionChatResponses, unknown, ThrowOnError>({
url: '/session/{id}/message',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
}
/**
* Get a message from a session
*/
public message<ThrowOnError extends boolean = false>(options: Options<SessionMessageData, ThrowOnError>) {
return (options.client ?? this._client).get<SessionMessageResponses, unknown, ThrowOnError>({
url: '/session/{id}/message/{messageID}',
...options
});
}
/**
* Revert a message
*/
public revert<ThrowOnError extends boolean = false>(options: Options<SessionRevertData, ThrowOnError>) {
return (options.client ?? this._client).post<SessionRevertResponses, unknown, ThrowOnError>({
url: '/session/{id}/revert',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
}
/**
* Restore all reverted messages
*/
public unrevert<ThrowOnError extends boolean = false>(options: Options<SessionUnrevertData, ThrowOnError>) {
return (options.client ?? this._client).post<SessionUnrevertResponses, unknown, ThrowOnError>({
url: '/session/{id}/unrevert',
...options
});
}
/**
* List all sessions
*/
public list<ThrowOnError extends boolean = false>(options?: Options<SessionListData, ThrowOnError>) {
return (options?.client ?? this._client).get<SessionListResponses, unknown, ThrowOnError>({
url: "/session",
...options,
})
}
/**
* Create a new session
*/
public create<ThrowOnError extends boolean = false>(options?: Options<SessionCreateData, ThrowOnError>) {
return (options?.client ?? this._client).post<SessionCreateResponses, SessionCreateErrors, ThrowOnError>({
url: "/session",
...options,
})
}
/**
* Delete a session and all its data
*/
public delete<ThrowOnError extends boolean = false>(options: Options<SessionDeleteData, ThrowOnError>) {
return (options.client ?? this._client).delete<SessionDeleteResponses, unknown, ThrowOnError>({
url: "/session/{id}",
...options,
})
}
/**
* Analyze the app and create an AGENTS.md file
*/
public init<ThrowOnError extends boolean = false>(options: Options<SessionInitData, ThrowOnError>) {
return (options.client ?? this._client).post<SessionInitResponses, unknown, ThrowOnError>({
url: "/session/{id}/init",
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
})
}
/**
* Abort a session
*/
public abort<ThrowOnError extends boolean = false>(options: Options<SessionAbortData, ThrowOnError>) {
return (options.client ?? this._client).post<SessionAbortResponses, unknown, ThrowOnError>({
url: "/session/{id}/abort",
...options,
})
}
/**
* Unshare the session
*/
public unshare<ThrowOnError extends boolean = false>(options: Options<SessionUnshareData, ThrowOnError>) {
return (options.client ?? this._client).delete<SessionUnshareResponses, unknown, ThrowOnError>({
url: "/session/{id}/share",
...options,
})
}
/**
* Share a session
*/
public share<ThrowOnError extends boolean = false>(options: Options<SessionShareData, ThrowOnError>) {
return (options.client ?? this._client).post<SessionShareResponses, unknown, ThrowOnError>({
url: "/session/{id}/share",
...options,
})
}
/**
* Summarize the session
*/
public summarize<ThrowOnError extends boolean = false>(options: Options<SessionSummarizeData, ThrowOnError>) {
return (options.client ?? this._client).post<SessionSummarizeResponses, unknown, ThrowOnError>({
url: "/session/{id}/summarize",
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
})
}
/**
* List messages for a session
*/
public messages<ThrowOnError extends boolean = false>(options: Options<SessionMessagesData, ThrowOnError>) {
return (options.client ?? this._client).get<SessionMessagesResponses, unknown, ThrowOnError>({
url: "/session/{id}/message",
...options,
})
}
/**
* Create and send a new message to a session
*/
public chat<ThrowOnError extends boolean = false>(options: Options<SessionChatData, ThrowOnError>) {
return (options.client ?? this._client).post<SessionChatResponses, unknown, ThrowOnError>({
url: "/session/{id}/message",
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
})
}
/**
* Get a message from a session
*/
public message<ThrowOnError extends boolean = false>(options: Options<SessionMessageData, ThrowOnError>) {
return (options.client ?? this._client).get<SessionMessageResponses, unknown, ThrowOnError>({
url: "/session/{id}/message/{messageID}",
...options,
})
}
/**
* Revert a message
*/
public revert<ThrowOnError extends boolean = false>(options: Options<SessionRevertData, ThrowOnError>) {
return (options.client ?? this._client).post<SessionRevertResponses, unknown, ThrowOnError>({
url: "/session/{id}/revert",
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
})
}
/**
* Restore all reverted messages
*/
public unrevert<ThrowOnError extends boolean = false>(options: Options<SessionUnrevertData, ThrowOnError>) {
return (options.client ?? this._client).post<SessionUnrevertResponses, unknown, ThrowOnError>({
url: "/session/{id}/unrevert",
...options,
})
}
}
class Find extends _HeyApiClient {
/**
* Find text in files
*/
public text<ThrowOnError extends boolean = false>(options: Options<FindTextData, ThrowOnError>) {
return (options.client ?? this._client).get<FindTextResponses, unknown, ThrowOnError>({
url: '/find',
...options
});
}
/**
* Find files
*/
public files<ThrowOnError extends boolean = false>(options: Options<FindFilesData, ThrowOnError>) {
return (options.client ?? this._client).get<FindFilesResponses, unknown, ThrowOnError>({
url: '/find/file',
...options
});
}
/**
* Find workspace symbols
*/
public symbols<ThrowOnError extends boolean = false>(options: Options<FindSymbolsData, ThrowOnError>) {
return (options.client ?? this._client).get<FindSymbolsResponses, unknown, ThrowOnError>({
url: '/find/symbol',
...options
});
}
/**
* Find text in files
*/
public text<ThrowOnError extends boolean = false>(options: Options<FindTextData, ThrowOnError>) {
return (options.client ?? this._client).get<FindTextResponses, unknown, ThrowOnError>({
url: "/find",
...options,
})
}
/**
* Find files
*/
public files<ThrowOnError extends boolean = false>(options: Options<FindFilesData, ThrowOnError>) {
return (options.client ?? this._client).get<FindFilesResponses, unknown, ThrowOnError>({
url: "/find/file",
...options,
})
}
/**
* Find workspace symbols
*/
public symbols<ThrowOnError extends boolean = false>(options: Options<FindSymbolsData, ThrowOnError>) {
return (options.client ?? this._client).get<FindSymbolsResponses, unknown, ThrowOnError>({
url: "/find/symbol",
...options,
})
}
}
class File extends _HeyApiClient {
/**
* Read a file
*/
public read<ThrowOnError extends boolean = false>(options: Options<FileReadData, ThrowOnError>) {
return (options.client ?? this._client).get<FileReadResponses, unknown, ThrowOnError>({
url: '/file',
...options
});
}
/**
* Get file status
*/
public status<ThrowOnError extends boolean = false>(options?: Options<FileStatusData, ThrowOnError>) {
return (options?.client ?? this._client).get<FileStatusResponses, unknown, ThrowOnError>({
url: '/file/status',
...options
});
}
/**
* Read a file
*/
public read<ThrowOnError extends boolean = false>(options: Options<FileReadData, ThrowOnError>) {
return (options.client ?? this._client).get<FileReadResponses, unknown, ThrowOnError>({
url: "/file",
...options,
})
}
/**
* Get file status
*/
public status<ThrowOnError extends boolean = false>(options?: Options<FileStatusData, ThrowOnError>) {
return (options?.client ?? this._client).get<FileStatusResponses, unknown, ThrowOnError>({
url: "/file/status",
...options,
})
}
}
class Tui extends _HeyApiClient {
/**
* Append prompt to the TUI
*/
public appendPrompt<ThrowOnError extends boolean = false>(options?: Options<TuiAppendPromptData, ThrowOnError>) {
return (options?.client ?? this._client).post<TuiAppendPromptResponses, unknown, ThrowOnError>({
url: '/tui/append-prompt',
...options,
headers: {
'Content-Type': 'application/json',
...options?.headers
}
});
}
/**
* Open the help dialog
*/
public openHelp<ThrowOnError extends boolean = false>(options?: Options<TuiOpenHelpData, ThrowOnError>) {
return (options?.client ?? this._client).post<TuiOpenHelpResponses, unknown, ThrowOnError>({
url: '/tui/open-help',
...options
});
}
/**
* Open the session dialog
*/
public openSessions<ThrowOnError extends boolean = false>(options?: Options<TuiOpenSessionsData, ThrowOnError>) {
return (options?.client ?? this._client).post<TuiOpenSessionsResponses, unknown, ThrowOnError>({
url: '/tui/open-sessions',
...options
});
}
/**
* Open the theme dialog
*/
public openThemes<ThrowOnError extends boolean = false>(options?: Options<TuiOpenThemesData, ThrowOnError>) {
return (options?.client ?? this._client).post<TuiOpenThemesResponses, unknown, ThrowOnError>({
url: '/tui/open-themes',
...options
});
}
/**
* Open the model dialog
*/
public openModels<ThrowOnError extends boolean = false>(options?: Options<TuiOpenModelsData, ThrowOnError>) {
return (options?.client ?? this._client).post<TuiOpenModelsResponses, unknown, ThrowOnError>({
url: '/tui/open-models',
...options
});
}
/**
* Submit the prompt
*/
public submitPrompt<ThrowOnError extends boolean = false>(options?: Options<TuiSubmitPromptData, ThrowOnError>) {
return (options?.client ?? this._client).post<TuiSubmitPromptResponses, unknown, ThrowOnError>({
url: '/tui/submit-prompt',
...options
});
}
/**
* Clear the prompt
*/
public clearPrompt<ThrowOnError extends boolean = false>(options?: Options<TuiClearPromptData, ThrowOnError>) {
return (options?.client ?? this._client).post<TuiClearPromptResponses, unknown, ThrowOnError>({
url: '/tui/clear-prompt',
...options
});
}
/**
* Execute a TUI command (e.g. switch_mode)
*/
public executeCommand<ThrowOnError extends boolean = false>(options?: Options<TuiExecuteCommandData, ThrowOnError>) {
return (options?.client ?? this._client).post<TuiExecuteCommandResponses, unknown, ThrowOnError>({
url: '/tui/execute-command',
...options,
headers: {
'Content-Type': 'application/json',
...options?.headers
}
});
}
/**
* Append prompt to the TUI
*/
public appendPrompt<ThrowOnError extends boolean = false>(options?: Options<TuiAppendPromptData, ThrowOnError>) {
return (options?.client ?? this._client).post<TuiAppendPromptResponses, unknown, ThrowOnError>({
url: "/tui/append-prompt",
...options,
headers: {
"Content-Type": "application/json",
...options?.headers,
},
})
}
/**
* Open the help dialog
*/
public openHelp<ThrowOnError extends boolean = false>(options?: Options<TuiOpenHelpData, ThrowOnError>) {
return (options?.client ?? this._client).post<TuiOpenHelpResponses, unknown, ThrowOnError>({
url: "/tui/open-help",
...options,
})
}
/**
* Open the session dialog
*/
public openSessions<ThrowOnError extends boolean = false>(options?: Options<TuiOpenSessionsData, ThrowOnError>) {
return (options?.client ?? this._client).post<TuiOpenSessionsResponses, unknown, ThrowOnError>({
url: "/tui/open-sessions",
...options,
})
}
/**
* Open the theme dialog
*/
public openThemes<ThrowOnError extends boolean = false>(options?: Options<TuiOpenThemesData, ThrowOnError>) {
return (options?.client ?? this._client).post<TuiOpenThemesResponses, unknown, ThrowOnError>({
url: "/tui/open-themes",
...options,
})
}
/**
* Open the model dialog
*/
public openModels<ThrowOnError extends boolean = false>(options?: Options<TuiOpenModelsData, ThrowOnError>) {
return (options?.client ?? this._client).post<TuiOpenModelsResponses, unknown, ThrowOnError>({
url: "/tui/open-models",
...options,
})
}
/**
* Submit the prompt
*/
public submitPrompt<ThrowOnError extends boolean = false>(options?: Options<TuiSubmitPromptData, ThrowOnError>) {
return (options?.client ?? this._client).post<TuiSubmitPromptResponses, unknown, ThrowOnError>({
url: "/tui/submit-prompt",
...options,
})
}
/**
* Clear the prompt
*/
public clearPrompt<ThrowOnError extends boolean = false>(options?: Options<TuiClearPromptData, ThrowOnError>) {
return (options?.client ?? this._client).post<TuiClearPromptResponses, unknown, ThrowOnError>({
url: "/tui/clear-prompt",
...options,
})
}
/**
* Execute a TUI command (e.g. switch_mode)
*/
public executeCommand<ThrowOnError extends boolean = false>(options?: Options<TuiExecuteCommandData, ThrowOnError>) {
return (options?.client ?? this._client).post<TuiExecuteCommandResponses, unknown, ThrowOnError>({
url: "/tui/execute-command",
...options,
headers: {
"Content-Type": "application/json",
...options?.headers,
},
})
}
}
export class OpencodeClient extends _HeyApiClient {
/**
* Respond to a permission request
*/
public postSessionByIdPermissionsByPermissionId<ThrowOnError extends boolean = false>(options: Options<PostSessionByIdPermissionsByPermissionIdData, ThrowOnError>) {
return (options.client ?? this._client).post<PostSessionByIdPermissionsByPermissionIdResponses, unknown, ThrowOnError>({
url: '/session/{id}/permissions/{permissionID}',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
}
event = new Event({ client: this._client });
app = new App({ client: this._client });
config = new Config({ client: this._client });
session = new Session({ client: this._client });
find = new Find({ client: this._client });
file = new File({ client: this._client });
tui = new Tui({ client: this._client });
}
/**
* Respond to a permission request
*/
public postSessionByIdPermissionsByPermissionId<ThrowOnError extends boolean = false>(
options: Options<PostSessionByIdPermissionsByPermissionIdData, ThrowOnError>,
) {
return (options.client ?? this._client).post<
PostSessionByIdPermissionsByPermissionIdResponses,
unknown,
ThrowOnError
>({
url: "/session/{id}/permissions/{permissionID}",
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
})
}
event = new Event({ client: this._client })
app = new App({ client: this._client })
config = new Config({ client: this._client })
session = new Session({ client: this._client })
find = new Find({ client: this._client })
file = new File({ client: this._client })
tui = new Tui({ client: this._client })
}
File diff suppressed because it is too large Load Diff
+1
View File
@@ -1,6 +1,7 @@
import { createClient } from "./gen/client/client"
import { type Config } from "./gen/client/types"
import { OpencodeClient } from "./gen/sdk.gen"
export * from "./gen/types.gen"
export function createOpencodeClient(config?: Config) {
const client = createClient(config)
+9
View File
@@ -0,0 +1,9 @@
/* This file is auto-generated by SST. Do not edit. */
/* tslint:disable */
/* eslint-disable */
/* deno-fmt-ignore-file */
/// <reference path="../../../sst-env.d.ts" />
import "sst"
export {}
+9 -1
View File
@@ -5,7 +5,15 @@
"outDir": "dist",
"module": "preserve",
"declaration": true,
"moduleResolution": "bundler"
"moduleResolution": "bundler",
"lib": [
"es2022",
"dom",
"dom.iterable"
],
"customConditions": [
"development"
]
},
"include": [
"src"
@@ -1,44 +0,0 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
export {
Permissions,
type Permission,
type PermissionRespondResponse,
type PermissionRespondParams,
} from './permissions';
export {
SessionResource,
type AssistantMessage,
type FilePart,
type FilePartInput,
type FilePartSource,
type FilePartSourceText,
type FileSource,
type Message,
type Part,
type Session,
type SnapshotPart,
type StepFinishPart,
type StepStartPart,
type SymbolSource,
type TextPart,
type TextPartInput,
type ToolPart,
type ToolStateCompleted,
type ToolStateError,
type ToolStatePending,
type ToolStateRunning,
type UserMessage,
type SessionListResponse,
type SessionDeleteResponse,
type SessionAbortResponse,
type SessionInitResponse,
type SessionMessageResponse,
type SessionMessagesResponse,
type SessionSummarizeResponse,
type SessionChatParams,
type SessionInitParams,
type SessionMessageParams,
type SessionRevertParams,
type SessionSummarizeParams,
} from './session';
@@ -1,64 +0,0 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { APIResource } from '../../core/resource';
import { APIPromise } from '../../core/api-promise';
import { RequestOptions } from '../../internal/request-options';
import { path } from '../../internal/utils/path';
export class Permissions extends APIResource {
/**
* Respond to a permission request
*/
respond(
permissionID: string,
params: PermissionRespondParams,
options?: RequestOptions,
): APIPromise<PermissionRespondResponse> {
const { id, ...body } = params;
return this._client.post(path`/session/${id}/permissions/${permissionID}`, { body, ...options });
}
}
export interface Permission {
id: string;
messageID: string;
metadata: { [key: string]: unknown };
sessionID: string;
time: Permission.Time;
title: string;
toolCallID?: string;
}
export namespace Permission {
export interface Time {
created: number;
}
}
export type PermissionRespondResponse = boolean;
export interface PermissionRespondParams {
/**
* Path param:
*/
id: string;
/**
* Body param:
*/
response: 'once' | 'always' | 'reject';
}
export declare namespace Permissions {
export {
type Permission as Permission,
type PermissionRespondResponse as PermissionRespondResponse,
type PermissionRespondParams as PermissionRespondParams,
};
}
@@ -1,645 +0,0 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { APIResource } from '../../core/resource';
import * as SessionAPI from './session';
import * as Shared from '../shared';
import * as PermissionsAPI from './permissions';
import { Permission, PermissionRespondParams, PermissionRespondResponse, Permissions } from './permissions';
import { APIPromise } from '../../core/api-promise';
import { RequestOptions } from '../../internal/request-options';
import { path } from '../../internal/utils/path';
export class SessionResource extends APIResource {
permissions: PermissionsAPI.Permissions = new PermissionsAPI.Permissions(this._client);
/**
* Create a new session
*/
create(options?: RequestOptions): APIPromise<Session> {
return this._client.post('/session', options);
}
/**
* List all sessions
*/
list(options?: RequestOptions): APIPromise<SessionListResponse> {
return this._client.get('/session', options);
}
/**
* Delete a session and all its data
*/
delete(id: string, options?: RequestOptions): APIPromise<SessionDeleteResponse> {
return this._client.delete(path`/session/${id}`, options);
}
/**
* Abort a session
*/
abort(id: string, options?: RequestOptions): APIPromise<SessionAbortResponse> {
return this._client.post(path`/session/${id}/abort`, options);
}
/**
* Create and send a new message to a session
*/
chat(id: string, body: SessionChatParams, options?: RequestOptions): APIPromise<AssistantMessage> {
return this._client.post(path`/session/${id}/message`, { body, ...options });
}
/**
* Analyze the app and create an AGENTS.md file
*/
init(id: string, body: SessionInitParams, options?: RequestOptions): APIPromise<SessionInitResponse> {
return this._client.post(path`/session/${id}/init`, { body, ...options });
}
/**
* Get a message from a session
*/
message(
messageID: string,
params: SessionMessageParams,
options?: RequestOptions,
): APIPromise<SessionMessageResponse> {
const { id } = params;
return this._client.get(path`/session/${id}/message/${messageID}`, options);
}
/**
* List messages for a session
*/
messages(id: string, options?: RequestOptions): APIPromise<SessionMessagesResponse> {
return this._client.get(path`/session/${id}/message`, options);
}
/**
* Revert a message
*/
revert(id: string, body: SessionRevertParams, options?: RequestOptions): APIPromise<Session> {
return this._client.post(path`/session/${id}/revert`, { body, ...options });
}
/**
* Share a session
*/
share(id: string, options?: RequestOptions): APIPromise<Session> {
return this._client.post(path`/session/${id}/share`, options);
}
/**
* Summarize the session
*/
summarize(
id: string,
body: SessionSummarizeParams,
options?: RequestOptions,
): APIPromise<SessionSummarizeResponse> {
return this._client.post(path`/session/${id}/summarize`, { body, ...options });
}
/**
* Restore all reverted messages
*/
unrevert(id: string, options?: RequestOptions): APIPromise<Session> {
return this._client.post(path`/session/${id}/unrevert`, options);
}
/**
* Unshare the session
*/
unshare(id: string, options?: RequestOptions): APIPromise<Session> {
return this._client.delete(path`/session/${id}/share`, options);
}
}
export interface AssistantMessage {
id: string;
cost: number;
mode: string;
modelID: string;
path: AssistantMessage.Path;
providerID: string;
role: 'assistant';
sessionID: string;
system: Array<string>;
time: AssistantMessage.Time;
tokens: AssistantMessage.Tokens;
error?:
| Shared.ProviderAuthError
| Shared.UnknownError
| AssistantMessage.MessageOutputLengthError
| Shared.MessageAbortedError;
summary?: boolean;
}
export namespace AssistantMessage {
export interface Path {
cwd: string;
root: string;
}
export interface Time {
created: number;
completed?: number;
}
export interface Tokens {
cache: Tokens.Cache;
input: number;
output: number;
reasoning: number;
}
export namespace Tokens {
export interface Cache {
read: number;
write: number;
}
}
export interface MessageOutputLengthError {
data: unknown;
name: 'MessageOutputLengthError';
}
}
export interface FilePart {
id: string;
messageID: string;
mime: string;
sessionID: string;
type: 'file';
url: string;
filename?: string;
source?: FilePartSource;
}
export interface FilePartInput {
mime: string;
type: 'file';
url: string;
id?: string;
filename?: string;
source?: FilePartSource;
}
export type FilePartSource = FileSource | SymbolSource;
export interface FilePartSourceText {
end: number;
start: number;
value: string;
}
export interface FileSource {
path: string;
text: FilePartSourceText;
type: 'file';
}
export type Message = UserMessage | AssistantMessage;
export type Part =
| TextPart
| FilePart
| ToolPart
| StepStartPart
| StepFinishPart
| SnapshotPart
| Part.PatchPart;
export namespace Part {
export interface PatchPart {
id: string;
files: Array<string>;
hash: string;
messageID: string;
sessionID: string;
type: 'patch';
}
}
export interface Session {
id: string;
time: Session.Time;
title: string;
version: string;
parentID?: string;
revert?: Session.Revert;
share?: Session.Share;
}
export namespace Session {
export interface Time {
created: number;
updated: number;
}
export interface Revert {
messageID: string;
diff?: string;
partID?: string;
snapshot?: string;
}
export interface Share {
url: string;
}
}
export interface SnapshotPart {
id: string;
messageID: string;
sessionID: string;
snapshot: string;
type: 'snapshot';
}
export interface StepFinishPart {
id: string;
cost: number;
messageID: string;
sessionID: string;
tokens: StepFinishPart.Tokens;
type: 'step-finish';
}
export namespace StepFinishPart {
export interface Tokens {
cache: Tokens.Cache;
input: number;
output: number;
reasoning: number;
}
export namespace Tokens {
export interface Cache {
read: number;
write: number;
}
}
}
export interface StepStartPart {
id: string;
messageID: string;
sessionID: string;
type: 'step-start';
}
export interface SymbolSource {
kind: number;
name: string;
path: string;
range: SymbolSource.Range;
text: FilePartSourceText;
type: 'symbol';
}
export namespace SymbolSource {
export interface Range {
end: Range.End;
start: Range.Start;
}
export namespace Range {
export interface End {
character: number;
line: number;
}
export interface Start {
character: number;
line: number;
}
}
}
export interface TextPart {
id: string;
messageID: string;
sessionID: string;
text: string;
type: 'text';
synthetic?: boolean;
time?: TextPart.Time;
}
export namespace TextPart {
export interface Time {
start: number;
end?: number;
}
}
export interface TextPartInput {
text: string;
type: 'text';
id?: string;
synthetic?: boolean;
time?: TextPartInput.Time;
}
export namespace TextPartInput {
export interface Time {
start: number;
end?: number;
}
}
export interface ToolPart {
id: string;
callID: string;
messageID: string;
sessionID: string;
state: ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError;
tool: string;
type: 'tool';
}
export interface ToolStateCompleted {
input: { [key: string]: unknown };
metadata: { [key: string]: unknown };
output: string;
status: 'completed';
time: ToolStateCompleted.Time;
title: string;
}
export namespace ToolStateCompleted {
export interface Time {
end: number;
start: number;
}
}
export interface ToolStateError {
error: string;
input: { [key: string]: unknown };
status: 'error';
time: ToolStateError.Time;
}
export namespace ToolStateError {
export interface Time {
end: number;
start: number;
}
}
export interface ToolStatePending {
status: 'pending';
}
export interface ToolStateRunning {
status: 'running';
time: ToolStateRunning.Time;
input?: unknown;
metadata?: { [key: string]: unknown };
title?: string;
}
export namespace ToolStateRunning {
export interface Time {
start: number;
}
}
export interface UserMessage {
id: string;
role: 'user';
sessionID: string;
time: UserMessage.Time;
}
export namespace UserMessage {
export interface Time {
created: number;
}
}
export type SessionListResponse = Array<Session>;
export type SessionDeleteResponse = boolean;
export type SessionAbortResponse = boolean;
export type SessionInitResponse = boolean;
export interface SessionMessageResponse {
info: Message;
parts: Array<Part>;
}
export type SessionMessagesResponse = Array<SessionMessagesResponse.SessionMessagesResponseItem>;
export namespace SessionMessagesResponse {
export interface SessionMessagesResponseItem {
info: SessionAPI.Message;
parts: Array<SessionAPI.Part>;
}
}
export type SessionSummarizeResponse = boolean;
export interface SessionChatParams {
modelID: string;
parts: Array<TextPartInput | FilePartInput>;
providerID: string;
messageID?: string;
mode?: string;
system?: string;
tools?: { [key: string]: boolean };
}
export interface SessionInitParams {
messageID: string;
modelID: string;
providerID: string;
}
export interface SessionMessageParams {
/**
* Session ID
*/
id: string;
}
export interface SessionRevertParams {
messageID: string;
partID?: string;
}
export interface SessionSummarizeParams {
modelID: string;
providerID: string;
}
SessionResource.Permissions = Permissions;
export declare namespace SessionResource {
export {
type AssistantMessage as AssistantMessage,
type FilePart as FilePart,
type FilePartInput as FilePartInput,
type FilePartSource as FilePartSource,
type FilePartSourceText as FilePartSourceText,
type FileSource as FileSource,
type Message as Message,
type Part as Part,
type Session as Session,
type SnapshotPart as SnapshotPart,
type StepFinishPart as StepFinishPart,
type StepStartPart as StepStartPart,
type SymbolSource as SymbolSource,
type TextPart as TextPart,
type TextPartInput as TextPartInput,
type ToolPart as ToolPart,
type ToolStateCompleted as ToolStateCompleted,
type ToolStateError as ToolStateError,
type ToolStatePending as ToolStatePending,
type ToolStateRunning as ToolStateRunning,
type UserMessage as UserMessage,
type SessionListResponse as SessionListResponse,
type SessionDeleteResponse as SessionDeleteResponse,
type SessionAbortResponse as SessionAbortResponse,
type SessionInitResponse as SessionInitResponse,
type SessionMessageResponse as SessionMessageResponse,
type SessionMessagesResponse as SessionMessagesResponse,
type SessionSummarizeResponse as SessionSummarizeResponse,
type SessionChatParams as SessionChatParams,
type SessionInitParams as SessionInitParams,
type SessionMessageParams as SessionMessageParams,
type SessionRevertParams as SessionRevertParams,
type SessionSummarizeParams as SessionSummarizeParams,
};
export {
Permissions as Permissions,
type Permission as Permission,
type PermissionRespondResponse as PermissionRespondResponse,
type PermissionRespondParams as PermissionRespondParams,
};
}
@@ -1,27 +0,0 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import Opencode from '@opencode-ai/sdk';
const client = new Opencode({ baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010' });
describe('resource permissions', () => {
// skipped: tests are disabled for the time being
test.skip('respond: only required params', async () => {
const responsePromise = client.session.permissions.respond('permissionID', {
id: 'id',
response: 'once',
});
const rawResponse = await responsePromise.asResponse();
expect(rawResponse).toBeInstanceOf(Response);
const response = await responsePromise;
expect(response).not.toBeInstanceOf(Response);
const dataAndResponse = await responsePromise.withResponse();
expect(dataAndResponse.data).toBe(response);
expect(dataAndResponse.response).toBe(rawResponse);
});
// skipped: tests are disabled for the time being
test.skip('respond: required and optional params', async () => {
const response = await client.session.permissions.respond('permissionID', { id: 'id', response: 'once' });
});
});
@@ -555,6 +555,8 @@ func renderToolName(name string) string {
switch name {
case "webfetch":
return "Fetch"
case "invalid":
return "Invalid"
default:
normalizedName := name
if after, ok := strings.CutPrefix(name, "opencode_"); ok {
@@ -657,6 +659,10 @@ func renderToolTitle(
title = getTodoTitle(toolCall)
case "todoread":
return "Plan"
case "invalid":
if actualTool, ok := toolArgsMap["tool"].(string); ok {
title = renderToolName(actualTool)
}
default:
toolName := renderToolName(toolCall.Tool)
title = fmt.Sprintf("%s %s", toolName, toolArgs)
@@ -0,0 +1,219 @@
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"vesperBg": "#101010",
"vesperFg": "#FFF",
"vesperComment": "#8b8b8b94",
"vesperKeyword": "#A0A0A0",
"vesperFunction": "#FFC799",
"vesperString": "#99FFE4",
"vesperNumber": "#FFC799",
"vesperError": "#FF8080",
"vesperWarning": "#FFC799",
"vesperSuccess": "#99FFE4",
"vesperMuted": "#A0A0A0"
},
"theme": {
"primary": {
"dark": "#FFC799",
"light": "#FFC799"
},
"secondary": {
"dark": "#99FFE4",
"light": "#99FFE4"
},
"accent": {
"dark": "#FFC799",
"light": "#FFC799"
},
"error": {
"dark": "vesperError",
"light": "vesperError"
},
"warning": {
"dark": "vesperWarning",
"light": "vesperWarning"
},
"success": {
"dark": "vesperSuccess",
"light": "vesperSuccess"
},
"info": {
"dark": "#FFC799",
"light": "#FFC799"
},
"text": {
"dark": "vesperFg",
"light": "vesperBg"
},
"textMuted": {
"dark": "vesperMuted",
"light": "vesperMuted"
},
"background": {
"dark": "vesperBg",
"light": "#FFF"
},
"backgroundPanel": {
"dark": "vesperBg",
"light": "#F0F0F0"
},
"backgroundElement": {
"dark": "vesperBg",
"light": "#E0E0E0"
},
"border": {
"dark": "#282828",
"light": "#D0D0D0"
},
"borderActive": {
"dark": "#FFC799",
"light": "#FFC799"
},
"borderSubtle": {
"dark": "#1C1C1C",
"light": "#E8E8E8"
},
"diffAdded": {
"dark": "vesperSuccess",
"light": "vesperSuccess"
},
"diffRemoved": {
"dark": "vesperError",
"light": "vesperError"
},
"diffContext": {
"dark": "vesperMuted",
"light": "vesperMuted"
},
"diffHunkHeader": {
"dark": "vesperMuted",
"light": "vesperMuted"
},
"diffHighlightAdded": {
"dark": "vesperSuccess",
"light": "vesperSuccess"
},
"diffHighlightRemoved": {
"dark": "vesperError",
"light": "vesperError"
},
"diffAddedBg": {
"dark": "#0d2818",
"light": "#e8f5e8"
},
"diffRemovedBg": {
"dark": "#281a1a",
"light": "#f5e8e8"
},
"diffContextBg": {
"dark": "vesperBg",
"light": "#F8F8F8"
},
"diffLineNumber": {
"dark": "#505050",
"light": "#808080"
},
"diffAddedLineNumberBg": {
"dark": "#0d2818",
"light": "#e8f5e8"
},
"diffRemovedLineNumberBg": {
"dark": "#281a1a",
"light": "#f5e8e8"
},
"markdownText": {
"dark": "vesperFg",
"light": "vesperBg"
},
"markdownHeading": {
"dark": "#FFC799",
"light": "#FFC799"
},
"markdownLink": {
"dark": "#FFC799",
"light": "#FFC799"
},
"markdownLinkText": {
"dark": "vesperMuted",
"light": "vesperMuted"
},
"markdownCode": {
"dark": "vesperMuted",
"light": "vesperMuted"
},
"markdownBlockQuote": {
"dark": "vesperFg",
"light": "vesperBg"
},
"markdownEmph": {
"dark": "vesperFg",
"light": "vesperBg"
},
"markdownStrong": {
"dark": "vesperFg",
"light": "vesperBg"
},
"markdownHorizontalRule": {
"dark": "#65737E",
"light": "#65737E"
},
"markdownListItem": {
"dark": "vesperFg",
"light": "vesperBg"
},
"markdownListEnumeration": {
"dark": "vesperFg",
"light": "vesperBg"
},
"markdownImage": {
"dark": "#FFC799",
"light": "#FFC799"
},
"markdownImageText": {
"dark": "vesperMuted",
"light": "vesperMuted"
},
"markdownCodeBlock": {
"dark": "vesperFg",
"light": "vesperBg"
},
"syntaxComment": {
"dark": "vesperComment",
"light": "vesperComment"
},
"syntaxKeyword": {
"dark": "vesperKeyword",
"light": "vesperKeyword"
},
"syntaxFunction": {
"dark": "vesperFunction",
"light": "vesperFunction"
},
"syntaxVariable": {
"dark": "vesperFg",
"light": "vesperBg"
},
"syntaxString": {
"dark": "vesperString",
"light": "vesperString"
},
"syntaxNumber": {
"dark": "vesperNumber",
"light": "vesperNumber"
},
"syntaxType": {
"dark": "vesperFunction",
"light": "vesperFunction"
},
"syntaxOperator": {
"dark": "vesperKeyword",
"light": "vesperKeyword"
},
"syntaxPunctuation": {
"dark": "vesperFg",
"light": "vesperBg"
}
}
}
+3 -2
View File
@@ -80,11 +80,12 @@ export default defineConfig({
"docs/agents",
"docs/models",
"docs/themes",
"docs/plugins",
"docs/keybinds",
"docs/permissions",
"docs/mcp-servers",
"docs/formatters",
"docs/permissions",
"docs/lsp",
"docs/mcp-servers",
],
},
],
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode/web",
"type": "module",
"version": "0.0.1",
"version": "0.3.129",
"scripts": {
"dev": "astro dev",
"dev:remote": "sst shell --stage=dev --target=Web astro dev",
+5
View File
@@ -0,0 +1,5 @@
User-agent: *
Allow: /
# Disallow shared content pages
Disallow: /s/
+26 -7
View File
@@ -230,6 +230,20 @@ You can also define agents using markdown files in `~/.config/opencode/agent/` o
You can disable providers that are loaded automatically through the `disabled_providers` option. This is useful when you want to prevent certain providers from being loaded even if their credentials are available.
The `disabled_providers` option accepts an array of provider IDs. When a provider is disabled:
- It won't be loaded even if environment variables are set
- It won't be loaded even if API keys are configured through `opencode auth login`
- The provider's models won't appear in the model selection list
```json title="opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"disabled_providers": ["openai", "gemini"]
}
```
---
### Formatters
@@ -239,16 +253,21 @@ You can configure code formatters through the `formatter` option. See [Formatter
```json title="opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"disabled_providers": ["openai", "gemini"]
"formatter": {
"prettier": {
"disabled": true
},
"custom-prettier": {
"command": ["npx", "prettier", "--write", "$FILE"],
"environment": {
"NODE_ENV": "development"
},
"extensions": [".js", ".ts", ".jsx", ".tsx"]
}
}
}
```
The `disabled_providers` option accepts an array of provider IDs. When a provider is disabled:
- It won't be loaded even if environment variables are set
- It won't be loaded even if API keys are configured through `opencode auth login`
- The provider's models won't appear in the model selection list
---
### Permissions
@@ -1,19 +1,21 @@
---
title: Formatters
description: Code formatting with opencode
description: opencode uses language specific formatters.
---
opencode automatically formats code files after they are written or edited using language-specific formatters. This ensures consistent code style across your project without manual intervention.
opencode automatically formats files after they are written or edited using language-specific formatters. This ensures that the code that is generated follows the code styles of your project.
## Built-in Formatters
---
opencode comes with several built-in formatters for popular languages and frameworks:
## Built-in
| Formatter | Languages/Extensions | Requirements |
opencode comes with several built-in formatters for popular languages and frameworks. Below is a list of the formatters, supported file extensions, and commands or config options it needs.
| Formatter | Extensions | Requirements |
| -------------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------- |
| gofmt | .go | `gofmt` command available |
| mix | .ex, .exs, .eex, .heex, .leex, .neex, .sface | `mix` command available |
| prettier | .js, .jsx, .ts, .tsx, .html, .css, .md, .json, .yaml, and [more](https://prettier.io/docs/en/index.html) | `prettier` dependency in package.json |
| prettier | .js, .jsx, .ts, .tsx, .html, .css, .md, .json, .yaml, and [more](https://prettier.io/docs/en/index.html) | `prettier` dependency in `package.json` |
| biome | .js, .jsx, .ts, .tsx, .html, .css, .md, .json, .yaml, and [more](https://biomejs.dev/) | `biome.json` config file |
| zig | .zig, .zon | `zig` command available |
| clang-format | .c, .cpp, .h, .hpp, .ino, and [more](https://clang.llvm.org/docs/ClangFormat.html) | `.clang-format` config file |
@@ -23,17 +25,51 @@ opencode comes with several built-in formatters for popular languages and framew
| standardrb | .rb, .rake, .gemspec, .ru | `standardrb` command available |
| htmlbeautifier | .erb, .html.erb | `htmlbeautifier` command available |
Formatters are automatically enabled when their requirements are met in your project environment.
So if your project has `prettier` in your `package.json`, opencode will automatically use it.
## Configuration
---
You can customize formatters through the `formatter` section in your `opencode.json` configuration file.
## How it works
### Disabling Formatters
When opencode writes or edits a file, it:
To disable a specific formatter, set its `disabled` property to `true`:
1. Checks the file extension against all enabled formatters.
2. Runs the appropriate formatter command on the file.
3. Applies the formatting changes automatically.
This process happens in the background, ensuring your code styles are maintained without any manual steps.
---
## Configure
You can customize formatters through the `formatter` section in your opencode config.
```json title="opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"formatter": { }
}
```
Each formatter configuration supports the following:
| Property | Type | Description |
| ------------- | -------- | ------------------------------------------------------- |
| `disabled` | boolean | Set this to `true` to disable the formatter |
| `command` | string[] | The command to run for formatting |
| `environment` | object | Environment variables to set when running the formatter |
| `extensions` | string[] | File extensions this formatter should handle |
Let's look at some examples.
---
### Disabling formatters
To disable a specific formatter, set `disabled` to `true`:
```json title="opencode.json" {5}
{
"$schema": "https://opencode.ai/config.json",
"formatter": {
@@ -44,11 +80,13 @@ To disable a specific formatter, set its `disabled` property to `true`:
}
```
### Custom Formatters
---
You can override the default formatters or add new ones by specifying the command, environment variables, and file extensions:
### Custom formatters
```json title="opencode.json"
You can override the built-in formatters or add new ones by specifying the command, environment variables, and file extensions:
```json title="opencode.json" {4-10}
{
"$schema": "https://opencode.ai/config.json",
"formatter": {
@@ -63,25 +101,4 @@ You can override the default formatters or add new ones by specifying the comman
}
```
The `$FILE` placeholder in the command will be replaced with the path to the file being formatted.
### Configuration Options
Each formatter configuration supports these properties:
| Property | Type | Description |
| ------------- | -------- | ------------------------------------------------------- |
| `disabled` | boolean | Set to `true` to disable the formatter |
| `command` | string[] | The command to run for formatting |
| `environment` | object | Environment variables to set when running the formatter |
| `extensions` | string[] | File extensions this formatter should handle |
## How It Works
When opencode writes or edits a file, it:
1. Checks the file extension against all enabled formatters
2. Runs the appropriate formatter command on the file
3. Applies the formatting changes automatically
This process happens seamlessly in the background, ensuring your code maintains consistent formatting without requiring manual steps.
The **`$FILE` placeholder** in the command will be replaced with the path to the file being formatted.
@@ -106,7 +106,7 @@ Here are some examples of how you can use opencode in GitHub.
And opencode will create a new branch, implement the changes, and open a PR with the changes.
- Review PRs and make changes
- **Review PRs and make changes**
Leave the following comment on a GitHub PR.
+47 -33
View File
@@ -1,15 +1,17 @@
---
title: LSP Servers
description: Language Server Protocol integration with opencode
description: opencode integrates with your LSP servers.
---
opencode integrates with Language Server Protocol (LSP) to enhance how the LLM interacts with your codebase. LSP servers provide intelligent code analysis and editing capabilities for different programming languages.
opencode integrates with your Language Server Protocol (LSP) to help the LLM interacts with your codebase. It uses diagnostics to provide feebdack to the LLM. And _go-to-definition_ and _find-references_ to help navigate your codebase.
## Built-in LSP Servers
---
## Built-in
opencode comes with several built-in LSP servers for popular languages:
| LSP Server | Languages/Extensions | Requirements |
| LSP Server | Extensions | Requirements |
| ---------- | -------------------------------------------- | ----------------------------------- |
| typescript | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts | `typescript` dependency in project |
| gopls | .go | `go` command available |
@@ -19,17 +21,49 @@ opencode comes with several built-in LSP servers for popular languages:
| zls | .zig, .zon | `zig` command available |
| csharp | .cs | `.NET SDK` installed |
LSP servers are automatically enabled when their requirements are met in your project environment.
LSP servers are automatically enabled when one of the above file extensions are detected and the requirements are met.
## Configuration
---
You can customize LSP servers through the `lsp` section in your `opencode.json` configuration file.
## How It Works
### Disabling LSP Servers
When opencode opens a file, it:
To disable a specific LSP server, set its configuration to `{ "disabled": true }`:
1. Checks the file extension against all enabled LSP servers.
2. Starts the appropriate LSP server if not already running.
---
## Configure
You can customize LSP servers through the `lsp` section in your opencode config.
```json title="opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"lsp": { }
}
```
Each LSP server supports the following:
| Property | Type | Description |
| ---------------- | -------- | ------------------------------------------------- |
| `disabled` | boolean | Set this to `true` to disable the LSP server |
| `command` | string[] | The command to start the LSP server |
| `extensions` | string[] | File extensions this LSP server should handle |
| `env` | object | Environment variables to set when starting server |
| `initialization` | object | Initialization options to send to the LSP server |
Let's look at some examples.
---
### Disabling LSP servers
To disable a specific LSP server, set `disabled` to `true`:
```json title="opencode.json" {5}
{
"$schema": "https://opencode.ai/config.json",
"lsp": {
@@ -40,11 +74,13 @@ To disable a specific LSP server, set its configuration to `{ "disabled": true }
}
```
### Custom LSP Servers
---
### Custom LSP servers
You can add custom LSP servers by specifying the command and file extensions:
```json title="opencode.json"
```json title="opencode.json" {4-7}
{
"$schema": "https://opencode.ai/config.json",
"lsp": {
@@ -55,25 +91,3 @@ You can add custom LSP servers by specifying the command and file extensions:
}
}
```
### Configuration Options
Each LSP server configuration supports these properties:
| Property | Type | Description |
| ---------------- | -------- | ------------------------------------------------- |
| `disabled` | boolean | Set to `true` to disable the LSP server |
| `command` | string[] | The command to start the LSP server |
| `extensions` | string[] | File extensions this LSP server should handle |
| `env` | object | Environment variables to set when starting server |
| `initialization` | object | Initialization options to send to the LSP server |
## How It Works
When opencode opens a file, it:
1. Checks the file extension against all enabled LSP servers
2. Starts the appropriate LSP server if not already running
3. Provides intelligent code analysis and editing capabilities
This integration allows the LLM to better understand your codebase through features like diagnostics, go-to-definition, and find-references.
@@ -16,6 +16,8 @@ Once added, MCP tools are automatically available to the LLM alongside built-in
You can define MCP servers in your opencode config under `mcp`.
---
### Local
Add local MCP servers using `"type": "local"` within the MCP object. Multiple MCP servers can be added. The key string for each server can be any arbitrary name.
@@ -43,6 +45,8 @@ Add local MCP servers using `"type": "local"` within the MCP object. Multiple MC
You can also disable a server by setting `enabled` to `false`. This is useful if you want to temporarily disable a server without removing it from your config.
---
### Remote
Add remote MCP servers under `mcp` with `"type": "remote"`.
@@ -0,0 +1,107 @@
---
title: Plugins
description: Write your own plugins to extend opencode.
---
Plugins allow you to extend opencode by hooking into various events and customizing behavior. You can create plugins to add new features, integrate with external services, or modify opencode's default behavior.
---
## Create a plugin
A plugin is a **JavaScript/TypeScript module** that exports one or more plugin
functions. Each function receives a context object and returns a hooks object.
---
### Location
Plugins are loaded from:
1. `.opencode/plugin` directory either in your proejct
2. Or, globally in `~/.config/opencode/plugin`
---
### Basic structure
```js title=".opencode/plugin/example.js"
export const MyPlugin = async ({ app, client, $ }) => {
console.log("Plugin initialized!")
return {
// Hook implementations go here
}
}
```
The plugin function receives:
- `app`: The opencode application instance.
- `client`: An opencode SDK client for interacting with the AI.
- `$`: Bun's [shell API](https://bun.com/docs/runtime/shell) for executing commands.
---
### TypeScript support
For TypeScript plugins, you can import types from the plugin package:
```ts title="my-plugin.ts" {1}
import type { Plugin } from "@opencode-ai/plugin"
export const MyPlugin: Plugin = async ({ app, client, $ }) => {
return {
// Type-safe hook implementations
}
}
```
---
## Examples
Here are some examples of plugins you can use to extend opencode.
---
### Send notifications
Send notifications when certain events occur:
```js title=".opencode/plugin/notification.js"
export const NotificationPlugin = async ({ client, $ }) => {
return {
event: async ({ event }) => {
// Send notification on session completion
if (event.type === "session.idle") {
await $`osascript -e 'display notification "Session completed!" with title "opencode"'`
}
},
}
}
```
Here `osascript` is a command that runs on macOS and sends notifications.
---
### .env protection
Prevent opencode from reading `.env` files:
```javascript title=".opencode/plugin/slack.js"
export const EnvProtection = async ({ client, $ }) => {
return {
tool: {
execute: {
before: async (input, output) => {
if (input.tool === "read" && output.args.filePath.includes(".env")) {
throw new Error("Do not read .env files")
}
}
}
}
}
}
```
@@ -331,7 +331,6 @@ Or if you already have an API key, you can select **Manually enter API Key** and
1. Head over to the [Cerebras console](https://inference.cerebras.ai/), create an account, and generate an API key.
2. Run `opencode auth login` and select **Cerebras**.
```bash
@@ -589,7 +588,7 @@ To use Kimi K2 from Moonshot AI:
5. Configure Moonshot in your opencode config.
```json title="opencode.json" "\"moonshot\"" {5-15}
```json title="opencode.json" ""moonshot"" {5-15}
{
"$schema": "https://opencode.ai/config.json",
"provider": {
@@ -796,6 +795,42 @@ https://platform.openai.com/api-keys
---
### Zhipu AI
1. Head over to the [Zhipu API console](https://z.ai/manage-apikey/apikey-list), create an account, and click **Create a new API key**.
2. Run `opencode auth login` and select **Zhipu AI**.
```bash
$ opencode auth login
┌ Add credential
◆ Select provider
│ ● Zhipu AI
│ ...
```
3. Enter your Zhipu AI API key.
```bash
$ opencode auth login
┌ Add credential
◇ Select provider
│ Zhipu AI
◇ Enter your API key
│ _
```
4. Run the `/models` command to select a model like _GLM-4.5_.
---
## Troubleshooting
If you are having trouble with configuring a provider, check the following:
+1 -1
View File
@@ -71,7 +71,7 @@ const ogImage = `${config.socialCard}/opencode-share/${encodedTitle}.png?model=$
tag: "meta",
attrs: {
name: "robots",
content: "noindex",
content: "noindex, nofollow, noarchive, nosnippet",
}
},
{
+16
View File
@@ -0,0 +1,16 @@
diff --git a/node_modules/marked-shiki/.bun-tag-5eae3435af8a0229 b/.bun-tag-5eae3435af8a0229
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/dist/index.js b/dist/index.js
index 535885e8569626d08f205e76c4944e4ebd0decab..92b5695c1b05fc7db5d26128c8f948d43c91f88a 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -4,7 +4,7 @@ function o(s = {}) {
async: !0,
async walkTokens(t) {
if (t.type !== "code" || typeof e != "function") return;
- const [a = "text", ...i] = t.lang.split(" "), { text: c } = t, r = await e(c, a, i), l = n ? n.replace("%l", String(a).toUpperCase()).replace("%s", r).replace("%t", c) : r;
+ const [a = "text", ...i] = t.lang?.split(" "), { text: c } = t, r = await e(c, a, i), l = n ? n.replace("%l", String(a).toUpperCase()).replace("%s", r).replace("%t", c) : r;
Object.assign(t, {
type: "html",
block: !0,
+32 -4
View File
@@ -2,6 +2,8 @@
import { $ } from "bun"
console.log("=== publishing ===\n")
const snapshot = process.env["OPENCODE_SNAPSHOT"] === "true"
const version = snapshot
? `0.0.0-${new Date().toISOString().slice(0, 16).replace(/[-:T]/g, "")}`
@@ -10,19 +12,45 @@ if (!version) {
throw new Error("OPENCODE_VERSION is required")
}
process.env["OPENCODE_VERSION"] = version
console.log("version:", version)
const pkgjsons = await Array.fromAsync(
new Bun.Glob("**/package.json").scan({
absolute: true,
}),
).then((arr) => arr.filter((x) => !x.includes("node_modules") && !x.includes("dist")))
const tree = await $`git add . && git write-tree`.text().then((x) => x.trim())
for (const file of pkgjsons) {
let pkg = await Bun.file(file).text()
pkg = pkg.replaceAll(/"version": "[^"]+"/g, `"version": "${version}"`)
console.log("updated:", file)
await Bun.file(file).write(pkg)
}
await $`bun install`
console.log("\n=== opencode ===\n")
await import(`../packages/opencode/script/publish.ts`)
console.log("\n=== sdk ===\n")
await import(`../packages/sdk/js/script/publish.ts`)
// await import(`../packages/sdk/stainless/generate.ts`)
console.log("\n=== plugin ===\n")
await import(`../packages/plugin/script/publish.ts`)
if (!snapshot) {
await $`git commit -am "release: v${version}"`
await $`git tag v${version}`
await $`git push origin HEAD --tags`
await $`git push origin HEAD --tags --no-verify`
}
if (snapshot) {
await $`git checkout -b snapshot-${version}`
await $`git commit --allow-empty -m "Snapshot release v${version}"`
await $`git tag v${version}`
await $`git push origin v${version}`
await $`git reset --soft HEAD~1`
await $`git push origin v${version} --no-verify`
await $`git checkout dev`
await $`git branch -D snapshot-${version}`
for (const file of pkgjsons) {
await $`git checkout ${tree} ${file}`
}
}
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "opencode",
"displayName": "opencode",
"description": "opencode for VS Code",
"version": "0.0.0",
"version": "0.3.129",
"publisher": "sst-dev",
"repository": {
"type": "git",
-15
View File
@@ -1,15 +0,0 @@
import * as assert from 'assert';
// You can import and use all API from the 'vscode' module
// as well as import your extension to test it
import * as vscode from 'vscode';
// import * as myExtension from '../../extension';
suite('Extension Test Suite', () => {
vscode.window.showInformationMessage('Start all tests.');
test('Sample test', () => {
assert.strictEqual(-1, [1, 2, 3].indexOf(5));
assert.strictEqual(-1, [1, 2, 3].indexOf(0));
});
});
+20
View File
@@ -5,6 +5,10 @@
declare module "sst" {
export interface Resource {
"ANTHROPIC_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
"Api": {
"type": "sst.cloudflare.Worker"
"url": string
@@ -20,10 +24,26 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
"GatewayApi": {
"type": "sst.cloudflare.Worker"
"url": string
}
"OPENAI_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
"OPENCODE_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
"Web": {
"type": "sst.cloudflare.Astro"
"url": string
}
"ZHIPU_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
}
}
/// <reference path="sst-env.d.ts" />
+2 -1
View File
@@ -10,9 +10,10 @@ export default $config({
}
},
async run() {
const { api } = await import("./infra/app.js")
const { api, gateway } = await import("./infra/app.js")
return {
api: api.url,
gateway: gateway.url,
}
},
})