Compare commits

..

27 Commits

Author SHA1 Message Date
Dax Raad 05f378650a fix(tui): hide read tool arguments 2026-06-21 09:12:57 -04:00
Dax Raad 8396395f17 fix(stats): update defect schemas 2026-06-21 08:08:31 -04:00
Dax Raad 1b8bab3e35 fix(ci): avoid Playwright Chromium install hang 2026-06-21 08:08:31 -04:00
opencode-agent[bot] 02687b6324 chore: generate 2026-06-21 12:07:16 +00:00
Dax c780d7cee7 feat(plugin): add v2 effect host (#33111) 2026-06-21 08:05:49 -04:00
opencode-agent[bot] 7a9337da8a chore: generate 2026-06-21 11:49:50 +00:00
Adam f96e6aa6ed fix(stats): bypass worker runtime crash 2026-06-21 06:48:25 -05:00
Adam c6f719e153 fix(stats): restore worker data exports 2026-06-21 06:39:49 -05:00
Adam 1a111be494 fix(stats): run worker effects directly 2026-06-21 06:30:15 -05:00
Adam a0aee82be9 fix(stats): inline worker runtime import 2026-06-21 06:22:26 -05:00
Adam c0dc6e50a7 fix(stats): restore worker runtime 2026-06-21 06:16:02 -05:00
Adam 418a9e4e66 fix(stats): restore model page rendering 2026-06-21 05:58:39 -05:00
Adam bd8ce5e6a9 chore(stats): remove deploy migrations 2026-06-21 05:41:10 -05:00
Adam a97c6de9db fix(stats): support planetscale cli variants 2026-06-21 05:17:22 -05:00
opencode-agent[bot] 24ea4dd008 chore: generate 2026-06-21 10:03:16 +00:00
Adam ffcb7542e1 fix(stats): run production migration safely 2026-06-21 05:01:43 -05:00
opencode-agent[bot] d4d841bafd chore: generate 2026-06-21 09:40:15 +00:00
Adam 6f0e934573 fix(stats): make unique users migration idempotent 2026-06-21 04:38:47 -05:00
Adam 233d065dd5 feat(stats): show model users metric 2026-06-21 04:30:25 -05:00
Dax f12ac6f234 fix(tui): reduce noisy MCP autocomplete matches (#33176) 2026-06-20 22:24:59 -04:00
James Long d3bbfff826 test(opencode): simplify message pagination layer wiring (#33157) 2026-06-20 21:44:37 -04:00
James Long 468f425e76 test(opencode): simplify session retry layer wiring (#33155) 2026-06-20 21:44:02 -04:00
James Long d59619fffd test(opencode): simplify git layer wiring (#33156) 2026-06-20 21:43:03 -04:00
opencode-agent[bot] 5606d2bab9 chore: update nix node_modules hashes 2026-06-21 00:19:31 +00:00
opencode e84d94d998 sync release versions for v1.17.9 2026-06-21 00:03:08 +00:00
Kit Langton 4f1ae1604e chore: upgrade Effect to beta 83 (#32340) 2026-06-20 19:57:29 -04:00
Dax d99f86b28d fix(tui): separate subagent tool rows (#33158) 2026-06-20 18:58:27 -04:00
204 changed files with 4198 additions and 2246 deletions
+2 -1
View File
@@ -99,7 +99,8 @@ jobs:
- name: Setup Node - name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with: with:
node-version: "24" # Playwright 1.59 hangs while extracting Chromium with Node 24.16.
node-version: "24.15"
- name: Setup Bun - name: Setup Bun
uses: ./.github/actions/setup-bun uses: ./.github/actions/setup-bun
+48 -38
View File
@@ -29,7 +29,7 @@
}, },
"packages/app": { "packages/app": {
"name": "@opencode-ai/app", "name": "@opencode-ai/app",
"version": "1.17.8", "version": "1.17.9",
"dependencies": { "dependencies": {
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@opencode-ai/core": "workspace:*", "@opencode-ai/core": "workspace:*",
@@ -86,7 +86,7 @@
}, },
"packages/cli": { "packages/cli": {
"name": "@opencode-ai/cli", "name": "@opencode-ai/cli",
"version": "1.17.8", "version": "1.17.9",
"bin": { "bin": {
"lildax": "./bin/lildax.cjs", "lildax": "./bin/lildax.cjs",
}, },
@@ -111,7 +111,7 @@
}, },
"packages/console/app": { "packages/console/app": {
"name": "@opencode-ai/console-app", "name": "@opencode-ai/console-app",
"version": "1.17.8", "version": "1.17.9",
"dependencies": { "dependencies": {
"@cloudflare/vite-plugin": "1.15.2", "@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1", "@ibm/plex": "6.4.1",
@@ -147,7 +147,7 @@
}, },
"packages/console/core": { "packages/console/core": {
"name": "@opencode-ai/console-core", "name": "@opencode-ai/console-core",
"version": "1.17.8", "version": "1.17.9",
"dependencies": { "dependencies": {
"@aws-sdk/client-sts": "3.782.0", "@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1", "@jsx-email/render": "1.1.1",
@@ -174,7 +174,7 @@
}, },
"packages/console/function": { "packages/console/function": {
"name": "@opencode-ai/console-function", "name": "@opencode-ai/console-function",
"version": "1.17.8", "version": "1.17.9",
"dependencies": { "dependencies": {
"@ai-sdk/anthropic": "3.0.82", "@ai-sdk/anthropic": "3.0.82",
"@ai-sdk/openai": "3.0.48", "@ai-sdk/openai": "3.0.48",
@@ -196,7 +196,7 @@
}, },
"packages/console/mail": { "packages/console/mail": {
"name": "@opencode-ai/console-mail", "name": "@opencode-ai/console-mail",
"version": "1.17.8", "version": "1.17.9",
"dependencies": { "dependencies": {
"@jsx-email/all": "2.2.3", "@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3", "@jsx-email/cli": "1.4.3",
@@ -220,7 +220,7 @@
}, },
"packages/console/support": { "packages/console/support": {
"name": "@opencode-ai/console-support", "name": "@opencode-ai/console-support",
"version": "1.17.8", "version": "1.17.9",
"dependencies": { "dependencies": {
"@cloudflare/vite-plugin": "1.15.2", "@cloudflare/vite-plugin": "1.15.2",
"@opencode-ai/console-core": "workspace:*", "@opencode-ai/console-core": "workspace:*",
@@ -240,7 +240,7 @@
}, },
"packages/core": { "packages/core": {
"name": "@opencode-ai/core", "name": "@opencode-ai/core",
"version": "1.17.8", "version": "1.17.9",
"bin": { "bin": {
"opencode": "./bin/opencode", "opencode": "./bin/opencode",
}, },
@@ -276,6 +276,7 @@
"@opencode-ai/effect-drizzle-sqlite": "workspace:*", "@opencode-ai/effect-drizzle-sqlite": "workspace:*",
"@opencode-ai/effect-sqlite-node": "workspace:*", "@opencode-ai/effect-sqlite-node": "workspace:*",
"@opencode-ai/llm": "workspace:*", "@opencode-ai/llm": "workspace:*",
"@opencode-ai/plugin": "workspace:*",
"@openrouter/ai-sdk-provider": "2.9.0", "@openrouter/ai-sdk-provider": "2.9.0",
"@opentelemetry/api": "1.9.0", "@opentelemetry/api": "1.9.0",
"@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/context-async-hooks": "2.6.1",
@@ -331,7 +332,7 @@
}, },
"packages/desktop": { "packages/desktop": {
"name": "@opencode-ai/desktop", "name": "@opencode-ai/desktop",
"version": "1.17.8", "version": "1.17.9",
"dependencies": { "dependencies": {
"@zip.js/zip.js": "2.7.62", "@zip.js/zip.js": "2.7.62",
"effect": "catalog:", "effect": "catalog:",
@@ -385,7 +386,7 @@
}, },
"packages/effect-drizzle-sqlite": { "packages/effect-drizzle-sqlite": {
"name": "@opencode-ai/effect-drizzle-sqlite", "name": "@opencode-ai/effect-drizzle-sqlite",
"version": "1.17.8", "version": "1.17.9",
"dependencies": { "dependencies": {
"drizzle-orm": "catalog:", "drizzle-orm": "catalog:",
"effect": "catalog:", "effect": "catalog:",
@@ -399,7 +400,7 @@
}, },
"packages/effect-sqlite-node": { "packages/effect-sqlite-node": {
"name": "@opencode-ai/effect-sqlite-node", "name": "@opencode-ai/effect-sqlite-node",
"version": "1.17.8", "version": "1.17.9",
"dependencies": { "dependencies": {
"effect": "catalog:", "effect": "catalog:",
}, },
@@ -411,7 +412,7 @@
}, },
"packages/enterprise": { "packages/enterprise": {
"name": "@opencode-ai/enterprise", "name": "@opencode-ai/enterprise",
"version": "1.17.8", "version": "1.17.9",
"dependencies": { "dependencies": {
"@hono/standard-validator": "catalog:", "@hono/standard-validator": "catalog:",
"@opencode-ai/core": "workspace:*", "@opencode-ai/core": "workspace:*",
@@ -442,7 +443,7 @@
}, },
"packages/function": { "packages/function": {
"name": "@opencode-ai/function", "name": "@opencode-ai/function",
"version": "1.17.8", "version": "1.17.9",
"dependencies": { "dependencies": {
"@octokit/auth-app": "8.0.1", "@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:", "@octokit/rest": "catalog:",
@@ -458,10 +459,10 @@
}, },
"packages/http-recorder": { "packages/http-recorder": {
"name": "@opencode-ai/http-recorder", "name": "@opencode-ai/http-recorder",
"version": "1.17.8", "version": "1.17.9",
"dependencies": { "dependencies": {
"@effect/platform-node": "4.0.0-beta.74", "@effect/platform-node": "4.0.0-beta.83",
"@effect/platform-node-shared": "4.0.0-beta.74", "@effect/platform-node-shared": "4.0.0-beta.83",
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/node22": "catalog:", "@tsconfig/node22": "catalog:",
@@ -472,12 +473,12 @@
"typescript": "catalog:", "typescript": "catalog:",
}, },
"peerDependencies": { "peerDependencies": {
"effect": "4.0.0-beta.74", "effect": "4.0.0-beta.83",
}, },
}, },
"packages/llm": { "packages/llm": {
"name": "@opencode-ai/llm", "name": "@opencode-ai/llm",
"version": "1.17.8", "version": "1.17.9",
"dependencies": { "dependencies": {
"@smithy/eventstream-codec": "4.2.14", "@smithy/eventstream-codec": "4.2.14",
"@smithy/util-utf8": "4.2.2", "@smithy/util-utf8": "4.2.2",
@@ -495,7 +496,7 @@
}, },
"packages/opencode": { "packages/opencode": {
"name": "opencode", "name": "opencode",
"version": "1.17.8", "version": "1.17.9",
"bin": { "bin": {
"opencode": "./bin/opencode", "opencode": "./bin/opencode",
}, },
@@ -623,8 +624,9 @@
}, },
"packages/plugin": { "packages/plugin": {
"name": "@opencode-ai/plugin", "name": "@opencode-ai/plugin",
"version": "1.17.8", "version": "1.17.9",
"dependencies": { "dependencies": {
"@ai-sdk/provider": "3.0.8",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"effect": "catalog:", "effect": "catalog:",
"zod": "catalog:", "zod": "catalog:",
@@ -661,7 +663,7 @@
}, },
"packages/sdk/js": { "packages/sdk/js": {
"name": "@opencode-ai/sdk", "name": "@opencode-ai/sdk",
"version": "1.17.8", "version": "1.17.9",
"dependencies": { "dependencies": {
"cross-spawn": "catalog:", "cross-spawn": "catalog:",
}, },
@@ -676,7 +678,7 @@
}, },
"packages/server": { "packages/server": {
"name": "@opencode-ai/server", "name": "@opencode-ai/server",
"version": "1.17.8", "version": "1.17.9",
"dependencies": { "dependencies": {
"@opencode-ai/core": "workspace:*", "@opencode-ai/core": "workspace:*",
"drizzle-orm": "catalog:", "drizzle-orm": "catalog:",
@@ -690,7 +692,7 @@
}, },
"packages/slack": { "packages/slack": {
"name": "@opencode-ai/slack", "name": "@opencode-ai/slack",
"version": "1.17.8", "version": "1.17.9",
"dependencies": { "dependencies": {
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1", "@slack/bolt": "^3.17.1",
@@ -703,7 +705,7 @@
}, },
"packages/stats/app": { "packages/stats/app": {
"name": "@opencode-ai/stats-app", "name": "@opencode-ai/stats-app",
"version": "1.17.8", "version": "1.17.9",
"dependencies": { "dependencies": {
"@ibm/plex": "6.4.1", "@ibm/plex": "6.4.1",
"@opencode-ai/stats-core": "workspace:*", "@opencode-ai/stats-core": "workspace:*",
@@ -736,7 +738,7 @@
}, },
"packages/stats/core": { "packages/stats/core": {
"name": "@opencode-ai/stats-core", "name": "@opencode-ai/stats-core",
"version": "1.17.8", "version": "1.17.9",
"dependencies": { "dependencies": {
"@aws-sdk/client-athena": "3.933.0", "@aws-sdk/client-athena": "3.933.0",
"@planetscale/database": "1.19.0", "@planetscale/database": "1.19.0",
@@ -755,7 +757,7 @@
}, },
"packages/stats/server": { "packages/stats/server": {
"name": "@opencode-ai/stats-server", "name": "@opencode-ai/stats-server",
"version": "1.17.8", "version": "1.17.9",
"dependencies": { "dependencies": {
"@aws-sdk/client-firehose": "3.933.0", "@aws-sdk/client-firehose": "3.933.0",
"@effect/platform-node": "catalog:", "@effect/platform-node": "catalog:",
@@ -795,7 +797,7 @@
}, },
"packages/tui": { "packages/tui": {
"name": "@opencode-ai/tui", "name": "@opencode-ai/tui",
"version": "1.17.8", "version": "1.17.9",
"dependencies": { "dependencies": {
"@opencode-ai/core": "workspace:*", "@opencode-ai/core": "workspace:*",
"@opencode-ai/plugin": "workspace:*", "@opencode-ai/plugin": "workspace:*",
@@ -822,7 +824,7 @@
}, },
"packages/ui": { "packages/ui": {
"name": "@opencode-ai/ui", "name": "@opencode-ai/ui",
"version": "1.17.8", "version": "1.17.9",
"dependencies": { "dependencies": {
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@opencode-ai/core": "workspace:*", "@opencode-ai/core": "workspace:*",
@@ -871,7 +873,7 @@
}, },
"packages/web": { "packages/web": {
"name": "@opencode-ai/web", "name": "@opencode-ai/web",
"version": "1.17.8", "version": "1.17.9",
"dependencies": { "dependencies": {
"@astrojs/cloudflare": "12.6.3", "@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1", "@astrojs/markdown-remark": "6.3.1",
@@ -935,9 +937,9 @@
}, },
"catalog": { "catalog": {
"@cloudflare/workers-types": "4.20251008.0", "@cloudflare/workers-types": "4.20251008.0",
"@effect/opentelemetry": "4.0.0-beta.74", "@effect/opentelemetry": "4.0.0-beta.83",
"@effect/platform-node": "4.0.0-beta.74", "@effect/platform-node": "4.0.0-beta.83",
"@effect/sql-sqlite-bun": "4.0.0-beta.74", "@effect/sql-sqlite-bun": "4.0.0-beta.83",
"@hono/standard-validator": "0.2.0", "@hono/standard-validator": "0.2.0",
"@hono/zod-validator": "0.4.2", "@hono/zod-validator": "0.4.2",
"@kobalte/core": "0.13.11", "@kobalte/core": "0.13.11",
@@ -973,7 +975,7 @@
"dompurify": "3.3.1", "dompurify": "3.3.1",
"drizzle-kit": "1.0.0-rc.2", "drizzle-kit": "1.0.0-rc.2",
"drizzle-orm": "1.0.0-rc.2", "drizzle-orm": "1.0.0-rc.2",
"effect": "4.0.0-beta.74", "effect": "4.0.0-beta.83",
"fuzzysort": "3.1.0", "fuzzysort": "3.1.0",
"hono": "4.10.7", "hono": "4.10.7",
"hono-openapi": "1.1.2", "hono-openapi": "1.1.2",
@@ -1328,13 +1330,13 @@
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="], "@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="],
"@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-beta.74", "", { "peerDependencies": { "@opentelemetry/api": "^1.9", "@opentelemetry/api-logs": ">=0.203.0 <0.300.0", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": "^2.0.0", "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/sdk-trace-node": "^2.0.0", "@opentelemetry/sdk-trace-web": "^2.0.0", "@opentelemetry/semantic-conventions": "^1.33.0", "effect": "^4.0.0-beta.74" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/api-logs", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-flpyqLPyr+THSe6ZCGRZl6hi+FqxbIXNSkslKGiRJAjbPabam9mSp7R3aC8biIMt6xE4Fd0LNfo4p2GplUkm2Q=="], "@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-beta.83", "", { "peerDependencies": { "@opentelemetry/api": "^1.9", "@opentelemetry/api-logs": ">=0.203.0 <0.300.0", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": "^2.0.0", "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/sdk-trace-node": "^2.0.0", "@opentelemetry/sdk-trace-web": "^2.0.0", "@opentelemetry/semantic-conventions": "^1.33.0", "effect": "^4.0.0-beta.83" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/api-logs", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-cPfCfp/ghu0itbX6Dqjdr4N0rbjng5ON4sUpnLHV5JJySG8zZpWmuOZLWIrfrNKT2ctYR1BYmp1aYCgkItaJLw=="],
"@effect/platform-node": ["@effect/platform-node@4.0.0-beta.74", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.74", "mime": "^4.1.0", "undici": "^8.2.0" }, "peerDependencies": { "effect": "^4.0.0-beta.74", "ioredis": "^5.7.0" } }, "sha512-/W16mKqxvhWINLjufzc0log1sl57exXQfwd+em398/zKCbmU3S7snXTDMN6w0ju2TtgK35qrsoGBXEochij6Sg=="], "@effect/platform-node": ["@effect/platform-node@4.0.0-beta.83", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.83", "mime": "^4.1.0", "undici": "^8.2.0" }, "peerDependencies": { "effect": "^4.0.0-beta.83", "ioredis": "^5.7.0" } }, "sha512-RmpVGu/+X/Bif3/g1Rzj8oFzTOknoVB3yHCa0b179vytPpKe+Kj9ZwKNcAnKWqHUDkbSPBq1Ca60mvOHr2/+LQ=="],
"@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.74", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.20.0" }, "peerDependencies": { "effect": "^4.0.0-beta.74" } }, "sha512-C6C2hXixNcZXLaFF2u7B/FtOsqpdY7luaPuiGFBJza0P7EnYDkwaT3kB6lv7l/qctmkADc24qOsSCWIKRbC4jg=="], "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.83", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.20.0" }, "peerDependencies": { "effect": "^4.0.0-beta.83" } }, "sha512-+yr/+PJmKTgmJq1QOINSBPgLu7Cjc4CZcotBXnGjyDEizOmimFgTkN2B8PBJAKIKUWYWfobjXqC+58/VhhPKAw=="],
"@effect/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0-beta.74", "", { "peerDependencies": { "effect": "^4.0.0-beta.74" } }, "sha512-RVMRVY7NhSoAp9cAAyy4TT6dt6NNZjOpWeqticoho9HNBukxQSUcu/kjcz4Iq9eoQfXadmepu8kZqtdZULM/fg=="], "@effect/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0-beta.83", "", { "peerDependencies": { "effect": "^4.0.0-beta.83" } }, "sha512-6OaxLsWffxkh9pXYUSyj/AxjVb9URY2rG9U6atjxClWy30Jx77R9Pm3Rrc7cQ63kQurePavEw1bQbzQ/SILiQQ=="],
"@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="], "@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="],
@@ -3366,7 +3368,7 @@
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
"effect": ["effect@4.0.0-beta.74", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-Yx+Kh12U+i2FmjwEfKs+ePFmpMd43RPD1oGqc/VraSS9bYzvF0Ff3PojwEFEVEewp8xc92Uxu28gTspU4qyvHA=="], "effect": ["effect@4.0.0-beta.83", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-0wsak8RtgGAr9UWSbVDgJHZcUqMSvicHcvaZv1MbMM7MCGgW4Rn/137J1MHQbwYPcwYGxT/IqehFd+UbYuj78w=="],
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
@@ -5898,6 +5900,10 @@
"@solidjs/start/vite": ["vite@7.1.10", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA=="], "@solidjs/start/vite": ["vite@7.1.10", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA=="],
"@standard-community/standard-json/effect": ["effect@4.0.0-beta.74", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-Yx+Kh12U+i2FmjwEfKs+ePFmpMd43RPD1oGqc/VraSS9bYzvF0Ff3PojwEFEVEewp8xc92Uxu28gTspU4qyvHA=="],
"@standard-community/standard-openapi/effect": ["effect@4.0.0-beta.74", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-Yx+Kh12U+i2FmjwEfKs+ePFmpMd43RPD1oGqc/VraSS9bYzvF0Ff3PojwEFEVEewp8xc92Uxu28gTspU4qyvHA=="],
"@storybook/csf-plugin/unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="], "@storybook/csf-plugin/unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="],
"@tailwindcss/oxide/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "@tailwindcss/oxide/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
@@ -6700,6 +6706,10 @@
"@solidjs/start/shiki/@shikijs/types": ["@shikijs/types@1.29.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw=="], "@solidjs/start/shiki/@shikijs/types": ["@shikijs/types@1.29.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw=="],
"@standard-community/standard-json/effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@standard-community/standard-openapi/effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@storybook/csf-plugin/unplugin/webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], "@storybook/csf-plugin/unplugin/webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="],
+4 -4
View File
@@ -1,8 +1,8 @@
{ {
"nodeModules": { "nodeModules": {
"x86_64-linux": "sha256-LOxTad/iCquvJyonFOcz6/rDTPNDmwyBnykhWZJ5GC4=", "x86_64-linux": "sha256-g0tDvRf7MErZ1PEeUazEYi492ZHiRT8kYv3bPdkss/I=",
"aarch64-linux": "sha256-iO+0vYhp+2x6ACmh5lQJ/2Ac4uZTqRZE/KhG3u0o6D8=", "aarch64-linux": "sha256-6sKgf3ftbIqlPxlFkoPzoWPsJp3IwXD+H3Y6g874xmk=",
"aarch64-darwin": "sha256-tpBydRbrJ+4QxmkGUt/BhME8q6ysCW/CXrsNshYgqDU=", "aarch64-darwin": "sha256-Se/Nls/KlkuK2ysDQ9DeAzSaX3NsL2iDdf/dsv2GIXc=",
"x86_64-darwin": "sha256-QQcI6SK7WJ7dSkX6xZuSQPoUdwfoCaimVgoHCnrO0wY=" "x86_64-darwin": "sha256-V9MCkqnvQ1nkD2PaaTfNFKkBZGymj6KxrSAK6+DTF8Y="
} }
} }
+4 -4
View File
@@ -30,9 +30,9 @@
"packages/slack" "packages/slack"
], ],
"catalog": { "catalog": {
"@effect/opentelemetry": "4.0.0-beta.74", "@effect/opentelemetry": "4.0.0-beta.83",
"@effect/platform-node": "4.0.0-beta.74", "@effect/platform-node": "4.0.0-beta.83",
"@effect/sql-sqlite-bun": "4.0.0-beta.74", "@effect/sql-sqlite-bun": "4.0.0-beta.83",
"@npmcli/arborist": "9.4.0", "@npmcli/arborist": "9.4.0",
"@types/bun": "1.3.13", "@types/bun": "1.3.13",
"@types/cross-spawn": "6.0.6", "@types/cross-spawn": "6.0.6",
@@ -61,7 +61,7 @@
"dompurify": "3.3.1", "dompurify": "3.3.1",
"drizzle-kit": "1.0.0-rc.2", "drizzle-kit": "1.0.0-rc.2",
"drizzle-orm": "1.0.0-rc.2", "drizzle-orm": "1.0.0-rc.2",
"effect": "4.0.0-beta.74", "effect": "4.0.0-beta.83",
"ai": "6.0.168", "ai": "6.0.168",
"cross-spawn": "7.0.6", "cross-spawn": "7.0.6",
"hono": "4.10.7", "hono": "4.10.7",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/app", "name": "@opencode-ai/app",
"version": "1.17.8", "version": "1.17.9",
"description": "", "description": "",
"type": "module", "type": "module",
"exports": { "exports": {
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/cli", "name": "@opencode-ai/cli",
"version": "1.17.8", "version": "1.17.9",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"bin": { "bin": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-app", "name": "@opencode-ai/console-app",
"version": "1.17.8", "version": "1.17.9",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core", "name": "@opencode-ai/console-core",
"version": "1.17.8", "version": "1.17.9",
"private": true, "private": true,
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-function", "name": "@opencode-ai/console-function",
"version": "1.17.8", "version": "1.17.9",
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"private": true, "private": true,
"type": "module", "type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-mail", "name": "@opencode-ai/console-mail",
"version": "1.17.8", "version": "1.17.9",
"dependencies": { "dependencies": {
"@jsx-email/all": "2.2.3", "@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3", "@jsx-email/cli": "1.4.3",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-support", "name": "@opencode-ai/console-support",
"version": "1.17.8", "version": "1.17.9",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
+2 -1
View File
@@ -1,6 +1,6 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"version": "1.17.8", "version": "1.17.9",
"name": "@opencode-ai/core", "name": "@opencode-ai/core",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
@@ -91,6 +91,7 @@
"@opencode-ai/effect-drizzle-sqlite": "workspace:*", "@opencode-ai/effect-drizzle-sqlite": "workspace:*",
"@opencode-ai/effect-sqlite-node": "workspace:*", "@opencode-ai/effect-sqlite-node": "workspace:*",
"@opencode-ai/llm": "workspace:*", "@opencode-ai/llm": "workspace:*",
"@opencode-ai/plugin": "workspace:*",
"@opentelemetry/api": "1.9.0", "@opentelemetry/api": "1.9.0",
"@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/context-async-hooks": "2.6.1",
"@opentelemetry/exporter-trace-otlp-http": "0.214.0", "@opentelemetry/exporter-trace-otlp-http": "0.214.0",
+4 -4
View File
@@ -35,19 +35,19 @@ export class Org extends Schema.Class<Org>("Org")({
export class AccountRepoError extends Schema.TaggedErrorClass<AccountRepoError>()("AccountRepoError", { export class AccountRepoError extends Schema.TaggedErrorClass<AccountRepoError>()("AccountRepoError", {
message: Schema.String, message: Schema.String,
cause: Schema.optional(Schema.Defect), cause: Schema.optional(Schema.Defect()),
}) {} }) {}
export class AccountServiceError extends Schema.TaggedErrorClass<AccountServiceError>()("AccountServiceError", { export class AccountServiceError extends Schema.TaggedErrorClass<AccountServiceError>()("AccountServiceError", {
message: Schema.String, message: Schema.String,
cause: Schema.optional(Schema.Defect), cause: Schema.optional(Schema.Defect()),
}) {} }) {}
export class AccountTransportError extends Schema.TaggedErrorClass<AccountTransportError>()("AccountTransportError", { export class AccountTransportError extends Schema.TaggedErrorClass<AccountTransportError>()("AccountTransportError", {
method: Schema.String, method: Schema.String,
url: Schema.String, url: Schema.String,
description: Schema.optional(Schema.String), description: Schema.optional(Schema.String),
cause: Schema.optional(Schema.Defect), cause: Schema.optional(Schema.Defect()),
}) { }) {
static fromHttpClientError(error: HttpClientError.TransportError): AccountTransportError { static fromHttpClientError(error: HttpClientError.TransportError): AccountTransportError {
return new AccountTransportError({ return new AccountTransportError({
@@ -94,7 +94,7 @@ export class PollExpired extends Schema.TaggedClass<PollExpired>()("PollExpired"
export class PollDenied extends Schema.TaggedClass<PollDenied>()("PollDenied", {}) {} export class PollDenied extends Schema.TaggedClass<PollDenied>()("PollDenied", {}) {}
export class PollError extends Schema.TaggedClass<PollError>()("PollError", { export class PollError extends Schema.TaggedClass<PollError>()("PollError", {
cause: Schema.Defect, cause: Schema.Defect(),
}) {} }) {}
export const PollResult = Schema.Union([PollSuccess, PollPending, PollSlow, PollExpired, PollDenied, PollError]) export const PollResult = Schema.Union([PollSuccess, PollPending, PollSlow, PollExpired, PollDenied, PollError])
+9 -14
View File
@@ -1,7 +1,6 @@
export * as AgentV2 from "./agent" export * as AgentV2 from "./agent"
import { Array, Context, Effect, Layer, Schema, Scope } from "effect" import { Array, Context, Effect, Layer, Schema, Scope, Types } from "effect"
import { castDraft, enableMapSet, type Draft } from "immer"
import { ModelV2 } from "./model" import { ModelV2 } from "./model"
import { PermissionSchema } from "./permission/schema" import { PermissionSchema } from "./permission/schema"
import { ProviderV2 } from "./provider" import { ProviderV2 } from "./provider"
@@ -49,21 +48,19 @@ export interface Selection {
} }
type Data = { type Data = {
agents: Map<ID, Info> agents: Map<ID, Types.DeepMutable<Info>>
default?: ID default?: ID
} }
export type Editor = { export type Draft = {
list: () => readonly Info[] list: () => readonly Info[]
get: (id: ID) => Info | undefined get: (id: ID) => Info | undefined
default: (id: ID | undefined) => void default: (id: ID | undefined) => void
update: (id: ID, fn: (agent: Draft<Info>) => void) => void update: (id: ID, fn: (agent: Types.DeepMutable<Info>) => void) => void
remove: (id: ID) => void remove: (id: ID) => void
} }
export interface Interface { export interface Interface extends State.Transformable<Draft> {
readonly transform: State.Interface<Data, Editor>["transform"]
readonly update: State.Interface<Data, Editor>["update"]
readonly get: (id: ID) => Effect.Effect<Info | undefined> readonly get: (id: ID) => Effect.Effect<Info | undefined>
readonly default: () => Effect.Effect<Info | undefined> readonly default: () => Effect.Effect<Info | undefined>
readonly resolve: (id?: ID | string) => Effect.Effect<Info | undefined> readonly resolve: (id?: ID | string) => Effect.Effect<Info | undefined>
@@ -73,21 +70,19 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Agent") {} export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Agent") {}
enableMapSet()
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const state = State.create<Data, Editor>({ const state = State.create<Data, Draft>({
initial: () => ({ agents: new Map() }), initial: () => ({ agents: new Map() }),
editor: (draft) => ({ draft: (draft) => ({
list: () => Array.fromIterable(draft.agents.values()) as Info[], list: () => Array.fromIterable(draft.agents.values()) as Info[],
get: (id) => draft.agents.get(id), get: (id) => draft.agents.get(id),
default: (id) => { default: (id) => {
draft.default = id draft.default = id
}, },
update: (id, fn) => { update: (id, fn) => {
const current = draft.agents.get(id) ?? castDraft(Info.empty(id)) const current = draft.agents.get(id) ?? (Info.empty(id) 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
@@ -113,7 +108,7 @@ export const layer = Layer.effect(
return Service.of({ return Service.of({
transform: state.transform, transform: state.transform,
update: state.update, rebuild: state.rebuild,
get: Effect.fn("AgentV2.get")(function* (id) { get: Effect.fn("AgentV2.get")(function* (id) {
return state.get().agents.get(id) return state.get().agents.get(id)
}), }),
+1 -1
View File
@@ -109,7 +109,7 @@ function prepareOptions(model: ModelV2.Info, pkg: string) {
export class InitError extends Schema.TaggedErrorClass<InitError>()("AISDK.InitError", { export class InitError extends Schema.TaggedErrorClass<InitError>()("AISDK.InitError", {
providerID: ProviderV2.ID, providerID: ProviderV2.ID,
cause: Schema.Defect, cause: Schema.Defect(),
}) {} }) {}
function initError(providerID: ProviderV2.ID) { function initError(providerID: ProviderV2.ID) {
+51 -93
View File
@@ -1,36 +1,21 @@
export * as Catalog from "./catalog" export * as Catalog from "./catalog"
import { Array, Context, Effect, Layer, Option, Order, pipe, Schema, Scope, Stream } from "effect" import { Array, Context, Effect, Layer, Option, Order, pipe, Schema } from "effect"
import { castDraft, enableMapSet, type Draft } from "immer"
import { ModelV2 } from "./model" import { ModelV2 } from "./model"
import { ModelRequest } from "./model-request" import { ModelRequest } from "./model-request"
import { PluginV2 } from "./plugin"
import { ProviderV2 } from "./provider" import { ProviderV2 } from "./provider"
import { Location } from "./location"
import { EventV2 } from "./event" import { EventV2 } from "./event"
import { Policy } from "./policy" import { Policy } from "./policy"
import { State } from "./state" import { State } from "./state"
import { Integration } from "./integration" import { Integration } from "./integration"
export type ProviderRecord = { export type ProviderRecord = {
provider: ProviderV2.Info provider: ProviderV2.MutableInfo
models: Map<ModelV2.ID, ModelV2.Info> models: Map<ModelV2.ID, ModelV2.MutableInfo>
} }
export type DefaultModel = { providerID: ProviderV2.ID; modelID: ModelV2.ID } export type DefaultModel = { providerID: ProviderV2.ID; modelID: ModelV2.ID }
export class ProviderNotFoundError extends Schema.TaggedErrorClass<ProviderNotFoundError>()(
"CatalogV2.ProviderNotFound",
{
providerID: ProviderV2.ID,
},
) {}
export class ModelNotFoundError extends Schema.TaggedErrorClass<ModelNotFoundError>()("CatalogV2.ModelNotFound", {
providerID: ProviderV2.ID,
modelID: ModelV2.ID,
}) {}
export const PolicyActions = Schema.Literals(["provider.use"]) export const PolicyActions = Schema.Literals(["provider.use"])
export const Event = { export const Event = {
@@ -42,16 +27,16 @@ type Data = {
defaultModel?: DefaultModel defaultModel?: DefaultModel
} }
export type Editor = { export type Draft = {
provider: { provider: {
list: () => readonly ProviderRecord[] list: () => readonly ProviderRecord[]
get: (providerID: ProviderV2.ID) => ProviderRecord | undefined get: (providerID: ProviderV2.ID) => ProviderRecord | undefined
update: (providerID: ProviderV2.ID, fn: (provider: Draft<ProviderV2.Info>) => void) => void update: (providerID: ProviderV2.ID, fn: (provider: ProviderV2.MutableInfo) => void) => void
remove: (providerID: ProviderV2.ID) => void remove: (providerID: ProviderV2.ID) => void
} }
model: { model: {
get: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => ModelV2.Info | undefined get: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => ModelV2.Info | undefined
update: (providerID: ProviderV2.ID, modelID: ModelV2.ID, fn: (model: Draft<ModelV2.Info>) => void) => void update: (providerID: ProviderV2.ID, modelID: ModelV2.ID, fn: (model: ModelV2.MutableInfo) => void) => void
remove: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => void remove: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => void
default: { default: {
get: () => DefaultModel | undefined get: () => DefaultModel | undefined
@@ -60,38 +45,29 @@ export type Editor = {
} }
} }
export interface Interface { export interface Interface extends State.Transformable<Draft> {
readonly transform: State.Interface<Data, Editor>["transform"]
readonly provider: { readonly provider: {
readonly get: (providerID: ProviderV2.ID) => Effect.Effect<ProviderV2.Info, ProviderNotFoundError> readonly get: (providerID: ProviderV2.ID) => Effect.Effect<ProviderV2.Info | undefined>
readonly all: () => Effect.Effect<ProviderV2.Info[]> readonly all: () => Effect.Effect<ProviderV2.Info[]>
readonly available: () => Effect.Effect<ProviderV2.Info[]> readonly available: () => Effect.Effect<ProviderV2.Info[]>
} }
readonly model: { readonly model: {
readonly get: ( readonly get: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => Effect.Effect<ModelV2.Info | undefined>
providerID: ProviderV2.ID,
modelID: ModelV2.ID,
) => Effect.Effect<ModelV2.Info, ProviderNotFoundError | ModelNotFoundError>
readonly all: () => Effect.Effect<ModelV2.Info[]> readonly all: () => Effect.Effect<ModelV2.Info[]>
readonly available: () => Effect.Effect<ModelV2.Info[]> readonly available: () => Effect.Effect<ModelV2.Info[]>
readonly default: () => Effect.Effect<Option.Option<ModelV2.Info>> readonly default: () => Effect.Effect<ModelV2.Info | undefined>
readonly small: (providerID: ProviderV2.ID) => Effect.Effect<Option.Option<ModelV2.Info>> readonly small: (providerID: ProviderV2.ID) => Effect.Effect<ModelV2.Info | undefined>
} }
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Catalog") {} export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Catalog") {}
enableMapSet()
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const location = yield* Location.Service
const plugin = yield* PluginV2.Service
const events = yield* EventV2.Service const events = yield* EventV2.Service
const policy = yield* Policy.Service const policy = yield* Policy.Service
const integrations = yield* Integration.Service const integrations = yield* Integration.Service
const scope = yield* Scope.Scope
const available = (provider: ProviderV2.Info, integration: Integration.Info | undefined, connected: boolean) => { const available = (provider: ProviderV2.Info, integration: Integration.Info | undefined, connected: boolean) => {
if (provider.disabled) return false if (provider.disabled) return false
@@ -120,32 +96,26 @@ export const layer = Layer.effect(
}) })
} }
function* getRecord(providerID: ProviderV2.ID) { const normalizeApi = (item: ProviderV2.MutableInfo | ModelV2.MutableInfo) => {
const match = state.get().providers.get(providerID)
if (!match) return yield* new ProviderNotFoundError({ providerID })
return match
}
const normalizeApi = (item: Draft<ProviderV2.Info> | Draft<ModelV2.Info>) => {
if (typeof item.request.body.baseURL !== "string") return if (typeof item.request.body.baseURL !== "string") return
item.api.url = item.request.body.baseURL item.api.url = item.request.body.baseURL
delete item.request.body.baseURL delete item.request.body.baseURL
} }
const state = State.create<Data, Editor>({ const state = State.create<Data, Draft>({
initial: () => ({ providers: new Map() }), initial: () => ({ providers: new Map() }),
editor: (draft) => { draft: (draft) => {
const result: Editor = { const result: Draft = {
provider: { provider: {
list: () => Array.fromIterable(draft.providers.values()) as ProviderRecord[], list: () => Array.fromIterable(draft.providers.values()) as ProviderRecord[],
get: (providerID) => draft.providers.get(providerID), get: (providerID) => draft.providers.get(providerID),
update: (providerID, fn) => { update: (providerID, fn) => {
let current = draft.providers.get(providerID) let current = draft.providers.get(providerID)
if (!current) { if (!current) {
current = castDraft({ current = {
provider: ProviderV2.Info.empty(providerID), provider: ProviderV2.Info.empty(providerID) as ProviderV2.MutableInfo,
models: new Map<ModelV2.ID, ModelV2.Info>(), models: new Map<ModelV2.ID, ModelV2.MutableInfo>(),
}) }
draft.providers.set(providerID, current) draft.providers.set(providerID, current)
} }
fn(current.provider) fn(current.provider)
@@ -160,13 +130,14 @@ export const layer = Layer.effect(
update: (providerID, modelID, fn) => { update: (providerID, modelID, fn) => {
let record = draft.providers.get(providerID) let record = draft.providers.get(providerID)
if (!record) { if (!record) {
record = castDraft({ record = {
provider: ProviderV2.Info.empty(providerID), provider: ProviderV2.Info.empty(providerID) as ProviderV2.MutableInfo,
models: new Map<ModelV2.ID, ModelV2.Info>(), models: new Map<ModelV2.ID, ModelV2.MutableInfo>(),
}) }
draft.providers.set(providerID, record) draft.providers.set(providerID, record)
} }
const model = record.models.get(modelID) ?? castDraft(ModelV2.Info.empty(providerID, modelID)) const model =
record.models.get(modelID) ?? (ModelV2.Info.empty(providerID, modelID) as ModelV2.MutableInfo)
if (!record.models.has(modelID)) record.models.set(modelID, model) if (!record.models.has(modelID)) record.models.set(modelID, model)
fn(model) fn(model)
model.id = modelID model.id = modelID
@@ -186,8 +157,7 @@ export const layer = Layer.effect(
} }
return result return result
}, },
finalize: Effect.fn("CatalogV2.finalize")(function* (catalog, reason) { finalize: Effect.fn("CatalogV2.finalize")(function* (catalog) {
if (reason !== "plugin.added") yield* plugin.trigger("catalog.transform", catalog, {}).pipe(Effect.asVoid)
if (policy.hasStatements()) { if (policy.hasStatements()) {
for (const record of [...catalog.provider.list()]) { for (const record of [...catalog.provider.list()]) {
if ((yield* policy.evaluate("provider.use", record.provider.id, "allow")) === "deny") { if ((yield* policy.evaluate("provider.use", record.provider.id, "allow")) === "deny") {
@@ -198,25 +168,13 @@ export const layer = Layer.effect(
yield* events.publish(Event.Updated, {}) yield* events.publish(Event.Updated, {})
}), }),
}) })
yield* events.subscribe(PluginV2.Event.Added).pipe(
// Plugin registries are location scoped even though the event bus is process scoped.
Stream.filter(
(event) =>
event.location?.directory === location.directory && event.location.workspaceID === location.workspaceID,
),
Stream.runForEach((event) =>
state.mutate((catalog) => plugin.triggerFor(event.data.id, "catalog.transform", catalog, {}), "plugin.added"),
),
Effect.forkIn(scope, { startImmediately: true }),
)
const result: Interface = { const result: Interface = {
transform: state.transform, transform: state.transform,
rebuild: state.rebuild,
provider: { provider: {
get: Effect.fn("CatalogV2.provider.get")(function* (providerID) { get: Effect.fn("CatalogV2.provider.get")(function* (providerID) {
const record = yield* getRecord(providerID) return state.get().providers.get(providerID)?.provider
return record.provider
}), }),
all: Effect.fn("CatalogV2.provider.all")(function* () { all: Effect.fn("CatalogV2.provider.all")(function* () {
@@ -238,10 +196,10 @@ export const layer = Layer.effect(
model: { model: {
get: Effect.fn("CatalogV2.model.get")(function* (providerID, modelID) { get: Effect.fn("CatalogV2.model.get")(function* (providerID, modelID) {
const record = yield* getRecord(providerID) const record = state.get().providers.get(providerID)
if (!record) return
const model = record.models.get(modelID) const model = record.models.get(modelID)
if (!model) return yield* new ModelNotFoundError({ providerID, modelID }) return model && projectModel(model, record.provider)
return projectModel(model, record.provider)
}), }),
all: Effect.fn("CatalogV2.model.all")(function* () { all: Effect.fn("CatalogV2.model.all")(function* () {
@@ -250,7 +208,7 @@ export const layer = Layer.effect(
Array.flatMap((record) => { Array.flatMap((record) => {
return Array.fromIterable(record.models.values()).map((model) => projectModel(model, record.provider)) return Array.fromIterable(record.models.values()).map((model) => projectModel(model, record.provider))
}), }),
Array.sortWith((item) => item.time.released.epochMilliseconds, Order.flip(Order.Number)), Array.sortWith((item) => item.time.released, Order.flip(Order.Number)),
) )
}), }),
@@ -262,31 +220,30 @@ export const layer = Layer.effect(
default: Effect.fn("CatalogV2.model.default")(function* () { default: Effect.fn("CatalogV2.model.default")(function* () {
const defaultModel = state.get().defaultModel const defaultModel = state.get().defaultModel
if (defaultModel) { if (defaultModel) {
const provider = yield* result.provider.get(defaultModel.providerID).pipe(Effect.option) const provider = yield* result.provider.get(defaultModel.providerID)
if ( if (provider && (yield* result.provider.available()).some((item) => item.id === provider.id)) {
Option.isSome(provider) && const model = yield* result.model.get(defaultModel.providerID, defaultModel.modelID)
(yield* result.provider.available()).some((item) => item.id === provider.value.id) if (model?.enabled) return model
) {
const model = yield* result.model.get(defaultModel.providerID, defaultModel.modelID).pipe(Effect.option)
if (Option.isSome(model) && model.value.enabled) return model
} }
} }
return pipe( return Option.getOrUndefined(
yield* result.model.available(), pipe(
Array.sortWith((item) => item.time.released.epochMilliseconds, Order.flip(Order.Number)), yield* result.model.available(),
Array.head, Array.sortWith((item) => item.time.released, Order.flip(Order.Number)),
Array.head,
),
) )
}), }),
small: Effect.fn("CatalogV2.model.small")(function* (providerID) { small: Effect.fn("CatalogV2.model.small")(function* (providerID) {
const record = state.get().providers.get(providerID) const record = state.get().providers.get(providerID)
if (!record) return Option.none<ModelV2.Info>() if (!record) return
const provider = record.provider const provider = record.provider
if (providerID === ProviderV2.ID.opencode) { if (providerID === ProviderV2.ID.opencode) {
const gpt5Nano = record.models.get(ModelV2.ID.make("gpt-5-nano")) const gpt5Nano = record.models.get(ModelV2.ID.make("gpt-5-nano"))
if (gpt5Nano?.enabled && gpt5Nano.status === "active") return Option.some(projectModel(gpt5Nano, provider)) if (gpt5Nano?.enabled && gpt5Nano.status === "active") return projectModel(gpt5Nano, provider)
} }
const candidates = pipe( const candidates = pipe(
@@ -302,7 +259,7 @@ export const layer = Layer.effect(
Array.map((model) => ({ Array.map((model) => ({
model, model,
cost: model.cost[0] ? model.cost[0].input + model.cost[0].output : 999, cost: model.cost[0] ? model.cost[0].input + model.cost[0].output : 999,
age: (Date.now() - model.time.released.epochMilliseconds) / (1000 * 60 * 60 * 24 * 30), age: (Date.now() - model.time.released) / (1000 * 60 * 60 * 24 * 30),
small: SMALL_MODEL_RE.test(`${model.id} ${model.family ?? ""} ${model.name}`.toLowerCase()), small: SMALL_MODEL_RE.test(`${model.id} ${model.family ?? ""} ${model.name}`.toLowerCase()),
})), })),
Array.filter((item) => item.cost > 0 && item.age <= 18), Array.filter((item) => item.cost > 0 && item.age <= 18),
@@ -319,10 +276,12 @@ export const layer = Layer.effect(
) )
} }
return pipe( return Option.getOrUndefined(
candidates, pipe(
Array.filter((item) => item.small), candidates,
(items) => (items.length > 0 ? pick(items) : pick(candidates)), Array.filter((item) => item.small),
(items) => (items.length > 0 ? pick(items) : pick(candidates)),
),
) )
}), }),
}, },
@@ -336,6 +295,5 @@ const SMALL_MODEL_RE = /\b(nano|flash|lite|mini|haiku|small|fast)\b/
export const locationLayer = layer.pipe( export const locationLayer = layer.pipe(
Layer.provideMerge(Integration.locationLayer), Layer.provideMerge(Integration.locationLayer),
Layer.provideMerge(PluginV2.locationLayer),
Layer.provideMerge(Policy.locationLayer), Layer.provideMerge(Policy.locationLayer),
) )
+9 -12
View File
@@ -1,7 +1,6 @@
export * as CommandV2 from "./command" export * as CommandV2 from "./command"
import { Context, Effect, Layer, Schema } from "effect" import { Context, Effect, Layer, Schema, Types } from "effect"
import { castDraft, type Draft } from "immer"
import { ModelV2 } from "./model" import { ModelV2 } from "./model"
import { State } from "./state" import { State } from "./state"
@@ -15,19 +14,17 @@ export class Info extends Schema.Class<Info>("CommandV2.Info")({
}) {} }) {}
export type Data = { export type Data = {
commands: Map<string, Info> commands: Map<string, Types.DeepMutable<Info>>
} }
export type Editor = { export type Draft = {
list: () => readonly Info[] list: () => readonly Info[]
get: (name: string) => Info | undefined get: (name: string) => Info | undefined
update: (name: string, update: (command: Draft<Info>) => void) => void update: (name: string, update: (command: Types.DeepMutable<Info>) => void) => void
remove: (name: string) => void remove: (name: string) => void
} }
export interface Interface { export interface Interface extends State.Transformable<Draft> {
readonly transform: State.Interface<Data, Editor>["transform"]
readonly update: State.Interface<Data, Editor>["update"]
readonly get: (name: string) => Effect.Effect<Info | undefined> readonly get: (name: string) => Effect.Effect<Info | undefined>
readonly list: () => Effect.Effect<Info[]> readonly list: () => Effect.Effect<Info[]>
} }
@@ -37,13 +34,13 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
Effect.sync(() => { Effect.sync(() => {
const state = State.create<Data, Editor>({ const state = State.create<Data, Draft>({
initial: () => ({ commands: new Map() }), initial: () => ({ commands: new Map() }),
editor: (draft) => ({ draft: (draft) => ({
list: () => Array.from(draft.commands.values()) as Info[], list: () => Array.from(draft.commands.values()) as Info[],
get: (name) => draft.commands.get(name), get: (name) => draft.commands.get(name),
update: (name, update) => { update: (name, update) => {
const current = draft.commands.get(name) ?? castDraft(new Info({ name, template: "" })) const current = draft.commands.get(name) ?? (new Info({ name, template: "" }) as Types.DeepMutable<Info>)
if (!draft.commands.has(name)) draft.commands.set(name, current) if (!draft.commands.has(name)) draft.commands.set(name, current)
update(current) update(current)
current.name = name current.name = name
@@ -55,7 +52,7 @@ export const layer = Layer.effect(
}) })
return Service.of({ return Service.of({
update: state.update, rebuild: state.rebuild,
transform: state.transform, transform: state.transform,
get: Effect.fn("CommandV2.get")(function* (name) { get: Effect.fn("CommandV2.get")(function* (name) {
return state.get().commands.get(name) return state.get().commands.get(name)
+61 -61
View File
@@ -1,5 +1,6 @@
export * as ConfigAgentPlugin from "./agent" export * as ConfigAgentPlugin from "./agent"
import { define } from "@opencode-ai/plugin/v2/effect"
import path from "path" import path from "path"
import { Effect, Option, Schema } from "effect" import { Effect, Option, Schema } from "effect"
import { AgentV2 } from "../../agent" import { AgentV2 } from "../../agent"
@@ -8,7 +9,6 @@ import { ConfigAgent } from "../agent"
import { ConfigMarkdown } from "../markdown" import { ConfigMarkdown } from "../markdown"
import { FSUtil } from "../../fs-util" import { FSUtil } from "../../fs-util"
import { ModelV2 } from "../../model" import { ModelV2 } from "../../model"
import { PluginV2 } from "../../plugin"
import { ConfigAgentV1 } from "../../v1/config/agent" import { ConfigAgentV1 } from "../../v1/config/agent"
import { ConfigMigrateV1 } from "../../v1/config/migrate" import { ConfigMigrateV1 } from "../../v1/config/migrate"
@@ -33,70 +33,70 @@ const agentKeys = new Set([
"permissions", "permissions",
]) ])
export const Plugin = PluginV2.define({ export const Plugin = define({
id: PluginV2.ID.make("config-agent"), id: "config-agent",
effect: Effect.gen(function* () { effect: Effect.fn(function* (ctx) {
const agent = yield* AgentV2.Service
const config = yield* Config.Service const config = yield* Config.Service
const fs = yield* FSUtil.Service const fs = yield* FSUtil.Service
const documents = yield* Effect.forEach(yield* config.entries(), (entry) => { yield* ctx.agent.transform(
if (entry.type === "document") return Effect.succeed([entry]) Effect.fn(function* (draft) {
return Effect.gen(function* () { const documents = yield* Effect.forEach(yield* config.entries(), (entry) => {
const files = yield* discover(fs, entry.path) if (entry.type === "document") return Effect.succeed([entry])
return yield* Effect.forEach(files, (file) => return Effect.gen(function* () {
fs.readFileStringSafe(file.filepath).pipe( const files = yield* discover(fs, entry.path)
Effect.map((content) => content && decode(file, content)), return yield* Effect.forEach(files, (file) =>
Effect.catch(() => Effect.succeed(undefined)), fs.readFileStringSafe(file.filepath).pipe(
), Effect.map((content) => content && decode(file, content)),
).pipe( Effect.catch(() => Effect.succeed(undefined)),
Effect.map((documents) => ),
documents.filter((document): document is Config.Document => document !== undefined), ).pipe(
), Effect.map((documents) =>
) documents.filter((document): document is Config.Document => document !== undefined),
}) ),
}).pipe(Effect.map((documents) => documents.flat())) )
yield* agent.update((editor) => {
const global = documents.flatMap((document) => document.info.permissions ?? [])
const configuredDefault = Config.latest(documents, "default_agent")
if (configuredDefault !== undefined) editor.default(AgentV2.ID.make(configuredDefault))
for (const current of editor.list()) {
editor.update(current.id, (agent) => agent.permissions.push(...global))
}
for (const document of documents) {
for (const [id, item] of Object.entries(document.info.agents ?? {})) {
const agentID = AgentV2.ID.make(id)
if (item.disabled) {
editor.remove(agentID)
continue
}
const exists = editor.get(agentID) !== undefined
editor.update(agentID, (agent) => {
if (!exists) agent.permissions.push(...global)
if (item.model !== undefined) {
const model = ModelV2.parse(item.model)
agent.model = { id: model.modelID, providerID: model.providerID, variant: agent.model?.variant }
}
if (item.variant !== undefined && agent.model !== undefined) {
agent.model.variant = ModelV2.VariantID.make(item.variant)
}
if (item.request !== undefined) {
Object.assign(agent.request.headers, item.request.headers ?? {})
Object.assign(agent.request.body, item.request.body ?? {})
}
if (item.system !== undefined) agent.system = item.system
if (item.description !== undefined) agent.description = item.description
if (item.mode !== undefined) agent.mode = item.mode
if (item.hidden !== undefined) agent.hidden = item.hidden
if (item.color !== undefined) agent.color = item.color
if (item.steps !== undefined) agent.steps = item.steps
if (item.permissions !== undefined) agent.permissions.push(...item.permissions)
}) })
}).pipe(Effect.map((documents) => documents.flat()))
const global = documents.flatMap((document) => document.info.permissions ?? [])
const configuredDefault = Config.latest(documents, "default_agent")
if (configuredDefault !== undefined) draft.default(AgentV2.ID.make(configuredDefault))
for (const current of draft.list()) {
draft.update(current.id, (agent) => agent.permissions.push(...global))
} }
}
}) for (const document of documents) {
for (const [id, item] of Object.entries(document.info.agents ?? {})) {
const agentID = AgentV2.ID.make(id)
if (item.disabled) {
draft.remove(agentID)
continue
}
const exists = draft.get(agentID) !== undefined
draft.update(agentID, (agent) => {
if (!exists) agent.permissions.push(...global)
if (item.model !== undefined) {
const model = ModelV2.parse(item.model)
agent.model = { id: model.modelID, providerID: model.providerID, variant: agent.model?.variant }
}
if (item.variant !== undefined && agent.model !== undefined) {
agent.model.variant = ModelV2.VariantID.make(item.variant)
}
if (item.request !== undefined) {
Object.assign(agent.request.headers, item.request.headers ?? {})
Object.assign(agent.request.body, item.request.body ?? {})
}
if (item.system !== undefined) agent.system = item.system
if (item.description !== undefined) agent.description = item.description
if (item.mode !== undefined) agent.mode = item.mode
if (item.hidden !== undefined) agent.hidden = item.hidden
if (item.color !== undefined) agent.color = item.color
if (item.steps !== undefined) agent.steps = item.steps
if (item.permissions !== undefined) agent.permissions.push(...item.permissions)
})
}
}
}),
)
}), }),
}) })
+32 -33
View File
@@ -1,52 +1,51 @@
export * as ConfigCommandPlugin from "./command" export * as ConfigCommandPlugin from "./command"
import { define } from "@opencode-ai/plugin/v2/effect"
import path from "path" import path from "path"
import { Effect, Option, Schema } from "effect" import { Effect, Option, Schema } from "effect"
import { CommandV2 } from "../../command" import { CommandV2 } from "../../command"
import { Config } from "../../config" import { Config } from "../../config"
import { FSUtil } from "../../fs-util" import { FSUtil } from "../../fs-util"
import { ModelV2 } from "../../model" import { ModelV2 } from "../../model"
import { PluginV2 } from "../../plugin"
import { ConfigCommand } from "../command" import { ConfigCommand } from "../command"
import { ConfigMarkdown } from "../markdown" import { ConfigMarkdown } from "../markdown"
const decodeCommand = Schema.decodeUnknownOption(ConfigCommand.Info) const decodeCommand = Schema.decodeUnknownOption(ConfigCommand.Info)
export const Plugin = PluginV2.define({ export const Plugin = define({
id: PluginV2.ID.make("config-command"), id: "config-command",
effect: Effect.gen(function* () { effect: Effect.fn(function* (ctx) {
const command = yield* CommandV2.Service
const config = yield* Config.Service const config = yield* Config.Service
const fs = yield* FSUtil.Service const fs = yield* FSUtil.Service
const transform = yield* command.transform() yield* ctx.command.transform(
const documents = yield* Effect.forEach(yield* config.entries(), (entry) => { Effect.fn(function* (draft) {
if (entry.type === "document") return Effect.succeed([{ commands: entry.info.commands }]) const documents = yield* Effect.forEach(yield* config.entries(), (entry) => {
return loadDirectory(fs, entry.path).pipe( if (entry.type === "document") return Effect.succeed([{ commands: entry.info.commands }])
Effect.map((commands) => [ return loadDirectory(fs, entry.path).pipe(
{ commands: Object.fromEntries(commands.map((command) => [command.name, command.info])) }, Effect.map((commands) => [
]), { commands: Object.fromEntries(commands.map((command) => [command.name, command.info])) },
) ]),
}).pipe(Effect.map((documents) => documents.flat())) )
}).pipe(Effect.map((documents) => documents.flat()))
yield* transform((editor) => { for (const document of documents) {
for (const document of documents) { for (const [name, command] of Object.entries(document.commands ?? {})) {
for (const [name, command] of Object.entries(document.commands ?? {})) { draft.update(name, (item) => {
editor.update(name, (item) => { item.template = command.template
item.template = command.template if (command.description !== undefined) item.description = command.description
if (command.description !== undefined) item.description = command.description if (command.agent !== undefined) item.agent = command.agent
if (command.agent !== undefined) item.agent = command.agent if (command.model !== undefined) {
if (command.model !== undefined) { const model = ModelV2.parse(command.model)
const model = ModelV2.parse(command.model) item.model = { id: model.modelID, providerID: model.providerID, variant: item.model?.variant }
item.model = { id: model.modelID, providerID: model.providerID, variant: item.model?.variant } }
} if (command.variant !== undefined && item.model !== undefined) {
if (command.variant !== undefined && item.model !== undefined) { item.model.variant = ModelV2.VariantID.make(command.variant)
item.model.variant = ModelV2.VariantID.make(command.variant) }
} if (command.subtask !== undefined) item.subtask = command.subtask
if (command.subtask !== undefined) item.subtask = command.subtask })
}) }
} }
} }),
}) )
}), }),
}) })
+109 -108
View File
@@ -1,123 +1,124 @@
export * as ConfigProviderPlugin from "./provider" export * as ConfigProviderPlugin from "./provider"
import { define } from "@opencode-ai/plugin/v2/effect"
import { Effect } from "effect" import { Effect } from "effect"
import { Catalog } from "../../catalog"
import { Config } from "../../config" import { Config } from "../../config"
import { Integration } from "../../integration"
import { ModelV2 } from "../../model" import { ModelV2 } from "../../model"
import { ModelRequest } from "../../model-request" import { ModelRequest } from "../../model-request"
import { PluginV2 } from "../../plugin"
import { ProviderV2 } from "../../provider" import { ProviderV2 } from "../../provider"
export const Plugin = PluginV2.define({ export const Plugin = define({
id: PluginV2.ID.make("config-provider"), id: "config-provider",
effect: Effect.gen(function* () { effect: Effect.fn(function* (ctx) {
const catalog = yield* Catalog.Service
const config = yield* Config.Service const config = yield* Config.Service
const integrations = yield* Integration.Service yield* ctx.integration.transform(
const transform = yield* catalog.transform() Effect.fn(function* (integrations) {
const integrationTransform = yield* integrations.transform() const files = (yield* config.entries()).filter((entry): entry is Config.Document => entry.type === "document")
const entries = yield* config.entries() const configuredIntegrations = new Set(
const files = entries.filter((entry): entry is Config.Document => entry.type === "document") files.flatMap((file) =>
const configuredIntegrations = new Set( Object.entries(file.info.providers ?? {}).flatMap(([id, provider]) =>
files.flatMap((file) => provider.env === undefined ? [] : [id],
Object.entries(file.info.providers ?? {}).flatMap(([id, provider]) => (provider.env === undefined ? [] : [id])), ),
), ),
) )
yield* integrationTransform((integrations) => { for (const file of files) {
for (const file of files) { for (const [id, item] of Object.entries(file.info.providers ?? {})) {
for (const [id, item] of Object.entries(file.info.providers ?? {})) { const integrationID = id
const integrationID = Integration.ID.make(id) if (!configuredIntegrations.has(id) && !integrations.get(integrationID)) continue
if (!configuredIntegrations.has(id) && !integrations.get(integrationID)) continue integrations.update(integrationID, (integration) => {
integrations.update(integrationID, (integration) => { integration.name = item.name ?? integration.name
integration.name = item.name ?? integration.name
})
if (item.env !== undefined) {
integrations.method.update({
integrationID,
method: { type: "env", names: [...item.env] },
}) })
} if (item.env !== undefined) {
} integrations.method.update({
} integrationID,
}) method: { type: "env", names: [...item.env] },
})
yield* transform((catalog) => {
const configuredDefault = Config.latest(entries, "model")
if (configuredDefault !== undefined) {
const model = ModelV2.parse(configuredDefault)
catalog.model.default.set(model.providerID, model.modelID)
}
for (const file of files) {
for (const [id, item] of Object.entries(file.info.providers ?? {})) {
const providerID = ProviderV2.ID.make(id)
catalog.provider.update(providerID, (provider) => {
if (item.name !== undefined) provider.name = item.name
if (item.api !== undefined) provider.api = { ...item.api }
if (item.request !== undefined) {
Object.assign(provider.request.headers, item.request.headers)
Object.assign(provider.request.body, item.request.body)
} }
})
const providerApi = catalog.provider.get(providerID)?.provider.api
const providerPackage = providerApi?.type === "aisdk" ? providerApi.package : undefined
for (const [id, config] of Object.entries(item.models ?? {})) {
catalog.model.update(providerID, ModelV2.ID.make(id), (model) => {
if (config.family !== undefined) model.family = config.family
if (config.name !== undefined) model.name = config.name
if (config.api !== undefined) model.api = { ...model.api, ...config.api }
const packageName = model.api.type === "aisdk" ? model.api.package : providerPackage
if (config.capabilities !== undefined) {
model.capabilities = {
tools: config.capabilities.tools,
input: [...config.capabilities.input],
output: [...config.capabilities.output],
}
}
if (config.request !== undefined) {
ModelRequest.assign(model.request, {
headers: config.request.headers,
...ModelRequest.normalizeAiSdkOptions(packageName, config.request.body ?? {}),
})
if (config.request.variant !== undefined) model.request.variant = config.request.variant
}
if (config.variants !== undefined) {
for (const variant of config.variants) {
let existing = model.variants.find((item) => item.id === variant.id)
if (!existing) {
existing = {
id: variant.id,
headers: {},
body: {},
generation: {},
options: {},
}
model.variants.push(existing)
}
ModelRequest.assign(existing, {
headers: variant.headers,
...ModelRequest.normalizeAiSdkOptions(packageName, variant.body ?? {}),
})
}
}
if (config.cost !== undefined) {
model.cost = (Array.isArray(config.cost) ? config.cost : [config.cost]).map((cost) => ({
tier: cost.tier && { ...cost.tier },
input: cost.input,
output: cost.output,
cache: {
read: cost.cache?.read ?? 0,
write: cost.cache?.write ?? 0,
},
}))
}
if (config.disabled !== undefined) model.enabled = !config.disabled
if (config.limit !== undefined) model.limit = { ...model.limit, ...config.limit }
})
} }
} }
} }),
}) )
yield* ctx.catalog.transform(
Effect.fn(function* (catalog) {
const entries = yield* config.entries()
const files = entries.filter((entry): entry is Config.Document => entry.type === "document")
const configuredDefault = Config.latest(entries, "model")
if (configuredDefault !== undefined) {
const model = ModelV2.parse(configuredDefault)
catalog.model.default.set(model.providerID, model.modelID)
}
for (const file of files) {
for (const [id, item] of Object.entries(file.info.providers ?? {})) {
const providerID = id
catalog.provider.update(providerID, (provider) => {
if (item.name !== undefined) provider.name = item.name
if (item.api !== undefined) provider.api = { ...item.api }
if (item.request !== undefined) {
Object.assign(provider.request.headers, item.request.headers)
Object.assign(provider.request.body, item.request.body)
}
})
const providerApi = catalog.provider.get(providerID)?.provider.api
const providerPackage = providerApi?.type === "aisdk" ? providerApi.package : undefined
for (const [id, config] of Object.entries(item.models ?? {})) {
catalog.model.update(providerID, id, (model) => {
if (config.family !== undefined) model.family = config.family
if (config.name !== undefined) model.name = config.name
if (config.api !== undefined) model.api = { ...model.api, ...config.api }
const packageName = model.api.type === "aisdk" ? model.api.package : providerPackage
if (config.capabilities !== undefined) {
model.capabilities = {
tools: config.capabilities.tools,
input: [...config.capabilities.input],
output: [...config.capabilities.output],
}
}
if (config.request !== undefined) {
ModelRequest.assign(model.request, {
headers: config.request.headers,
...ModelRequest.normalizeAiSdkOptions(packageName, config.request.body ?? {}),
})
if (config.request.variant !== undefined) model.request.variant = config.request.variant
}
if (config.variants !== undefined) {
for (const variant of config.variants) {
let existing = model.variants.find((item) => item.id === variant.id)
if (!existing) {
existing = {
id: variant.id,
headers: {},
body: {},
generation: {},
options: {},
}
model.variants.push(existing)
}
ModelRequest.assign(existing, {
headers: variant.headers,
...ModelRequest.normalizeAiSdkOptions(packageName, variant.body ?? {}),
})
}
}
if (config.cost !== undefined) {
model.cost = (Array.isArray(config.cost) ? config.cost : [config.cost]).map((cost) => ({
tier: cost.tier && { ...cost.tier },
input: cost.input,
output: cost.output,
cache: {
read: cost.cache?.read ?? 0,
write: cost.cache?.write ?? 0,
},
}))
}
if (config.disabled !== undefined) model.enabled = !config.disabled
if (config.limit !== undefined) model.limit = { ...model.limit, ...config.limit }
})
}
}
}
}),
)
}), }),
}) })
+38 -43
View File
@@ -1,57 +1,52 @@
export * as ConfigReferencePlugin from "./reference" export * as ConfigReferencePlugin from "./reference"
import { define } from "@opencode-ai/plugin/v2/effect"
import path from "path" import path from "path"
import { Effect } from "effect" import { Effect } from "effect"
import { Config } from "../../config" import { Config } from "../../config"
import { ConfigReference } from "../reference" import { ConfigReference } from "../reference"
import { Global } from "../../global"
import { Location } from "../../location"
import { PluginV2 } from "../../plugin"
import { Reference } from "../../reference" import { Reference } from "../../reference"
import { AbsolutePath } from "../../schema" import { AbsolutePath } from "../../schema"
export const Plugin = { export const Plugin = define({
id: PluginV2.ID.make("core/config-reference"), id: "core/config-reference",
effect: Effect.gen(function* () { effect: Effect.fn(function* (ctx) {
const config = yield* Config.Service const config = yield* Config.Service
const global = yield* Global.Service yield* ctx.reference.transform(
const location = yield* Location.Service Effect.fn(function* (draft) {
const references = yield* Reference.Service const entries = new Map<string, Reference.Source>()
const update = yield* references.transform() for (const doc of (yield* config.entries()).filter(
const entries = new Map<string, Reference.Source>() (entry): entry is Config.Document => entry.type === "document",
for (const doc of (yield* config.entries()).filter( )) {
(entry): entry is Config.Document => entry.type === "document", const directory = doc.path ? path.dirname(doc.path) : ctx.location.directory
)) { for (const [name, entry] of Object.entries(doc.info.references ?? {})) {
const directory = doc.path ? path.dirname(doc.path) : location.directory if (!validAlias(name)) continue
for (const [name, entry] of Object.entries(doc.info.references ?? {})) { entries.set(
if (!validAlias(name)) continue name,
entries.set( local(entry)
name, ? new Reference.LocalSource({
local(entry) type: "local",
? new Reference.LocalSource({ path: AbsolutePath.make(
type: "local", localPath(directory, ctx.path.home, typeof entry === "string" ? entry : entry.path),
path: AbsolutePath.make( ),
localPath(directory, global.home, typeof entry === "string" ? entry : entry.path), description: typeof entry === "string" ? undefined : entry.description,
), hidden: typeof entry === "string" ? undefined : entry.hidden,
description: typeof entry === "string" ? undefined : entry.description, })
hidden: typeof entry === "string" ? undefined : entry.hidden, : new Reference.GitSource({
}) type: "git",
: new Reference.GitSource({ repository: typeof entry === "string" ? entry : entry.repository,
type: "git", branch: typeof entry === "string" ? undefined : entry.branch,
repository: typeof entry === "string" ? entry : entry.repository, description: typeof entry === "string" ? undefined : entry.description,
branch: typeof entry === "string" ? undefined : entry.branch, hidden: typeof entry === "string" ? undefined : entry.hidden,
description: typeof entry === "string" ? undefined : entry.description, }),
hidden: typeof entry === "string" ? undefined : entry.hidden, )
}), }
) }
} for (const [name, source] of entries) draft.add(name, source)
} }),
)
yield* update((editor) => {
for (const [name, source] of entries) editor.add(name, source)
})
}), }),
} })
function validAlias(name: string) { function validAlias(name: string) {
return name.length > 0 && !/[\/\s`,]/.test(name) return name.length > 0 && !/[\/\s`,]/.test(name)
+33 -36
View File
@@ -1,48 +1,45 @@
export * as ConfigSkillPlugin from "./skill" export * as ConfigSkillPlugin from "./skill"
import { define } from "@opencode-ai/plugin/v2/effect"
import path from "path" import path from "path"
import { Effect } from "effect" import { Effect } from "effect"
import { Config } from "../../config" import { Config } from "../../config"
import { Global } from "../../global"
import { Location } from "../../location"
import { PluginV2 } from "../../plugin"
import { AbsolutePath } from "../../schema" import { AbsolutePath } from "../../schema"
import { SkillV2 } from "../../skill" import { SkillV2 } from "../../skill"
export const Plugin = PluginV2.define({ export const Plugin = define({
id: PluginV2.ID.make("config-skill"), id: "config-skill",
effect: Effect.gen(function* () { effect: Effect.fn(function* (ctx) {
const config = yield* Config.Service const config = yield* Config.Service
const global = yield* Global.Service yield* ctx.skill.transform(
const location = yield* Location.Service Effect.fn(function* (draft) {
const skill = yield* SkillV2.Service const entries = yield* config.entries()
const transform = yield* skill.transform() const directories = entries.flatMap((entry) => (entry.type === "directory" ? [entry.path] : []))
const entries = yield* config.entries() const items = entries.flatMap((entry) => (entry.type === "document" ? (entry.info.skills ?? []) : []))
const directories = entries.flatMap((entry) => (entry.type === "directory" ? [entry.path] : [])) for (const directory of directories) {
const items = entries.flatMap((entry) => (entry.type === "document" ? (entry.info.skills ?? []) : [])) draft.source(
new SkillV2.DirectorySource({ type: "directory", path: AbsolutePath.make(path.join(directory, "skill")) }),
yield* transform((editor) => { )
for (const directory of directories) { draft.source(
editor.source( new SkillV2.DirectorySource({ type: "directory", path: AbsolutePath.make(path.join(directory, "skills")) }),
new SkillV2.DirectorySource({ type: "directory", path: AbsolutePath.make(path.join(directory, "skill")) }), )
)
editor.source(
new SkillV2.DirectorySource({ type: "directory", path: AbsolutePath.make(path.join(directory, "skills")) }),
)
}
for (const item of items) {
if (URL.canParse(item) && /^(https?:)$/.test(new URL(item).protocol)) {
editor.source(new SkillV2.UrlSource({ type: "url", url: item }))
continue
} }
const expanded = item.startsWith("~/") ? path.join(global.home, item.slice(2)) : item for (const item of items) {
editor.source( if (URL.canParse(item) && /^(https?:)$/.test(new URL(item).protocol)) {
new SkillV2.DirectorySource({ draft.source(new SkillV2.UrlSource({ type: "url", url: item }))
type: "directory", continue
path: AbsolutePath.make(path.isAbsolute(expanded) ? expanded : path.join(location.directory, expanded)), }
}), const expanded = item.startsWith("~/") ? path.join(ctx.path.home, item.slice(2)) : item
) draft.source(
} new SkillV2.DirectorySource({
}) type: "directory",
path: AbsolutePath.make(
path.isAbsolute(expanded) ? expanded : path.join(ctx.location.directory, expanded),
),
}),
)
}
}),
)
}), }),
}) })
@@ -48,7 +48,7 @@ export class ResetSourceChangesError extends Schema.TaggedErrorClass<ResetSource
{ {
directory: AbsolutePath, directory: AbsolutePath,
message: Schema.String, message: Schema.String,
cause: Schema.optional(Schema.Defect), cause: Schema.optional(Schema.Defect()),
}, },
) {} ) {}
+1 -1
View File
@@ -13,7 +13,7 @@ import { filesystem } from "./effect/layer-node-platform"
export namespace FSUtil { export namespace FSUtil {
export class FileSystemError extends Schema.TaggedErrorClass<FileSystemError>()("FileSystemError", { export class FileSystemError extends Schema.TaggedErrorClass<FileSystemError>()("FileSystemError", {
method: Schema.String, method: Schema.String,
cause: Schema.optional(Schema.Defect), cause: Schema.optional(Schema.Defect()),
}) {} }) {}
export type Error = PlatformError | FileSystemError export type Error = PlatformError | FileSystemError
+2 -2
View File
@@ -32,14 +32,14 @@ export class WorktreeError extends Schema.TaggedErrorClass<WorktreeError>()("Git
message: Schema.String, message: Schema.String,
directory: Schema.optional(AbsolutePath), directory: Schema.optional(AbsolutePath),
forceRequired: Schema.optional(Schema.Boolean), forceRequired: Schema.optional(Schema.Boolean),
cause: Schema.optional(Schema.Defect), cause: Schema.optional(Schema.Defect()),
}) {} }) {}
export class PatchError extends Schema.TaggedErrorClass<PatchError>()("Git.PatchError", { export class PatchError extends Schema.TaggedErrorClass<PatchError>()("Git.PatchError", {
operation: Schema.Literals(["capture", "apply", "reset"]), operation: Schema.Literals(["capture", "apply", "reset"]),
directory: AbsolutePath, directory: AbsolutePath,
message: Schema.String, message: Schema.String,
cause: Schema.optional(Schema.Defect), cause: Schema.optional(Schema.Defect()),
}) {} }) {}
export interface Interface { export interface Interface {
+56 -44
View File
@@ -1,7 +1,19 @@
export * as Integration from "./integration" export * as Integration from "./integration"
import { Cause, Clock, Context, Duration, Effect, Exit, Layer, Schedule, Schema, Scope, SynchronizedRef } from "effect" import {
import { castDraft, enableMapSet, type Draft } from "immer" Cause,
Clock,
Context,
Duration,
Effect,
Exit,
Layer,
Schedule,
Schema,
Scope,
SynchronizedRef,
Types,
} from "effect"
import { Credential } from "./credential" import { Credential } from "./credential"
import { IntegrationSchema } from "./integration/schema" import { IntegrationSchema } from "./integration/schema"
import { withStatics } from "./schema" import { withStatics } from "./schema"
@@ -42,12 +54,14 @@ export const SelectPrompt = Schema.Struct({
type: Schema.Literal("select"), type: Schema.Literal("select"),
key: Schema.String, key: Schema.String,
message: Schema.String, message: Schema.String,
options: Schema.Array( options: Schema.mutable(
Schema.Struct({ Schema.Array(
label: Schema.String, Schema.Struct({
value: Schema.String, label: Schema.String,
hint: Schema.optional(Schema.String), value: Schema.String,
}), hint: Schema.optional(Schema.String),
}),
),
), ),
when: Schema.optional(When), when: Schema.optional(When),
}).annotate({ identifier: "Integration.SelectPrompt" }) }).annotate({ identifier: "Integration.SelectPrompt" })
@@ -60,7 +74,7 @@ export const OAuthMethod = Schema.Struct({
id: MethodID, id: MethodID,
type: Schema.Literal("oauth"), type: Schema.Literal("oauth"),
label: Schema.String, label: Schema.String,
prompts: Schema.optional(Schema.Array(Prompt)), prompts: Schema.optional(Schema.mutable(Schema.Array(Prompt))),
}).annotate({ identifier: "Integration.OAuthMethod" }) }).annotate({ identifier: "Integration.OAuthMethod" })
export type OAuthMethod = typeof OAuthMethod.Type export type OAuthMethod = typeof OAuthMethod.Type
@@ -72,7 +86,7 @@ export type KeyMethod = typeof KeyMethod.Type
export const EnvMethod = Schema.Struct({ export const EnvMethod = Schema.Struct({
type: Schema.Literal("env"), type: Schema.Literal("env"),
names: Schema.Array(Schema.String), names: Schema.mutable(Schema.Array(Schema.String)),
}).annotate({ identifier: "Integration.EnvMethod" }) }).annotate({ identifier: "Integration.EnvMethod" })
export type EnvMethod = typeof EnvMethod.Type export type EnvMethod = typeof EnvMethod.Type
@@ -82,8 +96,8 @@ export type Method = typeof Method.Type
export class Info extends Schema.Class<Info>("Integration.Info")({ export class Info extends Schema.Class<Info>("Integration.Info")({
id: ID, id: ID,
name: Schema.String, name: Schema.String,
methods: Schema.Array(Method), methods: Schema.mutable(Schema.Array(Method)),
connections: Schema.Array(IntegrationConnection.Info), connections: Schema.mutable(Schema.Array(IntegrationConnection.Info)),
}) {} }) {}
export type Inputs = Readonly<{ [key: string]: string }> export type Inputs = Readonly<{ [key: string]: string }>
@@ -154,7 +168,7 @@ export class CodeRequiredError extends Schema.TaggedErrorClass<CodeRequiredError
}) {} }) {}
export class AuthorizationError extends Schema.TaggedErrorClass<AuthorizationError>()("Integration.Authorization", { export class AuthorizationError extends Schema.TaggedErrorClass<AuthorizationError>()("Integration.Authorization", {
cause: Schema.Defect, cause: Schema.Defect(),
}) {} }) {}
export type Error = CodeRequiredError | AuthorizationError export type Error = CodeRequiredError | AuthorizationError
@@ -172,19 +186,19 @@ export type Ref = {
} }
type Entry = { type Entry = {
ref: Ref ref: Types.DeepMutable<Ref>
methods: Method[] methods: Types.DeepMutable<Method>[]
implementations: Map<MethodID, OAuthImplementation> implementations: Map<MethodID, Types.DeepMutable<OAuthImplementation>>
} }
type Data = { type Data = {
integrations: Map<ID, Entry> integrations: Map<ID, Entry>
} }
export type Editor = { export type Draft = {
list: () => readonly Ref[] list: () => readonly Ref[]
get: (id: ID) => Ref | undefined get: (id: ID) => Ref | undefined
update: (id: ID, update: (integration: Draft<Ref>) => void) => void update: (id: ID, update: (integration: Types.DeepMutable<Ref>) => void) => void
remove: (id: ID) => void remove: (id: ID) => void
method: { method: {
list: (integrationID: ID) => readonly Method[] list: (integrationID: ID) => readonly Method[]
@@ -193,11 +207,8 @@ export type Editor = {
} }
} }
export interface Interface { export interface Interface extends State.Transformable<Draft> {
/** Registers a scoped transform over the integration registry. */ /** Registers a scoped transform over the integration registry. */
readonly transform: State.Interface<Data, Editor>["transform"]
/** Registers and immediately applies a scoped integration registry update. */
readonly update: State.Interface<Data, Editor>["update"]
/** Returns one integration with its methods and current connections. */ /** Returns one integration with its methods and current connections. */
readonly get: (id: ID) => Effect.Effect<Info | undefined> readonly get: (id: ID) => Effect.Effect<Info | undefined>
/** Returns all integrations with their methods and current connections. */ /** Returns all integrations with their methods and current connections. */
@@ -252,8 +263,6 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Integration") {} export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Integration") {}
enableMapSet()
const attemptLifetime = Duration.toMillis(Duration.minutes(10)) const attemptLifetime = Duration.toMillis(Duration.minutes(10))
const terminalRetention = Duration.toMillis(Duration.minutes(1)) const terminalRetention = Duration.toMillis(Duration.minutes(1))
const scrubInterval = Duration.seconds(30) const scrubInterval = Duration.seconds(30)
@@ -284,15 +293,17 @@ export const locationLayer = Layer.effect(
const events = yield* EventV2.Service const events = yield* EventV2.Service
const scope = yield* Scope.Scope const scope = yield* Scope.Scope
const attempts = SynchronizedRef.makeUnsafe(new Map<AttemptID, AttemptEntry>()) const attempts = SynchronizedRef.makeUnsafe(new Map<AttemptID, AttemptEntry>())
const state = State.create<Data, Editor>({ const state = State.create<Data, Draft>({
initial: () => ({ integrations: new Map<ID, Entry>() }), initial: () => ({ integrations: new Map<ID, Entry>() }),
editor: (draft) => ({ draft: (draft) => ({
list: () => Array.from(draft.integrations.values(), (entry) => entry.ref) as Ref[], list: () => Array.from(draft.integrations.values(), (entry) => entry.ref) as Ref[],
get: (id) => draft.integrations.get(id)?.ref as Ref | undefined, get: (id) => draft.integrations.get(id)?.ref as Ref | undefined,
update: (id, update) => { update: (id, update) => {
const current = const current = draft.integrations.get(id) ?? {
draft.integrations.get(id) ?? ref: { id, name: id },
castDraft({ ref: { id, name: id } as Ref, methods: [], implementations: new Map() }) methods: [],
implementations: new Map(),
}
if (!draft.integrations.has(id)) draft.integrations.set(id, current) if (!draft.integrations.has(id)) draft.integrations.set(id, current)
update(current.ref) update(current.ref)
current.ref.id = id current.ref.id = id
@@ -301,16 +312,14 @@ export const locationLayer = Layer.effect(
method: { method: {
list: (integrationID) => (draft.integrations.get(integrationID)?.methods as Method[] | undefined) ?? [], list: (integrationID) => (draft.integrations.get(integrationID)?.methods as Method[] | undefined) ?? [],
update: (implementation) => { update: (implementation) => {
const current = const current = draft.integrations.get(implementation.integrationID) ?? {
draft.integrations.get(implementation.integrationID) ?? ref: {
castDraft({ id: implementation.integrationID,
ref: { name: implementation.integrationID,
id: implementation.integrationID, },
name: implementation.integrationID, methods: [],
} as Ref, implementations: new Map<MethodID, Types.DeepMutable<OAuthImplementation>>(),
methods: [], }
implementations: new Map<MethodID, OAuthImplementation>(),
})
if (!draft.integrations.has(implementation.integrationID)) { if (!draft.integrations.has(implementation.integrationID)) {
draft.integrations.set(implementation.integrationID, current) draft.integrations.set(implementation.integrationID, current)
} }
@@ -319,10 +328,13 @@ export const locationLayer = Layer.effect(
if (method.type !== "oauth" || implementation.method.type !== "oauth") return true if (method.type !== "oauth" || implementation.method.type !== "oauth") return true
return method.id === implementation.method.id return method.id === implementation.method.id
}) })
if (index === -1) current.methods.push(castDraft(implementation.method)) if (index === -1) current.methods.push(implementation.method as Types.DeepMutable<Method>)
else current.methods[index] = castDraft(implementation.method) else current.methods[index] = implementation.method as Types.DeepMutable<Method>
if (isOAuthImplementation(implementation)) { if (implementation.method.type === "oauth") {
current.implementations.set(implementation.method.id, castDraft(implementation)) current.implementations.set(
implementation.method.id,
implementation as Types.DeepMutable<OAuthImplementation>,
)
} }
}, },
remove: (integrationID, method) => { remove: (integrationID, method) => {
@@ -434,7 +446,7 @@ export const locationLayer = Layer.effect(
return Service.of({ return Service.of({
transform: state.transform, transform: state.transform,
update: state.update, rebuild: state.rebuild,
get: Effect.fn("Integration.get")(function* (id) { get: Effect.fn("Integration.get")(function* (id) {
const entry = state.get().integrations.get(id) const entry = state.get().integrations.get(id)
if (!entry) return undefined if (!entry) return undefined
+1 -1
View File
@@ -33,7 +33,7 @@ export type Request = typeof Request.Type
interface MutableRequest { interface MutableRequest {
headers: Record<string, string> headers: Record<string, string>
body: Record<string, unknown> body: Record<string, unknown>
generation?: Generation generation?: Record<string, unknown>
options?: Record<string, unknown> options?: Record<string, unknown>
} }
+11 -8
View File
@@ -1,5 +1,4 @@
import { DateTime, Schema } from "effect" import { Schema, Types } from "effect"
import { DateTimeUtcFromMillis } from "effect/Schema"
import { ProviderV2 } from "./provider" import { ProviderV2 } from "./provider"
import { ModelRequest } from "./model-request" import { ModelRequest } from "./model-request"
@@ -16,8 +15,8 @@ export type Family = typeof Family.Type
export const Capabilities = Schema.Struct({ export const Capabilities = Schema.Struct({
tools: Schema.Boolean, tools: Schema.Boolean,
// mime patterns, image, audio, video/*, text/* // mime patterns, image, audio, video/*, text/*
input: Schema.String.pipe(Schema.Array), input: Schema.String.pipe(Schema.Array, Schema.mutable),
output: Schema.String.pipe(Schema.Array), output: Schema.String.pipe(Schema.Array, Schema.mutable),
}) })
export type Capabilities = typeof Capabilities.Type export type Capabilities = typeof Capabilities.Type
@@ -67,11 +66,11 @@ export class Info extends Schema.Class<Info>("ModelV2.Info")({
variants: Schema.Struct({ variants: Schema.Struct({
id: VariantID, id: VariantID,
...ModelRequest.Request.fields, ...ModelRequest.Request.fields,
}).pipe(Schema.Array), }).pipe(Schema.Array, Schema.mutable),
time: Schema.Struct({ time: Schema.Struct({
released: DateTimeUtcFromMillis, released: Schema.Finite,
}), }),
cost: Cost.pipe(Schema.Array), cost: Cost.pipe(Schema.Array, Schema.mutable),
status: Schema.Literals(["alpha", "beta", "deprecated", "active"]), status: Schema.Literals(["alpha", "beta", "deprecated", "active"]),
enabled: Schema.Boolean, enabled: Schema.Boolean,
limit: Schema.Struct({ limit: Schema.Struct({
@@ -103,7 +102,7 @@ export class Info extends Schema.Class<Info>("ModelV2.Info")({
}, },
variants: [], variants: [],
time: { time: {
released: DateTime.makeUnsafe(0), released: 0,
}, },
cost: [], cost: [],
status: "active", status: "active",
@@ -116,6 +115,10 @@ export class Info extends Schema.Class<Info>("ModelV2.Info")({
} }
} }
export type MutableInfo = Omit<Types.DeepMutable<Info>, "api"> & {
api: ProviderV2.MutableApi<Api>
}
export function parse(input: string): { providerID: ProviderV2.ID; modelID: ID } { export function parse(input: string): { providerID: ProviderV2.ID; modelID: ID } {
const [providerID, ...modelID] = input.split("/") const [providerID, ...modelID] = input.split("/")
return { return {
+24 -28
View File
@@ -15,12 +15,12 @@ import { NpmConfig } from "./npm-config"
export class InstallFailedError extends Schema.TaggedErrorClass<InstallFailedError>()("NpmInstallFailedError", { export class InstallFailedError extends Schema.TaggedErrorClass<InstallFailedError>()("NpmInstallFailedError", {
add: Schema.Array(Schema.String).pipe(Schema.optional), add: Schema.Array(Schema.String).pipe(Schema.optional),
dir: Schema.String, dir: Schema.String,
cause: Schema.optional(Schema.Defect), cause: Schema.optional(Schema.Defect()),
}) {} }) {}
export interface EntryPoint { export interface EntryPoint {
readonly directory: string readonly directory: string
readonly entrypoint: Option.Option<string> readonly entrypoint?: string
} }
export interface Interface { export interface Interface {
@@ -34,7 +34,7 @@ export interface Interface {
}[] }[]
}, },
) => Effect.Effect<void, EffectFlock.LockError | InstallFailedError> ) => Effect.Effect<void, EffectFlock.LockError | InstallFailedError>
readonly which: (pkg: string, bin?: string) => Effect.Effect<Option.Option<string>> readonly which: (pkg: string, bin?: string) => Effect.Effect<string | undefined>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/Npm") {} export class Service extends Context.Service<Service, Interface>()("@opencode/Npm") {}
@@ -47,12 +47,11 @@ export function sanitize(pkg: string) {
} }
const resolveEntryPoint = (name: string, dir: string): EntryPoint => { const resolveEntryPoint = (name: string, dir: string): EntryPoint => {
let entrypoint: Option.Option<string> let entrypoint: string | undefined
try { try {
const resolved = typeof Bun !== "undefined" ? import.meta.resolve(name, dir) : import.meta.resolve(dir) entrypoint = typeof Bun !== "undefined" ? import.meta.resolve(name, dir) : import.meta.resolve(dir)
entrypoint = Option.some(resolved)
} catch { } catch {
entrypoint = Option.none() entrypoint = undefined
} }
return { return {
directory: dir, directory: dir,
@@ -130,7 +129,7 @@ export const layer = Layer.effect(
const first = tree.edgesOut.values().next().value?.to const first = tree.edgesOut.values().next().value?.to
if (!first) { if (!first) {
const result = resolveEntryPoint(name, path.join(dir, "node_modules", name)) const result = resolveEntryPoint(name, path.join(dir, "node_modules", name))
if (Option.isSome(result.entrypoint)) return result if (result.entrypoint) return result
return yield* new InstallFailedError({ add: [pkg], dir }) return yield* new InstallFailedError({ add: [pkg], dir })
} }
return resolveEntryPoint(first.name, first.path) return resolveEntryPoint(first.name, first.path)
@@ -219,22 +218,24 @@ export const layer = Layer.effect(
return Option.some(files[0]) return Option.some(files[0])
}) })
return yield* Effect.gen(function* () { return Option.getOrUndefined(
const bin = yield* pick() yield* Effect.gen(function* () {
if (Option.isSome(bin)) { const bin = yield* pick()
return Option.some(path.join(binDir, bin.value)) if (Option.isSome(bin)) {
} return Option.some(path.join(binDir, bin.value))
}
yield* fs.remove(path.join(dir, "package-lock.json")).pipe(Effect.orElseSucceed(() => {})) yield* fs.remove(path.join(dir, "package-lock.json")).pipe(Effect.orElseSucceed(() => {}))
yield* add(pkg) yield* add(pkg)
const resolved = yield* pick() const resolved = yield* pick()
if (Option.isNone(resolved)) return Option.none<string>() if (Option.isNone(resolved)) return Option.none<string>()
return Option.some(path.join(binDir, resolved.value)) return Option.some(path.join(binDir, resolved.value))
}).pipe( }).pipe(
Effect.scoped, Effect.scoped,
Effect.orElseSucceed(() => Option.none<string>()), Effect.orElseSucceed(() => Option.none<string>()),
),
) )
}) })
@@ -261,14 +262,9 @@ export async function install(...args: Parameters<Interface["install"]>) {
} }
export async function add(...args: Parameters<Interface["add"]>) { export async function add(...args: Parameters<Interface["add"]>) {
const entry = await runPromise((svc) => svc.add(...args)) return runPromise((svc) => svc.add(...args))
return {
directory: entry.directory,
entrypoint: Option.getOrUndefined(entry.entrypoint),
}
} }
export async function which(...args: Parameters<Interface["which"]>) { export async function which(...args: Parameters<Interface["which"]>) {
const resolved = await runPromise((svc) => svc.which(...args)) return runPromise((svc) => svc.which(...args))
return Option.getOrUndefined(resolved)
} }
+1 -1
View File
@@ -12,5 +12,5 @@ export const Rule = Schema.Struct({
}).annotate({ identifier: "PermissionV2.Rule" }) }).annotate({ identifier: "PermissionV2.Rule" })
export type Rule = typeof Rule.Type export type Rule = typeof Rule.Type
export const Ruleset = Schema.Array(Rule).annotate({ identifier: "PermissionV2.Ruleset" }) export const Ruleset = Schema.mutable(Schema.Array(Rule)).annotate({ identifier: "PermissionV2.Ruleset" })
export type Ruleset = typeof Ruleset.Type export type Ruleset = typeof Ruleset.Type
+43 -20
View File
@@ -7,6 +7,7 @@ import type { ModelV2 } from "./model"
import type { Catalog } from "./catalog" import type { Catalog } from "./catalog"
import { EventV2 } from "./event" import { EventV2 } from "./event"
import { KeyedMutex } from "./effect/keyed-mutex" import { KeyedMutex } from "./effect/keyed-mutex"
import { State } from "./state"
export const ID = Schema.String.pipe(Schema.brand("Plugin.ID")) export const ID = Schema.String.pipe(Schema.brand("Plugin.ID"))
export type ID = typeof ID.Type export type ID = typeof ID.Type
@@ -22,7 +23,7 @@ export const Event = {
type HookSpec = { type HookSpec = {
"catalog.transform": { "catalog.transform": {
input: Catalog.Editor input: Catalog.Draft
output: {} output: {}
} }
"aisdk.language": { "aisdk.language": {
@@ -62,18 +63,16 @@ export type HookFunctions = {
export type HookInput<Name extends keyof Hooks> = HookSpec[Name]["input"] export type HookInput<Name extends keyof Hooks> = HookSpec[Name]["input"]
export type HookOutput<Name extends keyof Hooks> = HookSpec[Name]["output"] export type HookOutput<Name extends keyof Hooks> = HookSpec[Name]["output"]
export type Effect<R = never> = Effect.Effect<HookFunctions | void, never, R | Scope.Scope>
export function define<R>(input: { id: ID; effect: Effect.Effect<HookFunctions | void, never, R> }) {
return input
}
export interface Interface { export interface Interface {
readonly add: (input: { readonly add: (input: {
id: ID id: string
effect: Effect.Effect<void | HookFunctions, never, Scope.Scope> effect: Effect.Effect<void | HookFunctions, never, Scope.Scope>
}) => Effect.Effect<void, never, never> }) => Effect.Effect<void, never, never>
readonly remove: (id: ID) => Effect.Effect<void> readonly remove: (id: ID) => Effect.Effect<void>
readonly hook: <Name extends keyof Hooks>(
name: Name,
callback: (input: Hooks[Name]) => Effect.Effect<void> | void,
) => Effect.Effect<State.Registration, never, Scope.Scope>
readonly triggerFor: <Name extends keyof Hooks>( readonly triggerFor: <Name extends keyof Hooks>(
id: ID, id: ID,
name: Name, name: Name,
@@ -97,35 +96,40 @@ export const layer = Layer.effect(
hooks: HookFunctions hooks: HookFunctions
scope: Scope.Closeable scope: Scope.Closeable
}[] = [] }[] = []
let registrations: {
[Name in keyof Hooks]: {
name: Name
callback: (input: Hooks[Name]) => Effect.Effect<void> | void
}
}[keyof Hooks][] = []
const events = yield* EventV2.Service const events = yield* EventV2.Service
const scope = yield* Scope.Scope const scope = yield* Scope.Scope
const locks = KeyedMutex.makeUnsafe<ID>() const locks = KeyedMutex.makeUnsafe<ID>()
const svc = Service.of({ const svc = Service.of({
add: Effect.fn("Plugin.add")(function* (input) { add: Effect.fn("Plugin.add")(function* (input) {
yield* locks.withLock(input.id)( const id = ID.make(input.id)
yield* locks.withLock(id)(
Effect.gen(function* () { Effect.gen(function* () {
const existing = hooks.find((item) => item.id === input.id) const existing = hooks.find((item) => item.id === id)
if (existing) yield* Scope.close(existing.scope, Exit.void).pipe(Effect.ignore) if (existing) yield* Scope.close(existing.scope, Exit.void).pipe(Effect.ignore)
const childScope = yield* Scope.fork(scope) const childScope = yield* Scope.fork(scope)
const result = yield* input.effect.pipe( const result = yield* input.effect.pipe(
Scope.provide(childScope), Scope.provide(childScope),
Effect.withSpan("Plugin.load", { Effect.withSpan("Plugin.load", {
attributes: { attributes: {
"plugin.id": input.id, "plugin.id": id,
}, },
}), }),
Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(childScope, exit) : Effect.void)), Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(childScope, exit) : Effect.void)),
) )
hooks = [ const next = {
...hooks.filter((item) => item.id !== input.id), id,
{ hooks: result ?? {},
id: input.id, scope: childScope,
hooks: result ?? {}, }
scope: childScope, hooks = existing ? hooks.with(hooks.indexOf(existing), next) : [...hooks, next]
}, yield* events.publish(Event.Added, { id })
]
yield* events.publish(Event.Added, { id: input.id })
}), }),
) )
}), }),
@@ -160,6 +164,12 @@ export const layer = Layer.effect(
) )
} }
for (const item of registrations) {
if (item.name !== name) continue
const result = item.callback(event as never)
if (Effect.isEffect(result)) yield* result
}
for (const [field, draft] of draftEntries) { for (const [field, draft] of draftEntries) {
event[field] = finishDraft(draft) event[field] = finishDraft(draft)
} }
@@ -175,6 +185,19 @@ export const layer = Layer.effect(
}), }),
) )
}), }),
hook: Effect.fn("Plugin.hook")(function* (name, callback) {
const scope = yield* Scope.Scope
const registration = { name, callback } as (typeof registrations)[number]
let active = true
registrations = [...registrations, registration]
const dispose = Effect.sync(() => {
if (!active) return
active = false
registrations = registrations.filter((item) => item !== registration)
})
yield* Scope.addFinalizer(scope, dispose)
return { dispose }
}),
}) })
return svc return svc
}), }),
+13 -16
View File
@@ -1,12 +1,11 @@
export * as AgentPlugin from "./agent" export * as AgentPlugin from "./agent"
import path from "path" import path from "path"
import { define } from "@opencode-ai/plugin/v2/effect"
import { Effect } from "effect" import { Effect } from "effect"
import { AgentV2 } from "../agent" import { AgentV2 } from "../agent"
import { Global } from "../global" import { Global } from "../global"
import { Location } from "../location"
import { PermissionV2 } from "../permission" import { PermissionV2 } from "../permission"
import { PluginV2 } from "../plugin"
const TRUNCATION_GLOB = path.join(Global.Path.data, "tool-output", "*") const TRUNCATION_GLOB = path.join(Global.Path.data, "tool-output", "*")
const BUILD_SYSTEM = const BUILD_SYSTEM =
@@ -97,12 +96,10 @@ Rules:
- If the conversation ends with an unanswered question to the user, preserve that exact question - If the conversation ends with an unanswered question to the user, preserve that exact question
- If the conversation ends with an imperative statement or request to the user (e.g. "Now please run the command and paste the console output"), always include that exact request in the summary` - If the conversation ends with an imperative statement or request to the user (e.g. "Now please run the command and paste the console output"), always include that exact request in the summary`
export const Plugin = PluginV2.define({ export const Plugin = define({
id: PluginV2.ID.make("agent"), id: "agent",
effect: Effect.gen(function* () { effect: Effect.fn(function* (ctx) {
const agent = yield* AgentV2.Service const worktree = ctx.location.directory
const location = yield* Location.Service
const worktree = location.directory
const whitelistedDirs = [TRUNCATION_GLOB, path.join(Global.Path.tmp, "*")] const whitelistedDirs = [TRUNCATION_GLOB, path.join(Global.Path.tmp, "*")]
const readonlyExternalDirectory: PermissionV2.Ruleset = [ const readonlyExternalDirectory: PermissionV2.Ruleset = [
{ action: "external_directory", resource: "*", effect: "ask" }, { action: "external_directory", resource: "*", effect: "ask" },
@@ -122,8 +119,8 @@ export const Plugin = PluginV2.define({
{ action: "read", resource: "*.env.example", effect: "allow" }, { action: "read", resource: "*.env.example", effect: "allow" },
] ]
yield* agent.update((editor) => { yield* ctx.agent.transform((draft) => {
editor.update(AgentV2.defaultID, (item) => { draft.update(AgentV2.defaultID, (item) => {
item.description = "The default agent. Executes tools based on configured permissions." item.description = "The default agent. Executes tools based on configured permissions."
item.system ??= BUILD_SYSTEM item.system ??= BUILD_SYSTEM
item.mode = "primary" item.mode = "primary"
@@ -135,7 +132,7 @@ export const Plugin = PluginV2.define({
) )
}) })
editor.update(AgentV2.ID.make("plan"), (item) => { draft.update(AgentV2.ID.make("plan"), (item) => {
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(
@@ -154,14 +151,14 @@ export const Plugin = PluginV2.define({
) )
}) })
editor.update(AgentV2.ID.make("general"), (item) => { draft.update(AgentV2.ID.make("general"), (item) => {
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(...PermissionV2.merge(defaults, [{ action: "todowrite", resource: "*", effect: "deny" }])) item.permissions.push(...PermissionV2.merge(defaults, [{ action: "todowrite", resource: "*", effect: "deny" }]))
}) })
editor.update(AgentV2.ID.make("explore"), (item) => { draft.update(AgentV2.ID.make("explore"), (item) => {
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.'
item.system = PROMPT_EXPLORE item.system = PROMPT_EXPLORE
@@ -182,21 +179,21 @@ export const Plugin = PluginV2.define({
) )
}) })
editor.update(AgentV2.ID.make("compaction"), (item) => { draft.update(AgentV2.ID.make("compaction"), (item) => {
item.mode = "primary" item.mode = "primary"
item.hidden = true item.hidden = true
item.system = PROMPT_COMPACTION item.system = PROMPT_COMPACTION
item.permissions.push(...PermissionV2.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }])) item.permissions.push(...PermissionV2.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }]))
}) })
editor.update(AgentV2.ID.make("title"), (item) => { draft.update(AgentV2.ID.make("title"), (item) => {
item.mode = "primary" item.mode = "primary"
item.hidden = true item.hidden = true
item.system = PROMPT_TITLE item.system = PROMPT_TITLE
item.permissions.push(...PermissionV2.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }])) item.permissions.push(...PermissionV2.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }]))
}) })
editor.update(AgentV2.ID.make("summary"), (item) => { draft.update(AgentV2.ID.make("summary"), (item) => {
item.mode = "primary" item.mode = "primary"
item.hidden = true item.hidden = true
item.system = PROMPT_SUMMARY item.system = PROMPT_SUMMARY
+56 -54
View File
@@ -1,7 +1,7 @@
export * as PluginBoot from "./boot" export * as PluginBoot from "./boot"
import type { Plugin as PublicPlugin } from "@opencode-ai/plugin/v2/effect"
import { Context, Deferred, Effect, Layer } from "effect" import { Context, Deferred, Effect, Layer } from "effect"
import { Credential } from "../credential"
import { Integration } from "../integration" import { Integration } from "../integration"
import { AgentV2 } from "../agent" import { AgentV2 } from "../agent"
import { Catalog } from "../catalog" import { Catalog } from "../catalog"
@@ -13,6 +13,7 @@ import { ConfigSkillPlugin } from "../config/plugin/skill"
import { ConfigReferencePlugin } from "../config/plugin/reference" import { ConfigReferencePlugin } from "../config/plugin/reference"
import { EventV2 } from "../event" import { EventV2 } from "../event"
import { FSUtil } from "../fs-util" import { FSUtil } from "../fs-util"
import { FileSystem } from "../filesystem"
import { Global } from "../global" import { Global } from "../global"
import { Location } from "../location" import { Location } from "../location"
import { ModelsDev } from "../models-dev" import { ModelsDev } from "../models-dev"
@@ -26,29 +27,13 @@ import { ModelsDevPlugin } from "./models-dev"
import { ProviderPlugins } from "./provider" import { ProviderPlugins } from "./provider"
import { SkillV2 } from "../skill" import { SkillV2 } from "../skill"
import { Reference } from "../reference" import { Reference } from "../reference"
import { State } from "../state"
import { PluginHost } from "./host"
type Plugin = { type InternalPlugin = PublicPlugin<any>
id: PluginV2.ID
effect: PluginV2.Effect<
| Catalog.Service
| CommandV2.Service
| Credential.Service
| Integration.Service
| AgentV2.Service
| Npm.Service
| EventV2.Service
| FSUtil.Service
| Global.Service
| Location.Service
| PluginV2.Service
| Config.Service
| ModelsDev.Service
| SkillV2.Service
| Reference.Service
>
}
export interface Interface { export interface Interface {
readonly add: (plugin: PublicPlugin<any>) => Effect.Effect<void>
readonly wait: () => Effect.Effect<void> readonly wait: () => Effect.Effect<void>
} }
@@ -60,8 +45,7 @@ export const layer = Layer.effect(
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
const commands = yield* CommandV2.Service const commands = yield* CommandV2.Service
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const credentials = yield* Credential.Service const integration = yield* Integration.Service
const integrations = yield* Integration.Service
const agents = yield* AgentV2.Service const agents = yield* AgentV2.Service
const config = yield* Config.Service const config = yield* Config.Service
const location = yield* Location.Service const location = yield* Location.Service
@@ -69,47 +53,54 @@ export const layer = Layer.effect(
const npm = yield* Npm.Service const npm = yield* Npm.Service
const events = yield* EventV2.Service const events = yield* EventV2.Service
const fs = yield* FSUtil.Service const fs = yield* FSUtil.Service
const filesystem = yield* FileSystem.Service
const global = yield* Global.Service const global = yield* Global.Service
const skill = yield* SkillV2.Service const skill = yield* SkillV2.Service
const references = yield* Reference.Service const reference = yield* Reference.Service
const host = yield* PluginHost.make()
const done = yield* Deferred.make<void>() const done = yield* Deferred.make<void>()
const add = Effect.fn("PluginBoot.add")(function* (input: Plugin) { const add = Effect.fn("PluginBoot.add")(function* (input: InternalPlugin) {
yield* plugin.add({ yield* plugin.add({
id: input.id, id: input.id,
effect: input.effect.pipe( effect: input
Effect.provideService(Catalog.Service, catalog), .effect(host)
Effect.provideService(CommandV2.Service, commands), .pipe(
Effect.provideService(Credential.Service, credentials), Effect.provideService(Catalog.Service, catalog),
Effect.provideService(Integration.Service, integrations), Effect.provideService(CommandV2.Service, commands),
Effect.provideService(AgentV2.Service, agents), Effect.provideService(Integration.Service, integration),
Effect.provideService(Config.Service, config), Effect.provideService(AgentV2.Service, agents),
Effect.provideService(Location.Service, location), Effect.provideService(Config.Service, config),
Effect.provideService(ModelsDev.Service, modelsDev), Effect.provideService(Location.Service, location),
Effect.provideService(Npm.Service, npm), Effect.provideService(ModelsDev.Service, modelsDev),
Effect.provideService(EventV2.Service, events), Effect.provideService(Npm.Service, npm),
Effect.provideService(FSUtil.Service, fs), Effect.provideService(EventV2.Service, events),
Effect.provideService(Global.Service, global), Effect.provideService(FSUtil.Service, fs),
Effect.provideService(SkillV2.Service, skill), Effect.provideService(FileSystem.Service, filesystem),
Effect.provideService(Reference.Service, references), Effect.provideService(Global.Service, global),
Effect.provideService(PluginV2.Service, plugin), Effect.provideService(SkillV2.Service, skill),
), Effect.provideService(Reference.Service, reference),
),
}) })
}) })
const boot = Effect.gen(function* () { const boot = Effect.gen(function* () {
yield* add(AgentPlugin.Plugin) yield* State.batch(
yield* add(CommandPlugin.Plugin) Effect.gen(function* () {
yield* add(SkillPlugin.Plugin) yield* add(AgentPlugin.Plugin)
for (const item of ProviderPlugins) { yield* add(CommandPlugin.Plugin)
yield* add(item) yield* add(SkillPlugin.Plugin)
} yield* add(ModelsDevPlugin)
yield* add(ModelsDevPlugin) yield* add(ConfigProviderPlugin.Plugin)
yield* add(ConfigProviderPlugin.Plugin) yield* add(ConfigAgentPlugin.Plugin)
yield* add(ConfigAgentPlugin.Plugin) yield* add(ConfigCommandPlugin.Plugin)
yield* add(ConfigCommandPlugin.Plugin) yield* add(ConfigSkillPlugin.Plugin)
yield* add(ConfigSkillPlugin.Plugin) yield* add(ConfigReferencePlugin.Plugin)
yield* add(ConfigReferencePlugin.Plugin) for (const item of ProviderPlugins) {
yield* add(item)
}
}),
)
}).pipe(Effect.withSpan("PluginBoot.boot")) }).pipe(Effect.withSpan("PluginBoot.boot"))
yield* boot.pipe( yield* boot.pipe(
@@ -119,12 +110,22 @@ export const layer = Layer.effect(
) )
return Service.of({ return Service.of({
add: (input) =>
Deferred.await(done).pipe(
Effect.andThen(
plugin.add({
id: input.id,
effect: input.effect(host),
}),
),
),
wait: () => Deferred.await(done), wait: () => Deferred.await(done),
}) })
}), }),
) )
export const locationLayer = layer.pipe( export const locationLayer = layer.pipe(
Layer.provideMerge(PluginV2.locationLayer),
Layer.provideMerge(Integration.locationLayer), Layer.provideMerge(Integration.locationLayer),
Layer.provideMerge(Catalog.locationLayer), Layer.provideMerge(Catalog.locationLayer),
Layer.provideMerge(CommandV2.locationLayer), Layer.provideMerge(CommandV2.locationLayer),
@@ -132,4 +133,5 @@ export const locationLayer = layer.pipe(
Layer.provideMerge(AgentV2.locationLayer), Layer.provideMerge(AgentV2.locationLayer),
Layer.provideMerge(SkillV2.locationLayer), Layer.provideMerge(SkillV2.locationLayer),
Layer.provideMerge(Reference.locationLayer), Layer.provideMerge(Reference.locationLayer),
Layer.provideMerge(FileSystem.locationLayer),
) )
+9 -15
View File
@@ -1,26 +1,20 @@
export * as CommandPlugin from "./command" export * as CommandPlugin from "./command"
import { define } from "@opencode-ai/plugin/v2/effect"
import { Effect } from "effect" import { Effect } from "effect"
import { CommandV2 } from "../command"
import { Location } from "../location"
import { PluginV2 } from "../plugin"
import PROMPT_INITIALIZE from "./command/initialize.txt" import PROMPT_INITIALIZE from "./command/initialize.txt"
import PROMPT_REVIEW from "./command/review.txt" import PROMPT_REVIEW from "./command/review.txt"
export const Plugin = PluginV2.define({ export const Plugin = define({
id: PluginV2.ID.make("command"), id: "command",
effect: Effect.gen(function* () { effect: Effect.fn(function* (ctx) {
const command = yield* CommandV2.Service yield* ctx.command.transform((draft) => {
const location = yield* Location.Service draft.update("init", (command) => {
const transform = yield* command.transform() command.template = PROMPT_INITIALIZE.replace("${path}", ctx.location.project.directory)
yield* transform((editor) => {
editor.update("init", (command) => {
command.template = PROMPT_INITIALIZE.replace("${path}", location.project.directory)
command.description = "guided AGENTS.md setup" command.description = "guided AGENTS.md setup"
}) })
editor.update("review", (command) => { draft.update("review", (command) => {
command.template = PROMPT_REVIEW.replace("${path}", location.project.directory) command.template = PROMPT_REVIEW.replace("${path}", ctx.location.project.directory)
command.description = "review changes [commit|branch|pr], defaults to uncommitted" command.description = "review changes [commit|branch|pr], defaults to uncommitted"
command.subtask = true command.subtask = true
}) })
+236
View File
@@ -0,0 +1,236 @@
export * as PluginHost from "./host"
import type { LanguageModelV3 } from "@ai-sdk/provider"
import type { PluginHost as Interface } from "@opencode-ai/plugin/v2/effect"
import type { Event as SDKEvent, ModelV2Info } from "@opencode-ai/sdk/v2/types"
import { Effect, Schema, Stream } from "effect"
import { AgentV2 } from "../agent"
import { Catalog } from "../catalog"
import { CommandV2 } from "../command"
import { EventV2 } from "../event"
import { FileSystem } from "../filesystem"
import { Global } from "../global"
import { Integration } from "../integration"
import { Location } from "../location"
import { ModelV2 } from "../model"
import { Npm } from "../npm"
import { PluginV2 } from "../plugin"
import { ProviderV2 } from "../provider"
import { Reference } from "../reference"
import { SkillV2 } from "../skill"
type EventMap = { [Item in SDKEvent as Item["type"]]: Item }
type SDKHook = (event: {
readonly model: ModelV2Info
readonly package: string
readonly options: Record<string, any>
sdk?: any
}) => Effect.Effect<void> | void
type LanguageHook = (event: {
readonly model: ModelV2Info
readonly sdk: any
readonly options: Record<string, any>
language?: LanguageModelV3
}) => Effect.Effect<void> | void
export const make = Effect.fn("PluginHost.make")(function* () {
const agents = yield* AgentV2.Service
const catalog = yield* Catalog.Service
const commands = yield* CommandV2.Service
const events = yield* EventV2.Service
const filesystem = yield* FileSystem.Service
const global = yield* Global.Service
const integration = yield* Integration.Service
const location = yield* Location.Service
const npm = yield* Npm.Service
const plugin = yield* PluginV2.Service
const reference = yield* Reference.Service
const skill = yield* SkillV2.Service
return {
agent: {
get: (id) => agents.get(AgentV2.ID.make(id)),
default: agents.default,
list: agents.all,
rebuild: agents.rebuild,
transform: (callback) =>
agents.transform((draft) =>
callback({
list: draft.list,
get: (id) => draft.get(AgentV2.ID.make(id)),
default: (id) => draft.default(id === undefined ? undefined : AgentV2.ID.make(id)),
update: (id, update) => draft.update(AgentV2.ID.make(id), update),
remove: (id) => draft.remove(AgentV2.ID.make(id)),
}),
),
},
aisdk: {
hook: (name, callback) => {
if (name === "sdk") {
const run = callback as SDKHook
return plugin.hook("aisdk.sdk", (event) => {
const output = {
model: event.model,
package: event.package,
options: event.options,
sdk: event.sdk,
}
const result = run(output)
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
Effect.tap(() => Effect.sync(() => (event.sdk = output.sdk))),
)
})
}
const run = callback as LanguageHook
return plugin.hook("aisdk.language", (event) => {
const output = {
model: event.model,
sdk: event.sdk,
options: event.options,
language: event.language,
}
const result = run(output)
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
Effect.tap(() => Effect.sync(() => (event.language = output.language))),
)
})
},
},
catalog: {
provider: {
get: (id) => catalog.provider.get(ProviderV2.ID.make(id)),
list: catalog.provider.all,
available: catalog.provider.available,
},
model: {
get: (providerID, modelID) => catalog.model.get(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
list: catalog.model.all,
available: catalog.model.available,
default: catalog.model.default,
small: (providerID) => catalog.model.small(ProviderV2.ID.make(providerID)),
},
rebuild: catalog.rebuild,
transform: (callback) =>
catalog.transform((draft) =>
callback({
provider: {
list: draft.provider.list,
get: (id) => draft.provider.get(ProviderV2.ID.make(id)),
update: (id, update) => draft.provider.update(ProviderV2.ID.make(id), update),
remove: (id) => draft.provider.remove(ProviderV2.ID.make(id)),
},
model: {
get: (providerID, modelID) => draft.model.get(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
update: (providerID, modelID, update) =>
draft.model.update(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID), update),
remove: (providerID, modelID) =>
draft.model.remove(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
default: {
get: draft.model.default.get,
set: (providerID, modelID) =>
draft.model.default.set(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
},
},
}),
),
},
command: {
get: commands.get,
list: commands.list,
rebuild: commands.rebuild,
transform: commands.transform,
},
event: {
subscribe: <Type extends keyof EventMap>(type: Type): Stream.Stream<EventMap[Type]> =>
Stream.unwrap(
Effect.sync(() => {
const definition = EventV2.registry.get(type)
if (!definition) throw new Error(`Unknown event type: ${type}`)
const encode = Schema.encodeUnknownSync(definition.data as Schema.Codec<unknown, unknown, never, never>)
return events.subscribe(definition).pipe(
Stream.map(
(event) =>
({
id: event.id,
type: event.type,
properties: encode(event.data),
}) as unknown as EventMap[Type],
),
)
}),
),
},
filesystem: {
read: (input) => filesystem.read(Schema.decodeUnknownSync(FileSystem.ReadInput)(input)),
list: (input) => filesystem.list(Schema.decodeUnknownSync(FileSystem.ListInput)(input ?? {})),
find: (input) => filesystem.find(Schema.decodeUnknownSync(FileSystem.FindInput)(input)),
glob: (input) => filesystem.glob(Schema.decodeUnknownSync(FileSystem.GlobInput)(input)),
},
integration: {
get: (id) => integration.get(Integration.ID.make(id)),
list: integration.list,
rebuild: integration.rebuild,
transform: (callback) =>
integration.transform((draft) =>
callback({
list: draft.list,
get: (id) => draft.get(Integration.ID.make(id)),
update: (id, update) => draft.update(Integration.ID.make(id), update),
remove: (id) => draft.remove(Integration.ID.make(id)),
method: {
list: (id) => draft.method.list(Integration.ID.make(id)),
update: (input) => {
if (input.method.type === "env") {
draft.method.update({
integrationID: Integration.ID.make(input.integrationID),
method: { type: "env", names: input.method.names },
})
return
}
draft.method.update({
integrationID: Integration.ID.make(input.integrationID),
method: { type: "key", label: input.method.label },
})
},
remove: (id, method) =>
draft.method.remove(Integration.ID.make(id), Schema.decodeUnknownSync(Integration.Method)(method)),
},
}),
),
},
location,
npm,
path: {
home: global.home,
data: global.data,
cache: global.cache,
config: global.config,
state: global.state,
temp: global.tmp,
},
reference: {
list: reference.list,
rebuild: reference.rebuild,
transform: (callback) =>
reference.transform((draft) =>
callback({
add: (name, source) => draft.add(name, Schema.decodeUnknownSync(Reference.Source)(source)),
remove: draft.remove,
list: draft.list,
}),
),
},
skill: {
sources: skill.sources,
list: skill.list,
rebuild: skill.rebuild,
transform: (callback) =>
skill.transform((draft) =>
callback({
source: (source) => draft.source(Schema.decodeUnknownSync(SkillV2.Source)(source)),
list: draft.list,
}),
),
},
} satisfies Interface
})
+19 -26
View File
@@ -1,16 +1,13 @@
import { DateTime, Effect, Scope, Stream } from "effect" import { define } from "@opencode-ai/plugin/v2/effect"
import { Catalog } from "../catalog" import { Effect, Stream } from "effect"
import { Integration } from "../integration"
import { EventV2 } from "../event"
import { ModelV2 } from "../model" import { ModelV2 } from "../model"
import { ModelRequest } from "../model-request" import { ModelRequest } from "../model-request"
import { ModelsDev } from "../models-dev" import { ModelsDev } from "../models-dev"
import { PluginV2 } from "../plugin"
import { ProviderV2 } from "../provider" import { ProviderV2 } from "../provider"
function released(date: string) { function released(date: string) {
const time = Date.parse(date) const time = Date.parse(date)
return DateTime.makeUnsafe(Number.isFinite(time) ? time : 0) return Number.isFinite(time) ? time : 0
} }
function cost(input: ModelsDev.Model["cost"]) { function cost(input: ModelsDev.Model["cost"]) {
@@ -51,22 +48,16 @@ function variants(model: ModelsDev.Model, packageName?: string) {
}) })
} }
export const ModelsDevPlugin = PluginV2.define({ export const ModelsDevPlugin = define({
id: PluginV2.ID.make("models-dev"), id: "models-dev",
effect: Effect.gen(function* () { effect: Effect.fn(function* (ctx) {
const catalog = yield* Catalog.Service
const integrations = yield* Integration.Service
const modelsDev = yield* ModelsDev.Service const modelsDev = yield* ModelsDev.Service
const events = yield* EventV2.Service yield* ctx.integration.transform(
const scope = yield* Scope.Scope Effect.fn(function* (integrations) {
const transform = yield* catalog.transform() const data = yield* modelsDev.get()
const integrationTransform = yield* integrations.transform()
const refresh = Effect.fn("ModelsDevPlugin.refresh")(function* () {
const data = yield* modelsDev.get()
yield* integrationTransform((integrations) => {
for (const item of Object.values(data)) { for (const item of Object.values(data)) {
if (item.env.length === 0) continue if (item.env.length === 0) continue
const integrationID = Integration.ID.make(item.id) const integrationID = item.id
integrations.update(integrationID, (integration) => (integration.name = item.name)) integrations.update(integrationID, (integration) => (integration.name = item.name))
integrations.method.update({ integrations.method.update({
integrationID, integrationID,
@@ -77,8 +68,11 @@ export const ModelsDevPlugin = PluginV2.define({
method: { type: "env", names: [...item.env] }, method: { type: "env", names: [...item.env] },
}) })
} }
}) }),
yield* transform((catalog) => { )
yield* ctx.catalog.transform(
Effect.fn(function* (catalog) {
const data = yield* modelsDev.get()
for (const item of Object.values(data)) { for (const item of Object.values(data)) {
const providerID = ProviderV2.ID.make(item.id) const providerID = ProviderV2.ID.make(item.id)
catalog.provider.update(providerID, (provider) => { catalog.provider.update(providerID, (provider) => {
@@ -132,11 +126,10 @@ export const ModelsDevPlugin = PluginV2.define({
}) })
} }
} }
}) }),
}) )
yield* refresh() yield* ctx.event.subscribe("models-dev.refreshed").pipe(
yield* events.subscribe(ModelsDev.Event.Refreshed).pipe( Stream.runForEach(() => ctx.integration.rebuild().pipe(Effect.andThen(ctx.catalog.rebuild()))),
Stream.runForEach(() => refresh()),
Effect.forkScoped({ startImmediately: true }), Effect.forkScoped({ startImmediately: true }),
) )
}), }),
+8 -7
View File
@@ -1,15 +1,16 @@
import { Effect } from "effect" import { Effect } from "effect"
import { PluginV2 } from "../../plugin" import { define } from "@opencode-ai/plugin/v2/effect"
export const AlibabaPlugin = PluginV2.define({ export const AlibabaPlugin = define({
id: PluginV2.ID.make("alibaba"), id: "alibaba",
effect: Effect.gen(function* () { effect: Effect.fn(function* (ctx) {
return { yield* ctx.aisdk.hook(
"aisdk.sdk": Effect.fn(function* (evt) { "sdk",
Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/alibaba") return if (evt.package !== "@ai-sdk/alibaba") return
const mod = yield* Effect.promise(() => import("@ai-sdk/alibaba")) const mod = yield* Effect.promise(() => import("@ai-sdk/alibaba"))
evt.sdk = mod.createAlibaba(evt.options) evt.sdk = mod.createAlibaba(evt.options)
}), }),
} )
}), }),
}) })
@@ -1,6 +1,6 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { LanguageModelV3 } from "@ai-sdk/provider" import type { LanguageModelV3 } from "@ai-sdk/provider"
import { PluginV2 } from "../../plugin" import { define } from "@opencode-ai/plugin/v2/effect"
import { ProviderV2 } from "../../provider" import { ProviderV2 } from "../../provider"
type MantleSDK = { type MantleSDK = {
@@ -59,11 +59,11 @@ function selectMantleModel(sdk: MantleSDK, modelID: string) {
return sdk.responses(modelID) return sdk.responses(modelID)
} }
export const AmazonBedrockPlugin = PluginV2.define({ export const AmazonBedrockPlugin = define({
id: PluginV2.ID.make("amazon-bedrock"), id: "amazon-bedrock",
effect: Effect.gen(function* () { effect: Effect.fn(function* (ctx) {
return { yield* ctx.catalog.transform(
"catalog.transform": Effect.fn(function* (evt) { Effect.fn(function* (evt) {
for (const item of evt.provider.list()) { for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/amazon-bedrock") continue if (item.provider.api.package !== "@ai-sdk/amazon-bedrock") continue
@@ -77,7 +77,10 @@ export const AmazonBedrockPlugin = PluginV2.define({
}) })
} }
}), }),
"aisdk.sdk": Effect.fn(function* (evt) { )
yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) {
if (!["@ai-sdk/amazon-bedrock", "@ai-sdk/amazon-bedrock/mantle"].includes(evt.package)) return if (!["@ai-sdk/amazon-bedrock", "@ai-sdk/amazon-bedrock/mantle"].includes(evt.package)) return
const options = { ...evt.options } const options = { ...evt.options }
const profile = typeof options.profile === "string" ? options.profile : process.env.AWS_PROFILE const profile = typeof options.profile === "string" ? options.profile : process.env.AWS_PROFILE
@@ -108,7 +111,10 @@ export const AmazonBedrockPlugin = PluginV2.define({
const mod = yield* Effect.promise(() => import("@ai-sdk/amazon-bedrock")) const mod = yield* Effect.promise(() => import("@ai-sdk/amazon-bedrock"))
evt.sdk = mod.createAmazonBedrock(options) evt.sdk = mod.createAmazonBedrock(options)
}), }),
"aisdk.language": Effect.fn(function* (evt) { )
yield* ctx.aisdk.hook(
"language",
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.amazonBedrock) return if (evt.model.providerID !== ProviderV2.ID.amazonBedrock) return
if (evt.model.api.type === "aisdk" && evt.model.api.package === "@ai-sdk/amazon-bedrock/mantle") { if (evt.model.api.type === "aisdk" && evt.model.api.package === "@ai-sdk/amazon-bedrock/mantle") {
evt.language = selectMantleModel(evt.sdk, evt.model.api.id) evt.language = selectMantleModel(evt.sdk, evt.model.api.id)
@@ -117,6 +123,6 @@ export const AmazonBedrockPlugin = PluginV2.define({
const region = typeof evt.options.region === "string" ? evt.options.region : process.env.AWS_REGION const region = typeof evt.options.region === "string" ? evt.options.region : process.env.AWS_REGION
evt.language = evt.sdk.languageModel(resolveModelID(evt.model.api.id, region)) evt.language = evt.sdk.languageModel(resolveModelID(evt.model.api.id, region))
}), }),
} )
}), }),
}) })
+11 -8
View File
@@ -1,11 +1,11 @@
import { Effect } from "effect" import { Effect } from "effect"
import { PluginV2 } from "../../plugin" import { define } from "@opencode-ai/plugin/v2/effect"
export const AnthropicPlugin = PluginV2.define({ export const AnthropicPlugin = define({
id: PluginV2.ID.make("anthropic"), id: "anthropic",
effect: Effect.gen(function* () { effect: Effect.fn(function* (ctx) {
return { yield* ctx.catalog.transform(
"catalog.transform": Effect.fn(function* (evt) { Effect.fn(function* (evt) {
for (const item of evt.provider.list()) { for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/anthropic") continue if (item.provider.api.package !== "@ai-sdk/anthropic") continue
@@ -15,11 +15,14 @@ export const AnthropicPlugin = PluginV2.define({
}) })
} }
}), }),
"aisdk.sdk": Effect.fn(function* (evt) { )
yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/anthropic") return if (evt.package !== "@ai-sdk/anthropic") return
const mod = yield* Effect.promise(() => import("@ai-sdk/anthropic")) const mod = yield* Effect.promise(() => import("@ai-sdk/anthropic"))
evt.sdk = mod.createAnthropic(evt.options) evt.sdk = mod.createAnthropic(evt.options)
}), }),
} )
}), }),
}) })
+25 -16
View File
@@ -1,5 +1,5 @@
import { Effect } from "effect" import { Effect } from "effect"
import { PluginV2 } from "../../plugin" import { define } from "@opencode-ai/plugin/v2/effect"
import { ProviderV2 } from "../../provider" import { ProviderV2 } from "../../provider"
function selectLanguage(sdk: any, modelID: string, useChat: boolean) { function selectLanguage(sdk: any, modelID: string, useChat: boolean) {
@@ -10,11 +10,11 @@ function selectLanguage(sdk: any, modelID: string, useChat: boolean) {
return sdk.languageModel(modelID) return sdk.languageModel(modelID)
} }
export const AzurePlugin = PluginV2.define({ export const AzurePlugin = define({
id: PluginV2.ID.make("azure"), id: "azure",
effect: Effect.gen(function* () { effect: Effect.fn(function* (ctx) {
return { yield* ctx.catalog.transform(
"catalog.transform": Effect.fn(function* (evt) { Effect.fn(function* (evt) {
for (const item of evt.provider.list()) { for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/azure") continue if (item.provider.api.package !== "@ai-sdk/azure") continue
@@ -27,7 +27,10 @@ export const AzurePlugin = PluginV2.define({
}) })
} }
}), }),
"aisdk.sdk": Effect.fn(function* (evt) { )
yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/azure") return if (evt.package !== "@ai-sdk/azure") return
if (evt.model.providerID === ProviderV2.ID.azure) { if (evt.model.providerID === ProviderV2.ID.azure) {
if ( if (
@@ -43,19 +46,22 @@ export const AzurePlugin = PluginV2.define({
const mod = yield* Effect.promise(() => import("@ai-sdk/azure")) const mod = yield* Effect.promise(() => import("@ai-sdk/azure"))
evt.sdk = mod.createAzure(evt.options) evt.sdk = mod.createAzure(evt.options)
}), }),
"aisdk.language": Effect.fn(function* (evt) { )
yield* ctx.aisdk.hook(
"language",
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.azure) return if (evt.model.providerID !== ProviderV2.ID.azure) return
evt.language = selectLanguage(evt.sdk, evt.model.api.id, Boolean(evt.options.useCompletionUrls)) evt.language = selectLanguage(evt.sdk, evt.model.api.id, Boolean(evt.options.useCompletionUrls))
}), }),
} )
}), }),
}) })
export const AzureCognitiveServicesPlugin = PluginV2.define({ export const AzureCognitiveServicesPlugin = define({
id: PluginV2.ID.make("azure-cognitive-services"), id: "azure-cognitive-services",
effect: Effect.gen(function* () { effect: Effect.fn(function* (ctx) {
return { yield* ctx.catalog.transform(
"catalog.transform": Effect.fn(function* (evt) { Effect.fn(function* (evt) {
const resourceName = process.env.AZURE_COGNITIVE_SERVICES_RESOURCE_NAME const resourceName = process.env.AZURE_COGNITIVE_SERVICES_RESOURCE_NAME
if (!resourceName) return if (!resourceName) return
for (const item of evt.provider.list()) { for (const item of evt.provider.list()) {
@@ -67,10 +73,13 @@ export const AzureCognitiveServicesPlugin = PluginV2.define({
}) })
} }
}), }),
"aisdk.language": Effect.fn(function* (evt) { )
yield* ctx.aisdk.hook(
"language",
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("azure-cognitive-services")) return if (evt.model.providerID !== ProviderV2.ID.make("azure-cognitive-services")) return
evt.language = selectLanguage(evt.sdk, evt.model.api.id, Boolean(evt.options.useCompletionUrls)) evt.language = selectLanguage(evt.sdk, evt.model.api.id, Boolean(evt.options.useCompletionUrls))
}), }),
} )
}), }),
}) })
+13 -10
View File
@@ -1,24 +1,27 @@
import { Effect } from "effect" import { Effect } from "effect"
import { PluginV2 } from "../../plugin" import { define } from "@opencode-ai/plugin/v2/effect"
export const CerebrasPlugin = PluginV2.define({ export const CerebrasPlugin = define({
id: PluginV2.ID.make("cerebras"), id: "cerebras",
effect: Effect.gen(function* () { effect: Effect.fn(function* (ctx) {
return { yield* ctx.catalog.transform(
"catalog.transform": Effect.fn(function* (ctx) { Effect.fn(function* (evt) {
for (const item of ctx.provider.list()) { for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/cerebras") continue if (item.provider.api.package !== "@ai-sdk/cerebras") continue
ctx.provider.update(item.provider.id, (provider) => { evt.provider.update(item.provider.id, (provider) => {
provider.request.headers["X-Cerebras-3rd-Party-Integration"] = "opencode" provider.request.headers["X-Cerebras-3rd-Party-Integration"] = "opencode"
}) })
} }
}), }),
"aisdk.sdk": Effect.fn(function* (evt) { )
yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/cerebras") return if (evt.package !== "@ai-sdk/cerebras") return
const mod = yield* Effect.promise(() => import("@ai-sdk/cerebras")) const mod = yield* Effect.promise(() => import("@ai-sdk/cerebras"))
evt.sdk = mod.createCerebras(evt.options) evt.sdk = mod.createCerebras(evt.options)
}), }),
} )
}), }),
}) })
@@ -1,13 +1,14 @@
import os from "os" import os from "os"
import { InstallationVersion } from "../../installation/version" import { InstallationVersion } from "../../installation/version"
import { Effect, Option, Schema } from "effect" import { Effect, Option, Schema } from "effect"
import { PluginV2 } from "../../plugin" import { define } from "@opencode-ai/plugin/v2/effect"
export const CloudflareAIGatewayPlugin = PluginV2.define({ export const CloudflareAIGatewayPlugin = define({
id: PluginV2.ID.make("cloudflare-ai-gateway"), id: "cloudflare-ai-gateway",
effect: Effect.gen(function* () { effect: Effect.fn(function* (ctx) {
return { yield* ctx.aisdk.hook(
"aisdk.sdk": Effect.fn(function* (evt) { "sdk",
Effect.fn(function* (evt) {
if (evt.package !== "ai-gateway-provider") return if (evt.package !== "ai-gateway-provider") return
if (evt.options.baseURL) return if (evt.options.baseURL) return
@@ -31,7 +32,7 @@ export const CloudflareAIGatewayPlugin = PluginV2.define({
}, },
} }
}), }),
} )
}), }),
}) })
@@ -1,16 +1,16 @@
import os from "os" import os from "os"
import { InstallationVersion } from "../../installation/version" import { InstallationVersion } from "../../installation/version"
import { Effect } from "effect" import { Effect } from "effect"
import { PluginV2 } from "../../plugin" import { define } from "@opencode-ai/plugin/v2/effect"
import { ProviderV2 } from "../../provider" import { ProviderV2 } from "../../provider"
const providerID = ProviderV2.ID.make("cloudflare-workers-ai") const providerID = ProviderV2.ID.make("cloudflare-workers-ai")
export const CloudflareWorkersAIPlugin = PluginV2.define({ export const CloudflareWorkersAIPlugin = define({
id: PluginV2.ID.make("cloudflare-workers-ai"), id: "cloudflare-workers-ai",
effect: Effect.gen(function* () { effect: Effect.fn(function* (ctx) {
return { yield* ctx.catalog.transform(
"catalog.transform": Effect.fn(function* (evt) { Effect.fn(function* (evt) {
const item = evt.provider.get(providerID) const item = evt.provider.get(providerID)
if (!item) return if (!item) return
evt.provider.update(item.provider.id, (provider) => { evt.provider.update(item.provider.id, (provider) => {
@@ -20,7 +20,10 @@ export const CloudflareWorkersAIPlugin = PluginV2.define({
if (accountId) provider.api.url = workersEndpoint(accountId) if (accountId) provider.api.url = workersEndpoint(accountId)
}) })
}), }),
"aisdk.sdk": Effect.fn(function* (evt) { )
yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) {
if (evt.model.providerID !== providerID) return if (evt.model.providerID !== providerID) return
if (evt.package !== "@ai-sdk/openai-compatible") return if (evt.package !== "@ai-sdk/openai-compatible") return
@@ -34,11 +37,14 @@ export const CloudflareWorkersAIPlugin = PluginV2.define({
}) as any, }) as any,
) )
}), }),
"aisdk.language": Effect.fn(function* (evt) { )
yield* ctx.aisdk.hook(
"language",
Effect.fn(function* (evt) {
if (evt.model.providerID !== providerID) return if (evt.model.providerID !== providerID) return
evt.language = evt.sdk.languageModel(evt.model.api.id) evt.language = evt.sdk.languageModel(evt.model.api.id)
}), }),
} )
}), }),
}) })
+8 -7
View File
@@ -1,15 +1,16 @@
import { Effect } from "effect" import { Effect } from "effect"
import { PluginV2 } from "../../plugin" import { define } from "@opencode-ai/plugin/v2/effect"
export const CoherePlugin = PluginV2.define({ export const CoherePlugin = define({
id: PluginV2.ID.make("cohere"), id: "cohere",
effect: Effect.gen(function* () { effect: Effect.fn(function* (ctx) {
return { yield* ctx.aisdk.hook(
"aisdk.sdk": Effect.fn(function* (evt) { "sdk",
Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/cohere") return if (evt.package !== "@ai-sdk/cohere") return
const mod = yield* Effect.promise(() => import("@ai-sdk/cohere")) const mod = yield* Effect.promise(() => import("@ai-sdk/cohere"))
evt.sdk = mod.createCohere(evt.options) evt.sdk = mod.createCohere(evt.options)
}), }),
} )
}), }),
}) })
@@ -1,15 +1,16 @@
import { Effect } from "effect" import { Effect } from "effect"
import { PluginV2 } from "../../plugin" import { define } from "@opencode-ai/plugin/v2/effect"
export const DeepInfraPlugin = PluginV2.define({ export const DeepInfraPlugin = define({
id: PluginV2.ID.make("deepinfra"), id: "deepinfra",
effect: Effect.gen(function* () { effect: Effect.fn(function* (ctx) {
return { yield* ctx.aisdk.hook(
"aisdk.sdk": Effect.fn(function* (evt) { "sdk",
Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/deepinfra") return if (evt.package !== "@ai-sdk/deepinfra") return
const mod = yield* Effect.promise(() => import("@ai-sdk/deepinfra")) const mod = yield* Effect.promise(() => import("@ai-sdk/deepinfra"))
evt.sdk = mod.createDeepInfra(evt.options) evt.sdk = mod.createDeepInfra(evt.options)
}), }),
} )
}), }),
}) })
+10 -11
View File
@@ -1,19 +1,18 @@
import { Npm } from "../../npm" import { Effect } from "effect"
import { Effect, Option } from "effect"
import { pathToFileURL } from "url" import { pathToFileURL } from "url"
import { PluginV2 } from "../../plugin" import { define } from "@opencode-ai/plugin/v2/effect"
export const DynamicProviderPlugin = PluginV2.define({ export const DynamicProviderPlugin = define({
id: PluginV2.ID.make("dynamic-provider"), id: "dynamic-provider",
effect: Effect.gen(function* () { effect: Effect.fn(function* (ctx) {
const npm = yield* Npm.Service yield* ctx.aisdk.hook(
return { "sdk",
"aisdk.sdk": Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.sdk) return if (evt.sdk) return
const installedPath = evt.package.startsWith("file://") const installedPath = evt.package.startsWith("file://")
? evt.package ? evt.package
: Option.getOrUndefined((yield* npm.add(evt.package).pipe(Effect.orDie)).entrypoint) : (yield* ctx.npm.add(evt.package).pipe(Effect.orDie)).entrypoint
if (!installedPath) throw new Error(`Package ${evt.package} has no import entrypoint`) if (!installedPath) throw new Error(`Package ${evt.package} has no import entrypoint`)
const mod = yield* Effect.promise(async () => { const mod = yield* Effect.promise(async () => {
@@ -26,6 +25,6 @@ export const DynamicProviderPlugin = PluginV2.define({
evt.sdk = mod[match](evt.options) evt.sdk = mod[match](evt.options)
}), }),
} )
}), }),
}) })
+8 -7
View File
@@ -1,15 +1,16 @@
import { Effect } from "effect" import { Effect } from "effect"
import { PluginV2 } from "../../plugin" import { define } from "@opencode-ai/plugin/v2/effect"
export const GatewayPlugin = PluginV2.define({ export const GatewayPlugin = define({
id: PluginV2.ID.make("gateway"), id: "gateway",
effect: Effect.gen(function* () { effect: Effect.fn(function* (ctx) {
return { yield* ctx.aisdk.hook(
"aisdk.sdk": Effect.fn(function* (evt) { "sdk",
Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/gateway") return if (evt.package !== "@ai-sdk/gateway") return
const mod = yield* Effect.promise(() => import("@ai-sdk/gateway")) const mod = yield* Effect.promise(() => import("@ai-sdk/gateway"))
evt.sdk = mod.createGateway(evt.options) evt.sdk = mod.createGateway(evt.options)
}), }),
} )
}), }),
}) })
@@ -1,6 +1,6 @@
import { Effect } from "effect" import { Effect } from "effect"
import { ModelV2 } from "../../model" import { ModelV2 } from "../../model"
import { PluginV2 } from "../../plugin" import { define } from "@opencode-ai/plugin/v2/effect"
import { ProviderV2 } from "../../provider" import { ProviderV2 } from "../../provider"
function shouldUseResponses(modelID: string) { function shouldUseResponses(modelID: string) {
@@ -11,16 +11,31 @@ function shouldUseResponses(modelID: string) {
return Number(match[1]) >= 5 && !modelID.startsWith("gpt-5-mini") return Number(match[1]) >= 5 && !modelID.startsWith("gpt-5-mini")
} }
export const GithubCopilotPlugin = PluginV2.define({ export const GithubCopilotPlugin = define({
id: PluginV2.ID.make("github-copilot"), id: "github-copilot",
effect: Effect.gen(function* () { effect: Effect.fn(function* (ctx) {
return { yield* ctx.catalog.transform(
"aisdk.sdk": Effect.fn(function* (evt) { Effect.fn(function* (evt) {
const item = evt.provider.get(ProviderV2.ID.githubCopilot)
if (!item || !item.models.has(ModelV2.ID.make("gpt-5-chat-latest"))) return
evt.model.update(item.provider.id, ModelV2.ID.make("gpt-5-chat-latest"), (model) => {
// This chat-only alias conflicts with the Copilot GPT-5 Responses route,
// so hide it only for Copilot rather than for every provider catalog.
model.enabled = false
})
}),
)
yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/github-copilot") return if (evt.package !== "@ai-sdk/github-copilot") return
const mod = yield* Effect.promise(() => import("../../github-copilot/copilot-provider")) const mod = yield* Effect.promise(() => import("../../github-copilot/copilot-provider"))
evt.sdk = mod.createOpenaiCompatible(evt.options) evt.sdk = mod.createOpenaiCompatible(evt.options)
}), }),
"aisdk.language": Effect.fn(function* (evt) { )
yield* ctx.aisdk.hook(
"language",
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.githubCopilot) return if (evt.model.providerID !== ProviderV2.ID.githubCopilot) return
if (evt.sdk.responses === undefined && evt.sdk.chat === undefined) { if (evt.sdk.responses === undefined && evt.sdk.chat === undefined) {
evt.language = evt.sdk.languageModel(evt.model.api.id) evt.language = evt.sdk.languageModel(evt.model.api.id)
@@ -30,15 +45,6 @@ export const GithubCopilotPlugin = PluginV2.define({
? evt.sdk.responses(evt.model.api.id) ? evt.sdk.responses(evt.model.api.id)
: evt.sdk.chat(evt.model.api.id) : evt.sdk.chat(evt.model.api.id)
}), }),
"catalog.transform": Effect.fn(function* (evt) { )
const item = evt.provider.get(ProviderV2.ID.githubCopilot)
if (!item || !item.models.has(ModelV2.ID.make("gpt-5-chat-latest"))) return
evt.model.update(item.provider.id, ModelV2.ID.make("gpt-5-chat-latest"), (model) => {
// This chat-only alias conflicts with the Copilot GPT-5 Responses route,
// so hide it only for Copilot rather than for every provider catalog.
model.enabled = false
})
}),
}
}), }),
}) })
+12 -8
View File
@@ -1,14 +1,15 @@
import os from "os" import os from "os"
import { InstallationVersion } from "../../installation/version" import { InstallationVersion } from "../../installation/version"
import { Effect } from "effect" import { Effect } from "effect"
import { PluginV2 } from "../../plugin" import { define } from "@opencode-ai/plugin/v2/effect"
import { ProviderV2 } from "../../provider" import { ProviderV2 } from "../../provider"
export const GitLabPlugin = PluginV2.define({ export const GitLabPlugin = define({
id: PluginV2.ID.make("gitlab"), id: "gitlab",
effect: Effect.gen(function* () { effect: Effect.fn(function* (ctx) {
return { yield* ctx.aisdk.hook(
"aisdk.sdk": Effect.fn(function* (evt) { "sdk",
Effect.fn(function* (evt) {
if (evt.package !== "gitlab-ai-provider") return if (evt.package !== "gitlab-ai-provider") return
const mod = yield* Effect.promise(() => import("gitlab-ai-provider")) const mod = yield* Effect.promise(() => import("gitlab-ai-provider"))
evt.sdk = mod.createGitLab({ evt.sdk = mod.createGitLab({
@@ -30,7 +31,10 @@ export const GitLabPlugin = PluginV2.define({
}, },
}) })
}), }),
"aisdk.language": Effect.fn(function* (evt) { )
yield* ctx.aisdk.hook(
"language",
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.gitlab) return if (evt.model.providerID !== ProviderV2.ID.gitlab) return
const featureFlags = const featureFlags =
typeof evt.options.featureFlags === "object" && evt.options.featureFlags ? evt.options.featureFlags : {} typeof evt.options.featureFlags === "object" && evt.options.featureFlags ? evt.options.featureFlags : {}
@@ -58,6 +62,6 @@ export const GitLabPlugin = PluginV2.define({
featureFlags, featureFlags,
}) })
}), }),
} )
}), }),
}) })
@@ -1,5 +1,5 @@
import { Effect } from "effect" import { Effect } from "effect"
import { PluginV2 } from "../../plugin" import { define } from "@opencode-ai/plugin/v2/effect"
import { ProviderV2 } from "../../provider" import { ProviderV2 } from "../../provider"
function resolveProject(options: Record<string, any>) { function resolveProject(options: Record<string, any>) {
@@ -54,11 +54,11 @@ function authFetch(fetchWithRuntimeOptions?: unknown) {
} }
} }
export const GoogleVertexPlugin = PluginV2.define({ export const GoogleVertexPlugin = define({
id: PluginV2.ID.make("google-vertex"), id: "google-vertex",
effect: Effect.gen(function* () { effect: Effect.fn(function* (ctx) {
return { yield* ctx.catalog.transform(
"catalog.transform": Effect.fn(function* (evt) { Effect.fn(function* (evt) {
for (const item of evt.provider.list()) { for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue if (item.provider.api.type !== "aisdk") continue
if ( if (
@@ -83,7 +83,10 @@ export const GoogleVertexPlugin = PluginV2.define({
}) })
} }
}), }),
"aisdk.sdk": Effect.fn(function* (evt) { )
yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) {
if (evt.model.providerID === ProviderV2.ID.googleVertex && evt.package.includes("@ai-sdk/openai-compatible")) { if (evt.model.providerID === ProviderV2.ID.googleVertex && evt.package.includes("@ai-sdk/openai-compatible")) {
evt.options.fetch = authFetch(evt.options.fetch) evt.options.fetch = authFetch(evt.options.fetch)
return return
@@ -100,19 +103,22 @@ export const GoogleVertexPlugin = PluginV2.define({
location, location,
}) })
}), }),
"aisdk.language": Effect.fn(function* (evt) { )
yield* ctx.aisdk.hook(
"language",
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.googleVertex) return if (evt.model.providerID !== ProviderV2.ID.googleVertex) return
evt.language = evt.sdk.languageModel(String(evt.model.api.id).trim()) evt.language = evt.sdk.languageModel(String(evt.model.api.id).trim())
}), }),
} )
}), }),
}) })
export const GoogleVertexAnthropicPlugin = PluginV2.define({ export const GoogleVertexAnthropicPlugin = define({
id: PluginV2.ID.make("google-vertex-anthropic"), id: "google-vertex-anthropic",
effect: Effect.gen(function* () { effect: Effect.fn(function* (ctx) {
return { yield* ctx.catalog.transform(
"catalog.transform": Effect.fn(function* (evt) { Effect.fn(function* (evt) {
for (const item of evt.provider.list()) { for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/google-vertex/anthropic") continue if (item.provider.api.package !== "@ai-sdk/google-vertex/anthropic") continue
@@ -132,7 +138,10 @@ export const GoogleVertexAnthropicPlugin = PluginV2.define({
}) })
} }
}), }),
"aisdk.sdk": Effect.fn(function* (evt) { )
yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/google-vertex/anthropic") return if (evt.package !== "@ai-sdk/google-vertex/anthropic") return
const mod = yield* Effect.promise(() => import("@ai-sdk/google-vertex/anthropic")) const mod = yield* Effect.promise(() => import("@ai-sdk/google-vertex/anthropic"))
const project = const project =
@@ -156,10 +165,13 @@ export const GoogleVertexAnthropicPlugin = PluginV2.define({
: {}), : {}),
}) })
}), }),
"aisdk.language": Effect.fn(function* (evt) { )
yield* ctx.aisdk.hook(
"language",
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("google-vertex-anthropic")) return if (evt.model.providerID !== ProviderV2.ID.make("google-vertex-anthropic")) return
evt.language = evt.sdk.languageModel(String(evt.model.api.id).trim()) evt.language = evt.sdk.languageModel(String(evt.model.api.id).trim())
}), }),
} )
}), }),
}) })
+8 -7
View File
@@ -1,15 +1,16 @@
import { Effect } from "effect" import { Effect } from "effect"
import { PluginV2 } from "../../plugin" import { define } from "@opencode-ai/plugin/v2/effect"
export const GooglePlugin = PluginV2.define({ export const GooglePlugin = define({
id: PluginV2.ID.make("google"), id: "google",
effect: Effect.gen(function* () { effect: Effect.fn(function* (ctx) {
return { yield* ctx.aisdk.hook(
"aisdk.sdk": Effect.fn(function* (evt) { "sdk",
Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/google") return if (evt.package !== "@ai-sdk/google") return
const mod = yield* Effect.promise(() => import("@ai-sdk/google")) const mod = yield* Effect.promise(() => import("@ai-sdk/google"))
evt.sdk = mod.createGoogleGenerativeAI(evt.options) evt.sdk = mod.createGoogleGenerativeAI(evt.options)
}), }),
} )
}), }),
}) })
+8 -7
View File
@@ -1,15 +1,16 @@
import { Effect } from "effect" import { Effect } from "effect"
import { PluginV2 } from "../../plugin" import { define } from "@opencode-ai/plugin/v2/effect"
export const GroqPlugin = PluginV2.define({ export const GroqPlugin = define({
id: PluginV2.ID.make("groq"), id: "groq",
effect: Effect.gen(function* () { effect: Effect.fn(function* (ctx) {
return { yield* ctx.aisdk.hook(
"aisdk.sdk": Effect.fn(function* (evt) { "sdk",
Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/groq") return if (evt.package !== "@ai-sdk/groq") return
const mod = yield* Effect.promise(() => import("@ai-sdk/groq")) const mod = yield* Effect.promise(() => import("@ai-sdk/groq"))
evt.sdk = mod.createGroq(evt.options) evt.sdk = mod.createGroq(evt.options)
}), }),
} )
}), }),
}) })
+7 -7
View File
@@ -1,11 +1,11 @@
import { Effect } from "effect" import { Effect } from "effect"
import { PluginV2 } from "../../plugin" import { define } from "@opencode-ai/plugin/v2/effect"
export const KiloPlugin = PluginV2.define({ export const KiloPlugin = define({
id: PluginV2.ID.make("kilo"), id: "kilo",
effect: Effect.gen(function* () { effect: Effect.fn(function* (ctx) {
return { yield* ctx.catalog.transform(
"catalog.transform": Effect.fn(function* (evt) { Effect.fn(function* (evt) {
for (const item of evt.provider.list()) { for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
@@ -16,6 +16,6 @@ export const KiloPlugin = PluginV2.define({
}) })
} }
}), }),
} )
}), }),
}) })
@@ -1,19 +1,17 @@
import { Effect } from "effect" import { Effect } from "effect"
import { Integration } from "../../integration" import { define } from "@opencode-ai/plugin/v2/effect"
import { PluginV2 } from "../../plugin"
export const LLMGatewayPlugin = PluginV2.define({ export const LLMGatewayPlugin = define({
id: PluginV2.ID.make("llmgateway"), id: "llmgateway",
effect: Effect.gen(function* () { effect: Effect.fn(function* (ctx) {
const integrations = yield* Integration.Service yield* ctx.catalog.transform(
return { Effect.fn(function* (evt) {
"catalog.transform": Effect.fn(function* (evt) {
for (const item of evt.provider.list()) { for (const item of evt.provider.list()) {
if (item.provider.disabled) continue if (item.provider.disabled) continue
if (!(yield* integrations.get(Integration.ID.make(item.provider.id)))) continue
if (item.provider.api.type !== "aisdk") continue if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
if (item.provider.api.url !== "https://api.llmgateway.io/v1") continue if (item.provider.api.url !== "https://api.llmgateway.io/v1") continue
if (!(yield* ctx.integration.get(item.provider.id))) continue
evt.provider.update(item.provider.id, (provider) => { evt.provider.update(item.provider.id, (provider) => {
provider.request.headers["HTTP-Referer"] = "https://opencode.ai/" provider.request.headers["HTTP-Referer"] = "https://opencode.ai/"
provider.request.headers["X-Title"] = "opencode" provider.request.headers["X-Title"] = "opencode"
@@ -21,6 +19,6 @@ export const LLMGatewayPlugin = PluginV2.define({
}) })
} }
}), }),
} )
}), }),
}) })
+8 -7
View File
@@ -1,15 +1,16 @@
import { Effect } from "effect" import { Effect } from "effect"
import { PluginV2 } from "../../plugin" import { define } from "@opencode-ai/plugin/v2/effect"
export const MistralPlugin = PluginV2.define({ export const MistralPlugin = define({
id: PluginV2.ID.make("mistral"), id: "mistral",
effect: Effect.gen(function* () { effect: Effect.fn(function* (ctx) {
return { yield* ctx.aisdk.hook(
"aisdk.sdk": Effect.fn(function* (evt) { "sdk",
Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/mistral") return if (evt.package !== "@ai-sdk/mistral") return
const mod = yield* Effect.promise(() => import("@ai-sdk/mistral")) const mod = yield* Effect.promise(() => import("@ai-sdk/mistral"))
evt.sdk = mod.createMistral(evt.options) evt.sdk = mod.createMistral(evt.options)
}), }),
} )
}), }),
}) })
+7 -7
View File
@@ -1,11 +1,11 @@
import { Effect } from "effect" import { Effect } from "effect"
import { PluginV2 } from "../../plugin" import { define } from "@opencode-ai/plugin/v2/effect"
export const NvidiaPlugin = PluginV2.define({ export const NvidiaPlugin = define({
id: PluginV2.ID.make("nvidia"), id: "nvidia",
effect: Effect.gen(function* () { effect: Effect.fn(function* (ctx) {
return { yield* ctx.catalog.transform(
"catalog.transform": Effect.fn(function* (evt) { Effect.fn(function* (evt) {
for (const item of evt.provider.list()) { for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
@@ -17,6 +17,6 @@ export const NvidiaPlugin = PluginV2.define({
}) })
} }
}), }),
} )
}), }),
}) })
@@ -1,17 +1,18 @@
import { Effect } from "effect" import { Effect } from "effect"
import { PluginV2 } from "../../plugin" import { define } from "@opencode-ai/plugin/v2/effect"
export const OpenAICompatiblePlugin = PluginV2.define({ export const OpenAICompatiblePlugin = define({
id: PluginV2.ID.make("openai-compatible"), id: "openai-compatible",
effect: Effect.gen(function* () { effect: Effect.fn(function* (ctx) {
return { yield* ctx.aisdk.hook(
"aisdk.sdk": Effect.fn(function* (evt) { "sdk",
Effect.fn(function* (evt) {
if (evt.sdk) return if (evt.sdk) return
if (!evt.package.includes("@ai-sdk/openai-compatible")) return if (!evt.package.includes("@ai-sdk/openai-compatible")) return
if (evt.options.includeUsage !== false) evt.options.includeUsage = true if (evt.options.includeUsage !== false) evt.options.includeUsage = true
const mod = yield* Effect.promise(() => import("@ai-sdk/openai-compatible")) const mod = yield* Effect.promise(() => import("@ai-sdk/openai-compatible"))
evt.sdk = mod.createOpenAICompatible(evt.options as any) evt.sdk = mod.createOpenAICompatible(evt.options as any)
}), }),
} )
}), }),
}) })
+25 -19
View File
@@ -1,29 +1,20 @@
import { Effect } from "effect" import { Effect } from "effect"
import { ModelV2 } from "../../model" import { ModelV2 } from "../../model"
import { PluginV2 } from "../../plugin" import { define } from "@opencode-ai/plugin/v2/effect"
import { ProviderV2 } from "../../provider" import { ProviderV2 } from "../../provider"
import { Integration } from "../../integration" import { Integration } from "../../integration"
import { browser, headless } from "./openai-auth" import { browser, headless } from "./openai-auth"
export const OpenAIPlugin = PluginV2.define({ export const OpenAIPlugin = define({
id: PluginV2.ID.make("openai"), id: "openai",
effect: Effect.gen(function* () { effect: Effect.fn(function* (ctx) {
const integrations = yield* Integration.Service const integrations = yield* Integration.Service
yield* integrations.update((editor) => { yield* integrations.transform((draft) => {
editor.method.update(browser) draft.method.update(browser)
editor.method.update(headless) draft.method.update(headless)
}) })
return { yield* ctx.catalog.transform(
"aisdk.sdk": Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/openai") return
const mod = yield* Effect.promise(() => import("@ai-sdk/openai"))
evt.sdk = mod.createOpenAI(evt.options)
}),
"aisdk.language": Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.openai) return
evt.language = evt.sdk.responses(evt.model.api.id)
}),
"catalog.transform": Effect.fn(function* (evt) {
for (const item of evt.provider.list()) { for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/openai") continue if (item.provider.api.package !== "@ai-sdk/openai") continue
@@ -35,6 +26,21 @@ export const OpenAIPlugin = PluginV2.define({
}) })
} }
}), }),
} )
yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/openai") return
const mod = yield* Effect.promise(() => import("@ai-sdk/openai"))
evt.sdk = mod.createOpenAI(evt.options)
}),
)
yield* ctx.aisdk.hook(
"language",
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.openai) return
evt.language = evt.sdk.responses(evt.model.api.id)
}),
)
}), }),
}) })
+8 -10
View File
@@ -1,18 +1,16 @@
import { Effect } from "effect" import { Effect } from "effect"
import { Integration } from "../../integration" import { define } from "@opencode-ai/plugin/v2/effect"
import { PluginV2 } from "../../plugin"
import { ProviderV2 } from "../../provider" import { ProviderV2 } from "../../provider"
export const OpencodePlugin = PluginV2.define({ export const OpencodePlugin = define({
id: PluginV2.ID.make("opencode"), id: "opencode",
effect: Effect.gen(function* () { effect: Effect.fn(function* (ctx) {
const integrations = yield* Integration.Service
let hasKey = false let hasKey = false
return { yield* ctx.catalog.transform(
"catalog.transform": Effect.fn(function* (evt) { Effect.fn(function* (evt) {
const item = evt.provider.get(ProviderV2.ID.opencode) const item = evt.provider.get(ProviderV2.ID.opencode)
if (!item) return if (!item) return
const integration = yield* integrations.get(Integration.ID.make(item.provider.id)) const integration = yield* ctx.integration.get(item.provider.id)
hasKey = Boolean( hasKey = Boolean(
process.env.OPENCODE_API_KEY || integration?.connections.length || item.provider.request.body.apiKey, process.env.OPENCODE_API_KEY || integration?.connections.length || item.provider.request.body.apiKey,
) )
@@ -27,6 +25,6 @@ export const OpencodePlugin = PluginV2.define({
}) })
} }
}), }),
} )
}), }),
}) })
@@ -1,12 +1,12 @@
import { Effect } from "effect" import { Effect } from "effect"
import { ModelV2 } from "../../model" import { ModelV2 } from "../../model"
import { PluginV2 } from "../../plugin" import { define } from "@opencode-ai/plugin/v2/effect"
export const OpenRouterPlugin = PluginV2.define({ export const OpenRouterPlugin = define({
id: PluginV2.ID.make("openrouter"), id: "openrouter",
effect: Effect.gen(function* () { effect: Effect.fn(function* (ctx) {
return { yield* ctx.catalog.transform(
"catalog.transform": Effect.fn(function* (evt) { Effect.fn(function* (evt) {
for (const item of evt.provider.list()) { for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@openrouter/ai-sdk-provider") continue if (item.provider.api.package !== "@openrouter/ai-sdk-provider") continue
@@ -24,11 +24,14 @@ export const OpenRouterPlugin = PluginV2.define({
} }
} }
}), }),
"aisdk.sdk": Effect.fn(function* (evt) { )
yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) {
if (evt.package !== "@openrouter/ai-sdk-provider") return if (evt.package !== "@openrouter/ai-sdk-provider") return
const mod = yield* Effect.promise(() => import("@openrouter/ai-sdk-provider")) const mod = yield* Effect.promise(() => import("@openrouter/ai-sdk-provider"))
evt.sdk = mod.createOpenRouter(evt.options) evt.sdk = mod.createOpenRouter(evt.options)
}), }),
} )
}), }),
}) })
@@ -1,15 +1,16 @@
import { Effect } from "effect" import { Effect } from "effect"
import { PluginV2 } from "../../plugin" import { define } from "@opencode-ai/plugin/v2/effect"
export const PerplexityPlugin = PluginV2.define({ export const PerplexityPlugin = define({
id: PluginV2.ID.make("perplexity"), id: "perplexity",
effect: Effect.gen(function* () { effect: Effect.fn(function* (ctx) {
return { yield* ctx.aisdk.hook(
"aisdk.sdk": Effect.fn(function* (evt) { "sdk",
Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/perplexity") return if (evt.package !== "@ai-sdk/perplexity") return
const mod = yield* Effect.promise(() => import("@ai-sdk/perplexity")) const mod = yield* Effect.promise(() => import("@ai-sdk/perplexity"))
evt.sdk = mod.createPerplexity(evt.options) evt.sdk = mod.createPerplexity(evt.options)
}), }),
} )
}), }),
}) })
@@ -1,15 +1,14 @@
import { Npm } from "../../npm" import { Effect } from "effect"
import { Effect, Option } from "effect"
import { pathToFileURL } from "url" import { pathToFileURL } from "url"
import { PluginV2 } from "../../plugin" import { define } from "@opencode-ai/plugin/v2/effect"
import { ProviderV2 } from "../../provider" import { ProviderV2 } from "../../provider"
export const SapAICorePlugin = PluginV2.define({ export const SapAICorePlugin = define({
id: PluginV2.ID.make("sap-ai-core"), id: "sap-ai-core",
effect: Effect.gen(function* () { effect: Effect.fn(function* (ctx) {
const npm = yield* Npm.Service yield* ctx.aisdk.hook(
return { "sdk",
"aisdk.sdk": Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return
const serviceKey = const serviceKey =
process.env.AICORE_SERVICE_KEY ?? process.env.AICORE_SERVICE_KEY ??
@@ -18,7 +17,7 @@ export const SapAICorePlugin = PluginV2.define({
const installedPath = evt.package.startsWith("file://") const installedPath = evt.package.startsWith("file://")
? evt.package ? evt.package
: Option.getOrUndefined((yield* npm.add(evt.package).pipe(Effect.orDie)).entrypoint) : (yield* ctx.npm.add(evt.package).pipe(Effect.orDie)).entrypoint
if (!installedPath) throw new Error(`Package ${evt.package} has no import entrypoint`) if (!installedPath) throw new Error(`Package ${evt.package} has no import entrypoint`)
const mod = yield* Effect.promise(async () => { const mod = yield* Effect.promise(async () => {
@@ -35,10 +34,13 @@ export const SapAICorePlugin = PluginV2.define({
: {}, : {},
) )
}), }),
"aisdk.language": Effect.fn(function* (evt) { )
yield* ctx.aisdk.hook(
"language",
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return
evt.language = evt.sdk(evt.model.api.id) evt.language = evt.sdk(evt.model.api.id)
}), }),
} )
}), }),
}) })
@@ -1,5 +1,5 @@
import { Effect } from "effect" import { Effect } from "effect"
import { PluginV2 } from "../../plugin" import { define } from "@opencode-ai/plugin/v2/effect"
import { ProviderV2 } from "../../provider" import { ProviderV2 } from "../../provider"
type FetchLike = (url: string | URL | Request, init?: RequestInit) => Promise<Response> type FetchLike = (url: string | URL | Request, init?: RequestInit) => Promise<Response>
@@ -64,11 +64,12 @@ export function cortexFetch(upstream: FetchLike = fetch) {
} }
} }
export const SnowflakeCortexPlugin = PluginV2.define({ export const SnowflakeCortexPlugin = define({
id: PluginV2.ID.make("snowflake-cortex"), id: "snowflake-cortex",
effect: Effect.gen(function* () { effect: Effect.fn(function* (ctx) {
return { yield* ctx.aisdk.hook(
"aisdk.sdk": Effect.fn(function* (evt) { "sdk",
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("snowflake-cortex")) return if (evt.model.providerID !== ProviderV2.ID.make("snowflake-cortex")) return
const token = const token =
process.env.SNOWFLAKE_CORTEX_TOKEN ?? process.env.SNOWFLAKE_CORTEX_TOKEN ??
@@ -84,6 +85,6 @@ export const SnowflakeCortexPlugin = PluginV2.define({
fetch: cortexFetch(upstream) as typeof fetch, fetch: cortexFetch(upstream) as typeof fetch,
} as any) } as any)
}), }),
} )
}), }),
}) })
@@ -1,15 +1,16 @@
import { Effect } from "effect" import { Effect } from "effect"
import { PluginV2 } from "../../plugin" import { define } from "@opencode-ai/plugin/v2/effect"
export const TogetherAIPlugin = PluginV2.define({ export const TogetherAIPlugin = define({
id: PluginV2.ID.make("togetherai"), id: "togetherai",
effect: Effect.gen(function* () { effect: Effect.fn(function* (ctx) {
return { yield* ctx.aisdk.hook(
"aisdk.sdk": Effect.fn(function* (evt) { "sdk",
Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/togetherai") return if (evt.package !== "@ai-sdk/togetherai") return
const mod = yield* Effect.promise(() => import("@ai-sdk/togetherai")) const mod = yield* Effect.promise(() => import("@ai-sdk/togetherai"))
evt.sdk = mod.createTogetherAI(evt.options) evt.sdk = mod.createTogetherAI(evt.options)
}), }),
} )
}), }),
}) })
+8 -7
View File
@@ -1,15 +1,16 @@
import { Effect } from "effect" import { Effect } from "effect"
import { PluginV2 } from "../../plugin" import { define } from "@opencode-ai/plugin/v2/effect"
export const VenicePlugin = PluginV2.define({ export const VenicePlugin = define({
id: PluginV2.ID.make("venice"), id: "venice",
effect: Effect.gen(function* () { effect: Effect.fn(function* (ctx) {
return { yield* ctx.aisdk.hook(
"aisdk.sdk": Effect.fn(function* (evt) { "sdk",
Effect.fn(function* (evt) {
if (evt.package !== "venice-ai-sdk-provider") return if (evt.package !== "venice-ai-sdk-provider") return
const mod = yield* Effect.promise(() => import("venice-ai-sdk-provider")) const mod = yield* Effect.promise(() => import("venice-ai-sdk-provider"))
evt.sdk = mod.createVenice(evt.options) evt.sdk = mod.createVenice(evt.options)
}), }),
} )
}), }),
}) })
+11 -8
View File
@@ -1,11 +1,11 @@
import { Effect } from "effect" import { Effect } from "effect"
import { PluginV2 } from "../../plugin" import { define } from "@opencode-ai/plugin/v2/effect"
export const VercelPlugin = PluginV2.define({ export const VercelPlugin = define({
id: PluginV2.ID.make("vercel"), id: "vercel",
effect: Effect.gen(function* () { effect: Effect.fn(function* (ctx) {
return { yield* ctx.catalog.transform(
"catalog.transform": Effect.fn(function* (evt) { Effect.fn(function* (evt) {
for (const item of evt.provider.list()) { for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/vercel") continue if (item.provider.api.package !== "@ai-sdk/vercel") continue
@@ -15,11 +15,14 @@ export const VercelPlugin = PluginV2.define({
}) })
} }
}), }),
"aisdk.sdk": Effect.fn(function* (evt) { )
yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/vercel") return if (evt.package !== "@ai-sdk/vercel") return
const mod = yield* Effect.promise(() => import("@ai-sdk/vercel")) const mod = yield* Effect.promise(() => import("@ai-sdk/vercel"))
evt.sdk = mod.createVercel(evt.options) evt.sdk = mod.createVercel(evt.options)
}), }),
} )
}), }),
}) })
+12 -8
View File
@@ -1,20 +1,24 @@
import { Effect } from "effect" import { Effect } from "effect"
import { PluginV2 } from "../../plugin" import { define } from "@opencode-ai/plugin/v2/effect"
import { ProviderV2 } from "../../provider" import { ProviderV2 } from "../../provider"
export const XAIPlugin = PluginV2.define({ export const XAIPlugin = define({
id: PluginV2.ID.make("xai"), id: "xai",
effect: Effect.gen(function* () { effect: Effect.fn(function* (ctx) {
return { yield* ctx.aisdk.hook(
"aisdk.sdk": Effect.fn(function* (evt) { "sdk",
Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/xai") return if (evt.package !== "@ai-sdk/xai") return
const mod = yield* Effect.promise(() => import("@ai-sdk/xai")) const mod = yield* Effect.promise(() => import("@ai-sdk/xai"))
evt.sdk = mod.createXai(evt.options) evt.sdk = mod.createXai(evt.options)
}), }),
"aisdk.language": Effect.fn(function* (evt) { )
yield* ctx.aisdk.hook(
"language",
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("xai")) return if (evt.model.providerID !== ProviderV2.ID.make("xai")) return
evt.language = evt.sdk.responses(evt.model.api.id) evt.language = evt.sdk.responses(evt.model.api.id)
}), }),
} )
}), }),
}) })
+7 -7
View File
@@ -1,11 +1,11 @@
import { Effect } from "effect" import { Effect } from "effect"
import { PluginV2 } from "../../plugin" import { define } from "@opencode-ai/plugin/v2/effect"
export const ZenmuxPlugin = PluginV2.define({ export const ZenmuxPlugin = define({
id: PluginV2.ID.make("zenmux"), id: "zenmux",
effect: Effect.gen(function* () { effect: Effect.fn(function* (ctx) {
return { yield* ctx.catalog.transform(
"catalog.transform": Effect.fn(function* (evt) { Effect.fn(function* (evt) {
for (const item of evt.provider.list()) { for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
@@ -16,6 +16,6 @@ export const ZenmuxPlugin = PluginV2.define({
}) })
} }
}), }),
} )
}), }),
}) })
+6 -9
View File
@@ -2,22 +2,19 @@
export * as SkillPlugin from "./skill" export * as SkillPlugin from "./skill"
import { define } from "@opencode-ai/plugin/v2/effect"
import { Effect } from "effect" import { Effect } from "effect"
import { PluginV2 } from "../plugin"
import { AbsolutePath } from "../schema" import { AbsolutePath } from "../schema"
import { SkillV2 } from "../skill" import { SkillV2 } from "../skill"
import customizeOpencodeContent from "./skill/customize-opencode.md" with { type: "text" } import customizeOpencodeContent from "./skill/customize-opencode.md" with { type: "text" }
export const CustomizeOpencodeContent = customizeOpencodeContent export const CustomizeOpencodeContent = customizeOpencodeContent
export const Plugin = PluginV2.define({ export const Plugin = define({
id: PluginV2.ID.make("skill"), id: "skill",
effect: Effect.gen(function* () { effect: Effect.fn(function* (ctx) {
const skill = yield* SkillV2.Service yield* ctx.skill.transform((draft) => {
const transform = yield* skill.transform() draft.source(
yield* transform((editor) => {
editor.source(
new SkillV2.EmbeddedSource({ new SkillV2.EmbeddedSource({
type: "embedded", type: "embedded",
skill: new SkillV2.Info({ skill: new SkillV2.Info({
+1 -1
View File
@@ -9,7 +9,7 @@ export class AppProcessError extends Schema.TaggedErrorClass<AppProcessError>()(
command: Schema.String, command: Schema.String,
exitCode: Schema.optional(Schema.Number), exitCode: Schema.optional(Schema.Number),
stderr: Schema.optional(Schema.String), stderr: Schema.optional(Schema.String),
cause: Schema.optional(Schema.Defect), cause: Schema.optional(Schema.Defect()),
}) {} }) {}
export interface RunOptions { export interface RunOptions {
+6 -1
View File
@@ -1,7 +1,7 @@
export * as ProviderV2 from "./provider" export * as ProviderV2 from "./provider"
import { withStatics } from "./schema" import { withStatics } from "./schema"
import { Schema } from "effect" import { Schema, Types } from "effect"
export const ID = Schema.String.pipe( export const ID = Schema.String.pipe(
Schema.brand("ProviderV2.ID"), Schema.brand("ProviderV2.ID"),
@@ -37,6 +37,9 @@ export const Native = Schema.Struct({
export const Api = Schema.Union([AISDK, Native]).pipe(Schema.toTaggedUnion("type")) export const Api = Schema.Union([AISDK, Native]).pipe(Schema.toTaggedUnion("type"))
export type Api = typeof Api.Type export type Api = typeof Api.Type
export type MutableApi<T extends Api = Api> = T extends Api
? Omit<Types.DeepMutable<T>, "settings"> & (undefined extends T["settings"] ? { settings?: any } : { settings: any })
: never
export const Request = Schema.Struct({ export const Request = Schema.Struct({
headers: Schema.Record(Schema.String, Schema.String), headers: Schema.Record(Schema.String, Schema.String),
@@ -66,3 +69,5 @@ export class Info extends Schema.Class<Info>("ProviderV2.Info")({
}) })
} }
} }
export type MutableInfo = Omit<Types.DeepMutable<Info>, "api"> & { api: MutableApi }
+10 -11
View File
@@ -1,7 +1,6 @@
export * as Reference from "./reference" export * as Reference from "./reference"
import { Context, Effect, Layer, Schema, Scope } from "effect" import { Context, Effect, Layer, Schema, Scope, Types } from "effect"
import { castDraft } from "immer"
import { Global } from "./global" import { Global } from "./global"
import { EventV2 } from "./event" import { EventV2 } from "./event"
import { Repository } from "./repository" import { Repository } from "./repository"
@@ -40,17 +39,16 @@ export class Info extends Schema.Class<Info>("Reference.Info")({
}) {} }) {}
type Data = { type Data = {
sources: Map<string, Source> sources: Map<string, Types.DeepMutable<Source>>
} }
type Editor = { type Draft = {
add(name: string, source: Source): void add(name: string, source: Source): void
remove(name: string): void remove(name: string): void
list(): readonly [string, Source][] list(): readonly [string, Source][]
} }
export interface Interface { export interface Interface extends State.Transformable<Draft> {
readonly transform: State.Interface<Data, Editor>["transform"]
readonly list: () => Effect.Effect<Info[]> readonly list: () => Effect.Effect<Info[]>
} }
@@ -64,18 +62,18 @@ export const layer = Layer.effect(
const cache = yield* RepositoryCache.Service const cache = yield* RepositoryCache.Service
const scope = yield* Scope.Scope const scope = yield* Scope.Scope
const materialized = new Map<string, Info>() const materialized = new Map<string, Info>()
const state = State.create<Data, Editor>({ const state = State.create<Data, Draft>({
initial: () => ({ sources: new Map() }), initial: () => ({ sources: new Map() }),
editor: (draft) => ({ draft: (draft) => ({
add: (name, source) => draft.sources.set(name, castDraft(source)), add: (name, source) => draft.sources.set(name, source as Types.DeepMutable<Source>),
remove: (name) => draft.sources.delete(name), remove: (name) => draft.sources.delete(name),
list: () => Array.from(draft.sources.entries()) as [string, Source][], list: () => Array.from(draft.sources.entries()) as [string, Source][],
}), }),
finalize: (editor) => finalize: (draft) =>
Effect.gen(function* () { Effect.gen(function* () {
materialized.clear() materialized.clear()
const seen = new Map<string, string | undefined>() const seen = new Map<string, string | undefined>()
for (const [name, source] of editor.list()) { for (const [name, source] of draft.list()) {
if (source.type === "local") { if (source.type === "local") {
materialized.set( materialized.set(
name, name,
@@ -128,6 +126,7 @@ export const layer = Layer.effect(
return Service.of({ return Service.of({
transform: state.transform, transform: state.transform,
rebuild: state.rebuild,
list: Effect.fn("Reference.list")(function* () { list: Effect.fn("Reference.list")(function* () {
return Array.from(materialized.values()) return Array.from(materialized.values())
}), }),
+1 -1
View File
@@ -42,7 +42,7 @@ type RawMatchData = (typeof RawMatch.Type)["data"]
export class Error extends Schema.TaggedErrorClass<Error>()("Ripgrep.Error", { export class Error extends Schema.TaggedErrorClass<Error>()("Ripgrep.Error", {
message: Schema.String, message: Schema.String,
cause: Schema.optional(Schema.Defect), cause: Schema.optional(Schema.Defect()),
}) {} }) {}
export class InvalidPatternError extends Schema.TaggedErrorClass<InvalidPatternError>()("Ripgrep.InvalidPatternError", { export class InvalidPatternError extends Schema.TaggedErrorClass<InvalidPatternError>()("Ripgrep.InvalidPatternError", {
+5 -7
View File
@@ -33,11 +33,7 @@ export class UnsupportedApiError extends Schema.TaggedErrorClass<UnsupportedApiE
}, },
) {} ) {}
export type Error = export type Error = ModelNotSelectedError | UnsupportedApiError
| Catalog.ProviderNotFoundError
| Catalog.ModelNotFoundError
| ModelNotSelectedError
| UnsupportedApiError
export interface Interface { export interface Interface {
readonly resolve: (session: SessionSchema.Info) => Effect.Effect<Model, Error> readonly resolve: (session: SessionSchema.Info) => Effect.Effect<Model, Error>
@@ -149,10 +145,12 @@ export const locationLayer = Layer.effect(
resolve: Effect.fn("SessionRunnerModel.resolve")(function* (session) { resolve: Effect.fn("SessionRunnerModel.resolve")(function* (session) {
// Location plugins populate and filter the catalog asynchronously during layer startup. // Location plugins populate and filter the catalog asynchronously during layer startup.
yield* boot.wait() yield* boot.wait()
const defaultModel = session.model ? undefined : yield* catalog.model.default()
const selected = session.model const selected = session.model
? yield* catalog.model.get(session.model.providerID, session.model.id) ? yield* catalog.model.get(session.model.providerID, session.model.id)
: (Option.getOrUndefined((yield* catalog.model.default()).pipe(Option.filter(supported))) ?? : defaultModel && supported(defaultModel)
(yield* catalog.model.available()).find(supported)) ? defaultModel
: (yield* catalog.model.available()).find(supported)
if (!selected) return yield* new ModelNotSelectedError({ sessionID: session.id }) if (!selected) return yield* new ModelNotSelectedError({ sessionID: session.id })
const connection = yield* integrations.connection.forIntegration(Integration.ID.make(selected.providerID)) const connection = yield* integrations.connection.forIntegration(Integration.ID.make(selected.providerID))
return yield* fromCatalogModel( return yield* fromCatalogModel(
+8 -9
View File
@@ -1,8 +1,7 @@
export * as SkillV2 from "./skill" export * as SkillV2 from "./skill"
import path from "path" import path from "path"
import { Context, Effect, Layer, Schema } from "effect" import { Context, Effect, Layer, Schema, Types } from "effect"
import { castDraft } from "immer"
import { AgentV2 } from "./agent" import { AgentV2 } from "./agent"
import { ConfigMarkdown } from "./config/markdown" import { ConfigMarkdown } from "./config/markdown"
import { FSUtil } from "./fs-util" import { FSUtil } from "./fs-util"
@@ -65,16 +64,15 @@ const Frontmatter = Schema.Struct({
const decodeFrontmatter = Schema.decodeUnknownOption(Frontmatter) const decodeFrontmatter = Schema.decodeUnknownOption(Frontmatter)
export type Data = { export type Data = {
sources: Source[] sources: Types.DeepMutable<Source>[]
} }
export type Editor = { export type Draft = {
source: (source: Source) => void source: (source: Source) => void
list: () => readonly Source[] list: () => readonly Source[]
} }
export interface Interface { export interface Interface extends State.Transformable<Draft> {
readonly transform: State.Interface<Data, Editor>["transform"]
readonly sources: () => Effect.Effect<Source[]> readonly sources: () => Effect.Effect<Source[]>
readonly list: () => Effect.Effect<Info[]> readonly list: () => Effect.Effect<Info[]>
} }
@@ -87,12 +85,12 @@ export const layer = Layer.effect(
const discovery = yield* SkillDiscovery.Service const discovery = yield* SkillDiscovery.Service
const fs = yield* FSUtil.Service const fs = yield* FSUtil.Service
const state = State.create<Data, Editor>({ const state = State.create<Data, Draft>({
initial: () => ({ sources: [] }), initial: () => ({ sources: [] }),
editor: (draft) => ({ draft: (draft) => ({
source: (source) => { source: (source) => {
if (draft.sources.some((item) => Source.equals(item, source))) return if (draft.sources.some((item) => Source.equals(item, source))) return
draft.sources.push(castDraft(source)) draft.sources.push(source as Types.DeepMutable<Source>)
}, },
list: () => draft.sources as Source[], list: () => draft.sources as Source[],
}), }),
@@ -150,6 +148,7 @@ export const layer = Layer.effect(
return Service.of({ return Service.of({
transform: state.transform, transform: state.transform,
rebuild: state.rebuild,
sources: Effect.fn("SkillV2.sources")(function* () { sources: Effect.fn("SkillV2.sources")(function* () {
return state.get().sources return state.get().sources
}), }),
+88 -72
View File
@@ -1,112 +1,128 @@
export * as State from "./state" export * as State from "./state"
import { Effect, Scope, Semaphore } from "effect" import { Context, Effect, Scope, Semaphore } from "effect"
import type { Draft, Objectish } from "immer"
/** /**
* A replayable transform applied to an editor during rebuild. * A replayable transform applied to a draft during rebuild.
* *
* Transforms are intentionally synchronous and mutation-shaped: domain editors * Domain drafts expose readable and writable state while preserving concise
* hide the draft representation while preserving concise plugin/config code. * plugin/config code. Transforms may perform Effects before returning.
*/ */
export type Transform<Editor> = (editor: Editor) => void type TransformCallback<DraftApi> = (draft: DraftApi) => Effect.Effect<void> | void
export type MakeEditor<State extends Objectish, Editor> = (draft: Draft<State>) => Editor export type MakeDraft<State, DraftApi> = (state: State) => DraftApi
export interface Options<State extends Objectish, Editor> { export interface Registration {
readonly dispose: Effect.Effect<void>
}
export type Transform<DraftApi> = (
transform: TransformCallback<DraftApi>,
) => Effect.Effect<Registration, never, Scope.Scope>
export type Rebuild = () => Effect.Effect<void>
export interface Transformable<DraftApi> {
readonly transform: Transform<DraftApi>
readonly rebuild: Rebuild
}
const CurrentBatch = Context.Reference<Set<Rebuild> | undefined>("@opencode/State/CurrentBatch", {
defaultValue: () => undefined,
})
export function batch<A, E, R>(effect: Effect.Effect<A, E, R>) {
return Effect.gen(function* () {
const current = yield* CurrentBatch
if (current) return yield* effect
const rebuilds = new Set<Rebuild>()
const result = yield* effect.pipe(Effect.provideService(CurrentBatch, rebuilds))
yield* Effect.forEach(rebuilds, (rebuild) => rebuild(), { discard: true })
return result
})
}
export interface Options<State, DraftApi> {
/** Creates the base value for initial state and every scoped-transform rebuild. */ /** Creates the base value for initial state and every scoped-transform rebuild. */
readonly initial: () => State readonly initial: () => State
/** Wraps the mutable draft in a domain-specific editor. */ /** Wraps mutable state in a domain-specific draft API. */
readonly editor: MakeEditor<State, Editor> readonly draft: MakeDraft<State, DraftApi>
/** /** Runs after all active transforms and before the rebuilt state becomes visible. */
* Completes every committed edit. readonly finalize?: (draft: DraftApi) => Effect.Effect<void>
*
* For rebuilds, this runs after all active transforms have been replayed and
* before the rebuilt state becomes visible. For direct updates, this runs
* after the current state has already been edited. The optional reason is
* caller-defined metadata for exceptional update origins.
*/
readonly finalize?: (editor: Editor, reason?: string) => Effect.Effect<void>
} }
export interface Interface<State extends Objectish, Editor> { export interface Interface<State, DraftApi> extends Transformable<DraftApi> {
readonly get: () => State readonly get: () => State
/** /**
* Registers a scoped transform slot and returns the slot updater. * Registers and applies a scoped transform. Closing the owning Scope removes
* * the transform and rebuilds the materialized state.
* Acquiring the slot has no visible effect until the returned updater is
* called. Each updater call replaces that slot's transform, then rebuilds the
* materialized state from `initial()` by replaying all active transforms in
* registration order. Closing the owning Scope removes the slot and rebuilds.
*/ */
readonly transform: () => Effect.Effect<(transform: Transform<Editor>) => Effect.Effect<void>, never, Scope.Scope>
/** Registers and applies a replayable transform in the current Scope. */
readonly update: (update: Transform<Editor>) => Effect.Effect<void, never, Scope.Scope>
/**
* Mutates the current materialized state directly, once.
*
* This is not replayable transform state: a later rebuild starts again
* from `initial()` plus active transforms, so direct edits must be reserved
* for current-state adjustments that are intentionally outside the transform
* fold.
*/
readonly mutate: (update: (editor: Editor) => Effect.Effect<void>, reason?: string) => Effect.Effect<void>
} }
export function create<State extends Objectish, Editor>(options: Options<State, Editor>): Interface<State, Editor> { export function create<State, DraftApi>(options: Options<State, DraftApi>): Interface<State, DraftApi> {
let state = options.initial() let state = options.initial()
let transforms: { update: Transform<Editor> }[] = [] let transforms: { run: TransformCallback<DraftApi> }[] = []
const semaphore = Semaphore.makeUnsafe(1) const semaphore = Semaphore.makeUnsafe(1)
const commit = Effect.fn("State.commit")(function* (next: State, reason?: string) { const commit = Effect.fn("State.commit")(function* (next: State) {
const api = options.editor(next as Draft<State>) const api = options.draft(next)
if (options.finalize) yield* options.finalize(api, reason) if (options.finalize) yield* options.finalize(api)
state = next state = next
}) })
const rebuild = Effect.fnUntraced(function* () { const apply = (transform: TransformCallback<DraftApi>, draft: DraftApi) =>
Effect.suspend(() => {
const result = transform(draft)
return Effect.isEffect(result) ? Effect.asVoid(result).pipe(Effect.orDie) : Effect.void
})
const materialize = Effect.fnUntraced(function* () {
const next = options.initial() const next = options.initial()
const api = options.editor(next as Draft<State>) const api = options.draft(next)
for (const transform of transforms) for (const transform of transforms) yield* apply(transform.run, api).pipe(Effect.withSpan("State.rebuild.update"))
yield* Effect.sync(() => transform.update(api)).pipe(Effect.withSpan("State.rebuild.update", {}))
yield* commit(next) yield* commit(next)
}) })
const result: Interface<State, Editor> = { const rebuild = () => semaphore.withPermit(materialize())
const result: Interface<State, DraftApi> = {
get: () => state, get: () => state,
transform: Effect.fn("State.transform")(function* () { transform: Effect.fn("State.transform")(function* (update) {
const scope = yield* Scope.Scope const scope = yield* Scope.Scope
return yield* Effect.uninterruptible( return yield* Effect.uninterruptible(
Effect.gen(function* () { Effect.gen(function* () {
const transform = { update: (_editor: Editor) => {} } const transform = { run: update }
transforms = [...transforms, transform] let active = true
yield* Scope.addFinalizer( const dispose = Effect.uninterruptible(
scope,
semaphore.withPermit( semaphore.withPermit(
Effect.sync(() => { Effect.suspend(() => {
if (!active) return Effect.void
active = false
transforms = transforms.filter((item) => item !== transform) transforms = transforms.filter((item) => item !== transform)
}).pipe(Effect.andThen(rebuild())), return Effect.gen(function* () {
const batch = yield* CurrentBatch
if (batch) {
batch.add(rebuild)
return
}
yield* materialize()
})
}),
), ),
) )
return (update: Transform<Editor>) => yield* semaphore.withPermit(
Effect.uninterruptible( Effect.sync(() => {
semaphore.withPermit( transforms = [...transforms, transform]
Effect.sync(() => { }),
transform.update = update )
}).pipe(Effect.andThen(rebuild())), yield* Scope.addFinalizer(scope, dispose)
), const batch = yield* CurrentBatch
) if (batch) batch.add(rebuild)
else yield* rebuild()
return { dispose }
}), }),
) )
}), }),
update: Effect.fn("State.update")(function* (update) { rebuild,
const transform = yield* result.transform()
yield* transform(update)
}),
mutate: Effect.fn("State.mutate")(function* (update, reason) {
const api = options.editor(state as Draft<State>)
yield* update(api)
if (options.finalize) yield* options.finalize(api, reason)
}, semaphore.withPermit),
} }
return result return result
} }
+1 -1
View File
@@ -28,7 +28,7 @@ export interface BoundResult {
export class StorageError extends Schema.TaggedErrorClass<StorageError>()("ToolOutputStore.StorageError", { export class StorageError extends Schema.TaggedErrorClass<StorageError>()("ToolOutputStore.StorageError", {
operation: Schema.Literals(["encode", "write"]), operation: Schema.Literals(["encode", "write"]),
cause: Schema.Defect, cause: Schema.Defect(),
}) {} }) {}
export type Error = StorageError export type Error = StorageError
+5 -9
View File
@@ -1,7 +1,6 @@
export * as ApplicationTools from "./application-tools" export * as ApplicationTools from "./application-tools"
import { Context, Effect, Layer, Scope } from "effect" import { Context, Effect, Layer, Scope } from "effect"
import { enableMapSet } from "immer"
import { State } from "../state" import { State } from "../state"
import { Tool } from "./tool" import { Tool } from "./tool"
@@ -9,7 +8,7 @@ type Data = {
readonly entries: Map<string, Entry> readonly entries: Map<string, Entry>
} }
type Editor = { type Draft = {
readonly set: (name: string, entry: Entry) => void readonly set: (name: string, entry: Entry) => void
} }
@@ -27,14 +26,12 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/ApplicationTools") {} export class Service extends Context.Service<Service, Interface>()("@opencode/ApplicationTools") {}
enableMapSet()
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const state = State.create<Data, Editor>({ const state = State.create<Data, Draft>({
initial: () => ({ entries: new Map() }), initial: () => ({ entries: new Map() }),
editor: (draft) => ({ draft: (draft) => ({
set: (name, tool) => { set: (name, tool) => {
draft.entries.set(name, tool) draft.entries.set(name, tool)
}, },
@@ -47,9 +44,8 @@ export const layer = Layer.effect(
if (entries.length === 0) return if (entries.length === 0) return
yield* Effect.forEach(entries, ([name]) => Tool.validateName(name), { discard: true }) yield* Effect.forEach(entries, ([name]) => Tool.validateName(name), { discard: true })
const registrations = entries.map(([name, tool]) => [name, { identity: {}, tool }] as const) const registrations = entries.map(([name, tool]) => [name, { identity: {}, tool }] as const)
const transform = yield* state.transform() yield* state.transform((draft) => {
yield* transform((editor) => { for (const [name, entry] of registrations) draft.set(name, entry)
for (const [name, entry] of registrations) editor.set(name, entry)
}) })
}), }),
entries: () => state.get().entries, entries: () => state.get().entries,
+1 -1
View File
@@ -24,7 +24,7 @@ export namespace EffectFlock {
class ReleaseError extends Schema.TaggedErrorClass<ReleaseError>()("ReleaseError", { class ReleaseError extends Schema.TaggedErrorClass<ReleaseError>()("ReleaseError", {
detail: Schema.String, detail: Schema.String,
cause: Schema.optional(Schema.Defect), cause: Schema.optional(Schema.Defect()),
}) { }) {
override get message() { override get message() {
return this.detail return this.detail
+19 -24
View File
@@ -6,6 +6,7 @@ import { AgentPlugin } from "@opencode-ai/core/plugin/agent"
import { AbsolutePath } from "@opencode-ai/core/schema" import { AbsolutePath } from "@opencode-ai/core/schema"
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"
const it = testEffect(AgentV2.locationLayer) const it = testEffect(AgentV2.locationLayer)
@@ -23,9 +24,7 @@ describe("AgentV2", () => {
Effect.gen(function* () { Effect.gen(function* () {
const agent = yield* AgentV2.Service const agent = yield* AgentV2.Service
const id = AgentV2.ID.make("reviewer") const id = AgentV2.ID.make("reviewer")
const transform = yield* agent.transform() yield* agent.transform((editor) =>
yield* transform((editor) =>
editor.update(id, (info) => { editor.update(id, (info) => {
info.description = "Reviews code" info.description = "Reviews code"
info.mode = "subagent" info.mode = "subagent"
@@ -41,19 +40,17 @@ describe("AgentV2", () => {
Effect.gen(function* () { Effect.gen(function* () {
const agent = yield* AgentV2.Service const agent = yield* AgentV2.Service
const id = AgentV2.ID.make("reviewer") const id = AgentV2.ID.make("reviewer")
const transform = yield* agent.transform() let description = "Old description"
let hidden = true
yield* transform((editor) => yield* agent.transform((editor) =>
editor.update(id, (info) => { editor.update(id, (info) => {
info.description = "Old description" info.description = description
info.hidden = true info.hidden = hidden
}),
)
yield* transform((editor) =>
editor.update(id, (info) => {
info.description = "New description"
}), }),
) )
description = "New description"
hidden = false
yield* agent.rebuild()
expect(yield* agent.get(id)).toMatchObject({ description: "New description", hidden: false }) expect(yield* agent.get(id)).toMatchObject({ description: "New description", hidden: false })
}), }),
@@ -64,9 +61,7 @@ describe("AgentV2", () => {
const agent = yield* AgentV2.Service const agent = yield* AgentV2.Service
const id = AgentV2.ID.make("scoped") const id = AgentV2.ID.make("scoped")
const scope = yield* Scope.make() const scope = yield* Scope.make()
const transform = yield* agent.transform().pipe(Scope.provide(scope)) yield* agent.transform((editor) => editor.update(id, () => {})).pipe(Scope.provide(scope))
yield* transform((editor) => editor.update(id, () => {}))
expect(yield* agent.get(id)).toBeDefined() expect(yield* agent.get(id)).toBeDefined()
yield* Scope.close(scope, Exit.void) yield* Scope.close(scope, Exit.void)
@@ -79,7 +74,7 @@ describe("AgentV2", () => {
const agent = yield* AgentV2.Service const agent = yield* AgentV2.Service
const id = AgentV2.ID.make("build") const id = AgentV2.ID.make("build")
yield* agent.update((editor) => yield* agent.transform((editor) =>
editor.update(id, (info) => { editor.update(id, (info) => {
info.mode = "primary" info.mode = "primary"
info.hidden = true info.hidden = true
@@ -95,10 +90,10 @@ describe("AgentV2", () => {
const agent = yield* AgentV2.Service const agent = yield* AgentV2.Service
const id = AgentV2.ID.make("custom") const id = AgentV2.ID.make("custom")
yield* agent.update((editor) => editor.update(id, () => {})) yield* agent.transform((editor) => editor.update(id, () => {}))
expect(yield* agent.get(id)).toEqual(AgentV2.Info.empty(id)) expect(yield* agent.get(id)).toEqual(AgentV2.Info.empty(id))
yield* agent.update((editor) => editor.remove(id)) yield* agent.transform((editor) => editor.remove(id))
expect(yield* agent.get(id)).toBeUndefined() expect(yield* agent.get(id)).toBeUndefined()
}), }),
) )
@@ -106,11 +101,11 @@ describe("AgentV2", () => {
it.effect("does not ambiently opt built-in agents into bash", () => it.effect("does not ambiently opt built-in agents into bash", () =>
Effect.gen(function* () { Effect.gen(function* () {
const agent = yield* AgentV2.Service const agent = yield* AgentV2.Service
yield* AgentPlugin.Plugin.effect.pipe( yield* AgentPlugin.Plugin.effect(
Effect.provideService( host({
Location.Service, agent: agentHost(agent),
Location.Service.of(location({ directory: AbsolutePath.make("/project") })), location: location({ directory: AbsolutePath.make("/project") }),
), }),
) )
const agents = yield* agent.all() const agents = yield* agent.all()
+38 -148
View File
@@ -1,18 +1,17 @@
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import { DateTime, Effect, Fiber, Layer, Option, Stream } from "effect" import { Effect, Fiber, Layer, Stream } from "effect"
import { Catalog } from "@opencode-ai/core/catalog" import { Catalog } from "@opencode-ai/core/catalog"
import { Integration } from "@opencode-ai/core/integration" import { Integration } from "@opencode-ai/core/integration"
import { Credential } from "@opencode-ai/core/credential" import { Credential } from "@opencode-ai/core/credential"
import { EventV2 } from "@opencode-ai/core/event" import { EventV2 } from "@opencode-ai/core/event"
import { Location } from "@opencode-ai/core/location" import { Location } from "@opencode-ai/core/location"
import { ModelV2 } from "@opencode-ai/core/model" import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { Policy } from "@opencode-ai/core/policy" import { Policy } from "@opencode-ai/core/policy"
import { Project } from "@opencode-ai/core/project"
import { ProviderV2 } from "@opencode-ai/core/provider" import { ProviderV2 } from "@opencode-ai/core/provider"
import { AbsolutePath } from "@opencode-ai/core/schema" import { AbsolutePath } from "@opencode-ai/core/schema"
import { location } from "./fixture/location" import { location } from "./fixture/location"
import { testEffect } from "./lib/effect" import { testEffect } from "./lib/effect"
import { required } from "./plugin/provider-helper"
const locationLayer = Layer.succeed( const locationLayer = Layer.succeed(
Location.Service, Location.Service,
@@ -41,7 +40,7 @@ describe("CatalogV2", () => {
.pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) .pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
yield* Effect.yieldNow yield* Effect.yieldNow
yield* (yield* catalog.transform())((editor) => editor.provider.update(ProviderV2.ID.make("test"), () => {})) yield* catalog.transform((editor) => editor.provider.update(ProviderV2.ID.make("test"), () => {}))
expect((yield* Fiber.join(updated)).length).toBe(1) expect((yield* Fiber.join(updated)).length).toBe(1)
}), }),
@@ -76,14 +75,13 @@ describe("CatalogV2", () => {
return Effect.gen(function* () { return Effect.gen(function* () {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
const transform = yield* catalog.transform() yield* catalog.transform((editor) => editor.provider.update(ProviderV2.ID.make("test"), () => {}))
yield* transform((editor) => editor.provider.update(ProviderV2.ID.make("test"), () => {}))
expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([ProviderV2.ID.make("test")]) expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([ProviderV2.ID.make("test")])
expect((yield* catalog.provider.get(ProviderV2.ID.make("test"))).request.body).toEqual({}) expect(required(yield* catalog.provider.get(ProviderV2.ID.make("test"))).request.body).toEqual({})
active = second active = second
expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([ProviderV2.ID.make("test")]) expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([ProviderV2.ID.make("test")])
expect((yield* catalog.provider.get(ProviderV2.ID.make("test"))).request.body).toEqual({}) expect(required(yield* catalog.provider.get(ProviderV2.ID.make("test"))).request.body).toEqual({})
}).pipe(Effect.provide(layer)) }).pipe(Effect.provide(layer))
}) })
@@ -99,13 +97,13 @@ describe("CatalogV2", () => {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
const integrations = yield* Integration.Service const integrations = yield* Integration.Service
const providerID = ProviderV2.ID.make("test") const providerID = ProviderV2.ID.make("test")
yield* integrations.update((editor) => yield* integrations.transform((editor) =>
editor.method.update({ editor.method.update({
integrationID: Integration.ID.make(providerID), integrationID: Integration.ID.make(providerID),
method: { type: "env", names: ["CATALOG_TEST_API_KEY"] }, method: { type: "env", names: ["CATALOG_TEST_API_KEY"] },
}), }),
) )
yield* (yield* catalog.transform())((editor) => editor.provider.update(providerID, () => {})) yield* catalog.transform((editor) => editor.provider.update(providerID, () => {}))
expect((yield* catalog.provider.available()).map((provider) => provider.id)).toContain(providerID) expect((yield* catalog.provider.available()).map((provider) => provider.id)).toContain(providerID)
}), }),
@@ -121,9 +119,7 @@ describe("CatalogV2", () => {
Effect.gen(function* () { Effect.gen(function* () {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
const providerID = ProviderV2.ID.make("test") const providerID = ProviderV2.ID.make("test")
const transform = yield* catalog.transform() yield* catalog.transform((catalog) =>
yield* transform((catalog) =>
catalog.provider.update(providerID, (provider) => { catalog.provider.update(providerID, (provider) => {
provider.api = { provider.api = {
type: "aisdk", type: "aisdk",
@@ -134,7 +130,7 @@ describe("CatalogV2", () => {
}), }),
) )
expect((yield* catalog.provider.get(providerID)).api).toEqual({ expect(required(yield* catalog.provider.get(providerID)).api).toEqual({
type: "aisdk", type: "aisdk",
package: "@ai-sdk/openai-compatible", package: "@ai-sdk/openai-compatible",
url: "https://override.example.com", url: "https://override.example.com",
@@ -147,9 +143,7 @@ describe("CatalogV2", () => {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
const providerID = ProviderV2.ID.make("test") const providerID = ProviderV2.ID.make("test")
const modelID = ModelV2.ID.make("model") const modelID = ModelV2.ID.make("model")
const transform = yield* catalog.transform() yield* catalog.transform((catalog) => {
yield* transform((catalog) => {
catalog.provider.update(providerID, (provider) => { catalog.provider.update(providerID, (provider) => {
provider.api = { provider.api = {
type: "aisdk", type: "aisdk",
@@ -168,7 +162,7 @@ describe("CatalogV2", () => {
}) })
}) })
expect((yield* catalog.model.get(providerID, modelID)).api).toEqual({ expect(required(yield* catalog.model.get(providerID, modelID)).api).toEqual({
id: modelID, id: modelID,
type: "aisdk", type: "aisdk",
package: "@ai-sdk/openai-compatible", package: "@ai-sdk/openai-compatible",
@@ -183,9 +177,7 @@ describe("CatalogV2", () => {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
const providerID = ProviderV2.ID.make("test") const providerID = ProviderV2.ID.make("test")
const modelID = ModelV2.ID.make("model") const modelID = ModelV2.ID.make("model")
const transform = yield* catalog.transform() yield* catalog.transform((catalog) => {
yield* transform((catalog) => {
catalog.provider.update(providerID, (provider) => { catalog.provider.update(providerID, (provider) => {
provider.api = { provider.api = {
type: "aisdk", type: "aisdk",
@@ -196,7 +188,7 @@ describe("CatalogV2", () => {
catalog.model.update(providerID, modelID, () => {}) catalog.model.update(providerID, modelID, () => {})
}) })
expect((yield* catalog.model.get(providerID, modelID)).api).toEqual({ expect(required(yield* catalog.model.get(providerID, modelID)).api).toEqual({
id: modelID, id: modelID,
type: "aisdk", type: "aisdk",
package: "@ai-sdk/openai-compatible", package: "@ai-sdk/openai-compatible",
@@ -205,106 +197,12 @@ describe("CatalogV2", () => {
}), }),
) )
it.effect("runs catalog transform hooks after baseURL is normalized", () =>
Effect.gen(function* () {
const catalog = yield* Catalog.Service
const plugin = yield* PluginV2.Service
const providerID = ProviderV2.ID.make("test")
const seen: unknown[] = []
const transform = yield* catalog.transform()
yield* plugin.add({
id: PluginV2.ID.make("test"),
effect: Effect.succeed({
"catalog.transform": (evt) =>
Effect.sync(() => {
const item = evt.provider.get(providerID)
if (!item) return
seen.push(item.provider.api.type)
if (item?.provider.api.type === "aisdk") seen.push(item.provider.api.url)
seen.push(item?.provider.request.body.baseURL)
}),
}),
})
yield* transform((catalog) =>
catalog.provider.update(providerID, (provider) => {
provider.api = { type: "aisdk", package: "@ai-sdk/openai-compatible" }
provider.request.body.baseURL = "https://provider.example.com"
}),
)
expect(seen).toEqual(["aisdk", "https://provider.example.com", undefined])
}),
)
it.effect("runs catalog transform when a plugin is added", () =>
Effect.gen(function* () {
const catalog = yield* Catalog.Service
const plugin = yield* PluginV2.Service
const providerID = ProviderV2.ID.make("test")
const transform = yield* catalog.transform()
yield* transform((catalog) =>
catalog.provider.update(providerID, (provider) => {
provider.name = "Before"
}),
)
yield* plugin.add({
id: PluginV2.ID.make("test-transform"),
effect: Effect.succeed({
"catalog.transform": (evt) =>
Effect.sync(() =>
evt.provider.update(providerID, (provider) => {
provider.name = "After"
}),
),
}),
})
yield* Effect.yieldNow
expect((yield* catalog.provider.get(providerID)).name).toBe("After")
}),
)
it.effect("ignores plugin additions from another location", () =>
Effect.gen(function* () {
const events = yield* EventV2.Service
const plugin = yield* PluginV2.Service
let invoked = 0
yield* plugin.add({
id: PluginV2.ID.make("test-transform"),
effect: Effect.succeed({
"catalog.transform": () => Effect.sync(() => invoked++),
}),
})
yield* Effect.yieldNow
expect(invoked).toBe(1)
yield* events.publish(
PluginV2.Event.Added,
{ id: PluginV2.ID.make("test-transform") },
{
location: new Location.Info({
directory: AbsolutePath.make("other"),
project: { id: Project.ID.global, directory: AbsolutePath.make("other") },
}),
},
)
yield* Effect.yieldNow
expect(invoked).toBe(1)
}),
)
it.effect("resolves provider and model request merges", () => it.effect("resolves provider and model request merges", () =>
Effect.gen(function* () { Effect.gen(function* () {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
const providerID = ProviderV2.ID.make("test") const providerID = ProviderV2.ID.make("test")
const modelID = ModelV2.ID.make("model") const modelID = ModelV2.ID.make("model")
const transform = yield* catalog.transform() yield* catalog.transform((catalog) => {
yield* transform((catalog) => {
catalog.provider.update(providerID, (provider) => { catalog.provider.update(providerID, (provider) => {
provider.request.headers.provider = "provider" provider.request.headers.provider = "provider"
provider.request.headers.shared = "provider" provider.request.headers.shared = "provider"
@@ -321,7 +219,7 @@ describe("CatalogV2", () => {
}) })
}) })
const model = yield* catalog.model.get(providerID, modelID) const model = required(yield* catalog.model.get(providerID, modelID))
expect(model.request.headers).toEqual({ provider: "provider", shared: "model", model: "model" }) expect(model.request.headers).toEqual({ provider: "provider", shared: "model", model: "model" })
expect(model.request.body).toEqual({ provider: true, model: true, request: true }) expect(model.request.body).toEqual({ provider: true, model: true, request: true })
expect(model.request.options).toEqual({ shared: "model", model: true }) expect(model.request.options).toEqual({ shared: "model", model: true })
@@ -332,19 +230,17 @@ describe("CatalogV2", () => {
Effect.gen(function* () { Effect.gen(function* () {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
const providerID = ProviderV2.ID.make("test") const providerID = ProviderV2.ID.make("test")
const transform = yield* catalog.transform() yield* catalog.transform((catalog) => {
yield* transform((catalog) => {
catalog.provider.update(providerID, () => {}) catalog.provider.update(providerID, () => {})
catalog.model.update(providerID, ModelV2.ID.make("old"), (model) => { catalog.model.update(providerID, ModelV2.ID.make("old"), (model) => {
model.time.released = DateTime.makeUnsafe(1000) model.time.released = 1000
}) })
catalog.model.update(providerID, ModelV2.ID.make("new"), (model) => { catalog.model.update(providerID, ModelV2.ID.make("new"), (model) => {
model.time.released = DateTime.makeUnsafe(2000) model.time.released = 2000
}) })
}) })
expect(Option.getOrUndefined(yield* catalog.model.default())?.id).toMatch("new") expect((yield* catalog.model.default())?.id).toMatch("new")
}), }),
) )
@@ -354,26 +250,26 @@ describe("CatalogV2", () => {
const providerID = ProviderV2.ID.make("test") const providerID = ProviderV2.ID.make("test")
const old = ModelV2.ID.make("old") const old = ModelV2.ID.make("old")
const newest = ModelV2.ID.make("new") const newest = ModelV2.ID.make("new")
const transform = yield* catalog.transform() const models = (catalog: Catalog.Draft) => {
const models = (catalog: Catalog.Editor) => {
catalog.provider.update(providerID, () => {}) catalog.provider.update(providerID, () => {})
catalog.model.update(providerID, old, (model) => { catalog.model.update(providerID, old, (model) => {
model.time.released = DateTime.makeUnsafe(1000) model.time.released = 1000
}) })
catalog.model.update(providerID, newest, (model) => { catalog.model.update(providerID, newest, (model) => {
model.time.released = DateTime.makeUnsafe(2000) model.time.released = 2000
}) })
} }
yield* transform((catalog) => { let configured = true
yield* catalog.transform((catalog) => {
models(catalog) models(catalog)
catalog.model.default.set(providerID, old) if (configured) catalog.model.default.set(providerID, old)
}) })
expect(Option.getOrUndefined(yield* catalog.model.default())?.id).toBe(old) expect((yield* catalog.model.default())?.id).toBe(old)
yield* transform(models) configured = false
expect(Option.getOrUndefined(yield* catalog.model.default())?.id).toBe(newest) yield* catalog.rebuild()
expect((yield* catalog.model.default())?.id).toBe(newest)
}), }),
) )
@@ -384,9 +280,7 @@ describe("CatalogV2", () => {
const enabledProvider = ProviderV2.ID.make("enabled") const enabledProvider = ProviderV2.ID.make("enabled")
const disabledModel = ModelV2.ID.make("configured") const disabledModel = ModelV2.ID.make("configured")
const fallbackModel = ModelV2.ID.make("fallback") const fallbackModel = ModelV2.ID.make("fallback")
const transform = yield* catalog.transform() yield* catalog.transform((catalog) => {
yield* transform((catalog) => {
catalog.provider.update(disabledProvider, (provider) => { catalog.provider.update(disabledProvider, (provider) => {
provider.disabled = true provider.disabled = true
}) })
@@ -396,7 +290,7 @@ describe("CatalogV2", () => {
catalog.model.default.set(disabledProvider, disabledModel) catalog.model.default.set(disabledProvider, disabledModel)
}) })
expect(Option.getOrUndefined(yield* catalog.model.default())).toMatchObject({ expect(yield* catalog.model.default()).toMatchObject({
providerID: enabledProvider, providerID: enabledProvider,
id: fallbackModel, id: fallbackModel,
}) })
@@ -407,25 +301,23 @@ describe("CatalogV2", () => {
Effect.gen(function* () { Effect.gen(function* () {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
const providerID = ProviderV2.ID.make("test") const providerID = ProviderV2.ID.make("test")
const transform = yield* catalog.transform() yield* catalog.transform((catalog) => {
yield* transform((catalog) => {
catalog.provider.update(providerID, () => {}) catalog.provider.update(providerID, () => {})
catalog.model.update(providerID, ModelV2.ID.make("cheap-large"), (model) => { catalog.model.update(providerID, ModelV2.ID.make("cheap-large"), (model) => {
model.capabilities.input = ["text"] model.capabilities.input = ["text"]
model.capabilities.output = ["text"] model.capabilities.output = ["text"]
model.cost = [{ input: 1, output: 1, cache: { read: 0, write: 0 } }] model.cost = [{ input: 1, output: 1, cache: { read: 0, write: 0 } }]
model.time.released = DateTime.makeUnsafe(Date.now()) model.time.released = Date.now()
}) })
catalog.model.update(providerID, ModelV2.ID.make("expensive-mini"), (model) => { catalog.model.update(providerID, ModelV2.ID.make("expensive-mini"), (model) => {
model.capabilities.input = ["text"] model.capabilities.input = ["text"]
model.capabilities.output = ["text"] model.capabilities.output = ["text"]
model.cost = [{ input: 10, output: 10, cache: { read: 0, write: 0 } }] model.cost = [{ input: 10, output: 10, cache: { read: 0, write: 0 } }]
model.time.released = DateTime.makeUnsafe(Date.now()) model.time.released = Date.now()
}) })
}) })
expect(Option.getOrUndefined(yield* catalog.model.small(providerID))?.id).toMatch("expensive-mini") expect((yield* catalog.model.small(providerID))?.id).toMatch("expensive-mini")
}), }),
) )
@@ -434,17 +326,15 @@ describe("CatalogV2", () => {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
const policy = yield* Policy.Service const policy = yield* Policy.Service
const providerID = ProviderV2.ID.make("blocked") const providerID = ProviderV2.ID.make("blocked")
const transform = yield* catalog.transform()
yield* policy.load([new Policy.Info({ effect: "deny", action: "provider.use", resource: "blocked" })]) yield* policy.load([new Policy.Info({ effect: "deny", action: "provider.use", resource: "blocked" })])
yield* transform((catalog) => { yield* catalog.transform((catalog) => {
catalog.provider.update(providerID, () => {}) catalog.provider.update(providerID, () => {})
catalog.model.update(providerID, ModelV2.ID.make("model"), () => {}) catalog.model.update(providerID, ModelV2.ID.make("model"), () => {})
}) })
expect(yield* catalog.provider.all()).toEqual([]) expect(yield* catalog.provider.all()).toEqual([])
expect(yield* catalog.model.all()).toEqual([]) expect(yield* catalog.model.all()).toEqual([])
expect(yield* catalog.provider.get(providerID).pipe(Effect.option)).toEqual(Option.none()) expect(yield* catalog.provider.get(providerID)).toBeUndefined()
}), }),
) )
}) })
+1 -2
View File
@@ -11,8 +11,7 @@ describe("CommandV2", () => {
it.effect("applies command transforms and preserves later overrides", () => it.effect("applies command transforms and preserves later overrides", () =>
Effect.gen(function* () { Effect.gen(function* () {
const command = yield* CommandV2.Service const command = yield* CommandV2.Service
const transform = yield* command.transform() yield* command.transform((editor) => {
yield* transform((editor) => {
editor.update("review", (command) => { editor.update("review", (command) => {
command.template = "First" command.template = "First"
command.description = "Review code" command.description = "Review code"
+7 -13
View File
@@ -10,6 +10,7 @@ import { PermissionV2 } from "@opencode-ai/core/permission"
import { AbsolutePath } from "@opencode-ai/core/schema" import { AbsolutePath } from "@opencode-ai/core/schema"
import { tmpdir } from "../fixture/tmpdir" import { tmpdir } from "../fixture/tmpdir"
import { testEffect } from "../lib/effect" import { testEffect } from "../lib/effect"
import { agentHost, host } from "../plugin/host"
const it = testEffect(Layer.mergeAll(AgentV2.locationLayer, FSUtil.defaultLayer)) const it = testEffect(Layer.mergeAll(AgentV2.locationLayer, FSUtil.defaultLayer))
const decode = Schema.decodeUnknownSync(Config.Info) const decode = Schema.decodeUnknownSync(Config.Info)
@@ -19,9 +20,7 @@ describe("ConfigAgentPlugin.Plugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
const agents = yield* AgentV2.Service const agents = yield* AgentV2.Service
const build = AgentV2.ID.make("build") const build = AgentV2.ID.make("build")
const defaults = yield* agents.transform() yield* agents.transform((editor) =>
yield* defaults((editor) =>
editor.update(build, (agent) => { editor.update(build, (agent) => {
agent.mode = "primary" agent.mode = "primary"
agent.permissions.push({ action: "bash", resource: "*", effect: "allow" }) agent.permissions.push({ action: "bash", resource: "*", effect: "allow" })
@@ -68,9 +67,8 @@ describe("ConfigAgentPlugin.Plugin", () => {
]), ]),
}) })
yield* ConfigAgentPlugin.Plugin.effect.pipe( yield* ConfigAgentPlugin.Plugin.effect(host({ agent: agentHost(agents) })).pipe(
Effect.provideService(Config.Service, config), Effect.provideService(Config.Service, config),
Effect.provideService(AgentV2.Service, agents),
) )
const buildAgent = yield* agents.get(build) const buildAgent = yield* agents.get(build)
@@ -150,9 +148,8 @@ describe("ConfigAgentPlugin.Plugin", () => {
]), ]),
}) })
yield* ConfigAgentPlugin.Plugin.effect.pipe( yield* ConfigAgentPlugin.Plugin.effect(host({ agent: agentHost(agents) })).pipe(
Effect.provideService(Config.Service, config), Effect.provideService(Config.Service, config),
Effect.provideService(AgentV2.Service, agents),
) )
const reviewer = yield* agents.get(AgentV2.ID.make("reviewer")) const reviewer = yield* agents.get(AgentV2.ID.make("reviewer"))
@@ -177,8 +174,7 @@ describe("ConfigAgentPlugin.Plugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
const agents = yield* AgentV2.Service const agents = yield* AgentV2.Service
const build = AgentV2.ID.make("build") const build = AgentV2.ID.make("build")
const defaults = yield* agents.transform() yield* agents.transform((editor) => editor.update(build, () => {}))
yield* defaults((editor) => editor.update(build, () => {}))
const config = Config.Service.of({ const config = Config.Service.of({
entries: () => entries: () =>
@@ -190,9 +186,8 @@ describe("ConfigAgentPlugin.Plugin", () => {
]), ]),
}) })
yield* ConfigAgentPlugin.Plugin.effect.pipe( yield* ConfigAgentPlugin.Plugin.effect(host({ agent: agentHost(agents) })).pipe(
Effect.provideService(Config.Service, config), Effect.provideService(Config.Service, config),
Effect.provideService(AgentV2.Service, agents),
) )
expect(yield* agents.get(build)).toBeUndefined() expect(yield* agents.get(build)).toBeUndefined()
@@ -251,9 +246,8 @@ Use native v2 fields.`,
]), ]),
}) })
yield* ConfigAgentPlugin.Plugin.effect.pipe( yield* ConfigAgentPlugin.Plugin.effect(host({ agent: agentHost(agents) })).pipe(
Effect.provideService(Config.Service, config), Effect.provideService(Config.Service, config),
Effect.provideService(AgentV2.Service, agents),
) )
expect(yield* agents.get(AgentV2.ID.make("reviewer"))).toMatchObject({ expect(yield* agents.get(AgentV2.ID.make("reviewer"))).toMatchObject({
+2 -2
View File
@@ -11,6 +11,7 @@ import { ProviderV2 } from "@opencode-ai/core/provider"
import { AbsolutePath } from "@opencode-ai/core/schema" import { AbsolutePath } from "@opencode-ai/core/schema"
import { tmpdir } from "../fixture/tmpdir" import { tmpdir } from "../fixture/tmpdir"
import { testEffect } from "../lib/effect" import { testEffect } from "../lib/effect"
import { host } from "../plugin/host"
const it = testEffect(Layer.mergeAll(CommandV2.locationLayer, FSUtil.defaultLayer)) const it = testEffect(Layer.mergeAll(CommandV2.locationLayer, FSUtil.defaultLayer))
const decode = Schema.decodeUnknownSync(Config.Info) const decode = Schema.decodeUnknownSync(Config.Info)
@@ -41,8 +42,7 @@ Review files`,
}) })
const command = yield* CommandV2.Service const command = yield* CommandV2.Service
yield* ConfigCommandPlugin.Plugin.effect.pipe( yield* ConfigCommandPlugin.Plugin.effect(host({ command })).pipe(
Effect.provideService(CommandV2.Service, command),
Effect.provideService( Effect.provideService(
Config.Service, Config.Service,
Config.Service.of({ Config.Service.of({
+16 -21
View File
@@ -7,7 +7,8 @@ import { Integration } from "@opencode-ai/core/integration"
import { ModelV2 } from "@opencode-ai/core/model" import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin" import { PluginV2 } from "@opencode-ai/core/plugin"
import { ProviderV2 } from "@opencode-ai/core/provider" import { ProviderV2 } from "@opencode-ai/core/provider"
import { it, withEnv } from "../plugin/provider-helper" import { it, required, withEnv } from "../plugin/provider-helper"
import { catalogHost, host, integrationHost } from "../plugin/host"
function request(headers: Record<string, string>, variant?: string) { function request(headers: Record<string, string>, variant?: string) {
return { return {
@@ -58,14 +59,12 @@ describe("ConfigProviderPlugin.Plugin", () => {
yield* plugin.add({ yield* plugin.add({
...ConfigProviderPlugin.Plugin, ...ConfigProviderPlugin.Plugin,
effect: ConfigProviderPlugin.Plugin.effect.pipe( effect: ConfigProviderPlugin.Plugin.effect(
Effect.provideService(Config.Service, config), host({ catalog: catalogHost(catalog), integration: integrationHost(integrations) }),
Effect.provideService(Catalog.Service, catalog), ).pipe(Effect.provideService(Config.Service, config)),
Effect.provideService(Integration.Service, integrations),
),
}) })
const model = yield* catalog.model.get(providerID, modelID) const model = required(yield* catalog.model.get(providerID, modelID))
expect(model.variants).toMatchObject([ expect(model.variants).toMatchObject([
{ {
id: "high", id: "high",
@@ -119,14 +118,12 @@ describe("ConfigProviderPlugin.Plugin", () => {
yield* plugin.add({ yield* plugin.add({
...ConfigProviderPlugin.Plugin, ...ConfigProviderPlugin.Plugin,
effect: ConfigProviderPlugin.Plugin.effect.pipe( effect: ConfigProviderPlugin.Plugin.effect(
Effect.provideService(Config.Service, config), host({ catalog: catalogHost(catalog), integration: integrationHost(integrations) }),
Effect.provideService(Catalog.Service, catalog), ).pipe(Effect.provideService(Config.Service, config)),
Effect.provideService(Integration.Service, integrations),
),
}) })
const model = yield* catalog.model.get(providerID, modelID) const model = required(yield* catalog.model.get(providerID, modelID))
expect(model.variants[0]).toMatchObject({ expect(model.variants[0]).toMatchObject({
id: "high", id: "high",
body: {}, body: {},
@@ -222,16 +219,14 @@ describe("ConfigProviderPlugin.Plugin", () => {
yield* plugin.add({ yield* plugin.add({
...ConfigProviderPlugin.Plugin, ...ConfigProviderPlugin.Plugin,
effect: ConfigProviderPlugin.Plugin.effect.pipe( effect: ConfigProviderPlugin.Plugin.effect(
Effect.provideService(Config.Service, config), host({ catalog: catalogHost(catalog), integration: integrationHost(integrations) }),
Effect.provideService(Catalog.Service, catalog), ).pipe(Effect.provideService(Config.Service, config)),
Effect.provideService(Integration.Service, integrations),
),
}) })
const provider = yield* catalog.provider.get(providerID) const provider = required(yield* catalog.provider.get(providerID))
const model = yield* catalog.model.get(providerID, modelID) const model = required(yield* catalog.model.get(providerID, modelID))
expect(Option.getOrUndefined(yield* catalog.model.default())?.id).toBe(ModelV2.ID.make("default")) expect((yield* catalog.model.default())?.id).toBe(ModelV2.ID.make("default"))
expect(provider.name).toBe("Renamed") expect(provider.name).toBe("Renamed")
expect((yield* integrations.get(Integration.ID.make("custom")))?.methods).toContainEqual({ expect((yield* integrations.get(Integration.ID.make("custom")))?.methods).toContainEqual({
type: "env", type: "env",
+25 -17
View File
@@ -9,6 +9,7 @@ import { AbsolutePath } from "@opencode-ai/core/schema"
import { SkillV2 } from "@opencode-ai/core/skill" import { SkillV2 } from "@opencode-ai/core/skill"
import { location } from "../fixture/location" import { location } from "../fixture/location"
import { testEffect } from "../lib/effect" import { testEffect } from "../lib/effect"
import { host } from "../plugin/host"
const it = testEffect(Layer.empty) const it = testEffect(Layer.empty)
const decode = Schema.decodeUnknownSync(Config.Info) const decode = Schema.decodeUnknownSync(Config.Info)
@@ -18,16 +19,33 @@ describe("ConfigSkillPlugin.Plugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
const directory = AbsolutePath.make("/repo/packages/app") const directory = AbsolutePath.make("/repo/packages/app")
const sources: SkillV2.Source[] = [] const sources: SkillV2.Source[] = []
const transform = Effect.fnUntraced(function* () { const transform = Effect.fnUntraced(function* (update: (draft: SkillV2.Draft) => void | Effect.Effect<void>) {
return Effect.fnUntraced(function* (update: (editor: SkillV2.Editor) => void) { const result = update({
update({ source: (source) => {
source: (source) => sources.push(source), sources.push(source)
list: () => sources, },
}) list: () => sources,
}) })
if (Effect.isEffect(result)) yield* result
const dispose = Effect.sync(() => {
sources.length = 0
})
yield* Effect.addFinalizer(() => dispose)
return { dispose }
}) })
yield* ConfigSkillPlugin.Plugin.effect.pipe( yield* ConfigSkillPlugin.Plugin.effect(
host({
location: location({ directory }),
path: { ...host().path, home: "/home/test" },
skill: SkillV2.Service.of({
transform,
rebuild: () => Effect.void,
sources: () => Effect.succeed(sources),
list: () => Effect.succeed([]),
}),
}),
).pipe(
Effect.provideService( Effect.provideService(
Config.Service, Config.Service,
Config.Service.of({ Config.Service.of({
@@ -43,16 +61,6 @@ describe("ConfigSkillPlugin.Plugin", () => {
]), ]),
}), }),
), ),
Effect.provideService(Global.Service, Global.Service.of(Global.make({ home: "/home/test" }))),
Effect.provideService(Location.Service, Location.Service.of(location({ directory }))),
Effect.provideService(
SkillV2.Service,
SkillV2.Service.of({
transform,
sources: () => Effect.succeed(sources),
list: () => Effect.succeed([]),
}),
),
) )
expect(sources).toEqual([ expect(sources).toEqual([
+11 -11
View File
@@ -51,7 +51,7 @@ describe("Integration", () => {
const openai = Integration.ID.make("openai") const openai = Integration.ID.make("openai")
yield* integrations yield* integrations
.update((editor) => editor.update(openai, (integration) => (integration.name = "OpenAI"))) .transform((editor) => editor.update(openai, (integration) => (integration.name = "OpenAI")))
.pipe(Scope.provide(scope)) .pipe(Scope.provide(scope))
expect(yield* integrations.get(openai)).toEqual( expect(yield* integrations.get(openai)).toEqual(
new Integration.Info({ id: openai, name: "OpenAI", methods: [], connections: [] }), new Integration.Info({ id: openai, name: "OpenAI", methods: [], connections: [] }),
@@ -70,10 +70,10 @@ describe("Integration", () => {
const second = yield* Scope.fork(yield* Scope.Scope) const second = yield* Scope.fork(yield* Scope.Scope)
yield* integrations yield* integrations
.update((editor) => editor.update(id, (integration) => (integration.name = "OpenAI"))) .transform((editor) => editor.update(id, (integration) => (integration.name = "OpenAI")))
.pipe(Scope.provide(first)) .pipe(Scope.provide(first))
yield* integrations yield* integrations
.update((editor) => editor.update(id, (integration) => (integration.name = "OpenAI Override"))) .transform((editor) => editor.update(id, (integration) => (integration.name = "OpenAI Override")))
.pipe(Scope.provide(second)) .pipe(Scope.provide(second))
expect((yield* integrations.get(id))?.name).toBe("OpenAI Override") expect((yield* integrations.get(id))?.name).toBe("OpenAI Override")
@@ -99,7 +99,7 @@ describe("Integration", () => {
}) })
yield* integrations yield* integrations
.update((editor) => .transform((editor) =>
editor.method.update({ editor.method.update({
integrationID, integrationID,
method: { id: methodID, type: "oauth", label: "ChatGPT" }, method: { id: methodID, type: "oauth", label: "ChatGPT" },
@@ -108,7 +108,7 @@ describe("Integration", () => {
) )
.pipe(Scope.provide(first)) .pipe(Scope.provide(first))
yield* integrations yield* integrations
.update((editor) => { .transform((editor) => {
expect(editor.get(integrationID)).toEqual({ id: integrationID, name: "openai" }) expect(editor.get(integrationID)).toEqual({ id: integrationID, name: "openai" })
expect(editor.list()).toEqual([{ id: integrationID, name: "openai" }]) expect(editor.list()).toEqual([{ id: integrationID, name: "openai" }])
expect(editor.method.list(integrationID)).toEqual([ expect(editor.method.list(integrationID)).toEqual([
@@ -141,7 +141,7 @@ describe("Integration", () => {
const integrations = yield* Integration.Service const integrations = yield* Integration.Service
const events = yield* EventV2.Service const events = yield* EventV2.Service
const integrationID = Integration.ID.make("openai") const integrationID = Integration.ID.make("openai")
yield* integrations.update((editor) => yield* integrations.transform((editor) =>
editor.method.update({ editor.method.update({
integrationID, integrationID,
method: { type: "key", label: "API key" }, method: { type: "key", label: "API key" },
@@ -179,7 +179,7 @@ describe("Integration", () => {
const integrations = yield* Integration.Service const integrations = yield* Integration.Service
const integrationID = Integration.ID.make("openai") const integrationID = Integration.ID.make("openai")
const methodID = Integration.MethodID.make("chatgpt") const methodID = Integration.MethodID.make("chatgpt")
yield* integrations.update((editor) => yield* integrations.transform((editor) =>
editor.method.update({ editor.method.update({
integrationID, integrationID,
method: { id: methodID, type: "oauth", label: "ChatGPT" }, method: { id: methodID, type: "oauth", label: "ChatGPT" },
@@ -238,7 +238,7 @@ describe("Integration", () => {
const integrationID = Integration.ID.make("openai") const integrationID = Integration.ID.make("openai")
const methodID = Integration.MethodID.make("chatgpt") const methodID = Integration.MethodID.make("chatgpt")
let closed = false let closed = false
yield* integrations.update((editor) => yield* integrations.transform((editor) =>
editor.method.update({ editor.method.update({
integrationID, integrationID,
method: { id: methodID, type: "oauth", label: "ChatGPT" }, method: { id: methodID, type: "oauth", label: "ChatGPT" },
@@ -275,7 +275,7 @@ describe("Integration", () => {
const integrations = yield* Integration.Service const integrations = yield* Integration.Service
const integrationID = Integration.ID.make("openai") const integrationID = Integration.ID.make("openai")
const methodID = Integration.MethodID.make("browser") const methodID = Integration.MethodID.make("browser")
yield* integrations.update((editor) => yield* integrations.transform((editor) =>
editor.method.update({ editor.method.update({
integrationID, integrationID,
method: { id: methodID, type: "oauth", label: "Browser" }, method: { id: methodID, type: "oauth", label: "Browser" },
@@ -312,7 +312,7 @@ describe("Integration", () => {
const integrationID = Integration.ID.make("openai") const integrationID = Integration.ID.make("openai")
const methodID = Integration.MethodID.make("browser") const methodID = Integration.MethodID.make("browser")
let closed = false let closed = false
yield* integrations.update((editor) => yield* integrations.transform((editor) =>
editor.method.update({ editor.method.update({
integrationID, integrationID,
method: { id: methodID, type: "oauth", label: "Browser" }, method: { id: methodID, type: "oauth", label: "Browser" },
@@ -375,7 +375,7 @@ describe("Integration", () => {
() => () =>
Effect.gen(function* () { Effect.gen(function* () {
const integrations = yield* Integration.Service const integrations = yield* Integration.Service
yield* integrations.update((editor) => yield* integrations.transform((editor) =>
editor.method.update({ editor.method.update({
integrationID, integrationID,
method: { method: {
+53 -3
View File
@@ -1,8 +1,10 @@
import fs from "fs/promises" import fs from "fs/promises"
import path from "path" import path from "path"
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import { Effect, Equal, Hash, Layer, Schema } from "effect" import { Deferred, Effect, Equal, Hash, Layer, Schema, Stream } from "effect"
import { Tool } from "@opencode-ai/core/public" import { Tool } from "@opencode-ai/core/public"
import { define } from "@opencode-ai/plugin/v2/effect"
import { AgentV2 } from "@opencode-ai/core/agent"
import { Catalog } from "@opencode-ai/core/catalog" import { Catalog } from "@opencode-ai/core/catalog"
import { LocationServiceMap } from "@opencode-ai/core/location-layer" import { LocationServiceMap } from "@opencode-ai/core/location-layer"
import { Location } from "@opencode-ai/core/location" import { Location } from "@opencode-ai/core/location"
@@ -86,8 +88,7 @@ describe("LocationServiceMap", () => {
yield* PluginBoot.Service.use((boot) => boot.wait()) yield* PluginBoot.Service.use((boot) => boot.wait())
yield* Reference.Service yield* Reference.Service
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
const transform = yield* catalog.transform() yield* catalog.transform((editor) => editor.provider.update(ProviderV2.ID.make("test"), () => {}))
yield* transform((editor) => editor.provider.update(ProviderV2.ID.make("test"), () => {}))
return { return {
providers: yield* catalog.provider.all(), providers: yield* catalog.provider.all(),
tools: yield* toolDefinitions(yield* ToolRegistry.Service), tools: yield* toolDefinitions(yield* ToolRegistry.Service),
@@ -135,4 +136,53 @@ describe("LocationServiceMap", () => {
), ),
), ),
) )
it.live("installs public plugins into a location", () =>
Effect.acquireRelease(
Effect.promise(() => tmpdir()),
(dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
).pipe(
Effect.flatMap((dir) =>
Effect.gen(function* () {
const boot = yield* PluginBoot.Service
const catalogUpdated = yield* Deferred.make<void>()
const seen: string[] = []
yield* boot.add(
define({
id: "reviewer",
effect: (ctx) =>
Effect.gen(function* () {
yield* ctx.event.subscribe("catalog.updated").pipe(
Stream.runForEach(() => Deferred.succeed(catalogUpdated, undefined).pipe(Effect.asVoid)),
Effect.forkScoped({ startImmediately: true }),
)
yield* ctx.agent.transform((agent) => {
agent.update("reviewer", (item) => {
item.description = "Reviews code"
item.mode = "subagent"
})
})
seen.push((yield* ctx.agent.get("reviewer"))?.description ?? "")
yield* ctx.catalog.transform((catalog) => {
catalog.provider.update("public", (provider) => {
provider.name = "Public provider"
})
})
}),
}),
)
yield* Deferred.await(catalogUpdated)
expect(seen).toEqual(["Reviews code"])
expect(yield* (yield* AgentV2.Service).get(AgentV2.ID.make("reviewer"))).toMatchObject({
description: "Reviews code",
mode: "subagent",
})
}).pipe(
Effect.scoped,
Effect.provide(LocationServiceMap.get(Location.Ref.make({ directory: AbsolutePath.make(dir.path) }))),
),
),
),
)
}) })
+1 -1
View File
@@ -60,7 +60,7 @@ describe("Npm.add", () => {
return yield* npm.add(spec) return yield* npm.add(spec)
}).pipe(Effect.scoped, Effect.provide(npmLayer(path.join(tmp.path, "cache"))), Effect.runPromise) }).pipe(Effect.scoped, Effect.provide(npmLayer(path.join(tmp.path, "cache"))), Effect.runPromise)
expect(Option.isSome(entry.entrypoint)).toBe(true) expect(entry.entrypoint).toBeDefined()
}) })
}) })
+5 -7
View File
@@ -74,8 +74,7 @@ function setup(rules: PermissionV2.Ruleset = []) {
function setRules(rules: PermissionV2.Ruleset) { function setRules(rules: PermissionV2.Ruleset) {
return Effect.gen(function* () { return Effect.gen(function* () {
const agents = yield* AgentV2.Service const agents = yield* AgentV2.Service
const update = yield* agents.transform() yield* agents.transform((editor) =>
yield* update((editor) =>
editor.update(AgentV2.ID.make("test"), (agent) => { editor.update(AgentV2.ID.make("test"), (agent) => {
agent.permissions = [...rules] agent.permissions = [...rules]
}), }),
@@ -130,7 +129,7 @@ describe("PermissionV2", () => {
Effect.gen(function* () { Effect.gen(function* () {
yield* setup([{ action: "read", resource: "*", effect: "allow" }]) yield* setup([{ action: "read", resource: "*", effect: "allow" }])
const agents = yield* AgentV2.Service const agents = yield* AgentV2.Service
yield* agents.update((editor) => yield* agents.transform((editor) =>
editor.update(AgentV2.ID.make("reviewer"), (agent) => { editor.update(AgentV2.ID.make("reviewer"), (agent) => {
agent.permissions.push({ action: "read", resource: "*", effect: "deny" }) agent.permissions.push({ action: "read", resource: "*", effect: "deny" })
}), }),
@@ -139,7 +138,7 @@ describe("PermissionV2", () => {
expect(yield* service.ask(assertion())).toMatchObject({ effect: "allow" }) expect(yield* service.ask(assertion())).toMatchObject({ effect: "allow" })
expect(yield* service.ask(assertion({ agent: AgentV2.ID.make("reviewer") }))).toMatchObject({ effect: "deny" }) expect(yield* service.ask(assertion({ agent: AgentV2.ID.make("reviewer") }))).toMatchObject({ effect: "deny" })
yield* agents.update((editor) => yield* agents.transform((editor) =>
editor.update(AgentV2.ID.make("reviewer"), (agent) => { editor.update(AgentV2.ID.make("reviewer"), (agent) => {
agent.permissions = [] agent.permissions = []
}), }),
@@ -187,8 +186,7 @@ describe("PermissionV2", () => {
.run() .run()
.pipe(Effect.orDie) .pipe(Effect.orDie)
const agents = yield* AgentV2.Service const agents = yield* AgentV2.Service
const update = yield* agents.transform() yield* agents.transform((editor) =>
yield* update((editor) =>
editor.update(AgentV2.ID.make("build"), (agent) => { editor.update(AgentV2.ID.make("build"), (agent) => {
agent.permissions = [{ action: "todowrite", resource: "*", effect: "allow" }] agent.permissions = [{ action: "todowrite", resource: "*", effect: "allow" }]
}), }),
@@ -214,7 +212,7 @@ describe("PermissionV2", () => {
.run() .run()
.pipe(Effect.orDie) .pipe(Effect.orDie)
const agents = yield* AgentV2.Service const agents = yield* AgentV2.Service
yield* agents.update((editor) => { yield* agents.transform((editor) => {
editor.remove(AgentV2.ID.make("test")) editor.remove(AgentV2.ID.make("test"))
editor.remove(AgentV2.ID.make("build")) editor.remove(AgentV2.ID.make("build"))
}) })
+10 -7
View File
@@ -18,7 +18,7 @@ const plugins = PluginV2.layer.pipe(Layer.provide(events))
function state() { function state() {
return State.create({ return State.create({
initial: () => ({ values: [] as string[] }), initial: () => ({ values: [] as string[] }),
editor: (draft) => ({ draft: (draft) => ({
add: (value: string) => draft.values.push(value), add: (value: string) => draft.values.push(value),
}), }),
}) })
@@ -34,8 +34,9 @@ describe("PluginV2", () => {
yield* plugin.add({ yield* plugin.add({
id: PluginV2.ID.make("scoped"), id: PluginV2.ID.make("scoped"),
effect: Effect.gen(function* () { effect: Effect.gen(function* () {
const transform = yield* values.transform() yield* values.transform((editor) => {
yield* transform((editor) => editor.add("scoped")) editor.add("scoped")
})
}), }),
}) })
expect(values.get().values).toEqual(["scoped"]) expect(values.get().values).toEqual(["scoped"])
@@ -58,8 +59,9 @@ describe("PluginV2", () => {
.add({ .add({
id, id,
effect: Effect.gen(function* () { effect: Effect.gen(function* () {
const transform = yield* values.transform() yield* values.transform((editor) => {
yield* transform((editor) => editor.add("first")) editor.add("first")
})
yield* Deferred.succeed(firstStarted, undefined) yield* Deferred.succeed(firstStarted, undefined)
yield* Deferred.await(releaseFirst) yield* Deferred.await(releaseFirst)
}), }),
@@ -71,8 +73,9 @@ describe("PluginV2", () => {
.add({ .add({
id, id,
effect: Effect.gen(function* () { effect: Effect.gen(function* () {
const transform = yield* values.transform() yield* values.transform((editor) => {
yield* transform((editor) => editor.add("second")) editor.add("second")
})
}), }),
}) })
.pipe(Effect.forkChild({ startImmediately: true })) .pipe(Effect.forkChild({ startImmediately: true }))
+6 -6
View File
@@ -6,6 +6,7 @@ import { CommandPlugin } from "@opencode-ai/core/plugin/command"
import { AbsolutePath } from "@opencode-ai/core/schema" import { AbsolutePath } from "@opencode-ai/core/schema"
import { location } from "../fixture/location" import { location } from "../fixture/location"
import { testEffect } from "../lib/effect" import { testEffect } from "../lib/effect"
import { host } from "./host"
const directory = AbsolutePath.make("/repo/packages/app") const directory = AbsolutePath.make("/repo/packages/app")
const project = AbsolutePath.make("/repo") const project = AbsolutePath.make("/repo")
@@ -21,12 +22,11 @@ describe("CommandPlugin.Plugin", () => {
it.effect("registers built-in init and review commands", () => it.effect("registers built-in init and review commands", () =>
Effect.gen(function* () { Effect.gen(function* () {
const command = yield* CommandV2.Service const command = yield* CommandV2.Service
yield* CommandPlugin.Plugin.effect.pipe( yield* CommandPlugin.Plugin.effect(
Effect.provideService(CommandV2.Service, command), host({
Effect.provideService( command,
Location.Service, location: location({ directory }, { projectDirectory: project }),
Location.Service.of(location({ directory }, { projectDirectory: project })), }),
),
) )
expect(yield* command.get("init")).toMatchObject({ expect(yield* command.get("init")).toMatchObject({
+319
View File
@@ -0,0 +1,319 @@
import type { AISDKHooks, PluginHost } from "@opencode-ai/plugin/v2/effect"
import { AgentV2 } from "@opencode-ai/core/agent"
import { Catalog } from "@opencode-ai/core/catalog"
import { Integration } from "@opencode-ai/core/integration"
import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { ProviderV2 } from "@opencode-ai/core/provider"
import type { IntegrationEnvMethod, IntegrationKeyMethod, IntegrationOAuthMethod } from "@opencode-ai/sdk/v2/types"
import { Effect, Stream } from "effect"
export function host(overrides: Partial<PluginHost> = {}): PluginHost {
return {
aisdk: {
hook: () => Effect.die("unused aisdk.hook"),
},
agent: {
get: () => Effect.die("unused agent.get"),
default: () => Effect.die("unused agent.default"),
list: () => Effect.die("unused agent.list"),
rebuild: () => Effect.die("unused agent.rebuild"),
transform: () => Effect.die("unused agent.transform"),
},
catalog: {
provider: {
get: () => Effect.die("unused catalog.provider.get"),
list: () => Effect.die("unused catalog.provider.list"),
available: () => Effect.die("unused catalog.provider.available"),
},
model: {
get: () => Effect.die("unused catalog.model.get"),
list: () => Effect.die("unused catalog.model.list"),
available: () => Effect.die("unused catalog.model.available"),
default: () => Effect.die("unused catalog.model.default"),
small: () => Effect.die("unused catalog.model.small"),
},
rebuild: () => Effect.die("unused catalog.rebuild"),
transform: () => Effect.die("unused catalog.transform"),
},
command: {
get: () => Effect.die("unused command.get"),
list: () => Effect.die("unused command.list"),
rebuild: () => Effect.die("unused command.rebuild"),
transform: () => Effect.die("unused command.transform"),
},
event: {
subscribe: () => Stream.die("unused event.subscribe"),
},
filesystem: {
read: () => Effect.die("unused filesystem.read"),
list: () => Effect.die("unused filesystem.list"),
find: () => Effect.die("unused filesystem.find"),
glob: () => Effect.die("unused filesystem.glob"),
},
integration: {
get: () => Effect.die("unused integration.get"),
list: () => Effect.die("unused integration.list"),
rebuild: () => Effect.die("unused integration.rebuild"),
transform: () => Effect.die("unused integration.transform"),
},
location: {
directory: "/unused/location",
project: { directory: "/unused/project" },
},
npm: {
add: () => Effect.die("unused npm.add"),
},
path: {
home: "/unused/home",
data: "/unused/data",
cache: "/unused/cache",
config: "/unused/config",
state: "/unused/state",
temp: "/unused/temp",
},
reference: {
list: () => Effect.die("unused reference.list"),
rebuild: () => Effect.die("unused reference.rebuild"),
transform: () => Effect.die("unused reference.transform"),
},
skill: {
sources: () => Effect.die("unused skill.sources"),
list: () => Effect.die("unused skill.list"),
rebuild: () => Effect.die("unused skill.rebuild"),
transform: () => Effect.die("unused skill.transform"),
},
...overrides,
}
}
export function aisdkHost(plugin: PluginV2.Interface): PluginHost["aisdk"] {
return {
hook: (name, callback) => {
if (name === "sdk") {
const run = callback as AISDKHooks["sdk"]
return plugin.hook("aisdk.sdk", (event) => {
const output = { ...event }
const result = run(output)
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
Effect.tap(() => Effect.sync(() => (event.sdk = output.sdk))),
)
})
}
const run = callback as AISDKHooks["language"]
return plugin.hook("aisdk.language", (event) => {
const output = { ...event }
const result = run(output)
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
Effect.tap(() => Effect.sync(() => (event.language = output.language))),
)
})
},
}
}
export function agentHost(agent: AgentV2.Interface): PluginHost["agent"] {
return {
...host().agent,
transform: (callback) =>
agent.transform((draft) =>
callback({
list: () => draft.list().map(agentInfo),
get: (id) => {
const value = draft.get(AgentV2.ID.make(id))
return value && agentInfo(value)
},
default: (id) => draft.default(id === undefined ? undefined : AgentV2.ID.make(id)),
update: (id, update) =>
draft.update(AgentV2.ID.make(id), (value) => {
const current = agentInfo(value)
update(current)
Object.assign(value, current, { id: AgentV2.ID.make(current.id) })
}),
remove: (id) => draft.remove(AgentV2.ID.make(id)),
}),
),
}
}
export function catalogHost(catalog: Catalog.Interface): PluginHost["catalog"] {
return {
...host().catalog,
rebuild: catalog.rebuild,
transform: (callback) =>
catalog.transform((draft) =>
callback({
provider: {
list: () =>
draft.provider.list().map((value) => ({
provider: providerInfo(value.provider),
models: new Map(Array.from(value.models, ([id, model]) => [id, modelInfo(model)])),
})),
get: (id) => {
const value = draft.provider.get(ProviderV2.ID.make(id))
return (
value && {
provider: providerInfo(value.provider),
models: new Map(Array.from(value.models, ([id, model]) => [id, modelInfo(model)])),
}
)
},
update: (id, update) =>
draft.provider.update(ProviderV2.ID.make(id), (value) => {
const current = providerInfo(value)
update(current)
Object.assign(value, current, { id: ProviderV2.ID.make(current.id) })
}),
remove: (id) => draft.provider.remove(ProviderV2.ID.make(id)),
},
model: {
get: (providerID, modelID) => {
const value = draft.model.get(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID))
return value && modelInfo(value)
},
update: (providerID, modelID, update) =>
draft.model.update(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID), (value) => {
const current = modelInfo(value)
update(current)
Object.assign(value, current, {
id: ModelV2.ID.make(current.id),
providerID: ProviderV2.ID.make(current.providerID),
family: current.family === undefined ? undefined : ModelV2.Family.make(current.family),
variants: current.variants.map((variant) => ({
...variant,
id: ModelV2.VariantID.make(variant.id),
})),
})
}),
remove: (providerID, modelID) =>
draft.model.remove(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
default: {
get: () => {
const value = draft.model.default.get()
return value && { providerID: value.providerID, modelID: value.modelID }
},
set: (providerID, modelID) =>
draft.model.default.set(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
},
},
}),
),
}
}
export function integrationHost(integration: Integration.Interface): PluginHost["integration"] {
const info = (value: Integration.Info) => ({
id: value.id,
name: value.name,
methods: value.methods.map(method),
connections: value.connections.map((item) => ({ ...item })),
})
return {
get: (id) => integration.get(Integration.ID.make(id)).pipe(Effect.map((value) => value && info(value))),
list: () => integration.list().pipe(Effect.map((items) => items.map(info))),
rebuild: integration.rebuild,
transform: (callback) =>
integration.transform((draft) =>
callback({
list: () => draft.list().map((value) => ({ id: value.id, name: value.name })),
get: (id) => {
const value = draft.get(Integration.ID.make(id))
return value && { id: value.id, name: value.name }
},
update: (id, update) => draft.update(Integration.ID.make(id), update),
remove: (id) => draft.remove(Integration.ID.make(id)),
method: {
list: (id) => draft.method.list(Integration.ID.make(id)).map(method),
update: (input) =>
input.method.type === "env"
? draft.method.update({
integrationID: Integration.ID.make(input.integrationID),
method: { ...input.method, names: [...input.method.names] },
})
: draft.method.update({
integrationID: Integration.ID.make(input.integrationID),
method: input.method,
}),
remove: (id, item) => draft.method.remove(Integration.ID.make(id), internalMethod(item)),
},
}),
),
}
}
function method(value: Integration.Method) {
if (value.type === "env") return { type: value.type, names: [...value.names] }
if (value.type === "key") return { type: value.type, label: value.label }
return {
type: value.type,
id: value.id,
label: value.label,
prompts: value.prompts?.map((prompt) => {
if (prompt.type === "text") return { ...prompt }
return { ...prompt, options: prompt.options.map((option) => ({ ...option })) }
}),
}
}
function internalMethod(
value: IntegrationOAuthMethod | IntegrationKeyMethod | IntegrationEnvMethod,
): Integration.Method {
if (value.type === "env") return value
if (value.type === "key") return value
return {
...value,
id: Integration.MethodID.make(value.id),
}
}
function agentInfo(value: AgentV2.Info) {
return {
...value,
model: value.model && { ...value.model },
request: { headers: { ...value.request.headers }, body: { ...value.request.body } },
permissions: value.permissions.map((permission) => ({ ...permission })),
}
}
function providerInfo(value: ProviderV2.MutableInfo) {
return {
...value,
api: { ...value.api, settings: value.api.settings && { ...value.api.settings } },
request: { headers: { ...value.request.headers }, body: { ...value.request.body } },
}
}
function modelInfo(value: ModelV2.Info | ModelV2.MutableInfo) {
return {
...value,
api: { ...value.api, settings: value.api.settings && { ...value.api.settings } },
capabilities: {
...value.capabilities,
input: [...value.capabilities.input],
output: [...value.capabilities.output],
},
request: {
...value.request,
headers: { ...value.request.headers },
body: { ...value.request.body },
generation: value.request.generation && {
...value.request.generation,
stop: value.request.generation.stop && [...value.request.generation.stop],
},
options: value.request.options && { ...value.request.options },
},
variants: value.variants.map((variant) => ({
...variant,
headers: { ...variant.headers },
body: { ...variant.body },
generation: variant.generation && {
...variant.generation,
stop: variant.generation.stop && [...variant.generation.stop],
},
options: variant.options && { ...variant.options },
})),
time: { ...value.time },
cost: value.cost.map((cost) => ({ ...cost, tier: cost.tier && { ...cost.tier }, cache: { ...cost.cache } })),
limit: { ...value.limit },
}
}
+10 -2
View File
@@ -1,6 +1,6 @@
import path from "path" import path from "path"
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect" import { Effect, Layer, Stream } from "effect"
import { Catalog } from "@opencode-ai/core/catalog" import { Catalog } from "@opencode-ai/core/catalog"
import { Integration } from "@opencode-ai/core/integration" import { Integration } from "@opencode-ai/core/integration"
import { Credential } from "@opencode-ai/core/credential" import { Credential } from "@opencode-ai/core/credential"
@@ -15,6 +15,7 @@ import { Policy } from "@opencode-ai/core/policy"
import { AbsolutePath } from "@opencode-ai/core/schema" import { AbsolutePath } from "@opencode-ai/core/schema"
import { location } from "../fixture/location" import { location } from "../fixture/location"
import { testEffect } from "../lib/effect" import { testEffect } from "../lib/effect"
import { catalogHost, host, integrationHost } from "./host"
const events = EventV2.defaultLayer const events = EventV2.defaultLayer
const locationLayer = Layer.succeed( const locationLayer = Layer.succeed(
@@ -56,8 +57,15 @@ describe("ModelsDevPlugin", () => {
}), }),
() => () =>
Effect.gen(function* () { Effect.gen(function* () {
yield* ModelsDevPlugin.effect
const integrations = yield* Integration.Service const integrations = yield* Integration.Service
const catalog = yield* Catalog.Service
yield* ModelsDevPlugin.effect(
host({
catalog: catalogHost(catalog),
event: { subscribe: () => Stream.never },
integration: integrationHost(integrations),
}),
)
expect(yield* integrations.list()).toEqual([ expect(yield* integrations.list()).toEqual([
new Integration.Info({ new Integration.Info({
id: Integration.ID.make("acme"), id: Integration.ID.make("acme"),
@@ -4,13 +4,13 @@ import { Effect } from "effect"
import { ModelV2 } from "@opencode-ai/core/model" import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin" import { PluginV2 } from "@opencode-ai/core/plugin"
import { AlibabaPlugin } from "@opencode-ai/core/plugin/provider/alibaba" import { AlibabaPlugin } from "@opencode-ai/core/plugin/provider/alibaba"
import { it, model } from "./provider-helper" import { addPlugin, it, model } from "./provider-helper"
describe("AlibabaPlugin", () => { describe("AlibabaPlugin", () => {
it.effect("creates an Alibaba SDK for @ai-sdk/alibaba", () => it.effect("creates an Alibaba SDK for @ai-sdk/alibaba", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
yield* plugin.add(AlibabaPlugin) yield* addPlugin(plugin, AlibabaPlugin)
const result = yield* plugin.trigger( const result = yield* plugin.trigger(
"aisdk.sdk", "aisdk.sdk",
{ model: model("alibaba", "qwen"), package: "@ai-sdk/alibaba", options: { name: "alibaba" } }, { model: model("alibaba", "qwen"), package: "@ai-sdk/alibaba", options: { name: "alibaba" } },
@@ -23,7 +23,7 @@ describe("AlibabaPlugin", () => {
it.effect("ignores non-Alibaba SDK packages", () => it.effect("ignores non-Alibaba SDK packages", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
yield* plugin.add(AlibabaPlugin) yield* addPlugin(plugin, AlibabaPlugin)
const result = yield* plugin.trigger( const result = yield* plugin.trigger(
"aisdk.sdk", "aisdk.sdk",
{ model: model("alibaba", "qwen"), package: "@ai-sdk/openai-compatible", options: { name: "alibaba" } }, { model: model("alibaba", "qwen"), package: "@ai-sdk/openai-compatible", options: { name: "alibaba" } },
@@ -36,7 +36,7 @@ describe("AlibabaPlugin", () => {
it.effect("matches the old bundled Alibaba SDK provider naming", () => it.effect("matches the old bundled Alibaba SDK provider naming", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
yield* plugin.add(AlibabaPlugin) yield* addPlugin(plugin, AlibabaPlugin)
const result = yield* plugin.trigger( const result = yield* plugin.trigger(
"aisdk.sdk", "aisdk.sdk",
{ {
@@ -56,7 +56,7 @@ describe("AlibabaPlugin", () => {
it.effect("uses the old default languageModel(api.id) behavior", () => it.effect("uses the old default languageModel(api.id) behavior", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
yield* plugin.add(AlibabaPlugin) yield* addPlugin(plugin, AlibabaPlugin)
const item = model("alibaba", "alias", { api: { id: ModelV2.ID.make("qwen-plus") } }) const item = model("alibaba", "alias", { api: { id: ModelV2.ID.make("qwen-plus") } })
const result = yield* plugin.trigger("aisdk.sdk", { model: item, package: "@ai-sdk/alibaba", options: {} }, {}) const result = yield* plugin.trigger("aisdk.sdk", { model: item, package: "@ai-sdk/alibaba", options: {} }, {})
const language = result.sdk?.languageModel(item.api.id) const language = result.sdk?.languageModel(item.api.id)
@@ -4,7 +4,7 @@ import { Catalog } from "@opencode-ai/core/catalog"
import { PluginV2 } from "@opencode-ai/core/plugin" import { PluginV2 } from "@opencode-ai/core/plugin"
import { AmazonBedrockPlugin } from "@opencode-ai/core/plugin/provider/amazon-bedrock" import { AmazonBedrockPlugin } from "@opencode-ai/core/plugin/provider/amazon-bedrock"
import { ProviderV2 } from "@opencode-ai/core/provider" import { ProviderV2 } from "@opencode-ai/core/provider"
import { fakeSelectorSdk, it, model, provider, withEnv } from "./provider-helper" import { addPlugin, fakeSelectorSdk, it, model, provider, required, withEnv } from "./provider-helper"
function bedrockBaseURL(sdk: unknown, modelID = "anthropic.claude-sonnet-4-5") { function bedrockBaseURL(sdk: unknown, modelID = "anthropic.claude-sonnet-4-5") {
const language = (sdk as { languageModel: (id: string) => unknown }).languageModel(modelID) const language = (sdk as { languageModel: (id: string) => unknown }).languageModel(modelID)
@@ -30,9 +30,8 @@ describe("AmazonBedrockPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* plugin.add(AmazonBedrockPlugin) yield* addPlugin(plugin, AmazonBedrockPlugin)
const transform = yield* catalog.transform() yield* catalog.transform((catalog) => {
yield* transform((catalog) => {
const bedrock = provider("amazon-bedrock", { const bedrock = provider("amazon-bedrock", {
api: { type: "aisdk", package: "@ai-sdk/amazon-bedrock" }, api: { type: "aisdk", package: "@ai-sdk/amazon-bedrock" },
request: { request: {
@@ -45,7 +44,7 @@ describe("AmazonBedrockPlugin", () => {
item.request = bedrock.request item.request = bedrock.request
}) })
}) })
const result = yield* catalog.provider.get(ProviderV2.ID.amazonBedrock) const result = required(yield* catalog.provider.get(ProviderV2.ID.amazonBedrock))
expect(result.api).toEqual({ expect(result.api).toEqual({
type: "aisdk", type: "aisdk",
package: "@ai-sdk/amazon-bedrock", package: "@ai-sdk/amazon-bedrock",
@@ -59,7 +58,7 @@ describe("AmazonBedrockPlugin", () => {
withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () => withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
yield* plugin.add(AmazonBedrockPlugin) yield* addPlugin(plugin, AmazonBedrockPlugin)
const result = yield* plugin.trigger( const result = yield* plugin.trigger(
"aisdk.sdk", "aisdk.sdk",
{ {
@@ -84,7 +83,7 @@ describe("AmazonBedrockPlugin", () => {
withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () => withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
yield* plugin.add(AmazonBedrockPlugin) yield* addPlugin(plugin, AmazonBedrockPlugin)
const result = yield* plugin.trigger( const result = yield* plugin.trigger(
"aisdk.sdk", "aisdk.sdk",
{ {
@@ -118,7 +117,7 @@ describe("AmazonBedrockPlugin", () => {
() => () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
yield* plugin.add(AmazonBedrockPlugin) yield* addPlugin(plugin, AmazonBedrockPlugin)
const result = yield* plugin.trigger( const result = yield* plugin.trigger(
"aisdk.sdk", "aisdk.sdk",
{ {
@@ -138,7 +137,7 @@ describe("AmazonBedrockPlugin", () => {
withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: "us-east-1" }, () => withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: "us-east-1" }, () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
yield* plugin.add(AmazonBedrockPlugin) yield* addPlugin(plugin, AmazonBedrockPlugin)
const result = yield* plugin.trigger( const result = yield* plugin.trigger(
"aisdk.sdk", "aisdk.sdk",
{ {
@@ -157,7 +156,7 @@ describe("AmazonBedrockPlugin", () => {
withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: "eu-west-1" }, () => withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: "eu-west-1" }, () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
yield* plugin.add(AmazonBedrockPlugin) yield* addPlugin(plugin, AmazonBedrockPlugin)
const result = yield* plugin.trigger( const result = yield* plugin.trigger(
"aisdk.sdk", "aisdk.sdk",
{ {
@@ -176,7 +175,7 @@ describe("AmazonBedrockPlugin", () => {
withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: undefined }, () => withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: undefined }, () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
yield* plugin.add(AmazonBedrockPlugin) yield* addPlugin(plugin, AmazonBedrockPlugin)
const result = yield* plugin.trigger( const result = yield* plugin.trigger(
"aisdk.sdk", "aisdk.sdk",
{ {
@@ -196,7 +195,7 @@ describe("AmazonBedrockPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const headers: Array<string | null> = [] const headers: Array<string | null> = []
yield* plugin.add(AmazonBedrockPlugin) yield* addPlugin(plugin, AmazonBedrockPlugin)
const result = yield* plugin.trigger( const result = yield* plugin.trigger(
"aisdk.sdk", "aisdk.sdk",
{ {
@@ -225,7 +224,7 @@ describe("AmazonBedrockPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const headers: Array<string | null> = [] const headers: Array<string | null> = []
yield* plugin.add(AmazonBedrockPlugin) yield* addPlugin(plugin, AmazonBedrockPlugin)
const result = yield* plugin.trigger( const result = yield* plugin.trigger(
"aisdk.sdk", "aisdk.sdk",
{ {
@@ -253,7 +252,7 @@ describe("AmazonBedrockPlugin", () => {
withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () => withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
yield* plugin.add(AmazonBedrockPlugin) yield* addPlugin(plugin, AmazonBedrockPlugin)
const result = yield* plugin.trigger( const result = yield* plugin.trigger(
"aisdk.sdk", "aisdk.sdk",
{ {
@@ -282,7 +281,7 @@ describe("AmazonBedrockPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const calls: string[] = [] const calls: string[] = []
yield* plugin.add(AmazonBedrockPlugin) yield* addPlugin(plugin, AmazonBedrockPlugin)
yield* plugin.trigger( yield* plugin.trigger(
"aisdk.language", "aisdk.language",
{ {
@@ -312,7 +311,7 @@ describe("AmazonBedrockPlugin", () => {
it.effect("ignores other Bedrock provider subpaths", () => it.effect("ignores other Bedrock provider subpaths", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
yield* plugin.add(AmazonBedrockPlugin) yield* addPlugin(plugin, AmazonBedrockPlugin)
const result = yield* plugin.trigger( const result = yield* plugin.trigger(
"aisdk.sdk", "aisdk.sdk",
{ {
@@ -341,7 +340,7 @@ describe("AmazonBedrockPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const headers: Array<string | null> = [] const headers: Array<string | null> = []
yield* plugin.add(AmazonBedrockPlugin) yield* addPlugin(plugin, AmazonBedrockPlugin)
const result = yield* plugin.trigger( const result = yield* plugin.trigger(
"aisdk.sdk", "aisdk.sdk",
{ {
@@ -372,7 +371,7 @@ describe("AmazonBedrockPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const calls: string[] = [] const calls: string[] = []
yield* plugin.add(AmazonBedrockPlugin) yield* addPlugin(plugin, AmazonBedrockPlugin)
yield* plugin.trigger( yield* plugin.trigger(
"aisdk.language", "aisdk.language",
{ {
@@ -433,7 +432,7 @@ describe("AmazonBedrockPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const calls: string[] = [] const calls: string[] = []
yield* plugin.add(AmazonBedrockPlugin) yield* addPlugin(plugin, AmazonBedrockPlugin)
yield* plugin.trigger( yield* plugin.trigger(
"aisdk.language", "aisdk.language",
{ {
@@ -518,7 +517,7 @@ describe("AmazonBedrockPlugin", () => {
expected: "au.anthropic.claude-sonnet-4-5", expected: "au.anthropic.claude-sonnet-4-5",
}, },
] ]
yield* plugin.add(AmazonBedrockPlugin) yield* addPlugin(plugin, AmazonBedrockPlugin)
for (const item of cases) { for (const item of cases) {
yield* plugin.trigger( yield* plugin.trigger(
"aisdk.language", "aisdk.language",
@@ -538,7 +537,7 @@ describe("AmazonBedrockPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const calls: string[] = [] const calls: string[] = []
yield* plugin.add(AmazonBedrockPlugin) yield* addPlugin(plugin, AmazonBedrockPlugin)
const result = yield* plugin.trigger( const result = yield* plugin.trigger(
"aisdk.language", "aisdk.language",
{ {
@@ -4,16 +4,15 @@ import { Catalog } from "@opencode-ai/core/catalog"
import { PluginV2 } from "@opencode-ai/core/plugin" import { PluginV2 } from "@opencode-ai/core/plugin"
import { AnthropicPlugin } from "@opencode-ai/core/plugin/provider/anthropic" import { AnthropicPlugin } from "@opencode-ai/core/plugin/provider/anthropic"
import { ProviderV2 } from "@opencode-ai/core/provider" import { ProviderV2 } from "@opencode-ai/core/provider"
import { it, model, provider } from "./provider-helper" import { addPlugin, it, model, provider, required } from "./provider-helper"
describe("AnthropicPlugin", () => { describe("AnthropicPlugin", () => {
it.effect("applies legacy beta headers", () => it.effect("applies legacy beta headers", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* plugin.add(AnthropicPlugin) yield* addPlugin(plugin, AnthropicPlugin)
const transform = yield* catalog.transform() yield* catalog.transform((catalog) => {
yield* transform((catalog) => {
const item = provider("anthropic", { const item = provider("anthropic", {
api: { type: "aisdk", package: "@ai-sdk/anthropic" }, api: { type: "aisdk", package: "@ai-sdk/anthropic" },
request: { headers: { Existing: "1" }, body: {} }, request: { headers: { Existing: "1" }, body: {} },
@@ -23,10 +22,10 @@ describe("AnthropicPlugin", () => {
draft.request = item.request draft.request = item.request
}) })
}) })
expect((yield* catalog.provider.get(ProviderV2.ID.anthropic)).request.headers["anthropic-beta"]).toBe( expect(required(yield* catalog.provider.get(ProviderV2.ID.anthropic)).request.headers["anthropic-beta"]).toBe(
"interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14", "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
) )
expect((yield* catalog.provider.get(ProviderV2.ID.anthropic)).request.headers.Existing).toBe("1") expect(required(yield* catalog.provider.get(ProviderV2.ID.anthropic)).request.headers.Existing).toBe("1")
}), }),
) )
@@ -34,10 +33,11 @@ describe("AnthropicPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* plugin.add(AnthropicPlugin) yield* addPlugin(plugin, AnthropicPlugin)
const transform = yield* catalog.transform() yield* catalog.transform((catalog) => catalog.provider.update(provider("openai").id, () => {}))
yield* transform((catalog) => catalog.provider.update(provider("openai").id, () => {})) expect(
expect((yield* catalog.provider.get(ProviderV2.ID.openai)).request.headers["anthropic-beta"]).toBeUndefined() required(yield* catalog.provider.get(ProviderV2.ID.openai)).request.headers["anthropic-beta"],
).toBeUndefined()
}), }),
) )
@@ -45,7 +45,7 @@ describe("AnthropicPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const providers: string[] = [] const providers: string[] = []
yield* plugin.add(AnthropicPlugin) yield* addPlugin(plugin, AnthropicPlugin)
yield* plugin.add({ yield* plugin.add({
id: PluginV2.ID.make("anthropic-sdk-inspector"), id: PluginV2.ID.make("anthropic-sdk-inspector"),
effect: Effect.succeed({ effect: Effect.succeed({
@@ -72,7 +72,7 @@ describe("AnthropicPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const providers: string[] = [] const providers: string[] = []
yield* plugin.add(AnthropicPlugin) yield* addPlugin(plugin, AnthropicPlugin)
yield* plugin.add({ yield* plugin.add({
id: PluginV2.ID.make("anthropic-sdk-inspector"), id: PluginV2.ID.make("anthropic-sdk-inspector"),
effect: Effect.succeed({ effect: Effect.succeed({
@@ -4,7 +4,7 @@ import { Catalog } from "@opencode-ai/core/catalog"
import { PluginV2 } from "@opencode-ai/core/plugin" import { PluginV2 } from "@opencode-ai/core/plugin"
import { AzureCognitiveServicesPlugin } from "@opencode-ai/core/plugin/provider/azure" import { AzureCognitiveServicesPlugin } from "@opencode-ai/core/plugin/provider/azure"
import { ProviderV2 } from "@opencode-ai/core/provider" import { ProviderV2 } from "@opencode-ai/core/provider"
import { fakeSelectorSdk, it, model, provider, withEnv } from "./provider-helper" import { addPlugin, fakeSelectorSdk, it, model, provider, required, withEnv } from "./provider-helper"
describe("AzureCognitiveServicesPlugin", () => { describe("AzureCognitiveServicesPlugin", () => {
it.effect("maps the resource env var to the Azure SDK baseURL", () => it.effect("maps the resource env var to the Azure SDK baseURL", () =>
@@ -12,14 +12,13 @@ describe("AzureCognitiveServicesPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* plugin.add(AzureCognitiveServicesPlugin) yield* addPlugin(plugin, AzureCognitiveServicesPlugin)
const transform = yield* catalog.transform() yield* catalog.transform((catalog) => {
yield* transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("azure-cognitive-services"), (item) => { catalog.provider.update(ProviderV2.ID.make("azure-cognitive-services"), (item) => {
item.api = { type: "aisdk", package: "@ai-sdk/openai-compatible" } item.api = { type: "aisdk", package: "@ai-sdk/openai-compatible" }
}) })
}) })
const result = yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services")) const result = required(yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services")))
expect(result.api).toEqual({ expect(result.api).toEqual({
type: "aisdk", type: "aisdk",
package: "@ai-sdk/openai-compatible", package: "@ai-sdk/openai-compatible",
@@ -36,9 +35,8 @@ describe("AzureCognitiveServicesPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* plugin.add(AzureCognitiveServicesPlugin) yield* addPlugin(plugin, AzureCognitiveServicesPlugin)
const transform = yield* catalog.transform() yield* catalog.transform((catalog) => {
yield* transform((catalog) => {
const azure = provider("azure-cognitive-services", { const azure = provider("azure-cognitive-services", {
api: { type: "aisdk", package: "@ai-sdk/openai-compatible" }, api: { type: "aisdk", package: "@ai-sdk/openai-compatible" },
}) })
@@ -50,8 +48,8 @@ describe("AzureCognitiveServicesPlugin", () => {
item.api = openai.api item.api = openai.api
}) })
}) })
const azure = yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services")) const azure = required(yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services")))
const openai = yield* catalog.provider.get(ProviderV2.ID.openai) const openai = required(yield* catalog.provider.get(ProviderV2.ID.openai))
expect(azure.request.body.baseURL).toBeUndefined() expect(azure.request.body.baseURL).toBeUndefined()
expect(azure.api).toEqual({ type: "aisdk", package: "@ai-sdk/openai-compatible" }) expect(azure.api).toEqual({ type: "aisdk", package: "@ai-sdk/openai-compatible" })
expect(openai.request.body.baseURL).toBeUndefined() expect(openai.request.body.baseURL).toBeUndefined()
@@ -64,7 +62,7 @@ describe("AzureCognitiveServicesPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const calls: string[] = [] const calls: string[] = []
yield* plugin.add(AzureCognitiveServicesPlugin) yield* addPlugin(plugin, AzureCognitiveServicesPlugin)
yield* plugin.trigger( yield* plugin.trigger(
"aisdk.language", "aisdk.language",
{ {
@@ -82,7 +80,7 @@ describe("AzureCognitiveServicesPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const calls: string[] = [] const calls: string[] = []
yield* plugin.add(AzureCognitiveServicesPlugin) yield* addPlugin(plugin, AzureCognitiveServicesPlugin)
yield* plugin.trigger( yield* plugin.trigger(
"aisdk.language", "aisdk.language",
{ model: model("azure-cognitive-services", "deployment"), sdk: fakeSelectorSdk(calls), options: {} }, { model: model("azure-cognitive-services", "deployment"), sdk: fakeSelectorSdk(calls), options: {} },
@@ -103,7 +101,7 @@ describe("AzureCognitiveServicesPlugin", () => {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const calls: string[] = [] const calls: string[] = []
const sdk = fakeSelectorSdk(calls) const sdk = fakeSelectorSdk(calls)
yield* plugin.add(AzureCognitiveServicesPlugin) yield* addPlugin(plugin, AzureCognitiveServicesPlugin)
yield* plugin.trigger( yield* plugin.trigger(
"aisdk.language", "aisdk.language",
{ {
@@ -1,35 +1,10 @@
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect" import { Effect } from "effect"
import { Credential } from "@opencode-ai/core/credential"
import { Integration } from "@opencode-ai/core/integration"
import { Database } from "@opencode-ai/core/database/database"
import { Catalog } from "@opencode-ai/core/catalog" import { Catalog } from "@opencode-ai/core/catalog"
import { EventV2 } from "@opencode-ai/core/event"
import { Location } from "@opencode-ai/core/location"
import { PluginV2 } from "@opencode-ai/core/plugin" import { PluginV2 } from "@opencode-ai/core/plugin"
import { AzurePlugin } from "@opencode-ai/core/plugin/provider/azure" import { AzurePlugin } from "@opencode-ai/core/plugin/provider/azure"
import { ProviderV2 } from "@opencode-ai/core/provider" import { ProviderV2 } from "@opencode-ai/core/provider"
import { AbsolutePath } from "@opencode-ai/core/schema" import { addPlugin, fakeSelectorSdk, it, model, provider, required, withEnv } from "./provider-helper"
import { location } from "../fixture/location"
import { testEffect } from "../lib/effect"
import { fakeSelectorSdk, it, model, npmLayer, provider, withEnv } from "./provider-helper"
const database = Database.layerFromPath(":memory:").pipe(Layer.fresh)
const preferences = Credential.layer.pipe(Layer.provide(database))
const accounts = Layer.merge(
Credential.layer.pipe(Layer.provide(database), Layer.provide(preferences), Layer.provide(EventV2.defaultLayer)),
preferences,
)
const itWithAccount = testEffect(
Catalog.locationLayer.pipe(
Layer.provideMerge(accounts),
Layer.provideMerge(EventV2.defaultLayer),
Layer.provideMerge(
Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("test") }))),
),
Layer.provideMerge(npmLayer),
),
)
describe("AzurePlugin", () => { describe("AzurePlugin", () => {
it.effect("resolves resourceName from env", () => it.effect("resolves resourceName from env", () =>
@@ -37,14 +12,13 @@ describe("AzurePlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* plugin.add(AzurePlugin) yield* catalog.transform((catalog) => {
const transform = yield* catalog.transform()
yield* transform((catalog) => {
catalog.provider.update(ProviderV2.ID.azure, (item) => { catalog.provider.update(ProviderV2.ID.azure, (item) => {
item.api = { type: "aisdk", package: "@ai-sdk/azure" } item.api = { type: "aisdk", package: "@ai-sdk/azure" }
}) })
}) })
expect((yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env") yield* addPlugin(plugin, AzurePlugin)
expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env")
}), }),
), ),
) )
@@ -54,9 +28,7 @@ describe("AzurePlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* plugin.add(AzurePlugin) yield* catalog.transform((catalog) => {
const transform = yield* catalog.transform()
yield* transform((catalog) => {
const azure = provider("azure", { const azure = provider("azure", {
api: { type: "aisdk", package: "@ai-sdk/azure" }, api: { type: "aisdk", package: "@ai-sdk/azure" },
request: { headers: {}, body: { resourceName: "from-config" } }, request: { headers: {}, body: { resourceName: "from-config" } },
@@ -67,50 +39,19 @@ describe("AzurePlugin", () => {
}) })
catalog.provider.update(ProviderV2.ID.openai, () => {}) catalog.provider.update(ProviderV2.ID.openai, () => {})
}) })
expect((yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-config") yield* addPlugin(plugin, AzurePlugin)
expect((yield* catalog.provider.get(ProviderV2.ID.openai)).request.body.resourceName).toBeUndefined() expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-config")
expect(required(yield* catalog.provider.get(ProviderV2.ID.openai)).request.body.resourceName).toBeUndefined()
}), }),
), ),
) )
itWithAccount.effect("prefers account resourceName over env", () =>
withEnv(
{
AZURE_RESOURCE_NAME: "from-env",
},
() =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const credentials = yield* Credential.Service
const catalog = yield* Catalog.Service
yield* credentials.create({
integrationID: Integration.ID.make("azure"),
value: new Credential.Key({
type: "key",
key: "key",
metadata: { resourceName: "from-account" },
}),
})
yield* plugin.add(AzurePlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) => {
catalog.provider.update(ProviderV2.ID.azure, (item) => {
item.api = { type: "aisdk", package: "@ai-sdk/azure" }
})
})
expect((yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-account")
}),
),
)
it.effect("falls back to env when configured resourceName is blank", () => it.effect("falls back to env when configured resourceName is blank", () =>
withEnv({ AZURE_RESOURCE_NAME: "from-env" }, () => withEnv({ AZURE_RESOURCE_NAME: "from-env" }, () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* plugin.add(AzurePlugin) yield* catalog.transform((catalog) => {
const transform = yield* catalog.transform()
yield* transform((catalog) => {
const azure = provider("azure", { const azure = provider("azure", {
api: { type: "aisdk", package: "@ai-sdk/azure" }, api: { type: "aisdk", package: "@ai-sdk/azure" },
request: { headers: {}, body: { resourceName: "" } }, request: { headers: {}, body: { resourceName: "" } },
@@ -120,7 +61,8 @@ describe("AzurePlugin", () => {
item.request = azure.request item.request = azure.request
}) })
}) })
expect((yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env") yield* addPlugin(plugin, AzurePlugin)
expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env")
}), }),
), ),
) )
@@ -130,9 +72,7 @@ describe("AzurePlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* plugin.add(AzurePlugin) yield* catalog.transform((catalog) => {
const transform = yield* catalog.transform()
yield* transform((catalog) => {
const azure = provider("azure", { const azure = provider("azure", {
api: { type: "aisdk", package: "@ai-sdk/azure" }, api: { type: "aisdk", package: "@ai-sdk/azure" },
request: { headers: {}, body: { resourceName: " " } }, request: { headers: {}, body: { resourceName: " " } },
@@ -142,7 +82,8 @@ describe("AzurePlugin", () => {
item.request = azure.request item.request = azure.request
}) })
}) })
expect((yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env") yield* addPlugin(plugin, AzurePlugin)
expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env")
}), }),
), ),
) )
@@ -151,7 +92,7 @@ describe("AzurePlugin", () => {
withEnv({ AZURE_RESOURCE_NAME: undefined }, () => withEnv({ AZURE_RESOURCE_NAME: undefined }, () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
yield* plugin.add(AzurePlugin) yield* addPlugin(plugin, AzurePlugin)
const result = yield* plugin.trigger( const result = yield* plugin.trigger(
"aisdk.sdk", "aisdk.sdk",
{ {
@@ -170,7 +111,7 @@ describe("AzurePlugin", () => {
withEnv({ AZURE_RESOURCE_NAME: undefined }, () => withEnv({ AZURE_RESOURCE_NAME: undefined }, () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
yield* plugin.add(AzurePlugin) yield* addPlugin(plugin, AzurePlugin)
const exit = yield* plugin const exit = yield* plugin
.trigger( .trigger(
"aisdk.sdk", "aisdk.sdk",
@@ -187,7 +128,7 @@ describe("AzurePlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const calls: string[] = [] const calls: string[] = []
yield* plugin.add(AzurePlugin) yield* addPlugin(plugin, AzurePlugin)
yield* plugin.trigger( yield* plugin.trigger(
"aisdk.language", "aisdk.language",
{ model: model("azure", "deployment"), sdk: fakeSelectorSdk(calls), options: { useCompletionUrls: true } }, { model: model("azure", "deployment"), sdk: fakeSelectorSdk(calls), options: { useCompletionUrls: true } },
@@ -201,7 +142,7 @@ describe("AzurePlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const calls: string[] = [] const calls: string[] = []
yield* plugin.add(AzurePlugin) yield* addPlugin(plugin, AzurePlugin)
yield* plugin.trigger( yield* plugin.trigger(
"aisdk.language", "aisdk.language",
{ model: model("azure", "deployment"), sdk: fakeSelectorSdk(calls), options: { useCompletionUrls: true } }, { model: model("azure", "deployment"), sdk: fakeSelectorSdk(calls), options: { useCompletionUrls: true } },
@@ -215,7 +156,7 @@ describe("AzurePlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const calls: string[] = [] const calls: string[] = []
yield* plugin.add(AzurePlugin) yield* addPlugin(plugin, AzurePlugin)
yield* plugin.trigger( yield* plugin.trigger(
"aisdk.language", "aisdk.language",
{ {
@@ -235,7 +176,7 @@ describe("AzurePlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const calls: string[] = [] const calls: string[] = []
yield* plugin.add(AzurePlugin) yield* addPlugin(plugin, AzurePlugin)
yield* plugin.trigger( yield* plugin.trigger(
"aisdk.language", "aisdk.language",
{ model: model("azure", "deployment"), sdk: fakeSelectorSdk(calls), options: {} }, { model: model("azure", "deployment"), sdk: fakeSelectorSdk(calls), options: {} },
@@ -259,7 +200,7 @@ describe("AzurePlugin", () => {
calls.push(`${method}:${id}`) calls.push(`${method}:${id}`)
return { modelId: id, provider: method, specificationVersion: "v3" } return { modelId: id, provider: method, specificationVersion: "v3" }
} }
yield* plugin.add(AzurePlugin) yield* addPlugin(plugin, AzurePlugin)
yield* plugin.trigger( yield* plugin.trigger(
"aisdk.language", "aisdk.language",
{ {
@@ -4,7 +4,7 @@ import { Catalog } from "@opencode-ai/core/catalog"
import { PluginV2 } from "@opencode-ai/core/plugin" import { PluginV2 } from "@opencode-ai/core/plugin"
import { CerebrasPlugin } from "@opencode-ai/core/plugin/provider/cerebras" import { CerebrasPlugin } from "@opencode-ai/core/plugin/provider/cerebras"
import { ProviderV2 } from "@opencode-ai/core/provider" import { ProviderV2 } from "@opencode-ai/core/provider"
import { it, model } from "./provider-helper" import { addPlugin, it, model, required } from "./provider-helper"
const cerebrasOptions: Record<string, unknown>[] = [] const cerebrasOptions: Record<string, unknown>[] = []
@@ -23,15 +23,14 @@ describe("CerebrasPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* plugin.add(CerebrasPlugin) yield* addPlugin(plugin, CerebrasPlugin)
const transform = yield* catalog.transform() yield* catalog.transform((catalog) => {
yield* transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("cerebras"), (item) => { catalog.provider.update(ProviderV2.ID.make("cerebras"), (item) => {
item.api = { type: "aisdk", package: "@ai-sdk/cerebras" } item.api = { type: "aisdk", package: "@ai-sdk/cerebras" }
item.request.headers.Existing = "1" item.request.headers.Existing = "1"
}) })
}) })
expect((yield* catalog.provider.get(ProviderV2.ID.make("cerebras"))).request.headers).toEqual({ expect(required(yield* catalog.provider.get(ProviderV2.ID.make("cerebras"))).request.headers).toEqual({
Existing: "1", Existing: "1",
"X-Cerebras-3rd-Party-Integration": "opencode", "X-Cerebras-3rd-Party-Integration": "opencode",
}) })
@@ -42,10 +41,9 @@ describe("CerebrasPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* plugin.add(CerebrasPlugin) yield* addPlugin(plugin, CerebrasPlugin)
const transform = yield* catalog.transform() yield* catalog.transform((catalog) => catalog.provider.update(ProviderV2.ID.make("groq"), () => {}))
yield* transform((catalog) => catalog.provider.update(ProviderV2.ID.make("groq"), () => {})) expect(required(yield* catalog.provider.get(ProviderV2.ID.make("groq"))).request.headers).toEqual({})
expect((yield* catalog.provider.get(ProviderV2.ID.make("groq"))).request.headers).toEqual({})
}), }),
) )
@@ -53,7 +51,7 @@ describe("CerebrasPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
cerebrasOptions.length = 0 cerebrasOptions.length = 0
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
yield* plugin.add(CerebrasPlugin) yield* addPlugin(plugin, CerebrasPlugin)
const result = yield* plugin.trigger( const result = yield* plugin.trigger(
"aisdk.sdk", "aisdk.sdk",
{ {
@@ -72,7 +70,7 @@ describe("CerebrasPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
cerebrasOptions.length = 0 cerebrasOptions.length = 0
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
yield* plugin.add(CerebrasPlugin) yield* addPlugin(plugin, CerebrasPlugin)
yield* plugin.trigger( yield* plugin.trigger(
"aisdk.sdk", "aisdk.sdk",
{ {
@@ -90,7 +88,7 @@ describe("CerebrasPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
cerebrasOptions.length = 0 cerebrasOptions.length = 0
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
yield* plugin.add(CerebrasPlugin) yield* addPlugin(plugin, CerebrasPlugin)
const result = yield* plugin.trigger( const result = yield* plugin.trigger(
"aisdk.sdk", "aisdk.sdk",
{ {

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