Compare commits

..
Author SHA1 Message Date
Kit Langton 9ff3f963f1 refactor(tool): convert multiedit tool to Tool.defineEffect
Migrate MultiEditTool from Tool.define to Tool.defineEffect pattern,
resolving EditTool via Effect.gen + Tool.init in the outer generator
and using Effect.promise + Effect.orDie in the execute body.
2026-04-10 15:42:38 -04:00
210 changed files with 63923 additions and 69763 deletions
-1
View File
@@ -26,7 +26,6 @@ kommander
r44vc0rp r44vc0rp
rekram1-node rekram1-node
-robinmordasiewicz -robinmordasiewicz
simonklee
-spider-yamet clawdbot/llm psychosis, spam pinging the team -spider-yamet clawdbot/llm psychosis, spam pinging the team
thdxr thdxr
-toastythebot -toastythebot
+25 -37
View File
@@ -319,7 +319,7 @@
"@actions/core": "1.11.1", "@actions/core": "1.11.1",
"@actions/github": "6.0.1", "@actions/github": "6.0.1",
"@agentclientprotocol/sdk": "0.16.1", "@agentclientprotocol/sdk": "0.16.1",
"@ai-sdk/amazon-bedrock": "4.0.93", "@ai-sdk/amazon-bedrock": "4.0.83",
"@ai-sdk/anthropic": "3.0.67", "@ai-sdk/anthropic": "3.0.67",
"@ai-sdk/azure": "3.0.49", "@ai-sdk/azure": "3.0.49",
"@ai-sdk/cerebras": "2.0.41", "@ai-sdk/cerebras": "2.0.41",
@@ -331,7 +331,7 @@
"@ai-sdk/groq": "3.0.31", "@ai-sdk/groq": "3.0.31",
"@ai-sdk/mistral": "3.0.27", "@ai-sdk/mistral": "3.0.27",
"@ai-sdk/openai": "3.0.48", "@ai-sdk/openai": "3.0.48",
"@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/openai-compatible": "2.0.37",
"@ai-sdk/perplexity": "3.0.26", "@ai-sdk/perplexity": "3.0.26",
"@ai-sdk/provider": "3.0.8", "@ai-sdk/provider": "3.0.8",
"@ai-sdk/provider-utils": "4.0.23", "@ai-sdk/provider-utils": "4.0.23",
@@ -341,6 +341,7 @@
"@aws-sdk/credential-providers": "3.993.0", "@aws-sdk/credential-providers": "3.993.0",
"@clack/prompts": "1.0.0-alpha.1", "@clack/prompts": "1.0.0-alpha.1",
"@effect/platform-node": "catalog:", "@effect/platform-node": "catalog:",
"@gitlab/gitlab-ai-provider": "3.6.0",
"@gitlab/opencode-gitlab-auth": "1.3.3", "@gitlab/opencode-gitlab-auth": "1.3.3",
"@hono/node-server": "1.19.11", "@hono/node-server": "1.19.11",
"@hono/node-ws": "1.3.0", "@hono/node-ws": "1.3.0",
@@ -356,7 +357,7 @@
"@opencode-ai/script": "workspace:*", "@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"@opencode-ai/util": "workspace:*", "@opencode-ai/util": "workspace:*",
"@openrouter/ai-sdk-provider": "2.5.1", "@openrouter/ai-sdk-provider": "2.4.2",
"@opentui/core": "0.1.97", "@opentui/core": "0.1.97",
"@opentui/solid": "0.1.97", "@opentui/solid": "0.1.97",
"@parcel/watcher": "2.5.1", "@parcel/watcher": "2.5.1",
@@ -412,7 +413,7 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.28.4", "@babel/core": "7.28.4",
"@effect/language-service": "0.84.2", "@effect/language-service": "0.79.0",
"@octokit/webhooks-types": "7.6.1", "@octokit/webhooks-types": "7.6.1",
"@opencode-ai/script": "workspace:*", "@opencode-ai/script": "workspace:*",
"@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-arm64": "2.5.1",
@@ -449,7 +450,6 @@
"version": "1.4.3", "version": "1.4.3",
"dependencies": { "dependencies": {
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"effect": "catalog:",
"zod": "catalog:", "zod": "catalog:",
}, },
"devDependencies": { "devDependencies": {
@@ -641,7 +641,7 @@
}, },
"catalog": { "catalog": {
"@cloudflare/workers-types": "4.20251008.0", "@cloudflare/workers-types": "4.20251008.0",
"@effect/platform-node": "4.0.0-beta.46", "@effect/platform-node": "4.0.0-beta.43",
"@hono/zod-validator": "0.4.2", "@hono/zod-validator": "0.4.2",
"@kobalte/core": "0.13.11", "@kobalte/core": "0.13.11",
"@lydell/node-pty": "1.2.0-beta.10", "@lydell/node-pty": "1.2.0-beta.10",
@@ -662,13 +662,13 @@
"@types/node": "22.13.9", "@types/node": "22.13.9",
"@types/semver": "7.7.1", "@types/semver": "7.7.1",
"@typescript/native-preview": "7.0.0-dev.20251207.1", "@typescript/native-preview": "7.0.0-dev.20251207.1",
"ai": "6.0.158", "ai": "6.0.149",
"cross-spawn": "7.0.6", "cross-spawn": "7.0.6",
"diff": "8.0.2", "diff": "8.0.2",
"dompurify": "3.3.1", "dompurify": "3.3.1",
"drizzle-kit": "1.0.0-beta.19-d95b7a4", "drizzle-kit": "1.0.0-beta.19-d95b7a4",
"drizzle-orm": "1.0.0-beta.19-d95b7a4", "drizzle-orm": "1.0.0-beta.19-d95b7a4",
"effect": "4.0.0-beta.46", "effect": "4.0.0-beta.43",
"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",
@@ -707,7 +707,7 @@
"@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.16.1", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-1ad+Sc/0sCtZGHthxxvgEUo5Wsbw16I+aF+YwdiLnPwkZG8KAGUEAPK6LM6Pf69lCyJPt1Aomk1d+8oE3C4ZEw=="], "@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.16.1", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-1ad+Sc/0sCtZGHthxxvgEUo5Wsbw16I+aF+YwdiLnPwkZG8KAGUEAPK6LM6Pf69lCyJPt1Aomk1d+8oE3C4ZEw=="],
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.93", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.69", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-hcXDU8QDwpAzLVTuY932TQVlIij9+iaVTxc5mPGY6yb//JMAAC5hMVhg93IrxlrxWLvMgjezNgoZGwquR+SGnw=="], "@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.83", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.64", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-DoRpvIWGU/r83UeJAM9L93Lca8Kf/yP5fIhfEOltMPGP/PXrGe0BZaz0maLSRn8djJ6+HzWIsgu5ZI6bZqXEXg=="],
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rwLi/Rsuj2pYniQXIrvClHvXDzgM4UQHHnvHTWEF14efnlKclG/1ghpNC+adsRujAbCTr6gRsSbDE2vEqriV7g=="], "@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rwLi/Rsuj2pYniQXIrvClHvXDzgM4UQHHnvHTWEF14efnlKclG/1ghpNC+adsRujAbCTr6gRsSbDE2vEqriV7g=="],
@@ -1025,11 +1025,11 @@
"@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/language-service": ["@effect/language-service@0.84.2", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-l04qNxpiA8rY5yXWckRPJ7Mk5MNerXuNymSFf+IdflfI5i8jgL1bpBNLuP6ijg7wgjdHc/KmTnCj2kT0SCntuA=="], "@effect/language-service": ["@effect/language-service@0.79.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-DEmIOsg1GjjP6s9HXH1oJrW+gDmzkhVv9WOZl6to5eNyyCrjz1S2PDqQ7aYrW/HuifhfwI5Bik1pK4pj7Z+lrg=="],
"@effect/platform-node": ["@effect/platform-node@4.0.0-beta.46", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.46", "mime": "^4.1.0", "undici": "^7.24.0" }, "peerDependencies": { "effect": "^4.0.0-beta.46", "ioredis": "^5.7.0" } }, "sha512-6AFRKjJO95dFl5lK/YnJi04uePjQDFi3+K1aXwcz/EfVlRwJ4+lg5O4vbievfKL/hnfcShVp3/eXnNS9tvlMZQ=="], "@effect/platform-node": ["@effect/platform-node@4.0.0-beta.43", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.43", "mime": "^4.1.0", "undici": "^7.24.0" }, "peerDependencies": { "effect": "^4.0.0-beta.43", "ioredis": "^5.7.0" } }, "sha512-Uq6E1rjaIpjHauzjwoB2HzAg3battYt2Boy8XO50GoHiWCXKE6WapYZ0/AnaBx5v5qg2sOfqpuiLsUf9ZgxOkA=="],
"@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.46", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.19.0" }, "peerDependencies": { "effect": "^4.0.0-beta.46" } }, "sha512-Yzci82XbZ1W3tuiownsJawrJZTGeTrTZKLD0uxdBWCBzlVyqDwoSwRwO5qh33DurJj9B7iS8MDf14fpGRBPNGQ=="], "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.43", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.19.0" }, "peerDependencies": { "effect": "^4.0.0-beta.43" } }, "sha512-A9q0GEb61pYcQ06Dr6gXj1nKlDI3KHsar1sk3qb1ZY+kVSR64tBAylI8zGon23KY+NPtTUj/sEIToB7jc3Qt5w=="],
"@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=="],
@@ -1161,6 +1161,8 @@
"@gar/promise-retry": ["@gar/promise-retry@1.0.3", "", {}, "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA=="], "@gar/promise-retry": ["@gar/promise-retry@1.0.3", "", {}, "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA=="],
"@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.6.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-8LmcIQ86xkMtC7L4P1/QYVEC+yKMTRerfPeniaaQGalnzXKtX6iMHLjLPOL9Rxp55lOXi6ed0WrFuJzZx+fNRg=="],
"@gitlab/opencode-gitlab-auth": ["@gitlab/opencode-gitlab-auth@1.3.3", "", { "dependencies": { "@fastify/rate-limit": "^10.2.0", "@opencode-ai/plugin": "*", "fastify": "^5.2.0", "open": "^10.0.0" } }, "sha512-FT+KsCmAJjtqWr1YAq0MywGgL9kaLQ4apmsoowAXrPqHtoYf2i/nY10/A+L06kNj22EATeEDRpbB1NWXMto/SA=="], "@gitlab/opencode-gitlab-auth": ["@gitlab/opencode-gitlab-auth@1.3.3", "", { "dependencies": { "@fastify/rate-limit": "^10.2.0", "@opencode-ai/plugin": "*", "fastify": "^5.2.0", "open": "^10.0.0" } }, "sha512-FT+KsCmAJjtqWr1YAq0MywGgL9kaLQ4apmsoowAXrPqHtoYf2i/nY10/A+L06kNj22EATeEDRpbB1NWXMto/SA=="],
"@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="], "@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="],
@@ -1537,7 +1539,7 @@
"@opencode-ai/web": ["@opencode-ai/web@workspace:packages/web"], "@opencode-ai/web": ["@opencode-ai/web@workspace:packages/web"],
"@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.5.1", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-r1fJL1Cb3gQDa2MpWH/sfx1BsEW0uzlRriJM6eihaKqbtKDmZoBisF32VcVaQYassighX7NGCkF68EsrZA43uQ=="], "@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.4.2", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-uRQZ4da77gru1I7/lNGJhKbqEIY7o/sPsLlbCM97VY9muGDjM/TaJzuwqIviqKTtXLzF0WDj5qBAi6FhxjvlSg=="],
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
@@ -2383,7 +2385,7 @@
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
"ai": ["ai@6.0.158", "", { "dependencies": { "@ai-sdk/gateway": "3.0.95", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-gLTp1UXFtMqKUi3XHs33K7UFglbvojkxF/aq337TxnLGOhHIW9+GyP2jwW4hYX87f1es+wId3VQoPRRu9zEStQ=="], "ai": ["ai@6.0.149", "", { "dependencies": { "@ai-sdk/gateway": "3.0.91", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3asRb/m3ZGH7H4+VTuTgj8eQYJZ9IJUmV0ljLslY92mQp6Zj+NVn4SmFj0TBr2Y/wFBWC3xgn++47tSGOXxdbw=="],
"ai-gateway-provider": ["ai-gateway-provider@3.1.2", "", { "optionalDependencies": { "@ai-sdk/amazon-bedrock": "^4.0.62", "@ai-sdk/anthropic": "^3.0.46", "@ai-sdk/azure": "^3.0.31", "@ai-sdk/cerebras": "^2.0.34", "@ai-sdk/cohere": "^3.0.21", "@ai-sdk/deepgram": "^2.0.20", "@ai-sdk/deepseek": "^2.0.20", "@ai-sdk/elevenlabs": "^2.0.20", "@ai-sdk/fireworks": "^2.0.34", "@ai-sdk/google": "^3.0.30", "@ai-sdk/google-vertex": "^4.0.61", "@ai-sdk/groq": "^3.0.24", "@ai-sdk/mistral": "^3.0.20", "@ai-sdk/openai": "^3.0.30", "@ai-sdk/perplexity": "^3.0.19", "@ai-sdk/xai": "^3.0.57", "@openrouter/ai-sdk-provider": "^2.2.3" }, "peerDependencies": { "@ai-sdk/openai-compatible": "^2.0.0", "@ai-sdk/provider": "^3.0.0", "@ai-sdk/provider-utils": "^4.0.0", "ai": "^6.0.0" } }, "sha512-krGNnJSoO/gJ7Hbe5nQDlsBpDUGIBGtMQTRUaW7s1MylsfvLduba0TLWzQaGtOmNRkP0pGhtGlwsnS6FNQMlyw=="], "ai-gateway-provider": ["ai-gateway-provider@3.1.2", "", { "optionalDependencies": { "@ai-sdk/amazon-bedrock": "^4.0.62", "@ai-sdk/anthropic": "^3.0.46", "@ai-sdk/azure": "^3.0.31", "@ai-sdk/cerebras": "^2.0.34", "@ai-sdk/cohere": "^3.0.21", "@ai-sdk/deepgram": "^2.0.20", "@ai-sdk/deepseek": "^2.0.20", "@ai-sdk/elevenlabs": "^2.0.20", "@ai-sdk/fireworks": "^2.0.34", "@ai-sdk/google": "^3.0.30", "@ai-sdk/google-vertex": "^4.0.61", "@ai-sdk/groq": "^3.0.24", "@ai-sdk/mistral": "^3.0.20", "@ai-sdk/openai": "^3.0.30", "@ai-sdk/perplexity": "^3.0.19", "@ai-sdk/xai": "^3.0.57", "@openrouter/ai-sdk-provider": "^2.2.3" }, "peerDependencies": { "@ai-sdk/openai-compatible": "^2.0.0", "@ai-sdk/provider": "^3.0.0", "@ai-sdk/provider-utils": "^4.0.0", "ai": "^6.0.0" } }, "sha512-krGNnJSoO/gJ7Hbe5nQDlsBpDUGIBGtMQTRUaW7s1MylsfvLduba0TLWzQaGtOmNRkP0pGhtGlwsnS6FNQMlyw=="],
@@ -2887,7 +2889,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.46", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-3f6gXvvUMtEueCRY0tU76Vq2Pej1SAwwE+s0Owd5nD53yS5n4RZhUA1rlCGFuSbQFA225pGy8vO72+lpvu7u5A=="], "effect": ["effect@4.0.0-beta.43", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-AJYyDimIwJOn87uUz/JzmgDc5GfjxJbXvEbTvNzMa+M3Uer344bLo/O5mMRkqc1vBleA+Ygs4+dbE3QsqOkKTQ=="],
"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=="],
@@ -5003,11 +5005,7 @@
"@actions/http-client/undici": ["undici@6.24.1", "", {}, "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA=="], "@actions/http-client/undici": ["undici@6.24.1", "", {}, "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA=="],
"@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.69", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-LshR7X3pFugY0o41G2VKTmg1XoGpSl7uoYWfzk6zjVZLhCfeFiwgpOga+eTV4XY1VVpZwKVqRnkDbIL7K2eH5g=="], "@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
"@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.12", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA=="],
"@ai-sdk/amazon-bedrock/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
"@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], "@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
@@ -5283,6 +5281,10 @@
"@fastify/proxy-addr/ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="], "@fastify/proxy-addr/ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="],
"@gitlab/gitlab-ai-provider/openai": ["openai@6.33.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-xAYN1W3YsDXJWA5F277135YfkEk6H7D3D6vWwRhJ3OEkzRgcyK8z/P5P9Gyi/wB4N8kK9kM5ZjprfvyHagKmpw=="],
"@gitlab/gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@gitlab/opencode-gitlab-auth/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], "@gitlab/opencode-gitlab-auth/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
"@hey-api/openapi-ts/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="], "@hey-api/openapi-ts/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="],
@@ -5507,10 +5509,6 @@
"@solidjs/start/vite-plugin-solid": ["vite-plugin-solid@2.11.11", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-YMZCXsLw9kyuvQFEdwLP27fuTQJLmjNoHy90AOJnbRuJ6DwShUxKFo38gdFrWn9v11hnGicKCZEaeI/TFs6JKw=="], "@solidjs/start/vite-plugin-solid": ["vite-plugin-solid@2.11.11", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-YMZCXsLw9kyuvQFEdwLP27fuTQJLmjNoHy90AOJnbRuJ6DwShUxKFo38gdFrWn9v11hnGicKCZEaeI/TFs6JKw=="],
"@standard-community/standard-json/effect": ["effect@4.0.0-beta.43", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-AJYyDimIwJOn87uUz/JzmgDc5GfjxJbXvEbTvNzMa+M3Uer344bLo/O5mMRkqc1vBleA+Ygs4+dbE3QsqOkKTQ=="],
"@standard-community/standard-openapi/effect": ["effect@4.0.0-beta.43", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-AJYyDimIwJOn87uUz/JzmgDc5GfjxJbXvEbTvNzMa+M3Uer344bLo/O5mMRkqc1vBleA+Ygs4+dbE3QsqOkKTQ=="],
"@tailwindcss/oxide/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "@tailwindcss/oxide/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="],
@@ -5551,9 +5549,7 @@
"accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"ai/@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.95", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZmUNNbZl3V42xwQzPaNUi+s8eqR2lnrxf0bvB6YbLXpLjHYv0k2Y78t12cNOfY0bxGeuVVTLyk856uLuQIuXEQ=="], "ai/@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.91", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-J39Dh6Gyg6HjG3A7OFKnJMp3QyZ3Eex+XDiX8aFBdRwwZm3jGWaMhkCxQPH7yiQ9kRiErZwHXX/Oexx4SyGGGA=="],
"ai-gateway-provider/@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.83", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.64", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-DoRpvIWGU/r83UeJAM9L93Lca8Kf/yP5fIhfEOltMPGP/PXrGe0BZaz0maLSRn8djJ6+HzWIsgu5ZI6bZqXEXg=="],
"ai-gateway-provider/@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.3.3", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-4fVteGkVedc7fGoA9+qJs4tpYwALezMq14m2Sjub3KmyRlksCbK+WJf67NPdGem8+NZrV2tAN42A1NU3+SiV3w=="], "ai-gateway-provider/@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.3.3", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-4fVteGkVedc7fGoA9+qJs4tpYwALezMq14m2Sjub3KmyRlksCbK+WJf67NPdGem8+NZrV2tAN42A1NU3+SiV3w=="],
@@ -5769,8 +5765,6 @@
"opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.67", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-FFX4P5Fd6lcQJc2OLngZQkbbJHa0IDDZi087Edb8qRZx6h90krtM61ArbMUL8us/7ZUwojCXnyJ/wQ2Eflx2jQ=="], "opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.67", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-FFX4P5Fd6lcQJc2OLngZQkbbJHa0IDDZi087Edb8qRZx6h90krtM61ArbMUL8us/7ZUwojCXnyJ/wQ2Eflx2jQ=="],
"opencode/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
"opencontrol/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.6.1", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="], "opencontrol/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.6.1", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="],
"opencontrol/@tsconfig/bun": ["@tsconfig/bun@1.0.7", "", {}, "sha512-udGrGJBNQdXGVulehc1aWT73wkR9wdaGBtB6yL70RJsqwW/yJhIg6ZbRlPOfIUiFNrnBuYLBi9CSmMKfDC7dvA=="], "opencontrol/@tsconfig/bun": ["@tsconfig/bun@1.0.7", "", {}, "sha512-udGrGJBNQdXGVulehc1aWT73wkR9wdaGBtB6yL70RJsqwW/yJhIg6ZbRlPOfIUiFNrnBuYLBi9CSmMKfDC7dvA=="],
@@ -5953,6 +5947,8 @@
"@actions/github/@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="], "@actions/github/@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="],
"@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/azure/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@ai-sdk/azure/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
@@ -6439,18 +6435,12 @@
"@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=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], "@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="],
"accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"ai-gateway-provider/@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
"ajv-keywords/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "ajv-keywords/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
"ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
@@ -6823,8 +6813,6 @@
"@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="], "@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="],
"ai-gateway-provider/@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], "app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
+4 -4
View File
@@ -1,8 +1,8 @@
{ {
"nodeModules": { "nodeModules": {
"x86_64-linux": "sha256-fNRQYkucjXr1D61HJRScJpDa6+oBdyhgTBxCu+PE2kQ=", "x86_64-linux": "sha256-285KZ7rZLRoc6XqCZRHc25NE+mmpGh/BVeMpv8aPQtQ=",
"aarch64-linux": "sha256-V8J6kn2nSdXrplyqi6aIqNlHcVjSxvye+yC/YFO7PF4=", "aarch64-linux": "sha256-qIwmY4TP4CI7R7G6A5OMYRrorVNXjkg25tTtVpIHm2o=",
"aarch64-darwin": "sha256-6cLmUJVUycGALCmslXuloVGBSlFOSHRjsWjx7KOW8rg=", "aarch64-darwin": "sha256-RwvnZQhdYZ0u7h7evyfxuPLHHX9eO/jXTAxIFc8B+IE=",
"x86_64-darwin": "sha256-kcOSO3NFIJh79ylLotG41ovWLQfH5kh1WYFghUu+4HE=" "x86_64-darwin": "sha256-vVj40al+TEeMpbe5XG2GmJEpN+eQAvtr9W0T98l5PBE="
} }
} }
+3 -3
View File
@@ -26,7 +26,7 @@
"packages/slack" "packages/slack"
], ],
"catalog": { "catalog": {
"@effect/platform-node": "4.0.0-beta.46", "@effect/platform-node": "4.0.0-beta.43",
"@types/bun": "1.3.11", "@types/bun": "1.3.11",
"@types/cross-spawn": "6.0.6", "@types/cross-spawn": "6.0.6",
"@octokit/rest": "22.0.0", "@octokit/rest": "22.0.0",
@@ -47,8 +47,8 @@
"dompurify": "3.3.1", "dompurify": "3.3.1",
"drizzle-kit": "1.0.0-beta.19-d95b7a4", "drizzle-kit": "1.0.0-beta.19-d95b7a4",
"drizzle-orm": "1.0.0-beta.19-d95b7a4", "drizzle-orm": "1.0.0-beta.19-d95b7a4",
"effect": "4.0.0-beta.46", "effect": "4.0.0-beta.43",
"ai": "6.0.158", "ai": "6.0.149",
"cross-spawn": "7.0.6", "cross-spawn": "7.0.6",
"hono": "4.10.7", "hono": "4.10.7",
"hono-openapi": "1.1.2", "hono-openapi": "1.1.2",
@@ -316,8 +316,7 @@
/* Download Hero Section */ /* Download Hero Section */
[data-component="download-hero"] { [data-component="download-hero"] {
/* display: grid; */ display: grid;
display: none;
grid-template-columns: 260px 1fr; grid-template-columns: 260px 1fr;
gap: 4rem; gap: 4rem;
padding-bottom: 2rem; padding-bottom: 2rem;
+5 -9
View File
@@ -39,16 +39,11 @@
"bun": "./src/pty/pty.bun.ts", "bun": "./src/pty/pty.bun.ts",
"node": "./src/pty/pty.node.ts", "node": "./src/pty/pty.node.ts",
"default": "./src/pty/pty.bun.ts" "default": "./src/pty/pty.bun.ts"
},
"#hono": {
"bun": "./src/server/adapter.bun.ts",
"node": "./src/server/adapter.node.ts",
"default": "./src/server/adapter.bun.ts"
} }
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.28.4", "@babel/core": "7.28.4",
"@effect/language-service": "0.84.2", "@effect/language-service": "0.79.0",
"@octokit/webhooks-types": "7.6.1", "@octokit/webhooks-types": "7.6.1",
"@opencode-ai/script": "workspace:*", "@opencode-ai/script": "workspace:*",
"@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-arm64": "2.5.1",
@@ -83,7 +78,7 @@
"@actions/core": "1.11.1", "@actions/core": "1.11.1",
"@actions/github": "6.0.1", "@actions/github": "6.0.1",
"@agentclientprotocol/sdk": "0.16.1", "@agentclientprotocol/sdk": "0.16.1",
"@ai-sdk/amazon-bedrock": "4.0.93", "@ai-sdk/amazon-bedrock": "4.0.83",
"@ai-sdk/anthropic": "3.0.67", "@ai-sdk/anthropic": "3.0.67",
"@ai-sdk/azure": "3.0.49", "@ai-sdk/azure": "3.0.49",
"@ai-sdk/cerebras": "2.0.41", "@ai-sdk/cerebras": "2.0.41",
@@ -95,7 +90,7 @@
"@ai-sdk/groq": "3.0.31", "@ai-sdk/groq": "3.0.31",
"@ai-sdk/mistral": "3.0.27", "@ai-sdk/mistral": "3.0.27",
"@ai-sdk/openai": "3.0.48", "@ai-sdk/openai": "3.0.48",
"@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/openai-compatible": "2.0.37",
"@ai-sdk/perplexity": "3.0.26", "@ai-sdk/perplexity": "3.0.26",
"@ai-sdk/provider": "3.0.8", "@ai-sdk/provider": "3.0.8",
"@ai-sdk/provider-utils": "4.0.23", "@ai-sdk/provider-utils": "4.0.23",
@@ -105,6 +100,7 @@
"@aws-sdk/credential-providers": "3.993.0", "@aws-sdk/credential-providers": "3.993.0",
"@clack/prompts": "1.0.0-alpha.1", "@clack/prompts": "1.0.0-alpha.1",
"@effect/platform-node": "catalog:", "@effect/platform-node": "catalog:",
"@gitlab/gitlab-ai-provider": "3.6.0",
"@gitlab/opencode-gitlab-auth": "1.3.3", "@gitlab/opencode-gitlab-auth": "1.3.3",
"@hono/node-server": "1.19.11", "@hono/node-server": "1.19.11",
"@hono/node-ws": "1.3.0", "@hono/node-ws": "1.3.0",
@@ -120,7 +116,7 @@
"@opencode-ai/script": "workspace:*", "@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"@opencode-ai/util": "workspace:*", "@opencode-ai/util": "workspace:*",
"@openrouter/ai-sdk-provider": "2.5.1", "@openrouter/ai-sdk-provider": "2.4.2",
"@opentui/core": "0.1.97", "@opentui/core": "0.1.97",
"@opentui/solid": "0.1.97", "@opentui/solid": "0.1.97",
"@parcel/watcher": "2.5.1", "@parcel/watcher": "2.5.1",
+10 -87
View File
@@ -23,7 +23,7 @@ export namespace Foo {
readonly get: (id: FooID) => Effect.Effect<FooInfo, FooError> readonly get: (id: FooID) => Effect.Effect<FooInfo, FooError>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/Foo") {} export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Foo") {}
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
@@ -217,37 +217,36 @@ Fully migrated (single namespace, InstanceState where needed, flattened facade):
- [x] `SessionSummary``session/summary.ts` - [x] `SessionSummary``session/summary.ts`
- [x] `SessionRevert``session/revert.ts` - [x] `SessionRevert``session/revert.ts`
- [x] `Instruction``session/instruction.ts` - [x] `Instruction``session/instruction.ts`
- [x] `SystemPrompt``session/system.ts`
- [x] `Provider``provider/provider.ts` - [x] `Provider``provider/provider.ts`
- [x] `Storage``storage/storage.ts` - [x] `Storage``storage/storage.ts`
- [x] `ShareNext``share/share-next.ts`
Still open: Still open:
- [x] `SessionTodo``session/todo.ts` - [ ] `SessionTodo``session/todo.ts`
- [ ] `ShareNext``share/share-next.ts`
- [ ] `SyncEvent``sync/index.ts` - [ ] `SyncEvent``sync/index.ts`
- [ ] `Workspace``control-plane/workspace.ts` - [ ] `Workspace``control-plane/workspace.ts`
## Tool interface → Effect ## Tool interface → Effect
`Tool.Def.execute` and `Tool.Info.init` already return `Effect` on this branch. Tool definitions should now stay Effect-native all the way through initialization instead of using Promise-returning init callbacks. Tools can still use lazy init callbacks when they need instance-bound state at init time, but those callbacks should return `Effect`, not `Promise`. Remaining work is: Once individual tools are effectified, change `Tool.Info` (`tool/tool.ts`) so `init` and `execute` return `Effect` instead of `Promise`. This lets tool implementations compose natively with the Effect pipeline rather than being wrapped in `Effect.promise()` at the call site. Requires:
1. Migrate each tool body to return Effects 1. Migrate each tool to return Effects
2. Keep `Tool.define()` inputs Effect-native 2. Update `Tool.define()` factory to work with Effects
3. Update remaining callers to `yield*` tool initialization instead of `await`ing 3. Update `SessionPrompt` to `yield*` tool results instead of `await`ing
### Tool migration details ### Tool migration details
With `Tool.Info.init()` now effectful, use this transitional pattern for migrated tools that still need Promise-based boundaries internally: Until the tool interface itself returns `Effect`, use this transitional pattern for migrated tools:
- `Tool.defineEffect(...)` should `yield*` the services the tool depends on and close over them in the returned tool definition. - `Tool.defineEffect(...)` should `yield*` the services the tool depends on and close over them in the returned tool definition.
- Keep the bridge at the Promise boundary only inside the tool body when required by external APIs. Do not return Promise-based init callbacks from `Tool.define()`. - Keep the bridge at the Promise boundary only. Prefer a single `Effect.runPromise(...)` in the temporary `async execute(...)` implementation, and move the inner logic into `Effect.fn(...)` helpers instead of scattering `runPromise` islands through the tool body.
- If a tool starts requiring new services, wire them into `ToolRegistry.defaultLayer` so production callers resolve the same dependencies as tests. - If a tool starts requiring new services, wire them into `ToolRegistry.defaultLayer` so production callers resolve the same dependencies as tests.
Tool tests should use the existing Effect helpers in `packages/opencode/test/lib/effect.ts`: Tool tests should use the existing Effect helpers in `packages/opencode/test/lib/effect.ts`:
- Use `testEffect(...)` / `it.live(...)` instead of creating fake local wrappers around effectful tools. - Use `testEffect(...)` / `it.live(...)` instead of creating fake local wrappers around effectful tools.
- Yield the real tool export, then initialize it: `const info = yield* ReadTool`, `const tool = yield* info.init()`. - Yield the real tool export, then initialize it: `const info = yield* ReadTool`, `const tool = yield* Effect.promise(() => info.init())`.
- Run tests inside a real instance with `provideTmpdirInstance(...)` or `provideInstance(tmpdirScoped(...))` so instance-scoped services resolve exactly as they do in production. - Run tests inside a real instance with `provideTmpdirInstance(...)` or `provideInstance(tmpdirScoped(...))` so instance-scoped services resolve exactly as they do in production.
This keeps migrated tool tests aligned with the production service graph today, and makes the eventual `Tool.Info``Effect` cleanup mostly mechanical later. This keeps migrated tool tests aligned with the production service graph today, and makes the eventual `Tool.Info``Effect` cleanup mostly mechanical later.
@@ -309,79 +308,3 @@ Current raw fs users that will convert during tool migration:
- [ ] `util/flock.ts` — file-based distributed lock with heartbeat → Effect.repeat + addFinalizer - [ ] `util/flock.ts` — file-based distributed lock with heartbeat → Effect.repeat + addFinalizer
- [ ] `util/process.ts` — child process spawn wrapper → return Effect instead of Promise - [ ] `util/process.ts` — child process spawn wrapper → return Effect instead of Promise
- [ ] `util/lazy.ts` — replace uses in Effect code with Effect.cached; keep for sync-only code - [ ] `util/lazy.ts` — replace uses in Effect code with Effect.cached; keep for sync-only code
## Destroying the facades
Every service currently exports async facade functions at the bottom of its namespace — `export async function read(...) { return runPromise(...) }` — backed by a per-service `makeRuntime`. These exist because cyclic imports used to force each service to build its own independent runtime. Now that the layer DAG is acyclic and `AppRuntime` (`src/effect/app-runtime.ts`) composes everything into one `ManagedRuntime`, we're removing them.
### Process
For each service, the migration is roughly:
1. **Find callers.** `grep -n "Namespace\.(methodA|methodB|...)"` across `src/` and `test/`. Skip the service file itself.
2. **Migrate production callers.** For each effectful caller that does `Effect.tryPromise(() => Namespace.method(...))`:
- Add the service to the caller's layer R type (`Layer.Layer<Self, never, ... | Namespace.Service>`)
- Yield it at the top of the layer: `const ns = yield* Namespace.Service`
- Replace `Effect.tryPromise(() => Namespace.method(...))` with `yield* ns.method(...)` (or `ns.method(...).pipe(Effect.orElseSucceed(...))` for the common fallback case)
- Add `Layer.provide(Namespace.defaultLayer)` to the caller's own `defaultLayer` chain
3. **Fix tests that used the caller's raw `.layer`.** Any test that composes `Caller.layer` (not `defaultLayer`) needs to also provide the newly-required service tag. The fastest fix is usually switching to `Caller.defaultLayer` since it now pulls in the new dependency.
4. **Migrate test callers of the facade.** Tests calling `Namespace.method(...)` directly get converted to full effectful style using `testEffect(Namespace.defaultLayer)` + `it.live` / `it.effect` + `yield* svc.method(...)`. Don't wrap the test body in `Effect.promise(async () => {...})` — do the whole thing in `Effect.gen` and use `AppFileSystem.Service` / `tmpdirScoped` / `Effect.addFinalizer` for what used to be raw `fs` / `Bun.write` / `try/finally`.
5. **Delete the facades.** Once `grep` shows zero callers, remove the `export async function` block AND the `makeRuntime(...)` line from the service namespace. Also remove the now-unused `import { makeRuntime }`.
### Pitfalls
- **Layer caching inside tests.** `testEffect(layer)` constructs the Storage (or whatever) service once and memoizes it. If a test then tries `inner.pipe(Effect.provide(customStorage))` to swap in a differently-configured Storage, the outer cached one wins and the inner provision is a no-op. Fix: wrap the overriding layer in `Layer.fresh(...)`, which forces a new instance to be built instead of hitting the memoMap cache. This lets a single `testEffect(...)` serve both simple and per-test-customized cases.
- **`Effect.tryPromise``yield*` drops the Promise layer.** The old code was `Effect.tryPromise(() => Storage.read(...))` — a `tryPromise` wrapper because the facade returned a Promise. The new code is `yield* storage.read(...)` directly — the service method already returns an Effect, so no wrapper is needed. Don't reach for `Effect.promise` or `Effect.tryPromise` during migration; if you're using them on a service method call, you're doing it wrong.
- **Raw `.layer` test callers break silently in the type checker.** When you add a new R requirement to a service's `.layer`, any test that composes it raw (not `defaultLayer`) becomes under-specified. `tsgo` will flag this — the error looks like `Type 'Storage.Service' is not assignable to type '... | Service | TestConsole'`. Usually the fix is to switch that composition to `defaultLayer`, or add `Layer.provide(NewDep.defaultLayer)` to the custom composition.
- **Tests that do async setup with `fs`, `Bun.write`, `tmpdir`.** Convert these to `AppFileSystem.Service` calls inside `Effect.gen`, and use `tmpdirScoped()` instead of `tmpdir()` so cleanup happens via the scope finalizer. For file operations on the actual filesystem (not via a service), a small helper like `const writeJson = Effect.fnUntraced(function* (file, value) { const fs = yield* AppFileSystem.Service; yield* fs.makeDirectory(path.dirname(file), { recursive: true }); yield* fs.writeFileString(file, JSON.stringify(value, null, 2)) })` keeps the migration tests clean.
### Migration log
- `SessionStatus` — migrated 2026-04-11. Replaced the last route and retry-policy callers with `AppRuntime.runPromise(SessionStatus.Service.use(...))` and removed the `makeRuntime(...)` facade.
- `ShareNext` — migrated 2026-04-11. Swapped remaining async callers to `AppRuntime.runPromise(ShareNext.Service.use(...))`, removed the `makeRuntime(...)` facade, and kept instance bootstrap on the shared app runtime.
- `SessionTodo` — migrated 2026-04-10. Already matched the target service shape in `session/todo.ts`: single namespace, traced Effect methods, and no `makeRuntime(...)` facade remained; checklist updated to reflect the completed migration.
- `Storage` — migrated 2026-04-10. One production caller (`Session.diff`) and all storage.test.ts tests converted to effectful style. Facades and `makeRuntime` removed.
- `SessionRunState` — migrated 2026-04-11. Single caller in `server/routes/session.ts` converted; facade removed.
- `Account` — migrated 2026-04-11. Callers in `server/routes/experimental.ts` and `cli/cmd/account.ts` converted; facade removed.
- `Instruction` — migrated 2026-04-11. Test-only callers converted; facade removed.
- `FileTime` — migrated 2026-04-11. Test-only callers converted; facade removed.
- `FileWatcher` — migrated 2026-04-11. Callers in `project/bootstrap.ts` and test converted; facade removed.
- `Question` — migrated 2026-04-11. Callers in `server/routes/question.ts` and test converted; facade removed.
- `Truncate` — migrated 2026-04-11. Caller in `tool/tool.ts` and test converted; facade removed.
## Route handler effectification
Route handlers should wrap their entire body in a single `AppRuntime.runPromise(Effect.gen(...))` call, yielding services from context rather than calling facades one-by-one. This eliminates multiple `runPromise` round-trips and lets handlers compose naturally.
```ts
// Before — one facade call per service
;async (c) => {
await SessionRunState.assertNotBusy(id)
await Session.removeMessage({ sessionID: id, messageID })
return c.json(true)
}
// After — one Effect.gen, yield services from context
;async (c) => {
await AppRuntime.runPromise(
Effect.gen(function* () {
const state = yield* SessionRunState.Service
const session = yield* Session.Service
yield* state.assertNotBusy(id)
yield* session.removeMessage({ sessionID: id, messageID })
}),
)
return c.json(true)
}
```
When migrating, always use `{ concurrency: "unbounded" }` with `Effect.all` — route handlers should run independent service calls in parallel, not sequentially.
Route files to convert (each handler that calls facades should be wrapped):
- [ ] `server/routes/session.ts` — heaviest; uses Session, SessionPrompt, SessionRevert, SessionCompaction, SessionShare, SessionSummary, SessionRunState, Agent, Permission, Bus
- [ ] `server/routes/global.ts` — uses Config, Project, Provider, Vcs, Snapshot, Agent
- [ ] `server/routes/provider.ts` — uses Provider, Auth, Config
- [ ] `server/routes/question.ts` — uses Question
- [ ] `server/routes/pty.ts` — uses Pty
- [ ] `server/routes/experimental.ts` — uses Account, ToolRegistry, Agent, MCP, Config
+17 -2
View File
@@ -1,4 +1,4 @@
import { Cache, Clock, Duration, Effect, Layer, Option, Schema, SchemaGetter, Context } from "effect" import { Cache, Clock, Duration, Effect, Layer, Option, Schema, SchemaGetter, ServiceMap } from "effect"
import { import {
FetchHttpClient, FetchHttpClient,
HttpClient, HttpClient,
@@ -7,6 +7,7 @@ import {
HttpClientResponse, HttpClientResponse,
} from "effect/unstable/http" } from "effect/unstable/http"
import { makeRuntime } from "@/effect/run-service"
import { withTransientReadRetry } from "@/util/effect-http-client" import { withTransientReadRetry } from "@/util/effect-http-client"
import { AccountRepo, type AccountRow } from "./repo" import { AccountRepo, type AccountRow } from "./repo"
import { normalizeServerUrl } from "./url" import { normalizeServerUrl } from "./url"
@@ -180,7 +181,7 @@ export namespace Account {
readonly poll: (input: Login) => Effect.Effect<PollResult, AccountError> readonly poll: (input: Login) => Effect.Effect<PollResult, AccountError>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/Account") {} export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Account") {}
export const layer: Layer.Layer<Service, never, AccountRepo | HttpClient.HttpClient> = Layer.effect( export const layer: Layer.Layer<Service, never, AccountRepo | HttpClient.HttpClient> = Layer.effect(
Service, Service,
@@ -453,4 +454,18 @@ export namespace Account {
) )
export const defaultLayer = layer.pipe(Layer.provide(AccountRepo.layer), Layer.provide(FetchHttpClient.layer)) export const defaultLayer = layer.pipe(Layer.provide(AccountRepo.layer), Layer.provide(FetchHttpClient.layer))
export const { runPromise } = makeRuntime(Service, defaultLayer)
export async function active(): Promise<Info | undefined> {
return Option.getOrUndefined(await runPromise((service) => service.active()))
}
export async function orgsByAccount(): Promise<readonly AccountOrgs[]> {
return runPromise((service) => service.orgsByAccount())
}
export async function switchOrg(accountID: AccountID, orgID: OrgID) {
return runPromise((service) => service.use(accountID, Option.some(orgID)))
}
} }
+2 -2
View File
@@ -1,5 +1,5 @@
import { eq } from "drizzle-orm" import { eq } from "drizzle-orm"
import { Effect, Layer, Option, Schema, Context } from "effect" import { Effect, Layer, Option, Schema, ServiceMap } from "effect"
import { Database } from "@/storage/db" import { Database } from "@/storage/db"
import { AccountStateTable, AccountTable } from "./account.sql" import { AccountStateTable, AccountTable } from "./account.sql"
@@ -38,7 +38,7 @@ export namespace AccountRepo {
} }
} }
export class AccountRepo extends Context.Service<AccountRepo, AccountRepo.Service>()("@opencode/AccountRepo") { export class AccountRepo extends ServiceMap.Service<AccountRepo, AccountRepo.Service>()("@opencode/AccountRepo") {
static readonly layer: Layer.Layer<AccountRepo> = Layer.effect( static readonly layer: Layer.Layer<AccountRepo> = Layer.effect(
AccountRepo, AccountRepo,
Effect.gen(function* () { Effect.gen(function* () {
+26 -6
View File
@@ -1,22 +1,42 @@
import { Schema } from "effect" import { Schema } from "effect"
import type * as HttpClientError from "effect/unstable/http/HttpClientError" import type * as HttpClientError from "effect/unstable/http/HttpClientError"
export const AccountID = Schema.String.pipe(Schema.brand("AccountID")) import { withStatics } from "@/util/schema"
export const AccountID = Schema.String.pipe(
Schema.brand("AccountID"),
withStatics((s) => ({ make: (id: string) => s.makeUnsafe(id) })),
)
export type AccountID = Schema.Schema.Type<typeof AccountID> export type AccountID = Schema.Schema.Type<typeof AccountID>
export const OrgID = Schema.String.pipe(Schema.brand("OrgID")) export const OrgID = Schema.String.pipe(
Schema.brand("OrgID"),
withStatics((s) => ({ make: (id: string) => s.makeUnsafe(id) })),
)
export type OrgID = Schema.Schema.Type<typeof OrgID> export type OrgID = Schema.Schema.Type<typeof OrgID>
export const AccessToken = Schema.String.pipe(Schema.brand("AccessToken")) export const AccessToken = Schema.String.pipe(
Schema.brand("AccessToken"),
withStatics((s) => ({ make: (token: string) => s.makeUnsafe(token) })),
)
export type AccessToken = Schema.Schema.Type<typeof AccessToken> export type AccessToken = Schema.Schema.Type<typeof AccessToken>
export const RefreshToken = Schema.String.pipe(Schema.brand("RefreshToken")) export const RefreshToken = Schema.String.pipe(
Schema.brand("RefreshToken"),
withStatics((s) => ({ make: (token: string) => s.makeUnsafe(token) })),
)
export type RefreshToken = Schema.Schema.Type<typeof RefreshToken> export type RefreshToken = Schema.Schema.Type<typeof RefreshToken>
export const DeviceCode = Schema.String.pipe(Schema.brand("DeviceCode")) export const DeviceCode = Schema.String.pipe(
Schema.brand("DeviceCode"),
withStatics((s) => ({ make: (code: string) => s.makeUnsafe(code) })),
)
export type DeviceCode = Schema.Schema.Type<typeof DeviceCode> export type DeviceCode = Schema.Schema.Type<typeof DeviceCode>
export const UserCode = Schema.String.pipe(Schema.brand("UserCode")) export const UserCode = Schema.String.pipe(
Schema.brand("UserCode"),
withStatics((s) => ({ make: (code: string) => s.makeUnsafe(code) })),
)
export type UserCode = Schema.Schema.Type<typeof UserCode> export type UserCode = Schema.Schema.Type<typeof UserCode>
export class Info extends Schema.Class<Info>("Account")({ export class Info extends Schema.Class<Info>("Account")({
+5 -3
View File
@@ -19,7 +19,7 @@ import { Global } from "@/global"
import path from "path" import path from "path"
import { Plugin } from "@/plugin" import { Plugin } from "@/plugin"
import { Skill } from "../skill" import { Skill } from "../skill"
import { Effect, Context, Layer } from "effect" import { Effect, ServiceMap, Layer } from "effect"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { makeRuntime } from "@/effect/run-service" import { makeRuntime } from "@/effect/run-service"
@@ -67,7 +67,7 @@ export namespace Agent {
type State = Omit<Interface, "generate"> type State = Omit<Interface, "generate">
export class Service extends Context.Service<Service, Interface>()("@opencode/Agent") {} export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Agent") {}
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
@@ -398,11 +398,13 @@ export namespace Agent {
}), }),
) )
export const defaultLayer = layer.pipe( export const defaultLayer = Layer.suspend(() =>
layer.pipe(
Layer.provide(Provider.defaultLayer), Layer.provide(Provider.defaultLayer),
Layer.provide(Auth.defaultLayer), Layer.provide(Auth.defaultLayer),
Layer.provide(Config.defaultLayer), Layer.provide(Config.defaultLayer),
Layer.provide(Skill.defaultLayer), Layer.provide(Skill.defaultLayer),
),
) )
const { runPromise } = makeRuntime(Service, defaultLayer) const { runPromise } = makeRuntime(Service, defaultLayer)
+2 -2
View File
@@ -1,5 +1,5 @@
import path from "path" import path from "path"
import { Effect, Layer, Record, Result, Schema, Context } from "effect" import { Effect, Layer, Record, Result, Schema, ServiceMap } from "effect"
import { makeRuntime } from "@/effect/run-service" import { makeRuntime } from "@/effect/run-service"
import { zod } from "@/util/effect-zod" import { zod } from "@/util/effect-zod"
import { Global } from "../global" import { Global } from "../global"
@@ -49,7 +49,7 @@ export namespace Auth {
readonly remove: (key: string) => Effect.Effect<void, AuthError> readonly remove: (key: string) => Effect.Effect<void, AuthError>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/Auth") {} export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Auth") {}
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
+3 -6
View File
@@ -1,6 +1,5 @@
import z from "zod" import z from "zod"
import { Effect, Exit, Layer, PubSub, Scope, Context, Stream } from "effect" import { Effect, Exit, Layer, PubSub, Scope, ServiceMap, Stream } from "effect"
import { EffectLogger } from "@/effect/logger"
import { Log } from "../util/log" import { Log } from "../util/log"
import { BusEvent } from "./bus-event" import { BusEvent } from "./bus-event"
import { GlobalBus } from "./global" import { GlobalBus } from "./global"
@@ -42,7 +41,7 @@ export namespace Bus {
readonly subscribeAllCallback: (callback: (event: any) => unknown) => Effect.Effect<() => void> readonly subscribeAllCallback: (callback: (event: any) => unknown) => Effect.Effect<() => void>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/Bus") {} export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Bus") {}
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
@@ -147,7 +146,7 @@ export namespace Bus {
return () => { return () => {
log.info("unsubscribing", { type }) log.info("unsubscribing", { type })
Effect.runFork(Scope.close(scope, Exit.void).pipe(Effect.provide(EffectLogger.layer))) Effect.runFork(Scope.close(scope, Exit.void))
} }
}) })
} }
@@ -170,8 +169,6 @@ export namespace Bus {
}), }),
) )
export const defaultLayer = layer
const { runPromise, runSync } = makeRuntime(Service, layer) const { runPromise, runSync } = makeRuntime(Service, layer)
// runSync is safe here because the subscribe chain (InstanceState.get, PubSub.subscribe, // runSync is safe here because the subscribe chain (InstanceState.get, PubSub.subscribe,
+5 -6
View File
@@ -3,7 +3,6 @@ import { Duration, Effect, Match, Option } from "effect"
import { UI } from "../ui" import { UI } from "../ui"
import { AccountID, Account, OrgID, PollExpired, type PollResult } from "@/account" import { AccountID, Account, OrgID, PollExpired, type PollResult } from "@/account"
import { type AccountError } from "@/account/schema" import { type AccountError } from "@/account/schema"
import { AppRuntime } from "@/effect/app-runtime"
import * as Prompt from "../effect/prompt" import * as Prompt from "../effect/prompt"
import open from "open" import open from "open"
@@ -183,7 +182,7 @@ export const LoginCommand = cmd({
}), }),
async handler(args) { async handler(args) {
UI.empty() UI.empty()
await AppRuntime.runPromise(loginEffect(args.url)) await Account.runPromise((_svc) => loginEffect(args.url))
}, },
}) })
@@ -197,7 +196,7 @@ export const LogoutCommand = cmd({
}), }),
async handler(args) { async handler(args) {
UI.empty() UI.empty()
await AppRuntime.runPromise(logoutEffect(args.email)) await Account.runPromise((_svc) => logoutEffect(args.email))
}, },
}) })
@@ -206,7 +205,7 @@ export const SwitchCommand = cmd({
describe: false, describe: false,
async handler() { async handler() {
UI.empty() UI.empty()
await AppRuntime.runPromise(switchEffect()) await Account.runPromise((_svc) => switchEffect())
}, },
}) })
@@ -215,7 +214,7 @@ export const OrgsCommand = cmd({
describe: false, describe: false,
async handler() { async handler() {
UI.empty() UI.empty()
await AppRuntime.runPromise(orgsEffect()) await Account.runPromise((_svc) => orgsEffect())
}, },
}) })
@@ -224,7 +223,7 @@ export const OpenCommand = cmd({
describe: false, describe: false,
async handler() { async handler() {
UI.empty() UI.empty()
await AppRuntime.runPromise(openEffect()) await Account.runPromise((_svc) => openEffect())
}, },
}) })
+2 -5
View File
@@ -1,6 +1,5 @@
import { EOL } from "os" import { EOL } from "os"
import { basename } from "path" import { basename } from "path"
import { Effect } from "effect"
import { Agent } from "../../../agent/agent" import { Agent } from "../../../agent/agent"
import { Provider } from "../../../provider/provider" import { Provider } from "../../../provider/provider"
import { Session } from "../../../session" import { Session } from "../../../session"
@@ -158,16 +157,14 @@ async function createToolContext(agent: Agent.Info) {
agent: agent.name, agent: agent.name,
abort: new AbortController().signal, abort: new AbortController().signal,
messages: [], messages: [],
metadata: () => Effect.void, metadata: () => {},
ask(req: Omit<Permission.Request, "id" | "sessionID" | "tool">) { async ask(req: Omit<Permission.Request, "id" | "sessionID" | "tool">) {
return Effect.sync(() => {
for (const pattern of req.patterns) { for (const pattern of req.patterns) {
const rule = Permission.evaluate(req.permission, pattern, ruleset) const rule = Permission.evaluate(req.permission, pattern, ruleset)
if (rule.action === "deny") { if (rule.action === "deny") {
throw new Permission.DeniedError({ ruleset }) throw new Permission.DeniedError({ ruleset })
} }
} }
})
}, },
} }
} }
+4 -8
View File
@@ -29,7 +29,6 @@ import { Provider } from "../../provider/provider"
import { Bus } from "../../bus" import { Bus } from "../../bus"
import { MessageV2 } from "../../session/message-v2" import { MessageV2 } from "../../session/message-v2"
import { SessionPrompt } from "@/session/prompt" import { SessionPrompt } from "@/session/prompt"
import { AppRuntime } from "@/effect/app-runtime"
import { Git } from "@/git" import { Git } from "@/git"
import { setTimeout as sleep } from "node:timers/promises" import { setTimeout as sleep } from "node:timers/promises"
import { Process } from "@/util/process" import { Process } from "@/util/process"
@@ -259,9 +258,7 @@ export const GithubInstallCommand = cmd({
} }
// Get repo info // Get repo info
const info = await AppRuntime.runPromise( const info = (await Git.run(["remote", "get-url", "origin"], { cwd: Instance.worktree })).text().trim()
Git.Service.use((git) => git.run(["remote", "get-url", "origin"], { cwd: Instance.worktree })),
).then((x) => x.text().trim())
const parsed = parseGitHubRemote(info) const parsed = parseGitHubRemote(info)
if (!parsed) { if (!parsed) {
prompts.log.error(`Could not find git repository. Please run this command from a git repository.`) prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
@@ -500,21 +497,20 @@ export const GithubRunCommand = cmd({
: "issue" : "issue"
: undefined : undefined
const gitText = async (args: string[]) => { const gitText = async (args: string[]) => {
const result = await AppRuntime.runPromise(Git.Service.use((git) => git.run(args, { cwd: Instance.worktree }))) const result = await Git.run(args, { cwd: Instance.worktree })
if (result.exitCode !== 0) { if (result.exitCode !== 0) {
throw new Process.RunFailedError(["git", ...args], result.exitCode, result.stdout, result.stderr) throw new Process.RunFailedError(["git", ...args], result.exitCode, result.stdout, result.stderr)
} }
return result.text().trim() return result.text().trim()
} }
const gitRun = async (args: string[]) => { const gitRun = async (args: string[]) => {
const result = await AppRuntime.runPromise(Git.Service.use((git) => git.run(args, { cwd: Instance.worktree }))) const result = await Git.run(args, { cwd: Instance.worktree })
if (result.exitCode !== 0) { if (result.exitCode !== 0) {
throw new Process.RunFailedError(["git", ...args], result.exitCode, result.stdout, result.stderr) throw new Process.RunFailedError(["git", ...args], result.exitCode, result.stdout, result.stderr)
} }
return result return result
} }
const gitStatus = (args: string[]) => const gitStatus = (args: string[]) => Git.run(args, { cwd: Instance.worktree })
AppRuntime.runPromise(Git.Service.use((git) => git.run(args, { cwd: Instance.worktree })))
const commitChanges = async (summary: string, actor?: string) => { const commitChanges = async (summary: string, actor?: string) => {
const args = ["commit", "-m", summary] const args = ["commit", "-m", summary]
if (actor) args.push("-m", `Co-authored-by: ${actor} <${actor}@users.noreply.github.com>`) if (actor) args.push("-m", `Co-authored-by: ${actor} <${actor}@users.noreply.github.com>`)
+2 -3
View File
@@ -10,7 +10,6 @@ import { Instance } from "../../project/instance"
import { ShareNext } from "../../share/share-next" import { ShareNext } from "../../share/share-next"
import { EOL } from "os" import { EOL } from "os"
import { Filesystem } from "../../util/filesystem" import { Filesystem } from "../../util/filesystem"
import { AppRuntime } from "@/effect/app-runtime"
/** Discriminated union returned by the ShareNext API (GET /api/shares/:id/data) */ /** Discriminated union returned by the ShareNext API (GET /api/shares/:id/data) */
export type ShareData = export type ShareData =
@@ -101,7 +100,7 @@ export const ImportCommand = cmd({
if (isUrl) { if (isUrl) {
const slug = parseShareUrl(args.file) const slug = parseShareUrl(args.file)
if (!slug) { if (!slug) {
const baseUrl = await AppRuntime.runPromise(ShareNext.Service.use((svc) => svc.url())) const baseUrl = await ShareNext.url()
process.stdout.write(`Invalid URL format. Expected: ${baseUrl}/share/<slug>`) process.stdout.write(`Invalid URL format. Expected: ${baseUrl}/share/<slug>`)
process.stdout.write(EOL) process.stdout.write(EOL)
return return
@@ -109,7 +108,7 @@ export const ImportCommand = cmd({
const parsed = new URL(args.file) const parsed = new URL(args.file)
const baseUrl = parsed.origin const baseUrl = parsed.origin
const req = await AppRuntime.runPromise(ShareNext.Service.use((svc) => svc.request())) const req = await ShareNext.request()
const headers = shouldAttachShareAuthHeaders(args.file, req.baseUrl) ? req.headers : {} const headers = shouldAttachShareAuthHeaders(args.file, req.baseUrl) ? req.headers : {}
const dataPath = req.api.data(slug) const dataPath = req.api.data(slug)
+5 -16
View File
@@ -1,6 +1,5 @@
import { UI } from "../ui" import { UI } from "../ui"
import { cmd } from "./cmd" import { cmd } from "./cmd"
import { AppRuntime } from "@/effect/app-runtime"
import { Git } from "@/git" import { Git } from "@/git"
import { Instance } from "@/project/instance" import { Instance } from "@/project/instance"
import { Process } from "@/util/process" import { Process } from "@/util/process"
@@ -68,29 +67,19 @@ export const PrCommand = cmd({
const remoteName = forkOwner const remoteName = forkOwner
// Check if remote already exists // Check if remote already exists
const remotes = await AppRuntime.runPromise( const remotes = (await Git.run(["remote"], { cwd: Instance.worktree })).text().trim()
Git.Service.use((git) => git.run(["remote"], { cwd: Instance.worktree })),
).then((x) => x.text().trim())
if (!remotes.split("\n").includes(remoteName)) { if (!remotes.split("\n").includes(remoteName)) {
await AppRuntime.runPromise( await Git.run(["remote", "add", remoteName, `https://github.com/${forkOwner}/${forkName}.git`], {
Git.Service.use((git) =>
git.run(["remote", "add", remoteName, `https://github.com/${forkOwner}/${forkName}.git`], {
cwd: Instance.worktree, cwd: Instance.worktree,
}), })
),
)
UI.println(`Added fork remote: ${remoteName}`) UI.println(`Added fork remote: ${remoteName}`)
} }
// Set upstream to the fork so pushes go there // Set upstream to the fork so pushes go there
const headRefName = prInfo.headRefName const headRefName = prInfo.headRefName
await AppRuntime.runPromise( await Git.run(["branch", `--set-upstream-to=${remoteName}/${headRefName}`, localBranchName], {
Git.Service.use((git) =>
git.run(["branch", `--set-upstream-to=${remoteName}/${headRefName}`, localBranchName], {
cwd: Instance.worktree, cwd: Instance.worktree,
}), })
),
)
} }
// Check for opencode session link in PR body // Check for opencode session link in PR body
+1 -2
View File
@@ -59,7 +59,6 @@ import { TuiConfigProvider, useTuiConfig } from "./context/tui-config"
import { TuiConfig } from "@/config/tui" import { TuiConfig } from "@/config/tui"
import { createTuiApi, TuiPluginRuntime, type RouteMap } from "./plugin" import { createTuiApi, TuiPluginRuntime, type RouteMap } from "./plugin"
import { FormatError, FormatUnknownError } from "@/cli/error" import { FormatError, FormatUnknownError } from "@/cli/error"
import { Keybind } from "@/util/keybind"
async function getTerminalBackgroundColor(): Promise<"dark" | "light"> { async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
// can't set raw mode if not a TTY // can't set raw mode if not a TTY
@@ -311,7 +310,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
// - Ctrl+C copies and dismisses selection // - Ctrl+C copies and dismisses selection
// - Esc dismisses selection // - Esc dismisses selection
// - Most other key input dismisses selection and is passed through // - Most other key input dismisses selection and is passed through
if (Keybind.matchParsedKey("ctrl+c", evt)) { if (evt.ctrl && evt.name === "c") {
if (!Selection.copy(renderer, toast)) { if (!Selection.copy(renderer, toast)) {
renderer.clearSelection() renderer.clearSelection()
return return
@@ -47,7 +47,7 @@ export function DialogMcp() {
const keybinds = createMemo(() => [ const keybinds = createMemo(() => [
{ {
keybind: Keybind.parseOne("space"), keybind: Keybind.parse("space")[0],
title: "toggle", title: "toggle",
onTrigger: async (option: DialogSelectOption<string>) => { onTrigger: async (option: DialogSelectOption<string>) => {
// Prevent toggling while an operation is already in progress // Prevent toggling while an operation is already in progress
@@ -162,7 +162,7 @@ export function DialogSessionList() {
}, },
}, },
{ {
keybind: Keybind.parseOne("ctrl+w"), keybind: Keybind.parse("ctrl+w")[0],
title: "new workspace", title: "new workspace",
side: "right", side: "right",
disabled: !Flag.OPENCODE_EXPERIMENTAL_WORKSPACES, disabled: !Flag.OPENCODE_EXPERIMENTAL_WORKSPACES,
@@ -5,7 +5,6 @@ import { createSignal } from "solid-js"
import { Installation } from "@/installation" import { Installation } from "@/installation"
import { win32FlushInputBuffer } from "../win32" import { win32FlushInputBuffer } from "../win32"
import { getScrollAcceleration } from "../util/scroll" import { getScrollAcceleration } from "../util/scroll"
import { Keybind } from "@/util/keybind"
export function ErrorComponent(props: { export function ErrorComponent(props: {
error: Error error: Error
@@ -26,7 +25,7 @@ export function ErrorComponent(props: {
} }
useKeyboard((evt) => { useKeyboard((evt) => {
if (Keybind.matchParsedKey("ctrl+c", evt)) { if (evt.ctrl && evt.name === "c") {
handleExit() handleExit()
} }
}) })
@@ -589,13 +589,6 @@ export function Prompt(props: PromptProps) {
]) ])
async function submit() { async function submit() {
// IME: double-defer may fire before onContentChange flushes the last
// composed character (e.g. Korean hangul) to the store, so read
// plainText directly and sync before any downstream reads.
if (input && !input.isDestroyed && input.plainText !== store.prompt.input) {
setStore("prompt", "input", input.plainText)
syncExtmarksWithPromptParts()
}
if (props.disabled) return if (props.disabled) return
if (autocomplete?.visible) return if (autocomplete?.visible) return
if (!store.prompt.input) return if (!store.prompt.input) return
@@ -1001,11 +994,7 @@ export function Prompt(props: PromptProps) {
input.cursorOffset = input.plainText.length input.cursorOffset = input.plainText.length
} }
}} }}
onSubmit={() => { onSubmit={submit}
// IME: double-defer so the last composed character (e.g. Korean
// hangul) is flushed to plainText before we read it for submission.
setTimeout(() => setTimeout(() => submit(), 0), 0)
}}
onPaste={async (event: PasteEvent) => { onPaste={async (event: PasteEvent) => {
if (props.disabled) { if (props.disabled) {
event.preventDefault() event.preventDefault()
+2 -8
View File
@@ -137,18 +137,12 @@ export const TuiThreadCommand = cmd({
), ),
}) })
worker.onerror = (e) => { worker.onerror = (e) => {
Log.Default.error("thread error", { Log.Default.error(e)
message: e.message,
filename: e.filename,
lineno: e.lineno,
colno: e.colno,
error: e.error,
})
} }
const client = Rpc.client<typeof rpc>(worker) const client = Rpc.client<typeof rpc>(worker)
const error = (e: unknown) => { const error = (e: unknown) => {
Log.Default.error("process error", { error: errorMessage(e) }) Log.Default.error(e)
} }
const reload = () => { const reload = () => {
client.call("reload", undefined).catch((err) => { client.call("reload", undefined).catch((err) => {
@@ -195,8 +195,8 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
useKeyboard((evt) => { useKeyboard((evt) => {
setStore("input", "keyboard") setStore("input", "keyboard")
if (evt.name === "up" || Keybind.matchParsedKey("ctrl+p", evt)) move(-1) if (evt.name === "up" || (evt.ctrl && evt.name === "p")) move(-1)
if (evt.name === "down" || Keybind.matchParsedKey("ctrl+n", evt)) move(1) if (evt.name === "down" || (evt.ctrl && evt.name === "n")) move(1)
if (evt.name === "pageup") move(-10) if (evt.name === "pageup") move(-10)
if (evt.name === "pagedown") move(10) if (evt.name === "pagedown") move(10)
if (evt.name === "home") moveTo(0) if (evt.name === "home") moveTo(0)
@@ -6,7 +6,6 @@ import { createStore } from "solid-js/store"
import { useToast } from "./toast" import { useToast } from "./toast"
import { Flag } from "@/flag/flag" import { Flag } from "@/flag/flag"
import { Selection } from "@tui/util/selection" import { Selection } from "@tui/util/selection"
import { Keybind } from "@/util/keybind"
export function Dialog( export function Dialog(
props: ParentProps<{ props: ParentProps<{
@@ -73,13 +72,12 @@ function init() {
}) })
const renderer = useRenderer() const renderer = useRenderer()
useKeyboard((evt) => { useKeyboard((evt) => {
if (store.stack.length === 0) return if (store.stack.length === 0) return
if (evt.defaultPrevented) return if (evt.defaultPrevented) return
const isCtrlC = Keybind.matchParsedKey("ctrl+c", evt) if ((evt.name === "escape" || (evt.ctrl && evt.name === "c")) && renderer.getSelection()?.getSelectedText()) return
if (evt.name === "escape" || (evt.ctrl && evt.name === "c")) {
if ((evt.name === "escape" || isCtrlC) && renderer.getSelection()?.getSelectedText()) return
if (evt.name === "escape" || isCtrlC) {
if (renderer.getSelection()) { if (renderer.getSelection()) {
renderer.clearSelection() renderer.clearSelection()
} }
+1 -2
View File
@@ -1,7 +1,6 @@
import type { Argv } from "yargs" import type { Argv } from "yargs"
import { UI } from "../ui" import { UI } from "../ui"
import * as prompts from "@clack/prompts" import * as prompts from "@clack/prompts"
import { AppRuntime } from "@/effect/app-runtime"
import { Installation } from "../../installation" import { Installation } from "../../installation"
import { Global } from "../../global" import { Global } from "../../global"
import fs from "fs/promises" import fs from "fs/promises"
@@ -58,7 +57,7 @@ export const UninstallCommand = {
UI.empty() UI.empty()
prompts.intro("Uninstall OpenCode") prompts.intro("Uninstall OpenCode")
const method = await AppRuntime.runPromise(Installation.Service.use((svc) => svc.method())) const method = await Installation.method()
prompts.log.info(`Installation method: ${method}`) prompts.log.info(`Installation method: ${method}`)
const targets = await collectRemovalTargets(args, method) const targets = await collectRemovalTargets(args, method)
+3 -8
View File
@@ -1,7 +1,6 @@
import type { Argv } from "yargs" import type { Argv } from "yargs"
import { UI } from "../ui" import { UI } from "../ui"
import * as prompts from "@clack/prompts" import * as prompts from "@clack/prompts"
import { AppRuntime } from "@/effect/app-runtime"
import { Installation } from "../../installation" import { Installation } from "../../installation"
export const UpgradeCommand = { export const UpgradeCommand = {
@@ -25,7 +24,7 @@ export const UpgradeCommand = {
UI.println(UI.logo(" ")) UI.println(UI.logo(" "))
UI.empty() UI.empty()
prompts.intro("Upgrade") prompts.intro("Upgrade")
const detectedMethod = await AppRuntime.runPromise(Installation.Service.use((svc) => svc.method())) const detectedMethod = await Installation.method()
const method = (args.method as Installation.Method) ?? detectedMethod const method = (args.method as Installation.Method) ?? detectedMethod
if (method === "unknown") { if (method === "unknown") {
prompts.log.error(`opencode is installed to ${process.execPath} and may be managed by a package manager`) prompts.log.error(`opencode is installed to ${process.execPath} and may be managed by a package manager`)
@@ -43,9 +42,7 @@ export const UpgradeCommand = {
} }
} }
prompts.log.info("Using method: " + method) prompts.log.info("Using method: " + method)
const target = args.target const target = args.target ? args.target.replace(/^v/, "") : await Installation.latest()
? args.target.replace(/^v/, "")
: await AppRuntime.runPromise(Installation.Service.use((svc) => svc.latest()))
if (Installation.VERSION === target) { if (Installation.VERSION === target) {
prompts.log.warn(`opencode upgrade skipped: ${target} is already installed`) prompts.log.warn(`opencode upgrade skipped: ${target} is already installed`)
@@ -56,9 +53,7 @@ export const UpgradeCommand = {
prompts.log.info(`From ${Installation.VERSION}${target}`) prompts.log.info(`From ${Installation.VERSION}${target}`)
const spinner = prompts.spinner() const spinner = prompts.spinner()
spinner.start("Upgrading...") spinner.start("Upgrading...")
const err = await AppRuntime.runPromise(Installation.Service.use((svc) => svc.upgrade(method, target))).catch( const err = await Installation.upgrade(method, target).catch((err) => err)
(err) => err,
)
if (err) { if (err) {
spinner.stop("Upgrade failed", 1) spinner.stop("Upgrade failed", 1)
if (err instanceof Installation.UpgradeFailedError) { if (err instanceof Installation.UpgradeFailedError) {
+3 -4
View File
@@ -1,13 +1,12 @@
import { Bus } from "@/bus" import { Bus } from "@/bus"
import { Config } from "@/config/config" import { Config } from "@/config/config"
import { AppRuntime } from "@/effect/app-runtime"
import { Flag } from "@/flag/flag" import { Flag } from "@/flag/flag"
import { Installation } from "@/installation" import { Installation } from "@/installation"
export async function upgrade() { export async function upgrade() {
const config = await Config.getGlobal() const config = await Config.getGlobal()
const method = await AppRuntime.runPromise(Installation.Service.use((svc) => svc.method())) const method = await Installation.method()
const latest = await AppRuntime.runPromise(Installation.Service.use((svc) => svc.latest(method))).catch(() => {}) const latest = await Installation.latest(method).catch(() => {})
if (!latest) return if (!latest) return
if (Flag.OPENCODE_ALWAYS_NOTIFY_UPDATE) { if (Flag.OPENCODE_ALWAYS_NOTIFY_UPDATE) {
@@ -26,7 +25,7 @@ export async function upgrade() {
} }
if (method === "unknown") return if (method === "unknown") return
await AppRuntime.runPromise(Installation.Service.use((svc) => svc.upgrade(method, latest))) await Installation.upgrade(method, latest)
.then(() => Bus.publish(Installation.Event.Updated, { version: latest })) .then(() => Bus.publish(Installation.Event.Updated, { version: latest }))
.catch(() => {}) .catch(() => {})
} }
+10 -6
View File
@@ -1,9 +1,8 @@
import { BusEvent } from "@/bus/bus-event" import { BusEvent } from "@/bus/bus-event"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import type { InstanceContext } from "@/project/instance" import { makeRuntime } from "@/effect/run-service"
import { SessionID, MessageID } from "@/session/schema" import { SessionID, MessageID } from "@/session/schema"
import { Effect, Layer, Context } from "effect" import { Effect, Layer, ServiceMap } from "effect"
import { EffectLogger } from "@/effect/logger"
import z from "zod" import z from "zod"
import { Config } from "../config/config" import { Config } from "../config/config"
import { MCP } from "../mcp" import { MCP } from "../mcp"
@@ -71,7 +70,7 @@ export namespace Command {
readonly list: () => Effect.Effect<Info[]> readonly list: () => Effect.Effect<Info[]>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/Command") {} export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Command") {}
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
@@ -80,7 +79,7 @@ export namespace Command {
const mcp = yield* MCP.Service const mcp = yield* MCP.Service
const skill = yield* Skill.Service const skill = yield* Skill.Service
const init = Effect.fn("Command.state")(function* (ctx: InstanceContext) { const init = Effect.fn("Command.state")(function* (ctx) {
const cfg = yield* config.get() const cfg = yield* config.get()
const commands: Record<string, Info> = {} const commands: Record<string, Info> = {}
@@ -141,7 +140,6 @@ export namespace Command {
.map((message) => (message.content.type === "text" ? message.content.text : "")) .map((message) => (message.content.type === "text" ? message.content.text : ""))
.join("\n") || "", .join("\n") || "",
), ),
Effect.provide(EffectLogger.layer),
), ),
) )
}, },
@@ -188,4 +186,10 @@ export namespace Command {
Layer.provide(MCP.defaultLayer), Layer.provide(MCP.defaultLayer),
Layer.provide(Skill.defaultLayer), Layer.provide(Skill.defaultLayer),
) )
const { runPromise } = makeRuntime(Service, defaultLayer)
export async function list() {
return runPromise((svc) => svc.list())
}
} }
+19 -23
View File
@@ -4,6 +4,7 @@ import { pathToFileURL } from "url"
import os from "os" import os from "os"
import { Process } from "../util/process" import { Process } from "../util/process"
import z from "zod" import z from "zod"
import { ModelsDev } from "../provider/models"
import { mergeDeep, pipe, unique } from "remeda" import { mergeDeep, pipe, unique } from "remeda"
import { Global } from "../global" import { Global } from "../global"
import fsNode from "fs/promises" import fsNode from "fs/promises"
@@ -36,11 +37,10 @@ import type { ConsoleState } from "./console-state"
import { AppFileSystem } from "@/filesystem" import { AppFileSystem } from "@/filesystem"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { makeRuntime } from "@/effect/run-service" import { makeRuntime } from "@/effect/run-service"
import { Duration, Effect, Layer, Option, Context } from "effect" import { Duration, Effect, Layer, Option, ServiceMap } from "effect"
import { Flock } from "@/util/flock" import { Flock } from "@/util/flock"
import { isPathPluginSpec, parsePluginSpecifier, resolvePathPluginTarget } from "@/plugin/shared" import { isPathPluginSpec, parsePluginSpecifier, resolvePathPluginTarget } from "@/plugin/shared"
import { Npm } from "@/npm" import { Npm } from "@/npm"
import { InstanceRef } from "@/effect/instance-ref"
export namespace Config { export namespace Config {
const ModelId = z.string().meta({ $ref: "https://models.dev/model-schema.json#/$defs/Model" }) const ModelId = z.string().meta({ $ref: "https://models.dev/model-schema.json#/$defs/Model" })
@@ -1126,7 +1126,7 @@ export namespace Config {
readonly waitForDependencies: () => Effect.Effect<void> readonly waitForDependencies: () => Effect.Effect<void>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/Config") {} export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Config") {}
function globalConfigFile() { function globalConfigFile() {
const candidates = ["opencode.jsonc", "opencode.json", "config.json"].map((file) => const candidates = ["opencode.jsonc", "opencode.json", "config.json"].map((file) =>
@@ -1327,31 +1327,27 @@ export namespace Config {
const consoleManagedProviders = new Set<string>() const consoleManagedProviders = new Set<string>()
let activeOrgName: string | undefined let activeOrgName: string | undefined
const scope = Effect.fnUntraced(function* (source: string) { const scope = (source: string): PluginScope => {
if (source.startsWith("http://") || source.startsWith("https://")) return "global" if (source.startsWith("http://") || source.startsWith("https://")) return "global"
if (source === "OPENCODE_CONFIG_CONTENT") return "local" if (source === "OPENCODE_CONFIG_CONTENT") return "local"
if (yield* InstanceRef.use((ctx) => Effect.succeed(Instance.containsPath(source, ctx)))) return "local" if (Instance.containsPath(source)) return "local"
return "global" return "global"
}) }
const track = Effect.fnUntraced(function* ( const track = (source: string, list: PluginSpec[] | undefined, kind?: PluginScope) => {
source: string,
list: PluginSpec[] | undefined,
kind?: PluginScope,
) {
if (!list?.length) return if (!list?.length) return
const hit = kind ?? (yield* scope(source)) const hit = kind ?? scope(source)
const plugins = deduplicatePluginOrigins([ const plugins = deduplicatePluginOrigins([
...(result.plugin_origins ?? []), ...(result.plugin_origins ?? []),
...list.map((spec) => ({ spec, source, scope: hit })), ...list.map((spec) => ({ spec, source, scope: hit })),
]) ])
result.plugin = plugins.map((item) => item.spec) result.plugin = plugins.map((item) => item.spec)
result.plugin_origins = plugins result.plugin_origins = plugins
}) }
const merge = (source: string, next: Info, kind?: PluginScope) => { const merge = (source: string, next: Info, kind?: PluginScope) => {
result = mergeConfigConcatArrays(result, next) result = mergeConfigConcatArrays(result, next)
return track(source, next.plugin, kind) track(source, next.plugin, kind)
} }
for (const [key, value] of Object.entries(auth)) { for (const [key, value] of Object.entries(auth)) {
@@ -1371,16 +1367,16 @@ export namespace Config {
dir: path.dirname(source), dir: path.dirname(source),
source, source,
}) })
yield* merge(source, next, "global") merge(source, next, "global")
log.debug("loaded remote config from well-known", { url }) log.debug("loaded remote config from well-known", { url })
} }
} }
const global = yield* getGlobal() const global = yield* getGlobal()
yield* merge(Global.Path.config, global, "global") merge(Global.Path.config, global, "global")
if (Flag.OPENCODE_CONFIG) { if (Flag.OPENCODE_CONFIG) {
yield* merge(Flag.OPENCODE_CONFIG, yield* loadFile(Flag.OPENCODE_CONFIG)) merge(Flag.OPENCODE_CONFIG, yield* loadFile(Flag.OPENCODE_CONFIG))
log.debug("loaded custom config", { path: Flag.OPENCODE_CONFIG }) log.debug("loaded custom config", { path: Flag.OPENCODE_CONFIG })
} }
@@ -1388,7 +1384,7 @@ export namespace Config {
for (const file of yield* Effect.promise(() => for (const file of yield* Effect.promise(() =>
ConfigPaths.projectFiles("opencode", ctx.directory, ctx.worktree), ConfigPaths.projectFiles("opencode", ctx.directory, ctx.worktree),
)) { )) {
yield* merge(file, yield* loadFile(file), "local") merge(file, yield* loadFile(file), "local")
} }
} }
@@ -1409,7 +1405,7 @@ export namespace Config {
for (const file of ["opencode.json", "opencode.jsonc"]) { for (const file of ["opencode.json", "opencode.jsonc"]) {
const source = path.join(dir, file) const source = path.join(dir, file)
log.debug(`loading config from ${source}`) log.debug(`loading config from ${source}`)
yield* merge(source, yield* loadFile(source)) merge(source, yield* loadFile(source))
result.agent ??= {} result.agent ??= {}
result.mode ??= {} result.mode ??= {}
result.plugin ??= [] result.plugin ??= []
@@ -1428,7 +1424,7 @@ export namespace Config {
result.agent = mergeDeep(result.agent, yield* Effect.promise(() => loadAgent(dir))) result.agent = mergeDeep(result.agent, yield* Effect.promise(() => loadAgent(dir)))
result.agent = mergeDeep(result.agent, yield* Effect.promise(() => loadMode(dir))) result.agent = mergeDeep(result.agent, yield* Effect.promise(() => loadMode(dir)))
const list = yield* Effect.promise(() => loadPlugin(dir)) const list = yield* Effect.promise(() => loadPlugin(dir))
yield* track(dir, list) track(dir, list)
} }
if (process.env.OPENCODE_CONFIG_CONTENT) { if (process.env.OPENCODE_CONFIG_CONTENT) {
@@ -1437,7 +1433,7 @@ export namespace Config {
dir: ctx.directory, dir: ctx.directory,
source, source,
}) })
yield* merge(source, next, "local") merge(source, next, "local")
log.debug("loaded custom config from OPENCODE_CONFIG_CONTENT") log.debug("loaded custom config from OPENCODE_CONFIG_CONTENT")
} }
@@ -1466,7 +1462,7 @@ export namespace Config {
for (const providerID of Object.keys(next.provider ?? {})) { for (const providerID of Object.keys(next.provider ?? {})) {
consoleManagedProviders.add(providerID) consoleManagedProviders.add(providerID)
} }
yield* merge(source, next, "global") merge(source, next, "global")
} }
}).pipe( }).pipe(
Effect.catch((err) => { Effect.catch((err) => {
@@ -1481,7 +1477,7 @@ export namespace Config {
if (existsSync(managedDir)) { if (existsSync(managedDir)) {
for (const file of ["opencode.json", "opencode.jsonc"]) { for (const file of ["opencode.json", "opencode.jsonc"]) {
const source = path.join(managedDir, file) const source = path.join(managedDir, file)
yield* merge(source, yield* loadFile(source), "global") merge(source, yield* loadFile(source), "global")
} }
} }
@@ -10,7 +10,8 @@ export type WorkspaceID = typeof workspaceIdSchema.Type
export const WorkspaceID = workspaceIdSchema.pipe( export const WorkspaceID = workspaceIdSchema.pipe(
withStatics((schema: typeof workspaceIdSchema) => ({ withStatics((schema: typeof workspaceIdSchema) => ({
ascending: (id?: string) => schema.make(Identifier.ascending("workspace", id)), make: (id: string) => schema.makeUnsafe(id),
ascending: (id?: string) => schema.makeUnsafe(Identifier.ascending("workspace", id)),
zod: Identifier.schema("workspace").pipe(z.custom<WorkspaceID>()), zod: Identifier.schema("workspace").pipe(z.custom<WorkspaceID>()),
})), })),
) )
@@ -1,11 +1,11 @@
import { LocalContext } from "../util/local-context" import { Context } from "../util/context"
import type { WorkspaceID } from "../control-plane/schema" import type { WorkspaceID } from "../control-plane/schema"
export interface WorkspaceContext { export interface WorkspaceContext {
workspaceID: string workspaceID: string
} }
const context = LocalContext.create<WorkspaceContext>("instance") const context = Context.create<WorkspaceContext>("instance")
export const WorkspaceContext = { export const WorkspaceContext = {
async provide<R>(input: { workspaceID: WorkspaceID; fn: () => R }): Promise<R> { async provide<R>(input: { workspaceID: WorkspaceID; fn: () => R }): Promise<R> {
-100
View File
@@ -1,100 +0,0 @@
import { Layer, ManagedRuntime } from "effect"
import { memoMap } from "./run-service"
import { Observability } from "./oltp"
import { AppFileSystem } from "@/filesystem"
import { Bus } from "@/bus"
import { Auth } from "@/auth"
import { Account } from "@/account"
import { Config } from "@/config/config"
import { Git } from "@/git"
import { Ripgrep } from "@/file/ripgrep"
import { FileTime } from "@/file/time"
import { File } from "@/file"
import { FileWatcher } from "@/file/watcher"
import { Storage } from "@/storage/storage"
import { Snapshot } from "@/snapshot"
import { Plugin } from "@/plugin"
import { Provider } from "@/provider/provider"
import { ProviderAuth } from "@/provider/auth"
import { Agent } from "@/agent/agent"
import { Skill } from "@/skill"
import { Discovery } from "@/skill/discovery"
import { Question } from "@/question"
import { Permission } from "@/permission"
import { Todo } from "@/session/todo"
import { Session } from "@/session"
import { SessionStatus } from "@/session/status"
import { SessionRunState } from "@/session/run-state"
import { SessionProcessor } from "@/session/processor"
import { SessionCompaction } from "@/session/compaction"
import { SessionRevert } from "@/session/revert"
import { SessionSummary } from "@/session/summary"
import { SessionPrompt } from "@/session/prompt"
import { Instruction } from "@/session/instruction"
import { LLM } from "@/session/llm"
import { LSP } from "@/lsp"
import { MCP } from "@/mcp"
import { McpAuth } from "@/mcp/auth"
import { Command } from "@/command"
import { Truncate } from "@/tool/truncate"
import { ToolRegistry } from "@/tool/registry"
import { Format } from "@/format"
import { Project } from "@/project/project"
import { Vcs } from "@/project/vcs"
import { Worktree } from "@/worktree"
import { Pty } from "@/pty"
import { Installation } from "@/installation"
import { ShareNext } from "@/share/share-next"
import { SessionShare } from "@/share/session"
export const AppLayer = Layer.mergeAll(
Observability.layer,
AppFileSystem.defaultLayer,
Bus.defaultLayer,
Auth.defaultLayer,
Account.defaultLayer,
Config.defaultLayer,
Git.defaultLayer,
Ripgrep.defaultLayer,
FileTime.defaultLayer,
File.defaultLayer,
FileWatcher.defaultLayer,
Storage.defaultLayer,
Snapshot.defaultLayer,
Plugin.defaultLayer,
Provider.defaultLayer,
ProviderAuth.defaultLayer,
Agent.defaultLayer,
Skill.defaultLayer,
Discovery.defaultLayer,
Question.defaultLayer,
Permission.defaultLayer,
Todo.defaultLayer,
Session.defaultLayer,
SessionStatus.defaultLayer,
SessionRunState.defaultLayer,
SessionProcessor.defaultLayer,
SessionCompaction.defaultLayer,
SessionRevert.defaultLayer,
SessionSummary.defaultLayer,
SessionPrompt.defaultLayer,
Instruction.defaultLayer,
LLM.defaultLayer,
LSP.defaultLayer,
MCP.defaultLayer,
McpAuth.defaultLayer,
Command.defaultLayer,
Truncate.defaultLayer,
ToolRegistry.defaultLayer,
Format.defaultLayer,
Project.defaultLayer,
Vcs.defaultLayer,
Worktree.defaultLayer,
Pty.defaultLayer,
Installation.defaultLayer,
ShareNext.defaultLayer,
SessionShare.defaultLayer,
)
export const AppRuntime = ManagedRuntime.make(AppLayer, { memoMap })
@@ -1,10 +0,0 @@
import { Layer, ManagedRuntime } from "effect"
import { memoMap } from "./run-service"
import { FileWatcher } from "@/file/watcher"
import { Format } from "@/format"
import { ShareNext } from "@/share/share-next"
export const BootstrapLayer = Layer.mergeAll(Format.defaultLayer, ShareNext.defaultLayer, FileWatcher.defaultLayer)
export const BootstrapRuntime = ManagedRuntime.make(BootstrapLayer, { memoMap })
@@ -402,7 +402,6 @@ export const make = Effect.gen(function* () {
const fd = yield* setupFds(command, proc, extra) const fd = yield* setupFds(command, proc, extra)
const out = setupOutput(command, proc, sout, serr) const out = setupOutput(command, proc, sout, serr)
let ref = true
return makeHandle({ return makeHandle({
pid: ProcessId(proc.pid!), pid: ProcessId(proc.pid!),
stdin: yield* setupStdin(command, proc, sin), stdin: yield* setupStdin(command, proc, sin),
@@ -433,18 +432,6 @@ export const make = Effect.gen(function* () {
orElse: () => send("SIGKILL").pipe(Effect.andThen(Deferred.await(signal)), Effect.asVoid), orElse: () => send("SIGKILL").pipe(Effect.andThen(Deferred.await(signal)), Effect.asVoid),
}) })
}, },
unref: Effect.sync(() => {
if (ref) {
proc.unref()
ref = false
}
return Effect.sync(() => {
if (!ref) {
proc.ref()
ref = true
}
})
}),
}) })
} }
case "PipedCommand": { case "PipedCommand": {
+3 -3
View File
@@ -1,10 +1,10 @@
import { Context } from "effect" import { ServiceMap } from "effect"
import type { InstanceContext } from "@/project/instance" import type { InstanceContext } from "@/project/instance"
export const InstanceRef = Context.Reference<InstanceContext | undefined>("~opencode/InstanceRef", { export const InstanceRef = ServiceMap.Reference<InstanceContext | undefined>("~opencode/InstanceRef", {
defaultValue: () => undefined, defaultValue: () => undefined,
}) })
export const WorkspaceRef = Context.Reference<string | undefined>("~opencode/WorkspaceRef", { export const WorkspaceRef = ServiceMap.Reference<string | undefined>("~opencode/WorkspaceRef", {
defaultValue: () => undefined, defaultValue: () => undefined,
}) })
@@ -1,7 +1,6 @@
import { Effect, Fiber, ScopedCache, Scope, Context } from "effect" import { Effect, Fiber, ScopedCache, Scope, ServiceMap } from "effect"
import { EffectLogger } from "@/effect/logger"
import { Instance, type InstanceContext } from "@/project/instance" import { Instance, type InstanceContext } from "@/project/instance"
import { LocalContext } from "@/util/local-context" import { Context } from "@/util/context"
import { InstanceRef, WorkspaceRef } from "./instance-ref" import { InstanceRef, WorkspaceRef } from "./instance-ref"
import { registerDisposer } from "./instance-registry" import { registerDisposer } from "./instance-registry"
import { WorkspaceContext } from "@/control-plane/workspace-context" import { WorkspaceContext } from "@/control-plane/workspace-context"
@@ -18,10 +17,10 @@ export namespace InstanceState {
try { try {
return Instance.bind(fn) return Instance.bind(fn)
} catch (err) { } catch (err) {
if (!(err instanceof LocalContext.NotFound)) throw err if (!(err instanceof Context.NotFound)) throw err
} }
const fiber = Fiber.getCurrent() const fiber = Fiber.getCurrent()
const ctx = fiber ? Context.getReferenceUnsafe(fiber.context, InstanceRef) : undefined const ctx = fiber ? ServiceMap.getReferenceUnsafe(fiber.services, InstanceRef) : undefined
if (!ctx) return fn if (!ctx) return fn
return ((...args: any[]) => Instance.restore(ctx, () => fn(...args))) as F return ((...args: any[]) => Instance.restore(ctx, () => fn(...args))) as F
} }
@@ -48,9 +47,7 @@ export namespace InstanceState {
}), }),
}) })
const off = registerDisposer((directory) => const off = registerDisposer((directory) => Effect.runPromise(ScopedCache.invalidate(cache, directory)))
Effect.runPromise(ScopedCache.invalidate(cache, directory).pipe(Effect.provide(EffectLogger.layer))),
)
yield* Effect.addFinalizer(() => Effect.sync(off)) yield* Effect.addFinalizer(() => Effect.sync(off))
return { return {
-67
View File
@@ -1,67 +0,0 @@
import { Cause, Effect, Logger, References } from "effect"
import { Log } from "@/util/log"
export namespace EffectLogger {
type Fields = Record<string, unknown>
export interface Handle {
readonly debug: (msg?: unknown, extra?: Fields) => Effect.Effect<void>
readonly info: (msg?: unknown, extra?: Fields) => Effect.Effect<void>
readonly warn: (msg?: unknown, extra?: Fields) => Effect.Effect<void>
readonly error: (msg?: unknown, extra?: Fields) => Effect.Effect<void>
readonly with: (extra: Fields) => Handle
}
const clean = (input?: Fields): Fields =>
Object.fromEntries(Object.entries(input ?? {}).filter((entry) => entry[1] !== undefined && entry[1] !== null))
const text = (input: unknown): string => {
if (Array.isArray(input)) return input.map((item) => String(item)).join(" ")
return input === undefined ? "" : String(input)
}
const call = (run: (msg?: unknown) => Effect.Effect<void>, base: Fields, msg?: unknown, extra?: Fields) => {
const ann = clean({ ...base, ...extra })
const fx = run(msg)
return Object.keys(ann).length ? Effect.annotateLogs(fx, ann) : fx
}
export const logger = Logger.make((opts) => {
const extra = clean(opts.fiber.getRef(References.CurrentLogAnnotations))
const now = opts.date.getTime()
for (const [key, start] of opts.fiber.getRef(References.CurrentLogSpans)) {
extra[`logSpan.${key}`] = `${now - start}ms`
}
if (opts.cause.reasons.length > 0) {
extra.cause = Cause.pretty(opts.cause)
}
const svc = typeof extra.service === "string" ? extra.service : undefined
if (svc) delete extra.service
const log = svc ? Log.create({ service: svc }) : Log.Default
const msg = text(opts.message)
switch (opts.logLevel) {
case "Trace":
case "Debug":
return log.debug(msg, extra)
case "Warn":
return log.warn(msg, extra)
case "Error":
case "Fatal":
return log.error(msg, extra)
default:
return log.info(msg, extra)
}
})
export const layer = Logger.layer([logger], { mergeWithExisting: false })
export const create = (base: Fields = {}): Handle => ({
debug: (msg, extra) => call((item) => Effect.logDebug(item), base, msg, extra),
info: (msg, extra) => call((item) => Effect.logInfo(item), base, msg, extra),
warn: (msg, extra) => call((item) => Effect.logWarning(item), base, msg, extra),
error: (msg, extra) => call((item) => Effect.logError(item), base, msg, extra),
with: (extra) => create({ ...base, ...extra }),
})
}
+12 -19
View File
@@ -1,24 +1,26 @@
import { Duration, Layer } from "effect" import { Layer } from "effect"
import { FetchHttpClient } from "effect/unstable/http" import { FetchHttpClient } from "effect/unstable/http"
import { Otlp } from "effect/unstable/observability" import { Otlp } from "effect/unstable/observability"
import { EffectLogger } from "@/effect/logger"
import { Flag } from "@/flag/flag" import { Flag } from "@/flag/flag"
import { CHANNEL, VERSION } from "@/installation/meta" import { CHANNEL, VERSION } from "@/installation/meta"
export namespace Observability { export namespace Observability {
const base = Flag.OTEL_EXPORTER_OTLP_ENDPOINT export const enabled = !!Flag.OTEL_EXPORTER_OTLP_ENDPOINT
export const enabled = !!base
const resource = { export const layer = !Flag.OTEL_EXPORTER_OTLP_ENDPOINT
? Layer.empty
: Otlp.layerJson({
baseUrl: Flag.OTEL_EXPORTER_OTLP_ENDPOINT,
loggerMergeWithExisting: false,
resource: {
serviceName: "opencode", serviceName: "opencode",
serviceVersion: VERSION, serviceVersion: VERSION,
attributes: { attributes: {
"deployment.environment.name": CHANNEL === "local" ? "local" : CHANNEL, "deployment.environment.name": CHANNEL === "local" ? "local" : CHANNEL,
"opencode.client": Flag.OPENCODE_CLIENT, "opencode.client": Flag.OPENCODE_CLIENT,
}, },
} },
headers: Flag.OTEL_EXPORTER_OTLP_HEADERS
const headers = Flag.OTEL_EXPORTER_OTLP_HEADERS
? Flag.OTEL_EXPORTER_OTLP_HEADERS.split(",").reduce( ? Flag.OTEL_EXPORTER_OTLP_HEADERS.split(",").reduce(
(acc, x) => { (acc, x) => {
const [key, value] = x.split("=") const [key, value] = x.split("=")
@@ -27,15 +29,6 @@ export namespace Observability {
}, },
{} as Record<string, string>, {} as Record<string, string>,
) )
: undefined : undefined,
}).pipe(Layer.provide(FetchHttpClient.layer))
export const layer = !base
? EffectLogger.layer
: Otlp.layerJson({
baseUrl: base,
loggerExportInterval: Duration.seconds(1),
loggerMergeWithExisting: true,
resource,
headers,
}).pipe(Layer.provide(EffectLogger.layer), Layer.provide(FetchHttpClient.layer))
} }
+4 -4
View File
@@ -1,7 +1,7 @@
import { Effect, Layer, ManagedRuntime } from "effect" import { Effect, Layer, ManagedRuntime } from "effect"
import * as Context from "effect/Context" import * as ServiceMap from "effect/ServiceMap"
import { Instance } from "@/project/instance" import { Instance } from "@/project/instance"
import { LocalContext } from "@/util/local-context" import { Context } from "@/util/context"
import { InstanceRef, WorkspaceRef } from "./instance-ref" import { InstanceRef, WorkspaceRef } from "./instance-ref"
import { Observability } from "./oltp" import { Observability } from "./oltp"
import { WorkspaceContext } from "@/control-plane/workspace-context" import { WorkspaceContext } from "@/control-plane/workspace-context"
@@ -14,12 +14,12 @@ export function attach<A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A
const workspaceID = WorkspaceContext.workspaceID const workspaceID = WorkspaceContext.workspaceID
return effect.pipe(Effect.provideService(InstanceRef, ctx), Effect.provideService(WorkspaceRef, workspaceID)) return effect.pipe(Effect.provideService(InstanceRef, ctx), Effect.provideService(WorkspaceRef, workspaceID))
} catch (err) { } catch (err) {
if (!(err instanceof LocalContext.NotFound)) throw err if (!(err instanceof Context.NotFound)) throw err
} }
return effect return effect
} }
export function makeRuntime<I, S, E>(service: Context.Service<I, S>, layer: Layer.Layer<I, E>) { export function makeRuntime<I, S, E>(service: ServiceMap.Service<I, S>, layer: Layer.Layer<I, E>) {
let rt: ManagedRuntime.ManagedRuntime<I, E> | undefined let rt: ManagedRuntime.ManagedRuntime<I, E> | undefined
const getRuntime = () => (rt ??= ManagedRuntime.make(Layer.merge(layer, Observability.layer), { memoMap })) const getRuntime = () => (rt ??= ManagedRuntime.make(Layer.merge(layer, Observability.layer), { memoMap }))
+2 -2
View File
@@ -3,7 +3,7 @@ import { InstanceState } from "@/effect/instance-state"
import { makeRuntime } from "@/effect/run-service" import { makeRuntime } from "@/effect/run-service"
import { AppFileSystem } from "@/filesystem" import { AppFileSystem } from "@/filesystem"
import { Git } from "@/git" import { Git } from "@/git"
import { Effect, Layer, Context } from "effect" import { Effect, Layer, ServiceMap } from "effect"
import { formatPatch, structuredPatch } from "diff" import { formatPatch, structuredPatch } from "diff"
import fuzzysort from "fuzzysort" import fuzzysort from "fuzzysort"
import ignore from "ignore" import ignore from "ignore"
@@ -337,7 +337,7 @@ export namespace File {
}) => Effect.Effect<string[]> }) => Effect.Effect<string[]>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/File") {} export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/File") {}
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
+2 -2
View File
@@ -3,7 +3,7 @@ import path from "path"
import { Global } from "../global" import { Global } from "../global"
import fs from "fs/promises" import fs from "fs/promises"
import z from "zod" import z from "zod"
import { Effect, Layer, Context } from "effect" import { Effect, Layer, ServiceMap } from "effect"
import * as Stream from "effect/Stream" import * as Stream from "effect/Stream"
import { ChildProcess } from "effect/unstable/process" import { ChildProcess } from "effect/unstable/process"
import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner" import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
@@ -291,7 +291,7 @@ export namespace Ripgrep {
}) => Stream.Stream<string, PlatformError> }) => Stream.Stream<string, PlatformError>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/Ripgrep") {} export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Ripgrep") {}
export const layer: Layer.Layer<Service, never, ChildProcessSpawner | AppFileSystem.Service> = Layer.effect( export const layer: Layer.Layer<Service, never, ChildProcessSpawner | AppFileSystem.Service> = Layer.effect(
Service, Service,
+24 -5
View File
@@ -1,5 +1,6 @@
import { DateTime, Effect, Layer, Option, Semaphore, Context } from "effect" import { DateTime, Effect, Layer, Option, Semaphore, ServiceMap } from "effect"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { makeRuntime } from "@/effect/run-service"
import { AppFileSystem } from "@/filesystem" import { AppFileSystem } from "@/filesystem"
import { Flag } from "@/flag/flag" import { Flag } from "@/flag/flag"
import type { SessionID } from "@/session/schema" import type { SessionID } from "@/session/schema"
@@ -33,10 +34,10 @@ export namespace FileTime {
readonly read: (sessionID: SessionID, file: string) => Effect.Effect<void> readonly read: (sessionID: SessionID, file: string) => Effect.Effect<void>
readonly get: (sessionID: SessionID, file: string) => Effect.Effect<Date | undefined> readonly get: (sessionID: SessionID, file: string) => Effect.Effect<Date | undefined>
readonly assert: (sessionID: SessionID, filepath: string) => Effect.Effect<void> readonly assert: (sessionID: SessionID, filepath: string) => Effect.Effect<void>
readonly withLock: <T>(filepath: string, fn: () => Effect.Effect<T>) => Effect.Effect<T> readonly withLock: <T>(filepath: string, fn: () => Promise<T>) => Effect.Effect<T>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/FileTime") {} export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/FileTime") {}
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
@@ -102,8 +103,8 @@ export namespace FileTime {
) )
}) })
const withLock = Effect.fn("FileTime.withLock")(function* <T>(filepath: string, fn: () => Effect.Effect<T>) { const withLock = Effect.fn("FileTime.withLock")(function* <T>(filepath: string, fn: () => Promise<T>) {
return yield* fn().pipe((yield* getLock(filepath)).withPermits(1)) return yield* Effect.promise(fn).pipe((yield* getLock(filepath)).withPermits(1))
}) })
return Service.of({ read, get, assert, withLock }) return Service.of({ read, get, assert, withLock })
@@ -111,4 +112,22 @@ export namespace FileTime {
).pipe(Layer.orDie) ).pipe(Layer.orDie)
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer)) export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
const { runPromise } = makeRuntime(Service, defaultLayer)
export function read(sessionID: SessionID, file: string) {
return runPromise((s) => s.read(sessionID, file))
}
export function get(sessionID: SessionID, file: string) {
return runPromise((s) => s.get(sessionID, file))
}
export async function assert(sessionID: SessionID, filepath: string) {
return runPromise((s) => s.assert(sessionID, filepath))
}
export async function withLock<T>(filepath: string, fn: () => Promise<T>): Promise<T> {
return runPromise((s) => s.withLock(filepath, fn))
}
} }
+9 -2
View File
@@ -1,4 +1,4 @@
import { Cause, Effect, Layer, Scope, Context } from "effect" import { Cause, Effect, Layer, Scope, ServiceMap } from "effect"
// @ts-ignore // @ts-ignore
import { createWrapper } from "@parcel/watcher/wrapper" import { createWrapper } from "@parcel/watcher/wrapper"
import type ParcelWatcher from "@parcel/watcher" import type ParcelWatcher from "@parcel/watcher"
@@ -8,6 +8,7 @@ import z from "zod"
import { Bus } from "@/bus" import { Bus } from "@/bus"
import { BusEvent } from "@/bus/bus-event" import { BusEvent } from "@/bus/bus-event"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { makeRuntime } from "@/effect/run-service"
import { Flag } from "@/flag/flag" import { Flag } from "@/flag/flag"
import { Git } from "@/git" import { Git } from "@/git"
import { Instance } from "@/project/instance" import { Instance } from "@/project/instance"
@@ -64,7 +65,7 @@ export namespace FileWatcher {
readonly init: () => Effect.Effect<void> readonly init: () => Effect.Effect<void>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/FileWatcher") {} export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/FileWatcher") {}
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
@@ -160,4 +161,10 @@ export namespace FileWatcher {
) )
export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer), Layer.provide(Git.defaultLayer)) export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer), Layer.provide(Git.defaultLayer))
const { runPromise } = makeRuntime(Service, defaultLayer)
export function init() {
return runPromise((svc) => svc.init())
}
} }
+2 -2
View File
@@ -3,7 +3,7 @@ import { dirname, join, relative, resolve as pathResolve } from "path"
import { realpathSync } from "fs" import { realpathSync } from "fs"
import * as NFS from "fs/promises" import * as NFS from "fs/promises"
import { lookup } from "mime-types" import { lookup } from "mime-types"
import { Effect, FileSystem, Layer, Schema, Context } from "effect" import { Effect, FileSystem, Layer, Schema, ServiceMap } from "effect"
import type { PlatformError } from "effect/PlatformError" import type { PlatformError } from "effect/PlatformError"
import { Glob } from "../util/glob" import { Glob } from "../util/glob"
@@ -36,7 +36,7 @@ export namespace AppFileSystem {
readonly globMatch: (pattern: string, filepath: string) => boolean readonly globMatch: (pattern: string, filepath: string) => boolean
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/FileSystem") {} export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/FileSystem") {}
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
+17 -2
View File
@@ -1,7 +1,8 @@
import { Effect, Layer, Context } from "effect" import { Effect, Layer, ServiceMap } from "effect"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { makeRuntime } from "@/effect/run-service"
import path from "path" import path from "path"
import { mergeDeep } from "remeda" import { mergeDeep } from "remeda"
import z from "zod" import z from "zod"
@@ -30,7 +31,7 @@ export namespace Format {
readonly file: (filepath: string) => Effect.Effect<void> readonly file: (filepath: string) => Effect.Effect<void>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/Format") {} export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Format") {}
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
@@ -192,4 +193,18 @@ export namespace Format {
Layer.provide(Config.defaultLayer), Layer.provide(Config.defaultLayer),
Layer.provide(CrossSpawnSpawner.defaultLayer), Layer.provide(CrossSpawnSpawner.defaultLayer),
) )
const { runPromise } = makeRuntime(Service, defaultLayer)
export async function init() {
return runPromise((s) => s.init())
}
export async function status() {
return runPromise((s) => s.status())
}
export async function file(filepath: string) {
return runPromise((s) => s.file(filepath))
}
} }
+13 -2
View File
@@ -1,6 +1,7 @@
import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
import { Effect, Layer, Context, Stream } from "effect" import { Effect, Layer, ServiceMap, Stream } from "effect"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
import { makeRuntime } from "@/effect/run-service"
export namespace Git { export namespace Git {
const cfg = [ const cfg = [
@@ -79,7 +80,7 @@ export namespace Git {
return "modified" return "modified"
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/Git") {} export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Git") {}
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
@@ -257,4 +258,14 @@ export namespace Git {
) )
export const defaultLayer = layer.pipe(Layer.provide(CrossSpawnSpawner.defaultLayer)) export const defaultLayer = layer.pipe(Layer.provide(CrossSpawnSpawner.defaultLayer))
const { runPromise } = makeRuntime(Service, defaultLayer)
export async function run(args: string[], opts: Options) {
return runPromise((git) => git.run(args, opts))
}
export async function defaultBranch(cwd: string) {
return runPromise((git) => git.defaultBranch(cwd))
}
} }
+17 -2
View File
@@ -1,6 +1,7 @@
import { Effect, Layer, Schema, Context, Stream } from "effect" import { Effect, Layer, Schema, ServiceMap, Stream } from "effect"
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
import { makeRuntime } from "@/effect/run-service"
import { withTransientReadRetry } from "@/util/effect-http-client" import { withTransientReadRetry } from "@/util/effect-http-client"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
import path from "path" import path from "path"
@@ -90,7 +91,7 @@ export namespace Installation {
readonly upgrade: (method: Method, target: string) => Effect.Effect<void, UpgradeFailedError> readonly upgrade: (method: Method, target: string) => Effect.Effect<void, UpgradeFailedError>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/Installation") {} export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Installation") {}
export const layer: Layer.Layer<Service, never, HttpClient.HttpClient | ChildProcessSpawner.ChildProcessSpawner> = export const layer: Layer.Layer<Service, never, HttpClient.HttpClient | ChildProcessSpawner.ChildProcessSpawner> =
Layer.effect( Layer.effect(
@@ -337,4 +338,18 @@ export namespace Installation {
Layer.provide(FetchHttpClient.layer), Layer.provide(FetchHttpClient.layer),
Layer.provide(CrossSpawnSpawner.defaultLayer), Layer.provide(CrossSpawnSpawner.defaultLayer),
) )
const { runPromise } = makeRuntime(Service, defaultLayer)
export async function method(): Promise<Method> {
return runPromise((svc) => svc.method())
}
export async function latest(installMethod?: Method): Promise<string> {
return runPromise((svc) => svc.latest(installMethod))
}
export async function upgrade(m: Method, target: string): Promise<void> {
return runPromise((svc) => svc.upgrade(m, target))
}
} }
+2 -13
View File
@@ -11,7 +11,7 @@ import { Instance } from "../project/instance"
import { Flag } from "@/flag/flag" import { Flag } from "@/flag/flag"
import { Process } from "../util/process" import { Process } from "../util/process"
import { spawn as lspspawn } from "./launch" import { spawn as lspspawn } from "./launch"
import { Effect, Layer, Context } from "effect" import { Effect, Layer, ServiceMap } from "effect"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { makeRuntime } from "@/effect/run-service" import { makeRuntime } from "@/effect/run-service"
@@ -156,7 +156,7 @@ export namespace LSP {
readonly outgoingCalls: (input: LocInput) => Effect.Effect<any[]> readonly outgoingCalls: (input: LocInput) => Effect.Effect<any[]>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/LSP") {} export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/LSP") {}
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
@@ -540,8 +540,6 @@ export namespace LSP {
export const outgoingCalls = async (input: LocInput) => runPromise((svc) => svc.outgoingCalls(input)) export const outgoingCalls = async (input: LocInput) => runPromise((svc) => svc.outgoingCalls(input))
export namespace Diagnostic { export namespace Diagnostic {
const MAX_PER_FILE = 20
export function pretty(diagnostic: LSPClient.Diagnostic) { export function pretty(diagnostic: LSPClient.Diagnostic) {
const severityMap = { const severityMap = {
1: "ERROR", 1: "ERROR",
@@ -556,14 +554,5 @@ export namespace LSP {
return `${severity} [${line}:${col}] ${diagnostic.message}` return `${severity} [${line}:${col}] ${diagnostic.message}`
} }
export function report(file: string, issues: LSPClient.Diagnostic[]) {
const errors = issues.filter((item) => item.severity === 1)
if (errors.length === 0) return ""
const limited = errors.slice(0, MAX_PER_FILE)
const more = errors.length - MAX_PER_FILE
const suffix = more > 0 ? `\n... and ${more} more` : ""
return `<diagnostics file="${file}">\n${limited.map(pretty).join("\n")}${suffix}\n</diagnostics>`
}
} }
} }
+3 -3
View File
@@ -1,7 +1,7 @@
import path from "path" import path from "path"
import z from "zod" import z from "zod"
import { Global } from "../global" import { Global } from "../global"
import { Effect, Layer, Context } from "effect" import { Effect, Layer, ServiceMap } from "effect"
import { AppFileSystem } from "@/filesystem" import { AppFileSystem } from "@/filesystem"
import { makeRuntime } from "@/effect/run-service" import { makeRuntime } from "@/effect/run-service"
@@ -49,7 +49,7 @@ export namespace McpAuth {
readonly isTokenExpired: (mcpName: string) => Effect.Effect<boolean | null> readonly isTokenExpired: (mcpName: string) => Effect.Effect<boolean | null>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/McpAuth") {} export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/McpAuth") {}
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
@@ -141,7 +141,7 @@ export namespace McpAuth {
}), }),
) )
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer)) const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
const { runPromise } = makeRuntime(Service, defaultLayer) const { runPromise } = makeRuntime(Service, defaultLayer)
+4 -7
View File
@@ -24,8 +24,7 @@ import { BusEvent } from "../bus/bus-event"
import { Bus } from "@/bus" import { Bus } from "@/bus"
import { TuiEvent } from "@/cli/cmd/tui/event" import { TuiEvent } from "@/cli/cmd/tui/event"
import open from "open" import open from "open"
import { Effect, Exit, Layer, Option, Context, Stream } from "effect" import { Effect, Exit, Layer, Option, ServiceMap, Stream } from "effect"
import { EffectLogger } from "@/effect/logger"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { makeRuntime } from "@/effect/run-service" import { makeRuntime } from "@/effect/run-service"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
@@ -240,7 +239,7 @@ export namespace MCP {
readonly getAuthStatus: (mcpName: string) => Effect.Effect<AuthStatus> readonly getAuthStatus: (mcpName: string) => Effect.Effect<AuthStatus>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/MCP") {} export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/MCP") {}
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
@@ -470,14 +469,12 @@ export namespace MCP {
log.info("tools list changed notification received", { server: name }) log.info("tools list changed notification received", { server: name })
if (s.clients[name] !== client || s.status[name]?.status !== "connected") return if (s.clients[name] !== client || s.status[name]?.status !== "connected") return
const listed = await Effect.runPromise(defs(name, client, timeout).pipe(Effect.provide(EffectLogger.layer))) const listed = await Effect.runPromise(defs(name, client, timeout))
if (!listed) return if (!listed) return
if (s.clients[name] !== client || s.status[name]?.status !== "connected") return if (s.clients[name] !== client || s.status[name]?.status !== "connected") return
s.defs[name] = listed s.defs[name] = listed
await Effect.runPromise( await Effect.runPromise(bus.publish(ToolsChanged, { server: name }).pipe(Effect.ignore))
bus.publish(ToolsChanged, { server: name }).pipe(Effect.ignore, Effect.provide(EffectLogger.layer)),
)
}) })
} }
+2 -2
View File
@@ -10,7 +10,7 @@ import { PermissionTable } from "@/session/session.sql"
import { Database, eq } from "@/storage/db" import { Database, eq } from "@/storage/db"
import { Log } from "@/util/log" import { Log } from "@/util/log"
import { Wildcard } from "@/util/wildcard" import { Wildcard } from "@/util/wildcard"
import { Deferred, Effect, Layer, Schema, Context } from "effect" import { Deferred, Effect, Layer, Schema, ServiceMap } from "effect"
import os from "os" import os from "os"
import z from "zod" import z from "zod"
import { evaluate as evalRule } from "./evaluate" import { evaluate as evalRule } from "./evaluate"
@@ -135,7 +135,7 @@ export namespace Permission {
return evalRule(permission, pattern, ...rulesets) return evalRule(permission, pattern, ...rulesets)
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/Permission") {} export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Permission") {}
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
+5 -1
View File
@@ -5,8 +5,12 @@ import { Identifier } from "@/id/id"
import { Newtype } from "@/util/schema" import { Newtype } from "@/util/schema"
export class PermissionID extends Newtype<PermissionID>()("PermissionID", Schema.String) { export class PermissionID extends Newtype<PermissionID>()("PermissionID", Schema.String) {
static make(id: string): PermissionID {
return this.makeUnsafe(id)
}
static ascending(id?: string): PermissionID { static ascending(id?: string): PermissionID {
return this.make(Identifier.ascending("permission", id)) return this.makeUnsafe(Identifier.ascending("permission", id))
} }
static readonly zod = Identifier.schema("permission") as unknown as z.ZodType<PermissionID> static readonly zod = Identifier.schema("permission") as unknown as z.ZodType<PermissionID>
@@ -5,7 +5,6 @@ import { iife } from "@/util/iife"
import { Log } from "../../util/log" import { Log } from "../../util/log"
import { setTimeout as sleep } from "node:timers/promises" import { setTimeout as sleep } from "node:timers/promises"
import { CopilotModels } from "./models" import { CopilotModels } from "./models"
import { MessageV2 } from "@/session/message-v2"
const log = Log.create({ service: "plugin.copilot" }) const log = Log.create({ service: "plugin.copilot" })
@@ -28,27 +27,11 @@ function base(enterpriseUrl?: string) {
return enterpriseUrl ? `https://copilot-api.${normalizeDomain(enterpriseUrl)}` : "https://api.githubcopilot.com" return enterpriseUrl ? `https://copilot-api.${normalizeDomain(enterpriseUrl)}` : "https://api.githubcopilot.com"
} }
// Check if a message is a synthetic user msg used to attach an image from a tool call function fix(model: Model): Model {
function imgMsg(msg: any): boolean {
if (msg?.role !== "user") return false
// Handle the 3 api formats
const content = msg.content
if (typeof content === "string") return content === MessageV2.SYNTHETIC_ATTACHMENT_PROMPT
if (!Array.isArray(content)) return false
return content.some(
(part: any) =>
(part?.type === "text" || part?.type === "input_text") && part.text === MessageV2.SYNTHETIC_ATTACHMENT_PROMPT,
)
}
function fix(model: Model, url: string): Model {
return { return {
...model, ...model,
api: { api: {
...model.api, ...model.api,
url,
npm: "@ai-sdk/github-copilot", npm: "@ai-sdk/github-copilot",
}, },
} }
@@ -61,23 +44,19 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise<Hooks> {
id: "github-copilot", id: "github-copilot",
async models(provider, ctx) { async models(provider, ctx) {
if (ctx.auth?.type !== "oauth") { if (ctx.auth?.type !== "oauth") {
return Object.fromEntries(Object.entries(provider.models).map(([id, model]) => [id, fix(model, base())])) return Object.fromEntries(Object.entries(provider.models).map(([id, model]) => [id, fix(model)]))
} }
const auth = ctx.auth
return CopilotModels.get( return CopilotModels.get(
base(auth.enterpriseUrl), base(ctx.auth.enterpriseUrl),
{ {
Authorization: `Bearer ${auth.refresh}`, Authorization: `Bearer ${ctx.auth.refresh}`,
"User-Agent": `opencode/${Installation.VERSION}`, "User-Agent": `opencode/${Installation.VERSION}`,
}, },
provider.models, provider.models,
).catch((error) => { ).catch((error) => {
log.error("failed to fetch copilot models", { error }) log.error("failed to fetch copilot models", { error })
return Object.fromEntries( return Object.fromEntries(Object.entries(provider.models).map(([id, model]) => [id, fix(model)]))
Object.entries(provider.models).map(([id, model]) => [id, fix(model, base(auth.enterpriseUrl))]),
)
}) })
}, },
}, },
@@ -87,7 +66,10 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise<Hooks> {
const info = await getAuth() const info = await getAuth()
if (!info || info.type !== "oauth") return {} if (!info || info.type !== "oauth") return {}
const baseURL = base(info.enterpriseUrl)
return { return {
baseURL,
apiKey: "", apiKey: "",
async fetch(request: RequestInfo | URL, init?: RequestInit) { async fetch(request: RequestInfo | URL, init?: RequestInit) {
const info = await getAuth() const info = await getAuth()
@@ -106,7 +88,7 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise<Hooks> {
(msg: any) => (msg: any) =>
Array.isArray(msg.content) && msg.content.some((part: any) => part.type === "image_url"), Array.isArray(msg.content) && msg.content.some((part: any) => part.type === "image_url"),
), ),
isAgent: last?.role !== "user" || imgMsg(last), isAgent: last?.role !== "user",
} }
} }
@@ -118,7 +100,7 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise<Hooks> {
(item: any) => (item: any) =>
Array.isArray(item?.content) && item.content.some((part: any) => part.type === "input_image"), Array.isArray(item?.content) && item.content.some((part: any) => part.type === "input_image"),
), ),
isAgent: last?.role !== "user" || imgMsg(last), isAgent: last?.role !== "user",
} }
} }
@@ -140,7 +122,7 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise<Hooks> {
part.content.some((nested: any) => nested?.type === "image")), part.content.some((nested: any) => nested?.type === "image")),
), ),
), ),
isAgent: !(last?.role === "user" && hasNonToolCalls) || imgMsg(last), isAgent: !(last?.role === "user" && hasNonToolCalls),
} }
} }
} catch {} } catch {}
@@ -52,15 +52,13 @@ export namespace CopilotModels {
(remote.capabilities.supports.vision ?? false) || (remote.capabilities.supports.vision ?? false) ||
(remote.capabilities.limits.vision?.supported_media_types ?? []).some((item) => item.startsWith("image/")) (remote.capabilities.limits.vision?.supported_media_types ?? []).some((item) => item.startsWith("image/"))
const isMsgApi = remote.supported_endpoints?.includes("/v1/messages")
return { return {
id: key, id: key,
providerID: "github-copilot", providerID: "github-copilot",
api: { api: {
id: remote.id, id: remote.id,
url: isMsgApi ? `${url}/v1` : url, url,
npm: isMsgApi ? "@ai-sdk/anthropic" : "@ai-sdk/github-copilot", npm: "@ai-sdk/github-copilot",
}, },
// API response wins // API response wins
status: "active", status: "active",
+10 -17
View File
@@ -11,8 +11,7 @@ import { CopilotAuthPlugin } from "./github-copilot/copilot"
import { gitlabAuthPlugin as GitlabAuthPlugin } from "opencode-gitlab-auth" import { gitlabAuthPlugin as GitlabAuthPlugin } from "opencode-gitlab-auth"
import { PoeAuthPlugin } from "opencode-poe-auth" import { PoeAuthPlugin } from "opencode-poe-auth"
import { CloudflareAIGatewayAuthPlugin, CloudflareWorkersAuthPlugin } from "./cloudflare" import { CloudflareAIGatewayAuthPlugin, CloudflareWorkersAuthPlugin } from "./cloudflare"
import { Effect, Layer, Context, Stream } from "effect" import { Effect, Layer, ServiceMap, Stream } from "effect"
import { EffectLogger } from "@/effect/logger"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { makeRuntime } from "@/effect/run-service" import { makeRuntime } from "@/effect/run-service"
import { errorMessage } from "@/util/error" import { errorMessage } from "@/util/error"
@@ -45,7 +44,7 @@ export namespace Plugin {
readonly init: () => Effect.Effect<void> readonly init: () => Effect.Effect<void>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/Plugin") {} export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Plugin") {}
// Built-in plugins that are directly imported (not installed from npm) // Built-in plugins that are directly imported (not installed from npm)
const INTERNAL_PLUGINS: PluginInstance[] = [ const INTERNAL_PLUGINS: PluginInstance[] = [
@@ -84,11 +83,7 @@ export namespace Plugin {
} }
function publishPluginError(bus: Bus.Interface, message: string) { function publishPluginError(bus: Bus.Interface, message: string) {
Effect.runFork( Effect.runFork(bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() }))
bus
.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() })
.pipe(Effect.provide(EffectLogger.layer)),
)
} }
async function applyPlugin(load: PluginLoader.Loaded, input: PluginInput, hooks: Hooks[]) { async function applyPlugin(load: PluginLoader.Loaded, input: PluginInput, hooks: Hooks[]) {
@@ -124,7 +119,7 @@ export namespace Plugin {
Authorization: `Basic ${Buffer.from(`${Flag.OPENCODE_SERVER_USERNAME ?? "opencode"}:${Flag.OPENCODE_SERVER_PASSWORD}`).toString("base64")}`, Authorization: `Basic ${Buffer.from(`${Flag.OPENCODE_SERVER_USERNAME ?? "opencode"}:${Flag.OPENCODE_SERVER_PASSWORD}`).toString("base64")}`,
} }
: undefined, : undefined,
fetch: async (...args) => (await Server.Default()).app.fetch(...args), fetch: async (...args) => Server.Default().app.fetch(...args),
}) })
const cfg = yield* config.get() const cfg = yield* config.get()
const input: PluginInput = { const input: PluginInput = {
@@ -210,15 +205,13 @@ export namespace Plugin {
return message return message
}, },
}).pipe( }).pipe(
Effect.catch(() => { Effect.catch((message) =>
// TODO: make proper events for this bus.publish(Session.Event.Error, {
// bus.publish(Session.Event.Error, { error: new NamedError.Unknown({
// error: new NamedError.Unknown({ message: `Failed to load plugin ${load.spec}: ${message}`,
// message: `Failed to load plugin ${load.spec}: ${message}`, }).toObject(),
// }).toObject(),
// })
return Effect.void
}), }),
),
) )
} }
+4 -5
View File
@@ -2,6 +2,7 @@ import { Plugin } from "../plugin"
import { Format } from "../format" import { Format } from "../format"
import { LSP } from "../lsp" import { LSP } from "../lsp"
import { File } from "../file" import { File } from "../file"
import { FileWatcher } from "../file/watcher"
import { Snapshot } from "../snapshot" import { Snapshot } from "../snapshot"
import { Project } from "./project" import { Project } from "./project"
import { Vcs } from "./vcs" import { Vcs } from "./vcs"
@@ -9,18 +10,16 @@ import { Bus } from "../bus"
import { Command } from "../command" import { Command } from "../command"
import { Instance } from "./instance" import { Instance } from "./instance"
import { Log } from "@/util/log" import { Log } from "@/util/log"
import { BootstrapRuntime } from "@/effect/bootstrap-runtime"
import { FileWatcher } from "@/file/watcher"
import { ShareNext } from "@/share/share-next" import { ShareNext } from "@/share/share-next"
export async function InstanceBootstrap() { export async function InstanceBootstrap() {
Log.Default.info("bootstrapping", { directory: Instance.directory }) Log.Default.info("bootstrapping", { directory: Instance.directory })
await Plugin.init() await Plugin.init()
void BootstrapRuntime.runPromise(ShareNext.Service.use((svc) => svc.init())) ShareNext.init()
void BootstrapRuntime.runPromise(Format.Service.use((svc) => svc.init())) Format.init()
await LSP.init() await LSP.init()
File.init() File.init()
void BootstrapRuntime.runPromise(FileWatcher.Service.use((svc) => svc.init())) FileWatcher.init()
Vcs.init() Vcs.init()
Snapshot.init() Snapshot.init()
+5 -6
View File
@@ -3,7 +3,7 @@ import { disposeInstance } from "@/effect/instance-registry"
import { Filesystem } from "@/util/filesystem" import { Filesystem } from "@/util/filesystem"
import { iife } from "@/util/iife" import { iife } from "@/util/iife"
import { Log } from "@/util/log" import { Log } from "@/util/log"
import { LocalContext } from "../util/local-context" import { Context } from "../util/context"
import { Project } from "./project" import { Project } from "./project"
import { WorkspaceContext } from "@/control-plane/workspace-context" import { WorkspaceContext } from "@/control-plane/workspace-context"
import { State } from "./state" import { State } from "./state"
@@ -14,7 +14,7 @@ export interface InstanceContext {
project: Project.Info project: Project.Info
} }
const context = LocalContext.create<InstanceContext>("instance") const context = Context.create<InstanceContext>("instance")
const cache = new Map<string, Promise<InstanceContext>>() const cache = new Map<string, Promise<InstanceContext>>()
const disposal = { const disposal = {
@@ -90,13 +90,12 @@ export const Instance = {
* Returns true if path is inside Instance.directory OR Instance.worktree. * Returns true if path is inside Instance.directory OR Instance.worktree.
* Paths within the worktree but outside the working directory should not trigger external_directory permission. * Paths within the worktree but outside the working directory should not trigger external_directory permission.
*/ */
containsPath(filepath: string, ctx?: InstanceContext) { containsPath(filepath: string) {
const instance = ctx ?? Instance if (Filesystem.contains(Instance.directory, filepath)) return true
if (Filesystem.contains(instance.directory, filepath)) return true
// Non-git projects set worktree to "/" which would match ANY absolute path. // Non-git projects set worktree to "/" which would match ANY absolute path.
// Skip worktree check in this case to preserve external_directory permissions. // Skip worktree check in this case to preserve external_directory permissions.
if (Instance.worktree === "/") return false if (Instance.worktree === "/") return false
return Filesystem.contains(instance.worktree, filepath) return Filesystem.contains(Instance.worktree, filepath)
}, },
/** /**
* Captures the current instance ALS context and returns a wrapper that * Captures the current instance ALS context and returns a wrapper that
+2 -2
View File
@@ -8,7 +8,7 @@ import { BusEvent } from "@/bus/bus-event"
import { GlobalBus } from "@/bus/global" import { GlobalBus } from "@/bus/global"
import { which } from "../util/which" import { which } from "../util/which"
import { ProjectID } from "./schema" import { ProjectID } from "./schema"
import { Effect, Layer, Path, Scope, Context, Stream } from "effect" import { Effect, Layer, Path, Scope, ServiceMap, Stream } from "effect"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
import { NodeFileSystem, NodePath } from "@effect/platform-node" import { NodeFileSystem, NodePath } from "@effect/platform-node"
import { makeRuntime } from "@/effect/run-service" import { makeRuntime } from "@/effect/run-service"
@@ -100,7 +100,7 @@ export namespace Project {
readonly removeSandbox: (id: ProjectID, directory: string) => Effect.Effect<void> readonly removeSandbox: (id: ProjectID, directory: string) => Effect.Effect<void>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/Project") {} export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Project") {}
type GitResult = { code: number; text: string; stderr: string } type GitResult = { code: number; text: string; stderr: string }
+2 -1
View File
@@ -9,7 +9,8 @@ export type ProjectID = typeof projectIdSchema.Type
export const ProjectID = projectIdSchema.pipe( export const ProjectID = projectIdSchema.pipe(
withStatics((schema: typeof projectIdSchema) => ({ withStatics((schema: typeof projectIdSchema) => ({
global: schema.make("global"), global: schema.makeUnsafe("global"),
make: (id: string) => schema.makeUnsafe(id),
zod: z.string().pipe(z.custom<ProjectID>()), zod: z.string().pipe(z.custom<ProjectID>()),
})), })),
) )
+3 -3
View File
@@ -1,4 +1,4 @@
import { Effect, Layer, Context, Stream } from "effect" import { Effect, Layer, ServiceMap, Stream } from "effect"
import { formatPatch, structuredPatch } from "diff" import { formatPatch, structuredPatch } from "diff"
import path from "path" import path from "path"
import { Bus } from "@/bus" import { Bus } from "@/bus"
@@ -151,7 +151,7 @@ export namespace Vcs {
root: Git.Base | undefined root: Git.Base | undefined
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/Vcs") {} export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Vcs") {}
export const layer: Layer.Layer<Service, never, AppFileSystem.Service | Git.Service | Bus.Service> = Layer.effect( export const layer: Layer.Layer<Service, never, AppFileSystem.Service | Git.Service | Bus.Service> = Layer.effect(
Service, Service,
@@ -226,7 +226,7 @@ export namespace Vcs {
}), }),
) )
export const defaultLayer = layer.pipe( const defaultLayer = layer.pipe(
Layer.provide(Git.defaultLayer), Layer.provide(Git.defaultLayer),
Layer.provide(AppFileSystem.defaultLayer), Layer.provide(AppFileSystem.defaultLayer),
Layer.provide(Bus.layer), Layer.provide(Bus.layer),
+21 -2
View File
@@ -2,9 +2,10 @@ import type { AuthOAuthResult, Hooks } from "@opencode-ai/plugin"
import { NamedError } from "@opencode-ai/util/error" import { NamedError } from "@opencode-ai/util/error"
import { Auth } from "@/auth" import { Auth } from "@/auth"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { makeRuntime } from "@/effect/run-service"
import { Plugin } from "../plugin" import { Plugin } from "../plugin"
import { ProviderID } from "./schema" import { ProviderID } from "./schema"
import { Array as Arr, Effect, Layer, Record, Result, Context } from "effect" import { Array as Arr, Effect, Layer, Record, Result, ServiceMap } from "effect"
import z from "zod" import z from "zod"
export namespace ProviderAuth { export namespace ProviderAuth {
@@ -108,7 +109,7 @@ export namespace ProviderAuth {
pending: Map<ProviderID, AuthOAuthResult> pending: Map<ProviderID, AuthOAuthResult>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/ProviderAuth") {} export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/ProviderAuth") {}
export const layer: Layer.Layer<Service, never, Auth.Service | Plugin.Service> = Layer.effect( export const layer: Layer.Layer<Service, never, Auth.Service | Plugin.Service> = Layer.effect(
Service, Service,
@@ -231,4 +232,22 @@ export namespace ProviderAuth {
export const defaultLayer = Layer.suspend(() => export const defaultLayer = Layer.suspend(() =>
layer.pipe(Layer.provide(Auth.defaultLayer), Layer.provide(Plugin.defaultLayer)), layer.pipe(Layer.provide(Auth.defaultLayer), Layer.provide(Plugin.defaultLayer)),
) )
const { runPromise } = makeRuntime(Service, defaultLayer)
export async function methods() {
return runPromise((svc) => svc.methods())
}
export async function authorize(input: {
providerID: ProviderID
method: number
inputs?: Record<string, string>
}): Promise<Authorization | undefined> {
return runPromise((svc) => svc.authorize(input))
}
export async function callback(input: { providerID: ProviderID; method: number; code?: string }) {
return runPromise((svc) => svc.callback(input))
}
} }
File diff suppressed because it is too large Load Diff
+3 -5
View File
@@ -19,8 +19,7 @@ import { iife } from "@/util/iife"
import { Global } from "../global" import { Global } from "../global"
import path from "path" import path from "path"
import { Filesystem } from "../util/filesystem" import { Filesystem } from "../util/filesystem"
import { Effect, Layer, Context } from "effect" import { Effect, Layer, ServiceMap } from "effect"
import { EffectLogger } from "@/effect/logger"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { makeRuntime } from "@/effect/run-service" import { makeRuntime } from "@/effect/run-service"
@@ -925,7 +924,7 @@ export namespace Provider {
varsLoaders: Record<string, CustomVarsLoader> varsLoaders: Record<string, CustomVarsLoader>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/Provider") {} export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Provider") {}
function cost(c: ModelsDev.Model["cost"]): Model["cost"] { function cost(c: ModelsDev.Model["cost"]): Model["cost"] {
const result: Model["cost"] = { const result: Model["cost"] = {
@@ -1216,8 +1215,7 @@ export namespace Provider {
const options = yield* Effect.promise(() => const options = yield* Effect.promise(() =>
plugin.auth!.loader!( plugin.auth!.loader!(
() => () => Effect.runPromise(auth.get(providerID).pipe(Effect.orDie)) as any,
Effect.runPromise(auth.get(providerID).pipe(Effect.orDie, Effect.provide(EffectLogger.layer))) as any,
database[plugin.auth!.provider], database[plugin.auth!.provider],
), ),
) )
+13 -11
View File
@@ -9,19 +9,20 @@ export type ProviderID = typeof providerIdSchema.Type
export const ProviderID = providerIdSchema.pipe( export const ProviderID = providerIdSchema.pipe(
withStatics((schema: typeof providerIdSchema) => ({ withStatics((schema: typeof providerIdSchema) => ({
make: (id: string) => schema.makeUnsafe(id),
zod: z.string().pipe(z.custom<ProviderID>()), zod: z.string().pipe(z.custom<ProviderID>()),
// Well-known providers // Well-known providers
opencode: schema.make("opencode"), opencode: schema.makeUnsafe("opencode"),
anthropic: schema.make("anthropic"), anthropic: schema.makeUnsafe("anthropic"),
openai: schema.make("openai"), openai: schema.makeUnsafe("openai"),
google: schema.make("google"), google: schema.makeUnsafe("google"),
googleVertex: schema.make("google-vertex"), googleVertex: schema.makeUnsafe("google-vertex"),
githubCopilot: schema.make("github-copilot"), githubCopilot: schema.makeUnsafe("github-copilot"),
amazonBedrock: schema.make("amazon-bedrock"), amazonBedrock: schema.makeUnsafe("amazon-bedrock"),
azure: schema.make("azure"), azure: schema.makeUnsafe("azure"),
openrouter: schema.make("openrouter"), openrouter: schema.makeUnsafe("openrouter"),
mistral: schema.make("mistral"), mistral: schema.makeUnsafe("mistral"),
gitlab: schema.make("gitlab"), gitlab: schema.makeUnsafe("gitlab"),
})), })),
) )
@@ -31,6 +32,7 @@ export type ModelID = typeof modelIdSchema.Type
export const ModelID = modelIdSchema.pipe( export const ModelID = modelIdSchema.pipe(
withStatics((schema: typeof modelIdSchema) => ({ withStatics((schema: typeof modelIdSchema) => ({
make: (id: string) => schema.makeUnsafe(id),
zod: z.string().pipe(z.custom<ModelID>()), zod: z.string().pipe(z.custom<ModelID>()),
})), })),
) )
+1 -4
View File
@@ -774,10 +774,7 @@ export namespace ProviderTransform {
result["chat_template_args"] = { enable_thinking: true } result["chat_template_args"] = { enable_thinking: true }
} }
if ( if (["zai", "zhipuai"].includes(input.model.providerID) && input.model.api.npm === "@ai-sdk/openai-compatible") {
["zai", "zhipuai"].some((id) => input.model.providerID.includes(id)) &&
input.model.api.npm === "@ai-sdk/openai-compatible"
) {
result["thinking"] = { result["thinking"] = {
type: "enabled", type: "enabled",
clear_thinking: false, clear_thinking: false,
+5 -6
View File
@@ -10,8 +10,7 @@ import { lazy } from "@opencode-ai/util/lazy"
import { Shell } from "@/shell/shell" import { Shell } from "@/shell/shell"
import { Plugin } from "@/plugin" import { Plugin } from "@/plugin"
import { PtyID } from "./schema" import { PtyID } from "./schema"
import { Effect, Layer, Context } from "effect" import { Effect, Layer, ServiceMap } from "effect"
import { EffectLogger } from "@/effect/logger"
export namespace Pty { export namespace Pty {
const log = Log.create({ service: "pty" }) const log = Log.create({ service: "pty" })
@@ -113,7 +112,7 @@ export namespace Pty {
) => Effect.Effect<{ onMessage: (message: string | ArrayBuffer) => void; onClose: () => void } | undefined> ) => Effect.Effect<{ onMessage: (message: string | ArrayBuffer) => void; onClose: () => void } | undefined>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/Pty") {} export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Pty") {}
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
@@ -257,8 +256,8 @@ export namespace Pty {
if (session.info.status === "exited") return if (session.info.status === "exited") return
log.info("session exited", { id, exitCode }) log.info("session exited", { id, exitCode })
session.info.status = "exited" session.info.status = "exited"
Effect.runFork(bus.publish(Event.Exited, { id, exitCode }).pipe(Effect.provide(EffectLogger.layer))) Effect.runFork(bus.publish(Event.Exited, { id, exitCode }))
Effect.runFork(remove(id).pipe(Effect.provide(EffectLogger.layer))) Effect.runFork(remove(id))
}), }),
) )
yield* bus.publish(Event.Created, { info }) yield* bus.publish(Event.Created, { info })
@@ -360,7 +359,7 @@ export namespace Pty {
}), }),
) )
export const defaultLayer = layer.pipe(Layer.provide(Bus.layer), Layer.provide(Plugin.defaultLayer)) const defaultLayer = layer.pipe(Layer.provide(Bus.layer), Layer.provide(Plugin.defaultLayer))
const { runPromise } = makeRuntime(Service, defaultLayer) const { runPromise } = makeRuntime(Service, defaultLayer)
+2 -1
View File
@@ -10,7 +10,8 @@ export type PtyID = typeof ptyIdSchema.Type
export const PtyID = ptyIdSchema.pipe( export const PtyID = ptyIdSchema.pipe(
withStatics((schema: typeof ptyIdSchema) => ({ withStatics((schema: typeof ptyIdSchema) => ({
ascending: (id?: string) => schema.make(Identifier.ascending("pty", id)), make: (id: string) => schema.makeUnsafe(id),
ascending: (id?: string) => schema.makeUnsafe(Identifier.ascending("pty", id)),
zod: Identifier.schema("pty").pipe(z.custom<PtyID>()), zod: Identifier.schema("pty").pipe(z.custom<PtyID>()),
})), })),
) )
+25 -2
View File
@@ -1,7 +1,8 @@
import { Deferred, Effect, Layer, Schema, Context } from "effect" import { Deferred, Effect, Layer, Schema, ServiceMap } from "effect"
import { Bus } from "@/bus" import { Bus } from "@/bus"
import { BusEvent } from "@/bus/bus-event" import { BusEvent } from "@/bus/bus-event"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { makeRuntime } from "@/effect/run-service"
import { SessionID, MessageID } from "@/session/schema" import { SessionID, MessageID } from "@/session/schema"
import { Log } from "@/util/log" import { Log } from "@/util/log"
import z from "zod" import z from "zod"
@@ -103,7 +104,7 @@ export namespace Question {
readonly list: () => Effect.Effect<Request[]> readonly list: () => Effect.Effect<Request[]>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/Question") {} export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Question") {}
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
@@ -198,4 +199,26 @@ export namespace Question {
) )
export const defaultLayer = layer.pipe(Layer.provide(Bus.layer)) export const defaultLayer = layer.pipe(Layer.provide(Bus.layer))
const { runPromise } = makeRuntime(Service, defaultLayer)
export async function ask(input: {
sessionID: SessionID
questions: Info[]
tool?: { messageID: MessageID; callID: string }
}): Promise<Answer[]> {
return runPromise((s) => s.ask(input))
}
export async function reply(input: { requestID: QuestionID; answers: Answer[] }) {
return runPromise((s) => s.reply(input))
}
export async function reject(requestID: QuestionID) {
return runPromise((s) => s.reject(requestID))
}
export async function list() {
return runPromise((s) => s.list())
}
} }
+5 -1
View File
@@ -5,8 +5,12 @@ import { Identifier } from "@/id/id"
import { Newtype } from "@/util/schema" import { Newtype } from "@/util/schema"
export class QuestionID extends Newtype<QuestionID>()("QuestionID", Schema.String) { export class QuestionID extends Newtype<QuestionID>()("QuestionID", Schema.String) {
static make(id: string): QuestionID {
return this.makeUnsafe(id)
}
static ascending(id?: string): QuestionID { static ascending(id?: string): QuestionID {
return this.make(Identifier.ascending("question", id)) return this.makeUnsafe(Identifier.ascending("question", id))
} }
static readonly zod = Identifier.schema("question") as unknown as z.ZodType<QuestionID> static readonly zod = Identifier.schema("question") as unknown as z.ZodType<QuestionID>
@@ -1,40 +0,0 @@
import type { Hono } from "hono"
import { createBunWebSocket } from "hono/bun"
import type { Adapter } from "./adapter"
export const adapter: Adapter = {
create(app: Hono) {
const ws = createBunWebSocket()
return {
upgradeWebSocket: ws.upgradeWebSocket,
async listen(opts) {
const args = {
fetch: app.fetch,
hostname: opts.hostname,
idleTimeout: 0,
websocket: ws.websocket,
} as const
const start = (port: number) => {
try {
return Bun.serve({ ...args, port })
} catch {
return
}
}
const server = opts.port === 0 ? (start(4096) ?? start(0)) : start(opts.port)
if (!server) {
throw new Error(`Failed to start server on port ${opts.port}`)
}
if (!server.port) {
throw new Error(`Failed to resolve server address for port ${opts.port}`)
}
return {
port: server.port,
stop(close?: boolean) {
return Promise.resolve(server.stop(close))
},
}
},
}
},
}
@@ -1,66 +0,0 @@
import { createAdaptorServer, type ServerType } from "@hono/node-server"
import { createNodeWebSocket } from "@hono/node-ws"
import type { Hono } from "hono"
import type { Adapter } from "./adapter"
export const adapter: Adapter = {
create(app: Hono) {
const ws = createNodeWebSocket({ app })
return {
upgradeWebSocket: ws.upgradeWebSocket,
async listen(opts) {
const start = (port: number) =>
new Promise<ServerType>((resolve, reject) => {
const server = createAdaptorServer({ fetch: app.fetch })
ws.injectWebSocket(server)
const fail = (err: Error) => {
cleanup()
reject(err)
}
const ready = () => {
cleanup()
resolve(server)
}
const cleanup = () => {
server.off("error", fail)
server.off("listening", ready)
}
server.once("error", fail)
server.once("listening", ready)
server.listen(port, opts.hostname)
})
const server = opts.port === 0 ? await start(4096).catch(() => start(0)) : await start(opts.port)
const addr = server.address()
if (!addr || typeof addr === "string") {
throw new Error(`Failed to resolve server address for port ${opts.port}`)
}
let closing: Promise<void> | undefined
return {
port: addr.port,
stop(close?: boolean) {
closing ??= new Promise((resolve, reject) => {
server.close((err) => {
if (err) {
reject(err)
return
}
resolve()
})
if (close) {
if ("closeAllConnections" in server && typeof server.closeAllConnections === "function") {
server.closeAllConnections()
}
if ("closeIdleConnections" in server && typeof server.closeIdleConnections === "function") {
server.closeIdleConnections()
}
}
})
return closing
},
}
},
}
},
}
-21
View File
@@ -1,21 +0,0 @@
import type { Hono } from "hono"
import type { UpgradeWebSocket } from "hono/ws"
export type Opts = {
port: number
hostname: string
}
export type Listener = {
port: number
stop: (close?: boolean) => Promise<void>
}
export interface Runtime {
upgradeWebSocket: UpgradeWebSocket
listen(opts: Opts): Promise<Listener>
}
export interface Adapter {
create(app: Hono): Runtime
}
@@ -1,150 +0,0 @@
import { Auth } from "@/auth"
import { Log } from "@/util/log"
import { ProviderID } from "@/provider/schema"
import { Hono } from "hono"
import { describeRoute, resolver, validator, openAPIRouteHandler } from "hono-openapi"
import z from "zod"
import { errors } from "../error"
import { GlobalRoutes } from "../instance/global"
export function ControlPlaneRoutes(): Hono {
const app = new Hono()
return app
.route("/global", GlobalRoutes())
.put(
"/auth/:providerID",
describeRoute({
summary: "Set auth credentials",
description: "Set authentication credentials",
operationId: "auth.set",
responses: {
200: {
description: "Successfully set authentication credentials",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
...errors(400),
},
}),
validator(
"param",
z.object({
providerID: ProviderID.zod,
}),
),
validator("json", Auth.Info.zod),
async (c) => {
const providerID = c.req.valid("param").providerID
const info = c.req.valid("json")
await Auth.set(providerID, info)
return c.json(true)
},
)
.delete(
"/auth/:providerID",
describeRoute({
summary: "Remove auth credentials",
description: "Remove authentication credentials",
operationId: "auth.remove",
responses: {
200: {
description: "Successfully removed authentication credentials",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
...errors(400),
},
}),
validator(
"param",
z.object({
providerID: ProviderID.zod,
}),
),
async (c) => {
const providerID = c.req.valid("param").providerID
await Auth.remove(providerID)
return c.json(true)
},
)
.get(
"/doc",
openAPIRouteHandler(app, {
documentation: {
info: {
title: "opencode",
version: "0.0.3",
description: "opencode api",
},
openapi: "3.1.1",
},
}),
)
.use(
validator(
"query",
z.object({
directory: z.string().optional(),
workspace: z.string().optional(),
}),
),
)
.post(
"/log",
describeRoute({
summary: "Write log",
description: "Write a log entry to the server logs with specified level and metadata.",
operationId: "app.log",
responses: {
200: {
description: "Log entry written successfully",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
...errors(400),
},
}),
validator(
"json",
z.object({
service: z.string().meta({ description: "Service name for the log entry" }),
level: z.enum(["debug", "info", "error", "warn"]).meta({ description: "Log level" }),
message: z.string().meta({ description: "Log message" }),
extra: z
.record(z.string(), z.any())
.optional()
.meta({ description: "Additional metadata for the log entry" }),
}),
),
async (c) => {
const { service, level, message, extra } = c.req.valid("json")
const logger = Log.create({ service })
switch (level) {
case "debug":
logger.debug(message, extra)
break
case "info":
logger.info(message, extra)
break
case "error":
logger.error(message, extra)
break
case "warn":
logger.warn(message, extra)
break
}
return c.json(true)
},
)
}
@@ -1,33 +1,52 @@
import { describeRoute, resolver, validator } from "hono-openapi" import { describeRoute, resolver, validator } from "hono-openapi"
import { Hono } from "hono" import { Hono } from "hono"
import { proxy } from "hono/proxy"
import type { UpgradeWebSocket } from "hono/ws" import type { UpgradeWebSocket } from "hono/ws"
import z from "zod" import z from "zod"
import { Format } from "../../format" import { createHash } from "node:crypto"
import { TuiRoutes } from "./tui" import * as fs from "node:fs/promises"
import { Instance } from "../../project/instance" import { Log } from "../util/log"
import { Vcs } from "../../project/vcs" import { Format } from "../format"
import { Agent } from "../../agent/agent" import { TuiRoutes } from "./routes/tui"
import { Skill } from "../../skill" import { Instance } from "../project/instance"
import { Global } from "../../global" import { Vcs } from "../project/vcs"
import { LSP } from "../../lsp" import { Agent } from "../agent/agent"
import { Command } from "../../command" import { Skill } from "../skill"
import { QuestionRoutes } from "./question" import { Global } from "../global"
import { PermissionRoutes } from "./permission" import { LSP } from "../lsp"
import { ProjectRoutes } from "./project" import { Command } from "../command"
import { SessionRoutes } from "./session" import { Flag } from "../flag/flag"
import { PtyRoutes } from "./pty" import { QuestionRoutes } from "./routes/question"
import { McpRoutes } from "./mcp" import { PermissionRoutes } from "./routes/permission"
import { FileRoutes } from "./file" import { Snapshot } from "@/snapshot"
import { ConfigRoutes } from "./config" import { ProjectRoutes } from "./routes/project"
import { ExperimentalRoutes } from "./experimental" import { SessionRoutes } from "./routes/session"
import { ProviderRoutes } from "./provider" import { PtyRoutes } from "./routes/pty"
import { EventRoutes } from "./event" import { McpRoutes } from "./routes/mcp"
import { WorkspaceRouterMiddleware } from "./middleware" import { FileRoutes } from "./routes/file"
import { AppRuntime } from "@/effect/app-runtime" import { ConfigRoutes } from "./routes/config"
import { ExperimentalRoutes } from "./routes/experimental"
import { ProviderRoutes } from "./routes/provider"
import { EventRoutes } from "./routes/event"
import { errorHandler } from "./middleware"
import { getMimeType } from "hono/utils/mime"
export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => const log = Log.create({ service: "server" })
new Hono()
.use(WorkspaceRouterMiddleware(upgrade)) const embeddedUIPromise = Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI
? Promise.resolve(null)
: // @ts-expect-error - generated file at build time
import("opencode-web-ui.gen.ts").then((module) => module.default as Record<string, string>).catch(() => null)
const DEFAULT_CSP =
"default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src 'self' data:"
const csp = (hash = "") =>
`default-src 'self'; script-src 'self' 'wasm-unsafe-eval'${hash ? ` 'sha256-${hash}'` : ""}; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src 'self' data:`
export const InstanceRoutes = (upgrade: UpgradeWebSocket, app: Hono = new Hono()) =>
app
.onError(errorHandler(log))
.route("/project", ProjectRoutes()) .route("/project", ProjectRoutes())
.route("/pty", PtyRoutes(upgrade)) .route("/pty", PtyRoutes(upgrade))
.route("/config", ConfigRoutes()) .route("/config", ConfigRoutes())
@@ -171,7 +190,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono =>
}, },
}), }),
async (c) => { async (c) => {
const commands = await AppRuntime.runPromise(Command.Service.use((svc) => svc.list())) const commands = await Command.list()
return c.json(commands) return c.json(commands)
}, },
) )
@@ -258,6 +277,42 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono =>
}, },
}), }),
async (c) => { async (c) => {
return c.json(await AppRuntime.runPromise(Format.Service.use((svc) => svc.status()))) return c.json(await Format.status())
}, },
) )
.all("/*", async (c) => {
const embeddedWebUI = await embeddedUIPromise
const path = c.req.path
if (embeddedWebUI) {
const match = embeddedWebUI[path.replace(/^\//, "")] ?? embeddedWebUI["index.html"] ?? null
if (!match) return c.json({ error: "Not Found" }, 404)
if (await fs.exists(match)) {
const mime = getMimeType(match) ?? "text/plain"
c.header("Content-Type", mime)
if (mime.startsWith("text/html")) {
c.header("Content-Security-Policy", DEFAULT_CSP)
}
return c.body(new Uint8Array(await fs.readFile(match)))
} else {
return c.json({ error: "Not Found" }, 404)
}
} else {
const response = await proxy(`https://app.opencode.ai${path}`, {
...c.req,
headers: {
...c.req.raw.headers,
host: "app.opencode.ai",
},
})
const match = response.headers.get("content-type")?.includes("text/html")
? (await response.clone().text()).match(
/<script\b(?![^>]*\bsrc\s*=)[^>]*\bid=(['"])oc-theme-preload-script\1[^>]*>([\s\S]*?)<\/script>/i,
)
: undefined
const hash = match ? createHash("sha256").update(match[2]).digest("base64") : ""
response.headers.set("Content-Security-Policy", csp(hash))
return response
}
})
+4 -63
View File
@@ -3,17 +3,12 @@ import { NamedError } from "@opencode-ai/util/error"
import { NotFoundError } from "../storage/db" import { NotFoundError } from "../storage/db"
import { Session } from "../session" import { Session } from "../session"
import type { ContentfulStatusCode } from "hono/utils/http-status" import type { ContentfulStatusCode } from "hono/utils/http-status"
import type { ErrorHandler, MiddlewareHandler } from "hono" import type { ErrorHandler } from "hono"
import { HTTPException } from "hono/http-exception" import { HTTPException } from "hono/http-exception"
import { Log } from "../util/log" import type { Log } from "../util/log"
import { Flag } from "@/flag/flag"
import { basicAuth } from "hono/basic-auth"
import { cors } from "hono/cors"
import { compress } from "hono/compress"
const log = Log.create({ service: "server" }) export function errorHandler(log: Log.Logger): ErrorHandler {
return (err, c) => {
export const ErrorMiddleware: ErrorHandler = (err, c) => {
log.error("failed", { log.error("failed", {
error: err, error: err,
}) })
@@ -34,59 +29,5 @@ export const ErrorMiddleware: ErrorHandler = (err, c) => {
return c.json(new NamedError.Unknown({ message }).toObject(), { return c.json(new NamedError.Unknown({ message }).toObject(), {
status: 500, status: 500,
}) })
}
export const AuthMiddleware: MiddlewareHandler = (c, next) => {
// Allow CORS preflight requests to succeed without auth.
// Browser clients sending Authorization headers will preflight with OPTIONS.
if (c.req.method === "OPTIONS") return next()
const password = Flag.OPENCODE_SERVER_PASSWORD
if (!password) return next()
const username = Flag.OPENCODE_SERVER_USERNAME ?? "opencode"
if (c.req.query("auth_token")) c.req.raw.headers.set("authorization", `Basic ${c.req.query("auth_token")}`)
return basicAuth({ username, password })(c, next)
}
export const LoggerMiddleware: MiddlewareHandler = async (c, next) => {
const skip = c.req.path === "/log"
if (!skip) {
log.info("request", {
method: c.req.method,
path: c.req.path,
})
} }
const timer = log.time("request", {
method: c.req.method,
path: c.req.path,
})
await next()
if (!skip) timer.stop()
}
export function CorsMiddleware(opts?: { cors?: string[] }): MiddlewareHandler {
return cors({
maxAge: 86_400,
origin(input) {
if (!input) return
if (input.startsWith("http://localhost:")) return input
if (input.startsWith("http://127.0.0.1:")) return input
if (input === "tauri://localhost" || input === "http://tauri.localhost" || input === "https://tauri.localhost")
return input
if (/^https:\/\/([a-z0-9-]+\.)*opencode\.ai$/.test(input)) return input
if (opts?.cors?.includes(input)) return input
},
})
}
const zipped = compress()
export const CompressionMiddleware: MiddlewareHandler = (c, next) => {
const path = c.req.path
const method = c.req.method
if (path === "/event" || path === "/global/event" || path === "/global/sync-event") return next()
if (method === "POST" && /\/session\/[^/]+\/(message|prompt_async)$/.test(path)) return next()
return zipped(c, next)
} }
@@ -3,10 +3,12 @@ import type { UpgradeWebSocket } from "hono/ws"
import { getAdaptor } from "@/control-plane/adaptors" import { getAdaptor } from "@/control-plane/adaptors"
import { WorkspaceID } from "@/control-plane/schema" import { WorkspaceID } from "@/control-plane/schema"
import { Workspace } from "@/control-plane/workspace" import { Workspace } from "@/control-plane/workspace"
import { ServerProxy } from "../proxy" import { ServerProxy } from "./proxy"
import { lazy } from "@/util/lazy"
import { Filesystem } from "@/util/filesystem" import { Filesystem } from "@/util/filesystem"
import { Instance } from "@/project/instance" import { Instance } from "@/project/instance"
import { InstanceBootstrap } from "@/project/bootstrap" import { InstanceBootstrap } from "@/project/bootstrap"
import { InstanceRoutes } from "./instance"
import { Session } from "@/session" import { Session } from "@/session"
import { SessionID } from "@/session/schema" import { SessionID } from "@/session/schema"
import { WorkspaceContext } from "@/control-plane/workspace-context" import { WorkspaceContext } from "@/control-plane/workspace-context"
@@ -45,7 +47,9 @@ async function getSessionWorkspace(url: URL) {
} }
export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): MiddlewareHandler { export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): MiddlewareHandler {
return async (c, next) => { const routes = lazy(() => InstanceRoutes(upgrade))
return async (c) => {
const raw = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd() const raw = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd()
const directory = Filesystem.resolve( const directory = Filesystem.resolve(
(() => { (() => {
@@ -68,7 +72,7 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware
directory, directory,
init: InstanceBootstrap, init: InstanceBootstrap,
async fn() { async fn() {
return next() return routes().fetch(c.req.raw, c.env)
}, },
}) })
} }
@@ -83,7 +87,7 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware
// The lets the `DELETE /session/:id` endpoint through and we've // The lets the `DELETE /session/:id` endpoint through and we've
// made sure that it will run without an instance // made sure that it will run without an instance
if (url.pathname.match(/\/session\/[^/]+$/) && c.req.method === "DELETE") { if (url.pathname.match(/\/session\/[^/]+$/) && c.req.method === "DELETE") {
return next() return routes().fetch(c.req.raw, c.env)
} }
return new Response(`Workspace not found: ${workspaceID}`, { return new Response(`Workspace not found: ${workspaceID}`, {
@@ -105,7 +109,7 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware
directory: target.directory, directory: target.directory,
init: InstanceBootstrap, init: InstanceBootstrap,
async fn() { async fn() {
return next() return routes().fetch(c.req.raw, c.env)
}, },
}), }),
}) })
@@ -114,7 +118,7 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware
if (local(c.req.method, url.pathname)) { if (local(c.req.method, url.pathname)) {
// No instance provided because we are serving cached data; there // No instance provided because we are serving cached data; there
// is no instance to work with // is no instance to work with
return next() return routes().fetch(c.req.raw, c.env)
} }
if (c.req.header("upgrade")?.toLowerCase() === "websocket") { if (c.req.header("upgrade")?.toLowerCase() === "websocket") {
@@ -11,11 +11,9 @@ import { Session } from "../../session"
import { Config } from "../../config/config" import { Config } from "../../config/config"
import { ConsoleState } from "../../config/console-state" import { ConsoleState } from "../../config/console-state"
import { Account, AccountID, OrgID } from "../../account" import { Account, AccountID, OrgID } from "../../account"
import { AppRuntime } from "../../effect/app-runtime"
import { zodToJsonSchema } from "zod-to-json-schema" import { zodToJsonSchema } from "zod-to-json-schema"
import { errors } from "../error" import { errors } from "../error"
import { lazy } from "../../util/lazy" import { lazy } from "../../util/lazy"
import { Effect, Option } from "effect"
import { WorkspaceRoutes } from "./workspace" import { WorkspaceRoutes } from "./workspace"
import { Agent } from "@/agent/agent" import { Agent } from "@/agent/agent"
@@ -57,20 +55,11 @@ export const ExperimentalRoutes = lazy(() =>
}, },
}), }),
async (c) => { async (c) => {
const result = await AppRuntime.runPromise( const [consoleState, groups] = await Promise.all([Config.getConsoleState(), Account.orgsByAccount()])
Effect.gen(function* () { return c.json({
const config = yield* Config.Service ...consoleState,
const account = yield* Account.Service
const [state, groups] = yield* Effect.all([config.getConsoleState(), account.orgsByAccount()], {
concurrency: "unbounded",
})
return {
...state,
switchableOrgCount: groups.reduce((count, group) => count + group.orgs.length, 0), switchableOrgCount: groups.reduce((count, group) => count + group.orgs.length, 0),
} })
}),
)
return c.json(result)
}, },
) )
.get( .get(
@@ -91,25 +80,18 @@ export const ExperimentalRoutes = lazy(() =>
}, },
}), }),
async (c) => { async (c) => {
const orgs = await AppRuntime.runPromise( const [groups, active] = await Promise.all([Account.orgsByAccount(), Account.active()])
Effect.gen(function* () {
const account = yield* Account.Service const orgs = groups.flatMap((group) =>
const [groups, active] = yield* Effect.all([account.orgsByAccount(), account.active()], {
concurrency: "unbounded",
})
const info = Option.getOrUndefined(active)
return groups.flatMap((group) =>
group.orgs.map((org) => ({ group.orgs.map((org) => ({
accountID: group.account.id, accountID: group.account.id,
accountEmail: group.account.email, accountEmail: group.account.email,
accountUrl: group.account.url, accountUrl: group.account.url,
orgID: org.id, orgID: org.id,
orgName: org.name, orgName: org.name,
active: !!info && info.id === group.account.id && info.active_org_id === org.id, active: !!active && active.id === group.account.id && active.active_org_id === org.id,
})), })),
) )
}),
)
return c.json({ orgs }) return c.json({ orgs })
}, },
) )
@@ -133,12 +115,7 @@ export const ExperimentalRoutes = lazy(() =>
validator("json", ConsoleSwitchBody), validator("json", ConsoleSwitchBody),
async (c) => { async (c) => {
const body = c.req.valid("json") const body = c.req.valid("json")
await AppRuntime.runPromise( await Account.switchOrg(AccountID.make(body.accountID), OrgID.make(body.orgID))
Effect.gen(function* () {
const account = yield* Account.Service
yield* account.use(AccountID.make(body.accountID), Option.some(OrgID.make(body.orgID)))
}),
)
return c.json(true) return c.json(true)
}, },
) )
@@ -1,12 +1,10 @@
import { Hono, type Context } from "hono" import { Hono, type Context } from "hono"
import { describeRoute, resolver, validator } from "hono-openapi" import { describeRoute, resolver, validator } from "hono-openapi"
import { streamSSE } from "hono/streaming" import { streamSSE } from "hono/streaming"
import { Effect } from "effect"
import z from "zod" import z from "zod"
import { BusEvent } from "@/bus/bus-event" import { BusEvent } from "@/bus/bus-event"
import { SyncEvent } from "@/sync" import { SyncEvent } from "@/sync"
import { GlobalBus } from "@/bus/global" import { GlobalBus } from "@/bus/global"
import { AppRuntime } from "@/effect/app-runtime"
import { AsyncQueue } from "@/util/queue" import { AsyncQueue } from "@/util/queue"
import { Instance } from "../../project/instance" import { Instance } from "../../project/instance"
import { Installation } from "@/installation" import { Installation } from "@/installation"
@@ -292,33 +290,15 @@ export const GlobalRoutes = lazy(() =>
}), }),
), ),
async (c) => { async (c) => {
const result = await AppRuntime.runPromise( const method = await Installation.method()
Installation.Service.use((svc) =>
Effect.gen(function* () {
const method = yield* svc.method()
if (method === "unknown") { if (method === "unknown") {
return { success: false as const, status: 400 as const, error: "Unknown installation method" } return c.json({ success: false, error: "Unknown installation method" }, 400)
} }
const target = c.req.valid("json").target || (await Installation.latest(method))
const target = c.req.valid("json").target || (yield* svc.latest(method)) const result = await Installation.upgrade(method, target)
const result = yield* Effect.catch( .then(() => ({ success: true as const, version: target }))
svc.upgrade(method, target).pipe(Effect.as({ success: true as const, version: target })), .catch((e) => ({ success: false as const, error: e instanceof Error ? e.message : String(e) }))
(err) => if (result.success) {
Effect.succeed({
success: false as const,
status: 500 as const,
error: err instanceof Error ? err.message : String(err),
}),
)
if (!result.success) return result
return { ...result, status: 200 as const }
}),
),
)
if (!result.success) {
return c.json({ success: false, error: result.error }, result.status)
}
const target = result.version
GlobalBus.emit("event", { GlobalBus.emit("event", {
directory: "global", directory: "global",
payload: { payload: {
@@ -326,7 +306,9 @@ export const GlobalRoutes = lazy(() =>
properties: { version: target }, properties: { version: target },
}, },
}) })
return c.json({ success: true, version: target }) return c.json(result)
}
return c.json(result, 500)
}, },
), ),
) )
@@ -6,7 +6,6 @@ import { Provider } from "../../provider/provider"
import { ModelsDev } from "../../provider/models" import { ModelsDev } from "../../provider/models"
import { ProviderAuth } from "../../provider/auth" import { ProviderAuth } from "../../provider/auth"
import { ProviderID } from "../../provider/schema" import { ProviderID } from "../../provider/schema"
import { AppRuntime } from "../../effect/app-runtime"
import { mapValues } from "remeda" import { mapValues } from "remeda"
import { errors } from "../error" import { errors } from "../error"
import { lazy } from "../../util/lazy" import { lazy } from "../../util/lazy"
@@ -82,7 +81,7 @@ export const ProviderRoutes = lazy(() =>
}, },
}), }),
async (c) => { async (c) => {
return c.json(await AppRuntime.runPromise(ProviderAuth.Service.use((svc) => svc.methods()))) return c.json(await ProviderAuth.methods())
}, },
) )
.post( .post(
@@ -119,15 +118,11 @@ export const ProviderRoutes = lazy(() =>
async (c) => { async (c) => {
const providerID = c.req.valid("param").providerID const providerID = c.req.valid("param").providerID
const { method, inputs } = c.req.valid("json") const { method, inputs } = c.req.valid("json")
const result = await AppRuntime.runPromise( const result = await ProviderAuth.authorize({
ProviderAuth.Service.use((svc) =>
svc.authorize({
providerID, providerID,
method, method,
inputs, inputs,
}), })
),
)
return c.json(result) return c.json(result)
}, },
) )
@@ -165,15 +160,11 @@ export const ProviderRoutes = lazy(() =>
async (c) => { async (c) => {
const providerID = c.req.valid("param").providerID const providerID = c.req.valid("param").providerID
const { method, code } = c.req.valid("json") const { method, code } = c.req.valid("json")
await AppRuntime.runPromise( await ProviderAuth.callback({
ProviderAuth.Service.use((svc) =>
svc.callback({
providerID, providerID,
method, method,
code, code,
}), })
),
)
return c.json(true) return c.json(true)
}, },
), ),
@@ -3,7 +3,6 @@ import { describeRoute, validator } from "hono-openapi"
import { resolver } from "hono-openapi" import { resolver } from "hono-openapi"
import { QuestionID } from "@/question/schema" import { QuestionID } from "@/question/schema"
import { Question } from "../../question" import { Question } from "../../question"
import { AppRuntime } from "@/effect/app-runtime"
import z from "zod" import z from "zod"
import { errors } from "../error" import { errors } from "../error"
import { lazy } from "../../util/lazy" import { lazy } from "../../util/lazy"
@@ -28,7 +27,7 @@ export const QuestionRoutes = lazy(() =>
}, },
}), }),
async (c) => { async (c) => {
const questions = await AppRuntime.runPromise(Question.Service.use((svc) => svc.list())) const questions = await Question.list()
return c.json(questions) return c.json(questions)
}, },
) )
@@ -60,14 +59,10 @@ export const QuestionRoutes = lazy(() =>
async (c) => { async (c) => {
const params = c.req.valid("param") const params = c.req.valid("param")
const json = c.req.valid("json") const json = c.req.valid("json")
await AppRuntime.runPromise( await Question.reply({
Question.Service.use((svc) =>
svc.reply({
requestID: params.requestID, requestID: params.requestID,
answers: json.answers, answers: json.answers,
}), })
),
)
return c.json(true) return c.json(true)
}, },
) )
@@ -97,7 +92,7 @@ export const QuestionRoutes = lazy(() =>
), ),
async (c) => { async (c) => {
const params = c.req.valid("param") const params = c.req.valid("param")
await AppRuntime.runPromise(Question.Service.use((svc) => svc.reject(params.requestID))) await Question.reject(params.requestID)
return c.json(true) return c.json(true)
}, },
), ),
@@ -13,8 +13,6 @@ import { SessionShare } from "@/share/session"
import { SessionStatus } from "@/session/status" import { SessionStatus } from "@/session/status"
import { SessionSummary } from "@/session/summary" import { SessionSummary } from "@/session/summary"
import { Todo } from "../../session/todo" import { Todo } from "../../session/todo"
import { Effect } from "effect"
import { AppRuntime } from "../../effect/app-runtime"
import { Agent } from "../../agent/agent" import { Agent } from "../../agent/agent"
import { Snapshot } from "@/snapshot" import { Snapshot } from "@/snapshot"
import { Command } from "../../command" import { Command } from "../../command"
@@ -95,7 +93,7 @@ export const SessionRoutes = lazy(() =>
}, },
}), }),
async (c) => { async (c) => {
const result = await AppRuntime.runPromise(SessionStatus.Service.use((svc) => svc.list())) const result = await SessionStatus.list()
return c.json(Object.fromEntries(result)) return c.json(Object.fromEntries(result))
}, },
) )
@@ -187,7 +185,7 @@ export const SessionRoutes = lazy(() =>
), ),
async (c) => { async (c) => {
const sessionID = c.req.valid("param").sessionID const sessionID = c.req.valid("param").sessionID
const todos = await AppRuntime.runPromise(Todo.Service.use((svc) => svc.get(sessionID))) const todos = await Todo.get(sessionID)
return c.json(todos) return c.json(todos)
}, },
) )
@@ -274,7 +272,6 @@ export const SessionRoutes = lazy(() =>
"json", "json",
z.object({ z.object({
title: z.string().optional(), title: z.string().optional(),
permission: Permission.Ruleset.optional(),
time: z time: z
.object({ .object({
archived: z.number().optional(), archived: z.number().optional(),
@@ -285,17 +282,10 @@ export const SessionRoutes = lazy(() =>
async (c) => { async (c) => {
const sessionID = c.req.valid("param").sessionID const sessionID = c.req.valid("param").sessionID
const updates = c.req.valid("json") const updates = c.req.valid("json")
const current = await Session.get(sessionID)
if (updates.title !== undefined) { if (updates.title !== undefined) {
await Session.setTitle({ sessionID, title: updates.title }) await Session.setTitle({ sessionID, title: updates.title })
} }
if (updates.permission !== undefined) {
await Session.setPermission({
sessionID,
permission: Permission.merge(current.permission ?? [], updates.permission),
})
}
if (updates.time?.archived !== undefined) { if (updates.time?.archived !== undefined) {
await Session.setArchived({ sessionID, time: updates.time.archived }) await Session.setArchived({ sessionID, time: updates.time.archived })
} }
@@ -725,17 +715,11 @@ export const SessionRoutes = lazy(() =>
), ),
async (c) => { async (c) => {
const params = c.req.valid("param") const params = c.req.valid("param")
await AppRuntime.runPromise( await SessionRunState.assertNotBusy(params.sessionID)
Effect.gen(function* () { await Session.removeMessage({
const state = yield* SessionRunState.Service
const session = yield* Session.Service
yield* state.assertNotBusy(params.sessionID)
yield* session.removeMessage({
sessionID: params.sessionID, sessionID: params.sessionID,
messageID: params.messageID, messageID: params.messageID,
}) })
}),
)
return c.json(true) return c.json(true)
}, },
) )
+275 -28
View File
@@ -1,14 +1,24 @@
import { generateSpecs } from "hono-openapi" import { Log } from "../util/log"
import { describeRoute, generateSpecs, validator, resolver, openAPIRouteHandler } from "hono-openapi"
import { Hono } from "hono" import { Hono } from "hono"
import { adapter } from "#hono" import { compress } from "hono/compress"
import { createNodeWebSocket } from "@hono/node-ws"
import { cors } from "hono/cors"
import { basicAuth } from "hono/basic-auth"
import type { UpgradeWebSocket } from "hono/ws"
import z from "zod"
import { Auth } from "../auth"
import { Flag } from "../flag/flag"
import { ProviderID } from "../provider/schema"
import { WorkspaceRouterMiddleware } from "./router"
import { errors } from "./error"
import { GlobalRoutes } from "./routes/global"
import { MDNS } from "./mdns" import { MDNS } from "./mdns"
import { lazy } from "@/util/lazy" import { lazy } from "@/util/lazy"
import { AuthMiddleware, CompressionMiddleware, CorsMiddleware, ErrorMiddleware, LoggerMiddleware } from "./middleware" import { errorHandler } from "./middleware"
import { InstanceRoutes } from "./instance" import { InstanceRoutes } from "./instance"
import { initProjectors } from "./projectors" import { initProjectors } from "./projectors"
import { Log } from "@/util/log" import { createAdaptorServer, type ServerType } from "@hono/node-server"
import { ControlPlaneRoutes } from "./control"
import { UIRoutes } from "./ui"
// @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85 // @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85
globalThis.AI_SDK_LOG_WARNINGS = false globalThis.AI_SDK_LOG_WARNINGS = false
@@ -16,8 +26,6 @@ globalThis.AI_SDK_LOG_WARNINGS = false
initProjectors() initProjectors()
export namespace Server { export namespace Server {
const log = Log.create({ service: "server" })
export type Listener = { export type Listener = {
hostname: string hostname: string
port: number port: number
@@ -25,31 +33,231 @@ export namespace Server {
stop: (close?: boolean) => Promise<void> stop: (close?: boolean) => Promise<void>
} }
const log = Log.create({ service: "server" })
const zipped = compress()
const skipCompress = (path: string, method: string) => {
if (path === "/event" || path === "/global/event" || path === "/global/sync-event") return true
if (method === "POST" && /\/session\/[^/]+\/(message|prompt_async)$/.test(path)) return true
return false
}
export const Default = lazy(() => create({})) export const Default = lazy(() => create({}))
export function ControlPlaneRoutes(upgrade: UpgradeWebSocket, app = new Hono(), opts?: { cors?: string[] }): Hono {
return app
.onError(errorHandler(log))
.use((c, next) => {
// Allow CORS preflight requests to succeed without auth.
// Browser clients sending Authorization headers will preflight with OPTIONS.
if (c.req.method === "OPTIONS") return next()
const password = Flag.OPENCODE_SERVER_PASSWORD
if (!password) return next()
const username = Flag.OPENCODE_SERVER_USERNAME ?? "opencode"
if (c.req.query("auth_token")) c.req.raw.headers.set("authorization", `Basic ${c.req.query("auth_token")}`)
return basicAuth({ username, password })(c, next)
})
.use(async (c, next) => {
const skip = c.req.path === "/log"
if (!skip) {
log.info("request", {
method: c.req.method,
path: c.req.path,
})
}
const timer = log.time("request", {
method: c.req.method,
path: c.req.path,
})
await next()
if (!skip) timer.stop()
})
.use(
cors({
maxAge: 86_400,
origin(input) {
if (!input) return
if (input.startsWith("http://localhost:")) return input
if (input.startsWith("http://127.0.0.1:")) return input
if (
input === "tauri://localhost" ||
input === "http://tauri.localhost" ||
input === "https://tauri.localhost"
)
return input
if (/^https:\/\/([a-z0-9-]+\.)*opencode\.ai$/.test(input)) return input
if (opts?.cors?.includes(input)) return input
},
}),
)
.use((c, next) => {
if (skipCompress(c.req.path, c.req.method)) return next()
return zipped(c, next)
})
.route("/global", GlobalRoutes())
.put(
"/auth/:providerID",
describeRoute({
summary: "Set auth credentials",
description: "Set authentication credentials",
operationId: "auth.set",
responses: {
200: {
description: "Successfully set authentication credentials",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
...errors(400),
},
}),
validator(
"param",
z.object({
providerID: ProviderID.zod,
}),
),
validator("json", Auth.Info.zod),
async (c) => {
const providerID = c.req.valid("param").providerID
const info = c.req.valid("json")
await Auth.set(providerID, info)
return c.json(true)
},
)
.delete(
"/auth/:providerID",
describeRoute({
summary: "Remove auth credentials",
description: "Remove authentication credentials",
operationId: "auth.remove",
responses: {
200: {
description: "Successfully removed authentication credentials",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
...errors(400),
},
}),
validator(
"param",
z.object({
providerID: ProviderID.zod,
}),
),
async (c) => {
const providerID = c.req.valid("param").providerID
await Auth.remove(providerID)
return c.json(true)
},
)
.get(
"/doc",
openAPIRouteHandler(app, {
documentation: {
info: {
title: "opencode",
version: "0.0.3",
description: "opencode api",
},
openapi: "3.1.1",
},
}),
)
.use(
validator(
"query",
z.object({
directory: z.string().optional(),
workspace: z.string().optional(),
}),
),
)
.post(
"/log",
describeRoute({
summary: "Write log",
description: "Write a log entry to the server logs with specified level and metadata.",
operationId: "app.log",
responses: {
200: {
description: "Log entry written successfully",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
...errors(400),
},
}),
validator(
"json",
z.object({
service: z.string().meta({ description: "Service name for the log entry" }),
level: z.enum(["debug", "info", "error", "warn"]).meta({ description: "Log level" }),
message: z.string().meta({ description: "Log message" }),
extra: z
.record(z.string(), z.any())
.optional()
.meta({ description: "Additional metadata for the log entry" }),
}),
),
async (c) => {
const { service, level, message, extra } = c.req.valid("json")
const logger = Log.create({ service })
switch (level) {
case "debug":
logger.debug(message, extra)
break
case "info":
logger.info(message, extra)
break
case "error":
logger.error(message, extra)
break
case "warn":
logger.warn(message, extra)
break
}
return c.json(true)
},
)
.use(WorkspaceRouterMiddleware(upgrade))
}
function create(opts: { cors?: string[] }) { function create(opts: { cors?: string[] }) {
const app = new Hono() const app = new Hono()
const runtime = adapter.create(app) const ws = createNodeWebSocket({ app })
return { return {
app: app app: ControlPlaneRoutes(ws.upgradeWebSocket, app, opts),
.onError(ErrorMiddleware) ws,
.use(AuthMiddleware)
.use(LoggerMiddleware)
.use(CompressionMiddleware)
.use(CorsMiddleware(opts))
.route("/", ControlPlaneRoutes())
.route("/", InstanceRoutes(runtime.upgradeWebSocket))
.route("/", UIRoutes()),
runtime,
} }
} }
export function createApp(opts: { cors?: string[] }) {
return create(opts).app
}
export async function openapi() { export async function openapi() {
// Build a fresh app with all routes registered directly so // Build a fresh app with all routes registered directly so
// hono-openapi can see describeRoute metadata (`.route()` wraps // hono-openapi can see describeRoute metadata (`.route()` wraps
// handlers when the sub-app has a custom errorHandler, which // handlers when the sub-app has a custom errorHandler, which
// strips the metadata symbol). // strips the metadata symbol).
const { app } = create({}) const { app, ws } = create({})
InstanceRoutes(ws.upgradeWebSocket, app)
const result = await generateSpecs(app, { const result = await generateSpecs(app, {
documentation: { documentation: {
info: { info: {
@@ -73,21 +281,46 @@ export namespace Server {
cors?: string[] cors?: string[]
}): Promise<Listener> { }): Promise<Listener> {
const built = create(opts) const built = create(opts)
const server = await built.runtime.listen(opts) const start = (port: number) =>
new Promise<ServerType>((resolve, reject) => {
const server = createAdaptorServer({ fetch: built.app.fetch })
built.ws.injectWebSocket(server)
const fail = (err: Error) => {
cleanup()
reject(err)
}
const ready = () => {
cleanup()
resolve(server)
}
const cleanup = () => {
server.off("error", fail)
server.off("listening", ready)
}
server.once("error", fail)
server.once("listening", ready)
server.listen(port, opts.hostname)
})
const server = opts.port === 0 ? await start(4096).catch(() => start(0)) : await start(opts.port)
const addr = server.address()
if (!addr || typeof addr === "string") {
throw new Error(`Failed to resolve server address for port ${opts.port}`)
}
const next = new URL("http://localhost") const next = new URL("http://localhost")
next.hostname = opts.hostname next.hostname = opts.hostname
next.port = String(server.port) next.port = String(addr.port)
url = next url = next
const mdns = const mdns =
opts.mdns && opts.mdns &&
server.port && addr.port &&
opts.hostname !== "127.0.0.1" && opts.hostname !== "127.0.0.1" &&
opts.hostname !== "localhost" && opts.hostname !== "localhost" &&
opts.hostname !== "::1" opts.hostname !== "::1"
if (mdns) { if (mdns) {
MDNS.publish(server.port, opts.mdnsDomain) MDNS.publish(addr.port, opts.mdnsDomain)
} else if (opts.mdns) { } else if (opts.mdns) {
log.warn("mDNS enabled but hostname is loopback; skipping mDNS publish") log.warn("mDNS enabled but hostname is loopback; skipping mDNS publish")
} }
@@ -95,13 +328,27 @@ export namespace Server {
let closing: Promise<void> | undefined let closing: Promise<void> | undefined
return { return {
hostname: opts.hostname, hostname: opts.hostname,
port: server.port, port: addr.port,
url: next, url: next,
stop(close?: boolean) { stop(close?: boolean) {
closing ??= (async () => { closing ??= new Promise((resolve, reject) => {
if (mdns) MDNS.unpublish() if (mdns) MDNS.unpublish()
await server.stop(close) server.close((err) => {
})() if (err) {
reject(err)
return
}
resolve()
})
if (close) {
if ("closeAllConnections" in server && typeof server.closeAllConnections === "function") {
server.closeAllConnections()
}
if ("closeIdleConnections" in server && typeof server.closeIdleConnections === "function") {
server.closeIdleConnections()
}
}
})
return closing return closing
}, },
} }
-55
View File
@@ -1,55 +0,0 @@
import { Flag } from "@/flag/flag"
import { Hono } from "hono"
import { proxy } from "hono/proxy"
import { getMimeType } from "hono/utils/mime"
import { createHash } from "node:crypto"
import fs from "node:fs/promises"
const embeddedUIPromise = Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI
? Promise.resolve(null)
: // @ts-expect-error - generated file at build time
import("opencode-web-ui.gen.ts").then((module) => module.default as Record<string, string>).catch(() => null)
const DEFAULT_CSP =
"default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src 'self' data:"
const csp = (hash = "") =>
`default-src 'self'; script-src 'self' 'wasm-unsafe-eval'${hash ? ` 'sha256-${hash}'` : ""}; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src 'self' data:`
export const UIRoutes = (): Hono =>
new Hono().all("/*", async (c) => {
const embeddedWebUI = await embeddedUIPromise
const path = c.req.path
if (embeddedWebUI) {
const match = embeddedWebUI[path.replace(/^\//, "")] ?? embeddedWebUI["index.html"] ?? null
if (!match) return c.json({ error: "Not Found" }, 404)
if (await fs.exists(match)) {
const mime = getMimeType(match) ?? "text/plain"
c.header("Content-Type", mime)
if (mime.startsWith("text/html")) {
c.header("Content-Security-Policy", DEFAULT_CSP)
}
return c.body(new Uint8Array(await fs.readFile(match)))
} else {
return c.json({ error: "Not Found" }, 404)
}
} else {
const response = await proxy(`https://app.opencode.ai${path}`, {
...c.req,
headers: {
...c.req.raw.headers,
host: "app.opencode.ai",
},
})
const match = response.headers.get("content-type")?.includes("text/html")
? (await response.clone().text()).match(
/<script\b(?![^>]*\bsrc\s*=)[^>]*\bid=(['"])oc-theme-preload-script\1[^>]*>([\s\S]*?)<\/script>/i,
)
: undefined
const hash = match ? createHash("sha256").update(match[2]).digest("base64") : ""
response.headers.set("Content-Security-Policy", csp(hash))
return response
}
})
+2 -2
View File
@@ -15,7 +15,7 @@ import { Plugin } from "@/plugin"
import { Config } from "@/config/config" import { Config } from "@/config/config"
import { NotFoundError } from "@/storage/db" import { NotFoundError } from "@/storage/db"
import { ModelID, ProviderID } from "@/provider/schema" import { ModelID, ProviderID } from "@/provider/schema"
import { Effect, Layer, Context } from "effect" import { Effect, Layer, ServiceMap } from "effect"
import { makeRuntime } from "@/effect/run-service" import { makeRuntime } from "@/effect/run-service"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { isOverflow as overflow } from "./overflow" import { isOverflow as overflow } from "./overflow"
@@ -58,7 +58,7 @@ export namespace SessionCompaction {
}) => Effect.Effect<void> }) => Effect.Effect<void>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/SessionCompaction") {} export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/SessionCompaction") {}
export const layer: Layer.Layer< export const layer: Layer.Layer<
Service, Service,
+18 -29
View File
@@ -29,7 +29,7 @@ import type { Provider } from "@/provider/provider"
import { Permission } from "@/permission" import { Permission } from "@/permission"
import { Global } from "@/global" import { Global } from "@/global"
import type { LanguageModelV2Usage } from "@ai-sdk/provider" import type { LanguageModelV2Usage } from "@ai-sdk/provider"
import { Effect, Layer, Option, Context } from "effect" import { Effect, Layer, ServiceMap } from "effect"
import { makeRuntime } from "@/effect/run-service" import { makeRuntime } from "@/effect/run-service"
export namespace Session { export namespace Session {
@@ -352,25 +352,19 @@ export namespace Session {
field: string field: string
delta: string delta: string
}) => Effect.Effect<void> }) => Effect.Effect<void>
/** Finds the first message matching the predicate, searching newest-first. */
readonly findMessage: (
sessionID: SessionID,
predicate: (msg: MessageV2.WithParts) => boolean,
) => Effect.Effect<Option.Option<MessageV2.WithParts>>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/Session") {} export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Session") {}
type Patch = z.infer<typeof Event.Updated.schema>["info"] type Patch = z.infer<typeof Event.Updated.schema>["info"]
const db = <T>(fn: (d: Parameters<typeof Database.use>[0] extends (trx: infer D) => any ? D : never) => T) => const db = <T>(fn: (d: Parameters<typeof Database.use>[0] extends (trx: infer D) => any ? D : never) => T) =>
Effect.sync(() => Database.use(fn)) Effect.sync(() => Database.use(fn))
export const layer: Layer.Layer<Service, never, Bus.Service | Storage.Service> = Layer.effect( export const layer: Layer.Layer<Service, never, Bus.Service> = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const bus = yield* Bus.Service const bus = yield* Bus.Service
const storage = yield* Storage.Service
const createNext = Effect.fn("Session.createNext")(function* (input: { const createNext = Effect.fn("Session.createNext")(function* (input: {
id?: SessionID id?: SessionID
@@ -591,9 +585,9 @@ export namespace Session {
}) })
const diff = Effect.fn("Session.diff")(function* (sessionID: SessionID) { const diff = Effect.fn("Session.diff")(function* (sessionID: SessionID) {
return yield* storage return yield* Effect.tryPromise(() => Storage.read<Snapshot.FileDiff[]>(["session_diff", sessionID])).pipe(
.read<Snapshot.FileDiff[]>(["session_diff", sessionID]) Effect.orElseSucceed((): Snapshot.FileDiff[] => []),
.pipe(Effect.orElseSucceed((): Snapshot.FileDiff[] => [])) )
}) })
const messages = Effect.fn("Session.messages")(function* (input: { sessionID: SessionID; limit?: number }) { const messages = Effect.fn("Session.messages")(function* (input: { sessionID: SessionID; limit?: number }) {
@@ -641,17 +635,6 @@ export namespace Session {
yield* bus.publish(MessageV2.Event.PartDelta, input) yield* bus.publish(MessageV2.Event.PartDelta, input)
}) })
/** Finds the first message matching the predicate, searching newest-first. */
const findMessage = Effect.fn("Session.findMessage")(function* (
sessionID: SessionID,
predicate: (msg: MessageV2.WithParts) => boolean,
) {
for (const item of MessageV2.stream(sessionID)) {
if (predicate(item)) return Option.some(item)
}
return Option.none<MessageV2.WithParts>()
})
return Service.of({ return Service.of({
create, create,
fork, fork,
@@ -673,12 +656,11 @@ export namespace Session {
updatePart, updatePart,
getPart, getPart,
updatePartDelta, updatePartDelta,
findMessage,
}) })
}), }),
) )
export const defaultLayer = layer.pipe(Layer.provide(Bus.layer), Layer.provide(Storage.defaultLayer)) export const defaultLayer = layer.pipe(Layer.provide(Bus.layer))
const { runPromise } = makeRuntime(Service, defaultLayer) const { runPromise } = makeRuntime(Service, defaultLayer)
@@ -708,10 +690,6 @@ export namespace Session {
runPromise((svc) => svc.setArchived(input)), runPromise((svc) => svc.setArchived(input)),
) )
export const setPermission = fn(z.object({ sessionID: SessionID.zod, permission: Permission.Ruleset }), (input) =>
runPromise((svc) => svc.setPermission(input)),
)
export const setRevert = fn( export const setRevert = fn(
z.object({ sessionID: SessionID.zod, revert: Info.shape.revert, summary: Info.shape.summary }), z.object({ sessionID: SessionID.zod, revert: Info.shape.revert, summary: Info.shape.summary }),
(input) => (input) =>
@@ -854,4 +832,15 @@ export namespace Session {
MessageV2.Part.parse(part) MessageV2.Part.parse(part)
return runPromise((svc) => svc.updatePart(part)) return runPromise((svc) => svc.updatePart(part))
} }
export const updatePartDelta = fn(
z.object({
sessionID: SessionID.zod,
messageID: MessageID.zod,
partID: PartID.zod,
field: z.string(),
delta: z.string(),
}),
(input) => runPromise((svc) => svc.updatePartDelta(input)),
)
} }
+17 -2
View File
@@ -1,9 +1,10 @@
import os from "os" import os from "os"
import path from "path" import path from "path"
import { Effect, Layer, Context } from "effect" import { Effect, Layer, ServiceMap } from "effect"
import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http" import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http"
import { Config } from "@/config/config" import { Config } from "@/config/config"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { makeRuntime } from "@/effect/run-service"
import { Flag } from "@/flag/flag" import { Flag } from "@/flag/flag"
import { AppFileSystem } from "@/filesystem" import { AppFileSystem } from "@/filesystem"
import { withTransientReadRetry } from "@/util/effect-http-client" import { withTransientReadRetry } from "@/util/effect-http-client"
@@ -63,7 +64,7 @@ export namespace Instruction {
) => Effect.Effect<{ filepath: string; content: string }[], AppFileSystem.Error> ) => Effect.Effect<{ filepath: string; content: string }[], AppFileSystem.Error>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/Instruction") {} export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Instruction") {}
export const layer: Layer.Layer<Service, never, AppFileSystem.Service | Config.Service | HttpClient.HttpClient> = export const layer: Layer.Layer<Service, never, AppFileSystem.Service | Config.Service | HttpClient.HttpClient> =
Layer.effect( Layer.effect(
@@ -237,7 +238,21 @@ export namespace Instruction {
Layer.provide(FetchHttpClient.layer), Layer.provide(FetchHttpClient.layer),
) )
const { runPromise } = makeRuntime(Service, defaultLayer)
export function clear(messageID: MessageID) {
return runPromise((svc) => svc.clear(messageID))
}
export async function systemPaths() {
return runPromise((svc) => svc.systemPaths())
}
export function loaded(messages: MessageV2.WithParts[]) { export function loaded(messages: MessageV2.WithParts[]) {
return extract(messages) return extract(messages)
} }
export async function resolve(messages: MessageV2.WithParts[], filepath: string, messageID: MessageID) {
return runPromise((svc) => svc.resolve(messages, filepath, messageID))
}
} }
+2 -2
View File
@@ -1,6 +1,6 @@
import { Provider } from "@/provider/provider" import { Provider } from "@/provider/provider"
import { Log } from "@/util/log" import { Log } from "@/util/log"
import { Cause, Effect, Layer, Record, Context } from "effect" import { Cause, Effect, Layer, Record, ServiceMap } from "effect"
import * as Queue from "effect/Queue" import * as Queue from "effect/Queue"
import * as Stream from "effect/Stream" import * as Stream from "effect/Stream"
import { streamText, wrapLanguageModel, type ModelMessage, type Tool, tool, jsonSchema } from "ai" import { streamText, wrapLanguageModel, type ModelMessage, type Tool, tool, jsonSchema } from "ai"
@@ -51,7 +51,7 @@ export namespace LLM {
readonly stream: (input: StreamInput) => Stream.Stream<Event, unknown> readonly stream: (input: StreamInput) => Stream.Stream<Event, unknown>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/LLM") {} export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/LLM") {}
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
+2 -5
View File
@@ -15,7 +15,6 @@ import type { SystemError } from "bun"
import type { Provider } from "@/provider/provider" import type { Provider } from "@/provider/provider"
import { ModelID, ProviderID } from "@/provider/schema" import { ModelID, ProviderID } from "@/provider/schema"
import { Effect } from "effect" import { Effect } from "effect"
import { EffectLogger } from "@/effect/logger"
/** Error shape thrown by Bun's fetch() when gzip/br decompression fails mid-stream */ /** Error shape thrown by Bun's fetch() when gzip/br decompression fails mid-stream */
interface FetchDecompressionError extends Error { interface FetchDecompressionError extends Error {
@@ -25,8 +24,6 @@ interface FetchDecompressionError extends Error {
} }
export namespace MessageV2 { export namespace MessageV2 {
export const SYNTHETIC_ATTACHMENT_PROMPT = "Attached image(s) from tool result:"
export function isMedia(mime: string) { export function isMedia(mime: string) {
return mime.startsWith("image/") || mime === "application/pdf" return mime.startsWith("image/") || mime === "application/pdf"
} }
@@ -810,7 +807,7 @@ export namespace MessageV2 {
parts: [ parts: [
{ {
type: "text" as const, type: "text" as const,
text: SYNTHETIC_ATTACHMENT_PROMPT, text: "Attached image(s) from tool result:",
}, },
...media.map((attachment) => ({ ...media.map((attachment) => ({
type: "file" as const, type: "file" as const,
@@ -842,7 +839,7 @@ export namespace MessageV2 {
model: Provider.Model, model: Provider.Model,
options?: { stripMedia?: boolean }, options?: { stripMedia?: boolean },
): Promise<ModelMessage[]> { ): Promise<ModelMessage[]> {
return Effect.runPromise(toModelMessagesEffect(input, model, options).pipe(Effect.provide(EffectLogger.layer))) return Effect.runPromise(toModelMessagesEffect(input, model, options))
} }
export function page(input: { sessionID: SessionID; limit: number; before?: string }) { export function page(input: { sessionID: SessionID; limit: number; before?: string }) {
+9 -10
View File
@@ -1,4 +1,4 @@
import { Cause, Deferred, Effect, Layer, Context } from "effect" import { Cause, Deferred, Effect, Layer, ServiceMap } from "effect"
import * as Stream from "effect/Stream" import * as Stream from "effect/Stream"
import { Agent } from "@/agent/agent" import { Agent } from "@/agent/agent"
import { Bus } from "@/bus" import { Bus } from "@/bus"
@@ -6,7 +6,7 @@ import { Config } from "@/config/config"
import { Permission } from "@/permission" import { Permission } from "@/permission"
import { Plugin } from "@/plugin" import { Plugin } from "@/plugin"
import { Snapshot } from "@/snapshot" import { Snapshot } from "@/snapshot"
import { EffectLogger } from "@/effect/logger" import { Log } from "@/util/log"
import { Session } from "." import { Session } from "."
import { LLM } from "./llm" import { LLM } from "./llm"
import { MessageV2 } from "./message-v2" import { MessageV2 } from "./message-v2"
@@ -23,7 +23,7 @@ import { isRecord } from "@/util/record"
export namespace SessionProcessor { export namespace SessionProcessor {
const DOOM_LOOP_THRESHOLD = 3 const DOOM_LOOP_THRESHOLD = 3
const log = EffectLogger.create({ service: "session.processor" }) const log = Log.create({ service: "session.processor" })
export type Result = "compact" | "stop" | "continue" export type Result = "compact" | "stop" | "continue"
@@ -76,7 +76,7 @@ export namespace SessionProcessor {
type StreamEvent = Event type StreamEvent = Event
export class Service extends Context.Service<Service, Interface>()("@opencode/SessionProcessor") {} export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/SessionProcessor") {}
export const layer: Layer.Layer< export const layer: Layer.Layer<
Service, Service,
@@ -121,7 +121,6 @@ export namespace SessionProcessor {
reasoningMap: {}, reasoningMap: {},
} }
let aborted = false let aborted = false
const slog = log.with({ sessionID: input.sessionID, messageID: input.assistantMessage.id })
const parse = (e: unknown) => const parse = (e: unknown) =>
MessageV2.fromError(e, { MessageV2.fromError(e, {
@@ -246,7 +245,7 @@ export namespace SessionProcessor {
case "reasoning-end": case "reasoning-end":
if (!(value.id in ctx.reasoningMap)) return if (!(value.id in ctx.reasoningMap)) return
ctx.reasoningMap[value.id].text = ctx.reasoningMap[value.id].text ctx.reasoningMap[value.id].text = ctx.reasoningMap[value.id].text.trimEnd()
ctx.reasoningMap[value.id].time = { ...ctx.reasoningMap[value.id].time, end: Date.now() } ctx.reasoningMap[value.id].time = { ...ctx.reasoningMap[value.id].time, end: Date.now() }
if (value.providerMetadata) ctx.reasoningMap[value.id].metadata = value.providerMetadata if (value.providerMetadata) ctx.reasoningMap[value.id].metadata = value.providerMetadata
yield* session.updatePart(ctx.reasoningMap[value.id]) yield* session.updatePart(ctx.reasoningMap[value.id])
@@ -426,7 +425,7 @@ export namespace SessionProcessor {
case "text-end": case "text-end":
if (!ctx.currentText) return if (!ctx.currentText) return
ctx.currentText.text = ctx.currentText.text ctx.currentText.text = ctx.currentText.text.trimEnd()
ctx.currentText.text = (yield* plugin.trigger( ctx.currentText.text = (yield* plugin.trigger(
"experimental.text.complete", "experimental.text.complete",
{ {
@@ -449,7 +448,7 @@ export namespace SessionProcessor {
return return
default: default:
yield* slog.info("unhandled", { event: value.type, value }) log.info("unhandled", { ...value })
return return
} }
}) })
@@ -515,7 +514,7 @@ export namespace SessionProcessor {
}) })
const halt = Effect.fn("SessionProcessor.halt")(function* (e: unknown) { const halt = Effect.fn("SessionProcessor.halt")(function* (e: unknown) {
yield* slog.error("process", { error: errorMessage(e), stack: e instanceof Error ? e.stack : undefined }) log.error("process", { error: e, stack: e instanceof Error ? e.stack : undefined })
const error = parse(e) const error = parse(e)
if (MessageV2.ContextOverflowError.isInstance(error)) { if (MessageV2.ContextOverflowError.isInstance(error)) {
ctx.needsCompaction = true ctx.needsCompaction = true
@@ -531,7 +530,7 @@ export namespace SessionProcessor {
}) })
const process = Effect.fn("SessionProcessor.process")(function* (streamInput: LLM.StreamInput) { const process = Effect.fn("SessionProcessor.process")(function* (streamInput: LLM.StreamInput) {
yield* slog.info("process") log.info("process")
ctx.needsCompaction = false ctx.needsCompaction = false
ctx.shouldBreak = (yield* config.get()).experimental?.continue_loop_on_deny !== true ctx.shouldBreak = (yield* config.get()).experimental?.continue_loop_on_deny !== true

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