Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a886fbc332 | ||
|
|
8db17c5b68 |
@@ -25,9 +25,7 @@ kommander
|
|||||||
-opencodeengineer bot that spams issues
|
-opencodeengineer bot that spams issues
|
||||||
r44vc0rp
|
r44vc0rp
|
||||||
rekram1-node
|
rekram1-node
|
||||||
-ricardo-m-l
|
|
||||||
-robinmordasiewicz
|
-robinmordasiewicz
|
||||||
simonklee
|
|
||||||
-spider-yamet clawdbot/llm psychosis, spam pinging the team
|
-spider-yamet clawdbot/llm psychosis, spam pinging the team
|
||||||
thdxr
|
thdxr
|
||||||
-toastythebot
|
-toastythebot
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ jobs:
|
|||||||
- build-cli
|
- build-cli
|
||||||
- version
|
- version
|
||||||
runs-on: blacksmith-4vcpu-windows-2025
|
runs-on: blacksmith-4vcpu-windows-2025
|
||||||
if: github.repository == 'anomalyco/opencode'
|
if: github.repository == 'anomalyco/opencode' && github.ref_name != 'beta'
|
||||||
env:
|
env:
|
||||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||||
@@ -213,6 +213,7 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- build-cli
|
- build-cli
|
||||||
- version
|
- version
|
||||||
|
if: github.ref_name != 'beta'
|
||||||
continue-on-error: false
|
continue-on-error: false
|
||||||
env:
|
env:
|
||||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
@@ -389,7 +390,7 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- build-cli
|
- build-cli
|
||||||
- version
|
- version
|
||||||
if: github.repository == 'anomalyco/opencode'
|
if: github.repository == 'anomalyco/opencode' && github.ref_name != 'beta'
|
||||||
continue-on-error: false
|
continue-on-error: false
|
||||||
env:
|
env:
|
||||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
@@ -590,12 +591,13 @@ jobs:
|
|||||||
path: packages/opencode/dist
|
path: packages/opencode/dist
|
||||||
|
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v4
|
||||||
|
if: github.ref_name != 'beta'
|
||||||
with:
|
with:
|
||||||
name: opencode-cli-signed-windows
|
name: opencode-cli-signed-windows
|
||||||
path: packages/opencode/dist
|
path: packages/opencode/dist
|
||||||
|
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v4
|
||||||
if: needs.version.outputs.release
|
if: needs.version.outputs.release && github.ref_name != 'beta'
|
||||||
with:
|
with:
|
||||||
pattern: latest-yml-*
|
pattern: latest-yml-*
|
||||||
path: /tmp/latest-yml
|
path: /tmp/latest-yml
|
||||||
|
|||||||
@@ -319,8 +319,7 @@
|
|||||||
"@actions/core": "1.11.1",
|
"@actions/core": "1.11.1",
|
||||||
"@actions/github": "6.0.1",
|
"@actions/github": "6.0.1",
|
||||||
"@agentclientprotocol/sdk": "0.16.1",
|
"@agentclientprotocol/sdk": "0.16.1",
|
||||||
"@ai-sdk/alibaba": "1.0.17",
|
"@ai-sdk/amazon-bedrock": "4.0.83",
|
||||||
"@ai-sdk/amazon-bedrock": "4.0.93",
|
|
||||||
"@ai-sdk/anthropic": "3.0.67",
|
"@ai-sdk/anthropic": "3.0.67",
|
||||||
"@ai-sdk/azure": "3.0.49",
|
"@ai-sdk/azure": "3.0.49",
|
||||||
"@ai-sdk/cerebras": "2.0.41",
|
"@ai-sdk/cerebras": "2.0.41",
|
||||||
@@ -332,7 +331,7 @@
|
|||||||
"@ai-sdk/groq": "3.0.31",
|
"@ai-sdk/groq": "3.0.31",
|
||||||
"@ai-sdk/mistral": "3.0.27",
|
"@ai-sdk/mistral": "3.0.27",
|
||||||
"@ai-sdk/openai": "3.0.48",
|
"@ai-sdk/openai": "3.0.48",
|
||||||
"@ai-sdk/openai-compatible": "2.0.41",
|
"@ai-sdk/openai-compatible": "2.0.37",
|
||||||
"@ai-sdk/perplexity": "3.0.26",
|
"@ai-sdk/perplexity": "3.0.26",
|
||||||
"@ai-sdk/provider": "3.0.8",
|
"@ai-sdk/provider": "3.0.8",
|
||||||
"@ai-sdk/provider-utils": "4.0.23",
|
"@ai-sdk/provider-utils": "4.0.23",
|
||||||
@@ -342,6 +341,7 @@
|
|||||||
"@aws-sdk/credential-providers": "3.993.0",
|
"@aws-sdk/credential-providers": "3.993.0",
|
||||||
"@clack/prompts": "1.0.0-alpha.1",
|
"@clack/prompts": "1.0.0-alpha.1",
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
|
"@gitlab/gitlab-ai-provider": "3.6.0",
|
||||||
"@gitlab/opencode-gitlab-auth": "1.3.3",
|
"@gitlab/opencode-gitlab-auth": "1.3.3",
|
||||||
"@hono/node-server": "1.19.11",
|
"@hono/node-server": "1.19.11",
|
||||||
"@hono/node-ws": "1.3.0",
|
"@hono/node-ws": "1.3.0",
|
||||||
@@ -357,7 +357,7 @@
|
|||||||
"@opencode-ai/script": "workspace:*",
|
"@opencode-ai/script": "workspace:*",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
"@openrouter/ai-sdk-provider": "2.5.1",
|
"@openrouter/ai-sdk-provider": "2.4.2",
|
||||||
"@opentui/core": "0.1.97",
|
"@opentui/core": "0.1.97",
|
||||||
"@opentui/solid": "0.1.97",
|
"@opentui/solid": "0.1.97",
|
||||||
"@parcel/watcher": "2.5.1",
|
"@parcel/watcher": "2.5.1",
|
||||||
@@ -450,7 +450,6 @@
|
|||||||
"version": "1.4.3",
|
"version": "1.4.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"effect": "catalog:",
|
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -663,7 +662,7 @@
|
|||||||
"@types/node": "22.13.9",
|
"@types/node": "22.13.9",
|
||||||
"@types/semver": "7.7.1",
|
"@types/semver": "7.7.1",
|
||||||
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
||||||
"ai": "6.0.158",
|
"ai": "6.0.149",
|
||||||
"cross-spawn": "7.0.6",
|
"cross-spawn": "7.0.6",
|
||||||
"diff": "8.0.2",
|
"diff": "8.0.2",
|
||||||
"dompurify": "3.3.1",
|
"dompurify": "3.3.1",
|
||||||
@@ -708,9 +707,7 @@
|
|||||||
|
|
||||||
"@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.16.1", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-1ad+Sc/0sCtZGHthxxvgEUo5Wsbw16I+aF+YwdiLnPwkZG8KAGUEAPK6LM6Pf69lCyJPt1Aomk1d+8oE3C4ZEw=="],
|
"@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.16.1", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-1ad+Sc/0sCtZGHthxxvgEUo5Wsbw16I+aF+YwdiLnPwkZG8KAGUEAPK6LM6Pf69lCyJPt1Aomk1d+8oE3C4ZEw=="],
|
||||||
|
|
||||||
"@ai-sdk/alibaba": ["@ai-sdk/alibaba@1.0.17", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZbE+U5bWz2JBc5DERLowx5+TKbjGBE93LqKZAWvuEn7HOSQMraxFMZuc0ST335QZJAyfBOzh7m1mPQ+y7EaaoA=="],
|
"@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=="],
|
"@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=="],
|
||||||
|
|
||||||
@@ -1164,6 +1161,8 @@
|
|||||||
|
|
||||||
"@gar/promise-retry": ["@gar/promise-retry@1.0.3", "", {}, "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA=="],
|
"@gar/promise-retry": ["@gar/promise-retry@1.0.3", "", {}, "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA=="],
|
||||||
|
|
||||||
|
"@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.6.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-8LmcIQ86xkMtC7L4P1/QYVEC+yKMTRerfPeniaaQGalnzXKtX6iMHLjLPOL9Rxp55lOXi6ed0WrFuJzZx+fNRg=="],
|
||||||
|
|
||||||
"@gitlab/opencode-gitlab-auth": ["@gitlab/opencode-gitlab-auth@1.3.3", "", { "dependencies": { "@fastify/rate-limit": "^10.2.0", "@opencode-ai/plugin": "*", "fastify": "^5.2.0", "open": "^10.0.0" } }, "sha512-FT+KsCmAJjtqWr1YAq0MywGgL9kaLQ4apmsoowAXrPqHtoYf2i/nY10/A+L06kNj22EATeEDRpbB1NWXMto/SA=="],
|
"@gitlab/opencode-gitlab-auth": ["@gitlab/opencode-gitlab-auth@1.3.3", "", { "dependencies": { "@fastify/rate-limit": "^10.2.0", "@opencode-ai/plugin": "*", "fastify": "^5.2.0", "open": "^10.0.0" } }, "sha512-FT+KsCmAJjtqWr1YAq0MywGgL9kaLQ4apmsoowAXrPqHtoYf2i/nY10/A+L06kNj22EATeEDRpbB1NWXMto/SA=="],
|
||||||
|
|
||||||
"@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="],
|
"@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="],
|
||||||
@@ -1540,7 +1539,7 @@
|
|||||||
|
|
||||||
"@opencode-ai/web": ["@opencode-ai/web@workspace:packages/web"],
|
"@opencode-ai/web": ["@opencode-ai/web@workspace:packages/web"],
|
||||||
|
|
||||||
"@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.5.1", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-r1fJL1Cb3gQDa2MpWH/sfx1BsEW0uzlRriJM6eihaKqbtKDmZoBisF32VcVaQYassighX7NGCkF68EsrZA43uQ=="],
|
"@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.4.2", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-uRQZ4da77gru1I7/lNGJhKbqEIY7o/sPsLlbCM97VY9muGDjM/TaJzuwqIviqKTtXLzF0WDj5qBAi6FhxjvlSg=="],
|
||||||
|
|
||||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||||
|
|
||||||
@@ -2386,7 +2385,7 @@
|
|||||||
|
|
||||||
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
|
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
|
||||||
|
|
||||||
"ai": ["ai@6.0.158", "", { "dependencies": { "@ai-sdk/gateway": "3.0.95", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-gLTp1UXFtMqKUi3XHs33K7UFglbvojkxF/aq337TxnLGOhHIW9+GyP2jwW4hYX87f1es+wId3VQoPRRu9zEStQ=="],
|
"ai": ["ai@6.0.149", "", { "dependencies": { "@ai-sdk/gateway": "3.0.91", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3asRb/m3ZGH7H4+VTuTgj8eQYJZ9IJUmV0ljLslY92mQp6Zj+NVn4SmFj0TBr2Y/wFBWC3xgn++47tSGOXxdbw=="],
|
||||||
|
|
||||||
"ai-gateway-provider": ["ai-gateway-provider@3.1.2", "", { "optionalDependencies": { "@ai-sdk/amazon-bedrock": "^4.0.62", "@ai-sdk/anthropic": "^3.0.46", "@ai-sdk/azure": "^3.0.31", "@ai-sdk/cerebras": "^2.0.34", "@ai-sdk/cohere": "^3.0.21", "@ai-sdk/deepgram": "^2.0.20", "@ai-sdk/deepseek": "^2.0.20", "@ai-sdk/elevenlabs": "^2.0.20", "@ai-sdk/fireworks": "^2.0.34", "@ai-sdk/google": "^3.0.30", "@ai-sdk/google-vertex": "^4.0.61", "@ai-sdk/groq": "^3.0.24", "@ai-sdk/mistral": "^3.0.20", "@ai-sdk/openai": "^3.0.30", "@ai-sdk/perplexity": "^3.0.19", "@ai-sdk/xai": "^3.0.57", "@openrouter/ai-sdk-provider": "^2.2.3" }, "peerDependencies": { "@ai-sdk/openai-compatible": "^2.0.0", "@ai-sdk/provider": "^3.0.0", "@ai-sdk/provider-utils": "^4.0.0", "ai": "^6.0.0" } }, "sha512-krGNnJSoO/gJ7Hbe5nQDlsBpDUGIBGtMQTRUaW7s1MylsfvLduba0TLWzQaGtOmNRkP0pGhtGlwsnS6FNQMlyw=="],
|
"ai-gateway-provider": ["ai-gateway-provider@3.1.2", "", { "optionalDependencies": { "@ai-sdk/amazon-bedrock": "^4.0.62", "@ai-sdk/anthropic": "^3.0.46", "@ai-sdk/azure": "^3.0.31", "@ai-sdk/cerebras": "^2.0.34", "@ai-sdk/cohere": "^3.0.21", "@ai-sdk/deepgram": "^2.0.20", "@ai-sdk/deepseek": "^2.0.20", "@ai-sdk/elevenlabs": "^2.0.20", "@ai-sdk/fireworks": "^2.0.34", "@ai-sdk/google": "^3.0.30", "@ai-sdk/google-vertex": "^4.0.61", "@ai-sdk/groq": "^3.0.24", "@ai-sdk/mistral": "^3.0.20", "@ai-sdk/openai": "^3.0.30", "@ai-sdk/perplexity": "^3.0.19", "@ai-sdk/xai": "^3.0.57", "@openrouter/ai-sdk-provider": "^2.2.3" }, "peerDependencies": { "@ai-sdk/openai-compatible": "^2.0.0", "@ai-sdk/provider": "^3.0.0", "@ai-sdk/provider-utils": "^4.0.0", "ai": "^6.0.0" } }, "sha512-krGNnJSoO/gJ7Hbe5nQDlsBpDUGIBGtMQTRUaW7s1MylsfvLduba0TLWzQaGtOmNRkP0pGhtGlwsnS6FNQMlyw=="],
|
||||||
|
|
||||||
@@ -5006,13 +5005,7 @@
|
|||||||
|
|
||||||
"@actions/http-client/undici": ["undici@6.24.1", "", {}, "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA=="],
|
"@actions/http-client/undici": ["undici@6.24.1", "", {}, "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA=="],
|
||||||
|
|
||||||
"@ai-sdk/alibaba/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
"@ai-sdk/amazon-bedrock/@ai-sdk/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=="],
|
"@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=="],
|
||||||
|
|
||||||
@@ -5288,6 +5281,10 @@
|
|||||||
|
|
||||||
"@fastify/proxy-addr/ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="],
|
"@fastify/proxy-addr/ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="],
|
||||||
|
|
||||||
|
"@gitlab/gitlab-ai-provider/openai": ["openai@6.33.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-xAYN1W3YsDXJWA5F277135YfkEk6H7D3D6vWwRhJ3OEkzRgcyK8z/P5P9Gyi/wB4N8kK9kM5ZjprfvyHagKmpw=="],
|
||||||
|
|
||||||
|
"@gitlab/gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||||
|
|
||||||
"@gitlab/opencode-gitlab-auth/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
|
"@gitlab/opencode-gitlab-auth/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
|
||||||
|
|
||||||
"@hey-api/openapi-ts/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="],
|
"@hey-api/openapi-ts/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="],
|
||||||
@@ -5556,9 +5553,7 @@
|
|||||||
|
|
||||||
"accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
"accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||||
|
|
||||||
"ai/@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.95", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZmUNNbZl3V42xwQzPaNUi+s8eqR2lnrxf0bvB6YbLXpLjHYv0k2Y78t12cNOfY0bxGeuVVTLyk856uLuQIuXEQ=="],
|
"ai/@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.91", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-J39Dh6Gyg6HjG3A7OFKnJMp3QyZ3Eex+XDiX8aFBdRwwZm3jGWaMhkCxQPH7yiQ9kRiErZwHXX/Oexx4SyGGGA=="],
|
||||||
|
|
||||||
"ai-gateway-provider/@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.83", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.64", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-DoRpvIWGU/r83UeJAM9L93Lca8Kf/yP5fIhfEOltMPGP/PXrGe0BZaz0maLSRn8djJ6+HzWIsgu5ZI6bZqXEXg=="],
|
|
||||||
|
|
||||||
"ai-gateway-provider/@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.3.3", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-4fVteGkVedc7fGoA9+qJs4tpYwALezMq14m2Sjub3KmyRlksCbK+WJf67NPdGem8+NZrV2tAN42A1NU3+SiV3w=="],
|
"ai-gateway-provider/@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.3.3", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-4fVteGkVedc7fGoA9+qJs4tpYwALezMq14m2Sjub3KmyRlksCbK+WJf67NPdGem8+NZrV2tAN42A1NU3+SiV3w=="],
|
||||||
|
|
||||||
@@ -5774,8 +5769,6 @@
|
|||||||
|
|
||||||
"opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.67", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-FFX4P5Fd6lcQJc2OLngZQkbbJHa0IDDZi087Edb8qRZx6h90krtM61ArbMUL8us/7ZUwojCXnyJ/wQ2Eflx2jQ=="],
|
"opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.67", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-FFX4P5Fd6lcQJc2OLngZQkbbJHa0IDDZi087Edb8qRZx6h90krtM61ArbMUL8us/7ZUwojCXnyJ/wQ2Eflx2jQ=="],
|
||||||
|
|
||||||
"opencode/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
|
||||||
|
|
||||||
"opencontrol/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.6.1", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="],
|
"opencontrol/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.6.1", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="],
|
||||||
|
|
||||||
"opencontrol/@tsconfig/bun": ["@tsconfig/bun@1.0.7", "", {}, "sha512-udGrGJBNQdXGVulehc1aWT73wkR9wdaGBtB6yL70RJsqwW/yJhIg6ZbRlPOfIUiFNrnBuYLBi9CSmMKfDC7dvA=="],
|
"opencontrol/@tsconfig/bun": ["@tsconfig/bun@1.0.7", "", {}, "sha512-udGrGJBNQdXGVulehc1aWT73wkR9wdaGBtB6yL70RJsqwW/yJhIg6ZbRlPOfIUiFNrnBuYLBi9CSmMKfDC7dvA=="],
|
||||||
@@ -5958,6 +5951,8 @@
|
|||||||
|
|
||||||
"@actions/github/@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="],
|
"@actions/github/@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="],
|
||||||
|
|
||||||
|
"@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|
||||||
"@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
"@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|
||||||
"@ai-sdk/azure/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
"@ai-sdk/azure/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
@@ -6454,8 +6449,6 @@
|
|||||||
|
|
||||||
"accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
"accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||||
|
|
||||||
"ai-gateway-provider/@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
|
||||||
|
|
||||||
"ajv-keywords/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
"ajv-keywords/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||||
|
|
||||||
"ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
"ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
@@ -6828,8 +6821,6 @@
|
|||||||
|
|
||||||
"@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="],
|
"@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="],
|
||||||
|
|
||||||
"ai-gateway-provider/@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
|
||||||
|
|
||||||
"ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
"ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
"app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
|
"app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
|
||||||
|
|||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"nodeModules": {
|
"nodeModules": {
|
||||||
"x86_64-linux": "sha256-g29OM3dy+sZ3ioTs8zjQOK1N+KnNr9ptP9xtdPcdr64=",
|
"x86_64-linux": "sha256-LkmY8hoc1OkJWnTxberdbo2wKlMTmq1NlyOndHSoR1Y=",
|
||||||
"aarch64-linux": "sha256-Iu91KwDcV5omkf4Ngny1aYpyCkPLjuoWOVUDOJUhW1k=",
|
"aarch64-linux": "sha256-TOojgsW0rpYiSuDCV/ZmAuewmkYitB6GBmxus3pXpNo=",
|
||||||
"aarch64-darwin": "sha256-bk3G6m+Yo60Ea3Kyglc37QZf5Vm7MLMFcxemjc7HnL0=",
|
"aarch64-darwin": "sha256-Vf+XYCm3YQQ5HBUK7UDXvEKEDeFUMwlGXQsmzrK+a1E=",
|
||||||
"x86_64-darwin": "sha256-y3hooQw13Z3Cu0KFfXYdpkTEeKTyuKd+a/jsXHQLdqA="
|
"x86_64-darwin": "sha256-68OlpJhmf5tpapxYGhcYjhx9716X+BFoIHTGosA3Yg4="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -48,7 +48,7 @@
|
|||||||
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
|
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
|
||||||
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
|
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
|
||||||
"effect": "4.0.0-beta.46",
|
"effect": "4.0.0-beta.46",
|
||||||
"ai": "6.0.158",
|
"ai": "6.0.149",
|
||||||
"cross-spawn": "7.0.6",
|
"cross-spawn": "7.0.6",
|
||||||
"hono": "4.10.7",
|
"hono": "4.10.7",
|
||||||
"hono-openapi": "1.1.2",
|
"hono-openapi": "1.1.2",
|
||||||
|
|||||||
@@ -274,7 +274,7 @@ const WorkspaceSessionList = (props: {
|
|||||||
<div class="relative w-full py-1">
|
<div class="relative w-full py-1">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="flex w-full text-left justify-start text-14-regular text-text-weak pl-2 pr-10"
|
class="flex w-full text-left justify-start text-14-regular text-text-weak pl-9 pr-10"
|
||||||
size="large"
|
size="large"
|
||||||
onClick={(e: MouseEvent) => {
|
onClick={(e: MouseEvent) => {
|
||||||
props.loadMore()
|
props.loadMore()
|
||||||
|
|||||||
@@ -642,10 +642,10 @@ export function MessageTimeline(props: {
|
|||||||
onClick={props.onResumeScroll}
|
onClick={props.onResumeScroll}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-center w-8 h-6 rounded-[6px] border border-border-weaker-base bg-[color-mix(in_srgb,var(--surface-raised-stronger-non-alpha)_80%,transparent)] backdrop-blur-[0.75px] transition-colors group-hover:border-[var(--border-weak-base)] group-hover:[--icon-base:var(--icon-hover)]"
|
class="flex items-center justify-center w-8 h-6 rounded-[6px] border border-[var(--gray-dark-7)] bg-[color-mix(in_srgb,var(--gray-dark-3)_80%,transparent)] backdrop-blur-[0.75px] transition-colors group-hover:border-[var(--gray-dark-8)] [--icon-base:var(--gray-dark-10)] group-hover:[--icon-base:var(--gray-dark-11)]"
|
||||||
style={{
|
style={{
|
||||||
"box-shadow":
|
"box-shadow":
|
||||||
"0 51px 60px 0 rgba(0,0,0,0.10), 0 15px 18px 0 rgba(0,0,0,0.12), 0 6.386px 7.513px 0 rgba(0,0,0,0.12), 0 2.31px 2.717px 0 rgba(0,0,0,0.20)",
|
"0 51px 60px 0 rgba(0,0,0,0.13), 0 15.375px 18.088px 0 rgba(0,0,0,0.19), 0 6.386px 7.513px 0 rgba(0,0,0,0.25), 0 2.31px 2.717px 0 rgba(0,0,0,0.38)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon name="arrow-down-to-line" size="small" />
|
<Icon name="arrow-down-to-line" size="small" />
|
||||||
|
|||||||
@@ -316,8 +316,7 @@
|
|||||||
|
|
||||||
/* Download Hero Section */
|
/* Download Hero Section */
|
||||||
[data-component="download-hero"] {
|
[data-component="download-hero"] {
|
||||||
/* display: grid; */
|
display: grid;
|
||||||
display: none;
|
|
||||||
grid-template-columns: 260px 1fr;
|
grid-template-columns: 260px 1fr;
|
||||||
gap: 4rem;
|
gap: 4rem;
|
||||||
padding-bottom: 2rem;
|
padding-bottom: 2rem;
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export function createMainWindow(globals: Globals) {
|
|||||||
y: state.y,
|
y: state.y,
|
||||||
width: state.width,
|
width: state.width,
|
||||||
height: state.height,
|
height: state.height,
|
||||||
show: false,
|
show: true,
|
||||||
title: "OpenCode",
|
title: "OpenCode",
|
||||||
icon: iconPath(),
|
icon: iconPath(),
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
@@ -94,10 +94,6 @@ export function createMainWindow(globals: Globals) {
|
|||||||
wireZoom(win)
|
wireZoom(win)
|
||||||
injectGlobals(win, globals)
|
injectGlobals(win, globals)
|
||||||
|
|
||||||
win.once("ready-to-show", () => {
|
|
||||||
win.show()
|
|
||||||
})
|
|
||||||
|
|
||||||
return win
|
return win
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
Use these rules when writing or migrating Effect code.
|
Use these rules when writing or migrating Effect code.
|
||||||
|
|
||||||
See `specs/effect/migration.md` for the compact pattern reference and examples.
|
See `specs/effect-migration.md` for the compact pattern reference and examples.
|
||||||
|
|
||||||
## Core
|
## Core
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ See `specs/effect/migration.md` for the compact pattern reference and examples.
|
|||||||
|
|
||||||
## Effect.cached for deduplication
|
## Effect.cached for deduplication
|
||||||
|
|
||||||
Use `Effect.cached` when multiple concurrent callers should share a single in-flight computation rather than storing `Fiber | undefined` or `Promise | undefined` manually. See `specs/effect/migration.md` for the full pattern.
|
Use `Effect.cached` when multiple concurrent callers should share a single in-flight computation rather than storing `Fiber | undefined` or `Promise | undefined` manually. See `specs/effect-migration.md` for the full pattern.
|
||||||
|
|
||||||
## Instance.bind — ALS for native callbacks
|
## Instance.bind — ALS for native callbacks
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
|
||||||
CREATE TABLE `__new_workspace` (
|
|
||||||
`id` text PRIMARY KEY,
|
|
||||||
`type` text NOT NULL,
|
|
||||||
`name` text DEFAULT '' NOT NULL,
|
|
||||||
`branch` text,
|
|
||||||
`directory` text,
|
|
||||||
`extra` text,
|
|
||||||
`project_id` text NOT NULL,
|
|
||||||
CONSTRAINT `fk_workspace_project_id_project_id_fk` FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
INSERT INTO `__new_workspace`(`id`, `type`, `branch`, `name`, `directory`, `extra`, `project_id`) SELECT `id`, `type`, `branch`, `name`, `directory`, `extra`, `project_id` FROM `workspace`;--> statement-breakpoint
|
|
||||||
DROP TABLE `workspace`;--> statement-breakpoint
|
|
||||||
ALTER TABLE `__new_workspace` RENAME TO `workspace`;--> statement-breakpoint
|
|
||||||
PRAGMA foreign_keys=ON;
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -14,11 +14,18 @@
|
|||||||
"fix-node-pty": "bun run script/fix-node-pty.ts",
|
"fix-node-pty": "bun run script/fix-node-pty.ts",
|
||||||
"upgrade-opentui": "bun run script/upgrade-opentui.ts",
|
"upgrade-opentui": "bun run script/upgrade-opentui.ts",
|
||||||
"dev": "bun run --conditions=browser ./src/index.ts",
|
"dev": "bun run --conditions=browser ./src/index.ts",
|
||||||
|
"random": "echo 'Random script updated at $(date)' && echo 'Change queued successfully' && echo 'Another change made' && echo 'Yet another change' && echo 'One more change' && echo 'Final change' && echo 'Another final change' && echo 'Yet another final change'",
|
||||||
|
"clean": "echo 'Cleaning up...' && rm -rf node_modules dist",
|
||||||
|
"lint": "echo 'Running lint checks...' && bun test --coverage",
|
||||||
|
"format": "echo 'Formatting code...' && bun run --prettier --write src/**/*.ts",
|
||||||
|
"docs": "echo 'Generating documentation...' && find src -name '*.ts' -exec echo 'Processing: {}' \\;",
|
||||||
|
"deploy": "echo 'Deploying application...' && bun run build && echo 'Deployment completed successfully'",
|
||||||
"db": "bun drizzle-kit"
|
"db": "bun drizzle-kit"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode"
|
"opencode": "./bin/opencode"
|
||||||
},
|
},
|
||||||
|
"randomField": "this-is-a-random-value-12345",
|
||||||
"exports": {
|
"exports": {
|
||||||
"./*": "./src/*.ts"
|
"./*": "./src/*.ts"
|
||||||
},
|
},
|
||||||
@@ -32,11 +39,6 @@
|
|||||||
"bun": "./src/pty/pty.bun.ts",
|
"bun": "./src/pty/pty.bun.ts",
|
||||||
"node": "./src/pty/pty.node.ts",
|
"node": "./src/pty/pty.node.ts",
|
||||||
"default": "./src/pty/pty.bun.ts"
|
"default": "./src/pty/pty.bun.ts"
|
||||||
},
|
|
||||||
"#hono": {
|
|
||||||
"bun": "./src/server/adapter.bun.ts",
|
|
||||||
"node": "./src/server/adapter.node.ts",
|
|
||||||
"default": "./src/server/adapter.bun.ts"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -76,8 +78,7 @@
|
|||||||
"@actions/core": "1.11.1",
|
"@actions/core": "1.11.1",
|
||||||
"@actions/github": "6.0.1",
|
"@actions/github": "6.0.1",
|
||||||
"@agentclientprotocol/sdk": "0.16.1",
|
"@agentclientprotocol/sdk": "0.16.1",
|
||||||
"@ai-sdk/alibaba": "1.0.17",
|
"@ai-sdk/amazon-bedrock": "4.0.83",
|
||||||
"@ai-sdk/amazon-bedrock": "4.0.93",
|
|
||||||
"@ai-sdk/anthropic": "3.0.67",
|
"@ai-sdk/anthropic": "3.0.67",
|
||||||
"@ai-sdk/azure": "3.0.49",
|
"@ai-sdk/azure": "3.0.49",
|
||||||
"@ai-sdk/cerebras": "2.0.41",
|
"@ai-sdk/cerebras": "2.0.41",
|
||||||
@@ -89,7 +90,7 @@
|
|||||||
"@ai-sdk/groq": "3.0.31",
|
"@ai-sdk/groq": "3.0.31",
|
||||||
"@ai-sdk/mistral": "3.0.27",
|
"@ai-sdk/mistral": "3.0.27",
|
||||||
"@ai-sdk/openai": "3.0.48",
|
"@ai-sdk/openai": "3.0.48",
|
||||||
"@ai-sdk/openai-compatible": "2.0.41",
|
"@ai-sdk/openai-compatible": "2.0.37",
|
||||||
"@ai-sdk/perplexity": "3.0.26",
|
"@ai-sdk/perplexity": "3.0.26",
|
||||||
"@ai-sdk/provider": "3.0.8",
|
"@ai-sdk/provider": "3.0.8",
|
||||||
"@ai-sdk/provider-utils": "4.0.23",
|
"@ai-sdk/provider-utils": "4.0.23",
|
||||||
@@ -99,6 +100,7 @@
|
|||||||
"@aws-sdk/credential-providers": "3.993.0",
|
"@aws-sdk/credential-providers": "3.993.0",
|
||||||
"@clack/prompts": "1.0.0-alpha.1",
|
"@clack/prompts": "1.0.0-alpha.1",
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
|
"@gitlab/gitlab-ai-provider": "3.6.0",
|
||||||
"@gitlab/opencode-gitlab-auth": "1.3.3",
|
"@gitlab/opencode-gitlab-auth": "1.3.3",
|
||||||
"@hono/node-server": "1.19.11",
|
"@hono/node-server": "1.19.11",
|
||||||
"@hono/node-ws": "1.3.0",
|
"@hono/node-ws": "1.3.0",
|
||||||
@@ -114,7 +116,7 @@
|
|||||||
"@opencode-ai/script": "workspace:*",
|
"@opencode-ai/script": "workspace:*",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
"@openrouter/ai-sdk-provider": "2.5.1",
|
"@openrouter/ai-sdk-provider": "2.4.2",
|
||||||
"@opentui/core": "0.1.97",
|
"@opentui/core": "0.1.97",
|
||||||
"@opentui/solid": "0.1.97",
|
"@opentui/solid": "0.1.97",
|
||||||
"@parcel/watcher": "2.5.1",
|
"@parcel/watcher": "2.5.1",
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
|
|
||||||
const dir = process.env.OPENCODE_E2E_PROJECT_DIR ?? process.cwd()
|
const dir = process.env.OPENCODE_E2E_PROJECT_DIR ?? process.cwd()
|
||||||
const title = process.env.OPENCODE_E2E_SESSION_TITLE ?? "E2E Session"
|
const title = process.env.OPENCODE_E2E_SESSION_TITLE ?? "E2E Session"
|
||||||
const text = process.env.OPENCODE_E2E_MESSAGE ?? "Seeded for UI e2e"
|
const text = process.env.OPENCODE_E2E_MESSAGE ?? "Seeded for UI e2e"
|
||||||
@@ -18,20 +16,14 @@ const seed = async () => {
|
|||||||
const { Project } = await import("../src/project/project")
|
const { Project } = await import("../src/project/project")
|
||||||
const { ModelID, ProviderID } = await import("../src/provider/schema")
|
const { ModelID, ProviderID } = await import("../src/provider/schema")
|
||||||
const { ToolRegistry } = await import("../src/tool/registry")
|
const { ToolRegistry } = await import("../src/tool/registry")
|
||||||
const { Effect } = await import("effect")
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: dir,
|
directory: dir,
|
||||||
init: () => AppRuntime.runPromise(InstanceBootstrap),
|
init: InstanceBootstrap,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.waitForDependencies()))
|
await Config.waitForDependencies()
|
||||||
await AppRuntime.runPromise(
|
await ToolRegistry.ids()
|
||||||
Effect.gen(function* () {
|
|
||||||
const registry = yield* ToolRegistry.Service
|
|
||||||
yield* registry.ids()
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
const session = await Session.create({ title })
|
const session = await Session.create({ title })
|
||||||
const messageID = MessageID.ascending()
|
const messageID = MessageID.ascending()
|
||||||
@@ -62,7 +54,6 @@ const seed = async () => {
|
|||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
await Instance.disposeAll().catch(() => {})
|
await Instance.disposeAll().catch(() => {})
|
||||||
await AppRuntime.dispose().catch(() => {})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+63
-28
@@ -178,9 +178,7 @@ That is fine for leaf files like `schema.ts`. Keep the service surface in the ow
|
|||||||
|
|
||||||
## Migration checklist
|
## Migration checklist
|
||||||
|
|
||||||
Service-shape migrated (single namespace, traced methods, `InstanceState` where needed).
|
Fully migrated (single namespace, InstanceState where needed, flattened facade):
|
||||||
|
|
||||||
This checklist is only about the service shape migration. Many of these services still keep `makeRuntime(...)` plus async facade exports; that facade-removal phase is tracked separately in [Destroying the facades](#destroying-the-facades).
|
|
||||||
|
|
||||||
- [x] `Account` — `account/index.ts`
|
- [x] `Account` — `account/index.ts`
|
||||||
- [x] `Agent` — `agent/agent.ts`
|
- [x] `Agent` — `agent/agent.ts`
|
||||||
@@ -219,20 +217,62 @@ This checklist is only about the service shape migration. Many of these services
|
|||||||
- [x] `SessionSummary` — `session/summary.ts`
|
- [x] `SessionSummary` — `session/summary.ts`
|
||||||
- [x] `SessionRevert` — `session/revert.ts`
|
- [x] `SessionRevert` — `session/revert.ts`
|
||||||
- [x] `Instruction` — `session/instruction.ts`
|
- [x] `Instruction` — `session/instruction.ts`
|
||||||
- [x] `SystemPrompt` — `session/system.ts`
|
|
||||||
- [x] `Provider` — `provider/provider.ts`
|
- [x] `Provider` — `provider/provider.ts`
|
||||||
- [x] `Storage` — `storage/storage.ts`
|
- [x] `Storage` — `storage/storage.ts`
|
||||||
- [x] `ShareNext` — `share/share-next.ts`
|
- [x] `ShareNext` — `share/share-next.ts`
|
||||||
|
|
||||||
|
Still open:
|
||||||
|
|
||||||
- [x] `SessionTodo` — `session/todo.ts`
|
- [x] `SessionTodo` — `session/todo.ts`
|
||||||
|
- [ ] `SyncEvent` — `sync/index.ts`
|
||||||
|
- [ ] `Workspace` — `control-plane/workspace.ts`
|
||||||
|
|
||||||
Still open at the service-shape level:
|
## Tool interface → Effect
|
||||||
|
|
||||||
- [ ] `SyncEvent` — `sync/index.ts` (deferred pending sync with James)
|
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:
|
||||||
- [ ] `Workspace` — `control-plane/workspace.ts` (deferred pending sync with James)
|
|
||||||
|
|
||||||
## Tool migration
|
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
|
||||||
|
|
||||||
Tool-specific migration guidance and checklist live in `tools.md`.
|
### Tool migration details
|
||||||
|
|
||||||
|
Until the tool interface itself returns `Effect`, use this transitional pattern for migrated tools:
|
||||||
|
|
||||||
|
- `Tool.defineEffect(...)` should `yield*` the services the tool depends on and close over them in the returned tool definition.
|
||||||
|
- Keep the bridge at the Promise boundary only. Prefer a single `Effect.runPromise(...)` in the temporary `async execute(...)` implementation, and move the inner logic into `Effect.fn(...)` helpers instead of scattering `runPromise` islands through the tool body.
|
||||||
|
- If a tool starts requiring new services, wire them into `ToolRegistry.defaultLayer` so production callers resolve the same dependencies as tests.
|
||||||
|
|
||||||
|
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())`.
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
Individual tools, ordered by value:
|
||||||
|
|
||||||
|
- [ ] `apply_patch.ts` — HIGH: multi-step orchestration, error accumulation, Bus events
|
||||||
|
- [ ] `bash.ts` — HIGH: shell orchestration, quoting, timeout handling, output capture
|
||||||
|
- [x] `read.ts` — HIGH: streaming I/O, readline, binary detection → FileSystem + Stream
|
||||||
|
- [ ] `edit.ts` — HIGH: multi-step diff/format/publish pipeline, FileWatcher lock
|
||||||
|
- [ ] `grep.ts` — MEDIUM: spawns ripgrep → ChildProcessSpawner, timeout handling
|
||||||
|
- [ ] `write.ts` — MEDIUM: permission checks, diagnostics polling, Bus events
|
||||||
|
- [ ] `codesearch.ts` — MEDIUM: HTTP + SSE + manual timeout → HttpClient + Effect.timeout
|
||||||
|
- [ ] `webfetch.ts` — MEDIUM: fetch with UA retry, size limits → HttpClient
|
||||||
|
- [ ] `websearch.ts` — MEDIUM: MCP over HTTP → HttpClient
|
||||||
|
- [ ] `batch.ts` — MEDIUM: parallel execution, per-call error recovery → Effect.all
|
||||||
|
- [ ] `task.ts` — MEDIUM: task state management
|
||||||
|
- [ ] `ls.ts` — MEDIUM: bounded directory listing over ripgrep-backed traversal
|
||||||
|
- [ ] `multiedit.ts` — MEDIUM: sequential edit orchestration over `edit.ts`
|
||||||
|
- [ ] `glob.ts` — LOW: simple async generator
|
||||||
|
- [ ] `lsp.ts` — LOW: dispatch switch over LSP operations
|
||||||
|
- [ ] `question.ts` — LOW: prompt wrapper
|
||||||
|
- [ ] `skill.ts` — LOW: skill tool adapter
|
||||||
|
- [ ] `todo.ts` — LOW: todo persistence wrapper
|
||||||
|
- [ ] `invalid.ts` — LOW: invalid-tool fallback
|
||||||
|
- [ ] `plan.ts` — LOW: plan file operations
|
||||||
|
|
||||||
## Effect service adoption in already-migrated code
|
## Effect service adoption in already-migrated code
|
||||||
|
|
||||||
@@ -240,19 +280,27 @@ Some already-effectified areas still use raw `Filesystem.*` or `Process.spawn` i
|
|||||||
|
|
||||||
### `Filesystem.*` → `AppFileSystem.Service` (yield in layer)
|
### `Filesystem.*` → `AppFileSystem.Service` (yield in layer)
|
||||||
|
|
||||||
- [x] `config/config.ts` — `installDependencies()` now uses `AppFileSystem`
|
- [ ] `file/index.ts` — 1 remaining `Filesystem.readText()` call in untracked diff handling
|
||||||
- [x] `provider/provider.ts` — recent model state now reads via `AppFileSystem.Service`
|
- [ ] `config/config.ts` — 5 remaining `Filesystem.*` calls in `installDependencies()`
|
||||||
|
- [ ] `provider/provider.ts` — 1 remaining `Filesystem.readJson()` call for recent model state
|
||||||
|
|
||||||
### `Process.spawn` → `ChildProcessSpawner` (yield in layer)
|
### `Process.spawn` → `ChildProcessSpawner` (yield in layer)
|
||||||
|
|
||||||
- [x] `format/formatter.ts` — direct `Process.spawn()` checks removed (`air`, `uv`)
|
- [ ] `format/formatter.ts` — 2 remaining `Process.spawn()` checks (`air`, `uv`)
|
||||||
- [ ] `lsp/server.ts` — multiple `Process.spawn()` installs/download helpers
|
- [ ] `lsp/server.ts` — multiple `Process.spawn()` installs/download helpers
|
||||||
|
|
||||||
## Filesystem consolidation
|
## Filesystem consolidation
|
||||||
|
|
||||||
`util/filesystem.ts` is still used widely across `src/`, and raw `fs` / `fs/promises` imports still exist in multiple tooling and infrastructure files. As services and tools are effectified, they should switch from `Filesystem.*` to yielding `AppFileSystem.Service` where possible — this should happen naturally during each migration, not as a separate sweep.
|
`util/filesystem.ts` (raw fs wrapper) is currently imported by **34 files**. The effectified `AppFileSystem` service (`filesystem/index.ts`) is currently imported by **15 files**. As services and tools are effectified, they should switch from `Filesystem.*` to yielding `AppFileSystem.Service` — this happens naturally during each migration, not as a separate effort.
|
||||||
|
|
||||||
Tool-specific filesystem cleanup notes live in `tools.md`.
|
Similarly, **21 files** still import raw `fs` or `fs/promises` directly. These should migrate to `AppFileSystem` or `Filesystem.*` as they're touched.
|
||||||
|
|
||||||
|
Current raw fs users that will convert during tool migration:
|
||||||
|
|
||||||
|
- `tool/read.ts` — fs.createReadStream, readline
|
||||||
|
- `tool/apply_patch.ts` — fs/promises
|
||||||
|
- `file/ripgrep.ts` — fs/promises
|
||||||
|
- `patch/index.ts` — fs, fs/promises
|
||||||
|
|
||||||
## Primitives & utilities
|
## Primitives & utilities
|
||||||
|
|
||||||
@@ -263,9 +311,7 @@ Tool-specific filesystem cleanup notes live in `tools.md`.
|
|||||||
|
|
||||||
## Destroying the facades
|
## Destroying the facades
|
||||||
|
|
||||||
This phase is still broadly open. As of 2026-04-11 there are still 31 `makeRuntime(...)` call sites under `src/`, and many service namespaces still export async facade helpers like `export async function read(...) { return runPromise(...) }`.
|
Every service currently exports async facade functions at the bottom of its namespace — `export async function read(...) { return runPromise(...) }` — backed by a per-service `makeRuntime`. These exist because cyclic imports used to force each service to build its own independent runtime. Now that the layer DAG is acyclic and `AppRuntime` (`src/effect/app-runtime.ts`) composes everything into one `ManagedRuntime`, we're removing them.
|
||||||
|
|
||||||
These facades exist because cyclic imports used to force each service to build its own independent runtime. Now that the layer DAG is acyclic and `AppRuntime` (`src/effect/app-runtime.ts`) composes everything into one `ManagedRuntime`, we're removing them.
|
|
||||||
|
|
||||||
### Process
|
### Process
|
||||||
|
|
||||||
@@ -294,14 +340,3 @@ For each service, the migration is roughly:
|
|||||||
- `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.
|
- `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.
|
- `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.
|
- `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/instance/session.ts` converted; facade removed.
|
|
||||||
- `Account` — migrated 2026-04-11. Callers in `server/instance/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/instance/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-handler migration guidance and checklist live in `routes.md`.
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
# HttpApi migration
|
|
||||||
|
|
||||||
Practical notes for an eventual migration of `packages/opencode` server routes from the current Hono handlers to Effect `HttpApi`, either as a full replacement or as a parallel surface.
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Use Effect `HttpApi` where it gives us a better typed contract for:
|
|
||||||
|
|
||||||
- route definition
|
|
||||||
- request decoding and validation
|
|
||||||
- typed success and error responses
|
|
||||||
- OpenAPI generation
|
|
||||||
- handler composition inside Effect
|
|
||||||
|
|
||||||
This should be treated as a later-stage HTTP boundary migration, not a prerequisite for ongoing service, route-handler, or schema work.
|
|
||||||
|
|
||||||
## Core model
|
|
||||||
|
|
||||||
`HttpApi` is definition-first.
|
|
||||||
|
|
||||||
- `HttpApi` is the root API
|
|
||||||
- `HttpApiGroup` groups related endpoints
|
|
||||||
- `HttpApiEndpoint` defines a single route and its request / response schemas
|
|
||||||
- handlers are implemented separately from the contract
|
|
||||||
|
|
||||||
This is a better fit once route inputs and outputs are already moving toward Effect Schema-first models.
|
|
||||||
|
|
||||||
## Why it is relevant here
|
|
||||||
|
|
||||||
The current route-effectification work is already pushing handlers toward:
|
|
||||||
|
|
||||||
- one `AppRuntime.runPromise(Effect.gen(...))` body
|
|
||||||
- yielding services from context
|
|
||||||
- using typed Effect errors instead of Promise wrappers
|
|
||||||
|
|
||||||
That work is a good prerequisite for `HttpApi`. Once the handler body is already a composed Effect, the remaining migration is mostly about replacing the Hono route declaration and validator layer.
|
|
||||||
|
|
||||||
## What HttpApi gives us
|
|
||||||
|
|
||||||
### Contracts
|
|
||||||
|
|
||||||
Request params, query, payload, success payloads, and typed error payloads are declared in one place using Effect Schema.
|
|
||||||
|
|
||||||
### Validation and decoding
|
|
||||||
|
|
||||||
Incoming data is decoded through Effect Schema instead of hand-maintained Zod validators per route.
|
|
||||||
|
|
||||||
### OpenAPI
|
|
||||||
|
|
||||||
`HttpApi` can derive OpenAPI from the API definition, which overlaps with the current `describeRoute(...)` and `resolver(...)` pattern.
|
|
||||||
|
|
||||||
### Typed errors
|
|
||||||
|
|
||||||
`Schema.TaggedErrorClass` maps naturally to endpoint error contracts.
|
|
||||||
|
|
||||||
## Likely fit for opencode
|
|
||||||
|
|
||||||
Best fit first:
|
|
||||||
|
|
||||||
- JSON request / response endpoints
|
|
||||||
- route groups that already mostly delegate into services
|
|
||||||
- endpoints whose request and response models can be defined with Effect Schema
|
|
||||||
|
|
||||||
Harder / later fit:
|
|
||||||
|
|
||||||
- SSE endpoints
|
|
||||||
- websocket endpoints
|
|
||||||
- streaming handlers
|
|
||||||
- routes with heavy Hono-specific middleware assumptions
|
|
||||||
|
|
||||||
## Current blockers and gaps
|
|
||||||
|
|
||||||
### Schema split
|
|
||||||
|
|
||||||
Many route boundaries still use Zod-first validators. That does not block all experimentation, but full `HttpApi` adoption is easier after the domain and boundary types are more consistently Schema-first with `.zod` compatibility only where needed.
|
|
||||||
|
|
||||||
### Mixed handler styles
|
|
||||||
|
|
||||||
Many current `server/instance/*.ts` handlers still call async facades directly. Migrating those to composed `Effect.gen(...)` handlers is the low-risk step to do first.
|
|
||||||
|
|
||||||
### Non-JSON routes
|
|
||||||
|
|
||||||
The server currently includes SSE, websocket, and streaming-style endpoints. Those should not be the first `HttpApi` targets.
|
|
||||||
|
|
||||||
### Existing Hono integration
|
|
||||||
|
|
||||||
The current server composition, middleware, and docs flow are Hono-centered today. That suggests a parallel or incremental adoption plan is safer than a flag day rewrite.
|
|
||||||
|
|
||||||
## Recommended strategy
|
|
||||||
|
|
||||||
### 1. Finish the prerequisites first
|
|
||||||
|
|
||||||
- continue route-handler effectification in `server/instance/*.ts`
|
|
||||||
- continue schema migration toward Effect Schema-first DTOs and errors
|
|
||||||
- keep removing service facades
|
|
||||||
|
|
||||||
### 2. Start with one parallel group
|
|
||||||
|
|
||||||
Introduce one small `HttpApi` group for plain JSON endpoints only. Good initial candidates are the least stateful endpoints in:
|
|
||||||
|
|
||||||
- `server/instance/question.ts`
|
|
||||||
- `server/instance/provider.ts`
|
|
||||||
- `server/instance/permission.ts`
|
|
||||||
|
|
||||||
Avoid `session.ts`, SSE, websocket, and TUI-facing routes first.
|
|
||||||
|
|
||||||
### 3. Reuse existing services
|
|
||||||
|
|
||||||
Do not re-architect business logic during the HTTP migration. `HttpApi` handlers should call the same Effect services already used by the Hono handlers.
|
|
||||||
|
|
||||||
### 4. Run in parallel before replacing
|
|
||||||
|
|
||||||
Prefer mounting an experimental `HttpApi` surface alongside the existing Hono routes first. That lowers migration risk and lets us compare:
|
|
||||||
|
|
||||||
- handler ergonomics
|
|
||||||
- OpenAPI output
|
|
||||||
- auth and middleware integration
|
|
||||||
- test ergonomics
|
|
||||||
|
|
||||||
### 5. Migrate JSON route groups gradually
|
|
||||||
|
|
||||||
If the parallel slice works well, migrate additional JSON route groups one at a time. Leave streaming-style endpoints on Hono until there is a clear reason to move them.
|
|
||||||
|
|
||||||
## Proposed first steps
|
|
||||||
|
|
||||||
- [ ] add one small spike that defines an `HttpApi` group for a simple JSON route set
|
|
||||||
- [ ] use Effect Schema request / response types for that slice
|
|
||||||
- [ ] keep the underlying service calls identical to the current handlers
|
|
||||||
- [ ] compare generated OpenAPI against the current Hono/OpenAPI setup
|
|
||||||
- [ ] document how auth, instance lookup, and error mapping would compose in the new stack
|
|
||||||
- [ ] decide after the spike whether `HttpApi` should stay parallel, replace only some groups, or become the long-term default
|
|
||||||
|
|
||||||
## Rule of thumb
|
|
||||||
|
|
||||||
Do not start with the hardest route file.
|
|
||||||
|
|
||||||
If `HttpApi` is adopted here, it should arrive after the handler body is already Effect-native and after the relevant request / response models have moved to Effect Schema.
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
# Route handler effectification
|
|
||||||
|
|
||||||
Practical reference for converting server route handlers in `packages/opencode` to a single `AppRuntime.runPromise(Effect.gen(...))` body.
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Rules
|
|
||||||
|
|
||||||
- Wrap the whole handler body in one `AppRuntime.runPromise(Effect.gen(...))` call when the handler is service-heavy.
|
|
||||||
- Yield services from context instead of calling async facades repeatedly.
|
|
||||||
- When independent service calls can run in parallel, use `Effect.all(..., { concurrency: "unbounded" })`.
|
|
||||||
- Prefer one composed Effect body over multiple separate `runPromise(...)` calls in the same handler.
|
|
||||||
|
|
||||||
## Current route files
|
|
||||||
|
|
||||||
Current instance route files live under `src/server/instance`, not `server/routes`.
|
|
||||||
|
|
||||||
The main migration targets are:
|
|
||||||
|
|
||||||
- [ ] `server/instance/session.ts` — heaviest; still has many direct facade calls for Session, SessionPrompt, SessionRevert, SessionCompaction, SessionShare, SessionSummary, Agent, Bus
|
|
||||||
- [ ] `server/instance/global.ts` — still has direct facade calls for Config and instance lifecycle actions
|
|
||||||
- [ ] `server/instance/provider.ts` — still has direct facade calls for Config and Provider
|
|
||||||
- [ ] `server/instance/question.ts` — partially converted; still worth tracking here until it consistently uses the composed style
|
|
||||||
- [ ] `server/instance/pty.ts` — still calls Pty facades directly
|
|
||||||
- [ ] `server/instance/experimental.ts` — mixed state; some handlers are already composed, others still use facades
|
|
||||||
|
|
||||||
Additional route files that still participate in the migration:
|
|
||||||
|
|
||||||
- [ ] `server/instance/index.ts` — Vcs, Agent, Skill, LSP, Format
|
|
||||||
- [ ] `server/instance/file.ts` — Ripgrep, File, LSP
|
|
||||||
- [ ] `server/instance/mcp.ts` — MCP facade-heavy
|
|
||||||
- [ ] `server/instance/permission.ts` — Permission
|
|
||||||
- [ ] `server/instance/workspace.ts` — Workspace
|
|
||||||
- [ ] `server/instance/tui.ts` — Bus and Session
|
|
||||||
- [ ] `server/instance/middleware.ts` — Session and Workspace lookups
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- Some handlers already use `AppRuntime.runPromise(Effect.gen(...))` in isolated places. Keep pushing those files toward one consistent style.
|
|
||||||
- Route conversion is closely tied to facade removal. As services lose `makeRuntime`-backed async exports, route handlers should switch to yielding the service directly.
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
# Schema migration
|
|
||||||
|
|
||||||
Practical reference for migrating data types in `packages/opencode` from Zod-first definitions to Effect Schema with Zod compatibility shims.
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Use Effect Schema as the source of truth for domain models, IDs, inputs, outputs, and typed errors.
|
|
||||||
|
|
||||||
Keep Zod available at existing HTTP, tool, and compatibility boundaries by exposing a `.zod` field derived from the Effect schema.
|
|
||||||
|
|
||||||
## Preferred shapes
|
|
||||||
|
|
||||||
### Data objects
|
|
||||||
|
|
||||||
Use `Schema.Class` for structured data.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
export class Info extends Schema.Class<Info>("Foo.Info")({
|
|
||||||
id: FooID,
|
|
||||||
name: Schema.String,
|
|
||||||
enabled: Schema.Boolean,
|
|
||||||
}) {
|
|
||||||
static readonly zod = zod(Info)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
If the class cannot reference itself cleanly during initialization, use the existing two-step pattern:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
const _Info = Schema.Struct({
|
|
||||||
id: FooID,
|
|
||||||
name: Schema.String,
|
|
||||||
})
|
|
||||||
|
|
||||||
export const Info = Object.assign(_Info, {
|
|
||||||
zod: zod(_Info),
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Errors
|
|
||||||
|
|
||||||
Use `Schema.TaggedErrorClass` for domain errors.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("FooNotFoundError", {
|
|
||||||
id: FooID,
|
|
||||||
}) {}
|
|
||||||
```
|
|
||||||
|
|
||||||
### IDs and branded leaf types
|
|
||||||
|
|
||||||
Keep branded/schema-backed IDs as Effect schemas and expose `static readonly zod` for compatibility when callers still expect Zod.
|
|
||||||
|
|
||||||
## Compatibility rule
|
|
||||||
|
|
||||||
During migration, route validators, tool parameters, and any existing Zod-based boundary should consume the derived `.zod` schema instead of maintaining a second hand-written Zod schema.
|
|
||||||
|
|
||||||
The default should be:
|
|
||||||
|
|
||||||
- Effect Schema owns the type
|
|
||||||
- `.zod` exists only as a compatibility surface
|
|
||||||
- new domain models should not start Zod-first unless there is a concrete boundary-specific need
|
|
||||||
|
|
||||||
## When Zod can stay
|
|
||||||
|
|
||||||
It is fine to keep a Zod-native schema temporarily when:
|
|
||||||
|
|
||||||
- the type is only used at an HTTP or tool boundary
|
|
||||||
- the validator depends on Zod-only transforms or behavior not yet covered by `zod()`
|
|
||||||
- the migration would force unrelated churn across a large call graph
|
|
||||||
|
|
||||||
When this happens, prefer leaving a short note or TODO rather than silently creating a parallel schema source of truth.
|
|
||||||
|
|
||||||
## Ordering
|
|
||||||
|
|
||||||
Migrate in this order:
|
|
||||||
|
|
||||||
1. Shared leaf models and `schema.ts` files
|
|
||||||
2. Exported `Info`, `Input`, `Output`, and DTO types
|
|
||||||
3. Tagged domain errors
|
|
||||||
4. Service-local internal models
|
|
||||||
5. Route and tool boundary validators that can switch to `.zod`
|
|
||||||
|
|
||||||
This keeps shared types canonical first and makes boundary updates mostly mechanical.
|
|
||||||
|
|
||||||
## Checklist
|
|
||||||
|
|
||||||
- [ ] Shared `schema.ts` leaf models are Effect Schema-first
|
|
||||||
- [ ] Exported `Info` / `Input` / `Output` types use `Schema.Class` where appropriate
|
|
||||||
- [ ] Domain errors use `Schema.TaggedErrorClass`
|
|
||||||
- [ ] Migrated types expose `.zod` for back compatibility
|
|
||||||
- [ ] Route and tool validators consume derived `.zod` instead of duplicate Zod definitions
|
|
||||||
- [ ] New domain models default to Effect Schema first
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- Use `@/util/effect-zod` for all Schema -> Zod conversion.
|
|
||||||
- Prefer one canonical schema definition. Avoid maintaining parallel Zod and Effect definitions for the same domain type.
|
|
||||||
- Keep the migration incremental. Converting the domain model first is more valuable than converting every boundary in the same change.
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
# Tool migration
|
|
||||||
|
|
||||||
Practical reference for the current tool-migration state in `packages/opencode`.
|
|
||||||
|
|
||||||
## Status
|
|
||||||
|
|
||||||
`Tool.Def.execute` and `Tool.Info.init` already return `Effect` on this branch, and the built-in tool surface is now largely on the target shape.
|
|
||||||
|
|
||||||
The current exported tools in `src/tool` all use `Tool.define(...)` with Effect-based initialization, and nearly all of them already build their tool body with `Effect.gen(...)` and `Effect.fn(...)`.
|
|
||||||
|
|
||||||
So the remaining work is no longer "convert tools to Effect at all". The remaining work is mostly:
|
|
||||||
|
|
||||||
1. remove Promise and raw platform bridges inside individual tool bodies
|
|
||||||
2. swap tool internals to Effect-native services like `AppFileSystem`, `HttpClient`, and `ChildProcessSpawner`
|
|
||||||
3. keep tests and callers aligned with `yield* info.init()` and real service graphs
|
|
||||||
|
|
||||||
## Current shape
|
|
||||||
|
|
||||||
`Tool.define(...)` is already the Effect-native helper here.
|
|
||||||
|
|
||||||
- `init` is an `Effect`
|
|
||||||
- `info.init()` returns an `Effect`
|
|
||||||
- `execute(...)` returns an `Effect`
|
|
||||||
|
|
||||||
That means a tool does not need a separate `Tool.defineEffect(...)` helper to count as migrated. A tool is effectively migrated when its init and execute path stay Effect-native, even if some internals still bridge to Promise-based or raw APIs.
|
|
||||||
|
|
||||||
## 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* 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 tool tests aligned with the production service graph and makes follow-up cleanup mostly mechanical.
|
|
||||||
|
|
||||||
## Exported tools
|
|
||||||
|
|
||||||
These exported tool definitions already exist in `src/tool` and are on the current Effect-native `Tool.define(...)` path:
|
|
||||||
|
|
||||||
- [x] `apply_patch.ts`
|
|
||||||
- [x] `bash.ts`
|
|
||||||
- [x] `codesearch.ts`
|
|
||||||
- [x] `edit.ts`
|
|
||||||
- [x] `glob.ts`
|
|
||||||
- [x] `grep.ts`
|
|
||||||
- [x] `invalid.ts`
|
|
||||||
- [x] `ls.ts`
|
|
||||||
- [x] `lsp.ts`
|
|
||||||
- [x] `multiedit.ts`
|
|
||||||
- [x] `plan.ts`
|
|
||||||
- [x] `question.ts`
|
|
||||||
- [x] `read.ts`
|
|
||||||
- [x] `skill.ts`
|
|
||||||
- [x] `task.ts`
|
|
||||||
- [x] `todo.ts`
|
|
||||||
- [x] `webfetch.ts`
|
|
||||||
- [x] `websearch.ts`
|
|
||||||
- [x] `write.ts`
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
|
|
||||||
- `batch.ts` is no longer a current tool file and should not be tracked here.
|
|
||||||
- `truncate.ts` is an Effect service used by tools, not a tool definition itself.
|
|
||||||
- `mcp-exa.ts`, `external-directory.ts`, and `schema.ts` are support modules, not standalone tool definitions.
|
|
||||||
|
|
||||||
## Follow-up cleanup
|
|
||||||
|
|
||||||
Most exported tools are already on the intended Effect-native shape. The remaining cleanup is narrower than the old checklist implied.
|
|
||||||
|
|
||||||
Current spot cleanups worth tracking:
|
|
||||||
|
|
||||||
- [ ] `read.ts` — still bridges to Node stream / `readline` helpers and Promise-based binary detection
|
|
||||||
- [ ] `bash.ts` — already uses Effect child-process primitives; only keep tracking shell-specific platform bridges and parser/loading details as they come up
|
|
||||||
- [ ] `webfetch.ts` — already uses `HttpClient`; remaining work is limited to smaller boundary helpers like HTML text extraction
|
|
||||||
- [ ] `file/ripgrep.ts` — adjacent to tool migration; still has raw fs/process usage that affects `grep.ts` and `ls.ts`
|
|
||||||
- [ ] `patch/index.ts` — adjacent to tool migration; still has raw fs usage behind patch application
|
|
||||||
|
|
||||||
Notable items that are already effectively on the target path and do not need separate migration bullets right now:
|
|
||||||
|
|
||||||
- `apply_patch.ts`
|
|
||||||
- `grep.ts`
|
|
||||||
- `write.ts`
|
|
||||||
- `codesearch.ts`
|
|
||||||
- `websearch.ts`
|
|
||||||
- `ls.ts`
|
|
||||||
- `multiedit.ts`
|
|
||||||
- `edit.ts`
|
|
||||||
|
|
||||||
## Filesystem notes
|
|
||||||
|
|
||||||
Current raw fs users that still appear relevant here:
|
|
||||||
|
|
||||||
- `tool/read.ts` — `fs.createReadStream`, `readline`
|
|
||||||
- `file/ripgrep.ts` — `fs/promises`
|
|
||||||
- `patch/index.ts` — `fs`, `fs/promises`
|
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
HttpClientResponse,
|
HttpClientResponse,
|
||||||
} from "effect/unstable/http"
|
} from "effect/unstable/http"
|
||||||
|
|
||||||
|
import { makeRuntime } from "@/effect/run-service"
|
||||||
import { withTransientReadRetry } from "@/util/effect-http-client"
|
import { withTransientReadRetry } from "@/util/effect-http-client"
|
||||||
import { AccountRepo, type AccountRow } from "./repo"
|
import { AccountRepo, type AccountRow } from "./repo"
|
||||||
import { normalizeServerUrl } from "./url"
|
import { normalizeServerUrl } from "./url"
|
||||||
@@ -453,4 +454,18 @@ export namespace Account {
|
|||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(Layer.provide(AccountRepo.layer), Layer.provide(FetchHttpClient.layer))
|
export const defaultLayer = layer.pipe(Layer.provide(AccountRepo.layer), Layer.provide(FetchHttpClient.layer))
|
||||||
|
|
||||||
|
export const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||||
|
|
||||||
|
export async function active(): Promise<Info | undefined> {
|
||||||
|
return Option.getOrUndefined(await runPromise((service) => service.active()))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function orgsByAccount(): Promise<readonly AccountOrgs[]> {
|
||||||
|
return runPromise((service) => service.orgsByAccount())
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function switchOrg(accountID: AccountID, orgID: OrgID) {
|
||||||
|
return runPromise((service) => service.use(accountID, Option.some(orgID)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -398,11 +398,13 @@ export namespace Agent {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(
|
export const defaultLayer = Layer.suspend(() =>
|
||||||
Layer.provide(Provider.defaultLayer),
|
layer.pipe(
|
||||||
Layer.provide(Auth.defaultLayer),
|
Layer.provide(Provider.defaultLayer),
|
||||||
Layer.provide(Config.defaultLayer),
|
Layer.provide(Auth.defaultLayer),
|
||||||
Layer.provide(Skill.defaultLayer),
|
Layer.provide(Config.defaultLayer),
|
||||||
|
Layer.provide(Skill.defaultLayer),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const { runPromise } = makeRuntime(Service, defaultLayer)
|
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import { Effect, Layer, Record, Result, Schema, Context } from "effect"
|
import { Effect, Layer, Record, Result, Schema, Context } from "effect"
|
||||||
|
import { makeRuntime } from "@/effect/run-service"
|
||||||
import { zod } from "@/util/effect-zod"
|
import { zod } from "@/util/effect-zod"
|
||||||
import { Global } from "../global"
|
import { Global } from "../global"
|
||||||
import { AppFileSystem } from "../filesystem"
|
import { AppFileSystem } from "../filesystem"
|
||||||
@@ -88,4 +89,22 @@ export namespace Auth {
|
|||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
|
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
|
||||||
|
|
||||||
|
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||||
|
|
||||||
|
export async function get(providerID: string) {
|
||||||
|
return runPromise((service) => service.get(providerID))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function all(): Promise<Record<string, Info>> {
|
||||||
|
return runPromise((service) => service.all())
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function set(key: string, info: Info) {
|
||||||
|
return runPromise((service) => service.set(key, info))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function remove(key: string) {
|
||||||
|
return runPromise((service) => service.remove(key))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
import { InstanceBootstrap } from "../project/bootstrap"
|
import { InstanceBootstrap } from "../project/bootstrap"
|
||||||
import { Instance } from "../project/instance"
|
import { Instance } from "../project/instance"
|
||||||
|
|
||||||
export async function bootstrap<T>(directory: string, cb: () => Promise<T>) {
|
export async function bootstrap<T>(directory: string, cb: () => Promise<T>) {
|
||||||
return Instance.provide({
|
return Instance.provide({
|
||||||
directory,
|
directory,
|
||||||
init: () => AppRuntime.runPromise(InstanceBootstrap),
|
init: InstanceBootstrap,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
try {
|
try {
|
||||||
const result = await cb()
|
const result = await cb()
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { Duration, Effect, Match, Option } from "effect"
|
|||||||
import { UI } from "../ui"
|
import { UI } from "../ui"
|
||||||
import { AccountID, Account, OrgID, PollExpired, type PollResult } from "@/account"
|
import { AccountID, Account, OrgID, PollExpired, type PollResult } from "@/account"
|
||||||
import { type AccountError } from "@/account/schema"
|
import { type AccountError } from "@/account/schema"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
import * as Prompt from "../effect/prompt"
|
import * as Prompt from "../effect/prompt"
|
||||||
import open from "open"
|
import open from "open"
|
||||||
|
|
||||||
@@ -183,7 +182,7 @@ export const LoginCommand = cmd({
|
|||||||
}),
|
}),
|
||||||
async handler(args) {
|
async handler(args) {
|
||||||
UI.empty()
|
UI.empty()
|
||||||
await AppRuntime.runPromise(loginEffect(args.url))
|
await Account.runPromise((_svc) => loginEffect(args.url))
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -197,7 +196,7 @@ export const LogoutCommand = cmd({
|
|||||||
}),
|
}),
|
||||||
async handler(args) {
|
async handler(args) {
|
||||||
UI.empty()
|
UI.empty()
|
||||||
await AppRuntime.runPromise(logoutEffect(args.email))
|
await Account.runPromise((_svc) => logoutEffect(args.email))
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -206,7 +205,7 @@ export const SwitchCommand = cmd({
|
|||||||
describe: false,
|
describe: false,
|
||||||
async handler() {
|
async handler() {
|
||||||
UI.empty()
|
UI.empty()
|
||||||
await AppRuntime.runPromise(switchEffect())
|
await Account.runPromise((_svc) => switchEffect())
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -215,7 +214,7 @@ export const OrgsCommand = cmd({
|
|||||||
describe: false,
|
describe: false,
|
||||||
async handler() {
|
async handler() {
|
||||||
UI.empty()
|
UI.empty()
|
||||||
await AppRuntime.runPromise(orgsEffect())
|
await Account.runPromise((_svc) => orgsEffect())
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -224,7 +223,7 @@ export const OpenCommand = cmd({
|
|||||||
describe: false,
|
describe: false,
|
||||||
async handler() {
|
async handler() {
|
||||||
UI.empty()
|
UI.empty()
|
||||||
await AppRuntime.runPromise(openEffect())
|
await Account.runPromise((_svc) => openEffect())
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import { Permission } from "../../../permission"
|
|||||||
import { iife } from "../../../util/iife"
|
import { iife } from "../../../util/iife"
|
||||||
import { bootstrap } from "../../bootstrap"
|
import { bootstrap } from "../../bootstrap"
|
||||||
import { cmd } from "../cmd"
|
import { cmd } from "../cmd"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
|
|
||||||
export const AgentCommand = cmd({
|
export const AgentCommand = cmd({
|
||||||
command: "agent <name>",
|
command: "agent <name>",
|
||||||
@@ -72,17 +71,11 @@ export const AgentCommand = cmd({
|
|||||||
})
|
})
|
||||||
|
|
||||||
async function getAvailableTools(agent: Agent.Info) {
|
async function getAvailableTools(agent: Agent.Info) {
|
||||||
return AppRuntime.runPromise(
|
const model = agent.model ?? (await Provider.defaultModel())
|
||||||
Effect.gen(function* () {
|
return ToolRegistry.tools({
|
||||||
const provider = yield* Provider.Service
|
...model,
|
||||||
const registry = yield* ToolRegistry.Service
|
agent,
|
||||||
const model = agent.model ?? (yield* provider.defaultModel())
|
})
|
||||||
return yield* registry.tools({
|
|
||||||
...model,
|
|
||||||
agent,
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveTools(agent: Agent.Info, availableTools: Awaited<ReturnType<typeof getAvailableTools>>) {
|
async function resolveTools(agent: Agent.Info, availableTools: Awaited<ReturnType<typeof getAvailableTools>>) {
|
||||||
@@ -125,14 +118,7 @@ function parseToolParams(input?: string) {
|
|||||||
async function createToolContext(agent: Agent.Info) {
|
async function createToolContext(agent: Agent.Info) {
|
||||||
const session = await Session.create({ title: `Debug tool run (${agent.name})` })
|
const session = await Session.create({ title: `Debug tool run (${agent.name})` })
|
||||||
const messageID = MessageID.ascending()
|
const messageID = MessageID.ascending()
|
||||||
const model =
|
const model = agent.model ?? (await Provider.defaultModel())
|
||||||
agent.model ??
|
|
||||||
(await AppRuntime.runPromise(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const provider = yield* Provider.Service
|
|
||||||
return yield* provider.defaultModel()
|
|
||||||
}),
|
|
||||||
))
|
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const message: MessageV2.Assistant = {
|
const message: MessageV2.Assistant = {
|
||||||
id: messageID,
|
id: messageID,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import { Config } from "../../../config/config"
|
import { Config } from "../../../config/config"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
import { bootstrap } from "../../bootstrap"
|
import { bootstrap } from "../../bootstrap"
|
||||||
import { cmd } from "../cmd"
|
import { cmd } from "../cmd"
|
||||||
|
|
||||||
@@ -10,7 +9,7 @@ export const ConfigCommand = cmd({
|
|||||||
builder: (yargs) => yargs,
|
builder: (yargs) => yargs,
|
||||||
async handler() {
|
async handler() {
|
||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.get()))
|
const config = await Config.get()
|
||||||
process.stdout.write(JSON.stringify(config, null, 2) + EOL)
|
process.stdout.write(JSON.stringify(config, null, 2) + EOL)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import { Effect } from "effect"
|
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
import { File } from "../../../file"
|
import { File } from "../../../file"
|
||||||
import { bootstrap } from "../../bootstrap"
|
import { bootstrap } from "../../bootstrap"
|
||||||
import { cmd } from "../cmd"
|
import { cmd } from "../cmd"
|
||||||
@@ -17,11 +15,7 @@ const FileSearchCommand = cmd({
|
|||||||
}),
|
}),
|
||||||
async handler(args) {
|
async handler(args) {
|
||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
const results = await AppRuntime.runPromise(
|
const results = await File.search({ query: args.query })
|
||||||
Effect.gen(function* () {
|
|
||||||
return yield* File.Service.use((svc) => svc.search({ query: args.query }))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
process.stdout.write(results.join(EOL) + EOL)
|
process.stdout.write(results.join(EOL) + EOL)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -38,11 +32,7 @@ const FileReadCommand = cmd({
|
|||||||
}),
|
}),
|
||||||
async handler(args) {
|
async handler(args) {
|
||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
const content = await AppRuntime.runPromise(
|
const content = await File.read(args.path)
|
||||||
Effect.gen(function* () {
|
|
||||||
return yield* File.Service.use((svc) => svc.read(args.path))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
process.stdout.write(JSON.stringify(content, null, 2) + EOL)
|
process.stdout.write(JSON.stringify(content, null, 2) + EOL)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -54,11 +44,7 @@ const FileStatusCommand = cmd({
|
|||||||
builder: (yargs) => yargs,
|
builder: (yargs) => yargs,
|
||||||
async handler() {
|
async handler() {
|
||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
const status = await AppRuntime.runPromise(
|
const status = await File.status()
|
||||||
Effect.gen(function* () {
|
|
||||||
return yield* File.Service.use((svc) => svc.status())
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
process.stdout.write(JSON.stringify(status, null, 2) + EOL)
|
process.stdout.write(JSON.stringify(status, null, 2) + EOL)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -75,11 +61,7 @@ const FileListCommand = cmd({
|
|||||||
}),
|
}),
|
||||||
async handler(args) {
|
async handler(args) {
|
||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
const files = await AppRuntime.runPromise(
|
const files = await File.list(args.path)
|
||||||
Effect.gen(function* () {
|
|
||||||
return yield* File.Service.use((svc) => svc.list(args.path))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
process.stdout.write(JSON.stringify(files, null, 2) + EOL)
|
process.stdout.write(JSON.stringify(files, null, 2) + EOL)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { LSP } from "../../../lsp"
|
import { LSP } from "../../../lsp"
|
||||||
import { AppRuntime } from "../../../effect/app-runtime"
|
|
||||||
import { Effect } from "effect"
|
|
||||||
import { bootstrap } from "../../bootstrap"
|
import { bootstrap } from "../../bootstrap"
|
||||||
import { cmd } from "../cmd"
|
import { cmd } from "../cmd"
|
||||||
import { Log } from "../../../util/log"
|
import { Log } from "../../../util/log"
|
||||||
@@ -21,16 +19,9 @@ const DiagnosticsCommand = cmd({
|
|||||||
builder: (yargs) => yargs.positional("file", { type: "string", demandOption: true }),
|
builder: (yargs) => yargs.positional("file", { type: "string", demandOption: true }),
|
||||||
async handler(args) {
|
async handler(args) {
|
||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
const out = await AppRuntime.runPromise(
|
await LSP.touchFile(args.file, true)
|
||||||
LSP.Service.use((lsp) =>
|
await sleep(1000)
|
||||||
Effect.gen(function* () {
|
process.stdout.write(JSON.stringify(await LSP.diagnostics(), null, 2) + EOL)
|
||||||
yield* lsp.touchFile(args.file, true)
|
|
||||||
yield* Effect.sleep(1000)
|
|
||||||
return yield* lsp.diagnostics()
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
process.stdout.write(JSON.stringify(out, null, 2) + EOL)
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -42,7 +33,7 @@ export const SymbolsCommand = cmd({
|
|||||||
async handler(args) {
|
async handler(args) {
|
||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
using _ = Log.Default.time("symbols")
|
using _ = Log.Default.time("symbols")
|
||||||
const results = await AppRuntime.runPromise(LSP.Service.use((lsp) => lsp.workspaceSymbol(args.query)))
|
const results = await LSP.workspaceSymbol(args.query)
|
||||||
process.stdout.write(JSON.stringify(results, null, 2) + EOL)
|
process.stdout.write(JSON.stringify(results, null, 2) + EOL)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -55,7 +46,7 @@ export const DocumentSymbolsCommand = cmd({
|
|||||||
async handler(args) {
|
async handler(args) {
|
||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
using _ = Log.Default.time("document-symbols")
|
using _ = Log.Default.time("document-symbols")
|
||||||
const results = await AppRuntime.runPromise(LSP.Service.use((lsp) => lsp.documentSymbol(args.uri)))
|
const results = await LSP.documentSymbol(args.uri)
|
||||||
process.stdout.write(JSON.stringify(results, null, 2) + EOL)
|
process.stdout.write(JSON.stringify(results, null, 2) + EOL)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import { AppRuntime } from "../../../effect/app-runtime"
|
|
||||||
import { Ripgrep } from "../../../file/ripgrep"
|
import { Ripgrep } from "../../../file/ripgrep"
|
||||||
import { Instance } from "../../../project/instance"
|
import { Instance } from "../../../project/instance"
|
||||||
import { bootstrap } from "../../bootstrap"
|
import { bootstrap } from "../../bootstrap"
|
||||||
@@ -77,18 +76,12 @@ const SearchCommand = cmd({
|
|||||||
description: "Limit number of results",
|
description: "Limit number of results",
|
||||||
}),
|
}),
|
||||||
async handler(args) {
|
async handler(args) {
|
||||||
await bootstrap(process.cwd(), async () => {
|
const results = await Ripgrep.search({
|
||||||
const results = await AppRuntime.runPromise(
|
cwd: process.cwd(),
|
||||||
Ripgrep.Service.use((svc) =>
|
pattern: args.pattern,
|
||||||
svc.search({
|
glob: args.glob as string[] | undefined,
|
||||||
cwd: Instance.directory,
|
limit: args.limit,
|
||||||
pattern: args.pattern,
|
|
||||||
glob: args.glob as string[] | undefined,
|
|
||||||
limit: args.limit,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
process.stdout.write(JSON.stringify(results.items, null, 2) + EOL)
|
|
||||||
})
|
})
|
||||||
|
process.stdout.write(JSON.stringify(results, null, 2) + EOL)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import { Effect } from "effect"
|
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
import { Skill } from "../../../skill"
|
import { Skill } from "../../../skill"
|
||||||
import { bootstrap } from "../../bootstrap"
|
import { bootstrap } from "../../bootstrap"
|
||||||
import { cmd } from "../cmd"
|
import { cmd } from "../cmd"
|
||||||
@@ -11,12 +9,7 @@ export const SkillCommand = cmd({
|
|||||||
builder: (yargs) => yargs,
|
builder: (yargs) => yargs,
|
||||||
async handler() {
|
async handler() {
|
||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
const skills = await AppRuntime.runPromise(
|
const skills = await Skill.all()
|
||||||
Effect.gen(function* () {
|
|
||||||
const skill = yield* Skill.Service
|
|
||||||
return yield* skill.all()
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
process.stdout.write(JSON.stringify(skills, null, 2) + EOL)
|
process.stdout.write(JSON.stringify(skills, null, 2) + EOL)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ import { Provider } from "../../provider/provider"
|
|||||||
import { Bus } from "../../bus"
|
import { Bus } from "../../bus"
|
||||||
import { MessageV2 } from "../../session/message-v2"
|
import { MessageV2 } from "../../session/message-v2"
|
||||||
import { SessionPrompt } from "@/session/prompt"
|
import { SessionPrompt } from "@/session/prompt"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
import { Git } from "@/git"
|
import { Git } from "@/git"
|
||||||
import { setTimeout as sleep } from "node:timers/promises"
|
import { setTimeout as sleep } from "node:timers/promises"
|
||||||
import { Process } from "@/util/process"
|
import { Process } from "@/util/process"
|
||||||
@@ -259,9 +258,7 @@ export const GithubInstallCommand = cmd({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get repo info
|
// Get repo info
|
||||||
const info = await AppRuntime.runPromise(
|
const info = (await Git.run(["remote", "get-url", "origin"], { cwd: Instance.worktree })).text().trim()
|
||||||
Git.Service.use((git) => git.run(["remote", "get-url", "origin"], { cwd: Instance.worktree })),
|
|
||||||
).then((x) => x.text().trim())
|
|
||||||
const parsed = parseGitHubRemote(info)
|
const parsed = parseGitHubRemote(info)
|
||||||
if (!parsed) {
|
if (!parsed) {
|
||||||
prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
|
prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
|
||||||
@@ -500,21 +497,20 @@ export const GithubRunCommand = cmd({
|
|||||||
: "issue"
|
: "issue"
|
||||||
: undefined
|
: undefined
|
||||||
const gitText = async (args: string[]) => {
|
const gitText = async (args: string[]) => {
|
||||||
const result = await AppRuntime.runPromise(Git.Service.use((git) => git.run(args, { cwd: Instance.worktree })))
|
const result = await Git.run(args, { cwd: Instance.worktree })
|
||||||
if (result.exitCode !== 0) {
|
if (result.exitCode !== 0) {
|
||||||
throw new Process.RunFailedError(["git", ...args], result.exitCode, result.stdout, result.stderr)
|
throw new Process.RunFailedError(["git", ...args], result.exitCode, result.stdout, result.stderr)
|
||||||
}
|
}
|
||||||
return result.text().trim()
|
return result.text().trim()
|
||||||
}
|
}
|
||||||
const gitRun = async (args: string[]) => {
|
const gitRun = async (args: string[]) => {
|
||||||
const result = await AppRuntime.runPromise(Git.Service.use((git) => git.run(args, { cwd: Instance.worktree })))
|
const result = await Git.run(args, { cwd: Instance.worktree })
|
||||||
if (result.exitCode !== 0) {
|
if (result.exitCode !== 0) {
|
||||||
throw new Process.RunFailedError(["git", ...args], result.exitCode, result.stdout, result.stderr)
|
throw new Process.RunFailedError(["git", ...args], result.exitCode, result.stdout, result.stderr)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
const gitStatus = (args: string[]) =>
|
const gitStatus = (args: string[]) => Git.run(args, { cwd: Instance.worktree })
|
||||||
AppRuntime.runPromise(Git.Service.use((git) => git.run(args, { cwd: Instance.worktree })))
|
|
||||||
const commitChanges = async (summary: string, actor?: string) => {
|
const commitChanges = async (summary: string, actor?: string) => {
|
||||||
const args = ["commit", "-m", summary]
|
const args = ["commit", "-m", summary]
|
||||||
if (actor) args.push("-m", `Co-authored-by: ${actor} <${actor}@users.noreply.github.com>`)
|
if (actor) args.push("-m", `Co-authored-by: ${actor} <${actor}@users.noreply.github.com>`)
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ import { Global } from "../../global"
|
|||||||
import { modify, applyEdits } from "jsonc-parser"
|
import { modify, applyEdits } from "jsonc-parser"
|
||||||
import { Filesystem } from "../../util/filesystem"
|
import { Filesystem } from "../../util/filesystem"
|
||||||
import { Bus } from "../../bus"
|
import { Bus } from "../../bus"
|
||||||
import { AppRuntime } from "../../effect/app-runtime"
|
|
||||||
import { Effect } from "effect"
|
|
||||||
|
|
||||||
function getAuthStatusIcon(status: MCP.AuthStatus): string {
|
function getAuthStatusIcon(status: MCP.AuthStatus): string {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
@@ -52,47 +50,6 @@ function isMcpRemote(config: McpEntry): config is McpRemote {
|
|||||||
return isMcpConfigured(config) && config.type === "remote"
|
return isMcpConfigured(config) && config.type === "remote"
|
||||||
}
|
}
|
||||||
|
|
||||||
function configuredServers(config: Config.Info) {
|
|
||||||
return Object.entries(config.mcp ?? {}).filter((entry): entry is [string, McpConfigured] => isMcpConfigured(entry[1]))
|
|
||||||
}
|
|
||||||
|
|
||||||
function oauthServers(config: Config.Info) {
|
|
||||||
return configuredServers(config).filter(
|
|
||||||
(entry): entry is [string, McpRemote] => isMcpRemote(entry[1]) && entry[1].oauth !== false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function listState() {
|
|
||||||
return AppRuntime.runPromise(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const cfg = yield* Config.Service
|
|
||||||
const mcp = yield* MCP.Service
|
|
||||||
const config = yield* cfg.get()
|
|
||||||
const statuses = yield* mcp.status()
|
|
||||||
const stored = yield* Effect.all(
|
|
||||||
Object.fromEntries(configuredServers(config).map(([name]) => [name, mcp.hasStoredTokens(name)])),
|
|
||||||
{ concurrency: "unbounded" },
|
|
||||||
)
|
|
||||||
return { config, statuses, stored }
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function authState() {
|
|
||||||
return AppRuntime.runPromise(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const cfg = yield* Config.Service
|
|
||||||
const mcp = yield* MCP.Service
|
|
||||||
const config = yield* cfg.get()
|
|
||||||
const auth = yield* Effect.all(
|
|
||||||
Object.fromEntries(oauthServers(config).map(([name]) => [name, mcp.getAuthStatus(name)])),
|
|
||||||
{ concurrency: "unbounded" },
|
|
||||||
)
|
|
||||||
return { config, auth }
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const McpCommand = cmd({
|
export const McpCommand = cmd({
|
||||||
command: "mcp",
|
command: "mcp",
|
||||||
describe: "manage MCP (Model Context Protocol) servers",
|
describe: "manage MCP (Model Context Protocol) servers",
|
||||||
@@ -118,8 +75,13 @@ export const McpListCommand = cmd({
|
|||||||
UI.empty()
|
UI.empty()
|
||||||
prompts.intro("MCP Servers")
|
prompts.intro("MCP Servers")
|
||||||
|
|
||||||
const { config, statuses, stored } = await listState()
|
const config = await Config.get()
|
||||||
const servers = configuredServers(config)
|
const mcpServers = config.mcp ?? {}
|
||||||
|
const statuses = await MCP.status()
|
||||||
|
|
||||||
|
const servers = Object.entries(mcpServers).filter((entry): entry is [string, McpConfigured] =>
|
||||||
|
isMcpConfigured(entry[1]),
|
||||||
|
)
|
||||||
|
|
||||||
if (servers.length === 0) {
|
if (servers.length === 0) {
|
||||||
prompts.log.warn("No MCP servers configured")
|
prompts.log.warn("No MCP servers configured")
|
||||||
@@ -130,7 +92,7 @@ export const McpListCommand = cmd({
|
|||||||
for (const [name, serverConfig] of servers) {
|
for (const [name, serverConfig] of servers) {
|
||||||
const status = statuses[name]
|
const status = statuses[name]
|
||||||
const hasOAuth = isMcpRemote(serverConfig) && !!serverConfig.oauth
|
const hasOAuth = isMcpRemote(serverConfig) && !!serverConfig.oauth
|
||||||
const hasStoredTokens = stored[name]
|
const hasStoredTokens = await MCP.hasStoredTokens(name)
|
||||||
|
|
||||||
let statusIcon: string
|
let statusIcon: string
|
||||||
let statusText: string
|
let statusText: string
|
||||||
@@ -190,11 +152,15 @@ export const McpAuthCommand = cmd({
|
|||||||
UI.empty()
|
UI.empty()
|
||||||
prompts.intro("MCP OAuth Authentication")
|
prompts.intro("MCP OAuth Authentication")
|
||||||
|
|
||||||
const { config, auth } = await authState()
|
const config = await Config.get()
|
||||||
const mcpServers = config.mcp ?? {}
|
const mcpServers = config.mcp ?? {}
|
||||||
const servers = oauthServers(config)
|
|
||||||
|
|
||||||
if (servers.length === 0) {
|
// Get OAuth-capable servers (remote servers with oauth not explicitly disabled)
|
||||||
|
const oauthServers = Object.entries(mcpServers).filter(
|
||||||
|
(entry): entry is [string, McpRemote] => isMcpRemote(entry[1]) && entry[1].oauth !== false,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (oauthServers.length === 0) {
|
||||||
prompts.log.warn("No OAuth-capable MCP servers configured")
|
prompts.log.warn("No OAuth-capable MCP servers configured")
|
||||||
prompts.log.info("Remote MCP servers support OAuth by default. Add a remote server in opencode.json:")
|
prompts.log.info("Remote MCP servers support OAuth by default. Add a remote server in opencode.json:")
|
||||||
prompts.log.info(`
|
prompts.log.info(`
|
||||||
@@ -211,17 +177,19 @@ export const McpAuthCommand = cmd({
|
|||||||
let serverName = args.name
|
let serverName = args.name
|
||||||
if (!serverName) {
|
if (!serverName) {
|
||||||
// Build options with auth status
|
// Build options with auth status
|
||||||
const options = servers.map(([name, cfg]) => {
|
const options = await Promise.all(
|
||||||
const authStatus = auth[name]
|
oauthServers.map(async ([name, cfg]) => {
|
||||||
const icon = getAuthStatusIcon(authStatus)
|
const authStatus = await MCP.getAuthStatus(name)
|
||||||
const statusText = getAuthStatusText(authStatus)
|
const icon = getAuthStatusIcon(authStatus)
|
||||||
const url = cfg.url
|
const statusText = getAuthStatusText(authStatus)
|
||||||
return {
|
const url = cfg.url
|
||||||
label: `${icon} ${name} (${statusText})`,
|
return {
|
||||||
value: name,
|
label: `${icon} ${name} (${statusText})`,
|
||||||
hint: url,
|
value: name,
|
||||||
}
|
hint: url,
|
||||||
})
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
const selected = await prompts.select({
|
const selected = await prompts.select({
|
||||||
message: "Select MCP server to authenticate",
|
message: "Select MCP server to authenticate",
|
||||||
@@ -245,8 +213,7 @@ export const McpAuthCommand = cmd({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if already authenticated
|
// Check if already authenticated
|
||||||
const authStatus =
|
const authStatus = await MCP.getAuthStatus(serverName)
|
||||||
auth[serverName] ?? (await AppRuntime.runPromise(MCP.Service.use((mcp) => mcp.getAuthStatus(serverName))))
|
|
||||||
if (authStatus === "authenticated") {
|
if (authStatus === "authenticated") {
|
||||||
const confirm = await prompts.confirm({
|
const confirm = await prompts.confirm({
|
||||||
message: `${serverName} already has valid credentials. Re-authenticate?`,
|
message: `${serverName} already has valid credentials. Re-authenticate?`,
|
||||||
@@ -273,7 +240,7 @@ export const McpAuthCommand = cmd({
|
|||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const status = await AppRuntime.runPromise(MCP.Service.use((mcp) => mcp.authenticate(serverName)))
|
const status = await MCP.authenticate(serverName)
|
||||||
|
|
||||||
if (status.status === "connected") {
|
if (status.status === "connected") {
|
||||||
spinner.stop("Authentication successful!")
|
spinner.stop("Authentication successful!")
|
||||||
@@ -322,17 +289,22 @@ export const McpAuthListCommand = cmd({
|
|||||||
UI.empty()
|
UI.empty()
|
||||||
prompts.intro("MCP OAuth Status")
|
prompts.intro("MCP OAuth Status")
|
||||||
|
|
||||||
const { config, auth } = await authState()
|
const config = await Config.get()
|
||||||
const servers = oauthServers(config)
|
const mcpServers = config.mcp ?? {}
|
||||||
|
|
||||||
if (servers.length === 0) {
|
// Get OAuth-capable servers
|
||||||
|
const oauthServers = Object.entries(mcpServers).filter(
|
||||||
|
(entry): entry is [string, McpRemote] => isMcpRemote(entry[1]) && entry[1].oauth !== false,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (oauthServers.length === 0) {
|
||||||
prompts.log.warn("No OAuth-capable MCP servers configured")
|
prompts.log.warn("No OAuth-capable MCP servers configured")
|
||||||
prompts.outro("Done")
|
prompts.outro("Done")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [name, serverConfig] of servers) {
|
for (const [name, serverConfig] of oauthServers) {
|
||||||
const authStatus = auth[name]
|
const authStatus = await MCP.getAuthStatus(name)
|
||||||
const icon = getAuthStatusIcon(authStatus)
|
const icon = getAuthStatusIcon(authStatus)
|
||||||
const statusText = getAuthStatusText(authStatus)
|
const statusText = getAuthStatusText(authStatus)
|
||||||
const url = serverConfig.url
|
const url = serverConfig.url
|
||||||
@@ -340,7 +312,7 @@ export const McpAuthListCommand = cmd({
|
|||||||
prompts.log.info(`${icon} ${name} ${UI.Style.TEXT_DIM}${statusText}\n ${UI.Style.TEXT_DIM}${url}`)
|
prompts.log.info(`${icon} ${name} ${UI.Style.TEXT_DIM}${statusText}\n ${UI.Style.TEXT_DIM}${url}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
prompts.outro(`${servers.length} OAuth-capable server(s)`)
|
prompts.outro(`${oauthServers.length} OAuth-capable server(s)`)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -362,7 +334,7 @@ export const McpLogoutCommand = cmd({
|
|||||||
prompts.intro("MCP OAuth Logout")
|
prompts.intro("MCP OAuth Logout")
|
||||||
|
|
||||||
const authPath = path.join(Global.Path.data, "mcp-auth.json")
|
const authPath = path.join(Global.Path.data, "mcp-auth.json")
|
||||||
const credentials = await AppRuntime.runPromise(McpAuth.Service.use((auth) => auth.all()))
|
const credentials = await McpAuth.all()
|
||||||
const serverNames = Object.keys(credentials)
|
const serverNames = Object.keys(credentials)
|
||||||
|
|
||||||
if (serverNames.length === 0) {
|
if (serverNames.length === 0) {
|
||||||
@@ -400,7 +372,7 @@ export const McpLogoutCommand = cmd({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await AppRuntime.runPromise(MCP.Service.use((mcp) => mcp.removeAuth(serverName)))
|
await MCP.removeAuth(serverName)
|
||||||
prompts.log.success(`Removed OAuth credentials for ${serverName}`)
|
prompts.log.success(`Removed OAuth credentials for ${serverName}`)
|
||||||
prompts.outro("Done")
|
prompts.outro("Done")
|
||||||
},
|
},
|
||||||
@@ -623,7 +595,7 @@ export const McpDebugCommand = cmd({
|
|||||||
UI.empty()
|
UI.empty()
|
||||||
prompts.intro("MCP OAuth Debug")
|
prompts.intro("MCP OAuth Debug")
|
||||||
|
|
||||||
const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.get()))
|
const config = await Config.get()
|
||||||
const mcpServers = config.mcp ?? {}
|
const mcpServers = config.mcp ?? {}
|
||||||
const serverName = args.name
|
const serverName = args.name
|
||||||
|
|
||||||
@@ -650,18 +622,10 @@ export const McpDebugCommand = cmd({
|
|||||||
prompts.log.info(`URL: ${serverConfig.url}`)
|
prompts.log.info(`URL: ${serverConfig.url}`)
|
||||||
|
|
||||||
// Check stored auth status
|
// Check stored auth status
|
||||||
const { authStatus, entry } = await AppRuntime.runPromise(
|
const authStatus = await MCP.getAuthStatus(serverName)
|
||||||
Effect.gen(function* () {
|
|
||||||
const mcp = yield* MCP.Service
|
|
||||||
const auth = yield* McpAuth.Service
|
|
||||||
return {
|
|
||||||
authStatus: yield* mcp.getAuthStatus(serverName),
|
|
||||||
entry: yield* auth.get(serverName),
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
prompts.log.info(`Auth status: ${getAuthStatusIcon(authStatus)} ${getAuthStatusText(authStatus)}`)
|
prompts.log.info(`Auth status: ${getAuthStatusIcon(authStatus)} ${getAuthStatusText(authStatus)}`)
|
||||||
|
|
||||||
|
const entry = await McpAuth.get(serverName)
|
||||||
if (entry?.tokens) {
|
if (entry?.tokens) {
|
||||||
prompts.log.info(` Access token: ${entry.tokens.accessToken.substring(0, 20)}...`)
|
prompts.log.info(` Access token: ${entry.tokens.accessToken.substring(0, 20)}...`)
|
||||||
if (entry.tokens.expiresAt) {
|
if (entry.tokens.expiresAt) {
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import { ModelsDev } from "../../provider/models"
|
|||||||
import { cmd } from "./cmd"
|
import { cmd } from "./cmd"
|
||||||
import { UI } from "../ui"
|
import { UI } from "../ui"
|
||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
import { Effect } from "effect"
|
|
||||||
|
|
||||||
export const ModelsCommand = cmd({
|
export const ModelsCommand = cmd({
|
||||||
command: "models [provider]",
|
command: "models [provider]",
|
||||||
@@ -37,51 +35,43 @@ export const ModelsCommand = cmd({
|
|||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: process.cwd(),
|
directory: process.cwd(),
|
||||||
async fn() {
|
async fn() {
|
||||||
await AppRuntime.runPromise(
|
const providers = await Provider.list()
|
||||||
Effect.gen(function* () {
|
|
||||||
const svc = yield* Provider.Service
|
|
||||||
const providers = yield* svc.list()
|
|
||||||
|
|
||||||
const print = (providerID: ProviderID, verbose?: boolean) => {
|
function printModels(providerID: ProviderID, verbose?: boolean) {
|
||||||
const provider = providers[providerID]
|
const provider = providers[providerID]
|
||||||
const sorted = Object.entries(provider.models).sort(([a], [b]) => a.localeCompare(b))
|
const sortedModels = Object.entries(provider.models).sort(([a], [b]) => a.localeCompare(b))
|
||||||
for (const [modelID, model] of sorted) {
|
for (const [modelID, model] of sortedModels) {
|
||||||
process.stdout.write(`${providerID}/${modelID}`)
|
process.stdout.write(`${providerID}/${modelID}`)
|
||||||
process.stdout.write(EOL)
|
process.stdout.write(EOL)
|
||||||
if (verbose) {
|
if (verbose) {
|
||||||
process.stdout.write(JSON.stringify(model, null, 2))
|
process.stdout.write(JSON.stringify(model, null, 2))
|
||||||
process.stdout.write(EOL)
|
process.stdout.write(EOL)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (args.provider) {
|
if (args.provider) {
|
||||||
const providerID = ProviderID.make(args.provider)
|
const provider = providers[ProviderID.make(args.provider)]
|
||||||
const provider = providers[providerID]
|
if (!provider) {
|
||||||
if (!provider) {
|
UI.error(`Provider not found: ${args.provider}`)
|
||||||
yield* Effect.sync(() => UI.error(`Provider not found: ${args.provider}`))
|
return
|
||||||
return
|
}
|
||||||
}
|
|
||||||
|
|
||||||
yield* Effect.sync(() => print(providerID, args.verbose))
|
printModels(ProviderID.make(args.provider), args.verbose)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const ids = Object.keys(providers).sort((a, b) => {
|
const providerIDs = Object.keys(providers).sort((a, b) => {
|
||||||
const aIsOpencode = a.startsWith("opencode")
|
const aIsOpencode = a.startsWith("opencode")
|
||||||
const bIsOpencode = b.startsWith("opencode")
|
const bIsOpencode = b.startsWith("opencode")
|
||||||
if (aIsOpencode && !bIsOpencode) return -1
|
if (aIsOpencode && !bIsOpencode) return -1
|
||||||
if (!aIsOpencode && bIsOpencode) return 1
|
if (!aIsOpencode && bIsOpencode) return 1
|
||||||
return a.localeCompare(b)
|
return a.localeCompare(b)
|
||||||
})
|
})
|
||||||
|
|
||||||
yield* Effect.sync(() => {
|
for (const providerID of providerIDs) {
|
||||||
for (const providerID of ids) {
|
printModels(ProviderID.make(providerID), args.verbose)
|
||||||
print(ProviderID.make(providerID), args.verbose)
|
}
|
||||||
}
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { UI } from "../ui"
|
import { UI } from "../ui"
|
||||||
import { cmd } from "./cmd"
|
import { cmd } from "./cmd"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
import { Git } from "@/git"
|
import { Git } from "@/git"
|
||||||
import { Instance } from "@/project/instance"
|
import { Instance } from "@/project/instance"
|
||||||
import { Process } from "@/util/process"
|
import { Process } from "@/util/process"
|
||||||
@@ -68,29 +67,19 @@ export const PrCommand = cmd({
|
|||||||
const remoteName = forkOwner
|
const remoteName = forkOwner
|
||||||
|
|
||||||
// Check if remote already exists
|
// Check if remote already exists
|
||||||
const remotes = await AppRuntime.runPromise(
|
const remotes = (await Git.run(["remote"], { cwd: Instance.worktree })).text().trim()
|
||||||
Git.Service.use((git) => git.run(["remote"], { cwd: Instance.worktree })),
|
|
||||||
).then((x) => x.text().trim())
|
|
||||||
if (!remotes.split("\n").includes(remoteName)) {
|
if (!remotes.split("\n").includes(remoteName)) {
|
||||||
await AppRuntime.runPromise(
|
await Git.run(["remote", "add", remoteName, `https://github.com/${forkOwner}/${forkName}.git`], {
|
||||||
Git.Service.use((git) =>
|
cwd: Instance.worktree,
|
||||||
git.run(["remote", "add", remoteName, `https://github.com/${forkOwner}/${forkName}.git`], {
|
})
|
||||||
cwd: Instance.worktree,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
UI.println(`Added fork remote: ${remoteName}`)
|
UI.println(`Added fork remote: ${remoteName}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set upstream to the fork so pushes go there
|
// Set upstream to the fork so pushes go there
|
||||||
const headRefName = prInfo.headRefName
|
const headRefName = prInfo.headRefName
|
||||||
await AppRuntime.runPromise(
|
await Git.run(["branch", `--set-upstream-to=${remoteName}/${headRefName}`, localBranchName], {
|
||||||
Git.Service.use((git) =>
|
cwd: Instance.worktree,
|
||||||
git.run(["branch", `--set-upstream-to=${remoteName}/${headRefName}`, localBranchName], {
|
})
|
||||||
cwd: Instance.worktree,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for opencode session link in PR body
|
// Check for opencode session link in PR body
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Auth } from "../../auth"
|
import { Auth } from "../../auth"
|
||||||
import { AppRuntime } from "../../effect/app-runtime"
|
|
||||||
import { cmd } from "./cmd"
|
import { cmd } from "./cmd"
|
||||||
import * as prompts from "@clack/prompts"
|
import * as prompts from "@clack/prompts"
|
||||||
import { UI } from "../ui"
|
import { UI } from "../ui"
|
||||||
@@ -14,18 +13,9 @@ import { Instance } from "../../project/instance"
|
|||||||
import type { Hooks } from "@opencode-ai/plugin"
|
import type { Hooks } from "@opencode-ai/plugin"
|
||||||
import { Process } from "../../util/process"
|
import { Process } from "../../util/process"
|
||||||
import { text } from "node:stream/consumers"
|
import { text } from "node:stream/consumers"
|
||||||
import { Effect } from "effect"
|
|
||||||
|
|
||||||
type PluginAuth = NonNullable<Hooks["auth"]>
|
type PluginAuth = NonNullable<Hooks["auth"]>
|
||||||
|
|
||||||
const put = (key: string, info: Auth.Info) =>
|
|
||||||
AppRuntime.runPromise(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const auth = yield* Auth.Service
|
|
||||||
yield* auth.set(key, info)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string, methodName?: string): Promise<boolean> {
|
async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string, methodName?: string): Promise<boolean> {
|
||||||
let index = 0
|
let index = 0
|
||||||
if (methodName) {
|
if (methodName) {
|
||||||
@@ -103,7 +93,7 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string,
|
|||||||
const saveProvider = result.provider ?? provider
|
const saveProvider = result.provider ?? provider
|
||||||
if ("refresh" in result) {
|
if ("refresh" in result) {
|
||||||
const { type: _, provider: __, refresh, access, expires, ...extraFields } = result
|
const { type: _, provider: __, refresh, access, expires, ...extraFields } = result
|
||||||
await put(saveProvider, {
|
await Auth.set(saveProvider, {
|
||||||
type: "oauth",
|
type: "oauth",
|
||||||
refresh,
|
refresh,
|
||||||
access,
|
access,
|
||||||
@@ -112,7 +102,7 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string,
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if ("key" in result) {
|
if ("key" in result) {
|
||||||
await put(saveProvider, {
|
await Auth.set(saveProvider, {
|
||||||
type: "api",
|
type: "api",
|
||||||
key: result.key,
|
key: result.key,
|
||||||
})
|
})
|
||||||
@@ -135,7 +125,7 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string,
|
|||||||
const saveProvider = result.provider ?? provider
|
const saveProvider = result.provider ?? provider
|
||||||
if ("refresh" in result) {
|
if ("refresh" in result) {
|
||||||
const { type: _, provider: __, refresh, access, expires, ...extraFields } = result
|
const { type: _, provider: __, refresh, access, expires, ...extraFields } = result
|
||||||
await put(saveProvider, {
|
await Auth.set(saveProvider, {
|
||||||
type: "oauth",
|
type: "oauth",
|
||||||
refresh,
|
refresh,
|
||||||
access,
|
access,
|
||||||
@@ -144,7 +134,7 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string,
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if ("key" in result) {
|
if ("key" in result) {
|
||||||
await put(saveProvider, {
|
await Auth.set(saveProvider, {
|
||||||
type: "api",
|
type: "api",
|
||||||
key: result.key,
|
key: result.key,
|
||||||
})
|
})
|
||||||
@@ -158,12 +148,6 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (method.type === "api") {
|
if (method.type === "api") {
|
||||||
const key = await prompts.password({
|
|
||||||
message: "Enter your API key",
|
|
||||||
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
|
|
||||||
})
|
|
||||||
if (prompts.isCancel(key)) throw new UI.CancelledError()
|
|
||||||
|
|
||||||
if (method.authorize) {
|
if (method.authorize) {
|
||||||
const result = await method.authorize(inputs)
|
const result = await method.authorize(inputs)
|
||||||
if (result.type === "failed") {
|
if (result.type === "failed") {
|
||||||
@@ -171,9 +155,9 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string,
|
|||||||
}
|
}
|
||||||
if (result.type === "success") {
|
if (result.type === "success") {
|
||||||
const saveProvider = result.provider ?? provider
|
const saveProvider = result.provider ?? provider
|
||||||
await put(saveProvider, {
|
await Auth.set(saveProvider, {
|
||||||
type: "api",
|
type: "api",
|
||||||
key: result.key ?? key,
|
key: result.key,
|
||||||
})
|
})
|
||||||
prompts.log.success("Login successful")
|
prompts.log.success("Login successful")
|
||||||
}
|
}
|
||||||
@@ -231,12 +215,7 @@ export const ProvidersListCommand = cmd({
|
|||||||
const homedir = os.homedir()
|
const homedir = os.homedir()
|
||||||
const displayPath = authPath.startsWith(homedir) ? authPath.replace(homedir, "~") : authPath
|
const displayPath = authPath.startsWith(homedir) ? authPath.replace(homedir, "~") : authPath
|
||||||
prompts.intro(`Credentials ${UI.Style.TEXT_DIM}${displayPath}`)
|
prompts.intro(`Credentials ${UI.Style.TEXT_DIM}${displayPath}`)
|
||||||
const results = await AppRuntime.runPromise(
|
const results = Object.entries(await Auth.all())
|
||||||
Effect.gen(function* () {
|
|
||||||
const auth = yield* Auth.Service
|
|
||||||
return Object.entries(yield* auth.all())
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
const database = await ModelsDev.get()
|
const database = await ModelsDev.get()
|
||||||
|
|
||||||
for (const [providerID, result] of results) {
|
for (const [providerID, result] of results) {
|
||||||
@@ -315,7 +294,7 @@ export const ProvidersLoginCommand = cmd({
|
|||||||
prompts.outro("Done")
|
prompts.outro("Done")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await put(url, {
|
await Auth.set(url, {
|
||||||
type: "wellknown",
|
type: "wellknown",
|
||||||
key: wellknown.auth.env,
|
key: wellknown.auth.env,
|
||||||
token: token.trim(),
|
token: token.trim(),
|
||||||
@@ -326,7 +305,7 @@ export const ProvidersLoginCommand = cmd({
|
|||||||
}
|
}
|
||||||
await ModelsDev.refresh(true).catch(() => {})
|
await ModelsDev.refresh(true).catch(() => {})
|
||||||
|
|
||||||
const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.get()))
|
const config = await Config.get()
|
||||||
|
|
||||||
const disabled = new Set(config.disabled_providers ?? [])
|
const disabled = new Set(config.disabled_providers ?? [])
|
||||||
const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined
|
const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined
|
||||||
@@ -462,7 +441,7 @@ export const ProvidersLoginCommand = cmd({
|
|||||||
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
|
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
|
||||||
})
|
})
|
||||||
if (prompts.isCancel(key)) throw new UI.CancelledError()
|
if (prompts.isCancel(key)) throw new UI.CancelledError()
|
||||||
await put(provider, {
|
await Auth.set(provider, {
|
||||||
type: "api",
|
type: "api",
|
||||||
key,
|
key,
|
||||||
})
|
})
|
||||||
@@ -478,33 +457,22 @@ export const ProvidersLogoutCommand = cmd({
|
|||||||
describe: "log out from a configured provider",
|
describe: "log out from a configured provider",
|
||||||
async handler(_args) {
|
async handler(_args) {
|
||||||
UI.empty()
|
UI.empty()
|
||||||
const credentials: Array<[string, Auth.Info]> = await AppRuntime.runPromise(
|
const credentials = await Auth.all().then((x) => Object.entries(x))
|
||||||
Effect.gen(function* () {
|
|
||||||
const auth = yield* Auth.Service
|
|
||||||
return Object.entries(yield* auth.all())
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
prompts.intro("Remove credential")
|
prompts.intro("Remove credential")
|
||||||
if (credentials.length === 0) {
|
if (credentials.length === 0) {
|
||||||
prompts.log.error("No credentials found")
|
prompts.log.error("No credentials found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const database = await ModelsDev.get()
|
const database = await ModelsDev.get()
|
||||||
const selected = await prompts.select({
|
const providerID = await prompts.select({
|
||||||
message: "Select provider",
|
message: "Select provider",
|
||||||
options: credentials.map(([key, value]) => ({
|
options: credentials.map(([key, value]) => ({
|
||||||
label: (database[key]?.name || key) + UI.Style.TEXT_DIM + " (" + value.type + ")",
|
label: (database[key]?.name || key) + UI.Style.TEXT_DIM + " (" + value.type + ")",
|
||||||
value: key,
|
value: key,
|
||||||
})),
|
})),
|
||||||
})
|
})
|
||||||
if (prompts.isCancel(selected)) throw new UI.CancelledError()
|
if (prompts.isCancel(providerID)) throw new UI.CancelledError()
|
||||||
const providerID = selected as string
|
await Auth.remove(providerID)
|
||||||
await AppRuntime.runPromise(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const auth = yield* Auth.Service
|
|
||||||
yield* auth.remove(providerID)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
prompts.outro("Logout successful")
|
prompts.outro("Logout successful")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { render, TimeToFirstDraw, useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid"
|
import { render, TimeToFirstDraw, useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid"
|
||||||
import { Clipboard } from "@tui/util/clipboard"
|
import { Clipboard } from "@tui/util/clipboard"
|
||||||
import { Selection } from "@tui/util/selection"
|
import { Selection } from "@tui/util/selection"
|
||||||
import { Terminal } from "@tui/util/terminal"
|
|
||||||
import { createCliRenderer, MouseButton, type CliRendererConfig } from "@opentui/core"
|
import { createCliRenderer, MouseButton, type CliRendererConfig } from "@opentui/core"
|
||||||
import { RouteProvider, useRoute } from "@tui/context/route"
|
import { RouteProvider, useRoute } from "@tui/context/route"
|
||||||
import {
|
import {
|
||||||
@@ -61,6 +60,66 @@ import { TuiConfig } from "@/config/tui"
|
|||||||
import { createTuiApi, TuiPluginRuntime, type RouteMap } from "./plugin"
|
import { createTuiApi, TuiPluginRuntime, type RouteMap } from "./plugin"
|
||||||
import { FormatError, FormatUnknownError } from "@/cli/error"
|
import { FormatError, FormatUnknownError } from "@/cli/error"
|
||||||
|
|
||||||
|
async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
|
||||||
|
// can't set raw mode if not a TTY
|
||||||
|
if (!process.stdin.isTTY) return "dark"
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let timeout: NodeJS.Timeout
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
process.stdin.setRawMode(false)
|
||||||
|
process.stdin.removeListener("data", handler)
|
||||||
|
clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handler = (data: Buffer) => {
|
||||||
|
const str = data.toString()
|
||||||
|
const match = str.match(/\x1b]11;([^\x07\x1b]+)/)
|
||||||
|
if (match) {
|
||||||
|
cleanup()
|
||||||
|
const color = match[1]
|
||||||
|
// Parse RGB values from color string
|
||||||
|
// Formats: rgb:RR/GG/BB or #RRGGBB or rgb(R,G,B)
|
||||||
|
let r = 0,
|
||||||
|
g = 0,
|
||||||
|
b = 0
|
||||||
|
|
||||||
|
if (color.startsWith("rgb:")) {
|
||||||
|
const parts = color.substring(4).split("/")
|
||||||
|
r = parseInt(parts[0], 16) >> 8 // Convert 16-bit to 8-bit
|
||||||
|
g = parseInt(parts[1], 16) >> 8 // Convert 16-bit to 8-bit
|
||||||
|
b = parseInt(parts[2], 16) >> 8 // Convert 16-bit to 8-bit
|
||||||
|
} else if (color.startsWith("#")) {
|
||||||
|
r = parseInt(color.substring(1, 3), 16)
|
||||||
|
g = parseInt(color.substring(3, 5), 16)
|
||||||
|
b = parseInt(color.substring(5, 7), 16)
|
||||||
|
} else if (color.startsWith("rgb(")) {
|
||||||
|
const parts = color.substring(4, color.length - 1).split(",")
|
||||||
|
r = parseInt(parts[0])
|
||||||
|
g = parseInt(parts[1])
|
||||||
|
b = parseInt(parts[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate luminance using relative luminance formula
|
||||||
|
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
|
||||||
|
|
||||||
|
// Determine if dark or light based on luminance threshold
|
||||||
|
resolve(luminance > 0.5 ? "light" : "dark")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
process.stdin.setRawMode(true)
|
||||||
|
process.stdin.on("data", handler)
|
||||||
|
process.stdout.write("\x1b]11;?\x07")
|
||||||
|
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
cleanup()
|
||||||
|
resolve("dark")
|
||||||
|
}, 1000)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
import type { EventSource } from "./context/sdk"
|
import type { EventSource } from "./context/sdk"
|
||||||
import { DialogVariant } from "./component/dialog-variant"
|
import { DialogVariant } from "./component/dialog-variant"
|
||||||
|
|
||||||
@@ -119,7 +178,7 @@ export function tui(input: {
|
|||||||
const unguard = win32InstallCtrlCGuard()
|
const unguard = win32InstallCtrlCGuard()
|
||||||
win32DisableProcessedInput()
|
win32DisableProcessedInput()
|
||||||
|
|
||||||
const mode = await Terminal.getTerminalBackgroundColor()
|
const mode = await getTerminalBackgroundColor()
|
||||||
|
|
||||||
// Re-clear after getTerminalBackgroundColor() — setRawMode(false) restores
|
// Re-clear after getTerminalBackgroundColor() — setRawMode(false) restores
|
||||||
// the original console mode which re-enables ENABLE_PROCESSED_INPUT.
|
// the original console mode which re-enables ENABLE_PROCESSED_INPUT.
|
||||||
|
|||||||
@@ -9,12 +9,6 @@ import { setTimeout as sleep } from "node:timers/promises"
|
|||||||
import { useSDK } from "../context/sdk"
|
import { useSDK } from "../context/sdk"
|
||||||
import { useToast } from "../ui/toast"
|
import { useToast } from "../ui/toast"
|
||||||
|
|
||||||
type Adaptor = {
|
|
||||||
type: string
|
|
||||||
name: string
|
|
||||||
description: string
|
|
||||||
}
|
|
||||||
|
|
||||||
function scoped(sdk: ReturnType<typeof useSDK>, sync: ReturnType<typeof useSync>, workspaceID: string) {
|
function scoped(sdk: ReturnType<typeof useSDK>, sync: ReturnType<typeof useSync>, workspaceID: string) {
|
||||||
return createOpencodeClient({
|
return createOpencodeClient({
|
||||||
baseUrl: sdk.url,
|
baseUrl: sdk.url,
|
||||||
@@ -69,27 +63,9 @@ export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) =
|
|||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const [creating, setCreating] = createSignal<string>()
|
const [creating, setCreating] = createSignal<string>()
|
||||||
const [adaptors, setAdaptors] = createSignal<Adaptor[]>()
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
dialog.setSize("medium")
|
dialog.setSize("medium")
|
||||||
void (async () => {
|
|
||||||
const dir = sync.path.directory || sdk.directory
|
|
||||||
const url = new URL("/experimental/workspace/adaptor", sdk.url)
|
|
||||||
if (dir) url.searchParams.set("directory", dir)
|
|
||||||
const res = await sdk
|
|
||||||
.fetch(url)
|
|
||||||
.then((x) => x.json() as Promise<Adaptor[]>)
|
|
||||||
.catch(() => undefined)
|
|
||||||
if (!res) {
|
|
||||||
toast.show({
|
|
||||||
message: "Failed to load workspace adaptors",
|
|
||||||
variant: "error",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setAdaptors(res)
|
|
||||||
})()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const options = createMemo(() => {
|
const options = createMemo(() => {
|
||||||
@@ -103,21 +79,13 @@ export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) =
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
const list = adaptors()
|
return [
|
||||||
if (!list) {
|
{
|
||||||
return [
|
title: "Worktree",
|
||||||
{
|
value: "worktree" as const,
|
||||||
title: "Loading workspaces...",
|
description: "Create a local git worktree",
|
||||||
value: "loading" as const,
|
},
|
||||||
description: "Fetching available workspace adaptors",
|
]
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
return list.map((item) => ({
|
|
||||||
title: item.name,
|
|
||||||
value: item.type,
|
|
||||||
description: item.description,
|
|
||||||
}))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const create = async (type: string) => {
|
const create = async (type: string) => {
|
||||||
@@ -145,7 +113,7 @@ export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) =
|
|||||||
skipFilter={true}
|
skipFilter={true}
|
||||||
options={options()}
|
options={options()}
|
||||||
onSelect={(option) => {
|
onSelect={(option) => {
|
||||||
if (option.value === "creating" || option.value === "loading") return
|
if (option.value === "creating") return
|
||||||
void create(option.value)
|
void create(option.value)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -589,13 +589,6 @@ export function Prompt(props: PromptProps) {
|
|||||||
])
|
])
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
// IME: double-defer may fire before onContentChange flushes the last
|
|
||||||
// composed character (e.g. Korean hangul) to the store, so read
|
|
||||||
// plainText directly and sync before any downstream reads.
|
|
||||||
if (input && !input.isDestroyed && input.plainText !== store.prompt.input) {
|
|
||||||
setStore("prompt", "input", input.plainText)
|
|
||||||
syncExtmarksWithPromptParts()
|
|
||||||
}
|
|
||||||
if (props.disabled) return
|
if (props.disabled) return
|
||||||
if (autocomplete?.visible) return
|
if (autocomplete?.visible) return
|
||||||
if (!store.prompt.input) return
|
if (!store.prompt.input) return
|
||||||
@@ -1001,11 +994,7 @@ export function Prompt(props: PromptProps) {
|
|||||||
input.cursorOffset = input.plainText.length
|
input.cursorOffset = input.plainText.length
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onSubmit={() => {
|
onSubmit={submit}
|
||||||
// IME: double-defer so the last composed character (e.g. Korean
|
|
||||||
// hangul) is flushed to plainText before we read it for submission.
|
|
||||||
setTimeout(() => setTimeout(() => submit(), 0), 0)
|
|
||||||
}}
|
|
||||||
onPaste={async (event: PasteEvent) => {
|
onPaste={async (event: PasteEvent) => {
|
||||||
if (props.disabled) {
|
if (props.disabled) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|||||||
@@ -137,18 +137,12 @@ export const TuiThreadCommand = cmd({
|
|||||||
),
|
),
|
||||||
})
|
})
|
||||||
worker.onerror = (e) => {
|
worker.onerror = (e) => {
|
||||||
Log.Default.error("thread error", {
|
Log.Default.error(e)
|
||||||
message: e.message,
|
|
||||||
filename: e.filename,
|
|
||||||
lineno: e.lineno,
|
|
||||||
colno: e.colno,
|
|
||||||
error: e.error,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = Rpc.client<typeof rpc>(worker)
|
const client = Rpc.client<typeof rpc>(worker)
|
||||||
const error = (e: unknown) => {
|
const error = (e: unknown) => {
|
||||||
Log.Default.error("process error", { error: errorMessage(e) })
|
Log.Default.error(e)
|
||||||
}
|
}
|
||||||
const reload = () => {
|
const reload = () => {
|
||||||
client.call("reload", undefined).catch((err) => {
|
client.call("reload", undefined).catch((err) => {
|
||||||
|
|||||||
@@ -2,28 +2,6 @@ import { RGBA } from "@opentui/core"
|
|||||||
|
|
||||||
export namespace Terminal {
|
export namespace Terminal {
|
||||||
export type Colors = Awaited<ReturnType<typeof colors>>
|
export type Colors = Awaited<ReturnType<typeof colors>>
|
||||||
|
|
||||||
function parse(color: string): RGBA | null {
|
|
||||||
if (color.startsWith("rgb:")) {
|
|
||||||
const parts = color.substring(4).split("/")
|
|
||||||
return RGBA.fromInts(parseInt(parts[0], 16) >> 8, parseInt(parts[1], 16) >> 8, parseInt(parts[2], 16) >> 8, 255)
|
|
||||||
}
|
|
||||||
if (color.startsWith("#")) {
|
|
||||||
return RGBA.fromHex(color)
|
|
||||||
}
|
|
||||||
if (color.startsWith("rgb(")) {
|
|
||||||
const parts = color.substring(4, color.length - 1).split(",")
|
|
||||||
return RGBA.fromInts(parseInt(parts[0]), parseInt(parts[1]), parseInt(parts[2]), 255)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
function mode(bg: RGBA | null): "dark" | "light" {
|
|
||||||
if (!bg) return "dark"
|
|
||||||
const luminance = (0.299 * bg.r + 0.587 * bg.g + 0.114 * bg.b) / 255
|
|
||||||
return luminance > 0.5 ? "light" : "dark"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query terminal colors including background, foreground, and palette (0-15).
|
* Query terminal colors including background, foreground, and palette (0-15).
|
||||||
* Uses OSC escape sequences to retrieve actual terminal color values.
|
* Uses OSC escape sequences to retrieve actual terminal color values.
|
||||||
@@ -53,26 +31,46 @@ export namespace Terminal {
|
|||||||
clearTimeout(timeout)
|
clearTimeout(timeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const parseColor = (colorStr: string): RGBA | null => {
|
||||||
|
if (colorStr.startsWith("rgb:")) {
|
||||||
|
const parts = colorStr.substring(4).split("/")
|
||||||
|
return RGBA.fromInts(
|
||||||
|
parseInt(parts[0], 16) >> 8, // Convert 16-bit to 8-bit
|
||||||
|
parseInt(parts[1], 16) >> 8,
|
||||||
|
parseInt(parts[2], 16) >> 8,
|
||||||
|
255,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (colorStr.startsWith("#")) {
|
||||||
|
return RGBA.fromHex(colorStr)
|
||||||
|
}
|
||||||
|
if (colorStr.startsWith("rgb(")) {
|
||||||
|
const parts = colorStr.substring(4, colorStr.length - 1).split(",")
|
||||||
|
return RGBA.fromInts(parseInt(parts[0]), parseInt(parts[1]), parseInt(parts[2]), 255)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
const handler = (data: Buffer) => {
|
const handler = (data: Buffer) => {
|
||||||
const str = data.toString()
|
const str = data.toString()
|
||||||
|
|
||||||
// Match OSC 11 (background color)
|
// Match OSC 11 (background color)
|
||||||
const bgMatch = str.match(/\x1b]11;([^\x07\x1b]+)/)
|
const bgMatch = str.match(/\x1b]11;([^\x07\x1b]+)/)
|
||||||
if (bgMatch) {
|
if (bgMatch) {
|
||||||
background = parse(bgMatch[1])
|
background = parseColor(bgMatch[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Match OSC 10 (foreground color)
|
// Match OSC 10 (foreground color)
|
||||||
const fgMatch = str.match(/\x1b]10;([^\x07\x1b]+)/)
|
const fgMatch = str.match(/\x1b]10;([^\x07\x1b]+)/)
|
||||||
if (fgMatch) {
|
if (fgMatch) {
|
||||||
foreground = parse(fgMatch[1])
|
foreground = parseColor(fgMatch[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Match OSC 4 (palette colors)
|
// Match OSC 4 (palette colors)
|
||||||
const paletteMatches = str.matchAll(/\x1b]4;(\d+);([^\x07\x1b]+)/g)
|
const paletteMatches = str.matchAll(/\x1b]4;(\d+);([^\x07\x1b]+)/g)
|
||||||
for (const match of paletteMatches) {
|
for (const match of paletteMatches) {
|
||||||
const index = parseInt(match[1])
|
const index = parseInt(match[1])
|
||||||
const color = parse(match[2])
|
const color = parseColor(match[2])
|
||||||
if (color) paletteColors[index] = color
|
if (color) paletteColors[index] = color
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,36 +100,15 @@ export namespace Terminal {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep startup mode detection separate from `colors()`: the TUI boot path only
|
|
||||||
// needs OSC 11 and should resolve on the first background response instead of
|
|
||||||
// waiting on the full palette query used by system theme generation.
|
|
||||||
export async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
|
export async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
|
||||||
if (!process.stdin.isTTY) return "dark"
|
const result = await colors()
|
||||||
|
if (!result.background) return "dark"
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
const { r, g, b } = result.background
|
||||||
let timeout: NodeJS.Timeout
|
// Calculate luminance using relative luminance formula
|
||||||
|
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
|
||||||
|
|
||||||
const cleanup = () => {
|
// Determine if dark or light based on luminance threshold
|
||||||
process.stdin.setRawMode(false)
|
return luminance > 0.5 ? "light" : "dark"
|
||||||
process.stdin.removeListener("data", handler)
|
|
||||||
clearTimeout(timeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handler = (data: Buffer) => {
|
|
||||||
const match = data.toString().match(/\x1b]11;([^\x07\x1b]+)/)
|
|
||||||
if (!match) return
|
|
||||||
cleanup()
|
|
||||||
resolve(mode(parse(match[1])))
|
|
||||||
}
|
|
||||||
|
|
||||||
process.stdin.setRawMode(true)
|
|
||||||
process.stdin.on("data", handler)
|
|
||||||
process.stdout.write("\x1b]11;?\x07")
|
|
||||||
|
|
||||||
timeout = setTimeout(() => {
|
|
||||||
cleanup()
|
|
||||||
resolve("dark")
|
|
||||||
}, 1000)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import { Rpc } from "@/util/rpc"
|
|||||||
import { upgrade } from "@/cli/upgrade"
|
import { upgrade } from "@/cli/upgrade"
|
||||||
import { Config } from "@/config/config"
|
import { Config } from "@/config/config"
|
||||||
import { GlobalBus } from "@/bus/global"
|
import { GlobalBus } from "@/bus/global"
|
||||||
|
import type { GlobalEvent } from "@opencode-ai/sdk/v2"
|
||||||
import { Flag } from "@/flag/flag"
|
import { Flag } from "@/flag/flag"
|
||||||
import { writeHeapSnapshot } from "node:v8"
|
import { writeHeapSnapshot } from "node:v8"
|
||||||
import { Heap } from "@/cli/heap"
|
import { Heap } from "@/cli/heap"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
|
|
||||||
await Log.init({
|
await Log.init({
|
||||||
print: process.argv.includes("--print-logs"),
|
print: process.argv.includes("--print-logs"),
|
||||||
@@ -74,14 +74,14 @@ export const rpc = {
|
|||||||
async checkUpgrade(input: { directory: string }) {
|
async checkUpgrade(input: { directory: string }) {
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: input.directory,
|
directory: input.directory,
|
||||||
init: () => AppRuntime.runPromise(InstanceBootstrap),
|
init: InstanceBootstrap,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
await upgrade().catch(() => {})
|
await upgrade().catch(() => {})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
async reload() {
|
async reload() {
|
||||||
await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.invalidate(true)))
|
await Config.invalidate(true)
|
||||||
},
|
},
|
||||||
async shutdown() {
|
async shutdown() {
|
||||||
Log.Default.info("worker shutting down")
|
Log.Default.info("worker shutting down")
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { Argv, InferredOptionTypes } from "yargs"
|
import type { Argv, InferredOptionTypes } from "yargs"
|
||||||
import { Config } from "../config/config"
|
import { Config } from "../config/config"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
port: {
|
port: {
|
||||||
@@ -38,7 +37,7 @@ export function withNetworkOptions<T>(yargs: Argv<T>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function resolveNetworkOptions(args: NetworkOptions) {
|
export async function resolveNetworkOptions(args: NetworkOptions) {
|
||||||
const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.getGlobal()))
|
const config = await Config.getGlobal()
|
||||||
const portExplicitlySet = process.argv.includes("--port")
|
const portExplicitlySet = process.argv.includes("--port")
|
||||||
const hostnameExplicitlySet = process.argv.includes("--hostname")
|
const hostnameExplicitlySet = process.argv.includes("--hostname")
|
||||||
const mdnsExplicitlySet = process.argv.includes("--mdns")
|
const mdnsExplicitlySet = process.argv.includes("--mdns")
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Flag } from "@/flag/flag"
|
|||||||
import { Installation } from "@/installation"
|
import { Installation } from "@/installation"
|
||||||
|
|
||||||
export async function upgrade() {
|
export async function upgrade() {
|
||||||
const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.getGlobal()))
|
const config = await Config.getGlobal()
|
||||||
const method = await AppRuntime.runPromise(Installation.Service.use((svc) => svc.method()))
|
const method = await AppRuntime.runPromise(Installation.Service.use((svc) => svc.method()))
|
||||||
const latest = await AppRuntime.runPromise(Installation.Service.use((svc) => svc.latest(method))).catch(() => {})
|
const latest = await AppRuntime.runPromise(Installation.Service.use((svc) => svc.latest(method))).catch(() => {})
|
||||||
if (!latest) return
|
if (!latest) return
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { BusEvent } from "@/bus/bus-event"
|
import { BusEvent } from "@/bus/bus-event"
|
||||||
import { InstanceState } from "@/effect/instance-state"
|
import { InstanceState } from "@/effect/instance-state"
|
||||||
|
import { makeRuntime } from "@/effect/run-service"
|
||||||
import type { InstanceContext } from "@/project/instance"
|
import type { InstanceContext } from "@/project/instance"
|
||||||
import { SessionID, MessageID } from "@/session/schema"
|
import { SessionID, MessageID } from "@/session/schema"
|
||||||
import { Effect, Layer, Context } from "effect"
|
import { Effect, Layer, Context } from "effect"
|
||||||
@@ -188,4 +189,10 @@ export namespace Command {
|
|||||||
Layer.provide(MCP.defaultLayer),
|
Layer.provide(MCP.defaultLayer),
|
||||||
Layer.provide(Skill.defaultLayer),
|
Layer.provide(Skill.defaultLayer),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||||
|
|
||||||
|
export async function list() {
|
||||||
|
return runPromise((svc) => svc.list())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { pathToFileURL } from "url"
|
|||||||
import os from "os"
|
import os from "os"
|
||||||
import { Process } from "../util/process"
|
import { Process } from "../util/process"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
|
import { ModelsDev } from "../provider/models"
|
||||||
import { mergeDeep, pipe, unique } from "remeda"
|
import { mergeDeep, pipe, unique } from "remeda"
|
||||||
import { Global } from "../global"
|
import { Global } from "../global"
|
||||||
import fsNode from "fs/promises"
|
import fsNode from "fs/promises"
|
||||||
@@ -22,18 +23,21 @@ import { Instance, type InstanceContext } from "../project/instance"
|
|||||||
import { LSPServer } from "../lsp/server"
|
import { LSPServer } from "../lsp/server"
|
||||||
import { Installation } from "@/installation"
|
import { Installation } from "@/installation"
|
||||||
import { ConfigMarkdown } from "./markdown"
|
import { ConfigMarkdown } from "./markdown"
|
||||||
import { existsSync } from "fs"
|
import { constants, existsSync } from "fs"
|
||||||
import { Bus } from "@/bus"
|
import { Bus } from "@/bus"
|
||||||
import { GlobalBus } from "@/bus/global"
|
import { GlobalBus } from "@/bus/global"
|
||||||
import { Event } from "../server/event"
|
import { Event } from "../server/event"
|
||||||
import { Glob } from "../util/glob"
|
import { Glob } from "../util/glob"
|
||||||
|
import { iife } from "@/util/iife"
|
||||||
import { Account } from "@/account"
|
import { Account } from "@/account"
|
||||||
import { isRecord } from "@/util/record"
|
import { isRecord } from "@/util/record"
|
||||||
import { ConfigPaths } from "./paths"
|
import { ConfigPaths } from "./paths"
|
||||||
|
import { Filesystem } from "@/util/filesystem"
|
||||||
import type { ConsoleState } from "./console-state"
|
import type { ConsoleState } from "./console-state"
|
||||||
import { AppFileSystem } from "@/filesystem"
|
import { AppFileSystem } from "@/filesystem"
|
||||||
import { InstanceState } from "@/effect/instance-state"
|
import { InstanceState } from "@/effect/instance-state"
|
||||||
import { Context, Duration, Effect, Exit, Fiber, Layer, Option } from "effect"
|
import { makeRuntime } from "@/effect/run-service"
|
||||||
|
import { Duration, Effect, Layer, Option, Context } from "effect"
|
||||||
import { Flock } from "@/util/flock"
|
import { Flock } from "@/util/flock"
|
||||||
import { isPathPluginSpec, parsePluginSpecifier, resolvePathPluginTarget } from "@/plugin/shared"
|
import { isPathPluginSpec, parsePluginSpecifier, resolvePathPluginTarget } from "@/plugin/shared"
|
||||||
import { Npm } from "@/npm"
|
import { Npm } from "@/npm"
|
||||||
@@ -137,11 +141,53 @@ export namespace Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type InstallInput = {
|
export type InstallInput = {
|
||||||
|
signal?: AbortSignal
|
||||||
waitTick?: (input: { dir: string; attempt: number; delay: number; waited: number }) => void | Promise<void>
|
waitTick?: (input: { dir: string; attempt: number; delay: number; waited: number }) => void | Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
type Package = {
|
export async function installDependencies(dir: string, input?: InstallInput) {
|
||||||
dependencies?: Record<string, string>
|
if (!(await isWritable(dir))) return
|
||||||
|
await using _ = await Flock.acquire(`config-install:${Filesystem.resolve(dir)}`, {
|
||||||
|
signal: input?.signal,
|
||||||
|
onWait: (tick) =>
|
||||||
|
input?.waitTick?.({
|
||||||
|
dir,
|
||||||
|
attempt: tick.attempt,
|
||||||
|
delay: tick.delay,
|
||||||
|
waited: tick.waited,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
input?.signal?.throwIfAborted()
|
||||||
|
|
||||||
|
const pkg = path.join(dir, "package.json")
|
||||||
|
const target = Installation.isLocal() ? "*" : Installation.VERSION
|
||||||
|
const json = await Filesystem.readJson<{ dependencies?: Record<string, string> }>(pkg).catch(() => ({
|
||||||
|
dependencies: {},
|
||||||
|
}))
|
||||||
|
json.dependencies = {
|
||||||
|
...json.dependencies,
|
||||||
|
"@opencode-ai/plugin": target,
|
||||||
|
}
|
||||||
|
await Filesystem.writeJson(pkg, json)
|
||||||
|
|
||||||
|
const gitignore = path.join(dir, ".gitignore")
|
||||||
|
const ignore = await Filesystem.exists(gitignore)
|
||||||
|
if (!ignore) {
|
||||||
|
await Filesystem.write(
|
||||||
|
gitignore,
|
||||||
|
["node_modules", "package.json", "package-lock.json", "bun.lock", ".gitignore"].join("\n"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
await Npm.install(dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isWritable(dir: string) {
|
||||||
|
try {
|
||||||
|
await fsNode.access(dir, constants.W_OK)
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function rel(item: string, patterns: string[]) {
|
function rel(item: string, patterns: string[]) {
|
||||||
@@ -1066,7 +1112,7 @@ export namespace Config {
|
|||||||
type State = {
|
type State = {
|
||||||
config: Info
|
config: Info
|
||||||
directories: string[]
|
directories: string[]
|
||||||
deps: Fiber.Fiber<void, never>[]
|
deps: Promise<void>[]
|
||||||
consoleState: ConsoleState
|
consoleState: ConsoleState
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1074,7 +1120,6 @@ export namespace Config {
|
|||||||
readonly get: () => Effect.Effect<Info>
|
readonly get: () => Effect.Effect<Info>
|
||||||
readonly getGlobal: () => Effect.Effect<Info>
|
readonly getGlobal: () => Effect.Effect<Info>
|
||||||
readonly getConsoleState: () => Effect.Effect<ConsoleState>
|
readonly getConsoleState: () => Effect.Effect<ConsoleState>
|
||||||
readonly installDependencies: (dir: string, input?: InstallInput) => Effect.Effect<void, AppFileSystem.Error>
|
|
||||||
readonly update: (config: Info) => Effect.Effect<void>
|
readonly update: (config: Info) => Effect.Effect<void>
|
||||||
readonly updateGlobal: (config: Info) => Effect.Effect<Info>
|
readonly updateGlobal: (config: Info) => Effect.Effect<Info>
|
||||||
readonly invalidate: (wait?: boolean) => Effect.Effect<void>
|
readonly invalidate: (wait?: boolean) => Effect.Effect<void>
|
||||||
@@ -1276,74 +1321,6 @@ export namespace Config {
|
|||||||
return yield* cachedGlobal
|
return yield* cachedGlobal
|
||||||
})
|
})
|
||||||
|
|
||||||
const install = Effect.fnUntraced(function* (dir: string) {
|
|
||||||
const pkg = path.join(dir, "package.json")
|
|
||||||
const gitignore = path.join(dir, ".gitignore")
|
|
||||||
const plugin = path.join(dir, "node_modules", "@opencode-ai", "plugin", "package.json")
|
|
||||||
const target = Installation.isLocal() ? "*" : Installation.VERSION
|
|
||||||
const json = yield* fs.readJson(pkg).pipe(
|
|
||||||
Effect.catch(() => Effect.succeed({} satisfies Package)),
|
|
||||||
Effect.map((x): Package => (isRecord(x) ? (x as Package) : {})),
|
|
||||||
)
|
|
||||||
const hasDep = json.dependencies?.["@opencode-ai/plugin"] === target
|
|
||||||
const hasIgnore = yield* fs.existsSafe(gitignore)
|
|
||||||
const hasPkg = yield* fs.existsSafe(plugin)
|
|
||||||
|
|
||||||
if (!hasDep) {
|
|
||||||
yield* fs.writeJson(pkg, {
|
|
||||||
...json,
|
|
||||||
dependencies: {
|
|
||||||
...json.dependencies,
|
|
||||||
"@opencode-ai/plugin": target,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasIgnore) {
|
|
||||||
yield* fs.writeFileString(
|
|
||||||
gitignore,
|
|
||||||
["node_modules", "package.json", "package-lock.json", "bun.lock", ".gitignore"].join("\n"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasDep && hasIgnore && hasPkg) return
|
|
||||||
|
|
||||||
yield* Effect.promise(() => Npm.install(dir))
|
|
||||||
})
|
|
||||||
|
|
||||||
const installDependencies = Effect.fn("Config.installDependencies")(function* (
|
|
||||||
dir: string,
|
|
||||||
input?: InstallInput,
|
|
||||||
) {
|
|
||||||
if (
|
|
||||||
!(yield* fs.access(dir, { writable: true }).pipe(
|
|
||||||
Effect.as(true),
|
|
||||||
Effect.orElseSucceed(() => false),
|
|
||||||
))
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
const key =
|
|
||||||
process.platform === "win32" ? "config-install:win32" : `config-install:${AppFileSystem.resolve(dir)}`
|
|
||||||
|
|
||||||
yield* Effect.acquireUseRelease(
|
|
||||||
Effect.promise((signal) =>
|
|
||||||
Flock.acquire(key, {
|
|
||||||
signal,
|
|
||||||
onWait: (tick) =>
|
|
||||||
input?.waitTick?.({
|
|
||||||
dir,
|
|
||||||
attempt: tick.attempt,
|
|
||||||
delay: tick.delay,
|
|
||||||
waited: tick.waited,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
() => install(dir),
|
|
||||||
(lease) => Effect.promise(() => lease.release()),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const loadInstanceState = Effect.fnUntraced(function* (ctx: InstanceContext) {
|
const loadInstanceState = Effect.fnUntraced(function* (ctx: InstanceContext) {
|
||||||
const auth = yield* authSvc.all().pipe(Effect.orDie)
|
const auth = yield* authSvc.all().pipe(Effect.orDie)
|
||||||
|
|
||||||
@@ -1426,7 +1403,7 @@ export namespace Config {
|
|||||||
log.debug("loading config from OPENCODE_CONFIG_DIR", { path: Flag.OPENCODE_CONFIG_DIR })
|
log.debug("loading config from OPENCODE_CONFIG_DIR", { path: Flag.OPENCODE_CONFIG_DIR })
|
||||||
}
|
}
|
||||||
|
|
||||||
const deps: Fiber.Fiber<void, never>[] = []
|
const deps: Promise<void>[] = []
|
||||||
|
|
||||||
for (const dir of unique(directories)) {
|
for (const dir of unique(directories)) {
|
||||||
if (dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) {
|
if (dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) {
|
||||||
@@ -1440,18 +1417,12 @@ export namespace Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const dep = yield* installDependencies(dir).pipe(
|
const dep = iife(async () => {
|
||||||
Effect.exit,
|
await installDependencies(dir)
|
||||||
Effect.tap((exit) =>
|
})
|
||||||
Exit.isFailure(exit)
|
void dep.catch((err) => {
|
||||||
? Effect.sync(() => {
|
log.warn("background dependency install failed", { dir, error: err })
|
||||||
log.warn("background dependency install failed", { dir, error: String(exit.cause) })
|
})
|
||||||
})
|
|
||||||
: Effect.void,
|
|
||||||
),
|
|
||||||
Effect.asVoid,
|
|
||||||
Effect.forkScoped,
|
|
||||||
)
|
|
||||||
deps.push(dep)
|
deps.push(dep)
|
||||||
|
|
||||||
result.command = mergeDeep(result.command ?? {}, yield* Effect.promise(() => loadCommand(dir)))
|
result.command = mergeDeep(result.command ?? {}, yield* Effect.promise(() => loadCommand(dir)))
|
||||||
@@ -1588,9 +1559,7 @@ export namespace Config {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const waitForDependencies = Effect.fn("Config.waitForDependencies")(function* () {
|
const waitForDependencies = Effect.fn("Config.waitForDependencies")(function* () {
|
||||||
yield* InstanceState.useEffect(state, (s) =>
|
yield* InstanceState.useEffect(state, (s) => Effect.promise(() => Promise.all(s.deps).then(() => undefined)))
|
||||||
Effect.forEach(s.deps, Fiber.join, { concurrency: "unbounded" }).pipe(Effect.asVoid),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const update = Effect.fn("Config.update")(function* (config: Info) {
|
const update = Effect.fn("Config.update")(function* (config: Info) {
|
||||||
@@ -1645,7 +1614,6 @@ export namespace Config {
|
|||||||
get,
|
get,
|
||||||
getGlobal,
|
getGlobal,
|
||||||
getConsoleState,
|
getConsoleState,
|
||||||
installDependencies,
|
|
||||||
update,
|
update,
|
||||||
updateGlobal,
|
updateGlobal,
|
||||||
invalidate,
|
invalidate,
|
||||||
@@ -1660,4 +1628,38 @@ export namespace Config {
|
|||||||
Layer.provide(Auth.defaultLayer),
|
Layer.provide(Auth.defaultLayer),
|
||||||
Layer.provide(Account.defaultLayer),
|
Layer.provide(Account.defaultLayer),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||||
|
|
||||||
|
export async function get() {
|
||||||
|
return runPromise((svc) => svc.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getGlobal() {
|
||||||
|
return runPromise((svc) => svc.getGlobal())
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getConsoleState() {
|
||||||
|
return runPromise((svc) => svc.getConsoleState())
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function update(config: Info) {
|
||||||
|
return runPromise((svc) => svc.update(config))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateGlobal(config: Info) {
|
||||||
|
return runPromise((svc) => svc.updateGlobal(config))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function invalidate(wait = false) {
|
||||||
|
return runPromise((svc) => svc.invalidate(wait))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function directories() {
|
||||||
|
return runPromise((svc) => svc.directories())
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function waitForDependencies() {
|
||||||
|
return runPromise((svc) => svc.waitForDependencies())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import { Flag } from "@/flag/flag"
|
|||||||
import { Log } from "@/util/log"
|
import { Log } from "@/util/log"
|
||||||
import { isRecord } from "@/util/record"
|
import { isRecord } from "@/util/record"
|
||||||
import { Global } from "@/global"
|
import { Global } from "@/global"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
|
|
||||||
export namespace TuiConfig {
|
export namespace TuiConfig {
|
||||||
const log = Log.create({ service: "tui.config" })
|
const log = Log.create({ service: "tui.config" })
|
||||||
@@ -52,7 +51,7 @@ export namespace TuiConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function installDeps(dir: string): Promise<void> {
|
function installDeps(dir: string): Promise<void> {
|
||||||
return AppRuntime.runPromise(Config.Service.use((cfg) => cfg.installDependencies(dir)))
|
return Config.installDependencies(dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function mergeFile(acc: Acc, file: string) {
|
async function mergeFile(acc: Acc, file: string) {
|
||||||
|
|||||||
@@ -1,52 +1,20 @@
|
|||||||
import { lazy } from "@/util/lazy"
|
import { lazy } from "@/util/lazy"
|
||||||
import type { ProjectID } from "@/project/schema"
|
import type { Adaptor } from "../types"
|
||||||
import type { WorkspaceAdaptor } from "../types"
|
|
||||||
|
|
||||||
export type WorkspaceAdaptorEntry = {
|
const ADAPTORS: Record<string, () => Promise<Adaptor>> = {
|
||||||
type: string
|
|
||||||
name: string
|
|
||||||
description: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const BUILTIN: Record<string, () => Promise<WorkspaceAdaptor>> = {
|
|
||||||
worktree: lazy(async () => (await import("./worktree")).WorktreeAdaptor),
|
worktree: lazy(async () => (await import("./worktree")).WorktreeAdaptor),
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = new Map<ProjectID, Map<string, WorkspaceAdaptor>>()
|
export function getAdaptor(type: string): Promise<Adaptor> {
|
||||||
|
return ADAPTORS[type]()
|
||||||
export async function getAdaptor(projectID: ProjectID, type: string): Promise<WorkspaceAdaptor> {
|
|
||||||
const custom = state.get(projectID)?.get(type)
|
|
||||||
if (custom) return custom
|
|
||||||
|
|
||||||
const builtin = BUILTIN[type]
|
|
||||||
if (builtin) return builtin()
|
|
||||||
|
|
||||||
throw new Error(`Unknown workspace adaptor: ${type}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listAdaptors(projectID: ProjectID): Promise<WorkspaceAdaptorEntry[]> {
|
export function installAdaptor(type: string, adaptor: Adaptor) {
|
||||||
const builtin = await Promise.all(
|
// This is experimental: mostly used for testing right now, but we
|
||||||
Object.entries(BUILTIN).map(async ([type, init]) => {
|
// will likely allow this in the future. Need to figure out the
|
||||||
const adaptor = await init()
|
// TypeScript story
|
||||||
return {
|
|
||||||
type,
|
|
||||||
name: adaptor.name,
|
|
||||||
description: adaptor.description,
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
const custom = [...(state.get(projectID)?.entries() ?? [])].map(([type, adaptor]) => ({
|
|
||||||
type,
|
|
||||||
name: adaptor.name,
|
|
||||||
description: adaptor.description,
|
|
||||||
}))
|
|
||||||
return [...builtin, ...custom]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Plugins can be loaded per-project so we need to scope them. If you
|
// @ts-expect-error we force the builtin types right now, but we
|
||||||
// want to install a global one pass `ProjectID.global`
|
// will implement a way to extend the types for custom adaptors
|
||||||
export function registerAdaptor(projectID: ProjectID, type: string, adaptor: WorkspaceAdaptor) {
|
ADAPTORS[type] = () => adaptor
|
||||||
const adaptors = state.get(projectID) ?? new Map<string, WorkspaceAdaptor>()
|
|
||||||
adaptors.set(type, adaptor)
|
|
||||||
state.set(projectID, adaptors)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { Worktree } from "@/worktree"
|
import { Worktree } from "@/worktree"
|
||||||
import { type WorkspaceAdaptor, WorkspaceInfo } from "../types"
|
import { type Adaptor, WorkspaceInfo } from "../types"
|
||||||
|
|
||||||
const WorktreeConfig = z.object({
|
const Config = WorkspaceInfo.extend({
|
||||||
name: WorkspaceInfo.shape.name,
|
name: WorkspaceInfo.shape.name.unwrap(),
|
||||||
branch: WorkspaceInfo.shape.branch.unwrap(),
|
branch: WorkspaceInfo.shape.branch.unwrap(),
|
||||||
directory: WorkspaceInfo.shape.directory.unwrap(),
|
directory: WorkspaceInfo.shape.directory.unwrap(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const WorktreeAdaptor: WorkspaceAdaptor = {
|
type Config = z.infer<typeof Config>
|
||||||
name: "Worktree",
|
|
||||||
description: "Create a git worktree",
|
export const WorktreeAdaptor: Adaptor = {
|
||||||
async configure(info) {
|
async configure(info) {
|
||||||
const worktree = await Worktree.makeWorktreeInfo(undefined)
|
const worktree = await Worktree.makeWorktreeInfo(info.name ?? undefined)
|
||||||
return {
|
return {
|
||||||
...info,
|
...info,
|
||||||
name: worktree.name,
|
name: worktree.name,
|
||||||
@@ -21,7 +21,7 @@ export const WorktreeAdaptor: WorkspaceAdaptor = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async create(info) {
|
async create(info) {
|
||||||
const config = WorktreeConfig.parse(info)
|
const config = Config.parse(info)
|
||||||
await Worktree.createFromInfo({
|
await Worktree.createFromInfo({
|
||||||
name: config.name,
|
name: config.name,
|
||||||
directory: config.directory,
|
directory: config.directory,
|
||||||
@@ -29,11 +29,11 @@ export const WorktreeAdaptor: WorkspaceAdaptor = {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
async remove(info) {
|
async remove(info) {
|
||||||
const config = WorktreeConfig.parse(info)
|
const config = Config.parse(info)
|
||||||
await Worktree.remove({ directory: config.directory })
|
await Worktree.remove({ directory: config.directory })
|
||||||
},
|
},
|
||||||
target(info) {
|
target(info) {
|
||||||
const config = WorktreeConfig.parse(info)
|
const config = Config.parse(info)
|
||||||
return {
|
return {
|
||||||
type: "local",
|
type: "local",
|
||||||
directory: config.directory,
|
directory: config.directory,
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import { WorkspaceID } from "./schema"
|
|||||||
export const WorkspaceInfo = z.object({
|
export const WorkspaceInfo = z.object({
|
||||||
id: WorkspaceID.zod,
|
id: WorkspaceID.zod,
|
||||||
type: z.string(),
|
type: z.string(),
|
||||||
name: z.string(),
|
|
||||||
branch: z.string().nullable(),
|
branch: z.string().nullable(),
|
||||||
|
name: z.string().nullable(),
|
||||||
directory: z.string().nullable(),
|
directory: z.string().nullable(),
|
||||||
extra: z.unknown().nullable(),
|
extra: z.unknown().nullable(),
|
||||||
projectID: ProjectID.zod,
|
projectID: ProjectID.zod,
|
||||||
@@ -24,11 +24,9 @@ export type Target =
|
|||||||
headers?: HeadersInit
|
headers?: HeadersInit
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WorkspaceAdaptor = {
|
export type Adaptor = {
|
||||||
name: string
|
configure(input: WorkspaceInfo): WorkspaceInfo | Promise<WorkspaceInfo>
|
||||||
description: string
|
create(config: WorkspaceInfo, from?: WorkspaceInfo): Promise<void>
|
||||||
configure(info: WorkspaceInfo): WorkspaceInfo | Promise<WorkspaceInfo>
|
remove(config: WorkspaceInfo): Promise<void>
|
||||||
create(info: WorkspaceInfo, from?: WorkspaceInfo): Promise<void>
|
target(config: WorkspaceInfo): Target | Promise<Target>
|
||||||
remove(info: WorkspaceInfo): Promise<void>
|
|
||||||
target(info: WorkspaceInfo): Target | Promise<Target>
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import type { WorkspaceID } from "./schema"
|
|||||||
export const WorkspaceTable = sqliteTable("workspace", {
|
export const WorkspaceTable = sqliteTable("workspace", {
|
||||||
id: text().$type<WorkspaceID>().primaryKey(),
|
id: text().$type<WorkspaceID>().primaryKey(),
|
||||||
type: text().notNull(),
|
type: text().notNull(),
|
||||||
name: text().notNull().default(""),
|
|
||||||
branch: text(),
|
branch: text(),
|
||||||
|
name: text(),
|
||||||
directory: text(),
|
directory: text(),
|
||||||
extra: text({ mode: "json" }),
|
extra: text({ mode: "json" }),
|
||||||
project_id: text()
|
project_id: text()
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { setTimeout as sleep } from "node:timers/promises"
|
import { AppFileSystem } from "@/filesystem"
|
||||||
import { fn } from "@/util/fn"
|
import { makeRuntime } from "@/effect/run-service"
|
||||||
import { Database, eq } from "@/storage/db"
|
import { Database, eq } from "@/storage/db"
|
||||||
import { Project } from "@/project/project"
|
import type { Project } from "@/project/project"
|
||||||
import { BusEvent } from "@/bus/bus-event"
|
import { BusEvent } from "@/bus/bus-event"
|
||||||
import { GlobalBus } from "@/bus/global"
|
import { GlobalBus } from "@/bus/global"
|
||||||
import { SyncEvent } from "@/sync"
|
import { SyncEvent } from "@/sync"
|
||||||
import { Log } from "@/util/log"
|
import { Log } from "@/util/log"
|
||||||
import { Filesystem } from "@/util/filesystem"
|
|
||||||
import { ProjectID } from "@/project/schema"
|
import { ProjectID } from "@/project/schema"
|
||||||
import { Slug } from "@opencode-ai/util/slug"
|
|
||||||
import { WorkspaceTable } from "./workspace.sql"
|
import { WorkspaceTable } from "./workspace.sql"
|
||||||
import { getAdaptor } from "./adaptors"
|
import { getAdaptor } from "./adaptors"
|
||||||
import { WorkspaceInfo } from "./types"
|
import { WorkspaceInfo } from "./types"
|
||||||
import { WorkspaceID } from "./schema"
|
import { WorkspaceID } from "./schema"
|
||||||
import { parseSSE } from "./sse"
|
import { parseSSE } from "./sse"
|
||||||
|
import { Context, Effect, Layer, Scope } from "effect"
|
||||||
|
|
||||||
export namespace Workspace {
|
export namespace Workspace {
|
||||||
export const Info = WorkspaceInfo.meta({
|
export const Info = WorkspaceInfo.meta({
|
||||||
@@ -57,174 +56,250 @@ export namespace Workspace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const CreateInput = z.object({
|
export const CreateInput = z.object({
|
||||||
id: WorkspaceID.zod.optional(),
|
id: WorkspaceID.zod.optional(),
|
||||||
type: Info.shape.type,
|
type: Info.shape.type,
|
||||||
branch: Info.shape.branch,
|
branch: Info.shape.branch,
|
||||||
projectID: ProjectID.zod,
|
projectID: ProjectID.zod,
|
||||||
extra: Info.shape.extra,
|
extra: Info.shape.extra,
|
||||||
})
|
})
|
||||||
|
export type CreateInput = z.infer<typeof CreateInput>
|
||||||
export const create = fn(CreateInput, async (input) => {
|
|
||||||
const id = WorkspaceID.ascending(input.id)
|
|
||||||
const adaptor = await getAdaptor(input.projectID, input.type)
|
|
||||||
|
|
||||||
const config = await adaptor.configure({ ...input, id, name: Slug.create(), directory: null })
|
|
||||||
|
|
||||||
const info: Info = {
|
|
||||||
id,
|
|
||||||
type: config.type,
|
|
||||||
branch: config.branch ?? null,
|
|
||||||
name: config.name ?? null,
|
|
||||||
directory: config.directory ?? null,
|
|
||||||
extra: config.extra ?? null,
|
|
||||||
projectID: input.projectID,
|
|
||||||
}
|
|
||||||
|
|
||||||
Database.use((db) => {
|
|
||||||
db.insert(WorkspaceTable)
|
|
||||||
.values({
|
|
||||||
id: info.id,
|
|
||||||
type: info.type,
|
|
||||||
branch: info.branch,
|
|
||||||
name: info.name,
|
|
||||||
directory: info.directory,
|
|
||||||
extra: info.extra,
|
|
||||||
project_id: info.projectID,
|
|
||||||
})
|
|
||||||
.run()
|
|
||||||
})
|
|
||||||
|
|
||||||
await adaptor.create(config)
|
|
||||||
|
|
||||||
startSync(info)
|
|
||||||
|
|
||||||
return info
|
|
||||||
})
|
|
||||||
|
|
||||||
export function list(project: Project.Info) {
|
|
||||||
const rows = Database.use((db) =>
|
|
||||||
db.select().from(WorkspaceTable).where(eq(WorkspaceTable.project_id, project.id)).all(),
|
|
||||||
)
|
|
||||||
const spaces = rows.map(fromRow).sort((a, b) => a.id.localeCompare(b.id))
|
|
||||||
for (const space of spaces) startSync(space)
|
|
||||||
return spaces
|
|
||||||
}
|
|
||||||
|
|
||||||
export const get = fn(WorkspaceID.zod, async (id) => {
|
|
||||||
const row = Database.use((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, id)).get())
|
|
||||||
if (!row) return
|
|
||||||
const space = fromRow(row)
|
|
||||||
startSync(space)
|
|
||||||
return space
|
|
||||||
})
|
|
||||||
|
|
||||||
export const remove = fn(WorkspaceID.zod, async (id) => {
|
|
||||||
const row = Database.use((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, id)).get())
|
|
||||||
if (row) {
|
|
||||||
stopSync(id)
|
|
||||||
|
|
||||||
const info = fromRow(row)
|
|
||||||
const adaptor = await getAdaptor(info.projectID, row.type)
|
|
||||||
adaptor.remove(info)
|
|
||||||
Database.use((db) => db.delete(WorkspaceTable).where(eq(WorkspaceTable.id, id)).run())
|
|
||||||
return info
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const connections = new Map<WorkspaceID, ConnectionStatus>()
|
|
||||||
const aborts = new Map<WorkspaceID, AbortController>()
|
|
||||||
|
|
||||||
function setStatus(id: WorkspaceID, status: ConnectionStatus["status"], error?: string) {
|
|
||||||
const prev = connections.get(id)
|
|
||||||
if (prev?.status === status && prev?.error === error) return
|
|
||||||
const next = { workspaceID: id, status, error }
|
|
||||||
connections.set(id, next)
|
|
||||||
GlobalBus.emit("event", {
|
|
||||||
directory: "global",
|
|
||||||
workspace: id,
|
|
||||||
payload: {
|
|
||||||
type: Event.Status.type,
|
|
||||||
properties: next,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function status(): ConnectionStatus[] {
|
|
||||||
return [...connections.values()]
|
|
||||||
}
|
|
||||||
|
|
||||||
const log = Log.create({ service: "workspace-sync" })
|
const log = Log.create({ service: "workspace-sync" })
|
||||||
|
|
||||||
async function workspaceEventLoop(space: Info, signal: AbortSignal) {
|
export interface Interface {
|
||||||
log.info("starting sync: " + space.id)
|
readonly create: (input: CreateInput) => Effect.Effect<Info>
|
||||||
|
readonly list: (projectID: ProjectID) => Effect.Effect<Info[]>
|
||||||
|
readonly get: (id: WorkspaceID) => Effect.Effect<Info | undefined>
|
||||||
|
readonly remove: (id: WorkspaceID) => Effect.Effect<Info | undefined>
|
||||||
|
readonly status: () => Effect.Effect<ConnectionStatus[]>
|
||||||
|
}
|
||||||
|
|
||||||
while (!signal.aborted) {
|
export class Service extends Context.Service<Service, Interface>()("@opencode/Workspace") {}
|
||||||
log.info("connecting to sync: " + space.id)
|
|
||||||
|
|
||||||
setStatus(space.id, "connecting")
|
export const layer = Layer.effect(
|
||||||
const adaptor = await getAdaptor(space.projectID, space.type)
|
Service,
|
||||||
const target = await adaptor.target(space)
|
Effect.gen(function* () {
|
||||||
|
const fs = yield* AppFileSystem.Service
|
||||||
|
const scope = yield* Scope.Scope
|
||||||
|
const connections = new Map<WorkspaceID, ConnectionStatus>()
|
||||||
|
const aborts = new Map<WorkspaceID, AbortController>()
|
||||||
|
|
||||||
if (target.type === "local") return
|
yield* Effect.addFinalizer(() =>
|
||||||
|
Effect.sync(() => {
|
||||||
|
for (const abort of aborts.values()) abort.abort()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
const res = await fetch(target.url + "/sync/event", { method: "GET", signal }).catch((err: unknown) => {
|
const db = <T>(fn: (db: Parameters<typeof Database.use>[0] extends (trx: infer D) => any ? D : never) => T) =>
|
||||||
setStatus(space.id, "error", String(err))
|
Effect.sync(() => Database.use(fn))
|
||||||
return undefined
|
|
||||||
|
const setStatus = Effect.fnUntraced(function* (
|
||||||
|
id: WorkspaceID,
|
||||||
|
status: ConnectionStatus["status"],
|
||||||
|
error?: string,
|
||||||
|
) {
|
||||||
|
const prev = connections.get(id)
|
||||||
|
if (prev?.status === status && prev?.error === error) return
|
||||||
|
const next = { workspaceID: id, status, error }
|
||||||
|
connections.set(id, next)
|
||||||
|
GlobalBus.emit("event", {
|
||||||
|
directory: "global",
|
||||||
|
workspace: id,
|
||||||
|
payload: {
|
||||||
|
type: Event.Status.type,
|
||||||
|
properties: next,
|
||||||
|
},
|
||||||
|
})
|
||||||
})
|
})
|
||||||
if (!res || !res.ok || !res.body) {
|
|
||||||
log.info("failed to connect to sync: " + res?.status)
|
|
||||||
|
|
||||||
setStatus(space.id, "error", res ? `HTTP ${res.status}` : "no response")
|
const stopSync = Effect.fnUntraced(function* (id: WorkspaceID) {
|
||||||
await sleep(1000)
|
aborts.get(id)?.abort()
|
||||||
continue
|
aborts.delete(id)
|
||||||
}
|
connections.delete(id)
|
||||||
setStatus(space.id, "connected")
|
})
|
||||||
await parseSSE(res.body, signal, (evt) => {
|
|
||||||
const event = evt as SyncEvent.SerializedEvent
|
|
||||||
|
|
||||||
try {
|
const workspaceEventLoop = Effect.fn("Workspace.workspaceEventLoop")(function* (
|
||||||
if (!event.type.startsWith("server.")) {
|
space: Info,
|
||||||
SyncEvent.replay(event)
|
signal: AbortSignal,
|
||||||
|
) {
|
||||||
|
log.info("starting sync: " + space.id)
|
||||||
|
|
||||||
|
while (!signal.aborted) {
|
||||||
|
log.info("connecting to sync: " + space.id)
|
||||||
|
|
||||||
|
yield* setStatus(space.id, "connecting")
|
||||||
|
const adaptor = yield* Effect.promise(() => getAdaptor(space.type)).pipe(Effect.orDie)
|
||||||
|
const target = yield* Effect.promise(() => Promise.resolve(adaptor.target(space))).pipe(Effect.orDie)
|
||||||
|
|
||||||
|
if (target.type === "local") return
|
||||||
|
|
||||||
|
const res = yield* Effect.tryPromise({
|
||||||
|
try: () => fetch(target.url + "/sync/event", { method: "GET", signal }),
|
||||||
|
catch: (err) => String(err),
|
||||||
|
}).pipe(
|
||||||
|
Effect.catch((err) => setStatus(space.id, "error", err).pipe(Effect.as(undefined as Response | undefined))),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!res || !res.ok || !res.body) {
|
||||||
|
log.info("failed to connect to sync: " + res?.status)
|
||||||
|
yield* setStatus(space.id, "error", res ? `HTTP ${res.status}` : "no response")
|
||||||
|
yield* Effect.sleep("1 second")
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
log.warn("failed to replay sync event", {
|
const body = res.body
|
||||||
workspaceID: space.id,
|
yield* setStatus(space.id, "connected")
|
||||||
error: err,
|
yield* Effect.promise(() =>
|
||||||
})
|
parseSSE(body, signal, (evt) => {
|
||||||
|
const event = evt as SyncEvent.SerializedEvent
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!event.type.startsWith("server.")) {
|
||||||
|
SyncEvent.replay(event)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
log.warn("failed to replay sync event", {
|
||||||
|
workspaceID: space.id,
|
||||||
|
error: err,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
).pipe(Effect.orDie)
|
||||||
|
|
||||||
|
yield* setStatus(space.id, "disconnected")
|
||||||
|
log.info("disconnected to sync: " + space.id)
|
||||||
|
yield* Effect.sleep("250 millis")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
setStatus(space.id, "disconnected")
|
|
||||||
log.info("disconnected to sync: " + space.id)
|
const startSync = Effect.fn("Workspace.startSync")(function* (space: Info) {
|
||||||
await sleep(250)
|
if (space.type === "worktree") {
|
||||||
}
|
yield* Effect.gen(function* () {
|
||||||
|
const exists = yield* fs.exists(space.directory!).pipe(Effect.orDie)
|
||||||
|
yield* setStatus(space.id, exists ? "connected" : "error", exists ? undefined : "directory does not exist")
|
||||||
|
}).pipe(Effect.forkIn(scope))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (aborts.has(space.id)) return
|
||||||
|
|
||||||
|
const abort = new AbortController()
|
||||||
|
aborts.set(space.id, abort)
|
||||||
|
yield* setStatus(space.id, "disconnected")
|
||||||
|
yield* workspaceEventLoop(space, abort.signal).pipe(
|
||||||
|
Effect.catchCause((cause) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* setStatus(space.id, "error", String(cause))
|
||||||
|
yield* Effect.sync(() => {
|
||||||
|
log.warn("workspace sync listener failed", {
|
||||||
|
workspaceID: space.id,
|
||||||
|
error: cause,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
Effect.forkIn(scope),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const create = Effect.fn("Workspace.create")(function* (input: CreateInput) {
|
||||||
|
const id = WorkspaceID.ascending(input.id)
|
||||||
|
const adaptor = yield* Effect.promise(() => getAdaptor(input.type)).pipe(Effect.orDie)
|
||||||
|
const config = yield* Effect.promise(() =>
|
||||||
|
Promise.resolve(adaptor.configure({ ...input, id, name: null, directory: null })),
|
||||||
|
).pipe(Effect.orDie)
|
||||||
|
|
||||||
|
const info: Info = {
|
||||||
|
id,
|
||||||
|
type: config.type,
|
||||||
|
branch: config.branch ?? null,
|
||||||
|
name: config.name ?? null,
|
||||||
|
directory: config.directory ?? null,
|
||||||
|
extra: config.extra ?? null,
|
||||||
|
projectID: input.projectID,
|
||||||
|
}
|
||||||
|
|
||||||
|
yield* db((db) => {
|
||||||
|
db.insert(WorkspaceTable)
|
||||||
|
.values({
|
||||||
|
id: info.id,
|
||||||
|
type: info.type,
|
||||||
|
branch: info.branch,
|
||||||
|
name: info.name,
|
||||||
|
directory: info.directory,
|
||||||
|
extra: info.extra,
|
||||||
|
project_id: info.projectID,
|
||||||
|
})
|
||||||
|
.run()
|
||||||
|
})
|
||||||
|
|
||||||
|
yield* Effect.promise(() => adaptor.create(config)).pipe(Effect.orDie)
|
||||||
|
yield* startSync(info)
|
||||||
|
return info
|
||||||
|
})
|
||||||
|
|
||||||
|
const list = Effect.fn("Workspace.list")(function* (projectID: ProjectID) {
|
||||||
|
const rows = yield* db((db) =>
|
||||||
|
db.select().from(WorkspaceTable).where(eq(WorkspaceTable.project_id, projectID)).all(),
|
||||||
|
)
|
||||||
|
const spaces = rows.map(fromRow).sort((a, b) => a.id.localeCompare(b.id))
|
||||||
|
for (const space of spaces) {
|
||||||
|
yield* startSync(space)
|
||||||
|
}
|
||||||
|
return spaces
|
||||||
|
})
|
||||||
|
|
||||||
|
const get = Effect.fn("Workspace.get")(function* (id: WorkspaceID) {
|
||||||
|
const row = yield* db((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, id)).get())
|
||||||
|
if (!row) return
|
||||||
|
const space = fromRow(row)
|
||||||
|
yield* startSync(space)
|
||||||
|
return space
|
||||||
|
})
|
||||||
|
|
||||||
|
const remove = Effect.fn("Workspace.remove")(function* (id: WorkspaceID) {
|
||||||
|
const row = yield* db((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, id)).get())
|
||||||
|
if (!row) return
|
||||||
|
|
||||||
|
yield* stopSync(id)
|
||||||
|
|
||||||
|
const info = fromRow(row)
|
||||||
|
const adaptor = yield* Effect.promise(() => getAdaptor(row.type)).pipe(Effect.orDie)
|
||||||
|
yield* Effect.sync(() => {
|
||||||
|
void adaptor.remove(info)
|
||||||
|
})
|
||||||
|
yield* db((db) => db.delete(WorkspaceTable).where(eq(WorkspaceTable.id, id)).run())
|
||||||
|
return info
|
||||||
|
})
|
||||||
|
|
||||||
|
const status = Effect.fn("Workspace.status")(() => Effect.succeed([...connections.values()]))
|
||||||
|
|
||||||
|
return Service.of({ create, list, get, remove, status })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
|
||||||
|
|
||||||
|
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||||
|
|
||||||
|
export async function create(input: CreateInput) {
|
||||||
|
return runPromise((svc) => svc.create(input))
|
||||||
}
|
}
|
||||||
|
|
||||||
function startSync(space: Info) {
|
export async function list(project: Project.Info) {
|
||||||
if (space.type === "worktree") {
|
return runPromise((svc) => svc.list(project.id))
|
||||||
void Filesystem.exists(space.directory!).then((exists) => {
|
|
||||||
setStatus(space.id, exists ? "connected" : "error", exists ? undefined : "directory does not exist")
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (aborts.has(space.id)) return
|
|
||||||
const abort = new AbortController()
|
|
||||||
aborts.set(space.id, abort)
|
|
||||||
setStatus(space.id, "disconnected")
|
|
||||||
|
|
||||||
void workspaceEventLoop(space, abort.signal).catch((error) => {
|
|
||||||
setStatus(space.id, "error", String(error))
|
|
||||||
log.warn("workspace sync listener failed", {
|
|
||||||
workspaceID: space.id,
|
|
||||||
error,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopSync(id: WorkspaceID) {
|
export async function get(id: WorkspaceID) {
|
||||||
aborts.get(id)?.abort()
|
return runPromise((svc) => svc.get(id))
|
||||||
aborts.delete(id)
|
}
|
||||||
connections.delete(id)
|
|
||||||
|
export async function remove(id: WorkspaceID) {
|
||||||
|
return runPromise((svc) => svc.remove(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function status() {
|
||||||
|
return runPromise((svc) => svc.status())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ import { ShareNext } from "@/share/share-next"
|
|||||||
import { SessionShare } from "@/share/session"
|
import { SessionShare } from "@/share/session"
|
||||||
|
|
||||||
export const AppLayer = Layer.mergeAll(
|
export const AppLayer = Layer.mergeAll(
|
||||||
// Observability.layer,
|
Observability.layer,
|
||||||
AppFileSystem.defaultLayer,
|
AppFileSystem.defaultLayer,
|
||||||
Bus.defaultLayer,
|
Bus.defaultLayer,
|
||||||
Auth.defaultLayer,
|
Auth.defaultLayer,
|
||||||
@@ -95,6 +95,6 @@ export const AppLayer = Layer.mergeAll(
|
|||||||
Installation.defaultLayer,
|
Installation.defaultLayer,
|
||||||
ShareNext.defaultLayer,
|
ShareNext.defaultLayer,
|
||||||
SessionShare.defaultLayer,
|
SessionShare.defaultLayer,
|
||||||
).pipe(Layer.provide(Observability.layer))
|
)
|
||||||
|
|
||||||
export const AppRuntime = ManagedRuntime.make(AppLayer, { memoMap })
|
export const AppRuntime = ManagedRuntime.make(AppLayer, { memoMap })
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
import { Layer, ManagedRuntime } from "effect"
|
|
||||||
import { memoMap } from "./run-service"
|
|
||||||
|
|
||||||
import { Plugin } from "@/plugin"
|
|
||||||
import { LSP } from "@/lsp"
|
|
||||||
import { FileWatcher } from "@/file/watcher"
|
|
||||||
import { Format } from "@/format"
|
|
||||||
import { ShareNext } from "@/share/share-next"
|
|
||||||
import { File } from "@/file"
|
|
||||||
import { Vcs } from "@/project/vcs"
|
|
||||||
import { Snapshot } from "@/snapshot"
|
|
||||||
import { Bus } from "@/bus"
|
|
||||||
import { Observability } from "./oltp"
|
|
||||||
|
|
||||||
export const BootstrapLayer = Layer.mergeAll(
|
|
||||||
Plugin.defaultLayer,
|
|
||||||
ShareNext.defaultLayer,
|
|
||||||
Format.defaultLayer,
|
|
||||||
LSP.defaultLayer,
|
|
||||||
File.defaultLayer,
|
|
||||||
FileWatcher.defaultLayer,
|
|
||||||
Vcs.defaultLayer,
|
|
||||||
Snapshot.defaultLayer,
|
|
||||||
Bus.defaultLayer,
|
|
||||||
).pipe(Layer.provide(Observability.layer))
|
|
||||||
|
|
||||||
export const BootstrapRuntime = ManagedRuntime.make(BootstrapLayer, { memoMap })
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { BusEvent } from "@/bus/bus-event"
|
import { BusEvent } from "@/bus/bus-event"
|
||||||
import { InstanceState } from "@/effect/instance-state"
|
import { InstanceState } from "@/effect/instance-state"
|
||||||
|
import { makeRuntime } from "@/effect/run-service"
|
||||||
import { AppFileSystem } from "@/filesystem"
|
import { AppFileSystem } from "@/filesystem"
|
||||||
import { Git } from "@/git"
|
import { Git } from "@/git"
|
||||||
import { Effect, Layer, Context } from "effect"
|
import { Effect, Layer, Context } from "effect"
|
||||||
@@ -643,4 +644,26 @@ export namespace File {
|
|||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer), Layer.provide(Git.defaultLayer))
|
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer), Layer.provide(Git.defaultLayer))
|
||||||
|
|
||||||
|
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||||
|
|
||||||
|
export function init() {
|
||||||
|
return runPromise((svc) => svc.init())
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function status() {
|
||||||
|
return runPromise((svc) => svc.status())
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function read(file: string): Promise<Content> {
|
||||||
|
return runPromise((svc) => svc.read(file))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function list(dir?: string) {
|
||||||
|
return runPromise((svc) => svc.list(dir))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function search(input: { query: string; limit?: number; dirs?: boolean; type?: "file" | "directory" }) {
|
||||||
|
return runPromise((svc) => svc.search(input))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import path from "path"
|
|||||||
import { Global } from "../global"
|
import { Global } from "../global"
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { Effect, Layer, Context, Schema } from "effect"
|
import { Effect, Layer, Context } from "effect"
|
||||||
import * as Stream from "effect/Stream"
|
import * as Stream from "effect/Stream"
|
||||||
import { ChildProcess } from "effect/unstable/process"
|
import { ChildProcess } from "effect/unstable/process"
|
||||||
import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
|
import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
|
||||||
@@ -94,43 +94,8 @@ export namespace Ripgrep {
|
|||||||
|
|
||||||
const Result = z.union([Begin, Match, End, Summary])
|
const Result = z.union([Begin, Match, End, Summary])
|
||||||
|
|
||||||
const Hit = Schema.Struct({
|
|
||||||
type: Schema.Literal("match"),
|
|
||||||
data: Schema.Struct({
|
|
||||||
path: Schema.Struct({
|
|
||||||
text: Schema.String,
|
|
||||||
}),
|
|
||||||
lines: Schema.Struct({
|
|
||||||
text: Schema.String,
|
|
||||||
}),
|
|
||||||
line_number: Schema.Number,
|
|
||||||
absolute_offset: Schema.Number,
|
|
||||||
submatches: Schema.mutable(
|
|
||||||
Schema.Array(
|
|
||||||
Schema.Struct({
|
|
||||||
match: Schema.Struct({
|
|
||||||
text: Schema.String,
|
|
||||||
}),
|
|
||||||
start: Schema.Number,
|
|
||||||
end: Schema.Number,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
const Row = Schema.Union([
|
|
||||||
Schema.Struct({ type: Schema.Literal("begin"), data: Schema.Unknown }),
|
|
||||||
Hit,
|
|
||||||
Schema.Struct({ type: Schema.Literal("end"), data: Schema.Unknown }),
|
|
||||||
Schema.Struct({ type: Schema.Literal("summary"), data: Schema.Unknown }),
|
|
||||||
])
|
|
||||||
|
|
||||||
const decode = Schema.decodeUnknownEffect(Schema.fromJsonString(Row))
|
|
||||||
|
|
||||||
export type Result = z.infer<typeof Result>
|
export type Result = z.infer<typeof Result>
|
||||||
export type Match = z.infer<typeof Match>
|
export type Match = z.infer<typeof Match>
|
||||||
export type Item = Match["data"]
|
|
||||||
export type Begin = z.infer<typeof Begin>
|
export type Begin = z.infer<typeof Begin>
|
||||||
export type End = z.infer<typeof End>
|
export type End = z.infer<typeof End>
|
||||||
export type Summary = z.infer<typeof Summary>
|
export type Summary = z.infer<typeof Summary>
|
||||||
@@ -324,13 +289,6 @@ export namespace Ripgrep {
|
|||||||
follow?: boolean
|
follow?: boolean
|
||||||
maxDepth?: number
|
maxDepth?: number
|
||||||
}) => Stream.Stream<string, PlatformError>
|
}) => Stream.Stream<string, PlatformError>
|
||||||
readonly search: (input: {
|
|
||||||
cwd: string
|
|
||||||
pattern: string
|
|
||||||
glob?: string[]
|
|
||||||
limit?: number
|
|
||||||
follow?: boolean
|
|
||||||
}) => Effect.Effect<{ items: Item[]; partial: boolean }, PlatformError | Error>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Ripgrep") {}
|
export class Service extends Context.Service<Service, Interface>()("@opencode/Ripgrep") {}
|
||||||
@@ -340,32 +298,6 @@ export namespace Ripgrep {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const spawner = yield* ChildProcessSpawner
|
const spawner = yield* ChildProcessSpawner
|
||||||
const afs = yield* AppFileSystem.Service
|
const afs = yield* AppFileSystem.Service
|
||||||
const bin = Effect.fn("Ripgrep.path")(function* () {
|
|
||||||
return yield* Effect.promise(() => filepath())
|
|
||||||
})
|
|
||||||
const args = Effect.fn("Ripgrep.args")(function* (input: {
|
|
||||||
mode: "files" | "search"
|
|
||||||
glob?: string[]
|
|
||||||
hidden?: boolean
|
|
||||||
follow?: boolean
|
|
||||||
maxDepth?: number
|
|
||||||
limit?: number
|
|
||||||
pattern?: string
|
|
||||||
}) {
|
|
||||||
const out = [yield* bin(), input.mode === "search" ? "--json" : "--files", "--glob=!.git/*"]
|
|
||||||
if (input.follow) out.push("--follow")
|
|
||||||
if (input.hidden !== false) out.push("--hidden")
|
|
||||||
if (input.maxDepth !== undefined) out.push(`--max-depth=${input.maxDepth}`)
|
|
||||||
if (input.glob) {
|
|
||||||
for (const g of input.glob) {
|
|
||||||
out.push(`--glob=${g}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (input.limit) out.push(`--max-count=${input.limit}`)
|
|
||||||
if (input.mode === "search") out.push("--no-messages")
|
|
||||||
if (input.pattern) out.push("--", input.pattern)
|
|
||||||
return out
|
|
||||||
})
|
|
||||||
|
|
||||||
const files = Effect.fn("Ripgrep.files")(function* (input: {
|
const files = Effect.fn("Ripgrep.files")(function* (input: {
|
||||||
cwd: string
|
cwd: string
|
||||||
@@ -374,7 +306,7 @@ export namespace Ripgrep {
|
|||||||
follow?: boolean
|
follow?: boolean
|
||||||
maxDepth?: number
|
maxDepth?: number
|
||||||
}) {
|
}) {
|
||||||
const rgPath = yield* bin()
|
const rgPath = yield* Effect.promise(() => filepath())
|
||||||
const isDir = yield* afs.isDir(input.cwd)
|
const isDir = yield* afs.isDir(input.cwd)
|
||||||
if (!isDir) {
|
if (!isDir) {
|
||||||
return yield* Effect.die(
|
return yield* Effect.die(
|
||||||
@@ -386,77 +318,23 @@ export namespace Ripgrep {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const cmd = yield* args({
|
const args = [rgPath, "--files", "--glob=!.git/*"]
|
||||||
mode: "files",
|
if (input.follow) args.push("--follow")
|
||||||
glob: input.glob,
|
if (input.hidden !== false) args.push("--hidden")
|
||||||
hidden: input.hidden,
|
if (input.maxDepth !== undefined) args.push(`--max-depth=${input.maxDepth}`)
|
||||||
follow: input.follow,
|
if (input.glob) {
|
||||||
maxDepth: input.maxDepth,
|
for (const g of input.glob) {
|
||||||
})
|
args.push(`--glob=${g}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return spawner
|
return spawner
|
||||||
.streamLines(ChildProcess.make(cmd[0], cmd.slice(1), { cwd: input.cwd }))
|
.streamLines(ChildProcess.make(args[0], args.slice(1), { cwd: input.cwd }))
|
||||||
.pipe(Stream.filter((line: string) => line.length > 0))
|
.pipe(Stream.filter((line: string) => line.length > 0))
|
||||||
})
|
})
|
||||||
|
|
||||||
const search = Effect.fn("Ripgrep.search")(function* (input: {
|
|
||||||
cwd: string
|
|
||||||
pattern: string
|
|
||||||
glob?: string[]
|
|
||||||
limit?: number
|
|
||||||
follow?: boolean
|
|
||||||
}) {
|
|
||||||
return yield* Effect.scoped(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const cmd = yield* args({
|
|
||||||
mode: "search",
|
|
||||||
glob: input.glob,
|
|
||||||
follow: input.follow,
|
|
||||||
limit: input.limit,
|
|
||||||
pattern: input.pattern,
|
|
||||||
})
|
|
||||||
|
|
||||||
const handle = yield* spawner.spawn(
|
|
||||||
ChildProcess.make(cmd[0], cmd.slice(1), {
|
|
||||||
cwd: input.cwd,
|
|
||||||
stdin: "ignore",
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
const [items, stderr, code] = yield* Effect.all(
|
|
||||||
[
|
|
||||||
Stream.decodeText(handle.stdout).pipe(
|
|
||||||
Stream.splitLines,
|
|
||||||
Stream.filter((line) => line.length > 0),
|
|
||||||
Stream.mapEffect((line) =>
|
|
||||||
decode(line).pipe(Effect.mapError((cause) => new Error("invalid ripgrep output", { cause }))),
|
|
||||||
),
|
|
||||||
Stream.filter((row): row is Schema.Schema.Type<typeof Hit> => row.type === "match"),
|
|
||||||
Stream.map((row): Item => row.data),
|
|
||||||
Stream.runCollect,
|
|
||||||
Effect.map((chunk) => [...chunk]),
|
|
||||||
),
|
|
||||||
Stream.mkString(Stream.decodeText(handle.stderr)),
|
|
||||||
handle.exitCode,
|
|
||||||
],
|
|
||||||
{ concurrency: "unbounded" },
|
|
||||||
)
|
|
||||||
|
|
||||||
if (code !== 0 && code !== 1 && code !== 2) {
|
|
||||||
return yield* Effect.fail(new Error(`ripgrep failed: ${stderr}`))
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
items,
|
|
||||||
partial: code === 2,
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
return Service.of({
|
return Service.of({
|
||||||
files: (input) => Stream.unwrap(files(input)),
|
files: (input) => Stream.unwrap(files(input)),
|
||||||
search,
|
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -523,4 +401,46 @@ export namespace Ripgrep {
|
|||||||
|
|
||||||
return lines.join("\n")
|
return lines.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function search(input: {
|
||||||
|
cwd: string
|
||||||
|
pattern: string
|
||||||
|
glob?: string[]
|
||||||
|
limit?: number
|
||||||
|
follow?: boolean
|
||||||
|
}) {
|
||||||
|
const args = [`${await filepath()}`, "--json", "--hidden", "--glob=!.git/*"]
|
||||||
|
if (input.follow) args.push("--follow")
|
||||||
|
|
||||||
|
if (input.glob) {
|
||||||
|
for (const g of input.glob) {
|
||||||
|
args.push(`--glob=${g}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.limit) {
|
||||||
|
args.push(`--max-count=${input.limit}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
args.push("--")
|
||||||
|
args.push(input.pattern)
|
||||||
|
|
||||||
|
const result = await Process.text(args, {
|
||||||
|
cwd: input.cwd,
|
||||||
|
nothrow: true,
|
||||||
|
})
|
||||||
|
if (result.code !== 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle both Unix (\n) and Windows (\r\n) line endings
|
||||||
|
const lines = result.text.trim().split(/\r?\n/).filter(Boolean)
|
||||||
|
// Parse JSON lines from ripgrep output
|
||||||
|
|
||||||
|
return lines
|
||||||
|
.map((line) => JSON.parse(line))
|
||||||
|
.map((parsed) => Result.parse(parsed))
|
||||||
|
.filter((r) => r.type === "match")
|
||||||
|
.map((r) => r.data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { DateTime, Effect, Layer, Option, Semaphore, Context } from "effect"
|
import { DateTime, Effect, Layer, Option, Semaphore, Context } from "effect"
|
||||||
import { InstanceState } from "@/effect/instance-state"
|
import { InstanceState } from "@/effect/instance-state"
|
||||||
|
import { makeRuntime } from "@/effect/run-service"
|
||||||
import { AppFileSystem } from "@/filesystem"
|
import { AppFileSystem } from "@/filesystem"
|
||||||
import { Flag } from "@/flag/flag"
|
import { Flag } from "@/flag/flag"
|
||||||
import type { SessionID } from "@/session/schema"
|
import type { SessionID } from "@/session/schema"
|
||||||
@@ -111,4 +112,22 @@ export namespace FileTime {
|
|||||||
).pipe(Layer.orDie)
|
).pipe(Layer.orDie)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
|
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
|
||||||
|
|
||||||
|
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||||
|
|
||||||
|
export function read(sessionID: SessionID, file: string) {
|
||||||
|
return runPromise((s) => s.read(sessionID, file))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function get(sessionID: SessionID, file: string) {
|
||||||
|
return runPromise((s) => s.get(sessionID, file))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function assert(sessionID: SessionID, filepath: string) {
|
||||||
|
return runPromise((s) => s.assert(sessionID, filepath))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function withLock<T>(filepath: string, fn: () => Promise<T>): Promise<T> {
|
||||||
|
return runPromise((s) => s.withLock(filepath, () => Effect.promise(fn)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import z from "zod"
|
|||||||
import { Bus } from "@/bus"
|
import { Bus } from "@/bus"
|
||||||
import { BusEvent } from "@/bus/bus-event"
|
import { BusEvent } from "@/bus/bus-event"
|
||||||
import { InstanceState } from "@/effect/instance-state"
|
import { InstanceState } from "@/effect/instance-state"
|
||||||
|
import { makeRuntime } from "@/effect/run-service"
|
||||||
import { Flag } from "@/flag/flag"
|
import { Flag } from "@/flag/flag"
|
||||||
import { Git } from "@/git"
|
import { Git } from "@/git"
|
||||||
import { Instance } from "@/project/instance"
|
import { Instance } from "@/project/instance"
|
||||||
@@ -160,4 +161,10 @@ export namespace FileWatcher {
|
|||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer), Layer.provide(Git.defaultLayer))
|
export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer), Layer.provide(Git.defaultLayer))
|
||||||
|
|
||||||
|
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||||
|
|
||||||
|
export function init() {
|
||||||
|
return runPromise((svc) => svc.init())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { text } from "node:stream/consumers"
|
||||||
import { Npm } from "@/npm"
|
import { Npm } from "@/npm"
|
||||||
import { Instance } from "../project/instance"
|
import { Instance } from "../project/instance"
|
||||||
import { Filesystem } from "../util/filesystem"
|
import { Filesystem } from "../util/filesystem"
|
||||||
@@ -216,16 +217,26 @@ export const rlang: Info = {
|
|||||||
name: "air",
|
name: "air",
|
||||||
extensions: [".R"],
|
extensions: [".R"],
|
||||||
async enabled() {
|
async enabled() {
|
||||||
const air = which("air")
|
const airPath = which("air")
|
||||||
if (air == null) return false
|
if (airPath == null) return false
|
||||||
|
|
||||||
const output = await Process.text([air, "--help"], { nothrow: true })
|
try {
|
||||||
|
const proc = Process.spawn(["air", "--help"], {
|
||||||
|
stdout: "pipe",
|
||||||
|
stderr: "pipe",
|
||||||
|
})
|
||||||
|
await proc.exited
|
||||||
|
if (!proc.stdout) return false
|
||||||
|
const output = await text(proc.stdout)
|
||||||
|
|
||||||
// Check for "Air: An R language server and formatter"
|
// Check for "Air: An R language server and formatter"
|
||||||
const firstLine = output.text.split("\n")[0]
|
const firstLine = output.split("\n")[0]
|
||||||
const hasR = firstLine.includes("R language")
|
const hasR = firstLine.includes("R language")
|
||||||
const hasFormatter = firstLine.includes("formatter")
|
const hasFormatter = firstLine.includes("formatter")
|
||||||
if (output.code === 0 && hasR && hasFormatter) return [air, "format", "$FILE"]
|
if (hasR && hasFormatter) return ["air", "format", "$FILE"]
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -235,10 +246,11 @@ export const uvformat: Info = {
|
|||||||
extensions: [".py", ".pyi"],
|
extensions: [".py", ".pyi"],
|
||||||
async enabled() {
|
async enabled() {
|
||||||
if (await ruff.enabled()) return false
|
if (await ruff.enabled()) return false
|
||||||
const uv = which("uv")
|
if (which("uv") !== null) {
|
||||||
if (uv == null) return false
|
const proc = Process.spawn(["uv", "format", "--help"], { stderr: "pipe", stdout: "pipe" })
|
||||||
const output = await Process.run([uv, "format", "--help"], { nothrow: true })
|
const code = await proc.exited
|
||||||
if (output.code === 0) return [uv, "format", "--", "$FILE"]
|
if (code === 0) return ["uv", "format", "--", "$FILE"]
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
|
import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
|
||||||
import { Effect, Layer, Context, Stream } from "effect"
|
import { Effect, Layer, Context, Stream } from "effect"
|
||||||
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||||
|
import { makeRuntime } from "@/effect/run-service"
|
||||||
|
|
||||||
export namespace Git {
|
export namespace Git {
|
||||||
const cfg = [
|
const cfg = [
|
||||||
@@ -257,4 +258,14 @@ export namespace Git {
|
|||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(Layer.provide(CrossSpawnSpawner.defaultLayer))
|
export const defaultLayer = layer.pipe(Layer.provide(CrossSpawnSpawner.defaultLayer))
|
||||||
|
|
||||||
|
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||||
|
|
||||||
|
export async function run(args: string[], opts: Options) {
|
||||||
|
return runPromise((git) => git.run(args, opts))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function defaultBranch(cwd: string) {
|
||||||
|
return runPromise((git) => git.defaultBranch(cwd))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ export namespace Identifier {
|
|||||||
pty: "pty",
|
pty: "pty",
|
||||||
tool: "tool",
|
tool: "tool",
|
||||||
workspace: "wrk",
|
workspace: "wrk",
|
||||||
entry: "ent",
|
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export function schema(prefix: keyof typeof prefixes) {
|
export function schema(prefix: keyof typeof prefixes) {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { Process } from "../util/process"
|
|||||||
import { spawn as lspspawn } from "./launch"
|
import { spawn as lspspawn } from "./launch"
|
||||||
import { Effect, Layer, Context } from "effect"
|
import { Effect, Layer, Context } from "effect"
|
||||||
import { InstanceState } from "@/effect/instance-state"
|
import { InstanceState } from "@/effect/instance-state"
|
||||||
|
import { makeRuntime } from "@/effect/run-service"
|
||||||
|
|
||||||
export namespace LSP {
|
export namespace LSP {
|
||||||
const log = Log.create({ service: "lsp" })
|
const log = Log.create({ service: "lsp" })
|
||||||
@@ -507,6 +508,37 @@ export namespace LSP {
|
|||||||
|
|
||||||
export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer))
|
export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer))
|
||||||
|
|
||||||
|
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||||
|
|
||||||
|
export const init = async () => runPromise((svc) => svc.init())
|
||||||
|
|
||||||
|
export const status = async () => runPromise((svc) => svc.status())
|
||||||
|
|
||||||
|
export const hasClients = async (file: string) => runPromise((svc) => svc.hasClients(file))
|
||||||
|
|
||||||
|
export const touchFile = async (input: string, waitForDiagnostics?: boolean) =>
|
||||||
|
runPromise((svc) => svc.touchFile(input, waitForDiagnostics))
|
||||||
|
|
||||||
|
export const diagnostics = async () => runPromise((svc) => svc.diagnostics())
|
||||||
|
|
||||||
|
export const hover = async (input: LocInput) => runPromise((svc) => svc.hover(input))
|
||||||
|
|
||||||
|
export const definition = async (input: LocInput) => runPromise((svc) => svc.definition(input))
|
||||||
|
|
||||||
|
export const references = async (input: LocInput) => runPromise((svc) => svc.references(input))
|
||||||
|
|
||||||
|
export const implementation = async (input: LocInput) => runPromise((svc) => svc.implementation(input))
|
||||||
|
|
||||||
|
export const documentSymbol = async (uri: string) => runPromise((svc) => svc.documentSymbol(uri))
|
||||||
|
|
||||||
|
export const workspaceSymbol = async (query: string) => runPromise((svc) => svc.workspaceSymbol(query))
|
||||||
|
|
||||||
|
export const prepareCallHierarchy = async (input: LocInput) => runPromise((svc) => svc.prepareCallHierarchy(input))
|
||||||
|
|
||||||
|
export const incomingCalls = async (input: LocInput) => runPromise((svc) => svc.incomingCalls(input))
|
||||||
|
|
||||||
|
export const outgoingCalls = async (input: LocInput) => runPromise((svc) => svc.outgoingCalls(input))
|
||||||
|
|
||||||
export namespace Diagnostic {
|
export namespace Diagnostic {
|
||||||
const MAX_PER_FILE = 20
|
const MAX_PER_FILE = 20
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import open from "open"
|
|||||||
import { Effect, Exit, Layer, Option, Context, Stream } from "effect"
|
import { Effect, Exit, Layer, Option, Context, Stream } from "effect"
|
||||||
import { EffectLogger } from "@/effect/logger"
|
import { EffectLogger } from "@/effect/logger"
|
||||||
import { InstanceState } from "@/effect/instance-state"
|
import { InstanceState } from "@/effect/instance-state"
|
||||||
|
import { makeRuntime } from "@/effect/run-service"
|
||||||
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||||
import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
|
import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
|
||||||
|
|
||||||
@@ -889,4 +890,37 @@ export namespace MCP {
|
|||||||
Layer.provide(CrossSpawnSpawner.defaultLayer),
|
Layer.provide(CrossSpawnSpawner.defaultLayer),
|
||||||
Layer.provide(AppFileSystem.defaultLayer),
|
Layer.provide(AppFileSystem.defaultLayer),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||||
|
|
||||||
|
// --- Async facade functions ---
|
||||||
|
|
||||||
|
export const status = async () => runPromise((svc) => svc.status())
|
||||||
|
|
||||||
|
export const tools = async () => runPromise((svc) => svc.tools())
|
||||||
|
|
||||||
|
export const prompts = async () => runPromise((svc) => svc.prompts())
|
||||||
|
|
||||||
|
export const resources = async () => runPromise((svc) => svc.resources())
|
||||||
|
|
||||||
|
export const add = async (name: string, mcp: Config.Mcp) => runPromise((svc) => svc.add(name, mcp))
|
||||||
|
|
||||||
|
export const connect = async (name: string) => runPromise((svc) => svc.connect(name))
|
||||||
|
|
||||||
|
export const disconnect = async (name: string) => runPromise((svc) => svc.disconnect(name))
|
||||||
|
|
||||||
|
export const startAuth = async (mcpName: string) => runPromise((svc) => svc.startAuth(mcpName))
|
||||||
|
|
||||||
|
export const authenticate = async (mcpName: string) => runPromise((svc) => svc.authenticate(mcpName))
|
||||||
|
|
||||||
|
export const finishAuth = async (mcpName: string, authorizationCode: string) =>
|
||||||
|
runPromise((svc) => svc.finishAuth(mcpName, authorizationCode))
|
||||||
|
|
||||||
|
export const removeAuth = async (mcpName: string) => runPromise((svc) => svc.removeAuth(mcpName))
|
||||||
|
|
||||||
|
export const supportsOAuth = async (mcpName: string) => runPromise((svc) => svc.supportsOAuth(mcpName))
|
||||||
|
|
||||||
|
export const hasStoredTokens = async (mcpName: string) => runPromise((svc) => svc.hasStoredTokens(mcpName))
|
||||||
|
|
||||||
|
export const getAuthStatus = async (mcpName: string) => runPromise((svc) => svc.getAuthStatus(mcpName))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { iife } from "@/util/iife"
|
|||||||
import { Log } from "../../util/log"
|
import { Log } from "../../util/log"
|
||||||
import { setTimeout as sleep } from "node:timers/promises"
|
import { setTimeout as sleep } from "node:timers/promises"
|
||||||
import { CopilotModels } from "./models"
|
import { CopilotModels } from "./models"
|
||||||
import { MessageV2 } from "@/session/message-v2"
|
|
||||||
|
|
||||||
const log = Log.create({ service: "plugin.copilot" })
|
const log = Log.create({ service: "plugin.copilot" })
|
||||||
|
|
||||||
@@ -28,27 +27,11 @@ function base(enterpriseUrl?: string) {
|
|||||||
return enterpriseUrl ? `https://copilot-api.${normalizeDomain(enterpriseUrl)}` : "https://api.githubcopilot.com"
|
return enterpriseUrl ? `https://copilot-api.${normalizeDomain(enterpriseUrl)}` : "https://api.githubcopilot.com"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if a message is a synthetic user msg used to attach an image from a tool call
|
function fix(model: Model): Model {
|
||||||
function imgMsg(msg: any): boolean {
|
|
||||||
if (msg?.role !== "user") return false
|
|
||||||
|
|
||||||
// Handle the 3 api formats
|
|
||||||
|
|
||||||
const content = msg.content
|
|
||||||
if (typeof content === "string") return content === MessageV2.SYNTHETIC_ATTACHMENT_PROMPT
|
|
||||||
if (!Array.isArray(content)) return false
|
|
||||||
return content.some(
|
|
||||||
(part: any) =>
|
|
||||||
(part?.type === "text" || part?.type === "input_text") && part.text === MessageV2.SYNTHETIC_ATTACHMENT_PROMPT,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function fix(model: Model, url: string): Model {
|
|
||||||
return {
|
return {
|
||||||
...model,
|
...model,
|
||||||
api: {
|
api: {
|
||||||
...model.api,
|
...model.api,
|
||||||
url,
|
|
||||||
npm: "@ai-sdk/github-copilot",
|
npm: "@ai-sdk/github-copilot",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -61,23 +44,19 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise<Hooks> {
|
|||||||
id: "github-copilot",
|
id: "github-copilot",
|
||||||
async models(provider, ctx) {
|
async models(provider, ctx) {
|
||||||
if (ctx.auth?.type !== "oauth") {
|
if (ctx.auth?.type !== "oauth") {
|
||||||
return Object.fromEntries(Object.entries(provider.models).map(([id, model]) => [id, fix(model, base())]))
|
return Object.fromEntries(Object.entries(provider.models).map(([id, model]) => [id, fix(model)]))
|
||||||
}
|
}
|
||||||
|
|
||||||
const auth = ctx.auth
|
|
||||||
|
|
||||||
return CopilotModels.get(
|
return CopilotModels.get(
|
||||||
base(auth.enterpriseUrl),
|
base(ctx.auth.enterpriseUrl),
|
||||||
{
|
{
|
||||||
Authorization: `Bearer ${auth.refresh}`,
|
Authorization: `Bearer ${ctx.auth.refresh}`,
|
||||||
"User-Agent": `opencode/${Installation.VERSION}`,
|
"User-Agent": `opencode/${Installation.VERSION}`,
|
||||||
},
|
},
|
||||||
provider.models,
|
provider.models,
|
||||||
).catch((error) => {
|
).catch((error) => {
|
||||||
log.error("failed to fetch copilot models", { error })
|
log.error("failed to fetch copilot models", { error })
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(Object.entries(provider.models).map(([id, model]) => [id, fix(model)]))
|
||||||
Object.entries(provider.models).map(([id, model]) => [id, fix(model, base(auth.enterpriseUrl))]),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -87,7 +66,10 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise<Hooks> {
|
|||||||
const info = await getAuth()
|
const info = await getAuth()
|
||||||
if (!info || info.type !== "oauth") return {}
|
if (!info || info.type !== "oauth") return {}
|
||||||
|
|
||||||
|
const baseURL = base(info.enterpriseUrl)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
baseURL,
|
||||||
apiKey: "",
|
apiKey: "",
|
||||||
async fetch(request: RequestInfo | URL, init?: RequestInit) {
|
async fetch(request: RequestInfo | URL, init?: RequestInit) {
|
||||||
const info = await getAuth()
|
const info = await getAuth()
|
||||||
@@ -106,7 +88,7 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise<Hooks> {
|
|||||||
(msg: any) =>
|
(msg: any) =>
|
||||||
Array.isArray(msg.content) && msg.content.some((part: any) => part.type === "image_url"),
|
Array.isArray(msg.content) && msg.content.some((part: any) => part.type === "image_url"),
|
||||||
),
|
),
|
||||||
isAgent: last?.role !== "user" || imgMsg(last),
|
isAgent: last?.role !== "user",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +100,7 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise<Hooks> {
|
|||||||
(item: any) =>
|
(item: any) =>
|
||||||
Array.isArray(item?.content) && item.content.some((part: any) => part.type === "input_image"),
|
Array.isArray(item?.content) && item.content.some((part: any) => part.type === "input_image"),
|
||||||
),
|
),
|
||||||
isAgent: last?.role !== "user" || imgMsg(last),
|
isAgent: last?.role !== "user",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,7 +122,7 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise<Hooks> {
|
|||||||
part.content.some((nested: any) => nested?.type === "image")),
|
part.content.some((nested: any) => nested?.type === "image")),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
isAgent: !(last?.role === "user" && hasNonToolCalls) || imgMsg(last),
|
isAgent: !(last?.role === "user" && hasNonToolCalls),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|||||||
@@ -52,15 +52,13 @@ export namespace CopilotModels {
|
|||||||
(remote.capabilities.supports.vision ?? false) ||
|
(remote.capabilities.supports.vision ?? false) ||
|
||||||
(remote.capabilities.limits.vision?.supported_media_types ?? []).some((item) => item.startsWith("image/"))
|
(remote.capabilities.limits.vision?.supported_media_types ?? []).some((item) => item.startsWith("image/"))
|
||||||
|
|
||||||
const isMsgApi = remote.supported_endpoints?.includes("/v1/messages")
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: key,
|
id: key,
|
||||||
providerID: "github-copilot",
|
providerID: "github-copilot",
|
||||||
api: {
|
api: {
|
||||||
id: remote.id,
|
id: remote.id,
|
||||||
url: isMsgApi ? `${url}/v1` : url,
|
url,
|
||||||
npm: isMsgApi ? "@ai-sdk/anthropic" : "@ai-sdk/github-copilot",
|
npm: "@ai-sdk/github-copilot",
|
||||||
},
|
},
|
||||||
// API response wins
|
// API response wins
|
||||||
status: "active",
|
status: "active",
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
import type {
|
import type { Hooks, PluginInput, Plugin as PluginInstance, PluginModule } from "@opencode-ai/plugin"
|
||||||
Hooks,
|
|
||||||
PluginInput,
|
|
||||||
Plugin as PluginInstance,
|
|
||||||
PluginModule,
|
|
||||||
WorkspaceAdaptor as PluginWorkspaceAdaptor,
|
|
||||||
} from "@opencode-ai/plugin"
|
|
||||||
import { Config } from "../config/config"
|
import { Config } from "../config/config"
|
||||||
import { Bus } from "../bus"
|
import { Bus } from "../bus"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
@@ -24,8 +18,6 @@ import { makeRuntime } from "@/effect/run-service"
|
|||||||
import { errorMessage } from "@/util/error"
|
import { errorMessage } from "@/util/error"
|
||||||
import { PluginLoader } from "./loader"
|
import { PluginLoader } from "./loader"
|
||||||
import { parsePluginSpecifier, readPluginId, readV1Plugin, resolvePluginId } from "./shared"
|
import { parsePluginSpecifier, readPluginId, readV1Plugin, resolvePluginId } from "./shared"
|
||||||
import { registerAdaptor } from "@/control-plane/adaptors"
|
|
||||||
import type { WorkspaceAdaptor } from "@/control-plane/types"
|
|
||||||
|
|
||||||
export namespace Plugin {
|
export namespace Plugin {
|
||||||
const log = Log.create({ service: "plugin" })
|
const log = Log.create({ service: "plugin" })
|
||||||
@@ -132,7 +124,7 @@ export namespace Plugin {
|
|||||||
Authorization: `Basic ${Buffer.from(`${Flag.OPENCODE_SERVER_USERNAME ?? "opencode"}:${Flag.OPENCODE_SERVER_PASSWORD}`).toString("base64")}`,
|
Authorization: `Basic ${Buffer.from(`${Flag.OPENCODE_SERVER_USERNAME ?? "opencode"}:${Flag.OPENCODE_SERVER_PASSWORD}`).toString("base64")}`,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
fetch: async (...args) => (await Server.Default()).app.fetch(...args),
|
fetch: async (...args) => Server.Default().app.fetch(...args),
|
||||||
})
|
})
|
||||||
const cfg = yield* config.get()
|
const cfg = yield* config.get()
|
||||||
const input: PluginInput = {
|
const input: PluginInput = {
|
||||||
@@ -140,11 +132,6 @@ export namespace Plugin {
|
|||||||
project: ctx.project,
|
project: ctx.project,
|
||||||
worktree: ctx.worktree,
|
worktree: ctx.worktree,
|
||||||
directory: ctx.directory,
|
directory: ctx.directory,
|
||||||
experimental_workspace: {
|
|
||||||
register(type: string, adaptor: PluginWorkspaceAdaptor) {
|
|
||||||
registerAdaptor(ctx.project.id, type, adaptor as WorkspaceAdaptor)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
get serverUrl(): URL {
|
get serverUrl(): URL {
|
||||||
return Server.url ?? new URL("http://localhost:4096")
|
return Server.url ?? new URL("http://localhost:4096")
|
||||||
},
|
},
|
||||||
@@ -223,15 +210,13 @@ export namespace Plugin {
|
|||||||
return message
|
return message
|
||||||
},
|
},
|
||||||
}).pipe(
|
}).pipe(
|
||||||
Effect.catch(() => {
|
Effect.catch((message) =>
|
||||||
// TODO: make proper events for this
|
bus.publish(Session.Event.Error, {
|
||||||
// bus.publish(Session.Event.Error, {
|
error: new NamedError.Unknown({
|
||||||
// error: new NamedError.Unknown({
|
message: `Failed to load plugin ${load.spec}: ${message}`,
|
||||||
// message: `Failed to load plugin ${load.spec}: ${message}`,
|
}).toObject(),
|
||||||
// }).toObject(),
|
}),
|
||||||
// })
|
),
|
||||||
return Effect.void
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Plugin } from "../plugin"
|
|||||||
import { Format } from "../format"
|
import { Format } from "../format"
|
||||||
import { LSP } from "../lsp"
|
import { LSP } from "../lsp"
|
||||||
import { File } from "../file"
|
import { File } from "../file"
|
||||||
|
import { FileWatcher } from "../file/watcher"
|
||||||
import { Snapshot } from "../snapshot"
|
import { Snapshot } from "../snapshot"
|
||||||
import { Project } from "./project"
|
import { Project } from "./project"
|
||||||
import { Vcs } from "./vcs"
|
import { Vcs } from "./vcs"
|
||||||
@@ -9,26 +10,23 @@ import { Bus } from "../bus"
|
|||||||
import { Command } from "../command"
|
import { Command } from "../command"
|
||||||
import { Instance } from "./instance"
|
import { Instance } from "./instance"
|
||||||
import { Log } from "@/util/log"
|
import { Log } from "@/util/log"
|
||||||
import { FileWatcher } from "@/file/watcher"
|
import { AppRuntime } from "@/effect/app-runtime"
|
||||||
import { ShareNext } from "@/share/share-next"
|
import { ShareNext } from "@/share/share-next"
|
||||||
import * as Effect from "effect/Effect"
|
|
||||||
|
|
||||||
export const InstanceBootstrap = Effect.gen(function* () {
|
export async function InstanceBootstrap() {
|
||||||
Log.Default.info("bootstrapping", { directory: Instance.directory })
|
Log.Default.info("bootstrapping", { directory: Instance.directory })
|
||||||
yield* Plugin.Service.use((svc) => svc.init())
|
await Plugin.init()
|
||||||
yield* ShareNext.Service.use((svc) => svc.init()).pipe(Effect.forkDetach)
|
void AppRuntime.runPromise(ShareNext.Service.use((svc) => svc.init()))
|
||||||
yield* Format.Service.use((svc) => svc.init()).pipe(Effect.forkDetach)
|
void AppRuntime.runPromise(Format.Service.use((svc) => svc.init()))
|
||||||
yield* LSP.Service.use((svc) => svc.init())
|
await LSP.init()
|
||||||
yield* File.Service.use((svc) => svc.init()).pipe(Effect.forkDetach)
|
File.init()
|
||||||
yield* FileWatcher.Service.use((svc) => svc.init()).pipe(Effect.forkDetach)
|
FileWatcher.init()
|
||||||
yield* Vcs.Service.use((svc) => svc.init()).pipe(Effect.forkDetach)
|
Vcs.init()
|
||||||
yield* Snapshot.Service.use((svc) => svc.init()).pipe(Effect.forkDetach)
|
Snapshot.init()
|
||||||
|
|
||||||
yield* Bus.Service.use((svc) =>
|
Bus.subscribe(Command.Event.Executed, async (payload) => {
|
||||||
svc.subscribeCallback(Command.Event.Executed, async (payload) => {
|
if (payload.properties.name === Command.Default.INIT) {
|
||||||
if (payload.properties.name === Command.Default.INIT) {
|
Project.setInitialized(Instance.project.id)
|
||||||
Project.setInitialized(Instance.project.id)
|
}
|
||||||
}
|
})
|
||||||
}),
|
}
|
||||||
)
|
|
||||||
}).pipe(Effect.withSpan("InstanceBootstrap"))
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import path from "path"
|
|||||||
import { Bus } from "@/bus"
|
import { Bus } from "@/bus"
|
||||||
import { BusEvent } from "@/bus/bus-event"
|
import { BusEvent } from "@/bus/bus-event"
|
||||||
import { InstanceState } from "@/effect/instance-state"
|
import { InstanceState } from "@/effect/instance-state"
|
||||||
|
import { makeRuntime } from "@/effect/run-service"
|
||||||
import { AppFileSystem } from "@/filesystem"
|
import { AppFileSystem } from "@/filesystem"
|
||||||
import { FileWatcher } from "@/file/watcher"
|
import { FileWatcher } from "@/file/watcher"
|
||||||
import { Git } from "@/git"
|
import { Git } from "@/git"
|
||||||
@@ -230,4 +231,22 @@ export namespace Vcs {
|
|||||||
Layer.provide(AppFileSystem.defaultLayer),
|
Layer.provide(AppFileSystem.defaultLayer),
|
||||||
Layer.provide(Bus.layer),
|
Layer.provide(Bus.layer),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||||
|
|
||||||
|
export async function init() {
|
||||||
|
return runPromise((svc) => svc.init())
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function branch() {
|
||||||
|
return runPromise((svc) => svc.branch())
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function defaultBranch() {
|
||||||
|
return runPromise((svc) => svc.defaultBranch())
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function diff(mode: Mode) {
|
||||||
|
return runPromise((svc) => svc.diff(mode))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
// Auto-generated by build.ts - do not edit
|
||||||
|
export declare const snapshot: Record<string, unknown>
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -209,9 +209,6 @@ export namespace ProviderTransform {
|
|||||||
copilot: {
|
copilot: {
|
||||||
copilot_cache_control: { type: "ephemeral" },
|
copilot_cache_control: { type: "ephemeral" },
|
||||||
},
|
},
|
||||||
alibaba: {
|
|
||||||
cacheControl: { type: "ephemeral" },
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const msg of unique([...system, ...final])) {
|
for (const msg of unique([...system, ...final])) {
|
||||||
@@ -288,8 +285,7 @@ export namespace ProviderTransform {
|
|||||||
model.api.id.includes("claude") ||
|
model.api.id.includes("claude") ||
|
||||||
model.id.includes("anthropic") ||
|
model.id.includes("anthropic") ||
|
||||||
model.id.includes("claude") ||
|
model.id.includes("claude") ||
|
||||||
model.api.npm === "@ai-sdk/anthropic" ||
|
model.api.npm === "@ai-sdk/anthropic") &&
|
||||||
model.api.npm === "@ai-sdk/alibaba") &&
|
|
||||||
model.api.npm !== "@ai-sdk/gateway"
|
model.api.npm !== "@ai-sdk/gateway"
|
||||||
) {
|
) {
|
||||||
msgs = applyCaching(msgs, model)
|
msgs = applyCaching(msgs, model)
|
||||||
@@ -778,10 +774,7 @@ export namespace ProviderTransform {
|
|||||||
result["chat_template_args"] = { enable_thinking: true }
|
result["chat_template_args"] = { enable_thinking: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (["zai", "zhipuai"].includes(input.model.providerID) && input.model.api.npm === "@ai-sdk/openai-compatible") {
|
||||||
["zai", "zhipuai"].some((id) => input.model.providerID.includes(id)) &&
|
|
||||||
input.model.api.npm === "@ai-sdk/openai-compatible"
|
|
||||||
) {
|
|
||||||
result["thinking"] = {
|
result["thinking"] = {
|
||||||
type: "enabled",
|
type: "enabled",
|
||||||
clear_thinking: false,
|
clear_thinking: false,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { BusEvent } from "@/bus/bus-event"
|
import { BusEvent } from "@/bus/bus-event"
|
||||||
import { Bus } from "@/bus"
|
import { Bus } from "@/bus"
|
||||||
import { InstanceState } from "@/effect/instance-state"
|
import { InstanceState } from "@/effect/instance-state"
|
||||||
|
import { makeRuntime } from "@/effect/run-service"
|
||||||
import { Instance } from "@/project/instance"
|
import { Instance } from "@/project/instance"
|
||||||
import type { Proc } from "#pty"
|
import type { Proc } from "#pty"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
@@ -360,4 +361,34 @@ export namespace Pty {
|
|||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(Layer.provide(Bus.layer), Layer.provide(Plugin.defaultLayer))
|
export const defaultLayer = layer.pipe(Layer.provide(Bus.layer), Layer.provide(Plugin.defaultLayer))
|
||||||
|
|
||||||
|
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||||
|
|
||||||
|
export async function list() {
|
||||||
|
return runPromise((svc) => svc.list())
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function get(id: PtyID) {
|
||||||
|
return runPromise((svc) => svc.get(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function write(id: PtyID, data: string) {
|
||||||
|
return runPromise((svc) => svc.write(id, data))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function connect(id: PtyID, ws: Socket, cursor?: number) {
|
||||||
|
return runPromise((svc) => svc.connect(id, ws, cursor))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function create(input: CreateInput) {
|
||||||
|
return runPromise((svc) => svc.create(input))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function update(id: PtyID, input: UpdateInput) {
|
||||||
|
return runPromise((svc) => svc.update(id, input))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function remove(id: PtyID) {
|
||||||
|
return runPromise((svc) => svc.remove(id))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Deferred, Effect, Layer, Schema, Context } from "effect"
|
|||||||
import { Bus } from "@/bus"
|
import { Bus } from "@/bus"
|
||||||
import { BusEvent } from "@/bus/bus-event"
|
import { BusEvent } from "@/bus/bus-event"
|
||||||
import { InstanceState } from "@/effect/instance-state"
|
import { InstanceState } from "@/effect/instance-state"
|
||||||
|
import { makeRuntime } from "@/effect/run-service"
|
||||||
import { SessionID, MessageID } from "@/session/schema"
|
import { SessionID, MessageID } from "@/session/schema"
|
||||||
import { Log } from "@/util/log"
|
import { Log } from "@/util/log"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
@@ -198,4 +199,26 @@ export namespace Question {
|
|||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(Layer.provide(Bus.layer))
|
export const defaultLayer = layer.pipe(Layer.provide(Bus.layer))
|
||||||
|
|
||||||
|
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||||
|
|
||||||
|
export async function ask(input: {
|
||||||
|
sessionID: SessionID
|
||||||
|
questions: Info[]
|
||||||
|
tool?: { messageID: MessageID; callID: string }
|
||||||
|
}): Promise<Answer[]> {
|
||||||
|
return runPromise((s) => s.ask(input))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function reply(input: { requestID: QuestionID; answers: Answer[] }) {
|
||||||
|
return runPromise((s) => s.reply(input))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function reject(requestID: QuestionID) {
|
||||||
|
return runPromise((s) => s.reject(requestID))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function list() {
|
||||||
|
return runPromise((s) => s.list())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
import type { Hono } from "hono"
|
|
||||||
import { createBunWebSocket } from "hono/bun"
|
|
||||||
import type { Adapter } from "./adapter"
|
|
||||||
|
|
||||||
export const adapter: Adapter = {
|
|
||||||
create(app: Hono) {
|
|
||||||
const ws = createBunWebSocket()
|
|
||||||
return {
|
|
||||||
upgradeWebSocket: ws.upgradeWebSocket,
|
|
||||||
async listen(opts) {
|
|
||||||
const args = {
|
|
||||||
fetch: app.fetch,
|
|
||||||
hostname: opts.hostname,
|
|
||||||
idleTimeout: 0,
|
|
||||||
websocket: ws.websocket,
|
|
||||||
} as const
|
|
||||||
const start = (port: number) => {
|
|
||||||
try {
|
|
||||||
return Bun.serve({ ...args, port })
|
|
||||||
} catch {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const server = opts.port === 0 ? (start(4096) ?? start(0)) : start(opts.port)
|
|
||||||
if (!server) {
|
|
||||||
throw new Error(`Failed to start server on port ${opts.port}`)
|
|
||||||
}
|
|
||||||
if (!server.port) {
|
|
||||||
throw new Error(`Failed to resolve server address for port ${opts.port}`)
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
port: server.port,
|
|
||||||
stop(close?: boolean) {
|
|
||||||
return Promise.resolve(server.stop(close))
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import { createAdaptorServer, type ServerType } from "@hono/node-server"
|
|
||||||
import { createNodeWebSocket } from "@hono/node-ws"
|
|
||||||
import type { Hono } from "hono"
|
|
||||||
import type { Adapter } from "./adapter"
|
|
||||||
|
|
||||||
export const adapter: Adapter = {
|
|
||||||
create(app: Hono) {
|
|
||||||
const ws = createNodeWebSocket({ app })
|
|
||||||
return {
|
|
||||||
upgradeWebSocket: ws.upgradeWebSocket,
|
|
||||||
async listen(opts) {
|
|
||||||
const start = (port: number) =>
|
|
||||||
new Promise<ServerType>((resolve, reject) => {
|
|
||||||
const server = createAdaptorServer({ fetch: app.fetch })
|
|
||||||
ws.injectWebSocket(server)
|
|
||||||
const fail = (err: Error) => {
|
|
||||||
cleanup()
|
|
||||||
reject(err)
|
|
||||||
}
|
|
||||||
const ready = () => {
|
|
||||||
cleanup()
|
|
||||||
resolve(server)
|
|
||||||
}
|
|
||||||
const cleanup = () => {
|
|
||||||
server.off("error", fail)
|
|
||||||
server.off("listening", ready)
|
|
||||||
}
|
|
||||||
server.once("error", fail)
|
|
||||||
server.once("listening", ready)
|
|
||||||
server.listen(port, opts.hostname)
|
|
||||||
})
|
|
||||||
|
|
||||||
const server = opts.port === 0 ? await start(4096).catch(() => start(0)) : await start(opts.port)
|
|
||||||
const addr = server.address()
|
|
||||||
if (!addr || typeof addr === "string") {
|
|
||||||
throw new Error(`Failed to resolve server address for port ${opts.port}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
let closing: Promise<void> | undefined
|
|
||||||
return {
|
|
||||||
port: addr.port,
|
|
||||||
stop(close?: boolean) {
|
|
||||||
closing ??= new Promise((resolve, reject) => {
|
|
||||||
server.close((err) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
if (close) {
|
|
||||||
if ("closeAllConnections" in server && typeof server.closeAllConnections === "function") {
|
|
||||||
server.closeAllConnections()
|
|
||||||
}
|
|
||||||
if ("closeIdleConnections" in server && typeof server.closeIdleConnections === "function") {
|
|
||||||
server.closeIdleConnections()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return closing
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import type { Hono } from "hono"
|
|
||||||
import type { UpgradeWebSocket } from "hono/ws"
|
|
||||||
|
|
||||||
export type Opts = {
|
|
||||||
port: number
|
|
||||||
hostname: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Listener = {
|
|
||||||
port: number
|
|
||||||
stop: (close?: boolean) => Promise<void>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Runtime {
|
|
||||||
upgradeWebSocket: UpgradeWebSocket
|
|
||||||
listen(opts: Opts): Promise<Listener>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Adapter {
|
|
||||||
create(app: Hono): Runtime
|
|
||||||
}
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
import { Auth } from "@/auth"
|
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
import { Log } from "@/util/log"
|
|
||||||
import { Effect } from "effect"
|
|
||||||
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 AppRuntime.runPromise(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const auth = yield* Auth.Service
|
|
||||||
yield* 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 AppRuntime.runPromise(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const auth = yield* Auth.Service
|
|
||||||
yield* 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)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
+89
-53
@@ -1,34 +1,53 @@
|
|||||||
import { describeRoute, resolver, validator } from "hono-openapi"
|
import { describeRoute, resolver, validator } from "hono-openapi"
|
||||||
import { Hono } from "hono"
|
import { Hono } from "hono"
|
||||||
|
import { proxy } from "hono/proxy"
|
||||||
import type { UpgradeWebSocket } from "hono/ws"
|
import type { UpgradeWebSocket } from "hono/ws"
|
||||||
import { Effect } from "effect"
|
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { Format } from "../../format"
|
import { createHash } from "node:crypto"
|
||||||
import { TuiRoutes } from "./tui"
|
import * as fs from "node:fs/promises"
|
||||||
import { Instance } from "../../project/instance"
|
import { Log } from "../util/log"
|
||||||
import { Vcs } from "../../project/vcs"
|
import { Format } from "../format"
|
||||||
import { Agent } from "../../agent/agent"
|
import { TuiRoutes } from "./routes/tui"
|
||||||
import { Skill } from "../../skill"
|
import { Instance } from "../project/instance"
|
||||||
import { Global } from "../../global"
|
import { Vcs } from "../project/vcs"
|
||||||
import { LSP } from "../../lsp"
|
import { Agent } from "../agent/agent"
|
||||||
import { Command } from "../../command"
|
import { Skill } from "../skill"
|
||||||
import { QuestionRoutes } from "./question"
|
import { Global } from "../global"
|
||||||
import { PermissionRoutes } from "./permission"
|
import { LSP } from "../lsp"
|
||||||
import { ProjectRoutes } from "./project"
|
import { Command } from "../command"
|
||||||
import { SessionRoutes } from "./session"
|
import { Flag } from "../flag/flag"
|
||||||
import { PtyRoutes } from "./pty"
|
import { QuestionRoutes } from "./routes/question"
|
||||||
import { McpRoutes } from "./mcp"
|
import { PermissionRoutes } from "./routes/permission"
|
||||||
import { FileRoutes } from "./file"
|
import { Snapshot } from "@/snapshot"
|
||||||
import { ConfigRoutes } from "./config"
|
import { ProjectRoutes } from "./routes/project"
|
||||||
import { ExperimentalRoutes } from "./experimental"
|
import { SessionRoutes } from "./routes/session"
|
||||||
import { ProviderRoutes } from "./provider"
|
import { PtyRoutes } from "./routes/pty"
|
||||||
import { EventRoutes } from "./event"
|
import { McpRoutes } from "./routes/mcp"
|
||||||
import { WorkspaceRouterMiddleware } from "./middleware"
|
import { FileRoutes } from "./routes/file"
|
||||||
|
import { 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 { AppRuntime } from "@/effect/app-runtime"
|
import { AppRuntime } from "@/effect/app-runtime"
|
||||||
|
|
||||||
export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono =>
|
const log = Log.create({ service: "server" })
|
||||||
new Hono()
|
|
||||||
.use(WorkspaceRouterMiddleware(upgrade))
|
const embeddedUIPromise = Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI
|
||||||
|
? Promise.resolve(null)
|
||||||
|
: // @ts-expect-error - generated file at build time
|
||||||
|
import("opencode-web-ui.gen.ts").then((module) => module.default as Record<string, string>).catch(() => null)
|
||||||
|
|
||||||
|
const DEFAULT_CSP =
|
||||||
|
"default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src 'self' data:"
|
||||||
|
|
||||||
|
const csp = (hash = "") =>
|
||||||
|
`default-src 'self'; script-src 'self' 'wasm-unsafe-eval'${hash ? ` 'sha256-${hash}'` : ""}; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src 'self' data:`
|
||||||
|
|
||||||
|
export const InstanceRoutes = (upgrade: UpgradeWebSocket, app: Hono = new Hono()) =>
|
||||||
|
app
|
||||||
|
.onError(errorHandler(log))
|
||||||
.route("/project", ProjectRoutes())
|
.route("/project", ProjectRoutes())
|
||||||
.route("/pty", PtyRoutes(upgrade))
|
.route("/pty", PtyRoutes(upgrade))
|
||||||
.route("/config", ConfigRoutes())
|
.route("/config", ConfigRoutes())
|
||||||
@@ -120,17 +139,11 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono =>
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
return c.json(
|
const [branch, default_branch] = await Promise.all([Vcs.branch(), Vcs.defaultBranch()])
|
||||||
await AppRuntime.runPromise(
|
return c.json({
|
||||||
Effect.gen(function* () {
|
branch,
|
||||||
const vcs = yield* Vcs.Service
|
default_branch,
|
||||||
const [branch, default_branch] = yield* Effect.all([vcs.branch(), vcs.defaultBranch()], {
|
})
|
||||||
concurrency: 2,
|
|
||||||
})
|
|
||||||
return { branch, default_branch }
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.get(
|
.get(
|
||||||
@@ -157,14 +170,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono =>
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
return c.json(
|
return c.json(await Vcs.diff(c.req.valid("query").mode))
|
||||||
await AppRuntime.runPromise(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const vcs = yield* Vcs.Service
|
|
||||||
return yield* vcs.diff(c.req.valid("query").mode)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.get(
|
.get(
|
||||||
@@ -185,7 +191,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono =>
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const commands = await AppRuntime.runPromise(Command.Service.use((svc) => svc.list()))
|
const commands = await Command.list()
|
||||||
return c.json(commands)
|
return c.json(commands)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -229,12 +235,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono =>
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const skills = await AppRuntime.runPromise(
|
const skills = await Skill.all()
|
||||||
Effect.gen(function* () {
|
|
||||||
const skill = yield* Skill.Service
|
|
||||||
return yield* skill.all()
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
return c.json(skills)
|
return c.json(skills)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -256,8 +257,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono =>
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const items = await AppRuntime.runPromise(LSP.Service.use((lsp) => lsp.status()))
|
return c.json(await LSP.status())
|
||||||
return c.json(items)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.get(
|
.get(
|
||||||
@@ -281,3 +281,39 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono =>
|
|||||||
return c.json(await AppRuntime.runPromise(Format.Service.use((svc) => svc.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,90 +3,31 @@ import { NamedError } from "@opencode-ai/util/error"
|
|||||||
import { NotFoundError } from "../storage/db"
|
import { NotFoundError } from "../storage/db"
|
||||||
import { Session } from "../session"
|
import { Session } from "../session"
|
||||||
import type { ContentfulStatusCode } from "hono/utils/http-status"
|
import type { ContentfulStatusCode } from "hono/utils/http-status"
|
||||||
import type { ErrorHandler, MiddlewareHandler } from "hono"
|
import type { ErrorHandler } from "hono"
|
||||||
import { HTTPException } from "hono/http-exception"
|
import { HTTPException } from "hono/http-exception"
|
||||||
import { Log } from "../util/log"
|
import type { Log } from "../util/log"
|
||||||
import { Flag } from "@/flag/flag"
|
|
||||||
import { basicAuth } from "hono/basic-auth"
|
|
||||||
import { cors } from "hono/cors"
|
|
||||||
import { compress } from "hono/compress"
|
|
||||||
|
|
||||||
const log = Log.create({ service: "server" })
|
export function errorHandler(log: Log.Logger): ErrorHandler {
|
||||||
|
return (err, c) => {
|
||||||
export const ErrorMiddleware: ErrorHandler = (err, c) => {
|
log.error("failed", {
|
||||||
log.error("failed", {
|
error: err,
|
||||||
error: err,
|
})
|
||||||
})
|
if (err instanceof NamedError) {
|
||||||
if (err instanceof NamedError) {
|
let status: ContentfulStatusCode
|
||||||
let status: ContentfulStatusCode
|
if (err instanceof NotFoundError) status = 404
|
||||||
if (err instanceof NotFoundError) status = 404
|
else if (err instanceof Provider.ModelNotFoundError) status = 400
|
||||||
else if (err instanceof Provider.ModelNotFoundError) status = 400
|
else if (err.name === "ProviderAuthValidationFailed") status = 400
|
||||||
else if (err.name === "ProviderAuthValidationFailed") status = 400
|
else if (err.name.startsWith("Worktree")) status = 400
|
||||||
else if (err.name.startsWith("Worktree")) status = 400
|
else status = 500
|
||||||
else status = 500
|
return c.json(err.toObject(), { status })
|
||||||
return c.json(err.toObject(), { status })
|
}
|
||||||
}
|
if (err instanceof Session.BusyError) {
|
||||||
if (err instanceof Session.BusyError) {
|
return c.json(new NamedError.Unknown({ message: err.message }).toObject(), { status: 400 })
|
||||||
return c.json(new NamedError.Unknown({ message: err.message }).toObject(), { status: 400 })
|
}
|
||||||
}
|
if (err instanceof HTTPException) return err.getResponse()
|
||||||
if (err instanceof HTTPException) return err.getResponse()
|
const message = err instanceof Error && err.stack ? err.stack : err.toString()
|
||||||
const message = err instanceof Error && err.stack ? err.stack : err.toString()
|
return c.json(new NamedError.Unknown({ message }).toObject(), {
|
||||||
return c.json(new NamedError.Unknown({ message }).toObject(), {
|
status: 500,
|
||||||
status: 500,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AuthMiddleware: MiddlewareHandler = (c, next) => {
|
|
||||||
// Allow CORS preflight requests to succeed without auth.
|
|
||||||
// Browser clients sending Authorization headers will preflight with OPTIONS.
|
|
||||||
if (c.req.method === "OPTIONS") return next()
|
|
||||||
const password = Flag.OPENCODE_SERVER_PASSWORD
|
|
||||||
if (!password) return next()
|
|
||||||
const username = Flag.OPENCODE_SERVER_USERNAME ?? "opencode"
|
|
||||||
|
|
||||||
if (c.req.query("auth_token")) c.req.raw.headers.set("authorization", `Basic ${c.req.query("auth_token")}`)
|
|
||||||
|
|
||||||
return basicAuth({ username, password })(c, next)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LoggerMiddleware: MiddlewareHandler = async (c, next) => {
|
|
||||||
const skip = c.req.path === "/log"
|
|
||||||
if (!skip) {
|
|
||||||
log.info("request", {
|
|
||||||
method: c.req.method,
|
|
||||||
path: c.req.path,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const timer = log.time("request", {
|
|
||||||
method: c.req.method,
|
|
||||||
path: c.req.path,
|
|
||||||
})
|
|
||||||
await next()
|
|
||||||
if (!skip) timer.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CorsMiddleware(opts?: { cors?: string[] }): MiddlewareHandler {
|
|
||||||
return cors({
|
|
||||||
maxAge: 86_400,
|
|
||||||
origin(input) {
|
|
||||||
if (!input) return
|
|
||||||
|
|
||||||
if (input.startsWith("http://localhost:")) return input
|
|
||||||
if (input.startsWith("http://127.0.0.1:")) return input
|
|
||||||
if (input === "tauri://localhost" || input === "http://tauri.localhost" || input === "https://tauri.localhost")
|
|
||||||
return input
|
|
||||||
|
|
||||||
if (/^https:\/\/([a-z0-9-]+\.)*opencode\.ai$/.test(input)) return input
|
|
||||||
if (opts?.cors?.includes(input)) return input
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const zipped = compress()
|
|
||||||
export const CompressionMiddleware: MiddlewareHandler = (c, next) => {
|
|
||||||
const path = c.req.path
|
|
||||||
const method = c.req.method
|
|
||||||
if (path === "/event" || path === "/global/event" || path === "/global/sync-event") return next()
|
|
||||||
if (method === "POST" && /\/session\/[^/]+\/(message|prompt_async)$/.test(path)) return next()
|
|
||||||
return zipped(c, next)
|
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-10
@@ -3,14 +3,15 @@ import type { UpgradeWebSocket } from "hono/ws"
|
|||||||
import { getAdaptor } from "@/control-plane/adaptors"
|
import { getAdaptor } from "@/control-plane/adaptors"
|
||||||
import { WorkspaceID } from "@/control-plane/schema"
|
import { WorkspaceID } from "@/control-plane/schema"
|
||||||
import { Workspace } from "@/control-plane/workspace"
|
import { Workspace } from "@/control-plane/workspace"
|
||||||
import { ServerProxy } from "../proxy"
|
import { ServerProxy } from "./proxy"
|
||||||
|
import { lazy } from "@/util/lazy"
|
||||||
import { Filesystem } from "@/util/filesystem"
|
import { Filesystem } from "@/util/filesystem"
|
||||||
import { Instance } from "@/project/instance"
|
import { Instance } from "@/project/instance"
|
||||||
import { InstanceBootstrap } from "@/project/bootstrap"
|
import { InstanceBootstrap } from "@/project/bootstrap"
|
||||||
|
import { InstanceRoutes } from "./instance"
|
||||||
import { Session } from "@/session"
|
import { Session } from "@/session"
|
||||||
import { SessionID } from "@/session/schema"
|
import { SessionID } from "@/session/schema"
|
||||||
import { WorkspaceContext } from "@/control-plane/workspace-context"
|
import { WorkspaceContext } from "@/control-plane/workspace-context"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
|
|
||||||
type Rule = { method?: string; path: string; exact?: boolean; action: "local" | "forward" }
|
type Rule = { method?: string; path: string; exact?: boolean; action: "local" | "forward" }
|
||||||
|
|
||||||
@@ -46,7 +47,9 @@ async function getSessionWorkspace(url: URL) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): MiddlewareHandler {
|
export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): MiddlewareHandler {
|
||||||
return async (c, next) => {
|
const routes = lazy(() => InstanceRoutes(upgrade))
|
||||||
|
|
||||||
|
return async (c) => {
|
||||||
const raw = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd()
|
const raw = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd()
|
||||||
const directory = Filesystem.resolve(
|
const directory = Filesystem.resolve(
|
||||||
(() => {
|
(() => {
|
||||||
@@ -67,9 +70,9 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware
|
|||||||
if (!workspaceID) {
|
if (!workspaceID) {
|
||||||
return Instance.provide({
|
return Instance.provide({
|
||||||
directory,
|
directory,
|
||||||
init: () => AppRuntime.runPromise(InstanceBootstrap),
|
init: InstanceBootstrap,
|
||||||
async fn() {
|
async fn() {
|
||||||
return next()
|
return routes().fetch(c.req.raw, c.env)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -84,7 +87,7 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware
|
|||||||
// The lets the `DELETE /session/:id` endpoint through and we've
|
// The lets the `DELETE /session/:id` endpoint through and we've
|
||||||
// made sure that it will run without an instance
|
// made sure that it will run without an instance
|
||||||
if (url.pathname.match(/\/session\/[^/]+$/) && c.req.method === "DELETE") {
|
if (url.pathname.match(/\/session\/[^/]+$/) && c.req.method === "DELETE") {
|
||||||
return next()
|
return routes().fetch(c.req.raw, c.env)
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Response(`Workspace not found: ${workspaceID}`, {
|
return new Response(`Workspace not found: ${workspaceID}`, {
|
||||||
@@ -95,7 +98,7 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const adaptor = await getAdaptor(workspace.projectID, workspace.type)
|
const adaptor = await getAdaptor(workspace.type)
|
||||||
const target = await adaptor.target(workspace)
|
const target = await adaptor.target(workspace)
|
||||||
|
|
||||||
if (target.type === "local") {
|
if (target.type === "local") {
|
||||||
@@ -104,9 +107,9 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware
|
|||||||
fn: () =>
|
fn: () =>
|
||||||
Instance.provide({
|
Instance.provide({
|
||||||
directory: target.directory,
|
directory: target.directory,
|
||||||
init: () => AppRuntime.runPromise(InstanceBootstrap),
|
init: InstanceBootstrap,
|
||||||
async fn() {
|
async fn() {
|
||||||
return next()
|
return routes().fetch(c.req.raw, c.env)
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
@@ -115,7 +118,7 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware
|
|||||||
if (local(c.req.method, url.pathname)) {
|
if (local(c.req.method, url.pathname)) {
|
||||||
// No instance provided because we are serving cached data; there
|
// No instance provided because we are serving cached data; there
|
||||||
// is no instance to work with
|
// is no instance to work with
|
||||||
return next()
|
return routes().fetch(c.req.raw, c.env)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (c.req.header("upgrade")?.toLowerCase() === "websocket") {
|
if (c.req.header("upgrade")?.toLowerCase() === "websocket") {
|
||||||
+3
-10
@@ -7,8 +7,6 @@ import { mapValues } from "remeda"
|
|||||||
import { errors } from "../error"
|
import { errors } from "../error"
|
||||||
import { Log } from "../../util/log"
|
import { Log } from "../../util/log"
|
||||||
import { lazy } from "../../util/lazy"
|
import { lazy } from "../../util/lazy"
|
||||||
import { AppRuntime } from "../../effect/app-runtime"
|
|
||||||
import { Effect } from "effect"
|
|
||||||
|
|
||||||
const log = Log.create({ service: "server" })
|
const log = Log.create({ service: "server" })
|
||||||
|
|
||||||
@@ -32,7 +30,7 @@ export const ConfigRoutes = lazy(() =>
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
return c.json(await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.get())))
|
return c.json(await Config.get())
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.patch(
|
.patch(
|
||||||
@@ -56,7 +54,7 @@ export const ConfigRoutes = lazy(() =>
|
|||||||
validator("json", Config.Info),
|
validator("json", Config.Info),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const config = c.req.valid("json")
|
const config = c.req.valid("json")
|
||||||
await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.update(config)))
|
await Config.update(config)
|
||||||
return c.json(config)
|
return c.json(config)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -84,12 +82,7 @@ export const ConfigRoutes = lazy(() =>
|
|||||||
}),
|
}),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
using _ = log.time("providers")
|
using _ = log.time("providers")
|
||||||
const providers = await AppRuntime.runPromise(
|
const providers = await Provider.list().then((x) => mapValues(x, (item) => item))
|
||||||
Effect.gen(function* () {
|
|
||||||
const svc = yield* Provider.Service
|
|
||||||
return mapValues(yield* svc.list(), (item) => item)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
return c.json({
|
return c.json({
|
||||||
providers: Object.values(providers),
|
providers: Object.values(providers),
|
||||||
default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
|
default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
|
||||||
+24
-66
@@ -11,11 +11,9 @@ import { Session } from "../../session"
|
|||||||
import { Config } from "../../config/config"
|
import { Config } from "../../config/config"
|
||||||
import { ConsoleState } from "../../config/console-state"
|
import { ConsoleState } from "../../config/console-state"
|
||||||
import { Account, AccountID, OrgID } from "../../account"
|
import { Account, AccountID, OrgID } from "../../account"
|
||||||
import { AppRuntime } from "../../effect/app-runtime"
|
|
||||||
import { zodToJsonSchema } from "zod-to-json-schema"
|
import { zodToJsonSchema } from "zod-to-json-schema"
|
||||||
import { errors } from "../error"
|
import { errors } from "../error"
|
||||||
import { lazy } from "../../util/lazy"
|
import { lazy } from "../../util/lazy"
|
||||||
import { Effect, Option } from "effect"
|
|
||||||
import { WorkspaceRoutes } from "./workspace"
|
import { WorkspaceRoutes } from "./workspace"
|
||||||
import { Agent } from "@/agent/agent"
|
import { Agent } from "@/agent/agent"
|
||||||
|
|
||||||
@@ -57,20 +55,11 @@ export const ExperimentalRoutes = lazy(() =>
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const result = await AppRuntime.runPromise(
|
const [consoleState, groups] = await Promise.all([Config.getConsoleState(), Account.orgsByAccount()])
|
||||||
Effect.gen(function* () {
|
return c.json({
|
||||||
const config = yield* Config.Service
|
...consoleState,
|
||||||
const account = yield* Account.Service
|
switchableOrgCount: groups.reduce((count, group) => count + group.orgs.length, 0),
|
||||||
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(
|
.get(
|
||||||
@@ -91,24 +80,17 @@ export const ExperimentalRoutes = lazy(() =>
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const orgs = await AppRuntime.runPromise(
|
const [groups, active] = await Promise.all([Account.orgsByAccount(), Account.active()])
|
||||||
Effect.gen(function* () {
|
|
||||||
const account = yield* Account.Service
|
const orgs = groups.flatMap((group) =>
|
||||||
const [groups, active] = yield* Effect.all([account.orgsByAccount(), account.active()], {
|
group.orgs.map((org) => ({
|
||||||
concurrency: "unbounded",
|
accountID: group.account.id,
|
||||||
})
|
accountEmail: group.account.email,
|
||||||
const info = Option.getOrUndefined(active)
|
accountUrl: group.account.url,
|
||||||
return groups.flatMap((group) =>
|
orgID: org.id,
|
||||||
group.orgs.map((org) => ({
|
orgName: org.name,
|
||||||
accountID: group.account.id,
|
active: !!active && active.id === group.account.id && active.active_org_id === org.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 })
|
return c.json({ orgs })
|
||||||
},
|
},
|
||||||
@@ -133,12 +115,7 @@ export const ExperimentalRoutes = lazy(() =>
|
|||||||
validator("json", ConsoleSwitchBody),
|
validator("json", ConsoleSwitchBody),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const body = c.req.valid("json")
|
const body = c.req.valid("json")
|
||||||
await AppRuntime.runPromise(
|
await Account.switchOrg(AccountID.make(body.accountID), OrgID.make(body.orgID))
|
||||||
Effect.gen(function* () {
|
|
||||||
const account = yield* Account.Service
|
|
||||||
yield* account.use(AccountID.make(body.accountID), Option.some(OrgID.make(body.orgID)))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
return c.json(true)
|
return c.json(true)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -162,13 +139,7 @@ export const ExperimentalRoutes = lazy(() =>
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const ids = await AppRuntime.runPromise(
|
return c.json(await ToolRegistry.ids())
|
||||||
Effect.gen(function* () {
|
|
||||||
const registry = yield* ToolRegistry.Service
|
|
||||||
return yield* registry.ids()
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
return c.json(ids)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.get(
|
.get(
|
||||||
@@ -211,17 +182,11 @@ export const ExperimentalRoutes = lazy(() =>
|
|||||||
),
|
),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const { provider, model } = c.req.valid("query")
|
const { provider, model } = c.req.valid("query")
|
||||||
const tools = await AppRuntime.runPromise(
|
const tools = await ToolRegistry.tools({
|
||||||
Effect.gen(function* () {
|
providerID: ProviderID.make(provider),
|
||||||
const agents = yield* Agent.Service
|
modelID: ModelID.make(model),
|
||||||
const registry = yield* ToolRegistry.Service
|
agent: await Agent.get(await Agent.defaultAgent()),
|
||||||
return yield* registry.tools({
|
})
|
||||||
providerID: ProviderID.make(provider),
|
|
||||||
modelID: ModelID.make(model),
|
|
||||||
agent: yield* agents.get(yield* agents.defaultAgent()),
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
return c.json(
|
return c.json(
|
||||||
tools.map((t) => ({
|
tools.map((t) => ({
|
||||||
id: t.id,
|
id: t.id,
|
||||||
@@ -408,14 +373,7 @@ export const ExperimentalRoutes = lazy(() =>
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
return c.json(
|
return c.json(await MCP.resources())
|
||||||
await AppRuntime.runPromise(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const mcp = yield* MCP.Service
|
|
||||||
return yield* mcp.resources()
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
+20
-33
@@ -1,8 +1,6 @@
|
|||||||
import { Hono } from "hono"
|
import { Hono } from "hono"
|
||||||
import { describeRoute, validator, resolver } from "hono-openapi"
|
import { describeRoute, validator, resolver } from "hono-openapi"
|
||||||
import { Effect } from "effect"
|
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { AppRuntime } from "../../effect/app-runtime"
|
|
||||||
import { File } from "../../file"
|
import { File } from "../../file"
|
||||||
import { Ripgrep } from "../../file/ripgrep"
|
import { Ripgrep } from "../../file/ripgrep"
|
||||||
import { LSP } from "../../lsp"
|
import { LSP } from "../../lsp"
|
||||||
@@ -36,10 +34,12 @@ export const FileRoutes = lazy(() =>
|
|||||||
),
|
),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const pattern = c.req.valid("query").pattern
|
const pattern = c.req.valid("query").pattern
|
||||||
const result = await AppRuntime.runPromise(
|
const result = await Ripgrep.search({
|
||||||
Ripgrep.Service.use((svc) => svc.search({ cwd: Instance.directory, pattern, limit: 10 })),
|
cwd: Instance.directory,
|
||||||
)
|
pattern,
|
||||||
return c.json(result.items)
|
limit: 10,
|
||||||
|
})
|
||||||
|
return c.json(result)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.get(
|
.get(
|
||||||
@@ -73,18 +73,12 @@ export const FileRoutes = lazy(() =>
|
|||||||
const dirs = c.req.valid("query").dirs
|
const dirs = c.req.valid("query").dirs
|
||||||
const type = c.req.valid("query").type
|
const type = c.req.valid("query").type
|
||||||
const limit = c.req.valid("query").limit
|
const limit = c.req.valid("query").limit
|
||||||
const results = await AppRuntime.runPromise(
|
const results = await File.search({
|
||||||
Effect.gen(function* () {
|
query,
|
||||||
return yield* File.Service.use((svc) =>
|
limit: limit ?? 10,
|
||||||
svc.search({
|
dirs: dirs !== "false",
|
||||||
query,
|
type,
|
||||||
limit: limit ?? 10,
|
})
|
||||||
dirs: dirs !== "false",
|
|
||||||
type,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
return c.json(results)
|
return c.json(results)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -112,6 +106,11 @@ export const FileRoutes = lazy(() =>
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
|
/*
|
||||||
|
const query = c.req.valid("query").query
|
||||||
|
const result = await LSP.workspaceSymbol(query)
|
||||||
|
return c.json(result)
|
||||||
|
*/
|
||||||
return c.json([])
|
return c.json([])
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -140,11 +139,7 @@ export const FileRoutes = lazy(() =>
|
|||||||
),
|
),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const path = c.req.valid("query").path
|
const path = c.req.valid("query").path
|
||||||
const content = await AppRuntime.runPromise(
|
const content = await File.list(path)
|
||||||
Effect.gen(function* () {
|
|
||||||
return yield* File.Service.use((svc) => svc.list(path))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
return c.json(content)
|
return c.json(content)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -173,11 +168,7 @@ export const FileRoutes = lazy(() =>
|
|||||||
),
|
),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const path = c.req.valid("query").path
|
const path = c.req.valid("query").path
|
||||||
const content = await AppRuntime.runPromise(
|
const content = await File.read(path)
|
||||||
Effect.gen(function* () {
|
|
||||||
return yield* File.Service.use((svc) => svc.read(path))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
return c.json(content)
|
return c.json(content)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -199,11 +190,7 @@ export const FileRoutes = lazy(() =>
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const content = await AppRuntime.runPromise(
|
const content = await File.status()
|
||||||
Effect.gen(function* () {
|
|
||||||
return yield* File.Service.use((svc) => svc.status())
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
return c.json(content)
|
return c.json(content)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
+2
-2
@@ -199,7 +199,7 @@ export const GlobalRoutes = lazy(() =>
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
return c.json(await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.getGlobal())))
|
return c.json(await Config.getGlobal())
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.patch(
|
.patch(
|
||||||
@@ -223,7 +223,7 @@ export const GlobalRoutes = lazy(() =>
|
|||||||
validator("json", Config.Info),
|
validator("json", Config.Info),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const config = c.req.valid("json")
|
const config = c.req.valid("json")
|
||||||
const next = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.updateGlobal(config)))
|
const next = await Config.updateGlobal(config)
|
||||||
return c.json(next)
|
return c.json(next)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
+14
-34
@@ -3,10 +3,8 @@ import { describeRoute, validator, resolver } from "hono-openapi"
|
|||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { MCP } from "../../mcp"
|
import { MCP } from "../../mcp"
|
||||||
import { Config } from "../../config/config"
|
import { Config } from "../../config/config"
|
||||||
import { AppRuntime } from "../../effect/app-runtime"
|
|
||||||
import { errors } from "../error"
|
import { errors } from "../error"
|
||||||
import { lazy } from "../../util/lazy"
|
import { lazy } from "../../util/lazy"
|
||||||
import { Effect } from "effect"
|
|
||||||
|
|
||||||
export const McpRoutes = lazy(() =>
|
export const McpRoutes = lazy(() =>
|
||||||
new Hono()
|
new Hono()
|
||||||
@@ -28,7 +26,7 @@ export const McpRoutes = lazy(() =>
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
return c.json(await AppRuntime.runPromise(MCP.Service.use((mcp) => mcp.status())))
|
return c.json(await MCP.status())
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.post(
|
.post(
|
||||||
@@ -58,7 +56,7 @@ export const McpRoutes = lazy(() =>
|
|||||||
),
|
),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const { name, config } = c.req.valid("json")
|
const { name, config } = c.req.valid("json")
|
||||||
const result = await AppRuntime.runPromise(MCP.Service.use((mcp) => mcp.add(name, config)))
|
const result = await MCP.add(name, config)
|
||||||
return c.json(result.status)
|
return c.json(result.status)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -86,21 +84,12 @@ export const McpRoutes = lazy(() =>
|
|||||||
}),
|
}),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const name = c.req.param("name")
|
const name = c.req.param("name")
|
||||||
const result = await AppRuntime.runPromise(
|
const supportsOAuth = await MCP.supportsOAuth(name)
|
||||||
Effect.gen(function* () {
|
if (!supportsOAuth) {
|
||||||
const mcp = yield* MCP.Service
|
|
||||||
const supports = yield* mcp.supportsOAuth(name)
|
|
||||||
if (!supports) return { supports }
|
|
||||||
return {
|
|
||||||
supports,
|
|
||||||
auth: yield* mcp.startAuth(name),
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
if (!result.supports) {
|
|
||||||
return c.json({ error: `MCP server ${name} does not support OAuth` }, 400)
|
return c.json({ error: `MCP server ${name} does not support OAuth` }, 400)
|
||||||
}
|
}
|
||||||
return c.json(result.auth)
|
const result = await MCP.startAuth(name)
|
||||||
|
return c.json(result)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.post(
|
.post(
|
||||||
@@ -131,7 +120,7 @@ export const McpRoutes = lazy(() =>
|
|||||||
async (c) => {
|
async (c) => {
|
||||||
const name = c.req.param("name")
|
const name = c.req.param("name")
|
||||||
const { code } = c.req.valid("json")
|
const { code } = c.req.valid("json")
|
||||||
const status = await AppRuntime.runPromise(MCP.Service.use((mcp) => mcp.finishAuth(name, code)))
|
const status = await MCP.finishAuth(name, code)
|
||||||
return c.json(status)
|
return c.json(status)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -155,21 +144,12 @@ export const McpRoutes = lazy(() =>
|
|||||||
}),
|
}),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const name = c.req.param("name")
|
const name = c.req.param("name")
|
||||||
const result = await AppRuntime.runPromise(
|
const supportsOAuth = await MCP.supportsOAuth(name)
|
||||||
Effect.gen(function* () {
|
if (!supportsOAuth) {
|
||||||
const mcp = yield* MCP.Service
|
|
||||||
const supports = yield* mcp.supportsOAuth(name)
|
|
||||||
if (!supports) return { supports }
|
|
||||||
return {
|
|
||||||
supports,
|
|
||||||
status: yield* mcp.authenticate(name),
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
if (!result.supports) {
|
|
||||||
return c.json({ error: `MCP server ${name} does not support OAuth` }, 400)
|
return c.json({ error: `MCP server ${name} does not support OAuth` }, 400)
|
||||||
}
|
}
|
||||||
return c.json(result.status)
|
const status = await MCP.authenticate(name)
|
||||||
|
return c.json(status)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.delete(
|
.delete(
|
||||||
@@ -192,7 +172,7 @@ export const McpRoutes = lazy(() =>
|
|||||||
}),
|
}),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const name = c.req.param("name")
|
const name = c.req.param("name")
|
||||||
await AppRuntime.runPromise(MCP.Service.use((mcp) => mcp.removeAuth(name)))
|
await MCP.removeAuth(name)
|
||||||
return c.json({ success: true as const })
|
return c.json({ success: true as const })
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -215,7 +195,7 @@ export const McpRoutes = lazy(() =>
|
|||||||
validator("param", z.object({ name: z.string() })),
|
validator("param", z.object({ name: z.string() })),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const { name } = c.req.valid("param")
|
const { name } = c.req.valid("param")
|
||||||
await AppRuntime.runPromise(MCP.Service.use((mcp) => mcp.connect(name)))
|
await MCP.connect(name)
|
||||||
return c.json(true)
|
return c.json(true)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -238,7 +218,7 @@ export const McpRoutes = lazy(() =>
|
|||||||
validator("param", z.object({ name: z.string() })),
|
validator("param", z.object({ name: z.string() })),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const { name } = c.req.valid("param")
|
const { name } = c.req.valid("param")
|
||||||
await AppRuntime.runPromise(MCP.Service.use((mcp) => mcp.disconnect(name)))
|
await MCP.disconnect(name)
|
||||||
return c.json(true)
|
return c.json(true)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
+1
-2
@@ -8,7 +8,6 @@ import { ProjectID } from "../../project/schema"
|
|||||||
import { errors } from "../error"
|
import { errors } from "../error"
|
||||||
import { lazy } from "../../util/lazy"
|
import { lazy } from "../../util/lazy"
|
||||||
import { InstanceBootstrap } from "../../project/bootstrap"
|
import { InstanceBootstrap } from "../../project/bootstrap"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
|
|
||||||
export const ProjectRoutes = lazy(() =>
|
export const ProjectRoutes = lazy(() =>
|
||||||
new Hono()
|
new Hono()
|
||||||
@@ -84,7 +83,7 @@ export const ProjectRoutes = lazy(() =>
|
|||||||
directory: dir,
|
directory: dir,
|
||||||
worktree: dir,
|
worktree: dir,
|
||||||
project: next,
|
project: next,
|
||||||
init: () => AppRuntime.runPromise(InstanceBootstrap),
|
init: InstanceBootstrap,
|
||||||
})
|
})
|
||||||
return c.json(next)
|
return c.json(next)
|
||||||
},
|
},
|
||||||
+19
-29
@@ -11,7 +11,6 @@ import { mapValues } from "remeda"
|
|||||||
import { errors } from "../error"
|
import { errors } from "../error"
|
||||||
import { lazy } from "../../util/lazy"
|
import { lazy } from "../../util/lazy"
|
||||||
import { Log } from "../../util/log"
|
import { Log } from "../../util/log"
|
||||||
import { Effect } from "effect"
|
|
||||||
|
|
||||||
const log = Log.create({ service: "server" })
|
const log = Log.create({ service: "server" })
|
||||||
|
|
||||||
@@ -41,36 +40,27 @@ export const ProviderRoutes = lazy(() =>
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const result = await AppRuntime.runPromise(
|
const config = await Config.get()
|
||||||
Effect.gen(function* () {
|
const disabled = new Set(config.disabled_providers ?? [])
|
||||||
const svc = yield* Provider.Service
|
const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined
|
||||||
const cfg = yield* Config.Service
|
|
||||||
const config = yield* cfg.get()
|
const allProviders = await ModelsDev.get()
|
||||||
const all = yield* Effect.promise(() => ModelsDev.get())
|
const filteredProviders: Record<string, (typeof allProviders)[string]> = {}
|
||||||
const disabled = new Set(config.disabled_providers ?? [])
|
for (const [key, value] of Object.entries(allProviders)) {
|
||||||
const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined
|
if ((enabled ? enabled.has(key) : true) && !disabled.has(key)) {
|
||||||
const filtered: Record<string, (typeof all)[string]> = {}
|
filteredProviders[key] = value
|
||||||
for (const [key, value] of Object.entries(all)) {
|
}
|
||||||
if ((enabled ? enabled.has(key) : true) && !disabled.has(key)) {
|
}
|
||||||
filtered[key] = value
|
|
||||||
}
|
const connected = await Provider.list()
|
||||||
}
|
const providers = Object.assign(
|
||||||
const connected = yield* svc.list()
|
mapValues(filteredProviders, (x) => Provider.fromModelsDevProvider(x)),
|
||||||
const providers = Object.assign(
|
connected,
|
||||||
mapValues(filtered, (x) => Provider.fromModelsDevProvider(x)),
|
|
||||||
connected,
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
all: Object.values(providers),
|
|
||||||
default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
|
|
||||||
connected: Object.keys(connected),
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
return c.json({
|
return c.json({
|
||||||
all: result.all,
|
all: Object.values(providers),
|
||||||
default: result.default,
|
default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
|
||||||
connected: result.connected,
|
connected: Object.keys(connected),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
+8
-56
@@ -1,9 +1,7 @@
|
|||||||
import { Hono, type MiddlewareHandler } from "hono"
|
import { Hono, type MiddlewareHandler } from "hono"
|
||||||
import { describeRoute, validator, resolver } from "hono-openapi"
|
import { describeRoute, validator, resolver } from "hono-openapi"
|
||||||
import type { UpgradeWebSocket } from "hono/ws"
|
import type { UpgradeWebSocket } from "hono/ws"
|
||||||
import { Effect } from "effect"
|
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
import { Pty } from "@/pty"
|
import { Pty } from "@/pty"
|
||||||
import { PtyID } from "@/pty/schema"
|
import { PtyID } from "@/pty/schema"
|
||||||
import { NotFoundError } from "../../storage/db"
|
import { NotFoundError } from "../../storage/db"
|
||||||
@@ -29,14 +27,7 @@ export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
return c.json(
|
return c.json(await Pty.list())
|
||||||
await AppRuntime.runPromise(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const pty = yield* Pty.Service
|
|
||||||
return yield* pty.list()
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.post(
|
.post(
|
||||||
@@ -59,12 +50,7 @@ export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) {
|
|||||||
}),
|
}),
|
||||||
validator("json", Pty.CreateInput),
|
validator("json", Pty.CreateInput),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const info = await AppRuntime.runPromise(
|
const info = await Pty.create(c.req.valid("json"))
|
||||||
Effect.gen(function* () {
|
|
||||||
const pty = yield* Pty.Service
|
|
||||||
return yield* pty.create(c.req.valid("json"))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
return c.json(info)
|
return c.json(info)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -88,12 +74,7 @@ export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) {
|
|||||||
}),
|
}),
|
||||||
validator("param", z.object({ ptyID: PtyID.zod })),
|
validator("param", z.object({ ptyID: PtyID.zod })),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const info = await AppRuntime.runPromise(
|
const info = await Pty.get(c.req.valid("param").ptyID)
|
||||||
Effect.gen(function* () {
|
|
||||||
const pty = yield* Pty.Service
|
|
||||||
return yield* pty.get(c.req.valid("param").ptyID)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
if (!info) {
|
if (!info) {
|
||||||
throw new NotFoundError({ message: "Session not found" })
|
throw new NotFoundError({ message: "Session not found" })
|
||||||
}
|
}
|
||||||
@@ -121,12 +102,7 @@ export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) {
|
|||||||
validator("param", z.object({ ptyID: PtyID.zod })),
|
validator("param", z.object({ ptyID: PtyID.zod })),
|
||||||
validator("json", Pty.UpdateInput),
|
validator("json", Pty.UpdateInput),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const info = await AppRuntime.runPromise(
|
const info = await Pty.update(c.req.valid("param").ptyID, c.req.valid("json"))
|
||||||
Effect.gen(function* () {
|
|
||||||
const pty = yield* Pty.Service
|
|
||||||
return yield* pty.update(c.req.valid("param").ptyID, c.req.valid("json"))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
return c.json(info)
|
return c.json(info)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -150,12 +126,7 @@ export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) {
|
|||||||
}),
|
}),
|
||||||
validator("param", z.object({ ptyID: PtyID.zod })),
|
validator("param", z.object({ ptyID: PtyID.zod })),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
await AppRuntime.runPromise(
|
await Pty.remove(c.req.valid("param").ptyID)
|
||||||
Effect.gen(function* () {
|
|
||||||
const pty = yield* Pty.Service
|
|
||||||
yield* pty.remove(c.req.valid("param").ptyID)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
return c.json(true)
|
return c.json(true)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -179,11 +150,6 @@ export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) {
|
|||||||
}),
|
}),
|
||||||
validator("param", z.object({ ptyID: PtyID.zod })),
|
validator("param", z.object({ ptyID: PtyID.zod })),
|
||||||
upgradeWebSocket(async (c) => {
|
upgradeWebSocket(async (c) => {
|
||||||
type Handler = {
|
|
||||||
onMessage: (message: string | ArrayBuffer) => void
|
|
||||||
onClose: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const id = PtyID.zod.parse(c.req.param("ptyID"))
|
const id = PtyID.zod.parse(c.req.param("ptyID"))
|
||||||
const cursor = (() => {
|
const cursor = (() => {
|
||||||
const value = c.req.query("cursor")
|
const value = c.req.query("cursor")
|
||||||
@@ -192,17 +158,8 @@ export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) {
|
|||||||
if (!Number.isSafeInteger(parsed) || parsed < -1) return
|
if (!Number.isSafeInteger(parsed) || parsed < -1) return
|
||||||
return parsed
|
return parsed
|
||||||
})()
|
})()
|
||||||
let handler: Handler | undefined
|
let handler: Awaited<ReturnType<typeof Pty.connect>>
|
||||||
if (
|
if (!(await Pty.get(id))) throw new Error("Session not found")
|
||||||
!(await AppRuntime.runPromise(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const pty = yield* Pty.Service
|
|
||||||
return yield* pty.get(id)
|
|
||||||
}),
|
|
||||||
))
|
|
||||||
) {
|
|
||||||
throw new Error("Session not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
type Socket = {
|
type Socket = {
|
||||||
readyState: number
|
readyState: number
|
||||||
@@ -228,12 +185,7 @@ export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) {
|
|||||||
ws.close()
|
ws.close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
handler = await AppRuntime.runPromise(
|
handler = await Pty.connect(id, socket, cursor)
|
||||||
Effect.gen(function* () {
|
|
||||||
const pty = yield* Pty.Service
|
|
||||||
return yield* pty.connect(id, socket, cursor)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
ready = true
|
ready = true
|
||||||
for (const msg of pending) handler?.onMessage(msg)
|
for (const msg of pending) handler?.onMessage(msg)
|
||||||
pending.length = 0
|
pending.length = 0
|
||||||
+6
-11
@@ -3,7 +3,6 @@ import { describeRoute, validator } from "hono-openapi"
|
|||||||
import { resolver } from "hono-openapi"
|
import { resolver } from "hono-openapi"
|
||||||
import { QuestionID } from "@/question/schema"
|
import { QuestionID } from "@/question/schema"
|
||||||
import { Question } from "../../question"
|
import { Question } from "../../question"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { errors } from "../error"
|
import { errors } from "../error"
|
||||||
import { lazy } from "../../util/lazy"
|
import { lazy } from "../../util/lazy"
|
||||||
@@ -28,7 +27,7 @@ export const QuestionRoutes = lazy(() =>
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const questions = await AppRuntime.runPromise(Question.Service.use((svc) => svc.list()))
|
const questions = await Question.list()
|
||||||
return c.json(questions)
|
return c.json(questions)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -60,14 +59,10 @@ export const QuestionRoutes = lazy(() =>
|
|||||||
async (c) => {
|
async (c) => {
|
||||||
const params = c.req.valid("param")
|
const params = c.req.valid("param")
|
||||||
const json = c.req.valid("json")
|
const json = c.req.valid("json")
|
||||||
await AppRuntime.runPromise(
|
await Question.reply({
|
||||||
Question.Service.use((svc) =>
|
requestID: params.requestID,
|
||||||
svc.reply({
|
answers: json.answers,
|
||||||
requestID: params.requestID,
|
})
|
||||||
answers: json.answers,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return c.json(true)
|
return c.json(true)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -97,7 +92,7 @@ export const QuestionRoutes = lazy(() =>
|
|||||||
),
|
),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const params = c.req.valid("param")
|
const params = c.req.valid("param")
|
||||||
await AppRuntime.runPromise(Question.Service.use((svc) => svc.reject(params.requestID)))
|
await Question.reject(params.requestID)
|
||||||
return c.json(true)
|
return c.json(true)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
+5
-20
@@ -13,7 +13,6 @@ import { SessionShare } from "@/share/session"
|
|||||||
import { SessionStatus } from "@/session/status"
|
import { SessionStatus } from "@/session/status"
|
||||||
import { SessionSummary } from "@/session/summary"
|
import { SessionSummary } from "@/session/summary"
|
||||||
import { Todo } from "../../session/todo"
|
import { Todo } from "../../session/todo"
|
||||||
import { Effect } from "effect"
|
|
||||||
import { AppRuntime } from "../../effect/app-runtime"
|
import { AppRuntime } from "../../effect/app-runtime"
|
||||||
import { Agent } from "../../agent/agent"
|
import { Agent } from "../../agent/agent"
|
||||||
import { Snapshot } from "@/snapshot"
|
import { Snapshot } from "@/snapshot"
|
||||||
@@ -274,7 +273,6 @@ export const SessionRoutes = lazy(() =>
|
|||||||
"json",
|
"json",
|
||||||
z.object({
|
z.object({
|
||||||
title: z.string().optional(),
|
title: z.string().optional(),
|
||||||
permission: Permission.Ruleset.optional(),
|
|
||||||
time: z
|
time: z
|
||||||
.object({
|
.object({
|
||||||
archived: z.number().optional(),
|
archived: z.number().optional(),
|
||||||
@@ -285,17 +283,10 @@ export const SessionRoutes = lazy(() =>
|
|||||||
async (c) => {
|
async (c) => {
|
||||||
const sessionID = c.req.valid("param").sessionID
|
const sessionID = c.req.valid("param").sessionID
|
||||||
const updates = c.req.valid("json")
|
const updates = c.req.valid("json")
|
||||||
const current = await Session.get(sessionID)
|
|
||||||
|
|
||||||
if (updates.title !== undefined) {
|
if (updates.title !== undefined) {
|
||||||
await Session.setTitle({ sessionID, title: updates.title })
|
await Session.setTitle({ sessionID, title: updates.title })
|
||||||
}
|
}
|
||||||
if (updates.permission !== undefined) {
|
|
||||||
await Session.setPermission({
|
|
||||||
sessionID,
|
|
||||||
permission: Permission.merge(current.permission ?? [], updates.permission),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (updates.time?.archived !== undefined) {
|
if (updates.time?.archived !== undefined) {
|
||||||
await Session.setArchived({ sessionID, time: updates.time.archived })
|
await Session.setArchived({ sessionID, time: updates.time.archived })
|
||||||
}
|
}
|
||||||
@@ -725,17 +716,11 @@ export const SessionRoutes = lazy(() =>
|
|||||||
),
|
),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const params = c.req.valid("param")
|
const params = c.req.valid("param")
|
||||||
await AppRuntime.runPromise(
|
await SessionRunState.assertNotBusy(params.sessionID)
|
||||||
Effect.gen(function* () {
|
await Session.removeMessage({
|
||||||
const state = yield* SessionRunState.Service
|
sessionID: params.sessionID,
|
||||||
const session = yield* Session.Service
|
messageID: params.messageID,
|
||||||
yield* state.assertNotBusy(params.sessionID)
|
})
|
||||||
yield* session.removeMessage({
|
|
||||||
sessionID: params.sessionID,
|
|
||||||
messageID: params.messageID,
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
return c.json(true)
|
return c.json(true)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
+4
-32
@@ -1,41 +1,13 @@
|
|||||||
import { Hono } from "hono"
|
import { Hono } from "hono"
|
||||||
import { describeRoute, resolver, validator } from "hono-openapi"
|
import { describeRoute, resolver, validator } from "hono-openapi"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { listAdaptors } from "../../control-plane/adaptors"
|
|
||||||
import { Workspace } from "../../control-plane/workspace"
|
import { Workspace } from "../../control-plane/workspace"
|
||||||
import { Instance } from "../../project/instance"
|
import { Instance } from "../../project/instance"
|
||||||
import { errors } from "../error"
|
import { errors } from "../error"
|
||||||
import { lazy } from "../../util/lazy"
|
import { lazy } from "../../util/lazy"
|
||||||
|
|
||||||
const WorkspaceAdaptor = z.object({
|
|
||||||
type: z.string(),
|
|
||||||
name: z.string(),
|
|
||||||
description: z.string(),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const WorkspaceRoutes = lazy(() =>
|
export const WorkspaceRoutes = lazy(() =>
|
||||||
new Hono()
|
new Hono()
|
||||||
.get(
|
|
||||||
"/adaptor",
|
|
||||||
describeRoute({
|
|
||||||
summary: "List workspace adaptors",
|
|
||||||
description: "List all available workspace adaptors for the current project.",
|
|
||||||
operationId: "experimental.workspace.adaptor.list",
|
|
||||||
responses: {
|
|
||||||
200: {
|
|
||||||
description: "Workspace adaptors",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: resolver(z.array(WorkspaceAdaptor)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
async (c) => {
|
|
||||||
return c.json(await listAdaptors(Instance.project.id))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.post(
|
.post(
|
||||||
"/",
|
"/",
|
||||||
describeRoute({
|
describeRoute({
|
||||||
@@ -56,7 +28,7 @@ export const WorkspaceRoutes = lazy(() =>
|
|||||||
}),
|
}),
|
||||||
validator(
|
validator(
|
||||||
"json",
|
"json",
|
||||||
Workspace.create.schema.omit({
|
Workspace.CreateInput.omit({
|
||||||
projectID: true,
|
projectID: true,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -87,7 +59,7 @@ export const WorkspaceRoutes = lazy(() =>
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
return c.json(Workspace.list(Instance.project))
|
return c.json(await Workspace.list(Instance.project))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.get(
|
.get(
|
||||||
@@ -108,8 +80,8 @@ export const WorkspaceRoutes = lazy(() =>
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const ids = new Set(Workspace.list(Instance.project).map((item) => item.id))
|
const ids = new Set((await Workspace.list(Instance.project)).map((item) => item.id))
|
||||||
return c.json(Workspace.status().filter((item) => ids.has(item.workspaceID)))
|
return c.json((await Workspace.status()).filter((item) => ids.has(item.workspaceID)))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.delete(
|
.delete(
|
||||||
@@ -1,14 +1,24 @@
|
|||||||
import { generateSpecs } from "hono-openapi"
|
import { Log } from "../util/log"
|
||||||
|
import { describeRoute, generateSpecs, validator, resolver, openAPIRouteHandler } from "hono-openapi"
|
||||||
import { Hono } from "hono"
|
import { Hono } from "hono"
|
||||||
import { adapter } from "#hono"
|
import { compress } from "hono/compress"
|
||||||
|
import { createNodeWebSocket } from "@hono/node-ws"
|
||||||
|
import { cors } from "hono/cors"
|
||||||
|
import { basicAuth } from "hono/basic-auth"
|
||||||
|
import type { UpgradeWebSocket } from "hono/ws"
|
||||||
|
import z from "zod"
|
||||||
|
import { Auth } from "../auth"
|
||||||
|
import { Flag } from "../flag/flag"
|
||||||
|
import { ProviderID } from "../provider/schema"
|
||||||
|
import { WorkspaceRouterMiddleware } from "./router"
|
||||||
|
import { errors } from "./error"
|
||||||
|
import { GlobalRoutes } from "./routes/global"
|
||||||
import { MDNS } from "./mdns"
|
import { MDNS } from "./mdns"
|
||||||
import { lazy } from "@/util/lazy"
|
import { lazy } from "@/util/lazy"
|
||||||
import { AuthMiddleware, CompressionMiddleware, CorsMiddleware, ErrorMiddleware, LoggerMiddleware } from "./middleware"
|
import { errorHandler } from "./middleware"
|
||||||
import { InstanceRoutes } from "./instance"
|
import { InstanceRoutes } from "./instance"
|
||||||
import { initProjectors } from "./projectors"
|
import { initProjectors } from "./projectors"
|
||||||
import { Log } from "@/util/log"
|
import { createAdaptorServer, type ServerType } from "@hono/node-server"
|
||||||
import { ControlPlaneRoutes } from "./control"
|
|
||||||
import { UIRoutes } from "./ui"
|
|
||||||
|
|
||||||
// @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85
|
// @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85
|
||||||
globalThis.AI_SDK_LOG_WARNINGS = false
|
globalThis.AI_SDK_LOG_WARNINGS = false
|
||||||
@@ -16,8 +26,6 @@ globalThis.AI_SDK_LOG_WARNINGS = false
|
|||||||
initProjectors()
|
initProjectors()
|
||||||
|
|
||||||
export namespace Server {
|
export namespace Server {
|
||||||
const log = Log.create({ service: "server" })
|
|
||||||
|
|
||||||
export type Listener = {
|
export type Listener = {
|
||||||
hostname: string
|
hostname: string
|
||||||
port: number
|
port: number
|
||||||
@@ -25,31 +33,231 @@ export namespace Server {
|
|||||||
stop: (close?: boolean) => Promise<void>
|
stop: (close?: boolean) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const log = Log.create({ service: "server" })
|
||||||
|
const zipped = compress()
|
||||||
|
|
||||||
|
const skipCompress = (path: string, method: string) => {
|
||||||
|
if (path === "/event" || path === "/global/event" || path === "/global/sync-event") return true
|
||||||
|
if (method === "POST" && /\/session\/[^/]+\/(message|prompt_async)$/.test(path)) return true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
export const Default = lazy(() => create({}))
|
export const Default = lazy(() => create({}))
|
||||||
|
|
||||||
|
export function ControlPlaneRoutes(upgrade: UpgradeWebSocket, app = new Hono(), opts?: { cors?: string[] }): Hono {
|
||||||
|
return app
|
||||||
|
.onError(errorHandler(log))
|
||||||
|
.use((c, next) => {
|
||||||
|
// Allow CORS preflight requests to succeed without auth.
|
||||||
|
// Browser clients sending Authorization headers will preflight with OPTIONS.
|
||||||
|
if (c.req.method === "OPTIONS") return next()
|
||||||
|
const password = Flag.OPENCODE_SERVER_PASSWORD
|
||||||
|
if (!password) return next()
|
||||||
|
const username = Flag.OPENCODE_SERVER_USERNAME ?? "opencode"
|
||||||
|
|
||||||
|
if (c.req.query("auth_token")) c.req.raw.headers.set("authorization", `Basic ${c.req.query("auth_token")}`)
|
||||||
|
|
||||||
|
return basicAuth({ username, password })(c, next)
|
||||||
|
})
|
||||||
|
.use(async (c, next) => {
|
||||||
|
const skip = c.req.path === "/log"
|
||||||
|
if (!skip) {
|
||||||
|
log.info("request", {
|
||||||
|
method: c.req.method,
|
||||||
|
path: c.req.path,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const timer = log.time("request", {
|
||||||
|
method: c.req.method,
|
||||||
|
path: c.req.path,
|
||||||
|
})
|
||||||
|
await next()
|
||||||
|
if (!skip) timer.stop()
|
||||||
|
})
|
||||||
|
.use(
|
||||||
|
cors({
|
||||||
|
maxAge: 86_400,
|
||||||
|
origin(input) {
|
||||||
|
if (!input) return
|
||||||
|
|
||||||
|
if (input.startsWith("http://localhost:")) return input
|
||||||
|
if (input.startsWith("http://127.0.0.1:")) return input
|
||||||
|
if (
|
||||||
|
input === "tauri://localhost" ||
|
||||||
|
input === "http://tauri.localhost" ||
|
||||||
|
input === "https://tauri.localhost"
|
||||||
|
)
|
||||||
|
return input
|
||||||
|
|
||||||
|
if (/^https:\/\/([a-z0-9-]+\.)*opencode\.ai$/.test(input)) return input
|
||||||
|
if (opts?.cors?.includes(input)) return input
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.use((c, next) => {
|
||||||
|
if (skipCompress(c.req.path, c.req.method)) return next()
|
||||||
|
return zipped(c, next)
|
||||||
|
})
|
||||||
|
.route("/global", GlobalRoutes())
|
||||||
|
.put(
|
||||||
|
"/auth/:providerID",
|
||||||
|
describeRoute({
|
||||||
|
summary: "Set auth credentials",
|
||||||
|
description: "Set authentication credentials",
|
||||||
|
operationId: "auth.set",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Successfully set authentication credentials",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(z.boolean()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...errors(400),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
validator(
|
||||||
|
"param",
|
||||||
|
z.object({
|
||||||
|
providerID: ProviderID.zod,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
validator("json", Auth.Info.zod),
|
||||||
|
async (c) => {
|
||||||
|
const providerID = c.req.valid("param").providerID
|
||||||
|
const info = c.req.valid("json")
|
||||||
|
await Auth.set(providerID, info)
|
||||||
|
return c.json(true)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.delete(
|
||||||
|
"/auth/:providerID",
|
||||||
|
describeRoute({
|
||||||
|
summary: "Remove auth credentials",
|
||||||
|
description: "Remove authentication credentials",
|
||||||
|
operationId: "auth.remove",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Successfully removed authentication credentials",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(z.boolean()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...errors(400),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
validator(
|
||||||
|
"param",
|
||||||
|
z.object({
|
||||||
|
providerID: ProviderID.zod,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
async (c) => {
|
||||||
|
const providerID = c.req.valid("param").providerID
|
||||||
|
await Auth.remove(providerID)
|
||||||
|
return c.json(true)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.get(
|
||||||
|
"/doc",
|
||||||
|
openAPIRouteHandler(app, {
|
||||||
|
documentation: {
|
||||||
|
info: {
|
||||||
|
title: "opencode",
|
||||||
|
version: "0.0.3",
|
||||||
|
description: "opencode api",
|
||||||
|
},
|
||||||
|
openapi: "3.1.1",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.use(
|
||||||
|
validator(
|
||||||
|
"query",
|
||||||
|
z.object({
|
||||||
|
directory: z.string().optional(),
|
||||||
|
workspace: z.string().optional(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.post(
|
||||||
|
"/log",
|
||||||
|
describeRoute({
|
||||||
|
summary: "Write log",
|
||||||
|
description: "Write a log entry to the server logs with specified level and metadata.",
|
||||||
|
operationId: "app.log",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Log entry written successfully",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(z.boolean()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...errors(400),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
validator(
|
||||||
|
"json",
|
||||||
|
z.object({
|
||||||
|
service: z.string().meta({ description: "Service name for the log entry" }),
|
||||||
|
level: z.enum(["debug", "info", "error", "warn"]).meta({ description: "Log level" }),
|
||||||
|
message: z.string().meta({ description: "Log message" }),
|
||||||
|
extra: z
|
||||||
|
.record(z.string(), z.any())
|
||||||
|
.optional()
|
||||||
|
.meta({ description: "Additional metadata for the log entry" }),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
async (c) => {
|
||||||
|
const { service, level, message, extra } = c.req.valid("json")
|
||||||
|
const logger = Log.create({ service })
|
||||||
|
|
||||||
|
switch (level) {
|
||||||
|
case "debug":
|
||||||
|
logger.debug(message, extra)
|
||||||
|
break
|
||||||
|
case "info":
|
||||||
|
logger.info(message, extra)
|
||||||
|
break
|
||||||
|
case "error":
|
||||||
|
logger.error(message, extra)
|
||||||
|
break
|
||||||
|
case "warn":
|
||||||
|
logger.warn(message, extra)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json(true)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.use(WorkspaceRouterMiddleware(upgrade))
|
||||||
|
}
|
||||||
|
|
||||||
function create(opts: { cors?: string[] }) {
|
function create(opts: { cors?: string[] }) {
|
||||||
const app = new Hono()
|
const app = new Hono()
|
||||||
const runtime = adapter.create(app)
|
const ws = createNodeWebSocket({ app })
|
||||||
return {
|
return {
|
||||||
app: app
|
app: ControlPlaneRoutes(ws.upgradeWebSocket, app, opts),
|
||||||
.onError(ErrorMiddleware)
|
ws,
|
||||||
.use(AuthMiddleware)
|
|
||||||
.use(LoggerMiddleware)
|
|
||||||
.use(CompressionMiddleware)
|
|
||||||
.use(CorsMiddleware(opts))
|
|
||||||
.route("/", ControlPlaneRoutes())
|
|
||||||
.route("/", InstanceRoutes(runtime.upgradeWebSocket))
|
|
||||||
.route("/", UIRoutes()),
|
|
||||||
runtime,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createApp(opts: { cors?: string[] }) {
|
||||||
|
return create(opts).app
|
||||||
|
}
|
||||||
|
|
||||||
export async function openapi() {
|
export async function openapi() {
|
||||||
// Build a fresh app with all routes registered directly so
|
// Build a fresh app with all routes registered directly so
|
||||||
// hono-openapi can see describeRoute metadata (`.route()` wraps
|
// hono-openapi can see describeRoute metadata (`.route()` wraps
|
||||||
// handlers when the sub-app has a custom errorHandler, which
|
// handlers when the sub-app has a custom errorHandler, which
|
||||||
// strips the metadata symbol).
|
// strips the metadata symbol).
|
||||||
const { app } = create({})
|
const { app, ws } = create({})
|
||||||
|
InstanceRoutes(ws.upgradeWebSocket, app)
|
||||||
const result = await generateSpecs(app, {
|
const result = await generateSpecs(app, {
|
||||||
documentation: {
|
documentation: {
|
||||||
info: {
|
info: {
|
||||||
@@ -73,21 +281,46 @@ export namespace Server {
|
|||||||
cors?: string[]
|
cors?: string[]
|
||||||
}): Promise<Listener> {
|
}): Promise<Listener> {
|
||||||
const built = create(opts)
|
const built = create(opts)
|
||||||
const server = await built.runtime.listen(opts)
|
const start = (port: number) =>
|
||||||
|
new Promise<ServerType>((resolve, reject) => {
|
||||||
|
const server = createAdaptorServer({ fetch: built.app.fetch })
|
||||||
|
built.ws.injectWebSocket(server)
|
||||||
|
const fail = (err: Error) => {
|
||||||
|
cleanup()
|
||||||
|
reject(err)
|
||||||
|
}
|
||||||
|
const ready = () => {
|
||||||
|
cleanup()
|
||||||
|
resolve(server)
|
||||||
|
}
|
||||||
|
const cleanup = () => {
|
||||||
|
server.off("error", fail)
|
||||||
|
server.off("listening", ready)
|
||||||
|
}
|
||||||
|
server.once("error", fail)
|
||||||
|
server.once("listening", ready)
|
||||||
|
server.listen(port, opts.hostname)
|
||||||
|
})
|
||||||
|
|
||||||
|
const server = opts.port === 0 ? await start(4096).catch(() => start(0)) : await start(opts.port)
|
||||||
|
const addr = server.address()
|
||||||
|
if (!addr || typeof addr === "string") {
|
||||||
|
throw new Error(`Failed to resolve server address for port ${opts.port}`)
|
||||||
|
}
|
||||||
|
|
||||||
const next = new URL("http://localhost")
|
const next = new URL("http://localhost")
|
||||||
next.hostname = opts.hostname
|
next.hostname = opts.hostname
|
||||||
next.port = String(server.port)
|
next.port = String(addr.port)
|
||||||
url = next
|
url = next
|
||||||
|
|
||||||
const mdns =
|
const mdns =
|
||||||
opts.mdns &&
|
opts.mdns &&
|
||||||
server.port &&
|
addr.port &&
|
||||||
opts.hostname !== "127.0.0.1" &&
|
opts.hostname !== "127.0.0.1" &&
|
||||||
opts.hostname !== "localhost" &&
|
opts.hostname !== "localhost" &&
|
||||||
opts.hostname !== "::1"
|
opts.hostname !== "::1"
|
||||||
if (mdns) {
|
if (mdns) {
|
||||||
MDNS.publish(server.port, opts.mdnsDomain)
|
MDNS.publish(addr.port, opts.mdnsDomain)
|
||||||
} else if (opts.mdns) {
|
} else if (opts.mdns) {
|
||||||
log.warn("mDNS enabled but hostname is loopback; skipping mDNS publish")
|
log.warn("mDNS enabled but hostname is loopback; skipping mDNS publish")
|
||||||
}
|
}
|
||||||
@@ -95,13 +328,27 @@ export namespace Server {
|
|||||||
let closing: Promise<void> | undefined
|
let closing: Promise<void> | undefined
|
||||||
return {
|
return {
|
||||||
hostname: opts.hostname,
|
hostname: opts.hostname,
|
||||||
port: server.port,
|
port: addr.port,
|
||||||
url: next,
|
url: next,
|
||||||
stop(close?: boolean) {
|
stop(close?: boolean) {
|
||||||
closing ??= (async () => {
|
closing ??= new Promise((resolve, reject) => {
|
||||||
if (mdns) MDNS.unpublish()
|
if (mdns) MDNS.unpublish()
|
||||||
await server.stop(close)
|
server.close((err) => {
|
||||||
})()
|
if (err) {
|
||||||
|
reject(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
if (close) {
|
||||||
|
if ("closeAllConnections" in server && typeof server.closeAllConnections === "function") {
|
||||||
|
server.closeAllConnections()
|
||||||
|
}
|
||||||
|
if ("closeIdleConnections" in server && typeof server.closeIdleConnections === "function") {
|
||||||
|
server.closeIdleConnections()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
return closing
|
return closing
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
import { Flag } from "@/flag/flag"
|
|
||||||
import { Hono } from "hono"
|
|
||||||
import { proxy } from "hono/proxy"
|
|
||||||
import { getMimeType } from "hono/utils/mime"
|
|
||||||
import { createHash } from "node:crypto"
|
|
||||||
import fs from "node:fs/promises"
|
|
||||||
|
|
||||||
const embeddedUIPromise = Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI
|
|
||||||
? Promise.resolve(null)
|
|
||||||
: // @ts-expect-error - generated file at build time
|
|
||||||
import("opencode-web-ui.gen.ts").then((module) => module.default as Record<string, string>).catch(() => null)
|
|
||||||
|
|
||||||
const DEFAULT_CSP =
|
|
||||||
"default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src 'self' data:"
|
|
||||||
|
|
||||||
const csp = (hash = "") =>
|
|
||||||
`default-src 'self'; script-src 'self' 'wasm-unsafe-eval'${hash ? ` 'sha256-${hash}'` : ""}; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src 'self' data:`
|
|
||||||
|
|
||||||
export const UIRoutes = (): Hono =>
|
|
||||||
new Hono().all("/*", async (c) => {
|
|
||||||
const embeddedWebUI = await embeddedUIPromise
|
|
||||||
const path = c.req.path
|
|
||||||
|
|
||||||
if (embeddedWebUI) {
|
|
||||||
const match = embeddedWebUI[path.replace(/^\//, "")] ?? embeddedWebUI["index.html"] ?? null
|
|
||||||
if (!match) return c.json({ error: "Not Found" }, 404)
|
|
||||||
|
|
||||||
if (await fs.exists(match)) {
|
|
||||||
const mime = getMimeType(match) ?? "text/plain"
|
|
||||||
c.header("Content-Type", mime)
|
|
||||||
if (mime.startsWith("text/html")) {
|
|
||||||
c.header("Content-Security-Policy", DEFAULT_CSP)
|
|
||||||
}
|
|
||||||
return c.body(new Uint8Array(await fs.readFile(match)))
|
|
||||||
} else {
|
|
||||||
return c.json({ error: "Not Found" }, 404)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const response = await proxy(`https://app.opencode.ai${path}`, {
|
|
||||||
...c.req,
|
|
||||||
headers: {
|
|
||||||
...c.req.raw.headers,
|
|
||||||
host: "app.opencode.ai",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const match = response.headers.get("content-type")?.includes("text/html")
|
|
||||||
? (await response.clone().text()).match(
|
|
||||||
/<script\b(?![^>]*\bsrc\s*=)[^>]*\bid=(['"])oc-theme-preload-script\1[^>]*>([\s\S]*?)<\/script>/i,
|
|
||||||
)
|
|
||||||
: undefined
|
|
||||||
const hash = match ? createHash("sha256").update(match[2]).digest("base64") : ""
|
|
||||||
response.headers.set("Content-Security-Policy", csp(hash))
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -4,7 +4,7 @@ import { BusEvent } from "@/bus/bus-event"
|
|||||||
import { Bus } from "@/bus"
|
import { Bus } from "@/bus"
|
||||||
import { Decimal } from "decimal.js"
|
import { Decimal } from "decimal.js"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { type ProviderMetadata, type LanguageModelUsage } from "ai"
|
import { type ProviderMetadata } from "ai"
|
||||||
import { Flag } from "../flag/flag"
|
import { Flag } from "../flag/flag"
|
||||||
import { Installation } from "../installation"
|
import { Installation } from "../installation"
|
||||||
|
|
||||||
@@ -28,6 +28,7 @@ import { SessionID, MessageID, PartID } from "./schema"
|
|||||||
import type { Provider } from "@/provider/provider"
|
import type { Provider } from "@/provider/provider"
|
||||||
import { Permission } from "@/permission"
|
import { Permission } from "@/permission"
|
||||||
import { Global } from "@/global"
|
import { Global } from "@/global"
|
||||||
|
import type { LanguageModelV2Usage } from "@ai-sdk/provider"
|
||||||
import { Effect, Layer, Option, Context } from "effect"
|
import { Effect, Layer, Option, Context } from "effect"
|
||||||
import { makeRuntime } from "@/effect/run-service"
|
import { makeRuntime } from "@/effect/run-service"
|
||||||
|
|
||||||
@@ -239,7 +240,7 @@ export namespace Session {
|
|||||||
|
|
||||||
export const getUsage = (input: {
|
export const getUsage = (input: {
|
||||||
model: Provider.Model
|
model: Provider.Model
|
||||||
usage: LanguageModelUsage
|
usage: LanguageModelV2Usage
|
||||||
metadata?: ProviderMetadata
|
metadata?: ProviderMetadata
|
||||||
}) => {
|
}) => {
|
||||||
const safe = (value: number) => {
|
const safe = (value: number) => {
|
||||||
@@ -248,14 +249,11 @@ export namespace Session {
|
|||||||
}
|
}
|
||||||
const inputTokens = safe(input.usage.inputTokens ?? 0)
|
const inputTokens = safe(input.usage.inputTokens ?? 0)
|
||||||
const outputTokens = safe(input.usage.outputTokens ?? 0)
|
const outputTokens = safe(input.usage.outputTokens ?? 0)
|
||||||
const reasoningTokens = safe(input.usage.outputTokenDetails?.reasoningTokens ?? input.usage.reasoningTokens ?? 0)
|
const reasoningTokens = safe(input.usage.reasoningTokens ?? 0)
|
||||||
|
|
||||||
const cacheReadInputTokens = safe(
|
const cacheReadInputTokens = safe(input.usage.cachedInputTokens ?? 0)
|
||||||
input.usage.inputTokenDetails?.cacheReadTokens ?? input.usage.cachedInputTokens ?? 0,
|
|
||||||
)
|
|
||||||
const cacheWriteInputTokens = safe(
|
const cacheWriteInputTokens = safe(
|
||||||
(input.usage.inputTokenDetails?.cacheWriteTokens ??
|
(input.metadata?.["anthropic"]?.["cacheCreationInputTokens"] ??
|
||||||
input.metadata?.["anthropic"]?.["cacheCreationInputTokens"] ??
|
|
||||||
// google-vertex-anthropic returns metadata under "vertex" key
|
// google-vertex-anthropic returns metadata under "vertex" key
|
||||||
// (AnthropicMessagesLanguageModel custom provider key from 'vertex.anthropic.messages')
|
// (AnthropicMessagesLanguageModel custom provider key from 'vertex.anthropic.messages')
|
||||||
input.metadata?.["vertex"]?.["cacheCreationInputTokens"] ??
|
input.metadata?.["vertex"]?.["cacheCreationInputTokens"] ??
|
||||||
@@ -276,7 +274,7 @@ export namespace Session {
|
|||||||
const tokens = {
|
const tokens = {
|
||||||
total,
|
total,
|
||||||
input: adjustedInputTokens,
|
input: adjustedInputTokens,
|
||||||
output: safe(outputTokens - reasoningTokens),
|
output: outputTokens - reasoningTokens,
|
||||||
reasoning: reasoningTokens,
|
reasoning: reasoningTokens,
|
||||||
cache: {
|
cache: {
|
||||||
write: cacheWriteInputTokens,
|
write: cacheWriteInputTokens,
|
||||||
@@ -710,10 +708,6 @@ export namespace Session {
|
|||||||
runPromise((svc) => svc.setArchived(input)),
|
runPromise((svc) => svc.setArchived(input)),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const setPermission = fn(z.object({ sessionID: SessionID.zod, permission: Permission.Ruleset }), (input) =>
|
|
||||||
runPromise((svc) => svc.setPermission(input)),
|
|
||||||
)
|
|
||||||
|
|
||||||
export const setRevert = fn(
|
export const setRevert = fn(
|
||||||
z.object({ sessionID: SessionID.zod, revert: Info.shape.revert, summary: Info.shape.summary }),
|
z.object({ sessionID: SessionID.zod, revert: Info.shape.revert, summary: Info.shape.summary }),
|
||||||
(input) =>
|
(input) =>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Effect, Layer, Context } from "effect"
|
|||||||
import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http"
|
import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http"
|
||||||
import { Config } from "@/config/config"
|
import { Config } from "@/config/config"
|
||||||
import { InstanceState } from "@/effect/instance-state"
|
import { InstanceState } from "@/effect/instance-state"
|
||||||
|
import { makeRuntime } from "@/effect/run-service"
|
||||||
import { Flag } from "@/flag/flag"
|
import { Flag } from "@/flag/flag"
|
||||||
import { AppFileSystem } from "@/filesystem"
|
import { AppFileSystem } from "@/filesystem"
|
||||||
import { withTransientReadRetry } from "@/util/effect-http-client"
|
import { withTransientReadRetry } from "@/util/effect-http-client"
|
||||||
@@ -237,7 +238,21 @@ export namespace Instruction {
|
|||||||
Layer.provide(FetchHttpClient.layer),
|
Layer.provide(FetchHttpClient.layer),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||||
|
|
||||||
|
export function clear(messageID: MessageID) {
|
||||||
|
return runPromise((svc) => svc.clear(messageID))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function systemPaths() {
|
||||||
|
return runPromise((svc) => svc.systemPaths())
|
||||||
|
}
|
||||||
|
|
||||||
export function loaded(messages: MessageV2.WithParts[]) {
|
export function loaded(messages: MessageV2.WithParts[]) {
|
||||||
return extract(messages)
|
return extract(messages)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function resolve(messages: MessageV2.WithParts[], filepath: string, messageID: MessageID) {
|
||||||
|
return runPromise((svc) => svc.resolve(messages, filepath, messageID))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,24 +94,14 @@ export namespace LLM {
|
|||||||
modelID: input.model.id,
|
modelID: input.model.id,
|
||||||
providerID: input.model.providerID,
|
providerID: input.model.providerID,
|
||||||
})
|
})
|
||||||
const [language, cfg, provider, info] = await Effect.runPromise(
|
const [language, cfg, provider, auth] = await Promise.all([
|
||||||
Effect.gen(function* () {
|
Provider.getLanguage(input.model),
|
||||||
const auth = yield* Auth.Service
|
Config.get(),
|
||||||
const cfg = yield* Config.Service
|
Provider.getProvider(input.model.providerID),
|
||||||
const provider = yield* Provider.Service
|
Auth.get(input.model.providerID),
|
||||||
return yield* Effect.all(
|
])
|
||||||
[
|
|
||||||
provider.getLanguage(input.model),
|
|
||||||
cfg.get(),
|
|
||||||
provider.getProvider(input.model.providerID),
|
|
||||||
auth.get(input.model.providerID),
|
|
||||||
],
|
|
||||||
{ concurrency: "unbounded" },
|
|
||||||
)
|
|
||||||
}).pipe(Effect.provide(Layer.mergeAll(Auth.defaultLayer, Config.defaultLayer, Provider.defaultLayer))),
|
|
||||||
)
|
|
||||||
// TODO: move this to a proper hook
|
// TODO: move this to a proper hook
|
||||||
const isOpenaiOauth = provider.id === "openai" && info?.type === "oauth"
|
const isOpenaiOauth = provider.id === "openai" && auth?.type === "oauth"
|
||||||
|
|
||||||
const system: string[] = []
|
const system: string[] = []
|
||||||
system.push(
|
system.push(
|
||||||
@@ -210,7 +200,7 @@ export namespace LLM {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
const tools = resolveTools(input)
|
const tools = await resolveTools(input)
|
||||||
|
|
||||||
// LiteLLM and some Anthropic proxies require the tools parameter to be present
|
// LiteLLM and some Anthropic proxies require the tools parameter to be present
|
||||||
// when message history contains tool calls, even if no tools are being used.
|
// when message history contains tool calls, even if no tools are being used.
|
||||||
|
|||||||
@@ -25,8 +25,6 @@ interface FetchDecompressionError extends Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export namespace MessageV2 {
|
export namespace MessageV2 {
|
||||||
export const SYNTHETIC_ATTACHMENT_PROMPT = "Attached image(s) from tool result:"
|
|
||||||
|
|
||||||
export function isMedia(mime: string) {
|
export function isMedia(mime: string) {
|
||||||
return mime.startsWith("image/") || mime === "application/pdf"
|
return mime.startsWith("image/") || mime === "application/pdf"
|
||||||
}
|
}
|
||||||
@@ -810,7 +808,7 @@ export namespace MessageV2 {
|
|||||||
parts: [
|
parts: [
|
||||||
{
|
{
|
||||||
type: "text" as const,
|
type: "text" as const,
|
||||||
text: SYNTHETIC_ATTACHMENT_PROMPT,
|
text: "Attached image(s) from tool result:",
|
||||||
},
|
},
|
||||||
...media.map((attachment) => ({
|
...media.map((attachment) => ({
|
||||||
type: "file" as const,
|
type: "file" as const,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user