mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-09 19:09:49 -04:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a841e828ce | |||
| b9c118e0fe | |||
| d1c9e39978 | |||
| 0b1ec457dc | |||
| 9bd99b4c91 | |||
| 94fcb119a3 | |||
| aa820db18d | |||
| 42e8d11552 | |||
| 28d784b83a | |||
| a4ad17347f |
@@ -88,6 +88,7 @@
|
|||||||
"@tanstack/solid-query": "5.91.4",
|
"@tanstack/solid-query": "5.91.4",
|
||||||
"@tanstack/solid-virtual": "catalog:",
|
"@tanstack/solid-virtual": "catalog:",
|
||||||
"@thisbeyond/solid-dnd": "0.7.5",
|
"@thisbeyond/solid-dnd": "0.7.5",
|
||||||
|
"diff": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
"fuzzysort": "catalog:",
|
"fuzzysort": "catalog:",
|
||||||
"ghostty-web": "github:anomalyco/ghostty-web#83c0a07b8628b748aed073b232cb4b52a6ca11c1",
|
"ghostty-web": "github:anomalyco/ghostty-web#83c0a07b8628b748aed073b232cb4b52a6ca11c1",
|
||||||
@@ -284,13 +285,9 @@
|
|||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.18.8",
|
"version": "1.18.8",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "3.0.82",
|
|
||||||
"@ai-sdk/openai": "3.0.48",
|
|
||||||
"@ai-sdk/openai-compatible": "2.0.37",
|
|
||||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||||
"@opencode-ai/console-core": "workspace:*",
|
"@opencode-ai/console-core": "workspace:*",
|
||||||
"@opencode-ai/console-resource": "workspace:*",
|
"@opencode-ai/console-resource": "workspace:*",
|
||||||
"ai": "catalog:",
|
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -298,7 +295,6 @@
|
|||||||
"@tsconfig/node22": "22.0.2",
|
"@tsconfig/node22": "22.0.2",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
"openai": "5.11.0",
|
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -361,18 +357,15 @@
|
|||||||
"@ai-sdk/cohere": "3.0.27",
|
"@ai-sdk/cohere": "3.0.27",
|
||||||
"@ai-sdk/deepinfra": "2.0.41",
|
"@ai-sdk/deepinfra": "2.0.41",
|
||||||
"@ai-sdk/gateway": "3.0.104",
|
"@ai-sdk/gateway": "3.0.104",
|
||||||
"@ai-sdk/google": "3.0.73",
|
|
||||||
"@ai-sdk/google-vertex": "4.0.128",
|
"@ai-sdk/google-vertex": "4.0.128",
|
||||||
"@ai-sdk/groq": "3.0.31",
|
"@ai-sdk/groq": "3.0.31",
|
||||||
"@ai-sdk/mistral": "3.0.51",
|
"@ai-sdk/mistral": "3.0.51",
|
||||||
"@ai-sdk/openai": "3.0.84",
|
|
||||||
"@ai-sdk/openai-compatible": "2.0.41",
|
"@ai-sdk/openai-compatible": "2.0.41",
|
||||||
"@ai-sdk/perplexity": "3.0.26",
|
"@ai-sdk/perplexity": "3.0.26",
|
||||||
"@ai-sdk/provider": "3.0.8",
|
"@ai-sdk/provider": "3.0.8",
|
||||||
"@ai-sdk/provider-utils": "4.0.23",
|
"@ai-sdk/provider-utils": "4.0.23",
|
||||||
"@ai-sdk/togetherai": "2.0.41",
|
"@ai-sdk/togetherai": "2.0.41",
|
||||||
"@ai-sdk/vercel": "2.0.39",
|
"@ai-sdk/vercel": "2.0.39",
|
||||||
"@ai-sdk/xai": "3.0.102",
|
|
||||||
"@aws-sdk/credential-providers": "3.1057.0",
|
"@aws-sdk/credential-providers": "3.1057.0",
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
"@effect/sql-sqlite-bun": "catalog:",
|
"@effect/sql-sqlite-bun": "catalog:",
|
||||||
@@ -386,7 +379,6 @@
|
|||||||
"@opencode-ai/plugin": "workspace:*",
|
"@opencode-ai/plugin": "workspace:*",
|
||||||
"@opencode-ai/schema": "workspace:*",
|
"@opencode-ai/schema": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
"@openrouter/ai-sdk-provider": "2.9.0",
|
|
||||||
"@parcel/watcher": "2.5.1",
|
"@parcel/watcher": "2.5.1",
|
||||||
"@silvia-odwyer/photon-node": "0.3.4",
|
"@silvia-odwyer/photon-node": "0.3.4",
|
||||||
"@standard-schema/spec": "catalog:",
|
"@standard-schema/spec": "catalog:",
|
||||||
@@ -911,7 +903,6 @@
|
|||||||
"@opentui/solid": "catalog:",
|
"@opentui/solid": "catalog:",
|
||||||
"@solid-primitives/event-bus": "1.1.2",
|
"@solid-primitives/event-bus": "1.1.2",
|
||||||
"clipboardy": "4.0.0",
|
"clipboardy": "4.0.0",
|
||||||
"diff": "catalog:",
|
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
"fuzzysort": "catalog:",
|
"fuzzysort": "catalog:",
|
||||||
"get-east-asian-width": "catalog:",
|
"get-east-asian-width": "catalog:",
|
||||||
@@ -1038,7 +1029,6 @@
|
|||||||
"@shikijs/transformers": "3.20.0",
|
"@shikijs/transformers": "3.20.0",
|
||||||
"@solid-primitives/resize-observer": "2.1.5",
|
"@solid-primitives/resize-observer": "2.1.5",
|
||||||
"@types/luxon": "catalog:",
|
"@types/luxon": "catalog:",
|
||||||
"ai": "catalog:",
|
|
||||||
"astro": "5.7.13",
|
"astro": "5.7.13",
|
||||||
"diff": "catalog:",
|
"diff": "catalog:",
|
||||||
"js-base64": "3.7.7",
|
"js-base64": "3.7.7",
|
||||||
@@ -1084,17 +1074,16 @@
|
|||||||
"tree-sitter-bash",
|
"tree-sitter-bash",
|
||||||
],
|
],
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
|
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
|
||||||
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
|
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
|
||||||
"effect@4.0.0-beta.101": "patches/effect@4.0.0-beta.101.patch",
|
"effect@4.0.0-beta.101": "patches/effect@4.0.0-beta.101.patch",
|
||||||
"@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch",
|
|
||||||
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
|
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
|
||||||
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch",
|
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch",
|
||||||
|
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
|
||||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||||
"@ai-sdk/mistral@3.0.51": "patches/@ai-sdk%2Fmistral@3.0.51.patch",
|
"@ai-sdk/mistral@3.0.51": "patches/@ai-sdk%2Fmistral@3.0.51.patch",
|
||||||
"@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch",
|
"@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch",
|
||||||
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
|
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
|
||||||
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
|
|
||||||
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
|
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"@opentui/core": "catalog:",
|
"@opentui/core": "catalog:",
|
||||||
@@ -1220,9 +1209,9 @@
|
|||||||
|
|
||||||
"@ai-sdk/mistral": ["@ai-sdk/mistral@3.0.51", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.40" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-83eXY6p0lUFhSuMvNDmTKDuMciK5XDAWDlNh5c0L80tKjmtCFRItA1MZHp4IKe1r7eK8Rb5nN7qtxqMLUFRIRw=="],
|
"@ai-sdk/mistral": ["@ai-sdk/mistral@3.0.51", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.40" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-83eXY6p0lUFhSuMvNDmTKDuMciK5XDAWDlNh5c0L80tKjmtCFRItA1MZHp4IKe1r7eK8Rb5nN7qtxqMLUFRIRw=="],
|
||||||
|
|
||||||
"@ai-sdk/openai": ["@ai-sdk/openai@3.0.48", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ALmj/53EXpcRqMbGpPJPP4UOSWw0q4VGpnDo7YctvsynjkrKDmoneDG/1a7VQnSPYHnJp6tTRMf5ZdxZ5whulg=="],
|
"@ai-sdk/openai": ["@ai-sdk/openai@3.0.67", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-oAiGC9eWG7IgtdsdS74bOCnAAHarAfTJhWN9x5INwnWPekL802AvF+0I5DvLzIF1MIRmNw4N8mPSL/GUVbX9Mw=="],
|
||||||
|
|
||||||
"@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.37", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-+POSFVcgiu47BK64dhsI6OpcDC0/VAE2ZSaXdXGNNhpC/ava++uSRJYks0k2bpfY0wwCTgpAWZsXn/dG2Yppiw=="],
|
"@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
||||||
|
|
||||||
"@ai-sdk/perplexity": ["@ai-sdk/perplexity@3.0.26", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-dXzrVsLR5f6tr+U04jq4AXoRroGFBTvODnLgss0SWbzNjGGQg3XqtQ9j7rCLo6o8qbYGuAHvqUrIpUCuiscuFg=="],
|
"@ai-sdk/perplexity": ["@ai-sdk/perplexity@3.0.26", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-dXzrVsLR5f6tr+U04jq4AXoRroGFBTvODnLgss0SWbzNjGGQg3XqtQ9j7rCLo6o8qbYGuAHvqUrIpUCuiscuFg=="],
|
||||||
|
|
||||||
@@ -4944,7 +4933,7 @@
|
|||||||
|
|
||||||
"open": ["open@10.1.2", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "is-wsl": "^3.1.0" } }, "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw=="],
|
"open": ["open@10.1.2", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "is-wsl": "^3.1.0" } }, "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw=="],
|
||||||
|
|
||||||
"openai": ["openai@5.11.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-+AuTc5pVjlnTuA9zvn8rA/k+1RluPIx9AD4eDcnutv6JNwHHZxIhkFy+tmMKCvmMFDQzfA/r1ujvPWB19DQkYg=="],
|
"openai": ["openai@6.49.0", "", { "peerDependencies": { "@aws-sdk/credential-provider-node": ">=3.972.0 <4", "@smithy/hash-node": ">=4.3.0 <5", "@smithy/signature-v4": ">=5.4.0 <6", "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["@aws-sdk/credential-provider-node", "@smithy/hash-node", "@smithy/signature-v4", "ws", "zod"] }, "sha512-aYCc0C6L864eR6WSYIwQGyXriw/nIyZx0ObvhzOEVuk0zoBDpynjSbrionWI7q65B5H8jJX0DXR9snEzM6bfPg=="],
|
||||||
|
|
||||||
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
|
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
|
||||||
|
|
||||||
@@ -5962,12 +5951,8 @@
|
|||||||
|
|
||||||
"@actions/http-client/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="],
|
"@actions/http-client/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="],
|
||||||
|
|
||||||
"@ai-sdk/alibaba/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
|
||||||
|
|
||||||
"@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.81", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-B1JDd9Ugq9R5AgIaW3674lhGCMMYJcPUxnrZh8fzbGojgg4QvHFRv6eZahGQAUsmGHbcf74G9bdSBDLWQGY2GA=="],
|
"@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.81", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-B1JDd9Ugq9R5AgIaW3674lhGCMMYJcPUxnrZh8fzbGojgg4QvHFRv6eZahGQAUsmGHbcf74G9bdSBDLWQGY2GA=="],
|
||||||
|
|
||||||
"@ai-sdk/amazon-bedrock/@ai-sdk/openai": ["@ai-sdk/openai@3.0.67", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-oAiGC9eWG7IgtdsdS74bOCnAAHarAfTJhWN9x5INwnWPekL802AvF+0I5DvLzIF1MIRmNw4N8mPSL/GUVbX9Mw=="],
|
|
||||||
|
|
||||||
"@ai-sdk/amazon-bedrock/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
|
"@ai-sdk/amazon-bedrock/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
|
||||||
|
|
||||||
"@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/@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=="],
|
||||||
@@ -5982,6 +5967,8 @@
|
|||||||
|
|
||||||
"@ai-sdk/azure/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.38", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-/HHGmtKllqjg1OLc023v9w9kK3laW7Z6TzfZukYQWCsGBbzB9p60zTvvpXFVcs44NZBVXL3viOa1HRKUbeee8g=="],
|
"@ai-sdk/azure/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.38", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-/HHGmtKllqjg1OLc023v9w9kK3laW7Z6TzfZukYQWCsGBbzB9p60zTvvpXFVcs44NZBVXL3viOa1HRKUbeee8g=="],
|
||||||
|
|
||||||
|
"@ai-sdk/cerebras/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.37", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-+POSFVcgiu47BK64dhsI6OpcDC0/VAE2ZSaXdXGNNhpC/ava++uSRJYks0k2bpfY0wwCTgpAWZsXn/dG2Yppiw=="],
|
||||||
|
|
||||||
"@ai-sdk/cerebras/@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/cerebras/@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/cohere/@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/cohere/@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=="],
|
||||||
@@ -5990,6 +5977,8 @@
|
|||||||
|
|
||||||
"@ai-sdk/deepgram/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.40", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-OL5IrpUm9Y8Dwy+w/vvFwPotS6m52O9W0op2oXgXdCROMJIBalBI0oro6OIBYkPxvm5Xg02GSkoQN25RlR0bnw=="],
|
"@ai-sdk/deepgram/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.40", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-OL5IrpUm9Y8Dwy+w/vvFwPotS6m52O9W0op2oXgXdCROMJIBalBI0oro6OIBYkPxvm5Xg02GSkoQN25RlR0bnw=="],
|
||||||
|
|
||||||
|
"@ai-sdk/deepinfra/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.37", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-+POSFVcgiu47BK64dhsI6OpcDC0/VAE2ZSaXdXGNNhpC/ava++uSRJYks0k2bpfY0wwCTgpAWZsXn/dG2Yppiw=="],
|
||||||
|
|
||||||
"@ai-sdk/deepinfra/@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/deepinfra/@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/deepseek/@ai-sdk/provider": ["@ai-sdk/provider@3.0.14", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-5X1k57JBJ4H7H1QjX7CnJYAB1I19r/trVZTMcSms7/kLNZ8RaU4Nt2agcwZzv82Hfx6Q7/TOLU7agAKeFfc8cA=="],
|
"@ai-sdk/deepseek/@ai-sdk/provider": ["@ai-sdk/provider@3.0.14", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-5X1k57JBJ4H7H1QjX7CnJYAB1I19r/trVZTMcSms7/kLNZ8RaU4Nt2agcwZzv82Hfx6Q7/TOLU7agAKeFfc8cA=="],
|
||||||
@@ -6024,14 +6013,18 @@
|
|||||||
|
|
||||||
"@ai-sdk/mistral/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.40", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-OL5IrpUm9Y8Dwy+w/vvFwPotS6m52O9W0op2oXgXdCROMJIBalBI0oro6OIBYkPxvm5Xg02GSkoQN25RlR0bnw=="],
|
"@ai-sdk/mistral/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.40", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-OL5IrpUm9Y8Dwy+w/vvFwPotS6m52O9W0op2oXgXdCROMJIBalBI0oro6OIBYkPxvm5Xg02GSkoQN25RlR0bnw=="],
|
||||||
|
|
||||||
"@ai-sdk/openai/@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/openai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
|
||||||
|
|
||||||
"@ai-sdk/openai-compatible/@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/openai/@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/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/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/togetherai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.37", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-+POSFVcgiu47BK64dhsI6OpcDC0/VAE2ZSaXdXGNNhpC/ava++uSRJYks0k2bpfY0wwCTgpAWZsXn/dG2Yppiw=="],
|
||||||
|
|
||||||
"@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/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/openai-compatible": ["@ai-sdk/openai-compatible@2.0.37", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-+POSFVcgiu47BK64dhsI6OpcDC0/VAE2ZSaXdXGNNhpC/ava++uSRJYks0k2bpfY0wwCTgpAWZsXn/dG2Yppiw=="],
|
||||||
|
|
||||||
"@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=="],
|
"@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=="],
|
||||||
|
|
||||||
"@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.56", "", { "dependencies": { "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.35" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cQrN6OUn/jvsY3OdsU6Wn+ss7vp1iwIcakZKSlSRMnYqShBfyT7Qht+eqmgxs7w9ttrw6FAG6o11AiBs+iEsTA=="],
|
"@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.56", "", { "dependencies": { "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.35" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cQrN6OUn/jvsY3OdsU6Wn+ss7vp1iwIcakZKSlSRMnYqShBfyT7Qht+eqmgxs7w9ttrw6FAG6o11AiBs+iEsTA=="],
|
||||||
@@ -6350,10 +6343,6 @@
|
|||||||
|
|
||||||
"@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/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=="],
|
|
||||||
|
|
||||||
"@opencode-ai/core/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
|
||||||
|
|
||||||
"@opencode-ai/desktop/@actions/artifact": ["@actions/artifact@4.0.0", "", { "dependencies": { "@actions/core": "^1.10.0", "@actions/github": "^6.0.1", "@actions/http-client": "^2.1.0", "@azure/core-http": "^3.0.5", "@azure/storage-blob": "^12.15.0", "@octokit/core": "^5.2.1", "@octokit/plugin-request-log": "^1.0.4", "@octokit/plugin-retry": "^3.0.9", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "@protobuf-ts/plugin": "^2.2.3-alpha.1", "archiver": "^7.0.1", "jwt-decode": "^3.1.2", "unzip-stream": "^0.3.1" } }, "sha512-HCc2jMJRAfviGFAh0FsOR/jNfWhirxl7W6z8zDtttt0GltwxBLdEIjLiweOPFl9WbyJRW1VWnPUSAixJqcWUMQ=="],
|
"@opencode-ai/desktop/@actions/artifact": ["@actions/artifact@4.0.0", "", { "dependencies": { "@actions/core": "^1.10.0", "@actions/github": "^6.0.1", "@actions/http-client": "^2.1.0", "@azure/core-http": "^3.0.5", "@azure/storage-blob": "^12.15.0", "@octokit/core": "^5.2.1", "@octokit/plugin-request-log": "^1.0.4", "@octokit/plugin-retry": "^3.0.9", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "@protobuf-ts/plugin": "^2.2.3-alpha.1", "archiver": "^7.0.1", "jwt-decode": "^3.1.2", "unzip-stream": "^0.3.1" } }, "sha512-HCc2jMJRAfviGFAh0FsOR/jNfWhirxl7W6z8zDtttt0GltwxBLdEIjLiweOPFl9WbyJRW1VWnPUSAixJqcWUMQ=="],
|
||||||
|
|
||||||
"@opencode-ai/desktop/marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="],
|
"@opencode-ai/desktop/marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="],
|
||||||
@@ -6578,6 +6567,8 @@
|
|||||||
|
|
||||||
"babel-plugin-module-resolver/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="],
|
"babel-plugin-module-resolver/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="],
|
||||||
|
|
||||||
|
"blume/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.37", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-+POSFVcgiu47BK64dhsI6OpcDC0/VAE2ZSaXdXGNNhpC/ava++uSRJYks0k2bpfY0wwCTgpAWZsXn/dG2Yppiw=="],
|
||||||
|
|
||||||
"blume/@astrojs/mdx": ["@astrojs/mdx@7.0.3", "", { "dependencies": { "@astrojs/internal-helpers": "0.10.1", "@astrojs/markdown-remark": "7.2.1", "@mdx-js/mdx": "^3.1.1", "acorn": "^8.16.0", "es-module-lexer": "^2.0.0", "estree-util-visit": "^2.0.0", "hast-util-to-html": "^9.0.5", "piccolore": "^0.1.3", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-smartypants": "^3.0.2", "source-map": "^0.7.6", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3" }, "peerDependencies": { "@astrojs/markdown-satteri": "^0.3.1", "astro": "^7.0.0" }, "optionalPeers": ["@astrojs/markdown-satteri"] }, "sha512-RxyIwU0uFam5ftwqKOjpIdhnFxZ/kEikeimLyQy3eGXbHT8WgRGzzesOIHVU8+m9TY8ag5WVOyvV24/GyqPdPQ=="],
|
"blume/@astrojs/mdx": ["@astrojs/mdx@7.0.3", "", { "dependencies": { "@astrojs/internal-helpers": "0.10.1", "@astrojs/markdown-remark": "7.2.1", "@mdx-js/mdx": "^3.1.1", "acorn": "^8.16.0", "es-module-lexer": "^2.0.0", "estree-util-visit": "^2.0.0", "hast-util-to-html": "^9.0.5", "piccolore": "^0.1.3", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-smartypants": "^3.0.2", "source-map": "^0.7.6", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3" }, "peerDependencies": { "@astrojs/markdown-satteri": "^0.3.1", "astro": "^7.0.0" }, "optionalPeers": ["@astrojs/markdown-satteri"] }, "sha512-RxyIwU0uFam5ftwqKOjpIdhnFxZ/kEikeimLyQy3eGXbHT8WgRGzzesOIHVU8+m9TY8ag5WVOyvV24/GyqPdPQ=="],
|
||||||
|
|
||||||
"blume/@clack/prompts": ["@clack/prompts@1.7.0", "", { "dependencies": { "@clack/core": "1.4.3", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A=="],
|
"blume/@clack/prompts": ["@clack/prompts@1.7.0", "", { "dependencies": { "@clack/core": "1.4.3", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A=="],
|
||||||
@@ -6714,8 +6705,6 @@
|
|||||||
|
|
||||||
"gcp-metadata/google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="],
|
"gcp-metadata/google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="],
|
||||||
|
|
||||||
"gitlab-ai-provider/openai": ["openai@6.49.0", "", { "peerDependencies": { "@aws-sdk/credential-provider-node": ">=3.972.0 <4", "@smithy/hash-node": ">=4.3.0 <5", "@smithy/signature-v4": ">=5.4.0 <6", "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["@aws-sdk/credential-provider-node", "@smithy/hash-node", "@smithy/signature-v4", "ws", "zod"] }, "sha512-aYCc0C6L864eR6WSYIwQGyXriw/nIyZx0ObvhzOEVuk0zoBDpynjSbrionWI7q65B5H8jJX0DXR9snEzM6bfPg=="],
|
|
||||||
|
|
||||||
"gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
"gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||||
|
|
||||||
"globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
"globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||||
@@ -7380,10 +7369,6 @@
|
|||||||
|
|
||||||
"@octokit/rest/@octokit/core/before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="],
|
"@octokit/rest/@octokit/core/before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="],
|
||||||
|
|
||||||
"@opencode-ai/core/@ai-sdk/openai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.14", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-5X1k57JBJ4H7H1QjX7CnJYAB1I19r/trVZTMcSms7/kLNZ8RaU4Nt2agcwZzv82Hfx6Q7/TOLU7agAKeFfc8cA=="],
|
|
||||||
|
|
||||||
"@opencode-ai/core/@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.38", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-/HHGmtKllqjg1OLc023v9w9kK3laW7Z6TzfZukYQWCsGBbzB9p60zTvvpXFVcs44NZBVXL3viOa1HRKUbeee8g=="],
|
|
||||||
|
|
||||||
"@opencode-ai/desktop/@actions/artifact/@actions/core": ["@actions/core@1.11.1", "", { "dependencies": { "@actions/exec": "^1.1.1", "@actions/http-client": "^2.0.1" } }, "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A=="],
|
"@opencode-ai/desktop/@actions/artifact/@actions/core": ["@actions/core@1.11.1", "", { "dependencies": { "@actions/exec": "^1.1.1", "@actions/http-client": "^2.0.1" } }, "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A=="],
|
||||||
|
|
||||||
"@opencode-ai/desktop/@actions/artifact/@actions/http-client": ["@actions/http-client@2.2.3", "", { "dependencies": { "tunnel": "^0.0.6", "undici": "^5.25.4" } }, "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA=="],
|
"@opencode-ai/desktop/@actions/artifact/@actions/http-client": ["@actions/http-client@2.2.3", "", { "dependencies": { "tunnel": "^0.0.6", "undici": "^5.25.4" } }, "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA=="],
|
||||||
@@ -7620,6 +7605,8 @@
|
|||||||
|
|
||||||
"babel-plugin-module-resolver/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
|
"babel-plugin-module-resolver/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
|
||||||
|
|
||||||
|
"blume/@ai-sdk/openai-compatible/@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=="],
|
||||||
|
|
||||||
"blume/@astrojs/mdx/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.10.1", "", { "dependencies": { "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", "js-yaml": "^4.1.1", "picomatch": "^4.0.4", "retext-smartypants": "^6.2.0", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "unified": "^11.0.5" } }, "sha512-5phcroT/vmOOrYuuAxtkbPixy5hePtlz9i8K4OeDv3dNK6/UQRuXPOSRTxIOBbUY5Sonw2UaxjbuVc43Mcir6Q=="],
|
"blume/@astrojs/mdx/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.10.1", "", { "dependencies": { "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", "js-yaml": "^4.1.1", "picomatch": "^4.0.4", "retext-smartypants": "^6.2.0", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "unified": "^11.0.5" } }, "sha512-5phcroT/vmOOrYuuAxtkbPixy5hePtlz9i8K4OeDv3dNK6/UQRuXPOSRTxIOBbUY5Sonw2UaxjbuVc43Mcir6Q=="],
|
||||||
|
|
||||||
"blume/@astrojs/mdx/@astrojs/markdown-remark": ["@astrojs/markdown-remark@7.2.1", "", { "dependencies": { "@astrojs/internal-helpers": "0.10.1", "@astrojs/prism": "4.0.2", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.1.0", "unist-util-visit-parents": "^6.0.2", "vfile": "^6.0.3" } }, "sha512-jPVNIqTvk+yKviikszv/Y1U4jGUSKpp/Nw48QZV4qjWgp70j4Lkq3lhSDRbWwCfgKvEyO9GHuVbV1dM2WYXy1w=="],
|
"blume/@astrojs/mdx/@astrojs/markdown-remark": ["@astrojs/markdown-remark@7.2.1", "", { "dependencies": { "@astrojs/internal-helpers": "0.10.1", "@astrojs/prism": "4.0.2", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.1.0", "unist-util-visit-parents": "^6.0.2", "vfile": "^6.0.3" } }, "sha512-jPVNIqTvk+yKviikszv/Y1U4jGUSKpp/Nw48QZV4qjWgp70j4Lkq3lhSDRbWwCfgKvEyO9GHuVbV1dM2WYXy1w=="],
|
||||||
|
|||||||
@@ -161,7 +161,6 @@
|
|||||||
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
|
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
|
||||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||||
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
|
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
|
||||||
"@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch",
|
|
||||||
"@ai-sdk/mistral@3.0.51": "patches/@ai-sdk%2Fmistral@3.0.51.patch",
|
"@ai-sdk/mistral@3.0.51": "patches/@ai-sdk%2Fmistral@3.0.51.patch",
|
||||||
"gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch",
|
"gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch",
|
||||||
"pacote@21.5.0": "patches/pacote@21.5.0.patch",
|
"pacote@21.5.0": "patches/pacote@21.5.0.patch",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Endpoint, type EndpointPatch } from "./endpoint"
|
|||||||
import { RequestExecutor } from "./executor"
|
import { RequestExecutor } from "./executor"
|
||||||
import { Framing } from "./framing"
|
import { Framing } from "./framing"
|
||||||
import { HttpTransport } from "./transport"
|
import { HttpTransport } from "./transport"
|
||||||
import type { HttpMiddleware, HttpRequestTransform, Transport, TransportRuntime } from "./transport"
|
import type { HttpMiddleware, Transport, TransportRuntime } from "./transport"
|
||||||
import { WebSocketExecutor } from "./transport"
|
import { WebSocketExecutor } from "./transport"
|
||||||
import type { Protocol } from "./protocol"
|
import type { Protocol } from "./protocol"
|
||||||
import { applyCachePolicy } from "../cache-policy"
|
import { applyCachePolicy } from "../cache-policy"
|
||||||
@@ -155,7 +155,6 @@ export interface Interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface StreamOptions {
|
export interface StreamOptions {
|
||||||
readonly transform?: HttpRequestTransform
|
|
||||||
readonly http?: HttpMiddleware
|
readonly http?: HttpMiddleware
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,7 +307,6 @@ function makeFromTransport<Body, Prepared, Frame, Event, State>(
|
|||||||
auth: routeInput.auth ?? Auth.none,
|
auth: routeInput.auth ?? Auth.none,
|
||||||
encodeBody,
|
encodeBody,
|
||||||
headers: routeInput.headers,
|
headers: routeInput.headers,
|
||||||
transform: options?.transform,
|
|
||||||
middleware: options?.http,
|
middleware: options?.http,
|
||||||
}),
|
}),
|
||||||
streamPrepared: (prepared: Prepared, request: LLMRequest, runtime: TransportRuntime) => {
|
streamPrepared: (prepared: Prepared, request: LLMRequest, runtime: TransportRuntime) => {
|
||||||
|
|||||||
@@ -270,7 +270,7 @@ const toHttpError = (redactedNames: ReadonlyArray<string | RegExp>) => (error: u
|
|||||||
return transportError({ message: error.message, kind: "Timeout" })
|
return transportError({ message: error.message, kind: "Timeout" })
|
||||||
}
|
}
|
||||||
if (!HttpClientError.isHttpClientError(error)) {
|
if (!HttpClientError.isHttpClientError(error)) {
|
||||||
return transportError({ message: "HTTP transport failed" })
|
return transportError({ message: error instanceof Error ? error.message : "HTTP transport failed" })
|
||||||
}
|
}
|
||||||
const request = "request" in error ? error.request : undefined
|
const request = "request" in error ? error.request : undefined
|
||||||
if (error.reason._tag === "TransportError") {
|
if (error.reason._tag === "TransportError") {
|
||||||
|
|||||||
@@ -23,11 +23,4 @@ export type { ApiKeyMode, AuthOverride, ProviderAuthOption } from "./auth-option
|
|||||||
export type { Definition as EndpointFn, EndpointInput } from "./endpoint"
|
export type { Definition as EndpointFn, EndpointInput } from "./endpoint"
|
||||||
export type { Definition as FramingDef } from "./framing"
|
export type { Definition as FramingDef } from "./framing"
|
||||||
export type { Protocol as ProtocolDef } from "./protocol"
|
export type { Protocol as ProtocolDef } from "./protocol"
|
||||||
export type {
|
export type { HttpHandler, HttpMiddleware, Transport as TransportDef, TransportRuntime } from "./transport"
|
||||||
HttpHandler,
|
|
||||||
HttpMiddleware,
|
|
||||||
HttpRequest,
|
|
||||||
HttpRequestTransform,
|
|
||||||
Transport as TransportDef,
|
|
||||||
TransportRuntime,
|
|
||||||
} from "./transport"
|
|
||||||
|
|||||||
@@ -75,12 +75,10 @@ export const httpJson = <Body, Frame>(input: HttpJsonInput<Body, Frame>): HttpJs
|
|||||||
prepare: (prepareInput) =>
|
prepare: (prepareInput) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const parts = yield* jsonRequestParts({ ...prepareInput })
|
const parts = yield* jsonRequestParts({ ...prepareInput })
|
||||||
const transformed = { url: parts.url, method: "POST", headers: { ...parts.headers }, body: parts.bodyText }
|
|
||||||
yield* prepareInput.transform?.(transformed) ?? Effect.void
|
|
||||||
const request = ProviderShared.jsonPost({
|
const request = ProviderShared.jsonPost({
|
||||||
url: transformed.url,
|
url: parts.url,
|
||||||
body: transformed.body ?? "",
|
body: parts.bodyText,
|
||||||
headers: Headers.fromInput(transformed.headers),
|
headers: parts.headers,
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
request,
|
request,
|
||||||
|
|||||||
@@ -10,15 +10,6 @@ export interface TransportRuntime {
|
|||||||
readonly webSocket?: WebSocketExecutorInterface
|
readonly webSocket?: WebSocketExecutorInterface
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HttpRequest {
|
|
||||||
url: string
|
|
||||||
readonly method: string
|
|
||||||
headers: Record<string, string>
|
|
||||||
body: string | undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
export type HttpRequestTransform = (request: HttpRequest) => Effect.Effect<void>
|
|
||||||
|
|
||||||
export interface Transport<Body, Prepared, Frame> {
|
export interface Transport<Body, Prepared, Frame> {
|
||||||
readonly id: string
|
readonly id: string
|
||||||
readonly prepare: (input: TransportPrepareInput<Body>) => Effect.Effect<Prepared, AIError>
|
readonly prepare: (input: TransportPrepareInput<Body>) => Effect.Effect<Prepared, AIError>
|
||||||
@@ -32,7 +23,6 @@ export interface TransportPrepareInput<Body> {
|
|||||||
readonly auth: Auth.Definition
|
readonly auth: Auth.Definition
|
||||||
readonly encodeBody: (body: Body) => string
|
readonly encodeBody: (body: Body) => string
|
||||||
readonly headers?: (input: { readonly request: LLMRequest }) => Record<string, string>
|
readonly headers?: (input: { readonly request: LLMRequest }) => Record<string, string>
|
||||||
readonly transform?: HttpRequestTransform
|
|
||||||
readonly middleware?: HttpMiddleware
|
readonly middleware?: HttpMiddleware
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,18 @@ const expectAIError = (error: unknown) => {
|
|||||||
const errorHttp = (error: AIError) => ("http" in error.reason ? error.reason.http : undefined)
|
const errorHttp = (error: AIError) => ("http" in error.reason ? error.reason.http : undefined)
|
||||||
|
|
||||||
describe("RequestExecutor", () => {
|
describe("RequestExecutor", () => {
|
||||||
|
it.effect("preserves middleware error messages", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const executor = yield* RequestExecutor.Service
|
||||||
|
const error = yield* executor
|
||||||
|
.execute(request, () => Effect.fail(new Error("plugin rejected request")))
|
||||||
|
.pipe(Effect.flip)
|
||||||
|
|
||||||
|
expectAIError(error)
|
||||||
|
expect(error.reason.message).toBe("plugin rejected request")
|
||||||
|
}).pipe(Effect.provide(responsesLayer([]))),
|
||||||
|
)
|
||||||
|
|
||||||
it.effect("classifies context overflow responses", () =>
|
it.effect("classifies context overflow responses", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const executor = yield* RequestExecutor.Service
|
const executor = yield* RequestExecutor.Service
|
||||||
|
|||||||
@@ -1502,18 +1502,25 @@ export interface ProjectCopyApi<E = never> {
|
|||||||
export type Endpoint25_0Input = {
|
export type Endpoint25_0Input = {
|
||||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||||
}
|
}
|
||||||
export type Endpoint25_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Vcs.FileStatus> }
|
export type Endpoint25_0Output = { readonly location: Location.Info; readonly data: Vcs.Info }
|
||||||
export type VcsStatusOperation<E = never> = (input?: Endpoint25_0Input) => Effect.Effect<Endpoint25_0Output, E>
|
export type VcsGetOperation<E = never> = (input?: Endpoint25_0Input) => Effect.Effect<Endpoint25_0Output, E>
|
||||||
|
|
||||||
export type Endpoint25_1Input = {
|
export type Endpoint25_1Input = {
|
||||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||||
|
}
|
||||||
|
export type Endpoint25_1Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Vcs.FileStatus> }
|
||||||
|
export type VcsStatusOperation<E = never> = (input?: Endpoint25_1Input) => Effect.Effect<Endpoint25_1Output, E>
|
||||||
|
|
||||||
|
export type Endpoint25_2Input = {
|
||||||
|
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||||
readonly mode: Vcs.Mode
|
readonly mode: Vcs.Mode
|
||||||
readonly context?: number | undefined
|
readonly context?: number | undefined
|
||||||
}
|
}
|
||||||
export type Endpoint25_1Output = { readonly location: Location.Info; readonly data: ReadonlyArray<FileDiff.Info> }
|
export type Endpoint25_2Output = { readonly location: Location.Info; readonly data: ReadonlyArray<FileDiff.Info> }
|
||||||
export type VcsDiffOperation<E = never> = (input: Endpoint25_1Input) => Effect.Effect<Endpoint25_1Output, E>
|
export type VcsDiffOperation<E = never> = (input: Endpoint25_2Input) => Effect.Effect<Endpoint25_2Output, E>
|
||||||
|
|
||||||
export interface VcsApi<E = never> {
|
export interface VcsApi<E = never> {
|
||||||
|
readonly get: VcsGetOperation<E>
|
||||||
readonly status: VcsStatusOperation<E>
|
readonly status: VcsStatusOperation<E>
|
||||||
readonly diff: VcsDiffOperation<E>
|
readonly diff: VcsDiffOperation<E>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -210,6 +210,8 @@ import type {
|
|||||||
Endpoint25_0Output,
|
Endpoint25_0Output,
|
||||||
Endpoint25_1Input,
|
Endpoint25_1Input,
|
||||||
Endpoint25_1Output,
|
Endpoint25_1Output,
|
||||||
|
Endpoint25_2Input,
|
||||||
|
Endpoint25_2Output,
|
||||||
Endpoint26_0Output,
|
Endpoint26_0Output,
|
||||||
Endpoint26_1Input,
|
Endpoint26_1Input,
|
||||||
Endpoint26_1Output,
|
Endpoint26_1Output,
|
||||||
@@ -1182,17 +1184,26 @@ const adaptGroup24 = (raw: RawClient["server.projectCopy"]) => ({
|
|||||||
|
|
||||||
const Endpoint25_0 = (raw: RawClient["server.vcs"]) => (input?: Endpoint25_0Input) =>
|
const Endpoint25_0 = (raw: RawClient["server.vcs"]) => (input?: Endpoint25_0Input) =>
|
||||||
preserveEffect<Endpoint25_0Output>()(
|
preserveEffect<Endpoint25_0Output>()(
|
||||||
|
raw["vcs.get"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)),
|
||||||
|
)
|
||||||
|
|
||||||
|
const Endpoint25_1 = (raw: RawClient["server.vcs"]) => (input?: Endpoint25_1Input) =>
|
||||||
|
preserveEffect<Endpoint25_1Output>()(
|
||||||
raw["vcs.status"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)),
|
raw["vcs.status"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)),
|
||||||
)
|
)
|
||||||
|
|
||||||
const Endpoint25_1 = (raw: RawClient["server.vcs"]) => (input: Endpoint25_1Input) =>
|
const Endpoint25_2 = (raw: RawClient["server.vcs"]) => (input: Endpoint25_2Input) =>
|
||||||
preserveEffect<Endpoint25_1Output>()(
|
preserveEffect<Endpoint25_2Output>()(
|
||||||
raw["vcs.diff"]({ query: { location: input["location"], mode: input["mode"], context: input["context"] } }).pipe(
|
raw["vcs.diff"]({ query: { location: input["location"], mode: input["mode"], context: input["context"] } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const adaptGroup25 = (raw: RawClient["server.vcs"]) => ({ status: Endpoint25_0(raw), diff: Endpoint25_1(raw) })
|
const adaptGroup25 = (raw: RawClient["server.vcs"]) => ({
|
||||||
|
get: Endpoint25_0(raw),
|
||||||
|
status: Endpoint25_1(raw),
|
||||||
|
diff: Endpoint25_2(raw),
|
||||||
|
})
|
||||||
|
|
||||||
const Endpoint26_0 = (raw: RawClient["server.debug"]) => () =>
|
const Endpoint26_0 = (raw: RawClient["server.debug"]) => () =>
|
||||||
preserveEffect<Endpoint26_0Output>()(raw["debug.location"]({}).pipe(Effect.mapError(mapClientError)))
|
preserveEffect<Endpoint26_0Output>()(raw["debug.location"]({}).pipe(Effect.mapError(mapClientError)))
|
||||||
|
|||||||
@@ -202,6 +202,8 @@ import type {
|
|||||||
ProjectCopyRemoveOutput,
|
ProjectCopyRemoveOutput,
|
||||||
ProjectCopyRefreshInput,
|
ProjectCopyRefreshInput,
|
||||||
ProjectCopyRefreshOutput,
|
ProjectCopyRefreshOutput,
|
||||||
|
VcsGetInput,
|
||||||
|
VcsGetOutput,
|
||||||
VcsStatusInput,
|
VcsStatusInput,
|
||||||
VcsStatusOutput,
|
VcsStatusOutput,
|
||||||
VcsDiffInput,
|
VcsDiffInput,
|
||||||
@@ -1702,6 +1704,18 @@ export function make(options: ClientOptions) {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
vcs: {
|
vcs: {
|
||||||
|
get: (input?: VcsGetInput, requestOptions?: RequestOptions) =>
|
||||||
|
request<VcsGetOutput>(
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
path: `/api/vcs`,
|
||||||
|
query: { location: input?.["location"] },
|
||||||
|
successStatus: 200,
|
||||||
|
declaredStatuses: [401, 400],
|
||||||
|
empty: false,
|
||||||
|
},
|
||||||
|
requestOptions,
|
||||||
|
),
|
||||||
status: (input?: VcsStatusInput, requestOptions?: RequestOptions) =>
|
status: (input?: VcsStatusInput, requestOptions?: RequestOptions) =>
|
||||||
request<VcsStatusOutput>(
|
request<VcsStatusOutput>(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -530,6 +530,8 @@ export type ReferenceGitSource = {
|
|||||||
|
|
||||||
export type ProjectCopyCopy = { directory: string }
|
export type ProjectCopyCopy = { directory: string }
|
||||||
|
|
||||||
|
export type VcsBranch = { current?: string; default?: string }
|
||||||
|
|
||||||
export type VcsFileStatus = {
|
export type VcsFileStatus = {
|
||||||
file: string
|
file: string
|
||||||
additions: number
|
additions: number
|
||||||
@@ -1745,6 +1747,8 @@ export type SessionStatus2 = {
|
|||||||
|
|
||||||
export type ReferenceSource = ReferenceLocalSource | ReferenceGitSource
|
export type ReferenceSource = ReferenceLocalSource | ReferenceGitSource
|
||||||
|
|
||||||
|
export type VcsInfo = { branch: VcsBranch }
|
||||||
|
|
||||||
export type PermissionRuleset = Array<PermissionRule>
|
export type PermissionRuleset = Array<PermissionRule>
|
||||||
|
|
||||||
export type SessionInfo = {
|
export type SessionInfo = {
|
||||||
@@ -4902,6 +4906,17 @@ export type ProjectCopyRefreshInput = {
|
|||||||
|
|
||||||
export type ProjectCopyRefreshOutput = void
|
export type ProjectCopyRefreshOutput = void
|
||||||
|
|
||||||
|
export type VcsGetInput = {
|
||||||
|
readonly location?: {
|
||||||
|
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||||
|
}["location"]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type VcsGetOutput = {
|
||||||
|
location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } }
|
||||||
|
data: VcsInfo
|
||||||
|
}
|
||||||
|
|
||||||
export type VcsStatusInput = {
|
export type VcsStatusInput = {
|
||||||
readonly location?: {
|
readonly location?: {
|
||||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ test("exposes every standard HTTP API group", () => {
|
|||||||
expect(Object.keys(client.integration.command)).toEqual(["connect", "status", "cancel"])
|
expect(Object.keys(client.integration.command)).toEqual(["connect", "status", "cancel"])
|
||||||
expect(Object.keys(client.websearch)).toEqual(["providers", "query"])
|
expect(Object.keys(client.websearch)).toEqual(["providers", "query"])
|
||||||
expect(Object.keys(client.file)).toEqual(["read", "list", "find"])
|
expect(Object.keys(client.file)).toEqual(["read", "list", "find"])
|
||||||
expect(Object.keys(client.vcs)).toEqual(["status", "diff"])
|
expect(Object.keys(client.vcs)).toEqual(["get", "status", "diff"])
|
||||||
expect(Object.keys(client.pty)).toEqual(["list", "create", "get", "update", "remove"])
|
expect(Object.keys(client.pty)).toEqual(["list", "create", "get", "update", "remove"])
|
||||||
expect(Object.keys(client.shell)).toEqual(["list", "create", "get", "timeout", "output", "remove"])
|
expect(Object.keys(client.shell)).toEqual(["list", "create", "get", "timeout", "output", "remove"])
|
||||||
expect(Object.keys(client.project)).toEqual(["list", "current", "directories"])
|
expect(Object.keys(client.project)).toEqual(["list", "current", "directories"])
|
||||||
|
|||||||
@@ -12,18 +12,13 @@
|
|||||||
"@cloudflare/workers-types": "catalog:",
|
"@cloudflare/workers-types": "catalog:",
|
||||||
"@tsconfig/node22": "22.0.2",
|
"@tsconfig/node22": "22.0.2",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"openai": "5.11.0",
|
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:"
|
"@typescript/native-preview": "catalog:"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "3.0.82",
|
|
||||||
"@ai-sdk/openai": "3.0.48",
|
|
||||||
"@ai-sdk/openai-compatible": "2.0.37",
|
|
||||||
"@opencode-ai/console-core": "workspace:*",
|
"@opencode-ai/console-core": "workspace:*",
|
||||||
"@opencode-ai/console-resource": "workspace:*",
|
"@opencode-ai/console-resource": "workspace:*",
|
||||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||||
"ai": "catalog:",
|
|
||||||
"zod": "catalog:"
|
"zod": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,18 +78,15 @@
|
|||||||
"@ai-sdk/cohere": "3.0.27",
|
"@ai-sdk/cohere": "3.0.27",
|
||||||
"@ai-sdk/deepinfra": "2.0.41",
|
"@ai-sdk/deepinfra": "2.0.41",
|
||||||
"@ai-sdk/gateway": "3.0.104",
|
"@ai-sdk/gateway": "3.0.104",
|
||||||
"@ai-sdk/google": "3.0.73",
|
|
||||||
"@ai-sdk/google-vertex": "4.0.128",
|
"@ai-sdk/google-vertex": "4.0.128",
|
||||||
"@ai-sdk/groq": "3.0.31",
|
"@ai-sdk/groq": "3.0.31",
|
||||||
"@ai-sdk/mistral": "3.0.51",
|
"@ai-sdk/mistral": "3.0.51",
|
||||||
"@ai-sdk/openai": "3.0.84",
|
|
||||||
"@ai-sdk/openai-compatible": "2.0.41",
|
"@ai-sdk/openai-compatible": "2.0.41",
|
||||||
"@ai-sdk/perplexity": "3.0.26",
|
"@ai-sdk/perplexity": "3.0.26",
|
||||||
"@ai-sdk/provider": "3.0.8",
|
"@ai-sdk/provider": "3.0.8",
|
||||||
"@ai-sdk/provider-utils": "4.0.23",
|
"@ai-sdk/provider-utils": "4.0.23",
|
||||||
"@ai-sdk/togetherai": "2.0.41",
|
"@ai-sdk/togetherai": "2.0.41",
|
||||||
"@ai-sdk/vercel": "2.0.39",
|
"@ai-sdk/vercel": "2.0.39",
|
||||||
"@ai-sdk/xai": "3.0.102",
|
|
||||||
"@aws-sdk/credential-providers": "3.1057.0",
|
"@aws-sdk/credential-providers": "3.1057.0",
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
"@effect/sql-sqlite-bun": "catalog:",
|
"@effect/sql-sqlite-bun": "catalog:",
|
||||||
@@ -106,7 +103,6 @@
|
|||||||
"@standard-schema/spec": "catalog:",
|
"@standard-schema/spec": "catalog:",
|
||||||
"@parcel/watcher": "2.5.1",
|
"@parcel/watcher": "2.5.1",
|
||||||
"@silvia-odwyer/photon-node": "0.3.4",
|
"@silvia-odwyer/photon-node": "0.3.4",
|
||||||
"@openrouter/ai-sdk-provider": "2.9.0",
|
|
||||||
"ai-gateway-provider": "3.1.2",
|
"ai-gateway-provider": "3.1.2",
|
||||||
"bun-pty": "0.4.8",
|
"bun-pty": "0.4.8",
|
||||||
"diff": "catalog:",
|
"diff": "catalog:",
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
export * as Agent from "./agent"
|
export * as Agent from "./agent"
|
||||||
|
|
||||||
|
import path from "path"
|
||||||
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||||
import { Array, Context, Effect, Layer, Types } from "effect"
|
import { Array, Context, Effect, Layer, Types } from "effect"
|
||||||
import { Agent } from "@opencode-ai/schema/agent"
|
import { Agent } from "@opencode-ai/schema/agent"
|
||||||
|
import { Global } from "@opencode-ai/util/global"
|
||||||
import { Bus } from "./bus"
|
import { Bus } from "./bus"
|
||||||
import { State } from "./state"
|
import { State } from "./state"
|
||||||
|
|
||||||
|
const SHELL_OUTPUT_GLOB = (data: string) => path.join(data, "shell", "*", "*")
|
||||||
|
const TOOL_OUTPUT_GLOB = (data: string) => path.join(data, "tool-output", "*")
|
||||||
|
|
||||||
export const ID = Agent.ID
|
export const ID = Agent.ID
|
||||||
export type ID = typeof ID.Type
|
export type ID = typeof ID.Type
|
||||||
export const Name = Agent.Name
|
export const Name = Agent.Name
|
||||||
@@ -51,6 +56,13 @@ const layer = Layer.effect(
|
|||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const bus = yield* Bus.Service
|
const bus = yield* Bus.Service
|
||||||
|
const global = yield* Global.Service
|
||||||
|
const permissions: Info["permissions"] = [
|
||||||
|
{ action: "external_directory", resource: SHELL_OUTPUT_GLOB(global.data), effect: "allow" },
|
||||||
|
{ action: "external_directory", resource: TOOL_OUTPUT_GLOB(global.data), effect: "allow" },
|
||||||
|
{ action: "external_directory", resource: path.join(global.tmp, "*"), effect: "allow" },
|
||||||
|
{ action: "external_directory", resource: path.join(global.config, "*"), effect: "allow" },
|
||||||
|
]
|
||||||
const state = State.create<Data, Draft>({
|
const state = State.create<Data, Draft>({
|
||||||
name: "agent",
|
name: "agent",
|
||||||
initial: () => ({ agents: new Map() }),
|
initial: () => ({ agents: new Map() }),
|
||||||
@@ -61,7 +73,13 @@ const layer = Layer.effect(
|
|||||||
draft.default = id
|
draft.default = id
|
||||||
},
|
},
|
||||||
update: (id, fn) => {
|
update: (id, fn) => {
|
||||||
const current = draft.agents.get(id) ?? (Info.default(id) as Types.DeepMutable<Info>)
|
const defaults = Info.default(id)
|
||||||
|
const current =
|
||||||
|
draft.agents.get(id) ??
|
||||||
|
({
|
||||||
|
...defaults,
|
||||||
|
permissions: [...defaults.permissions, ...permissions],
|
||||||
|
} as Types.DeepMutable<Info>)
|
||||||
if (!draft.agents.has(id)) draft.agents.set(id, current)
|
if (!draft.agents.has(id)) draft.agents.set(id, current)
|
||||||
fn(current)
|
fn(current)
|
||||||
current.id = id
|
current.id = id
|
||||||
@@ -114,4 +132,4 @@ const layer = Layer.effect(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const node = makeLocationNode({ service: Service, layer, deps: [Bus.node] })
|
export const node = makeLocationNode({ service: Service, layer, deps: [Bus.node, Global.node] })
|
||||||
|
|||||||
@@ -413,7 +413,7 @@ function mapBodyToProviderOptions(model: Info, packageName: string) {
|
|||||||
function callOptions(request: LLMRequest): LanguageModelV3CallOptions {
|
function callOptions(request: LLMRequest): LanguageModelV3CallOptions {
|
||||||
return {
|
return {
|
||||||
prompt: prompt(request),
|
prompt: prompt(request),
|
||||||
maxOutputTokens: request.generation?.maxTokens ?? request.model.route.defaults.limits?.output,
|
maxOutputTokens: request.generation?.maxTokens,
|
||||||
temperature: request.generation?.temperature,
|
temperature: request.generation?.temperature,
|
||||||
stopSequences: request.generation?.stop === undefined ? undefined : [...request.generation.stop],
|
stopSequences: request.generation?.stop === undefined ? undefined : [...request.generation.stop],
|
||||||
topP: request.generation?.topP,
|
topP: request.generation?.topP,
|
||||||
|
|||||||
@@ -71,6 +71,14 @@ const layer = Layer.effect(
|
|||||||
yield* updates.pipe(Stream.runForEach(publish), Effect.forkScoped)
|
yield* updates.pipe(Stream.runForEach(publish), Effect.forkScoped)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (location.vcs?.type === "hg") {
|
||||||
|
const store = location.vcs.store
|
||||||
|
const vcs = yield* fs.realPath(store).pipe(Effect.catch(() => Effect.succeed(store)))
|
||||||
|
if (!config.includes(".hg") && !config.includes(vcs)) {
|
||||||
|
const updates = yield* watcher.subscribe({ path: path.join(vcs, "branch"), type: "file" })
|
||||||
|
yield* updates.pipe(Stream.runForEach(publish), Effect.forkScoped)
|
||||||
|
}
|
||||||
|
}
|
||||||
}).pipe(
|
}).pipe(
|
||||||
Effect.withSpan("LocationWatcher.start", { attributes: { directory: location.directory } }),
|
Effect.withSpan("LocationWatcher.start", { attributes: { directory: location.directory } }),
|
||||||
Effect.catchCause((cause) => Effect.logError("failed to init location watcher service", { cause })),
|
Effect.catchCause((cause) => Effect.logError("failed to init location watcher service", { cause })),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export * as InstructionBuiltIns from "./builtins"
|
|||||||
|
|
||||||
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||||
import { Context, DateTime, Effect, Layer, Schema } from "effect"
|
import { Context, DateTime, Effect, Layer, Schema } from "effect"
|
||||||
|
import { Global } from "@opencode-ai/util/global"
|
||||||
import { Location } from "../location"
|
import { Location } from "../location"
|
||||||
import { SessionSchema } from "../session/schema"
|
import { SessionSchema } from "../session/schema"
|
||||||
import { Instructions } from "./index"
|
import { Instructions } from "./index"
|
||||||
@@ -15,6 +16,7 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/In
|
|||||||
const layer = Layer.effect(
|
const layer = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
const global = yield* Global.Service
|
||||||
const location = yield* Location.Service
|
const location = yield* Location.Service
|
||||||
return Service.of({
|
return Service.of({
|
||||||
load: (sessionID) =>
|
load: (sessionID) =>
|
||||||
@@ -31,6 +33,7 @@ const layer = Layer.effect(
|
|||||||
` Workspace root folder: ${location.project.directory}`,
|
` Workspace root folder: ${location.project.directory}`,
|
||||||
` Is directory a git repo: ${location.vcs?.type === "git" ? "yes" : "no"}`,
|
` Is directory a git repo: ${location.vcs?.type === "git" ? "yes" : "no"}`,
|
||||||
` Platform: ${process.platform}`,
|
` Platform: ${process.platform}`,
|
||||||
|
` Use ${global.tmp} for temporary work outside the workspace; it already exists and is pre-approved for external directory access.`,
|
||||||
"</env>",
|
"</env>",
|
||||||
].join("\n"),
|
].join("\n"),
|
||||||
),
|
),
|
||||||
@@ -58,4 +61,4 @@ const layer = Layer.effect(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const node = makeLocationNode({ service: Service, layer, deps: [Location.node] })
|
export const node = makeLocationNode({ service: Service, layer, deps: [Global.node, Location.node] })
|
||||||
|
|||||||
@@ -1,17 +1,10 @@
|
|||||||
export * as AgentPlugin from "./agent"
|
export * as AgentPlugin from "./agent"
|
||||||
|
|
||||||
import path from "path"
|
|
||||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { Agent } from "../agent"
|
import { Agent } from "../agent"
|
||||||
import { Global } from "@opencode-ai/util/global"
|
|
||||||
import { Location } from "../location"
|
|
||||||
import { Permission } from "../permission"
|
import { Permission } from "../permission"
|
||||||
|
|
||||||
// Combined output files written by the Shell service, e.g. `<data>/shell/<projectID>/<shellID>.out`.
|
|
||||||
// Whitelisted so agents can read a command's full captured output without an external-directory prompt.
|
|
||||||
const SHELL_OUTPUT_GLOB = path.join(Global.Path.data, "shell", "*", "*")
|
|
||||||
|
|
||||||
const PROMPT_EXPLORE = `You are a file search specialist. You excel at thoroughly navigating and exploring codebases.
|
const PROMPT_EXPLORE = `You are a file search specialist. You excel at thoroughly navigating and exploring codebases.
|
||||||
|
|
||||||
Your strengths:
|
Your strengths:
|
||||||
@@ -100,38 +93,12 @@ Rules:
|
|||||||
export const Plugin = define({
|
export const Plugin = define({
|
||||||
id: "opencode.agent",
|
id: "opencode.agent",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
const location = yield* Location.Service
|
|
||||||
const worktree = location.directory
|
|
||||||
const whitelistedDirs = [SHELL_OUTPUT_GLOB, path.join(Global.Path.tmp, "*")]
|
|
||||||
const readonlyExternalDirectory: Permission.Ruleset = [
|
|
||||||
{ action: "external_directory", resource: "*", effect: "ask" },
|
|
||||||
...whitelistedDirs.map(
|
|
||||||
(resource): Permission.Rule => ({ action: "external_directory", resource, effect: "allow" }),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
const defaults: Permission.Ruleset = [
|
|
||||||
{ action: "*", resource: "*", effect: "allow" },
|
|
||||||
...readonlyExternalDirectory,
|
|
||||||
{ action: "question", resource: "*", effect: "deny" },
|
|
||||||
{ action: "plan_enter", resource: "*", effect: "deny" },
|
|
||||||
{ action: "plan_exit", resource: "*", effect: "deny" },
|
|
||||||
{ action: "read", resource: "*", effect: "allow" },
|
|
||||||
{ action: "read", resource: "*.env", effect: "ask" },
|
|
||||||
{ action: "read", resource: "*.env.*", effect: "ask" },
|
|
||||||
{ action: "read", resource: "*.env.example", effect: "allow" },
|
|
||||||
]
|
|
||||||
|
|
||||||
yield* ctx.agent.transform((draft) => {
|
yield* ctx.agent.transform((draft) => {
|
||||||
draft.update(Agent.defaultID, (item) => {
|
draft.update(Agent.defaultID, (item) => {
|
||||||
item.name = Agent.Name.make("Build")
|
item.name = Agent.Name.make("Build")
|
||||||
item.description = "The default agent. Executes tools based on configured permissions."
|
item.description = "The default agent. Executes tools based on configured permissions."
|
||||||
item.mode = "primary"
|
item.mode = "primary"
|
||||||
item.permissions.push(
|
item.permissions.push({ action: "question", resource: "*", effect: "allow" })
|
||||||
...Permission.merge(defaults, [
|
|
||||||
{ action: "question", resource: "*", effect: "allow" },
|
|
||||||
{ action: "plan_enter", resource: "*", effect: "allow" },
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
draft.update(Agent.ID.make("plan"), (item) => {
|
draft.update(Agent.ID.make("plan"), (item) => {
|
||||||
@@ -139,18 +106,8 @@ export const Plugin = define({
|
|||||||
item.description = "Plan mode. Disallows all edit tools."
|
item.description = "Plan mode. Disallows all edit tools."
|
||||||
item.mode = "primary"
|
item.mode = "primary"
|
||||||
item.permissions.push(
|
item.permissions.push(
|
||||||
...Permission.merge(defaults, [
|
|
||||||
{ action: "question", resource: "*", effect: "allow" },
|
{ action: "question", resource: "*", effect: "allow" },
|
||||||
{ action: "plan_exit", resource: "*", effect: "allow" },
|
|
||||||
{ action: "external_directory", resource: path.join(Global.Path.data, "plans", "*"), effect: "allow" },
|
|
||||||
{ action: "edit", resource: "*", effect: "deny" },
|
{ action: "edit", resource: "*", effect: "deny" },
|
||||||
{ action: "edit", resource: path.join(".opencode", "plans", "*.md"), effect: "allow" },
|
|
||||||
{
|
|
||||||
action: "edit",
|
|
||||||
resource: path.relative(worktree, path.join(Global.Path.data, "plans", "*.md")),
|
|
||||||
effect: "allow",
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -159,10 +116,16 @@ export const Plugin = define({
|
|||||||
item.description =
|
item.description =
|
||||||
"General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel."
|
"General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel."
|
||||||
item.mode = "subagent"
|
item.mode = "subagent"
|
||||||
item.permissions.push(...Permission.merge(defaults, [{ action: "subagent", resource: "*", effect: "deny" }]))
|
item.permissions.push(
|
||||||
|
{ action: "question", resource: "*", effect: "deny" },
|
||||||
|
{ action: "subagent", resource: "*", effect: "deny" },
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
draft.update(Agent.ID.make("explore"), (item) => {
|
draft.update(Agent.ID.make("explore"), (item) => {
|
||||||
|
const externalDirectories = item.permissions.filter(
|
||||||
|
(rule) => rule.action === "external_directory" && rule.effect === "allow",
|
||||||
|
)
|
||||||
item.name = Agent.Name.make("Explore")
|
item.name = Agent.Name.make("Explore")
|
||||||
item.description =
|
item.description =
|
||||||
'Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.'
|
'Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.'
|
||||||
@@ -170,7 +133,6 @@ export const Plugin = define({
|
|||||||
item.mode = "subagent"
|
item.mode = "subagent"
|
||||||
item.permissions.push(
|
item.permissions.push(
|
||||||
...Permission.merge(
|
...Permission.merge(
|
||||||
defaults,
|
|
||||||
[
|
[
|
||||||
{ action: "*", resource: "*", effect: "deny" },
|
{ action: "*", resource: "*", effect: "deny" },
|
||||||
{ action: "grep", resource: "*", effect: "allow" },
|
{ action: "grep", resource: "*", effect: "allow" },
|
||||||
@@ -178,9 +140,12 @@ export const Plugin = define({
|
|||||||
{ action: "webfetch", resource: "*", effect: "allow" },
|
{ action: "webfetch", resource: "*", effect: "allow" },
|
||||||
{ action: "websearch", resource: "*", effect: "allow" },
|
{ action: "websearch", resource: "*", effect: "allow" },
|
||||||
{ action: "read", resource: "*", effect: "allow" },
|
{ action: "read", resource: "*", effect: "allow" },
|
||||||
|
{ action: "read", resource: "*.env", effect: "ask" },
|
||||||
|
{ action: "read", resource: "*.env.*", effect: "ask" },
|
||||||
|
{ action: "read", resource: "*.env.example", effect: "allow" },
|
||||||
{ action: "subagent", resource: "*", effect: "deny" },
|
{ action: "subagent", resource: "*", effect: "deny" },
|
||||||
],
|
],
|
||||||
readonlyExternalDirectory,
|
[{ action: "external_directory", resource: "*", effect: "ask" }, ...externalDirectories],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -190,7 +155,7 @@ export const Plugin = define({
|
|||||||
item.mode = "primary"
|
item.mode = "primary"
|
||||||
item.hidden = true
|
item.hidden = true
|
||||||
item.system = PROMPT_COMPACTION
|
item.system = PROMPT_COMPACTION
|
||||||
item.permissions.push(...Permission.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }]))
|
item.permissions.push({ action: "*", resource: "*", effect: "deny" })
|
||||||
})
|
})
|
||||||
|
|
||||||
draft.update(Agent.ID.make("title"), (item) => {
|
draft.update(Agent.ID.make("title"), (item) => {
|
||||||
@@ -198,7 +163,7 @@ export const Plugin = define({
|
|||||||
item.mode = "primary"
|
item.mode = "primary"
|
||||||
item.hidden = true
|
item.hidden = true
|
||||||
item.system = PROMPT_TITLE
|
item.system = PROMPT_TITLE
|
||||||
item.permissions.push(...Permission.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }]))
|
item.permissions.push({ action: "*", resource: "*", effect: "deny" })
|
||||||
})
|
})
|
||||||
|
|
||||||
draft.update(Agent.ID.make("summary"), (item) => {
|
draft.update(Agent.ID.make("summary"), (item) => {
|
||||||
@@ -206,7 +171,7 @@ export const Plugin = define({
|
|||||||
item.mode = "primary"
|
item.mode = "primary"
|
||||||
item.hidden = true
|
item.hidden = true
|
||||||
item.system = PROMPT_SUMMARY
|
item.system = PROMPT_SUMMARY
|
||||||
item.permissions.push(...Permission.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }]))
|
item.permissions.push({ action: "*", resource: "*", effect: "deny" })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export * as PluginPromise from "./promise"
|
|||||||
|
|
||||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||||
import type { Context, Plugin } from "@opencode-ai/plugin/promise/plugin"
|
import type { Context, Plugin } from "@opencode-ai/plugin/promise/plugin"
|
||||||
|
import type { SessionHooks, SessionHttp, SessionHttpMiddleware } from "@opencode-ai/plugin/promise/session"
|
||||||
import type { Info } from "@opencode-ai/plugin/promise/tool"
|
import type { Info } from "@opencode-ai/plugin/promise/tool"
|
||||||
import { Agent } from "@opencode-ai/schema/agent"
|
import { Agent } from "@opencode-ai/schema/agent"
|
||||||
import { Integration } from "@opencode-ai/schema/integration"
|
import { Integration } from "@opencode-ai/schema/integration"
|
||||||
@@ -57,6 +58,62 @@ export function fromPromise(plugin: Plugin) {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
function sessionHook<Name extends keyof SessionHooks>(
|
||||||
|
name: Name,
|
||||||
|
callback: (event: SessionHooks[Name]) => Promise<void> | void,
|
||||||
|
): Promise<Registration>
|
||||||
|
function sessionHook(
|
||||||
|
...registration: {
|
||||||
|
[Name in keyof SessionHooks]: [
|
||||||
|
name: Name,
|
||||||
|
callback: (event: SessionHooks[Name]) => Promise<void> | void,
|
||||||
|
]
|
||||||
|
}[keyof SessionHooks]
|
||||||
|
) {
|
||||||
|
if (registration[0] !== "http")
|
||||||
|
return register(
|
||||||
|
host.session.hook(registration[0], (event) =>
|
||||||
|
Effect.promise(() => Promise.resolve(registration[1](event))),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return register(
|
||||||
|
host.session.hook("http", (event) => {
|
||||||
|
const middlewares: SessionHttpMiddleware[] = []
|
||||||
|
const output: SessionHttp = {
|
||||||
|
...event,
|
||||||
|
use: (item) => {
|
||||||
|
middlewares.push(item)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return Effect.promise(() => Promise.resolve(registration[1](output))).pipe(
|
||||||
|
Effect.flatMap(() =>
|
||||||
|
Effect.forEach(
|
||||||
|
middlewares,
|
||||||
|
(item) =>
|
||||||
|
event.use((input, next) =>
|
||||||
|
Effect.tryPromise({
|
||||||
|
try: (signal) => {
|
||||||
|
const inputSignal = AbortSignal.any([signal, input.signal])
|
||||||
|
return Promise.resolve(
|
||||||
|
item(new Request(input, { signal: inputSignal }), (request) => {
|
||||||
|
const requestSignal = AbortSignal.any([signal, request.signal])
|
||||||
|
return Effect.runPromiseWith(
|
||||||
|
context,
|
||||||
|
)(next(new Request(request, { signal: requestSignal })), { signal: requestSignal })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
{ discard: true },
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const context2: Context = {
|
const context2: Context = {
|
||||||
app: host.app,
|
app: host.app,
|
||||||
options: host.options,
|
options: host.options,
|
||||||
@@ -194,7 +251,9 @@ export function fromPromise(plugin: Plugin) {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
refresh:
|
refresh:
|
||||||
refresh === undefined ? undefined : (credential) => Effect.promise(() => refresh(credential)),
|
refresh === undefined
|
||||||
|
? undefined
|
||||||
|
: (credential) => Effect.promise(() => refresh(credential)),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
remove: draft.method.remove,
|
remove: draft.method.remove,
|
||||||
@@ -263,8 +322,7 @@ export function fromPromise(plugin: Plugin) {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
session: {
|
session: {
|
||||||
hook: (name, callback) =>
|
hook: sessionHook,
|
||||||
register(host.session.hook(name, (event) => Effect.promise(() => Promise.resolve(callback(event))))),
|
|
||||||
create: (input) =>
|
create: (input) =>
|
||||||
run(
|
run(
|
||||||
host.session.create(
|
host.session.create(
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { DynamicProviderPlugin } from "./provider/dynamic"
|
|||||||
import { GatewayPlugin } from "./provider/gateway"
|
import { GatewayPlugin } from "./provider/gateway"
|
||||||
import { GithubCopilotPlugin } from "./provider/github-copilot"
|
import { GithubCopilotPlugin } from "./provider/github-copilot"
|
||||||
import { GitLabPlugin } from "./provider/gitlab"
|
import { GitLabPlugin } from "./provider/gitlab"
|
||||||
import { GooglePlugin } from "./provider/google"
|
|
||||||
import { GoogleVertexAnthropicPlugin, GoogleVertexPlugin } from "./provider/google-vertex"
|
import { GoogleVertexAnthropicPlugin, GoogleVertexPlugin } from "./provider/google-vertex"
|
||||||
import { GroqPlugin } from "./provider/groq"
|
import { GroqPlugin } from "./provider/groq"
|
||||||
import { KiloPlugin } from "./provider/kilo"
|
import { KiloPlugin } from "./provider/kilo"
|
||||||
@@ -46,7 +45,6 @@ export const ProviderPlugins: PluginInternal.InternalPlugin[] = [
|
|||||||
GatewayPlugin,
|
GatewayPlugin,
|
||||||
GithubCopilotPlugin,
|
GithubCopilotPlugin,
|
||||||
GitLabPlugin,
|
GitLabPlugin,
|
||||||
GooglePlugin,
|
|
||||||
GoogleVertexAnthropicPlugin,
|
GoogleVertexAnthropicPlugin,
|
||||||
GoogleVertexPlugin,
|
GoogleVertexPlugin,
|
||||||
GroqPlugin,
|
GroqPlugin,
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
import { Effect } from "effect"
|
|
||||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
|
||||||
|
|
||||||
export const GooglePlugin = define({
|
|
||||||
id: "opencode.provider.google",
|
|
||||||
effect: Effect.fn(function* (ctx) {
|
|
||||||
yield* ctx.aisdk.hook(
|
|
||||||
"sdk",
|
|
||||||
Effect.fn(function* (evt) {
|
|
||||||
if (evt.package !== "@ai-sdk/google") return
|
|
||||||
const mod = yield* Effect.promise(() => import("@ai-sdk/google"))
|
|
||||||
evt.sdk = mod.createGoogleGenerativeAI(evt.options)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
@@ -225,15 +225,14 @@ export const OpenAIPlugin = define({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
yield* ctx.session.hook("request", (evt) =>
|
yield* ctx.session.hook("http", (evt) =>
|
||||||
Effect.sync(() => {
|
evt.use((request, next) => {
|
||||||
if (!chatgpt || evt.model.providerID !== Provider.ID.openai) return
|
if (!chatgpt || evt.model.providerID !== Provider.ID.openai) return next(request)
|
||||||
const url = new URL(evt.url)
|
const url = new URL(request.url)
|
||||||
if (url.origin === "https://api.openai.com") {
|
request.headers.set("originator", "opencode")
|
||||||
evt.url = `${codexBaseURL}${url.pathname.replace(/^\/v1/, "")}${url.search}`
|
request.headers.set("session-id", evt.sessionID)
|
||||||
}
|
if (url.origin !== "https://api.openai.com") return next(request)
|
||||||
evt.headers.originator = "opencode"
|
return next(new Request(`${codexBaseURL}${url.pathname.replace(/^\/v1/, "")}${url.search}`, request))
|
||||||
evt.headers["session-id"] = evt.sessionID
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -23,13 +23,5 @@ export const OpenRouterPlugin = define({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
yield* ctx.aisdk.hook(
|
|
||||||
"sdk",
|
|
||||||
Effect.fn(function* (evt) {
|
|
||||||
if (evt.package !== "@openrouter/ai-sdk-provider") return
|
|
||||||
const mod = yield* Effect.promise(() => import("@openrouter/ai-sdk-provider"))
|
|
||||||
evt.sdk = mod.createOpenRouter(evt.options)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { App } from "../../app"
|
|||||||
import { Credential } from "../../credential"
|
import { Credential } from "../../credential"
|
||||||
import { Integration } from "../../integration"
|
import { Integration } from "../../integration"
|
||||||
import { OauthCallbackPage } from "../../oauth/page"
|
import { OauthCallbackPage } from "../../oauth/page"
|
||||||
import { Provider } from "../../provider"
|
|
||||||
|
|
||||||
const clientID = "b1a00492-073a-47ea-816f-4c329264a828"
|
const clientID = "b1a00492-073a-47ea-816f-4c329264a828"
|
||||||
const issuer = "https://auth.x.ai/oauth2"
|
const issuer = "https://auth.x.ai/oauth2"
|
||||||
@@ -162,21 +161,6 @@ export const XAIPlugin = define({
|
|||||||
draft.method.update(device(ctx.app))
|
draft.method.update(device(ctx.app))
|
||||||
draft.method.update({ integrationID: "xai", method: { type: "key", label: "Manually enter API Key" } })
|
draft.method.update({ integrationID: "xai", method: { type: "key", label: "Manually enter API Key" } })
|
||||||
})
|
})
|
||||||
yield* ctx.aisdk.hook(
|
|
||||||
"sdk",
|
|
||||||
Effect.fn(function* (evt) {
|
|
||||||
if (evt.package !== "@ai-sdk/xai") return
|
|
||||||
const mod = yield* Effect.promise(() => import("@ai-sdk/xai"))
|
|
||||||
evt.sdk = mod.createXai(evt.options)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
yield* ctx.aisdk.hook(
|
|
||||||
"language",
|
|
||||||
Effect.fn(function* (evt) {
|
|
||||||
if (evt.model.providerID !== Provider.ID.make("xai")) return
|
|
||||||
evt.language = evt.sdk.responses(evt.model.modelID ?? evt.model.id)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ export * as SessionModelRequest from "./model-request"
|
|||||||
|
|
||||||
import { LLM, Message, SystemPart, type LLMRequest } from "@opencode-ai/ai"
|
import { LLM, Message, SystemPart, type LLMRequest } from "@opencode-ai/ai"
|
||||||
import type { StreamOptions } from "@opencode-ai/ai/route"
|
import type { StreamOptions } from "@opencode-ai/ai/route"
|
||||||
|
import type { SessionHttpHandler, SessionHttpMiddleware } from "@opencode-ai/plugin/effect/session"
|
||||||
import type { Content } from "@opencode-ai/schema/tool"
|
import type { Content } from "@opencode-ai/schema/tool"
|
||||||
import { SessionError } from "@opencode-ai/schema/session-error"
|
import { SessionError } from "@opencode-ai/schema/session-error"
|
||||||
import { Cause, Config, Context, Effect, Layer, Result } from "effect"
|
import { Cause, Config, Context, Effect, Layer, Result, Stream } from "effect"
|
||||||
|
import { HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
|
||||||
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||||
import { App } from "../app"
|
import { App } from "../app"
|
||||||
import { Model } from "../model"
|
import { Model } from "../model"
|
||||||
@@ -48,9 +50,7 @@ interface Prepared {
|
|||||||
* One request-scoped execution operation. Unknown, hook-removed, and
|
* One request-scoped execution operation. Unknown, hook-removed, and
|
||||||
* step-limit-violating calls fail individually through the same seam.
|
* step-limit-violating calls fail individually through the same seam.
|
||||||
*/
|
*/
|
||||||
readonly executeTool: (
|
readonly executeTool: (input: Parameters<Tool.Snapshot["execute"]>[0]) => Effect.Effect<Tool.Result, ExecuteError>
|
||||||
input: Parameters<Tool.Snapshot["execute"]>[0],
|
|
||||||
) => Effect.Effect<Tool.Result, ExecuteError>
|
|
||||||
/** True when this request is the final Step; violating calls are rejected and no continuation follows. */
|
/** True when this request is the final Step; violating calls are rejected and no continuation follows. */
|
||||||
readonly stepLimitReached: boolean
|
readonly stepLimitReached: boolean
|
||||||
}
|
}
|
||||||
@@ -137,8 +137,7 @@ export const boundImages = (messages: LLMRequest["messages"]) => {
|
|||||||
result: {
|
result: {
|
||||||
...part.result,
|
...part.result,
|
||||||
value: part.result.value.map((item: Content) => {
|
value: part.result.value.map((item: Content) => {
|
||||||
if (item.type !== "file" || !isImage(item.mime) || imageBytes - removed <= IMAGE_BYTES_TARGET)
|
if (item.type !== "file" || !isImage(item.mime) || imageBytes - removed <= IMAGE_BYTES_TARGET) return item
|
||||||
return item
|
|
||||||
removed += Buffer.byteLength(item.uri)
|
removed += Buffer.byteLength(item.uri)
|
||||||
return { type: "text" as const, text: IMAGE_REMOVED }
|
return { type: "text" as const, text: IMAGE_REMOVED }
|
||||||
}),
|
}),
|
||||||
@@ -189,55 +188,87 @@ export const layer = Layer.effect(
|
|||||||
.map(SystemPart.make)
|
.map(SystemPart.make)
|
||||||
const history = toLLMMessages(input.context.messages, resolved.ref, providerMetadataKey)
|
const history = toLLMMessages(input.context.messages, resolved.ref, providerMetadataKey)
|
||||||
const messages = stepLimitReached ? [...history, Message.assistant(MAX_STEPS_PROMPT)] : history
|
const messages = stepLimitReached ? [...history, Message.assistant(MAX_STEPS_PROMPT)] : history
|
||||||
const toolDefinitions = tools.definitions
|
const registry = new Map(tools.definitions.map((tool) => [tool.name, tool]))
|
||||||
const toolsByName = new Map(toolDefinitions.map((tool) => [tool.name, tool]))
|
// The definition objects we hand to hooks, mapped back to their tools. Hooks rename a
|
||||||
// Hooks may reshape available definitions but cannot advertise tools omitted by permissions or the Step limit.
|
// tool by moving its definition to a new key; recognizing the object recovers the tool.
|
||||||
const contextEvent = yield* hooks.trigger("session", "context", {
|
const given = new Map(
|
||||||
|
tools.definitions.map(
|
||||||
|
(tool) => [{ description: tool.description, input: { ...tool.inputSchema } }, tool] as const,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
// Hooks mutate this record in place: edit descriptions and schemas, rename, or remove.
|
||||||
|
const context = yield* hooks.trigger("session", "context", {
|
||||||
sessionID: session.id,
|
sessionID: session.id,
|
||||||
agent: agent.id,
|
agent: agent.id,
|
||||||
model: resolved.ref,
|
model: resolved.ref,
|
||||||
system,
|
system,
|
||||||
messages,
|
messages,
|
||||||
tools: Object.fromEntries(
|
tools: Object.fromEntries(Array.from(given, ([definition, tool]) => [tool.name, definition])),
|
||||||
toolDefinitions.map((tool) => [tool.name, { description: tool.description, input: { ...tool.inputSchema } }]),
|
|
||||||
),
|
|
||||||
})
|
|
||||||
const hookedTools = Object.entries(contextEvent.tools).flatMap(([name, tool]) => {
|
|
||||||
const registered = toolsByName.get(name)
|
|
||||||
return registered
|
|
||||||
? [{ ...registered, description: tool.description, inputSchema: tool.input }]
|
|
||||||
: []
|
|
||||||
})
|
})
|
||||||
|
// Match each surviving entry back to its tool, by recognizing a moved definition or
|
||||||
|
// by key. Identity wins so a definition moved onto another tool's name still executes
|
||||||
|
// the tool it describes. Entries matching neither were invented by a hook and dropped.
|
||||||
|
// `tool.name` stays canonical so execution can translate renamed calls back.
|
||||||
|
const hooked = new Map(
|
||||||
|
Object.entries(context.tools).flatMap(([name, definition]) => {
|
||||||
|
const tool = given.get(definition) ?? registry.get(name)
|
||||||
|
if (!tool) return []
|
||||||
|
return [[name, { ...tool, description: definition.description, inputSchema: definition.input }] as const]
|
||||||
|
}),
|
||||||
|
)
|
||||||
const request = LLM.request({
|
const request = LLM.request({
|
||||||
model,
|
model,
|
||||||
http: {
|
http: {
|
||||||
headers: SessionModelHeaders.make(session, app),
|
headers: SessionModelHeaders.make(session, app),
|
||||||
},
|
},
|
||||||
providerOptions: { [providerMetadataKey]: { promptCacheKey } },
|
providerOptions: { [providerMetadataKey]: { promptCacheKey } },
|
||||||
system: contextEvent.system,
|
system: context.system,
|
||||||
messages: boundImages(unsupportedParts(contextEvent.messages, resolved.capabilities)),
|
messages: boundImages(unsupportedParts(context.messages, resolved.capabilities)),
|
||||||
tools: hookedTools,
|
tools: Array.from(hooked, ([name, tool]) => ({ ...tool, name })),
|
||||||
toolChoice: stepLimitReached ? "none" : undefined,
|
toolChoice: stepLimitReached ? "none" : undefined,
|
||||||
})
|
})
|
||||||
const options: StreamOptions = {
|
const options: StreamOptions = {
|
||||||
transform: (request) =>
|
http: (request, handler) =>
|
||||||
hooks
|
Effect.gen(function* () {
|
||||||
.trigger("session", "request", {
|
let latest = request
|
||||||
|
const origins = new WeakMap<Response, HttpClientRequest.HttpClientRequest>()
|
||||||
|
const middlewares: SessionHttpMiddleware[] = []
|
||||||
|
const web = yield* HttpClientRequest.toWeb(request)
|
||||||
|
yield* hooks.trigger("session", "http", {
|
||||||
sessionID: session.id,
|
sessionID: session.id,
|
||||||
agent: agent.id,
|
agent: agent.id,
|
||||||
model: resolved.ref,
|
model: resolved.ref,
|
||||||
...request,
|
use: (item) =>
|
||||||
})
|
|
||||||
.pipe(
|
|
||||||
Effect.tap((event) =>
|
|
||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
request.url = event.url
|
middlewares.push(item)
|
||||||
request.headers = event.headers
|
|
||||||
request.body = event.body
|
|
||||||
}),
|
}),
|
||||||
),
|
})
|
||||||
Effect.asVoid,
|
const send = (input: Request) =>
|
||||||
),
|
Effect.gen(function* () {
|
||||||
|
let sent = HttpClientRequest.fromWeb(input)
|
||||||
|
if (input.body)
|
||||||
|
sent = HttpClientRequest.bodyUint8Array(
|
||||||
|
sent,
|
||||||
|
new Uint8Array(yield* Effect.promise(() => input.clone().arrayBuffer())),
|
||||||
|
input.headers.get("content-type") ?? undefined,
|
||||||
|
)
|
||||||
|
latest = sent
|
||||||
|
const response = yield* handler(sent)
|
||||||
|
const body = [204, 205, 304].includes(response.status)
|
||||||
|
? null
|
||||||
|
: yield* Stream.toReadableStreamEffect(response.stream)
|
||||||
|
const output = new Response(body, { status: response.status, headers: response.headers })
|
||||||
|
origins.set(output, sent)
|
||||||
|
return output
|
||||||
|
})
|
||||||
|
const dispatch = middlewares.reduce<SessionHttpHandler>(
|
||||||
|
(next, item) => (input: Request) => item(input, next),
|
||||||
|
send,
|
||||||
|
)
|
||||||
|
const response = yield* dispatch(web)
|
||||||
|
const origin = origins.get(response) ?? latest
|
||||||
|
return HttpClientResponse.fromWeb(origin, response)
|
||||||
|
}).pipe(Effect.mapError((cause) => (cause instanceof Error ? cause : new Error(String(cause))))),
|
||||||
}
|
}
|
||||||
if (promptCacheSnapshots) {
|
if (promptCacheSnapshots) {
|
||||||
const current = PromptCacheDiagnostics.snapshot(request)
|
const current = PromptCacheDiagnostics.snapshot(request)
|
||||||
@@ -256,13 +287,15 @@ export const layer = Layer.effect(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const executeTool: Prepared["executeTool"] = (executeInput) => {
|
const executeTool: Prepared["executeTool"] = (input) => {
|
||||||
if (stepLimitReached)
|
if (stepLimitReached)
|
||||||
return new Tool.Error({ message: "Tools are disabled after the maximum agent steps" })
|
return new Tool.Error({ message: "Tools are disabled after the maximum agent steps" })
|
||||||
if (toolsByName.has(executeInput.call.name) && !Object.hasOwn(contextEvent.tools, executeInput.call.name))
|
const tool = hooked.get(input.call.name)
|
||||||
return new Tool.Error({ message: `Tool is not available for this request: ${executeInput.call.name}` })
|
// A registered tool absent from the hooked set was removed or renamed by a hook.
|
||||||
|
if (!tool && registry.has(input.call.name))
|
||||||
|
return new Tool.Error({ message: `Tool is not available for this request: ${input.call.name}` })
|
||||||
return tools
|
return tools
|
||||||
.execute(executeInput)
|
.execute(tool ? { ...input, call: { ...input.call, name: tool.name } } : input)
|
||||||
.pipe(Effect.catchCauseFilter(declineDefect, (decline) => Effect.fail(decline)))
|
.pipe(Effect.catchCauseFilter(declineDefect, (decline) => Effect.fail(decline)))
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,22 +1,27 @@
|
|||||||
export * as Vcs from "./vcs"
|
export * as Vcs from "./vcs"
|
||||||
|
|
||||||
import { Context, Effect, Layer } from "effect"
|
import path from "path"
|
||||||
|
import { Context, Effect, Layer, Stream } from "effect"
|
||||||
import { FileDiff } from "@opencode-ai/schema/file-diff"
|
import { FileDiff } from "@opencode-ai/schema/file-diff"
|
||||||
import { FileStatus, Mode } from "@opencode-ai/schema/vcs"
|
import { FileSystem } from "@opencode-ai/schema/filesystem"
|
||||||
|
import { FileStatus, Info, Mode } from "@opencode-ai/schema/vcs"
|
||||||
|
import { VcsEvent } from "@opencode-ai/schema/vcs-event"
|
||||||
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||||
import { FSUtil } from "@opencode-ai/util/fs-util"
|
import { FSUtil } from "@opencode-ai/util/fs-util"
|
||||||
import { Location } from "./location"
|
import { Location } from "./location"
|
||||||
import { AppProcess } from "@opencode-ai/util/process"
|
import { AppProcess } from "@opencode-ai/util/process"
|
||||||
|
import { Bus } from "./bus"
|
||||||
import { VcsGit } from "./vcs/git"
|
import { VcsGit } from "./vcs/git"
|
||||||
import { VcsHg } from "./vcs/hg"
|
import { VcsHg } from "./vcs/hg"
|
||||||
|
|
||||||
export { FileStatus, Mode }
|
export { FileStatus, Info, Mode }
|
||||||
|
|
||||||
export interface DiffOptions {
|
export interface DiffOptions {
|
||||||
readonly context?: number
|
readonly context?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
|
readonly info: () => Effect.Effect<Info>
|
||||||
readonly status: () => Effect.Effect<FileStatus[]>
|
readonly status: () => Effect.Effect<FileStatus[]>
|
||||||
readonly diff: (mode: Mode, options?: DiffOptions) => Effect.Effect<FileDiff.Info[]>
|
readonly diff: (mode: Mode, options?: DiffOptions) => Effect.Effect<FileDiff.Info[]>
|
||||||
}
|
}
|
||||||
@@ -38,8 +43,36 @@ const layer = Layer.effect(
|
|||||||
const proc = yield* AppProcess.Service
|
const proc = yield* AppProcess.Service
|
||||||
const fs = yield* FSUtil.Service
|
const fs = yield* FSUtil.Service
|
||||||
const location = yield* Location.Service
|
const location = yield* Location.Service
|
||||||
|
const bus = yield* Bus.Service
|
||||||
const impl = adapter(proc, fs, location)
|
const impl = adapter(proc, fs, location)
|
||||||
|
const vcs = location.vcs
|
||||||
|
const state = { info: impl ? yield* impl.info() : { branch: {} } satisfies Info }
|
||||||
|
|
||||||
|
if (vcs && impl) {
|
||||||
|
const store = yield* fs.realPath(vcs.store).pipe(Effect.catch(() => Effect.succeed(vcs.store)))
|
||||||
|
const isBranchMetadata =
|
||||||
|
vcs.type === "git"
|
||||||
|
? (file: string) => path.basename(file) === "HEAD" && FSUtil.contains(store, file)
|
||||||
|
: (file: string) => path.resolve(file) === path.join(store, "branch")
|
||||||
|
yield* bus.subscribe(FileSystem.Event.Changed).pipe(
|
||||||
|
Stream.filter((event) => isBranchMetadata(event.data.file)),
|
||||||
|
Stream.runForEach((event) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const next = yield* impl.info()
|
||||||
|
const changed = state.info.branch.current !== next.branch.current
|
||||||
|
state.info = next
|
||||||
|
if (!changed) return
|
||||||
|
yield* bus.publish(VcsEvent.BranchUpdated, { branch: next.branch.current })
|
||||||
|
}).pipe(Effect.withSpan("Vcs.refreshBranch", { attributes: { file: event.data.file } })),
|
||||||
|
),
|
||||||
|
Effect.forkScoped({ startImmediately: true }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return Service.of({
|
return Service.of({
|
||||||
|
info: Effect.fn("Vcs.info")(function* () {
|
||||||
|
return state.info
|
||||||
|
}),
|
||||||
status: Effect.fn("Vcs.status")(function* () {
|
status: Effect.fn("Vcs.status")(function* () {
|
||||||
if (!impl) return []
|
if (!impl) return []
|
||||||
return yield* impl.status()
|
return yield* impl.status()
|
||||||
@@ -55,5 +88,5 @@ const layer = Layer.effect(
|
|||||||
export const node = makeLocationNode({
|
export const node = makeLocationNode({
|
||||||
service: Service,
|
service: Service,
|
||||||
layer: layer,
|
layer: layer,
|
||||||
deps: [AppProcess.node, FSUtil.node, Location.node],
|
deps: [AppProcess.node, FSUtil.node, Location.node, Bus.node],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ export * as VcsGit from "./git"
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { ChildProcess } from "effect/unstable/process"
|
import { ChildProcess } from "effect/unstable/process"
|
||||||
import { FileDiff } from "@opencode-ai/schema/file-diff"
|
import { FileDiff } from "@opencode-ai/schema/file-diff"
|
||||||
import { FileStatus, Mode } from "@opencode-ai/schema/vcs"
|
import { FileStatus, Info, Mode } from "@opencode-ai/schema/vcs"
|
||||||
import { AppProcess } from "@opencode-ai/util/process"
|
import { AppProcess } from "@opencode-ai/util/process"
|
||||||
import type { DiffOptions, Interface } from "../vcs"
|
import type { DiffOptions, Interface } from "../vcs"
|
||||||
import { chunksByFile, emptyPatch, MAX_PATCH_BYTES, MAX_TOTAL_PATCH_BYTES, PATCH_CONTEXT_LINES } from "./patch"
|
import { chunksByFile, emptyPatch, MAX_PATCH_BYTES, MAX_TOTAL_PATCH_BYTES, PATCH_CONTEXT_LINES } from "./patch"
|
||||||
@@ -20,6 +20,12 @@ export function make(proc: AppProcess.Interface, input: { directory: string; wor
|
|||||||
const ctx: Ctx = { git: makeGit(proc), directory: input.directory, worktree: input.worktree }
|
const ctx: Ctx = { git: makeGit(proc), directory: input.directory, worktree: input.worktree }
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
info: Effect.fn("VcsGit.info")(function* () {
|
||||||
|
const [current, root] = yield* Effect.all([ctx.git.branch(ctx.directory), ctx.git.defaultBranch(ctx.directory)], {
|
||||||
|
concurrency: 2,
|
||||||
|
})
|
||||||
|
return { branch: { current, default: root?.name } } satisfies Info
|
||||||
|
}),
|
||||||
status: Effect.fn("VcsGit.status")(function* () {
|
status: Effect.fn("VcsGit.status")(function* () {
|
||||||
const git = ctx.git
|
const git = ctx.git
|
||||||
const ref = (yield* git.hasHead(ctx.directory)) ? "HEAD" : undefined
|
const ref = (yield* git.hasHead(ctx.directory)) ? "HEAD" : undefined
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import path from "path"
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { ChildProcess } from "effect/unstable/process"
|
import { ChildProcess } from "effect/unstable/process"
|
||||||
import { FileDiff } from "@opencode-ai/schema/file-diff"
|
import { FileDiff } from "@opencode-ai/schema/file-diff"
|
||||||
import { FileStatus, Mode } from "@opencode-ai/schema/vcs"
|
import { FileStatus, Info, Mode } from "@opencode-ai/schema/vcs"
|
||||||
import { FSUtil } from "@opencode-ai/util/fs-util"
|
import { FSUtil } from "@opencode-ai/util/fs-util"
|
||||||
import { AppProcess } from "@opencode-ai/util/process"
|
import { AppProcess } from "@opencode-ai/util/process"
|
||||||
import type { DiffOptions, Interface } from "../vcs"
|
import type { DiffOptions, Interface } from "../vcs"
|
||||||
@@ -73,6 +73,9 @@ export function make(
|
|||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
info: Effect.fn("VcsHg.info")(function* () {
|
||||||
|
return { branch: { current: yield* hg.branch(), default: "default" } } satisfies Info
|
||||||
|
}),
|
||||||
status: Effect.fn("VcsHg.status")(function* () {
|
status: Effect.fn("VcsHg.status")(function* () {
|
||||||
const [items, batch] = yield* Effect.all(
|
const [items, batch] = yield* Effect.all(
|
||||||
// Zero-context patches are enough to count changed lines.
|
// Zero-context patches are enough to count changed lines.
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import path from "path"
|
||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { Effect, Exit, Fiber, Layer, Scope, Stream } from "effect"
|
import { Effect, Exit, Fiber, Layer, Scope, Stream } from "effect"
|
||||||
import { TestClock } from "effect/testing"
|
import { TestClock } from "effect/testing"
|
||||||
@@ -9,15 +10,19 @@ import { Location } from "@opencode-ai/core/location"
|
|||||||
import { Permission } from "@opencode-ai/core/permission"
|
import { Permission } from "@opencode-ai/core/permission"
|
||||||
import { AgentPlugin } from "@opencode-ai/core/plugin/agent"
|
import { AgentPlugin } from "@opencode-ai/core/plugin/agent"
|
||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
|
import { Global } from "@opencode-ai/util/global"
|
||||||
import { location } from "./fixture/location"
|
import { location } from "./fixture/location"
|
||||||
import { testEffect } from "./lib/effect"
|
import { testEffect } from "./lib/effect"
|
||||||
import { agentHost, host } from "./plugin/host"
|
import { agentHost, host } from "./plugin/host"
|
||||||
|
|
||||||
const testLocation = location({ directory: AbsolutePath.make("/project") })
|
const testLocation = location({ directory: AbsolutePath.make("/project") })
|
||||||
const locationLayer = Layer.succeed(Location.Service, Location.Service.of(testLocation))
|
const locationLayer = Layer.succeed(Location.Service, Location.Service.of(testLocation))
|
||||||
|
const global = Global.make({ data: "/data", config: "/config", tmp: "/tmp/opencode" })
|
||||||
|
const globalLayer = Layer.succeed(Global.Service, Global.Service.of(global))
|
||||||
|
|
||||||
const it = testEffect(
|
const it = testEffect(
|
||||||
AppNodeBuilder.build(LayerNode.group([Agent.node, Bus.node, Location.node]), [
|
AppNodeBuilder.build(LayerNode.group([Agent.node, Bus.node, Location.node]), [
|
||||||
|
[Global.node, globalLayer],
|
||||||
[Location.node, locationLayer],
|
[Location.node, locationLayer],
|
||||||
]) as unknown as Layer.Layer<unknown, never>,
|
]) as unknown as Layer.Layer<unknown, never>,
|
||||||
)
|
)
|
||||||
@@ -120,25 +125,35 @@ describe("Agent", () => {
|
|||||||
const id = Agent.ID.make("custom")
|
const id = Agent.ID.make("custom")
|
||||||
|
|
||||||
yield* agent.transform((editor) => editor.update(id, () => {}))
|
yield* agent.transform((editor) => editor.update(id, () => {}))
|
||||||
expect(yield* agent.get(id)).toEqual(Agent.Info.default(id))
|
const info = yield* agent.get(id)
|
||||||
|
expect(info?.permissions.slice(0, Agent.Info.default(id).permissions.length)).toEqual(
|
||||||
|
Agent.Info.default(id).permissions,
|
||||||
|
)
|
||||||
|
expect(Permission.evaluate("external_directory", path.join(global.data, "shell", "*", "*"), info?.permissions ?? []).effect).toBe(
|
||||||
|
"allow",
|
||||||
|
)
|
||||||
|
expect(Permission.evaluate("external_directory", path.join(global.data, "tool-output", "*"), info?.permissions ?? []).effect).toBe(
|
||||||
|
"allow",
|
||||||
|
)
|
||||||
|
expect(Permission.evaluate("external_directory", path.join(global.config, "*"), info?.permissions ?? []).effect).toBe(
|
||||||
|
"allow",
|
||||||
|
)
|
||||||
|
expect(Permission.evaluate("external_directory", path.join(global.tmp, "*"), info?.permissions ?? []).effect).toBe(
|
||||||
|
"allow",
|
||||||
|
)
|
||||||
|
|
||||||
yield* agent.transform((editor) => editor.remove(id))
|
yield* agent.transform((editor) => editor.remove(id))
|
||||||
expect(yield* agent.get(id)).toBeUndefined()
|
expect(yield* agent.get(id)).toBeUndefined()
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("does not ambiently opt built-in agents into bash", () =>
|
it.effect("applies managed external directories without opting built-in agents into bash", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const agent = yield* Agent.Service
|
const agent = yield* Agent.Service
|
||||||
yield* AgentPlugin.Plugin.effect(
|
yield* AgentPlugin.Plugin.effect(
|
||||||
host({
|
host({
|
||||||
agent: agentHost(agent),
|
agent: agentHost(agent),
|
||||||
}),
|
}),
|
||||||
).pipe(
|
|
||||||
Effect.provideService(
|
|
||||||
Location.Service,
|
|
||||||
Location.Service.of(location({ directory: AbsolutePath.make("/project") })),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const agents = yield* agent.list()
|
const agents = yield* agent.list()
|
||||||
@@ -152,6 +167,20 @@ describe("Agent", () => {
|
|||||||
"title",
|
"title",
|
||||||
])
|
])
|
||||||
expect((yield* agent.get(Agent.defaultID))?.system).toBeUndefined()
|
expect((yield* agent.get(Agent.defaultID))?.system).toBeUndefined()
|
||||||
|
const permissions = (yield* agent.get(Agent.defaultID))?.permissions ?? []
|
||||||
|
expect(
|
||||||
|
Permission.evaluate("external_directory", path.join(global.data, "shell", "*", "*"), permissions).effect,
|
||||||
|
).toBe("allow")
|
||||||
|
expect(
|
||||||
|
Permission.evaluate("external_directory", path.join(global.data, "tool-output", "*"), permissions).effect,
|
||||||
|
).toBe("allow")
|
||||||
|
expect(Permission.evaluate("external_directory", path.join(global.config, "*"), permissions).effect).toBe("allow")
|
||||||
|
expect(Permission.evaluate("external_directory", path.join(global.tmp, "*"), permissions).effect).toBe("allow")
|
||||||
|
const explore = yield* agent.get(Agent.ID.make("explore"))
|
||||||
|
expect(Permission.evaluate("read", ".env", explore?.permissions ?? []).effect).toBe("ask")
|
||||||
|
expect(Permission.evaluate("read", ".env.local", explore?.permissions ?? []).effect).toBe("ask")
|
||||||
|
expect(Permission.evaluate("read", ".env.example", explore?.permissions ?? []).effect).toBe("allow")
|
||||||
|
expect(Permission.evaluate("read", "src/index.ts", explore?.permissions ?? []).effect).toBe("allow")
|
||||||
for (const item of agents) {
|
for (const item of agents) {
|
||||||
expect(item.permissions.some((rule) => rule.action === "bash" && rule.effect !== "deny")).toBe(false)
|
expect(item.permissions.some((rule) => rule.action === "bash" && rule.effect !== "deny")).toBe(false)
|
||||||
}
|
}
|
||||||
@@ -165,11 +194,6 @@ describe("Agent", () => {
|
|||||||
host({
|
host({
|
||||||
agent: agentHost(agent),
|
agent: agentHost(agent),
|
||||||
}),
|
}),
|
||||||
).pipe(
|
|
||||||
Effect.provideService(
|
|
||||||
Location.Service,
|
|
||||||
Location.Service.of(location({ directory: AbsolutePath.make("/project") })),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
yield* Effect.forEach(["general", "explore"], (id) =>
|
yield* Effect.forEach(["general", "explore"], (id) =>
|
||||||
|
|||||||
@@ -98,6 +98,23 @@ it.effect("projects request settings, headers, and body overlays", () =>
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.effect("leaves max output tokens unset when the request omits them", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
|
yield* aisdk.hook.sdk((event) => {
|
||||||
|
event.sdk = { languageModel: () => ({ provider: event.model.providerID }) }
|
||||||
|
})
|
||||||
|
|
||||||
|
const resolved = yield* aisdk.model({
|
||||||
|
...model("@openrouter/ai-sdk-provider"),
|
||||||
|
limit: { context: 500_000, output: 500_000 },
|
||||||
|
})
|
||||||
|
const prepared = yield* compileRequest(LLM.request({ model: resolved, prompt: "Hello" }))
|
||||||
|
|
||||||
|
expect(prepared.body.maxOutputTokens).toBeUndefined()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.effect("maps pro reasoning bodies to AI SDK provider options", () =>
|
it.effect("maps pro reasoning bodies to AI SDK provider options", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const aisdk = yield* AISDK.Service
|
const aisdk = yield* AISDK.Service
|
||||||
|
|||||||
@@ -20,10 +20,13 @@ import { agentHost, host } from "../plugin/host"
|
|||||||
|
|
||||||
const it = testEffect(AppNodeBuilder.build(LayerNode.group([Agent.node, Bus.node, FSUtil.node, Global.node])))
|
const it = testEffect(AppNodeBuilder.build(LayerNode.group([Agent.node, Bus.node, FSUtil.node, Global.node])))
|
||||||
const decode = Schema.decodeUnknownSync(Config.Info)
|
const decode = Schema.decodeUnknownSync(Config.Info)
|
||||||
const defaultPermissions = [
|
const defaultPermissions = (global: Global.Interface): Permission.Ruleset => [
|
||||||
{ action: "*", resource: "*", effect: "allow" },
|
...Agent.Info.default(Agent.ID.make("test")).permissions,
|
||||||
{ action: "external_directory", resource: "*", effect: "ask" },
|
{ action: "external_directory", resource: path.join(global.data, "shell", "*", "*"), effect: "allow" },
|
||||||
] satisfies Permission.Ruleset
|
{ action: "external_directory", resource: path.join(global.data, "tool-output", "*"), effect: "allow" },
|
||||||
|
{ action: "external_directory", resource: path.join(global.tmp, "*"), effect: "allow" },
|
||||||
|
{ action: "external_directory", resource: path.join(global.config, "*"), effect: "allow" },
|
||||||
|
]
|
||||||
|
|
||||||
test("rejects named agent color tokens", () => {
|
test("rejects named agent color tokens", () => {
|
||||||
expect(() => decode({ agents: { reviewer: { color: "warning" } } })).toThrow()
|
expect(() => decode({ agents: { reviewer: { color: "warning" } } })).toThrow()
|
||||||
@@ -58,6 +61,7 @@ describe("ConfigAgentPlugin.Plugin", () => {
|
|||||||
it.effect("applies all global permissions before agent-specific permissions", () =>
|
it.effect("applies all global permissions before agent-specific permissions", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const agents = yield* Agent.Service
|
const agents = yield* Agent.Service
|
||||||
|
const global = yield* Global.Service
|
||||||
const build = Agent.ID.make("build")
|
const build = Agent.ID.make("build")
|
||||||
yield* agents.transform((editor) =>
|
yield* agents.transform((editor) =>
|
||||||
editor.update(build, (agent) => {
|
editor.update(build, (agent) => {
|
||||||
@@ -110,7 +114,7 @@ describe("ConfigAgentPlugin.Plugin", () => {
|
|||||||
const buildAgent = yield* agents.get(build)
|
const buildAgent = yield* agents.get(build)
|
||||||
if (!buildAgent) throw new Error("expected configured build agent")
|
if (!buildAgent) throw new Error("expected configured build agent")
|
||||||
expect(buildAgent.permissions).toEqual([
|
expect(buildAgent.permissions).toEqual([
|
||||||
...defaultPermissions,
|
...defaultPermissions(global),
|
||||||
{ action: "bash", resource: "*", effect: "allow" },
|
{ action: "bash", resource: "*", effect: "allow" },
|
||||||
{ action: "bash", resource: "*", effect: "ask" },
|
{ action: "bash", resource: "*", effect: "ask" },
|
||||||
{ action: "read", resource: "*", effect: "allow" },
|
{ action: "read", resource: "*", effect: "allow" },
|
||||||
@@ -128,7 +132,7 @@ describe("ConfigAgentPlugin.Plugin", () => {
|
|||||||
model: { providerID: "openrouter", id: "openai/gpt-5", variant: "high" },
|
model: { providerID: "openrouter", id: "openai/gpt-5", variant: "high" },
|
||||||
})
|
})
|
||||||
expect(reviewer.permissions).toEqual([
|
expect(reviewer.permissions).toEqual([
|
||||||
...defaultPermissions,
|
...defaultPermissions(global),
|
||||||
{ action: "bash", resource: "*", effect: "ask" },
|
{ action: "bash", resource: "*", effect: "ask" },
|
||||||
{ action: "read", resource: "*", effect: "allow" },
|
{ action: "read", resource: "*", effect: "allow" },
|
||||||
{ action: "edit", resource: "*", effect: "deny" },
|
{ action: "edit", resource: "*", effect: "deny" },
|
||||||
@@ -136,7 +140,7 @@ describe("ConfigAgentPlugin.Plugin", () => {
|
|||||||
])
|
])
|
||||||
expect(Permission.evaluate("read", "README.md", reviewer.permissions).effect).toBe("deny")
|
expect(Permission.evaluate("read", "README.md", reviewer.permissions).effect).toBe("deny")
|
||||||
expect((yield* agents.get(Agent.ID.make("late")))?.permissions).toEqual([
|
expect((yield* agents.get(Agent.ID.make("late")))?.permissions).toEqual([
|
||||||
...defaultPermissions,
|
...defaultPermissions(global),
|
||||||
{ action: "bash", resource: "*", effect: "ask" },
|
{ action: "bash", resource: "*", effect: "ask" },
|
||||||
{ action: "read", resource: "*", effect: "allow" },
|
{ action: "read", resource: "*", effect: "allow" },
|
||||||
{ action: "edit", resource: "*", effect: "allow" },
|
{ action: "edit", resource: "*", effect: "allow" },
|
||||||
@@ -270,6 +274,7 @@ Use native v2 fields.`,
|
|||||||
await fs.writeFile(path.join(tmp.path, "modes", "plan.md"), "Make a plan.")
|
await fs.writeFile(path.join(tmp.path, "modes", "plan.md"), "Make a plan.")
|
||||||
})
|
})
|
||||||
const agents = yield* Agent.Service
|
const agents = yield* Agent.Service
|
||||||
|
const global = yield* Global.Service
|
||||||
const entries = [
|
const entries = [
|
||||||
new Config.Document({
|
new Config.Document({
|
||||||
type: "document",
|
type: "document",
|
||||||
@@ -287,13 +292,13 @@ Use native v2 fields.`,
|
|||||||
system: "Review carefully.",
|
system: "Review carefully.",
|
||||||
description: "Markdown description",
|
description: "Markdown description",
|
||||||
request: { body: { temperature: 0.5 } },
|
request: { body: { temperature: 0.5 } },
|
||||||
permissions: [...defaultPermissions, { action: "edit", resource: "*", effect: "deny" }],
|
permissions: [...defaultPermissions(global), { action: "edit", resource: "*", effect: "deny" }],
|
||||||
})
|
})
|
||||||
expect(yield* agents.get(Agent.ID.make("team/helper"))).toMatchObject({ system: "Help the team." })
|
expect(yield* agents.get(Agent.ID.make("team/helper"))).toMatchObject({ system: "Help the team." })
|
||||||
expect(yield* agents.get(Agent.ID.make("native"))).toMatchObject({
|
expect(yield* agents.get(Agent.ID.make("native"))).toMatchObject({
|
||||||
system: "Use native v2 fields.",
|
system: "Use native v2 fields.",
|
||||||
request: { headers: { "x-agent": "native" }, body: { effort: "high" } },
|
request: { headers: { "x-agent": "native" }, body: { effort: "high" } },
|
||||||
permissions: [...defaultPermissions, { action: "edit", resource: "*", effect: "deny" }],
|
permissions: [...defaultPermissions(global), { action: "edit", resource: "*", effect: "deny" }],
|
||||||
})
|
})
|
||||||
expect(yield* agents.get(Agent.ID.make("disabled"))).toBeUndefined()
|
expect(yield* agents.get(Agent.ID.make("disabled"))).toBeUndefined()
|
||||||
expect(yield* agents.get(Agent.ID.make("empty"))).toBeUndefined()
|
expect(yield* agents.get(Agent.ID.make("empty"))).toBeUndefined()
|
||||||
|
|||||||
@@ -153,12 +153,16 @@ function provide(directory: string, vcs?: Location.Interface["vcs"]) {
|
|||||||
|
|
||||||
function withTmp<A, E, R>(
|
function withTmp<A, E, R>(
|
||||||
f: (directory: string, vcs?: Location.Interface["vcs"]) => Effect.Effect<A, E, R>,
|
f: (directory: string, vcs?: Location.Interface["vcs"]) => Effect.Effect<A, E, R>,
|
||||||
options?: { git?: boolean; init?: (directory: string) => Promise<void> },
|
options?: { vcs?: "git" | "hg"; init?: (directory: string) => Promise<void> },
|
||||||
) {
|
) {
|
||||||
return Effect.acquireRelease(
|
return Effect.acquireRelease(
|
||||||
Effect.promise(async () => {
|
Effect.promise(async () => {
|
||||||
const tmp = await tmpdir()
|
const tmp = await tmpdir()
|
||||||
if (!options?.git) return { tmp, vcs: undefined }
|
if (options?.vcs === "hg") {
|
||||||
|
await fs.mkdir(path.join(tmp.path, ".hg"))
|
||||||
|
return { tmp, vcs: { type: "hg" as const, store: AbsolutePath.make(path.join(tmp.path, ".hg")) } }
|
||||||
|
}
|
||||||
|
if (options?.vcs !== "git") return { tmp, vcs: undefined }
|
||||||
await $`git init`.cwd(tmp.path).quiet()
|
await $`git init`.cwd(tmp.path).quiet()
|
||||||
await $`git config core.fsmonitor false`.cwd(tmp.path).quiet()
|
await $`git config core.fsmonitor false`.cwd(tmp.path).quiet()
|
||||||
await $`git config commit.gpgsign false`.cwd(tmp.path).quiet()
|
await $`git config commit.gpgsign false`.cwd(tmp.path).quiet()
|
||||||
@@ -292,7 +296,7 @@ describeWatcher("LocationWatcher", () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
{ git: true },
|
{ vcs: "git" },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -322,7 +326,7 @@ describeWatcher("LocationWatcher", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
{ git: true },
|
{ vcs: "git" },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -359,7 +363,7 @@ describeWatcher("LocationWatcher", () => {
|
|||||||
.pipe(Effect.andThen(Effect.promise(() => $`git add .`.cwd(directory).quiet())), Effect.asVoid),
|
.pipe(Effect.andThen(Effect.promise(() => $`git add .`.cwd(directory).quiet())), Effect.asVoid),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
{ git: true },
|
{ vcs: "git" },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -376,7 +380,7 @@ describeWatcher("LocationWatcher", () => {
|
|||||||
yield* nextUpdate((event) => event.file === head, fs.writeFileString(head, `ref: refs/heads/${branch}\n`)),
|
yield* nextUpdate((event) => event.file === head, fs.writeFileString(head, `ref: refs/heads/${branch}\n`)),
|
||||||
).toMatchObject({ file: head })
|
).toMatchObject({ file: head })
|
||||||
}),
|
}),
|
||||||
{ git: true },
|
{ vcs: "git" },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -401,7 +405,7 @@ describeWatcher("LocationWatcher", () => {
|
|||||||
).toEqual({ file: path.join(actual, "HEAD"), event: "change" })
|
).toEqual({ file: path.join(actual, "HEAD"), event: "change" })
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
git: true,
|
vcs: "git",
|
||||||
init: async (directory) => {
|
init: async (directory) => {
|
||||||
const actual = path.join(directory, "..", `actual_${path.basename(directory)}`)
|
const actual = path.join(directory, "..", `actual_${path.basename(directory)}`)
|
||||||
await fs.rename(path.join(directory, ".git"), actual)
|
await fs.rename(path.join(directory, ".git"), actual)
|
||||||
@@ -411,4 +415,19 @@ describeWatcher("LocationWatcher", () => {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it.live("publishes .hg/branch events", () =>
|
||||||
|
withTmp(
|
||||||
|
(directory) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const fs = yield* FSUtil.Service
|
||||||
|
const branch = path.join(directory, ".hg", "branch")
|
||||||
|
yield* ready(directory)
|
||||||
|
expect(
|
||||||
|
yield* nextUpdate((event) => event.file === branch, fs.writeFileString(branch, "feature\n")),
|
||||||
|
).toMatchObject({ file: branch })
|
||||||
|
}),
|
||||||
|
{ vcs: "hg" },
|
||||||
|
),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import path from "path"
|
|||||||
import { Global } from "@opencode-ai/util/global"
|
import { Global } from "@opencode-ai/util/global"
|
||||||
|
|
||||||
describe("global paths", () => {
|
describe("global paths", () => {
|
||||||
test("tmp path is under the system temp directory", () => {
|
test("tmp path is the canonical system temp directory", async () => {
|
||||||
expect(Global.Path.tmp).toBe(path.join(os.tmpdir(), "opencode"))
|
expect(Global.Path.tmp).toBe(await fs.realpath(path.join(os.tmpdir(), "opencode")))
|
||||||
expect(Global.make().tmp).toBe(Global.Path.tmp)
|
expect(Global.make().tmp).toBe(Global.Path.tmp)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const locationLayer = Layer.succeed(
|
|||||||
const it = testEffect(
|
const it = testEffect(
|
||||||
AppNodeBuilder.build(InstructionBuiltIns.node, [
|
AppNodeBuilder.build(InstructionBuiltIns.node, [
|
||||||
[Location.node, locationLayer],
|
[Location.node, locationLayer],
|
||||||
[Global.node, Global.layerWith({ config: "/global" })],
|
[Global.node, Global.layerWith({ config: "/global", tmp: "/temporary" })],
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -49,6 +49,7 @@ describe("InstructionBuiltIns", () => {
|
|||||||
` Workspace root folder: ${projectDirectory}`,
|
` Workspace root folder: ${projectDirectory}`,
|
||||||
" Is directory a git repo: yes",
|
" Is directory a git repo: yes",
|
||||||
` Platform: ${process.platform}`,
|
` Platform: ${process.platform}`,
|
||||||
|
" Use /temporary for temporary work outside the workspace; it already exists and is pre-approved for external directory access.",
|
||||||
"</env>",
|
"</env>",
|
||||||
"",
|
"",
|
||||||
`Today's date: ${localDate(timestamp)}`,
|
`Today's date: ${localDate(timestamp)}`,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { Message, SystemPart } from "@opencode-ai/ai"
|
import { Message, SystemPart } from "@opencode-ai/ai"
|
||||||
import { DateTime, Effect, Schema } from "effect"
|
import { DateTime, Deferred, Effect, Fiber, Schema } from "effect"
|
||||||
import { Agent } from "@opencode-ai/core/agent"
|
import { Agent } from "@opencode-ai/core/agent"
|
||||||
import { Catalog } from "@opencode-ai/core/catalog"
|
import { Catalog } from "@opencode-ai/core/catalog"
|
||||||
import { Model } from "@opencode-ai/core/model"
|
import { Model } from "@opencode-ai/core/model"
|
||||||
@@ -15,7 +15,7 @@ import { SessionPending } from "@opencode-ai/core/session/pending"
|
|||||||
import { Tool } from "@opencode-ai/core/tool"
|
import { Tool } from "@opencode-ai/core/tool"
|
||||||
import { Provider } from "@opencode-ai/core/provider"
|
import { Provider } from "@opencode-ai/core/provider"
|
||||||
import { define } from "@opencode-ai/plugin/promise/plugin"
|
import { define } from "@opencode-ai/plugin/promise/plugin"
|
||||||
import type { SessionHooks } from "@opencode-ai/plugin/effect/session"
|
import type { SessionHooks, SessionHttpHandler } from "@opencode-ai/plugin/effect/session"
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
import { PluginTestLayer } from "./fixture"
|
import { PluginTestLayer } from "./fixture"
|
||||||
import { host as testHost } from "./host"
|
import { host as testHost } from "./host"
|
||||||
@@ -148,7 +148,9 @@ describe("fromPromise", () => {
|
|||||||
expect((await ctx.agent.get({ agentID: Agent.ID.make("reviewer") })).data).toMatchObject({
|
expect((await ctx.agent.get({ agentID: Agent.ID.make("reviewer") })).data).toMatchObject({
|
||||||
description: "Reviews code",
|
description: "Reviews code",
|
||||||
})
|
})
|
||||||
await expect(ctx.agent.get({ agentID: Agent.ID.make("missing") })).rejects.toThrow("Agent not found: missing")
|
await expect(ctx.agent.get({ agentID: Agent.ID.make("missing") })).rejects.toThrow(
|
||||||
|
"Agent not found: missing",
|
||||||
|
)
|
||||||
const models = (await ctx.catalog.model.list()).data
|
const models = (await ctx.catalog.model.list()).data
|
||||||
expect(models.find((model) => model.providerID === "test" && model.id === "alias")).toMatchObject({
|
expect(models.find((model) => model.providerID === "test" && model.id === "alias")).toMatchObject({
|
||||||
modelID: "gpt-5",
|
modelID: "gpt-5",
|
||||||
@@ -221,6 +223,105 @@ describe("fromPromise", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.effect("adapts promise session HTTP hooks", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const plugin = yield* Plugin.Service
|
||||||
|
const hooks = yield* PluginHooks.Service
|
||||||
|
const host = yield* PluginHost.make(plugin)
|
||||||
|
const bodies: string[] = []
|
||||||
|
yield* PluginPromise.fromPromise(
|
||||||
|
define({
|
||||||
|
id: "promise-session-http",
|
||||||
|
setup: async (ctx) => {
|
||||||
|
await ctx.session.hook("http", (event) => {
|
||||||
|
event.use(async (request, next) => {
|
||||||
|
request.headers.set("x-hook", "promise")
|
||||||
|
await next(request)
|
||||||
|
const response = await next(request)
|
||||||
|
return new Response(`${await response.text()}-response`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
await ctx.session.hook("http", (event) => {
|
||||||
|
event.use(async (request, next) => {
|
||||||
|
const response = await next(request)
|
||||||
|
return new Response(`${await response.text()}-outer`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).effect(host)
|
||||||
|
const middlewares: Parameters<PluginHooks.Domains["session"]["http"]["use"]>[0][] = []
|
||||||
|
const event: PluginHooks.Domains["session"]["http"] = {
|
||||||
|
sessionID: Session.ID.make("ses_promise_session_http"),
|
||||||
|
agent: Agent.ID.make("build"),
|
||||||
|
model: Model.Ref.make({ providerID: Provider.ID.make("test"), id: Model.ID.make("model") }),
|
||||||
|
use: (item) =>
|
||||||
|
Effect.sync(() => {
|
||||||
|
middlewares.push(item)
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
yield* hooks.trigger("session", "http", event)
|
||||||
|
const request = middlewares.reduce<SessionHttpHandler>(
|
||||||
|
(next, item) => (input: Request) => item(input, next),
|
||||||
|
(input: Request) =>
|
||||||
|
Effect.promise(() => input.text()).pipe(
|
||||||
|
Effect.tap((body) => Effect.sync(() => bodies.push(body))),
|
||||||
|
Effect.as(new Response(input.headers.get("x-hook") ?? "missing")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
const response = yield* request(new Request("https://provider.test", { method: "POST", body: "payload" }))
|
||||||
|
|
||||||
|
expect(bodies).toEqual(["payload", "payload"])
|
||||||
|
expect(yield* Effect.promise(() => response.text())).toBe("promise-response-outer")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("interrupts the Effect request through a promise session HTTP hook", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const plugin = yield* Plugin.Service
|
||||||
|
const hooks = yield* PluginHooks.Service
|
||||||
|
const host = yield* PluginHost.make(plugin)
|
||||||
|
yield* PluginPromise.fromPromise(
|
||||||
|
define({
|
||||||
|
id: "promise-session-http-interrupt",
|
||||||
|
setup: async (ctx) => {
|
||||||
|
await ctx.session.hook("http", (event) => {
|
||||||
|
event.use((request, next) => next(request))
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).effect(host)
|
||||||
|
const started = yield* Deferred.make<void>()
|
||||||
|
const interrupted = yield* Deferred.make<void>()
|
||||||
|
const middlewares: Parameters<PluginHooks.Domains["session"]["http"]["use"]>[0][] = []
|
||||||
|
const event: PluginHooks.Domains["session"]["http"] = {
|
||||||
|
sessionID: Session.ID.make("ses_promise_session_http_interrupt"),
|
||||||
|
agent: Agent.ID.make("build"),
|
||||||
|
model: Model.Ref.make({ providerID: Provider.ID.make("test"), id: Model.ID.make("model") }),
|
||||||
|
use: (item) =>
|
||||||
|
Effect.sync(() => {
|
||||||
|
middlewares.push(item)
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
yield* hooks.trigger("session", "http", event)
|
||||||
|
const request = middlewares.reduce<SessionHttpHandler>(
|
||||||
|
(next, item) => (input: Request) => item(input, next),
|
||||||
|
() =>
|
||||||
|
Deferred.succeed(started, undefined).pipe(
|
||||||
|
Effect.andThen(Effect.never),
|
||||||
|
Effect.onInterrupt(() => Deferred.succeed(interrupted, undefined)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
const fiber = yield* request(new Request("https://provider.test")).pipe(Effect.forkChild)
|
||||||
|
yield* Deferred.await(started)
|
||||||
|
yield* Fiber.interrupt(fiber)
|
||||||
|
|
||||||
|
expect(yield* Deferred.isDone(interrupted)).toBeTrue()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.effect("disposes a hook registration on request", () =>
|
it.effect("disposes a hook registration on request", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const agents = yield* Agent.Service
|
const agents = yield* Agent.Service
|
||||||
@@ -315,8 +416,7 @@ describe("fromPromise", () => {
|
|||||||
id: "promise-tool",
|
id: "promise-tool",
|
||||||
setup: async (ctx) => {
|
setup: async (ctx) => {
|
||||||
await ctx.tool.transform((tools) => {
|
await ctx.tool.transform((tools) => {
|
||||||
tools.add(
|
tools.add({
|
||||||
{
|
|
||||||
name: "hello",
|
name: "hello",
|
||||||
options: { codemode: false },
|
options: { codemode: false },
|
||||||
description: "Hello",
|
description: "Hello",
|
||||||
@@ -326,8 +426,7 @@ describe("fromPromise", () => {
|
|||||||
await context.progress({ phase: "greeting" })
|
await context.progress({ phase: "greeting" })
|
||||||
return { output: `Hello, ${name}!` }
|
return { output: `Hello, ${name}!` }
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
)
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,103 +0,0 @@
|
|||||||
import { AISDK } from "@opencode-ai/core/aisdk"
|
|
||||||
import { describe, expect } from "bun:test"
|
|
||||||
import { Effect } from "effect"
|
|
||||||
import { Model } from "@opencode-ai/core/model"
|
|
||||||
import { Plugin } from "@opencode-ai/core/plugin"
|
|
||||||
import { PluginHost } from "@opencode-ai/core/plugin/host"
|
|
||||||
import { GooglePlugin } from "@opencode-ai/core/plugin/provider/google"
|
|
||||||
import { Provider } from "@opencode-ai/core/provider"
|
|
||||||
import { testEffect } from "../lib/effect"
|
|
||||||
import { PluginTestLayer } from "./fixture"
|
|
||||||
|
|
||||||
const it = testEffect(PluginTestLayer)
|
|
||||||
|
|
||||||
const addPlugin = Effect.fn(function* () {
|
|
||||||
const plugin = yield* Plugin.Service
|
|
||||||
const aisdk = yield* AISDK.Service
|
|
||||||
const host = yield* PluginHost.make(plugin)
|
|
||||||
yield* GooglePlugin.effect(host)
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("GooglePlugin", () => {
|
|
||||||
it.effect("creates a Google Generative AI SDK for @ai-sdk/google using the provider ID as SDK name", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const plugin = yield* Plugin.Service
|
|
||||||
const aisdk = yield* AISDK.Service
|
|
||||||
yield* addPlugin()
|
|
||||||
const result = yield* aisdk.runSDK({
|
|
||||||
model: Model.Info.make({
|
|
||||||
...Model.Info.default(Provider.ID.make("custom-google"), Model.ID.make("gemini")),
|
|
||||||
modelID: Model.ID.make("gemini"),
|
|
||||||
package: "aisdk:@ai-sdk/google",
|
|
||||||
}),
|
|
||||||
package: "@ai-sdk/google",
|
|
||||||
options: { name: "custom-google", apiKey: "test" },
|
|
||||||
})
|
|
||||||
expect(result.sdk).toBeDefined()
|
|
||||||
expect(result.sdk?.languageModel("gemini").provider).toBe("custom-google")
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.effect("ignores non-Google SDK packages", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const plugin = yield* Plugin.Service
|
|
||||||
const aisdk = yield* AISDK.Service
|
|
||||||
yield* addPlugin()
|
|
||||||
const result = yield* aisdk.runSDK({
|
|
||||||
model: Model.Info.make({
|
|
||||||
...Model.Info.default(Provider.ID.make("google"), Model.ID.make("gemini")),
|
|
||||||
modelID: Model.ID.make("gemini"),
|
|
||||||
package: "aisdk:@ai-sdk/google",
|
|
||||||
}),
|
|
||||||
package: "@ai-sdk/google-vertex",
|
|
||||||
options: { name: "google" },
|
|
||||||
})
|
|
||||||
expect(result.sdk).toBeUndefined()
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.effect("uses default languageModel loading with provider ID parity", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const plugin = yield* Plugin.Service
|
|
||||||
const aisdk = yield* AISDK.Service
|
|
||||||
yield* addPlugin()
|
|
||||||
const sdkEvent = yield* aisdk.runSDK({
|
|
||||||
model: Model.Info.make({
|
|
||||||
...Model.Info.default(Provider.ID.make("custom-google"), Model.ID.make("alias")),
|
|
||||||
modelID: Model.ID.make("gemini-api"),
|
|
||||||
package: "aisdk:@ai-sdk/google",
|
|
||||||
}),
|
|
||||||
package: "@ai-sdk/google",
|
|
||||||
options: { name: "custom-google", apiKey: "test" },
|
|
||||||
})
|
|
||||||
const result = yield* aisdk.runLanguage({
|
|
||||||
model: sdkEvent.model,
|
|
||||||
sdk: sdkEvent.sdk,
|
|
||||||
options: sdkEvent.options,
|
|
||||||
})
|
|
||||||
const language = result.language ?? result.sdk.languageModel(result.model.modelID ?? result.model.id)
|
|
||||||
expect(language.modelId).toBe("gemini-api")
|
|
||||||
expect(language.provider).toBe("custom-google")
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.effect("wraps AI SDK language models for the native runner", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const aisdk = yield* AISDK.Service
|
|
||||||
yield* addPlugin()
|
|
||||||
|
|
||||||
const resolved = yield* aisdk.model(
|
|
||||||
Model.Info.make({
|
|
||||||
...Model.Info.default(Provider.ID.make("custom-google"), Model.ID.make("alias")),
|
|
||||||
modelID: Model.ID.make("gemini-api"),
|
|
||||||
package: "aisdk:@ai-sdk/google",
|
|
||||||
settings: { apiKey: "test" },
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(String(resolved.id)).toBe("gemini-api")
|
|
||||||
expect(String(resolved.provider)).toBe("custom-google")
|
|
||||||
expect(resolved.route.id).toBe("ai-sdk:@ai-sdk/google")
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
@@ -12,6 +12,7 @@ import { PluginHost } from "@opencode-ai/core/plugin/host"
|
|||||||
import { PluginHooks } from "@opencode-ai/core/plugin/hooks"
|
import { PluginHooks } from "@opencode-ai/core/plugin/hooks"
|
||||||
import { OpenAIPlugin } from "@opencode-ai/core/plugin/provider/openai"
|
import { OpenAIPlugin } from "@opencode-ai/core/plugin/provider/openai"
|
||||||
import { Provider } from "@opencode-ai/core/provider"
|
import { Provider } from "@opencode-ai/core/provider"
|
||||||
|
import type { SessionHttpHandler } from "@opencode-ai/plugin/effect/session"
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
import { PluginTestLayer } from "./fixture"
|
import { PluginTestLayer } from "./fixture"
|
||||||
|
|
||||||
@@ -29,6 +30,29 @@ function required<T>(value: T | undefined): T {
|
|||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const http = Effect.fn(function* (providerID: Provider.ID, url: string) {
|
||||||
|
const middlewares: Parameters<PluginHooks.Domains["session"]["http"]["use"]>[0][] = []
|
||||||
|
yield* (yield* PluginHooks.Service).trigger("session", "http", {
|
||||||
|
sessionID: Session.ID.make("ses_test"),
|
||||||
|
agent: Agent.ID.make("build"),
|
||||||
|
model: Model.Ref.make({ providerID, id: Model.ID.make("gpt-5.5") }),
|
||||||
|
use: (item) =>
|
||||||
|
Effect.sync(() => {
|
||||||
|
middlewares.push(item)
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
const request = middlewares.reduce<SessionHttpHandler>(
|
||||||
|
(next, item) => (input: Request) => item(input, next),
|
||||||
|
(input: Request) => {
|
||||||
|
const headers = new Headers(input.headers)
|
||||||
|
headers.set("x-seen-url", input.url)
|
||||||
|
return Effect.succeed(new Response(null, { headers }))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
const response = yield* request(new Request(url, { method: "POST", body: "{}" }))
|
||||||
|
return { url: response.headers.get("x-seen-url"), headers: Object.fromEntries(response.headers.entries()) }
|
||||||
|
})
|
||||||
|
|
||||||
describe("OpenAIPlugin", () => {
|
describe("OpenAIPlugin", () => {
|
||||||
it.effect("registers browser and headless ChatGPT OAuth methods", () =>
|
it.effect("registers browser and headless ChatGPT OAuth methods", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
@@ -100,33 +124,9 @@ describe("OpenAIPlugin", () => {
|
|||||||
})
|
})
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
|
|
||||||
const request = yield* (yield* PluginHooks.Service).trigger("session", "request", {
|
const request = yield* http(Provider.ID.openai, "https://api.openai.com/v1/responses")
|
||||||
sessionID: Session.ID.make("ses_test"),
|
const custom = yield* http(Provider.ID.make("custom-openai"), "https://custom.example/v1/responses")
|
||||||
agent: Agent.ID.make("build"),
|
const proxy = yield* http(Provider.ID.openai, "https://proxy.example/v1/responses?region=us")
|
||||||
model: Model.Ref.make({ providerID: Provider.ID.openai, id: Model.ID.make("gpt-5.5") }),
|
|
||||||
url: "https://api.openai.com/v1/responses",
|
|
||||||
method: "POST",
|
|
||||||
headers: {},
|
|
||||||
body: "{}",
|
|
||||||
})
|
|
||||||
const custom = yield* (yield* PluginHooks.Service).trigger("session", "request", {
|
|
||||||
sessionID: Session.ID.make("ses_test"),
|
|
||||||
agent: Agent.ID.make("build"),
|
|
||||||
model: Model.Ref.make({ providerID: Provider.ID.make("custom-openai"), id: Model.ID.make("gpt-5.5") }),
|
|
||||||
url: "https://custom.example/v1/responses",
|
|
||||||
method: "POST",
|
|
||||||
headers: {},
|
|
||||||
body: "{}",
|
|
||||||
})
|
|
||||||
const proxy = yield* (yield* PluginHooks.Service).trigger("session", "request", {
|
|
||||||
sessionID: Session.ID.make("ses_test"),
|
|
||||||
agent: Agent.ID.make("build"),
|
|
||||||
model: Model.Ref.make({ providerID: Provider.ID.openai, id: Model.ID.make("gpt-5.5") }),
|
|
||||||
url: "https://proxy.example/v1/responses?region=us",
|
|
||||||
method: "POST",
|
|
||||||
headers: {},
|
|
||||||
body: "{}",
|
|
||||||
})
|
|
||||||
|
|
||||||
const provider = required(yield* catalog.provider.get(Provider.ID.openai))
|
const provider = required(yield* catalog.provider.get(Provider.ID.openai))
|
||||||
expect(provider.package).toBe("@opencode-ai/ai/providers/openai")
|
expect(provider.package).toBe("@opencode-ai/ai/providers/openai")
|
||||||
@@ -134,7 +134,7 @@ describe("OpenAIPlugin", () => {
|
|||||||
expect(provider.headers).toMatchObject({ "chatgpt-account-id": "acct_123" })
|
expect(provider.headers).toMatchObject({ "chatgpt-account-id": "acct_123" })
|
||||||
expect(request.url).toBe("https://chatgpt.com/backend-api/codex/responses")
|
expect(request.url).toBe("https://chatgpt.com/backend-api/codex/responses")
|
||||||
expect(request.headers).toMatchObject({ originator: "opencode", "session-id": "ses_test" })
|
expect(request.headers).toMatchObject({ originator: "opencode", "session-id": "ses_test" })
|
||||||
expect(custom.headers).toEqual({})
|
expect(custom.headers).not.toHaveProperty("originator")
|
||||||
expect(proxy.url).toBe("https://proxy.example/v1/responses?region=us")
|
expect(proxy.url).toBe("https://proxy.example/v1/responses?region=us")
|
||||||
expect(proxy.headers).toMatchObject({ originator: "opencode", "session-id": "ses_test" })
|
expect(proxy.headers).toMatchObject({ originator: "opencode", "session-id": "ses_test" })
|
||||||
const eligible = required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5.5")))
|
const eligible = required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5.5")))
|
||||||
@@ -184,21 +184,13 @@ describe("OpenAIPlugin", () => {
|
|||||||
})
|
})
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
|
|
||||||
const request = yield* (yield* PluginHooks.Service).trigger("session", "request", {
|
const request = yield* http(Provider.ID.openai, "https://api.openai.com/v1/responses")
|
||||||
sessionID: Session.ID.make("ses_test"),
|
|
||||||
agent: Agent.ID.make("build"),
|
|
||||||
model: Model.Ref.make({ providerID: Provider.ID.openai, id: Model.ID.make("gpt-5.5") }),
|
|
||||||
url: "https://api.openai.com/v1/responses",
|
|
||||||
method: "POST",
|
|
||||||
headers: {},
|
|
||||||
body: "{}",
|
|
||||||
})
|
|
||||||
|
|
||||||
const model = required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5.5")))
|
const model = required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5.5")))
|
||||||
expect(model.package).toBe("@opencode-ai/ai/providers/openai")
|
expect(model.package).toBe("@opencode-ai/ai/providers/openai")
|
||||||
expect(model.enabled).toBe(true)
|
expect(model.enabled).toBe(true)
|
||||||
expect(model.limit).toEqual({ context: 1_050_000, input: 922_000, output: 128_000 })
|
expect(model.limit).toEqual({ context: 1_050_000, input: 922_000, output: 128_000 })
|
||||||
expect(request.headers).toEqual({})
|
expect(request.headers).not.toHaveProperty("originator")
|
||||||
expect(required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-4.1"))).enabled).toBe(true)
|
expect(required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-4.1"))).enabled).toBe(true)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { AISDK } from "@opencode-ai/core/aisdk"
|
|
||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { Catalog } from "@opencode-ai/core/catalog"
|
import { Catalog } from "@opencode-ai/core/catalog"
|
||||||
@@ -15,7 +14,6 @@ const it = testEffect(PluginTestLayer)
|
|||||||
|
|
||||||
const addPlugin = Effect.fn(function* () {
|
const addPlugin = Effect.fn(function* () {
|
||||||
const plugin = yield* Plugin.Service
|
const plugin = yield* Plugin.Service
|
||||||
const aisdk = yield* AISDK.Service
|
|
||||||
const host = yield* PluginHost.make(plugin)
|
const host = yield* PluginHost.make(plugin)
|
||||||
yield* OpenRouterPlugin.effect(host)
|
yield* OpenRouterPlugin.effect(host)
|
||||||
})
|
})
|
||||||
@@ -46,36 +44,6 @@ describe("OpenRouterPlugin", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("creates an SDK only for the OpenRouter package", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const plugin = yield* Plugin.Service
|
|
||||||
const aisdk = yield* AISDK.Service
|
|
||||||
yield* addPlugin()
|
|
||||||
|
|
||||||
const ignored = yield* aisdk.runSDK({
|
|
||||||
model: Model.Info.make({
|
|
||||||
...Model.Info.default(Provider.ID.openrouter, Model.ID.make("openai/gpt-5")),
|
|
||||||
modelID: Model.ID.make("openai/gpt-5"),
|
|
||||||
package: Provider.aisdk("test-provider"),
|
|
||||||
}),
|
|
||||||
package: "@ai-sdk/openai-compatible",
|
|
||||||
options: { name: "openrouter" },
|
|
||||||
})
|
|
||||||
expect(ignored.sdk).toBeUndefined()
|
|
||||||
|
|
||||||
const result = yield* aisdk.runSDK({
|
|
||||||
model: Model.Info.make({
|
|
||||||
...Model.Info.default(Provider.ID.make("custom"), Model.ID.make("openai/gpt-5")),
|
|
||||||
modelID: Model.ID.make("openai/gpt-5"),
|
|
||||||
package: Provider.aisdk("test-provider"),
|
|
||||||
}),
|
|
||||||
package: "@openrouter/ai-sdk-provider",
|
|
||||||
options: { name: "custom" },
|
|
||||||
})
|
|
||||||
expect(result.sdk).toBeDefined()
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.effect("filters OpenRouter's gpt-5 chat alias", () =>
|
it.effect("filters OpenRouter's gpt-5 chat alias", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import { AISDK } from "@opencode-ai/core/aisdk"
|
|
||||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
|
||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { Integration } from "@opencode-ai/core/integration"
|
import { Integration } from "@opencode-ai/core/integration"
|
||||||
import { Model } from "@opencode-ai/core/model"
|
|
||||||
import { Plugin } from "@opencode-ai/core/plugin"
|
import { Plugin } from "@opencode-ai/core/plugin"
|
||||||
import { PluginHost } from "@opencode-ai/core/plugin/host"
|
import { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||||
import { XAIPlugin } from "@opencode-ai/core/plugin/provider/xai"
|
import { XAIPlugin } from "@opencode-ai/core/plugin/provider/xai"
|
||||||
import { Provider } from "@opencode-ai/core/provider"
|
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
import { PluginTestLayer } from "./fixture"
|
import { PluginTestLayer } from "./fixture"
|
||||||
|
|
||||||
@@ -19,19 +15,6 @@ const addPlugin = Effect.fn(function* () {
|
|||||||
yield* XAIPlugin.effect(host)
|
yield* XAIPlugin.effect(host)
|
||||||
})
|
})
|
||||||
|
|
||||||
function fakeSelectorSdk(calls: string[]) {
|
|
||||||
const make = (method: string) => (id: string) => {
|
|
||||||
calls.push(`${method}:${id}`)
|
|
||||||
return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
responses: make("responses"),
|
|
||||||
messages: make("messages"),
|
|
||||||
chat: make("chat"),
|
|
||||||
languageModel: make("languageModel"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("XAIPlugin", () => {
|
describe("XAIPlugin", () => {
|
||||||
it.effect("registers browser OAuth, device OAuth, and API key methods", () =>
|
it.effect("registers browser OAuth, device OAuth, and API key methods", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
@@ -54,95 +37,4 @@ describe("XAIPlugin", () => {
|
|||||||
])
|
])
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("creates an xAI SDK only for @ai-sdk/xai", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const aisdk = yield* AISDK.Service
|
|
||||||
yield* addPlugin()
|
|
||||||
|
|
||||||
const ignored = yield* aisdk.runSDK({
|
|
||||||
model: Model.Info.make({
|
|
||||||
...Model.Info.default(Provider.ID.make("xai"), Model.ID.make("grok-4")),
|
|
||||||
modelID: Model.ID.make("grok-4"),
|
|
||||||
package: "aisdk:@ai-sdk/xai",
|
|
||||||
}),
|
|
||||||
package: "@ai-sdk/openai-compatible",
|
|
||||||
options: {},
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = yield* aisdk.runSDK({
|
|
||||||
model: Model.Info.make({
|
|
||||||
...Model.Info.default(Provider.ID.make("xai"), Model.ID.make("grok-4")),
|
|
||||||
modelID: Model.ID.make("grok-4"),
|
|
||||||
package: "aisdk:@ai-sdk/xai",
|
|
||||||
}),
|
|
||||||
package: "@ai-sdk/xai",
|
|
||||||
options: {},
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(ignored.sdk).toBeUndefined()
|
|
||||||
expect(typeof result.sdk?.responses).toBe("function")
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.effect("creates xAI SDKs for custom provider IDs", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const aisdk = yield* AISDK.Service
|
|
||||||
yield* addPlugin()
|
|
||||||
|
|
||||||
const result = yield* aisdk.runSDK({
|
|
||||||
model: Model.Info.make({
|
|
||||||
...Model.Info.default(Provider.ID.make("custom-xai"), Model.ID.make("grok-4")),
|
|
||||||
modelID: Model.ID.make("grok-4"),
|
|
||||||
package: "aisdk:@ai-sdk/xai",
|
|
||||||
}),
|
|
||||||
package: "@ai-sdk/xai",
|
|
||||||
options: {},
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(result.sdk.responses("grok-4").provider).toBe("xai.responses")
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.effect("uses responses with the model modelID for xAI language models", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const aisdk = yield* AISDK.Service
|
|
||||||
const calls: string[] = []
|
|
||||||
|
|
||||||
yield* addPlugin()
|
|
||||||
const result = yield* aisdk.runLanguage({
|
|
||||||
model: Model.Info.make({
|
|
||||||
...Model.Info.default(Provider.ID.make("xai"), Model.ID.make("alias")),
|
|
||||||
modelID: Model.ID.make("grok-4"),
|
|
||||||
package: "aisdk:@ai-sdk/xai",
|
|
||||||
}),
|
|
||||||
sdk: fakeSelectorSdk(calls),
|
|
||||||
options: {},
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(calls).toEqual(["responses:grok-4"])
|
|
||||||
expect(result.language).toBeDefined()
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.effect("ignores non-xAI providers", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const aisdk = yield* AISDK.Service
|
|
||||||
const calls: string[] = []
|
|
||||||
|
|
||||||
yield* addPlugin()
|
|
||||||
const result = yield* aisdk.runLanguage({
|
|
||||||
model: Model.Info.make({
|
|
||||||
...Model.Info.default(Provider.ID.openai, Model.ID.make("grok-4")),
|
|
||||||
modelID: Model.ID.make("grok-4"),
|
|
||||||
package: "aisdk:@ai-sdk/xai",
|
|
||||||
}),
|
|
||||||
sdk: fakeSelectorSdk(calls),
|
|
||||||
options: {},
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(calls).toEqual([])
|
|
||||||
expect(result.language).toBeUndefined()
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import { createXai } from "@ai-sdk/xai"
|
|
||||||
import { expect, test } from "bun:test"
|
|
||||||
|
|
||||||
test("xAI Responses sends promptCacheKey as prompt_cache_key", async () => {
|
|
||||||
let body: Record<string, unknown> | undefined
|
|
||||||
const mockFetch = Object.assign(
|
|
||||||
async (_input: Parameters<typeof fetch>[0], init?: RequestInit) => {
|
|
||||||
body = JSON.parse(String(init?.body))
|
|
||||||
return Response.json({
|
|
||||||
id: "response-1",
|
|
||||||
created_at: 0,
|
|
||||||
model: "grok-4",
|
|
||||||
object: "response",
|
|
||||||
output: [],
|
|
||||||
usage: { input_tokens: 1, output_tokens: 0 },
|
|
||||||
status: "completed",
|
|
||||||
})
|
|
||||||
},
|
|
||||||
{ preconnect: fetch.preconnect },
|
|
||||||
)
|
|
||||||
const model = createXai({
|
|
||||||
apiKey: "test",
|
|
||||||
fetch: mockFetch,
|
|
||||||
}).responses("grok-4")
|
|
||||||
|
|
||||||
await model.doGenerate({
|
|
||||||
prompt: [{ role: "user", content: [{ type: "text", text: "Hello" }] }],
|
|
||||||
providerOptions: { xai: { promptCacheKey: "session-123" } },
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(body?.prompt_cache_key).toBe("session-123")
|
|
||||||
})
|
|
||||||
@@ -41,6 +41,7 @@ import { SystemPromptPlugin } from "@opencode-ai/core/plugin/system-prompt"
|
|||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { eq } from "drizzle-orm"
|
import { eq } from "drizzle-orm"
|
||||||
import { Effect, Layer, Stream } from "effect"
|
import { Effect, Layer, Stream } from "effect"
|
||||||
|
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
|
||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
import { testEffect } from "./lib/effect"
|
import { testEffect } from "./lib/effect"
|
||||||
import { agentHost, catalogHost, host } from "./plugin/host"
|
import { agentHost, catalogHost, host } from "./plugin/host"
|
||||||
@@ -104,9 +105,10 @@ const promptCatalog = Layer.mock(Catalog.Service, {
|
|||||||
small: () => Effect.succeed(undefined),
|
small: () => Effect.succeed(undefined),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const runnerLayer = AppNodeBuilder.build(SessionRunnerLLM.node, [
|
const runnerLayer = (llmClient: Layer.Layer<typeof LLMClient.Service>) =>
|
||||||
|
AppNodeBuilder.build(SessionRunnerLLM.node, [
|
||||||
[Snapshot.node, Snapshot.noopLayer],
|
[Snapshot.node, Snapshot.noopLayer],
|
||||||
[LayerNodePlatform.llmClient, client],
|
[LayerNodePlatform.llmClient, llmClient],
|
||||||
[SessionRunnerModel.node, models],
|
[SessionRunnerModel.node, models],
|
||||||
[InstructionBuiltIns.node, systemContext],
|
[InstructionBuiltIns.node, systemContext],
|
||||||
[InstructionDiscovery.node, instructionContext],
|
[InstructionDiscovery.node, instructionContext],
|
||||||
@@ -117,8 +119,9 @@ const runnerLayer = AppNodeBuilder.build(SessionRunnerLLM.node, [
|
|||||||
[Config.node, config],
|
[Config.node, config],
|
||||||
[Permission.node, permission],
|
[Permission.node, permission],
|
||||||
[PluginSupervisor.node, pluginSupervisor],
|
[PluginSupervisor.node, pluginSupervisor],
|
||||||
])
|
])
|
||||||
const execution = Layer.effect(
|
const execution = (llmClient: Layer.Layer<typeof LLMClient.Service>) =>
|
||||||
|
Layer.effect(
|
||||||
SessionExecution.Service,
|
SessionExecution.Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const sessionRunner = yield* SessionRunner.Service
|
const sessionRunner = yield* SessionRunner.Service
|
||||||
@@ -133,8 +136,8 @@ const execution = Layer.effect(
|
|||||||
awaitIdle: coordinator.awaitIdle,
|
awaitIdle: coordinator.awaitIdle,
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
).pipe(Layer.provide(runnerLayer))
|
).pipe(Layer.provide(runnerLayer(llmClient)))
|
||||||
const it = testEffect(
|
const testLayer = (llmClient: Layer.Layer<typeof LLMClient.Service>) =>
|
||||||
AppNodeBuilder.build(
|
AppNodeBuilder.build(
|
||||||
LayerNode.group([
|
LayerNode.group([
|
||||||
Database.node,
|
Database.node,
|
||||||
@@ -156,7 +159,7 @@ const it = testEffect(
|
|||||||
Session.node,
|
Session.node,
|
||||||
]),
|
]),
|
||||||
[
|
[
|
||||||
[LayerNodePlatform.llmClient, client],
|
[LayerNodePlatform.llmClient, llmClient],
|
||||||
[Permission.node, permission],
|
[Permission.node, permission],
|
||||||
[Catalog.node, promptCatalog],
|
[Catalog.node, promptCatalog],
|
||||||
[SessionRunnerModel.node, models],
|
[SessionRunnerModel.node, models],
|
||||||
@@ -168,10 +171,10 @@ const it = testEffect(
|
|||||||
[Config.node, config],
|
[Config.node, config],
|
||||||
[Snapshot.node, Snapshot.noopLayer],
|
[Snapshot.node, Snapshot.noopLayer],
|
||||||
[PluginSupervisor.node, pluginSupervisor],
|
[PluginSupervisor.node, pluginSupervisor],
|
||||||
[SessionExecution.node, execution],
|
[SessionExecution.node, execution(llmClient)],
|
||||||
],
|
],
|
||||||
),
|
)
|
||||||
)
|
const it = testEffect(testLayer(client))
|
||||||
const sessionID = Session.ID.make("ses_runner_recorded")
|
const sessionID = Session.ID.make("ses_runner_recorded")
|
||||||
|
|
||||||
describe("SessionRunnerLLM recorded", () => {
|
describe("SessionRunnerLLM recorded", () => {
|
||||||
@@ -247,3 +250,90 @@ describe("SessionRunnerLLM recorded", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("SessionModelRequest HTTP bridge", () => {
|
||||||
|
const bodies: Uint8Array[] = []
|
||||||
|
const methods: string[] = []
|
||||||
|
const response = [
|
||||||
|
'data: {"id":"chatcmpl_test","object":"chat.completion.chunk","created":0,"model":"gpt-4o-mini","choices":[{"index":0,"delta":{"role":"assistant","content":"Hello!"},"finish_reason":null}]}',
|
||||||
|
'data: {"id":"chatcmpl_test","object":"chat.completion.chunk","created":0,"model":"gpt-4o-mini","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}',
|
||||||
|
"data: [DONE]",
|
||||||
|
"",
|
||||||
|
].join("\n\n")
|
||||||
|
const transport = Layer.succeed(
|
||||||
|
HttpClient.HttpClient,
|
||||||
|
HttpClient.make((request) =>
|
||||||
|
Effect.sync(() => {
|
||||||
|
if (request.body._tag !== "Uint8Array") throw new Error(`Unexpected request body: ${request.body._tag}`)
|
||||||
|
methods.push(request.method)
|
||||||
|
bodies.push(request.body.body.slice())
|
||||||
|
return HttpClientResponse.fromWeb(
|
||||||
|
request,
|
||||||
|
new Response(response, { headers: { "content-type": "text/event-stream" } }),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
const retryIt = testEffect(
|
||||||
|
testLayer(LLMClient.layer.pipe(Layer.provide(RequestExecutor.layer.pipe(Layer.provide(transport))))),
|
||||||
|
)
|
||||||
|
|
||||||
|
retryIt.effect("lets an Effect plugin send the same POST Request twice", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
bodies.length = 0
|
||||||
|
methods.length = 0
|
||||||
|
const agents = yield* Agent.Service
|
||||||
|
const catalog = yield* Catalog.Service
|
||||||
|
const hooks = yield* PluginHooks.Service
|
||||||
|
yield* agents.transform((draft) =>
|
||||||
|
draft.update(Agent.ID.make("build"), (agent) => {
|
||||||
|
agent.mode = "primary"
|
||||||
|
agent.permissions.push({ action: "execute", resource: "*", effect: "deny" })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
const pluginHost = host({
|
||||||
|
agent: agentHost(agents),
|
||||||
|
catalog: catalogHost(catalog),
|
||||||
|
session: { hook: (name, callback) => hooks.register("session", name, callback) },
|
||||||
|
})
|
||||||
|
yield* pluginHost.session.hook("http", (event) =>
|
||||||
|
event.use((request, next) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* next(request).pipe(Effect.flatMap((response) => Effect.promise(() => response.text())))
|
||||||
|
return yield* next(request)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
yield* Effect.forEach(SystemPromptPlugin.Plugins, (plugin) => plugin.effect(pluginHost), { discard: true })
|
||||||
|
const { db } = yield* Database.Service
|
||||||
|
yield* db
|
||||||
|
.insert(ProjectTable)
|
||||||
|
.values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] })
|
||||||
|
.onConflictDoNothing()
|
||||||
|
.run()
|
||||||
|
.pipe(Effect.orDie)
|
||||||
|
const retrySessionID = Session.ID.make("ses_model_request_http_retry")
|
||||||
|
yield* db
|
||||||
|
.insert(SessionTable)
|
||||||
|
.values({
|
||||||
|
id: retrySessionID,
|
||||||
|
project_id: Project.ID.global,
|
||||||
|
slug: "test",
|
||||||
|
directory: "/project",
|
||||||
|
title: "test",
|
||||||
|
version: "test",
|
||||||
|
})
|
||||||
|
.run()
|
||||||
|
.pipe(Effect.orDie)
|
||||||
|
const session = yield* Session.Service
|
||||||
|
yield* session.prompt({ sessionID: retrySessionID, text: "Say hello.", resume: false })
|
||||||
|
|
||||||
|
yield* session.resume(retrySessionID)
|
||||||
|
|
||||||
|
expect(methods).toEqual(["POST", "POST"])
|
||||||
|
expect(bodies).toHaveLength(2)
|
||||||
|
expect(bodies[0]?.byteLength).toBeGreaterThan(0)
|
||||||
|
expect(bodies[1]).toEqual(bodies[0])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|||||||
@@ -887,6 +887,27 @@ describe("SessionRunnerLLM", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.effect("executes a tool renamed by a session context hook", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const session = yield* setup
|
||||||
|
const hooks = yield* PluginHooks.Service
|
||||||
|
yield* hooks.register("session", "context", (event) =>
|
||||||
|
Effect.sync(() => {
|
||||||
|
event.tools.renamed_echo = event.tools.echo!
|
||||||
|
delete event.tools.echo
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
yield* admit(session, "Use the renamed tool")
|
||||||
|
yield* TestLLM.push(TestLLM.tool("call-renamed", "renamed_echo", { text: "renamed" }), [])
|
||||||
|
|
||||||
|
yield* session.resume(sessionID)
|
||||||
|
|
||||||
|
expect(requests[0]?.tools.map((tool) => tool.name)).toContain("renamed_echo")
|
||||||
|
expect(requests[0]?.tools.map((tool) => tool.name)).not.toContain("echo")
|
||||||
|
expect(executions).toEqual(["renamed"])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.effect("advertises and executes a location registered tool", () =>
|
it.effect("advertises and executes a location registered tool", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const session = yield* setup
|
const session = yield* setup
|
||||||
|
|||||||
@@ -2,11 +2,14 @@ import { $ } from "bun"
|
|||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Effect, Layer } from "effect"
|
import { Effect, Fiber, Layer, Stream } from "effect"
|
||||||
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
|
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
|
||||||
|
import { Bus } from "@opencode-ai/core/bus"
|
||||||
import { Location } from "@opencode-ai/core/location"
|
import { Location } from "@opencode-ai/core/location"
|
||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
import { Vcs } from "@opencode-ai/core/vcs"
|
import { Vcs } from "@opencode-ai/core/vcs"
|
||||||
|
import { FileSystem } from "@opencode-ai/schema/filesystem"
|
||||||
|
import { VcsEvent } from "@opencode-ai/schema/vcs-event"
|
||||||
import { location } from "./fixture/location"
|
import { location } from "./fixture/location"
|
||||||
import { tmpdir } from "./fixture/tmpdir"
|
import { tmpdir } from "./fixture/tmpdir"
|
||||||
import { it } from "./lib/effect"
|
import { it } from "./lib/effect"
|
||||||
@@ -15,7 +18,7 @@ const describeHg = Bun.which("hg") ? describe : describe.skip
|
|||||||
|
|
||||||
const provide = (directory: string) =>
|
const provide = (directory: string) =>
|
||||||
Effect.provide(
|
Effect.provide(
|
||||||
LayerNode.compile(Vcs.node, [
|
LayerNode.compile(LayerNode.group([Vcs.node, Bus.node]), [
|
||||||
[
|
[
|
||||||
Location.node,
|
Location.node,
|
||||||
Layer.succeed(
|
Layer.succeed(
|
||||||
@@ -37,6 +40,11 @@ const withTmp = <A, E, R>(f: (directory: string) => Effect.Effect<A, E, R>) =>
|
|||||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||||
).pipe(Effect.flatMap((tmp) => f(tmp.path)))
|
).pipe(Effect.flatMap((tmp) => f(tmp.path)))
|
||||||
|
|
||||||
|
const withHg = <A, E, R>(f: (directory: string) => Effect.Effect<A, E, R>) =>
|
||||||
|
withTmp((directory) =>
|
||||||
|
Effect.promise(() => hg(directory, "init")).pipe(Effect.andThen(f(directory).pipe(provide(directory)))),
|
||||||
|
)
|
||||||
|
|
||||||
async function hg(directory: string, ...args: string[]) {
|
async function hg(directory: string, ...args: string[]) {
|
||||||
await $`hg ${args}`.cwd(directory).env({ ...process.env, HGPLAIN: "1" }).quiet()
|
await $`hg ${args}`.cwd(directory).env({ ...process.env, HGPLAIN: "1" }).quiet()
|
||||||
}
|
}
|
||||||
@@ -48,10 +56,9 @@ async function commitAll(directory: string, message: string) {
|
|||||||
|
|
||||||
describeHg("Vcs mercurial", () => {
|
describeHg("Vcs mercurial", () => {
|
||||||
it.live("reports modified, missing, and untracked files", () =>
|
it.live("reports modified, missing, and untracked files", () =>
|
||||||
withTmp((directory) =>
|
withHg((directory) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* Effect.promise(async () => {
|
yield* Effect.promise(async () => {
|
||||||
await hg(directory, "init")
|
|
||||||
await fs.writeFile(path.join(directory, "keep.txt"), "one\ntwo\n")
|
await fs.writeFile(path.join(directory, "keep.txt"), "one\ntwo\n")
|
||||||
await fs.writeFile(path.join(directory, "gone.txt"), "bye\n")
|
await fs.writeFile(path.join(directory, "gone.txt"), "bye\n")
|
||||||
await commitAll(directory, "initial")
|
await commitAll(directory, "initial")
|
||||||
@@ -66,15 +73,14 @@ describeHg("Vcs mercurial", () => {
|
|||||||
{ file: "keep.txt", additions: 1, deletions: 1, status: "modified" },
|
{ file: "keep.txt", additions: 1, deletions: 1, status: "modified" },
|
||||||
{ file: "new.txt", additions: 2, deletions: 0, status: "added" },
|
{ file: "new.txt", additions: 2, deletions: 0, status: "added" },
|
||||||
])
|
])
|
||||||
}).pipe(provide(directory)),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.live("diffs the working copy with synthesized untracked and missing patches", () =>
|
it.live("diffs the working copy with synthesized untracked and missing patches", () =>
|
||||||
withTmp((directory) =>
|
withHg((directory) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* Effect.promise(async () => {
|
yield* Effect.promise(async () => {
|
||||||
await hg(directory, "init")
|
|
||||||
await fs.writeFile(path.join(directory, "keep.txt"), "one\ntwo\n")
|
await fs.writeFile(path.join(directory, "keep.txt"), "one\ntwo\n")
|
||||||
await fs.writeFile(path.join(directory, "gone.txt"), "bye\n")
|
await fs.writeFile(path.join(directory, "gone.txt"), "bye\n")
|
||||||
await commitAll(directory, "initial")
|
await commitAll(directory, "initial")
|
||||||
@@ -96,16 +102,47 @@ describeHg("Vcs mercurial", () => {
|
|||||||
expect(diff[1].deletions).toBe(1)
|
expect(diff[1].deletions).toBe(1)
|
||||||
expect(diff[2].patch).toContain("+hello")
|
expect(diff[2].patch).toContain("+hello")
|
||||||
expect(diff[2].additions).toBe(1)
|
expect(diff[2].additions).toBe(1)
|
||||||
}).pipe(provide(directory)),
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.live("caches branch info and publishes branch metadata changes", () =>
|
||||||
|
withHg((directory) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* Effect.promise(async () => {
|
||||||
|
await fs.writeFile(path.join(directory, "file.txt"), "one\n")
|
||||||
|
await commitAll(directory, "initial")
|
||||||
|
})
|
||||||
|
const vcs = yield* Vcs.Service
|
||||||
|
const bus = yield* Bus.Service
|
||||||
|
expect(yield* vcs.info()).toEqual({ branch: { current: "default", default: "default" } })
|
||||||
|
|
||||||
|
const updated = yield* bus.subscribe(VcsEvent.BranchUpdated).pipe(
|
||||||
|
Stream.take(1),
|
||||||
|
Stream.runHead,
|
||||||
|
Effect.forkScoped({ startImmediately: true }),
|
||||||
|
)
|
||||||
|
yield* Effect.promise(() => hg(directory, "branch", "-q", "feature"))
|
||||||
|
expect(yield* vcs.info()).toEqual({ branch: { current: "default", default: "default" } })
|
||||||
|
|
||||||
|
yield* bus.publish(FileSystem.Event.Changed, {
|
||||||
|
file: path.join(directory, ".hg", "branch"),
|
||||||
|
event: "change",
|
||||||
|
})
|
||||||
|
expect(yield* Fiber.join(updated)).toMatchObject({
|
||||||
|
_tag: "Some",
|
||||||
|
value: { location: { directory }, data: { branch: "feature" } },
|
||||||
|
})
|
||||||
|
expect(yield* vcs.info()).toEqual({ branch: { current: "feature", default: "default" } })
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.live("respects the context option", () =>
|
it.live("respects the context option", () =>
|
||||||
withTmp((directory) =>
|
withHg((directory) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const body = Array.from({ length: 20 }, (_, index) => `line-${index}`).join("\n") + "\n"
|
const body = Array.from({ length: 20 }, (_, index) => `line-${index}`).join("\n") + "\n"
|
||||||
yield* Effect.promise(async () => {
|
yield* Effect.promise(async () => {
|
||||||
await hg(directory, "init")
|
|
||||||
await fs.writeFile(path.join(directory, "file.txt"), body)
|
await fs.writeFile(path.join(directory, "file.txt"), body)
|
||||||
await commitAll(directory, "initial")
|
await commitAll(directory, "initial")
|
||||||
await fs.writeFile(path.join(directory, "file.txt"), body.replace("line-10", "changed"))
|
await fs.writeFile(path.join(directory, "file.txt"), body.replace("line-10", "changed"))
|
||||||
@@ -117,15 +154,14 @@ describeHg("Vcs mercurial", () => {
|
|||||||
const tight = yield* vcs.diff("working", { context: 1 })
|
const tight = yield* vcs.diff("working", { context: 1 })
|
||||||
expect(tight[0].patch).toContain("line-9")
|
expect(tight[0].patch).toContain("line-9")
|
||||||
expect(tight[0].patch).not.toContain("line-0")
|
expect(tight[0].patch).not.toContain("line-0")
|
||||||
}).pipe(provide(directory)),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.live("diffs before the first commit", () =>
|
it.live("diffs before the first commit", () =>
|
||||||
withTmp((directory) =>
|
withHg((directory) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* Effect.promise(async () => {
|
yield* Effect.promise(async () => {
|
||||||
await hg(directory, "init")
|
|
||||||
await fs.writeFile(path.join(directory, "tracked.txt"), "a\nb\n")
|
await fs.writeFile(path.join(directory, "tracked.txt"), "a\nb\n")
|
||||||
await hg(directory, "add", "-q", "tracked.txt")
|
await hg(directory, "add", "-q", "tracked.txt")
|
||||||
await fs.writeFile(path.join(directory, "loose.txt"), "hello\n")
|
await fs.writeFile(path.join(directory, "loose.txt"), "hello\n")
|
||||||
@@ -139,15 +175,14 @@ describeHg("Vcs mercurial", () => {
|
|||||||
expect(diff).toHaveLength(2)
|
expect(diff).toHaveLength(2)
|
||||||
expect(diff[0].patch).toContain("+hello")
|
expect(diff[0].patch).toContain("+hello")
|
||||||
expect(diff[1].patch).toContain("+a")
|
expect(diff[1].patch).toContain("+a")
|
||||||
}).pipe(provide(directory)),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.live("diffs a named branch against the default branch", () =>
|
it.live("diffs a named branch against the default branch", () =>
|
||||||
withTmp((directory) =>
|
withHg((directory) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* Effect.promise(async () => {
|
yield* Effect.promise(async () => {
|
||||||
await hg(directory, "init")
|
|
||||||
await fs.writeFile(path.join(directory, "file.txt"), "one\n")
|
await fs.writeFile(path.join(directory, "file.txt"), "one\n")
|
||||||
await commitAll(directory, "initial")
|
await commitAll(directory, "initial")
|
||||||
})
|
})
|
||||||
@@ -164,7 +199,7 @@ describeHg("Vcs mercurial", () => {
|
|||||||
{ file: "file.txt", status: "modified" },
|
{ file: "file.txt", status: "modified" },
|
||||||
])
|
])
|
||||||
expect(diff[0].patch).toContain("+two")
|
expect(diff[0].patch).toContain("+two")
|
||||||
}).pipe(provide(directory)),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,18 +2,21 @@ import { $ } from "bun"
|
|||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Effect, Layer } from "effect"
|
import { Effect, Fiber, Layer, Stream } from "effect"
|
||||||
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
|
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
|
||||||
|
import { Bus } from "@opencode-ai/core/bus"
|
||||||
import { Location } from "@opencode-ai/core/location"
|
import { Location } from "@opencode-ai/core/location"
|
||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
import { Vcs } from "@opencode-ai/core/vcs"
|
import { Vcs } from "@opencode-ai/core/vcs"
|
||||||
|
import { FileSystem } from "@opencode-ai/schema/filesystem"
|
||||||
|
import { VcsEvent } from "@opencode-ai/schema/vcs-event"
|
||||||
import { location } from "./fixture/location"
|
import { location } from "./fixture/location"
|
||||||
import { tmpdir } from "./fixture/tmpdir"
|
import { tmpdir } from "./fixture/tmpdir"
|
||||||
import { it } from "./lib/effect"
|
import { it } from "./lib/effect"
|
||||||
|
|
||||||
const provide = (directory: string, input: { git?: boolean } = {}) =>
|
const provide = (directory: string, input: { git?: boolean } = {}) =>
|
||||||
Effect.provide(
|
Effect.provide(
|
||||||
LayerNode.compile(Vcs.node, [
|
LayerNode.compile(LayerNode.group([Vcs.node, Bus.node]), [
|
||||||
[
|
[
|
||||||
Location.node,
|
Location.node,
|
||||||
Layer.succeed(
|
Layer.succeed(
|
||||||
@@ -35,6 +38,13 @@ const withTmp = <A, E, R>(f: (directory: string) => Effect.Effect<A, E, R>) =>
|
|||||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||||
).pipe(Effect.flatMap((tmp) => f(tmp.path)))
|
).pipe(Effect.flatMap((tmp) => f(tmp.path)))
|
||||||
|
|
||||||
|
const withGit = <A, E, R>(f: (directory: string) => Effect.Effect<A, E, R>) =>
|
||||||
|
withTmp((directory) =>
|
||||||
|
Effect.promise(() => initRepo(directory)).pipe(
|
||||||
|
Effect.andThen(f(directory).pipe(provide(directory, { git: true }))),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
async function initRepo(directory: string) {
|
async function initRepo(directory: string) {
|
||||||
await $`git init -b main`.cwd(directory).quiet()
|
await $`git init -b main`.cwd(directory).quiet()
|
||||||
await $`git config core.fsmonitor false`.cwd(directory).quiet()
|
await $`git config core.fsmonitor false`.cwd(directory).quiet()
|
||||||
@@ -53,6 +63,7 @@ describe("Vcs", () => {
|
|||||||
withTmp((directory) =>
|
withTmp((directory) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const vcs = yield* Vcs.Service
|
const vcs = yield* Vcs.Service
|
||||||
|
expect(yield* vcs.info()).toEqual({ branch: {} })
|
||||||
expect(yield* vcs.status()).toEqual([])
|
expect(yield* vcs.status()).toEqual([])
|
||||||
expect(yield* vcs.diff("working")).toEqual([])
|
expect(yield* vcs.diff("working")).toEqual([])
|
||||||
expect(yield* vcs.diff("branch")).toEqual([])
|
expect(yield* vcs.diff("branch")).toEqual([])
|
||||||
@@ -61,10 +72,9 @@ describe("Vcs", () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
it.live("reports modified, deleted, and untracked files", () =>
|
it.live("reports modified, deleted, and untracked files", () =>
|
||||||
withTmp((directory) =>
|
withGit((directory) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* Effect.promise(async () => {
|
yield* Effect.promise(async () => {
|
||||||
await initRepo(directory)
|
|
||||||
await fs.writeFile(path.join(directory, "keep.txt"), "one\ntwo\n")
|
await fs.writeFile(path.join(directory, "keep.txt"), "one\ntwo\n")
|
||||||
await fs.writeFile(path.join(directory, "gone.txt"), "bye\n")
|
await fs.writeFile(path.join(directory, "gone.txt"), "bye\n")
|
||||||
await commitAll(directory, "initial")
|
await commitAll(directory, "initial")
|
||||||
@@ -79,15 +89,45 @@ describe("Vcs", () => {
|
|||||||
{ file: "keep.txt", additions: 1, deletions: 1, status: "modified" },
|
{ file: "keep.txt", additions: 1, deletions: 1, status: "modified" },
|
||||||
{ file: "new.txt", additions: 2, deletions: 0, status: "added" },
|
{ file: "new.txt", additions: 2, deletions: 0, status: "added" },
|
||||||
])
|
])
|
||||||
}).pipe(provide(directory, { git: true })),
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.live("caches branch info and publishes HEAD changes", () =>
|
||||||
|
withGit((directory) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* Effect.promise(async () => {
|
||||||
|
await fs.writeFile(path.join(directory, "file.txt"), "one\n")
|
||||||
|
await commitAll(directory, "initial")
|
||||||
|
})
|
||||||
|
const vcs = yield* Vcs.Service
|
||||||
|
const bus = yield* Bus.Service
|
||||||
|
expect(yield* vcs.info()).toEqual({ branch: { current: "main", default: undefined } })
|
||||||
|
|
||||||
|
const updated = yield* bus.subscribe(VcsEvent.BranchUpdated).pipe(
|
||||||
|
Stream.take(1),
|
||||||
|
Stream.runHead,
|
||||||
|
Effect.forkScoped({ startImmediately: true }),
|
||||||
|
)
|
||||||
|
yield* Effect.promise(() => $`git checkout -q -b feature`.cwd(directory).quiet())
|
||||||
|
|
||||||
|
yield* bus.publish(FileSystem.Event.Changed, { file: path.join(directory, "HEAD"), event: "change" })
|
||||||
|
expect(yield* vcs.info()).toEqual({ branch: { current: "main", default: undefined } })
|
||||||
|
|
||||||
|
yield* bus.publish(FileSystem.Event.Changed, { file: path.join(directory, ".git", "HEAD"), event: "change" })
|
||||||
|
expect(yield* Fiber.join(updated)).toMatchObject({
|
||||||
|
_tag: "Some",
|
||||||
|
value: { location: { directory }, data: { branch: "feature" } },
|
||||||
|
})
|
||||||
|
expect(yield* vcs.info()).toEqual({ branch: { current: "feature", default: "main" } })
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.live("diffs the working copy against HEAD with patches", () =>
|
it.live("diffs the working copy against HEAD with patches", () =>
|
||||||
withTmp((directory) =>
|
withGit((directory) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* Effect.promise(async () => {
|
yield* Effect.promise(async () => {
|
||||||
await initRepo(directory)
|
|
||||||
await fs.writeFile(path.join(directory, "keep.txt"), "one\ntwo\n")
|
await fs.writeFile(path.join(directory, "keep.txt"), "one\ntwo\n")
|
||||||
await commitAll(directory, "initial")
|
await commitAll(directory, "initial")
|
||||||
await fs.writeFile(path.join(directory, "keep.txt"), "one\nthree\n")
|
await fs.writeFile(path.join(directory, "keep.txt"), "one\nthree\n")
|
||||||
@@ -105,16 +145,15 @@ describe("Vcs", () => {
|
|||||||
expect(diff[0].deletions).toBe(1)
|
expect(diff[0].deletions).toBe(1)
|
||||||
expect(diff[1].patch).toContain("+hello")
|
expect(diff[1].patch).toContain("+hello")
|
||||||
expect(diff[1].additions).toBe(1)
|
expect(diff[1].additions).toBe(1)
|
||||||
}).pipe(provide(directory, { git: true })),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.live("respects the context option", () =>
|
it.live("respects the context option", () =>
|
||||||
withTmp((directory) =>
|
withGit((directory) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const body = Array.from({ length: 20 }, (_, index) => `line-${index}`).join("\n") + "\n"
|
const body = Array.from({ length: 20 }, (_, index) => `line-${index}`).join("\n") + "\n"
|
||||||
yield* Effect.promise(async () => {
|
yield* Effect.promise(async () => {
|
||||||
await initRepo(directory)
|
|
||||||
await fs.writeFile(path.join(directory, "file.txt"), body)
|
await fs.writeFile(path.join(directory, "file.txt"), body)
|
||||||
await commitAll(directory, "initial")
|
await commitAll(directory, "initial")
|
||||||
await fs.writeFile(path.join(directory, "file.txt"), body.replace("line-10", "changed"))
|
await fs.writeFile(path.join(directory, "file.txt"), body.replace("line-10", "changed"))
|
||||||
@@ -126,15 +165,14 @@ describe("Vcs", () => {
|
|||||||
const tight = yield* vcs.diff("working", { context: 1 })
|
const tight = yield* vcs.diff("working", { context: 1 })
|
||||||
expect(tight[0].patch).toContain("line-9")
|
expect(tight[0].patch).toContain("line-9")
|
||||||
expect(tight[0].patch).not.toContain("line-0")
|
expect(tight[0].patch).not.toContain("line-0")
|
||||||
}).pipe(provide(directory, { git: true })),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.live("diffs before the first commit", () =>
|
it.live("diffs before the first commit", () =>
|
||||||
withTmp((directory) =>
|
withGit((directory) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* Effect.promise(async () => {
|
yield* Effect.promise(async () => {
|
||||||
await initRepo(directory)
|
|
||||||
await fs.writeFile(path.join(directory, "new.txt"), "hello\n")
|
await fs.writeFile(path.join(directory, "new.txt"), "hello\n")
|
||||||
})
|
})
|
||||||
const vcs = yield* Vcs.Service
|
const vcs = yield* Vcs.Service
|
||||||
@@ -142,15 +180,14 @@ describe("Vcs", () => {
|
|||||||
const diff = yield* vcs.diff("working")
|
const diff = yield* vcs.diff("working")
|
||||||
expect(diff).toHaveLength(1)
|
expect(diff).toHaveLength(1)
|
||||||
expect(diff[0].patch).toContain("+hello")
|
expect(diff[0].patch).toContain("+hello")
|
||||||
}).pipe(provide(directory, { git: true })),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.live("diffs a feature branch against the default branch", () =>
|
it.live("diffs a feature branch against the default branch", () =>
|
||||||
withTmp((directory) =>
|
withGit((directory) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* Effect.promise(async () => {
|
yield* Effect.promise(async () => {
|
||||||
await initRepo(directory)
|
|
||||||
await fs.writeFile(path.join(directory, "file.txt"), "one\n")
|
await fs.writeFile(path.join(directory, "file.txt"), "one\n")
|
||||||
await commitAll(directory, "initial")
|
await commitAll(directory, "initial")
|
||||||
})
|
})
|
||||||
@@ -167,7 +204,7 @@ describe("Vcs", () => {
|
|||||||
{ file: "file.txt", status: "modified" },
|
{ file: "file.txt", status: "modified" },
|
||||||
])
|
])
|
||||||
expect(diff[0].patch).toContain("+two")
|
expect(diff[0].patch).toContain("+two")
|
||||||
}).pipe(provide(directory, { git: true })),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import type { SessionApi } from "@opencode-ai/client/effect/api"
|
import type { SessionApi } from "@opencode-ai/client/effect/api"
|
||||||
import type { Message, SystemPart } from "@opencode-ai/ai"
|
import type { Message, SystemPart } from "@opencode-ai/ai"
|
||||||
import type { HttpRequest } from "@opencode-ai/ai/route"
|
|
||||||
import type { Agent } from "@opencode-ai/schema/agent"
|
import type { Agent } from "@opencode-ai/schema/agent"
|
||||||
import type { Model } from "@opencode-ai/schema/model"
|
import type { Model } from "@opencode-ai/schema/model"
|
||||||
import type { Session } from "@opencode-ai/schema/session"
|
import type { Session } from "@opencode-ai/schema/session"
|
||||||
import type { JsonSchema } from "effect"
|
import type { Effect, JsonSchema } from "effect"
|
||||||
import type { Hooks } from "./registration.js"
|
import type { Hooks } from "./registration.js"
|
||||||
|
|
||||||
export interface SessionContext {
|
export interface SessionContext {
|
||||||
@@ -16,15 +15,23 @@ export interface SessionContext {
|
|||||||
tools: Record<string, { description: string; input: JsonSchema.JsonSchema }>
|
tools: Record<string, { description: string; input: JsonSchema.JsonSchema }>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SessionRequest extends HttpRequest {
|
export interface SessionHttp {
|
||||||
readonly sessionID: Session.ID
|
readonly sessionID: Session.ID
|
||||||
readonly agent: Agent.ID
|
readonly agent: Agent.ID
|
||||||
readonly model: Model.Ref
|
readonly model: Model.Ref
|
||||||
|
readonly use: (middleware: SessionHttpMiddleware) => Effect.Effect<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SessionHttpHandler = (request: Request) => Effect.Effect<Response, Error>
|
||||||
|
|
||||||
|
export type SessionHttpMiddleware = (
|
||||||
|
request: Request,
|
||||||
|
next: SessionHttpHandler,
|
||||||
|
) => Effect.Effect<Response, Error>
|
||||||
|
|
||||||
export interface SessionHooks {
|
export interface SessionHooks {
|
||||||
readonly context: SessionContext
|
readonly context: SessionContext
|
||||||
readonly request: SessionRequest
|
readonly http: SessionHttp
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SessionDomain = Pick<
|
export type SessionDomain = Pick<
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { SessionApi } from "@opencode-ai/client/promise/api"
|
import type { SessionApi } from "@opencode-ai/client/promise/api"
|
||||||
import type { Message, SystemPart } from "@opencode-ai/ai"
|
import type { Message, SystemPart } from "@opencode-ai/ai"
|
||||||
import type { HttpRequest } from "@opencode-ai/ai/route"
|
|
||||||
import type { Agent } from "@opencode-ai/schema/agent"
|
import type { Agent } from "@opencode-ai/schema/agent"
|
||||||
import type { Model } from "@opencode-ai/schema/model"
|
import type { Model } from "@opencode-ai/schema/model"
|
||||||
import type { Session } from "@opencode-ai/schema/session"
|
import type { Session } from "@opencode-ai/schema/session"
|
||||||
@@ -16,15 +15,23 @@ export interface SessionContext {
|
|||||||
tools: Record<string, { description: string; input: JsonSchema.JsonSchema }>
|
tools: Record<string, { description: string; input: JsonSchema.JsonSchema }>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SessionRequest extends HttpRequest {
|
export interface SessionHttp {
|
||||||
readonly sessionID: Session.ID
|
readonly sessionID: Session.ID
|
||||||
readonly agent: Agent.ID
|
readonly agent: Agent.ID
|
||||||
readonly model: Model.Ref
|
readonly model: Model.Ref
|
||||||
|
readonly use: (middleware: SessionHttpMiddleware) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SessionHttpHandler = (request: Request) => Promise<Response>
|
||||||
|
|
||||||
|
export type SessionHttpMiddleware = (
|
||||||
|
request: Request,
|
||||||
|
next: SessionHttpHandler,
|
||||||
|
) => Promise<Response> | Response
|
||||||
|
|
||||||
export interface SessionHooks {
|
export interface SessionHooks {
|
||||||
readonly context: SessionContext
|
readonly context: SessionContext
|
||||||
readonly request: SessionRequest
|
readonly http: SessionHttp
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SessionDomain = Pick<
|
export type SessionDomain = Pick<
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import type {
|
|||||||
SessionPendingInfo,
|
SessionPendingInfo,
|
||||||
ShellInfo,
|
ShellInfo,
|
||||||
SkillInfo,
|
SkillInfo,
|
||||||
|
VcsInfo,
|
||||||
} from "@opencode-ai/client"
|
} from "@opencode-ai/client"
|
||||||
import type { ResolvedTheme } from "@opencode-ai/theme/tui"
|
import type { ResolvedTheme } from "@opencode-ai/theme/tui"
|
||||||
import type { CliRenderer, KeyEvent, Renderable } from "@opentui/core"
|
import type { CliRenderer, KeyEvent, Renderable } from "@opentui/core"
|
||||||
@@ -113,6 +114,11 @@ export interface Data {
|
|||||||
default(): LocationRef
|
default(): LocationRef
|
||||||
sync(location?: LocationRef): Promise<void>
|
sync(location?: LocationRef): Promise<void>
|
||||||
invalidate(location?: LocationRef): void
|
invalidate(location?: LocationRef): void
|
||||||
|
readonly vcs: {
|
||||||
|
info(location?: LocationRef): VcsInfo | undefined
|
||||||
|
sync(location?: LocationRef): Promise<void>
|
||||||
|
invalidate(location?: LocationRef): void
|
||||||
|
}
|
||||||
readonly agent: LocationCollection<AgentInfo>
|
readonly agent: LocationCollection<AgentInfo>
|
||||||
readonly command: LocationCollection<CommandInfo>
|
readonly command: LocationCollection<CommandInfo>
|
||||||
readonly integration: LocationCollection<IntegrationInfo>
|
readonly integration: LocationCollection<IntegrationInfo>
|
||||||
|
|||||||
@@ -851,6 +851,16 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"title": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"agent": {
|
"agent": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@@ -11301,6 +11311,104 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/vcs": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"vcs"
|
||||||
|
],
|
||||||
|
"operationId": "v2.vcs.get",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "location",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"directory": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"workspace": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"required": false,
|
||||||
|
"style": "deepObject",
|
||||||
|
"explode": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"security": [],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Success",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"location": {
|
||||||
|
"$ref": "#/components/schemas/Location.Info"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/components/schemas/Vcs.Info"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"location",
|
||||||
|
"data"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "InvalidRequestError",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/InvalidRequestError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "UnauthorizedError",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UnauthorizedError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Get current and default branch information for the requested location.",
|
||||||
|
"summary": "VCS info"
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/vcs/status": {
|
"/api/vcs/status": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -12032,11 +12140,15 @@
|
|||||||
},
|
},
|
||||||
"directory": {
|
"directory": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"canonical": {
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"id",
|
"id",
|
||||||
"directory"
|
"directory",
|
||||||
|
"canonical"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
@@ -12498,7 +12610,6 @@
|
|||||||
"cost",
|
"cost",
|
||||||
"tokens",
|
"tokens",
|
||||||
"time",
|
"time",
|
||||||
"title",
|
|
||||||
"location"
|
"location"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
@@ -13837,6 +13948,15 @@
|
|||||||
},
|
},
|
||||||
"message": {
|
"message": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "integer",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"minimum": 100,
|
||||||
|
"maximum": 599
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -20765,7 +20885,7 @@
|
|||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"worktree": {
|
"canonical": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"vcs": {
|
"vcs": {
|
||||||
@@ -20792,7 +20912,7 @@
|
|||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"id",
|
"id",
|
||||||
"worktree",
|
"canonical",
|
||||||
"time",
|
"time",
|
||||||
"sandboxes"
|
"sandboxes"
|
||||||
],
|
],
|
||||||
@@ -20806,11 +20926,15 @@
|
|||||||
},
|
},
|
||||||
"directory": {
|
"directory": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"canonical": {
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"id",
|
"id",
|
||||||
"directory"
|
"directory",
|
||||||
|
"canonical"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
@@ -22445,7 +22569,6 @@
|
|||||||
"slug",
|
"slug",
|
||||||
"projectID",
|
"projectID",
|
||||||
"directory",
|
"directory",
|
||||||
"title",
|
|
||||||
"version",
|
"version",
|
||||||
"time"
|
"time"
|
||||||
],
|
],
|
||||||
@@ -29127,6 +29250,30 @@
|
|||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
|
"Vcs.Branch": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"current": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"Vcs.Info": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"branch": {
|
||||||
|
"$ref": "#/components/schemas/Vcs.Branch"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"branch"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
"Vcs.FileStatus": {
|
"Vcs.FileStatus": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -13,6 +13,20 @@ const DiffQuery = Schema.Struct({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const VcsGroup = HttpApiGroup.make("server.vcs")
|
export const VcsGroup = HttpApiGroup.make("server.vcs")
|
||||||
|
.add(
|
||||||
|
HttpApiEndpoint.get("vcs.get", "/api/vcs", {
|
||||||
|
query: LocationQuery,
|
||||||
|
success: Location.response(Vcs.Info),
|
||||||
|
})
|
||||||
|
.annotateMerge(locationQueryOpenApi)
|
||||||
|
.annotateMerge(
|
||||||
|
OpenApi.annotations({
|
||||||
|
identifier: "v2.vcs.get",
|
||||||
|
summary: "VCS info",
|
||||||
|
description: "Get current and default branch information for the requested location.",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
.add(
|
.add(
|
||||||
HttpApiEndpoint.get("vcs.status", "/api/vcs/status", {
|
HttpApiEndpoint.get("vcs.status", "/api/vcs/status", {
|
||||||
query: LocationQuery,
|
query: LocationQuery,
|
||||||
|
|||||||
@@ -46,6 +46,9 @@ export const Info = Schema.Struct({
|
|||||||
permissions: [
|
permissions: [
|
||||||
{ action: "*", resource: "*", effect: "allow" },
|
{ action: "*", resource: "*", effect: "allow" },
|
||||||
{ action: "external_directory", resource: "*", effect: "ask" },
|
{ action: "external_directory", resource: "*", effect: "ask" },
|
||||||
|
{ action: "read", resource: "*.env", effect: "ask" },
|
||||||
|
{ action: "read", resource: "*.env.*", effect: "ask" },
|
||||||
|
{ action: "read", resource: "*.env.example", effect: "allow" },
|
||||||
],
|
],
|
||||||
}) satisfies Info,
|
}) satisfies Info,
|
||||||
})),
|
})),
|
||||||
|
|||||||
@@ -1,7 +1,18 @@
|
|||||||
export * as Vcs from "./vcs.js"
|
export * as Vcs from "./vcs.js"
|
||||||
|
|
||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
import { NonNegativeInt } from "./schema.js"
|
import { NonNegativeInt, optional } from "./schema.js"
|
||||||
|
|
||||||
|
export const Branch = Schema.Struct({
|
||||||
|
current: optional(Schema.String),
|
||||||
|
default: optional(Schema.String),
|
||||||
|
}).annotate({ identifier: "Vcs.Branch" })
|
||||||
|
export interface Branch extends Schema.Schema.Type<typeof Branch> {}
|
||||||
|
|
||||||
|
export const Info = Schema.Struct({
|
||||||
|
branch: Branch,
|
||||||
|
}).annotate({ identifier: "Vcs.Info" })
|
||||||
|
export interface Info extends Schema.Schema.Type<typeof Info> {}
|
||||||
|
|
||||||
export const Mode = Schema.Literals(["working", "branch"]).annotate({ identifier: "Vcs.Mode" })
|
export const Mode = Schema.Literals(["working", "branch"]).annotate({ identifier: "Vcs.Mode" })
|
||||||
export type Mode = typeof Mode.Type
|
export type Mode = typeof Mode.Type
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { FileDiff } from "../src/file-diff.js"
|
|||||||
import { Money } from "../src/money.js"
|
import { Money } from "../src/money.js"
|
||||||
import { Skill } from "../src/skill.js"
|
import { Skill } from "../src/skill.js"
|
||||||
import { Shell } from "../src/shell.js"
|
import { Shell } from "../src/shell.js"
|
||||||
|
import { Vcs } from "../src/vcs.js"
|
||||||
import { PersistedRevert } from "../src/session-revert.js"
|
import { PersistedRevert } from "../src/session-revert.js"
|
||||||
import { AbsolutePath, optional } from "../src/schema.js"
|
import { AbsolutePath, optional } from "../src/schema.js"
|
||||||
|
|
||||||
@@ -134,6 +135,10 @@ describe("contract hygiene", () => {
|
|||||||
expect(Pty.ID.create()).toStartWith("pty_")
|
expect(Pty.ID.create()).toStartWith("pty_")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("VCS info omits unavailable branch names", () => {
|
||||||
|
expect(Schema.encodeSync(Vcs.Info)({ branch: { current: undefined, default: undefined } })).toEqual({ branch: {} })
|
||||||
|
})
|
||||||
|
|
||||||
test("reusable public identifiers are stable and unique", () => {
|
test("reusable public identifiers are stable and unique", () => {
|
||||||
const identifiers = [
|
const identifiers = [
|
||||||
Agent.Color,
|
Agent.Color,
|
||||||
@@ -166,6 +171,8 @@ describe("contract hygiene", () => {
|
|||||||
SessionPending.SyntheticData,
|
SessionPending.SyntheticData,
|
||||||
SessionPending.User,
|
SessionPending.User,
|
||||||
SessionPending.Synthetic,
|
SessionPending.Synthetic,
|
||||||
|
Vcs.Branch,
|
||||||
|
Vcs.Info,
|
||||||
].map((schema) => schema.ast.annotations?.identifier)
|
].map((schema) => schema.ast.annotations?.identifier)
|
||||||
|
|
||||||
expect(identifiers.every((identifier) => typeof identifier === "string")).toBe(true)
|
expect(identifiers.every((identifier) => typeof identifier === "string")).toBe(true)
|
||||||
|
|||||||
@@ -7,6 +7,14 @@ import { response } from "../location"
|
|||||||
export const VcsHandler = HttpApiBuilder.group(Api, "server.vcs", (handlers) =>
|
export const VcsHandler = HttpApiBuilder.group(Api, "server.vcs", (handlers) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
return handlers
|
return handlers
|
||||||
|
.handle("vcs.get", () =>
|
||||||
|
response(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const vcs = yield* Vcs.Service
|
||||||
|
return yield* vcs.info()
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
.handle("vcs.status", () =>
|
.handle("vcs.status", () =>
|
||||||
response(
|
response(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
|||||||
@@ -327,10 +327,7 @@ export function Prompt(props: PromptProps) {
|
|||||||
if (!session) return
|
if (!session) return
|
||||||
const agent = session.agent && local.agent.list().find((agent) => agent.id === session.agent)
|
const agent = session.agent && local.agent.list().find((agent) => agent.id === session.agent)
|
||||||
if (agent && !args.agent) local.agent.set(agent.id)
|
if (agent && !args.agent) local.agent.set(agent.id)
|
||||||
if (session.model) {
|
if (!local.model.hydrate(session.model)) return
|
||||||
local.model.set({ providerID: session.model.providerID, modelID: session.model.id })
|
|
||||||
local.model.variant.set(session.model.variant)
|
|
||||||
}
|
|
||||||
syncedSessionID = sessionID
|
syncedSessionID = sessionID
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1329,12 +1326,17 @@ export function Prompt(props: PromptProps) {
|
|||||||
const locationLabel = createMemo(() => {
|
const locationLabel = createMemo(() => {
|
||||||
if (!props.sessionID) {
|
if (!props.sessionID) {
|
||||||
// No session yet: show where the next session will be created.
|
// No session yet: show where the next session will be created.
|
||||||
const directory = currentLocation.ref?.directory ?? data.location.default().directory
|
const location = currentLocation.ref ?? data.location.default()
|
||||||
return abbreviateHome(directory, paths.home)
|
const directory = abbreviateHome(location.directory, paths.home)
|
||||||
|
const branch = data.location.vcs.info(location)?.branch.current
|
||||||
|
return branch ? `${directory}:${branch}` : directory
|
||||||
}
|
}
|
||||||
if (status() !== "idle") return
|
if (status() !== "idle") return
|
||||||
const directory = data.session.get(props.sessionID)?.location.directory
|
const location = data.session.get(props.sessionID)?.location
|
||||||
return directory ? abbreviateHome(directory, paths.home) : undefined
|
if (!location) return
|
||||||
|
const directory = abbreviateHome(location.directory, paths.home)
|
||||||
|
const branch = data.location.vcs.info(location)?.branch.current
|
||||||
|
return branch ? `${directory}:${branch}` : directory
|
||||||
})
|
})
|
||||||
|
|
||||||
const spinnerDef = createMemo(() => {
|
const spinnerDef = createMemo(() => {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
type SessionTab,
|
type SessionTab,
|
||||||
type SessionTabUnread,
|
type SessionTabUnread,
|
||||||
} from "../context/session-tabs-model"
|
} from "../context/session-tabs-model"
|
||||||
import { createAnimatable, spring, tween } from "../ui/animation"
|
import { createAnimatable, spring } from "../ui/animation"
|
||||||
import { Locale } from "../util/locale"
|
import { Locale } from "../util/locale"
|
||||||
import { stringWidth } from "../util/string-width"
|
import { stringWidth } from "../util/string-width"
|
||||||
import { TabPulse, unreadGlowIntensity } from "./tab-pulse"
|
import { TabPulse, unreadGlowIntensity } from "./tab-pulse"
|
||||||
@@ -554,21 +554,6 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
|
|||||||
const glowColor = () => feedbackColor() ?? accent()
|
const glowColor = () => feedbackColor() ?? accent()
|
||||||
const glows = () => !selected() && (status().attention || (!status().busy && status().unread !== undefined))
|
const glows = () => !selected() && (status().attention || (!status().busy && status().unread !== undefined))
|
||||||
const title = () => tab.title ?? "Untitled session"
|
const title = () => tab.title ?? "Untitled session"
|
||||||
const [outgoingTitle, setOutgoingTitle] = createSignal<string>()
|
|
||||||
const wipe = createAnimatable({ front: 1 }, { enabled: animations, transition: tween({ duration: 0.3 }) })
|
|
||||||
createEffect((previous: string) => {
|
|
||||||
const next = title()
|
|
||||||
if (next === previous) return next
|
|
||||||
if (previous === NEW_SESSION_TAB_TITLE) {
|
|
||||||
setOutgoingTitle(undefined)
|
|
||||||
wipe.jump({ front: 1 })
|
|
||||||
return next
|
|
||||||
}
|
|
||||||
setOutgoingTitle(previous)
|
|
||||||
wipe.jump({ front: 0 })
|
|
||||||
wipe.animate({ front: 1 })
|
|
||||||
return next
|
|
||||||
}, title())
|
|
||||||
const tabNumber = createMemo(() => items().findIndex((item) => item.sessionID === tab.sessionID) + 1)
|
const tabNumber = createMemo(() => items().findIndex((item) => item.sessionID === tab.sessionID) + 1)
|
||||||
// The number cell keeps one trailing space, even for double-digit tabs.
|
// The number cell keeps one trailing space, even for double-digit tabs.
|
||||||
const numberWidth = () => String(tabNumber()).length + 1
|
const numberWidth = () => String(tabNumber()).length + 1
|
||||||
@@ -577,20 +562,6 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
|
|||||||
Math.max(1, width() - 1 - numberWidth() - (hovered() === tab.sessionID ? 2 : 0))
|
Math.max(1, width() - 1 - numberWidth() - (hovered() === tab.sessionID ? 2 : 0))
|
||||||
const visibleTitle = createMemo(() => Locale.takeWidth(title(), availableTitleWidth()))
|
const visibleTitle = createMemo(() => Locale.takeWidth(title(), availableTitleWidth()))
|
||||||
const visibleTitleParts = createMemo(() => Locale.graphemes(visibleTitle()))
|
const visibleTitleParts = createMemo(() => Locale.graphemes(visibleTitle()))
|
||||||
const outgoingTitleParts = createMemo(() => {
|
|
||||||
const outgoing = outgoingTitle()
|
|
||||||
if (outgoing === undefined) return undefined
|
|
||||||
return Locale.graphemes(Locale.takeWidth(outgoing, availableTitleWidth()))
|
|
||||||
})
|
|
||||||
// A new title wipes in from the left over the previous one.
|
|
||||||
const displayedParts = createMemo(() => {
|
|
||||||
const front = wipe.value().front
|
|
||||||
const parts = visibleTitleParts()
|
|
||||||
const previous = outgoingTitleParts()
|
|
||||||
if (previous === undefined || front >= 1) return parts
|
|
||||||
const cut = Math.round(front * Math.max(parts.length, previous.length))
|
|
||||||
return [...parts.slice(0, cut), ...previous.slice(cut)]
|
|
||||||
})
|
|
||||||
const titleFades = createMemo(
|
const titleFades = createMemo(
|
||||||
() => stringWidth(title()) >= availableTitleWidth() && availableTitleWidth() > FADE_WIDTH,
|
() => stringWidth(title()) >= availableTitleWidth() && availableTitleWidth() > FADE_WIDTH,
|
||||||
)
|
)
|
||||||
@@ -603,8 +574,8 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
|
|||||||
const characterColor = (index: number) => {
|
const characterColor = (index: number) => {
|
||||||
const base = foreground()
|
const base = foreground()
|
||||||
const color = glows() ? glowTextColor(base, glowColor(), 1 + numberWidth() + index, width()) : base
|
const color = glows() ? glowTextColor(base, glowColor(), 1 + numberWidth() + index, width()) : base
|
||||||
if (!titleFades() || index < displayedParts().length - FADE_WIDTH) return color
|
if (!titleFades() || index < visibleTitleParts().length - FADE_WIDTH) return color
|
||||||
const position = index - (displayedParts().length - FADE_WIDTH)
|
const position = index - (visibleTitleParts().length - FADE_WIDTH)
|
||||||
return tint(color, background(), 0.2 + 0.72 * (position / Math.max(1, FADE_WIDTH - 1)))
|
return tint(color, background(), 0.2 + 0.72 * (position / Math.max(1, FADE_WIDTH - 1)))
|
||||||
}
|
}
|
||||||
// The running sweep's level under the number cell, reported by the pulse renderable.
|
// The running sweep's level under the number cell, reported by the pulse renderable.
|
||||||
@@ -677,8 +648,8 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
|
|||||||
selectable={false}
|
selectable={false}
|
||||||
attributes={bold()}
|
attributes={bold()}
|
||||||
>
|
>
|
||||||
<Show when={glows() || titleFades()} fallback={displayedParts().join("")}>
|
<Show when={glows() || titleFades()} fallback={visibleTitle()}>
|
||||||
<For each={displayedParts()}>
|
<For each={visibleTitleParts()}>
|
||||||
{(character, index) => <span style={{ fg: characterColor(index()) }}>{character}</span>}
|
{(character, index) => <span style={{ fg: characterColor(index()) }}>{character}</span>}
|
||||||
</For>
|
</For>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import type {
|
|||||||
SessionPendingInfo,
|
SessionPendingInfo,
|
||||||
ShellInfo,
|
ShellInfo,
|
||||||
SkillInfo,
|
SkillInfo,
|
||||||
|
VcsInfo,
|
||||||
OpenCodeEvent,
|
OpenCodeEvent,
|
||||||
WebSearchProvider,
|
WebSearchProvider,
|
||||||
} from "@opencode-ai/client"
|
} from "@opencode-ai/client"
|
||||||
@@ -49,6 +50,7 @@ type ShellWithLocation = ShellInfo & { readonly location: LocationRef }
|
|||||||
|
|
||||||
type LocationData = {
|
type LocationData = {
|
||||||
info?: LocationGetOutput
|
info?: LocationGetOutput
|
||||||
|
vcs?: VcsInfo
|
||||||
agent?: AgentInfo[]
|
agent?: AgentInfo[]
|
||||||
command?: CommandInfo[]
|
command?: CommandInfo[]
|
||||||
integration?: IntegrationInfo[]
|
integration?: IntegrationInfo[]
|
||||||
@@ -339,6 +341,17 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||||||
result.location.skill.invalidate(event.location)
|
result.location.skill.invalidate(event.location)
|
||||||
void result.location.skill.sync(event.location)
|
void result.location.skill.sync(event.location)
|
||||||
break
|
break
|
||||||
|
case "vcs.branch.updated":
|
||||||
|
setStore("location", locationKey(event.location ?? defaultLocation()), (data) => ({
|
||||||
|
...data,
|
||||||
|
vcs: {
|
||||||
|
branch: {
|
||||||
|
...data?.vcs?.branch,
|
||||||
|
current: event.data.branch,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
break
|
||||||
case "session.agent.selected":
|
case "session.agent.selected":
|
||||||
if (store.session.info[event.data.sessionID])
|
if (store.session.info[event.data.sessionID])
|
||||||
setStore("session", "info", event.data.sessionID, "agent", event.data.agent)
|
setStore("session", "info", event.data.sessionID, "agent", event.data.agent)
|
||||||
@@ -1152,6 +1165,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||||||
})
|
})
|
||||||
const location = ref ?? defaultLocation()
|
const location = ref ?? defaultLocation()
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
result.location.vcs.sync(location),
|
||||||
result.location.agent.sync(location),
|
result.location.agent.sync(location),
|
||||||
result.location.command.sync(location),
|
result.location.command.sync(location),
|
||||||
result.location.integration.sync(location),
|
result.location.integration.sync(location),
|
||||||
@@ -1168,6 +1182,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||||||
invalidate(ref?: LocationRef) {
|
invalidate(ref?: LocationRef) {
|
||||||
const location = ref ?? defaultLocation()
|
const location = ref ?? defaultLocation()
|
||||||
sync.invalidate(`location:${locationKey(location)}`)
|
sync.invalidate(`location:${locationKey(location)}`)
|
||||||
|
result.location.vcs.invalidate(location)
|
||||||
result.location.agent.invalidate(location)
|
result.location.agent.invalidate(location)
|
||||||
result.location.command.invalidate(location)
|
result.location.command.invalidate(location)
|
||||||
result.location.integration.invalidate(location)
|
result.location.integration.invalidate(location)
|
||||||
@@ -1180,6 +1195,22 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||||||
result.shell.invalidate(location)
|
result.shell.invalidate(location)
|
||||||
result.session.form.invalidate("global", location)
|
result.session.form.invalidate("global", location)
|
||||||
},
|
},
|
||||||
|
vcs: {
|
||||||
|
info(location?: LocationRef) {
|
||||||
|
return store.location[locationKey(location ?? defaultLocation())]?.vcs
|
||||||
|
},
|
||||||
|
sync(ref?: LocationRef) {
|
||||||
|
const location = ref ?? defaultLocation()
|
||||||
|
return sync.run(`location.vcs:${locationKey(location)}`, async () => {
|
||||||
|
const response = await client.api.vcs.get({ location: locationQuery(location) })
|
||||||
|
const key = locationKey(response.location)
|
||||||
|
setStore("location", key, { ...store.location[key], vcs: response.data })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
invalidate(ref?: LocationRef) {
|
||||||
|
sync.invalidate(`location.vcs:${locationKey(ref ?? defaultLocation())}`)
|
||||||
|
},
|
||||||
|
},
|
||||||
agent: {
|
agent: {
|
||||||
list(location?: LocationRef) {
|
list(location?: LocationRef) {
|
||||||
return store.location[locationKey(location ?? defaultLocation())]?.agent
|
return store.location[locationKey(location ?? defaultLocation())]?.agent
|
||||||
@@ -1377,6 +1408,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||||||
setStore("location", key, { ...store.location[key], info: location })
|
setStore("location", key, { ...store.location[key], info: location })
|
||||||
})
|
})
|
||||||
.catch((error) => console.error("Failed to preload location", error))
|
.catch((error) => console.error("Failed to preload location", error))
|
||||||
|
void result.location.vcs.sync().catch((error) => console.error("Failed to preload VCS info", error))
|
||||||
void result.project.sync().catch((error) => console.error("Failed to preload projects", error))
|
void result.project.sync().catch((error) => console.error("Failed to preload projects", error))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { useEvent } from "./event"
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import { useTuiPaths } from "./runtime"
|
import { useTuiPaths } from "./runtime"
|
||||||
import { useArgs } from "./args"
|
import { useArgs } from "./args"
|
||||||
import { useClient } from "./client"
|
|
||||||
import { RGBA } from "@opentui/core"
|
import { RGBA } from "@opentui/core"
|
||||||
import { readJson, writeJsonAtomic } from "../util/persistence"
|
import { readJson, writeJsonAtomic } from "../util/persistence"
|
||||||
import {
|
import {
|
||||||
@@ -48,7 +47,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
name: "Local",
|
name: "Local",
|
||||||
init: () => {
|
init: () => {
|
||||||
const data = useData()
|
const data = useData()
|
||||||
const client = useClient()
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const { mode } = useThemes()
|
const { mode } = useThemes()
|
||||||
@@ -210,6 +208,47 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function select(model: ModelPreferenceModel, options?: { recent?: boolean }) {
|
||||||
|
batch(() => {
|
||||||
|
if (!isModelValid(model)) return
|
||||||
|
const a = agent.current()
|
||||||
|
if (!a) return
|
||||||
|
setModelStore("model", a.id, model)
|
||||||
|
if (!options?.recent) return
|
||||||
|
setModelStore("recent", recentModels(model, modelStore.recent))
|
||||||
|
save()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectVariant(value: string | undefined) {
|
||||||
|
const model = currentModel()
|
||||||
|
if (!model) return
|
||||||
|
const key = modelPreferenceKey(model)
|
||||||
|
const variant = normalizeModelVariant(value)
|
||||||
|
if (modelStore.variant[key] === variant) return
|
||||||
|
setModelStore("variant", key, variant)
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
|
||||||
|
function matches(model?: { providerID: string; id: string }) {
|
||||||
|
if (!modelStore.ready) return false
|
||||||
|
const current = currentModel()
|
||||||
|
if (!current) return false
|
||||||
|
if (!model) return true
|
||||||
|
return current.providerID === model.providerID && current.modelID === model.id
|
||||||
|
}
|
||||||
|
|
||||||
|
function hydrate(model?: { providerID: string; id: string; variant?: string }) {
|
||||||
|
if (!modelStore.ready) return false
|
||||||
|
if (!model) return true
|
||||||
|
if (data.location.model.list() === undefined) return false
|
||||||
|
const selected = { providerID: model.providerID, modelID: model.id }
|
||||||
|
if (!isModelValid(selected)) return false
|
||||||
|
select(selected)
|
||||||
|
selectVariant(model.variant)
|
||||||
|
return matches(model)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
current: currentModel,
|
current: currentModel,
|
||||||
get ready() {
|
get ready() {
|
||||||
@@ -221,6 +260,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
favorite() {
|
favorite() {
|
||||||
return modelStore.favorite
|
return modelStore.favorite
|
||||||
},
|
},
|
||||||
|
hydrate,
|
||||||
|
matches,
|
||||||
parsed: createMemo(() => {
|
parsed: createMemo(() => {
|
||||||
const value = currentModel()
|
const value = currentModel()
|
||||||
if (!value) {
|
if (!value) {
|
||||||
@@ -285,18 +326,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
setModelStore("recent", recentModels(next, modelStore.recent))
|
setModelStore("recent", recentModels(next, modelStore.recent))
|
||||||
save()
|
save()
|
||||||
},
|
},
|
||||||
set(model: { providerID: string; modelID: string }, options?: { recent?: boolean }) {
|
set: select,
|
||||||
batch(() => {
|
|
||||||
if (!isModelValid(model)) return
|
|
||||||
const a = agent.current()
|
|
||||||
if (!a) return
|
|
||||||
setModelStore("model", a.id, model)
|
|
||||||
if (options?.recent) {
|
|
||||||
setModelStore("recent", recentModels(model, modelStore.recent))
|
|
||||||
save()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
toggleFavorite(model: { providerID: string; modelID: string }) {
|
toggleFavorite(model: { providerID: string; modelID: string }) {
|
||||||
batch(() => {
|
batch(() => {
|
||||||
if (!isModelValid(model)) return
|
if (!isModelValid(model)) return
|
||||||
@@ -333,10 +363,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
return info?.variants?.map((variant) => variant.id) ?? []
|
return info?.variants?.map((variant) => variant.id) ?? []
|
||||||
},
|
},
|
||||||
set(value: string | undefined) {
|
set(value: string | undefined) {
|
||||||
const m = currentModel()
|
selectVariant(value)
|
||||||
if (!m) return
|
|
||||||
setModelStore("variant", modelPreferenceKey(m), normalizeModelVariant(value))
|
|
||||||
save()
|
|
||||||
},
|
},
|
||||||
cycle() {
|
cycle() {
|
||||||
const variants = this.list()
|
const variants = this.list()
|
||||||
|
|||||||
@@ -3,9 +3,12 @@ import { createMemo, Show } from "solid-js"
|
|||||||
import { FilePath } from "../../ui/file-path"
|
import { FilePath } from "../../ui/file-path"
|
||||||
|
|
||||||
function View(props: { context: Plugin.Context }) {
|
function View(props: { context: Plugin.Context }) {
|
||||||
const directory = createMemo(() =>
|
const directory = createMemo(() => {
|
||||||
props.context.location ? props.context.ui.format.path(props.context.location.directory) : undefined,
|
if (!props.context.location) return undefined
|
||||||
)
|
const value = props.context.ui.format.path(props.context.location.directory)
|
||||||
|
const branch = props.context.data.location.vcs.info(props.context.location)?.branch.current
|
||||||
|
return branch ? `${value}:${branch}` : value
|
||||||
|
})
|
||||||
return (
|
return (
|
||||||
<Show when={directory()}>
|
<Show when={directory()}>
|
||||||
{(value) => <FilePath value={value()} maxWidth={38} fg={props.context.theme.text.subdued} />}
|
{(value) => <FilePath value={value()} maxWidth={38} fg={props.context.theme.text.subdued} />}
|
||||||
|
|||||||
@@ -125,7 +125,6 @@ type ToolName =
|
|||||||
| "webfetch"
|
| "webfetch"
|
||||||
| "websearch"
|
| "websearch"
|
||||||
| "skill"
|
| "skill"
|
||||||
| "plan_exit"
|
|
||||||
|
|
||||||
type ToolRule = {
|
type ToolRule = {
|
||||||
view: ToolView
|
view: ToolView
|
||||||
@@ -516,15 +515,6 @@ function runLsp(p: ToolProps): ToolInline {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function runPlanExit(p: ToolProps): ToolInline {
|
|
||||||
return {
|
|
||||||
icon: "→",
|
|
||||||
title: "Switching to build agent",
|
|
||||||
mode: "block",
|
|
||||||
body: p.frame.status === "completed" ? p.frame.output : undefined,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function patchTitle(file: PatchFile, directory?: string): string {
|
function patchTitle(file: PatchFile, directory?: string): string {
|
||||||
if (file.status === "added") {
|
if (file.status === "added") {
|
||||||
return `# Created ${toolPath(file.file, { directory })}`
|
return `# Created ${toolPath(file.file, { directory })}`
|
||||||
@@ -1077,16 +1067,6 @@ const TOOL_RULES = {
|
|||||||
start: scrollSkillStart,
|
start: scrollSkillStart,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plan_exit: {
|
|
||||||
view: {
|
|
||||||
output: true,
|
|
||||||
final: false,
|
|
||||||
},
|
|
||||||
run: runPlanExit,
|
|
||||||
scroll: {
|
|
||||||
start: () => "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as const satisfies ToolRegistry
|
} as const satisfies ToolRegistry
|
||||||
|
|
||||||
function key(name: string): name is ToolName {
|
function key(name: string): name is ToolName {
|
||||||
|
|||||||
@@ -360,7 +360,7 @@ export function Session() {
|
|||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const current = prompt()
|
const current = prompt()
|
||||||
if (sent || !current || !synced() || !local.model.ready) return
|
if (sent || !current || !synced() || !local.model.ready) return
|
||||||
if (!local.agent.current() || !local.model.current()) return
|
if (!local.agent.current() || !local.model.matches(session()?.model)) return
|
||||||
if (!args.prompt || route.prompt?.text !== args.prompt || current.current.text !== args.prompt) return
|
if (!args.prompt || route.prompt?.text !== args.prompt || current.current.text !== args.prompt) return
|
||||||
sent = true
|
sent = true
|
||||||
current.submit()
|
current.submit()
|
||||||
|
|||||||
@@ -226,30 +226,42 @@ test("session title generated while an untitled session is loading remains visib
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
test("session startup prompt is submitted exactly once", async () => {
|
for (const scenario of [
|
||||||
|
{ name: "session startup prompt is submitted exactly once", delayed: false },
|
||||||
|
{ name: "session model hydration retries after the model catalog loads", delayed: true },
|
||||||
|
]) {
|
||||||
|
test(scenario.name, async () => {
|
||||||
const setup = await createTestRenderer({ width: 80, height: 24, useThread: false })
|
const setup = await createTestRenderer({ width: 80, height: 24, useThread: false })
|
||||||
const core = await import("@opentui/core")
|
const core = await import("@opentui/core")
|
||||||
mock.module("@opentui/core", () => ({ ...core, createCliRenderer: async () => setup.renderer }))
|
mock.module("@opentui/core", () => ({ ...core, createCliRenderer: async () => setup.renderer }))
|
||||||
const events = createEventStream()
|
const events = createEventStream()
|
||||||
const cwd = process.cwd()
|
const cwd = process.cwd()
|
||||||
const location = { directory: cwd, project: { id: "project", directory: cwd } }
|
const location = { directory: cwd, project: { id: "project", directory: cwd } }
|
||||||
|
const selected = scenario.delayed ? "selected" : "model"
|
||||||
const session = {
|
const session = {
|
||||||
id: "dummy",
|
id: "dummy",
|
||||||
title: "Demo session",
|
title: "Demo session",
|
||||||
projectID: "project",
|
projectID: "project",
|
||||||
location: { directory: cwd },
|
location: { directory: cwd },
|
||||||
agent: "build",
|
agent: "build",
|
||||||
model: { providerID: "provider", id: "model" },
|
model: { providerID: "provider", id: selected },
|
||||||
cost: 0,
|
cost: 0,
|
||||||
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||||
time: { created: 0, updated: 0 },
|
time: { created: 0, updated: 0 },
|
||||||
}
|
}
|
||||||
const bodies: unknown[] = []
|
const sessionRequested = Promise.withResolvers<void>()
|
||||||
|
const modelRequested = Promise.withResolvers<void>()
|
||||||
|
const releaseModels = Promise.withResolvers<void>()
|
||||||
const promptSubmitted = Promise.withResolvers<void>()
|
const promptSubmitted = Promise.withResolvers<void>()
|
||||||
|
const bodies: unknown[] = []
|
||||||
|
const selections: unknown[] = []
|
||||||
const calls = createFetch(async (url, request) => {
|
const calls = createFetch(async (url, request) => {
|
||||||
if (url.pathname === "/api/location") return json(location)
|
if (url.pathname === "/api/location") return json(location)
|
||||||
if (url.pathname === "/api/session") return json({ data: [session], cursor: {} })
|
if (url.pathname === "/api/session") return json({ data: [session], cursor: {} })
|
||||||
if (url.pathname === "/api/session/dummy") return json({ data: session })
|
if (url.pathname === "/api/session/dummy") {
|
||||||
|
sessionRequested.resolve()
|
||||||
|
return json({ data: session })
|
||||||
|
}
|
||||||
if (url.pathname === "/api/session/dummy/message") return json({ data: [], cursor: {} })
|
if (url.pathname === "/api/session/dummy/message") return json({ data: [], cursor: {} })
|
||||||
if (url.pathname === "/api/session/dummy/pending") return json({ data: [] })
|
if (url.pathname === "/api/session/dummy/pending") return json({ data: [] })
|
||||||
if (url.pathname === "/api/session/dummy/permission") return json({ data: [] })
|
if (url.pathname === "/api/session/dummy/permission") return json({ data: [] })
|
||||||
@@ -258,11 +270,21 @@ test("session startup prompt is submitted exactly once", async () => {
|
|||||||
location,
|
location,
|
||||||
data: [{ id: "build", mode: "primary", hidden: false, permissions: [] }],
|
data: [{ id: "build", mode: "primary", hidden: false, permissions: [] }],
|
||||||
})
|
})
|
||||||
if (url.pathname === "/api/model")
|
if (url.pathname === "/api/model") {
|
||||||
|
modelRequested.resolve()
|
||||||
|
if (scenario.delayed) await releaseModels.promise
|
||||||
return json({
|
return json({
|
||||||
location,
|
location,
|
||||||
data: [{ id: "model", providerID: "provider", name: "Model", variants: [] }],
|
data: [
|
||||||
|
...(scenario.delayed ? [{ id: "fallback", providerID: "provider", name: "Fallback", variants: [] }] : []),
|
||||||
|
{ id: selected, providerID: "provider", name: "Selected", variants: [] },
|
||||||
|
],
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
if (url.pathname === "/api/session/dummy/model") {
|
||||||
|
selections.push(await request.json())
|
||||||
|
return new Response(null, { status: 204 })
|
||||||
|
}
|
||||||
if (url.pathname === "/api/session/dummy/prompt") {
|
if (url.pathname === "/api/session/dummy/prompt") {
|
||||||
bodies.push(await request.json())
|
bodies.push(await request.json())
|
||||||
promptSubmitted.resolve()
|
promptSubmitted.resolve()
|
||||||
@@ -284,6 +306,12 @@ test("session startup prompt is submitted exactly once", async () => {
|
|||||||
}).pipe(Effect.provide(AppNodeBuilder.build(Global.node)), Effect.provide(FileSystem.layerNoop({}))),
|
}).pipe(Effect.provide(AppNodeBuilder.build(Global.node)), Effect.provide(FileSystem.layerNoop({}))),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (scenario.delayed) {
|
||||||
|
await Promise.all([sessionRequested.promise, modelRequested.promise])
|
||||||
|
await Bun.sleep(20)
|
||||||
|
expect(bodies).toEqual([])
|
||||||
|
releaseModels.resolve()
|
||||||
|
}
|
||||||
await Promise.race([
|
await Promise.race([
|
||||||
promptSubmitted.promise,
|
promptSubmitted.promise,
|
||||||
Bun.sleep(2000).then(() => {
|
Bun.sleep(2000).then(() => {
|
||||||
@@ -296,9 +324,12 @@ test("session startup prompt is submitted exactly once", async () => {
|
|||||||
|
|
||||||
expect(bodies).toHaveLength(1)
|
expect(bodies).toHaveLength(1)
|
||||||
expect(bodies[0]).toMatchObject({ text: "RESUME_READY" })
|
expect(bodies[0]).toMatchObject({ text: "RESUME_READY" })
|
||||||
|
expect(selections).toEqual([])
|
||||||
} finally {
|
} finally {
|
||||||
|
releaseModels.resolve()
|
||||||
if (!setup.renderer.isDestroyed) setup.renderer.destroy()
|
if (!setup.renderer.isDestroyed) setup.renderer.destroy()
|
||||||
await server.stop()
|
await server.stop()
|
||||||
mock.restore()
|
mock.restore()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -106,6 +106,49 @@ test("does not preload session summaries into the data context", async () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("syncs VCS info and applies branch updates", async () => {
|
||||||
|
const events = createEventStream()
|
||||||
|
const calls = createFetch((url) => {
|
||||||
|
if (url.pathname !== "/api/vcs") return undefined
|
||||||
|
return json({
|
||||||
|
location: { directory, project: { id: "proj_test", directory: worktree, canonical: worktree } },
|
||||||
|
data: { branch: { current: "main", default: "main" } },
|
||||||
|
})
|
||||||
|
}, events)
|
||||||
|
let data!: ReturnType<typeof useData>
|
||||||
|
|
||||||
|
function Probe() {
|
||||||
|
data = useData()
|
||||||
|
return <box />
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = await testRender(() => (
|
||||||
|
<TestTuiContexts>
|
||||||
|
<ClientProvider api={createApi(calls.fetch)}>
|
||||||
|
<ProjectProvider>
|
||||||
|
<DataProvider>
|
||||||
|
<Probe />
|
||||||
|
</DataProvider>
|
||||||
|
</ProjectProvider>
|
||||||
|
</ClientProvider>
|
||||||
|
</TestTuiContexts>
|
||||||
|
))
|
||||||
|
|
||||||
|
try {
|
||||||
|
await wait(() => data.location.vcs.info()?.branch.current === "main")
|
||||||
|
emitEvent(events, {
|
||||||
|
id: "evt_vcs_branch",
|
||||||
|
created: Date.now(),
|
||||||
|
type: "vcs.branch.updated",
|
||||||
|
data: { branch: "feature" },
|
||||||
|
})
|
||||||
|
await wait(() => data.location.vcs.info()?.branch.current === "feature")
|
||||||
|
expect(data.location.vcs.info()?.branch).toEqual({ current: "feature", default: "main" })
|
||||||
|
} finally {
|
||||||
|
app.renderer.destroy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
test("proactively syncs project metadata newest first", async () => {
|
test("proactively syncs project metadata newest first", async () => {
|
||||||
const events = createEventStream()
|
const events = createEventStream()
|
||||||
const calls = createFetch((url) => {
|
const calls = createFetch((url) => {
|
||||||
|
|||||||
@@ -95,6 +95,11 @@ export function createFetch(override?: FetchHandler, events?: ReturnType<typeof
|
|||||||
if (url.pathname === "/path") return json({ home: "", state: "", config: "", worktree, directory })
|
if (url.pathname === "/path") return json({ home: "", state: "", config: "", worktree, directory })
|
||||||
if (url.pathname === "/api/location")
|
if (url.pathname === "/api/location")
|
||||||
return json({ directory, project: { id: "proj_test", directory: worktree, canonical: worktree } })
|
return json({ directory, project: { id: "proj_test", directory: worktree, canonical: worktree } })
|
||||||
|
if (url.pathname === "/api/vcs")
|
||||||
|
return json({
|
||||||
|
location: { directory, project: { id: "proj_test", directory: worktree, canonical: worktree } },
|
||||||
|
data: { branch: { current: "main", default: "main" } },
|
||||||
|
})
|
||||||
if (url.pathname === "/api/fs/list")
|
if (url.pathname === "/api/fs/list")
|
||||||
return json({ location: { directory, project: { id: "proj_test", directory: worktree, canonical: worktree } }, data: [] })
|
return json({ location: { directory, project: { id: "proj_test", directory: worktree, canonical: worktree } }, data: [] })
|
||||||
if (url.pathname === "/api/project/current") return json({ id: "proj_test", directory: worktree })
|
if (url.pathname === "/api/project/current") return json({ id: "proj_test", directory: worktree })
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ const config = path.join(xdgConfig!, app)
|
|||||||
const state = path.join(xdgState!, app)
|
const state = path.join(xdgState!, app)
|
||||||
const tmp = path.join(os.tmpdir(), app)
|
const tmp = path.join(os.tmpdir(), app)
|
||||||
|
|
||||||
|
await fs.mkdir(tmp, { recursive: true })
|
||||||
|
|
||||||
const paths = {
|
const paths = {
|
||||||
get home() {
|
get home() {
|
||||||
return process.env.OPENCODE_TEST_HOME ?? os.homedir()
|
return process.env.OPENCODE_TEST_HOME ?? os.homedir()
|
||||||
@@ -24,7 +26,7 @@ const paths = {
|
|||||||
cache,
|
cache,
|
||||||
config,
|
config,
|
||||||
state,
|
state,
|
||||||
tmp,
|
tmp: await fs.realpath(tmp),
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Path = paths
|
export const Path = paths
|
||||||
@@ -35,7 +37,6 @@ await Promise.all([
|
|||||||
fs.mkdir(Path.data, { recursive: true }),
|
fs.mkdir(Path.data, { recursive: true }),
|
||||||
fs.mkdir(Path.config, { recursive: true }),
|
fs.mkdir(Path.config, { recursive: true }),
|
||||||
fs.mkdir(Path.state, { recursive: true }),
|
fs.mkdir(Path.state, { recursive: true }),
|
||||||
fs.mkdir(Path.tmp, { recursive: true }),
|
|
||||||
fs.mkdir(Path.log, { recursive: true }),
|
fs.mkdir(Path.log, { recursive: true }),
|
||||||
fs.mkdir(Path.bin, { recursive: true }),
|
fs.mkdir(Path.bin, { recursive: true }),
|
||||||
fs.mkdir(Path.repos, { recursive: true }),
|
fs.mkdir(Path.repos, { recursive: true }),
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
"@shikijs/transformers": "3.20.0",
|
"@shikijs/transformers": "3.20.0",
|
||||||
"@solid-primitives/resize-observer": "2.1.5",
|
"@solid-primitives/resize-observer": "2.1.5",
|
||||||
"@types/luxon": "catalog:",
|
"@types/luxon": "catalog:",
|
||||||
"ai": "catalog:",
|
|
||||||
"astro": "5.7.13",
|
"astro": "5.7.13",
|
||||||
"diff": "catalog:",
|
"diff": "catalog:",
|
||||||
"js-base64": "3.7.7",
|
"js-base64": "3.7.7",
|
||||||
|
|||||||
@@ -81,8 +81,7 @@ current built-in actions use these resources:
|
|||||||
| `<server>_<tool>` | `*` for an MCP tool; unsupported characters in both names become `_` |
|
| `<server>_<tool>` | `*` for an MCP tool; unsupported characters in both names become `_` |
|
||||||
| `execute` | `*`; controls availability of the Code Mode dispatcher, while each nested tool still enforces its own permission |
|
| `execute` | `*`; controls availability of the Code Mode dispatcher, while each nested tool still enforces its own permission |
|
||||||
|
|
||||||
Built-in agent policy also reserves `plan_enter` and `plan_exit` for plan-mode
|
`doom_loop` and `lsp` are not current V2 Core permission actions.
|
||||||
transitions. `doom_loop` and `lsp` are not current V2 Core permission actions.
|
|
||||||
|
|
||||||
## External directories
|
## External directories
|
||||||
|
|
||||||
@@ -132,7 +131,9 @@ matching, so authorize only trusted directory boundaries.
|
|||||||
|
|
||||||
## Defaults
|
## Defaults
|
||||||
|
|
||||||
The evaluator's fallback is `ask`, but shipped agents include ordered defaults:
|
Every agent, including custom agents, starts with ordered defaults that allow
|
||||||
|
tools, ask for external directories, ask for `.env` reads, and allow
|
||||||
|
`.env.example` reads. Shipped agents then add their own policies:
|
||||||
|
|
||||||
| Agent | Effective default policy |
|
| Agent | Effective default policy |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
@@ -153,8 +154,12 @@ The base read rules are ordered as follows:
|
|||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
OpenCode also permits its managed tool-output and temporary directories where
|
OpenCode also permits its managed tool-output, shell-output, temporary, and
|
||||||
needed. These exceptions do not grant general external-directory access.
|
global configuration directories. These exceptions apply only to the
|
||||||
|
external-directory boundary for every agent; the underlying action still uses
|
||||||
|
its own permission rules. The environment instructions identify the temporary
|
||||||
|
directory available for work outside the workspace. Later global and
|
||||||
|
agent-specific rules can override these defaults.
|
||||||
|
|
||||||
## Agent overrides
|
## Agent overrides
|
||||||
|
|
||||||
|
|||||||
+17
-4
@@ -239,17 +239,30 @@ without restarting OpenCode.
|
|||||||
|
|
||||||
### Runtime hooks
|
### Runtime hooks
|
||||||
|
|
||||||
Runtime hooks intercept live operations. Their event objects expose specific
|
Runtime hooks intercept live operations:
|
||||||
mutable fields:
|
|
||||||
|
|
||||||
| Hook | Mutable fields |
|
| Hook | Mutable fields |
|
||||||
| ------------------------------------------- | ------------------------------------------------------------------------------ |
|
| ------------------------------------------- | ------------------------------------------------------------------------------ |
|
||||||
| `ctx.aisdk.hook("sdk", callback)` | `sdk`, after inspecting `model`, `package`, and `options` |
|
| `ctx.aisdk.hook("sdk", callback)` | `sdk`, after inspecting `model`, `package`, and `options` |
|
||||||
| `ctx.aisdk.hook("language", callback)` | `language`, after inspecting `model`, `sdk`, and `options` |
|
| `ctx.aisdk.hook("language", callback)` | `language`, after inspecting `model`, `sdk`, and `options` |
|
||||||
| `ctx.session.hook("request", callback)` | `system`, `messages`, and the `tools` record immediately before model dispatch |
|
| `ctx.session.hook("context", callback)` | `system`, `messages`, and the `tools` record immediately before model dispatch |
|
||||||
|
| `ctx.session.hook("http", callback)` | `use`, registering request and response handling |
|
||||||
| `ctx.tool.hook("execute.before", callback)` | `input`, before the selected tool executes |
|
| `ctx.tool.hook("execute.before", callback)` | `input`, before the selected tool executes |
|
||||||
| `ctx.tool.hook("execute.after", callback)` | Terminal `result` on success or `error` on failure |
|
| `ctx.tool.hook("execute.after", callback)` | Terminal `result` on success or `error` on failure |
|
||||||
|
|
||||||
|
HTTP hooks can modify requests, inspect responses, retry, or return a
|
||||||
|
response without calling the provider. It applies to native models; AI SDK
|
||||||
|
models do not currently pass through this hook.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await ctx.session.hook("http", (event) => {
|
||||||
|
event.use((request, next) => {
|
||||||
|
request.headers.set("x-session-id", event.sessionID)
|
||||||
|
return next(request)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
For example, remove a tool from selected model requests and normalize another
|
For example, remove a tool from selected model requests and normalize another
|
||||||
tool's input:
|
tool's input:
|
||||||
|
|
||||||
@@ -259,7 +272,7 @@ import { Plugin } from "@opencode-ai/plugin"
|
|||||||
export default Plugin.define({
|
export default Plugin.define({
|
||||||
id: "acme.guards",
|
id: "acme.guards",
|
||||||
setup: async (ctx) => {
|
setup: async (ctx) => {
|
||||||
await ctx.session.hook("request", (event) => {
|
await ctx.session.hook("context", (event) => {
|
||||||
delete event.tools.write
|
delete event.tools.write
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+153
-6
@@ -851,6 +851,16 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"title": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"agent": {
|
"agent": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@@ -11301,6 +11311,104 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/vcs": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"vcs"
|
||||||
|
],
|
||||||
|
"operationId": "v2.vcs.get",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "location",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"directory": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"workspace": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"required": false,
|
||||||
|
"style": "deepObject",
|
||||||
|
"explode": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"security": [],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Success",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"location": {
|
||||||
|
"$ref": "#/components/schemas/Location.Info"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/components/schemas/Vcs.Info"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"location",
|
||||||
|
"data"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "InvalidRequestError",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/InvalidRequestError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "UnauthorizedError",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UnauthorizedError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Get current and default branch information for the requested location.",
|
||||||
|
"summary": "VCS info"
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/vcs/status": {
|
"/api/vcs/status": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -12032,11 +12140,15 @@
|
|||||||
},
|
},
|
||||||
"directory": {
|
"directory": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"canonical": {
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"id",
|
"id",
|
||||||
"directory"
|
"directory",
|
||||||
|
"canonical"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
@@ -12498,7 +12610,6 @@
|
|||||||
"cost",
|
"cost",
|
||||||
"tokens",
|
"tokens",
|
||||||
"time",
|
"time",
|
||||||
"title",
|
|
||||||
"location"
|
"location"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
@@ -13837,6 +13948,15 @@
|
|||||||
},
|
},
|
||||||
"message": {
|
"message": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "integer",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"minimum": 100,
|
||||||
|
"maximum": 599
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -20765,7 +20885,7 @@
|
|||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"worktree": {
|
"canonical": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"vcs": {
|
"vcs": {
|
||||||
@@ -20792,7 +20912,7 @@
|
|||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"id",
|
"id",
|
||||||
"worktree",
|
"canonical",
|
||||||
"time",
|
"time",
|
||||||
"sandboxes"
|
"sandboxes"
|
||||||
],
|
],
|
||||||
@@ -20806,11 +20926,15 @@
|
|||||||
},
|
},
|
||||||
"directory": {
|
"directory": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"canonical": {
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"id",
|
"id",
|
||||||
"directory"
|
"directory",
|
||||||
|
"canonical"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
@@ -22445,7 +22569,6 @@
|
|||||||
"slug",
|
"slug",
|
||||||
"projectID",
|
"projectID",
|
||||||
"directory",
|
"directory",
|
||||||
"title",
|
|
||||||
"version",
|
"version",
|
||||||
"time"
|
"time"
|
||||||
],
|
],
|
||||||
@@ -29127,6 +29250,30 @@
|
|||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
|
"Vcs.Branch": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"current": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"Vcs.Info": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"branch": {
|
||||||
|
"$ref": "#/components/schemas/Vcs.Branch"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"branch"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
"Vcs.FileStatus": {
|
"Vcs.FileStatus": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -851,6 +851,16 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"title": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"agent": {
|
"agent": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@@ -11301,6 +11311,104 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/vcs": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"vcs"
|
||||||
|
],
|
||||||
|
"operationId": "v2.vcs.get",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "location",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"directory": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"workspace": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"required": false,
|
||||||
|
"style": "deepObject",
|
||||||
|
"explode": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"security": [],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Success",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"location": {
|
||||||
|
"$ref": "#/components/schemas/Location.Info"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/components/schemas/Vcs.Info"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"location",
|
||||||
|
"data"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "InvalidRequestError",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/InvalidRequestError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "UnauthorizedError",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UnauthorizedError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Get current and default branch information for the requested location.",
|
||||||
|
"summary": "VCS info"
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/vcs/status": {
|
"/api/vcs/status": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -12032,11 +12140,15 @@
|
|||||||
},
|
},
|
||||||
"directory": {
|
"directory": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"canonical": {
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"id",
|
"id",
|
||||||
"directory"
|
"directory",
|
||||||
|
"canonical"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
@@ -12498,7 +12610,6 @@
|
|||||||
"cost",
|
"cost",
|
||||||
"tokens",
|
"tokens",
|
||||||
"time",
|
"time",
|
||||||
"title",
|
|
||||||
"location"
|
"location"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
@@ -13837,6 +13948,15 @@
|
|||||||
},
|
},
|
||||||
"message": {
|
"message": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "integer",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"minimum": 100,
|
||||||
|
"maximum": 599
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -20765,7 +20885,7 @@
|
|||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"worktree": {
|
"canonical": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"vcs": {
|
"vcs": {
|
||||||
@@ -20792,7 +20912,7 @@
|
|||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"id",
|
"id",
|
||||||
"worktree",
|
"canonical",
|
||||||
"time",
|
"time",
|
||||||
"sandboxes"
|
"sandboxes"
|
||||||
],
|
],
|
||||||
@@ -20806,11 +20926,15 @@
|
|||||||
},
|
},
|
||||||
"directory": {
|
"directory": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"canonical": {
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"id",
|
"id",
|
||||||
"directory"
|
"directory",
|
||||||
|
"canonical"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
@@ -22445,7 +22569,6 @@
|
|||||||
"slug",
|
"slug",
|
||||||
"projectID",
|
"projectID",
|
||||||
"directory",
|
"directory",
|
||||||
"title",
|
|
||||||
"version",
|
"version",
|
||||||
"time"
|
"time"
|
||||||
],
|
],
|
||||||
@@ -29127,6 +29250,30 @@
|
|||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
|
"Vcs.Branch": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"current": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"Vcs.Info": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"branch": {
|
||||||
|
"$ref": "#/components/schemas/Vcs.Branch"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"branch"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
"Vcs.FileStatus": {
|
"Vcs.FileStatus": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -1,174 +0,0 @@
|
|||||||
diff --git a/dist/index.d.mts b/dist/index.d.mts
|
|
||||||
index 266c5ffdd9ee74ff95908ce90858ee4369d4e4ae..990ef4195bc67b6d25f249e1c81cf51710390f9a 100644
|
|
||||||
--- a/dist/index.d.mts
|
|
||||||
+++ b/dist/index.d.mts
|
|
||||||
@@ -78,6 +78,7 @@ declare const xaiLanguageModelResponsesOptions: z.ZodObject<{
|
|
||||||
topLogprobs: z.ZodOptional<z.ZodNumber>;
|
|
||||||
store: z.ZodOptional<z.ZodBoolean>;
|
|
||||||
previousResponseId: z.ZodOptional<z.ZodString>;
|
|
||||||
+ promptCacheKey: z.ZodOptional<z.ZodString>;
|
|
||||||
include: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodEnum<{
|
|
||||||
"file_search_call.results": "file_search_call.results";
|
|
||||||
}>>>>;
|
|
||||||
diff --git a/dist/index.d.ts b/dist/index.d.ts
|
|
||||||
index 266c5ffdd9ee74ff95908ce90858ee4369d4e4ae..990ef4195bc67b6d25f249e1c81cf51710390f9a 100644
|
|
||||||
--- a/dist/index.d.ts
|
|
||||||
+++ b/dist/index.d.ts
|
|
||||||
@@ -78,6 +78,7 @@ declare const xaiLanguageModelResponsesOptions: z.ZodObject<{
|
|
||||||
topLogprobs: z.ZodOptional<z.ZodNumber>;
|
|
||||||
store: z.ZodOptional<z.ZodBoolean>;
|
|
||||||
previousResponseId: z.ZodOptional<z.ZodString>;
|
|
||||||
+ promptCacheKey: z.ZodOptional<z.ZodString>;
|
|
||||||
include: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodEnum<{
|
|
||||||
"file_search_call.results": "file_search_call.results";
|
|
||||||
}>>>>;
|
|
||||||
diff --git a/dist/index.js b/dist/index.js
|
|
||||||
index 717b74538f5c8f0d6ab1475ebb2a84a47ccd3950..dd7dbeb3bc307e0d355f4bb4939d06cc8eae7528 100644
|
|
||||||
--- a/dist/index.js
|
|
||||||
+++ b/dist/index.js
|
|
||||||
@@ -1119,6 +1119,14 @@ async function convertToXaiResponsesInput({
|
|
||||||
type: "input_file",
|
|
||||||
file_url: block.data.toString()
|
|
||||||
});
|
|
||||||
+ } else if (block.mediaType === "application/pdf") {
|
|
||||||
+ contentParts.push({
|
|
||||||
+ type: "input_file",
|
|
||||||
+ ...typeof block.data === "string" && block.data.startsWith("file-") ? { file_id: block.data } : {
|
|
||||||
+ filename: block.filename ?? "file",
|
|
||||||
+ file_data: `data:application/pdf;base64,${(0, import_provider_utils5.convertToBase64)(block.data)}`
|
|
||||||
+ }
|
|
||||||
+ });
|
|
||||||
} else {
|
|
||||||
throw new import_provider4.UnsupportedFunctionalityError({
|
|
||||||
functionality: `file part media type ${block.mediaType} as inline data (xAI Responses requires a URL or a Files API reference for non-image files)`
|
|
||||||
@@ -1760,6 +1768,10 @@ var xaiLanguageModelResponsesOptions = import_v47.z.object({
|
|
||||||
* The ID of the previous response from the model.
|
|
||||||
*/
|
|
||||||
previousResponseId: import_v47.z.string().optional(),
|
|
||||||
+ /**
|
|
||||||
+ * A stable key used by xAI to improve prompt cache routing.
|
|
||||||
+ */
|
|
||||||
+ promptCacheKey: import_v47.z.string().optional(),
|
|
||||||
/**
|
|
||||||
* Specify additional output data to include in the model response.
|
|
||||||
* Example values: 'file_search_call.results'.
|
|
||||||
@@ -2165,6 +2177,9 @@ var XaiResponsesLanguageModel = class {
|
|
||||||
},
|
|
||||||
...options.previousResponseId != null && {
|
|
||||||
previous_response_id: options.previousResponseId
|
|
||||||
+ },
|
|
||||||
+ ...options.promptCacheKey != null && {
|
|
||||||
+ prompt_cache_key: options.promptCacheKey
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (xaiTools2 && xaiTools2.length > 0) {
|
|
||||||
diff --git a/dist/index.mjs b/dist/index.mjs
|
|
||||||
index a26af109585fc2bd3053b320142aa869c06d36f4..774adaf971b648544317a4fc65d0c56e488d4fc7 100644
|
|
||||||
--- a/dist/index.mjs
|
|
||||||
+++ b/dist/index.mjs
|
|
||||||
@@ -1122,6 +1122,14 @@ async function convertToXaiResponsesInput({
|
|
||||||
type: "input_file",
|
|
||||||
file_url: block.data.toString()
|
|
||||||
});
|
|
||||||
+ } else if (block.mediaType === "application/pdf") {
|
|
||||||
+ contentParts.push({
|
|
||||||
+ type: "input_file",
|
|
||||||
+ ...typeof block.data === "string" && block.data.startsWith("file-") ? { file_id: block.data } : {
|
|
||||||
+ filename: block.filename ?? "file",
|
|
||||||
+ file_data: `data:application/pdf;base64,${convertToBase642(block.data)}`
|
|
||||||
+ }
|
|
||||||
+ });
|
|
||||||
} else {
|
|
||||||
throw new UnsupportedFunctionalityError3({
|
|
||||||
functionality: `file part media type ${block.mediaType} as inline data (xAI Responses requires a URL or a Files API reference for non-image files)`
|
|
||||||
@@ -1763,6 +1771,10 @@ var xaiLanguageModelResponsesOptions = z7.object({
|
|
||||||
* The ID of the previous response from the model.
|
|
||||||
*/
|
|
||||||
previousResponseId: z7.string().optional(),
|
|
||||||
+ /**
|
|
||||||
+ * A stable key used by xAI to improve prompt cache routing.
|
|
||||||
+ */
|
|
||||||
+ promptCacheKey: z7.string().optional(),
|
|
||||||
/**
|
|
||||||
* Specify additional output data to include in the model response.
|
|
||||||
* Example values: 'file_search_call.results'.
|
|
||||||
@@ -2186,6 +2198,9 @@ var XaiResponsesLanguageModel = class {
|
|
||||||
},
|
|
||||||
...options.previousResponseId != null && {
|
|
||||||
previous_response_id: options.previousResponseId
|
|
||||||
+ },
|
|
||||||
+ ...options.promptCacheKey != null && {
|
|
||||||
+ prompt_cache_key: options.promptCacheKey
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (xaiTools2 && xaiTools2.length > 0) {
|
|
||||||
diff --git a/src/responses/convert-to-xai-responses-input.ts b/src/responses/convert-to-xai-responses-input.ts
|
|
||||||
index 9a3712990cf646a6f7ba757d3f2893a3c37c6ef9..b018a9b95eb21e6c12794e6bddb3d953f40441e5 100644
|
|
||||||
--- a/src/responses/convert-to-xai-responses-input.ts
|
|
||||||
+++ b/src/responses/convert-to-xai-responses-input.ts
|
|
||||||
@@ -64,6 +64,17 @@ export async function convertToXaiResponsesInput({
|
|
||||||
type: 'input_file',
|
|
||||||
file_url: block.data.toString(),
|
|
||||||
});
|
|
||||||
+ } else if (block.mediaType === 'application/pdf') {
|
|
||||||
+ contentParts.push({
|
|
||||||
+ type: 'input_file',
|
|
||||||
+ ...(typeof block.data === 'string' &&
|
|
||||||
+ block.data.startsWith('file-')
|
|
||||||
+ ? { file_id: block.data }
|
|
||||||
+ : {
|
|
||||||
+ filename: block.filename ?? 'file',
|
|
||||||
+ file_data: `data:application/pdf;base64,${convertToBase64(block.data)}`,
|
|
||||||
+ }),
|
|
||||||
+ });
|
|
||||||
} else {
|
|
||||||
throw new UnsupportedFunctionalityError({
|
|
||||||
functionality: `file part media type ${block.mediaType} as inline data (xAI Responses requires a URL or a Files API reference for non-image files)`,
|
|
||||||
diff --git a/src/responses/xai-responses-api.ts b/src/responses/xai-responses-api.ts
|
|
||||||
index 70483ea4f7c340b84c23766ac1d1df21e052842b..4b4d052860caa019556151e9e46dbfda8dfb18a9 100644
|
|
||||||
--- a/src/responses/xai-responses-api.ts
|
|
||||||
+++ b/src/responses/xai-responses-api.ts
|
|
||||||
@@ -27,7 +27,13 @@ export type XaiResponsesSystemMessage = {
|
|
||||||
export type XaiResponsesUserMessageContentPart =
|
|
||||||
| { type: 'input_text'; text: string }
|
|
||||||
| { type: 'input_image'; image_url: string }
|
|
||||||
- | { type: 'input_file'; file_url: string };
|
|
||||||
+ | {
|
|
||||||
+ type: 'input_file';
|
|
||||||
+ file_url?: string;
|
|
||||||
+ file_id?: string;
|
|
||||||
+ file_data?: string;
|
|
||||||
+ filename?: string;
|
|
||||||
+ };
|
|
||||||
|
|
||||||
export type XaiResponsesUserMessage = {
|
|
||||||
role: 'user';
|
|
||||||
diff --git a/src/responses/xai-responses-language-model.ts b/src/responses/xai-responses-language-model.ts
|
|
||||||
index f90df62eb9a30154388b1390e9f3acc3ccc022bf..00e61cba6cf048ae0045be692f33cb7e1e6a1188 100644
|
|
||||||
--- a/src/responses/xai-responses-language-model.ts
|
|
||||||
+++ b/src/responses/xai-responses-language-model.ts
|
|
||||||
@@ -182,6 +182,9 @@ export class XaiResponsesLanguageModel implements LanguageModelV3 {
|
|
||||||
...(options.previousResponseId != null && {
|
|
||||||
previous_response_id: options.previousResponseId,
|
|
||||||
}),
|
|
||||||
+ ...(options.promptCacheKey != null && {
|
|
||||||
+ prompt_cache_key: options.promptCacheKey,
|
|
||||||
+ }),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (xaiTools && xaiTools.length > 0) {
|
|
||||||
diff --git a/src/responses/xai-responses-options.ts b/src/responses/xai-responses-options.ts
|
|
||||||
index f8e96c061bf8793a402ababb8cad65bb2ad6aead..15c168892c1e8755453c61d3061e958cfd51ac71 100644
|
|
||||||
--- a/src/responses/xai-responses-options.ts
|
|
||||||
+++ b/src/responses/xai-responses-options.ts
|
|
||||||
@@ -32,6 +32,10 @@ export const xaiLanguageModelResponsesOptions = z.object({
|
|
||||||
* The ID of the previous response from the model.
|
|
||||||
*/
|
|
||||||
previousResponseId: z.string().optional(),
|
|
||||||
+ /**
|
|
||||||
+ * A stable key used by xAI to improve prompt cache routing.
|
|
||||||
+ */
|
|
||||||
+ promptCacheKey: z.string().optional(),
|
|
||||||
/**
|
|
||||||
* Specify additional output data to include in the model response.
|
|
||||||
* Example values: 'file_search_call.results'.
|
|
||||||
Reference in New Issue
Block a user