Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4778e6e031 | |||
| ac1b802820 | |||
| 5ee4c1082a | |||
| 8a70d70006 | |||
| 91b4634363 | |||
| 4f60bde502 | |||
| 0d3b6d430e | |||
| a7cf21e157 | |||
| e4a16830f1 | |||
| 103f764624 | |||
| 7a7075d86f | |||
| c764732aea | |||
| 72af084dc1 | |||
| 65a42fd549 | |||
| ad8e6b1fb6 | |||
| 041cda905d | |||
| 2ad6c42143 | |||
| 60c7f847c1 | |||
| 7ab8a08efa | |||
| 7388e69b5c | |||
| 56c0658e6f | |||
| 06a2592836 | |||
| 4829308f2d | |||
| eec6cd5234 | |||
| fc37ae4452 | |||
| 5fcef6773c | |||
| a5b28c2af2 | |||
| d01dfa57b7 | |||
| 198ca749fd | |||
| 75f9fd5208 | |||
| 282f3f7eb2 | |||
| 4678bd1049 | |||
| 5fb0470b44 | |||
| 6bf92aa7de | |||
| 0a5530d0be | |||
| dc7e0f6d2e | |||
| b4be28d4c3 | |||
| 7624c4f7b3 | |||
| 720f062cd0 | |||
| b5177adb5b | |||
| a772767e9b | |||
| fd19fa89c4 | |||
| 0cfa7b39db | |||
| 5e2e0d6965 | |||
| f92d84746b | |||
| f2f5eb6f16 |
@@ -1,2 +1,3 @@
|
||||
packages/core/migration/**/snapshot.json linguist-generated
|
||||
packages/core/src/database/migration.gen.ts linguist-generated
|
||||
packages/core/src/**/*.txt text eol=lf
|
||||
|
||||
@@ -19,8 +19,6 @@ 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
|
||||
|
||||
@@ -30,9 +30,30 @@
|
||||
"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.17.20",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"@corvu/drawer": "catalog:",
|
||||
"@dnd-kit/abstract": "0.5.0",
|
||||
@@ -98,7 +119,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@opencode-ai/cli",
|
||||
"version": "1.17.18",
|
||||
"version": "1.18.2",
|
||||
"bin": {
|
||||
"opencode2": "./bin/opencode2.cjs",
|
||||
},
|
||||
@@ -158,7 +179,7 @@
|
||||
},
|
||||
"packages/codemode": {
|
||||
"name": "@opencode-ai/codemode",
|
||||
"version": "1.17.20",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"acorn": "8.15.0",
|
||||
"effect": "catalog:",
|
||||
@@ -168,12 +189,11 @@
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.17.20",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -209,7 +229,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.17.20",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -236,7 +256,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.17.20",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "3.0.82",
|
||||
"@ai-sdk/openai": "3.0.48",
|
||||
@@ -258,7 +278,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.17.20",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -282,7 +302,7 @@
|
||||
},
|
||||
"packages/console/support": {
|
||||
"name": "@opencode-ai/console-support",
|
||||
"version": "1.17.20",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@opencode-ai/console-core": "workspace:*",
|
||||
@@ -302,7 +322,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@opencode-ai/core",
|
||||
"version": "1.17.20",
|
||||
"version": "1.18.2",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -336,10 +356,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/ai": "workspace:*",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@openrouter/ai-sdk-provider": "2.9.0",
|
||||
@@ -398,7 +418,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.17.20",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"@zip.js/zip.js": "2.7.62",
|
||||
"effect": "catalog:",
|
||||
@@ -458,7 +478,7 @@
|
||||
},
|
||||
"packages/effect-drizzle-sqlite": {
|
||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||
"version": "1.17.20",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -472,7 +492,7 @@
|
||||
},
|
||||
"packages/effect-sqlite-node": {
|
||||
"name": "@opencode-ai/effect-sqlite-node",
|
||||
"version": "1.17.20",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"effect": "catalog:",
|
||||
},
|
||||
@@ -484,7 +504,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.17.20",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"@hono/standard-validator": "catalog:",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
@@ -516,7 +536,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.17.20",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -532,7 +552,7 @@
|
||||
},
|
||||
"packages/http-recorder": {
|
||||
"name": "@opencode-ai/http-recorder",
|
||||
"version": "1.17.20",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"@effect/platform-node-shared": "4.0.0-beta.83",
|
||||
},
|
||||
@@ -562,29 +582,9 @@
|
||||
"@typescript/native-preview": "catalog:",
|
||||
},
|
||||
},
|
||||
"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:",
|
||||
},
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.17.20",
|
||||
"version": "1.18.2",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -621,10 +621,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/ai": "workspace:*",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/protocol": "workspace:*",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
@@ -717,12 +717,14 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.17.20",
|
||||
"version": "1.18.2",
|
||||
"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:",
|
||||
},
|
||||
@@ -804,7 +806,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.17.20",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"cross-spawn": "catalog:",
|
||||
},
|
||||
@@ -819,7 +821,7 @@
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@opencode-ai/server",
|
||||
"version": "1.17.20",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
@@ -836,7 +838,7 @@
|
||||
},
|
||||
"packages/session-ui": {
|
||||
"name": "@opencode-ai/session-ui",
|
||||
"version": "1.17.20",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
@@ -884,8 +886,8 @@
|
||||
"dependencies": {
|
||||
"@fontsource/commit-mono": "5.2.5",
|
||||
"@napi-rs/canvas": "1.0.2",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/ai": "workspace:*",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opentui/core": "catalog:",
|
||||
"effect": "catalog:",
|
||||
},
|
||||
@@ -897,7 +899,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.17.20",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -910,7 +912,7 @@
|
||||
},
|
||||
"packages/stats/app": {
|
||||
"name": "@opencode-ai/stats-app",
|
||||
"version": "1.17.20",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"@ibm/plex": "6.4.1",
|
||||
"@kobalte/core": "catalog:",
|
||||
@@ -944,7 +946,7 @@
|
||||
},
|
||||
"packages/stats/core": {
|
||||
"name": "@opencode-ai/stats-core",
|
||||
"version": "1.17.20",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-athena": "3.933.0",
|
||||
"@planetscale/database": "1.19.0",
|
||||
@@ -963,7 +965,7 @@
|
||||
},
|
||||
"packages/stats/server": {
|
||||
"name": "@opencode-ai/stats-server",
|
||||
"version": "1.17.20",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-firehose": "3.933.0",
|
||||
"@effect/platform-node": "catalog:",
|
||||
@@ -1004,7 +1006,7 @@
|
||||
},
|
||||
"packages/tui": {
|
||||
"name": "@opencode-ai/tui",
|
||||
"version": "1.17.20",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"@opencode-ai/client": "workspace:*",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
@@ -1034,7 +1036,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.17.20",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@pierre/diffs": "catalog:",
|
||||
@@ -1085,7 +1087,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.17.20",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -2167,6 +2169,8 @@
|
||||
|
||||
"@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"],
|
||||
@@ -2205,8 +2209,6 @@
|
||||
|
||||
"@opencode-ai/httpapi-codegen": ["@opencode-ai/httpapi-codegen@workspace:packages/httpapi-codegen"],
|
||||
|
||||
"@opencode-ai/ai": ["@opencode-ai/ai@workspace:packages/ai"],
|
||||
|
||||
"@opencode-ai/plugin": ["@opencode-ai/plugin@workspace:packages/plugin"],
|
||||
|
||||
"@opencode-ai/protocol": ["@opencode-ai/protocol@workspace:packages/protocol"],
|
||||
@@ -2871,7 +2873,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.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-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-serde-browser": ["@smithy/eventstream-serde-browser@4.3.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-M9rMkTar7JcRrvUHsK1271AuWDmrISIPQpQ4TSHmYZ4KMisGnMH0gfjCWnBwdndR7skvvp/UheHhZGvO3Cr8/g=="],
|
||||
|
||||
@@ -2945,7 +2947,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.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="],
|
||||
"@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-waiter": ["@smithy/util-waiter@4.4.5", "", { "dependencies": { "@smithy/core": "^3.24.5", "tslib": "^2.6.2" } }, "sha512-EYviebytZE6vplW0AGwZ2Rc3sNuVR83lfUCNZu11VchUiKhMwJqrRWy7iVDTNEwG/vEwItno591Iad6/prj6Bw=="],
|
||||
|
||||
@@ -3007,7 +3009,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.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
@@ -6335,10 +6337,6 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -6395,8 +6393,6 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -6457,26 +6453,18 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -6499,10 +6487,6 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -6551,10 +6535,6 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -6629,6 +6609,8 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -6849,6 +6831,10 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -6863,10 +6849,6 @@
|
||||
|
||||
"@opencode-ai/desktop/typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="],
|
||||
|
||||
"@opencode-ai/ai/@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/ai/@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=="],
|
||||
@@ -7351,8 +7333,6 @@
|
||||
|
||||
"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=="],
|
||||
@@ -7413,8 +7393,6 @@
|
||||
|
||||
"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=="],
|
||||
@@ -7553,6 +7531,8 @@
|
||||
|
||||
"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=="],
|
||||
@@ -7759,46 +7739,6 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -8401,10 +8341,6 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -8479,10 +8415,6 @@
|
||||
|
||||
"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=="],
|
||||
@@ -8873,10 +8805,6 @@
|
||||
|
||||
"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=="],
|
||||
@@ -8989,12 +8917,8 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -9145,8 +9069,6 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -9329,14 +9251,6 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -9479,8 +9393,6 @@
|
||||
|
||||
"@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=="],
|
||||
|
||||
+19
-5
@@ -127,23 +127,37 @@ OpenAI Chat and OpenAI Responses are separate semantic entrypoints:
|
||||
- `@opencode-ai/ai/providers/openai/responses`
|
||||
- `@opencode-ai/ai/providers/openai-compatible/responses`
|
||||
- `@opencode-ai/ai/providers/anthropic-compatible`
|
||||
- `@opencode-ai/ai/providers/google-vertex`
|
||||
- `@opencode-ai/ai/providers/google-vertex/anthropic`
|
||||
- `@opencode-ai/ai/providers/google-vertex/gemini`
|
||||
- `@opencode-ai/ai/providers/google-vertex/chat`
|
||||
- `@opencode-ai/ai/providers/google-vertex/responses`
|
||||
- `@opencode-ai/ai/providers/google-vertex/messages`
|
||||
|
||||
Responses HTTP versus WebSocket is a scoped `transport` setting on the OpenAI Responses entrypoint, not another entrypoint. Azure follows the same Chat/Responses split at `providers/azure/chat` and `providers/azure/responses`. Generic OpenAI-compatible Chat remains at `providers/openai-compatible`; compatible Responses is separate at `providers/openai-compatible/responses`. Generic Anthropic Messages-compatible providers use `providers/anthropic-compatible`, which the named Anthropic provider composes. Google Gemini and Amazon Bedrock expose their single native API through their existing provider paths.
|
||||
|
||||
Vertex Gemini and Vertex Anthropic are separate products with separate entrypoints. Both accept `project`, `location`, and an optional `accessToken`; when no explicit token or auth override is supplied they lazily use Google Application Default Credentials. Vertex Gemini instead selects express mode when `apiKey` or `GOOGLE_VERTEX_API_KEY` is present.
|
||||
Vertex Gemini, Vertex Chat, Vertex Responses, and Vertex Messages are separate API entrypoints. All accept `project`, `location`, and an optional `accessToken`; when no explicit token or auth override is supplied they lazily use Google Application Default Credentials. Vertex Gemini instead selects express mode when `apiKey` or `GOOGLE_VERTEX_API_KEY` is present. Vertex Chat targets MaaS models through the OpenAI-compatible Chat Completions endpoint, while Vertex Responses targets Grok models and defaults `store` to `false` as required by Vertex. `providers/google-vertex` remains the default alias for `providers/google-vertex/gemini`.
|
||||
|
||||
Tuned Vertex Gemini deployments use model ids shaped like `endpoints/1234567890` and require OAuth or ADC; Vertex express-mode API keys support publisher models only.
|
||||
|
||||
```ts
|
||||
import { model } from "@opencode-ai/ai/providers/google-vertex"
|
||||
import { model } from "@opencode-ai/ai/providers/google-vertex/gemini"
|
||||
|
||||
model("gemini-3.5-flash", { project: "my-project", location: "global" })
|
||||
```
|
||||
|
||||
```ts
|
||||
import { model } from "@opencode-ai/ai/providers/google-vertex/anthropic"
|
||||
import { model } from "@opencode-ai/ai/providers/google-vertex/chat"
|
||||
|
||||
model("deepseek-ai/deepseek-v3.2-maas", { project: "my-project", location: "global" })
|
||||
```
|
||||
|
||||
```ts
|
||||
import { model } from "@opencode-ai/ai/providers/google-vertex/responses"
|
||||
|
||||
model("xai/grok-4.20-reasoning", { project: "my-project", location: "global" })
|
||||
```
|
||||
|
||||
```ts
|
||||
import { model } from "@opencode-ai/ai/providers/google-vertex/messages"
|
||||
|
||||
model("claude-sonnet-4-6", { project: "my-project", location: "global" })
|
||||
```
|
||||
|
||||
+23
-21
@@ -1,15 +1,15 @@
|
||||
# LLM Provider Parity Status
|
||||
|
||||
Last reviewed: 2026-07-15
|
||||
Last reviewed: 2026-07-16
|
||||
|
||||
This file tracks the gap between the native `@opencode-ai/ai` package and the AI SDK provider packages that opencode still depends on for many catalog/runtime paths.
|
||||
|
||||
## Existing Status Sources
|
||||
|
||||
| File | What it tracks | Limitation |
|
||||
| ------------------------------------ | -------------------------------------------------------------------------------- | ------------------------------------------------------- |
|
||||
| `packages/ai/DESIGN.md` | Future clean-break API proposal for `@opencode-ai/ai`. | Not a provider parity tracker. |
|
||||
| `packages/ai/example/call-sites.md` | Route/value/provider-facade migration checklist and call-site sketches. | Architecture migration only; not AI SDK package parity. |
|
||||
| File | What it tracks | Limitation |
|
||||
| ----------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------- |
|
||||
| `packages/ai/DESIGN.md` | Future clean-break API proposal for `@opencode-ai/ai`. | Not a provider parity tracker. |
|
||||
| `packages/ai/example/call-sites.md` | Route/value/provider-facade migration checklist and call-site sketches. | Architecture migration only; not AI SDK package parity. |
|
||||
|
||||
## Current Implementation Snapshot
|
||||
|
||||
@@ -23,8 +23,10 @@ This file tracks the gap between the native `@opencode-ai/ai` package and the AI
|
||||
| Anthropic-compatible Messages | `src/protocols/anthropic-messages.ts`, `src/providers/anthropic-compatible.ts` | Usable for deployments that implement the Anthropic Messages wire protocol. Named Anthropic composes this base. | No named compatible family profiles or recorded deployment coverage yet. |
|
||||
| Anthropic Messages | `src/protocols/anthropic-messages.ts`, `src/providers/anthropic.ts` | Usable. Supports tools, thinking, cache control, images, server-hosted tool events, and usage. | Provider option surface is small. Beta/header handling, metadata, and newer Messages fields need a typed parity pass. |
|
||||
| Gemini Developer API | `src/protocols/gemini.ts`, `src/providers/google.ts` | Usable for Google API key flow. Supports text, images, tools, thinking signatures, and cache usage. | This is not Vertex. Typed provider options are narrow; many Gemini request fields currently require raw `http.body` overlays. |
|
||||
| Vertex Gemini | `src/protocols/google-vertex-gemini.ts`, `src/providers/google-vertex.ts` | Usable through API-key express mode, explicit OAuth tokens, or ADC with project/location endpoint derivation, including tuned `endpoints/...` deployments. | Core runner/catalog mapping and recorded provider coverage are missing. |
|
||||
| Vertex Anthropic Messages | `src/protocols/google-vertex-anthropic.ts`, `src/providers/google-vertex-anthropic.ts` | Usable through explicit OAuth tokens or ADC, including global, regional, and `eu`/`us` multi-region endpoints. | Core runner/catalog mapping and recorded provider coverage are missing; Vertex-specific hosted-tool parity needs review. |
|
||||
| Vertex Gemini | `src/protocols/gemini.ts`, `src/providers/google-vertex.ts` | Usable through API-key express mode, explicit OAuth tokens, or ADC with project/location endpoint derivation, including tuned `endpoints/...` deployments. | Core runner/catalog mapping and recorded provider coverage are missing. |
|
||||
| Vertex Chat | `src/protocols/openai-chat.ts`, `src/providers/google-vertex-chat.ts` | Usable for MaaS models through OpenAI-compatible Chat Completions with explicit OAuth tokens or ADC and project/location endpoint derivation. | Core runner/catalog mapping and recorded provider coverage are missing; MaaS family-specific request parity needs review. |
|
||||
| Vertex Responses | `src/protocols/openai-responses.ts`, `src/providers/google-vertex-responses.ts` | Usable for Grok models through OpenAI-compatible Responses with explicit OAuth tokens or ADC, project/location endpoint derivation, and storage disabled by default. | Core runner/catalog mapping and recorded provider coverage are missing; stateful continuation is not supported by Vertex. |
|
||||
| Vertex Messages | `src/protocols/anthropic-messages.ts`, `src/providers/google-vertex-messages.ts` | Usable through explicit OAuth tokens or ADC, including global, regional, and `eu`/`us` multi-region endpoints. | Core runner/catalog mapping and recorded provider coverage are missing; Vertex-specific hosted-tool parity needs review. |
|
||||
| Bedrock Converse | `src/protocols/bedrock-converse.ts`, `src/providers/amazon-bedrock.ts` | Partial but real. Supports AWS event-stream framing, SigV4 with supplied credentials, bearer auth, tools, reasoning signatures, media, cache points, and recorded tests. | Native facade does not mirror the AI SDK plugin's default AWS credential chain/profile behavior. Runner/catalog mapping is missing. Guardrails, inference profiles, region-specific model ID fixes, and model-specific request fields need a parity pass. |
|
||||
| Azure OpenAI | `src/providers/azure.ts` using OpenAI Chat/Responses protocols | Partial. Supports resource/base URL setup, API key auth, API version query, Chat, and Responses selectors. | Core runner does not map `@ai-sdk/azure` to this native facade. AAD/token auth and Azure-specific endpoint variants need review. |
|
||||
| Cloudflare AI Gateway / Workers AI | `src/providers/cloudflare.ts` | Present via OpenAI-compatible Chat routes. | Useful but not part of the critical AI SDK replacement set yet. Needs per-product recorded coverage before relying on it broadly. |
|
||||
@@ -54,8 +56,8 @@ Other `aisdk:` packages, including Google Vertex, Azure, and Bedrock, currently
|
||||
| `@ai-sdk/google` | Gemini Developer API | Partial / usable | Add typed options for safety, response schema/modalities, cached content, grounding/search/code execution, and non-text output modes where supported. |
|
||||
| `@ai-sdk/google-vertex` | Vertex Gemini namespace/facade | Partial / usable | Add runner/catalog mapping, recorded coverage, and broader provider-option parity. |
|
||||
| `@ai-sdk/google-vertex/anthropic` | Anthropic Messages over Vertex namespace/facade | Partial / usable | Add runner/catalog mapping, recorded coverage, and Vertex-specific hosted-tool parity. |
|
||||
| `@ai-sdk/google-vertex/maas` | Vertex MaaS OpenAI-compatible namespace/facade | Missing | Decide native Chat/Responses selection, endpoint derivation, auth, and catalog mapping. |
|
||||
| `@ai-sdk/google-vertex/xai` | Vertex xAI OpenAI-compatible namespace/facade | Missing | Decide whether this composes the generic compatible bases or the xAI facade, then add endpoint/auth mapping and tests. |
|
||||
| `@ai-sdk/google-vertex/maas` | Vertex Chat | Partial / usable | Add runner/catalog mapping, recorded coverage, and MaaS family-specific request parity. |
|
||||
| `@ai-sdk/google-vertex/xai` | Vertex Chat / Responses | Partial / usable | Decide Chat/Responses selection for catalog models, add runner mapping and recorded coverage, and review xAI-specific request options. |
|
||||
| `@ai-sdk/azure` | Azure OpenAI Chat/Responses facade | Partial | Map runner/catalog metadata to native Azure, handle resourceName/baseURL/apiVersion variants, add AAD/token auth story, and verify Chat vs Responses deployment selection. |
|
||||
| `@ai-sdk/amazon-bedrock` | Bedrock Converse | Partial | Add default AWS credential chain/profile support, region/inference-profile model ID handling, provider option parity via `additionalModelRequestFields`, guardrails/performance config, and runner/catalog mapping. |
|
||||
| `@ai-sdk/amazon-bedrock/mantle` | Bedrock Mantle OpenAI-compatible Chat/Responses namespace | Missing | Decide native Mantle shape, likely separate from Converse because it uses OpenAI-compatible Chat/Responses semantics over Bedrock. Add package mapping and tests. |
|
||||
@@ -65,19 +67,19 @@ Other `aisdk:` packages, including Google Vertex, Azure, and Bedrock, currently
|
||||
1. Runner support is narrower than the LLM package. The package has native provider facades for Google, Azure, and Bedrock, but the V2 Session runner only maps OpenAI, Anthropic, and explicit OpenAI-compatible Chat from `aisdk` catalog metadata.
|
||||
2. OpenAI-compatible Responses is available as a separate package entrypoint, but the V2 runner still maps `@ai-sdk/openai-compatible` to Chat only. Catalog selection must become API-aware before Responses deployments can use it.
|
||||
3. Bedrock native auth is not AI SDK parity. The AI SDK plugin uses the default AWS provider chain, profile, container credentials, and Bedrock bearer token env behavior. Native Bedrock currently expects explicit credentials or bearer auth on the facade.
|
||||
4. Vertex Gemini and Vertex Anthropic now have native package entrypoints, but the core runner does not map catalog metadata to them yet and recorded provider coverage is still missing.
|
||||
4. Vertex Gemini, Vertex Chat, Vertex Responses, and Vertex Messages now have native package entrypoints, but the core runner does not map catalog metadata to them yet and recorded provider coverage is still missing.
|
||||
5. Azure is only a provider facade, not a full runtime replacement. Native Azure exists, but the catalog runner does not select it, and token auth/resource variants need review.
|
||||
6. Provider option typing is uneven. OpenAI, Anthropic, Gemini, Bedrock, and OpenRouter each expose a small typed subset plus raw HTTP overlays; this is useful but not equivalent to AI SDK provider option coverage.
|
||||
7. Structured output is not provider-native yet. `LLM.generateObject` still uses a synthetic tool strategy, while the future design expects native structured output where reliable and tool fallback where needed.
|
||||
8. Package/namespace boundaries for the current native loading set are explicit in docs and exports. Other exported provider facades are not catalog package entrypoints until they implement the contract. Missing native boundaries remain for Vertex MaaS, Vertex xAI, and Bedrock Mantle.
|
||||
8. Package/namespace boundaries for the current native loading set are explicit in docs and exports. Other exported provider facades are not catalog package entrypoints until they implement the contract. Vertex xAI still needs catalog API selection; the missing native boundary is Bedrock Mantle.
|
||||
9. Recorded coverage is uneven. OpenAI, Anthropic, Gemini, Bedrock Converse, Cloudflare, OpenRouter, and several OpenAI-compatible Chat providers have cassettes. Azure, Vertex, and Mantle need first-class recorded scenarios before switching defaults.
|
||||
|
||||
## Native Namespace Shape
|
||||
|
||||
These are implementation/API slices, not separate npm packages.
|
||||
|
||||
| API slice | Package-like entrypoint | Purpose |
|
||||
| ----------------------------- | -------------------------------------------------------- | ---------------------------------------------------------------------------- |
|
||||
| API slice | Package-like entrypoint | Purpose |
|
||||
| ----------------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------- |
|
||||
| OpenAI Chat | `@opencode-ai/ai/providers/openai/chat` | OpenAI `/chat/completions` semantics. |
|
||||
| OpenAI Responses | `@opencode-ai/ai/providers/openai/responses` | OpenAI `/responses` semantics with HTTP/WebSocket selected through settings. |
|
||||
| OpenAI-compatible Chat | `@opencode-ai/ai/providers/openai-compatible` | Generic OpenAI-compatible `/chat/completions`. |
|
||||
@@ -85,12 +87,12 @@ These are implementation/API slices, not separate npm packages.
|
||||
| Anthropic-compatible Messages | `@opencode-ai/ai/providers/anthropic-compatible` | Generic Anthropic-compatible `/messages`. |
|
||||
| Anthropic Messages | `@opencode-ai/ai/providers/anthropic` | Anthropic Messages API. |
|
||||
| Gemini Developer API | `@opencode-ai/ai/providers/google` | Google AI Studio Gemini API. |
|
||||
| Vertex Gemini | `@opencode-ai/ai/providers/google-vertex` | Vertex Gemini API. |
|
||||
| Vertex Anthropic Messages | `@opencode-ai/ai/providers/google-vertex/anthropic` | Vertex-hosted Anthropic Messages API. |
|
||||
| Vertex MaaS | Missing | Vertex OpenAI-compatible MaaS APIs. |
|
||||
| Vertex xAI | Missing | Vertex-hosted xAI APIs. |
|
||||
| Vertex Gemini | `@opencode-ai/ai/providers/google-vertex/gemini` | Vertex Gemini API; `providers/google-vertex` is the default alias. |
|
||||
| Vertex Chat | `@opencode-ai/ai/providers/google-vertex/chat` | Vertex OpenAI-compatible Chat Completions for MaaS models. |
|
||||
| Vertex Responses | `@opencode-ai/ai/providers/google-vertex/responses` | Vertex OpenAI-compatible Responses for Grok models. |
|
||||
| Vertex Messages | `@opencode-ai/ai/providers/google-vertex/messages` | Vertex-hosted Anthropic Messages API. |
|
||||
| Bedrock Converse | `@opencode-ai/ai/providers/amazon-bedrock` | AWS Bedrock Converse API. |
|
||||
| Bedrock Mantle | Missing | AWS Bedrock Mantle OpenAI-compatible APIs. |
|
||||
| Bedrock Mantle | Missing | AWS Bedrock Mantle OpenAI-compatible APIs. |
|
||||
| Azure OpenAI Chat | `@opencode-ai/ai/providers/azure/chat` | Azure specialization of OpenAI Chat. |
|
||||
| Azure OpenAI Responses | `@opencode-ai/ai/providers/azure/responses` | Azure specialization of OpenAI Responses. |
|
||||
|
||||
@@ -99,8 +101,8 @@ These are implementation/API slices, not separate npm packages.
|
||||
1. Add native runner/catalog mappings for `@ai-sdk/azure`, `@ai-sdk/google`, and `@ai-sdk/amazon-bedrock` where the existing native facades are already close.
|
||||
2. Add API-aware runner/catalog selection between OpenAI-compatible Chat and Responses.
|
||||
3. Bring Bedrock native auth/config to AI SDK parity: region, profile, default AWS credential chain, bearer token env, endpoint override, and cross-region inference profile handling.
|
||||
4. Add runner/catalog mappings and recorded scenarios for the native Vertex Gemini and Vertex Anthropic entrypoints.
|
||||
5. Add native Vertex MaaS and Vertex xAI entrypoints by composing the compatible bases and shared Vertex auth/endpoint setup.
|
||||
4. Add runner/catalog mappings and recorded scenarios for the native Vertex Gemini, Chat, Responses, and Messages entrypoints.
|
||||
5. Decide Chat/Responses selection for `@ai-sdk/google-vertex/xai` catalog models.
|
||||
6. Add Bedrock Mantle as a separate OpenAI-compatible Bedrock namespace after deciding whether it uses Chat, Responses, or both by model.
|
||||
7. Expand typed provider options from the existing V1 lowerer knowledge in `packages/core/src/v1/config/provider-options.ts` before adding more raw overlay examples.
|
||||
8. Add recorded provider tests for Azure, Vertex Gemini, Vertex Anthropic, Bedrock credential-chain behavior, and Mantle before making native runtime the default for those packages.
|
||||
8. Add recorded provider tests for Azure, Vertex Gemini, Vertex Chat, Vertex Responses, Vertex Messages, Bedrock credential-chain behavior, and Mantle before making native runtime the default for those packages.
|
||||
|
||||
@@ -360,17 +360,29 @@ import { model } from "@opencode-ai/ai/providers/openai/responses"
|
||||
model("gpt-4o", { apiKey, transport: "websocket" })
|
||||
```
|
||||
|
||||
Vertex keeps Gemini and Anthropic Messages as separate package-like entrypoints,
|
||||
Vertex keeps Gemini, Chat, Responses, and Messages as separate package-like entrypoints,
|
||||
while sharing project/location resolution and ADC authentication internally:
|
||||
|
||||
```ts
|
||||
import { model } from "@opencode-ai/ai/providers/google-vertex"
|
||||
import { model } from "@opencode-ai/ai/providers/google-vertex/gemini"
|
||||
|
||||
model("gemini-3.5-flash", { project, location: "global" })
|
||||
```
|
||||
|
||||
```ts
|
||||
import { model } from "@opencode-ai/ai/providers/google-vertex/anthropic"
|
||||
import { model } from "@opencode-ai/ai/providers/google-vertex/chat"
|
||||
|
||||
model("deepseek-ai/deepseek-v3.2-maas", { project, location: "global" })
|
||||
```
|
||||
|
||||
```ts
|
||||
import { model } from "@opencode-ai/ai/providers/google-vertex/responses"
|
||||
|
||||
model("xai/grok-4.20-reasoning", { project, location: "global" })
|
||||
```
|
||||
|
||||
```ts
|
||||
import { model } from "@opencode-ai/ai/providers/google-vertex/messages"
|
||||
|
||||
model("claude-sonnet-4-6", { project, location: "global" })
|
||||
```
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { Effect, Schema, Struct } from "effect"
|
||||
import { AnthropicMessages } from "./anthropic-messages"
|
||||
import { Auth } from "../route/auth"
|
||||
import { Route } from "../route/client"
|
||||
import { Endpoint } from "../route/endpoint"
|
||||
import { Framing } from "../route/framing"
|
||||
import { Protocol } from "../route/protocol"
|
||||
|
||||
const VERSION = "vertex-2023-10-16" as const
|
||||
|
||||
export const GoogleVertexAnthropicBody = Schema.Struct({
|
||||
...Struct.omit(AnthropicMessages.AnthropicMessagesBody.fields, ["model"]),
|
||||
anthropic_version: Schema.Literal(VERSION),
|
||||
})
|
||||
export type GoogleVertexAnthropicBody = Schema.Schema.Type<typeof GoogleVertexAnthropicBody>
|
||||
|
||||
export const protocol = Protocol.make({
|
||||
id: "google-vertex-anthropic",
|
||||
body: {
|
||||
schema: GoogleVertexAnthropicBody,
|
||||
from: (request) =>
|
||||
AnthropicMessages.protocol.body.from(request).pipe(
|
||||
Effect.map((body) => ({
|
||||
...Struct.omit(body, ["model"]),
|
||||
anthropic_version: VERSION,
|
||||
})),
|
||||
),
|
||||
},
|
||||
stream: AnthropicMessages.protocol.stream,
|
||||
})
|
||||
|
||||
export const route = Route.make({
|
||||
id: "google-vertex-anthropic",
|
||||
provider: "google-vertex-anthropic",
|
||||
providerMetadataKey: "anthropic",
|
||||
protocol,
|
||||
endpoint: Endpoint.path(({ request }) => `/${request.model.id}:streamRawPredict`),
|
||||
auth: Auth.none,
|
||||
framing: Framing.sse,
|
||||
})
|
||||
|
||||
export * as GoogleVertexAnthropic from "./google-vertex-anthropic"
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Gemini } from "./gemini"
|
||||
import { Auth } from "../route/auth"
|
||||
import { Route } from "../route/client"
|
||||
import { Endpoint } from "../route/endpoint"
|
||||
import { Framing } from "../route/framing"
|
||||
|
||||
export const route = Route.make({
|
||||
id: "google-vertex-gemini",
|
||||
provider: "google-vertex",
|
||||
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 * as GoogleVertexGemini from "./google-vertex-gemini"
|
||||
@@ -1,6 +1,4 @@
|
||||
export * as AnthropicMessages from "./anthropic-messages"
|
||||
export * as GoogleVertexAnthropic from "./google-vertex-anthropic"
|
||||
export * as GoogleVertexGemini from "./google-vertex-gemini"
|
||||
export * as BedrockConverse from "./bedrock-converse"
|
||||
export * as Gemini from "./gemini"
|
||||
export * as OpenAIChat from "./openai-chat"
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
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)
|
||||
}
|
||||
+12
-7
@@ -1,10 +1,10 @@
|
||||
import type { ProviderPackage } from "../provider-package"
|
||||
import { GoogleVertexAnthropic } from "../protocols/google-vertex-anthropic"
|
||||
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-anthropic")
|
||||
export const id = ProviderID.make("google-vertex")
|
||||
|
||||
export type Config = RouteDefaultsInput &
|
||||
GoogleVertexShared.OAuthOptions & {
|
||||
@@ -22,11 +22,16 @@ export interface Settings extends ProviderPackage.Settings {
|
||||
readonly providerOptions?: ProviderOptions
|
||||
}
|
||||
|
||||
export const routes = [GoogleVertexAnthropic.route]
|
||||
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 Anthropic does not support API keys")
|
||||
throw new Error("Google Vertex Responses does not support API keys")
|
||||
const {
|
||||
accessToken: _accessToken,
|
||||
auth: _auth,
|
||||
@@ -37,12 +42,12 @@ const configuredRoute = (input: Config) => {
|
||||
} = input
|
||||
const location = GoogleVertexShared.location(inputLocation, "global")
|
||||
const project = GoogleVertexShared.project(inputProject)
|
||||
return GoogleVertexAnthropic.route.with({
|
||||
return route.with({
|
||||
...rest,
|
||||
endpoint: {
|
||||
baseURL:
|
||||
baseURL ??
|
||||
`https://${GoogleVertexShared.host(location)}/v1/projects/${GoogleVertexShared.requireProject(project)}/locations/${location}/publishers/anthropic/models`,
|
||||
`https://aiplatform.googleapis.com/v1/projects/${GoogleVertexShared.requireProject(project)}/locations/${location}/endpoints/openapi`,
|
||||
},
|
||||
auth: GoogleVertexShared.oauth(input, project),
|
||||
})
|
||||
@@ -63,7 +68,7 @@ export const provider = {
|
||||
}
|
||||
|
||||
export const model: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) => {
|
||||
if (settings.apiKey !== undefined) throw new Error("Google Vertex Anthropic does not support API keys")
|
||||
if (settings.apiKey !== undefined) throw new Error("Google Vertex Responses does not support API keys")
|
||||
return configure({
|
||||
accessToken: settings.accessToken,
|
||||
baseURL: settings.baseURL,
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { ProviderPackage } from "../provider-package"
|
||||
import { GoogleVertexGemini } from "../protocols/google-vertex-gemini"
|
||||
import { Gemini } from "../protocols/gemini"
|
||||
import { Auth } from "../route/auth"
|
||||
import type { RouteDefaultsInput } from "../route/client"
|
||||
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"
|
||||
|
||||
@@ -25,7 +27,20 @@ export type Settings = ProviderPackage.Settings &
|
||||
readonly providerOptions?: ProviderOptions
|
||||
}
|
||||
|
||||
export const routes = [GoogleVertexGemini.route]
|
||||
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 {
|
||||
@@ -48,7 +63,7 @@ const configuredRoute = (input: Config, modelID: string | ModelID) => {
|
||||
(apiKey
|
||||
? "https://aiplatform.googleapis.com/v1/publishers/google"
|
||||
: `https://${GoogleVertexShared.host(location)}/v1beta1/projects/${GoogleVertexShared.requireProject(project)}/locations/${location}${endpointModel ? "" : "/publishers/google"}`)
|
||||
return GoogleVertexGemini.route.with({
|
||||
return route.with({
|
||||
...rest,
|
||||
endpoint: { baseURL: endpoint },
|
||||
auth: apiKey === undefined ? GoogleVertexShared.oauth(input, project) : Auth.header("x-goog-api-key", apiKey),
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export { model } from "../google-vertex-anthropic"
|
||||
export type { Settings } from "../google-vertex-anthropic"
|
||||
@@ -0,0 +1,2 @@
|
||||
export { model } from "../google-vertex-chat"
|
||||
export type { Settings } from "../google-vertex-chat"
|
||||
@@ -0,0 +1,2 @@
|
||||
export { model } from "../google-vertex"
|
||||
export type { Settings } from "../google-vertex"
|
||||
@@ -0,0 +1,2 @@
|
||||
export { model } from "../google-vertex-messages"
|
||||
export type { Settings } from "../google-vertex-messages"
|
||||
@@ -0,0 +1,2 @@
|
||||
export { model } from "../google-vertex-responses"
|
||||
export type { Settings } from "../google-vertex-responses"
|
||||
@@ -7,7 +7,9 @@ export { CloudflareAIGateway, CloudflareWorkersAI } from "./cloudflare"
|
||||
export * as GitHubCopilot from "./github-copilot"
|
||||
export * as Google from "./google"
|
||||
export * as GoogleVertex from "./google-vertex"
|
||||
export * as GoogleVertexAnthropic from "./google-vertex-anthropic"
|
||||
export * as GoogleVertexChat from "./google-vertex-chat"
|
||||
export * as GoogleVertexMessages from "./google-vertex-messages"
|
||||
export * as GoogleVertexResponses from "./google-vertex-responses"
|
||||
export * as OpenAI from "./openai"
|
||||
export * as OpenAICompatible from "./openai-compatible"
|
||||
export * as OpenAICompatibleResponses from "./openai-compatible-responses"
|
||||
|
||||
@@ -11,7 +11,9 @@ import * as Cloudflare from "../src/providers/cloudflare"
|
||||
import * as GitHubCopilot from "../src/providers/github-copilot"
|
||||
import * as Google from "../src/providers/google"
|
||||
import * as GoogleVertex from "../src/providers/google-vertex"
|
||||
import * as GoogleVertexAnthropic from "../src/providers/google-vertex-anthropic"
|
||||
import * as GoogleVertexChat from "../src/providers/google-vertex-chat"
|
||||
import * as GoogleVertexMessages from "../src/providers/google-vertex-messages"
|
||||
import * as GoogleVertexResponses from "../src/providers/google-vertex-responses"
|
||||
import * as OpenAI from "../src/providers/openai"
|
||||
import * as OpenAICompatible from "../src/providers/openai-compatible"
|
||||
import * as OpenRouter from "../src/providers/openrouter"
|
||||
@@ -171,20 +173,56 @@ GoogleVertex.configure({ accessToken: "vertex-token", apiKey: "vertex-key", proj
|
||||
// @ts-expect-error Vertex Gemini package settings accept only one auth source.
|
||||
GoogleVertex.model("gemini-3.5-flash", { accessToken: "vertex-token", apiKey: "vertex-key", project: "project" })
|
||||
|
||||
GoogleVertexAnthropic.configure({ accessToken: "vertex-token", project: "project" }).model("claude-sonnet-4-6")
|
||||
// @ts-expect-error Vertex Anthropic package settings do not accept API keys.
|
||||
GoogleVertexAnthropic.model("claude-sonnet-4-6", { apiKey: "vertex-key", project: "project" })
|
||||
GoogleVertexAnthropic.configure({ auth: RuntimeAuth.bearer("vertex-token"), project: "project" }).model(
|
||||
"claude-sonnet-4-6",
|
||||
GoogleVertexChat.configure({ accessToken: "vertex-token", project: "project" }).model("deepseek-ai/deepseek-v3.2-maas")
|
||||
GoogleVertexChat.configure({ auth: RuntimeAuth.bearer("vertex-token"), project: "project" }).model(
|
||||
"deepseek-ai/deepseek-v3.2-maas",
|
||||
)
|
||||
GoogleVertexAnthropic.configure({ accessToken: "vertex-token", project: "project" }).model(
|
||||
"claude-sonnet-4-6",
|
||||
// @ts-expect-error Vertex Anthropic model selectors only accept model ids.
|
||||
// @ts-expect-error Vertex Chat package settings do not accept API keys.
|
||||
GoogleVertexChat.model("deepseek-ai/deepseek-v3.2-maas", { apiKey: "vertex-key", project: "project" })
|
||||
GoogleVertexChat.configure({ accessToken: "vertex-token", project: "project" }).model(
|
||||
"deepseek-ai/deepseek-v3.2-maas",
|
||||
// @ts-expect-error Vertex Chat model selectors only accept model ids.
|
||||
{},
|
||||
)
|
||||
GoogleVertexAnthropic.configure({
|
||||
GoogleVertexChat.configure({
|
||||
accessToken: "vertex-token",
|
||||
// @ts-expect-error Vertex Anthropic config accepts only one auth source.
|
||||
// @ts-expect-error Vertex Chat config accepts only one auth source.
|
||||
auth: RuntimeAuth.bearer("vertex-token"),
|
||||
project: "project",
|
||||
})
|
||||
|
||||
GoogleVertexResponses.configure({ accessToken: "vertex-token", project: "project" }).model("xai/grok-4.20-reasoning")
|
||||
GoogleVertexResponses.configure({ auth: RuntimeAuth.bearer("vertex-token"), project: "project" }).model(
|
||||
"xai/grok-4.20-reasoning",
|
||||
)
|
||||
// @ts-expect-error Vertex Responses package settings do not accept API keys.
|
||||
GoogleVertexResponses.model("xai/grok-4.20-reasoning", { apiKey: "vertex-key", project: "project" })
|
||||
GoogleVertexResponses.configure({ accessToken: "vertex-token", project: "project" }).model(
|
||||
"xai/grok-4.20-reasoning",
|
||||
// @ts-expect-error Vertex Responses model selectors only accept model ids.
|
||||
{},
|
||||
)
|
||||
GoogleVertexResponses.configure({
|
||||
accessToken: "vertex-token",
|
||||
// @ts-expect-error Vertex Responses config accepts only one auth source.
|
||||
auth: RuntimeAuth.bearer("vertex-token"),
|
||||
project: "project",
|
||||
})
|
||||
|
||||
GoogleVertexMessages.configure({ accessToken: "vertex-token", project: "project" }).model("claude-sonnet-4-6")
|
||||
// @ts-expect-error Vertex Messages package settings do not accept API keys.
|
||||
GoogleVertexMessages.model("claude-sonnet-4-6", { apiKey: "vertex-key", project: "project" })
|
||||
GoogleVertexMessages.configure({ auth: RuntimeAuth.bearer("vertex-token"), project: "project" }).model(
|
||||
"claude-sonnet-4-6",
|
||||
)
|
||||
GoogleVertexMessages.configure({ accessToken: "vertex-token", project: "project" }).model(
|
||||
"claude-sonnet-4-6",
|
||||
// @ts-expect-error Vertex Messages model selectors only accept model ids.
|
||||
{},
|
||||
)
|
||||
GoogleVertexMessages.configure({
|
||||
accessToken: "vertex-token",
|
||||
// @ts-expect-error Vertex Messages config accepts only one auth source.
|
||||
auth: RuntimeAuth.bearer("vertex-token"),
|
||||
project: "project",
|
||||
})
|
||||
|
||||
@@ -17,12 +17,16 @@ describe("provider package entrypoints", () => {
|
||||
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/anthropic"),
|
||||
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", () => {
|
||||
@@ -179,20 +183,35 @@ describe("provider package entrypoints", () => {
|
||||
|
||||
test("selects Vertex entrypoints with the same model contract", async () => {
|
||||
const GoogleVertex = await import("@opencode-ai/ai/providers/google-vertex")
|
||||
const GoogleVertexAnthropic = await import("@opencode-ai/ai/providers/google-vertex/anthropic")
|
||||
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 anthropic = GoogleVertexAnthropic.model("claude-sonnet-4-6", {
|
||||
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: [] })
|
||||
@@ -204,15 +223,31 @@ describe("provider package entrypoints", () => {
|
||||
project: "vertex-project",
|
||||
}).route.endpoint.baseURL,
|
||||
).toBe("https://aiplatform.eu.rep.googleapis.com/v1beta1/projects/vertex-project/locations/eu/publishers/google")
|
||||
expect(anthropic.route.id).toBe("google-vertex-anthropic")
|
||||
expect(anthropic.route.endpoint.baseURL).toBe(
|
||||
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 GoogleVertexAnthropic = await import("@opencode-ai/ai/providers/google-vertex/anthropic")
|
||||
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, [
|
||||
@@ -225,15 +260,37 @@ describe("provider package entrypoints", () => {
|
||||
])
|
||||
expect(() => configured.model("gemini-3.5-flash")).toThrow("Google Vertex accessToken cannot be combined with auth")
|
||||
expect(() =>
|
||||
Reflect.apply(GoogleVertexAnthropic.model, undefined, [
|
||||
Reflect.apply(GoogleVertexMessages.model, undefined, [
|
||||
"claude-sonnet-4-6",
|
||||
{ apiKey: "fixture", project: "vertex-project" },
|
||||
]),
|
||||
).toThrow("Google Vertex Anthropic does not support API keys")
|
||||
).toThrow("Google Vertex Messages does not support API keys")
|
||||
expect(() =>
|
||||
Reflect.apply(Providers.GoogleVertexAnthropic.configure, undefined, [
|
||||
Reflect.apply(Providers.GoogleVertexMessages.configure, undefined, [
|
||||
{ apiKey: "fixture", project: "vertex-project" },
|
||||
]),
|
||||
).toThrow("Google Vertex Anthropic does not support API keys")
|
||||
).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")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,10 +2,11 @@ import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { LLM } from "../../src"
|
||||
import { GoogleVertex, GoogleVertexAnthropic } from "../../src/providers"
|
||||
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", () => {
|
||||
@@ -56,7 +57,7 @@ describe("Google Vertex providers", () => {
|
||||
Effect.gen(function* () {
|
||||
const response = yield* LLMClient.generate(
|
||||
LLM.request({
|
||||
model: GoogleVertexAnthropic.configure({
|
||||
model: GoogleVertexMessages.configure({
|
||||
accessToken: "vertex-token",
|
||||
location: "eu",
|
||||
project: "vertex-project",
|
||||
@@ -99,11 +100,91 @@ describe("Google Vertex providers", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("protects the Vertex Anthropic API version from body overlays", () =>
|
||||
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: GoogleVertexAnthropic.configure({
|
||||
model: GoogleVertexMessages.configure({
|
||||
accessToken: "vertex-token",
|
||||
http: { body: { anthropic_version: "wrong" } },
|
||||
project: "vertex-project",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.17.20",
|
||||
"version": "1.18.2",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -32,8 +32,6 @@ const triggerClass =
|
||||
const showPopover = () => true
|
||||
|
||||
export function HelpButton() {
|
||||
if (import.meta.env.VITE_OPENCODE_CHANNEL !== "dev") return null
|
||||
|
||||
const platform = usePlatform()
|
||||
|
||||
return (
|
||||
@@ -56,6 +54,7 @@ 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 (
|
||||
@@ -114,16 +113,18 @@ 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">
|
||||
June 16
|
||||
July 14
|
||||
</p>
|
||||
<DrawerClose
|
||||
as={IconButtonV2}
|
||||
type="button"
|
||||
size="small"
|
||||
variant="ghost-muted"
|
||||
aria-label="Close"
|
||||
icon={<IconV2 name="xmark-small" />}
|
||||
/>
|
||||
<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>
|
||||
</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">
|
||||
@@ -147,7 +148,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-tab-body.settings-v2-models {
|
||||
.settings-v2-models {
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
|
||||
@@ -392,7 +392,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
||||
id: "tab.new",
|
||||
category: "tab",
|
||||
title: language.t("command.session.new"),
|
||||
keybind: "mod+t",
|
||||
keybind: "mod+t,mod+n",
|
||||
hidden: true,
|
||||
onSelect: openNewTab,
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/cli",
|
||||
"version": "1.17.18",
|
||||
"version": "1.18.2",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
|
||||
@@ -66,6 +66,17 @@ 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: [
|
||||
@@ -178,10 +189,7 @@ 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),
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
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 })
|
||||
}
|
||||
@@ -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.connect.oauth(
|
||||
client.integration.oauth.connect(
|
||||
{
|
||||
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.attempt.cancel(
|
||||
{ attemptID: attempt.attemptID, location },
|
||||
client.integration.oauth.cancel(
|
||||
{ integrationID, 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, attempt.attemptID)
|
||||
const status = yield* waitForConsoleLogin(client, integrationID, 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,11 +85,12 @@ 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.attempt.status({ attemptID, location }, { signal }),
|
||||
client.integration.oauth.status({ integrationID, attemptID, location }, { signal }),
|
||||
)
|
||||
if (response.data.status !== "pending") return response.data
|
||||
yield* Effect.sleep(500)
|
||||
|
||||
@@ -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.connect.oauth({ integrationID: integration.id, methodID: method.id, inputs: {}, location }),
|
||||
client.integration.oauth.connect({ 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, attempt.attemptID)
|
||||
const result = yield* poll(client, integration.id, attempt.attemptID)
|
||||
if (result.status === "complete") {
|
||||
process.stdout.write(`Authenticated with ${input.name}` + EOL)
|
||||
return
|
||||
@@ -52,15 +52,16 @@ 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.attempt.status({ attemptID, location })).pipe(
|
||||
Effect.map((result) => result.data),
|
||||
)
|
||||
const status = yield* Effect.promise(() =>
|
||||
client.integration.oauth.status({ integrationID, attemptID, location }),
|
||||
).pipe(Effect.map((result) => result.data))
|
||||
if (status.status === "pending") {
|
||||
yield* Effect.sleep("1 second")
|
||||
return yield* poll(client, attemptID)
|
||||
return yield* poll(client, integrationID, attemptID)
|
||||
}
|
||||
return status
|
||||
})
|
||||
|
||||
@@ -17,6 +17,9 @@ 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"),
|
||||
},
|
||||
|
||||
@@ -407,75 +407,124 @@ 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.connect.key"]>[0]
|
||||
type Endpoint10_2Request = Parameters<RawClient["server.integration"]["integration.wellknown.add"]>[0]
|
||||
export type Endpoint10_2Input = {
|
||||
readonly integrationID: Endpoint10_2Request["params"]["integrationID"]
|
||||
readonly location?: Endpoint10_2Request["query"]["location"]
|
||||
readonly key: Endpoint10_2Request["payload"]["key"]
|
||||
readonly label?: Endpoint10_2Request["payload"]["label"]
|
||||
readonly url: Endpoint10_2Request["payload"]["url"]
|
||||
}
|
||||
export type Endpoint10_2Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.connect.key"]>>
|
||||
export type IntegrationConnectKeyOperation<E = never> = (
|
||||
export type Endpoint10_2Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.wellknown.add"]>>
|
||||
export type IntegrationWellknownAddOperation<E = never> = (
|
||||
input: Endpoint10_2Input,
|
||||
) => Effect.Effect<Endpoint10_2Output, E>
|
||||
|
||||
type Endpoint10_3Request = Parameters<RawClient["server.integration"]["integration.connect.oauth"]>[0]
|
||||
type Endpoint10_3Request = Parameters<RawClient["server.integration"]["integration.connect.key"]>[0]
|
||||
export 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 key: Endpoint10_3Request["payload"]["key"]
|
||||
readonly label?: Endpoint10_3Request["payload"]["label"]
|
||||
}
|
||||
export type Endpoint10_3Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.connect.oauth"]>>
|
||||
export type IntegrationConnectOauthOperation<E = never> = (
|
||||
export type Endpoint10_3Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.connect.key"]>>
|
||||
export type IntegrationConnectKeyOperation<E = never> = (
|
||||
input: Endpoint10_3Input,
|
||||
) => Effect.Effect<Endpoint10_3Output, E>
|
||||
|
||||
type Endpoint10_4Request = Parameters<RawClient["server.integration"]["integration.attempt.status"]>[0]
|
||||
type Endpoint10_4Request = Parameters<RawClient["server.integration"]["integration.oauth.connect"]>[0]
|
||||
export type Endpoint10_4Input = {
|
||||
readonly attemptID: Endpoint10_4Request["params"]["attemptID"]
|
||||
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"]
|
||||
}
|
||||
export type Endpoint10_4Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.attempt.status"]>>
|
||||
export type IntegrationAttemptStatusOperation<E = never> = (
|
||||
export type Endpoint10_4Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.oauth.connect"]>>
|
||||
export type IntegrationOauthConnectOperation<E = never> = (
|
||||
input: Endpoint10_4Input,
|
||||
) => Effect.Effect<Endpoint10_4Output, E>
|
||||
|
||||
type Endpoint10_5Request = Parameters<RawClient["server.integration"]["integration.attempt.complete"]>[0]
|
||||
type Endpoint10_5Request = Parameters<RawClient["server.integration"]["integration.oauth.status"]>[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.attempt.complete"]>
|
||||
>
|
||||
export type IntegrationAttemptCompleteOperation<E = never> = (
|
||||
export type Endpoint10_5Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.oauth.status"]>>
|
||||
export type IntegrationOauthStatusOperation<E = never> = (
|
||||
input: Endpoint10_5Input,
|
||||
) => Effect.Effect<Endpoint10_5Output, E>
|
||||
|
||||
type Endpoint10_6Request = Parameters<RawClient["server.integration"]["integration.attempt.cancel"]>[0]
|
||||
type Endpoint10_6Request = Parameters<RawClient["server.integration"]["integration.oauth.complete"]>[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.attempt.cancel"]>>
|
||||
export type IntegrationAttemptCancelOperation<E = never> = (
|
||||
export type Endpoint10_6Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.oauth.complete"]>>
|
||||
export type IntegrationOauthCompleteOperation<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 connect: {
|
||||
readonly key: IntegrationConnectKeyOperation<E>
|
||||
readonly oauth: IntegrationConnectOauthOperation<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 attempt: {
|
||||
readonly status: IntegrationAttemptStatusOperation<E>
|
||||
readonly complete: IntegrationAttemptCompleteOperation<E>
|
||||
readonly cancel: IntegrationAttemptCancelOperation<E>
|
||||
readonly command: {
|
||||
readonly connect: IntegrationCommandConnectOperation<E>
|
||||
readonly status: IntegrationCommandStatusOperation<E>
|
||||
readonly cancel: IntegrationCommandCancelOperation<E>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -504,75 +504,133 @@ 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.connect.key"]>[0]
|
||||
type Endpoint10_2Request = Parameters<RawClient["server.integration"]["integration.wellknown.add"]>[0]
|
||||
type Endpoint10_2Input = {
|
||||
readonly integrationID: Endpoint10_2Request["params"]["integrationID"]
|
||||
readonly location?: Endpoint10_2Request["query"]["location"]
|
||||
readonly key: Endpoint10_2Request["payload"]["key"]
|
||||
readonly label?: Endpoint10_2Request["payload"]["label"]
|
||||
readonly url: Endpoint10_2Request["payload"]["url"]
|
||||
}
|
||||
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_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"]
|
||||
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"]
|
||||
}
|
||||
const Endpoint10_3 = (raw: RawClient["server.integration"]) => (input: Endpoint10_3Input) =>
|
||||
raw["integration.connect.oauth"]({
|
||||
const Endpoint10_4 = (raw: RawClient["server.integration"]) => (input: Endpoint10_4Input) =>
|
||||
raw["integration.oauth.connect"]({
|
||||
params: { integrationID: input["integrationID"] },
|
||||
query: { location: input["location"] },
|
||||
payload: { methodID: input["methodID"], inputs: input["inputs"], label: input["label"] },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
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"]
|
||||
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"]
|
||||
}
|
||||
const Endpoint10_4 = (raw: RawClient["server.integration"]) => (input: Endpoint10_4Input) =>
|
||||
raw["integration.attempt.status"]({
|
||||
params: { attemptID: input["attemptID"] },
|
||||
const Endpoint10_5 = (raw: RawClient["server.integration"]) => (input: Endpoint10_5Input) =>
|
||||
raw["integration.oauth.status"]({
|
||||
params: { integrationID: input["integrationID"], attemptID: input["attemptID"] },
|
||||
query: { location: input["location"] },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
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"]
|
||||
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"]
|
||||
}
|
||||
const Endpoint10_5 = (raw: RawClient["server.integration"]) => (input: Endpoint10_5Input) =>
|
||||
raw["integration.attempt.complete"]({
|
||||
params: { attemptID: input["attemptID"] },
|
||||
const Endpoint10_6 = (raw: RawClient["server.integration"]) => (input: Endpoint10_6Input) =>
|
||||
raw["integration.oauth.complete"]({
|
||||
params: { integrationID: input["integrationID"], attemptID: input["attemptID"] },
|
||||
query: { location: input["location"] },
|
||||
payload: { code: input["code"] },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
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"]
|
||||
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"]
|
||||
}
|
||||
const Endpoint10_6 = (raw: RawClient["server.integration"]) => (input: Endpoint10_6Input) =>
|
||||
raw["integration.attempt.cancel"]({
|
||||
params: { attemptID: input["attemptID"] },
|
||||
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"] },
|
||||
query: { location: input["location"] },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
const adaptGroup10 = (raw: RawClient["server.integration"]) => ({
|
||||
list: Endpoint10_0(raw),
|
||||
get: Endpoint10_1(raw),
|
||||
connect: { key: Endpoint10_2(raw), oauth: Endpoint10_3(raw) },
|
||||
attempt: { status: Endpoint10_4(raw), complete: Endpoint10_5(raw), cancel: Endpoint10_6(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) },
|
||||
})
|
||||
|
||||
type Endpoint11_0Request = Parameters<RawClient["server.mcp"]["mcp.list"]>[0]
|
||||
|
||||
@@ -82,16 +82,24 @@ import type {
|
||||
IntegrationListOutput,
|
||||
IntegrationGetInput,
|
||||
IntegrationGetOutput,
|
||||
IntegrationWellknownAddInput,
|
||||
IntegrationWellknownAddOutput,
|
||||
IntegrationConnectKeyInput,
|
||||
IntegrationConnectKeyOutput,
|
||||
IntegrationConnectOauthInput,
|
||||
IntegrationConnectOauthOutput,
|
||||
IntegrationAttemptStatusInput,
|
||||
IntegrationAttemptStatusOutput,
|
||||
IntegrationAttemptCompleteInput,
|
||||
IntegrationAttemptCompleteOutput,
|
||||
IntegrationAttemptCancelInput,
|
||||
IntegrationAttemptCancelOutput,
|
||||
IntegrationOauthConnectInput,
|
||||
IntegrationOauthConnectOutput,
|
||||
IntegrationOauthStatusInput,
|
||||
IntegrationOauthStatusOutput,
|
||||
IntegrationOauthCompleteInput,
|
||||
IntegrationOauthCompleteOutput,
|
||||
IntegrationOauthCancelInput,
|
||||
IntegrationOauthCancelOutput,
|
||||
IntegrationCommandConnectInput,
|
||||
IntegrationCommandConnectOutput,
|
||||
IntegrationCommandStatusInput,
|
||||
IntegrationCommandStatusOutput,
|
||||
IntegrationCommandCancelInput,
|
||||
IntegrationCommandCancelOutput,
|
||||
McpListInput,
|
||||
McpListOutput,
|
||||
McpResourceCatalogInput,
|
||||
@@ -887,6 +895,21 @@ 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>(
|
||||
@@ -901,8 +924,10 @@ export function make(options: ClientOptions) {
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
oauth: (input: IntegrationConnectOauthInput, requestOptions?: RequestOptions) =>
|
||||
request<IntegrationConnectOauthOutput>(
|
||||
},
|
||||
oauth: {
|
||||
connect: (input: IntegrationOauthConnectInput, requestOptions?: RequestOptions) =>
|
||||
request<IntegrationOauthConnectOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth`,
|
||||
@@ -914,13 +939,11 @@ export function make(options: ClientOptions) {
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
attempt: {
|
||||
status: (input: IntegrationAttemptStatusInput, requestOptions?: RequestOptions) =>
|
||||
request<IntegrationAttemptStatusOutput>(
|
||||
status: (input: IntegrationOauthStatusInput, requestOptions?: RequestOptions) =>
|
||||
request<IntegrationOauthStatusOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}`,
|
||||
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth/${encodeURIComponent(input.attemptID)}`,
|
||||
query: { location: input["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [401, 400],
|
||||
@@ -928,11 +951,11 @@ export function make(options: ClientOptions) {
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
complete: (input: IntegrationAttemptCompleteInput, requestOptions?: RequestOptions) =>
|
||||
request<IntegrationAttemptCompleteOutput>(
|
||||
complete: (input: IntegrationOauthCompleteInput, requestOptions?: RequestOptions) =>
|
||||
request<IntegrationOauthCompleteOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}/complete`,
|
||||
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth/${encodeURIComponent(input.attemptID)}/complete`,
|
||||
query: { location: input["location"] },
|
||||
body: { code: input["code"] },
|
||||
successStatus: 204,
|
||||
@@ -941,11 +964,50 @@ export function make(options: ClientOptions) {
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
cancel: (input: IntegrationAttemptCancelInput, requestOptions?: RequestOptions) =>
|
||||
request<IntegrationAttemptCancelOutput>(
|
||||
cancel: (input: IntegrationOauthCancelInput, requestOptions?: RequestOptions) =>
|
||||
request<IntegrationOauthCancelOutput>(
|
||||
{
|
||||
method: "DELETE",
|
||||
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}`,
|
||||
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)}`,
|
||||
query: { location: input["location"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [401, 400],
|
||||
|
||||
@@ -187,6 +187,8 @@ 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> }
|
||||
@@ -214,6 +216,31 @@ 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" }
|
||||
@@ -1992,7 +2019,11 @@ export type SessionMessageAssistantTool = {
|
||||
time: { created: number; ran?: number; completed?: number }
|
||||
}
|
||||
|
||||
export type IntegrationMethod = IntegrationOAuthMethod | IntegrationKeyMethod | IntegrationEnvMethod
|
||||
export type IntegrationMethod =
|
||||
| IntegrationOAuthMethod
|
||||
| IntegrationCommandMethod
|
||||
| IntegrationKeyMethod
|
||||
| IntegrationEnvMethod
|
||||
|
||||
export type FormFields = [FormField, ...Array<FormField>]
|
||||
|
||||
@@ -3273,6 +3304,15 @@ 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?: {
|
||||
@@ -3284,7 +3324,7 @@ export type IntegrationConnectKeyInput = {
|
||||
|
||||
export type IntegrationConnectKeyOutput = void
|
||||
|
||||
export type IntegrationConnectOauthInput = {
|
||||
export type IntegrationOauthConnectInput = {
|
||||
readonly integrationID: { readonly integrationID: string }["integrationID"]
|
||||
readonly location?: {
|
||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||
@@ -3306,7 +3346,7 @@ export type IntegrationConnectOauthInput = {
|
||||
}["label"]
|
||||
}
|
||||
|
||||
export type IntegrationConnectOauthOutput = {
|
||||
export type IntegrationOauthConnectOutput = {
|
||||
location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
|
||||
data: {
|
||||
attemptID: string
|
||||
@@ -3317,36 +3357,76 @@ export type IntegrationConnectOauthOutput = {
|
||||
}
|
||||
}
|
||||
|
||||
export type IntegrationAttemptStatusInput = {
|
||||
readonly attemptID: { readonly attemptID: string }["attemptID"]
|
||||
export type IntegrationOauthStatusInput = {
|
||||
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 IntegrationAttemptStatusOutput = {
|
||||
export type IntegrationOauthStatusOutput = {
|
||||
location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
|
||||
data: IntegrationAttemptStatus
|
||||
}
|
||||
|
||||
export type IntegrationAttemptCompleteInput = {
|
||||
readonly attemptID: { readonly attemptID: string }["attemptID"]
|
||||
export type IntegrationOauthCompleteInput = {
|
||||
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"]
|
||||
readonly code?: { readonly code?: string | undefined }["code"]
|
||||
}
|
||||
|
||||
export type IntegrationAttemptCompleteOutput = void
|
||||
export type IntegrationOauthCompleteOutput = void
|
||||
|
||||
export type IntegrationAttemptCancelInput = {
|
||||
readonly attemptID: { readonly attemptID: string }["attemptID"]
|
||||
export type IntegrationOauthCancelInput = {
|
||||
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 IntegrationAttemptCancelOutput = void
|
||||
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 McpListInput = {
|
||||
readonly location?: {
|
||||
|
||||
@@ -36,9 +36,11 @@ 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", "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.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.file)).toEqual(["read", "list", "find"])
|
||||
expect(Object.keys(client.vcs)).toEqual(["status", "diff"])
|
||||
expect(Object.keys(client.pty)).toEqual(["list", "create", "get", "update", "remove"])
|
||||
@@ -61,6 +63,28 @@ 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({
|
||||
|
||||
@@ -4,10 +4,9 @@ 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 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.
|
||||
- `[ ]` 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.
|
||||
|
||||
When behavior changes, update this file and the tests in the same change. The implementation and tests remain the
|
||||
ultimate source of truth.
|
||||
@@ -19,42 +18,45 @@ 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] JSON-like host boundaries with `undefined` and non-finite numbers normalized to `null`.
|
||||
- [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] 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 object spread.
|
||||
- [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] Template literals with interpolation.
|
||||
- [x] Regular-expression literals.
|
||||
- [x] `NaN` and `Infinity` globals.
|
||||
- [ ] BigInt literals and values.
|
||||
- [ ] Symbols.
|
||||
- [ ] Tagged template literals.
|
||||
- [ ] Getters and setters in object literals.
|
||||
- [ ] 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.
|
||||
|
||||
## 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, object fields, array indexes, and writable URL fields.
|
||||
- [x] Function declarations are hoisted within their interpreted scope.
|
||||
- [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] Parameter defaults observe a temporal dead zone for later parameters.
|
||||
- [ ] 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]`.
|
||||
- [ ] 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.
|
||||
|
||||
## Statements and control flow
|
||||
|
||||
@@ -69,7 +71,6 @@ 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
|
||||
|
||||
@@ -90,15 +91,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.
|
||||
- [x] Async string replacement callbacks; replacements are evaluated sequentially.
|
||||
- [ ] Stop automatically awaiting promise-returning string replacers; match JavaScript's synchronous callback-result
|
||||
coercion.
|
||||
- [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`, `super`, user-defined constructor functions, or function prototype methods such as `call`, `apply`,
|
||||
and `bind`.
|
||||
- [ ] `this` in non-arrow CodeMode functions and callbacks.
|
||||
- [ ] User-defined constructor calls.
|
||||
- [ ] `Function.prototype.call`, `apply`, and `bind` for CodeMode functions.
|
||||
- [ ] 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
|
||||
|
||||
@@ -108,16 +109,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 Error types, Date, RegExp, Map, Set, URL, URLSearchParams, and Promise.
|
||||
- [x] `new` for Array, Object, 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 `-`, `typeof`, `instanceof`, and own-property-only `in`.
|
||||
- [x] Unary `+`, unary `-`, `void`, `typeof`, `instanceof`, and own-property-only `in`.
|
||||
- [x] Prefix and postfix `++` and `--`.
|
||||
- [x] Plain, arithmetic, bitwise, and logical assignment operators.
|
||||
- [ ] Unary `void` and `delete`.
|
||||
- [ ] Arbitrary constructors.
|
||||
- [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.
|
||||
|
||||
## Promises and tools
|
||||
|
||||
@@ -152,8 +153,13 @@ 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 `then` method are plain data, not promises).
|
||||
- [ ] Async iterables, host streams, and stream consumption.
|
||||
- [ ] 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.
|
||||
|
||||
## Objects and properties
|
||||
|
||||
@@ -164,19 +170,17 @@ 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] Blocked access to `__proto__`, `constructor`, and `prototype`.
|
||||
- [ ] `Object.is`; runtime and tool-reference identity semantics need to be defined first.
|
||||
- [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.
|
||||
- [ ] `Object.groupBy`.
|
||||
- [ ] Object creation, descriptors, freezing/sealing, prototype APIs, and reflection APIs.
|
||||
- [ ] A final policy for legal data keys named `__proto__`, `constructor`, or `prototype` (tool path segments
|
||||
already allow them; see known semantic gaps).
|
||||
|
||||
## Arrays
|
||||
|
||||
- [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] 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] 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`.
|
||||
@@ -190,8 +194,10 @@ 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 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.
|
||||
- [ ] 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.
|
||||
|
||||
## Strings
|
||||
|
||||
@@ -204,8 +210,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`.
|
||||
- [ ] Locale/options-aware `localeCompare` and locale formatting APIs.
|
||||
- [ ] Exact native coercion across every string method; CodeMode often requires explicit strings/numbers.
|
||||
- [ ] Native argument coercion for supported String methods; for example, `includes(1)` and `slice("1")` currently
|
||||
reject instead of coercing.
|
||||
- [ ] Native no-argument parity for `match()` and `search()`.
|
||||
|
||||
## Numbers and Math
|
||||
@@ -221,20 +227,21 @@ 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 `""`.
|
||||
- [ ] Safe interpreter coercion for `++` and `--` rather than host `Number(...)` coercion.
|
||||
- [ ] Reliable feature detection for unknown static members.
|
||||
- [ ] `++` 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.
|
||||
- [ ] `Math.sumPrecise`.
|
||||
- [ ] Global coercing `isFinite` and `isNaN`.
|
||||
|
||||
## JSON and console
|
||||
|
||||
- [x] `JSON.parse` and `JSON.stringify`.
|
||||
- [x] `JSON.parse` and `JSON.stringify` for supported data objects; the blocked data-key gap listed above still applies.
|
||||
- [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
|
||||
|
||||
@@ -250,22 +257,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.
|
||||
- [ ] `toUTCString`, locale methods, and other Date formatting methods.
|
||||
- [ ] Exact native constructor coercion, local-time, and loose-equality semantics.
|
||||
- [ ] `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.
|
||||
- [ ] 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, named groups, match indexes, and stateful global matching.
|
||||
- [x] Integration with supported String methods, including async function replacers.
|
||||
- [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.
|
||||
- [ ] Writable `lastIndex`.
|
||||
- [ ] Exposed metadata for the `d` and `v` flags.
|
||||
- [ ] `hasIndices`, match `indices`, and `unicodeSets` metadata for the `d` and `v` flags.
|
||||
- [ ] `RegExp.escape`.
|
||||
- [ ] Protection from pathological host-regex backtracking beyond the cooperative execution timeout.
|
||||
|
||||
## Map and Set
|
||||
|
||||
@@ -276,9 +283,8 @@ 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 methods such as `union`, `intersection`, `difference`, and relation predicates.
|
||||
- [ ] WeakMap and WeakSet.
|
||||
- [ ] Native iterator objects and custom iterators.
|
||||
- [ ] Set composition and relation methods: `union`, `intersection`, `difference`, `symmetricDifference`, `isSubsetOf`,
|
||||
`isSupersetOf`, and `isDisjointFrom`.
|
||||
|
||||
## URL and URI helpers
|
||||
|
||||
@@ -301,48 +307,12 @@ 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 interpreter failures and awaited tool failures.
|
||||
- [x] Source locations on unsupported-syntax diagnostics when available.
|
||||
- [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] Sanitized model-visible diagnostics and explicit safe `ToolError` messages.
|
||||
- [ ] 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`.
|
||||
- [x] Canonicalize dotted tool names into namespace paths so every advertised dotted path is executable, one
|
||||
canonical path can be both a callable tool and a namespace, and the last definition supplied for a canonical
|
||||
path wins.
|
||||
- [x] Allow blocked member names (`constructor`, `prototype`, `__proto__`) as tool path segments: segments are Map
|
||||
keys and inert strings, never plain-object property accesses, so every advertised path is executable. Blocked
|
||||
member access on data values stays rejected. Tool names with empty segments are rejected at construction.
|
||||
- [ ] 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.
|
||||
- [ ] 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.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/codemode",
|
||||
"version": "1.17.20",
|
||||
"version": "1.18.2",
|
||||
"description": "Effect-native confined code execution over schema-described tools",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -34,30 +34,51 @@ export const isRuntimeReference = (value: unknown): boolean =>
|
||||
value instanceof ErrorConstructorReference ||
|
||||
isCodeModeValue(value)
|
||||
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
// CodeMode values are data here, not opaque interpreter references.
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
// Reject cycles before mutation so later boundary walks remain safe.
|
||||
@@ -66,15 +87,22 @@ export const rejectCircularInsertion = (
|
||||
value: unknown,
|
||||
label: string,
|
||||
node: AstNode,
|
||||
seen = new Set<object>(),
|
||||
): void => {
|
||||
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)
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
export const typeofValue = (value: unknown): string => {
|
||||
|
||||
@@ -777,9 +777,9 @@ export class Interpreter<R> {
|
||||
}
|
||||
|
||||
if (pattern.type === "ObjectPattern") {
|
||||
if (value === null || typeof value !== "object" || Array.isArray(value) || isRuntimeReference(value)) {
|
||||
if (value === null || typeof value !== "object" || isRuntimeReference(value)) {
|
||||
throw new InterpreterRuntimeError(
|
||||
"Object destructuring requires a data object value.",
|
||||
"Object destructuring requires a data object or array value.",
|
||||
pattern,
|
||||
"InvalidDataValue",
|
||||
)
|
||||
@@ -798,38 +798,35 @@ export class Interpreter<R> {
|
||||
continue
|
||||
}
|
||||
|
||||
if (
|
||||
property.type !== "Property" ||
|
||||
getBoolean(property, "computed") ||
|
||||
getString(property, "kind") !== "init"
|
||||
) {
|
||||
throw new InterpreterRuntimeError("Only named object destructuring properties are supported.", property)
|
||||
const key = yield* self.destructuringPropertyKey(property)
|
||||
if (isBlockedMember(String(key))) {
|
||||
throw new InterpreterRuntimeError(`Property '${String(key)}' is not available in CodeMode.`, 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)
|
||||
consumed.add(String(key))
|
||||
yield* self.declarePattern(
|
||||
getNode(property, "value"),
|
||||
self.destructuringPropertyValue(value as SafeObject | Array<unknown>, key),
|
||||
mutable,
|
||||
property,
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (pattern.type === "ArrayPattern") {
|
||||
if (!Array.isArray(value)) {
|
||||
throw new InterpreterRuntimeError("Array destructuring requires an array value.", pattern)
|
||||
const items = spreadItems(value)
|
||||
if (items === undefined) {
|
||||
throw new InterpreterRuntimeError("Array destructuring requires a supported iterable 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"), value.slice(index), mutable, element)
|
||||
yield* self.declarePattern(getNode(element, "argument"), items.slice(index), mutable, element)
|
||||
break
|
||||
}
|
||||
yield* self.declarePattern(element, value[index], mutable, pattern)
|
||||
yield* self.declarePattern(element, items[index], mutable, pattern)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -858,15 +855,15 @@ export class Interpreter<R> {
|
||||
}
|
||||
|
||||
if (pattern.type === "ObjectPattern") {
|
||||
if (value === null || typeof value !== "object" || Array.isArray(value) || isRuntimeReference(value)) {
|
||||
if (value === null || typeof value !== "object" || isRuntimeReference(value)) {
|
||||
throw new InterpreterRuntimeError(
|
||||
"Object destructuring requires a data object value.",
|
||||
"Object destructuring requires a data object or array value.",
|
||||
pattern,
|
||||
"InvalidDataValue",
|
||||
)
|
||||
}
|
||||
|
||||
const source = value as SafeObject
|
||||
const source = value as SafeObject | Array<unknown>
|
||||
const consumed = new Set<string>()
|
||||
for (const propertyValue of getArray(pattern, "properties")) {
|
||||
const property = asNode(propertyValue, "properties")
|
||||
@@ -878,36 +875,29 @@ export class Interpreter<R> {
|
||||
yield* self.assignPattern(getNode(property, "argument"), rest, property)
|
||||
continue
|
||||
}
|
||||
if (
|
||||
property.type !== "Property" ||
|
||||
getBoolean(property, "computed") ||
|
||||
getString(property, "kind") !== "init"
|
||||
) {
|
||||
throw new InterpreterRuntimeError("Only named object destructuring properties are supported.", property)
|
||||
const key = yield* self.destructuringPropertyKey(property)
|
||||
if (isBlockedMember(String(key))) {
|
||||
throw new InterpreterRuntimeError(`Property '${String(key)}' is not available in CodeMode.`, 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)
|
||||
consumed.add(String(key))
|
||||
yield* self.assignPattern(getNode(property, "value"), self.destructuringPropertyValue(source, key), property)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (pattern.type === "ArrayPattern") {
|
||||
if (!Array.isArray(value)) {
|
||||
throw new InterpreterRuntimeError("Array destructuring requires an array value.", pattern)
|
||||
const items = spreadItems(value)
|
||||
if (items === undefined) {
|
||||
throw new InterpreterRuntimeError("Array destructuring requires a supported iterable 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"), value.slice(index), element)
|
||||
yield* self.assignPattern(getNode(element, "argument"), items.slice(index), element)
|
||||
break
|
||||
}
|
||||
yield* self.assignPattern(element, value[index], pattern)
|
||||
yield* self.assignPattern(element, items[index], pattern)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -916,6 +906,26 @@ 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": {
|
||||
@@ -1279,6 +1289,7 @@ 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")
|
||||
@@ -1286,6 +1297,7 @@ 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")
|
||||
}
|
||||
@@ -1729,6 +1741,7 @@ export class Interpreter<R> {
|
||||
|
||||
private getMemberReference(
|
||||
node: AstNode,
|
||||
operation: "read" | "delete" = "read",
|
||||
): Effect.Effect<
|
||||
| MemberReference
|
||||
| ToolReference
|
||||
@@ -1875,6 +1888,7 @@ export class Interpreter<R> {
|
||||
}
|
||||
|
||||
if (Array.isArray(objectValue)) {
|
||||
if (operation === "delete") return { target: objectValue, key }
|
||||
if (
|
||||
key !== "length" &&
|
||||
!(typeof key === "string" && arrayMethods.has(key)) &&
|
||||
@@ -1923,6 +1937,29 @@ 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,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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"
|
||||
@@ -44,6 +45,11 @@ 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)) {
|
||||
|
||||
@@ -99,6 +99,84 @@ 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)
|
||||
@@ -470,4 +548,71 @@ 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")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -593,6 +593,24 @@ 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"],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.17.20",
|
||||
"version": "1.18.2",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -9,6 +9,7 @@ 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()
|
||||
@@ -33,6 +34,7 @@ export default function App() {
|
||||
<MetaProvider>
|
||||
<AppMeta />
|
||||
<Suspense>{props.children}</Suspense>
|
||||
<DesktopPromo />
|
||||
</MetaProvider>
|
||||
</I18nProvider>
|
||||
</LanguageProvider>
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,103 @@
|
||||
[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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -102,10 +102,14 @@ 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": "نماذج مجانية مضمّنة أو اربط أي نموذج من أي مزوّد،",
|
||||
@@ -700,11 +704,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 لسطح المكتب (Beta)",
|
||||
"download.section.desktop": "OpenCode لسطح المكتب",
|
||||
"download.section.extensions": "امتدادات OpenCode",
|
||||
"download.section.integrations": "تكاملات OpenCode",
|
||||
"download.action.download": "تنزيل",
|
||||
|
||||
@@ -102,10 +102,14 @@ export const dict = {
|
||||
"temp.logoDarkAlt": "logo opencode escuro",
|
||||
|
||||
"home.banner.badge": "Novo",
|
||||
"home.banner.text": "App desktop disponível em beta",
|
||||
"home.banner.platforms": "no macOS, Windows e Linux",
|
||||
"home.banner.text": "Conheça as abas no app desktop.",
|
||||
"home.banner.platforms": "Disponível 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,",
|
||||
@@ -711,11 +715,11 @@ export const dict = {
|
||||
|
||||
"download.title": "OpenCode | Baixar",
|
||||
"download.meta.description": "Baixe o OpenCode para macOS, Windows e Linux",
|
||||
"download.hero.title": "Baixar OpenCode",
|
||||
"download.hero.title": "Baixar OpenCode Desktop",
|
||||
"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 (Beta)",
|
||||
"download.section.desktop": "OpenCode Desktop",
|
||||
"download.section.extensions": "Extensões OpenCode",
|
||||
"download.section.integrations": "Integrações OpenCode",
|
||||
"download.action.download": "Baixar",
|
||||
|
||||
@@ -102,10 +102,14 @@ export const dict = {
|
||||
"temp.logoDarkAlt": "opencode logo dark",
|
||||
|
||||
"home.banner.badge": "Ny",
|
||||
"home.banner.text": "Desktop-app tilgængelig i beta",
|
||||
"home.banner.platforms": "på macOS, Windows og Linux",
|
||||
"home.banner.text": "Introduktion af Tabs til Desktop.",
|
||||
"home.banner.platforms": "Tilgængelig 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,",
|
||||
@@ -706,11 +710,11 @@ export const dict = {
|
||||
|
||||
"download.title": "OpenCode | Download",
|
||||
"download.meta.description": "Download OpenCode til macOS, Windows og Linux",
|
||||
"download.hero.title": "Download OpenCode",
|
||||
"download.hero.title": "Download OpenCode Desktop",
|
||||
"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 (Beta)",
|
||||
"download.section.desktop": "OpenCode Desktop",
|
||||
"download.section.extensions": "OpenCode Extensions",
|
||||
"download.section.integrations": "OpenCode Integrations",
|
||||
"download.action.download": "Download",
|
||||
|
||||
@@ -102,10 +102,14 @@ export const dict = {
|
||||
"temp.logoDarkAlt": "OpenCode Logo dunkel",
|
||||
|
||||
"home.banner.badge": "Neu",
|
||||
"home.banner.text": "Desktop-App in der Beta verfügbar",
|
||||
"home.banner.platforms": "auf macOS, Windows und Linux",
|
||||
"home.banner.text": "Neu: Tabs für Desktop.",
|
||||
"home.banner.platforms": "Verfügbar für 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,",
|
||||
@@ -711,11 +715,11 @@ export const dict = {
|
||||
|
||||
"download.title": "OpenCode | Download",
|
||||
"download.meta.description": "Lade OpenCode für macOS, Windows und Linux herunter",
|
||||
"download.hero.title": "OpenCode herunterladen",
|
||||
"download.hero.title": "OpenCode Desktop 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 (Beta)",
|
||||
"download.section.desktop": "OpenCode Desktop",
|
||||
"download.section.extensions": "OpenCode Extensions",
|
||||
"download.section.integrations": "OpenCode Integrationen",
|
||||
"download.action.download": "Download",
|
||||
|
||||
@@ -102,11 +102,16 @@ export const dict = {
|
||||
"temp.logoDarkAlt": "opencode logo dark",
|
||||
|
||||
"home.banner.badge": "New",
|
||||
"home.banner.text": "Desktop app available in beta",
|
||||
"home.banner.platforms": "on macOS, Windows, and Linux",
|
||||
"home.banner.text": "Introducing tabs for desktop.",
|
||||
"home.banner.platforms": "Available 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.",
|
||||
@@ -703,11 +708,11 @@ export const dict = {
|
||||
|
||||
"download.title": "OpenCode | Download",
|
||||
"download.meta.description": "Download OpenCode for macOS, Windows, and Linux",
|
||||
"download.hero.title": "Download OpenCode",
|
||||
"download.hero.title": "Download OpenCode Desktop",
|
||||
"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 (Beta)",
|
||||
"download.section.desktop": "OpenCode Desktop",
|
||||
"download.section.extensions": "OpenCode Extensions",
|
||||
"download.section.integrations": "OpenCode Integrations",
|
||||
"download.action.download": "Download",
|
||||
|
||||
@@ -102,10 +102,14 @@ export const dict = {
|
||||
"temp.logoDarkAlt": "logo de opencode oscuro",
|
||||
|
||||
"home.banner.badge": "Nuevo",
|
||||
"home.banner.text": "Aplicación de escritorio disponible en beta",
|
||||
"home.banner.platforms": "en macOS, Windows y Linux",
|
||||
"home.banner.text": "Presentamos Tabs para Desktop.",
|
||||
"home.banner.platforms": "Disponible 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,",
|
||||
@@ -711,11 +715,11 @@ export const dict = {
|
||||
|
||||
"download.title": "OpenCode | Descargar",
|
||||
"download.meta.description": "Descarga OpenCode para macOS, Windows y Linux",
|
||||
"download.hero.title": "Descargar OpenCode",
|
||||
"download.hero.title": "Descargar OpenCode Desktop",
|
||||
"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 (Beta)",
|
||||
"download.section.desktop": "OpenCode Desktop",
|
||||
"download.section.extensions": "Extensiones OpenCode",
|
||||
"download.section.integrations": "Integraciones OpenCode",
|
||||
"download.action.download": "Descargar",
|
||||
|
||||
@@ -100,10 +100,14 @@ export const dict = {
|
||||
"temp.logoDarkAlt": "opencode logo dark",
|
||||
|
||||
"home.banner.badge": "Nouveau",
|
||||
"home.banner.text": "Application desktop disponible en bêta",
|
||||
"home.banner.platforms": "sur macOS, Windows et Linux",
|
||||
"home.banner.text": "Découvrez Tabs pour Desktop.",
|
||||
"home.banner.platforms": "Disponible 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 l’annonce de l’application Desktop",
|
||||
|
||||
"home.hero.title": "L'agent de code IA open source",
|
||||
"home.hero.subtitle.a":
|
||||
@@ -718,11 +722,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",
|
||||
"download.hero.title": "Télécharger OpenCode Desktop",
|
||||
"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 (Bêta)",
|
||||
"download.section.desktop": "OpenCode Desktop",
|
||||
"download.section.extensions": "Extensions OpenCode",
|
||||
"download.section.integrations": "Intégrations OpenCode",
|
||||
"download.action.download": "Télécharger",
|
||||
|
||||
@@ -102,10 +102,14 @@ export const dict = {
|
||||
"temp.logoDarkAlt": "logo scuro di opencode",
|
||||
|
||||
"home.banner.badge": "Nuovo",
|
||||
"home.banner.text": "App desktop disponibile in beta",
|
||||
"home.banner.platforms": "su macOS, Windows e Linux",
|
||||
"home.banner.text": "Scopri Tabs per Desktop.",
|
||||
"home.banner.platforms": "Disponibile 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 l’ultima versione per iniziare.",
|
||||
"home.promo.close": "Chiudi l’annuncio dell’app 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,",
|
||||
@@ -709,11 +713,11 @@ export const dict = {
|
||||
|
||||
"download.title": "OpenCode | Download",
|
||||
"download.meta.description": "Scarica OpenCode per macOS, Windows e Linux",
|
||||
"download.hero.title": "Scarica OpenCode",
|
||||
"download.hero.title": "Scarica OpenCode Desktop",
|
||||
"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 (Beta)",
|
||||
"download.section.desktop": "OpenCode Desktop",
|
||||
"download.section.extensions": "OpenCode Extensions",
|
||||
"download.section.integrations": "OpenCode Integrations",
|
||||
"download.action.download": "Scarica",
|
||||
|
||||
@@ -102,10 +102,14 @@ 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": "無料モデルが含まれています。また、任意のプロバイダーの任意のモデルに接続でき、",
|
||||
@@ -708,11 +712,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 (Beta)",
|
||||
"download.section.desktop": "OpenCode Desktop",
|
||||
"download.section.extensions": "OpenCode Extensions",
|
||||
"download.section.integrations": "OpenCode Integrations",
|
||||
"download.action.download": "ダウンロード",
|
||||
|
||||
@@ -102,10 +102,14 @@ 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": "무료 모델이 포함되어 있으며, 어떤 제공자의 모델이든 연결 가능합니다.",
|
||||
@@ -700,11 +704,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": "다운로드",
|
||||
|
||||
@@ -102,10 +102,14 @@ export const dict = {
|
||||
"temp.logoDarkAlt": "opencode logo mørk",
|
||||
|
||||
"home.banner.badge": "Ny",
|
||||
"home.banner.text": "Desktop-app tilgjengelig i beta",
|
||||
"home.banner.platforms": "på macOS, Windows og Linux",
|
||||
"home.banner.text": "Vi introduserer Tabs for Desktop.",
|
||||
"home.banner.platforms": "Tilgjengelig 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":
|
||||
@@ -707,11 +711,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",
|
||||
"download.hero.title": "Last ned OpenCode Desktop",
|
||||
"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 (Beta)",
|
||||
"download.section.desktop": "OpenCode Desktop",
|
||||
"download.section.extensions": "OpenCode Extensions",
|
||||
"download.section.integrations": "OpenCode Integrations",
|
||||
"download.action.download": "Last ned",
|
||||
|
||||
@@ -101,10 +101,14 @@ export const dict = {
|
||||
"temp.logoDarkAlt": "ciemne logo opencode",
|
||||
|
||||
"home.banner.badge": "Nowość",
|
||||
"home.banner.text": "Aplikacja desktopowa dostępna w wersji beta",
|
||||
"home.banner.platforms": "na macOS, Windows i Linux",
|
||||
"home.banner.text": "Przedstawiamy karty w aplikacji desktopowej.",
|
||||
"home.banner.platforms": "Dostępne 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,",
|
||||
@@ -708,11 +712,11 @@ export const dict = {
|
||||
|
||||
"download.title": "OpenCode | Pobierz",
|
||||
"download.meta.description": "Pobierz OpenCode na macOS, Windows i Linux",
|
||||
"download.hero.title": "Pobierz OpenCode",
|
||||
"download.hero.title": "Pobierz OpenCode Desktop",
|
||||
"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 (Beta)",
|
||||
"download.section.desktop": "Pulpit OpenCode",
|
||||
"download.section.extensions": "Rozszerzenia OpenCode",
|
||||
"download.section.integrations": "Integracje OpenCode",
|
||||
"download.action.download": "Pobierz",
|
||||
|
||||
@@ -102,10 +102,14 @@ 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": "Бесплатные модели включены, или подключите любую модель от любого провайдера,",
|
||||
@@ -716,11 +720,11 @@ export const dict = {
|
||||
|
||||
"download.title": "OpenCode | Скачать",
|
||||
"download.meta.description": "Скачать OpenCode для macOS, Windows и Linux",
|
||||
"download.hero.title": "Скачать OpenCode",
|
||||
"download.hero.title": "Скачать OpenCode Desktop",
|
||||
"download.hero.subtitle": "Доступна бета для macOS, Windows и Linux",
|
||||
"download.hero.button": "Скачать для {{os}}",
|
||||
"download.section.terminal": "OpenCode Terminal",
|
||||
"download.section.desktop": "OpenCode Desktop (Beta)",
|
||||
"download.section.desktop": "OpenCode Desktop",
|
||||
"download.section.extensions": "Расширения OpenCode",
|
||||
"download.section.integrations": "Интеграции OpenCode",
|
||||
"download.action.download": "Скачать",
|
||||
|
||||
@@ -102,10 +102,14 @@ 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": "มีโมเดลฟรีรวมอยู่ หรือเชื่อมต่อโมเดลใดก็ได้จากผู้ให้บริการรายใดก็ได้,",
|
||||
@@ -703,11 +707,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 (Beta)",
|
||||
"download.section.desktop": "OpenCode Desktop",
|
||||
"download.section.extensions": "ส่วนขยาย OpenCode",
|
||||
"download.section.integrations": "การเชื่อมต่อ OpenCode",
|
||||
"download.action.download": "ดาวน์โหลด",
|
||||
|
||||
@@ -102,10 +102,14 @@ export const dict = {
|
||||
"temp.logoDarkAlt": "opencode koyu logo",
|
||||
|
||||
"home.banner.badge": "Yeni",
|
||||
"home.banner.text": "Masaüstü uygulaması beta olarak mevcut",
|
||||
"home.banner.platforms": "macOS, Windows ve Linux'ta",
|
||||
"home.banner.text": "Masaüstü uygulamasında sekmelerle tanışın.",
|
||||
"home.banner.platforms": "macOS, Windows ve Linux'ta kullanılabilir",
|
||||
"home.banner.downloadNow": "Şimdi indir",
|
||||
"home.banner.downloadBetaNow": "Masaüstü betayı şimdi indir",
|
||||
"home.promo.title": "Masaüstü uygulamasında sekmelerle tanışın",
|
||||
"home.promo.body": "Çalışmalarınızı ve etkin oturumlarınızı sekmelerle düzenleyin.",
|
||||
"home.promo.cta": "Başlamak için en son sürümü indirin.",
|
||||
"home.promo.close": "Masaüstü uygulaması duyurusunu kapat",
|
||||
|
||||
"home.hero.title": "Açık kaynaklı yapay zeka kodlama ajanı",
|
||||
"home.hero.subtitle.a": "Ücretsiz modeller dahil veya herhangi bir sağlayıcıdan herhangi bir modeli bağlayın,",
|
||||
@@ -711,11 +715,11 @@ export const dict = {
|
||||
|
||||
"download.title": "OpenCode | İndir",
|
||||
"download.meta.description": "OpenCode'u macOS, Windows ve Linux için indirin",
|
||||
"download.hero.title": "OpenCode'u İndir",
|
||||
"download.hero.title": "OpenCode Desktop'u İndir",
|
||||
"download.hero.subtitle": "macOS, Windows ve Linux için Beta olarak sunuluyor",
|
||||
"download.hero.button": "{{os}} için indir",
|
||||
"download.section.terminal": "OpenCode Terminal",
|
||||
"download.section.desktop": "OpenCode Desktop (Beta)",
|
||||
"download.section.desktop": "OpenCode Desktop",
|
||||
"download.section.extensions": "OpenCode Eklentileri",
|
||||
"download.section.integrations": "OpenCode Entegrasyonları",
|
||||
"download.action.download": "İndir",
|
||||
|
||||
@@ -102,10 +102,14 @@ 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": "Безкоштовні моделі включено або підключіть будь-яку модель від будь-якого провайдера,",
|
||||
@@ -670,11 +674,11 @@ export const dict = {
|
||||
|
||||
"download.title": "OpenCode | Завантажити",
|
||||
"download.meta.description": "Завантажте OpenCode для macOS, Windows та Linux",
|
||||
"download.hero.title": "Завантажити OpenCode",
|
||||
"download.hero.title": "Завантажити OpenCode Desktop",
|
||||
"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": "Завантажити",
|
||||
|
||||
@@ -103,10 +103,14 @@ export const dict = {
|
||||
"temp.logoDarkAlt": "opencode logo 暗色",
|
||||
|
||||
"home.banner.badge": "新",
|
||||
"home.banner.text": "桌面应用 Beta 版现已推出",
|
||||
"home.banner.platforms": "支持 macOS, Windows, 和 Linux",
|
||||
"home.banner.text": "桌面版标签页现已推出。",
|
||||
"home.banner.platforms": "适用于 macOS、Windows 和 Linux",
|
||||
"home.banner.downloadNow": "立即下载",
|
||||
"home.banner.downloadBetaNow": "立即下载桌面 Beta 版",
|
||||
"home.promo.title": "桌面版标签页现已推出",
|
||||
"home.promo.body": "使用标签页整理工作和活跃会话。",
|
||||
"home.promo.cta": "下载最新版本即可开始使用。",
|
||||
"home.promo.close": "关闭桌面应用公告",
|
||||
|
||||
"home.hero.title": "开源 AI 编程代理",
|
||||
"home.hero.subtitle.a": "内置免费模型,或连接任意提供商的任意模型,",
|
||||
@@ -682,11 +686,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 的 Beta 版",
|
||||
"download.hero.button": "下载 {{os}} 版",
|
||||
"download.section.terminal": "OpenCode 终端",
|
||||
"download.section.desktop": "OpenCode 桌面版 (Beta)",
|
||||
"download.section.desktop": "OpenCode 桌面版",
|
||||
"download.section.extensions": "OpenCode 扩展",
|
||||
"download.section.integrations": "OpenCode 集成",
|
||||
"download.action.download": "下载",
|
||||
|
||||
@@ -103,10 +103,14 @@ export const dict = {
|
||||
"temp.logoDarkAlt": "opencode 深色標誌",
|
||||
|
||||
"home.banner.badge": "新",
|
||||
"home.banner.text": "桌面應用已推出 Beta",
|
||||
"home.banner.platforms": "支援 macOS、Windows 與 Linux",
|
||||
"home.banner.text": "桌面版分頁功能全新推出。",
|
||||
"home.banner.platforms": "適用於 macOS、Windows 與 Linux",
|
||||
"home.banner.downloadNow": "立即下載",
|
||||
"home.banner.downloadBetaNow": "立即下載桌面 Beta 版",
|
||||
"home.promo.title": "桌面版分頁功能全新推出",
|
||||
"home.promo.body": "使用分頁整理工作和作用中的工作階段。",
|
||||
"home.promo.cta": "下載最新版本即可開始使用。",
|
||||
"home.promo.close": "關閉桌面應用程式公告",
|
||||
|
||||
"home.hero.title": "開源 AI 編碼代理",
|
||||
"home.hero.subtitle.a": "內建免費模型,或連接任意供應商的任意模型,",
|
||||
@@ -682,11 +686,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 的 Beta 版現已提供",
|
||||
"download.hero.button": "下載 {{os}} 版",
|
||||
"download.section.terminal": "OpenCode 終端",
|
||||
"download.section.desktop": "OpenCode 桌面版(Beta)",
|
||||
"download.section.desktop": "OpenCode 桌面版",
|
||||
"download.section.extensions": "OpenCode 擴充功能",
|
||||
"download.section.integrations": "OpenCode 整合",
|
||||
"download.action.download": "下載",
|
||||
|
||||
@@ -284,7 +284,7 @@
|
||||
}
|
||||
|
||||
[data-component="content"] {
|
||||
padding: 6rem 5rem;
|
||||
padding: 3rem 5rem 6rem;
|
||||
|
||||
@media (max-width: 60rem) {
|
||||
padding: 4rem 1.5rem;
|
||||
@@ -316,67 +316,43 @@
|
||||
|
||||
/* Download Hero Section */
|
||||
[data-component="download-hero"] {
|
||||
/* display: grid; */
|
||||
display: none;
|
||||
grid-template-columns: 260px 1fr;
|
||||
display: grid;
|
||||
grid-template-columns: 255px 1fr;
|
||||
gap: 4rem;
|
||||
padding-bottom: 2rem;
|
||||
padding: 4px;
|
||||
border: 1px solid var(--color-border-weak);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 4rem;
|
||||
|
||||
@media (max-width: 50rem) {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1.5rem;
|
||||
padding-bottom: 2rem;
|
||||
gap: 0;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
[data-component="hero-icon"] {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
|
||||
@media (max-width: 40rem) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-slot="icon-placeholder"] {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
background: var(--color-background-weak);
|
||||
border: 1px solid var(--color-border-weak);
|
||||
border-radius: 24px;
|
||||
[data-component="hero-video"] {
|
||||
video {
|
||||
display: block;
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
|
||||
@media (max-width: 50rem) {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 24px;
|
||||
box-shadow:
|
||||
0 1.467px 2.847px 0 rgba(0, 0, 0, 0.42),
|
||||
0 0.779px 1.512px 0 rgba(0, 0, 0, 0.34),
|
||||
0 0.324px 0.629px 0 rgba(0, 0, 0, 0.24);
|
||||
|
||||
@media (max-width: 50rem) {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 50rem) {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="hero-text"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding-right: 3rem;
|
||||
|
||||
@media (max-width: 50rem) {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
|
||||
@@ -11,7 +11,7 @@ import { LocaleLinks } from "~/component/locale-links"
|
||||
import { config } from "~/config"
|
||||
import { useI18n } from "~/context/i18n"
|
||||
import { useLanguage } from "~/context/language"
|
||||
import desktopAppIcon from "../../asset/lander/opencode-desktop-icon.png"
|
||||
import desktopTabsVideo from "../../asset/lander/desktop-tabs-landscape.mp4"
|
||||
import type { DownloadPlatform } from "./types"
|
||||
|
||||
type OS = "macOS" | "Windows" | "Linux" | null
|
||||
@@ -93,12 +93,14 @@ export default function Download() {
|
||||
|
||||
<div data-component="content">
|
||||
<section data-component="download-hero">
|
||||
<div data-component="hero-icon">
|
||||
<img src={desktopAppIcon} alt="" />
|
||||
<div data-component="hero-video">
|
||||
<video src={desktopTabsVideo} autoplay playsinline loop muted preload="metadata" aria-hidden="true" />
|
||||
</div>
|
||||
<div data-component="hero-text">
|
||||
<h1>{i18n.t("download.hero.title")}</h1>
|
||||
<p>{i18n.t("download.hero.subtitle")}</p>
|
||||
<p>
|
||||
{i18n.t("home.promo.body")} {i18n.t("home.promo.cta")}
|
||||
</p>
|
||||
<Show when={detectedOS()}>
|
||||
<a
|
||||
href={language.route(getDownloadHref(getDownloadPlatform(detectedOS())))}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.17.20",
|
||||
"version": "1.18.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.17.20",
|
||||
"version": "1.18.2",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.17.20",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-support",
|
||||
"version": "1.17.20",
|
||||
"version": "1.18.2",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.17.20",
|
||||
"version": "1.18.2",
|
||||
"name": "@opencode-ai/core",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"id": "5f0a1db8-d4bf-42c3-becb-96b46fe66bed",
|
||||
"id": "a4ba73b4-21bc-41ab-a415-94e2ca38d798",
|
||||
"prevIds": [
|
||||
"666138ef-82cb-4a9a-a765-e6669a436ff3"
|
||||
"5f0a1db8-d4bf-42c3-becb-96b46fe66bed"
|
||||
],
|
||||
"ddl": [
|
||||
{
|
||||
@@ -38,6 +38,10 @@
|
||||
"name": "event",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "kv",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "permission",
|
||||
"entityType": "tables"
|
||||
@@ -556,6 +560,46 @@
|
||||
"entityType": "columns",
|
||||
"table": "event"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "key",
|
||||
"entityType": "columns",
|
||||
"table": "kv"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "value",
|
||||
"entityType": "columns",
|
||||
"table": "kv"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "kv"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "kv"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
@@ -1844,6 +1888,15 @@
|
||||
"table": "event",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"key"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "kv_pk",
|
||||
"table": "kv",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"id"
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
const token = "dummy-wellknown-token"
|
||||
const port = Number(process.env.PORT ?? 8787)
|
||||
const config = {
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
share: "manual",
|
||||
model: "example-primary/example-chat",
|
||||
enabled_providers: ["example-primary", "example-secondary"],
|
||||
disabled_providers: ["opencode", "anthropic", "openai", "google", "xai", "amazon-bedrock", "azure"],
|
||||
provider: {
|
||||
"example-primary": {
|
||||
name: "Example Primary",
|
||||
npm: "@ai-sdk/openai-compatible",
|
||||
whitelist: ["example-chat", "example-code"],
|
||||
options: {
|
||||
baseURL: "https://models.example.com/v1",
|
||||
apiKey: "{env:TOKEN}",
|
||||
},
|
||||
models: {
|
||||
"example-chat": {
|
||||
name: "Example Chat",
|
||||
reasoning: true,
|
||||
tool_call: true,
|
||||
attachment: true,
|
||||
modalities: {
|
||||
input: ["text", "image"],
|
||||
output: ["text"],
|
||||
},
|
||||
limit: {
|
||||
context: 128000,
|
||||
output: 16000,
|
||||
},
|
||||
},
|
||||
"example-code": {
|
||||
name: "Example Code",
|
||||
reasoning: true,
|
||||
tool_call: true,
|
||||
limit: {
|
||||
context: 200000,
|
||||
output: 32000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"example-secondary": {
|
||||
name: "Example Secondary",
|
||||
npm: "@ai-sdk/openai-compatible",
|
||||
whitelist: ["example-fast"],
|
||||
options: {
|
||||
baseURL: "https://inference.example.org/v1",
|
||||
apiKey: "{env:TOKEN}",
|
||||
},
|
||||
models: {
|
||||
"example-fast": {
|
||||
name: "Example Fast",
|
||||
tool_call: true,
|
||||
limit: {
|
||||
context: 64000,
|
||||
output: 8000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mcp: {
|
||||
"example-tools": {
|
||||
type: "remote",
|
||||
url: "https://tools.example.net/mcp",
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
permission: {
|
||||
bash: "ask",
|
||||
edit: "ask",
|
||||
webfetch: "ask",
|
||||
read: {
|
||||
"*": "allow",
|
||||
"*.env": "deny",
|
||||
"*.env.*": "deny",
|
||||
"*.env.example": "allow",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const server = Bun.serve({
|
||||
hostname: "127.0.0.1",
|
||||
port,
|
||||
async fetch(request) {
|
||||
const url = new URL(request.url)
|
||||
if (url.pathname === "/.well-known/opencode") {
|
||||
return Response.json({
|
||||
auth: {
|
||||
command: ["bun", "-e", `await Bun.sleep(5000); process.stdout.write(${JSON.stringify(token)})`],
|
||||
env: "TOKEN",
|
||||
},
|
||||
remote_config: {
|
||||
url: `${url.origin}/config/opencode.json`,
|
||||
headers: { authorization: "Bearer {env:TOKEN}" },
|
||||
},
|
||||
})
|
||||
}
|
||||
if (url.pathname === "/config/opencode.json") {
|
||||
if (request.headers.get("authorization") !== `Bearer ${token}`) {
|
||||
return new Response("Unauthorized", { status: 401 })
|
||||
}
|
||||
return Response.json(config)
|
||||
}
|
||||
return new Response("Not found", { status: 404 })
|
||||
},
|
||||
})
|
||||
|
||||
console.log(`Well-known fixture listening at ${server.url.origin}`)
|
||||
console.log(`Test with: bun run dev auth connect ${server.url.origin}`)
|
||||
+114
-18
@@ -2,10 +2,13 @@ export * as Config from "./config"
|
||||
|
||||
import { makeLocationNode } from "./effect/app-node"
|
||||
import path from "path"
|
||||
import { isDeepStrictEqual } from "node:util"
|
||||
import { type ParseError, parse } from "jsonc-parser"
|
||||
import { Context, Effect, Fiber, Layer, Option, PubSub, Schema, Stream } from "effect"
|
||||
import { Context, Effect, Fiber, Layer, Option, PubSub, Schema, Semaphore, Stream } from "effect"
|
||||
import { Permission } from "@opencode-ai/schema/permission"
|
||||
import { Config as ConfigSchema } from "@opencode-ai/schema/config"
|
||||
import { Integration } from "@opencode-ai/schema/integration"
|
||||
import { Credential } from "./credential"
|
||||
import { EventV2 } from "./event"
|
||||
import { Watcher } from "./filesystem/watcher"
|
||||
import { FSUtil } from "./fs-util"
|
||||
@@ -16,6 +19,7 @@ import { ConfigAgent } from "./config/agent"
|
||||
import { ConfigAttachments } from "./config/attachments"
|
||||
import { ConfigCompaction } from "./config/compaction"
|
||||
import { ConfigCommand } from "./config/command"
|
||||
import { ConfigExperimental } from "./config/experimental"
|
||||
import { ConfigFormatter } from "./config/formatter"
|
||||
import { ConfigLSP } from "./config/lsp"
|
||||
import { ConfigMCP } from "./config/mcp"
|
||||
@@ -28,6 +32,7 @@ import { ConfigVariable } from "./config/variable"
|
||||
import { ConfigWatcher } from "./config/watcher"
|
||||
import { ConfigV1 } from "./v1/config/config"
|
||||
import { ConfigMigrateV1 } from "./v1/config/migrate"
|
||||
import { WellKnown } from "./wellknown"
|
||||
|
||||
export class Info extends Schema.Class<Info>("Config.Info")({
|
||||
$schema: Schema.optional(Schema.String).annotate({
|
||||
@@ -106,6 +111,7 @@ export class Info extends Schema.Class<Info>("Config.Info")({
|
||||
description: "Ordered plugin enablement directives and external package declarations",
|
||||
}),
|
||||
providers: Schema.Record(Schema.String, ConfigProvider.Info).pipe(Schema.optional),
|
||||
experimental: ConfigExperimental.Info.pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
export class Document extends Schema.Class<Document>("Config.Document")({
|
||||
@@ -157,29 +163,68 @@ const layer = Layer.effect(
|
||||
const location = yield* Location.Service
|
||||
const watcher = yield* Watcher.Service
|
||||
const events = yield* EventV2.Service
|
||||
const credentials = yield* Credential.Service
|
||||
const wellknown = yield* WellKnown.Service
|
||||
const names = ["opencode.json", "opencode.jsonc"]
|
||||
const reloadLock = Semaphore.makeUnsafe(1)
|
||||
const decodeOptions = { errors: "all", onExcessProperty: "ignore", propertyOrder: "original" } as const
|
||||
const decodeInfo = Schema.decodeUnknownOption(Info, decodeOptions)
|
||||
const decodeV1Info = Schema.decodeUnknownOption(ConfigV1.Info, decodeOptions)
|
||||
|
||||
const parseInfo = (text: string) => {
|
||||
const errors: ParseError[] = []
|
||||
const input: unknown = parse(text, errors, { allowTrailingComma: true })
|
||||
if (errors.length) return
|
||||
return Option.getOrUndefined(
|
||||
ConfigMigrateV1.isV1(input)
|
||||
? decodeV1Info(input).pipe(Option.map(ConfigMigrateV1.migrate), Option.flatMap(decodeInfo))
|
||||
: decodeInfo(input),
|
||||
)
|
||||
}
|
||||
|
||||
const loadFile = Effect.fnUntraced(function* (filepath: string) {
|
||||
const text = yield* fs.readFileStringSafe(filepath)
|
||||
if (!text) return
|
||||
const substituted = yield* ConfigVariable.substitute({ type: "path", path: filepath, text })
|
||||
|
||||
const errors: ParseError[] = []
|
||||
const input: unknown = parse(substituted, errors, { allowTrailingComma: true })
|
||||
if (errors.length) return
|
||||
|
||||
const info = Option.getOrUndefined(
|
||||
ConfigMigrateV1.isV1(input)
|
||||
? decodeV1Info(input).pipe(Option.map(ConfigMigrateV1.migrate), Option.flatMap(decodeInfo))
|
||||
: decodeInfo(input),
|
||||
)
|
||||
const info = parseInfo(substituted)
|
||||
if (!info) return
|
||||
return new Document({ type: "document", path: filepath, info })
|
||||
})
|
||||
|
||||
const loadWellknown = Effect.fn("Config.loadWellknown")(function* () {
|
||||
const entries = yield* wellknown
|
||||
.entries()
|
||||
.pipe(
|
||||
Effect.catch((error) =>
|
||||
Effect.logWarning("failed to discover wellknown config", { error }).pipe(Effect.as([] as const)),
|
||||
),
|
||||
)
|
||||
return yield* Effect.forEach(entries, (entry) =>
|
||||
Effect.gen(function* () {
|
||||
const auth = entry.manifest.auth
|
||||
if (!auth) return []
|
||||
const credential = (yield* credentials.list(entry.integrationID)).findLast(
|
||||
(credential) => credential.value.type === "key",
|
||||
)
|
||||
if (!credential || credential.value.type !== "key") return []
|
||||
const variables = { [auth.env]: credential.value.key }
|
||||
const configs = yield* wellknown.resolve(entry, variables).pipe(Effect.orDie)
|
||||
return yield* Effect.forEach(configs, (config) =>
|
||||
ConfigVariable.substitute({
|
||||
type: "virtual",
|
||||
source: entry.origin,
|
||||
dir: entry.origin,
|
||||
text: JSON.stringify(config),
|
||||
env: variables,
|
||||
}).pipe(
|
||||
Effect.map(parseInfo),
|
||||
Effect.map((info) => (info ? new Document({ type: "document", info }) : undefined)),
|
||||
),
|
||||
).pipe(Effect.map((documents) => documents.filter((document) => document !== undefined)))
|
||||
}),
|
||||
).pipe(Effect.map((documents) => documents.flat()))
|
||||
})
|
||||
|
||||
const loadDirectory = Effect.fnUntraced(function* (directory: AbsolutePath) {
|
||||
return [
|
||||
...(yield* Effect.forEach(names, (file) => loadFile(path.join(directory, file))).pipe(
|
||||
@@ -201,7 +246,7 @@ const layer = Layer.effect(
|
||||
targets: [".opencode", ".claude", ".agents", ...names.toReversed()],
|
||||
start: location.directory,
|
||||
})
|
||||
.pipe(Effect.orDie)
|
||||
.pipe(Effect.orDie)
|
||||
|
||||
// We load certain files from a few other folders in the ecosystem
|
||||
const claude = [
|
||||
@@ -244,7 +289,14 @@ const layer = Layer.effect(
|
||||
)
|
||||
|
||||
const supplementary = yield* Effect.forEach(directories, loadDirectory).pipe(Effect.orDie)
|
||||
return [...claude, ...agents, ...(supplementary[0] ?? []), ...direct, ...supplementary.slice(1).flat()]
|
||||
return [
|
||||
...claude,
|
||||
...agents,
|
||||
...(supplementary[0] ?? []),
|
||||
...direct,
|
||||
...supplementary.slice(1).flat(),
|
||||
...(yield* loadWellknown().pipe(Effect.orDie)),
|
||||
]
|
||||
})
|
||||
|
||||
const initial = yield* discover()
|
||||
@@ -276,16 +328,60 @@ const layer = Layer.effect(
|
||||
}
|
||||
})
|
||||
|
||||
yield* Stream.fromPubSub(updates).pipe(
|
||||
Stream.debounce("100 millis"),
|
||||
Stream.runForEach((update) =>
|
||||
const reload = Effect.fn("Config.reload")(() =>
|
||||
reloadLock.withPermit(
|
||||
Effect.gen(function* () {
|
||||
const next = yield* discover()
|
||||
if (isDeepStrictEqual(configs, next)) return
|
||||
configs = next
|
||||
yield* reconcile(next)
|
||||
yield* events.publish(ConfigSchema.Event.Updated, {})
|
||||
}).pipe(Effect.catchCause((cause) => Effect.logError("failed to reload config", { path: update.path, cause }))),
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
yield* Stream.fromPubSub(updates).pipe(
|
||||
Stream.debounce("100 millis"),
|
||||
Stream.runForEach((update) =>
|
||||
reload().pipe(
|
||||
Effect.catchCause((cause) => Effect.logError("failed to reload config", { path: update.path, cause })),
|
||||
),
|
||||
),
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
yield* events.subscribe(Integration.Event.ConnectionUpdated).pipe(
|
||||
Stream.filterEffect((event) =>
|
||||
wellknown.entries().pipe(
|
||||
Effect.map((entries) => entries.some((entry) => entry.integrationID === event.data.integrationID)),
|
||||
Effect.catch(() => Effect.succeed(false)),
|
||||
),
|
||||
),
|
||||
Stream.runForEach(() =>
|
||||
reload().pipe(Effect.catchCause((cause) => Effect.logError("failed to reload wellknown config", { cause }))),
|
||||
),
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
yield* events.subscribe(WellKnown.Event.Updated).pipe(
|
||||
Stream.runForEach(() =>
|
||||
reload().pipe(Effect.catchCause((cause) => Effect.logError("failed to reload wellknown sources", { cause }))),
|
||||
),
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
yield* Effect.sleep("10 minutes").pipe(
|
||||
Effect.andThen(
|
||||
Effect.suspend(() => {
|
||||
if (!wellknown.snapshot().length) return Effect.void
|
||||
return Effect.gen(function* () {
|
||||
const changed = yield* wellknown.refresh().pipe(
|
||||
Effect.catch((error) =>
|
||||
Effect.logWarning("failed to refresh wellknown manifests", { error }).pipe(Effect.as(false)),
|
||||
),
|
||||
)
|
||||
if (!changed) yield* reload()
|
||||
}).pipe(Effect.catchCause((cause) => Effect.logWarning("failed to refresh wellknown config", { cause })))
|
||||
}),
|
||||
),
|
||||
Effect.forever,
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
yield* reconcile(initial)
|
||||
@@ -301,5 +397,5 @@ const layer = Layer.effect(
|
||||
export const node = makeLocationNode({
|
||||
service: Service,
|
||||
layer,
|
||||
deps: [Watcher.node, EventV2.node, FSUtil.node, Global.node, Location.node],
|
||||
deps: [Watcher.node, EventV2.node, FSUtil.node, Global.node, Location.node, Credential.node, WellKnown.node],
|
||||
})
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
export * as ConfigExperimental from "./experimental"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { NonNegativeInt } from "../schema"
|
||||
import { ConfigPolicy } from "./policy"
|
||||
|
||||
export class Info extends Schema.Class<Info>("ConfigExperimental.Info")({
|
||||
subagent_depth: NonNegativeInt.pipe(Schema.optional).annotate({
|
||||
description: "Maximum subagent nesting depth. Defaults to 1.",
|
||||
}),
|
||||
policies: ConfigPolicy.Info.pipe(Schema.Array, Schema.optional).annotate({
|
||||
description: "Ordered policies controlling access to configured resources",
|
||||
}),
|
||||
}) {}
|
||||
@@ -0,0 +1,35 @@
|
||||
export * as ConfigPolicyPlugin from "./policy"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
import { Effect, Stream } from "effect"
|
||||
import { Config } from "../../config"
|
||||
import { Wildcard } from "../../util/wildcard"
|
||||
|
||||
export const Plugin = define({
|
||||
id: "opencode.config.policy",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
const config = yield* Config.Service
|
||||
const loaded = { entries: yield* config.entries() }
|
||||
yield* ctx.catalog.transform((catalog) => {
|
||||
// User-global policy takes priority over policy authored by a repository.
|
||||
const policies = loaded.entries
|
||||
.filter((entry): entry is Config.Document => entry.type === "document")
|
||||
.toReversed()
|
||||
.flatMap((entry) => entry.info.experimental?.policies ?? [])
|
||||
for (const record of catalog.provider.list()) {
|
||||
const policy = policies.findLast((policy) => Wildcard.match(record.provider.id, policy.resource))
|
||||
if (policy?.effect === "deny") catalog.provider.remove(record.provider.id)
|
||||
}
|
||||
})
|
||||
yield* ctx.event.subscribe().pipe(
|
||||
Stream.filter((event) => event.type === "config.updated"),
|
||||
Stream.runForEach(() =>
|
||||
config.entries().pipe(
|
||||
Effect.tap((entries) => Effect.sync(() => (loaded.entries = entries))),
|
||||
Effect.andThen(ctx.catalog.reload()),
|
||||
),
|
||||
),
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
}),
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
export * as ConfigPolicy from "./policy"
|
||||
|
||||
import { Schema } from "effect"
|
||||
|
||||
export const Effect = Schema.Literals(["allow", "deny"])
|
||||
export type Effect = typeof Effect.Type
|
||||
|
||||
export const Info = Schema.Struct({
|
||||
action: Schema.Literal("provider.use"),
|
||||
resource: Schema.String,
|
||||
effect: Effect,
|
||||
})
|
||||
export type Info = typeof Info.Type
|
||||
+1
@@ -54,5 +54,6 @@ export const migrations = (
|
||||
import("./migration/20260709163752_time_suspended"),
|
||||
import("./migration/20260709190621_session_pending_table"),
|
||||
import("./migration/20260710025429_instruction_sync"),
|
||||
import("./migration/20260716020354_kv"),
|
||||
])
|
||||
).map((module) => module.default) satisfies DatabaseMigration.Migration[]
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260716020354_kv",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`kv\` (
|
||||
\`key\` text PRIMARY KEY,
|
||||
\`value\` text NOT NULL,
|
||||
\`time_created\` integer NOT NULL,
|
||||
\`time_updated\` integer NOT NULL
|
||||
);
|
||||
`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
@@ -87,6 +87,14 @@ export default {
|
||||
CONSTRAINT \`fk_event_aggregate_id_event_sequence_aggregate_id_fk\` FOREIGN KEY (\`aggregate_id\`) REFERENCES \`event_sequence\`(\`aggregate_id\`) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`kv\` (
|
||||
\`key\` text PRIMARY KEY,
|
||||
\`value\` text NOT NULL,
|
||||
\`time_created\` integer NOT NULL,
|
||||
\`time_updated\` integer NOT NULL
|
||||
);
|
||||
`)
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`permission\` (
|
||||
\`id\` text PRIMARY KEY,
|
||||
|
||||
@@ -116,7 +116,14 @@ export namespace FSUtil {
|
||||
})
|
||||
|
||||
const ensureDir = Effect.fn("FileSystem.ensureDir")(function* (path: string) {
|
||||
yield* fs.makeDirectory(path, { recursive: true })
|
||||
yield* fs.makeDirectory(path, { recursive: true }).pipe(
|
||||
// Bun on Windows can throw EEXIST here despite recursive mode.
|
||||
// https://github.com/oven-sh/bun/issues/21901
|
||||
Effect.catchIf(
|
||||
(error) => error.reason._tag === "AlreadyExists",
|
||||
(error) => isDir(path).pipe(Effect.flatMap((exists) => (exists ? Effect.void : Effect.fail(error)))),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
const writeWithDirs = Effect.fn("FileSystem.writeWithDirs")(function* (
|
||||
|
||||
@@ -8,10 +8,12 @@ import {
|
||||
Duration,
|
||||
Effect,
|
||||
Exit,
|
||||
Fiber,
|
||||
Layer,
|
||||
Schedule,
|
||||
Schema,
|
||||
Scope,
|
||||
Stream,
|
||||
SynchronizedRef,
|
||||
Types,
|
||||
} from "effect"
|
||||
@@ -20,6 +22,8 @@ import { Credential } from "./credential"
|
||||
import { State } from "./state"
|
||||
import { EventV2 } from "./event"
|
||||
import { IntegrationConnection } from "./integration/connection"
|
||||
import { AppProcess } from "./process"
|
||||
import { ChildProcess } from "effect/unstable/process"
|
||||
|
||||
export const ID = Integration.ID
|
||||
export type ID = Integration.ID
|
||||
@@ -45,6 +49,9 @@ export type Prompt = Integration.Prompt
|
||||
export const OAuthMethod = Integration.OAuthMethod
|
||||
export type OAuthMethod = Integration.OAuthMethod
|
||||
|
||||
export const CommandMethod = Integration.CommandMethod
|
||||
export type CommandMethod = Integration.CommandMethod
|
||||
|
||||
export const KeyMethod = Integration.KeyMethod
|
||||
export type KeyMethod = Integration.KeyMethod
|
||||
|
||||
@@ -87,12 +94,17 @@ export interface KeyImplementation {
|
||||
readonly method: KeyMethod
|
||||
}
|
||||
|
||||
export interface CommandImplementation {
|
||||
readonly integrationID: ID
|
||||
readonly method: CommandMethod
|
||||
}
|
||||
|
||||
export interface EnvImplementation {
|
||||
readonly integrationID: ID
|
||||
readonly method: EnvMethod
|
||||
}
|
||||
|
||||
export type Implementation = OAuthImplementation | KeyImplementation | EnvImplementation
|
||||
export type Implementation = OAuthImplementation | CommandImplementation | KeyImplementation | EnvImplementation
|
||||
|
||||
export const Attempt = Integration.Attempt
|
||||
export type Attempt = Integration.Attempt
|
||||
@@ -100,6 +112,12 @@ export type Attempt = Integration.Attempt
|
||||
export const AttemptStatus = Integration.AttemptStatus
|
||||
export type AttemptStatus = typeof AttemptStatus.Type
|
||||
|
||||
export const CommandAttempt = Integration.CommandAttempt
|
||||
export type CommandAttempt = Integration.CommandAttempt
|
||||
|
||||
export const CommandAttemptStatus = Integration.CommandAttemptStatus
|
||||
export type CommandAttemptStatus = Integration.CommandAttemptStatus
|
||||
|
||||
export class CodeRequiredError extends Schema.TaggedErrorClass<CodeRequiredError>()("Integration.CodeRequired", {
|
||||
attemptID: AttemptID,
|
||||
}) {}
|
||||
@@ -159,17 +177,6 @@ export interface Interface extends State.Transformable<Draft> {
|
||||
/** User-facing label for the stored credential. */
|
||||
readonly label?: string
|
||||
}) => Effect.Effect<void, AuthorizationError>
|
||||
/** Starts a stateful OAuth attempt. */
|
||||
readonly oauth: (input: {
|
||||
/** Integration being authenticated. */
|
||||
readonly integrationID: ID
|
||||
/** OAuth method selected by the caller. */
|
||||
readonly methodID: MethodID
|
||||
/** Answers to the method's optional prompts. */
|
||||
readonly inputs: Inputs
|
||||
/** User-facing label for the credential created on completion. */
|
||||
readonly label?: string
|
||||
}) => Effect.Effect<Attempt, AuthorizationError>
|
||||
/** Updates a stored credential exposed as a connection. */
|
||||
readonly update: (
|
||||
credentialID: Credential.ID,
|
||||
@@ -178,18 +185,39 @@ export interface Interface extends State.Transformable<Draft> {
|
||||
/** Removes a stored credential connection. */
|
||||
readonly remove: (credentialID: Credential.ID) => Effect.Effect<void>
|
||||
}
|
||||
readonly attempt: {
|
||||
readonly oauth: {
|
||||
/** Starts a stateful OAuth attempt. */
|
||||
readonly connect: (input: {
|
||||
readonly integrationID: ID
|
||||
readonly methodID: MethodID
|
||||
readonly inputs: Inputs
|
||||
readonly label?: string
|
||||
}) => Effect.Effect<Attempt, AuthorizationError>
|
||||
/** Returns the current state of an OAuth attempt. */
|
||||
readonly status: (attemptID: AttemptID) => Effect.Effect<AttemptStatus>
|
||||
readonly status: (input: {
|
||||
readonly integrationID: ID
|
||||
readonly attemptID: AttemptID
|
||||
}) => Effect.Effect<AttemptStatus>
|
||||
/** Completes the attempt and stores its credential. */
|
||||
readonly complete: (input: {
|
||||
/** Opaque handle returned by `oauth`. */
|
||||
readonly integrationID: ID
|
||||
readonly attemptID: AttemptID
|
||||
/** Authorization code required by attempts in code mode. */
|
||||
readonly code?: string
|
||||
}) => Effect.Effect<void, CodeRequiredError | AuthorizationError>
|
||||
/** Cancels an attempt and releases its resources. */
|
||||
readonly cancel: (attemptID: AttemptID) => Effect.Effect<void>
|
||||
readonly cancel: (input: { readonly integrationID: ID; readonly attemptID: AttemptID }) => Effect.Effect<void>
|
||||
}
|
||||
readonly command: {
|
||||
readonly connect: (input: {
|
||||
readonly integrationID: ID
|
||||
readonly methodID: MethodID
|
||||
readonly label?: string
|
||||
}) => Effect.Effect<CommandAttempt, AuthorizationError>
|
||||
readonly status: (input: {
|
||||
readonly integrationID: ID
|
||||
readonly attemptID: AttemptID
|
||||
}) => Effect.Effect<CommandAttemptStatus>
|
||||
readonly cancel: (input: { readonly integrationID: ID; readonly attemptID: AttemptID }) => Effect.Effect<void>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,19 +241,40 @@ type PendingAttempt = {
|
||||
}
|
||||
type TerminalAttempt = {
|
||||
status: "complete" | "failed" | "expired"
|
||||
integrationID: ID
|
||||
message?: string
|
||||
removeAt: number
|
||||
time: AttemptTime
|
||||
}
|
||||
type AttemptEntry = PendingAttempt | TerminalAttempt
|
||||
|
||||
type PendingCommandAttempt = {
|
||||
status: "pending"
|
||||
integrationID: ID
|
||||
label?: string
|
||||
message?: string
|
||||
persisting: boolean
|
||||
scope: Scope.Closeable
|
||||
time: AttemptTime
|
||||
}
|
||||
type TerminalCommandAttempt = {
|
||||
status: "complete" | "failed" | "expired"
|
||||
integrationID: ID
|
||||
message?: string
|
||||
removeAt: number
|
||||
time: AttemptTime
|
||||
}
|
||||
type CommandAttemptEntry = PendingCommandAttempt | TerminalCommandAttempt
|
||||
|
||||
const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const credentials = yield* Credential.Service
|
||||
const events = yield* EventV2.Service
|
||||
const processes = yield* AppProcess.Service
|
||||
const scope = yield* Scope.Scope
|
||||
const attempts = SynchronizedRef.makeUnsafe(new Map<AttemptID, AttemptEntry>())
|
||||
const commandAttempts = SynchronizedRef.makeUnsafe(new Map<AttemptID, CommandAttemptEntry>())
|
||||
const state = State.create<Data, Draft>({
|
||||
name: "integration",
|
||||
initial: () => ({ integrations: new Map<ID, Entry>() }),
|
||||
@@ -259,8 +308,11 @@ const layer = Layer.effect(
|
||||
}
|
||||
const index = current.methods.findIndex((method) => {
|
||||
if (method.type !== implementation.method.type) return false
|
||||
if (method.type !== "oauth" || implementation.method.type !== "oauth") return true
|
||||
return method.id === implementation.method.id
|
||||
if (method.type === "oauth" && implementation.method.type === "oauth")
|
||||
return method.id === implementation.method.id
|
||||
if (method.type === "command" && implementation.method.type === "command")
|
||||
return method.id === implementation.method.id
|
||||
return true
|
||||
})
|
||||
if (index === -1) current.methods.push(implementation.method as Types.DeepMutable<Method>)
|
||||
else current.methods[index] = implementation.method as Types.DeepMutable<Method>
|
||||
@@ -276,8 +328,9 @@ const layer = Layer.effect(
|
||||
if (!current) return
|
||||
const index = current.methods.findIndex((candidate) => {
|
||||
if (candidate.type !== method.type) return false
|
||||
if (candidate.type !== "oauth" || method.type !== "oauth") return true
|
||||
return candidate.id === method.id
|
||||
if (candidate.type === "oauth" && method.type === "oauth") return candidate.id === method.id
|
||||
if (candidate.type === "command" && method.type === "command") return candidate.id === method.id
|
||||
return true
|
||||
})
|
||||
if (index !== -1) current.methods.splice(index, 1)
|
||||
if (method.type === "oauth") current.implementations.delete(method.id)
|
||||
@@ -332,6 +385,7 @@ const layer = Layer.effect(
|
||||
? { ...match, persisting: true }
|
||||
: {
|
||||
status: "failed" as const,
|
||||
integrationID: match.integrationID,
|
||||
message: message(exit.cause),
|
||||
time: match.time,
|
||||
removeAt: now + terminalRetention,
|
||||
@@ -362,13 +416,19 @@ const layer = Layer.effect(
|
||||
)
|
||||
const settledAt = yield* Clock.currentTimeMillis
|
||||
const terminal: TerminalAttempt = Exit.isSuccess(persistence)
|
||||
? { status: "complete", time: attempt.time, removeAt: settledAt + terminalRetention }
|
||||
? {
|
||||
status: "complete",
|
||||
integrationID: attempt.integrationID,
|
||||
time: attempt.time,
|
||||
removeAt: settledAt + terminalRetention,
|
||||
}
|
||||
: {
|
||||
status: "failed",
|
||||
message: message(persistence.cause),
|
||||
time: attempt.time,
|
||||
removeAt: settledAt + terminalRetention,
|
||||
}
|
||||
integrationID: attempt.integrationID,
|
||||
message: message(persistence.cause),
|
||||
time: attempt.time,
|
||||
removeAt: settledAt + terminalRetention,
|
||||
}
|
||||
// Persisting attempts cannot be cancelled, expired, or claimed again.
|
||||
yield* SynchronizedRef.update(attempts, (current) => new Map(current).set(attemptID, terminal))
|
||||
if (Exit.isFailure(persistence)) yield* Effect.failCause(persistence.cause)
|
||||
@@ -379,6 +439,61 @@ const layer = Layer.effect(
|
||||
)
|
||||
})
|
||||
|
||||
const settleCommand = Effect.fnUntraced(function* (attemptID: AttemptID, exit: Exit.Exit<string, unknown>) {
|
||||
return yield* Effect.uninterruptible(
|
||||
Effect.gen(function* () {
|
||||
const now = yield* Clock.currentTimeMillis
|
||||
const attempt = yield* SynchronizedRef.modify(commandAttempts, (current) => {
|
||||
const match = current.get(attemptID)
|
||||
if (!match || match.status !== "pending" || match.persisting) return [undefined, current]
|
||||
const next = Exit.isSuccess(exit)
|
||||
? { ...match, persisting: true }
|
||||
: {
|
||||
status: "failed" as const,
|
||||
integrationID: match.integrationID,
|
||||
message: message(exit.cause),
|
||||
time: match.time,
|
||||
removeAt: now + terminalRetention,
|
||||
}
|
||||
return [match, new Map(current).set(attemptID, next)]
|
||||
})
|
||||
if (!attempt) return
|
||||
if (Exit.isFailure(exit)) {
|
||||
yield* close(attempt.scope)
|
||||
return
|
||||
}
|
||||
|
||||
const persistence = yield* credentials
|
||||
.create({
|
||||
integrationID: attempt.integrationID,
|
||||
label: attempt.label,
|
||||
value: Credential.Key.make({ type: "key", key: exit.value }),
|
||||
})
|
||||
.pipe(Effect.asVoid, Effect.exit)
|
||||
const settledAt = yield* Clock.currentTimeMillis
|
||||
const terminal: TerminalCommandAttempt = Exit.isSuccess(persistence)
|
||||
? {
|
||||
status: "complete",
|
||||
integrationID: attempt.integrationID,
|
||||
time: attempt.time,
|
||||
removeAt: settledAt + terminalRetention,
|
||||
}
|
||||
: {
|
||||
status: "failed",
|
||||
integrationID: attempt.integrationID,
|
||||
message: message(persistence.cause),
|
||||
time: attempt.time,
|
||||
removeAt: settledAt + terminalRetention,
|
||||
}
|
||||
yield* SynchronizedRef.update(commandAttempts, (current) => new Map(current).set(attemptID, terminal))
|
||||
yield* close(attempt.scope)
|
||||
if (Exit.isFailure(persistence)) return
|
||||
yield* events.publish(Event.ConnectionUpdated, { integrationID: attempt.integrationID })
|
||||
yield* events.publish(Event.Updated, {})
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
const scrub = Effect.fnUntraced(function* () {
|
||||
const now = yield* Clock.currentTimeMillis
|
||||
const expired = yield* SynchronizedRef.modify(attempts, (current) => {
|
||||
@@ -387,7 +502,35 @@ const layer = Layer.effect(
|
||||
for (const [id, attempt] of current) {
|
||||
if (attempt.status === "pending" && !attempt.persisting && attempt.time.expires <= now) {
|
||||
scopes.push(attempt.scope)
|
||||
next.set(id, { status: "expired", time: attempt.time, removeAt: now + terminalRetention })
|
||||
next.set(id, {
|
||||
status: "expired",
|
||||
integrationID: attempt.integrationID,
|
||||
time: attempt.time,
|
||||
removeAt: now + terminalRetention,
|
||||
})
|
||||
continue
|
||||
}
|
||||
if (attempt.status !== "pending" && attempt.removeAt <= now) next.delete(id)
|
||||
}
|
||||
return [scopes, next]
|
||||
})
|
||||
yield* Effect.forEach(expired, close, { discard: true })
|
||||
})
|
||||
|
||||
const scrubCommands = Effect.fnUntraced(function* () {
|
||||
const now = yield* Clock.currentTimeMillis
|
||||
const expired = yield* SynchronizedRef.modify(commandAttempts, (current) => {
|
||||
const next = new Map(current)
|
||||
const scopes: Scope.Closeable[] = []
|
||||
for (const [id, attempt] of current) {
|
||||
if (attempt.status === "pending" && !attempt.persisting && attempt.time.expires <= now) {
|
||||
scopes.push(attempt.scope)
|
||||
next.set(id, {
|
||||
status: "expired",
|
||||
integrationID: attempt.integrationID,
|
||||
time: attempt.time,
|
||||
removeAt: now + terminalRetention,
|
||||
})
|
||||
continue
|
||||
}
|
||||
if (attempt.status !== "pending" && attempt.removeAt <= now) next.delete(id)
|
||||
@@ -398,6 +541,120 @@ const layer = Layer.effect(
|
||||
})
|
||||
|
||||
yield* scrub().pipe(Effect.repeat(Schedule.spaced(scrubInterval)), Effect.forkIn(scope))
|
||||
yield* scrubCommands().pipe(Effect.repeat(Schedule.spaced(scrubInterval)), Effect.forkIn(scope))
|
||||
|
||||
const connectOAuth = Effect.fn("Integration.oauth.connect")(function* (input: {
|
||||
readonly integrationID: ID
|
||||
readonly methodID: MethodID
|
||||
readonly inputs: Inputs
|
||||
readonly label?: string
|
||||
}) {
|
||||
const method = state.get().integrations.get(input.integrationID)?.implementations.get(input.methodID)
|
||||
if (!method) {
|
||||
return yield* Effect.die(new Error(`OAuth method not found: ${input.integrationID}/${input.methodID}`))
|
||||
}
|
||||
const attemptScope = yield* Scope.fork(scope)
|
||||
const authorization = yield* authorize(method.authorize(input.inputs)).pipe(
|
||||
Scope.provide(attemptScope),
|
||||
Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(attemptScope, exit) : Effect.void)),
|
||||
)
|
||||
const id = AttemptID.create()
|
||||
const created = yield* Clock.currentTimeMillis
|
||||
const time = { created, expires: created + attemptLifetime }
|
||||
yield* SynchronizedRef.update(attempts, (current) =>
|
||||
new Map(current).set(id, {
|
||||
status: "pending",
|
||||
completing: authorization.mode === "auto",
|
||||
persisting: false,
|
||||
authorization,
|
||||
integrationID: input.integrationID,
|
||||
methodID: input.methodID,
|
||||
label: input.label,
|
||||
scope: attemptScope,
|
||||
time,
|
||||
}),
|
||||
)
|
||||
if (authorization.mode === "auto") {
|
||||
yield* authorization.callback.pipe(
|
||||
Effect.exit,
|
||||
Effect.flatMap((exit) => settle(id, exit)),
|
||||
Effect.forkIn(attemptScope, { startImmediately: true }),
|
||||
)
|
||||
}
|
||||
return new Attempt({
|
||||
attemptID: id,
|
||||
url: authorization.url,
|
||||
instructions: authorization.instructions,
|
||||
mode: authorization.mode,
|
||||
time,
|
||||
})
|
||||
})
|
||||
|
||||
const connectCommand = Effect.fn("Integration.command.connect")(function* (input: {
|
||||
readonly integrationID: ID
|
||||
readonly methodID: MethodID
|
||||
readonly label?: string
|
||||
}) {
|
||||
const method = state
|
||||
.get()
|
||||
.integrations.get(input.integrationID)
|
||||
?.methods.find((method) => method.type === "command" && method.id === input.methodID)
|
||||
if (!method || method.type !== "command" || !method.command[0]) {
|
||||
return yield* Effect.die(new Error(`Command method not found: ${input.integrationID}/${input.methodID}`))
|
||||
}
|
||||
|
||||
const attemptScope = yield* Scope.fork(scope)
|
||||
const attemptID = AttemptID.create()
|
||||
const created = yield* Clock.currentTimeMillis
|
||||
const time = { created, expires: created + attemptLifetime }
|
||||
yield* SynchronizedRef.update(commandAttempts, (current) =>
|
||||
new Map(current).set(attemptID, {
|
||||
status: "pending",
|
||||
integrationID: input.integrationID,
|
||||
label: input.label,
|
||||
persisting: false,
|
||||
scope: attemptScope,
|
||||
time,
|
||||
}),
|
||||
)
|
||||
|
||||
yield* Effect.gen(function* () {
|
||||
const handle = yield* processes.spawn(
|
||||
ChildProcess.make(method.command[0], method.command.slice(1), {
|
||||
extendEnv: true,
|
||||
stdin: "ignore",
|
||||
}),
|
||||
)
|
||||
const stdout = yield* AppProcess.collectStream(handle.stdout, undefined).pipe(Effect.forkScoped)
|
||||
yield* handle.stderr.pipe(
|
||||
Stream.decodeText,
|
||||
Stream.tap((chunk) =>
|
||||
SynchronizedRef.update(commandAttempts, (current) => {
|
||||
const attempt = current.get(attemptID)
|
||||
if (!attempt || attempt.status !== "pending") return current
|
||||
const message = (attempt.message ?? "") + chunk
|
||||
return new Map(current).set(attemptID, { ...attempt, message })
|
||||
}),
|
||||
),
|
||||
Stream.runDrain,
|
||||
)
|
||||
const exitCode = yield* handle.exitCode
|
||||
if (exitCode !== 0) {
|
||||
const attempt = (yield* SynchronizedRef.get(commandAttempts)).get(attemptID)
|
||||
return yield* Effect.fail(new Error(attempt?.message?.trim() || `Authentication command exited ${exitCode}`))
|
||||
}
|
||||
const credential = (yield* Fiber.join(stdout)).buffer.toString("utf8").trim()
|
||||
if (!credential) return yield* Effect.fail(new Error("Authentication command returned no credential"))
|
||||
return credential
|
||||
}).pipe(
|
||||
Scope.provide(attemptScope),
|
||||
Effect.exit,
|
||||
Effect.flatMap((exit) => settleCommand(attemptID, exit)),
|
||||
Effect.forkIn(attemptScope, { startImmediately: true }),
|
||||
)
|
||||
|
||||
return CommandAttempt.make({ attemptID, time })
|
||||
})
|
||||
|
||||
return Service.of({
|
||||
transform: state.transform,
|
||||
@@ -451,47 +708,6 @@ const layer = Layer.effect(
|
||||
yield* events.publish(Event.ConnectionUpdated, { integrationID: input.integrationID })
|
||||
yield* events.publish(Event.Updated, {})
|
||||
}),
|
||||
oauth: Effect.fn("Integration.connection.oauth")(function* (input) {
|
||||
const method = state.get().integrations.get(input.integrationID)?.implementations.get(input.methodID)
|
||||
if (!method) {
|
||||
return yield* Effect.die(new Error(`OAuth method not found: ${input.integrationID}/${input.methodID}`))
|
||||
}
|
||||
const attemptScope = yield* Scope.fork(scope)
|
||||
const authorization = yield* authorize(method.authorize(input.inputs)).pipe(
|
||||
Scope.provide(attemptScope),
|
||||
Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(attemptScope, exit) : Effect.void)),
|
||||
)
|
||||
const id = AttemptID.create()
|
||||
const created = yield* Clock.currentTimeMillis
|
||||
const time = { created, expires: created + attemptLifetime }
|
||||
yield* SynchronizedRef.update(attempts, (current) =>
|
||||
new Map(current).set(id, {
|
||||
status: "pending",
|
||||
completing: authorization.mode === "auto",
|
||||
persisting: false,
|
||||
authorization,
|
||||
integrationID: input.integrationID,
|
||||
methodID: input.methodID,
|
||||
label: input.label,
|
||||
scope: attemptScope,
|
||||
time,
|
||||
}),
|
||||
)
|
||||
if (authorization.mode === "auto") {
|
||||
yield* authorization.callback.pipe(
|
||||
Effect.exit,
|
||||
Effect.flatMap((exit) => settle(id, exit)),
|
||||
Effect.forkIn(attemptScope, { startImmediately: true }),
|
||||
)
|
||||
}
|
||||
return new Attempt({
|
||||
attemptID: id,
|
||||
url: authorization.url,
|
||||
instructions: authorization.instructions,
|
||||
mode: authorization.mode,
|
||||
time,
|
||||
})
|
||||
}),
|
||||
update: Effect.fn("Integration.connection.update")(function* (credentialID, updates) {
|
||||
const credential = yield* credentials.get(credentialID)
|
||||
yield* credentials.update(credentialID, updates)
|
||||
@@ -509,19 +725,22 @@ const layer = Layer.effect(
|
||||
yield* events.publish(Event.Updated, {})
|
||||
}),
|
||||
},
|
||||
attempt: {
|
||||
status: Effect.fn("Integration.attempt.status")(function* (attemptID) {
|
||||
const attempt = (yield* SynchronizedRef.get(attempts)).get(attemptID)
|
||||
if (!attempt) return yield* Effect.die(new Error(`OAuth attempt not found: ${attemptID}`))
|
||||
oauth: {
|
||||
connect: connectOAuth,
|
||||
status: Effect.fn("Integration.oauth.status")(function* (input) {
|
||||
const attempt = (yield* SynchronizedRef.get(attempts)).get(input.attemptID)
|
||||
if (!attempt || attempt.integrationID !== input.integrationID)
|
||||
return yield* Effect.die(new Error(`OAuth attempt not found: ${input.attemptID}`))
|
||||
if (attempt.status === "failed") {
|
||||
return { status: attempt.status, message: attempt.message ?? "Authorization failed", time: attempt.time }
|
||||
}
|
||||
return { status: attempt.status, time: attempt.time }
|
||||
}),
|
||||
complete: Effect.fn("Integration.attempt.complete")(function* (input) {
|
||||
complete: Effect.fn("Integration.oauth.complete")(function* (input) {
|
||||
const attempt = yield* SynchronizedRef.modify(attempts, (current) => {
|
||||
const match = current.get(input.attemptID)
|
||||
if (!match || match.status !== "pending" || match.completing) return [match, current]
|
||||
if (!match || match.integrationID !== input.integrationID) return [undefined, current]
|
||||
if (match.status !== "pending" || match.completing) return [match, current]
|
||||
if (match.authorization.mode === "code" && input.code === undefined) return [match, current]
|
||||
return [match, new Map(current).set(input.attemptID, { ...match, completing: true })]
|
||||
})
|
||||
@@ -540,12 +759,43 @@ const layer = Layer.effect(
|
||||
yield* settle(input.attemptID, exit)
|
||||
if (Exit.isFailure(exit)) return yield* exit
|
||||
}),
|
||||
cancel: Effect.fn("Integration.attempt.cancel")(function* (attemptID) {
|
||||
cancel: Effect.fn("Integration.oauth.cancel")(function* (input) {
|
||||
const attempt = yield* SynchronizedRef.modify(attempts, (current) => {
|
||||
const match = current.get(attemptID)
|
||||
if (!match || match.status !== "pending" || match.persisting) return [undefined, current]
|
||||
const match = current.get(input.attemptID)
|
||||
if (!match || match.integrationID !== input.integrationID || match.status !== "pending" || match.persisting)
|
||||
return [undefined, current]
|
||||
const next = new Map(current)
|
||||
next.delete(attemptID)
|
||||
next.delete(input.attemptID)
|
||||
return [match, next]
|
||||
})
|
||||
if (attempt) yield* Scope.close(attempt.scope, Exit.void)
|
||||
}),
|
||||
},
|
||||
command: {
|
||||
connect: connectCommand,
|
||||
status: Effect.fn("Integration.command.status")(function* (input) {
|
||||
const attempt = (yield* SynchronizedRef.get(commandAttempts)).get(input.attemptID)
|
||||
if (!attempt || attempt.integrationID !== input.integrationID)
|
||||
return yield* Effect.die(new Error(`Command attempt not found: ${input.attemptID}`))
|
||||
if (attempt.status === "pending") {
|
||||
return {
|
||||
status: attempt.status,
|
||||
...(attempt.message ? { message: attempt.message } : {}),
|
||||
time: attempt.time,
|
||||
}
|
||||
}
|
||||
if (attempt.status === "failed") {
|
||||
return { status: attempt.status, message: attempt.message ?? "Authentication failed", time: attempt.time }
|
||||
}
|
||||
return { status: attempt.status, time: attempt.time }
|
||||
}),
|
||||
cancel: Effect.fn("Integration.command.cancel")(function* (input) {
|
||||
const attempt = yield* SynchronizedRef.modify(commandAttempts, (current) => {
|
||||
const match = current.get(input.attemptID)
|
||||
if (!match || match.integrationID !== input.integrationID || match.status !== "pending" || match.persisting)
|
||||
return [undefined, current]
|
||||
const next = new Map(current)
|
||||
next.delete(input.attemptID)
|
||||
return [match, next]
|
||||
})
|
||||
if (attempt) yield* Scope.close(attempt.scope, Exit.void)
|
||||
@@ -555,4 +805,8 @@ const layer = Layer.effect(
|
||||
}),
|
||||
)
|
||||
|
||||
export const node = makeLocationNode({ service: Service, layer, deps: [Credential.node, EventV2.node] })
|
||||
export const node = makeLocationNode({
|
||||
service: Service,
|
||||
layer,
|
||||
deps: [Credential.node, EventV2.node, AppProcess.node],
|
||||
})
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
export * as KV from "./kv"
|
||||
|
||||
import { eq } from "drizzle-orm"
|
||||
import { Context, Effect, Layer, Schema } from "effect"
|
||||
import { Database } from "./database/database"
|
||||
import { makeGlobalNode } from "./effect/app-node"
|
||||
import { KVTable } from "./kv/sql"
|
||||
|
||||
export type Value = Schema.Json
|
||||
|
||||
export interface Interface {
|
||||
readonly get: (key: string) => Effect.Effect<Value | undefined>
|
||||
readonly set: (key: string, value: Value) => Effect.Effect<void>
|
||||
readonly remove: (key: string) => Effect.Effect<void>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/KV") {}
|
||||
|
||||
const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const { db } = yield* Database.Service
|
||||
return Service.of({
|
||||
get: Effect.fn("KV.get")(function* (key) {
|
||||
return (yield* db
|
||||
.select({ value: KVTable.value })
|
||||
.from(KVTable)
|
||||
.where(eq(KVTable.key, key))
|
||||
.get()
|
||||
.pipe(Effect.orDie))?.value
|
||||
}),
|
||||
set: Effect.fn("KV.set")(function* (key, value) {
|
||||
yield* db
|
||||
.insert(KVTable)
|
||||
.values({ key, value })
|
||||
.onConflictDoUpdate({ target: KVTable.key, set: { value, time_updated: Date.now() } })
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
}),
|
||||
remove: Effect.fn("KV.remove")(function* (key) {
|
||||
yield* db.delete(KVTable).where(eq(KVTable.key, key)).run().pipe(Effect.orDie)
|
||||
}),
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
export const node = makeGlobalNode({ service: Service, layer, deps: [Database.node] })
|
||||
@@ -0,0 +1,9 @@
|
||||
import { sqliteTable, text } from "drizzle-orm/sqlite-core"
|
||||
import { Timestamps } from "../database/schema.sql"
|
||||
import type { KV } from "../kv"
|
||||
|
||||
export const KVTable = sqliteTable("kv", {
|
||||
key: text().primaryKey(),
|
||||
value: text({ mode: "json" }).$type<KV.Value>().notNull(),
|
||||
...Timestamps,
|
||||
})
|
||||
@@ -27,18 +27,19 @@ import { Pty } from "./pty"
|
||||
import { QuestionV2 } from "./question"
|
||||
import { Shell } from "./shell"
|
||||
import { Reference } from "./reference"
|
||||
import { ReferenceGuidance } from "./reference/guidance"
|
||||
import { ReferenceInstructions } from "./reference/instructions"
|
||||
import { SessionRunnerLLM } from "./session/runner/llm"
|
||||
import { SessionRunnerModel } from "./session/runner/model"
|
||||
import { SessionCompaction } from "./session/compaction"
|
||||
import { SessionTitle } from "./session/title"
|
||||
import { SkillV2 } from "./skill"
|
||||
import { SkillGuidance } from "./skill/guidance"
|
||||
import { SkillInstructions } from "./skill/instructions"
|
||||
import { Snapshot } from "./snapshot"
|
||||
import { InstructionDiscovery } from "./instruction-discovery"
|
||||
import { InstructionBuiltIns } from "./instructions/builtins"
|
||||
import { InstructionEntry } from "./session/instruction-entry"
|
||||
import { SessionInstructions } from "./session/instructions"
|
||||
import { SessionGenerateNode } from "./session/generate-node"
|
||||
import { McpTool } from "./tool/mcp"
|
||||
import { ReadToolFileSystem } from "./tool/read-filesystem"
|
||||
import { ToolRegistry } from "./tool/registry"
|
||||
@@ -75,12 +76,13 @@ const locationServiceNodes = [
|
||||
ToolRegistry.node,
|
||||
ToolRegistry.toolsNode,
|
||||
Image.node,
|
||||
SkillGuidance.node,
|
||||
ReferenceGuidance.node,
|
||||
SkillInstructions.node,
|
||||
ReferenceInstructions.node,
|
||||
InstructionEntry.node,
|
||||
Form.node,
|
||||
QuestionV2.node,
|
||||
Generate.node,
|
||||
SessionGenerateNode.node,
|
||||
ReadToolFileSystem.node,
|
||||
McpTool.node,
|
||||
SessionInstructions.node,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export * as McpGuidance from "./guidance"
|
||||
export * as McpInstructions from "./instructions"
|
||||
|
||||
import { makeLocationNode } from "../effect/app-node"
|
||||
import { Context, Effect, Layer, Schema } from "effect"
|
||||
@@ -54,7 +54,7 @@ export interface Interface {
|
||||
readonly load: (agent: AgentV2.Selection) => Effect.Effect<Instructions.Instructions>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/McpGuidance") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/McpInstructions") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
@@ -62,7 +62,7 @@ export const layer = Layer.effect(
|
||||
const mcp = yield* MCP.Service
|
||||
|
||||
return Service.of({
|
||||
load: Effect.fn("McpGuidance.load")(function* (selection) {
|
||||
load: Effect.fn("McpInstructions.load")(function* (selection) {
|
||||
const agent = selection.info
|
||||
if (!agent) return Instructions.empty
|
||||
const source = (value: ReadonlyArray<Summary> | Instructions.Removed) =>
|
||||
@@ -1,6 +1,7 @@
|
||||
export * as PluginHooks from "./hooks"
|
||||
|
||||
import type { AISDKHooks } from "@opencode-ai/plugin/v2/effect/aisdk"
|
||||
import type { SessionHooks } from "@opencode-ai/plugin/v2/effect/session"
|
||||
import type { ToolHooks } from "@opencode-ai/plugin/v2/effect/tool"
|
||||
import { Context, Effect, Layer, Scope } from "effect"
|
||||
import { makeLocationNode } from "../effect/app-node"
|
||||
@@ -8,6 +9,7 @@ import { State } from "../state"
|
||||
|
||||
export interface Domains {
|
||||
readonly aisdk: AISDKHooks
|
||||
readonly session: SessionHooks
|
||||
readonly tool: ToolHooks
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
|
||||
return {
|
||||
options: {},
|
||||
agent: {
|
||||
get: (id) => agents.get(AgentV2.ID.make(id)),
|
||||
list: (input) => {
|
||||
const ref = locationRef(input)
|
||||
if (ref && !isCurrentLocation(ref)) return runtime.location.agent.list(ref)
|
||||
@@ -123,6 +124,8 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
|
||||
),
|
||||
},
|
||||
model: {
|
||||
get: (providerID, modelID) =>
|
||||
catalog.model.get(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
|
||||
list: () => response(catalog.model.available()),
|
||||
default: () => response(catalog.model.default()),
|
||||
},
|
||||
@@ -173,21 +176,57 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
|
||||
key: input.key,
|
||||
label: input.label,
|
||||
}),
|
||||
oauth: (input) =>
|
||||
},
|
||||
oauth: {
|
||||
connect: (input) =>
|
||||
response(
|
||||
integration.connection.oauth({
|
||||
integration.oauth.connect({
|
||||
integrationID: Integration.ID.make(input.integrationID),
|
||||
methodID: Integration.MethodID.make(input.methodID),
|
||||
inputs: input.inputs,
|
||||
label: input.label,
|
||||
}),
|
||||
),
|
||||
},
|
||||
attempt: {
|
||||
status: (input) => response(integration.attempt.status(Integration.AttemptID.make(input.attemptID))),
|
||||
status: (input) =>
|
||||
response(
|
||||
integration.oauth.status({
|
||||
integrationID: Integration.ID.make(input.integrationID),
|
||||
attemptID: Integration.AttemptID.make(input.attemptID),
|
||||
}),
|
||||
),
|
||||
complete: (input) =>
|
||||
integration.attempt.complete({ attemptID: Integration.AttemptID.make(input.attemptID), code: input.code }),
|
||||
cancel: (input) => integration.attempt.cancel(Integration.AttemptID.make(input.attemptID)),
|
||||
integration.oauth.complete({
|
||||
integrationID: Integration.ID.make(input.integrationID),
|
||||
attemptID: Integration.AttemptID.make(input.attemptID),
|
||||
code: input.code,
|
||||
}),
|
||||
cancel: (input) =>
|
||||
integration.oauth.cancel({
|
||||
integrationID: Integration.ID.make(input.integrationID),
|
||||
attemptID: Integration.AttemptID.make(input.attemptID),
|
||||
}),
|
||||
},
|
||||
command: {
|
||||
connect: (input) =>
|
||||
response(
|
||||
integration.command.connect({
|
||||
integrationID: Integration.ID.make(input.integrationID),
|
||||
methodID: Integration.MethodID.make(input.methodID),
|
||||
label: input.label,
|
||||
}),
|
||||
),
|
||||
status: (input) =>
|
||||
response(
|
||||
integration.command.status({
|
||||
integrationID: Integration.ID.make(input.integrationID),
|
||||
attemptID: Integration.AttemptID.make(input.attemptID),
|
||||
}),
|
||||
),
|
||||
cancel: (input) =>
|
||||
integration.command.cancel({
|
||||
integrationID: Integration.ID.make(input.integrationID),
|
||||
attemptID: Integration.AttemptID.make(input.attemptID),
|
||||
}),
|
||||
},
|
||||
reload: integration.reload,
|
||||
connection: {
|
||||
@@ -267,6 +306,13 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
|
||||
})
|
||||
return
|
||||
}
|
||||
if (input.method.type === "command") {
|
||||
draft.method.update({
|
||||
integrationID: Integration.ID.make(input.integrationID),
|
||||
method: Schema.decodeUnknownSync(Integration.CommandMethod)(input.method),
|
||||
})
|
||||
return
|
||||
}
|
||||
draft.method.update({
|
||||
integrationID: Integration.ID.make(input.integrationID),
|
||||
method: { type: "key", label: input.method.label },
|
||||
@@ -367,6 +413,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
|
||||
},
|
||||
},
|
||||
session: {
|
||||
hook: (name, callback) => hooks.register("session", name, callback),
|
||||
create: (input) =>
|
||||
runtime.session.create({
|
||||
id: input?.id,
|
||||
@@ -378,6 +425,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
|
||||
get: (input) => runtime.session.get(input.sessionID),
|
||||
prompt: runtime.session.prompt,
|
||||
command: runtime.session.command,
|
||||
synthetic: runtime.session.synthetic,
|
||||
interrupt: (input) => runtime.session.interrupt(input.sessionID),
|
||||
},
|
||||
} satisfies Plugin.Context
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Config } from "../config"
|
||||
import { ConfigAgentPlugin } from "../config/plugin/agent"
|
||||
import { ConfigCommandPlugin } from "../config/plugin/command"
|
||||
import { ConfigProviderPlugin } from "../config/plugin/provider"
|
||||
import { ConfigPolicyPlugin } from "../config/plugin/policy"
|
||||
import { ConfigReferencePlugin } from "../config/plugin/reference"
|
||||
import { ConfigSkillPlugin } from "../config/plugin/skill"
|
||||
import { EventV2 } from "../event"
|
||||
@@ -43,6 +44,7 @@ import { SubagentTool } from "../tool/subagent"
|
||||
import { Tools } from "../tool/tools"
|
||||
import { WebFetchTool } from "../tool/webfetch"
|
||||
import { WebSearchTool } from "../tool/websearch"
|
||||
import { WellKnown } from "../wellknown"
|
||||
import { WriteTool } from "../tool/write"
|
||||
import { AgentPlugin } from "./agent"
|
||||
import { CommandPlugin } from "./command"
|
||||
@@ -50,7 +52,9 @@ import { ModelsDevPlugin } from "./models-dev"
|
||||
import { ProviderPlugins } from "./provider"
|
||||
import { PluginRuntime } from "./runtime"
|
||||
import { SkillPlugin } from "./skill"
|
||||
import { SystemPromptPlugin } from "./system-prompt"
|
||||
import { VariantPlugin } from "./variant"
|
||||
import { WellKnownPlugin } from "../wellknown/plugin"
|
||||
|
||||
const services = Effect.fn("PluginInternal.services")(function* () {
|
||||
const agent = yield* AgentV2.Service
|
||||
@@ -80,6 +84,7 @@ const services = Effect.fn("PluginInternal.services")(function* () {
|
||||
const skill = yield* SkillV2.Service
|
||||
const tools = yield* Tools.Service
|
||||
const websearch = yield* WebSearchTool.ConfigService
|
||||
const wellknown = yield* WellKnown.Service
|
||||
return Context.mergeAll(
|
||||
Context.make(AgentV2.Service, agent),
|
||||
Context.make(Catalog.Service, catalog),
|
||||
@@ -108,6 +113,7 @@ const services = Effect.fn("PluginInternal.services")(function* () {
|
||||
Context.make(SkillV2.Service, skill),
|
||||
Context.make(Tools.Service, tools),
|
||||
Context.make(WebSearchTool.ConfigService, websearch),
|
||||
Context.make(WellKnown.Service, wellknown),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -118,9 +124,11 @@ export type Requirements = ContextServices<Effect.Success<ReturnType<typeof serv
|
||||
export type InternalPlugin = Plugin<Requirements | Scope.Scope>
|
||||
|
||||
const pre = [
|
||||
WellKnownPlugin.Plugin,
|
||||
AgentPlugin.Plugin,
|
||||
CommandPlugin.Plugin,
|
||||
SkillPlugin.Plugin,
|
||||
...SystemPromptPlugin.Plugins,
|
||||
ModelsDevPlugin,
|
||||
...ProviderPlugins,
|
||||
PatchTool.Plugin,
|
||||
@@ -144,6 +152,7 @@ const post = [
|
||||
ConfigSkillPlugin.Plugin,
|
||||
ConfigProviderPlugin.Plugin,
|
||||
VariantPlugin.Plugin,
|
||||
ConfigPolicyPlugin.Plugin,
|
||||
] as const satisfies readonly InternalPlugin[]
|
||||
|
||||
export const list = Effect.fn("PluginInternal.list")(function* () {
|
||||
|
||||
@@ -59,6 +59,7 @@ export function fromPromise(plugin: Plugin) {
|
||||
const context2: Context = {
|
||||
options: host.options,
|
||||
agent: {
|
||||
get: (id) => run(host.agent.get(id)),
|
||||
list: (input) => run(host.agent.list(input)),
|
||||
transform: transform(host.agent),
|
||||
reload: () => run(host.agent.reload()),
|
||||
@@ -70,9 +71,11 @@ export function fromPromise(plugin: Plugin) {
|
||||
catalog: {
|
||||
provider: {
|
||||
list: (input) => run(host.catalog.provider.list(input)),
|
||||
get: (input) => run(host.catalog.provider.get({ ...input, providerID: Provider.ID.make(input.providerID) })),
|
||||
get: (input) =>
|
||||
run(host.catalog.provider.get({ ...input, providerID: Provider.ID.make(input.providerID) })),
|
||||
},
|
||||
model: {
|
||||
get: (providerID, modelID) => run(host.catalog.model.get(providerID, modelID)),
|
||||
list: (input) => run(host.catalog.model.list(input)),
|
||||
default: (input) =>
|
||||
run(host.catalog.model.default(input)).then((result) => ({ ...result, data: result.data ?? null })),
|
||||
@@ -96,35 +99,66 @@ export function fromPromise(plugin: Plugin) {
|
||||
),
|
||||
connect: {
|
||||
key: (input) =>
|
||||
run(host.integration.connect.key({ ...input, integrationID: Integration.ID.make(input.integrationID) })),
|
||||
oauth: (input) =>
|
||||
run(
|
||||
host.integration.connect.oauth({
|
||||
host.integration.connect.key({ ...input, integrationID: Integration.ID.make(input.integrationID) }),
|
||||
),
|
||||
},
|
||||
oauth: {
|
||||
connect: (input) =>
|
||||
run(
|
||||
host.integration.oauth.connect({
|
||||
...input,
|
||||
integrationID: Integration.ID.make(input.integrationID),
|
||||
methodID: Integration.MethodID.make(input.methodID),
|
||||
}),
|
||||
),
|
||||
},
|
||||
attempt: {
|
||||
status: (input) =>
|
||||
run(
|
||||
host.integration.attempt.status({
|
||||
host.integration.oauth.status({
|
||||
...input,
|
||||
integrationID: Integration.ID.make(input.integrationID),
|
||||
attemptID: Integration.AttemptID.make(input.attemptID),
|
||||
}),
|
||||
),
|
||||
complete: (input) =>
|
||||
run(
|
||||
host.integration.attempt.complete({
|
||||
host.integration.oauth.complete({
|
||||
...input,
|
||||
integrationID: Integration.ID.make(input.integrationID),
|
||||
attemptID: Integration.AttemptID.make(input.attemptID),
|
||||
}),
|
||||
),
|
||||
cancel: (input) =>
|
||||
run(
|
||||
host.integration.attempt.cancel({
|
||||
host.integration.oauth.cancel({
|
||||
...input,
|
||||
integrationID: Integration.ID.make(input.integrationID),
|
||||
attemptID: Integration.AttemptID.make(input.attemptID),
|
||||
}),
|
||||
),
|
||||
},
|
||||
command: {
|
||||
connect: (input) =>
|
||||
run(
|
||||
host.integration.command.connect({
|
||||
...input,
|
||||
integrationID: Integration.ID.make(input.integrationID),
|
||||
methodID: Integration.MethodID.make(input.methodID),
|
||||
}),
|
||||
),
|
||||
status: (input) =>
|
||||
run(
|
||||
host.integration.command.status({
|
||||
...input,
|
||||
integrationID: Integration.ID.make(input.integrationID),
|
||||
attemptID: Integration.AttemptID.make(input.attemptID),
|
||||
}),
|
||||
),
|
||||
cancel: (input) =>
|
||||
run(
|
||||
host.integration.command.cancel({
|
||||
...input,
|
||||
integrationID: Integration.ID.make(input.integrationID),
|
||||
attemptID: Integration.AttemptID.make(input.attemptID),
|
||||
}),
|
||||
),
|
||||
@@ -162,6 +196,8 @@ export function fromPromise(plugin: Plugin) {
|
||||
register(host.tool.hook(name, (event) => Effect.promise(() => Promise.resolve(callback(event))))),
|
||||
},
|
||||
session: {
|
||||
hook: (name, callback) =>
|
||||
register(host.session.hook(name, (event) => Effect.promise(() => Promise.resolve(callback(event))))),
|
||||
create: (input) =>
|
||||
run(
|
||||
host.session.create(
|
||||
@@ -208,6 +244,17 @@ export function fromPromise(plugin: Plugin) {
|
||||
resume: input.resume ?? undefined,
|
||||
}),
|
||||
),
|
||||
synthetic: (input) =>
|
||||
run(
|
||||
host.session.synthetic({
|
||||
...input,
|
||||
sessionID: Session.ID.make(input.sessionID),
|
||||
id: input.id == null ? undefined : SessionMessage.ID.make(input.id),
|
||||
description: input.description ?? undefined,
|
||||
delivery: input.delivery ?? undefined,
|
||||
resume: input.resume ?? undefined,
|
||||
}),
|
||||
),
|
||||
interrupt: (input) => run(host.session.interrupt({ sessionID: Session.ID.make(input.sessionID) })),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import { SkillV2 } from "../skill"
|
||||
import { ReadToolFileSystem } from "../tool/read-filesystem"
|
||||
import { ToolRegistry } from "../tool/registry"
|
||||
import { WebSearchTool } from "../tool/websearch"
|
||||
import { WellKnown } from "../wellknown"
|
||||
import { PluginInternal } from "./internal"
|
||||
import { PluginRuntime } from "./runtime"
|
||||
import { SdkPlugins } from "./sdk"
|
||||
@@ -163,9 +164,7 @@ const load = Effect.fn("PluginSupervisor.load")(function* (operation: Extract<Op
|
||||
if (!entrypoint) return
|
||||
// Bun currently ignores query parameters when caching file:// imports.
|
||||
const source =
|
||||
operation.mtime === undefined
|
||||
? entrypoint
|
||||
: `${operation.target.replaceAll("\\", "/")}?mtime=${operation.mtime}`
|
||||
operation.mtime === undefined ? entrypoint : `${operation.target.replaceAll("\\", "/")}?mtime=${operation.mtime}`
|
||||
yield* Effect.log({ msg: "loading plugin", id: operation.target, entrypoint: source })
|
||||
const mod = yield* Effect.promise(() => import(source))
|
||||
const value = (yield* Schema.decodeUnknownEffect(PluginModule)(mod)).default
|
||||
@@ -296,6 +295,7 @@ export const node = makeLocationNode({
|
||||
SkillV2.node,
|
||||
ToolRegistry.toolsNode,
|
||||
WebSearchTool.configNode,
|
||||
WellKnown.node,
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
export * as SystemPromptPlugin from "./system-prompt"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
import { Effect } from "effect"
|
||||
|
||||
import PROMPT_ANTHROPIC from "./system-prompt/anthropic.txt"
|
||||
import PROMPT_CODEX from "./system-prompt/codex.txt"
|
||||
import PROMPT_GEMINI from "./system-prompt/gemini.txt"
|
||||
import PROMPT_GPT from "./system-prompt/gpt.txt"
|
||||
import PROMPT_KIMI from "./system-prompt/kimi.txt"
|
||||
import PROMPT_META from "./system-prompt/meta.txt"
|
||||
import PROMPT_TRINITY from "./system-prompt/trinity.txt"
|
||||
|
||||
export const OpenAIPlugin = make("openai", (id) => {
|
||||
if (id.includes("gpt")) {
|
||||
if (id.includes("codex")) return PROMPT_CODEX
|
||||
return PROMPT_GPT
|
||||
}
|
||||
if (id.includes("o1") || id.includes("o3")) return PROMPT_GPT
|
||||
})
|
||||
|
||||
export const GooglePlugin = make("google", (id) => (id.includes("gemini-") ? PROMPT_GEMINI : undefined))
|
||||
export const AnthropicPlugin = make("anthropic", (id) => (id.includes("claude") ? PROMPT_ANTHROPIC : undefined))
|
||||
export const KimiPlugin = make("kimi", (id) => (id.includes("kimi") ? PROMPT_KIMI : undefined))
|
||||
export const ArceePlugin = make("arcee", (id) => (id.includes("trinity") ? PROMPT_TRINITY : undefined))
|
||||
export const MetaPlugin = make("meta", (id) => (id.includes("muse-spark") ? PROMPT_META : undefined))
|
||||
|
||||
export const Plugins = [OpenAIPlugin, GooglePlugin, AnthropicPlugin, KimiPlugin, ArceePlugin, MetaPlugin] as const
|
||||
|
||||
function make(id: string, select: (modelID: string) => string | undefined) {
|
||||
return define({
|
||||
id: `opencode.system-prompt.${id}`,
|
||||
effect: Effect.fn(`SystemPromptPlugin.${id}`)(function* (ctx) {
|
||||
yield* ctx.session.hook("context", (event) =>
|
||||
Effect.gen(function* () {
|
||||
if ((yield* ctx.agent.get(event.agent))?.system) return
|
||||
const system = event.system[0]
|
||||
if (!system) return
|
||||
const model = yield* ctx.catalog.model.get(event.model.providerID, event.model.id)
|
||||
const prompt = select(`${model?.modelID ?? event.model.id} ${model?.family ?? ""}`.toLowerCase())
|
||||
if (!prompt) return
|
||||
event.system[0] = { ...system, text: prompt }
|
||||
}),
|
||||
)
|
||||
}),
|
||||
})
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user