Compare commits

..

2 Commits

Author SHA1 Message Date
Kit Langton 25a4f833cf fix(simulation): align vertical box drawing 2026-07-15 11:21:59 -04:00
Kit Langton 04fdf59db8 feat(simulation): expose normalized terminal frames 2026-07-15 11:09:53 -04:00
523 changed files with 3696 additions and 11092 deletions
-1
View File
@@ -1,3 +1,2 @@
packages/core/migration/**/snapshot.json linguist-generated
packages/core/src/database/migration.gen.ts linguist-generated
packages/core/src/**/*.txt text eol=lf
+2
View File
@@ -19,6 +19,8 @@ Valid types are `feat`, `fix`, `docs`, `chore`, `refactor`, and `test`. Scopes a
Examples: `fix(tui): simplify thinking toggle styling`, `docs: update contributing guide`, `chore(sdk): regenerate types`.
Never bypass Git hooks. Do not use `--no-verify` or otherwise disable, skip, or circumvent commit or push hooks. If a hook fails, fix the failure or stop and report it to the user.
## Style Guide
### General Principles
+152 -66
View File
@@ -30,30 +30,9 @@
"turbo": "2.10.2",
},
},
"packages/ai": {
"name": "@opencode-ai/ai",
"version": "1.17.20",
"dependencies": {
"@opencode-ai/schema": "workspace:*",
"@smithy/eventstream-codec": "4.2.14",
"@smithy/util-utf8": "4.2.2",
"aws4fetch": "1.0.20",
"effect": "catalog:",
"google-auth-library": "10.5.0",
},
"devDependencies": {
"@clack/prompts": "1.0.0-alpha.1",
"@effect/platform-node": "catalog:",
"@opencode-ai/http-recorder": "workspace:*",
"@tsconfig/bun": "catalog:",
"@types/bun": "catalog:",
"@typescript/native-preview": "catalog:",
"typescript": "catalog:",
},
},
"packages/app": {
"name": "@opencode-ai/app",
"version": "1.18.2",
"version": "1.17.20",
"dependencies": {
"@corvu/drawer": "catalog:",
"@dnd-kit/abstract": "0.5.0",
@@ -119,7 +98,7 @@
},
"packages/cli": {
"name": "@opencode-ai/cli",
"version": "1.18.2",
"version": "1.17.18",
"bin": {
"opencode2": "./bin/opencode2.cjs",
},
@@ -179,7 +158,7 @@
},
"packages/codemode": {
"name": "@opencode-ai/codemode",
"version": "1.18.2",
"version": "1.17.20",
"dependencies": {
"acorn": "8.15.0",
"effect": "catalog:",
@@ -193,7 +172,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "1.18.2",
"version": "1.17.20",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -229,7 +208,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "1.18.2",
"version": "1.17.20",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -256,7 +235,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "1.18.2",
"version": "1.17.20",
"dependencies": {
"@ai-sdk/anthropic": "3.0.82",
"@ai-sdk/openai": "3.0.48",
@@ -278,7 +257,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "1.18.2",
"version": "1.17.20",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -302,7 +281,7 @@
},
"packages/console/support": {
"name": "@opencode-ai/console-support",
"version": "1.18.2",
"version": "1.17.20",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@opencode-ai/console-core": "workspace:*",
@@ -322,7 +301,7 @@
},
"packages/core": {
"name": "@opencode-ai/core",
"version": "1.18.2",
"version": "1.17.20",
"bin": {
"opencode": "./bin/opencode",
},
@@ -356,10 +335,10 @@
"@modelcontextprotocol/sdk": "1.29.0",
"@npmcli/arborist": "9.4.0",
"@npmcli/config": "10.8.1",
"@opencode-ai/ai": "workspace:*",
"@opencode-ai/codemode": "workspace:*",
"@opencode-ai/effect-drizzle-sqlite": "workspace:*",
"@opencode-ai/effect-sqlite-node": "workspace:*",
"@opencode-ai/llm": "workspace:*",
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/schema": "workspace:*",
"@openrouter/ai-sdk-provider": "2.9.0",
@@ -418,7 +397,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.18.2",
"version": "1.17.20",
"dependencies": {
"@zip.js/zip.js": "2.7.62",
"effect": "catalog:",
@@ -478,7 +457,7 @@
},
"packages/effect-drizzle-sqlite": {
"name": "@opencode-ai/effect-drizzle-sqlite",
"version": "1.18.2",
"version": "1.17.20",
"dependencies": {
"drizzle-orm": "catalog:",
"effect": "catalog:",
@@ -492,7 +471,7 @@
},
"packages/effect-sqlite-node": {
"name": "@opencode-ai/effect-sqlite-node",
"version": "1.18.2",
"version": "1.17.20",
"dependencies": {
"effect": "catalog:",
},
@@ -504,7 +483,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "1.18.2",
"version": "1.17.20",
"dependencies": {
"@hono/standard-validator": "catalog:",
"@opencode-ai/core": "workspace:*",
@@ -536,7 +515,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.18.2",
"version": "1.17.20",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -552,7 +531,7 @@
},
"packages/http-recorder": {
"name": "@opencode-ai/http-recorder",
"version": "1.18.2",
"version": "1.17.20",
"dependencies": {
"@effect/platform-node-shared": "4.0.0-beta.83",
},
@@ -582,9 +561,28 @@
"@typescript/native-preview": "catalog:",
},
},
"packages/llm": {
"name": "@opencode-ai/llm",
"version": "1.17.20",
"dependencies": {
"@opencode-ai/schema": "workspace:*",
"@smithy/eventstream-codec": "4.2.14",
"@smithy/util-utf8": "4.2.2",
"aws4fetch": "1.0.20",
"effect": "catalog:",
},
"devDependencies": {
"@clack/prompts": "1.0.0-alpha.1",
"@effect/platform-node": "catalog:",
"@opencode-ai/http-recorder": "workspace:*",
"@tsconfig/bun": "catalog:",
"@types/bun": "catalog:",
"@typescript/native-preview": "catalog:",
},
},
"packages/opencode": {
"name": "opencode",
"version": "1.18.2",
"version": "1.17.20",
"bin": {
"opencode": "./bin/opencode",
},
@@ -621,10 +619,10 @@
"@octokit/graphql": "9.0.2",
"@octokit/rest": "catalog:",
"@openauthjs/openauth": "catalog:",
"@opencode-ai/ai": "workspace:*",
"@opencode-ai/cli": "workspace:*",
"@opencode-ai/client": "workspace:*",
"@opencode-ai/codemode": "workspace:*",
"@opencode-ai/llm": "workspace:*",
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/protocol": "workspace:*",
"@opencode-ai/schema": "workspace:*",
@@ -717,14 +715,12 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.18.2",
"version": "1.17.20",
"dependencies": {
"@ai-sdk/provider": "3.0.8",
"@opencode-ai/ai": "workspace:*",
"@opencode-ai/client": "workspace:*",
"@opencode-ai/schema": "workspace:*",
"@opencode-ai/sdk": "workspace:*",
"@standard-schema/spec": "^1.1.0",
"effect": "catalog:",
"zod": "catalog:",
},
@@ -806,7 +802,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.18.2",
"version": "1.17.20",
"dependencies": {
"cross-spawn": "catalog:",
},
@@ -821,7 +817,7 @@
},
"packages/server": {
"name": "@opencode-ai/server",
"version": "1.18.2",
"version": "1.17.20",
"dependencies": {
"@effect/platform-node": "catalog:",
"@opencode-ai/core": "workspace:*",
@@ -838,7 +834,7 @@
},
"packages/session-ui": {
"name": "@opencode-ai/session-ui",
"version": "1.18.2",
"version": "1.17.20",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/core": "workspace:*",
@@ -886,8 +882,8 @@
"dependencies": {
"@fontsource/commit-mono": "5.2.5",
"@napi-rs/canvas": "1.0.2",
"@opencode-ai/ai": "workspace:*",
"@opencode-ai/core": "workspace:*",
"@opencode-ai/llm": "workspace:*",
"@opentui/core": "catalog:",
"effect": "catalog:",
},
@@ -899,7 +895,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.18.2",
"version": "1.17.20",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -912,7 +908,7 @@
},
"packages/stats/app": {
"name": "@opencode-ai/stats-app",
"version": "1.18.2",
"version": "1.17.20",
"dependencies": {
"@ibm/plex": "6.4.1",
"@kobalte/core": "catalog:",
@@ -946,7 +942,7 @@
},
"packages/stats/core": {
"name": "@opencode-ai/stats-core",
"version": "1.18.2",
"version": "1.17.20",
"dependencies": {
"@aws-sdk/client-athena": "3.933.0",
"@planetscale/database": "1.19.0",
@@ -965,7 +961,7 @@
},
"packages/stats/server": {
"name": "@opencode-ai/stats-server",
"version": "1.18.2",
"version": "1.17.20",
"dependencies": {
"@aws-sdk/client-firehose": "3.933.0",
"@effect/platform-node": "catalog:",
@@ -1006,7 +1002,7 @@
},
"packages/tui": {
"name": "@opencode-ai/tui",
"version": "1.18.2",
"version": "1.17.20",
"dependencies": {
"@opencode-ai/client": "workspace:*",
"@opencode-ai/core": "workspace:*",
@@ -1036,7 +1032,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.18.2",
"version": "1.17.20",
"dependencies": {
"@kobalte/core": "catalog:",
"@pierre/diffs": "catalog:",
@@ -1087,7 +1083,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.18.2",
"version": "1.17.20",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@@ -2169,8 +2165,6 @@
"@openauthjs/openauth": ["@openauthjs/openauth@0.0.0-20250322224806", "", { "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-p5IWSRXvABcwocH2dNI0w8c1QJelIOFulwhKk+aLLFfUbs8u1pr7kQbYe8yCSM2+bcLHiwbogpUQc2ovrGwCuw=="],
"@opencode-ai/ai": ["@opencode-ai/ai@workspace:packages/ai"],
"@opencode-ai/app": ["@opencode-ai/app@workspace:packages/app"],
"@opencode-ai/cli": ["@opencode-ai/cli@workspace:packages/cli"],
@@ -2209,6 +2203,8 @@
"@opencode-ai/httpapi-codegen": ["@opencode-ai/httpapi-codegen@workspace:packages/httpapi-codegen"],
"@opencode-ai/llm": ["@opencode-ai/llm@workspace:packages/llm"],
"@opencode-ai/plugin": ["@opencode-ai/plugin@workspace:packages/plugin"],
"@opencode-ai/protocol": ["@opencode-ai/protocol@workspace:packages/protocol"],
@@ -2873,7 +2869,7 @@
"@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-tHhdiWZfG1ZIh2YcRfPJmY2gHcBmqbAzqm3ER4TIDFYsSEqTD5tICT7cgQ/kI8LRakxp12myOYyK68XPn7MnHw=="],
"@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.14", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw=="],
"@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.7", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.11.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-DrpkEoM3j9cBBWhufqBwnbbn+3nf1N9FP6xuVJ+e220jbactKuQgaZwjwP5CP1t+O94brm2JgVMD2atMGX3xIQ=="],
"@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.3.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-M9rMkTar7JcRrvUHsK1271AuWDmrISIPQpQ4TSHmYZ4KMisGnMH0gfjCWnBwdndR7skvvp/UheHhZGvO3Cr8/g=="],
@@ -2947,7 +2943,7 @@
"@smithy/util-stream": ["@smithy/util-stream@4.6.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-PFzBVEBP5k8R+mK/c+VAKmtpUTL+KzBIXWJ6oM0GWOb31K+QgymXV9IW03XLPM1wtkC7oAb9ZBN2aswSSVbNFg=="],
"@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
"@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="],
"@smithy/util-waiter": ["@smithy/util-waiter@4.4.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-EYviebytZE6vplW0AGwZ2Rc3sNuVR83lfUCNZu11VchUiKhMwJqrRWy7iVDTNEwG/vEwItno591Iad6/prj6Bw=="],
@@ -3009,7 +3005,7 @@
"@standard-community/standard-openapi": ["@standard-community/standard-openapi@0.2.9", "", { "peerDependencies": { "@standard-community/standard-json": "^0.3.5", "@standard-schema/spec": "^1.0.0", "arktype": "^2.1.20", "effect": "^3.17.14", "openapi-types": "^12.1.3", "sury": "^10.0.0", "typebox": "^1.0.0", "valibot": "^1.1.0", "zod": "^3.25.0 || ^4.0.0", "zod-openapi": "^4" }, "optionalPeers": ["arktype", "effect", "sury", "typebox", "valibot", "zod", "zod-openapi"] }, "sha512-htj+yldvN1XncyZi4rehbf9kLbu8os2Ke/rfqoZHCMHuw34kiF3LP/yQPdA0tQ940y8nDq3Iou8R3wG+AGGyvg=="],
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
"@stoplight/better-ajv-errors": ["@stoplight/better-ajv-errors@1.0.3", "", { "dependencies": { "jsonpointer": "^5.0.0", "leven": "^3.1.0" }, "peerDependencies": { "ajv": ">=8" } }, "sha512-0p9uXkuB22qGdNfy3VeEhxkU5uwvp/KrBTAbrLBURv6ilxIVwanKwjMc41lQfIVgPGcOkmLbTolfFrSsueu7zA=="],
@@ -6337,6 +6333,10 @@
"@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
"@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.14", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw=="],
"@ai-sdk/amazon-bedrock/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
"@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
"@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
@@ -6393,6 +6393,8 @@
"@ai-sdk/perplexity/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
"@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/togetherai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
"@ai-sdk/vercel/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
@@ -6453,18 +6455,26 @@
"@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
"@aws-sdk/client-athena/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
"@aws-sdk/client-cognito-identity/@aws-sdk/core": ["@aws-sdk/core@3.974.15", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@aws-sdk/xml-builder": "^3.972.26", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.5", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw=="],
"@aws-sdk/client-cognito-identity/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.47", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.41", "@aws-sdk/credential-provider-http": "^3.972.43", "@aws-sdk/credential-provider-ini": "^3.972.46", "@aws-sdk/credential-provider-process": "^3.972.41", "@aws-sdk/credential-provider-sso": "^3.972.45", "@aws-sdk/credential-provider-web-identity": "^3.972.45", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/credential-provider-imds": "^4.3.6", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-HrId+C0DWA5qDIyLG64/kjUB2RNtPypxmABnIctK+TA1P1kHlOYoE/Wf5T5tKOMKgb08P7k/zNyhvfJ3lh5Oag=="],
"@aws-sdk/client-cognito-identity/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="],
"@aws-sdk/client-firehose/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
"@aws-sdk/client-lambda/@aws-sdk/core": ["@aws-sdk/core@3.974.15", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@aws-sdk/xml-builder": "^3.972.26", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.5", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw=="],
"@aws-sdk/client-lambda/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.47", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.41", "@aws-sdk/credential-provider-http": "^3.972.43", "@aws-sdk/credential-provider-ini": "^3.972.46", "@aws-sdk/credential-provider-process": "^3.972.41", "@aws-sdk/credential-provider-sso": "^3.972.45", "@aws-sdk/credential-provider-web-identity": "^3.972.45", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/credential-provider-imds": "^4.3.6", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-HrId+C0DWA5qDIyLG64/kjUB2RNtPypxmABnIctK+TA1P1kHlOYoE/Wf5T5tKOMKgb08P7k/zNyhvfJ3lh5Oag=="],
"@aws-sdk/client-lambda/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="],
"@aws-sdk/client-s3/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
"@aws-sdk/client-sso/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
"@aws-sdk/client-sts/@aws-sdk/core": ["@aws-sdk/core@3.775.0", "", { "dependencies": { "@aws-sdk/types": "3.775.0", "@smithy/core": "^3.2.0", "@smithy/node-config-provider": "^4.0.2", "@smithy/property-provider": "^4.0.2", "@smithy/protocol-http": "^5.1.0", "@smithy/signature-v4": "^5.0.2", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/util-middleware": "^4.0.2", "fast-xml-parser": "4.4.1", "tslib": "^2.6.2" } }, "sha512-8vpW4WihVfz0DX+7WnnLGm3GuQER++b0IwQG35JlQMlgqnc44M//KbJPsIHA0aJUJVwJAEShgfr5dUbY8WUzaA=="],
"@aws-sdk/client-sts/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.782.0", "", { "dependencies": { "@aws-sdk/credential-provider-env": "3.775.0", "@aws-sdk/credential-provider-http": "3.775.0", "@aws-sdk/credential-provider-ini": "3.782.0", "@aws-sdk/credential-provider-process": "3.775.0", "@aws-sdk/credential-provider-sso": "3.782.0", "@aws-sdk/credential-provider-web-identity": "3.782.0", "@aws-sdk/types": "3.775.0", "@smithy/credential-provider-imds": "^4.0.2", "@smithy/property-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-HZiAF+TCEyKjju9dgysjiPIWgt/+VerGaeEp18mvKLNfgKz1d+/82A2USEpNKTze7v3cMFASx3CvL8yYyF7mJw=="],
@@ -6487,6 +6497,10 @@
"@aws-sdk/client-sts/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.782.0", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "3.782.0", "@aws-sdk/types": "3.775.0", "@smithy/node-config-provider": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-dMFkUBgh2Bxuw8fYZQoH/u3H4afQ12VSkzEi//qFiDTwbKYq+u+RYjc8GLDM6JSK1BShMu5AVR7HD4ap1TYUnA=="],
"@aws-sdk/client-sts/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
"@aws-sdk/core/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
"@aws-sdk/credential-provider-cognito-identity/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="],
"@aws-sdk/credential-provider-env/@aws-sdk/core": ["@aws-sdk/core@3.974.15", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@aws-sdk/xml-builder": "^3.972.26", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.5", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw=="],
@@ -6535,6 +6549,10 @@
"@aws-sdk/credential-providers/@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="],
"@aws-sdk/middleware-flexible-checksums/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
"@aws-sdk/middleware-sdk-s3/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
"@aws-sdk/nested-clients/@aws-sdk/core": ["@aws-sdk/core@3.974.15", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@aws-sdk/xml-builder": "^3.972.26", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.5", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw=="],
"@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.30", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-HULDLMVzkmTSEv6//7kx2kRevp/VYUpm8hJNNFbmhxDn0fUiGTxVcM9yg31TukvTq8nyOBDUN2gH0o5IRbKjdw=="],
@@ -6609,8 +6627,6 @@
"@hey-api/openapi-ts/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
"@hono/standard-validator/@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
"@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
"@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
@@ -6831,10 +6847,6 @@
"@opencode-ai/cli/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"@opencode-ai/console-app/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.7", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.11.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-DrpkEoM3j9cBBWhufqBwnbbn+3nf1N9FP6xuVJ+e220jbactKuQgaZwjwP5CP1t+O94brm2JgVMD2atMGX3xIQ=="],
"@opencode-ai/console-app/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="],
"@opencode-ai/console-mail/@types/react": ["@types/react@18.0.25", "", { "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", "csstype": "^3.0.2" } }, "sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g=="],
"@opencode-ai/core/@ai-sdk/openai": ["@ai-sdk/openai@3.0.84", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cmgbeJL0bbY0yTJH4/AdmP5E7MjWRL9G8UdhIi0JlV/So03o82ORJofW8OzwCZPTORVQblFbpZXYGDcUd9NdUQ=="],
@@ -6849,6 +6861,10 @@
"@opencode-ai/desktop/typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="],
"@opencode-ai/llm/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.14", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw=="],
"@opencode-ai/llm/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
"@opencode-ai/script/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
"@opencode-ai/session-ui/@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="],
@@ -7333,6 +7349,8 @@
"editorconfig/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
"effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"electron-builder/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"electron-builder/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
@@ -7393,6 +7411,8 @@
"fumadocs-core/shiki": ["shiki@4.3.1", "", { "dependencies": { "@shikijs/core": "4.3.1", "@shikijs/engine-javascript": "4.3.1", "@shikijs/engine-oniguruma": "4.3.1", "@shikijs/langs": "4.3.1", "@shikijs/themes": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw=="],
"fumadocs-mdx/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"fumadocs-mdx/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
"fumadocs-mdx/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="],
@@ -7531,8 +7551,6 @@
"opencode/@solid-primitives/scheduled": ["@solid-primitives/scheduled@1.5.2", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-/j2igE0xyNaHhj6kMfcUQn5rAVSTLbAX+CDEBm25hSNBmNiHLu2lM7Usj2kJJ5j36D67bE8wR1hBNA8hjtvsQA=="],
"opencode/@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
"opencode/minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="],
"opencode/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
@@ -7739,6 +7757,46 @@
"@actions/github/@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="],
"@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/azure/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/cerebras/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/cohere/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/deepgram/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/deepinfra/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/deepseek/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/elevenlabs/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/fireworks/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/google-vertex/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/google/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/groq/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/mistral/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/openai-compatible/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/openai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/perplexity/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/togetherai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/vercel/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/xai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@astrojs/check/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
"@astrojs/check/yargs/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=="],
@@ -8341,6 +8399,10 @@
"@solidjs/start/shiki/@shikijs/types": ["@shikijs/types@1.29.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw=="],
"@standard-community/standard-json/effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@standard-community/standard-openapi/effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@stoplight/spectral-core/minimatch/brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="],
"@storybook/addon-docs/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
@@ -8415,6 +8477,10 @@
"ai-gateway-provider/@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
"ai-gateway-provider/@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.14", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw=="],
"ai-gateway-provider/@ai-sdk/amazon-bedrock/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
"ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
"ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
@@ -8805,6 +8871,10 @@
"venice-ai-sdk-provider/@ai-sdk/provider-utils/@ai-sdk/provider": ["@ai-sdk/provider@3.0.12", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-sj9DWTJ2Ze0WR9qsiOPqoqzNx3OxL6iMxHImbhvoe9qOspekbzxNDMiJ4TIGfYHYh9w4OmBjz3prvqhzTi96+Q=="],
"venice-ai-sdk-provider/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"vitest/@vitest/expect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"vitest/@vitest/expect/chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
"wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="],
@@ -8917,8 +8987,12 @@
"@aws-sdk/credential-provider-login/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.7.3", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg=="],
"@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
"@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.933.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-o1GX0+IPlFi/D8ei9y/jj3yucJWNfPnbB5appVBWevAyUdZA5KzQ2nK/hDxiu9olTZlFEFpf1m1Rn3FaGxHqsw=="],
"@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
"@aws-sdk/credential-provider-process/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.7.3", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg=="],
"@aws-sdk/credential-provider-sso/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.7.3", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg=="],
@@ -9069,6 +9143,8 @@
"@octokit/rest/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
"@opencode-ai/core/@ai-sdk/openai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@opencode-ai/desktop/@actions/artifact/@actions/http-client/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="],
"@opencode-ai/www/@cloudflare/vite-plugin/@cloudflare/unenv-preset/workerd": ["workerd@1.20260708.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260708.1", "@cloudflare/workerd-darwin-arm64": "1.20260708.1", "@cloudflare/workerd-linux-64": "1.20260708.1", "@cloudflare/workerd-linux-arm64": "1.20260708.1", "@cloudflare/workerd-windows-64": "1.20260708.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-WAK+Kt/VVCSldH2qSr8lx46XCJ4Q+bdlHNaFqUtOHthBEIB8C1N8HVW+VOLrxDoTCk0NGNv0zajnBeQK4JOB9w=="],
@@ -9251,6 +9327,14 @@
"@vitejs/plugin-react/vite/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
"ai-gateway-provider/@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"ai-gateway-provider/@ai-sdk/azure/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"ai-gateway-provider/@ai-sdk/deepseek/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
@@ -9393,6 +9477,8 @@
"@aws-sdk/credential-provider-login/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="],
"@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
"@aws-sdk/credential-provider-process/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="],
"@aws-sdk/credential-provider-sso/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="],
+31 -93
View File
@@ -30,50 +30,6 @@ This proposal does not introduce a supervisor process, warm candidate server,
protocol negotiation, idle background restart, or general execution-recovery
framework.
## Architecture at a Glance
```text
╭───────────────────╮
│ CLI ServiceConfig │
╰─────────┬─────────╯
╭──────────────────────╮
│ CLI ServerConnection │
╰───────────┬──────────╯
╭──────────────────╰───────────────────╮
▼ ▼
╭──────────────────────────╮ ╭─────────────────────────╮
│ Client Service lifecycle │ │ CLI runPromiseWith seam │
╰─────────────┬────────────╯ ╰─────────────┬───────────╯
╰─────╮ │
▼ ▼
╭────────────────────────────╮ ╭─────────────╮
│ Background service process │ │ TUI / Solid │
╰──────────────┬─────────────╯ ╰──────┬──────╯
│ │
╰────────────◀────────────────────╯
╭───────────────────────╮
│ Server HTTP transport │
╰───────────┬───────────╯
╭──────────────────╮
│ Core application │
╰──────────────────╯
```
| Owner | Responsibility |
| ------------------------------------------------ | --------------------------------------------------------------------------------------------------- |
| `packages/client/src/effect/service.ts` | Effect-native discovery, start, and stop lifecycle operations |
| `packages/cli/src/services/service-config.ts` | CLI registration path, installed version, and daemon command |
| `packages/cli/src/services/server-connection.ts` | Resolve an endpoint and, only for the shared service, grouped reconnect and restart Effects |
| `packages/cli/src/server-process.ts` | Daemon election, registration, and server process boot |
| `packages/server/src/process.ts` | HTTP lifecycle shell and application transport |
| `packages/core` | Application behavior behind the transport |
| CLI default handler | Convert lifecycle Effects with the outer `FileSystem` context and pass grouped Promise capabilities |
| `packages/tui` Solid client context | Own event-stream reconnect, endpoint replacement, status, and user-triggered restart UI |
## Implementation Status
| Area | State |
@@ -208,22 +164,19 @@ This design gives each concept one authority.
## System Model
```text
╭───────────────────────╮ ╭──────────────────────────────╮
Fresh or existing TUI │ │ Process-held OS service lock │
╰───────────┬───────────╯ ╰───────────────┬──────────────╯
╰─────┬ normal requests observe ───────────────────────╮ │
│ discover │ ├──╯ authorizes one owner
▼ │ ▼
╭───────────────────╮ │ ╭─────────────────╮
│ Registration file │ │ │ Lifecycle shell │
╰───────────────────╯ │ ╰────────┬────────╯
│ │
├────────────────────────╯
╭──────────────────────╮
│ OpenCode application │
╰──────────────────────╯
```mermaid
flowchart LR
TUI[Fresh or existing TUI]
REG[Registration file]
LOCK[Process-held OS service lock]
SHELL[Lifecycle shell]
APP[OpenCode application]
TUI -->|discover| REG
TUI -->|observe| SHELL
TUI -->|normal requests| APP
SHELL --> APP
LOCK -->|authorizes one owner| SHELL
```
The lifecycle shell and application run in the same process. The distinction is
@@ -358,39 +311,24 @@ Windows, where Bun FFI is not available on every shipped architecture. It lives
the foundation of the design, so the delivery sequence spikes it first.
```mermaid
Contender Lock Lifecycle Application
│ │ │ │
├─ try acquire ───▶ │ │
│ │ │ │
╭─ alt: lock held ────────────────────────────────────────────────╮
│ │ │ │ │ │
│ ◀─ busy ──────────┤ │ │ │
│ │ │ │ │ │
│ ├─────────╮ │ │ │ │
│ │ exit │ │ │ │ │
│ ◀─────────╯ │ │ │ │
│ │ │ │ │ │
├─ else: lock acquired ───────────────────────────────────────────┤
│ │ │ │ │ │
│ ◀─ owner ─────────┤ │ │ │
│ │ │ │ │ │
│ ├─ bind, register, starting ────────▶ │ │
│ │ │ │ │ │
│ ├─ initialize ──────────────────────────────────────────────▶ │
│ │ │ │ │ │
│╭─ alt: boot succeeds ──────────────────────────────────────────╮│
││ │ │ │ │ ││
││ │ │ ◀─ ready ───────────────┤ ││
││ │ │ │ │ ││
│├─ else: boot fails ────────────────────────────────────────────┤│
││ │ │ │ │ ││
││ │ │ ◀─ failed, stay bound ──┤ ││
││ │ │ │ │ ││
│╰───────────────────────────────────────────────────────────────╯│
│ │ │ │ │ │
╰─────────────────────────────────────────────────────────────────╯
│ │ │ │
```
flowchart TD
A[Contender process starts]
B{Acquire service lock?}
C[Exit successfully]
D[Bind lifecycle shell]
E[Write registration]
F[Report starting]
G[Initialize application]
H[Report ready]
I[Serve until shutdown]
A --> B
B -->|No| C
B -->|Yes| D
D --> E --> F --> G
G -->|Success| H --> I
G -->|Failure| J[Report failed and stay bound]
```
Lock acquisition by a contender is nonblocking or tightly bounded. A loser
must exit before constructing application routes or importing startup-heavy
-37
View File
@@ -1,37 +0,0 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.17.20",
"name": "@opencode-ai/ai",
"type": "module",
"license": "MIT",
"scripts": {
"setup:recording-env": "bun run script/setup-recording-env.ts",
"test": "bun test --timeout 30000 --only-failures",
"typecheck": "tsgo --noEmit",
"build": "tsc -p tsconfig.build.json"
},
"files": [
"dist"
],
"exports": {
".": "./src/index.ts",
"./*": "./src/*.ts"
},
"devDependencies": {
"@clack/prompts": "1.0.0-alpha.1",
"@effect/platform-node": "catalog:",
"@opencode-ai/http-recorder": "workspace:*",
"@tsconfig/bun": "catalog:",
"@types/bun": "catalog:",
"@typescript/native-preview": "catalog:",
"typescript": "catalog:"
},
"dependencies": {
"@smithy/eventstream-codec": "4.2.14",
"@smithy/util-utf8": "4.2.2",
"@opencode-ai/schema": "workspace:*",
"aws4fetch": "1.0.20",
"effect": "catalog:",
"google-auth-library": "10.5.0"
}
}
-38
View File
@@ -1,38 +0,0 @@
#!/usr/bin/env bun
import { Script } from "@opencode-ai/script"
import { $ } from "bun"
import { fileURLToPath } from "url"
const dir = fileURLToPath(new URL("..", import.meta.url))
process.chdir(dir)
async function published(name: string, version: string) {
return (await $`npm view ${name}@${version} version`.nothrow()).exitCode === 0
}
await $`bun run build`
const originalText = await Bun.file("package.json").text()
const pkg = JSON.parse(originalText) as {
name: string
version: string
exports: Record<string, string>
}
if (await published(pkg.name, pkg.version)) {
console.log(`already published ${pkg.name}@${pkg.version}`)
} else {
for (const [key, value] of Object.entries(pkg.exports)) {
const file = value.replace("./src/", "./dist/").replace(".ts", "")
// @ts-ignore
pkg.exports[key] = {
import: file + ".js",
types: file + ".d.ts",
}
}
await Bun.write("package.json", JSON.stringify(pkg, null, 2))
try {
await $`bun pm pack`
await $`npm publish *.tgz --tag ${Script.channel} --access public`
} finally {
await Bun.write("package.json", originalText)
}
}
-1
View File
@@ -1 +0,0 @@
export * from "./protocols/index"
-1
View File
@@ -1 +0,0 @@
export * from "./providers/index"
@@ -1,67 +0,0 @@
import type { ProviderPackage } from "../provider-package"
import { AnthropicMessages } from "../protocols/anthropic-messages"
import { Auth } from "../route/auth"
import type { ProviderAuthOption } from "../route/auth-options"
import type { RouteDefaultsInput } from "../route/client"
import { ProviderID, type ModelID } from "../schema"
export const id = ProviderID.make("anthropic-compatible")
export type Config = RouteDefaultsInput &
ProviderAuthOption<"optional"> & {
readonly provider?: string
readonly baseURL: string
}
export type Settings = ProviderPackage.Settings &
(
| { readonly apiKey?: string; readonly authToken?: never }
| { readonly apiKey?: never; readonly authToken?: string }
) & {
readonly baseURL: string
readonly provider?: string
}
export const routes = [AnthropicMessages.route]
const auth = (input: ProviderAuthOption<"optional">) => {
if ("auth" in input && input.auth) return input.auth
return Auth.optional("apiKey" in input ? input.apiKey : undefined, "apiKey").pipe(Auth.header("x-api-key"))
}
export const configure = (input: Config) => {
if (!input.baseURL) throw new Error("Anthropic-compatible providers require a baseURL")
const provider = input.provider ?? "anthropic-compatible"
const { provider: _, baseURL, apiKey: _apiKey, auth: _auth, ...rest } = input
const route = AnthropicMessages.route.with({
...rest,
provider,
endpoint: { baseURL },
auth: auth(input),
})
return {
id: ProviderID.make(provider),
model: (modelID: string | ModelID) => route.model({ id: modelID }),
configure,
}
}
export const provider = {
id,
configure,
}
export const model: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) => {
if (settings.apiKey !== undefined && settings.authToken !== undefined)
throw new Error("Anthropic-compatible apiKey cannot be combined with authToken")
return configure({
...(settings.authToken === undefined ? { apiKey: settings.apiKey } : { auth: Auth.bearer(settings.authToken) }),
baseURL: settings.baseURL,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
limits: settings.limits,
provider: settings.provider,
}).model(modelID)
}
export * as AnthropicCompatible from "./anthropic-compatible"
@@ -1,81 +0,0 @@
import type { ProviderPackage } from "../provider-package"
import { OpenAICompatibleChat } from "../protocols/openai-compatible-chat"
import type { RouteDefaultsInput } from "../route/client"
import { ProviderID, type ModelID, type ProviderOptions } from "../schema"
import { GoogleVertexShared } from "./google-vertex-shared"
export const id = ProviderID.make("google-vertex")
export type Config = RouteDefaultsInput &
GoogleVertexShared.OAuthOptions & {
readonly baseURL?: string
readonly location?: string
readonly project?: string
}
export interface Settings extends ProviderPackage.Settings {
readonly accessToken?: string
readonly apiKey?: never
readonly baseURL?: string
readonly location?: string
readonly project?: string
readonly providerOptions?: ProviderOptions
}
const route = OpenAICompatibleChat.route.with({
id: "google-vertex-chat",
provider: id,
})
export const routes = [route]
const configuredRoute = (input: Config) => {
if ("apiKey" in input && input.apiKey !== undefined) throw new Error("Google Vertex Chat does not support API keys")
const {
accessToken: _accessToken,
auth: _auth,
baseURL,
location: inputLocation,
project: inputProject,
...rest
} = input
const location = GoogleVertexShared.location(inputLocation, "global")
const project = GoogleVertexShared.project(inputProject)
return route.with({
...rest,
endpoint: {
baseURL:
baseURL ??
`https://aiplatform.googleapis.com/v1/projects/${GoogleVertexShared.requireProject(project)}/locations/${location}/endpoints/openapi`,
},
auth: GoogleVertexShared.oauth(input, project),
})
}
export const configure = (input: Config = {}) => {
const route = configuredRoute(input)
return {
id,
model: (modelID: string | ModelID) => route.model({ id: modelID }),
configure,
}
}
export const provider = {
id,
configure,
}
export const model: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) => {
if (settings.apiKey !== undefined) throw new Error("Google Vertex Chat does not support API keys")
return configure({
accessToken: settings.accessToken,
baseURL: settings.baseURL,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
limits: settings.limits,
location: settings.location,
project: settings.project,
providerOptions: settings.providerOptions,
}).model(modelID)
}
@@ -1,111 +0,0 @@
import { Effect, Schema, Struct } from "effect"
import type { ProviderPackage } from "../provider-package"
import { AnthropicMessages } from "../protocols/anthropic-messages"
import { Auth } from "../route/auth"
import { Route, type RouteDefaultsInput } from "../route/client"
import { Endpoint } from "../route/endpoint"
import { Framing } from "../route/framing"
import { Protocol } from "../route/protocol"
import { ProviderID, type ModelID, type ProviderOptions } from "../schema"
import { GoogleVertexShared } from "./google-vertex-shared"
const VERSION = "vertex-2023-10-16" as const
// models.dev uses this provider id even though the API contract is Anthropic Messages.
export const id = ProviderID.make("google-vertex-anthropic")
export type Config = RouteDefaultsInput &
GoogleVertexShared.OAuthOptions & {
readonly baseURL?: string
readonly location?: string
readonly project?: string
}
export interface Settings extends ProviderPackage.Settings {
readonly accessToken?: string
readonly apiKey?: never
readonly baseURL?: string
readonly location?: string
readonly project?: string
readonly providerOptions?: ProviderOptions
}
const route = Route.make({
id: "google-vertex-messages",
provider: id,
providerMetadataKey: "anthropic",
protocol: Protocol.make({
id: AnthropicMessages.protocol.id,
body: {
schema: Schema.Struct({
...Struct.omit(AnthropicMessages.AnthropicMessagesBody.fields, ["model"]),
anthropic_version: Schema.Literal(VERSION),
}),
from: (request) =>
AnthropicMessages.protocol.body.from(request).pipe(
Effect.map((body) => ({
...Struct.omit(body, ["model"]),
anthropic_version: VERSION,
})),
),
},
stream: AnthropicMessages.protocol.stream,
}),
endpoint: Endpoint.path(({ request }) => `/${request.model.id}:streamRawPredict`),
auth: Auth.none,
framing: Framing.sse,
})
export const routes = [route]
const configuredRoute = (input: Config) => {
if ("apiKey" in input && input.apiKey !== undefined)
throw new Error("Google Vertex Messages does not support API keys")
const {
accessToken: _accessToken,
auth: _auth,
baseURL,
location: inputLocation,
project: inputProject,
...rest
} = input
const location = GoogleVertexShared.location(inputLocation, "global")
const project = GoogleVertexShared.project(inputProject)
return route.with({
...rest,
endpoint: {
baseURL:
baseURL ??
`https://${GoogleVertexShared.host(location)}/v1/projects/${GoogleVertexShared.requireProject(project)}/locations/${location}/publishers/anthropic/models`,
},
auth: GoogleVertexShared.oauth(input, project),
})
}
export const configure = (input: Config = {}) => {
const route = configuredRoute(input)
return {
id,
model: (modelID: string | ModelID) => route.model({ id: modelID }),
configure,
}
}
export const provider = {
id,
configure,
}
export const model: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) => {
if (settings.apiKey !== undefined) throw new Error("Google Vertex Messages does not support API keys")
return configure({
accessToken: settings.accessToken,
baseURL: settings.baseURL,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
limits: settings.limits,
location: settings.location,
project: settings.project,
providerOptions: settings.providerOptions,
}).model(modelID)
}
@@ -1,82 +0,0 @@
import type { ProviderPackage } from "../provider-package"
import { OpenAICompatibleResponses } from "../protocols/openai-compatible-responses"
import type { RouteDefaultsInput } from "../route/client"
import { ProviderID, type ModelID, type ProviderOptions } from "../schema"
import { GoogleVertexShared } from "./google-vertex-shared"
export const id = ProviderID.make("google-vertex")
export type Config = RouteDefaultsInput &
GoogleVertexShared.OAuthOptions & {
readonly baseURL?: string
readonly location?: string
readonly project?: string
}
export interface Settings extends ProviderPackage.Settings {
readonly accessToken?: string
readonly apiKey?: never
readonly baseURL?: string
readonly location?: string
readonly project?: string
readonly providerOptions?: ProviderOptions
}
const route = OpenAICompatibleResponses.route.with({
id: "google-vertex-responses",
provider: id,
})
export const routes = [route]
const configuredRoute = (input: Config) => {
if ("apiKey" in input && input.apiKey !== undefined)
throw new Error("Google Vertex Responses does not support API keys")
const {
accessToken: _accessToken,
auth: _auth,
baseURL,
location: inputLocation,
project: inputProject,
...rest
} = input
const location = GoogleVertexShared.location(inputLocation, "global")
const project = GoogleVertexShared.project(inputProject)
return route.with({
...rest,
endpoint: {
baseURL:
baseURL ??
`https://aiplatform.googleapis.com/v1/projects/${GoogleVertexShared.requireProject(project)}/locations/${location}/endpoints/openapi`,
},
auth: GoogleVertexShared.oauth(input, project),
})
}
export const configure = (input: Config = {}) => {
const route = configuredRoute(input)
return {
id,
model: (modelID: string | ModelID) => route.model({ id: modelID }),
configure,
}
}
export const provider = {
id,
configure,
}
export const model: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) => {
if (settings.apiKey !== undefined) throw new Error("Google Vertex Responses does not support API keys")
return configure({
accessToken: settings.accessToken,
baseURL: settings.baseURL,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
limits: settings.limits,
location: settings.location,
project: settings.project,
providerOptions: settings.providerOptions,
}).model(modelID)
}
@@ -1,77 +0,0 @@
import type { AnyAuthClient } from "google-auth-library"
import { Effect, Redacted } from "effect"
import { Auth, MissingCredentialError } from "../route/auth"
const SCOPE = "https://www.googleapis.com/auth/cloud-platform"
export type OAuthOptions =
| { readonly accessToken?: string; readonly auth?: never }
| { readonly accessToken?: never; readonly auth?: Auth.Definition }
export type ApiKeyOptions =
| (OAuthOptions & { readonly apiKey?: never })
| { readonly accessToken?: never; readonly apiKey?: string; readonly auth?: never }
export const project = (value?: string) =>
value ??
process.env.GOOGLE_VERTEX_PROJECT ??
process.env.GOOGLE_CLOUD_PROJECT ??
process.env.GCP_PROJECT ??
process.env.GCLOUD_PROJECT
export const location = (value: string | undefined, fallback: string) =>
value ??
process.env.GOOGLE_VERTEX_LOCATION ??
process.env.GOOGLE_CLOUD_LOCATION ??
process.env.VERTEX_LOCATION ??
fallback
export const host = (location: string) => {
if (location === "global") return "aiplatform.googleapis.com"
// Jurisdictional multi-regions use Regional Endpoint Platform domains.
if (location === "eu" || location === "us") return `aiplatform.${location}.rep.googleapis.com`
return `${location}-aiplatform.googleapis.com`
}
export const requireProject = (value: string | undefined) => {
if (value) return value
throw new Error("Google Vertex requires a project when baseURL is not configured")
}
export const apiKey = (input: ApiKeyOptions) => {
if (input.apiKey !== undefined && (input.accessToken !== undefined || input.auth !== undefined))
throw new Error("Google Vertex apiKey cannot be combined with accessToken or auth")
if (input.accessToken !== undefined || input.auth !== undefined) return undefined
return input.apiKey ?? process.env.GOOGLE_VERTEX_API_KEY
}
const adc = (project?: string) => {
let client: Promise<AnyAuthClient> | undefined
const loadClient = () => {
if (client) return client
client = import("google-auth-library").then(({ GoogleAuth }) =>
new GoogleAuth({ projectId: project, scopes: [SCOPE] }).getClient(),
)
return client
}
return Auth.effect(
Effect.tryPromise({
try: async () => {
const token = await (await loadClient()).getAccessToken()
if (!token.token) throw new Error("Google ADC returned an empty access token")
return Redacted.make(token.token)
},
catch: () => new MissingCredentialError("Google Application Default Credentials"),
}),
).bearer()
}
export const oauth = (input: OAuthOptions, project?: string) => {
if (input.accessToken !== undefined && input.auth !== undefined)
throw new Error("Google Vertex accessToken cannot be combined with auth")
if (input.auth) return input.auth
if (input.accessToken !== undefined) return Auth.bearer(input.accessToken)
return adc(project)
}
export * as GoogleVertexShared from "./google-vertex-shared"
@@ -1,98 +0,0 @@
import type { ProviderPackage } from "../provider-package"
import { Gemini } from "../protocols/gemini"
import { Auth } from "../route/auth"
import { Route, type RouteDefaultsInput } from "../route/client"
import { Endpoint } from "../route/endpoint"
import { Framing } from "../route/framing"
import { ProviderID, type ModelID, type ProviderOptions } from "../schema"
import { GoogleVertexShared } from "./google-vertex-shared"
export const id = ProviderID.make("google-vertex")
export type Config = RouteDefaultsInput &
GoogleVertexShared.ApiKeyOptions & {
readonly baseURL?: string
readonly location?: string
readonly project?: string
}
export type Settings = ProviderPackage.Settings &
(
| { readonly accessToken?: string; readonly apiKey?: never }
| { readonly accessToken?: never; readonly apiKey?: string }
) & {
readonly baseURL?: string
readonly location?: string
readonly project?: string
readonly providerOptions?: ProviderOptions
}
const route = Route.make({
id: "google-vertex-gemini",
provider: id,
providerMetadataKey: "google",
protocol: Gemini.protocol,
endpoint: Endpoint.path(({ request }) => {
const model = String(request.model.id)
return `/${model.startsWith("endpoints/") ? model : `models/${model}`}:streamGenerateContent?alt=sse`
}),
auth: Auth.none,
framing: Framing.sse,
})
export const routes = [route]
const configuredRoute = (input: Config, modelID: string | ModelID) => {
const {
accessToken: _accessToken,
apiKey: _apiKey,
auth: _auth,
baseURL,
location: inputLocation,
project: inputProject,
...rest
} = input
const apiKey = GoogleVertexShared.apiKey(input)
const endpointModel = String(modelID).startsWith("endpoints/")
if (apiKey !== undefined && endpointModel)
throw new Error("Google Vertex tuned models do not support Express Mode API keys")
const location = GoogleVertexShared.location(inputLocation, "us-central1")
const project = GoogleVertexShared.project(inputProject)
const endpoint =
baseURL ??
(apiKey
? "https://aiplatform.googleapis.com/v1/publishers/google"
: `https://${GoogleVertexShared.host(location)}/v1beta1/projects/${GoogleVertexShared.requireProject(project)}/locations/${location}${endpointModel ? "" : "/publishers/google"}`)
return route.with({
...rest,
endpoint: { baseURL: endpoint },
auth: apiKey === undefined ? GoogleVertexShared.oauth(input, project) : Auth.header("x-goog-api-key", apiKey),
})
}
export const configure = (input: Config = {}) => {
return {
id,
model: (modelID: string | ModelID) => configuredRoute(input, modelID).model({ id: modelID }),
configure,
}
}
export const provider = {
id,
configure,
}
export const model: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) => {
if (settings.apiKey !== undefined && settings.accessToken !== undefined)
throw new Error("Google Vertex apiKey cannot be combined with accessToken or auth")
return configure({
...(settings.apiKey === undefined ? { accessToken: settings.accessToken } : { apiKey: settings.apiKey }),
baseURL: settings.baseURL,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
limits: settings.limits,
location: settings.location,
project: settings.project,
providerOptions: settings.providerOptions,
}).model(modelID)
}
@@ -1,2 +0,0 @@
export { model } from "../google-vertex-chat"
export type { Settings } from "../google-vertex-chat"
@@ -1,2 +0,0 @@
export { model } from "../google-vertex"
export type { Settings } from "../google-vertex"
@@ -1,2 +0,0 @@
export { model } from "../google-vertex-messages"
export type { Settings } from "../google-vertex-messages"
@@ -1,2 +0,0 @@
export { model } from "../google-vertex-responses"
export type { Settings } from "../google-vertex-responses"
@@ -1 +0,0 @@
export * from "../openai-compatible-responses"
-1
View File
@@ -1 +0,0 @@
export * from "./route/index"
-296
View File
@@ -1,296 +0,0 @@
import { describe, expect, test } from "bun:test"
import { model } from "@opencode-ai/ai/providers/openai"
describe("provider package entrypoints", () => {
test("semantic API aliases expose the same contract", async () => {
const modules = await Promise.all([
import("@opencode-ai/ai/providers/openai"),
import("@opencode-ai/ai/providers/openai/responses"),
import("@opencode-ai/ai/providers/openai/chat"),
import("@opencode-ai/ai/providers/anthropic"),
import("@opencode-ai/ai/providers/anthropic-compatible"),
import("@opencode-ai/ai/providers/openai-compatible"),
import("@opencode-ai/ai/providers/openai-compatible/responses"),
import("@opencode-ai/ai/providers/amazon-bedrock"),
import("@opencode-ai/ai/providers/azure"),
import("@opencode-ai/ai/providers/azure/responses"),
import("@opencode-ai/ai/providers/azure/chat"),
import("@opencode-ai/ai/providers/google"),
import("@opencode-ai/ai/providers/google-vertex"),
import("@opencode-ai/ai/providers/google-vertex/gemini"),
import("@opencode-ai/ai/providers/google-vertex/chat"),
import("@opencode-ai/ai/providers/google-vertex/responses"),
import("@opencode-ai/ai/providers/google-vertex/messages"),
])
for (const module of modules) expect(module.model).toBeFunction()
expect(modules[0].model).toBe(modules[1].model)
expect(modules[8].model).toBe(modules[9].model)
expect(modules[12].model).toBe(modules[13].model)
})
test("maps package settings onto the executable model", () => {
const selected = model("gpt-5", {
apiKey: "fixture",
baseURL: "https://api.openai.test/v1",
headers: { "x-application": "opencode" },
body: { service_tier: "priority" },
limits: { context: 200_000, output: 64_000 },
unrelatedInheritedSetting: true,
})
expect(selected.route.id).toBe("openai-responses")
expect(selected.route.defaults.headers).toEqual({ "x-application": "opencode" })
expect(selected.route.defaults.http?.body).toEqual({ service_tier: "priority" })
expect(selected.route.defaults.limits).toEqual({ context: 200_000, output: 64_000 })
})
test("selects transport without changing the semantic API", () => {
expect(model("gpt-5", { apiKey: "fixture" }).route.id).toBe("openai-responses")
expect(model("gpt-5", { apiKey: "fixture", transport: "websocket" }).route.id).toBe("openai-responses-websocket")
})
test("maps OpenAI-compatible Responses settings onto the executable model", async () => {
const OpenAICompatibleResponses = await import("@opencode-ai/ai/providers/openai-compatible/responses")
const selected = OpenAICompatibleResponses.model("custom-model", {
apiKey: "fixture",
baseURL: "https://responses.example.test/v1",
provider: "example",
headers: { "x-application": "opencode" },
body: { service_tier: "priority" },
limits: { context: 200_000, output: 64_000 },
providerOptions: { openai: { reasoningEffort: "low", store: true } },
})
expect(String(selected.provider)).toBe("example")
expect(selected.route.id).toBe("openai-compatible-responses")
expect(selected.route.endpoint).toMatchObject({
baseURL: "https://responses.example.test/v1",
path: "/responses",
})
expect(selected.route.defaults.headers).toEqual({ "x-application": "opencode" })
expect(selected.route.defaults.http?.body).toEqual({ service_tier: "priority" })
expect(selected.route.defaults.limits).toEqual({ context: 200_000, output: 64_000 })
expect(selected.route.defaults.providerOptions).toEqual({
openai: { reasoningEffort: "low", store: true },
})
})
test("maps Anthropic-compatible settings onto the executable model", async () => {
const AnthropicCompatible = await import("@opencode-ai/ai/providers/anthropic-compatible")
const selected = AnthropicCompatible.model("compatible-model", {
apiKey: "fixture",
baseURL: "https://messages.example.test/v1",
provider: "example",
headers: { "x-application": "opencode" },
body: { metadata: { user_id: "user_1" } },
limits: { context: 200_000, output: 64_000 },
})
expect(String(selected.provider)).toBe("example")
expect(selected.route.id).toBe("anthropic-messages")
expect(selected.route.endpoint).toMatchObject({
baseURL: "https://messages.example.test/v1",
path: "/messages",
})
expect(selected.route.defaults.headers).toEqual({ "x-application": "opencode" })
expect(selected.route.defaults.http?.body).toEqual({ metadata: { user_id: "user_1" } })
expect(selected.route.defaults.limits).toEqual({ context: 200_000, output: 64_000 })
})
test("requires an Anthropic-compatible base URL at runtime", async () => {
const AnthropicCompatible = await import("@opencode-ai/ai/providers/anthropic-compatible")
expect(() =>
Reflect.apply(AnthropicCompatible.model, undefined, ["compatible-model", { apiKey: "fixture" }]),
).toThrow("Anthropic-compatible providers require a baseURL")
})
test("rejects conflicting Anthropic-compatible auth settings at runtime", async () => {
const Anthropic = await import("@opencode-ai/ai/providers/anthropic")
const AnthropicCompatible = await import("@opencode-ai/ai/providers/anthropic-compatible")
expect(() =>
Reflect.apply(AnthropicCompatible.model, undefined, [
"compatible-model",
{
apiKey: "fixture",
authToken: "token",
baseURL: "https://messages.example.test/v1",
},
]),
).toThrow("Anthropic-compatible apiKey cannot be combined with authToken")
expect(() =>
Reflect.apply(Anthropic.model, undefined, ["claude-sonnet-4-6", { apiKey: "fixture", authToken: "token" }]),
).toThrow("Anthropic apiKey cannot be combined with authToken")
})
test("maps legacy OpenAI organization and project settings to headers", () => {
const selected = model("gpt-5", {
apiKey: "fixture",
organization: "org_123",
project: "proj_123",
})
expect(selected.route.defaults.headers).toMatchObject({
"OpenAI-Organization": "org_123",
"OpenAI-Project": "proj_123",
})
})
test("selects Azure API entrypoints with the same model contract", async () => {
const Azure = await import("@opencode-ai/ai/providers/azure")
const AzureChat = await import("@opencode-ai/ai/providers/azure/chat")
const AzureResponses = await import("@opencode-ai/ai/providers/azure/responses")
const settings = {
apiKey: "fixture",
resourceName: "opencode-test",
headers: { "x-application": "opencode" },
body: { service_tier: "priority" },
limits: { context: 200_000, output: 64_000 },
}
const responses = AzureResponses.model("deployment", settings)
const chat = AzureChat.model("deployment", settings)
expect(Azure.model("deployment", settings).route.id).toBe("azure-openai-responses")
expect(responses.route.id).toBe("azure-openai-responses")
expect(responses.route.endpoint.baseURL).toBe("https://opencode-test.openai.azure.com/openai/v1")
expect(responses.route.defaults.headers).toEqual({ "x-application": "opencode" })
expect(responses.route.defaults.http?.body).toEqual({ service_tier: "priority" })
expect(responses.route.defaults.limits).toEqual({ context: 200_000, output: 64_000 })
expect(chat.route.id).toBe("azure-openai-chat")
})
test("maps Google package settings onto the Gemini model", async () => {
const Google = await import("@opencode-ai/ai/providers/google")
const selected = Google.model("gemini-2.5-flash", {
apiKey: "fixture",
baseURL: "https://generativelanguage.test/v1beta",
headers: { "x-application": "opencode" },
body: { safetySettings: [] },
limits: { context: 1_000_000, output: 65_536 },
providerOptions: { gemini: { thinkingConfig: { thinkingBudget: 1_024 } } },
})
expect(selected.route.id).toBe("gemini")
expect(selected.route.endpoint.baseURL).toBe("https://generativelanguage.test/v1beta")
expect(selected.route.defaults.headers).toEqual({ "x-application": "opencode" })
expect(selected.route.defaults.http?.body).toEqual({ safetySettings: [] })
expect(selected.route.defaults.limits).toEqual({ context: 1_000_000, output: 65_536 })
expect(selected.route.defaults.providerOptions).toEqual({
gemini: { thinkingConfig: { thinkingBudget: 1_024 } },
})
})
test("selects Vertex entrypoints with the same model contract", async () => {
const GoogleVertex = await import("@opencode-ai/ai/providers/google-vertex")
const GoogleVertexGemini = await import("@opencode-ai/ai/providers/google-vertex/gemini")
const GoogleVertexChat = await import("@opencode-ai/ai/providers/google-vertex/chat")
const GoogleVertexResponses = await import("@opencode-ai/ai/providers/google-vertex/responses")
const GoogleVertexMessages = await import("@opencode-ai/ai/providers/google-vertex/messages")
const gemini = GoogleVertex.model("gemini-3.5-flash", {
apiKey: "fixture",
headers: { "x-application": "opencode" },
body: { safetySettings: [] },
limits: { context: 1_000_000, output: 65_536 },
})
const messages = GoogleVertexMessages.model("claude-sonnet-4-6", {
accessToken: "fixture",
location: "global",
project: "vertex-project",
})
const chat = GoogleVertexChat.model("deepseek-ai/deepseek-v3.2-maas", {
accessToken: "fixture",
location: "global",
project: "vertex-project",
})
const responses = GoogleVertexResponses.model("xai/grok-4.20-reasoning", {
accessToken: "fixture",
location: "global",
project: "vertex-project",
})
expect(GoogleVertexGemini.model).toBe(GoogleVertex.model)
expect(gemini.route.id).toBe("google-vertex-gemini")
expect(gemini.route.protocol).toBe("gemini")
expect(gemini.route.endpoint.baseURL).toBe("https://aiplatform.googleapis.com/v1/publishers/google")
expect(gemini.route.defaults.headers).toEqual({ "x-application": "opencode" })
expect(gemini.route.defaults.http?.body).toEqual({ safetySettings: [] })
expect(gemini.route.defaults.limits).toEqual({ context: 1_000_000, output: 65_536 })
expect(
GoogleVertex.model("gemini-3.5-flash", {
accessToken: "fixture",
location: "eu",
project: "vertex-project",
}).route.endpoint.baseURL,
).toBe("https://aiplatform.eu.rep.googleapis.com/v1beta1/projects/vertex-project/locations/eu/publishers/google")
expect(messages.route.id).toBe("google-vertex-messages")
expect(messages.route.protocol).toBe("anthropic-messages")
expect(messages.route.endpoint.baseURL).toBe(
"https://aiplatform.googleapis.com/v1/projects/vertex-project/locations/global/publishers/anthropic/models",
)
expect(chat.route.id).toBe("google-vertex-chat")
expect(chat.route.protocol).toBe("openai-chat")
expect(chat.route.endpoint).toMatchObject({
baseURL: "https://aiplatform.googleapis.com/v1/projects/vertex-project/locations/global/endpoints/openapi",
path: "/chat/completions",
})
expect(responses.route.id).toBe("google-vertex-responses")
expect(responses.route.protocol).toBe("openai-responses")
expect(responses.route.endpoint).toMatchObject({
baseURL: "https://aiplatform.googleapis.com/v1/projects/vertex-project/locations/global/endpoints/openapi",
path: "/responses",
})
expect(responses.route.defaults.providerOptions).toEqual({ openai: { store: false } })
})
test("rejects conflicting Vertex auth settings at runtime", async () => {
const GoogleVertex = await import("@opencode-ai/ai/providers/google-vertex")
const GoogleVertexChat = await import("@opencode-ai/ai/providers/google-vertex/chat")
const GoogleVertexMessages = await import("@opencode-ai/ai/providers/google-vertex/messages")
const GoogleVertexResponses = await import("@opencode-ai/ai/providers/google-vertex/responses")
const Providers = await import("@opencode-ai/ai/providers")
expect(() =>
Reflect.apply(GoogleVertex.model, undefined, [
"gemini-3.5-flash",
{ accessToken: "token", apiKey: "fixture", project: "vertex-project" },
]),
).toThrow("Google Vertex apiKey cannot be combined with accessToken or auth")
const configured = Reflect.apply(GoogleVertex.configure, undefined, [
{ accessToken: "token", auth: {}, project: "vertex-project" },
])
expect(() => configured.model("gemini-3.5-flash")).toThrow("Google Vertex accessToken cannot be combined with auth")
expect(() =>
Reflect.apply(GoogleVertexMessages.model, undefined, [
"claude-sonnet-4-6",
{ apiKey: "fixture", project: "vertex-project" },
]),
).toThrow("Google Vertex Messages does not support API keys")
expect(() =>
Reflect.apply(Providers.GoogleVertexMessages.configure, undefined, [
{ apiKey: "fixture", project: "vertex-project" },
]),
).toThrow("Google Vertex Messages does not support API keys")
expect(() =>
Reflect.apply(GoogleVertexChat.model, undefined, [
"deepseek-ai/deepseek-v3.2-maas",
{ apiKey: "fixture", project: "vertex-project" },
]),
).toThrow("Google Vertex Chat does not support API keys")
expect(() =>
Reflect.apply(Providers.GoogleVertexChat.configure, undefined, [
{ apiKey: "fixture", project: "vertex-project" },
]),
).toThrow("Google Vertex Chat does not support API keys")
expect(() =>
Reflect.apply(GoogleVertexResponses.model, undefined, [
"xai/grok-4.20-reasoning",
{ apiKey: "fixture", project: "vertex-project" },
]),
).toThrow("Google Vertex Responses does not support API keys")
expect(() =>
Reflect.apply(Providers.GoogleVertexResponses.configure, undefined, [
{ apiKey: "fixture", project: "vertex-project" },
]),
).toThrow("Google Vertex Responses does not support API keys")
})
})
@@ -1,246 +0,0 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { HttpClientRequest } from "effect/unstable/http"
import { LLM } from "../../src"
import { GoogleVertex, GoogleVertexChat, GoogleVertexMessages, GoogleVertexResponses } from "../../src/providers"
import { LLMClient } from "../../src/route"
import { it } from "../lib/effect"
import { dynamicResponse } from "../lib/http"
import { deltaChunk, finishChunk } from "../lib/openai-chunks"
import { sseEvents } from "../lib/sse"
describe("Google Vertex providers", () => {
it.effect("sends Gemini requests to the global Vertex endpoint", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(
LLM.request({
model: GoogleVertex.configure({
accessToken: "vertex-token",
location: "global",
project: "vertex-project",
}).model("gemini-3.5-flash"),
prompt: "Say hello.",
}),
).pipe(
Effect.provide(
dynamicResponse((input) =>
Effect.gen(function* () {
const request = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
expect(request.url).toBe(
"https://aiplatform.googleapis.com/v1beta1/projects/vertex-project/locations/global/publishers/google/models/gemini-3.5-flash:streamGenerateContent?alt=sse",
)
expect(request.headers.get("authorization")).toBe("Bearer vertex-token")
expect(yield* Effect.promise(() => request.json())).toMatchObject({
contents: [{ role: "user", parts: [{ text: "Say hello." }] }],
})
return input.respond(
sseEvents({
candidates: [
{
content: { role: "model", parts: [{ text: "Hello." }] },
finishReason: "STOP",
},
],
}),
{ headers: { "content-type": "text/event-stream" } },
)
}),
),
),
)
expect(response.text).toBe("Hello.")
}),
)
it.effect("projects Anthropic Messages onto the Vertex raw-predict API", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(
LLM.request({
model: GoogleVertexMessages.configure({
accessToken: "vertex-token",
location: "eu",
project: "vertex-project",
}).model("claude-sonnet-4-6"),
prompt: "Say hello.",
}),
).pipe(
Effect.provide(
dynamicResponse((input) =>
Effect.gen(function* () {
const request = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
expect(request.url).toBe(
"https://aiplatform.eu.rep.googleapis.com/v1/projects/vertex-project/locations/eu/publishers/anthropic/models/claude-sonnet-4-6:streamRawPredict",
)
expect(request.headers.get("authorization")).toBe("Bearer vertex-token")
expect(request.headers.get("anthropic-version")).toBeNull()
const body = yield* Effect.promise(() => request.json())
expect(body).toMatchObject({
anthropic_version: "vertex-2023-10-16",
messages: [{ role: "user", content: [{ type: "text", text: "Say hello." }] }],
stream: true,
})
expect(body).not.toHaveProperty("model")
return input.respond(
sseEvents(
{ type: "content_block_start", index: 0, content_block: { type: "text", text: "" } },
{ type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "Hello." } },
{ type: "content_block_stop", index: 0 },
{ type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 2 } },
{ type: "message_stop" },
),
{ headers: { "content-type": "text/event-stream" } },
)
}),
),
),
)
expect(response.text).toBe("Hello.")
}),
)
it.effect("sends MaaS requests through Vertex Chat Completions", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(
LLM.request({
model: GoogleVertexChat.configure({
accessToken: "vertex-token",
location: "global",
project: "vertex-project",
}).model("deepseek-ai/deepseek-v3.2-maas"),
prompt: "Say hello.",
}),
).pipe(
Effect.provide(
dynamicResponse((input) =>
Effect.gen(function* () {
const request = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
expect(request.url).toBe(
"https://aiplatform.googleapis.com/v1/projects/vertex-project/locations/global/endpoints/openapi/chat/completions",
)
expect(request.headers.get("authorization")).toBe("Bearer vertex-token")
expect(yield* Effect.promise(() => request.json())).toMatchObject({
model: "deepseek-ai/deepseek-v3.2-maas",
messages: [{ role: "user", content: "Say hello." }],
stream: true,
stream_options: { include_usage: true },
})
return input.respond(sseEvents(deltaChunk({ content: "Hello." }), finishChunk("stop")), {
headers: { "content-type": "text/event-stream" },
})
}),
),
),
)
expect(response.text).toBe("Hello.")
}),
)
it.effect("sends Grok requests through Vertex Responses", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(
LLM.request({
model: GoogleVertexResponses.configure({
accessToken: "vertex-token",
location: "global",
project: "vertex-project",
}).model("xai/grok-4.20-reasoning"),
prompt: "Say hello.",
}),
).pipe(
Effect.provide(
dynamicResponse((input) =>
Effect.gen(function* () {
const request = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
expect(request.url).toBe(
"https://aiplatform.googleapis.com/v1/projects/vertex-project/locations/global/endpoints/openapi/responses",
)
expect(request.headers.get("authorization")).toBe("Bearer vertex-token")
expect(yield* Effect.promise(() => request.json())).toMatchObject({
model: "xai/grok-4.20-reasoning",
input: [{ role: "user", content: [{ type: "input_text", text: "Say hello." }] }],
store: false,
stream: true,
})
return input.respond(
sseEvents(
{ type: "response.output_text.delta", item_id: "msg_1", delta: "Hello." },
{ type: "response.completed", response: { id: "resp_1" } },
),
{ headers: { "content-type": "text/event-stream" } },
)
}),
),
),
)
expect(response.text).toBe("Hello.")
}),
)
it.effect("protects the Vertex Messages API version from body overlays", () =>
Effect.gen(function* () {
const error = yield* LLMClient.prepare(
LLM.request({
model: GoogleVertexMessages.configure({
accessToken: "vertex-token",
http: { body: { anthropic_version: "wrong" } },
project: "vertex-project",
}).model("claude-sonnet-4-6"),
prompt: "Say hello.",
}),
).pipe(Effect.flip)
expect(error.message).toContain("http.body cannot overlay protocol-owned field(s): anthropic_version")
}),
)
it.effect("routes tuned Gemini models through their deployed endpoint", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(
LLM.request({
model: GoogleVertex.configure({
accessToken: "vertex-token",
location: "us-central1",
project: "vertex-project",
}).model("endpoints/1234567890"),
prompt: "Say hello.",
}),
).pipe(
Effect.provide(
dynamicResponse((input) =>
Effect.gen(function* () {
const request = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
expect(request.url).toBe(
"https://us-central1-aiplatform.googleapis.com/v1beta1/projects/vertex-project/locations/us-central1/endpoints/1234567890:streamGenerateContent?alt=sse",
)
return input.respond(
sseEvents({
candidates: [
{
content: { role: "model", parts: [{ text: "Hello." }] },
finishReason: "STOP",
},
],
}),
{ headers: { "content-type": "text/event-stream" } },
)
}),
),
),
)
expect(response.text).toBe("Hello.")
}),
)
it.effect("rejects tuned Gemini models in express mode", () =>
Effect.sync(() => {
expect(() => GoogleVertex.configure({ apiKey: "fixture" }).model("endpoints/1234567890")).toThrow(
"Google Vertex tuned models do not support Express Mode API keys",
)
}),
)
})
-8
View File
@@ -1,8 +0,0 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./tsconfig.json",
"compilerOptions": {
"allowImportingTsExtensions": false,
"noEmit": false
}
}
-12
View File
@@ -1,12 +0,0 @@
{
"$schema": "https://json.schemastore.org/tsconfig.json",
"extends": "@tsconfig/bun/tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"declaration": true,
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"noUncheckedIndexedAccess": false
},
"include": ["src"]
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.18.2",
"version": "1.17.20",
"description": "",
"type": "module",
"exports": {
+12 -13
View File
@@ -32,6 +32,8 @@ const triggerClass =
const showPopover = () => true
export function HelpButton() {
if (import.meta.env.VITE_OPENCODE_CHANNEL !== "dev") return null
const platform = usePlatform()
return (
@@ -54,7 +56,6 @@ export function HelpButton() {
// can remove this after the tabs rollout has been out for a while
export function TabsInfoPopup() {
const settings = useSettings()
const platform = usePlatform()
const [drawerOpen, setDrawerOpen] = createSignal(false)
return (
@@ -113,18 +114,16 @@ export function TabsInfoPopup() {
<DrawerContent>
<div class="flex h-[52px] w-full shrink-0 items-center gap-4 self-stretch border-b border-v2-border-border-muted p-4">
<p class="min-h-0 min-w-0 flex-1 text-[13px] font-[530] leading-5 tracking-[-0.04px] tabular-nums text-v2-text-text-muted">
July 14
June 16
</p>
<Show when={platform.platform !== "desktop" || platform.os !== "windows"}>
<DrawerClose
as={IconButtonV2}
type="button"
size="small"
variant="ghost-muted"
aria-label="Close"
icon={<IconV2 name="xmark-small" />}
/>
</Show>
<DrawerClose
as={IconButtonV2}
type="button"
size="small"
variant="ghost-muted"
aria-label="Close"
icon={<IconV2 name="xmark-small" />}
/>
</div>
<div class="relative flex min-h-0 w-full flex-1 flex-col items-start gap-6 overflow-y-auto p-8">
<p class="w-full shrink-0 self-stretch text-[21px] font-[610] leading-6 tracking-[-0.37px] tabular-nums text-v2-text-text-base">
@@ -148,7 +147,7 @@ export function TabsInfoPopup() {
<p>When you reopen the app, your tabs are still open.</p>
<p>
The new design does not support Git Worktrees yet, it's coming soon. So if you'd prefer to continue using
the previous layout, you can switch between layouts in Settings. Just keep in mind that the new layout
the previous layout , you can switch between layouts in Settings. Just keep in mind that the new layout
will become permanent in a few weeks.
</p>
</div>
@@ -371,7 +371,7 @@
transform: translateY(-50%);
}
.settings-v2-models {
.settings-v2-tab-body.settings-v2-models {
gap: 24px;
}
+1 -1
View File
@@ -392,7 +392,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
id: "tab.new",
category: "tab",
title: language.t("command.session.new"),
keybind: "mod+t,mod+n",
keybind: "mod+t",
hidden: true,
onSelect: openNewTab,
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/cli",
"version": "1.18.2",
"version": "1.17.18",
"type": "module",
"license": "MIT",
"bin": {
+15 -6
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bun
import { Service } from "@opencode-ai/client/effect/service"
import { Service } from "@opencode-ai/client/effect"
import { ServiceStatus } from "@opencode-ai/protocol/groups/health"
import { Schema } from "effect"
import fs from "node:fs/promises"
@@ -37,7 +37,8 @@ try {
const headers = { authorization: "Basic " + credential }
const token = encodeURIComponent(credential)
const health = await waitForReady(info.url, headers)
if (health.pid !== info.pid) throw new Error("Health process does not match registration")
if (health.pid !== info.pid || health.instanceID !== info.id)
throw new Error("Health identity does not match registration")
const tokenHealth = await fetch(
new URL(`/api/health?auth_token=${token}`, info.url),
{ signal: AbortSignal.timeout(5_000) },
@@ -74,7 +75,7 @@ try {
await fetch(new URL("/api/service/stop", info.url), {
method: "POST",
headers: { ...headers, "content-type": "application/json" },
body: JSON.stringify({ instanceID: info.id }),
body: JSON.stringify({ instanceID: info.id, targetVersion: "smoke-next" }),
signal: AbortSignal.timeout(5_000),
}).then((response) => response.json()),
)
@@ -119,11 +120,19 @@ async function waitForRegistration() {
async function waitForReady(url: string, headers: HeadersInit) {
const deadline = Date.now() + 20_000
while (Date.now() < deadline) {
const response = await fetch(new URL("/api/health", url), {
const health = await fetch(new URL("/api/health", url), {
headers,
signal: AbortSignal.timeout(1_000),
}).catch(() => undefined)
if (response?.ok) return Schema.decodeUnknownPromise(ServiceStatus.Health)(await response.json())
})
.then((response) => response.json())
.then(Schema.decodeUnknownPromise(ServiceStatus.Health))
.catch(() => undefined)
if (health === undefined) {
await Bun.sleep(25)
continue
}
if (health.status.type === "ready") return health
if (health.status.type === "failed") throw new Error(health.status.message)
await Bun.sleep(25)
}
throw new Error("Compiled service did not become ready")
+4 -12
View File
@@ -66,17 +66,6 @@ export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCO
}),
],
}),
Spec.make("auth", {
description: "Manage authentication",
commands: [
Spec.make("connect", {
description: "Connect to a wellknown authentication provider",
params: {
url: Argument.string("url").pipe(Argument.withDescription("Wellknown provider URL")),
},
}),
],
}),
Spec.make("mcp", {
description: "Manage MCP (Model Context Protocol) servers",
commands: [
@@ -189,7 +178,10 @@ export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCO
Flag.atMost(100),
),
title: Flag.string("title").pipe(Flag.withDescription("Session title"), Flag.optional),
thinking: Flag.boolean("thinking").pipe(Flag.withDescription("Show thinking blocks"), Flag.withDefault(false)),
thinking: Flag.boolean("thinking").pipe(
Flag.withDescription("Show thinking blocks"),
Flag.withDefault(false),
),
auto: Flag.boolean("auto").pipe(
Flag.withDescription("Auto-approve permissions that are not explicitly denied"),
Flag.withDefault(false),
+8 -4
View File
@@ -2,8 +2,8 @@ import { EOL } from "node:os"
import { Effect, Option } from "effect"
import { Commands } from "../commands"
import { Runtime } from "../../framework/runtime"
import { Service, type Endpoint } from "@opencode-ai/client/effect/service"
import { ServerConnection } from "../../services/server-connection"
import { Service } from "@opencode-ai/client/effect"
import { Server } from "../../services/server"
const methods = new Set(["delete", "get", "head", "options", "patch", "post", "put"])
@@ -18,7 +18,7 @@ type OpenApi = {
export default Runtime.handler(
Commands.commands.api,
Effect.fn("cli.api")(function* (input) {
const server = yield* ServerConnection.resolve({
const server = yield* Server.resolve({
server: Option.getOrUndefined(input.server),
standalone: input.standalone,
mismatch: "ignore",
@@ -62,7 +62,11 @@ export function rawRequest(input: readonly string[]) {
return { method: input[0].toUpperCase(), path: input[1] }
}
function resolveRequest(endpoint: Endpoint, input: readonly string[], params: Record<string, string>) {
function resolveRequest(
endpoint: Service.Endpoint,
input: readonly string[],
params: Record<string, string>,
) {
const raw = rawRequest(input)
if (raw) return Effect.succeed(raw)
if (input.length !== 1) return Effect.fail(new Error("Expected an operation name or an HTTP method and path"))
@@ -1,52 +0,0 @@
import { EOL } from "node:os"
import { Effect } from "effect"
import { Service } from "@opencode-ai/client/effect/service"
import { OpenCode, type IntegrationCommandStatusOutput, type OpenCodeClient } from "@opencode-ai/client/promise"
import { Commands } from "../../commands"
import { Runtime } from "../../../framework/runtime"
import { ServiceConfig } from "../../../services/service-config"
const location = { directory: process.cwd() }
export default Runtime.handler(
Commands.commands.auth.commands.connect,
Effect.fn("cli.auth.connect")(function* (input) {
process.stdout.write("Connecting..." + EOL + EOL)
const endpoint = yield* Service.ensure(yield* ServiceConfig.options())
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
yield* request(() => client.integration.wellknown.add({ url: input.url, location }))
const integrationID = input.url.replace(/\/+$/, "")
const started = yield* request(() =>
client.integration.command.connect({ integrationID, methodID: "login", location }),
)
yield* Effect.addFinalizer(() =>
request(() =>
client.integration.command.cancel({ integrationID, attemptID: started.data.attemptID, location }),
).pipe(Effect.ignore),
)
const status = yield* wait(client, integrationID, started.data.attemptID)
if (status.status === "failed") return yield* Effect.fail(new Error(status.message))
if (status.status === "expired") return yield* Effect.fail(new Error("Authentication expired"))
process.stdout.write("Connected" + EOL)
}),
)
const wait = (
client: OpenCodeClient,
integrationID: string,
attemptID: string,
shown = false,
): Effect.Effect<Exclude<IntegrationCommandStatusOutput["data"], { status: "pending" }>, unknown> =>
Effect.gen(function* () {
const response = yield* request(() => client.integration.command.status({ integrationID, attemptID, location }))
if (response.data.status !== "pending") return response.data
const output = response.data.message?.trim()
if (!shown && output) process.stdout.write(output + EOL + EOL)
yield* Effect.sleep(500)
return yield* wait(client, integrationID, attemptID, shown || !!output)
})
function request<A>(task: () => Promise<A>) {
return Effect.tryPromise({ try: task, catch: (cause) => cause })
}
@@ -1,5 +1,5 @@
import { Cause, Effect, Exit, Option } from "effect"
import { Service } from "@opencode-ai/client/effect/service"
import { Service } from "@opencode-ai/client/effect"
import { OpenCode, type OpenCodeClient } from "@opencode-ai/client/promise"
import { AppProcess } from "@opencode-ai/core/process"
import { Commands } from "../../commands"
@@ -35,7 +35,7 @@ const login = Effect.fn("cli.console.login.run")(function* (timeline: TimelineHo
yield* request(() => timeline.intro("Log in"))
yield* request(() => timeline.pending("Connecting to OpenCode..."))
const endpoint = yield* Service.ensure(yield* ServiceConfig.options())
const endpoint = yield* Service.start(yield* ServiceConfig.options())
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
const found = yield* request((signal) => client.integration.get({ integrationID, location }, { signal }))
const integration = yield* required(found.data, "OpenCode Console integration is unavailable")
@@ -46,7 +46,7 @@ const login = Effect.fn("cli.console.login.run")(function* (timeline: TimelineHo
yield* request(() => timeline.pending("Starting authorization..."))
const started = yield* request((signal) =>
client.integration.oauth.connect(
client.integration.connect.oauth(
{
integrationID,
methodID: method.id,
@@ -59,8 +59,8 @@ const login = Effect.fn("cli.console.login.run")(function* (timeline: TimelineHo
const attempt = started.data
yield* Effect.addFinalizer(() =>
request(() =>
client.integration.oauth.cancel(
{ integrationID, attemptID: attempt.attemptID, location },
client.integration.attempt.cancel(
{ attemptID: attempt.attemptID, location },
{ signal: AbortSignal.timeout(5_000) },
),
).pipe(Effect.ignore),
@@ -75,7 +75,7 @@ const login = Effect.fn("cli.console.login.run")(function* (timeline: TimelineHo
}).pipe(Effect.ignore)
yield* request(() => timeline.pending("Waiting for authorization..."))
const status = yield* waitForConsoleLogin(client, integrationID, attempt.attemptID)
const status = yield* waitForConsoleLogin(client, attempt.attemptID)
if (status.status === "failed") yield* Effect.fail(new Error(status.message))
if (status.status === "expired") yield* Effect.fail(new Error("Device code expired"))
@@ -85,12 +85,11 @@ const login = Effect.fn("cli.console.login.run")(function* (timeline: TimelineHo
const waitForConsoleLogin = Effect.fn("cli.console.login.wait")(function* (
client: OpenCodeClient,
integrationID: string,
attemptID: string,
) {
while (true) {
const response = yield* request((signal) =>
client.integration.oauth.status({ integrationID, attemptID, location }, { signal }),
client.integration.attempt.status({ attemptID, location }, { signal }),
)
if (response.data.status !== "pending") return response.data
yield* Effect.sleep(500)
@@ -3,7 +3,7 @@ import { Effect } from "effect"
import { OpenCode } from "@opencode-ai/client"
import { Commands } from "../../commands"
import { Runtime } from "../../../framework/runtime"
import { Service } from "@opencode-ai/client/effect/service"
import { Service } from "@opencode-ai/client/effect"
import { ServiceConfig } from "../../../services/service-config"
export default Runtime.handler(
@@ -11,7 +11,7 @@ export default Runtime.handler(
Effect.fn("cli.debug.agents")(function* () {
const options = yield* ServiceConfig.options()
const found = yield* Service.discover(options)
const endpoint = found ?? (yield* Service.ensure(options))
const endpoint = found ?? (yield* Service.start(options))
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
const response = yield* Effect.promise(() => client.agent.list({ location: { directory: process.cwd() } }))
process.stdout.write(
+7 -18
View File
@@ -4,8 +4,8 @@ import { run } from "@opencode-ai/tui"
import { Commands } from "../commands"
import { Runtime } from "../../framework/runtime"
import { Config } from "../../config"
import { Context, Effect, FileSystem, Option } from "effect"
import { ServerConnection } from "../../services/server-connection"
import { Effect, Option } from "effect"
import { Server } from "../../services/server"
import { Updater } from "../../services/updater"
import { UpdatePreflight } from "../../services/update-preflight"
import { Npm } from "@opencode-ai/core/npm"
@@ -18,11 +18,11 @@ export default Runtime.handler(Commands, (input) =>
yield* updater.check().pipe(Effect.forkScoped)
const preflight = UpdatePreflight.make()
yield* Effect.addFinalizer(() => Effect.promise(() => preflight.close()))
const server = yield* ServerConnection.resolve({
const server = yield* Server.resolve({
server: Option.getOrUndefined(input.server),
standalone: input.standalone,
onStart: (reason, previousVersion) => {
if (reason === "version-mismatch" && preflight.begin(previousVersion)) return
onStart: (reason, existing) => {
if (reason === "version-mismatch" && preflight.begin(existing?.version)) return
process.stderr.write(
reason === "version-mismatch"
? "Restarting background server (version mismatch)...\n"
@@ -37,22 +37,11 @@ export default Runtime.handler(Commands, (input) =>
preflight.loading()
const config = yield* Config.Service
const npm = yield* Npm.Service
const fileSystem = yield* FileSystem.FileSystem
const runServicePromise = Effect.runPromiseWith(Context.make(FileSystem.FileSystem, fileSystem))
const context = yield* Effect.context<FileSystem.FileSystem>()
const context = yield* Effect.context()
const runFork = Effect.runForkWith(context)
const runPromise = Effect.runPromiseWith(context)
const service = server.service
yield* run({
server: {
endpoint: server.endpoint,
service: service
? {
reconnect: (signal) => runServicePromise(service.reconnect(), { signal }),
restart: () => runServicePromise(service.restart()),
}
: undefined,
},
server,
args: { continue: input.continue, sessionID: Option.getOrUndefined(input.session) },
config: {
path: config.path,
@@ -8,7 +8,7 @@ import {
} from "@opencode-ai/client"
import { Commands } from "../../commands"
import { Runtime } from "../../../framework/runtime"
import { Service } from "@opencode-ai/client/effect/service"
import { Service } from "@opencode-ai/client/effect"
import { ServiceConfig } from "../../../services/service-config"
import { resolveIntegration } from "./resolve"
@@ -19,7 +19,7 @@ export default Runtime.handler(
Effect.fn("cli.mcp.auth")(function* (input) {
const options = yield* ServiceConfig.options()
const found = yield* Service.discover(options)
const endpoint = found ?? (yield* Service.ensure(options))
const endpoint = found ?? (yield* Service.start(options))
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
const integration = yield* resolveIntegration(client, input.name, location)
@@ -32,7 +32,7 @@ export default Runtime.handler(
return yield* Effect.fail(new Error(`MCP server "${input.name}" is not an OAuth-capable remote server`))
const started = yield* Effect.promise(() =>
client.integration.oauth.connect({ integrationID: integration.id, methodID: method.id, inputs: {}, location }),
client.integration.connect.oauth({ integrationID: integration.id, methodID: method.id, inputs: {}, location }),
)
const attempt = started.data
if (attempt.mode === "code")
@@ -40,7 +40,7 @@ export default Runtime.handler(
process.stdout.write(attempt.instructions + EOL + attempt.url + EOL)
const result = yield* poll(client, integration.id, attempt.attemptID)
const result = yield* poll(client, attempt.attemptID)
if (result.status === "complete") {
process.stdout.write(`Authenticated with ${input.name}` + EOL)
return
@@ -52,16 +52,15 @@ export default Runtime.handler(
const poll = (
client: OpenCodeClient,
integrationID: string,
attemptID: string,
): Effect.Effect<Exclude<IntegrationAttemptStatus, { status: "pending" }>> =>
Effect.gen(function* () {
const status = yield* Effect.promise(() =>
client.integration.oauth.status({ integrationID, attemptID, location }),
).pipe(Effect.map((result) => result.data))
const status = yield* Effect.promise(() => client.integration.attempt.status({ attemptID, location })).pipe(
Effect.map((result) => result.data),
)
if (status.status === "pending") {
yield* Effect.sleep("1 second")
return yield* poll(client, integrationID, attemptID)
return yield* poll(client, attemptID)
}
return status
})
@@ -3,7 +3,7 @@ import { Effect } from "effect"
import { OpenCode, type McpServer } from "@opencode-ai/client"
import { Commands } from "../../commands"
import { Runtime } from "../../../framework/runtime"
import { Service } from "@opencode-ai/client/effect/service"
import { Service } from "@opencode-ai/client/effect"
import { ServiceConfig } from "../../../services/service-config"
export default Runtime.handler(
@@ -11,7 +11,7 @@ export default Runtime.handler(
Effect.fn("cli.mcp.list")(function* () {
const options = yield* ServiceConfig.options()
const found = yield* Service.discover(options)
const endpoint = found ?? (yield* Service.ensure(options))
const endpoint = found ?? (yield* Service.start(options))
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
const response = yield* Effect.promise(() => client.mcp.list({ location: { directory: process.cwd() } }))
const servers = response.data.toSorted((a, b) => a.name.localeCompare(b.name))
@@ -3,7 +3,7 @@ import { Effect } from "effect"
import { OpenCode } from "@opencode-ai/client"
import { Commands } from "../../commands"
import { Runtime } from "../../../framework/runtime"
import { Service } from "@opencode-ai/client/effect/service"
import { Service } from "@opencode-ai/client/effect"
import { ServiceConfig } from "../../../services/service-config"
import { resolveIntegration } from "./resolve"
@@ -14,7 +14,7 @@ export default Runtime.handler(
Effect.fn("cli.mcp.logout")(function* (input) {
const options = yield* ServiceConfig.options()
const found = yield* Service.discover(options)
const endpoint = found ?? (yield* Service.ensure(options))
const endpoint = found ?? (yield* Service.start(options))
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
const integration = yield* resolveIntegration(client, input.name, location)
+2 -2
View File
@@ -1,14 +1,14 @@
import { Effect, Option } from "effect"
import { Commands } from "../commands"
import { Runtime } from "../../framework/runtime"
import { ServerConnection } from "../../services/server-connection"
import { Server } from "../../services/server"
export default Runtime.handler(Commands.commands.mini, (input) =>
Effect.gen(function* () {
const { runMini, validateMiniTerminal } = yield* Effect.promise(() => import("../../mini"))
yield* Effect.promise(async () => validateMiniTerminal())
const serverURL = Option.getOrUndefined(input.server)
const server = yield* ServerConnection.resolve({ server: serverURL, standalone: input.standalone })
const server = yield* Server.resolve({ server: serverURL, standalone: input.standalone })
yield* Effect.promise(() =>
runMini({
server,
+2 -2
View File
@@ -1,6 +1,6 @@
import { EOL } from "os"
import { Effect } from "effect"
import { Service } from "@opencode-ai/client/effect/service"
import { Service } from "@opencode-ai/client/effect"
import { OpenCode } from "@opencode-ai/client/promise"
import { renderUnicodeCompact } from "uqr"
import { Commands } from "../commands"
@@ -10,7 +10,7 @@ import { ServiceConfig } from "../../services/service-config"
export default Runtime.handler(
Commands.commands.pair,
Effect.fn("cli.pair")(function* () {
const endpoint = yield* Service.ensure(yield* ServiceConfig.options())
const endpoint = yield* Service.start(yield* ServiceConfig.options())
const password = yield* ServiceConfig.password()
const server = yield* Effect.tryPromise(() =>
OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) }).server.get(),
+2 -2
View File
@@ -1,13 +1,13 @@
import { Effect, Option } from "effect"
import { Commands } from "../commands"
import { Runtime } from "../../framework/runtime"
import { ServerConnection } from "../../services/server-connection"
import { Server } from "../../services/server"
export default Runtime.handler(Commands.commands.run, (input) =>
Effect.gen(function* () {
const { runNonInteractive } = yield* Effect.promise(() => import("../../mini"))
const separator = process.argv.indexOf("--", 2)
const server = yield* ServerConnection.resolve({
const server = yield* Server.resolve({
server: Option.getOrUndefined(input.server),
standalone: input.standalone,
})
@@ -1,6 +1,6 @@
import { EOL } from "os"
import { Effect } from "effect"
import { Service } from "@opencode-ai/client/effect/service"
import { Service } from "@opencode-ai/client/effect"
import { Commands } from "../../commands"
import { Runtime } from "../../../framework/runtime"
import { ServiceConfig } from "../../../services/service-config"
@@ -9,8 +9,8 @@ export default Runtime.handler(
Commands.commands.service.commands.restart,
Effect.fn("cli.service.restart")(function* () {
const options = yield* ServiceConfig.options()
yield* Service.stop(options)
const transport = yield* Service.ensure(options)
yield* Service.stop(options, { targetVersion: options.version })
const transport = yield* Service.start(options)
process.stdout.write(transport.url + EOL)
}),
)
@@ -1,6 +1,6 @@
import { EOL } from "os"
import { Effect } from "effect"
import { Service } from "@opencode-ai/client/effect/service"
import { Service } from "@opencode-ai/client/effect"
import { Commands } from "../../commands"
import { Runtime } from "../../../framework/runtime"
import { ServiceConfig } from "../../../services/service-config"
@@ -8,7 +8,7 @@ import { ServiceConfig } from "../../../services/service-config"
export default Runtime.handler(
Commands.commands.service.commands.start,
Effect.fn("cli.service.start")(function* () {
const transport = yield* Service.ensure(yield* ServiceConfig.options())
const transport = yield* Service.start(yield* ServiceConfig.options())
process.stdout.write(transport.url + EOL)
}),
)
@@ -1,6 +1,6 @@
import { EOL } from "os"
import { Effect } from "effect"
import { Service } from "@opencode-ai/client/effect/service"
import { Service } from "@opencode-ai/client/effect"
import { Commands } from "../../commands"
import { Runtime } from "../../../framework/runtime"
import { ServiceConfig } from "../../../services/service-config"
@@ -9,7 +9,12 @@ export default Runtime.handler(
Commands.commands.service.commands.status,
Effect.fn("cli.service.status")(function* () {
const options = yield* ServiceConfig.options()
const status = yield* Service.status(options)
if (status.type !== "ready") {
process.stdout.write(status.type + EOL)
return
}
const found = yield* Service.discover({ ...options, version: undefined })
process.stdout.write((found?.url ?? "stopped") + EOL)
process.stdout.write((found?.url ?? status.type) + EOL)
}),
)
@@ -1,5 +1,5 @@
import { Effect } from "effect"
import { Service } from "@opencode-ai/client/effect/service"
import { Service } from "@opencode-ai/client/effect"
import { Commands } from "../../commands"
import { Runtime } from "../../../framework/runtime"
import { ServiceConfig } from "../../../services/service-config"
-3
View File
@@ -17,9 +17,6 @@ import { Npm } from "@opencode-ai/core/npm"
const Handlers = Runtime.handlers(Commands, {
$: () => import("./commands/handlers/default"),
api: () => import("./commands/handlers/api"),
auth: {
connect: () => import("./commands/handlers/auth/connect"),
},
debug: {
agents: () => import("./commands/handlers/debug/agents"),
},
+7 -4
View File
@@ -1,12 +1,12 @@
import { Service } from "@opencode-ai/client/effect/service"
import { Service } from "@opencode-ai/client/effect"
import { OpenCode, type OpenCodeClient } from "@opencode-ai/client/promise"
import { ServerConnection } from "../services/server-connection"
import { Server } from "../services/server"
import { waitForCatalogReady } from "./catalog.shared"
import { INTERACTIVE_INPUT_ERROR, resolveInteractiveStdin } from "./runtime.stdin"
import type { RunInput, RunTuiConfig } from "./types"
export type MiniCommandInput = {
server: ServerConnection.Resolved
server: Server.Resolved
continue?: boolean
session?: string
fork?: boolean
@@ -38,7 +38,10 @@ export async function runMini(input: MiniCommandInput) {
return agentTask
}
const resolveSession = async () => {
const [agent, selected] = await Promise.all([resolveAgent(), selectSession(sdk, directory, input)])
const [agent, selected] = await Promise.all([
resolveAgent(),
selectSession(sdk, directory, input),
])
const readyModel =
model ?? (selected?.model ? { providerID: selected.model.providerID, modelID: selected.model.id } : undefined)
if (readyModel) await waitForCatalogReady({ sdk, directory, model: readyModel })
+6 -5
View File
@@ -1,10 +1,10 @@
import { Service, type Endpoint } from "@opencode-ai/client/effect/service"
import { Service } from "@opencode-ai/client/effect"
import { OpenCode, type OpenCodeClient } from "@opencode-ai/client/promise"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Model } from "@opencode-ai/schema/model"
import { open } from "node:fs/promises"
import path from "node:path"
import { ServerConnection } from "../services/server-connection"
import { Server } from "../services/server"
import { loadRunAgents, waitForCatalogReady } from "./catalog.shared"
import { runNonInteractivePrompt } from "./noninteractive"
import { toolInlineInfo } from "./tool"
@@ -12,7 +12,7 @@ import type { MiniToolPart } from "./types"
import { UI } from "./ui"
export type RunCommandInput = {
server: ServerConnection.Resolved
server: Server.Resolved
message: string[]
continue?: boolean
session?: string
@@ -55,7 +55,7 @@ async function run(input: RunCommandInput) {
return execute(input, prepared, input.server.endpoint)
}
async function execute(input: RunCommandInput, prepared: Prepared, endpoint: Endpoint) {
async function execute(input: RunCommandInput, prepared: Prepared, endpoint: Service.Endpoint) {
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
const requestedDirectory = prepared.directory ?? (await client.location.get()).directory
if (!requestedDirectory) fail("Failed to resolve server directory")
@@ -73,7 +73,8 @@ async function execute(input: RunCommandInput, prepared: Prepared, endpoint: End
.then((result) => (result.data ? { providerID: result.data.providerID, modelID: result.data.id } : undefined))
: undefined
const model = pickRunModel(explicitModel, variant, sessionModel, defaultModel)
if (variant && !model) return reportError(input, "Cannot select a variant before selecting a model", session?.id)
if (variant && !model)
return reportError(input, "Cannot select a variant before selecting a model", session?.id)
if (model) {
await waitForCatalogReady({ sdk: client, directory: cwd, workspace, model })
const available = await client.model.list({ location: { directory: cwd, workspace } })
+1 -1
View File
@@ -1,7 +1,7 @@
export * as ServerProcess from "./server-process"
import { NodeServices } from "@effect/platform-node"
import { Service } from "@opencode-ai/client/effect/service"
import { Service } from "@opencode-ai/client/effect"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Global } from "@opencode-ai/core/global"
@@ -1,4 +1,5 @@
import { Service, type Endpoint, type EnsureOptions } from "@opencode-ai/client/effect/service"
import { NodeFileSystem } from "@effect/platform-node"
import { Service } from "@opencode-ai/client/effect"
import { ClientError, isUnauthorizedError, OpenCode } from "@opencode-ai/client/promise"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { Effect, Redacted } from "effect"
@@ -10,15 +11,16 @@ export type Args = {
readonly server?: string
readonly standalone?: boolean
readonly mismatch?: "replace" | "ignore" | "error"
readonly onStart?: EnsureOptions["onStart"]
readonly onStart?: Service.StartOptions["onStart"]
}
export type Resolved = {
readonly endpoint: Endpoint
readonly service?: ReturnType<typeof managedService>
readonly endpoint: Service.Endpoint
readonly reconnect?: (onStatus: (status: Service.Status) => void, signal: AbortSignal) => Promise<Service.Endpoint>
readonly reload?: () => Promise<void>
}
export const resolve = Effect.fn("cli.server-connection.resolve")(function* (args: Args) {
export const resolve = Effect.fn("cli.server.resolve")(function* (args: Args) {
if (args.server !== undefined && args.standalone)
return yield* Effect.fail(new Error("--server and --standalone cannot be combined"))
if (args.server !== undefined) {
@@ -26,7 +28,7 @@ export const resolve = Effect.fn("cli.server-connection.resolve")(function* (arg
const endpoint = {
url: args.server,
auth: password ? { type: "basic" as const, username: "opencode", password: Redacted.value(password) } : undefined,
} satisfies Endpoint
} satisfies Service.Endpoint
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
const health = yield* Effect.tryPromise({
try: () => client.health.get({ signal: AbortSignal.timeout(5_000) }),
@@ -43,40 +45,40 @@ export const resolve = Effect.fn("cli.server-connection.resolve")(function* (arg
}
const options = yield* ServiceConfig.options()
const endpoint = yield* resolveManaged({ ...options, onStart: args.onStart }, args.mismatch ?? "replace")
const reconnectOptions = { ...options, version: undefined }
return {
endpoint: yield* resolveManaged({ ...options, onStart: args.onStart }, args.mismatch ?? "replace"),
service: managedService(options),
endpoint,
reconnect: (onStatus, signal) =>
Effect.runPromise(Service.start({ ...reconnectOptions, onStatus }).pipe(Effect.provide(NodeFileSystem.layer)), {
signal,
}),
reload: () =>
Effect.runPromise(
Effect.gen(function* () {
yield* Service.stop(options, { targetVersion: options.version })
yield* Service.start(options)
}).pipe(Effect.provide(NodeFileSystem.layer)),
),
} satisfies Resolved
})
function managedService(options: EnsureOptions) {
const reconnectOptions = { ...options, version: undefined }
return {
reconnect: () => Service.ensure(reconnectOptions),
restart: () =>
Effect.gen(function* () {
yield* Service.stop(options)
yield* Service.ensure(options)
}),
}
}
const resolveManaged = Effect.fnUntraced(function* (
options: EnsureOptions,
options: Service.StartOptions,
mismatch: NonNullable<Args["mismatch"]>,
) {
if (mismatch === "replace") return yield* Service.ensure(options)
if (mismatch === "ignore") return yield* Service.ensure({ ...options, version: undefined })
if (mismatch === "replace") return yield* Service.start(options)
if (mismatch === "ignore") return yield* Service.start({ ...options, version: undefined })
const compatible = yield* Service.discover(options)
if (compatible !== undefined) return compatible
const existing = yield* Service.discover({ ...options, version: undefined })
if (existing !== undefined)
return yield* Effect.fail(new Error("Background server version does not match this client"))
return yield* Service.ensure(options)
return yield* Service.start(options)
})
function connectError(endpoint: Endpoint, cause: unknown) {
function connectError(endpoint: Service.Endpoint, cause: unknown) {
if (isUnauthorizedError(cause)) {
return new Error(
endpoint.auth === undefined
@@ -90,4 +92,4 @@ function connectError(endpoint: Endpoint, cause: unknown) {
return new Error(`Server at ${endpoint.url} did not provide a compatible V2 health response`, { cause })
}
export * as ServerConnection from "./server-connection"
export * as Server from "./server"
+2 -2
View File
@@ -1,12 +1,12 @@
import { Global } from "@opencode-ai/core/global"
import { InstallationChannel, InstallationVersion } from "@opencode-ai/core/installation/version"
import { Hash } from "@opencode-ai/core/util/hash"
import { Service } from "@opencode-ai/client/effect/service"
import { Service } from "@opencode-ai/client/effect"
import { Effect, FileSystem, Option, Schema } from "effect"
import { randomBytes } from "crypto"
import path from "path"
// The CLI's service configuration file, plus the Service.EnsureOptions binding that
// The CLI's service configuration file, plus the Service.Options binding that
// points the client package's service operations at this CLI: which
// registration file (by channel), which version, and how to spawn opencode.
+2 -2
View File
@@ -1,4 +1,4 @@
import { Service, type Endpoint } from "@opencode-ai/client/effect/service"
import { Service } from "@opencode-ai/client/effect"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { Effect, Schema, Stream } from "effect"
@@ -46,7 +46,7 @@ const makeEndpoint = Effect.fn("cli.standalone.endpoint")(
url: ready.url,
auth: { type: "basic" as const, username: "opencode", password },
pid: proc.pid,
} satisfies Endpoint & { readonly pid: number }
} satisfies Service.Endpoint & { readonly pid: number }
},
Effect.provide(AppNodeBuilder.build(CrossSpawnSpawner.node)),
)
@@ -1,5 +1,5 @@
import { Effect } from "effect"
import { Service } from "@opencode-ai/client/effect/service"
import { Service } from "@opencode-ai/client/effect"
import path from "node:path"
import { Standalone } from "../../src/services/standalone"
@@ -1,57 +0,0 @@
import { NodeFileSystem } from "@effect/platform-node"
import { Global } from "@opencode-ai/core/global"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { expect, test } from "bun:test"
import { Effect, FileSystem, Scope } from "effect"
import fs from "node:fs/promises"
import os from "node:os"
import path from "node:path"
import { ServerConnection } from "../src/services/server-connection"
import { ServiceConfig } from "../src/services/service-config"
test("resolution groups Effect-native lifecycle operations only for the managed service", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-server-resolution-"))
const id = "server-resolution-test"
const server = Bun.serve({
port: 0,
fetch() {
return Response.json({
healthy: true,
version: InstallationVersion,
pid: process.pid,
})
},
})
const registration = path.join(root, "state", ServiceConfig.filename())
const layer = Global.layerWith({ config: path.join(root, "config"), state: path.join(root, "state") })
const runPromise = <A, E>(effect: Effect.Effect<A, E, Global.Service | FileSystem.FileSystem | Scope.Scope>) =>
Effect.runPromise(effect.pipe(Effect.provide(layer), Effect.provide(NodeFileSystem.layer), Effect.scoped))
try {
await fs.mkdir(path.dirname(registration), { recursive: true })
await fs.writeFile(
registration,
JSON.stringify({
id,
version: InstallationVersion,
url: server.url.toString(),
pid: process.pid,
}),
)
const resolved = await runPromise(ServerConnection.resolve({}))
expect(resolved.endpoint.url).toBe(server.url.toString())
expect(resolved.service).toBeDefined()
if (!resolved.service) throw new Error("Expected managed service capabilities")
expect(Effect.isEffect(resolved.service.reconnect())).toBe(true)
expect(Effect.isEffect(resolved.service.restart())).toBe(true)
expect(await runPromise(resolved.service.reconnect())).toEqual(resolved.endpoint)
const explicit = await runPromise(ServerConnection.resolve({ server: server.url.toString() }))
expect(explicit.endpoint.url).toBe(server.url.toString())
expect(explicit.service).toBeUndefined()
} finally {
await server.stop(true)
await fs.rm(root, { recursive: true, force: true })
}
})
+18 -4
View File
@@ -1,5 +1,5 @@
import { NodeFileSystem } from "@effect/platform-node"
import { Service } from "@opencode-ai/client/effect/service"
import { Service } from "@opencode-ai/client/effect"
import { Database } from "@opencode-ai/core/database/database"
import { EventV2 } from "@opencode-ai/core/event"
import { EventTable } from "@opencode-ai/core/event/sql"
@@ -146,10 +146,11 @@ test("concurrent service processes elect one server", async () => {
await fetch(new URL("/api/health", info.url), {
headers: { authorization: "Basic " + btoa(`opencode:${info.password}`) },
}).then((response) => response.json()),
).toEqual({
).toMatchObject({
healthy: true,
version: info.version,
pid: info.pid,
instanceID: info.id,
status: { type: "ready" },
})
const blockedTemp = registration + "." + info.id + ".tmp"
await fs.mkdir(blockedTemp)
@@ -192,7 +193,7 @@ test("concurrent service processes elect one server", async () => {
).toEqual({ timeSuspended: null })
expect(await waitForExecutionStart(database, sessionID)).toBe(1)
await Effect.runPromise(
Service.stop({ file: registration }).pipe(Effect.provide(NodeFileSystem.layer)),
Service.stop({ file: registration }, { targetVersion: "next" }).pipe(Effect.provide(NodeFileSystem.layer)),
)
await winner?.exited
} finally {
@@ -226,6 +227,19 @@ test("a failed service stays registered and owns the lock until stopped", async
try {
const info = await waitForInfo(registration)
const status = await Effect.runPromise(
Service.status({ file: registration }).pipe(
Effect.filterOrFail((status) => status.type === "failed"),
Effect.retry(Schedule.spaced("50 millis").pipe(Schedule.both(Schedule.recurs(200)))),
Effect.provide(NodeFileSystem.layer),
),
)
expect(status).toEqual({
type: "failed",
version: info.version,
message: "The background service could not start.",
action: "Run `opencode service restart` after checking the service logs.",
})
expect(owner.exitCode).toBe(null)
const contender = Bun.spawn(command, { env, stderr: "pipe", stdout: "ignore" })
+1 -3
View File
@@ -19,10 +19,8 @@
".": "./src/promise/index.ts",
"./promise": "./src/promise/index.ts",
"./promise/api": "./src/promise/api.ts",
"./service": "./src/promise/service.ts",
"./effect": "./src/effect/index.ts",
"./effect/api": "./src/effect/api.ts",
"./effect/service": "./src/effect/service.ts"
"./effect/api": "./src/effect/api.ts"
},
"scripts": {
"build": "bun run script/build-package.ts",
+35 -81
View File
@@ -11,7 +11,10 @@ export type Endpoint0_0Output = EffectValue<ReturnType<RawClient["server.health"
export type HealthGetOperation<E = never> = () => Effect.Effect<Endpoint0_0Output, E>
type Endpoint0_1Request = Parameters<RawClient["server.health"]["health.stop"]>[0]
export type Endpoint0_1Input = { readonly instanceID: Endpoint0_1Request["payload"]["instanceID"] }
export type Endpoint0_1Input = {
readonly instanceID: Endpoint0_1Request["payload"]["instanceID"]
readonly targetVersion?: Endpoint0_1Request["payload"]["targetVersion"]
}
export type Endpoint0_1Output = EffectValue<ReturnType<RawClient["server.health"]["health.stop"]>>
export type HealthStopOperation<E = never> = (input: Endpoint0_1Input) => Effect.Effect<Endpoint0_1Output, E>
@@ -407,124 +410,75 @@ export type Endpoint10_1Input = {
export type Endpoint10_1Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.get"]>>
export type IntegrationGetOperation<E = never> = (input: Endpoint10_1Input) => Effect.Effect<Endpoint10_1Output, E>
type Endpoint10_2Request = Parameters<RawClient["server.integration"]["integration.wellknown.add"]>[0]
type Endpoint10_2Request = Parameters<RawClient["server.integration"]["integration.connect.key"]>[0]
export type Endpoint10_2Input = {
readonly integrationID: Endpoint10_2Request["params"]["integrationID"]
readonly location?: Endpoint10_2Request["query"]["location"]
readonly url: Endpoint10_2Request["payload"]["url"]
readonly key: Endpoint10_2Request["payload"]["key"]
readonly label?: Endpoint10_2Request["payload"]["label"]
}
export type Endpoint10_2Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.wellknown.add"]>>
export type IntegrationWellknownAddOperation<E = never> = (
export type Endpoint10_2Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.connect.key"]>>
export type IntegrationConnectKeyOperation<E = never> = (
input: Endpoint10_2Input,
) => Effect.Effect<Endpoint10_2Output, E>
type Endpoint10_3Request = Parameters<RawClient["server.integration"]["integration.connect.key"]>[0]
type Endpoint10_3Request = Parameters<RawClient["server.integration"]["integration.connect.oauth"]>[0]
export type Endpoint10_3Input = {
readonly integrationID: Endpoint10_3Request["params"]["integrationID"]
readonly location?: Endpoint10_3Request["query"]["location"]
readonly key: Endpoint10_3Request["payload"]["key"]
readonly methodID: Endpoint10_3Request["payload"]["methodID"]
readonly inputs: Endpoint10_3Request["payload"]["inputs"]
readonly label?: Endpoint10_3Request["payload"]["label"]
}
export type Endpoint10_3Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.connect.key"]>>
export type IntegrationConnectKeyOperation<E = never> = (
export type Endpoint10_3Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.connect.oauth"]>>
export type IntegrationConnectOauthOperation<E = never> = (
input: Endpoint10_3Input,
) => Effect.Effect<Endpoint10_3Output, E>
type Endpoint10_4Request = Parameters<RawClient["server.integration"]["integration.oauth.connect"]>[0]
type Endpoint10_4Request = Parameters<RawClient["server.integration"]["integration.attempt.status"]>[0]
export type Endpoint10_4Input = {
readonly integrationID: Endpoint10_4Request["params"]["integrationID"]
readonly attemptID: Endpoint10_4Request["params"]["attemptID"]
readonly location?: Endpoint10_4Request["query"]["location"]
readonly methodID: Endpoint10_4Request["payload"]["methodID"]
readonly inputs: Endpoint10_4Request["payload"]["inputs"]
readonly label?: Endpoint10_4Request["payload"]["label"]
}
export type Endpoint10_4Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.oauth.connect"]>>
export type IntegrationOauthConnectOperation<E = never> = (
export type Endpoint10_4Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.attempt.status"]>>
export type IntegrationAttemptStatusOperation<E = never> = (
input: Endpoint10_4Input,
) => Effect.Effect<Endpoint10_4Output, E>
type Endpoint10_5Request = Parameters<RawClient["server.integration"]["integration.oauth.status"]>[0]
type Endpoint10_5Request = Parameters<RawClient["server.integration"]["integration.attempt.complete"]>[0]
export type Endpoint10_5Input = {
readonly integrationID: Endpoint10_5Request["params"]["integrationID"]
readonly attemptID: Endpoint10_5Request["params"]["attemptID"]
readonly location?: Endpoint10_5Request["query"]["location"]
readonly code?: Endpoint10_5Request["payload"]["code"]
}
export type Endpoint10_5Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.oauth.status"]>>
export type IntegrationOauthStatusOperation<E = never> = (
export type Endpoint10_5Output = EffectValue<
ReturnType<RawClient["server.integration"]["integration.attempt.complete"]>
>
export type IntegrationAttemptCompleteOperation<E = never> = (
input: Endpoint10_5Input,
) => Effect.Effect<Endpoint10_5Output, E>
type Endpoint10_6Request = Parameters<RawClient["server.integration"]["integration.oauth.complete"]>[0]
type Endpoint10_6Request = Parameters<RawClient["server.integration"]["integration.attempt.cancel"]>[0]
export type Endpoint10_6Input = {
readonly integrationID: Endpoint10_6Request["params"]["integrationID"]
readonly attemptID: Endpoint10_6Request["params"]["attemptID"]
readonly location?: Endpoint10_6Request["query"]["location"]
readonly code?: Endpoint10_6Request["payload"]["code"]
}
export type Endpoint10_6Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.oauth.complete"]>>
export type IntegrationOauthCompleteOperation<E = never> = (
export type Endpoint10_6Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.attempt.cancel"]>>
export type IntegrationAttemptCancelOperation<E = never> = (
input: Endpoint10_6Input,
) => Effect.Effect<Endpoint10_6Output, E>
type Endpoint10_7Request = Parameters<RawClient["server.integration"]["integration.oauth.cancel"]>[0]
export type Endpoint10_7Input = {
readonly integrationID: Endpoint10_7Request["params"]["integrationID"]
readonly attemptID: Endpoint10_7Request["params"]["attemptID"]
readonly location?: Endpoint10_7Request["query"]["location"]
}
export type Endpoint10_7Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.oauth.cancel"]>>
export type IntegrationOauthCancelOperation<E = never> = (
input: Endpoint10_7Input,
) => Effect.Effect<Endpoint10_7Output, E>
type Endpoint10_8Request = Parameters<RawClient["server.integration"]["integration.command.connect"]>[0]
export type Endpoint10_8Input = {
readonly integrationID: Endpoint10_8Request["params"]["integrationID"]
readonly location?: Endpoint10_8Request["query"]["location"]
readonly methodID: Endpoint10_8Request["payload"]["methodID"]
readonly label?: Endpoint10_8Request["payload"]["label"]
}
export type Endpoint10_8Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.command.connect"]>>
export type IntegrationCommandConnectOperation<E = never> = (
input: Endpoint10_8Input,
) => Effect.Effect<Endpoint10_8Output, E>
type Endpoint10_9Request = Parameters<RawClient["server.integration"]["integration.command.status"]>[0]
export type Endpoint10_9Input = {
readonly integrationID: Endpoint10_9Request["params"]["integrationID"]
readonly attemptID: Endpoint10_9Request["params"]["attemptID"]
readonly location?: Endpoint10_9Request["query"]["location"]
}
export type Endpoint10_9Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.command.status"]>>
export type IntegrationCommandStatusOperation<E = never> = (
input: Endpoint10_9Input,
) => Effect.Effect<Endpoint10_9Output, E>
type Endpoint10_10Request = Parameters<RawClient["server.integration"]["integration.command.cancel"]>[0]
export type Endpoint10_10Input = {
readonly integrationID: Endpoint10_10Request["params"]["integrationID"]
readonly attemptID: Endpoint10_10Request["params"]["attemptID"]
readonly location?: Endpoint10_10Request["query"]["location"]
}
export type Endpoint10_10Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.command.cancel"]>>
export type IntegrationCommandCancelOperation<E = never> = (
input: Endpoint10_10Input,
) => Effect.Effect<Endpoint10_10Output, E>
export interface IntegrationApi<E = never> {
readonly list: IntegrationListOperation<E>
readonly get: IntegrationGetOperation<E>
readonly wellknown: { readonly add: IntegrationWellknownAddOperation<E> }
readonly connect: { readonly key: IntegrationConnectKeyOperation<E> }
readonly oauth: {
readonly connect: IntegrationOauthConnectOperation<E>
readonly status: IntegrationOauthStatusOperation<E>
readonly complete: IntegrationOauthCompleteOperation<E>
readonly cancel: IntegrationOauthCancelOperation<E>
readonly connect: {
readonly key: IntegrationConnectKeyOperation<E>
readonly oauth: IntegrationConnectOauthOperation<E>
}
readonly command: {
readonly connect: IntegrationCommandConnectOperation<E>
readonly status: IntegrationCommandStatusOperation<E>
readonly cancel: IntegrationCommandCancelOperation<E>
readonly attempt: {
readonly status: IntegrationAttemptStatusOperation<E>
readonly complete: IntegrationAttemptCompleteOperation<E>
readonly cancel: IntegrationAttemptCancelOperation<E>
}
}
+44 -97
View File
@@ -17,9 +17,14 @@ const Endpoint0_0 = (raw: RawClient["server.health"]) => () =>
raw["health.get"]({}).pipe(Effect.mapError(mapClientError))
type Endpoint0_1Request = Parameters<RawClient["server.health"]["health.stop"]>[0]
type Endpoint0_1Input = { readonly instanceID: Endpoint0_1Request["payload"]["instanceID"] }
type Endpoint0_1Input = {
readonly instanceID: Endpoint0_1Request["payload"]["instanceID"]
readonly targetVersion?: Endpoint0_1Request["payload"]["targetVersion"]
}
const Endpoint0_1 = (raw: RawClient["server.health"]) => (input: Endpoint0_1Input) =>
raw["health.stop"]({ payload: { instanceID: input["instanceID"] } }).pipe(Effect.mapError(mapClientError))
raw["health.stop"]({ payload: { instanceID: input["instanceID"], targetVersion: input["targetVersion"] } }).pipe(
Effect.mapError(mapClientError),
)
const adaptGroup0 = (raw: RawClient["server.health"]) => ({ get: Endpoint0_0(raw), stop: Endpoint0_1(raw) })
@@ -504,133 +509,75 @@ const Endpoint10_1 = (raw: RawClient["server.integration"]) => (input: Endpoint1
query: { location: input["location"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint10_2Request = Parameters<RawClient["server.integration"]["integration.wellknown.add"]>[0]
type Endpoint10_2Request = Parameters<RawClient["server.integration"]["integration.connect.key"]>[0]
type Endpoint10_2Input = {
readonly integrationID: Endpoint10_2Request["params"]["integrationID"]
readonly location?: Endpoint10_2Request["query"]["location"]
readonly url: Endpoint10_2Request["payload"]["url"]
readonly key: Endpoint10_2Request["payload"]["key"]
readonly label?: Endpoint10_2Request["payload"]["label"]
}
const Endpoint10_2 = (raw: RawClient["server.integration"]) => (input: Endpoint10_2Input) =>
raw["integration.wellknown.add"]({ query: { location: input["location"] }, payload: { url: input["url"] } }).pipe(
Effect.mapError(mapClientError),
)
type Endpoint10_3Request = Parameters<RawClient["server.integration"]["integration.connect.key"]>[0]
type Endpoint10_3Input = {
readonly integrationID: Endpoint10_3Request["params"]["integrationID"]
readonly location?: Endpoint10_3Request["query"]["location"]
readonly key: Endpoint10_3Request["payload"]["key"]
readonly label?: Endpoint10_3Request["payload"]["label"]
}
const Endpoint10_3 = (raw: RawClient["server.integration"]) => (input: Endpoint10_3Input) =>
raw["integration.connect.key"]({
params: { integrationID: input["integrationID"] },
query: { location: input["location"] },
payload: { key: input["key"], label: input["label"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint10_4Request = Parameters<RawClient["server.integration"]["integration.oauth.connect"]>[0]
type Endpoint10_4Input = {
readonly integrationID: Endpoint10_4Request["params"]["integrationID"]
readonly location?: Endpoint10_4Request["query"]["location"]
readonly methodID: Endpoint10_4Request["payload"]["methodID"]
readonly inputs: Endpoint10_4Request["payload"]["inputs"]
readonly label?: Endpoint10_4Request["payload"]["label"]
type Endpoint10_3Request = Parameters<RawClient["server.integration"]["integration.connect.oauth"]>[0]
type Endpoint10_3Input = {
readonly integrationID: Endpoint10_3Request["params"]["integrationID"]
readonly location?: Endpoint10_3Request["query"]["location"]
readonly methodID: Endpoint10_3Request["payload"]["methodID"]
readonly inputs: Endpoint10_3Request["payload"]["inputs"]
readonly label?: Endpoint10_3Request["payload"]["label"]
}
const Endpoint10_4 = (raw: RawClient["server.integration"]) => (input: Endpoint10_4Input) =>
raw["integration.oauth.connect"]({
const Endpoint10_3 = (raw: RawClient["server.integration"]) => (input: Endpoint10_3Input) =>
raw["integration.connect.oauth"]({
params: { integrationID: input["integrationID"] },
query: { location: input["location"] },
payload: { methodID: input["methodID"], inputs: input["inputs"], label: input["label"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint10_5Request = Parameters<RawClient["server.integration"]["integration.oauth.status"]>[0]
type Endpoint10_5Input = {
readonly integrationID: Endpoint10_5Request["params"]["integrationID"]
readonly attemptID: Endpoint10_5Request["params"]["attemptID"]
readonly location?: Endpoint10_5Request["query"]["location"]
type Endpoint10_4Request = Parameters<RawClient["server.integration"]["integration.attempt.status"]>[0]
type Endpoint10_4Input = {
readonly attemptID: Endpoint10_4Request["params"]["attemptID"]
readonly location?: Endpoint10_4Request["query"]["location"]
}
const Endpoint10_5 = (raw: RawClient["server.integration"]) => (input: Endpoint10_5Input) =>
raw["integration.oauth.status"]({
params: { integrationID: input["integrationID"], attemptID: input["attemptID"] },
const Endpoint10_4 = (raw: RawClient["server.integration"]) => (input: Endpoint10_4Input) =>
raw["integration.attempt.status"]({
params: { attemptID: input["attemptID"] },
query: { location: input["location"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint10_6Request = Parameters<RawClient["server.integration"]["integration.oauth.complete"]>[0]
type Endpoint10_6Input = {
readonly integrationID: Endpoint10_6Request["params"]["integrationID"]
readonly attemptID: Endpoint10_6Request["params"]["attemptID"]
readonly location?: Endpoint10_6Request["query"]["location"]
readonly code?: Endpoint10_6Request["payload"]["code"]
type Endpoint10_5Request = Parameters<RawClient["server.integration"]["integration.attempt.complete"]>[0]
type Endpoint10_5Input = {
readonly attemptID: Endpoint10_5Request["params"]["attemptID"]
readonly location?: Endpoint10_5Request["query"]["location"]
readonly code?: Endpoint10_5Request["payload"]["code"]
}
const Endpoint10_6 = (raw: RawClient["server.integration"]) => (input: Endpoint10_6Input) =>
raw["integration.oauth.complete"]({
params: { integrationID: input["integrationID"], attemptID: input["attemptID"] },
const Endpoint10_5 = (raw: RawClient["server.integration"]) => (input: Endpoint10_5Input) =>
raw["integration.attempt.complete"]({
params: { attemptID: input["attemptID"] },
query: { location: input["location"] },
payload: { code: input["code"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint10_7Request = Parameters<RawClient["server.integration"]["integration.oauth.cancel"]>[0]
type Endpoint10_7Input = {
readonly integrationID: Endpoint10_7Request["params"]["integrationID"]
readonly attemptID: Endpoint10_7Request["params"]["attemptID"]
readonly location?: Endpoint10_7Request["query"]["location"]
type Endpoint10_6Request = Parameters<RawClient["server.integration"]["integration.attempt.cancel"]>[0]
type Endpoint10_6Input = {
readonly attemptID: Endpoint10_6Request["params"]["attemptID"]
readonly location?: Endpoint10_6Request["query"]["location"]
}
const Endpoint10_7 = (raw: RawClient["server.integration"]) => (input: Endpoint10_7Input) =>
raw["integration.oauth.cancel"]({
params: { integrationID: input["integrationID"], attemptID: input["attemptID"] },
query: { location: input["location"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint10_8Request = Parameters<RawClient["server.integration"]["integration.command.connect"]>[0]
type Endpoint10_8Input = {
readonly integrationID: Endpoint10_8Request["params"]["integrationID"]
readonly location?: Endpoint10_8Request["query"]["location"]
readonly methodID: Endpoint10_8Request["payload"]["methodID"]
readonly label?: Endpoint10_8Request["payload"]["label"]
}
const Endpoint10_8 = (raw: RawClient["server.integration"]) => (input: Endpoint10_8Input) =>
raw["integration.command.connect"]({
params: { integrationID: input["integrationID"] },
query: { location: input["location"] },
payload: { methodID: input["methodID"], label: input["label"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint10_9Request = Parameters<RawClient["server.integration"]["integration.command.status"]>[0]
type Endpoint10_9Input = {
readonly integrationID: Endpoint10_9Request["params"]["integrationID"]
readonly attemptID: Endpoint10_9Request["params"]["attemptID"]
readonly location?: Endpoint10_9Request["query"]["location"]
}
const Endpoint10_9 = (raw: RawClient["server.integration"]) => (input: Endpoint10_9Input) =>
raw["integration.command.status"]({
params: { integrationID: input["integrationID"], attemptID: input["attemptID"] },
query: { location: input["location"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint10_10Request = Parameters<RawClient["server.integration"]["integration.command.cancel"]>[0]
type Endpoint10_10Input = {
readonly integrationID: Endpoint10_10Request["params"]["integrationID"]
readonly attemptID: Endpoint10_10Request["params"]["attemptID"]
readonly location?: Endpoint10_10Request["query"]["location"]
}
const Endpoint10_10 = (raw: RawClient["server.integration"]) => (input: Endpoint10_10Input) =>
raw["integration.command.cancel"]({
params: { integrationID: input["integrationID"], attemptID: input["attemptID"] },
const Endpoint10_6 = (raw: RawClient["server.integration"]) => (input: Endpoint10_6Input) =>
raw["integration.attempt.cancel"]({
params: { attemptID: input["attemptID"] },
query: { location: input["location"] },
}).pipe(Effect.mapError(mapClientError))
const adaptGroup10 = (raw: RawClient["server.integration"]) => ({
list: Endpoint10_0(raw),
get: Endpoint10_1(raw),
wellknown: { add: Endpoint10_2(raw) },
connect: { key: Endpoint10_3(raw) },
oauth: {
connect: Endpoint10_4(raw),
status: Endpoint10_5(raw),
complete: Endpoint10_6(raw),
cancel: Endpoint10_7(raw),
},
command: { connect: Endpoint10_8(raw), status: Endpoint10_9(raw), cancel: Endpoint10_10(raw) },
connect: { key: Endpoint10_2(raw), oauth: Endpoint10_3(raw) },
attempt: { status: Endpoint10_4(raw), complete: Endpoint10_5(raw), cancel: Endpoint10_6(raw) },
})
type Endpoint11_0Request = Parameters<RawClient["server.mcp"]["mcp.list"]>[0]
+1
View File
@@ -17,6 +17,7 @@ export type {
SessionApi,
SkillApi,
} from "./api.js"
export { Service } from "./service.js"
export { Agent } from "@opencode-ai/schema/agent"
export { Command } from "@opencode-ai/schema/command"
export { Credential } from "@opencode-ai/schema/credential"
+111 -44
View File
@@ -3,16 +3,6 @@ import { Effect, FileSystem, Option, Schedule, Schema } from "effect"
import { spawn, type ChildProcess } from "node:child_process"
import { homedir } from "node:os"
import { join } from "node:path"
import type {
DiscoverOptions,
Endpoint,
EnsureOptions,
StopOptions,
} from "../service.js"
export * from "../service.js"
/** Contents of the local service registration file. */
export type Info = import("../service.js").Info
// Find, start, and stop the local opencode background service.
//
@@ -22,21 +12,73 @@ export type Info = import("../service.js").Info
// is all a client needs to connect. The daemon's own configuration (port,
// persisted password) is CLI-owned and never read here.
export type Endpoint = {
readonly url: string
readonly auth?: {
readonly type: "basic"
readonly username: string
readonly password: string
}
}
export type Options = {
// Absolute path to the service registration file. Defaults to
// opencode/service.json in the XDG state directory.
readonly file?: string
// When set, discovery only returns a server reporting this exact version,
// and start() replaces a healthy server whose version differs.
readonly version?: string
// Argv used to spawn the service. Defaults to ["opencode", "serve",
// "--service"] resolved from PATH.
readonly command?: ReadonlyArray<string>
}
export type StartReason = "missing" | "version-mismatch"
export type StartOptions = Options & {
// Called once when start() decides it must spawn: either no service was
// found, or a healthy service with a different version is being replaced.
// `existing` carries the registration of the service being replaced.
readonly onStart?: (reason: StartReason, existing?: Info) => void
readonly onStatus?: (status: Status) => void
}
export type Status =
| { readonly type: "missing" }
| { readonly type: "unreachable" }
| { readonly type: "unresponsive" }
| (ServiceStatus.State & { readonly version?: string })
export class FailedError extends Schema.TaggedErrorClass<FailedError>()("ServiceFailedError", {
message: Schema.String,
action: Schema.String,
}) {}
type Contender = {
readonly child: ChildProcess
readonly error: () => Error | undefined
}
// Read-only lookup: registration file plus health check and version gate.
// Never spawns; escalation to ensure() is the caller's policy.
/** Discover a healthy, compatible local service without starting one. */
export const discover = Effect.fn("service.discover")(function* (options: DiscoverOptions = {}) {
// Never spawns; escalation to start() is the caller's policy.
export const discover = Effect.fn("service.discover")(function* (options: Options = {}) {
return (yield* discoverLocal(options))?.endpoint
})
const discoverLocal = Effect.fnUntraced(function* (options: DiscoverOptions) {
export const status = Effect.fn("service.status")(function* (options: Options = {}) {
const result = yield* registered(options.file, true)
if (result.info === undefined) return { type: "missing" } satisfies Status
if (result.service === undefined) return { type: "unreachable" } satisfies Status
return publicStatus(result.service)
})
function publicStatus(service: LocalService): Status {
return { ...service.status, version: service.version }
}
const discoverLocal = Effect.fnUntraced(function* (options: Options) {
const found = (yield* registered(options.file)).service
if (found?.state !== "ready") return undefined
if (found?.status.type !== "ready") return undefined
if (options.version !== undefined && found.version !== options.version) return undefined
return found
})
@@ -44,18 +86,18 @@ const discoverLocal = Effect.fnUntraced(function* (options: DiscoverOptions) {
// Idempotent ensure-running: reuses a healthy compatible server, replaces a
// version-mismatched one, and otherwise spawns small contenders until a server
// becomes discoverable. A contender is never killed merely for slow startup.
/** Ensure a healthy, compatible local service is running. */
export const ensure = Effect.fn("service.ensure")(function* (options: EnsureOptions = {}) {
export const start = Effect.fn("service.start")(function* (options: StartOptions = {}) {
const contenders = new Set<Contender>()
let announced = false
let reported: Status | undefined
let lastSpawn = 0
let spawnDelay = 5_000
let ownerHeld = false
const announce = (reason: "missing" | "version-mismatch", previousVersion?: string) =>
const announce = (reason: StartReason, existing?: Info) =>
Effect.sync(() => {
if (announced) return
announced = true
options.onStart?.(reason, previousVersion)
options.onStart?.(reason, existing)
})
const spawnContender = Effect.gen(function* () {
const [command, ...args] = options.command ?? ["opencode", "serve", "--service"]
@@ -77,15 +119,24 @@ export const ensure = Effect.fn("service.ensure")(function* (options: EnsureOpti
const registration = yield* registered(options.file, true)
const info = registration.info
const service = registration.service
const current: Status =
service === undefined ? { type: info === undefined ? "missing" : "unreachable" } : publicStatus(service)
const next = ownerHeld && service === undefined ? ({ type: "unresponsive" } satisfies Status) : current
yield* Effect.sync(() => {
if (sameStatus(reported, next)) return
reported = next
options.onStatus?.(next)
})
if (service !== undefined) {
ownerHeld = false
spawnDelay = 5_000
const compatible = !service.legacy && (options.version === undefined || service.version === options.version)
if (compatible && service.state === "ready") return Option.some(service)
if (compatible && service.state === "failed") return yield* Effect.fail(new Error("Background service failed to start"))
if (compatible) return Option.none<LocalService>()
yield* announce("version-mismatch", service.version)
yield* kill(service, options).pipe(Effect.ignore)
if (compatible && service.status.type === "ready") return Option.some(service)
if (compatible && service.status.type === "failed")
return yield* new FailedError({ message: service.status.message, action: service.status.action })
if (compatible || service.status.type === "stopping") return Option.none<LocalService>()
yield* announce("version-mismatch", service.info)
yield* kill(service, options, options.version).pipe(Effect.ignore)
lastSpawn = 0
return Option.none<LocalService>()
} else if (lastSpawn === 0 && info !== undefined) lastSpawn = Date.now()
@@ -109,6 +160,22 @@ export const ensure = Effect.fn("service.ensure")(function* (options: EnsureOpti
return Option.getOrThrow(found).endpoint
})
function sameStatus(left: Status | undefined, right: Status) {
if (left?.type !== right.type) return false
if (right.type === "failed")
return (
left.type === "failed" &&
left.version === right.version &&
left.message === right.message &&
left.action === right.action
)
if (right.type === "stopping")
return left.type === "stopping" && left.version === right.version && left.targetVersion === right.targetVersion
if (right.type === "starting" || right.type === "ready")
return left.type === right.type && left.version === right.version
return true
}
function contenderFailure(contender: Contender) {
const error = contender.error()
if (error !== undefined) return error
@@ -123,10 +190,13 @@ function contenderFinished(contender: Contender) {
return contender.error() !== undefined || contender.child.exitCode !== null || contender.child.signalCode !== null
}
/** Stop the registered local service. */
export const stop = Effect.fn("service.stop")(function* (options: StopOptions = {}) {
export type StopMetadata = {
readonly targetVersion?: string
}
export const stop = Effect.fn("service.stop")(function* (options: Options = {}, metadata: StopMetadata = {}) {
const existing = yield* find(options)
if (existing !== undefined) yield* kill(existing, options)
if (existing !== undefined) yield* kill(existing, options, metadata.targetVersion)
})
function fallback() {
@@ -134,13 +204,11 @@ function fallback() {
return join(state, "opencode", "service.json")
}
/** Create HTTP authentication headers for a service endpoint. */
export function headers(endpoint: Endpoint) {
if (endpoint.auth === undefined) return undefined
return { authorization: "Basic " + btoa(endpoint.auth.username + ":" + endpoint.auth.password) }
}
/** Schema for the local service registration file. */
export const Info = Schema.Struct({
id: Schema.optional(Schema.String),
version: Schema.optional(Schema.String),
@@ -148,6 +216,7 @@ export const Info = Schema.Struct({
pid: Schema.Int.check(Schema.isGreaterThan(0)),
password: Schema.optional(Schema.String),
})
export type Info = typeof Info.Type
const decode = Schema.decodeUnknownEffect(Schema.fromJsonString(Info))
const decodeHealth = Schema.decodeUnknownOption(ServiceStatus.Health)
@@ -166,7 +235,7 @@ type LocalService = {
readonly info: Info
readonly endpoint: Endpoint
readonly version?: string
readonly state: "ready" | "waiting" | "failed"
readonly status: ServiceStatus.State
readonly legacy: boolean
}
@@ -190,11 +259,13 @@ const probe = Effect.fnUntraced(function* (info: Info, allowLegacy = false) {
if (Option.isSome(health)) {
if (health.value.pid !== info.pid) return undefined
if (info.version !== undefined && health.value.version !== info.version) return undefined
if (info.id !== undefined && health.value.instanceID !== undefined && health.value.instanceID !== info.id)
return undefined
return {
info,
endpoint,
version: health.value.version,
state: response.ok ? "ready" : response.status === 500 ? "failed" : "waiting",
status: health.value.status,
legacy: false,
} satisfies LocalService
}
@@ -204,7 +275,7 @@ const probe = Effect.fnUntraced(function* (info: Info, allowLegacy = false) {
(typeof body === "object" && body !== null && ("version" in body || "pid" in body))
)
return undefined
return { info, endpoint, state: "ready", legacy: true } satisfies LocalService
return { info, endpoint, status: { type: "ready" }, legacy: true } satisfies LocalService
})
const registered = Effect.fnUntraced(function* (file?: string, allowLegacy = false) {
@@ -213,13 +284,13 @@ const registered = Effect.fnUntraced(function* (file?: string, allowLegacy = fal
return { info, service: yield* probe(info, allowLegacy) }
})
// Health-checked lookup without the version gate: lifecycle operations must be
// Health-checked lookup without the version gate: status operations must be
// able to see (and replace or stop) a server from a different version.
const find = Effect.fnUntraced(function* (options: { readonly file?: string }) {
const find = Effect.fnUntraced(function* (options: Options) {
return (yield* registered(options.file, true)).service
})
// 50ms cadence bounded at ~5s, shared by stop escalation and each ensure
// 50ms cadence bounded at ~5s, shared by stop escalation and each start
// discovery window.
const poll = Schedule.spaced("50 millis").pipe(Schedule.both(Schedule.recurs(100)))
@@ -238,11 +309,8 @@ function same(left: Info, right: Info) {
return left.id === right.id && left.version === right.version && left.url === right.url && left.pid === right.pid
}
const kill = Effect.fnUntraced(function* (
service: LocalService,
options: { readonly file?: string },
) {
const requested = yield* requestStop(service)
const kill = Effect.fnUntraced(function* (service: LocalService, options: Options, targetVersion?: string) {
const requested = yield* requestStop(service, targetVersion)
if (requested === "rejected") return
if (requested === "unsupported") {
// A stale registration may point at a reused PID. Authenticate again
@@ -262,13 +330,13 @@ const kill = Effect.fnUntraced(function* (
const decodeStopResponse = Schema.decodeUnknownOption(ServiceStatus.StopResponse)
const requestStop = Effect.fnUntraced(function* (service: LocalService) {
const requestStop = Effect.fnUntraced(function* (service: LocalService, targetVersion?: string) {
if (service.info.id === undefined || service.legacy) return "unsupported" as const
const response = yield* Effect.tryPromise(() =>
fetch(new URL("/api/service/stop", service.info.url), {
method: "POST",
headers: { ...headers(service.endpoint), "content-type": "application/json" },
body: JSON.stringify({ instanceID: service.info.id }),
body: JSON.stringify({ instanceID: service.info.id, targetVersion }),
signal: AbortSignal.timeout(2_000),
}),
).pipe(Effect.option, Effect.map(Option.getOrUndefined))
@@ -279,5 +347,4 @@ const requestStop = Effect.fnUntraced(function* (service: LocalService) {
return "accepted" as const
})
/** Effect-based local service lifecycle operations. */
export const Service = { discover, ensure, stop, headers, Info }
export * as Service from "./service.js"
+22 -84
View File
@@ -82,24 +82,16 @@ import type {
IntegrationListOutput,
IntegrationGetInput,
IntegrationGetOutput,
IntegrationWellknownAddInput,
IntegrationWellknownAddOutput,
IntegrationConnectKeyInput,
IntegrationConnectKeyOutput,
IntegrationOauthConnectInput,
IntegrationOauthConnectOutput,
IntegrationOauthStatusInput,
IntegrationOauthStatusOutput,
IntegrationOauthCompleteInput,
IntegrationOauthCompleteOutput,
IntegrationOauthCancelInput,
IntegrationOauthCancelOutput,
IntegrationCommandConnectInput,
IntegrationCommandConnectOutput,
IntegrationCommandStatusInput,
IntegrationCommandStatusOutput,
IntegrationCommandCancelInput,
IntegrationCommandCancelOutput,
IntegrationConnectOauthInput,
IntegrationConnectOauthOutput,
IntegrationAttemptStatusInput,
IntegrationAttemptStatusOutput,
IntegrationAttemptCompleteInput,
IntegrationAttemptCompleteOutput,
IntegrationAttemptCancelInput,
IntegrationAttemptCancelOutput,
McpListInput,
McpListOutput,
McpResourceCatalogInput,
@@ -347,7 +339,7 @@ export function make(options: ClientOptions) {
{
method: "POST",
path: `/api/service/stop`,
body: { instanceID: input["instanceID"] },
body: { instanceID: input["instanceID"], targetVersion: input["targetVersion"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
@@ -895,21 +887,6 @@ export function make(options: ClientOptions) {
},
requestOptions,
),
wellknown: {
add: (input: IntegrationWellknownAddInput, requestOptions?: RequestOptions) =>
request<IntegrationWellknownAddOutput>(
{
method: "POST",
path: `/api/experimental/integration/wellknown`,
query: { location: input["location"] },
body: { url: input["url"] },
successStatus: 204,
declaredStatuses: [400, 401],
empty: true,
},
requestOptions,
),
},
connect: {
key: (input: IntegrationConnectKeyInput, requestOptions?: RequestOptions) =>
request<IntegrationConnectKeyOutput>(
@@ -924,10 +901,8 @@ export function make(options: ClientOptions) {
},
requestOptions,
),
},
oauth: {
connect: (input: IntegrationOauthConnectInput, requestOptions?: RequestOptions) =>
request<IntegrationOauthConnectOutput>(
oauth: (input: IntegrationConnectOauthInput, requestOptions?: RequestOptions) =>
request<IntegrationConnectOauthOutput>(
{
method: "POST",
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth`,
@@ -939,11 +914,13 @@ export function make(options: ClientOptions) {
},
requestOptions,
),
status: (input: IntegrationOauthStatusInput, requestOptions?: RequestOptions) =>
request<IntegrationOauthStatusOutput>(
},
attempt: {
status: (input: IntegrationAttemptStatusInput, requestOptions?: RequestOptions) =>
request<IntegrationAttemptStatusOutput>(
{
method: "GET",
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth/${encodeURIComponent(input.attemptID)}`,
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}`,
query: { location: input["location"] },
successStatus: 200,
declaredStatuses: [401, 400],
@@ -951,11 +928,11 @@ export function make(options: ClientOptions) {
},
requestOptions,
),
complete: (input: IntegrationOauthCompleteInput, requestOptions?: RequestOptions) =>
request<IntegrationOauthCompleteOutput>(
complete: (input: IntegrationAttemptCompleteInput, requestOptions?: RequestOptions) =>
request<IntegrationAttemptCompleteOutput>(
{
method: "POST",
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth/${encodeURIComponent(input.attemptID)}/complete`,
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}/complete`,
query: { location: input["location"] },
body: { code: input["code"] },
successStatus: 204,
@@ -964,50 +941,11 @@ export function make(options: ClientOptions) {
},
requestOptions,
),
cancel: (input: IntegrationOauthCancelInput, requestOptions?: RequestOptions) =>
request<IntegrationOauthCancelOutput>(
cancel: (input: IntegrationAttemptCancelInput, requestOptions?: RequestOptions) =>
request<IntegrationAttemptCancelOutput>(
{
method: "DELETE",
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth/${encodeURIComponent(input.attemptID)}`,
query: { location: input["location"] },
successStatus: 204,
declaredStatuses: [401, 400],
empty: true,
},
requestOptions,
),
},
command: {
connect: (input: IntegrationCommandConnectInput, requestOptions?: RequestOptions) =>
request<IntegrationCommandConnectOutput>(
{
method: "POST",
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/command`,
query: { location: input["location"] },
body: { methodID: input["methodID"], label: input["label"] },
successStatus: 200,
declaredStatuses: [400, 401],
empty: false,
},
requestOptions,
),
status: (input: IntegrationCommandStatusInput, requestOptions?: RequestOptions) =>
request<IntegrationCommandStatusOutput>(
{
method: "GET",
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/command/${encodeURIComponent(input.attemptID)}`,
query: { location: input["location"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
cancel: (input: IntegrationCommandCancelInput, requestOptions?: RequestOptions) =>
request<IntegrationCommandCancelOutput>(
{
method: "DELETE",
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/command/${encodeURIComponent(input.attemptID)}`,
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}`,
query: { location: input["location"] },
successStatus: 204,
declaredStatuses: [401, 400],
+29 -94
View File
@@ -1,6 +1,10 @@
export type JsonValue = null | boolean | number | string | Array<JsonValue> | { [key: string]: JsonValue }
export type ServiceHealth = { healthy: true; version: string; pid: number }
export type ServiceStatus =
| { type: "starting" }
| { type: "ready" }
| { type: "stopping"; targetVersion?: string | null }
| { type: "failed"; message: string; action: string }
export type ServiceStopResponse = { accepted: boolean }
@@ -187,8 +191,6 @@ export type ProviderV2Info = {
export type IntegrationWhen = { key: string; op: "eq" | "neq"; value: string }
export type IntegrationCommandMethod = { id: string; type: "command"; label: string; command: Array<string> }
export type IntegrationKeyMethod = { type: "key"; label?: string }
export type IntegrationEnvMethod = { type: "env"; names: Array<string> }
@@ -216,31 +218,6 @@ export type IntegrationAttemptStatus =
time: { created: number | "Infinity" | "-Infinity" | "NaN"; expires: number | "Infinity" | "-Infinity" | "NaN" }
}
export type IntegrationCommandAttempt = {
attemptID: string
time: { created: number | "Infinity" | "-Infinity" | "NaN"; expires: number | "Infinity" | "-Infinity" | "NaN" }
}
export type IntegrationCommandAttemptStatus =
| {
status: "pending"
message?: string
time: { created: number | "Infinity" | "-Infinity" | "NaN"; expires: number | "Infinity" | "-Infinity" | "NaN" }
}
| {
status: "complete"
time: { created: number | "Infinity" | "-Infinity" | "NaN"; expires: number | "Infinity" | "-Infinity" | "NaN" }
}
| {
status: "failed"
message: string
time: { created: number | "Infinity" | "-Infinity" | "NaN"; expires: number | "Infinity" | "-Infinity" | "NaN" }
}
| {
status: "expired"
time: { created: number | "Infinity" | "-Infinity" | "NaN"; expires: number | "Infinity" | "-Infinity" | "NaN" }
}
export type McpStatusConnected = { status: "connected" }
export type McpStatusPending = { status: "pending" }
@@ -529,6 +506,14 @@ export type VcsFileStatus = {
status: "added" | "deleted" | "modified"
}
export type ServiceHealth = {
healthy: true
version: string
pid: number
instanceID?: string | null
status?: ServiceStatus
}
export type SessionMessageModelSelected = {
id: string
metadata?: { [x: string]: JsonValue }
@@ -2019,11 +2004,7 @@ export type SessionMessageAssistantTool = {
time: { created: number; ran?: number; completed?: number }
}
export type IntegrationMethod =
| IntegrationOAuthMethod
| IntegrationCommandMethod
| IntegrationKeyMethod
| IntegrationEnvMethod
export type IntegrationMethod = IntegrationOAuthMethod | IntegrationKeyMethod | IntegrationEnvMethod
export type FormFields = [FormField, ...Array<FormField>]
@@ -2520,7 +2501,10 @@ export const isProjectCopyError = (value: unknown): value is ProjectCopyError =>
export type HealthGetOutput = ServiceHealth
export type HealthStopInput = { readonly instanceID: { readonly instanceID: string }["instanceID"] }
export type HealthStopInput = {
readonly instanceID: { readonly instanceID: string; readonly targetVersion?: string | undefined }["instanceID"]
readonly targetVersion?: { readonly instanceID: string; readonly targetVersion?: string | undefined }["targetVersion"]
}
export type HealthStopOutput = ServiceStopResponse
@@ -3304,15 +3288,6 @@ export type IntegrationGetOutput = {
data: IntegrationInfo | null
}
export type IntegrationWellknownAddInput = {
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly url: { readonly url: string }["url"]
}
export type IntegrationWellknownAddOutput = void
export type IntegrationConnectKeyInput = {
readonly integrationID: { readonly integrationID: string }["integrationID"]
readonly location?: {
@@ -3324,7 +3299,7 @@ export type IntegrationConnectKeyInput = {
export type IntegrationConnectKeyOutput = void
export type IntegrationOauthConnectInput = {
export type IntegrationConnectOauthInput = {
readonly integrationID: { readonly integrationID: string }["integrationID"]
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
@@ -3346,7 +3321,7 @@ export type IntegrationOauthConnectInput = {
}["label"]
}
export type IntegrationOauthConnectOutput = {
export type IntegrationConnectOauthOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: {
attemptID: string
@@ -3357,76 +3332,36 @@ export type IntegrationOauthConnectOutput = {
}
}
export type IntegrationOauthStatusInput = {
readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"]
readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"]
export type IntegrationAttemptStatusInput = {
readonly attemptID: { readonly attemptID: string }["attemptID"]
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
}
export type IntegrationOauthStatusOutput = {
export type IntegrationAttemptStatusOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: IntegrationAttemptStatus
}
export type IntegrationOauthCompleteInput = {
readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"]
readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"]
export type IntegrationAttemptCompleteInput = {
readonly attemptID: { readonly attemptID: string }["attemptID"]
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly code?: { readonly code?: string | undefined }["code"]
}
export type IntegrationOauthCompleteOutput = void
export type IntegrationAttemptCompleteOutput = void
export type IntegrationOauthCancelInput = {
readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"]
readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"]
export type IntegrationAttemptCancelInput = {
readonly attemptID: { readonly attemptID: string }["attemptID"]
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
}
export type IntegrationOauthCancelOutput = void
export type IntegrationCommandConnectInput = {
readonly integrationID: { readonly integrationID: string }["integrationID"]
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly methodID: { readonly methodID: string; readonly label?: string | undefined }["methodID"]
readonly label?: { readonly methodID: string; readonly label?: string | undefined }["label"]
}
export type IntegrationCommandConnectOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: IntegrationCommandAttempt
}
export type IntegrationCommandStatusInput = {
readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"]
readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"]
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
}
export type IntegrationCommandStatusOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: IntegrationCommandAttemptStatus
}
export type IntegrationCommandCancelInput = {
readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"]
readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"]
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
}
export type IntegrationCommandCancelOutput = void
export type IntegrationAttemptCancelOutput = void
export type McpListInput = {
readonly location?: {
-252
View File
@@ -1,252 +0,0 @@
import { readFile } from "node:fs/promises"
import { spawn, type ChildProcess } from "node:child_process"
import { homedir } from "node:os"
import { join } from "node:path"
import type {
DiscoverOptions,
Endpoint,
Info,
EnsureOptions,
StopOptions,
} from "../service.js"
import type { ServiceHealth, ServiceStopResponse } from "./generated/types.js"
export * from "../service.js"
// Find, start, and stop the local opencode background service.
//
// The registration file is the complete discovery contract. This module is
// intentionally implemented with Node APIs so Promise clients do not need
// Effect or @effect/platform-node at runtime.
type Contender = {
readonly child: ChildProcess
readonly error: () => Error | undefined
}
/** Discover a healthy, compatible local service without starting one. */
export async function discover(options: DiscoverOptions = {}) {
return (await discoverLocal(options))?.endpoint
}
async function discoverLocal(options: DiscoverOptions) {
const found = (await registered(options.file)).service
if (found?.state !== "ready") return undefined
if (options.version !== undefined && found.version !== options.version) return undefined
return found
}
/** Ensure a healthy, compatible local service is running. */
export async function ensure(options: EnsureOptions = {}): Promise<Endpoint> {
const contenders = new Set<Contender>()
let announced = false
let lastSpawn = 0
let spawnDelay = 5_000
let ownerHeld = false
const announce = (reason: "missing" | "version-mismatch", previousVersion?: string) => {
if (announced) return
announced = true
options.onStart?.(reason, previousVersion)
}
const spawnContender = () => {
const [command, ...args] = options.command ?? ["opencode", "serve", "--service"]
if (command === undefined) throw new Error("Missing service command")
try {
const child = spawn(command, args, { detached: true, stdio: "ignore" })
let error: Error | undefined
child.once("error", (cause) => {
error = new Error("Failed to start server", { cause })
})
child.unref()
return { child, error: () => error }
} catch (cause) {
throw new Error("Failed to start server", { cause })
}
}
while (true) {
const registration = await registered(options.file, true)
if (registration.service !== undefined) {
ownerHeld = false
spawnDelay = 5_000
const service = registration.service
const compatible = !service.legacy && (options.version === undefined || service.version === options.version)
if (compatible && service.state === "ready") return service.endpoint
if (compatible && service.state === "failed") throw new Error("Background service failed to start")
if (!compatible) {
announce("version-mismatch", service.version)
await kill(service, options).catch(() => undefined)
lastSpawn = 0
}
} else {
if (lastSpawn === 0 && registration.info !== undefined) lastSpawn = Date.now()
const failure = [...contenders].map(contenderFailure).find((error) => error !== undefined)
if (failure !== undefined) throw failure
const finished = [...contenders].filter(contenderFinished)
if (finished.some((item) => item.child.exitCode === 0)) {
ownerHeld = true
spawnDelay = Math.min(spawnDelay * 2, 30_000)
}
finished.forEach((item) => contenders.delete(item))
// Keep one candidate plus one lock probe so a pre-lock stall cannot block recovery.
if (contenders.size < 2 && Date.now() - lastSpawn >= spawnDelay) {
announce("missing")
contenders.add(spawnContender())
lastSpawn = Date.now()
}
}
await delay(1_000)
}
}
function contenderFailure(contender: Contender) {
const error = contender.error()
if (error !== undefined) return error
if (contender.child.exitCode !== null && contender.child.exitCode !== 0)
return new Error(`Server process exited with code ${contender.child.exitCode}`)
if (contender.child.signalCode !== null)
return new Error(`Server process terminated by ${contender.child.signalCode}`)
return undefined
}
function contenderFinished(contender: Contender) {
return contender.error() !== undefined || contender.child.exitCode !== null || contender.child.signalCode !== null
}
/** Stop the registered local service. */
export async function stop(options: StopOptions = {}) {
const existing = await find(options)
if (existing !== undefined) await kill(existing, options)
}
function fallback() {
return join(process.env["XDG_STATE_HOME"] ?? join(homedir(), ".local", "state"), "opencode", "service.json")
}
/** Create HTTP authentication headers for a service endpoint. */
export function headers(endpoint: Endpoint) {
if (endpoint.auth === undefined) return undefined
return { authorization: "Basic " + Buffer.from(endpoint.auth.username + ":" + endpoint.auth.password).toString("base64") }
}
async function read(file?: string) {
const text = await readFile(file ?? fallback(), "utf8").catch(() => undefined)
if (text === undefined) return undefined
try {
return JSON.parse(text) as Info
} catch {
return undefined
}
}
type LocalService = {
readonly info: Info
readonly endpoint: Endpoint
readonly version?: string
readonly state: "ready" | "waiting" | "failed"
readonly legacy: boolean
}
async function probe(info: Info, allowLegacy = false): Promise<LocalService | undefined> {
const endpoint = {
url: info.url,
auth:
info.password === undefined
? undefined
: { type: "basic" as const, username: "opencode", password: info.password },
} satisfies Endpoint
const response = await fetch(new URL("/api/health", info.url), {
headers: headers(endpoint),
signal: AbortSignal.timeout(2_000),
}).catch(() => undefined)
const body = (await response?.json().catch(() => undefined)) as ServiceHealth | { readonly healthy: true } | undefined
if (body !== undefined && "version" in body && "pid" in body) {
if (body.pid !== info.pid) return undefined
if (info.version !== undefined && body.version !== info.version) return undefined
return {
info,
endpoint,
version: body.version,
state: response?.ok ? "ready" : response?.status === 500 ? "failed" : "waiting",
legacy: false,
}
}
if (!allowLegacy || body?.healthy !== true) return undefined
return { info, endpoint, state: "ready", legacy: true }
}
async function registered(file?: string, allowLegacy = false) {
const info = await read(file)
if (info === undefined) return { info: undefined, service: undefined }
return { info, service: await probe(info, allowLegacy) }
}
async function find(options: { readonly file?: string }) {
return (await registered(options.file, true)).service
}
function signal(pid: number, name: NodeJS.Signals) {
try {
process.kill(pid, name)
} catch {}
}
function stopped(pid: number) {
try {
process.kill(pid, 0)
return false
} catch {
return true
}
}
async function waitUntilStopped(pid: number) {
for (let attempt = 0; attempt <= 100; attempt++) {
if (stopped(pid)) return true
if (attempt < 100) await delay(50)
}
return false
}
function same(left: Info, right: Info) {
return left.id === right.id && left.version === right.version && left.url === right.url && left.pid === right.pid
}
async function kill(service: LocalService, options: { readonly file?: string }) {
const requested = await requestStop(service)
if (requested === "rejected") return
if (requested === "unsupported") {
const current = await find(options)
if (current === undefined || !same(current.info, service.info)) return
signal(service.info.pid, "SIGTERM")
}
if (await waitUntilStopped(service.info.pid)) return
const latest = await find(options)
if (latest === undefined || !same(latest.info, service.info)) return
signal(service.info.pid, "SIGKILL")
if (!(await waitUntilStopped(service.info.pid))) throw new Error(`Server process ${service.info.pid} is still running`)
}
async function requestStop(service: LocalService) {
if (service.info.id === undefined || service.legacy) return "unsupported" as const
const response = await fetch(new URL("/api/service/stop", service.info.url), {
method: "POST",
headers: { ...headers(service.endpoint), "content-type": "application/json" },
body: JSON.stringify({ instanceID: service.info.id }),
signal: AbortSignal.timeout(2_000),
}).catch(() => undefined)
if (response === undefined || response.status === 404 || response.status === 405) return "unsupported" as const
const body = (await response.json().catch(() => undefined)) as ServiceStopResponse | undefined
if (!response.ok || body?.accepted !== true) return "rejected" as const
return "accepted" as const
}
function delay(milliseconds: number) {
return new Promise<void>((resolve) => setTimeout(resolve, milliseconds))
}
/** Promise-based local service lifecycle operations. */
export const Service = { discover, ensure, stop, headers }
-53
View File
@@ -1,53 +0,0 @@
/** Connection details for a local OpenCode service. */
export type Endpoint = {
/** Base URL of the service. */
readonly url: string
/** Authentication required by the service, when configured. */
readonly auth?: {
/** HTTP authentication scheme. */
readonly type: "basic"
/** Basic authentication username. */
readonly username: string
/** Basic authentication password. */
readonly password: string
}
}
/** Options used to discover the local OpenCode service. */
export type DiscoverOptions = {
/** Absolute registration file path. Defaults to the XDG state directory. */
readonly file?: string
/** Required service version. */
readonly version?: string
}
/** Reason ensuring the service requires a new process. */
export type EnsureReason = "missing" | "version-mismatch"
/** Options used to ensure the local OpenCode service is running. */
export type EnsureOptions = DiscoverOptions & {
/** Service command and arguments. Defaults to `opencode serve --service`. */
readonly command?: ReadonlyArray<string>
/** Called once before spawning a new service process. */
readonly onStart?: (reason: EnsureReason, previousVersion?: string) => void
}
/** Options used to stop the local OpenCode service. */
export type StopOptions = {
/** Absolute registration file path. Defaults to the XDG state directory. */
readonly file?: string
}
/** Contents of the local service registration file. */
export type Info = {
/** Unique service instance identifier. */
readonly id?: string
/** OpenCode version served by the process. */
readonly version?: string
/** Base URL advertised by the service. */
readonly url: string
/** Operating system process identifier. */
readonly pid: number
/** Private service password, when authentication is enabled. */
readonly password?: string
}
+2 -2
View File
@@ -15,7 +15,7 @@ import {
const synced = { type: "log.synced" as const, aggregateID: "ses_test", seq: Event.Seq.make(1) }
test("health.get decodes the readiness response", async () => {
test("health.get treats an old server as ready", async () => {
const httpClient = HttpClient.make((request) =>
Effect.succeed(HttpClientResponse.fromWeb(request, Response.json({ healthy: true, version: "old", pid: 123 }))),
)
@@ -24,7 +24,7 @@ test("health.get decodes the readiness response", async () => {
return yield* client.health.get()
}).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise)
expect(result).toEqual({ healthy: true, version: "old", pid: 123 })
expect(result.status).toEqual({ type: "ready" })
})
test("session.get returns the decoded Effect projection", async () => {
+15 -3
View File
@@ -48,11 +48,23 @@ const server = Bun.serve({
}
if (mode === "legacy") return Response.json({ healthy: true })
if (mode === "starting" && !(await Bun.file(registration + ".release").exists()))
return Response.json({ healthy: true, version, pid: process.pid }, { status: 503 })
return Response.json(
{ healthy: true, version, pid: process.pid, instanceID: id, status: { type: "starting" } },
{ status: 503 },
)
if (mode === "failed-owner")
return Response.json({ healthy: true, version, pid: process.pid }, { status: 500 })
return Response.json(
{
healthy: true,
version,
pid: process.pid,
instanceID: id,
status: { type: "failed", message: "Could not open the database.", action: "Check the service logs." },
},
{ status: 503 },
)
if (mode === "starting" || mode === "graceful" || mode === "reject-stop")
return Response.json({ healthy: true, version, pid: process.pid })
return Response.json({ healthy: true, version, pid: process.pid, instanceID: id, status: { type: "ready" } })
return Response.json({ healthy: true, version, pid: process.pid })
},
})
+3 -16
View File
@@ -20,28 +20,15 @@ describe("public import boundaries", () => {
expect(within(root, core)).toEqual([])
expect(within(root, server)).toEqual([])
const network = await bundleInputs("@opencode-ai/client/effect", "browser")
// The effect entry includes local service lifecycle (node spawn/fs), so it
// bundles for bun; the boundary assertions below are what matter.
const network = await bundleInputs("@opencode-ai/client/effect", "bun")
expect(within(network, effect).length).toBeGreaterThan(0)
expect(within(network, schema).length).toBeGreaterThan(0)
expect(within(network, protocol).length).toBeGreaterThan(0)
expect(within(network, core)).toEqual([])
expect(within(network, server)).toEqual([])
const promiseService = await bundleInputs("@opencode-ai/client/service", "bun")
expect(within(promiseService, effect)).toEqual([])
expect(within(promiseService, schema)).toEqual([])
expect(within(promiseService, protocol)).toEqual([])
expect(within(promiseService, core)).toEqual([])
expect(within(promiseService, server)).toEqual([])
const effectService = await bundleInputs("@opencode-ai/client/effect/service", "bun")
expect(within(effectService, effect).length).toBeGreaterThan(0)
expect(within(effectService, protocol).length).toBeGreaterThan(0)
expect(within(effectService, core)).toEqual([])
expect(within(effectService, server)).toEqual([])
})
})
@@ -1,96 +0,0 @@
import { afterEach, expect, test } from "bun:test"
import { mkdtemp, rm } from "node:fs/promises"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { Service, type EnsureReason } from "../src/promise/service"
const fixture = join(import.meta.dir, "fixture/service.ts")
const processes: Bun.Subprocess[] = []
const directories: string[] = []
afterEach(async () => {
processes.forEach((process) => process.kill("SIGTERM"))
await Promise.all(processes.splice(0).map((process) => process.exited))
await Promise.all(directories.splice(0).map((directory) => rm(directory, { recursive: true, force: true })))
})
test("discovers a registered service", async () => {
const registration = await setup("graceful")
expect(await Service.discover({ file: registration, version: "test" })).toEqual(
expect.objectContaining({ url: expect.stringMatching(/^http:\/\//) }),
)
expect(await Service.discover({ file: registration, version: "other" })).toBeUndefined()
})
test("ensures a missing service with native promises", async () => {
const directory = await temp()
const registration = join(directory, "service.json")
const starts: EnsureReason[] = []
const endpoint = await Service.ensure({
file: registration,
version: "test",
command: [process.execPath, fixture, registration, "coordinated"],
onStart: (reason) => starts.push(reason),
})
const info = await Bun.file(registration).json()
try {
expect(endpoint.url).toBe(info.url)
expect(starts).toEqual(["missing"])
} finally {
process.kill(info.pid, "SIGTERM")
await waitForExit(info.pid)
}
}, 15_000)
test("reports a failed registered service", async () => {
const registration = await setup("failed-owner")
await expect(Service.ensure({ file: registration, version: "test", command: [] })).rejects.toThrow(
"Background service failed to start",
)
})
test("requests graceful stop of the exact service instance", async () => {
const registration = await setup("graceful")
const info = await Bun.file(registration).json()
await Service.stop({ file: registration })
expect(await Bun.file(registration + ".stop").json()).toEqual({ instanceID: info.id })
})
async function setup(mode: string) {
const directory = await temp()
const registration = join(directory, "service.json")
processes.push(Bun.spawn([process.execPath, fixture, registration, mode], { stdout: "ignore", stderr: "inherit" }))
await waitForFile(registration)
return registration
}
async function temp() {
const directory = await mkdtemp(join(tmpdir(), "opencode-promise-service-"))
directories.push(directory)
return directory
}
async function waitForFile(file: string) {
for (let attempt = 0; attempt < 600; attempt++) {
if (await Bun.file(file).exists()) return
await Bun.sleep(5)
}
throw new Error(`Timed out waiting for ${file}`)
}
async function waitForExit(pid: number) {
for (let attempt = 0; attempt < 600; attempt++) {
try {
process.kill(pid, 0)
} catch {
return
}
await Bun.sleep(5)
}
throw new Error(`Timed out waiting for process ${pid}`)
}
+5 -29
View File
@@ -36,11 +36,9 @@ test("exposes every standard HTTP API group", () => {
expect(Object.keys(client.debug)).toEqual(["location"])
expect(Object.keys(client.debug.location)).toEqual(["list", "evict"])
expect(Object.keys(client.message)).toEqual(["list"])
expect(Object.keys(client.integration)).toEqual(["list", "get", "wellknown", "connect", "oauth", "command"])
expect(Object.keys(client.integration.wellknown)).toEqual(["add"])
expect(Object.keys(client.integration.connect)).toEqual(["key"])
expect(Object.keys(client.integration.oauth)).toEqual(["connect", "status", "complete", "cancel"])
expect(Object.keys(client.integration.command)).toEqual(["connect", "status", "cancel"])
expect(Object.keys(client.integration)).toEqual(["list", "get", "connect", "attempt"])
expect(Object.keys(client.integration.connect)).toEqual(["key", "oauth"])
expect(Object.keys(client.integration.attempt)).toEqual(["status", "complete", "cancel"])
expect(Object.keys(client.file)).toEqual(["read", "list", "find"])
expect(Object.keys(client.vcs)).toEqual(["status", "diff"])
expect(Object.keys(client.pty)).toEqual(["list", "create", "get", "update", "remove"])
@@ -63,28 +61,6 @@ test("server.get uses the public HTTP contract", async () => {
expect(request?.url).toBe("http://localhost:3000/api/server")
})
test("experimental wellknown integration add uses the public HTTP contract", async () => {
let request: Request | undefined
const client = OpenCode.make({
baseUrl: "http://localhost:3000",
fetch: async (input, init) => {
request = input instanceof Request ? input : new Request(input, init)
return new Response(null, { status: 204 })
},
})
await client.integration.wellknown.add({
url: "https://example.com",
location: { directory: "/tmp/project" },
})
expect(request?.method).toBe("POST")
expect(request?.url).toBe(
"http://localhost:3000/api/experimental/integration/wellknown?location%5Bdirectory%5D=%2Ftmp%2Fproject",
)
expect(await request?.json()).toEqual({ url: "https://example.com" })
})
test("health.stop sends exact replacement identity", async () => {
let request: Request | undefined
const client = OpenCode.make({
@@ -95,10 +71,10 @@ test("health.stop sends exact replacement identity", async () => {
},
})
expect(await client.health.stop({ instanceID: "instance" })).toEqual({ accepted: true })
expect(await client.health.stop({ instanceID: "instance", targetVersion: "next" })).toEqual({ accepted: true })
expect(request?.method).toBe("POST")
expect(request?.url).toBe("http://localhost:3000/api/service/stop")
expect(await request?.json()).toEqual({ instanceID: "instance" })
expect(await request?.json()).toEqual({ instanceID: "instance", targetVersion: "next" })
})
test("MCP resource catalog uses the public HTTP contract", async () => {
+26 -19
View File
@@ -4,7 +4,7 @@ import { Effect } from "effect"
import { mkdtemp, rm, writeFile } from "node:fs/promises"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { Service, type EnsureReason } from "../src/effect/service"
import { Service } from "../src/effect/index"
const fixture = join(import.meta.dir, "fixture/service.ts")
const processes: Bun.Subprocess[] = []
@@ -23,9 +23,9 @@ test("a concurrent same-version start cannot invalidate a resolved endpoint", as
await waitForFile(registration)
const original = await Bun.file(registration).json()
const starts: EnsureReason[] = []
const starts: Service.StartReason[] = []
const first = run(
Service.ensure({
Service.start({
file: registration,
version: "test",
command: [],
@@ -34,7 +34,7 @@ test("a concurrent same-version start cannot invalidate a resolved endpoint", as
)
await waitForFile(registration + ".first-request")
const resolved = await run(Service.ensure({ file: registration, version: "test" }))
const resolved = await run(Service.start({ file: registration, version: "test" }))
expect(resolved.url).toBe(original.url)
await writeFile(registration + ".release", "")
@@ -43,6 +43,7 @@ test("a concurrent same-version start cannot invalidate a resolved endpoint", as
expect(starts).toEqual([])
expect(await Bun.file(registration).json()).toEqual(original)
expect(await health(resolved.url)).toEqual({ healthy: true, version: "test", pid: original.pid })
expect(await run(Service.status({ file: registration }))).toEqual({ type: "ready", version: "test" })
})
test("waits for a registered service to finish starting", async () => {
@@ -50,10 +51,15 @@ test("waits for a registered service to finish starting", async () => {
const registration = join(directory, "service.json")
const process = spawn(registration, "starting")
await waitForFile(registration)
const result = run(Service.ensure({ file: registration, version: "test", command: [] }))
const statuses: Service.Status[] = []
const result = run(
Service.start({ file: registration, version: "test", command: [], onStatus: (status) => statuses.push(status) }),
)
await Bun.sleep(500)
expect(process.exitCode).toBe(null)
expect(statuses).toContainEqual({ type: "starting", version: "test" })
expect(statuses.filter((status) => status.type === "starting")).toHaveLength(1)
await writeFile(registration + ".release", "")
expect((await result).url).toBe((await Bun.file(registration).json()).url)
})
@@ -64,22 +70,23 @@ test("reports a failed registered service without spawning", async () => {
const process = spawn(registration, "failed-owner")
await waitForFile(registration)
await expect(run(Service.ensure({ file: registration, version: "test", command: [] }))).rejects.toThrow(
"Background service failed to start",
)
await expect(run(Service.start({ file: registration, version: "test", command: [] }))).rejects.toMatchObject({
message: "Could not open the database.",
action: "Check the service logs.",
})
expect(process.exitCode).toBe(null)
})
test("requests graceful stop of the exact service instance", async () => {
test("requests graceful replacement of the exact service instance", async () => {
const directory = await temp()
const registration = join(directory, "service.json")
const process = spawn(registration, "graceful")
await waitForFile(registration)
const info = await Bun.file(registration).json()
await run(Service.stop({ file: registration }))
await run(Service.stop({ file: registration }, { targetVersion: "next" }))
await process.exited
expect(await Bun.file(registration + ".stop").json()).toEqual({ instanceID: info.id })
expect(await Bun.file(registration + ".stop").json()).toEqual({ instanceID: info.id, targetVersion: "next" })
})
test("does not spawn contenders while an incompatible service rejects replacement", async () => {
@@ -90,7 +97,7 @@ test("does not spawn contenders while an incompatible service rejects replacemen
await waitForFile(registration)
const controller = new AbortController()
const starting = Effect.runPromise(
Service.ensure({
Service.start({
file: registration,
version: "test",
command: [process.execPath, fixture, contender, "record-start"],
@@ -113,8 +120,8 @@ test("a legacy health response is still replaced", async () => {
const existing = spawn(registration, "legacy")
await waitForFile(registration)
const starts: EnsureReason[] = []
const result = run(Service.ensure({ file: registration, command: [], onStart: (reason) => starts.push(reason) }))
const starts: Service.StartReason[] = []
const result = run(Service.start({ file: registration, command: [], onStart: (reason) => starts.push(reason) }))
await expect(result).rejects.toThrow("Missing service command")
expect(starts).toEqual(["version-mismatch"])
@@ -125,7 +132,7 @@ test("waits for a slow winner while bounding lock probes", async () => {
const directory = await temp()
const registration = join(directory, "service.json")
const endpoint = await run(
Service.ensure({
Service.start({
file: registration,
version: "test",
command: [process.execPath, fixture, registration, "coordinated"],
@@ -146,7 +153,7 @@ test("reports a contender that fails to start", async () => {
const registration = join(directory, "service.json")
await expect(
run(
Service.ensure({
Service.start({
file: registration,
version: "test",
command: [process.execPath, fixture, registration, "failed"],
@@ -160,7 +167,7 @@ test("reports a contender terminated by a signal", async () => {
const registration = join(directory, "service.json")
await expect(
run(
Service.ensure({
Service.start({
file: registration,
version: "test",
command: [process.execPath, fixture, registration, "signal"],
@@ -174,7 +181,7 @@ test("reports a slow contender that eventually fails", async () => {
const registration = join(directory, "service.json")
await expect(
run(
Service.ensure({
Service.start({
file: registration,
version: "test",
command: [process.execPath, fixture, registration, "delayed-failed", "8000"],
@@ -187,7 +194,7 @@ test("replaces an incompatible owner that appears during startup", async () => {
const directory = await temp()
const registration = join(directory, "service.json")
const starting = run(
Service.ensure({
Service.start({
file: registration,
version: "test",
command: [process.execPath, fixture, registration, "delayed", "8000"],
-4
View File
@@ -75,10 +75,6 @@ is decoded before `run` is invoked; an Effect Schema `output` is decoded and cop
Schemas only shape the model-visible signature. Without `output` the signature advertises `Promise<unknown>`.
Descriptions and schemas are model-visible contract; keep authorization in `run`.
Dots in tool names are namespace separators: `{ "issues.list": tool }` exposes `tools.issues.list(...)`, exactly like
`{ issues: { list: tool } }`. Other non-identifier characters render with bracket notation, e.g.
`tools.context7["resolve-library-id"](...)`.
### `CodeMode.execute` and `CodeMode.make`
`CodeMode.execute({ ...options, code })` runs once and is equivalent to `CodeMode.make(options).execute(code)`. A
+97 -73
View File
@@ -4,9 +4,10 @@ This is the checkable support matrix for CodeMode's confined JavaScript interpre
standard-library surface that programs can use today, plus concrete gaps that may be implemented later.
- `[x]` means the feature is implemented at the scope described here.
- `[ ]` means a concrete compatibility gap remains.
- Checked items do not promise complete ECMAScript edge-case parity; known differences are stated explicitly.
- Intentional boundaries are not listed as compatibility work.
- `[ ]` means the feature is unavailable, incomplete, or intentionally divergent as described.
- A checked item does not promise complete ECMAScript edge-case parity. Known differences are listed next to the
supported surface or under [Known semantic gaps](#known-semantic-gaps).
- [Intentional exclusions](#intentional-exclusions) are boundaries, not backlog.
When behavior changes, update this file and the tests in the same change. The implementation and tests remain the
ultimate source of truth.
@@ -18,45 +19,42 @@ ultimate source of truth.
TypeScript is transpiled first; the emitted JavaScript must still use the supported subset.
- [x] Top-level `await` and `return` through the program's implicit async-function scope.
- [x] Explicit `return`, final top-level expression as a REPL-style result, and `null` when no value is produced.
- [x] Program results use JSON-like boundaries, with `undefined` and non-finite numbers normalized to `null`. Tool
arguments remain subject to their schema and the outbound-handling gap listed below.
- [x] JSON-like host boundaries with `undefined` and non-finite numbers normalized to `null`.
- [x] Live Date, RegExp, Map, Set, URL, and URLSearchParams values inside CodeMode.
- [x] Tool calls through the host-provided `tools` tree only.
- [x] The global `search(...)` built-in: synchronous tool discovery that counts as an admitted tool call and is
shadowable by program declarations like other globals.
- [x] Cooperative timeout, an optional total tool-call limit, output bounding, and unrestricted tool-call concurrency.
- [ ] Full JavaScript or TypeScript compatibility. CodeMode is a bounded orchestration language.
## Values and literals
- [x] `null`, `undefined`, booleans, finite and non-finite numbers, and strings.
- [x] Array literals, including holes and spread from arrays, strings, Maps, Sets, and URLSearchParams.
- [x] Object literals with shorthand, computed string/number keys, and spread from plain data objects; `null` and
`undefined` are no-ops, while arrays are rejected.
- [x] Object literals with shorthand, computed string/number keys, and object spread.
- [x] Template literals with interpolation.
- [x] Regular-expression literals.
- [x] `NaN` and `Infinity` globals.
- [ ] BigInt literals and in-interpreter BigInt arithmetic; BigInt remains invalid at JSON-like host boundaries.
- [ ] Symbol primitive values and symbol-keyed properties.
- [ ] Tagged-template calls.
- [ ] Getter and setter definitions in object literals.
- [ ] BigInt literals and values.
- [ ] Symbols.
- [ ] Tagged template literals.
- [ ] Getters and setters in object literals.
## Bindings and destructuring
- [x] `const`, `let`, and accepted `var` declarations.
- [x] Object and array destructuring in declarations, parameters, assignment expressions, and `for...of` bindings.
- [x] Nested patterns, defaults, elisions, and rest elements.
- [x] Assignment to identifiers, unblocked plain-object fields, non-negative integer array indexes, and writable URL
fields.
- [x] Direct function declarations are hoisted in program and block statement lists.
- [x] Assignment to identifiers, object fields, array indexes, and writable URL fields.
- [x] Function declarations are hoisted within their interpreted scope.
- [x] Parameter defaults observe a temporal dead zone for later parameters.
- [ ] JavaScript-correct function scoping, hoisting, and redeclaration for accepted `var` declarations.
- [ ] Predeclare `let` and `const` bindings in every lexical scope, including program/block bodies, switch bodies, and
loop headers, so reads before initialization and self- or cross-referential initializers observe the JavaScript
temporal dead zone.
- [ ] Hoist function declarations accepted directly in switch cases.
- [x] Computed object destructuring keys such as `const { [field]: value } = record`.
- [x] Object destructuring from arrays, such as `const { length } = values`.
- [x] Array destructuring from supported non-array iterables: strings, Maps, Sets, and URLSearchParams.
- [ ] JavaScript-correct `var` function scope, hoisting, and redeclaration. Accepted `var` currently behaves like a
lexical declaration; prefer `let` or `const`.
- [ ] Complete `let`/`const` temporal-dead-zone and declaration-hoisting semantics.
- [ ] Computed object destructuring keys such as `const { [field]: value } = record`.
- [ ] Object destructuring from arrays, such as `const { length } = values`.
- [ ] Iterable array destructuring from Map, Set, string, or URLSearchParams values.
- [ ] Dynamic property deletion with `delete object[key]`.
## Statements and control flow
@@ -71,6 +69,7 @@ ultimate source of truth.
- [x] `throw` with arbitrary values.
- [ ] Labeled statements, labeled `break`, and labeled `continue`.
- [ ] `for await...of` and async iteration.
- [ ] `with` and `debugger` statements.
## Functions and callbacks
@@ -91,15 +90,15 @@ ultimate source of truth.
like JS.
- [x] Tool references and detached `Promise` statics are rejected as callbacks with a hint to wrap them in an
arrow function.
- [ ] Stop automatically awaiting promise-returning string replacers; match JavaScript's synchronous callback-result
coercion.
- [x] Async string replacement callbacks; replacements are evaluated sequentially.
- [x] The optional `thisArg` of iteration methods is accepted and ignored: CodeMode functions have no `this`, so
ignoring it matches JS arrow-function semantics exactly.
- [ ] `this` in non-arrow CodeMode functions and callbacks.
- [ ] User-defined constructor calls.
- [ ] `Function.prototype.call`, `apply`, and `bind` for CodeMode functions.
- [ ] `this`, `super`, user-defined constructor functions, or function prototype methods such as `call`, `apply`,
and `bind`.
- [ ] Classes and private fields.
- [ ] Generator functions and `yield`.
- [ ] Async predicates, reducers, and comparators with automatic awaiting. Async mapping can be joined explicitly with
`Promise.all`, but a promise is not a meaningful predicate or sort result.
## Expressions and operators
@@ -109,16 +108,16 @@ ultimate source of truth.
- [x] Sequence expressions (the comma operator).
- [x] `await` for CodeMode promises; a plain value passes through unchanged, though every `await` still defers its
continuation one reaction turn.
- [x] `new` for Array, Object, Error types, Date, RegExp, Map, Set, URL, URLSearchParams, and Promise.
- [x] `new` for Error types, Date, RegExp, Map, Set, URL, URLSearchParams, and Promise.
- [x] Arithmetic operators: `+`, `-`, `*`, `/`, `%`, and `**`.
- [x] Equality and ordering: `==`, `!=`, `===`, `!==`, `<`, `<=`, `>`, and `>=`.
- [x] Bitwise operators: `&`, `|`, `^`, `~`, `<<`, `>>`, and `>>>`.
- [x] Logical operators: `&&`, `||`, `??`, and `!`, with short-circuiting.
- [x] Unary `+`, unary `-`, `void`, `typeof`, `instanceof`, and own-property-only `in`.
- [x] Unary `+`, unary `-`, `typeof`, `instanceof`, and own-property-only `in`.
- [x] Prefix and postfix `++` and `--`.
- [x] Plain, arithmetic, bitwise, and logical assignment operators.
- [x] Property deletion on plain data objects and arrays, including computed and optional forms; deleting an array index
creates a hole without changing its length.
- [ ] Unary `void` and `delete`.
- [ ] Arbitrary constructors.
## Promises and tools
@@ -153,13 +152,8 @@ ultimate source of truth.
promise itself rejects with a `TypeError`. Resolver callables work anywhere callbacks are accepted, including
`.then`/`.catch` handlers and collection callbacks, but remain opaque references that cannot cross the data
boundary.
- [ ] Thenable assimilation; objects with a callable `then` field remain plain data.
- [x] Dotted tool names are canonicalized into namespace paths; a path can be both callable and a namespace, and the
last definition supplied for a canonical path wins.
- [x] Tool path segments may be named `constructor`, `prototype`, or `__proto__` because paths use inert Map keys.
- [ ] Reject `undefined` and non-finite numbers in outbound tool arguments before render-only and OpenAPI tools run;
retain null normalization for program results and JSON serialization.
- [ ] Tokenize and case-fold non-ASCII tool paths, descriptions, and queries for tool search.
- [ ] Thenable assimilation (objects with a `then` method are plain data, not promises).
- [ ] Async iterables, host streams, and stream consumption.
## Objects and properties
@@ -170,17 +164,18 @@ ultimate source of truth.
- [x] `Object.keys`, `Object.values`, `Object.entries`, `Object.hasOwn`, `Object.assign`, and `Object.fromEntries`.
- [x] `Object.keys` over arrays and tool references.
- [x] Object identity is preserved by in-CodeMode Object helpers.
- [x] Prototype traversal and mutation through `__proto__`, `constructor`, and `prototype` are blocked.
- [ ] Legal own data fields named `__proto__`, `constructor`, or `prototype` are rejected at JSON/tool boundaries and
cannot be created, read, or written in CodeMode; tool path segments with those names remain supported.
- [x] `Object.is` for supported data values.
- [x] Blocked access to `__proto__`, `constructor`, and `prototype`.
- [ ] `Object.is`; runtime and tool-reference identity semantics need to be defined first.
- [ ] `Object.groupBy`.
- [ ] Object creation, descriptors, freezing/sealing, prototype APIs, and reflection APIs.
- [ ] A final policy for legal data/tool keys named `__proto__`, `constructor`, or `prototype`.
## Arrays
- [x] The `Array` constructor with or without `new`: `Array(a, b)` collects arguments and `Array(n)` creates a sparse
array of that length; invalid lengths throw `RangeError`. Iteration, spread, join, and JSON handle holes like
JavaScript, and host results normalize holes to `null`.
- [x] The `Array` constructor with or without `new`: `Array(a, b)` collects arguments, `Array(n)` creates a
sparse array of that length (invalid lengths throw a `RangeError`). Holes behave like JS in iteration,
spread, join, and JSON; `sort` densifies holes into trailing `undefined`, and results returned to the
host normalize holes to `null`.
- [x] Static methods: `Array.isArray`, `Array.of`, and `Array.from`, including the `Array.from` mapper form with
`(value, index)` arguments.
- [x] Iteration/transformation: `map`, `filter`, `flatMap`, and `forEach`.
@@ -194,10 +189,8 @@ ultimate source of truth.
- [x] `length`, numeric indexing, index assignment, spread, and `for...of`.
- [x] The `thisArg` argument of `Array.from` is accepted and ignored, like JS arrows.
- [ ] `Array.prototype.toSpliced`.
- [ ] Canonical array/string index parsing: a key such as `"01"` must remain an ordinary property key rather than
aliasing index `1`.
- [ ] `Array.prototype.sort` and `toSorted` must preserve trailing holes; they currently turn holes into own
`undefined` elements.
- [ ] Canonical index handling: a key such as `"01"` must not alias index `1`.
- [ ] Complete sparse-array parity. Promise combinators do consume holes as `undefined` members, as in JS.
## Strings
@@ -210,8 +203,8 @@ ultimate source of truth.
- [x] `localeCompare`; locale and options arguments are currently ignored.
- [x] `toString`, `length`, numeric indexing, spread, and `for...of` by Unicode code point.
- [x] Static `String.fromCharCode` and `String.fromCodePoint`.
- [ ] Native argument coercion for supported String methods; for example, `includes(1)` and `slice("1")` currently
reject instead of coercing.
- [ ] Locale/options-aware `localeCompare` and locale formatting APIs.
- [ ] Exact native coercion across every string method; CodeMode often requires explicit strings/numbers.
- [ ] Native no-argument parity for `match()` and `search()`.
## Numbers and Math
@@ -227,21 +220,20 @@ ultimate source of truth.
`floor`, `ceil`, `round`, `trunc`, `sign`, `sqrt`, `cbrt`, `pow`, `hypot`, `cos`, `cosh`, `sin`, `sinh`,
`tan`, `tanh`, `log`, `log2`, `log10`, `log1p`, `exp`, `expm1`, `f16round`, `fround`, `clz32`, and `imul`.
- [ ] Native zero-argument behavior for `Number()` and `String()`; they currently do not produce `0` and `""`.
- [ ] `++` and `--` must use CodeMode numeric coercion and reject opaque runtime references; they currently call host
`Number(...)` directly.
- [ ] Unknown static members must read as `undefined` for feature detection; some currently appear callable or throw
during property access.
- [ ] Safe interpreter coercion for `++` and `--` rather than host `Number(...)` coercion.
- [ ] Reliable feature detection for unknown static members.
- [ ] `Math.sumPrecise`.
- [ ] Global coercing `isFinite` and `isNaN`.
## JSON and console
- [x] `JSON.parse` and `JSON.stringify` for supported data objects; the blocked data-key gap listed above still applies.
- [x] `JSON.parse` and `JSON.stringify`.
- [x] Numeric/string indentation for `JSON.stringify`.
- [x] Captured `console.log`, `console.info`, `console.debug`, `console.warn`, and `console.error`.
- [x] Captured `console.dir` and `console.table`.
- [ ] `JSON.parse` reviver callbacks.
- [ ] `JSON.stringify` function/array replacers.
- [ ] Other console methods, timers, counters, groups, and host console access.
## Date
@@ -257,22 +249,22 @@ ultimate source of truth.
- [x] `getTimezoneOffset`, arithmetic, relational comparison, and `instanceof Date`.
- [x] Date values serialize to ISO strings; invalid dates serialize to `null`.
- [ ] Date setters.
- [ ] `Date.prototype.toUTCString` and its `toGMTString` alias.
- [ ] Native one-argument Date coercion; unsupported boolean/object inputs currently become invalid dates instead of
being coerced.
- [ ] Native Date loose-equality and default primitive-coercion semantics.
- [ ] `toUTCString`, locale methods, and other Date formatting methods.
- [ ] Exact native constructor coercion, local-time, and loose-equality semantics.
- [ ] Native `RangeError` branding for invalid `toISOString()` calls.
- [ ] Temporal and Intl date/time APIs.
## Regular expressions
- [x] Literal and `RegExp(pattern, flags)` construction, with or without `new`.
- [x] `test`, `exec`, and `toString`.
- [x] Readable `source`, `flags`, `lastIndex`, `global`, `ignoreCase`, `multiline`, `sticky`, `unicode`, and `dotAll`.
- [x] Captures, safe named groups (blocked member names are omitted), match `.index`, and stateful global matching.
- [x] Integration with supported String methods, including function replacers.
- [x] Captures, named groups, match indexes, and stateful global matching.
- [x] Integration with supported String methods, including async function replacers.
- [ ] Writable `lastIndex`.
- [ ] `hasIndices`, match `indices`, and `unicodeSets` metadata for the `d` and `v` flags.
- [ ] Exposed metadata for the `d` and `v` flags.
- [ ] `RegExp.escape`.
- [ ] Protection from pathological host-regex backtracking beyond the cooperative execution timeout.
## Map and Set
@@ -283,8 +275,9 @@ ultimate source of truth.
- [x] Materialized `keys`, `values`, and `entries` arrays for Map and Set.
- [x] Spread, `for...of`, `Array.from`, and `Object.fromEntries` integration.
- [x] Map and Set values serialize to `{}` at host/JSON boundaries.
- [ ] Set composition and relation methods: `union`, `intersection`, `difference`, `symmetricDifference`, `isSubsetOf`,
`isSupersetOf`, and `isDisjointFrom`.
- [ ] Set composition methods such as `union`, `intersection`, `difference`, and relation predicates.
- [ ] WeakMap and WeakSet.
- [ ] Native iterator objects and custom iterators.
## URL and URI helpers
@@ -307,12 +300,43 @@ ultimate source of truth.
an all-rejected `Promise.any`.
- [x] Error `name`/`message`, error inheritance through `instanceof`, and plain-data serialization.
- [x] `instanceof` for Date, RegExp, Map, Set, URL, URLSearchParams, Array, Object, Promise, and Error types.
- [x] Catchable user throws, runtime failures raised during interpreted evaluation, awaited tool failures, and awaited
tool-call-limit failures; parse/compile failures, cooperative timeout, and output bounding remain outside program
`catch`.
- [x] Source locations on unsupported-syntax diagnostics for JavaScript-shaped input; TypeScript transpilation may
shift them.
- [x] Catchable interpreter failures and awaited tool failures.
- [x] Source locations on unsupported-syntax diagnostics when available.
- [x] Sanitized model-visible diagnostics and explicit safe `ToolError` messages.
- [ ] Distinguish user-thrown failures from interpreter defects and explicit tool refusals from sanitized internal tool
failures; preserve those categories in caught errors, promise rejection handlers, and `Promise.allSettled`
reasons.
- [ ] Distinct public categories for user throws, tool refusal, tool internal failure, invalid returned data, compile
failures, and genuine interpreter defects.
- [ ] Preservation of detailed recoverable failure categories inside `catch` and `Promise.allSettled`.
## Known semantic gaps
These are actionable implementation items. Check them off only when behavior and direct tests land.
- [x] Return real promises from `Promise.all`, `Promise.allSettled`, and `Promise.race`.
- [ ] Guarantee every advertised tool path is executable, including dotted and blocked path segments.
- [ ] Define safe outbound handling for non-finite numbers and `undefined` so invalid values cannot silently become
`null` in render-only or OpenAPI tool calls.
- [ ] Make regular-expression execution genuinely timeout-safe, or narrow the timeout guarantee explicitly.
- [ ] Complete lexical declaration and destructuring semantics listed above.
- [x] Make callback acceptance consistent across built-ins: collections, sort, string replacers, `Array.from`
mappers, and promise reactions share one acceptance rule.
- [x] Reject every unsupported callable callback argument explicitly rather than silently ignoring it
(`JSON.stringify` replacers and `JSON.parse` revivers fail loudly). The iteration-method `thisArg` is
accepted and ignored: CodeMode functions have no `this`, so ignoring it matches JS arrow semantics.
- [ ] Make async callback behavior consistent across built-ins; only string replacers settle async callback results
today.
- [ ] Resolve the built-in correctness gaps listed in the Array, String, Number, Date, and RegExp sections.
- [ ] Make tool search tokenization Unicode-aware.
- [ ] Design explicit tagged representations and size limits before adding binary values or streams.
## Intentional exclusions
These constraints preserve CodeMode's confinement and host-neutral scope. They are not TODO items.
- Ambient filesystem, process, environment, credential, network, or application access.
- `fetch`, timers, crypto, or other host globals unless a future host explicitly supplies a bounded capability.
- Static imports, dynamic imports, modules, npm packages, and module loading.
- `eval`, `Function(...)`, arbitrary host execution, and prototype mutation.
- Generic permission prompts, authorization policy, persistence, replay, or exactly-once side effects.
- Arbitrary method dispatch outside the documented allowlists.
- Automatic parsing of text tool results as JSON.
- Full browser, Node.js, Bun, or ECMAScript runtime compatibility.
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/codemode",
"version": "1.18.2",
"version": "1.17.20",
"description": "Effect-native confined code execution over schema-described tools",
"private": true,
"type": "module",
+29 -57
View File
@@ -34,51 +34,30 @@ export const isRuntimeReference = (value: unknown): boolean =>
value instanceof ErrorConstructorReference ||
isCodeModeValue(value)
function* childValues(value: object): Generator<unknown> {
if (Array.isArray(value)) {
const length = value.length
for (let index = 0; index < length; index++) yield value[index]
return
}
yield* Object.values(value)
}
export const containsRuntimeReference = (value: unknown): boolean => {
const pending: Array<Iterator<unknown>> = [[value].values()]
const seen = new Set<object>()
while (pending.length > 0) {
const next = pending.at(-1)!.next()
if (next.done) {
pending.pop()
continue
}
const current = next.value
if (isRuntimeReference(current)) return true
if (current === null || typeof current !== "object" || seen.has(current)) continue
seen.add(current)
pending.push(childValues(current))
}
return false
export const containsRuntimeReference = (value: unknown, seen = new Set<object>()): boolean => {
if (isRuntimeReference(value)) return true
if (value === null || typeof value !== "object") return false
if (seen.has(value)) return false
seen.add(value)
const contains = Array.isArray(value)
? value.some((item) => containsRuntimeReference(item, seen))
: Object.values(value).some((item) => containsRuntimeReference(item, seen))
seen.delete(value)
return contains
}
// CodeMode values are data here, not opaque interpreter references.
export const containsOpaqueReference = (value: unknown): boolean => {
const pending: Array<Iterator<unknown>> = [[value].values()]
const seen = new Set<object>()
while (pending.length > 0) {
const next = pending.at(-1)!.next()
if (next.done) {
pending.pop()
continue
}
const current = next.value
if (isCodeModeValue(current)) continue
if (isRuntimeReference(current)) return true
if (current === null || typeof current !== "object" || seen.has(current)) continue
seen.add(current)
pending.push(childValues(current))
}
return false
export const containsOpaqueReference = (value: unknown, seen = new Set<object>()): boolean => {
if (isCodeModeValue(value)) return false
if (isRuntimeReference(value)) return true
if (value === null || typeof value !== "object") return false
if (seen.has(value)) return false
seen.add(value)
const contains = Array.isArray(value)
? value.some((item) => containsOpaqueReference(item, seen))
: Object.values(value).some((item) => containsOpaqueReference(item, seen))
seen.delete(value)
return contains
}
// Reject cycles before mutation so later boundary walks remain safe.
@@ -87,22 +66,15 @@ export const rejectCircularInsertion = (
value: unknown,
label: string,
node: AstNode,
seen = new Set<object>(),
): void => {
const pending: Array<Iterator<unknown>> = [[value].values()]
const seen = new Set<object>()
while (pending.length > 0) {
const next = pending.at(-1)!.next()
if (next.done) {
pending.pop()
continue
}
const current = next.value
if (current === container)
throw new InterpreterRuntimeError(`${label} contains a circular value.`, node, "InvalidDataValue")
if (current === null || typeof current !== "object" || isRuntimeReference(current) || seen.has(current)) continue
seen.add(current)
pending.push(Array.isArray(current) ? current[Symbol.iterator]() : childValues(current))
}
if (value === container)
throw new InterpreterRuntimeError(`${label} contains a circular value.`, node, "InvalidDataValue")
if (value === null || typeof value !== "object" || isRuntimeReference(value) || seen.has(value)) return
seen.add(value)
const items = Array.isArray(value) ? value : Object.values(value)
for (const item of items) rejectCircularInsertion(container, item, label, node, seen)
seen.delete(value)
}
export const typeofValue = (value: unknown): string => {
+42 -79
View File
@@ -777,9 +777,9 @@ export class Interpreter<R> {
}
if (pattern.type === "ObjectPattern") {
if (value === null || typeof value !== "object" || isRuntimeReference(value)) {
if (value === null || typeof value !== "object" || Array.isArray(value) || isRuntimeReference(value)) {
throw new InterpreterRuntimeError(
"Object destructuring requires a data object or array value.",
"Object destructuring requires a data object value.",
pattern,
"InvalidDataValue",
)
@@ -798,35 +798,38 @@ export class Interpreter<R> {
continue
}
const key = yield* self.destructuringPropertyKey(property)
if (isBlockedMember(String(key))) {
throw new InterpreterRuntimeError(`Property '${String(key)}' is not available in CodeMode.`, property)
if (
property.type !== "Property" ||
getBoolean(property, "computed") ||
getString(property, "kind") !== "init"
) {
throw new InterpreterRuntimeError("Only named object destructuring properties are supported.", property)
}
consumed.add(String(key))
yield* self.declarePattern(
getNode(property, "value"),
self.destructuringPropertyValue(value as SafeObject | Array<unknown>, key),
mutable,
property,
)
const keyNode = getNode(property, "key")
const key = keyNode.type === "Identifier" ? getString(keyNode, "name") : String(keyNode.value)
if (isBlockedMember(key)) {
throw new InterpreterRuntimeError(`Property '${key}' is not available in CodeMode.`, keyNode)
}
consumed.add(key)
yield* self.declarePattern(getNode(property, "value"), (value as SafeObject)[key], mutable, property)
}
return
}
if (pattern.type === "ArrayPattern") {
const items = spreadItems(value)
if (items === undefined) {
throw new InterpreterRuntimeError("Array destructuring requires a supported iterable value.", pattern)
if (!Array.isArray(value)) {
throw new InterpreterRuntimeError("Array destructuring requires an array value.", pattern)
}
for (const [index, item] of getArray(pattern, "elements").entries()) {
if (item === null) continue
const element = asNode(item, `elements[${index}]`)
if (element.type === "RestElement") {
yield* self.declarePattern(getNode(element, "argument"), items.slice(index), mutable, element)
yield* self.declarePattern(getNode(element, "argument"), value.slice(index), mutable, element)
break
}
yield* self.declarePattern(element, items[index], mutable, pattern)
yield* self.declarePattern(element, value[index], mutable, pattern)
}
return
}
@@ -855,15 +858,15 @@ export class Interpreter<R> {
}
if (pattern.type === "ObjectPattern") {
if (value === null || typeof value !== "object" || isRuntimeReference(value)) {
if (value === null || typeof value !== "object" || Array.isArray(value) || isRuntimeReference(value)) {
throw new InterpreterRuntimeError(
"Object destructuring requires a data object or array value.",
"Object destructuring requires a data object value.",
pattern,
"InvalidDataValue",
)
}
const source = value as SafeObject | Array<unknown>
const source = value as SafeObject
const consumed = new Set<string>()
for (const propertyValue of getArray(pattern, "properties")) {
const property = asNode(propertyValue, "properties")
@@ -875,29 +878,36 @@ export class Interpreter<R> {
yield* self.assignPattern(getNode(property, "argument"), rest, property)
continue
}
const key = yield* self.destructuringPropertyKey(property)
if (isBlockedMember(String(key))) {
throw new InterpreterRuntimeError(`Property '${String(key)}' is not available in CodeMode.`, property)
if (
property.type !== "Property" ||
getBoolean(property, "computed") ||
getString(property, "kind") !== "init"
) {
throw new InterpreterRuntimeError("Only named object destructuring properties are supported.", property)
}
consumed.add(String(key))
yield* self.assignPattern(getNode(property, "value"), self.destructuringPropertyValue(source, key), property)
const keyNode = getNode(property, "key")
const key = keyNode.type === "Identifier" ? getString(keyNode, "name") : String(keyNode.value)
if (isBlockedMember(key)) {
throw new InterpreterRuntimeError(`Property '${key}' is not available in CodeMode.`, keyNode)
}
consumed.add(key)
yield* self.assignPattern(getNode(property, "value"), source[key], property)
}
return
}
if (pattern.type === "ArrayPattern") {
const items = spreadItems(value)
if (items === undefined) {
throw new InterpreterRuntimeError("Array destructuring requires a supported iterable value.", pattern)
if (!Array.isArray(value)) {
throw new InterpreterRuntimeError("Array destructuring requires an array value.", pattern)
}
for (const [index, item] of getArray(pattern, "elements").entries()) {
if (item === null) continue
const element = asNode(item, `elements[${index}]`)
if (element.type === "RestElement") {
yield* self.assignPattern(getNode(element, "argument"), items.slice(index), element)
yield* self.assignPattern(getNode(element, "argument"), value.slice(index), element)
break
}
yield* self.assignPattern(element, items[index], pattern)
yield* self.assignPattern(element, value[index], pattern)
}
return
}
@@ -906,26 +916,6 @@ export class Interpreter<R> {
})
}
private destructuringPropertyKey(property: AstNode): Effect.Effect<string | number, unknown, R> {
if (property.type !== "Property" || getString(property, "kind") !== "init") {
throw new InterpreterRuntimeError("Unsupported object destructuring property.", property)
}
const keyNode = getNode(property, "key")
if (getBoolean(property, "computed")) {
return Effect.map(this.evaluateExpression(keyNode), (value) => this.toPropertyKey(value, keyNode))
}
return Effect.succeed(keyNode.type === "Identifier" ? getString(keyNode, "name") : String(keyNode.value))
}
private destructuringPropertyValue(source: SafeObject | Array<unknown>, key: string | number): unknown {
if (!Array.isArray(source)) return source[String(key)]
if (key === "length") return source.length
if (typeof key === "number") return source[key]
if (Object.hasOwn(source, key)) return (source as Record<string, unknown> & Array<unknown>)[key]
if (arrayMethods.has(key)) return new IntrinsicReference(source, key)
return undefined
}
private evaluateExpression(node: AstNode): Effect.Effect<unknown, unknown, R> {
switch (node.type) {
case "Literal": {
@@ -1289,7 +1279,6 @@ export class Interpreter<R> {
private evaluateUnaryExpression(node: AstNode): Effect.Effect<unknown, unknown, R> {
const operator = getString(node, "operator")
const argument = getNode(node, "argument")
if (operator === "delete") return this.evaluateDeleteExpression(argument)
// Undeclared names short-circuit, but declared TDZ bindings must still throw.
if (operator === "typeof" && argument.type === "Identifier" && !this.scopes.resolve(getString(argument, "name"))) {
return Effect.succeed("undefined")
@@ -1297,7 +1286,6 @@ export class Interpreter<R> {
return Effect.map(this.evaluateExpression(argument), (value) => {
if (operator === "typeof") return typeofValue(value)
if (operator === "!") return !value
if (operator === "void") return undefined
if (containsOpaqueReference(value)) {
throw new InterpreterRuntimeError("Unary operators require data values in CodeMode.", node, "InvalidDataValue")
}
@@ -1741,7 +1729,6 @@ export class Interpreter<R> {
private getMemberReference(
node: AstNode,
operation: "read" | "delete" = "read",
): Effect.Effect<
| MemberReference
| ToolReference
@@ -1772,8 +1759,8 @@ export class Interpreter<R> {
: self.toPropertyKey(yield* self.evaluateExpression(propertyNode), propertyNode)
if (objectValue instanceof ToolReference) {
if (typeof key !== "string") {
throw new InterpreterRuntimeError("Tool paths must use string property names.", propertyNode)
if (typeof key !== "string" || isBlockedMember(key)) {
throw new InterpreterRuntimeError("Tool paths must use safe string property names.", propertyNode)
}
return new ToolReference([...objectValue.path, key])
}
@@ -1888,7 +1875,6 @@ export class Interpreter<R> {
}
if (Array.isArray(objectValue)) {
if (operation === "delete") return { target: objectValue, key }
if (
key !== "length" &&
!(typeof key === "string" && arrayMethods.has(key)) &&
@@ -1937,29 +1923,6 @@ export class Interpreter<R> {
return this.modifyMember(node, () => Effect.succeed({ write: true, next: value, result: value }))
}
private evaluateDeleteExpression(argument: AstNode): Effect.Effect<boolean, unknown, R> {
const target = argument.type === "ChainExpression" ? getNode(argument, "expression") : argument
if (target.type !== "MemberExpression") {
throw new InterpreterRuntimeError("Only data fields may be deleted in CodeMode.", argument)
}
return Effect.map(this.getMemberReference(target, "delete"), (reference) => {
if (reference === OptionalShortCircuit) return true
if (
reference instanceof ComputedValue ||
reference === undefined ||
reference instanceof ToolReference ||
reference instanceof PromiseMethodReference ||
reference instanceof PromiseInstanceMethodReference ||
reference instanceof IntrinsicReference ||
reference instanceof GlobalMethodReference ||
reference.target instanceof CodeModeURL
) {
throw new InterpreterRuntimeError("Only data fields may be deleted in CodeMode.", target, "InvalidDataValue")
}
return Reflect.deleteProperty(reference.target, reference.key)
})
}
// Resolve side-effecting object and key expressions exactly once.
private modifyMember(
node: AstNode,
-6
View File
@@ -1,5 +1,4 @@
import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js"
import { containsOpaqueReference } from "../interpreter/references.js"
import { isBlockedMember } from "../tool-runtime.js"
import { isCodeModeValue, CodeModeMap, CodeModePromise, CodeModeSet, CodeModeURLSearchParams } from "../values.js"
import { boundedData, coerceToString } from "./value.js"
@@ -45,11 +44,6 @@ export const invokeObjectMethod = (name: string, args: Array<unknown>, node: Ast
return Object.entries(requireObject()).map(([key, item]) => [key, item])
case "hasOwn":
return Object.hasOwn(requireObject(), String(args[1]))
case "is":
if (containsOpaqueReference(args[0]) || containsOpaqueReference(args[1])) {
throw new InterpreterRuntimeError("Object.is requires data values in CodeMode.", node, "InvalidDataValue")
}
return Object.is(args[0], args[1])
case "assign": {
const target = args[0]
if (target === null || typeof target !== "object" || Array.isArray(target) || isCodeModeValue(target)) {
+37 -57
View File
@@ -274,41 +274,15 @@ export const copyOut = (value: unknown, undefinedAsNull = false): unknown => {
return value
}
// Dots in tool names are namespace separators; the last definition for a canonical path wins.
type ToolNode<R> = {
definition?: Definition<R>
readonly children: Map<string, ToolNode<R>>
}
const toolTrie = <R>(tools: Tools<R>): ToolNode<R> => {
const root: ToolNode<R> = { children: new Map() }
const insert = (node: ToolNode<R>, group: Tools<R>): void => {
for (const [name, value] of Object.entries(group)) {
let current = node
for (const segment of name.split(".")) {
if (segment === "") throw new TypeError(`Tool name '${name}' contains an empty segment.`)
const child = current.children.get(segment) ?? { children: new Map() }
current.children.set(segment, child)
current = child
}
if (isDefinition(value)) current.definition = value
else insert(current, value)
}
}
insert(root, tools)
return root
}
const canonicalSegments = (path: ReadonlyArray<string>): ReadonlyArray<string> =>
path.flatMap((segment) => segment.split("."))
const definitions = <R>(
node: ToolNode<R>,
tools: Tools<R>,
path: ReadonlyArray<string> = [],
): Array<{ path: string; definition: Definition<R> }> => [
...(node.definition === undefined ? [] : [{ path: path.join("."), definition: node.definition }]),
...Array.from(node.children, ([name, child]) => definitions(child, [...path, name])).flat(),
]
): Array<{ path: string; definition: Definition<R> }> =>
Object.entries(tools).flatMap(([name, value]) => {
const next = [...path, name]
if (isDefinition(value)) return [{ path: next.join("."), definition: value }]
return definitions(value, next)
})
const describeDefinition = <R>(path: string, definition: Definition<R>): ToolDescription => ({
path,
@@ -317,7 +291,7 @@ const describeDefinition = <R>(path: string, definition: Definition<R>): ToolDes
})
const visibleDefinitions = <R>(tools: Tools<R>) =>
definitions(toolTrie(tools)).map(({ path, definition }) => ({
definitions(tools).map(({ path, definition }) => ({
path,
definition,
description: describeDefinition(path, definition),
@@ -581,30 +555,37 @@ export const prepare = <R>(tools: Tools<R>, catalogBudget = defaultCatalogBudget
}
}
const lookup = <R>(root: ToolNode<R>, segments: ReadonlyArray<string>): ToolNode<R> | undefined =>
segments.reduce<ToolNode<R> | undefined>((node, segment) => node?.children.get(segment), root)
const namespaceKeys = <R>(root: ToolNode<R>, path: ReadonlyArray<string>): ReadonlyArray<string> => {
const segments = canonicalSegments(path)
const node = lookup(root, segments)
if (node === undefined) {
throw new ToolRuntimeError("UnknownTool", `Unknown tool namespace '${segments.join(".")}'.`)
const namespaceKeys = <R>(tools: Tools<R>, path: ReadonlyArray<string>): ReadonlyArray<string> => {
let value: Definition<R> | Tools<R> = tools
for (const segment of path) {
if (isBlockedMember(segment) || isDefinition(value) || !Object.hasOwn(value, segment)) {
throw new ToolRuntimeError("UnknownTool", `Unknown tool namespace '${path.join(".")}'.`, [
"Object.keys(tools) lists the available namespaces; search({ query }) finds described tools.",
])
}
value = value[segment] as Definition<R> | Tools<R>
}
return Array.from(node.children.keys())
if (isDefinition(value)) return []
return Object.keys(value)
}
const resolve = <R>(root: ToolNode<R>, path: ReadonlyArray<string>): Definition<R> => {
const segments = canonicalSegments(path)
const node = lookup(root, segments)
if (node === undefined) {
throw new ToolRuntimeError("UnknownTool", `Unknown tool '${segments.join(".")}'.`, [
"Use search({ query }) to find available described tools.",
])
const resolve = <R>(tools: Tools<R>, path: ReadonlyArray<string>): Definition<R> => {
let value: Definition<R> | Tools<R> = tools
for (const segment of path) {
if (isBlockedMember(segment) || isDefinition(value) || !Object.hasOwn(value, segment)) {
throw new ToolRuntimeError("UnknownTool", `Unknown tool '${path.join(".")}'.`, [
"Use search({ query }) to find available described tools.",
])
}
value = value[segment] as Definition<R> | Tools<R>
}
if (node.definition === undefined) {
throw new ToolRuntimeError("UnknownTool", `Tool '${segments.join(".")}' is not callable.`)
if (!isDefinition(value)) {
throw new ToolRuntimeError("UnknownTool", `Tool '${path.join(".")}' is not callable.`)
}
return node.definition
return value
}
export type ToolRuntime<R = never> = {
@@ -622,7 +603,6 @@ export const make = <R>(
hooks?: ToolCallHooks<R>,
): ToolRuntime<R> => {
const calls: Array<ToolCall> = []
const root = toolTrie(tools)
const searchTool = makeSearchTool(searchIndex)
// End hooks observe settled success or failure; interruption emits neither outcome.
@@ -690,7 +670,7 @@ export const make = <R>(
return {
root: new ToolReference([]),
calls,
keys: (path) => namespaceKeys(root, path),
keys: (path) => namespaceKeys(tools, path),
search: (args) =>
Effect.suspend(() =>
invokeDefinition(
@@ -701,9 +681,9 @@ export const make = <R>(
),
invoke: (path, args) =>
Effect.gen(function* () {
const name = canonicalSegments(path).join(".")
const name = path.join(".")
const externalArgs = args.map((arg) => copyOut(copyIn(arg, `Arguments for tool '${name}'`)))
const tool = resolve(root, path)
const tool = resolve(tools, path)
return yield* invokeDefinition(name, tool, externalArgs)
}),
}
+1 -6
View File
@@ -50,13 +50,8 @@ export type Options<I extends SchemaType, O extends SchemaType | undefined, R =
readonly run: (input: InputType<I>) => Effect.Effect<ResultType<O>, unknown, R>
}
// Object.hasOwn: an inherited _tag must not classify a namespace as a Definition.
export const isDefinition = <R = never>(value: unknown): value is Definition<R> =>
typeof value === "object" &&
value !== null &&
"_tag" in value &&
Object.hasOwn(value, "_tag") &&
value._tag === "CodeModeTool"
typeof value === "object" && value !== null && "_tag" in value && value._tag === "CodeModeTool"
/**
* Defines one schema-described tool available to a CodeMode program through `tools.*`.
+2 -1
View File
@@ -56,10 +56,11 @@ describe("Object.keys over tool references", () => {
expect(await value(`return typeof search`)).toBe("function")
})
test("an unknown namespace is an UnknownTool error", async () => {
test("an unknown namespace is an UnknownTool error pointing at the discovery idioms", async () => {
const failure = await error(`return Object.keys(tools.nonexistent)`)
expect(failure.kind).toBe("UnknownTool")
expect(failure.message).toContain("Unknown tool namespace 'nonexistent'")
expect(failure.suggestions?.join(" ")).toContain("Object.keys(tools)")
})
test("Object.values/entries on a tool reference explain the working idioms", async () => {
-145
View File
@@ -99,84 +99,6 @@ describe("H4: typeof on an undeclared identifier is 'undefined'", () => {
})
})
describe("unary void", () => {
test("evaluates its operand and returns undefined", async () => {
expect(await value(`let count = 0; const result = void (count += 1); return [count, result === undefined]`)).toEqual([
1,
true,
])
})
test("discards opaque values", async () => {
expect(await value(`return void tools === undefined`)).toBe(true)
})
})
describe("property deletion", () => {
test("deletes plain object fields and reports missing fields as successful", async () => {
expect(
await value(`
const object = { keep: 1, remove: 2 }
return [delete object.remove, delete object.missing, object]
`),
).toEqual([true, true, { keep: 1 }])
})
test("evaluates computed object and key expressions once", async () => {
expect(
await value(`
const object = { remove: true }
let objectReads = 0
let keyReads = 0
function getObject() { objectReads++; return object }
function getKey() { keyReads++; return "remove" }
const removed = delete getObject()[getKey()]
return [removed, objectReads, keyReads, Object.hasOwn(object, "remove")]
`),
).toEqual([true, 1, 1, false])
})
test("deleting an array index creates a hole without changing its length", async () => {
expect(await value(`const values = [1, 2, 3]; const removed = delete values[1]; return [removed, values.length, 1 in values, values]`)).toEqual([
true,
3,
false,
[1, null, 3],
])
})
test("array length is not configurable", async () => {
expect(await value(`const values = [1, 2]; return [delete values.length, values.length]`)).toEqual([false, 2])
})
test("does not broaden unsupported array property assignment", async () => {
expect(
await value(`
const values = []
let rightHandSideRuns = 0
function next() { rightHandSideRuns++; return 1 }
try { values.field = next() } catch {}
return rightHandSideRuns
`),
).toBe(0)
})
test("optional deletion short-circuits without evaluating the key", async () => {
expect(
await value(`let keyReads = 0; const object = null; return [delete object?.[keyReads++], keyReads]`),
).toEqual([true, 0])
})
test("rejects deletion from opaque runtime references", async () => {
expect((await error(`return delete tools.example`)).kind).toBe("InvalidDataValue")
})
test("keeps blocked property names unavailable", async () => {
expect((await error(`const object = {}; return delete object.__proto__`)).kind).toBe("ExecutionFailure")
expect((await error(`const values = []; return delete values["constructor"]`)).kind).toBe("ExecutionFailure")
})
})
describe("H1: NaN/Infinity flow as intermediates and normalize to null at the boundary", () => {
test("guards run instead of the program crashing on a transient NaN", async () => {
expect(await value(`return parseInt("abc") || 0`)).toBe(0)
@@ -548,71 +470,4 @@ describe("destructuring assignment", () => {
test("returns the assigned value", async () => {
expect(await value(`let a = 0; const result = ([a] = [7]); return [a, result]`)).toEqual([7, [7]])
})
test("supports computed object keys and evaluates them once", async () => {
expect(
await value(`
let calls = 0
const field = () => { calls++; return "name" }
const { [field()]: name, ...rest } = { name: "Ada", role: "engineer" }
return { calls, name, rest }
`),
).toEqual({ calls: 1, name: "Ada", rest: { role: "engineer" } })
})
test("supports object patterns over arrays", async () => {
expect(
await value(`
const { 0: first, length, slice, ...rest } = ["a", "b", "c"]
return { first, length, sliced: slice(1), rest }
`),
).toEqual({ first: "a", length: 3, sliced: ["b", "c"], rest: { 1: "b", 2: "c" } })
})
test("preserves exact computed property names on arrays", async () => {
expect(
await value(`
const { ["01"]: item, ...rest } = [10, 20]
return { missing: item === undefined, rest }
`),
).toEqual({ missing: true, rest: { 0: 10, 1: 20 } })
})
test("supports array patterns over strings, Maps, Sets, and URLSearchParams", async () => {
expect(
await value(`
const [letter, ...letters] = "A😀B"
const [[mapKey, mapValue]] = new Map([["key", 1]])
const [setFirst, setSecond] = new Set([2, 3])
const [[queryKey, queryValue]] = new URLSearchParams("q=test&page=2")
return { letter, letters, mapKey, mapValue, setFirst, setSecond, queryKey, queryValue }
`),
).toEqual({
letter: "A",
letters: ["😀", "B"],
mapKey: "key",
mapValue: 1,
setFirst: 2,
setSecond: 3,
queryKey: "q",
queryValue: "test",
})
})
test("supports iterable patterns in assignment and parameters", async () => {
expect(
await value(`
let first
let rest
;[first, ...rest] = new Set([1, 2, 3])
const read = ([[key, value]]) => key + value
return { first, rest, entry: read(new Map([["a", 4]])) }
`),
).toEqual({ first: 1, rest: [2, 3], entry: "a4" })
})
test("rejects computed keys that are not confined property keys", async () => {
const err = await error(`const key = {}; const { [key]: value } = {}`)
expect(err.message).toContain("Property key must be a string or number")
})
})
-18
View File
@@ -593,24 +593,6 @@ describe("Set", () => {
})
describe("stdlib integration", () => {
test("Object.is uses SameValue semantics", async () => {
expect(
await value(`
const object = {}
return [
Object.is(NaN, NaN),
Object.is(0, -0),
Object.is(object, object),
Object.is({}, {}),
]
`),
).toEqual([true, false, true, false])
})
test("Object.is rejects opaque runtime references", async () => {
expect((await error(`return Object.is(Math.max, Math.max)`)).kind).toBe("InvalidDataValue")
})
test("Object values and entries accept arrays", async () => {
expect(await value(`return [Object.values(["a", "b"]), Object.entries(["a", "b"])]`)).toEqual([
["a", "b"],
-164
View File
@@ -1,164 +0,0 @@
import { describe, expect, test } from "bun:test"
import { Effect, Schema } from "effect"
import { CodeMode, Tool } from "../src/index.js"
const echo = (description: string, result: string) =>
Tool.make({
description,
input: Schema.Struct({}),
output: Schema.String,
run: () => Effect.succeed(result),
})
const value = async (runtime: CodeMode.Runtime, code: string) => {
const result = await Effect.runPromise(runtime.execute(code))
if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`)
return result.value
}
const failure = async (runtime: CodeMode.Runtime, code: string) => {
const result = await Effect.runPromise(runtime.execute(code))
if (result.ok) throw new Error(`expected failure, got value ${JSON.stringify(result.value)}`)
return result.error
}
describe("dotted tool names", () => {
const runtime = CodeMode.make({ tools: { api: { "issues.list": echo("List issues", "listed") } } })
test("a dotted name becomes nested namespaces in the catalog", () => {
const catalog = runtime.catalog()
expect(catalog).toHaveLength(1)
expect(catalog[0]?.path).toBe("api.issues.list")
expect(catalog[0]?.signature).toStartWith("tools.api.issues.list(input:")
expect(runtime.instructions()).toContain("tools.api.issues.list(input:")
})
test("the advertised dotted path is executable", async () => {
expect(await value(runtime, `return await tools.api.issues.list({})`)).toBe("listed")
})
test("bracket access with a dotted segment spells the same canonical path", async () => {
expect(await value(runtime, `return await tools.api["issues.list"]({})`)).toBe("listed")
expect(await value(runtime, `return await tools["api.issues"].list({})`)).toBe("listed")
})
test("intermediate segments enumerate like ordinary namespaces", async () => {
expect(await value(runtime, `return [Object.keys(tools.api), Object.keys(tools.api.issues)]`)).toEqual([
["issues"],
["list"],
])
expect(await value(runtime, `return Object.keys(tools["api.issues"])`)).toEqual(["list"])
})
test("a top-level dotted name nests from the root", async () => {
const flat = CodeMode.make({ tools: { "issues.list": echo("List issues", "flat") } })
expect(flat.catalog()[0]?.path).toBe("issues.list")
expect(await value(flat, `return await tools.issues.list({})`)).toBe("flat")
})
})
describe("callable namespaces", () => {
const runtime = CodeMode.make({
tools: { issues: echo("All issues", "all"), "issues.list": echo("List issues", "list") },
})
test("a path can hold a tool and child tools at once", async () => {
expect(await value(runtime, `return await tools.issues({})`)).toBe("all")
expect(await value(runtime, `return await tools.issues.list({})`)).toBe("list")
expect(runtime.catalog().map((tool) => tool.path)).toEqual(["issues", "issues.list"])
})
test("a callable namespace enumerates its children", async () => {
expect(await value(runtime, `return Object.keys(tools.issues)`)).toEqual(["list"])
})
test("search returns executable paths for both", async () => {
const result = await value(runtime, `return search({ query: "", namespace: "issues" })`)
expect((result as { items: Array<{ path: string }> }).items.map((item) => item.path)).toEqual([
"tools.issues",
"tools.issues.list",
])
const exact = await value(runtime, `return search({ query: "tools.issues.list" })`)
expect((exact as { items: Array<{ path: string }> }).items.map((item) => item.path)).toEqual(["tools.issues.list"])
})
test("an unknown child under a callable tool is an UnknownTool error", async () => {
const diagnostic = await failure(runtime, `return await tools.issues.missing({})`)
expect(diagnostic.kind).toBe("UnknownTool")
expect(diagnostic.message).toContain("Unknown tool 'issues.missing'")
})
test("a namespace without its own definition stays non-callable", async () => {
const nested = CodeMode.make({ tools: { "issues.list": echo("List issues", "list") } })
const diagnostic = await failure(nested, `return await tools.issues({})`)
expect(diagnostic.kind).toBe("UnknownTool")
expect(diagnostic.message).toContain("Tool 'issues' is not callable")
})
})
describe("blocked member names on tool paths", () => {
const runtime = CodeMode.make({
tools: {
prototype: echo("Prototype tool", "proto"),
"issues.constructor": echo("Constructor tool", "ctor"),
nested: { ["__proto__"]: echo("Proto tool", "dunder") },
},
})
test("tools may use blocked member names because path segments never touch real properties", async () => {
expect(runtime.catalog().map((tool) => tool.path)).toEqual(["prototype", "issues.constructor", "nested.__proto__"])
expect(await value(runtime, `return await tools.prototype({})`)).toBe("proto")
expect(await value(runtime, `return await tools.issues.constructor({})`)).toBe("ctor")
expect(await value(runtime, `return await tools["issues.constructor"]({})`)).toBe("ctor")
expect(await value(runtime, `return await tools.nested.__proto__({})`)).toBe("dunder")
expect(await value(runtime, `return Object.keys(tools.issues)`)).toEqual(["constructor"])
})
test("a literal __proto__ key cannot poison a namespace into a fake definition", async () => {
const poisoned = CodeMode.make({
tools: { ns: { "__proto__": echo("Hidden", "hidden"), real: echo("Real tool", "real") } },
})
expect(poisoned.catalog().map((tool) => tool.path)).toEqual(["ns.real"])
expect(await value(poisoned, `return await tools.ns.real({})`)).toBe("real")
})
test("blocked member access on data values stays blocked", async () => {
const diagnostic = await failure(runtime, `const x = {}; return x.constructor`)
expect(diagnostic.message).toContain("constructor")
expect(Object.keys(Object.prototype)).toEqual([])
})
})
describe("empty segments", () => {
test("tool names with empty segments are rejected at make", () => {
for (const name of ["", "a..b", "trail.", ".lead"]) {
expect(() => CodeMode.make({ tools: { [name]: echo("Bad", "bad") } })).toThrow("empty segment")
}
})
})
describe("canonical path collisions", () => {
test("the last definition supplied for a canonical path wins", async () => {
const runtime = CodeMode.make({
tools: { "issues.list": echo("First", "first"), issues: { list: echo("Second", "second") } },
})
expect(await value(runtime, `return await tools.issues.list({})`)).toBe("second")
expect(runtime.catalog()).toHaveLength(1)
expect(runtime.catalog()[0]?.description).toBe("Second")
})
test("overriding one path keeps sibling tools from both shapes", async () => {
const runtime = CodeMode.make({
tools: {
"issues.list": echo("First list", "first"),
issues: { list: echo("Second list", "second"), get: echo("Get issue", "got") },
"issues.close": echo("Close issue", "closed"),
},
})
// Catalog order follows first appearance of each canonical path.
expect(runtime.catalog().map((tool) => tool.path)).toEqual(["issues.list", "issues.get", "issues.close"])
expect(await value(runtime, `return await tools.issues.list({})`)).toBe("second")
expect(await value(runtime, `return await tools.issues.get({})`)).toBe("got")
expect(await value(runtime, `return await tools.issues.close({})`)).toBe("closed")
})
})
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.18.2",
"version": "1.17.20",
"type": "module",
"license": "MIT",
"scripts": {
-2
View File
@@ -9,7 +9,6 @@ import "./app.css"
import { LanguageProvider } from "~/context/language"
import { I18nProvider, useI18n } from "~/context/i18n"
import { strip } from "~/lib/language"
import { DesktopPromo } from "~/component/desktop-promo"
function AppMeta() {
const i18n = useI18n()
@@ -34,7 +33,6 @@ export default function App() {
<MetaProvider>
<AppMeta />
<Suspense>{props.children}</Suspense>
<DesktopPromo />
</MetaProvider>
</I18nProvider>
</LanguageProvider>
@@ -1,103 +0,0 @@
[data-component="desktop-promo"] {
--promo-background: hsl(0, 20%, 99%);
--promo-background-weak: hsl(0, 8%, 97%);
--promo-text: hsl(0, 1%, 39%);
--promo-text-strong: hsl(0, 5%, 12%);
--promo-border: hsla(0, 100%, 3%, 0.12);
position: fixed;
z-index: 20;
right: 1.5rem;
bottom: 1.5rem;
width: min(28rem, calc(100vw - 2rem));
padding: 4px;
overflow: hidden;
color: var(--promo-text);
border: 1px solid var(--promo-border);
border-radius: 8px;
background: var(--promo-background);
box-shadow: 0 0.75rem 2rem rgb(0 0 0 / 15%);
font-family: var(--font-mono);
@media (prefers-color-scheme: dark) {
--promo-background: hsl(0, 9%, 7%);
--promo-background-weak: hsl(0, 6%, 10%);
--promo-text: hsl(0, 4%, 71%);
--promo-text-strong: hsl(0, 15%, 94%);
--promo-border: hsl(0, 4%, 23%);
}
@media (max-width: 40rem) {
right: 1rem;
bottom: 1rem;
}
[data-slot="desktop-promo-link"] {
display: block;
color: var(--promo-text);
text-decoration: none;
}
[data-slot="desktop-promo-link"]:focus-visible {
outline: 2px solid var(--promo-text-strong);
outline-offset: -3px;
}
video {
display: block;
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
border-radius: 4px;
background: var(--promo-background-weak);
}
[data-slot="desktop-promo-copy"] {
display: flex;
flex-direction: column;
gap: 0.35rem;
padding: 1rem;
font-size: 0.875rem;
line-height: 1.4;
}
[data-slot="desktop-promo-copy"] strong {
color: var(--promo-text-strong);
font-weight: 500;
}
[data-slot="desktop-promo-close"] {
position: absolute;
top: 0.5rem;
right: 0.5rem;
display: grid;
width: 2rem;
height: 2rem;
padding: 0;
place-items: center;
cursor: pointer;
color: white;
border: none;
border-radius: 0.25rem;
background: rgb(0 0 0 / 70%);
opacity: 0;
transition:
opacity 150ms ease,
background 150ms ease;
}
&:hover [data-slot="desktop-promo-close"],
[data-slot="desktop-promo-close"]:focus-visible {
opacity: 1;
}
[data-slot="desktop-promo-close"]:hover {
background: rgb(0 0 0 / 90%);
}
@media (hover: none) {
[data-slot="desktop-promo-close"] {
opacity: 1;
}
}
}
@@ -1,48 +0,0 @@
import "./desktop-promo.css"
import { A, useLocation } from "@solidjs/router"
import { createSignal, Show } from "solid-js"
import { getRequestEvent } from "solid-js/web"
import desktopPromoVideo from "~/asset/lander/desktop-tabs-landscape.mp4"
import { useI18n } from "~/context/i18n"
import { useLanguage } from "~/context/language"
import { strip } from "~/lib/language"
export function DesktopPromo() {
const i18n = useI18n()
const language = useLanguage()
const location = useLocation()
const [visible, setVisible] = createSignal(true)
const request = getRequestEvent()?.request
const hostname = request ? new URL(request.url).hostname : typeof window === "object" ? window.location.hostname : ""
const primaryHost =
hostname === "opencode.ai" || hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1"
return (
<Show
when={
visible() &&
primaryHost &&
strip(location.pathname) !== "/download" &&
!strip(location.pathname).startsWith("/download/")
}
>
<aside data-component="desktop-promo">
<A href={language.route("/download")} data-slot="desktop-promo-link">
<video src={desktopPromoVideo} autoplay playsinline loop muted preload="metadata" aria-hidden="true" />
<span data-slot="desktop-promo-copy">
<strong>{i18n.t("home.promo.title")}</strong>
<span>
{i18n.t("home.promo.body")} {i18n.t("home.promo.cta")}
</span>
</span>
</A>
<button type="button" data-slot="desktop-promo-close" onClick={() => setVisible(false)}>
<span class="sr-only">{i18n.t("home.promo.close")}</span>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" aria-hidden="true">
<path d="M5 5L15 15M15 5L5 15" stroke="currentColor" stroke-width="1.5" />
</svg>
</button>
</aside>
</Show>
)
}
+4 -8
View File
@@ -102,14 +102,10 @@ export const dict = {
"temp.logoDarkAlt": "شعار opencode الداكن",
"home.banner.badge": "جديد",
"home.banner.text": "نقدّم علامات التبويب لتطبيق سطح المكتب.",
"home.banner.platforms": "متاحة على macOS وWindows وLinux",
"home.banner.text": "تطبيق سطح المكتب متاح بنسخة تجريبية",
"home.banner.platforms": "على macOS، Windows، وLinux",
"home.banner.downloadNow": "حمّل الآن",
"home.banner.downloadBetaNow": "حمّل النسخة التجريبية لتطبيق سطح المكتب الآن",
"home.promo.title": "نقدّم علامات التبويب لتطبيق سطح المكتب",
"home.promo.body": "نظّم عملك وجلساتك النشطة باستخدام علامات التبويب.",
"home.promo.cta": "نزّل أحدث إصدار للبدء.",
"home.promo.close": "إغلاق إعلان تطبيق سطح المكتب",
"home.hero.title": "وكيل برمجة بالذكاء الاصطناعي مفتوح المصدر",
"home.hero.subtitle.a": "نماذج مجانية مضمّنة أو اربط أي نموذج من أي مزوّد،",
@@ -704,11 +700,11 @@ export const dict = {
"download.title": "OpenCode | تنزيل",
"download.meta.description": "نزّل OpenCode لـ macOS، Windows، وLinux",
"download.hero.title": "تنزيل OpenCode لسطح المكتب",
"download.hero.title": "تنزيل OpenCode",
"download.hero.subtitle": "متاح في نسخة تجريبية لـ macOS، Windows، وLinux",
"download.hero.button": "تنزيل لـ {{os}}",
"download.section.terminal": "OpenCode للطرفية",
"download.section.desktop": "OpenCode لسطح المكتب",
"download.section.desktop": "OpenCode لسطح المكتب (Beta)",
"download.section.extensions": "امتدادات OpenCode",
"download.section.integrations": "تكاملات OpenCode",
"download.action.download": "تنزيل",
+4 -8
View File
@@ -102,14 +102,10 @@ export const dict = {
"temp.logoDarkAlt": "logo opencode escuro",
"home.banner.badge": "Novo",
"home.banner.text": "Conheça as abas no app desktop.",
"home.banner.platforms": "Disponível no macOS, Windows e Linux",
"home.banner.text": "App desktop disponível em beta",
"home.banner.platforms": "no macOS, Windows e Linux",
"home.banner.downloadNow": "Baixar agora",
"home.banner.downloadBetaNow": "Baixe agora o beta do desktop",
"home.promo.title": "Conheça as abas no app desktop",
"home.promo.body": "Organize seu trabalho e suas sessões ativas com abas.",
"home.promo.cta": "Baixe a versão mais recente para começar.",
"home.promo.close": "Fechar anúncio do app desktop",
"home.hero.title": "O agente de codificação de código aberto com IA",
"home.hero.subtitle.a": "Modelos grátis incluídos ou conecte qualquer modelo de qualquer provedor,",
@@ -715,11 +711,11 @@ export const dict = {
"download.title": "OpenCode | Baixar",
"download.meta.description": "Baixe o OpenCode para macOS, Windows e Linux",
"download.hero.title": "Baixar OpenCode Desktop",
"download.hero.title": "Baixar OpenCode",
"download.hero.subtitle": "Disponível em Beta para macOS, Windows e Linux",
"download.hero.button": "Baixar para {{os}}",
"download.section.terminal": "OpenCode Terminal",
"download.section.desktop": "OpenCode Desktop",
"download.section.desktop": "OpenCode Desktop (Beta)",
"download.section.extensions": "Extensões OpenCode",
"download.section.integrations": "Integrações OpenCode",
"download.action.download": "Baixar",
+4 -8
View File
@@ -102,14 +102,10 @@ export const dict = {
"temp.logoDarkAlt": "opencode logo dark",
"home.banner.badge": "Ny",
"home.banner.text": "Introduktion af Tabs til Desktop.",
"home.banner.platforms": "Tilgængelig på macOS, Windows og Linux",
"home.banner.text": "Desktop-app tilgængelig i beta",
"home.banner.platforms": "på macOS, Windows og Linux",
"home.banner.downloadNow": "Download nu",
"home.banner.downloadBetaNow": "Download desktop-betaen nu",
"home.promo.title": "Introduktion af Tabs til Desktop",
"home.promo.body": "Organiser dit arbejde og aktive sessioner med faner.",
"home.promo.cta": "Download den nyeste version for at komme i gang.",
"home.promo.close": "Luk meddelelsen om Desktop-appen",
"home.hero.title": "Den open source AI-kodningsagent",
"home.hero.subtitle.a": "Gratis modeller inkluderet, eller forbind enhver model fra enhver udbyder,",
@@ -710,11 +706,11 @@ export const dict = {
"download.title": "OpenCode | Download",
"download.meta.description": "Download OpenCode til macOS, Windows og Linux",
"download.hero.title": "Download OpenCode Desktop",
"download.hero.title": "Download OpenCode",
"download.hero.subtitle": "Tilgængelig i beta til macOS, Windows og Linux",
"download.hero.button": "Download til {{os}}",
"download.section.terminal": "OpenCode Terminal",
"download.section.desktop": "OpenCode Desktop",
"download.section.desktop": "OpenCode Desktop (Beta)",
"download.section.extensions": "OpenCode Extensions",
"download.section.integrations": "OpenCode Integrations",
"download.action.download": "Download",
+4 -8
View File
@@ -102,14 +102,10 @@ export const dict = {
"temp.logoDarkAlt": "OpenCode Logo dunkel",
"home.banner.badge": "Neu",
"home.banner.text": "Neu: Tabs für Desktop.",
"home.banner.platforms": "Verfügbar für macOS, Windows und Linux",
"home.banner.text": "Desktop-App in der Beta verfügbar",
"home.banner.platforms": "auf macOS, Windows und Linux",
"home.banner.downloadNow": "Jetzt herunterladen",
"home.banner.downloadBetaNow": "Desktop-Beta jetzt herunterladen",
"home.promo.title": "Neu: Tabs für Desktop",
"home.promo.body": "Organisiere deine Arbeit und aktiven Sitzungen mit Tabs.",
"home.promo.cta": "Lade die neueste Version herunter, um loszulegen.",
"home.promo.close": "Ankündigung zur Desktop-App schließen",
"home.hero.title": "Der Open-Source AI-Coding-Agent",
"home.hero.subtitle.a": "Kostenlose Modelle inklusive oder verbinde jedes Modell eines beliebigen Anbieters,",
@@ -715,11 +711,11 @@ export const dict = {
"download.title": "OpenCode | Download",
"download.meta.description": "Lade OpenCode für macOS, Windows und Linux herunter",
"download.hero.title": "OpenCode Desktop herunterladen",
"download.hero.title": "OpenCode herunterladen",
"download.hero.subtitle": "In Beta verfügbar für macOS, Windows und Linux",
"download.hero.button": "Download für {{os}}",
"download.section.terminal": "OpenCode Terminal",
"download.section.desktop": "OpenCode Desktop",
"download.section.desktop": "OpenCode Desktop (Beta)",
"download.section.extensions": "OpenCode Extensions",
"download.section.integrations": "OpenCode Integrationen",
"download.action.download": "Download",
+4 -9
View File
@@ -102,16 +102,11 @@ export const dict = {
"temp.logoDarkAlt": "opencode logo dark",
"home.banner.badge": "New",
"home.banner.text": "Introducing tabs for desktop.",
"home.banner.platforms": "Available on macOS, Windows, and Linux",
"home.banner.text": "Desktop app available in beta",
"home.banner.platforms": "on macOS, Windows, and Linux",
"home.banner.downloadNow": "Download now",
"home.banner.downloadBetaNow": "Download the desktop beta now",
"home.promo.title": "Introducing Tabs for Desktop",
"home.promo.body": "Organize your work and active sessions with tabs.",
"home.promo.cta": "Download the latest to get started.",
"home.promo.close": "Dismiss desktop app announcement",
"home.hero.title": "The open source AI coding agent",
"home.hero.subtitle.a": "Free models included or connect any model from any provider,",
"home.hero.subtitle.b": "including Claude, GPT, Gemini and more.",
@@ -708,11 +703,11 @@ export const dict = {
"download.title": "OpenCode | Download",
"download.meta.description": "Download OpenCode for macOS, Windows, and Linux",
"download.hero.title": "Download OpenCode Desktop",
"download.hero.title": "Download OpenCode",
"download.hero.subtitle": "Available in Beta for macOS, Windows, and Linux",
"download.hero.button": "Download for {{os}}",
"download.section.terminal": "OpenCode Terminal",
"download.section.desktop": "OpenCode Desktop",
"download.section.desktop": "OpenCode Desktop (Beta)",
"download.section.extensions": "OpenCode Extensions",
"download.section.integrations": "OpenCode Integrations",
"download.action.download": "Download",
+4 -8
View File
@@ -102,14 +102,10 @@ export const dict = {
"temp.logoDarkAlt": "logo de opencode oscuro",
"home.banner.badge": "Nuevo",
"home.banner.text": "Presentamos Tabs para Desktop.",
"home.banner.platforms": "Disponible en macOS, Windows y Linux",
"home.banner.text": "Aplicación de escritorio disponible en beta",
"home.banner.platforms": "en macOS, Windows y Linux",
"home.banner.downloadNow": "Descargar ahora",
"home.banner.downloadBetaNow": "Descargar la beta de escritorio ahora",
"home.promo.title": "Presentamos Tabs para Desktop",
"home.promo.body": "Organiza tu trabajo y tus sesiones activas con pestañas.",
"home.promo.cta": "Descarga la última versión para empezar.",
"home.promo.close": "Cerrar el anuncio de la aplicación Desktop",
"home.hero.title": "El agente de codificación IA de código abierto",
"home.hero.subtitle.a": "Modelos gratuitos incluidos o conecta cualquier modelo de cualquier proveedor,",
@@ -715,11 +711,11 @@ export const dict = {
"download.title": "OpenCode | Descargar",
"download.meta.description": "Descarga OpenCode para macOS, Windows y Linux",
"download.hero.title": "Descargar OpenCode Desktop",
"download.hero.title": "Descargar OpenCode",
"download.hero.subtitle": "Disponible en Beta para macOS, Windows y Linux",
"download.hero.button": "Descargar para {{os}}",
"download.section.terminal": "OpenCode Terminal",
"download.section.desktop": "OpenCode Desktop",
"download.section.desktop": "OpenCode Desktop (Beta)",
"download.section.extensions": "Extensiones OpenCode",
"download.section.integrations": "Integraciones OpenCode",
"download.action.download": "Descargar",
+4 -8
View File
@@ -100,14 +100,10 @@ export const dict = {
"temp.logoDarkAlt": "opencode logo dark",
"home.banner.badge": "Nouveau",
"home.banner.text": "Découvrez Tabs pour Desktop.",
"home.banner.platforms": "Disponible sur macOS, Windows et Linux",
"home.banner.text": "Application desktop disponible en bêta",
"home.banner.platforms": "sur macOS, Windows et Linux",
"home.banner.downloadNow": "Télécharger maintenant",
"home.banner.downloadBetaNow": "Télécharger la bêta desktop maintenant",
"home.promo.title": "Découvrez Tabs pour Desktop",
"home.promo.body": "Organisez votre travail et vos sessions actives avec des onglets.",
"home.promo.cta": "Téléchargez la dernière version pour commencer.",
"home.promo.close": "Fermer lannonce de lapplication Desktop",
"home.hero.title": "L'agent de code IA open source",
"home.hero.subtitle.a":
@@ -722,11 +718,11 @@ export const dict = {
"download.title": "OpenCode | Téléchargement",
"download.meta.description": "Téléchargez OpenCode pour macOS, Windows et Linux",
"download.hero.title": "Télécharger OpenCode Desktop",
"download.hero.title": "Télécharger OpenCode",
"download.hero.subtitle": "Disponible en bêta pour macOS, Windows et Linux",
"download.hero.button": "Télécharger pour {{os}}",
"download.section.terminal": "OpenCode Terminal",
"download.section.desktop": "OpenCode Desktop",
"download.section.desktop": "OpenCode Desktop (Bêta)",
"download.section.extensions": "Extensions OpenCode",
"download.section.integrations": "Intégrations OpenCode",
"download.action.download": "Télécharger",
+4 -8
View File
@@ -102,14 +102,10 @@ export const dict = {
"temp.logoDarkAlt": "logo scuro di opencode",
"home.banner.badge": "Nuovo",
"home.banner.text": "Scopri Tabs per Desktop.",
"home.banner.platforms": "Disponibile su macOS, Windows e Linux",
"home.banner.text": "App desktop disponibile in beta",
"home.banner.platforms": "su macOS, Windows e Linux",
"home.banner.downloadNow": "Scarica ora",
"home.banner.downloadBetaNow": "Scarica ora la beta desktop",
"home.promo.title": "Scopri Tabs per Desktop",
"home.promo.body": "Organizza il tuo lavoro e le sessioni attive con le schede.",
"home.promo.cta": "Scarica lultima versione per iniziare.",
"home.promo.close": "Chiudi lannuncio dellapp Desktop",
"home.hero.title": "L'agente di coding IA open source",
"home.hero.subtitle.a": "Modelli gratuiti inclusi o collega qualsiasi modello da qualsiasi provider,",
@@ -713,11 +709,11 @@ export const dict = {
"download.title": "OpenCode | Download",
"download.meta.description": "Scarica OpenCode per macOS, Windows e Linux",
"download.hero.title": "Scarica OpenCode Desktop",
"download.hero.title": "Scarica OpenCode",
"download.hero.subtitle": "Disponibile in Beta per macOS, Windows e Linux",
"download.hero.button": "Scarica per {{os}}",
"download.section.terminal": "OpenCode Terminal",
"download.section.desktop": "OpenCode Desktop",
"download.section.desktop": "OpenCode Desktop (Beta)",
"download.section.extensions": "OpenCode Extensions",
"download.section.integrations": "OpenCode Integrations",
"download.action.download": "Scarica",
+4 -8
View File
@@ -102,14 +102,10 @@ export const dict = {
"temp.logoDarkAlt": "opencodeのロゴ(ダーク)",
"home.banner.badge": "新着",
"home.banner.text": "デスクトップ版にタブが登場。",
"home.banner.platforms": "macOS、Windows、Linux で利用できます",
"home.banner.text": "デスクトップアプリのベータ版が利用可能",
"home.banner.platforms": "macOS、Windows、Linux で",
"home.banner.downloadNow": "今すぐダウンロード",
"home.banner.downloadBetaNow": "デスクトップベータ版を今すぐダウンロード",
"home.promo.title": "デスクトップ版にタブが登場",
"home.promo.body": "タブで作業とアクティブなセッションを整理できます。",
"home.promo.cta": "始めるには最新版をダウンロードしてください。",
"home.promo.close": "デスクトップアプリのお知らせを閉じる",
"home.hero.title": "オープンソースのAIコーディングエージェント",
"home.hero.subtitle.a": "無料モデルが含まれています。また、任意のプロバイダーの任意のモデルに接続でき、",
@@ -712,11 +708,11 @@ export const dict = {
"download.title": "OpenCode | ダウンロード",
"download.meta.description": "OpenCode を macOS、Windows、Linux 向けにダウンロード",
"download.hero.title": "OpenCode デスクトップ版をダウンロード",
"download.hero.title": "OpenCode をダウンロード",
"download.hero.subtitle": "macOS、Windows、Linux 向けベータ版を利用可能",
"download.hero.button": "{{os}} 向けダウンロード",
"download.section.terminal": "OpenCode Terminal",
"download.section.desktop": "OpenCode Desktop",
"download.section.desktop": "OpenCode Desktop (Beta)",
"download.section.extensions": "OpenCode Extensions",
"download.section.integrations": "OpenCode Integrations",
"download.action.download": "ダウンロード",
+4 -8
View File
@@ -102,14 +102,10 @@ export const dict = {
"temp.logoDarkAlt": "opencode 어두운 로고",
"home.banner.badge": "신규",
"home.banner.text": "데스크톱 탭을 소개합니다.",
"home.banner.platforms": "macOS, Windows, Linux에서 사용할 수 있습니다",
"home.banner.text": "데스크톱 앱 베타 버전 출시",
"home.banner.platforms": "macOS, Windows, Linux 지원",
"home.banner.downloadNow": "지금 다운로드",
"home.banner.downloadBetaNow": "데스크톱 베타 다운로드",
"home.promo.title": "데스크톱 탭을 소개합니다",
"home.promo.body": "탭으로 작업과 활성 세션을 정리하세요.",
"home.promo.cta": "최신 버전을 다운로드하여 시작하세요.",
"home.promo.close": "데스크톱 앱 안내 닫기",
"home.hero.title": "오픈 소스 AI 코딩 에이전트",
"home.hero.subtitle.a": "무료 모델이 포함되어 있으며, 어떤 제공자의 모델이든 연결 가능합니다.",
@@ -704,11 +700,11 @@ export const dict = {
"download.title": "OpenCode | 다운로드",
"download.meta.description": "macOS, Windows, Linux용 OpenCode 다운로드",
"download.hero.title": "OpenCode 데스크톱 다운로드",
"download.hero.title": "OpenCode 다운로드",
"download.hero.subtitle": "macOS, Windows, Linux용 베타 버전 사용 가능",
"download.hero.button": "{{os}}용 다운로드",
"download.section.terminal": "OpenCode 터미널",
"download.section.desktop": "OpenCode 데스크톱",
"download.section.desktop": "OpenCode 데스크톱 (베타)",
"download.section.extensions": "OpenCode 확장 프로그램",
"download.section.integrations": "OpenCode 통합",
"download.action.download": "다운로드",
+4 -8
View File
@@ -102,14 +102,10 @@ export const dict = {
"temp.logoDarkAlt": "opencode logo mørk",
"home.banner.badge": "Ny",
"home.banner.text": "Vi introduserer Tabs for Desktop.",
"home.banner.platforms": "Tilgjengelig på macOS, Windows og Linux",
"home.banner.text": "Desktop-app tilgjengelig i beta",
"home.banner.platforms": "på macOS, Windows og Linux",
"home.banner.downloadNow": "Last ned nå",
"home.banner.downloadBetaNow": "Last ned desktop-betaen nå",
"home.promo.title": "Vi introduserer Tabs for Desktop",
"home.promo.body": "Organiser arbeidet ditt og aktive økter med faner.",
"home.promo.cta": "Last ned den nyeste versjonen for å komme i gang.",
"home.promo.close": "Lukk kunngjøringen om Desktop-appen",
"home.hero.title": "Den åpne kildekode AI-kodingsagenten",
"home.hero.subtitle.a":
@@ -711,11 +707,11 @@ export const dict = {
"download.title": "OpenCode | Last ned",
"download.meta.description": "Last ned OpenCode for macOS, Windows og Linux",
"download.hero.title": "Last ned OpenCode Desktop",
"download.hero.title": "Last ned OpenCode",
"download.hero.subtitle": "Tilgjengelig i beta for macOS, Windows og Linux",
"download.hero.button": "Last ned for {{os}}",
"download.section.terminal": "OpenCode Terminal",
"download.section.desktop": "OpenCode Desktop",
"download.section.desktop": "OpenCode Desktop (Beta)",
"download.section.extensions": "OpenCode Extensions",
"download.section.integrations": "OpenCode Integrations",
"download.action.download": "Last ned",
+4 -8
View File
@@ -101,14 +101,10 @@ export const dict = {
"temp.logoDarkAlt": "ciemne logo opencode",
"home.banner.badge": "Nowość",
"home.banner.text": "Przedstawiamy karty w aplikacji desktopowej.",
"home.banner.platforms": "Dostępne na macOS, Windows i Linux",
"home.banner.text": "Aplikacja desktopowa dostępna w wersji beta",
"home.banner.platforms": "na macOS, Windows i Linux",
"home.banner.downloadNow": "Pobierz teraz",
"home.banner.downloadBetaNow": "Pobierz betę wersji desktopowej",
"home.promo.title": "Przedstawiamy karty w aplikacji desktopowej",
"home.promo.body": "Organizuj swoją pracę i aktywne sesje za pomocą kart.",
"home.promo.cta": "Pobierz najnowszą wersję, aby rozpocząć.",
"home.promo.close": "Zamknij ogłoszenie o aplikacji desktopowej",
"home.hero.title": "Open source'owy agent AI do kodowania",
"home.hero.subtitle.a": "Darmowe modele w zestawie lub podłącz dowolny model od dowolnego dostawcy,",
@@ -712,11 +708,11 @@ export const dict = {
"download.title": "OpenCode | Pobierz",
"download.meta.description": "Pobierz OpenCode na macOS, Windows i Linux",
"download.hero.title": "Pobierz OpenCode Desktop",
"download.hero.title": "Pobierz OpenCode",
"download.hero.subtitle": "Dostępne w wersji Beta na macOS, Windows i Linux",
"download.hero.button": "Pobierz na {{os}}",
"download.section.terminal": "Terminal OpenCode",
"download.section.desktop": "Pulpit OpenCode",
"download.section.desktop": "Pulpit OpenCode (Beta)",
"download.section.extensions": "Rozszerzenia OpenCode",
"download.section.integrations": "Integracje OpenCode",
"download.action.download": "Pobierz",

Some files were not shown because too many files have changed in this diff Show More