mirror of
https://github.com/anomalyco/opencode.git
synced 2026-04-12 17:05:05 +00:00
Compare commits
57 Commits
kit/motel-
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b9b9ad31e | ||
|
|
3729fd5706 | ||
|
|
74b14a2d4e | ||
|
|
cdb951ec2f | ||
|
|
fc01cad2b8 | ||
|
|
c1ddc0ea2d | ||
|
|
319b7655b7 | ||
|
|
824c12c01a | ||
|
|
17b2900884 | ||
|
|
003010bdb6 | ||
|
|
82a4292934 | ||
|
|
eea4253d67 | ||
|
|
1eacc3c339 | ||
|
|
1a509d62a0 | ||
|
|
4c4eef46f1 | ||
|
|
d62ec7776e | ||
|
|
cb1e5d9e41 | ||
|
|
ca5f086759 | ||
|
|
57c40eb7c2 | ||
|
|
63035f977f | ||
|
|
514d2a36bc | ||
|
|
0b6fd5f612 | ||
|
|
029e7135b7 | ||
|
|
c43591f8a2 | ||
|
|
a2c22714cb | ||
|
|
312f10f797 | ||
|
|
d1f05b0f3a | ||
|
|
ccb0b320e1 | ||
|
|
5ee7edaf9e | ||
|
|
27190635ea | ||
|
|
2e340d976f | ||
|
|
fe4dfb9f6f | ||
|
|
5e3dc80999 | ||
|
|
d84cc33742 | ||
|
|
c92c462148 | ||
|
|
9ca06e0336 | ||
|
|
3b523b32f5 | ||
|
|
ba3600a515 | ||
|
|
03ce2e5288 | ||
|
|
87e23abb10 | ||
|
|
2868000c20 | ||
|
|
f38f415bf0 | ||
|
|
4341ab838e | ||
|
|
cd004cf0b2 | ||
|
|
19ae8c88b0 | ||
|
|
3dd09147c2 | ||
|
|
9581bf0670 | ||
|
|
af8aff3788 | ||
|
|
2a8a59ded9 | ||
|
|
5917ac2162 | ||
|
|
b6af4d0dc6 | ||
|
|
577139c626 | ||
|
|
c5fb6281f0 | ||
|
|
f99812443c | ||
|
|
b898c6d0ea | ||
|
|
9e7045eaec | ||
|
|
a17ac02061 |
1
.github/VOUCHED.td
vendored
1
.github/VOUCHED.td
vendored
@@ -26,6 +26,7 @@ kommander
|
||||
r44vc0rp
|
||||
rekram1-node
|
||||
-robinmordasiewicz
|
||||
simonklee
|
||||
-spider-yamet clawdbot/llm psychosis, spam pinging the team
|
||||
thdxr
|
||||
-toastythebot
|
||||
|
||||
62
bun.lock
62
bun.lock
@@ -319,7 +319,7 @@
|
||||
"@actions/core": "1.11.1",
|
||||
"@actions/github": "6.0.1",
|
||||
"@agentclientprotocol/sdk": "0.16.1",
|
||||
"@ai-sdk/amazon-bedrock": "4.0.83",
|
||||
"@ai-sdk/amazon-bedrock": "4.0.93",
|
||||
"@ai-sdk/anthropic": "3.0.67",
|
||||
"@ai-sdk/azure": "3.0.49",
|
||||
"@ai-sdk/cerebras": "2.0.41",
|
||||
@@ -331,7 +331,7 @@
|
||||
"@ai-sdk/groq": "3.0.31",
|
||||
"@ai-sdk/mistral": "3.0.27",
|
||||
"@ai-sdk/openai": "3.0.48",
|
||||
"@ai-sdk/openai-compatible": "2.0.37",
|
||||
"@ai-sdk/openai-compatible": "2.0.41",
|
||||
"@ai-sdk/perplexity": "3.0.26",
|
||||
"@ai-sdk/provider": "3.0.8",
|
||||
"@ai-sdk/provider-utils": "4.0.23",
|
||||
@@ -341,7 +341,6 @@
|
||||
"@aws-sdk/credential-providers": "3.993.0",
|
||||
"@clack/prompts": "1.0.0-alpha.1",
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@gitlab/gitlab-ai-provider": "3.6.0",
|
||||
"@gitlab/opencode-gitlab-auth": "1.3.3",
|
||||
"@hono/node-server": "1.19.11",
|
||||
"@hono/node-ws": "1.3.0",
|
||||
@@ -357,7 +356,7 @@
|
||||
"@opencode-ai/script": "workspace:*",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@openrouter/ai-sdk-provider": "2.4.2",
|
||||
"@openrouter/ai-sdk-provider": "2.5.1",
|
||||
"@opentui/core": "0.1.97",
|
||||
"@opentui/solid": "0.1.97",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
@@ -413,7 +412,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.28.4",
|
||||
"@effect/language-service": "0.79.0",
|
||||
"@effect/language-service": "0.84.2",
|
||||
"@octokit/webhooks-types": "7.6.1",
|
||||
"@opencode-ai/script": "workspace:*",
|
||||
"@parcel/watcher-darwin-arm64": "2.5.1",
|
||||
@@ -450,6 +449,7 @@
|
||||
"version": "1.4.3",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
"zod": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -641,7 +641,7 @@
|
||||
},
|
||||
"catalog": {
|
||||
"@cloudflare/workers-types": "4.20251008.0",
|
||||
"@effect/platform-node": "4.0.0-beta.43",
|
||||
"@effect/platform-node": "4.0.0-beta.46",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"@kobalte/core": "0.13.11",
|
||||
"@lydell/node-pty": "1.2.0-beta.10",
|
||||
@@ -662,13 +662,13 @@
|
||||
"@types/node": "22.13.9",
|
||||
"@types/semver": "7.7.1",
|
||||
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
||||
"ai": "6.0.149",
|
||||
"ai": "6.0.158",
|
||||
"cross-spawn": "7.0.6",
|
||||
"diff": "8.0.2",
|
||||
"dompurify": "3.3.1",
|
||||
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
|
||||
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
|
||||
"effect": "4.0.0-beta.43",
|
||||
"effect": "4.0.0-beta.46",
|
||||
"fuzzysort": "3.1.0",
|
||||
"hono": "4.10.7",
|
||||
"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=="],
|
||||
|
||||
"@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/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/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=="],
|
||||
|
||||
"@effect/language-service": ["@effect/language-service@0.79.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-DEmIOsg1GjjP6s9HXH1oJrW+gDmzkhVv9WOZl6to5eNyyCrjz1S2PDqQ7aYrW/HuifhfwI5Bik1pK4pj7Z+lrg=="],
|
||||
"@effect/language-service": ["@effect/language-service@0.84.2", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-l04qNxpiA8rY5yXWckRPJ7Mk5MNerXuNymSFf+IdflfI5i8jgL1bpBNLuP6ijg7wgjdHc/KmTnCj2kT0SCntuA=="],
|
||||
|
||||
"@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": ["@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-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=="],
|
||||
"@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=="],
|
||||
|
||||
"@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,8 +1161,6 @@
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@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=="],
|
||||
@@ -1539,7 +1537,7 @@
|
||||
|
||||
"@opencode-ai/web": ["@opencode-ai/web@workspace:packages/web"],
|
||||
|
||||
"@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=="],
|
||||
"@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=="],
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||
|
||||
@@ -2385,7 +2383,7 @@
|
||||
|
||||
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
|
||||
|
||||
"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": ["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-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=="],
|
||||
|
||||
@@ -2889,7 +2887,7 @@
|
||||
|
||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||
|
||||
"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=="],
|
||||
"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=="],
|
||||
|
||||
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
|
||||
|
||||
@@ -5005,7 +5003,11 @@
|
||||
|
||||
"@actions/http-client/undici": ["undici@6.24.1", "", {}, "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA=="],
|
||||
|
||||
"@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/@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/@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=="],
|
||||
|
||||
@@ -5281,10 +5283,6 @@
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@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=="],
|
||||
@@ -5509,6 +5507,10 @@
|
||||
|
||||
"@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-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=="],
|
||||
@@ -5549,7 +5551,9 @@
|
||||
|
||||
"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.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/@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-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=="],
|
||||
|
||||
@@ -5765,6 +5769,8 @@
|
||||
|
||||
"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/@tsconfig/bun": ["@tsconfig/bun@1.0.7", "", {}, "sha512-udGrGJBNQdXGVulehc1aWT73wkR9wdaGBtB6yL70RJsqwW/yJhIg6ZbRlPOfIUiFNrnBuYLBi9CSmMKfDC7dvA=="],
|
||||
@@ -5947,8 +5953,6 @@
|
||||
|
||||
"@actions/github/@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@ai-sdk/azure/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
@@ -6435,12 +6439,18 @@
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
@@ -6813,6 +6823,8 @@
|
||||
|
||||
"@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=="],
|
||||
|
||||
"app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-285KZ7rZLRoc6XqCZRHc25NE+mmpGh/BVeMpv8aPQtQ=",
|
||||
"aarch64-linux": "sha256-qIwmY4TP4CI7R7G6A5OMYRrorVNXjkg25tTtVpIHm2o=",
|
||||
"aarch64-darwin": "sha256-RwvnZQhdYZ0u7h7evyfxuPLHHX9eO/jXTAxIFc8B+IE=",
|
||||
"x86_64-darwin": "sha256-vVj40al+TEeMpbe5XG2GmJEpN+eQAvtr9W0T98l5PBE="
|
||||
"x86_64-linux": "sha256-fNRQYkucjXr1D61HJRScJpDa6+oBdyhgTBxCu+PE2kQ=",
|
||||
"aarch64-linux": "sha256-V8J6kn2nSdXrplyqi6aIqNlHcVjSxvye+yC/YFO7PF4=",
|
||||
"aarch64-darwin": "sha256-6cLmUJVUycGALCmslXuloVGBSlFOSHRjsWjx7KOW8rg=",
|
||||
"x86_64-darwin": "sha256-kcOSO3NFIJh79ylLotG41ovWLQfH5kh1WYFghUu+4HE="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"packages/slack"
|
||||
],
|
||||
"catalog": {
|
||||
"@effect/platform-node": "4.0.0-beta.43",
|
||||
"@effect/platform-node": "4.0.0-beta.46",
|
||||
"@types/bun": "1.3.11",
|
||||
"@types/cross-spawn": "6.0.6",
|
||||
"@octokit/rest": "22.0.0",
|
||||
@@ -47,8 +47,8 @@
|
||||
"dompurify": "3.3.1",
|
||||
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
|
||||
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
|
||||
"effect": "4.0.0-beta.43",
|
||||
"ai": "6.0.149",
|
||||
"effect": "4.0.0-beta.46",
|
||||
"ai": "6.0.158",
|
||||
"cross-spawn": "7.0.6",
|
||||
"hono": "4.10.7",
|
||||
"hono-openapi": "1.1.2",
|
||||
|
||||
@@ -316,7 +316,8 @@
|
||||
|
||||
/* Download Hero Section */
|
||||
[data-component="download-hero"] {
|
||||
display: grid;
|
||||
/* display: grid; */
|
||||
display: none;
|
||||
grid-template-columns: 260px 1fr;
|
||||
gap: 4rem;
|
||||
padding-bottom: 2rem;
|
||||
|
||||
@@ -39,11 +39,16 @@
|
||||
"bun": "./src/pty/pty.bun.ts",
|
||||
"node": "./src/pty/pty.node.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": {
|
||||
"@babel/core": "7.28.4",
|
||||
"@effect/language-service": "0.79.0",
|
||||
"@effect/language-service": "0.84.2",
|
||||
"@octokit/webhooks-types": "7.6.1",
|
||||
"@opencode-ai/script": "workspace:*",
|
||||
"@parcel/watcher-darwin-arm64": "2.5.1",
|
||||
@@ -78,7 +83,7 @@
|
||||
"@actions/core": "1.11.1",
|
||||
"@actions/github": "6.0.1",
|
||||
"@agentclientprotocol/sdk": "0.16.1",
|
||||
"@ai-sdk/amazon-bedrock": "4.0.83",
|
||||
"@ai-sdk/amazon-bedrock": "4.0.93",
|
||||
"@ai-sdk/anthropic": "3.0.67",
|
||||
"@ai-sdk/azure": "3.0.49",
|
||||
"@ai-sdk/cerebras": "2.0.41",
|
||||
@@ -90,7 +95,7 @@
|
||||
"@ai-sdk/groq": "3.0.31",
|
||||
"@ai-sdk/mistral": "3.0.27",
|
||||
"@ai-sdk/openai": "3.0.48",
|
||||
"@ai-sdk/openai-compatible": "2.0.37",
|
||||
"@ai-sdk/openai-compatible": "2.0.41",
|
||||
"@ai-sdk/perplexity": "3.0.26",
|
||||
"@ai-sdk/provider": "3.0.8",
|
||||
"@ai-sdk/provider-utils": "4.0.23",
|
||||
@@ -100,7 +105,6 @@
|
||||
"@aws-sdk/credential-providers": "3.993.0",
|
||||
"@clack/prompts": "1.0.0-alpha.1",
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@gitlab/gitlab-ai-provider": "3.6.0",
|
||||
"@gitlab/opencode-gitlab-auth": "1.3.3",
|
||||
"@hono/node-server": "1.19.11",
|
||||
"@hono/node-ws": "1.3.0",
|
||||
@@ -116,7 +120,7 @@
|
||||
"@opencode-ai/script": "workspace:*",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@openrouter/ai-sdk-provider": "2.4.2",
|
||||
"@openrouter/ai-sdk-provider": "2.5.1",
|
||||
"@opentui/core": "0.1.97",
|
||||
"@opentui/solid": "0.1.97",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
|
||||
@@ -23,7 +23,7 @@ export namespace Foo {
|
||||
readonly get: (id: FooID) => Effect.Effect<FooInfo, FooError>
|
||||
}
|
||||
|
||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Foo") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Foo") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
@@ -217,36 +217,37 @@ Fully migrated (single namespace, InstanceState where needed, flattened facade):
|
||||
- [x] `SessionSummary` — `session/summary.ts`
|
||||
- [x] `SessionRevert` — `session/revert.ts`
|
||||
- [x] `Instruction` — `session/instruction.ts`
|
||||
- [x] `SystemPrompt` — `session/system.ts`
|
||||
- [x] `Provider` — `provider/provider.ts`
|
||||
- [x] `Storage` — `storage/storage.ts`
|
||||
- [x] `ShareNext` — `share/share-next.ts`
|
||||
|
||||
Still open:
|
||||
|
||||
- [ ] `SessionTodo` — `session/todo.ts`
|
||||
- [ ] `ShareNext` — `share/share-next.ts`
|
||||
- [x] `SessionTodo` — `session/todo.ts`
|
||||
- [ ] `SyncEvent` — `sync/index.ts`
|
||||
- [ ] `Workspace` — `control-plane/workspace.ts`
|
||||
|
||||
## Tool interface → Effect
|
||||
|
||||
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:
|
||||
`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:
|
||||
|
||||
1. Migrate each tool to return Effects
|
||||
2. Update `Tool.define()` factory to work with Effects
|
||||
3. Update `SessionPrompt` to `yield*` tool results instead of `await`ing
|
||||
1. Migrate each tool body to return Effects
|
||||
2. Keep `Tool.define()` inputs Effect-native
|
||||
3. Update remaining callers to `yield*` tool initialization instead of `await`ing
|
||||
|
||||
### Tool migration details
|
||||
|
||||
Until the tool interface itself returns `Effect`, use this transitional pattern for migrated tools:
|
||||
With `Tool.Info.init()` now effectful, use this transitional pattern for migrated tools that still need Promise-based boundaries internally:
|
||||
|
||||
- `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. 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.
|
||||
- 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()`.
|
||||
- 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`:
|
||||
|
||||
- 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* Effect.promise(() => info.init())`.
|
||||
- Yield the real tool export, then initialize it: `const info = yield* ReadTool`, `const tool = yield* info.init()`.
|
||||
- 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.
|
||||
@@ -336,4 +337,51 @@ For each service, the migration is roughly:
|
||||
|
||||
### 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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Cache, Clock, Duration, Effect, Layer, Option, Schema, SchemaGetter, ServiceMap } from "effect"
|
||||
import { Cache, Clock, Duration, Effect, Layer, Option, Schema, SchemaGetter, Context } from "effect"
|
||||
import {
|
||||
FetchHttpClient,
|
||||
HttpClient,
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
HttpClientResponse,
|
||||
} from "effect/unstable/http"
|
||||
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
import { withTransientReadRetry } from "@/util/effect-http-client"
|
||||
import { AccountRepo, type AccountRow } from "./repo"
|
||||
import { normalizeServerUrl } from "./url"
|
||||
@@ -181,7 +180,7 @@ export namespace Account {
|
||||
readonly poll: (input: Login) => Effect.Effect<PollResult, AccountError>
|
||||
}
|
||||
|
||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Account") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Account") {}
|
||||
|
||||
export const layer: Layer.Layer<Service, never, AccountRepo | HttpClient.HttpClient> = Layer.effect(
|
||||
Service,
|
||||
@@ -454,18 +453,4 @@ export namespace Account {
|
||||
)
|
||||
|
||||
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)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { eq } from "drizzle-orm"
|
||||
import { Effect, Layer, Option, Schema, ServiceMap } from "effect"
|
||||
import { Effect, Layer, Option, Schema, Context } from "effect"
|
||||
|
||||
import { Database } from "@/storage/db"
|
||||
import { AccountStateTable, AccountTable } from "./account.sql"
|
||||
@@ -38,7 +38,7 @@ export namespace AccountRepo {
|
||||
}
|
||||
}
|
||||
|
||||
export class AccountRepo extends ServiceMap.Service<AccountRepo, AccountRepo.Service>()("@opencode/AccountRepo") {
|
||||
export class AccountRepo extends Context.Service<AccountRepo, AccountRepo.Service>()("@opencode/AccountRepo") {
|
||||
static readonly layer: Layer.Layer<AccountRepo> = Layer.effect(
|
||||
AccountRepo,
|
||||
Effect.gen(function* () {
|
||||
|
||||
@@ -1,42 +1,22 @@
|
||||
import { Schema } from "effect"
|
||||
import type * as HttpClientError from "effect/unstable/http/HttpClientError"
|
||||
|
||||
import { withStatics } from "@/util/schema"
|
||||
|
||||
export const AccountID = Schema.String.pipe(
|
||||
Schema.brand("AccountID"),
|
||||
withStatics((s) => ({ make: (id: string) => s.makeUnsafe(id) })),
|
||||
)
|
||||
export const AccountID = Schema.String.pipe(Schema.brand("AccountID"))
|
||||
export type AccountID = Schema.Schema.Type<typeof AccountID>
|
||||
|
||||
export const OrgID = Schema.String.pipe(
|
||||
Schema.brand("OrgID"),
|
||||
withStatics((s) => ({ make: (id: string) => s.makeUnsafe(id) })),
|
||||
)
|
||||
export const OrgID = Schema.String.pipe(Schema.brand("OrgID"))
|
||||
export type OrgID = Schema.Schema.Type<typeof OrgID>
|
||||
|
||||
export const AccessToken = Schema.String.pipe(
|
||||
Schema.brand("AccessToken"),
|
||||
withStatics((s) => ({ make: (token: string) => s.makeUnsafe(token) })),
|
||||
)
|
||||
export const AccessToken = Schema.String.pipe(Schema.brand("AccessToken"))
|
||||
export type AccessToken = Schema.Schema.Type<typeof AccessToken>
|
||||
|
||||
export const RefreshToken = Schema.String.pipe(
|
||||
Schema.brand("RefreshToken"),
|
||||
withStatics((s) => ({ make: (token: string) => s.makeUnsafe(token) })),
|
||||
)
|
||||
export const RefreshToken = Schema.String.pipe(Schema.brand("RefreshToken"))
|
||||
export type RefreshToken = Schema.Schema.Type<typeof RefreshToken>
|
||||
|
||||
export const DeviceCode = Schema.String.pipe(
|
||||
Schema.brand("DeviceCode"),
|
||||
withStatics((s) => ({ make: (code: string) => s.makeUnsafe(code) })),
|
||||
)
|
||||
export const DeviceCode = Schema.String.pipe(Schema.brand("DeviceCode"))
|
||||
export type DeviceCode = Schema.Schema.Type<typeof DeviceCode>
|
||||
|
||||
export const UserCode = Schema.String.pipe(
|
||||
Schema.brand("UserCode"),
|
||||
withStatics((s) => ({ make: (code: string) => s.makeUnsafe(code) })),
|
||||
)
|
||||
export const UserCode = Schema.String.pipe(Schema.brand("UserCode"))
|
||||
export type UserCode = Schema.Schema.Type<typeof UserCode>
|
||||
|
||||
export class Info extends Schema.Class<Info>("Account")({
|
||||
|
||||
@@ -19,7 +19,7 @@ import { Global } from "@/global"
|
||||
import path from "path"
|
||||
import { Plugin } from "@/plugin"
|
||||
import { Skill } from "../skill"
|
||||
import { Effect, ServiceMap, Layer } from "effect"
|
||||
import { Effect, Context, Layer } from "effect"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
|
||||
@@ -67,7 +67,7 @@ export namespace Agent {
|
||||
|
||||
type State = Omit<Interface, "generate">
|
||||
|
||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Agent") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Agent") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
@@ -398,13 +398,11 @@ export namespace Agent {
|
||||
}),
|
||||
)
|
||||
|
||||
export const defaultLayer = Layer.suspend(() =>
|
||||
layer.pipe(
|
||||
Layer.provide(Provider.defaultLayer),
|
||||
Layer.provide(Auth.defaultLayer),
|
||||
Layer.provide(Config.defaultLayer),
|
||||
Layer.provide(Skill.defaultLayer),
|
||||
),
|
||||
export const defaultLayer = layer.pipe(
|
||||
Layer.provide(Provider.defaultLayer),
|
||||
Layer.provide(Auth.defaultLayer),
|
||||
Layer.provide(Config.defaultLayer),
|
||||
Layer.provide(Skill.defaultLayer),
|
||||
)
|
||||
|
||||
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import path from "path"
|
||||
import { Effect, Layer, Record, Result, Schema, ServiceMap } from "effect"
|
||||
import { Effect, Layer, Record, Result, Schema, Context } from "effect"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { Global } from "../global"
|
||||
@@ -49,7 +49,7 @@ export namespace Auth {
|
||||
readonly remove: (key: string) => Effect.Effect<void, AuthError>
|
||||
}
|
||||
|
||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Auth") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Auth") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import z from "zod"
|
||||
import { Effect, Exit, Layer, PubSub, Scope, ServiceMap, Stream } from "effect"
|
||||
import { Effect, Exit, Layer, PubSub, Scope, Context, Stream } from "effect"
|
||||
import { EffectLogger } from "@/effect/logger"
|
||||
import { Log } from "../util/log"
|
||||
import { BusEvent } from "./bus-event"
|
||||
import { GlobalBus } from "./global"
|
||||
@@ -41,7 +42,7 @@ export namespace Bus {
|
||||
readonly subscribeAllCallback: (callback: (event: any) => unknown) => Effect.Effect<() => void>
|
||||
}
|
||||
|
||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Bus") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Bus") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
@@ -146,7 +147,7 @@ export namespace Bus {
|
||||
|
||||
return () => {
|
||||
log.info("unsubscribing", { type })
|
||||
Effect.runFork(Scope.close(scope, Exit.void))
|
||||
Effect.runFork(Scope.close(scope, Exit.void).pipe(Effect.provide(EffectLogger.layer)))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Duration, Effect, Match, Option } from "effect"
|
||||
import { UI } from "../ui"
|
||||
import { AccountID, Account, OrgID, PollExpired, type PollResult } from "@/account"
|
||||
import { type AccountError } from "@/account/schema"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import * as Prompt from "../effect/prompt"
|
||||
import open from "open"
|
||||
|
||||
@@ -182,7 +183,7 @@ export const LoginCommand = cmd({
|
||||
}),
|
||||
async handler(args) {
|
||||
UI.empty()
|
||||
await Account.runPromise((_svc) => loginEffect(args.url))
|
||||
await AppRuntime.runPromise(loginEffect(args.url))
|
||||
},
|
||||
})
|
||||
|
||||
@@ -196,7 +197,7 @@ export const LogoutCommand = cmd({
|
||||
}),
|
||||
async handler(args) {
|
||||
UI.empty()
|
||||
await Account.runPromise((_svc) => logoutEffect(args.email))
|
||||
await AppRuntime.runPromise(logoutEffect(args.email))
|
||||
},
|
||||
})
|
||||
|
||||
@@ -205,7 +206,7 @@ export const SwitchCommand = cmd({
|
||||
describe: false,
|
||||
async handler() {
|
||||
UI.empty()
|
||||
await Account.runPromise((_svc) => switchEffect())
|
||||
await AppRuntime.runPromise(switchEffect())
|
||||
},
|
||||
})
|
||||
|
||||
@@ -214,7 +215,7 @@ export const OrgsCommand = cmd({
|
||||
describe: false,
|
||||
async handler() {
|
||||
UI.empty()
|
||||
await Account.runPromise((_svc) => orgsEffect())
|
||||
await AppRuntime.runPromise(orgsEffect())
|
||||
},
|
||||
})
|
||||
|
||||
@@ -223,7 +224,7 @@ export const OpenCommand = cmd({
|
||||
describe: false,
|
||||
async handler() {
|
||||
UI.empty()
|
||||
await Account.runPromise((_svc) => openEffect())
|
||||
await AppRuntime.runPromise(openEffect())
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { EOL } from "os"
|
||||
import { basename } from "path"
|
||||
import { Effect } from "effect"
|
||||
import { Agent } from "../../../agent/agent"
|
||||
import { Provider } from "../../../provider/provider"
|
||||
import { Session } from "../../../session"
|
||||
@@ -157,14 +158,16 @@ async function createToolContext(agent: Agent.Info) {
|
||||
agent: agent.name,
|
||||
abort: new AbortController().signal,
|
||||
messages: [],
|
||||
metadata: () => {},
|
||||
async ask(req: Omit<Permission.Request, "id" | "sessionID" | "tool">) {
|
||||
for (const pattern of req.patterns) {
|
||||
const rule = Permission.evaluate(req.permission, pattern, ruleset)
|
||||
if (rule.action === "deny") {
|
||||
throw new Permission.DeniedError({ ruleset })
|
||||
metadata: () => Effect.void,
|
||||
ask(req: Omit<Permission.Request, "id" | "sessionID" | "tool">) {
|
||||
return Effect.sync(() => {
|
||||
for (const pattern of req.patterns) {
|
||||
const rule = Permission.evaluate(req.permission, pattern, ruleset)
|
||||
if (rule.action === "deny") {
|
||||
throw new Permission.DeniedError({ ruleset })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import { Provider } from "../../provider/provider"
|
||||
import { Bus } from "../../bus"
|
||||
import { MessageV2 } from "../../session/message-v2"
|
||||
import { SessionPrompt } from "@/session/prompt"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { Git } from "@/git"
|
||||
import { setTimeout as sleep } from "node:timers/promises"
|
||||
import { Process } from "@/util/process"
|
||||
@@ -258,7 +259,9 @@ export const GithubInstallCommand = cmd({
|
||||
}
|
||||
|
||||
// Get repo info
|
||||
const info = (await Git.run(["remote", "get-url", "origin"], { cwd: Instance.worktree })).text().trim()
|
||||
const info = await AppRuntime.runPromise(
|
||||
Git.Service.use((git) => git.run(["remote", "get-url", "origin"], { cwd: Instance.worktree })),
|
||||
).then((x) => x.text().trim())
|
||||
const parsed = parseGitHubRemote(info)
|
||||
if (!parsed) {
|
||||
prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
|
||||
@@ -497,20 +500,21 @@ export const GithubRunCommand = cmd({
|
||||
: "issue"
|
||||
: undefined
|
||||
const gitText = async (args: string[]) => {
|
||||
const result = await Git.run(args, { cwd: Instance.worktree })
|
||||
const result = await AppRuntime.runPromise(Git.Service.use((git) => git.run(args, { cwd: Instance.worktree })))
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Process.RunFailedError(["git", ...args], result.exitCode, result.stdout, result.stderr)
|
||||
}
|
||||
return result.text().trim()
|
||||
}
|
||||
const gitRun = async (args: string[]) => {
|
||||
const result = await Git.run(args, { cwd: Instance.worktree })
|
||||
const result = await AppRuntime.runPromise(Git.Service.use((git) => git.run(args, { cwd: Instance.worktree })))
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Process.RunFailedError(["git", ...args], result.exitCode, result.stdout, result.stderr)
|
||||
}
|
||||
return result
|
||||
}
|
||||
const gitStatus = (args: string[]) => Git.run(args, { cwd: Instance.worktree })
|
||||
const gitStatus = (args: string[]) =>
|
||||
AppRuntime.runPromise(Git.Service.use((git) => git.run(args, { cwd: Instance.worktree })))
|
||||
const commitChanges = async (summary: string, actor?: string) => {
|
||||
const args = ["commit", "-m", summary]
|
||||
if (actor) args.push("-m", `Co-authored-by: ${actor} <${actor}@users.noreply.github.com>`)
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Instance } from "../../project/instance"
|
||||
import { ShareNext } from "../../share/share-next"
|
||||
import { EOL } from "os"
|
||||
import { Filesystem } from "../../util/filesystem"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
|
||||
/** Discriminated union returned by the ShareNext API (GET /api/shares/:id/data) */
|
||||
export type ShareData =
|
||||
@@ -100,7 +101,7 @@ export const ImportCommand = cmd({
|
||||
if (isUrl) {
|
||||
const slug = parseShareUrl(args.file)
|
||||
if (!slug) {
|
||||
const baseUrl = await ShareNext.url()
|
||||
const baseUrl = await AppRuntime.runPromise(ShareNext.Service.use((svc) => svc.url()))
|
||||
process.stdout.write(`Invalid URL format. Expected: ${baseUrl}/share/<slug>`)
|
||||
process.stdout.write(EOL)
|
||||
return
|
||||
@@ -108,7 +109,7 @@ export const ImportCommand = cmd({
|
||||
|
||||
const parsed = new URL(args.file)
|
||||
const baseUrl = parsed.origin
|
||||
const req = await ShareNext.request()
|
||||
const req = await AppRuntime.runPromise(ShareNext.Service.use((svc) => svc.request()))
|
||||
const headers = shouldAttachShareAuthHeaders(args.file, req.baseUrl) ? req.headers : {}
|
||||
|
||||
const dataPath = req.api.data(slug)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { UI } from "../ui"
|
||||
import { cmd } from "./cmd"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { Git } from "@/git"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { Process } from "@/util/process"
|
||||
@@ -67,19 +68,29 @@ export const PrCommand = cmd({
|
||||
const remoteName = forkOwner
|
||||
|
||||
// Check if remote already exists
|
||||
const remotes = (await Git.run(["remote"], { cwd: Instance.worktree })).text().trim()
|
||||
const remotes = await AppRuntime.runPromise(
|
||||
Git.Service.use((git) => git.run(["remote"], { cwd: Instance.worktree })),
|
||||
).then((x) => x.text().trim())
|
||||
if (!remotes.split("\n").includes(remoteName)) {
|
||||
await Git.run(["remote", "add", remoteName, `https://github.com/${forkOwner}/${forkName}.git`], {
|
||||
cwd: Instance.worktree,
|
||||
})
|
||||
await AppRuntime.runPromise(
|
||||
Git.Service.use((git) =>
|
||||
git.run(["remote", "add", remoteName, `https://github.com/${forkOwner}/${forkName}.git`], {
|
||||
cwd: Instance.worktree,
|
||||
}),
|
||||
),
|
||||
)
|
||||
UI.println(`Added fork remote: ${remoteName}`)
|
||||
}
|
||||
|
||||
// Set upstream to the fork so pushes go there
|
||||
const headRefName = prInfo.headRefName
|
||||
await Git.run(["branch", `--set-upstream-to=${remoteName}/${headRefName}`, localBranchName], {
|
||||
cwd: Instance.worktree,
|
||||
})
|
||||
await AppRuntime.runPromise(
|
||||
Git.Service.use((git) =>
|
||||
git.run(["branch", `--set-upstream-to=${remoteName}/${headRefName}`, localBranchName], {
|
||||
cwd: Instance.worktree,
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Check for opencode session link in PR body
|
||||
|
||||
@@ -589,6 +589,13 @@ export function Prompt(props: PromptProps) {
|
||||
])
|
||||
|
||||
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 (autocomplete?.visible) return
|
||||
if (!store.prompt.input) return
|
||||
@@ -994,7 +1001,11 @@ export function Prompt(props: PromptProps) {
|
||||
input.cursorOffset = input.plainText.length
|
||||
}
|
||||
}}
|
||||
onSubmit={submit}
|
||||
onSubmit={() => {
|
||||
// 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) => {
|
||||
if (props.disabled) {
|
||||
event.preventDefault()
|
||||
|
||||
@@ -137,12 +137,18 @@ export const TuiThreadCommand = cmd({
|
||||
),
|
||||
})
|
||||
worker.onerror = (e) => {
|
||||
Log.Default.error(e)
|
||||
Log.Default.error("thread error", {
|
||||
message: e.message,
|
||||
filename: e.filename,
|
||||
lineno: e.lineno,
|
||||
colno: e.colno,
|
||||
error: e.error,
|
||||
})
|
||||
}
|
||||
|
||||
const client = Rpc.client<typeof rpc>(worker)
|
||||
const error = (e: unknown) => {
|
||||
Log.Default.error(e)
|
||||
Log.Default.error("process error", { error: errorMessage(e) })
|
||||
}
|
||||
const reload = () => {
|
||||
client.call("reload", undefined).catch((err) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Argv } from "yargs"
|
||||
import { UI } from "../ui"
|
||||
import * as prompts from "@clack/prompts"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { Installation } from "../../installation"
|
||||
import { Global } from "../../global"
|
||||
import fs from "fs/promises"
|
||||
@@ -57,7 +58,7 @@ export const UninstallCommand = {
|
||||
UI.empty()
|
||||
prompts.intro("Uninstall OpenCode")
|
||||
|
||||
const method = await Installation.method()
|
||||
const method = await AppRuntime.runPromise(Installation.Service.use((svc) => svc.method()))
|
||||
prompts.log.info(`Installation method: ${method}`)
|
||||
|
||||
const targets = await collectRemovalTargets(args, method)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Argv } from "yargs"
|
||||
import { UI } from "../ui"
|
||||
import * as prompts from "@clack/prompts"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { Installation } from "../../installation"
|
||||
|
||||
export const UpgradeCommand = {
|
||||
@@ -24,7 +25,7 @@ export const UpgradeCommand = {
|
||||
UI.println(UI.logo(" "))
|
||||
UI.empty()
|
||||
prompts.intro("Upgrade")
|
||||
const detectedMethod = await Installation.method()
|
||||
const detectedMethod = await AppRuntime.runPromise(Installation.Service.use((svc) => svc.method()))
|
||||
const method = (args.method as Installation.Method) ?? detectedMethod
|
||||
if (method === "unknown") {
|
||||
prompts.log.error(`opencode is installed to ${process.execPath} and may be managed by a package manager`)
|
||||
@@ -42,7 +43,9 @@ export const UpgradeCommand = {
|
||||
}
|
||||
}
|
||||
prompts.log.info("Using method: " + method)
|
||||
const target = args.target ? args.target.replace(/^v/, "") : await Installation.latest()
|
||||
const target = args.target
|
||||
? args.target.replace(/^v/, "")
|
||||
: await AppRuntime.runPromise(Installation.Service.use((svc) => svc.latest()))
|
||||
|
||||
if (Installation.VERSION === target) {
|
||||
prompts.log.warn(`opencode upgrade skipped: ${target} is already installed`)
|
||||
@@ -53,7 +56,9 @@ export const UpgradeCommand = {
|
||||
prompts.log.info(`From ${Installation.VERSION} → ${target}`)
|
||||
const spinner = prompts.spinner()
|
||||
spinner.start("Upgrading...")
|
||||
const err = await Installation.upgrade(method, target).catch((err) => err)
|
||||
const err = await AppRuntime.runPromise(Installation.Service.use((svc) => svc.upgrade(method, target))).catch(
|
||||
(err) => err,
|
||||
)
|
||||
if (err) {
|
||||
spinner.stop("Upgrade failed", 1)
|
||||
if (err instanceof Installation.UpgradeFailedError) {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Bus } from "@/bus"
|
||||
import { Config } from "@/config/config"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { Flag } from "@/flag/flag"
|
||||
import { Installation } from "@/installation"
|
||||
|
||||
export async function upgrade() {
|
||||
const config = await Config.getGlobal()
|
||||
const method = await Installation.method()
|
||||
const latest = await Installation.latest(method).catch(() => {})
|
||||
const method = await AppRuntime.runPromise(Installation.Service.use((svc) => svc.method()))
|
||||
const latest = await AppRuntime.runPromise(Installation.Service.use((svc) => svc.latest(method))).catch(() => {})
|
||||
if (!latest) return
|
||||
|
||||
if (Flag.OPENCODE_ALWAYS_NOTIFY_UPDATE) {
|
||||
@@ -25,7 +26,7 @@ export async function upgrade() {
|
||||
}
|
||||
|
||||
if (method === "unknown") return
|
||||
await Installation.upgrade(method, latest)
|
||||
await AppRuntime.runPromise(Installation.Service.use((svc) => svc.upgrade(method, latest)))
|
||||
.then(() => Bus.publish(Installation.Event.Updated, { version: latest }))
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
import type { InstanceContext } from "@/project/instance"
|
||||
import { SessionID, MessageID } from "@/session/schema"
|
||||
import { Effect, Layer, ServiceMap } from "effect"
|
||||
import { Effect, Layer, Context } from "effect"
|
||||
import { EffectLogger } from "@/effect/logger"
|
||||
import z from "zod"
|
||||
import { Config } from "../config/config"
|
||||
import { MCP } from "../mcp"
|
||||
@@ -70,7 +71,7 @@ export namespace Command {
|
||||
readonly list: () => Effect.Effect<Info[]>
|
||||
}
|
||||
|
||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Command") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Command") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
@@ -79,7 +80,7 @@ export namespace Command {
|
||||
const mcp = yield* MCP.Service
|
||||
const skill = yield* Skill.Service
|
||||
|
||||
const init = Effect.fn("Command.state")(function* (ctx) {
|
||||
const init = Effect.fn("Command.state")(function* (ctx: InstanceContext) {
|
||||
const cfg = yield* config.get()
|
||||
const commands: Record<string, Info> = {}
|
||||
|
||||
@@ -140,6 +141,7 @@ export namespace Command {
|
||||
.map((message) => (message.content.type === "text" ? message.content.text : ""))
|
||||
.join("\n") || "",
|
||||
),
|
||||
Effect.provide(EffectLogger.layer),
|
||||
),
|
||||
)
|
||||
},
|
||||
@@ -186,10 +188,4 @@ export namespace Command {
|
||||
Layer.provide(MCP.defaultLayer),
|
||||
Layer.provide(Skill.defaultLayer),
|
||||
)
|
||||
|
||||
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||
|
||||
export async function list() {
|
||||
return runPromise((svc) => svc.list())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { pathToFileURL } from "url"
|
||||
import os from "os"
|
||||
import { Process } from "../util/process"
|
||||
import z from "zod"
|
||||
import { ModelsDev } from "../provider/models"
|
||||
import { mergeDeep, pipe, unique } from "remeda"
|
||||
import { Global } from "../global"
|
||||
import fsNode from "fs/promises"
|
||||
@@ -37,10 +36,11 @@ import type { ConsoleState } from "./console-state"
|
||||
import { AppFileSystem } from "@/filesystem"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
import { Duration, Effect, Layer, Option, ServiceMap } from "effect"
|
||||
import { Duration, Effect, Layer, Option, Context } from "effect"
|
||||
import { Flock } from "@/util/flock"
|
||||
import { isPathPluginSpec, parsePluginSpecifier, resolvePathPluginTarget } from "@/plugin/shared"
|
||||
import { Npm } from "@/npm"
|
||||
import { InstanceRef } from "@/effect/instance-ref"
|
||||
|
||||
export namespace Config {
|
||||
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>
|
||||
}
|
||||
|
||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Config") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Config") {}
|
||||
|
||||
function globalConfigFile() {
|
||||
const candidates = ["opencode.jsonc", "opencode.json", "config.json"].map((file) =>
|
||||
@@ -1327,27 +1327,31 @@ export namespace Config {
|
||||
const consoleManagedProviders = new Set<string>()
|
||||
let activeOrgName: string | undefined
|
||||
|
||||
const scope = (source: string): PluginScope => {
|
||||
const scope = Effect.fnUntraced(function* (source: string) {
|
||||
if (source.startsWith("http://") || source.startsWith("https://")) return "global"
|
||||
if (source === "OPENCODE_CONFIG_CONTENT") return "local"
|
||||
if (Instance.containsPath(source)) return "local"
|
||||
if (yield* InstanceRef.use((ctx) => Effect.succeed(Instance.containsPath(source, ctx)))) return "local"
|
||||
return "global"
|
||||
}
|
||||
})
|
||||
|
||||
const track = (source: string, list: PluginSpec[] | undefined, kind?: PluginScope) => {
|
||||
const track = Effect.fnUntraced(function* (
|
||||
source: string,
|
||||
list: PluginSpec[] | undefined,
|
||||
kind?: PluginScope,
|
||||
) {
|
||||
if (!list?.length) return
|
||||
const hit = kind ?? scope(source)
|
||||
const hit = kind ?? (yield* scope(source))
|
||||
const plugins = deduplicatePluginOrigins([
|
||||
...(result.plugin_origins ?? []),
|
||||
...list.map((spec) => ({ spec, source, scope: hit })),
|
||||
])
|
||||
result.plugin = plugins.map((item) => item.spec)
|
||||
result.plugin_origins = plugins
|
||||
}
|
||||
})
|
||||
|
||||
const merge = (source: string, next: Info, kind?: PluginScope) => {
|
||||
result = mergeConfigConcatArrays(result, next)
|
||||
track(source, next.plugin, kind)
|
||||
return track(source, next.plugin, kind)
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(auth)) {
|
||||
@@ -1367,16 +1371,16 @@ export namespace Config {
|
||||
dir: path.dirname(source),
|
||||
source,
|
||||
})
|
||||
merge(source, next, "global")
|
||||
yield* merge(source, next, "global")
|
||||
log.debug("loaded remote config from well-known", { url })
|
||||
}
|
||||
}
|
||||
|
||||
const global = yield* getGlobal()
|
||||
merge(Global.Path.config, global, "global")
|
||||
yield* merge(Global.Path.config, global, "global")
|
||||
|
||||
if (Flag.OPENCODE_CONFIG) {
|
||||
merge(Flag.OPENCODE_CONFIG, yield* loadFile(Flag.OPENCODE_CONFIG))
|
||||
yield* merge(Flag.OPENCODE_CONFIG, yield* loadFile(Flag.OPENCODE_CONFIG))
|
||||
log.debug("loaded custom config", { path: Flag.OPENCODE_CONFIG })
|
||||
}
|
||||
|
||||
@@ -1384,7 +1388,7 @@ export namespace Config {
|
||||
for (const file of yield* Effect.promise(() =>
|
||||
ConfigPaths.projectFiles("opencode", ctx.directory, ctx.worktree),
|
||||
)) {
|
||||
merge(file, yield* loadFile(file), "local")
|
||||
yield* merge(file, yield* loadFile(file), "local")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1405,7 +1409,7 @@ export namespace Config {
|
||||
for (const file of ["opencode.json", "opencode.jsonc"]) {
|
||||
const source = path.join(dir, file)
|
||||
log.debug(`loading config from ${source}`)
|
||||
merge(source, yield* loadFile(source))
|
||||
yield* merge(source, yield* loadFile(source))
|
||||
result.agent ??= {}
|
||||
result.mode ??= {}
|
||||
result.plugin ??= []
|
||||
@@ -1424,7 +1428,7 @@ export namespace Config {
|
||||
result.agent = mergeDeep(result.agent, yield* Effect.promise(() => loadAgent(dir)))
|
||||
result.agent = mergeDeep(result.agent, yield* Effect.promise(() => loadMode(dir)))
|
||||
const list = yield* Effect.promise(() => loadPlugin(dir))
|
||||
track(dir, list)
|
||||
yield* track(dir, list)
|
||||
}
|
||||
|
||||
if (process.env.OPENCODE_CONFIG_CONTENT) {
|
||||
@@ -1433,7 +1437,7 @@ export namespace Config {
|
||||
dir: ctx.directory,
|
||||
source,
|
||||
})
|
||||
merge(source, next, "local")
|
||||
yield* merge(source, next, "local")
|
||||
log.debug("loaded custom config from OPENCODE_CONFIG_CONTENT")
|
||||
}
|
||||
|
||||
@@ -1462,7 +1466,7 @@ export namespace Config {
|
||||
for (const providerID of Object.keys(next.provider ?? {})) {
|
||||
consoleManagedProviders.add(providerID)
|
||||
}
|
||||
merge(source, next, "global")
|
||||
yield* merge(source, next, "global")
|
||||
}
|
||||
}).pipe(
|
||||
Effect.catch((err) => {
|
||||
@@ -1477,7 +1481,7 @@ export namespace Config {
|
||||
if (existsSync(managedDir)) {
|
||||
for (const file of ["opencode.json", "opencode.jsonc"]) {
|
||||
const source = path.join(managedDir, file)
|
||||
merge(source, yield* loadFile(source), "global")
|
||||
yield* merge(source, yield* loadFile(source), "global")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,7 @@ export type WorkspaceID = typeof workspaceIdSchema.Type
|
||||
|
||||
export const WorkspaceID = workspaceIdSchema.pipe(
|
||||
withStatics((schema: typeof workspaceIdSchema) => ({
|
||||
make: (id: string) => schema.makeUnsafe(id),
|
||||
ascending: (id?: string) => schema.makeUnsafe(Identifier.ascending("workspace", id)),
|
||||
ascending: (id?: string) => schema.make(Identifier.ascending("workspace", id)),
|
||||
zod: Identifier.schema("workspace").pipe(z.custom<WorkspaceID>()),
|
||||
})),
|
||||
)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Context } from "../util/context"
|
||||
import { LocalContext } from "../util/local-context"
|
||||
import type { WorkspaceID } from "../control-plane/schema"
|
||||
|
||||
export interface WorkspaceContext {
|
||||
workspaceID: string
|
||||
}
|
||||
|
||||
const context = Context.create<WorkspaceContext>("instance")
|
||||
const context = LocalContext.create<WorkspaceContext>("instance")
|
||||
|
||||
export const WorkspaceContext = {
|
||||
async provide<R>(input: { workspaceID: WorkspaceID; fn: () => R }): Promise<R> {
|
||||
|
||||
10
packages/opencode/src/effect/bootstrap-runtime.ts
Normal file
10
packages/opencode/src/effect/bootstrap-runtime.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
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,6 +402,7 @@ export const make = Effect.gen(function* () {
|
||||
|
||||
const fd = yield* setupFds(command, proc, extra)
|
||||
const out = setupOutput(command, proc, sout, serr)
|
||||
let ref = true
|
||||
return makeHandle({
|
||||
pid: ProcessId(proc.pid!),
|
||||
stdin: yield* setupStdin(command, proc, sin),
|
||||
@@ -432,6 +433,18 @@ export const make = Effect.gen(function* () {
|
||||
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": {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { ServiceMap } from "effect"
|
||||
import { Context } from "effect"
|
||||
import type { InstanceContext } from "@/project/instance"
|
||||
|
||||
export const InstanceRef = ServiceMap.Reference<InstanceContext | undefined>("~opencode/InstanceRef", {
|
||||
export const InstanceRef = Context.Reference<InstanceContext | undefined>("~opencode/InstanceRef", {
|
||||
defaultValue: () => undefined,
|
||||
})
|
||||
|
||||
export const WorkspaceRef = ServiceMap.Reference<string | undefined>("~opencode/WorkspaceRef", {
|
||||
export const WorkspaceRef = Context.Reference<string | undefined>("~opencode/WorkspaceRef", {
|
||||
defaultValue: () => undefined,
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Effect, Fiber, ScopedCache, Scope, ServiceMap } from "effect"
|
||||
import { Effect, Fiber, ScopedCache, Scope, Context } from "effect"
|
||||
import { EffectLogger } from "@/effect/logger"
|
||||
import { Instance, type InstanceContext } from "@/project/instance"
|
||||
import { Context } from "@/util/context"
|
||||
import { LocalContext } from "@/util/local-context"
|
||||
import { InstanceRef, WorkspaceRef } from "./instance-ref"
|
||||
import { registerDisposer } from "./instance-registry"
|
||||
import { WorkspaceContext } from "@/control-plane/workspace-context"
|
||||
@@ -17,10 +18,10 @@ export namespace InstanceState {
|
||||
try {
|
||||
return Instance.bind(fn)
|
||||
} catch (err) {
|
||||
if (!(err instanceof Context.NotFound)) throw err
|
||||
if (!(err instanceof LocalContext.NotFound)) throw err
|
||||
}
|
||||
const fiber = Fiber.getCurrent()
|
||||
const ctx = fiber ? ServiceMap.getReferenceUnsafe(fiber.services, InstanceRef) : undefined
|
||||
const ctx = fiber ? Context.getReferenceUnsafe(fiber.context, InstanceRef) : undefined
|
||||
if (!ctx) return fn
|
||||
return ((...args: any[]) => Instance.restore(ctx, () => fn(...args))) as F
|
||||
}
|
||||
@@ -47,7 +48,9 @@ export namespace InstanceState {
|
||||
}),
|
||||
})
|
||||
|
||||
const off = registerDisposer((directory) => Effect.runPromise(ScopedCache.invalidate(cache, directory)))
|
||||
const off = registerDisposer((directory) =>
|
||||
Effect.runPromise(ScopedCache.invalidate(cache, directory).pipe(Effect.provide(EffectLogger.layer))),
|
||||
)
|
||||
yield* Effect.addFinalizer(() => Effect.sync(off))
|
||||
|
||||
return {
|
||||
|
||||
@@ -55,7 +55,7 @@ export namespace EffectLogger {
|
||||
}
|
||||
})
|
||||
|
||||
export const layer = Logger.layer([Logger.tracerLogger, logger], { mergeWithExisting: false })
|
||||
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),
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Flag } from "@/flag/flag"
|
||||
import { CHANNEL, VERSION } from "@/installation/meta"
|
||||
|
||||
export namespace Observability {
|
||||
const base = Flag.OTEL_EXPORTER_OTLP_ENDPOINT ?? (CHANNEL === "local" ? "http://127.0.0.1:27686" : undefined)
|
||||
const base = Flag.OTEL_EXPORTER_OTLP_ENDPOINT
|
||||
export const enabled = !!base
|
||||
|
||||
const resource = {
|
||||
@@ -34,7 +34,6 @@ export namespace Observability {
|
||||
: Otlp.layerJson({
|
||||
baseUrl: base,
|
||||
loggerExportInterval: Duration.seconds(1),
|
||||
tracerExportInterval: Duration.seconds(1),
|
||||
loggerMergeWithExisting: true,
|
||||
resource,
|
||||
headers,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect, Layer, ManagedRuntime } from "effect"
|
||||
import * as ServiceMap from "effect/ServiceMap"
|
||||
import * as Context from "effect/Context"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { Context } from "@/util/context"
|
||||
import { LocalContext } from "@/util/local-context"
|
||||
import { InstanceRef, WorkspaceRef } from "./instance-ref"
|
||||
import { Observability } from "./oltp"
|
||||
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
|
||||
return effect.pipe(Effect.provideService(InstanceRef, ctx), Effect.provideService(WorkspaceRef, workspaceID))
|
||||
} catch (err) {
|
||||
if (!(err instanceof Context.NotFound)) throw err
|
||||
if (!(err instanceof LocalContext.NotFound)) throw err
|
||||
}
|
||||
return effect
|
||||
}
|
||||
|
||||
export function makeRuntime<I, S, E>(service: ServiceMap.Service<I, S>, layer: Layer.Layer<I, E>) {
|
||||
export function makeRuntime<I, S, E>(service: Context.Service<I, S>, layer: Layer.Layer<I, E>) {
|
||||
let rt: ManagedRuntime.ManagedRuntime<I, E> | undefined
|
||||
const getRuntime = () => (rt ??= ManagedRuntime.make(Layer.merge(layer, Observability.layer), { memoMap }))
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { InstanceState } from "@/effect/instance-state"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
import { AppFileSystem } from "@/filesystem"
|
||||
import { Git } from "@/git"
|
||||
import { Effect, Layer, ServiceMap } from "effect"
|
||||
import { Effect, Layer, Context } from "effect"
|
||||
import { formatPatch, structuredPatch } from "diff"
|
||||
import fuzzysort from "fuzzysort"
|
||||
import ignore from "ignore"
|
||||
@@ -337,7 +337,7 @@ export namespace File {
|
||||
}) => Effect.Effect<string[]>
|
||||
}
|
||||
|
||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/File") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/File") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
|
||||
@@ -3,7 +3,7 @@ import path from "path"
|
||||
import { Global } from "../global"
|
||||
import fs from "fs/promises"
|
||||
import z from "zod"
|
||||
import { Effect, Layer, ServiceMap } from "effect"
|
||||
import { Effect, Layer, Context } from "effect"
|
||||
import * as Stream from "effect/Stream"
|
||||
import { ChildProcess } from "effect/unstable/process"
|
||||
import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
|
||||
@@ -291,7 +291,7 @@ export namespace Ripgrep {
|
||||
}) => Stream.Stream<string, PlatformError>
|
||||
}
|
||||
|
||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Ripgrep") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Ripgrep") {}
|
||||
|
||||
export const layer: Layer.Layer<Service, never, ChildProcessSpawner | AppFileSystem.Service> = Layer.effect(
|
||||
Service,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { DateTime, Effect, Layer, Option, Semaphore, ServiceMap } from "effect"
|
||||
import { DateTime, Effect, Layer, Option, Semaphore, Context } from "effect"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
import { AppFileSystem } from "@/filesystem"
|
||||
import { Flag } from "@/flag/flag"
|
||||
import type { SessionID } from "@/session/schema"
|
||||
@@ -34,10 +33,10 @@ export namespace FileTime {
|
||||
readonly read: (sessionID: SessionID, file: string) => Effect.Effect<void>
|
||||
readonly get: (sessionID: SessionID, file: string) => Effect.Effect<Date | undefined>
|
||||
readonly assert: (sessionID: SessionID, filepath: string) => Effect.Effect<void>
|
||||
readonly withLock: <T>(filepath: string, fn: () => Promise<T>) => Effect.Effect<T>
|
||||
readonly withLock: <T>(filepath: string, fn: () => Effect.Effect<T>) => Effect.Effect<T>
|
||||
}
|
||||
|
||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/FileTime") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/FileTime") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
@@ -103,8 +102,8 @@ export namespace FileTime {
|
||||
)
|
||||
})
|
||||
|
||||
const withLock = Effect.fn("FileTime.withLock")(function* <T>(filepath: string, fn: () => Promise<T>) {
|
||||
return yield* Effect.promise(fn).pipe((yield* getLock(filepath)).withPermits(1))
|
||||
const withLock = Effect.fn("FileTime.withLock")(function* <T>(filepath: string, fn: () => Effect.Effect<T>) {
|
||||
return yield* fn().pipe((yield* getLock(filepath)).withPermits(1))
|
||||
})
|
||||
|
||||
return Service.of({ read, get, assert, withLock })
|
||||
@@ -112,22 +111,4 @@ export namespace FileTime {
|
||||
).pipe(Layer.orDie)
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Cause, Effect, Layer, Scope, ServiceMap } from "effect"
|
||||
import { Cause, Effect, Layer, Scope, Context } from "effect"
|
||||
// @ts-ignore
|
||||
import { createWrapper } from "@parcel/watcher/wrapper"
|
||||
import type ParcelWatcher from "@parcel/watcher"
|
||||
@@ -8,7 +8,6 @@ import z from "zod"
|
||||
import { Bus } from "@/bus"
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
import { Flag } from "@/flag/flag"
|
||||
import { Git } from "@/git"
|
||||
import { Instance } from "@/project/instance"
|
||||
@@ -65,7 +64,7 @@ export namespace FileWatcher {
|
||||
readonly init: () => Effect.Effect<void>
|
||||
}
|
||||
|
||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/FileWatcher") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/FileWatcher") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
@@ -161,10 +160,4 @@ export namespace FileWatcher {
|
||||
)
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { dirname, join, relative, resolve as pathResolve } from "path"
|
||||
import { realpathSync } from "fs"
|
||||
import * as NFS from "fs/promises"
|
||||
import { lookup } from "mime-types"
|
||||
import { Effect, FileSystem, Layer, Schema, ServiceMap } from "effect"
|
||||
import { Effect, FileSystem, Layer, Schema, Context } from "effect"
|
||||
import type { PlatformError } from "effect/PlatformError"
|
||||
import { Glob } from "../util/glob"
|
||||
|
||||
@@ -36,7 +36,7 @@ export namespace AppFileSystem {
|
||||
readonly globMatch: (pattern: string, filepath: string) => boolean
|
||||
}
|
||||
|
||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/FileSystem") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/FileSystem") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Effect, Layer, ServiceMap } from "effect"
|
||||
import { Effect, Layer, Context } from "effect"
|
||||
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||
import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
import path from "path"
|
||||
import { mergeDeep } from "remeda"
|
||||
import z from "zod"
|
||||
@@ -31,7 +30,7 @@ export namespace Format {
|
||||
readonly file: (filepath: string) => Effect.Effect<void>
|
||||
}
|
||||
|
||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Format") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Format") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
@@ -193,18 +192,4 @@ export namespace Format {
|
||||
Layer.provide(Config.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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
|
||||
import { Effect, Layer, ServiceMap, Stream } from "effect"
|
||||
import { Effect, Layer, Context, Stream } from "effect"
|
||||
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
|
||||
export namespace Git {
|
||||
const cfg = [
|
||||
@@ -80,7 +79,7 @@ export namespace Git {
|
||||
return "modified"
|
||||
}
|
||||
|
||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Git") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Git") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
@@ -258,14 +257,4 @@ export namespace Git {
|
||||
)
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Effect, Layer, Schema, ServiceMap, Stream } from "effect"
|
||||
import { Effect, Layer, Schema, Context, Stream } from "effect"
|
||||
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
|
||||
import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
import { withTransientReadRetry } from "@/util/effect-http-client"
|
||||
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||
import path from "path"
|
||||
@@ -91,7 +90,7 @@ export namespace Installation {
|
||||
readonly upgrade: (method: Method, target: string) => Effect.Effect<void, UpgradeFailedError>
|
||||
}
|
||||
|
||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Installation") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Installation") {}
|
||||
|
||||
export const layer: Layer.Layer<Service, never, HttpClient.HttpClient | ChildProcessSpawner.ChildProcessSpawner> =
|
||||
Layer.effect(
|
||||
@@ -338,18 +337,4 @@ export namespace Installation {
|
||||
Layer.provide(FetchHttpClient.layer),
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Instance } from "../project/instance"
|
||||
import { Flag } from "@/flag/flag"
|
||||
import { Process } from "../util/process"
|
||||
import { spawn as lspspawn } from "./launch"
|
||||
import { Effect, Layer, ServiceMap } from "effect"
|
||||
import { Effect, Layer, Context } from "effect"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
|
||||
@@ -156,7 +156,7 @@ export namespace LSP {
|
||||
readonly outgoingCalls: (input: LocInput) => Effect.Effect<any[]>
|
||||
}
|
||||
|
||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/LSP") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/LSP") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
@@ -540,6 +540,8 @@ export namespace LSP {
|
||||
export const outgoingCalls = async (input: LocInput) => runPromise((svc) => svc.outgoingCalls(input))
|
||||
|
||||
export namespace Diagnostic {
|
||||
const MAX_PER_FILE = 20
|
||||
|
||||
export function pretty(diagnostic: LSPClient.Diagnostic) {
|
||||
const severityMap = {
|
||||
1: "ERROR",
|
||||
@@ -554,5 +556,14 @@ export namespace LSP {
|
||||
|
||||
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>`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import path from "path"
|
||||
import z from "zod"
|
||||
import { Global } from "../global"
|
||||
import { Effect, Layer, ServiceMap } from "effect"
|
||||
import { Effect, Layer, Context } from "effect"
|
||||
import { AppFileSystem } from "@/filesystem"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
|
||||
@@ -49,7 +49,7 @@ export namespace McpAuth {
|
||||
readonly isTokenExpired: (mcpName: string) => Effect.Effect<boolean | null>
|
||||
}
|
||||
|
||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/McpAuth") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/McpAuth") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
|
||||
@@ -24,7 +24,8 @@ import { BusEvent } from "../bus/bus-event"
|
||||
import { Bus } from "@/bus"
|
||||
import { TuiEvent } from "@/cli/cmd/tui/event"
|
||||
import open from "open"
|
||||
import { Effect, Exit, Layer, Option, ServiceMap, Stream } from "effect"
|
||||
import { Effect, Exit, Layer, Option, Context, Stream } from "effect"
|
||||
import { EffectLogger } from "@/effect/logger"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||
@@ -239,7 +240,7 @@ export namespace MCP {
|
||||
readonly getAuthStatus: (mcpName: string) => Effect.Effect<AuthStatus>
|
||||
}
|
||||
|
||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/MCP") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/MCP") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
@@ -469,12 +470,14 @@ export namespace MCP {
|
||||
log.info("tools list changed notification received", { server: name })
|
||||
if (s.clients[name] !== client || s.status[name]?.status !== "connected") return
|
||||
|
||||
const listed = await Effect.runPromise(defs(name, client, timeout))
|
||||
const listed = await Effect.runPromise(defs(name, client, timeout).pipe(Effect.provide(EffectLogger.layer)))
|
||||
if (!listed) return
|
||||
if (s.clients[name] !== client || s.status[name]?.status !== "connected") return
|
||||
|
||||
s.defs[name] = listed
|
||||
await Effect.runPromise(bus.publish(ToolsChanged, { server: name }).pipe(Effect.ignore))
|
||||
await Effect.runPromise(
|
||||
bus.publish(ToolsChanged, { server: name }).pipe(Effect.ignore, Effect.provide(EffectLogger.layer)),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { PermissionTable } from "@/session/session.sql"
|
||||
import { Database, eq } from "@/storage/db"
|
||||
import { Log } from "@/util/log"
|
||||
import { Wildcard } from "@/util/wildcard"
|
||||
import { Deferred, Effect, Layer, Schema, ServiceMap } from "effect"
|
||||
import { Deferred, Effect, Layer, Schema, Context } from "effect"
|
||||
import os from "os"
|
||||
import z from "zod"
|
||||
import { evaluate as evalRule } from "./evaluate"
|
||||
@@ -135,7 +135,7 @@ export namespace Permission {
|
||||
return evalRule(permission, pattern, ...rulesets)
|
||||
}
|
||||
|
||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Permission") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Permission") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
|
||||
@@ -5,12 +5,8 @@ import { Identifier } from "@/id/id"
|
||||
import { Newtype } from "@/util/schema"
|
||||
|
||||
export class PermissionID extends Newtype<PermissionID>()("PermissionID", Schema.String) {
|
||||
static make(id: string): PermissionID {
|
||||
return this.makeUnsafe(id)
|
||||
}
|
||||
|
||||
static ascending(id?: string): PermissionID {
|
||||
return this.makeUnsafe(Identifier.ascending("permission", id))
|
||||
return this.make(Identifier.ascending("permission", id))
|
||||
}
|
||||
|
||||
static readonly zod = Identifier.schema("permission") as unknown as z.ZodType<PermissionID>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { iife } from "@/util/iife"
|
||||
import { Log } from "../../util/log"
|
||||
import { setTimeout as sleep } from "node:timers/promises"
|
||||
import { CopilotModels } from "./models"
|
||||
import { MessageV2 } from "@/session/message-v2"
|
||||
|
||||
const log = Log.create({ service: "plugin.copilot" })
|
||||
|
||||
@@ -27,11 +28,27 @@ function base(enterpriseUrl?: string) {
|
||||
return enterpriseUrl ? `https://copilot-api.${normalizeDomain(enterpriseUrl)}` : "https://api.githubcopilot.com"
|
||||
}
|
||||
|
||||
function fix(model: Model): Model {
|
||||
// Check if a message is a synthetic user msg used to attach an image from a tool call
|
||||
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 {
|
||||
...model,
|
||||
api: {
|
||||
...model.api,
|
||||
url,
|
||||
npm: "@ai-sdk/github-copilot",
|
||||
},
|
||||
}
|
||||
@@ -44,19 +61,23 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise<Hooks> {
|
||||
id: "github-copilot",
|
||||
async models(provider, ctx) {
|
||||
if (ctx.auth?.type !== "oauth") {
|
||||
return Object.fromEntries(Object.entries(provider.models).map(([id, model]) => [id, fix(model)]))
|
||||
return Object.fromEntries(Object.entries(provider.models).map(([id, model]) => [id, fix(model, base())]))
|
||||
}
|
||||
|
||||
const auth = ctx.auth
|
||||
|
||||
return CopilotModels.get(
|
||||
base(ctx.auth.enterpriseUrl),
|
||||
base(auth.enterpriseUrl),
|
||||
{
|
||||
Authorization: `Bearer ${ctx.auth.refresh}`,
|
||||
Authorization: `Bearer ${auth.refresh}`,
|
||||
"User-Agent": `opencode/${Installation.VERSION}`,
|
||||
},
|
||||
provider.models,
|
||||
).catch((error) => {
|
||||
log.error("failed to fetch copilot models", { error })
|
||||
return Object.fromEntries(Object.entries(provider.models).map(([id, model]) => [id, fix(model)]))
|
||||
return Object.fromEntries(
|
||||
Object.entries(provider.models).map(([id, model]) => [id, fix(model, base(auth.enterpriseUrl))]),
|
||||
)
|
||||
})
|
||||
},
|
||||
},
|
||||
@@ -66,10 +87,7 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise<Hooks> {
|
||||
const info = await getAuth()
|
||||
if (!info || info.type !== "oauth") return {}
|
||||
|
||||
const baseURL = base(info.enterpriseUrl)
|
||||
|
||||
return {
|
||||
baseURL,
|
||||
apiKey: "",
|
||||
async fetch(request: RequestInfo | URL, init?: RequestInit) {
|
||||
const info = await getAuth()
|
||||
@@ -88,7 +106,7 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise<Hooks> {
|
||||
(msg: any) =>
|
||||
Array.isArray(msg.content) && msg.content.some((part: any) => part.type === "image_url"),
|
||||
),
|
||||
isAgent: last?.role !== "user",
|
||||
isAgent: last?.role !== "user" || imgMsg(last),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +118,7 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise<Hooks> {
|
||||
(item: any) =>
|
||||
Array.isArray(item?.content) && item.content.some((part: any) => part.type === "input_image"),
|
||||
),
|
||||
isAgent: last?.role !== "user",
|
||||
isAgent: last?.role !== "user" || imgMsg(last),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,7 +140,7 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise<Hooks> {
|
||||
part.content.some((nested: any) => nested?.type === "image")),
|
||||
),
|
||||
),
|
||||
isAgent: !(last?.role === "user" && hasNonToolCalls),
|
||||
isAgent: !(last?.role === "user" && hasNonToolCalls) || imgMsg(last),
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
@@ -52,13 +52,15 @@ export namespace CopilotModels {
|
||||
(remote.capabilities.supports.vision ?? false) ||
|
||||
(remote.capabilities.limits.vision?.supported_media_types ?? []).some((item) => item.startsWith("image/"))
|
||||
|
||||
const isMsgApi = remote.supported_endpoints?.includes("/v1/messages")
|
||||
|
||||
return {
|
||||
id: key,
|
||||
providerID: "github-copilot",
|
||||
api: {
|
||||
id: remote.id,
|
||||
url,
|
||||
npm: "@ai-sdk/github-copilot",
|
||||
url: isMsgApi ? `${url}/v1` : url,
|
||||
npm: isMsgApi ? "@ai-sdk/anthropic" : "@ai-sdk/github-copilot",
|
||||
},
|
||||
// API response wins
|
||||
status: "active",
|
||||
|
||||
@@ -11,7 +11,8 @@ import { CopilotAuthPlugin } from "./github-copilot/copilot"
|
||||
import { gitlabAuthPlugin as GitlabAuthPlugin } from "opencode-gitlab-auth"
|
||||
import { PoeAuthPlugin } from "opencode-poe-auth"
|
||||
import { CloudflareAIGatewayAuthPlugin, CloudflareWorkersAuthPlugin } from "./cloudflare"
|
||||
import { Effect, Layer, ServiceMap, Stream } from "effect"
|
||||
import { Effect, Layer, Context, Stream } from "effect"
|
||||
import { EffectLogger } from "@/effect/logger"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
import { errorMessage } from "@/util/error"
|
||||
@@ -44,7 +45,7 @@ export namespace Plugin {
|
||||
readonly init: () => Effect.Effect<void>
|
||||
}
|
||||
|
||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Plugin") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Plugin") {}
|
||||
|
||||
// Built-in plugins that are directly imported (not installed from npm)
|
||||
const INTERNAL_PLUGINS: PluginInstance[] = [
|
||||
@@ -83,7 +84,11 @@ export namespace Plugin {
|
||||
}
|
||||
|
||||
function publishPluginError(bus: Bus.Interface, message: string) {
|
||||
Effect.runFork(bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() }))
|
||||
Effect.runFork(
|
||||
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[]) {
|
||||
@@ -119,7 +124,7 @@ export namespace Plugin {
|
||||
Authorization: `Basic ${Buffer.from(`${Flag.OPENCODE_SERVER_USERNAME ?? "opencode"}:${Flag.OPENCODE_SERVER_PASSWORD}`).toString("base64")}`,
|
||||
}
|
||||
: undefined,
|
||||
fetch: async (...args) => Server.Default().app.fetch(...args),
|
||||
fetch: async (...args) => (await Server.Default()).app.fetch(...args),
|
||||
})
|
||||
const cfg = yield* config.get()
|
||||
const input: PluginInput = {
|
||||
@@ -205,13 +210,15 @@ export namespace Plugin {
|
||||
return message
|
||||
},
|
||||
}).pipe(
|
||||
Effect.catch((message) =>
|
||||
bus.publish(Session.Event.Error, {
|
||||
error: new NamedError.Unknown({
|
||||
message: `Failed to load plugin ${load.spec}: ${message}`,
|
||||
}).toObject(),
|
||||
}),
|
||||
),
|
||||
Effect.catch(() => {
|
||||
// TODO: make proper events for this
|
||||
// bus.publish(Session.Event.Error, {
|
||||
// error: new NamedError.Unknown({
|
||||
// message: `Failed to load plugin ${load.spec}: ${message}`,
|
||||
// }).toObject(),
|
||||
// })
|
||||
return Effect.void
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Plugin } from "../plugin"
|
||||
import { Format } from "../format"
|
||||
import { LSP } from "../lsp"
|
||||
import { File } from "../file"
|
||||
import { FileWatcher } from "../file/watcher"
|
||||
import { Snapshot } from "../snapshot"
|
||||
import { Project } from "./project"
|
||||
import { Vcs } from "./vcs"
|
||||
@@ -10,16 +9,18 @@ import { Bus } from "../bus"
|
||||
import { Command } from "../command"
|
||||
import { Instance } from "./instance"
|
||||
import { Log } from "@/util/log"
|
||||
import { BootstrapRuntime } from "@/effect/bootstrap-runtime"
|
||||
import { FileWatcher } from "@/file/watcher"
|
||||
import { ShareNext } from "@/share/share-next"
|
||||
|
||||
export async function InstanceBootstrap() {
|
||||
Log.Default.info("bootstrapping", { directory: Instance.directory })
|
||||
await Plugin.init()
|
||||
ShareNext.init()
|
||||
Format.init()
|
||||
void BootstrapRuntime.runPromise(ShareNext.Service.use((svc) => svc.init()))
|
||||
void BootstrapRuntime.runPromise(Format.Service.use((svc) => svc.init()))
|
||||
await LSP.init()
|
||||
File.init()
|
||||
FileWatcher.init()
|
||||
void BootstrapRuntime.runPromise(FileWatcher.Service.use((svc) => svc.init()))
|
||||
Vcs.init()
|
||||
Snapshot.init()
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { disposeInstance } from "@/effect/instance-registry"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { iife } from "@/util/iife"
|
||||
import { Log } from "@/util/log"
|
||||
import { Context } from "../util/context"
|
||||
import { LocalContext } from "../util/local-context"
|
||||
import { Project } from "./project"
|
||||
import { WorkspaceContext } from "@/control-plane/workspace-context"
|
||||
import { State } from "./state"
|
||||
@@ -14,7 +14,7 @@ export interface InstanceContext {
|
||||
project: Project.Info
|
||||
}
|
||||
|
||||
const context = Context.create<InstanceContext>("instance")
|
||||
const context = LocalContext.create<InstanceContext>("instance")
|
||||
const cache = new Map<string, Promise<InstanceContext>>()
|
||||
|
||||
const disposal = {
|
||||
@@ -90,12 +90,13 @@ export const Instance = {
|
||||
* 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.
|
||||
*/
|
||||
containsPath(filepath: string) {
|
||||
if (Filesystem.contains(Instance.directory, filepath)) return true
|
||||
containsPath(filepath: string, ctx?: InstanceContext) {
|
||||
const instance = ctx ?? Instance
|
||||
if (Filesystem.contains(instance.directory, filepath)) return true
|
||||
// Non-git projects set worktree to "/" which would match ANY absolute path.
|
||||
// Skip worktree check in this case to preserve external_directory permissions.
|
||||
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
|
||||
|
||||
@@ -8,7 +8,7 @@ import { BusEvent } from "@/bus/bus-event"
|
||||
import { GlobalBus } from "@/bus/global"
|
||||
import { which } from "../util/which"
|
||||
import { ProjectID } from "./schema"
|
||||
import { Effect, Layer, Path, Scope, ServiceMap, Stream } from "effect"
|
||||
import { Effect, Layer, Path, Scope, Context, Stream } from "effect"
|
||||
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||
import { NodeFileSystem, NodePath } from "@effect/platform-node"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
@@ -100,7 +100,7 @@ export namespace Project {
|
||||
readonly removeSandbox: (id: ProjectID, directory: string) => Effect.Effect<void>
|
||||
}
|
||||
|
||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Project") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Project") {}
|
||||
|
||||
type GitResult = { code: number; text: string; stderr: string }
|
||||
|
||||
|
||||
@@ -9,8 +9,7 @@ export type ProjectID = typeof projectIdSchema.Type
|
||||
|
||||
export const ProjectID = projectIdSchema.pipe(
|
||||
withStatics((schema: typeof projectIdSchema) => ({
|
||||
global: schema.makeUnsafe("global"),
|
||||
make: (id: string) => schema.makeUnsafe(id),
|
||||
global: schema.make("global"),
|
||||
zod: z.string().pipe(z.custom<ProjectID>()),
|
||||
})),
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Effect, Layer, ServiceMap, Stream } from "effect"
|
||||
import { Effect, Layer, Context, Stream } from "effect"
|
||||
import { formatPatch, structuredPatch } from "diff"
|
||||
import path from "path"
|
||||
import { Bus } from "@/bus"
|
||||
@@ -151,7 +151,7 @@ export namespace Vcs {
|
||||
root: Git.Base | undefined
|
||||
}
|
||||
|
||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Vcs") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Vcs") {}
|
||||
|
||||
export const layer: Layer.Layer<Service, never, AppFileSystem.Service | Git.Service | Bus.Service> = Layer.effect(
|
||||
Service,
|
||||
|
||||
@@ -2,10 +2,9 @@ import type { AuthOAuthResult, Hooks } from "@opencode-ai/plugin"
|
||||
import { NamedError } from "@opencode-ai/util/error"
|
||||
import { Auth } from "@/auth"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
import { Plugin } from "../plugin"
|
||||
import { ProviderID } from "./schema"
|
||||
import { Array as Arr, Effect, Layer, Record, Result, ServiceMap } from "effect"
|
||||
import { Array as Arr, Effect, Layer, Record, Result, Context } from "effect"
|
||||
import z from "zod"
|
||||
|
||||
export namespace ProviderAuth {
|
||||
@@ -109,7 +108,7 @@ export namespace ProviderAuth {
|
||||
pending: Map<ProviderID, AuthOAuthResult>
|
||||
}
|
||||
|
||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/ProviderAuth") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/ProviderAuth") {}
|
||||
|
||||
export const layer: Layer.Layer<Service, never, Auth.Service | Plugin.Service> = Layer.effect(
|
||||
Service,
|
||||
@@ -232,22 +231,4 @@ export namespace ProviderAuth {
|
||||
export const defaultLayer = Layer.suspend(() =>
|
||||
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
@@ -19,7 +19,8 @@ import { iife } from "@/util/iife"
|
||||
import { Global } from "../global"
|
||||
import path from "path"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
import { Effect, Layer, ServiceMap } from "effect"
|
||||
import { Effect, Layer, Context } from "effect"
|
||||
import { EffectLogger } from "@/effect/logger"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
|
||||
@@ -924,7 +925,7 @@ export namespace Provider {
|
||||
varsLoaders: Record<string, CustomVarsLoader>
|
||||
}
|
||||
|
||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Provider") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Provider") {}
|
||||
|
||||
function cost(c: ModelsDev.Model["cost"]): Model["cost"] {
|
||||
const result: Model["cost"] = {
|
||||
@@ -1215,7 +1216,8 @@ export namespace Provider {
|
||||
|
||||
const options = yield* Effect.promise(() =>
|
||||
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],
|
||||
),
|
||||
)
|
||||
|
||||
@@ -9,20 +9,19 @@ export type ProviderID = typeof providerIdSchema.Type
|
||||
|
||||
export const ProviderID = providerIdSchema.pipe(
|
||||
withStatics((schema: typeof providerIdSchema) => ({
|
||||
make: (id: string) => schema.makeUnsafe(id),
|
||||
zod: z.string().pipe(z.custom<ProviderID>()),
|
||||
// Well-known providers
|
||||
opencode: schema.makeUnsafe("opencode"),
|
||||
anthropic: schema.makeUnsafe("anthropic"),
|
||||
openai: schema.makeUnsafe("openai"),
|
||||
google: schema.makeUnsafe("google"),
|
||||
googleVertex: schema.makeUnsafe("google-vertex"),
|
||||
githubCopilot: schema.makeUnsafe("github-copilot"),
|
||||
amazonBedrock: schema.makeUnsafe("amazon-bedrock"),
|
||||
azure: schema.makeUnsafe("azure"),
|
||||
openrouter: schema.makeUnsafe("openrouter"),
|
||||
mistral: schema.makeUnsafe("mistral"),
|
||||
gitlab: schema.makeUnsafe("gitlab"),
|
||||
opencode: schema.make("opencode"),
|
||||
anthropic: schema.make("anthropic"),
|
||||
openai: schema.make("openai"),
|
||||
google: schema.make("google"),
|
||||
googleVertex: schema.make("google-vertex"),
|
||||
githubCopilot: schema.make("github-copilot"),
|
||||
amazonBedrock: schema.make("amazon-bedrock"),
|
||||
azure: schema.make("azure"),
|
||||
openrouter: schema.make("openrouter"),
|
||||
mistral: schema.make("mistral"),
|
||||
gitlab: schema.make("gitlab"),
|
||||
})),
|
||||
)
|
||||
|
||||
@@ -32,7 +31,6 @@ export type ModelID = typeof modelIdSchema.Type
|
||||
|
||||
export const ModelID = modelIdSchema.pipe(
|
||||
withStatics((schema: typeof modelIdSchema) => ({
|
||||
make: (id: string) => schema.makeUnsafe(id),
|
||||
zod: z.string().pipe(z.custom<ModelID>()),
|
||||
})),
|
||||
)
|
||||
|
||||
@@ -774,7 +774,10 @@ export namespace ProviderTransform {
|
||||
result["chat_template_args"] = { enable_thinking: true }
|
||||
}
|
||||
|
||||
if (["zai", "zhipuai"].includes(input.model.providerID) && input.model.api.npm === "@ai-sdk/openai-compatible") {
|
||||
if (
|
||||
["zai", "zhipuai"].some((id) => input.model.providerID.includes(id)) &&
|
||||
input.model.api.npm === "@ai-sdk/openai-compatible"
|
||||
) {
|
||||
result["thinking"] = {
|
||||
type: "enabled",
|
||||
clear_thinking: false,
|
||||
|
||||
@@ -10,7 +10,8 @@ import { lazy } from "@opencode-ai/util/lazy"
|
||||
import { Shell } from "@/shell/shell"
|
||||
import { Plugin } from "@/plugin"
|
||||
import { PtyID } from "./schema"
|
||||
import { Effect, Layer, ServiceMap } from "effect"
|
||||
import { Effect, Layer, Context } from "effect"
|
||||
import { EffectLogger } from "@/effect/logger"
|
||||
|
||||
export namespace Pty {
|
||||
const log = Log.create({ service: "pty" })
|
||||
@@ -112,7 +113,7 @@ export namespace Pty {
|
||||
) => Effect.Effect<{ onMessage: (message: string | ArrayBuffer) => void; onClose: () => void } | undefined>
|
||||
}
|
||||
|
||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Pty") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Pty") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
@@ -256,8 +257,8 @@ export namespace Pty {
|
||||
if (session.info.status === "exited") return
|
||||
log.info("session exited", { id, exitCode })
|
||||
session.info.status = "exited"
|
||||
Effect.runFork(bus.publish(Event.Exited, { id, exitCode }))
|
||||
Effect.runFork(remove(id))
|
||||
Effect.runFork(bus.publish(Event.Exited, { id, exitCode }).pipe(Effect.provide(EffectLogger.layer)))
|
||||
Effect.runFork(remove(id).pipe(Effect.provide(EffectLogger.layer)))
|
||||
}),
|
||||
)
|
||||
yield* bus.publish(Event.Created, { info })
|
||||
|
||||
@@ -10,8 +10,7 @@ export type PtyID = typeof ptyIdSchema.Type
|
||||
|
||||
export const PtyID = ptyIdSchema.pipe(
|
||||
withStatics((schema: typeof ptyIdSchema) => ({
|
||||
make: (id: string) => schema.makeUnsafe(id),
|
||||
ascending: (id?: string) => schema.makeUnsafe(Identifier.ascending("pty", id)),
|
||||
ascending: (id?: string) => schema.make(Identifier.ascending("pty", id)),
|
||||
zod: Identifier.schema("pty").pipe(z.custom<PtyID>()),
|
||||
})),
|
||||
)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Deferred, Effect, Layer, Schema, ServiceMap } from "effect"
|
||||
import { Deferred, Effect, Layer, Schema, Context } from "effect"
|
||||
import { Bus } from "@/bus"
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
import { SessionID, MessageID } from "@/session/schema"
|
||||
import { Log } from "@/util/log"
|
||||
import z from "zod"
|
||||
@@ -104,7 +103,7 @@ export namespace Question {
|
||||
readonly list: () => Effect.Effect<Request[]>
|
||||
}
|
||||
|
||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Question") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Question") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
@@ -199,26 +198,4 @@ export namespace Question {
|
||||
)
|
||||
|
||||
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,12 +5,8 @@ import { Identifier } from "@/id/id"
|
||||
import { Newtype } from "@/util/schema"
|
||||
|
||||
export class QuestionID extends Newtype<QuestionID>()("QuestionID", Schema.String) {
|
||||
static make(id: string): QuestionID {
|
||||
return this.makeUnsafe(id)
|
||||
}
|
||||
|
||||
static ascending(id?: string): QuestionID {
|
||||
return this.makeUnsafe(Identifier.ascending("question", id))
|
||||
return this.make(Identifier.ascending("question", id))
|
||||
}
|
||||
|
||||
static readonly zod = Identifier.schema("question") as unknown as z.ZodType<QuestionID>
|
||||
|
||||
40
packages/opencode/src/server/adapter.bun.ts
Normal file
40
packages/opencode/src/server/adapter.bun.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
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))
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
66
packages/opencode/src/server/adapter.node.ts
Normal file
66
packages/opencode/src/server/adapter.node.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
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
packages/opencode/src/server/adapter.ts
Normal file
21
packages/opencode/src/server/adapter.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
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
|
||||
}
|
||||
150
packages/opencode/src/server/control/index.ts
Normal file
150
packages/opencode/src/server/control/index.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
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)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -11,9 +11,11 @@ import { Session } from "../../session"
|
||||
import { Config } from "../../config/config"
|
||||
import { ConsoleState } from "../../config/console-state"
|
||||
import { Account, AccountID, OrgID } from "../../account"
|
||||
import { AppRuntime } from "../../effect/app-runtime"
|
||||
import { zodToJsonSchema } from "zod-to-json-schema"
|
||||
import { errors } from "../error"
|
||||
import { lazy } from "../../util/lazy"
|
||||
import { Effect, Option } from "effect"
|
||||
import { WorkspaceRoutes } from "./workspace"
|
||||
import { Agent } from "@/agent/agent"
|
||||
|
||||
@@ -55,11 +57,20 @@ export const ExperimentalRoutes = lazy(() =>
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
const [consoleState, groups] = await Promise.all([Config.getConsoleState(), Account.orgsByAccount()])
|
||||
return c.json({
|
||||
...consoleState,
|
||||
switchableOrgCount: groups.reduce((count, group) => count + group.orgs.length, 0),
|
||||
})
|
||||
const result = await AppRuntime.runPromise(
|
||||
Effect.gen(function* () {
|
||||
const config = yield* Config.Service
|
||||
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),
|
||||
}
|
||||
}),
|
||||
)
|
||||
return c.json(result)
|
||||
},
|
||||
)
|
||||
.get(
|
||||
@@ -80,17 +91,24 @@ export const ExperimentalRoutes = lazy(() =>
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
const [groups, active] = await Promise.all([Account.orgsByAccount(), Account.active()])
|
||||
|
||||
const orgs = groups.flatMap((group) =>
|
||||
group.orgs.map((org) => ({
|
||||
accountID: group.account.id,
|
||||
accountEmail: group.account.email,
|
||||
accountUrl: group.account.url,
|
||||
orgID: org.id,
|
||||
orgName: org.name,
|
||||
active: !!active && active.id === group.account.id && active.active_org_id === org.id,
|
||||
})),
|
||||
const orgs = await AppRuntime.runPromise(
|
||||
Effect.gen(function* () {
|
||||
const account = yield* Account.Service
|
||||
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) => ({
|
||||
accountID: group.account.id,
|
||||
accountEmail: group.account.email,
|
||||
accountUrl: group.account.url,
|
||||
orgID: org.id,
|
||||
orgName: org.name,
|
||||
active: !!info && info.id === group.account.id && info.active_org_id === org.id,
|
||||
})),
|
||||
)
|
||||
}),
|
||||
)
|
||||
return c.json({ orgs })
|
||||
},
|
||||
@@ -115,7 +133,12 @@ export const ExperimentalRoutes = lazy(() =>
|
||||
validator("json", ConsoleSwitchBody),
|
||||
async (c) => {
|
||||
const body = c.req.valid("json")
|
||||
await Account.switchOrg(AccountID.make(body.accountID), OrgID.make(body.orgID))
|
||||
await AppRuntime.runPromise(
|
||||
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)
|
||||
},
|
||||
)
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Hono, type Context } from "hono"
|
||||
import { describeRoute, resolver, validator } from "hono-openapi"
|
||||
import { streamSSE } from "hono/streaming"
|
||||
import { Effect } from "effect"
|
||||
import z from "zod"
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { SyncEvent } from "@/sync"
|
||||
import { GlobalBus } from "@/bus/global"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { AsyncQueue } from "@/util/queue"
|
||||
import { Instance } from "../../project/instance"
|
||||
import { Installation } from "@/installation"
|
||||
@@ -290,25 +292,41 @@ export const GlobalRoutes = lazy(() =>
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const method = await Installation.method()
|
||||
if (method === "unknown") {
|
||||
return c.json({ success: false, error: "Unknown installation method" }, 400)
|
||||
const result = await AppRuntime.runPromise(
|
||||
Installation.Service.use((svc) =>
|
||||
Effect.gen(function* () {
|
||||
const method = yield* svc.method()
|
||||
if (method === "unknown") {
|
||||
return { success: false as const, status: 400 as const, error: "Unknown installation method" }
|
||||
}
|
||||
|
||||
const target = c.req.valid("json").target || (yield* svc.latest(method))
|
||||
const result = yield* Effect.catch(
|
||||
svc.upgrade(method, target).pipe(Effect.as({ success: true as const, version: target })),
|
||||
(err) =>
|
||||
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 = c.req.valid("json").target || (await Installation.latest(method))
|
||||
const result = await Installation.upgrade(method, target)
|
||||
.then(() => ({ success: true as const, version: target }))
|
||||
.catch((e) => ({ success: false as const, error: e instanceof Error ? e.message : String(e) }))
|
||||
if (result.success) {
|
||||
GlobalBus.emit("event", {
|
||||
directory: "global",
|
||||
payload: {
|
||||
type: Installation.Event.Updated.type,
|
||||
properties: { version: target },
|
||||
},
|
||||
})
|
||||
return c.json(result)
|
||||
}
|
||||
return c.json(result, 500)
|
||||
const target = result.version
|
||||
GlobalBus.emit("event", {
|
||||
directory: "global",
|
||||
payload: {
|
||||
type: Installation.Event.Updated.type,
|
||||
properties: { version: target },
|
||||
},
|
||||
})
|
||||
return c.json({ success: true, version: target })
|
||||
},
|
||||
),
|
||||
)
|
||||
@@ -1,52 +1,33 @@
|
||||
import { describeRoute, resolver, validator } from "hono-openapi"
|
||||
import { Hono } from "hono"
|
||||
import { proxy } from "hono/proxy"
|
||||
import type { UpgradeWebSocket } from "hono/ws"
|
||||
import z from "zod"
|
||||
import { createHash } from "node:crypto"
|
||||
import * as fs from "node:fs/promises"
|
||||
import { Log } from "../util/log"
|
||||
import { Format } from "../format"
|
||||
import { TuiRoutes } from "./routes/tui"
|
||||
import { Instance } from "../project/instance"
|
||||
import { Vcs } from "../project/vcs"
|
||||
import { Agent } from "../agent/agent"
|
||||
import { Skill } from "../skill"
|
||||
import { Global } from "../global"
|
||||
import { LSP } from "../lsp"
|
||||
import { Command } from "../command"
|
||||
import { Flag } from "../flag/flag"
|
||||
import { QuestionRoutes } from "./routes/question"
|
||||
import { PermissionRoutes } from "./routes/permission"
|
||||
import { Snapshot } from "@/snapshot"
|
||||
import { ProjectRoutes } from "./routes/project"
|
||||
import { SessionRoutes } from "./routes/session"
|
||||
import { PtyRoutes } from "./routes/pty"
|
||||
import { McpRoutes } from "./routes/mcp"
|
||||
import { FileRoutes } from "./routes/file"
|
||||
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"
|
||||
import { Format } from "../../format"
|
||||
import { TuiRoutes } from "./tui"
|
||||
import { Instance } from "../../project/instance"
|
||||
import { Vcs } from "../../project/vcs"
|
||||
import { Agent } from "../../agent/agent"
|
||||
import { Skill } from "../../skill"
|
||||
import { Global } from "../../global"
|
||||
import { LSP } from "../../lsp"
|
||||
import { Command } from "../../command"
|
||||
import { QuestionRoutes } from "./question"
|
||||
import { PermissionRoutes } from "./permission"
|
||||
import { ProjectRoutes } from "./project"
|
||||
import { SessionRoutes } from "./session"
|
||||
import { PtyRoutes } from "./pty"
|
||||
import { McpRoutes } from "./mcp"
|
||||
import { FileRoutes } from "./file"
|
||||
import { ConfigRoutes } from "./config"
|
||||
import { ExperimentalRoutes } from "./experimental"
|
||||
import { ProviderRoutes } from "./provider"
|
||||
import { EventRoutes } from "./event"
|
||||
import { WorkspaceRouterMiddleware } from "./middleware"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
|
||||
const log = Log.create({ service: "server" })
|
||||
|
||||
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))
|
||||
export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono =>
|
||||
new Hono()
|
||||
.use(WorkspaceRouterMiddleware(upgrade))
|
||||
.route("/project", ProjectRoutes())
|
||||
.route("/pty", PtyRoutes(upgrade))
|
||||
.route("/config", ConfigRoutes())
|
||||
@@ -190,7 +171,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket, app: Hono = new Hono()
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
const commands = await Command.list()
|
||||
const commands = await AppRuntime.runPromise(Command.Service.use((svc) => svc.list()))
|
||||
return c.json(commands)
|
||||
},
|
||||
)
|
||||
@@ -277,42 +258,6 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket, app: Hono = new Hono()
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
return c.json(await Format.status())
|
||||
return c.json(await AppRuntime.runPromise(Format.Service.use((svc) => svc.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
|
||||
}
|
||||
})
|
||||
@@ -3,12 +3,10 @@ import type { UpgradeWebSocket } from "hono/ws"
|
||||
import { getAdaptor } from "@/control-plane/adaptors"
|
||||
import { WorkspaceID } from "@/control-plane/schema"
|
||||
import { Workspace } from "@/control-plane/workspace"
|
||||
import { ServerProxy } from "./proxy"
|
||||
import { lazy } from "@/util/lazy"
|
||||
import { ServerProxy } from "../proxy"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { InstanceBootstrap } from "@/project/bootstrap"
|
||||
import { InstanceRoutes } from "./instance"
|
||||
import { Session } from "@/session"
|
||||
import { SessionID } from "@/session/schema"
|
||||
import { WorkspaceContext } from "@/control-plane/workspace-context"
|
||||
@@ -47,9 +45,7 @@ async function getSessionWorkspace(url: URL) {
|
||||
}
|
||||
|
||||
export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): MiddlewareHandler {
|
||||
const routes = lazy(() => InstanceRoutes(upgrade))
|
||||
|
||||
return async (c) => {
|
||||
return async (c, next) => {
|
||||
const raw = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd()
|
||||
const directory = Filesystem.resolve(
|
||||
(() => {
|
||||
@@ -72,7 +68,7 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware
|
||||
directory,
|
||||
init: InstanceBootstrap,
|
||||
async fn() {
|
||||
return routes().fetch(c.req.raw, c.env)
|
||||
return next()
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -87,7 +83,7 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware
|
||||
// The lets the `DELETE /session/:id` endpoint through and we've
|
||||
// made sure that it will run without an instance
|
||||
if (url.pathname.match(/\/session\/[^/]+$/) && c.req.method === "DELETE") {
|
||||
return routes().fetch(c.req.raw, c.env)
|
||||
return next()
|
||||
}
|
||||
|
||||
return new Response(`Workspace not found: ${workspaceID}`, {
|
||||
@@ -109,7 +105,7 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware
|
||||
directory: target.directory,
|
||||
init: InstanceBootstrap,
|
||||
async fn() {
|
||||
return routes().fetch(c.req.raw, c.env)
|
||||
return next()
|
||||
},
|
||||
}),
|
||||
})
|
||||
@@ -118,7 +114,7 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware
|
||||
if (local(c.req.method, url.pathname)) {
|
||||
// No instance provided because we are serving cached data; there
|
||||
// is no instance to work with
|
||||
return routes().fetch(c.req.raw, c.env)
|
||||
return next()
|
||||
}
|
||||
|
||||
if (c.req.header("upgrade")?.toLowerCase() === "websocket") {
|
||||
@@ -6,6 +6,7 @@ import { Provider } from "../../provider/provider"
|
||||
import { ModelsDev } from "../../provider/models"
|
||||
import { ProviderAuth } from "../../provider/auth"
|
||||
import { ProviderID } from "../../provider/schema"
|
||||
import { AppRuntime } from "../../effect/app-runtime"
|
||||
import { mapValues } from "remeda"
|
||||
import { errors } from "../error"
|
||||
import { lazy } from "../../util/lazy"
|
||||
@@ -81,7 +82,7 @@ export const ProviderRoutes = lazy(() =>
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
return c.json(await ProviderAuth.methods())
|
||||
return c.json(await AppRuntime.runPromise(ProviderAuth.Service.use((svc) => svc.methods())))
|
||||
},
|
||||
)
|
||||
.post(
|
||||
@@ -118,11 +119,15 @@ export const ProviderRoutes = lazy(() =>
|
||||
async (c) => {
|
||||
const providerID = c.req.valid("param").providerID
|
||||
const { method, inputs } = c.req.valid("json")
|
||||
const result = await ProviderAuth.authorize({
|
||||
providerID,
|
||||
method,
|
||||
inputs,
|
||||
})
|
||||
const result = await AppRuntime.runPromise(
|
||||
ProviderAuth.Service.use((svc) =>
|
||||
svc.authorize({
|
||||
providerID,
|
||||
method,
|
||||
inputs,
|
||||
}),
|
||||
),
|
||||
)
|
||||
return c.json(result)
|
||||
},
|
||||
)
|
||||
@@ -160,11 +165,15 @@ export const ProviderRoutes = lazy(() =>
|
||||
async (c) => {
|
||||
const providerID = c.req.valid("param").providerID
|
||||
const { method, code } = c.req.valid("json")
|
||||
await ProviderAuth.callback({
|
||||
providerID,
|
||||
method,
|
||||
code,
|
||||
})
|
||||
await AppRuntime.runPromise(
|
||||
ProviderAuth.Service.use((svc) =>
|
||||
svc.callback({
|
||||
providerID,
|
||||
method,
|
||||
code,
|
||||
}),
|
||||
),
|
||||
)
|
||||
return c.json(true)
|
||||
},
|
||||
),
|
||||
@@ -3,6 +3,7 @@ import { describeRoute, validator } from "hono-openapi"
|
||||
import { resolver } from "hono-openapi"
|
||||
import { QuestionID } from "@/question/schema"
|
||||
import { Question } from "../../question"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import z from "zod"
|
||||
import { errors } from "../error"
|
||||
import { lazy } from "../../util/lazy"
|
||||
@@ -27,7 +28,7 @@ export const QuestionRoutes = lazy(() =>
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
const questions = await Question.list()
|
||||
const questions = await AppRuntime.runPromise(Question.Service.use((svc) => svc.list()))
|
||||
return c.json(questions)
|
||||
},
|
||||
)
|
||||
@@ -59,10 +60,14 @@ export const QuestionRoutes = lazy(() =>
|
||||
async (c) => {
|
||||
const params = c.req.valid("param")
|
||||
const json = c.req.valid("json")
|
||||
await Question.reply({
|
||||
requestID: params.requestID,
|
||||
answers: json.answers,
|
||||
})
|
||||
await AppRuntime.runPromise(
|
||||
Question.Service.use((svc) =>
|
||||
svc.reply({
|
||||
requestID: params.requestID,
|
||||
answers: json.answers,
|
||||
}),
|
||||
),
|
||||
)
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
@@ -92,7 +97,7 @@ export const QuestionRoutes = lazy(() =>
|
||||
),
|
||||
async (c) => {
|
||||
const params = c.req.valid("param")
|
||||
await Question.reject(params.requestID)
|
||||
await AppRuntime.runPromise(Question.Service.use((svc) => svc.reject(params.requestID)))
|
||||
return c.json(true)
|
||||
},
|
||||
),
|
||||
@@ -13,6 +13,7 @@ import { SessionShare } from "@/share/session"
|
||||
import { SessionStatus } from "@/session/status"
|
||||
import { SessionSummary } from "@/session/summary"
|
||||
import { Todo } from "../../session/todo"
|
||||
import { Effect } from "effect"
|
||||
import { AppRuntime } from "../../effect/app-runtime"
|
||||
import { Agent } from "../../agent/agent"
|
||||
import { Snapshot } from "@/snapshot"
|
||||
@@ -94,7 +95,7 @@ export const SessionRoutes = lazy(() =>
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
const result = await SessionStatus.list()
|
||||
const result = await AppRuntime.runPromise(SessionStatus.Service.use((svc) => svc.list()))
|
||||
return c.json(Object.fromEntries(result))
|
||||
},
|
||||
)
|
||||
@@ -273,6 +274,7 @@ export const SessionRoutes = lazy(() =>
|
||||
"json",
|
||||
z.object({
|
||||
title: z.string().optional(),
|
||||
permission: Permission.Ruleset.optional(),
|
||||
time: z
|
||||
.object({
|
||||
archived: z.number().optional(),
|
||||
@@ -283,10 +285,17 @@ export const SessionRoutes = lazy(() =>
|
||||
async (c) => {
|
||||
const sessionID = c.req.valid("param").sessionID
|
||||
const updates = c.req.valid("json")
|
||||
const current = await Session.get(sessionID)
|
||||
|
||||
if (updates.title !== undefined) {
|
||||
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) {
|
||||
await Session.setArchived({ sessionID, time: updates.time.archived })
|
||||
}
|
||||
@@ -716,11 +725,17 @@ export const SessionRoutes = lazy(() =>
|
||||
),
|
||||
async (c) => {
|
||||
const params = c.req.valid("param")
|
||||
await SessionRunState.assertNotBusy(params.sessionID)
|
||||
await Session.removeMessage({
|
||||
sessionID: params.sessionID,
|
||||
messageID: params.messageID,
|
||||
})
|
||||
await AppRuntime.runPromise(
|
||||
Effect.gen(function* () {
|
||||
const state = yield* SessionRunState.Service
|
||||
const session = yield* Session.Service
|
||||
yield* state.assertNotBusy(params.sessionID)
|
||||
yield* session.removeMessage({
|
||||
sessionID: params.sessionID,
|
||||
messageID: params.messageID,
|
||||
})
|
||||
}),
|
||||
)
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
@@ -3,31 +3,90 @@ import { NamedError } from "@opencode-ai/util/error"
|
||||
import { NotFoundError } from "../storage/db"
|
||||
import { Session } from "../session"
|
||||
import type { ContentfulStatusCode } from "hono/utils/http-status"
|
||||
import type { ErrorHandler } from "hono"
|
||||
import type { ErrorHandler, MiddlewareHandler } from "hono"
|
||||
import { HTTPException } from "hono/http-exception"
|
||||
import type { Log } from "../util/log"
|
||||
import { 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"
|
||||
|
||||
export function errorHandler(log: Log.Logger): ErrorHandler {
|
||||
return (err, c) => {
|
||||
log.error("failed", {
|
||||
error: err,
|
||||
})
|
||||
if (err instanceof NamedError) {
|
||||
let status: ContentfulStatusCode
|
||||
if (err instanceof NotFoundError) status = 404
|
||||
else if (err instanceof Provider.ModelNotFoundError) status = 400
|
||||
else if (err.name === "ProviderAuthValidationFailed") status = 400
|
||||
else if (err.name.startsWith("Worktree")) status = 400
|
||||
else status = 500
|
||||
return c.json(err.toObject(), { status })
|
||||
}
|
||||
if (err instanceof Session.BusyError) {
|
||||
return c.json(new NamedError.Unknown({ message: err.message }).toObject(), { status: 400 })
|
||||
}
|
||||
if (err instanceof HTTPException) return err.getResponse()
|
||||
const message = err instanceof Error && err.stack ? err.stack : err.toString()
|
||||
return c.json(new NamedError.Unknown({ message }).toObject(), {
|
||||
status: 500,
|
||||
const log = Log.create({ service: "server" })
|
||||
|
||||
export const ErrorMiddleware: ErrorHandler = (err, c) => {
|
||||
log.error("failed", {
|
||||
error: err,
|
||||
})
|
||||
if (err instanceof NamedError) {
|
||||
let status: ContentfulStatusCode
|
||||
if (err instanceof NotFoundError) status = 404
|
||||
else if (err instanceof Provider.ModelNotFoundError) status = 400
|
||||
else if (err.name === "ProviderAuthValidationFailed") status = 400
|
||||
else if (err.name.startsWith("Worktree")) status = 400
|
||||
else status = 500
|
||||
return c.json(err.toObject(), { status })
|
||||
}
|
||||
if (err instanceof Session.BusyError) {
|
||||
return c.json(new NamedError.Unknown({ message: err.message }).toObject(), { status: 400 })
|
||||
}
|
||||
if (err instanceof HTTPException) return err.getResponse()
|
||||
const message = err instanceof Error && err.stack ? err.stack : err.toString()
|
||||
return c.json(new NamedError.Unknown({ message }).toObject(), {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -1,24 +1,14 @@
|
||||
import { Log } from "../util/log"
|
||||
import { describeRoute, generateSpecs, validator, resolver, openAPIRouteHandler } from "hono-openapi"
|
||||
import { generateSpecs } from "hono-openapi"
|
||||
import { Hono } 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 { adapter } from "#hono"
|
||||
import { MDNS } from "./mdns"
|
||||
import { lazy } from "@/util/lazy"
|
||||
import { errorHandler } from "./middleware"
|
||||
import { AuthMiddleware, CompressionMiddleware, CorsMiddleware, ErrorMiddleware, LoggerMiddleware } from "./middleware"
|
||||
import { InstanceRoutes } from "./instance"
|
||||
import { initProjectors } from "./projectors"
|
||||
import { createAdaptorServer, type ServerType } from "@hono/node-server"
|
||||
import { Log } from "@/util/log"
|
||||
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
|
||||
globalThis.AI_SDK_LOG_WARNINGS = false
|
||||
@@ -26,6 +16,8 @@ globalThis.AI_SDK_LOG_WARNINGS = false
|
||||
initProjectors()
|
||||
|
||||
export namespace Server {
|
||||
const log = Log.create({ service: "server" })
|
||||
|
||||
export type Listener = {
|
||||
hostname: string
|
||||
port: number
|
||||
@@ -33,231 +25,31 @@ export namespace Server {
|
||||
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 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[] }) {
|
||||
const app = new Hono()
|
||||
const ws = createNodeWebSocket({ app })
|
||||
const runtime = adapter.create(app)
|
||||
return {
|
||||
app: ControlPlaneRoutes(ws.upgradeWebSocket, app, opts),
|
||||
ws,
|
||||
app: app
|
||||
.onError(ErrorMiddleware)
|
||||
.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() {
|
||||
// Build a fresh app with all routes registered directly so
|
||||
// hono-openapi can see describeRoute metadata (`.route()` wraps
|
||||
// handlers when the sub-app has a custom errorHandler, which
|
||||
// strips the metadata symbol).
|
||||
const { app, ws } = create({})
|
||||
InstanceRoutes(ws.upgradeWebSocket, app)
|
||||
const { app } = create({})
|
||||
const result = await generateSpecs(app, {
|
||||
documentation: {
|
||||
info: {
|
||||
@@ -281,46 +73,21 @@ export namespace Server {
|
||||
cors?: string[]
|
||||
}): Promise<Listener> {
|
||||
const built = create(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 server = await built.runtime.listen(opts)
|
||||
|
||||
const next = new URL("http://localhost")
|
||||
next.hostname = opts.hostname
|
||||
next.port = String(addr.port)
|
||||
next.port = String(server.port)
|
||||
url = next
|
||||
|
||||
const mdns =
|
||||
opts.mdns &&
|
||||
addr.port &&
|
||||
server.port &&
|
||||
opts.hostname !== "127.0.0.1" &&
|
||||
opts.hostname !== "localhost" &&
|
||||
opts.hostname !== "::1"
|
||||
if (mdns) {
|
||||
MDNS.publish(addr.port, opts.mdnsDomain)
|
||||
MDNS.publish(server.port, opts.mdnsDomain)
|
||||
} else if (opts.mdns) {
|
||||
log.warn("mDNS enabled but hostname is loopback; skipping mDNS publish")
|
||||
}
|
||||
@@ -328,27 +95,13 @@ export namespace Server {
|
||||
let closing: Promise<void> | undefined
|
||||
return {
|
||||
hostname: opts.hostname,
|
||||
port: addr.port,
|
||||
port: server.port,
|
||||
url: next,
|
||||
stop(close?: boolean) {
|
||||
closing ??= new Promise((resolve, reject) => {
|
||||
closing ??= (async () => {
|
||||
if (mdns) MDNS.unpublish()
|
||||
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()
|
||||
}
|
||||
}
|
||||
})
|
||||
await server.stop(close)
|
||||
})()
|
||||
return closing
|
||||
},
|
||||
}
|
||||
|
||||
55
packages/opencode/src/server/ui/index.ts
Normal file
55
packages/opencode/src/server/ui/index.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
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
|
||||
}
|
||||
})
|
||||
@@ -15,7 +15,7 @@ import { Plugin } from "@/plugin"
|
||||
import { Config } from "@/config/config"
|
||||
import { NotFoundError } from "@/storage/db"
|
||||
import { ModelID, ProviderID } from "@/provider/schema"
|
||||
import { Effect, Layer, ServiceMap } from "effect"
|
||||
import { Effect, Layer, Context } from "effect"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { isOverflow as overflow } from "./overflow"
|
||||
@@ -58,7 +58,7 @@ export namespace SessionCompaction {
|
||||
}) => Effect.Effect<void>
|
||||
}
|
||||
|
||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/SessionCompaction") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/SessionCompaction") {}
|
||||
|
||||
export const layer: Layer.Layer<
|
||||
Service,
|
||||
|
||||
@@ -29,7 +29,7 @@ import type { Provider } from "@/provider/provider"
|
||||
import { Permission } from "@/permission"
|
||||
import { Global } from "@/global"
|
||||
import type { LanguageModelV2Usage } from "@ai-sdk/provider"
|
||||
import { Effect, Layer, ServiceMap } from "effect"
|
||||
import { Effect, Layer, Option, Context } from "effect"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
|
||||
export namespace Session {
|
||||
@@ -352,9 +352,14 @@ export namespace Session {
|
||||
field: string
|
||||
delta: string
|
||||
}) => 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 ServiceMap.Service<Service, Interface>()("@opencode/Session") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Session") {}
|
||||
|
||||
type Patch = z.infer<typeof Event.Updated.schema>["info"]
|
||||
|
||||
@@ -636,6 +641,17 @@ export namespace Session {
|
||||
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({
|
||||
create,
|
||||
fork,
|
||||
@@ -657,6 +673,7 @@ export namespace Session {
|
||||
updatePart,
|
||||
getPart,
|
||||
updatePartDelta,
|
||||
findMessage,
|
||||
})
|
||||
}),
|
||||
)
|
||||
@@ -691,6 +708,10 @@ export namespace Session {
|
||||
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(
|
||||
z.object({ sessionID: SessionID.zod, revert: Info.shape.revert, summary: Info.shape.summary }),
|
||||
(input) =>
|
||||
@@ -833,15 +854,4 @@ export namespace Session {
|
||||
MessageV2.Part.parse(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)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
import { Effect, Layer, ServiceMap } from "effect"
|
||||
import { Effect, Layer, Context } from "effect"
|
||||
import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http"
|
||||
import { Config } from "@/config/config"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
import { Flag } from "@/flag/flag"
|
||||
import { AppFileSystem } from "@/filesystem"
|
||||
import { withTransientReadRetry } from "@/util/effect-http-client"
|
||||
@@ -64,7 +63,7 @@ export namespace Instruction {
|
||||
) => Effect.Effect<{ filepath: string; content: string }[], AppFileSystem.Error>
|
||||
}
|
||||
|
||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Instruction") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Instruction") {}
|
||||
|
||||
export const layer: Layer.Layer<Service, never, AppFileSystem.Service | Config.Service | HttpClient.HttpClient> =
|
||||
Layer.effect(
|
||||
@@ -238,21 +237,7 @@ export namespace Instruction {
|
||||
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[]) {
|
||||
return extract(messages)
|
||||
}
|
||||
|
||||
export async function resolve(messages: MessageV2.WithParts[], filepath: string, messageID: MessageID) {
|
||||
return runPromise((svc) => svc.resolve(messages, filepath, messageID))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { Log } from "@/util/log"
|
||||
import { Cause, Effect, Layer, Record, ServiceMap } from "effect"
|
||||
import { Cause, Effect, Layer, Record, Context } from "effect"
|
||||
import * as Queue from "effect/Queue"
|
||||
import * as Stream from "effect/Stream"
|
||||
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>
|
||||
}
|
||||
|
||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/LLM") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/LLM") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
|
||||
@@ -15,6 +15,7 @@ import type { SystemError } from "bun"
|
||||
import type { Provider } from "@/provider/provider"
|
||||
import { ModelID, ProviderID } from "@/provider/schema"
|
||||
import { Effect } from "effect"
|
||||
import { EffectLogger } from "@/effect/logger"
|
||||
|
||||
/** Error shape thrown by Bun's fetch() when gzip/br decompression fails mid-stream */
|
||||
interface FetchDecompressionError extends Error {
|
||||
@@ -24,6 +25,8 @@ interface FetchDecompressionError extends Error {
|
||||
}
|
||||
|
||||
export namespace MessageV2 {
|
||||
export const SYNTHETIC_ATTACHMENT_PROMPT = "Attached image(s) from tool result:"
|
||||
|
||||
export function isMedia(mime: string) {
|
||||
return mime.startsWith("image/") || mime === "application/pdf"
|
||||
}
|
||||
@@ -807,7 +810,7 @@ export namespace MessageV2 {
|
||||
parts: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: "Attached image(s) from tool result:",
|
||||
text: SYNTHETIC_ATTACHMENT_PROMPT,
|
||||
},
|
||||
...media.map((attachment) => ({
|
||||
type: "file" as const,
|
||||
@@ -839,7 +842,7 @@ export namespace MessageV2 {
|
||||
model: Provider.Model,
|
||||
options?: { stripMedia?: boolean },
|
||||
): Promise<ModelMessage[]> {
|
||||
return Effect.runPromise(toModelMessagesEffect(input, model, options))
|
||||
return Effect.runPromise(toModelMessagesEffect(input, model, options).pipe(Effect.provide(EffectLogger.layer)))
|
||||
}
|
||||
|
||||
export function page(input: { sessionID: SessionID; limit: number; before?: string }) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Cause, Deferred, Effect, Layer, ServiceMap } from "effect"
|
||||
import { Cause, Deferred, Effect, Layer, Context } from "effect"
|
||||
import * as Stream from "effect/Stream"
|
||||
import { Agent } from "@/agent/agent"
|
||||
import { Bus } from "@/bus"
|
||||
@@ -76,7 +76,7 @@ export namespace SessionProcessor {
|
||||
|
||||
type StreamEvent = Event
|
||||
|
||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/SessionProcessor") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/SessionProcessor") {}
|
||||
|
||||
export const layer: Layer.Layer<
|
||||
Service,
|
||||
@@ -123,14 +123,6 @@ export namespace SessionProcessor {
|
||||
let aborted = false
|
||||
const slog = log.with({ sessionID: input.sessionID, messageID: input.assistantMessage.id })
|
||||
|
||||
yield* Effect.annotateCurrentSpan({
|
||||
sessionID: input.sessionID,
|
||||
messageID: input.assistantMessage.id,
|
||||
agent: input.assistantMessage.agent,
|
||||
providerID: input.model.providerID,
|
||||
modelID: input.model.id,
|
||||
})
|
||||
|
||||
const parse = (e: unknown) =>
|
||||
MessageV2.fromError(e, {
|
||||
providerID: input.model.providerID,
|
||||
@@ -539,18 +531,7 @@ export namespace SessionProcessor {
|
||||
})
|
||||
|
||||
const process = Effect.fn("SessionProcessor.process")(function* (streamInput: LLM.StreamInput) {
|
||||
yield* Effect.annotateCurrentSpan({
|
||||
sessionID: ctx.sessionID,
|
||||
messageID: ctx.assistantMessage.id,
|
||||
agent: ctx.assistantMessage.agent,
|
||||
providerID: ctx.model.providerID,
|
||||
modelID: ctx.model.id,
|
||||
})
|
||||
yield* slog.info("process", {
|
||||
agent: ctx.assistantMessage.agent,
|
||||
providerID: ctx.model.providerID,
|
||||
modelID: ctx.model.id,
|
||||
})
|
||||
yield* slog.info("process")
|
||||
ctx.needsCompaction = false
|
||||
ctx.shouldBreak = (yield* config.get()).experimental?.continue_loop_on_deny !== true
|
||||
|
||||
@@ -564,17 +545,6 @@ export namespace SessionProcessor {
|
||||
Stream.tap((event) => handleEvent(event)),
|
||||
Stream.takeUntil(() => ctx.needsCompaction),
|
||||
Stream.runDrain,
|
||||
Effect.withSpan(
|
||||
"SessionProcessor.stream",
|
||||
{
|
||||
attributes: {
|
||||
sessionID: ctx.sessionID,
|
||||
messageID: ctx.assistantMessage.id,
|
||||
agent: ctx.assistantMessage.agent,
|
||||
},
|
||||
},
|
||||
{ captureStackTrace: false },
|
||||
),
|
||||
)
|
||||
}).pipe(
|
||||
Effect.onInterrupt(() =>
|
||||
@@ -605,19 +575,8 @@ export namespace SessionProcessor {
|
||||
Effect.ensuring(cleanup()),
|
||||
)
|
||||
|
||||
if (ctx.needsCompaction) {
|
||||
yield* slog.warn("compact", { finish: ctx.assistantMessage.finish, blocked: ctx.blocked })
|
||||
return "compact"
|
||||
}
|
||||
if (ctx.blocked || ctx.assistantMessage.error) {
|
||||
yield* slog.warn("stop", {
|
||||
blocked: ctx.blocked,
|
||||
finish: ctx.assistantMessage.finish,
|
||||
hasError: !!ctx.assistantMessage.error,
|
||||
})
|
||||
return "stop"
|
||||
}
|
||||
yield* slog.info("continue", { finish: ctx.assistantMessage.finish })
|
||||
if (ctx.needsCompaction) return "compact"
|
||||
if (ctx.blocked || ctx.assistantMessage.error) return "stop"
|
||||
return "continue"
|
||||
})
|
||||
})
|
||||
|
||||
@@ -43,7 +43,7 @@ import { AppFileSystem } from "@/filesystem"
|
||||
import { Truncate } from "@/tool/truncate"
|
||||
import { decodeDataUrl } from "@/util/data-url"
|
||||
import { Process } from "@/util/process"
|
||||
import { Cause, Effect, Exit, Layer, Option, Scope, ServiceMap } from "effect"
|
||||
import { Cause, Effect, Exit, Layer, Option, Scope, Context } from "effect"
|
||||
import { EffectLogger } from "@/effect/logger"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
@@ -76,7 +76,7 @@ export namespace SessionPrompt {
|
||||
readonly resolvePromptParts: (template: string) => Effect.Effect<PromptInput["parts"]>
|
||||
}
|
||||
|
||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/SessionPrompt") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/SessionPrompt") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
@@ -102,6 +102,14 @@ export namespace SessionPrompt {
|
||||
const instruction = yield* Instruction.Service
|
||||
const state = yield* SessionRunState.Service
|
||||
const revert = yield* SessionRevert.Service
|
||||
const sys = yield* SystemPrompt.Service
|
||||
const llm = yield* LLM.Service
|
||||
|
||||
const run = {
|
||||
promise: <A, E>(effect: Effect.Effect<A, E>) =>
|
||||
Effect.runPromise(effect.pipe(Effect.provide(EffectLogger.layer))),
|
||||
fork: <A, E>(effect: Effect.Effect<A, E>) => Effect.runFork(effect.pipe(Effect.provide(EffectLogger.layer))),
|
||||
}
|
||||
|
||||
const cancel = Effect.fn("SessionPrompt.cancel")(function* (sessionID: SessionID) {
|
||||
yield* elog.info("cancel", { sessionID })
|
||||
@@ -174,21 +182,24 @@ export namespace SessionPrompt {
|
||||
const msgs = onlySubtasks
|
||||
? [{ role: "user" as const, content: subtasks.map((p) => p.prompt).join("\n") }]
|
||||
: yield* MessageV2.toModelMessagesEffect(context, mdl)
|
||||
const text = yield* Effect.promise(async (signal) => {
|
||||
const result = await LLM.stream({
|
||||
const text = yield* llm
|
||||
.stream({
|
||||
agent: ag,
|
||||
user: firstInfo,
|
||||
system: [],
|
||||
small: true,
|
||||
tools: {},
|
||||
model: mdl,
|
||||
abort: signal,
|
||||
sessionID: input.session.id,
|
||||
retries: 2,
|
||||
messages: [{ role: "user", content: "Generate a title for this conversation:\n" }, ...msgs],
|
||||
})
|
||||
return result.text
|
||||
})
|
||||
.pipe(
|
||||
Stream.filter((e): e is Extract<LLM.Event, { type: "text-delta" }> => e.type === "text-delta"),
|
||||
Stream.map((e) => e.text),
|
||||
Stream.mkString,
|
||||
Effect.orDie,
|
||||
)
|
||||
const cleaned = text
|
||||
.replace(/<think>[\s\S]*?<\/think>\s*/g, "")
|
||||
.split("\n")
|
||||
@@ -358,30 +369,28 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
agent: input.agent.name,
|
||||
messages: input.messages,
|
||||
metadata: (val) =>
|
||||
Effect.runPromise(
|
||||
input.processor.updateToolCall(options.toolCallId, (match) => {
|
||||
if (!["running", "pending"].includes(match.state.status)) return match
|
||||
return {
|
||||
...match,
|
||||
state: {
|
||||
title: val.title,
|
||||
metadata: val.metadata,
|
||||
status: "running",
|
||||
input: args,
|
||||
time: { start: Date.now() },
|
||||
},
|
||||
}
|
||||
}),
|
||||
),
|
||||
input.processor.updateToolCall(options.toolCallId, (match) => {
|
||||
if (!["running", "pending"].includes(match.state.status)) return match
|
||||
return {
|
||||
...match,
|
||||
state: {
|
||||
title: val.title,
|
||||
metadata: val.metadata,
|
||||
status: "running",
|
||||
input: args,
|
||||
time: { start: Date.now() },
|
||||
},
|
||||
}
|
||||
}),
|
||||
ask: (req) =>
|
||||
Effect.runPromise(
|
||||
permission.ask({
|
||||
permission
|
||||
.ask({
|
||||
...req,
|
||||
sessionID: input.session.id,
|
||||
tool: { messageID: input.processor.message.id, callID: options.toolCallId },
|
||||
ruleset: Permission.merge(input.agent.permission, input.session.permission ?? []),
|
||||
}),
|
||||
),
|
||||
})
|
||||
.pipe(Effect.orDie),
|
||||
})
|
||||
|
||||
for (const item of yield* registry.tools({
|
||||
@@ -395,26 +404,15 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
description: item.description,
|
||||
inputSchema: jsonSchema(schema as any),
|
||||
execute(args, options) {
|
||||
return Effect.runPromise(
|
||||
return run.promise(
|
||||
Effect.gen(function* () {
|
||||
const ctx = context(args, options)
|
||||
yield* Effect.annotateCurrentSpan({
|
||||
tool: item.id,
|
||||
sessionID: ctx.sessionID,
|
||||
messageID: input.processor.message.id,
|
||||
callID: ctx.callID,
|
||||
})
|
||||
yield* elog.info("tool.start", {
|
||||
tool: item.id,
|
||||
sessionID: ctx.sessionID,
|
||||
callID: ctx.callID,
|
||||
})
|
||||
yield* plugin.trigger(
|
||||
"tool.execute.before",
|
||||
{ tool: item.id, sessionID: ctx.sessionID, callID: ctx.callID },
|
||||
{ args },
|
||||
)
|
||||
const result = yield* Effect.promise(() => item.execute(args, ctx))
|
||||
const result = yield* item.execute(args, ctx)
|
||||
const output = {
|
||||
...result,
|
||||
attachments: result.attachments?.map((attachment) => ({
|
||||
@@ -432,27 +430,8 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
if (options.abortSignal?.aborted) {
|
||||
yield* input.processor.completeToolCall(options.toolCallId, output)
|
||||
}
|
||||
yield* elog.info("tool.done", {
|
||||
tool: item.id,
|
||||
sessionID: ctx.sessionID,
|
||||
callID: ctx.callID,
|
||||
truncated: output.metadata.truncated,
|
||||
})
|
||||
return output
|
||||
}).pipe(
|
||||
Effect.withSpan(
|
||||
`Tool.${item.id}`,
|
||||
{
|
||||
attributes: {
|
||||
tool: item.id,
|
||||
sessionID: input.session.id,
|
||||
messageID: input.processor.message.id,
|
||||
callID: options.toolCallId,
|
||||
},
|
||||
},
|
||||
{ captureStackTrace: false },
|
||||
),
|
||||
),
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -466,22 +445,15 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
const transformed = ProviderTransform.schema(input.model, schema)
|
||||
item.inputSchema = jsonSchema(transformed)
|
||||
item.execute = (args, opts) =>
|
||||
Effect.runPromise(
|
||||
run.promise(
|
||||
Effect.gen(function* () {
|
||||
const ctx = context(args, opts)
|
||||
yield* Effect.annotateCurrentSpan({
|
||||
tool: key,
|
||||
sessionID: ctx.sessionID,
|
||||
messageID: input.processor.message.id,
|
||||
callID: ctx.callID,
|
||||
})
|
||||
yield* elog.info("tool.start", { tool: key, sessionID: ctx.sessionID, callID: ctx.callID })
|
||||
yield* plugin.trigger(
|
||||
"tool.execute.before",
|
||||
{ tool: key, sessionID: ctx.sessionID, callID: opts.toolCallId },
|
||||
{ args },
|
||||
)
|
||||
yield* Effect.promise(() => ctx.ask({ permission: key, metadata: {}, patterns: ["*"], always: ["*"] }))
|
||||
yield* ctx.ask({ permission: key, metadata: {}, patterns: ["*"], always: ["*"] })
|
||||
const result: Awaited<ReturnType<NonNullable<typeof execute>>> = yield* Effect.promise(() =>
|
||||
execute(args, opts),
|
||||
)
|
||||
@@ -537,27 +509,8 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
if (opts.abortSignal?.aborted) {
|
||||
yield* input.processor.completeToolCall(opts.toolCallId, output)
|
||||
}
|
||||
yield* elog.info("tool.done", {
|
||||
tool: key,
|
||||
sessionID: ctx.sessionID,
|
||||
callID: ctx.callID,
|
||||
truncated: output.metadata.truncated,
|
||||
})
|
||||
return output
|
||||
}).pipe(
|
||||
Effect.withSpan(
|
||||
`Tool.${key}`,
|
||||
{
|
||||
attributes: {
|
||||
tool: key,
|
||||
sessionID: input.session.id,
|
||||
messageID: input.processor.message.id,
|
||||
callID: opts.toolCallId,
|
||||
},
|
||||
},
|
||||
{ captureStackTrace: false },
|
||||
),
|
||||
),
|
||||
}),
|
||||
)
|
||||
tools[key] = item
|
||||
}
|
||||
@@ -632,63 +585,61 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
}
|
||||
|
||||
let error: Error | undefined
|
||||
const result = yield* Effect.promise((signal) =>
|
||||
taskTool
|
||||
.execute(taskArgs, {
|
||||
agent: task.agent,
|
||||
messageID: assistantMessage.id,
|
||||
sessionID,
|
||||
abort: signal,
|
||||
callID: part.callID,
|
||||
extra: { bypassAgentCheck: true, promptOps },
|
||||
messages: msgs,
|
||||
metadata(val: { title?: string; metadata?: Record<string, any> }) {
|
||||
return Effect.runPromise(
|
||||
Effect.gen(function* () {
|
||||
part = yield* sessions.updatePart({
|
||||
...part,
|
||||
type: "tool",
|
||||
state: { ...part.state, ...val },
|
||||
} satisfies MessageV2.ToolPart)
|
||||
}),
|
||||
)
|
||||
},
|
||||
ask(req: any) {
|
||||
return Effect.runPromise(
|
||||
permission.ask({
|
||||
...req,
|
||||
sessionID,
|
||||
ruleset: Permission.merge(taskAgent.permission, session.permission ?? []),
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
.catch((e) => {
|
||||
error = e instanceof Error ? e : new Error(String(e))
|
||||
log.error("subtask execution failed", { error, agent: task.agent, description: task.description })
|
||||
return undefined
|
||||
}),
|
||||
).pipe(
|
||||
Effect.onInterrupt(() =>
|
||||
Effect.gen(function* () {
|
||||
assistantMessage.finish = "tool-calls"
|
||||
assistantMessage.time.completed = Date.now()
|
||||
yield* sessions.updateMessage(assistantMessage)
|
||||
if (part.state.status === "running") {
|
||||
yield* sessions.updatePart({
|
||||
const taskAbort = new AbortController()
|
||||
const result = yield* taskTool
|
||||
.execute(taskArgs, {
|
||||
agent: task.agent,
|
||||
messageID: assistantMessage.id,
|
||||
sessionID,
|
||||
abort: taskAbort.signal,
|
||||
callID: part.callID,
|
||||
extra: { bypassAgentCheck: true, promptOps },
|
||||
messages: msgs,
|
||||
metadata: (val: { title?: string; metadata?: Record<string, any> }) =>
|
||||
Effect.gen(function* () {
|
||||
part = yield* sessions.updatePart({
|
||||
...part,
|
||||
state: {
|
||||
status: "error",
|
||||
error: "Cancelled",
|
||||
time: { start: part.state.time.start, end: Date.now() },
|
||||
metadata: part.state.metadata,
|
||||
input: part.state.input,
|
||||
},
|
||||
type: "tool",
|
||||
state: { ...part.state, ...val },
|
||||
} satisfies MessageV2.ToolPart)
|
||||
}
|
||||
}),
|
||||
ask: (req: any) =>
|
||||
permission
|
||||
.ask({
|
||||
...req,
|
||||
sessionID,
|
||||
ruleset: Permission.merge(taskAgent.permission, session.permission ?? []),
|
||||
})
|
||||
.pipe(Effect.orDie),
|
||||
})
|
||||
.pipe(
|
||||
Effect.catchCause((cause) => {
|
||||
const defect = Cause.squash(cause)
|
||||
error = defect instanceof Error ? defect : new Error(String(defect))
|
||||
log.error("subtask execution failed", { error, agent: task.agent, description: task.description })
|
||||
return Effect.void
|
||||
}),
|
||||
),
|
||||
)
|
||||
Effect.onInterrupt(() =>
|
||||
Effect.gen(function* () {
|
||||
taskAbort.abort()
|
||||
assistantMessage.finish = "tool-calls"
|
||||
assistantMessage.time.completed = Date.now()
|
||||
yield* sessions.updateMessage(assistantMessage)
|
||||
if (part.state.status === "running") {
|
||||
yield* sessions.updatePart({
|
||||
...part,
|
||||
state: {
|
||||
status: "error",
|
||||
error: "Cancelled",
|
||||
time: { start: part.state.time.start, end: Date.now() },
|
||||
metadata: part.state.metadata,
|
||||
input: part.state.input,
|
||||
},
|
||||
} satisfies MessageV2.ToolPart)
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
const attachments = result?.attachments?.map((attachment) => ({
|
||||
...attachment,
|
||||
@@ -911,7 +862,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
output += chunk
|
||||
if (part.state.status === "running") {
|
||||
part.state.metadata = { output, description: "" }
|
||||
void Effect.runFork(sessions.updatePart(part))
|
||||
void run.fork(sessions.updatePart(part))
|
||||
}
|
||||
}),
|
||||
)
|
||||
@@ -956,12 +907,8 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
})
|
||||
|
||||
const lastModel = Effect.fnUntraced(function* (sessionID: SessionID) {
|
||||
const model = yield* Effect.promise(async () => {
|
||||
for await (const item of MessageV2.stream(sessionID)) {
|
||||
if (item.info.role === "user" && item.info.model) return item.info.model
|
||||
}
|
||||
})
|
||||
if (model) return model
|
||||
const match = yield* sessions.findMessage(sessionID, (m) => m.info.role === "user" && !!m.info.model)
|
||||
if (Option.isSome(match) && match.value.info.role === "user") return match.value.info.model
|
||||
return yield* provider.defaultModel()
|
||||
})
|
||||
|
||||
@@ -1093,19 +1040,21 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
if (yield* fsys.isDir(filepath)) part.mime = "application/x-directory"
|
||||
|
||||
const { read } = yield* registry.named()
|
||||
const execRead = (args: Parameters<typeof read.execute>[0], extra?: Tool.Context["extra"]) =>
|
||||
Effect.promise((signal: AbortSignal) =>
|
||||
read.execute(args, {
|
||||
const execRead = (args: Parameters<typeof read.execute>[0], extra?: Tool.Context["extra"]) => {
|
||||
const controller = new AbortController()
|
||||
return read
|
||||
.execute(args, {
|
||||
sessionID: input.sessionID,
|
||||
abort: signal,
|
||||
abort: controller.signal,
|
||||
agent: input.agent!,
|
||||
messageID: info.id,
|
||||
extra: { bypassCwdCheck: true, ...extra },
|
||||
messages: [],
|
||||
metadata: async () => {},
|
||||
ask: async () => {},
|
||||
}),
|
||||
)
|
||||
metadata: () => Effect.void,
|
||||
ask: () => Effect.void,
|
||||
})
|
||||
.pipe(Effect.onInterrupt(() => Effect.sync(() => controller.abort())))
|
||||
}
|
||||
|
||||
if (part.mime === "text/plain") {
|
||||
let offset: number | undefined
|
||||
@@ -1342,16 +1291,13 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
},
|
||||
)
|
||||
|
||||
const lastAssistant = (sessionID: SessionID) =>
|
||||
Effect.promise(async () => {
|
||||
let latest: MessageV2.WithParts | undefined
|
||||
for await (const item of MessageV2.stream(sessionID)) {
|
||||
latest ??= item
|
||||
if (item.info.role !== "user") return item
|
||||
}
|
||||
if (latest) return latest
|
||||
throw new Error("Impossible")
|
||||
})
|
||||
const lastAssistant = Effect.fnUntraced(function* (sessionID: SessionID) {
|
||||
const match = yield* sessions.findMessage(sessionID, (m) => m.info.role !== "user")
|
||||
if (Option.isSome(match)) return match.value
|
||||
const msgs = yield* sessions.messages({ sessionID, limit: 1 })
|
||||
if (msgs.length > 0) return msgs[0]
|
||||
throw new Error("Impossible")
|
||||
})
|
||||
|
||||
const runLoop: (sessionID: SessionID) => Effect.Effect<MessageV2.WithParts> = Effect.fn("SessionPrompt.run")(
|
||||
function* (sessionID: SessionID) {
|
||||
@@ -1383,14 +1329,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
|
||||
if (!lastUser) throw new Error("No user message found in stream. This should never happen.")
|
||||
|
||||
yield* Effect.annotateCurrentSpan({
|
||||
sessionID,
|
||||
step,
|
||||
agent: lastUser.agent,
|
||||
providerID: lastUser.model.providerID,
|
||||
modelID: lastUser.model.modelID,
|
||||
})
|
||||
|
||||
const lastAssistantMsg = msgs.findLast(
|
||||
(msg) => msg.info.role === "assistant" && msg.info.id === lastAssistant?.id,
|
||||
)
|
||||
@@ -1412,12 +1350,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
}
|
||||
|
||||
step++
|
||||
yield* slog.info("step", {
|
||||
step,
|
||||
agent: lastUser.agent,
|
||||
providerID: lastUser.model.providerID,
|
||||
modelID: lastUser.model.modelID,
|
||||
})
|
||||
if (step === 1)
|
||||
yield* title({
|
||||
session,
|
||||
@@ -1435,7 +1367,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
}
|
||||
|
||||
if (task?.type === "compaction") {
|
||||
yield* slog.warn("compaction", { step, auto: task.auto, overflow: task.overflow })
|
||||
const result = yield* compaction.process({
|
||||
messages: msgs,
|
||||
parentID: lastUser.id,
|
||||
@@ -1536,25 +1467,11 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
yield* plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs })
|
||||
|
||||
const [skills, env, instructions, modelMsgs] = yield* Effect.all([
|
||||
Effect.promise(() => SystemPrompt.skills(agent)),
|
||||
Effect.promise(() => SystemPrompt.environment(model)),
|
||||
sys.skills(agent),
|
||||
Effect.sync(() => sys.environment(model)),
|
||||
instruction.system().pipe(Effect.orDie),
|
||||
MessageV2.toModelMessagesEffect(msgs, model),
|
||||
]).pipe(
|
||||
Effect.withSpan(
|
||||
"SessionPrompt.prepareInput",
|
||||
{
|
||||
attributes: {
|
||||
sessionID,
|
||||
step,
|
||||
agent: agent.name,
|
||||
providerID: model.providerID,
|
||||
modelID: model.id,
|
||||
},
|
||||
},
|
||||
{ captureStackTrace: false },
|
||||
),
|
||||
)
|
||||
])
|
||||
const system = [...env, ...(skills ? [skills] : []), ...instructions]
|
||||
const format = lastUser.format ?? { type: "text" as const }
|
||||
if (format.type === "json_schema") system.push(STRUCTURED_OUTPUT_SYSTEM_PROMPT)
|
||||
@@ -1740,9 +1657,9 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
})
|
||||
|
||||
const promptOps: TaskPromptOps = {
|
||||
cancel: (sessionID) => Effect.runFork(cancel(sessionID)),
|
||||
resolvePromptParts: (template) => Effect.runPromise(resolvePromptParts(template)),
|
||||
prompt: (input) => Effect.runPromise(prompt(input)),
|
||||
cancel: (sessionID) => run.fork(cancel(sessionID)),
|
||||
resolvePromptParts: (template) => resolvePromptParts(template),
|
||||
prompt: (input) => prompt(input),
|
||||
}
|
||||
|
||||
return Service.of({
|
||||
@@ -1775,9 +1692,15 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
Layer.provide(Plugin.defaultLayer),
|
||||
Layer.provide(Session.defaultLayer),
|
||||
Layer.provide(SessionRevert.defaultLayer),
|
||||
Layer.provide(Agent.defaultLayer),
|
||||
Layer.provide(Bus.layer),
|
||||
Layer.provide(CrossSpawnSpawner.defaultLayer),
|
||||
Layer.provide(
|
||||
Layer.mergeAll(
|
||||
Agent.defaultLayer,
|
||||
SystemPrompt.defaultLayer,
|
||||
LLM.defaultLayer,
|
||||
Bus.layer,
|
||||
CrossSpawnSpawner.defaultLayer,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import z from "zod"
|
||||
import { Effect, Layer, ServiceMap } from "effect"
|
||||
import { Effect, Layer, Context } from "effect"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
import { Bus } from "../bus"
|
||||
import { Snapshot } from "../snapshot"
|
||||
@@ -29,7 +29,7 @@ export namespace SessionRevert {
|
||||
readonly cleanup: (session: Session.Info) => Effect.Effect<void>
|
||||
}
|
||||
|
||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/SessionRevert") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/SessionRevert") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { Runner } from "@/effect/runner"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
import { Effect, Layer, Scope, ServiceMap } from "effect"
|
||||
import { Effect, Layer, Scope, Context } from "effect"
|
||||
import { Session } from "."
|
||||
import { MessageV2 } from "./message-v2"
|
||||
import { SessionID } from "./schema"
|
||||
@@ -23,7 +22,7 @@ export namespace SessionRunState {
|
||||
) => Effect.Effect<MessageV2.WithParts>
|
||||
}
|
||||
|
||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/SessionRunState") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/SessionRunState") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
@@ -106,9 +105,4 @@ export namespace SessionRunState {
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(SessionStatus.defaultLayer))
|
||||
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||
|
||||
export async function assertNotBusy(sessionID: SessionID) {
|
||||
return runPromise((svc) => svc.assertNotBusy(sessionID))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,7 @@ import { withStatics } from "@/util/schema"
|
||||
export const SessionID = Schema.String.pipe(
|
||||
Schema.brand("SessionID"),
|
||||
withStatics((s) => ({
|
||||
make: (id: string) => s.makeUnsafe(id),
|
||||
descending: (id?: string) => s.makeUnsafe(Identifier.descending("session", id)),
|
||||
descending: (id?: string) => s.make(Identifier.descending("session", id)),
|
||||
zod: Identifier.schema("session").pipe(z.custom<Schema.Schema.Type<typeof s>>()),
|
||||
})),
|
||||
)
|
||||
@@ -18,8 +17,7 @@ export type SessionID = Schema.Schema.Type<typeof SessionID>
|
||||
export const MessageID = Schema.String.pipe(
|
||||
Schema.brand("MessageID"),
|
||||
withStatics((s) => ({
|
||||
make: (id: string) => s.makeUnsafe(id),
|
||||
ascending: (id?: string) => s.makeUnsafe(Identifier.ascending("message", id)),
|
||||
ascending: (id?: string) => s.make(Identifier.ascending("message", id)),
|
||||
zod: Identifier.schema("message").pipe(z.custom<Schema.Schema.Type<typeof s>>()),
|
||||
})),
|
||||
)
|
||||
@@ -29,8 +27,7 @@ export type MessageID = Schema.Schema.Type<typeof MessageID>
|
||||
export const PartID = Schema.String.pipe(
|
||||
Schema.brand("PartID"),
|
||||
withStatics((s) => ({
|
||||
make: (id: string) => s.makeUnsafe(id),
|
||||
ascending: (id?: string) => s.makeUnsafe(Identifier.ascending("part", id)),
|
||||
ascending: (id?: string) => s.make(Identifier.ascending("part", id)),
|
||||
zod: Identifier.schema("part").pipe(z.custom<Schema.Schema.Type<typeof s>>()),
|
||||
})),
|
||||
)
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { Bus } from "@/bus"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
import { SessionID } from "./schema"
|
||||
import { Effect, Layer, ServiceMap } from "effect"
|
||||
import { Effect, Layer, Context } from "effect"
|
||||
import z from "zod"
|
||||
|
||||
export namespace SessionStatus {
|
||||
@@ -50,7 +49,7 @@ export namespace SessionStatus {
|
||||
readonly set: (sessionID: SessionID, status: Info) => Effect.Effect<void>
|
||||
}
|
||||
|
||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/SessionStatus") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/SessionStatus") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
@@ -86,17 +85,4 @@ export namespace SessionStatus {
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(Bus.layer))
|
||||
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||
|
||||
export async function get(sessionID: SessionID) {
|
||||
return runPromise((svc) => svc.get(sessionID))
|
||||
}
|
||||
|
||||
export async function list() {
|
||||
return runPromise((svc) => svc.list())
|
||||
}
|
||||
|
||||
export async function set(sessionID: SessionID, status: Info) {
|
||||
return runPromise((svc) => svc.set(sessionID, status))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import z from "zod"
|
||||
import { Effect, Layer, ServiceMap } from "effect"
|
||||
import { Effect, Layer, Context } from "effect"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
import { Bus } from "@/bus"
|
||||
import { Snapshot } from "@/snapshot"
|
||||
@@ -71,7 +71,7 @@ export namespace SessionSummary {
|
||||
readonly computeDiff: (input: { messages: MessageV2.WithParts[] }) => Effect.Effect<Snapshot.FileDiff[]>
|
||||
}
|
||||
|
||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/SessionSummary") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/SessionSummary") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Ripgrep } from "../file/ripgrep"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
|
||||
import { Instance } from "../project/instance"
|
||||
|
||||
@@ -33,44 +33,52 @@ export namespace SystemPrompt {
|
||||
return [PROMPT_DEFAULT]
|
||||
}
|
||||
|
||||
export async function environment(model: Provider.Model) {
|
||||
const project = Instance.project
|
||||
return [
|
||||
[
|
||||
`You are powered by the model named ${model.api.id}. The exact model ID is ${model.providerID}/${model.api.id}`,
|
||||
`Here is some useful information about the environment you are running in:`,
|
||||
`<env>`,
|
||||
` Working directory: ${Instance.directory}`,
|
||||
` Workspace root folder: ${Instance.worktree}`,
|
||||
` Is directory a git repo: ${project.vcs === "git" ? "yes" : "no"}`,
|
||||
` Platform: ${process.platform}`,
|
||||
` Today's date: ${new Date().toDateString()}`,
|
||||
`</env>`,
|
||||
`<directories>`,
|
||||
` ${
|
||||
project.vcs === "git" && false
|
||||
? await Ripgrep.tree({
|
||||
cwd: Instance.directory,
|
||||
limit: 50,
|
||||
})
|
||||
: ""
|
||||
}`,
|
||||
`</directories>`,
|
||||
].join("\n"),
|
||||
]
|
||||
export interface Interface {
|
||||
readonly environment: (model: Provider.Model) => string[]
|
||||
readonly skills: (agent: Agent.Info) => Effect.Effect<string | undefined>
|
||||
}
|
||||
|
||||
export async function skills(agent: Agent.Info) {
|
||||
if (Permission.disabled(["skill"], agent.permission).has("skill")) return
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/SystemPrompt") {}
|
||||
|
||||
const list = await Skill.available(agent)
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const skill = yield* Skill.Service
|
||||
|
||||
return [
|
||||
"Skills provide specialized instructions and workflows for specific tasks.",
|
||||
"Use the skill tool to load a skill when a task matches its description.",
|
||||
// the agents seem to ingest the information about skills a bit better if we present a more verbose
|
||||
// version of them here and a less verbose version in tool description, rather than vice versa.
|
||||
Skill.fmt(list, { verbose: true }),
|
||||
].join("\n")
|
||||
}
|
||||
return Service.of({
|
||||
environment(model) {
|
||||
const project = Instance.project
|
||||
return [
|
||||
[
|
||||
`You are powered by the model named ${model.api.id}. The exact model ID is ${model.providerID}/${model.api.id}`,
|
||||
`Here is some useful information about the environment you are running in:`,
|
||||
`<env>`,
|
||||
` Working directory: ${Instance.directory}`,
|
||||
` Workspace root folder: ${Instance.worktree}`,
|
||||
` Is directory a git repo: ${project.vcs === "git" ? "yes" : "no"}`,
|
||||
` Platform: ${process.platform}`,
|
||||
` Today's date: ${new Date().toDateString()}`,
|
||||
`</env>`,
|
||||
].join("\n"),
|
||||
]
|
||||
},
|
||||
|
||||
skills: Effect.fn("SystemPrompt.skills")(function* (agent: Agent.Info) {
|
||||
if (Permission.disabled(["skill"], agent.permission).has("skill")) return
|
||||
|
||||
const list = yield* skill.available(agent)
|
||||
|
||||
return [
|
||||
"Skills provide specialized instructions and workflows for specific tasks.",
|
||||
"Use the skill tool to load a skill when a task matches its description.",
|
||||
// the agents seem to ingest the information about skills a bit better if we present a more verbose
|
||||
// version of them here and a less verbose version in tool description, rather than vice versa.
|
||||
Skill.fmt(list, { verbose: true }),
|
||||
].join("\n")
|
||||
}),
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(Skill.defaultLayer))
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user