Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b09ebbea3c | |||
| 42b63d6660 | |||
| e113aad5e0 |
@@ -101,6 +101,7 @@
|
||||
"dependencies": {
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@opencode-ai/client": "workspace:*",
|
||||
"@opencode-ai/codemode": "workspace:*",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
@@ -873,7 +874,6 @@
|
||||
"name": "@opencode-ai/simulation",
|
||||
"version": "1.17.13",
|
||||
"dependencies": {
|
||||
"@napi-rs/canvas": "1.0.2",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/llm": "workspace:*",
|
||||
"@opentui/core": "catalog:",
|
||||
@@ -998,7 +998,6 @@
|
||||
"@opencode-ai/client": "workspace:*",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opencode-ai/simulation": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -1613,11 +1612,11 @@
|
||||
|
||||
"@emmetio/stream-reader-utils": ["@emmetio/stream-reader-utils@0.1.0", "", {}, "sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A=="],
|
||||
|
||||
"@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="],
|
||||
"@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="],
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="],
|
||||
|
||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
|
||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
|
||||
|
||||
"@emotion/is-prop-valid": ["@emotion/is-prop-valid@0.8.8", "", { "dependencies": { "@emotion/memoize": "0.7.4" } }, "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA=="],
|
||||
|
||||
@@ -1985,30 +1984,6 @@
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4", "", { "os": "win32", "cpu": "x64" }, "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ=="],
|
||||
|
||||
"@napi-rs/canvas": ["@napi-rs/canvas@1.0.2", "", { "optionalDependencies": { "@napi-rs/canvas-android-arm64": "1.0.2", "@napi-rs/canvas-darwin-arm64": "1.0.2", "@napi-rs/canvas-darwin-x64": "1.0.2", "@napi-rs/canvas-linux-arm-gnueabihf": "1.0.2", "@napi-rs/canvas-linux-arm64-gnu": "1.0.2", "@napi-rs/canvas-linux-arm64-musl": "1.0.2", "@napi-rs/canvas-linux-riscv64-gnu": "1.0.2", "@napi-rs/canvas-linux-x64-gnu": "1.0.2", "@napi-rs/canvas-linux-x64-musl": "1.0.2", "@napi-rs/canvas-win32-arm64-msvc": "1.0.2", "@napi-rs/canvas-win32-x64-msvc": "1.0.2" } }, "sha512-EYEqlMYaCbpZDz+IgDH5xp9MTd3ui4dmGqbQYryhMLnSRxrhHKq5KQWHHKxFUcEP4Hp8/BWgvqXocX4j7iSbOQ=="],
|
||||
|
||||
"@napi-rs/canvas-android-arm64": ["@napi-rs/canvas-android-arm64@1.0.2", "", { "os": "android", "cpu": "arm64" }, "sha512-IMXKVQod0ol4vt3gmClUfXz4JAgHYESGPCUqmH3lQxBoL0K/2greJaQE1HVBVxWWFKfLc4OLZVdxg7kXVyXv+g=="],
|
||||
|
||||
"@napi-rs/canvas-darwin-arm64": ["@napi-rs/canvas-darwin-arm64@1.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sc8tPi6cF+5lqOzCCKFALJHhDiRwyMzTPYm3bbhdXsOunU0lQO5f05ucyOzN2r55I23Hg5bsjH63uSCvWp3EgQ=="],
|
||||
|
||||
"@napi-rs/canvas-darwin-x64": ["@napi-rs/canvas-darwin-x64@1.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-niDXZ9LhKB1zLrUdYB64RHQFDGz9rr0eGx061qtJJU3U20EMMIx28ADF5fVYbhtOgkWQrBjFicfaye1yM0U62A=="],
|
||||
|
||||
"@napi-rs/canvas-linux-arm-gnueabihf": ["@napi-rs/canvas-linux-arm-gnueabihf@1.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-sgatQL9JxGRH/Amzcvu0P3t8Am3duou74CisfuJ41Dwt8cWy723z/9KZ8LlgmxfypEwEZxSTNFJtU8d281lmhQ=="],
|
||||
|
||||
"@napi-rs/canvas-linux-arm64-gnu": ["@napi-rs/canvas-linux-arm64-gnu@1.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-dgKuX0peF3xwY6ZF5QxGS4wbfDqpoFAJYXiLSp+guZKARQUKMkRqZSDrXKj7nfrec3UCMzC0PFCPte0ES98AiA=="],
|
||||
|
||||
"@napi-rs/canvas-linux-arm64-musl": ["@napi-rs/canvas-linux-arm64-musl@1.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-qwROoDIC9upfvDoRLuPn2aNg9CGW1x0Ygr4k2Or+8paA9d0qBLwk87U+g8KQpoOviKoPoiwl97kvBYuYD7qZoA=="],
|
||||
|
||||
"@napi-rs/canvas-linux-riscv64-gnu": ["@napi-rs/canvas-linux-riscv64-gnu@1.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-fXRjnPihdnbO6qy1QQOgxAonb68A0TCEG7rj1x7v7rxNElsE8EVIKIEUTvyDtU+sthYSbX+8e7g3oZiLGnOmxw=="],
|
||||
|
||||
"@napi-rs/canvas-linux-x64-gnu": ["@napi-rs/canvas-linux-x64-gnu@1.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-nPR97DXhbWIAy7yazF3jc06kEPMqYMLmPzFOVNlwKPfIoSChnI+x7dc0hTLaihz3jxrjL6j4BbA7earxfx4X3g=="],
|
||||
|
||||
"@napi-rs/canvas-linux-x64-musl": ["@napi-rs/canvas-linux-x64-musl@1.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-l7zZY5+jL5qnBZtDz7CoBtY6p7EkHu422g/0zWwrOrzIwWyWxZFRfZZORY1UG7YApymPLx+UbOkN206xXn/c1Q=="],
|
||||
|
||||
"@napi-rs/canvas-win32-arm64-msvc": ["@napi-rs/canvas-win32-arm64-msvc@1.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-yE0koHCFF4PIbMc2o2SEALhnipz7WBISh5glLvQiomtIoCcW0np3H4Lw93ceJAfJttTTeIIWFbwH84F7EVzjMQ=="],
|
||||
|
||||
"@napi-rs/canvas-win32-x64-msvc": ["@napi-rs/canvas-win32-x64-msvc@1.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-okU8/t2foV6C31n0GtvEMbfD5rOFc70+/6xUNME9Guld29sgSOIGUEDScAWFlcP3k5TYQRl9TNkwJEEjh15w8A=="],
|
||||
|
||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
|
||||
|
||||
"@noble/hashes": ["@noble/hashes@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="],
|
||||
@@ -2303,7 +2278,7 @@
|
||||
|
||||
"@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.127.0", "", { "os": "win32", "cpu": "x64" }, "sha512-rOrnSQSCbhI2kowr9XxE7m9a8oQXnBHjnS6j95LxxAnEZ0+Fz20WlRXG4ondQb+ejjt2KOsa65sE6++L6kUd+w=="],
|
||||
|
||||
"@oxc-project/types": ["@oxc-project/types@0.138.0", "", {}, "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA=="],
|
||||
"@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="],
|
||||
|
||||
"@oxc-resolver/binding-android-arm-eabi": ["@oxc-resolver/binding-android-arm-eabi@11.20.0", "", { "os": "android", "cpu": "arm" }, "sha512-IjfWOXRgJFNdORDl+Uf1aibNgZY2guOD3zmOhx1BGVb/MIiqlFTdmjpQNplSN58lhWehnX4UNqC3QwpUo8pjJg=="],
|
||||
|
||||
@@ -2595,37 +2570,7 @@
|
||||
|
||||
"@remix-run/router": ["@remix-run/router@1.9.0", "", {}, "sha512-bV63itrKBC0zdT27qYm6SDZHlkXwFL1xMBuhkn+X7l0+IIhNaH5wuuvZKp6eKhCD4KFhujhfhCT1YxXW6esUIA=="],
|
||||
|
||||
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.4", "", { "os": "android", "cpu": "arm64" }, "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw=="],
|
||||
|
||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.1.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ=="],
|
||||
|
||||
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.1.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg=="],
|
||||
|
||||
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.1.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ=="],
|
||||
|
||||
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.1.4", "", { "os": "linux", "cpu": "arm" }, "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.1.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.1.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng=="],
|
||||
|
||||
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.1.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg=="],
|
||||
|
||||
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.1.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ=="],
|
||||
|
||||
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.1.4", "", { "os": "none", "cpu": "arm64" }, "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA=="],
|
||||
|
||||
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.1.4", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg=="],
|
||||
|
||||
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.1.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA=="],
|
||||
|
||||
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.1.4", "", { "os": "win32", "cpu": "x64" }, "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
|
||||
|
||||
"@rollup/pluginutils": ["@rollup/pluginutils@5.4.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg=="],
|
||||
|
||||
@@ -3167,8 +3112,6 @@
|
||||
|
||||
"@types/react": ["@types/react@18.0.25", "", { "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", "csstype": "^3.0.2" } }, "sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g=="],
|
||||
|
||||
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||
|
||||
"@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="],
|
||||
|
||||
"@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="],
|
||||
@@ -3241,12 +3184,6 @@
|
||||
|
||||
"@valibot/to-json-schema": ["@valibot/to-json-schema@1.6.0", "", { "peerDependencies": { "valibot": "^1.3.0" } }, "sha512-d6rYyK5KVa2XdqamWgZ4/Nr+cXhxjy7lmpe6Iajw15J/jmU+gyxl2IEd1Otg1d7Rl3gOQL5reulnSypzBtYy1A=="],
|
||||
|
||||
"@vercel/cli-config": ["@vercel/cli-config@0.2.0", "", { "dependencies": { "xdg-app-paths": "5", "zod": "4.1.11" } }, "sha512-fJRRRB7734BDuXZ89yBEaA2ncYhH7bWX30mk04W80J6VAfQc+4iB8lyzAdaGpFV3/vNlkt9VZt+/uoQoWX6UsQ=="],
|
||||
|
||||
"@vercel/cli-exec": ["@vercel/cli-exec@1.0.0", "", { "dependencies": { "execa": "5.1.1" } }, "sha512-kQF8LGie/Hbdq9/psJxLE7owRTcqMQMhgybU04gCeR7cbQAr5t8OrjefDNColJv1QSSucFt4pLwRiARVmlOnug=="],
|
||||
|
||||
"@vercel/functions": ["@vercel/functions@3.7.5", "", { "dependencies": { "@vercel/oidc": "3.8.0" }, "peerDependencies": { "@aws-sdk/credential-provider-web-identity": "*", "ws": ">=8" }, "optionalPeers": ["@aws-sdk/credential-provider-web-identity", "ws"] }, "sha512-ESf8BbeDebqRUyMi09JwRbQqpLn4g6fjcVVHPsHB56j2dSqRrSHO4h3X4aaxJf6iQQjzhAtDGI2xCWQ27JE8PA=="],
|
||||
|
||||
"@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="],
|
||||
|
||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="],
|
||||
@@ -5051,8 +4988,6 @@
|
||||
|
||||
"opentui-spinner": ["opentui-spinner@0.0.7", "", { "dependencies": { "cli-spinners": "^3.3.0" }, "peerDependencies": { "@opentui/core": "^0.3.4", "@opentui/react": "^0.3.4", "@opentui/solid": "^0.3.4", "typescript": "^5" }, "optionalPeers": ["@opentui/react", "@opentui/solid"] }, "sha512-nPzwAvJG+y9rVEwwHLHqbsMzLnIk2zw+F9LqwA7aYJvpM5gsrKC2rrGi36A+tZpA+1RnWxXeWEgVZMchnaH18Q=="],
|
||||
|
||||
"os-paths": ["os-paths@4.4.0", "", {}, "sha512-wrAwOeXp1RRMFfQY8Sy7VaGVmPocaLwSFOYCGKSyo8qmJ+/yaafCl5BCA1IQZWqFSRBrKDYFeR9d/VyQzfH/jg=="],
|
||||
|
||||
"own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="],
|
||||
|
||||
"oxc-minify": ["oxc-minify@0.96.0", "", { "optionalDependencies": { "@oxc-minify/binding-android-arm64": "0.96.0", "@oxc-minify/binding-darwin-arm64": "0.96.0", "@oxc-minify/binding-darwin-x64": "0.96.0", "@oxc-minify/binding-freebsd-x64": "0.96.0", "@oxc-minify/binding-linux-arm-gnueabihf": "0.96.0", "@oxc-minify/binding-linux-arm-musleabihf": "0.96.0", "@oxc-minify/binding-linux-arm64-gnu": "0.96.0", "@oxc-minify/binding-linux-arm64-musl": "0.96.0", "@oxc-minify/binding-linux-riscv64-gnu": "0.96.0", "@oxc-minify/binding-linux-s390x-gnu": "0.96.0", "@oxc-minify/binding-linux-x64-gnu": "0.96.0", "@oxc-minify/binding-linux-x64-musl": "0.96.0", "@oxc-minify/binding-wasm32-wasi": "0.96.0", "@oxc-minify/binding-win32-arm64-msvc": "0.96.0", "@oxc-minify/binding-win32-x64-msvc": "0.96.0" } }, "sha512-dXeeGrfPJJ4rMdw+NrqiCRtbzVX2ogq//R0Xns08zql2HjV3Zi2SBJ65saqfDaJzd2bcHqvGWH+M44EQCHPAcA=="],
|
||||
@@ -5467,8 +5402,6 @@
|
||||
|
||||
"roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="],
|
||||
|
||||
"rolldown": ["rolldown@1.1.4", "", { "dependencies": { "@oxc-project/types": "=0.138.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.4", "@rolldown/binding-darwin-arm64": "1.1.4", "@rolldown/binding-darwin-x64": "1.1.4", "@rolldown/binding-freebsd-x64": "1.1.4", "@rolldown/binding-linux-arm-gnueabihf": "1.1.4", "@rolldown/binding-linux-arm64-gnu": "1.1.4", "@rolldown/binding-linux-arm64-musl": "1.1.4", "@rolldown/binding-linux-ppc64-gnu": "1.1.4", "@rolldown/binding-linux-s390x-gnu": "1.1.4", "@rolldown/binding-linux-x64-gnu": "1.1.4", "@rolldown/binding-linux-x64-musl": "1.1.4", "@rolldown/binding-openharmony-arm64": "1.1.4", "@rolldown/binding-wasm32-wasi": "1.1.4", "@rolldown/binding-win32-arm64-msvc": "1.1.4", "@rolldown/binding-win32-x64-msvc": "1.1.4" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA=="],
|
||||
|
||||
"rollup": ["rollup@4.60.4", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.4", "@rollup/rollup-android-arm64": "4.60.4", "@rollup/rollup-darwin-arm64": "4.60.4", "@rollup/rollup-darwin-x64": "4.60.4", "@rollup/rollup-freebsd-arm64": "4.60.4", "@rollup/rollup-freebsd-x64": "4.60.4", "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", "@rollup/rollup-linux-arm-musleabihf": "4.60.4", "@rollup/rollup-linux-arm64-gnu": "4.60.4", "@rollup/rollup-linux-arm64-musl": "4.60.4", "@rollup/rollup-linux-loong64-gnu": "4.60.4", "@rollup/rollup-linux-loong64-musl": "4.60.4", "@rollup/rollup-linux-ppc64-gnu": "4.60.4", "@rollup/rollup-linux-ppc64-musl": "4.60.4", "@rollup/rollup-linux-riscv64-gnu": "4.60.4", "@rollup/rollup-linux-riscv64-musl": "4.60.4", "@rollup/rollup-linux-s390x-gnu": "4.60.4", "@rollup/rollup-linux-x64-gnu": "4.60.4", "@rollup/rollup-linux-x64-musl": "4.60.4", "@rollup/rollup-openbsd-x64": "4.60.4", "@rollup/rollup-openharmony-arm64": "4.60.4", "@rollup/rollup-win32-arm64-msvc": "4.60.4", "@rollup/rollup-win32-ia32-msvc": "4.60.4", "@rollup/rollup-win32-x64-gnu": "4.60.4", "@rollup/rollup-win32-x64-msvc": "4.60.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g=="],
|
||||
|
||||
"rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="],
|
||||
@@ -6089,12 +6022,8 @@
|
||||
|
||||
"wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="],
|
||||
|
||||
"xdg-app-paths": ["xdg-app-paths@5.5.1", "", { "dependencies": { "os-paths": "^4.0.1", "xdg-portable": "^7.2.0" } }, "sha512-hI3flOB4PLZIy5prbtTpirobtPE2ZtZ52szO+2mM9Efp6ErM398La+C1lIpNWDfNoQk+6Lsi6nMcCwVB7pxeMQ=="],
|
||||
|
||||
"xdg-basedir": ["xdg-basedir@5.1.0", "", {}, "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ=="],
|
||||
|
||||
"xdg-portable": ["xdg-portable@7.3.0", "", { "dependencies": { "os-paths": "^4.0.1" } }, "sha512-sqMMuL1rc0FmMBOzCpd0yuy9trqF2yTTVe+E9ogwCSWQCdDEtQUwrZPT6AxqtsFGRNxycgncbP/xmOOSPw5ZUw=="],
|
||||
|
||||
"xml-naming": ["xml-naming@0.1.0", "", {}, "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw=="],
|
||||
|
||||
"xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="],
|
||||
@@ -6691,10 +6620,6 @@
|
||||
|
||||
"@oslojs/jwt/@oslojs/encoding": ["@oslojs/encoding@0.4.1", "", {}, "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q=="],
|
||||
|
||||
"@oxc-parser/binding-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="],
|
||||
|
||||
"@oxc-parser/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="],
|
||||
|
||||
"@oxc-resolver/binding-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
|
||||
|
||||
"@oxc-resolver/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
|
||||
@@ -6715,8 +6640,6 @@
|
||||
|
||||
"@puppeteer/browsers/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
|
||||
|
||||
"@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="],
|
||||
|
||||
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
|
||||
"@sentry/bundler-plugin-core/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="],
|
||||
@@ -6825,14 +6748,6 @@
|
||||
|
||||
"@types/plist/xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="],
|
||||
|
||||
"@vercel/cli-config/zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="],
|
||||
|
||||
"@vercel/cli-exec/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
|
||||
|
||||
"@vercel/functions/@vercel/oidc": ["@vercel/oidc@3.8.0", "", { "dependencies": { "@vercel/cli-config": "0.2.0", "@vercel/cli-exec": "1.0.0", "jose": "^5.9.6" } }, "sha512-r00laGW6Pv778RoR6M2NxX91ycSj+PBwVo+fOb9Bif+F0IyUKt25zrvBzfEzQpeAzbqOgPZyQibEWDdDFApd+A=="],
|
||||
|
||||
"@vitejs/plugin-react/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
|
||||
|
||||
"@vitest/coverage-v8/@vitest/utils": ["@vitest/utils@4.1.8", "", { "dependencies": { "@vitest/pretty-format": "4.1.8", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg=="],
|
||||
|
||||
"@vitest/coverage-v8/magicast": ["magicast@0.5.3", "", { "dependencies": { "@babel/parser": "^7.29.3", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw=="],
|
||||
@@ -7129,8 +7044,6 @@
|
||||
|
||||
"openid-client/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
|
||||
|
||||
"oxc-parser/@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="],
|
||||
|
||||
"p-any/p-cancelable": ["p-cancelable@3.0.0", "", {}, "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw=="],
|
||||
|
||||
"p-locate/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
|
||||
@@ -7795,10 +7708,6 @@
|
||||
|
||||
"@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@oxc-parser/binding-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
|
||||
|
||||
"@oxc-resolver/binding-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
|
||||
|
||||
"@pierre/diffs/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="],
|
||||
|
||||
"@pierre/diffs/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="],
|
||||
@@ -7807,8 +7716,6 @@
|
||||
|
||||
"@puppeteer/browsers/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
|
||||
|
||||
"@sentry/bundler-plugin-core/glob/minimatch": ["minimatch@8.0.7", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg=="],
|
||||
|
||||
"@sentry/bundler-plugin-core/glob/minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="],
|
||||
@@ -7879,22 +7786,6 @@
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="],
|
||||
|
||||
"@vercel/cli-exec/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
|
||||
|
||||
"@vercel/cli-exec/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="],
|
||||
|
||||
"@vercel/cli-exec/execa/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
|
||||
|
||||
"@vercel/cli-exec/execa/npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="],
|
||||
|
||||
"@vercel/cli-exec/execa/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
|
||||
|
||||
"@vercel/cli-exec/execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||
|
||||
"@vercel/cli-exec/execa/strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="],
|
||||
|
||||
"@vercel/functions/@vercel/oidc/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="],
|
||||
|
||||
"@vitest/coverage-v8/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@4.1.8", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA=="],
|
||||
|
||||
"@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="],
|
||||
@@ -8461,8 +8352,6 @@
|
||||
|
||||
"@stoplight/spectral-core/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"@vercel/cli-exec/execa/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
Executable → Regular
@@ -33,6 +33,7 @@
|
||||
"dependencies": {
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@opencode-ai/client": "workspace:*",
|
||||
"@opencode-ai/codemode": "workspace:*",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
export * as CodeModeHost from "./code-mode"
|
||||
|
||||
import { NodeHttpClient } from "@effect/platform-node"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
||||
import { OpenAPI, Tool } from "@opencode-ai/codemode"
|
||||
import { Api } from "@opencode-ai/server/api"
|
||||
import { ServerAuth } from "@opencode-ai/server/auth"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { HttpClient, HttpClientRequest } from "effect/unstable/http"
|
||||
import { OpenApi } from "effect/unstable/httpapi"
|
||||
import type { Server } from "node:http"
|
||||
|
||||
export function replacements(server: Server, password: string): LayerNode.Replacements {
|
||||
return [ToolRegistry.codeModeReplacement(makeTools(client(server), password))]
|
||||
}
|
||||
|
||||
export function makeTools(client: Layer.Layer<HttpClient.HttpClient>, password: string): ToolRegistry.CodeModeTools {
|
||||
return {
|
||||
opencode: bindTools(
|
||||
OpenAPI.fromSpec({
|
||||
spec: { ...OpenApi.fromApi(Api) },
|
||||
baseUrl: "http://opencode.local",
|
||||
headers: ServerAuth.headers({ username: "opencode", password }),
|
||||
}).tools,
|
||||
client,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
function client(server: Server) {
|
||||
return Layer.effect(
|
||||
HttpClient.HttpClient,
|
||||
Effect.gen(function* () {
|
||||
const client = yield* HttpClient.HttpClient
|
||||
return HttpClient.mapRequest(client, (request) => {
|
||||
const address = server.address()
|
||||
if (!address || typeof address === "string") throw new Error("OpenCode server is not listening")
|
||||
const local =
|
||||
address.address === "0.0.0.0" ? "127.0.0.1" : address.address === "::" ? "::1" : address.address
|
||||
const host = local.includes(":") && !local.startsWith("[") ? `[${local}]` : local
|
||||
const url = new URL(request.url)
|
||||
return HttpClientRequest.setUrl(
|
||||
request,
|
||||
new URL(`${url.pathname}${url.search}${url.hash}`, `http://${host}:${address.port}`),
|
||||
)
|
||||
})
|
||||
}),
|
||||
).pipe(Layer.provide(NodeHttpClient.layerNodeHttp))
|
||||
}
|
||||
|
||||
function bindTools(tools: OpenAPI.Tools, client: Layer.Layer<HttpClient.HttpClient>): ToolRegistry.CodeModeTools {
|
||||
return Object.fromEntries(
|
||||
Object.entries(tools).map(([name, value]) => [
|
||||
name,
|
||||
Tool.isDefinition<HttpClient.HttpClient>(value)
|
||||
? Tool.make({
|
||||
description: value.description,
|
||||
input: value.input,
|
||||
output: value.output,
|
||||
run: (input) => value.run(input).pipe(Effect.provide(client)),
|
||||
})
|
||||
: bindTools(value, client),
|
||||
]),
|
||||
)
|
||||
}
|
||||
@@ -7,11 +7,13 @@ import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
import { AppProcess } from "@opencode-ai/core/process"
|
||||
import { Flock } from "@opencode-ai/core/util/flock"
|
||||
import { start } from "@opencode-ai/server/process"
|
||||
import { randomBytes, randomUUID } from "node:crypto"
|
||||
import path from "node:path"
|
||||
import { Effect, FileSystem, Logger, Option, Redacted, Schedule, Schema } from "effect"
|
||||
import { HttpServer } from "effect/unstable/http"
|
||||
import { CodeModeHost } from "./code-mode"
|
||||
import { Env } from "./env"
|
||||
import { ServiceConfig } from "./services/service-config"
|
||||
import { Updater } from "./services/updater"
|
||||
@@ -36,6 +38,14 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
|
||||
if (options.mode === "service") yield* Effect.sync(() => process.chdir(Global.Path.home))
|
||||
return yield* Effect.scoped(
|
||||
Effect.gen(function* () {
|
||||
if (options.mode === "service") {
|
||||
const service = yield* ServiceConfig.options()
|
||||
yield* Flock.effect(path.basename(service.file, ".json") + "-process", {
|
||||
dir: path.dirname(service.file),
|
||||
staleMs: 3_000,
|
||||
timeoutMs: 15_000,
|
||||
})
|
||||
}
|
||||
const environmentPassword = yield* Env.password
|
||||
// Keep the lease credential out of the environment inherited by tools.
|
||||
if (options.mode === "stdio") {
|
||||
@@ -54,6 +64,7 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
|
||||
hostname: options.hostname ?? config.hostname ?? "127.0.0.1",
|
||||
port: Option.fromNullishOr(options.port ?? config.port),
|
||||
password,
|
||||
replacements: (server) => CodeModeHost.replacements(server, password),
|
||||
}).pipe(Effect.provide(Logger.layer([], { mergeWithExisting: false })))
|
||||
if (options.mode === "service") yield* register(address, password)
|
||||
const url = HttpServer.formatAddress(address)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { CodeMode } from "@opencode-ai/codemode"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
|
||||
import { CodeModeHost } from "../src/code-mode"
|
||||
|
||||
test("exposes the authenticated OpenCode API through CodeMode", async () => {
|
||||
const requests: Array<{ readonly url: string; readonly authorization?: string }> = []
|
||||
const client = Layer.succeed(
|
||||
HttpClient.HttpClient,
|
||||
HttpClient.make((request) => {
|
||||
requests.push({ url: request.url, authorization: request.headers.authorization })
|
||||
return Effect.succeed(
|
||||
HttpClientResponse.fromWeb(
|
||||
request,
|
||||
Response.json(
|
||||
{ healthy: true, version: "test", pid: 1 },
|
||||
{ headers: { "content-type": "application/json" } },
|
||||
),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
const result = await CodeMode.make({ tools: CodeModeHost.makeTools(client, "secret") })
|
||||
.execute("return await tools.opencode.v2.health.get({})")
|
||||
.pipe(Effect.runPromise)
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: true,
|
||||
value: { healthy: true, version: "test", pid: 1 },
|
||||
toolCalls: [{ name: "opencode.v2.health.get" }],
|
||||
})
|
||||
expect(requests).toEqual([
|
||||
{
|
||||
url: "http://opencode.local/api/health",
|
||||
authorization: `Basic ${Buffer.from("opencode:secret").toString("base64")}`,
|
||||
},
|
||||
])
|
||||
})
|
||||
@@ -111,167 +111,157 @@ export type Endpoint4_8Input = {
|
||||
export type Endpoint4_8Output = EffectValue<ReturnType<RawClient["server.session"]["session.rename"]>>
|
||||
export type SessionRenameOperation<E = never> = (input: Endpoint4_8Input) => Effect.Effect<Endpoint4_8Output, E>
|
||||
|
||||
type Endpoint4_9Request = Parameters<RawClient["server.session"]["session.move"]>[0]
|
||||
type Endpoint4_9Request = Parameters<RawClient["server.session"]["session.prompt"]>[0]
|
||||
export type Endpoint4_9Input = {
|
||||
readonly sessionID: Endpoint4_9Request["params"]["sessionID"]
|
||||
readonly destination: Endpoint4_9Request["payload"]["destination"]
|
||||
readonly moveChanges?: Endpoint4_9Request["payload"]["moveChanges"]
|
||||
readonly id?: Endpoint4_9Request["payload"]["id"]
|
||||
readonly prompt: Endpoint4_9Request["payload"]["prompt"]
|
||||
readonly delivery?: Endpoint4_9Request["payload"]["delivery"]
|
||||
readonly resume?: Endpoint4_9Request["payload"]["resume"]
|
||||
}
|
||||
export type Endpoint4_9Output = EffectValue<ReturnType<RawClient["server.session"]["session.move"]>>
|
||||
export type SessionMoveOperation<E = never> = (input: Endpoint4_9Input) => Effect.Effect<Endpoint4_9Output, E>
|
||||
export type Endpoint4_9Output = EffectValue<ReturnType<RawClient["server.session"]["session.prompt"]>>["data"]
|
||||
export type SessionPromptOperation<E = never> = (input: Endpoint4_9Input) => Effect.Effect<Endpoint4_9Output, E>
|
||||
|
||||
type Endpoint4_10Request = Parameters<RawClient["server.session"]["session.prompt"]>[0]
|
||||
type Endpoint4_10Request = Parameters<RawClient["server.session"]["session.command"]>[0]
|
||||
export type Endpoint4_10Input = {
|
||||
readonly sessionID: Endpoint4_10Request["params"]["sessionID"]
|
||||
readonly id?: Endpoint4_10Request["payload"]["id"]
|
||||
readonly prompt: Endpoint4_10Request["payload"]["prompt"]
|
||||
readonly command: Endpoint4_10Request["payload"]["command"]
|
||||
readonly arguments?: Endpoint4_10Request["payload"]["arguments"]
|
||||
readonly agent?: Endpoint4_10Request["payload"]["agent"]
|
||||
readonly model?: Endpoint4_10Request["payload"]["model"]
|
||||
readonly files?: Endpoint4_10Request["payload"]["files"]
|
||||
readonly agents?: Endpoint4_10Request["payload"]["agents"]
|
||||
readonly delivery?: Endpoint4_10Request["payload"]["delivery"]
|
||||
readonly resume?: Endpoint4_10Request["payload"]["resume"]
|
||||
}
|
||||
export type Endpoint4_10Output = EffectValue<ReturnType<RawClient["server.session"]["session.prompt"]>>["data"]
|
||||
export type SessionPromptOperation<E = never> = (input: Endpoint4_10Input) => Effect.Effect<Endpoint4_10Output, E>
|
||||
export type Endpoint4_10Output = EffectValue<ReturnType<RawClient["server.session"]["session.command"]>>["data"]
|
||||
export type SessionCommandOperation<E = never> = (input: Endpoint4_10Input) => Effect.Effect<Endpoint4_10Output, E>
|
||||
|
||||
type Endpoint4_11Request = Parameters<RawClient["server.session"]["session.command"]>[0]
|
||||
type Endpoint4_11Request = Parameters<RawClient["server.session"]["session.skill"]>[0]
|
||||
export type Endpoint4_11Input = {
|
||||
readonly sessionID: Endpoint4_11Request["params"]["sessionID"]
|
||||
readonly id?: Endpoint4_11Request["payload"]["id"]
|
||||
readonly command: Endpoint4_11Request["payload"]["command"]
|
||||
readonly arguments?: Endpoint4_11Request["payload"]["arguments"]
|
||||
readonly agent?: Endpoint4_11Request["payload"]["agent"]
|
||||
readonly model?: Endpoint4_11Request["payload"]["model"]
|
||||
readonly files?: Endpoint4_11Request["payload"]["files"]
|
||||
readonly agents?: Endpoint4_11Request["payload"]["agents"]
|
||||
readonly delivery?: Endpoint4_11Request["payload"]["delivery"]
|
||||
readonly skill: Endpoint4_11Request["payload"]["skill"]
|
||||
readonly resume?: Endpoint4_11Request["payload"]["resume"]
|
||||
}
|
||||
export type Endpoint4_11Output = EffectValue<ReturnType<RawClient["server.session"]["session.command"]>>["data"]
|
||||
export type SessionCommandOperation<E = never> = (input: Endpoint4_11Input) => Effect.Effect<Endpoint4_11Output, E>
|
||||
export type Endpoint4_11Output = EffectValue<ReturnType<RawClient["server.session"]["session.skill"]>>
|
||||
export type SessionSkillOperation<E = never> = (input: Endpoint4_11Input) => Effect.Effect<Endpoint4_11Output, E>
|
||||
|
||||
type Endpoint4_12Request = Parameters<RawClient["server.session"]["session.skill"]>[0]
|
||||
type Endpoint4_12Request = Parameters<RawClient["server.session"]["session.synthetic"]>[0]
|
||||
export type Endpoint4_12Input = {
|
||||
readonly sessionID: Endpoint4_12Request["params"]["sessionID"]
|
||||
readonly id?: Endpoint4_12Request["payload"]["id"]
|
||||
readonly skill: Endpoint4_12Request["payload"]["skill"]
|
||||
readonly resume?: Endpoint4_12Request["payload"]["resume"]
|
||||
readonly text: Endpoint4_12Request["payload"]["text"]
|
||||
readonly description?: Endpoint4_12Request["payload"]["description"]
|
||||
readonly metadata?: Endpoint4_12Request["payload"]["metadata"]
|
||||
}
|
||||
export type Endpoint4_12Output = EffectValue<ReturnType<RawClient["server.session"]["session.skill"]>>
|
||||
export type SessionSkillOperation<E = never> = (input: Endpoint4_12Input) => Effect.Effect<Endpoint4_12Output, E>
|
||||
export type Endpoint4_12Output = EffectValue<ReturnType<RawClient["server.session"]["session.synthetic"]>>
|
||||
export type SessionSyntheticOperation<E = never> = (input: Endpoint4_12Input) => Effect.Effect<Endpoint4_12Output, E>
|
||||
|
||||
type Endpoint4_13Request = Parameters<RawClient["server.session"]["session.synthetic"]>[0]
|
||||
type Endpoint4_13Request = Parameters<RawClient["server.session"]["session.shell"]>[0]
|
||||
export type Endpoint4_13Input = {
|
||||
readonly sessionID: Endpoint4_13Request["params"]["sessionID"]
|
||||
readonly text: Endpoint4_13Request["payload"]["text"]
|
||||
readonly description?: Endpoint4_13Request["payload"]["description"]
|
||||
readonly metadata?: Endpoint4_13Request["payload"]["metadata"]
|
||||
readonly resume?: Endpoint4_13Request["payload"]["resume"]
|
||||
readonly id?: Endpoint4_13Request["payload"]["id"]
|
||||
readonly command: Endpoint4_13Request["payload"]["command"]
|
||||
}
|
||||
export type Endpoint4_13Output = EffectValue<ReturnType<RawClient["server.session"]["session.synthetic"]>>
|
||||
export type SessionSyntheticOperation<E = never> = (input: Endpoint4_13Input) => Effect.Effect<Endpoint4_13Output, E>
|
||||
export type Endpoint4_13Output = EffectValue<ReturnType<RawClient["server.session"]["session.shell"]>>
|
||||
export type SessionShellOperation<E = never> = (input: Endpoint4_13Input) => Effect.Effect<Endpoint4_13Output, E>
|
||||
|
||||
type Endpoint4_14Request = Parameters<RawClient["server.session"]["session.shell"]>[0]
|
||||
type Endpoint4_14Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
|
||||
export type Endpoint4_14Input = {
|
||||
readonly sessionID: Endpoint4_14Request["params"]["sessionID"]
|
||||
readonly id?: Endpoint4_14Request["payload"]["id"]
|
||||
readonly command: Endpoint4_14Request["payload"]["command"]
|
||||
}
|
||||
export type Endpoint4_14Output = EffectValue<ReturnType<RawClient["server.session"]["session.shell"]>>
|
||||
export type SessionShellOperation<E = never> = (input: Endpoint4_14Input) => Effect.Effect<Endpoint4_14Output, E>
|
||||
export type Endpoint4_14Output = EffectValue<ReturnType<RawClient["server.session"]["session.compact"]>>["data"]
|
||||
export type SessionCompactOperation<E = never> = (input: Endpoint4_14Input) => Effect.Effect<Endpoint4_14Output, E>
|
||||
|
||||
type Endpoint4_15Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
|
||||
export type Endpoint4_15Input = {
|
||||
readonly sessionID: Endpoint4_15Request["params"]["sessionID"]
|
||||
readonly id?: Endpoint4_15Request["payload"]["id"]
|
||||
type Endpoint4_15Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
|
||||
export type Endpoint4_15Input = { readonly sessionID: Endpoint4_15Request["params"]["sessionID"] }
|
||||
export type Endpoint4_15Output = EffectValue<ReturnType<RawClient["server.session"]["session.wait"]>>
|
||||
export type SessionWaitOperation<E = never> = (input: Endpoint4_15Input) => Effect.Effect<Endpoint4_15Output, E>
|
||||
|
||||
type Endpoint4_16Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
|
||||
export type Endpoint4_16Input = {
|
||||
readonly sessionID: Endpoint4_16Request["params"]["sessionID"]
|
||||
readonly messageID: Endpoint4_16Request["payload"]["messageID"]
|
||||
readonly files?: Endpoint4_16Request["payload"]["files"]
|
||||
}
|
||||
export type Endpoint4_15Output = EffectValue<ReturnType<RawClient["server.session"]["session.compact"]>>["data"]
|
||||
export type SessionCompactOperation<E = never> = (input: Endpoint4_15Input) => Effect.Effect<Endpoint4_15Output, E>
|
||||
export type Endpoint4_16Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.stage"]>>["data"]
|
||||
export type SessionRevertStageOperation<E = never> = (input: Endpoint4_16Input) => Effect.Effect<Endpoint4_16Output, E>
|
||||
|
||||
type Endpoint4_16Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
|
||||
export type Endpoint4_16Input = { readonly sessionID: Endpoint4_16Request["params"]["sessionID"] }
|
||||
export type Endpoint4_16Output = EffectValue<ReturnType<RawClient["server.session"]["session.wait"]>>
|
||||
export type SessionWaitOperation<E = never> = (input: Endpoint4_16Input) => Effect.Effect<Endpoint4_16Output, E>
|
||||
type Endpoint4_17Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
|
||||
export type Endpoint4_17Input = { readonly sessionID: Endpoint4_17Request["params"]["sessionID"] }
|
||||
export type Endpoint4_17Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.clear"]>>
|
||||
export type SessionRevertClearOperation<E = never> = (input: Endpoint4_17Input) => Effect.Effect<Endpoint4_17Output, E>
|
||||
|
||||
type Endpoint4_17Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
|
||||
export type Endpoint4_17Input = {
|
||||
readonly sessionID: Endpoint4_17Request["params"]["sessionID"]
|
||||
readonly messageID: Endpoint4_17Request["payload"]["messageID"]
|
||||
readonly files?: Endpoint4_17Request["payload"]["files"]
|
||||
}
|
||||
export type Endpoint4_17Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.stage"]>>["data"]
|
||||
export type SessionRevertStageOperation<E = never> = (input: Endpoint4_17Input) => Effect.Effect<Endpoint4_17Output, E>
|
||||
|
||||
type Endpoint4_18Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
|
||||
type Endpoint4_18Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
|
||||
export type Endpoint4_18Input = { readonly sessionID: Endpoint4_18Request["params"]["sessionID"] }
|
||||
export type Endpoint4_18Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.clear"]>>
|
||||
export type SessionRevertClearOperation<E = never> = (input: Endpoint4_18Input) => Effect.Effect<Endpoint4_18Output, E>
|
||||
export type Endpoint4_18Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.commit"]>>
|
||||
export type SessionRevertCommitOperation<E = never> = (input: Endpoint4_18Input) => Effect.Effect<Endpoint4_18Output, E>
|
||||
|
||||
type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
|
||||
type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.context"]>[0]
|
||||
export type Endpoint4_19Input = { readonly sessionID: Endpoint4_19Request["params"]["sessionID"] }
|
||||
export type Endpoint4_19Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.commit"]>>
|
||||
export type SessionRevertCommitOperation<E = never> = (input: Endpoint4_19Input) => Effect.Effect<Endpoint4_19Output, E>
|
||||
export type Endpoint4_19Output = EffectValue<ReturnType<RawClient["server.session"]["session.context"]>>["data"]
|
||||
export type SessionContextOperation<E = never> = (input: Endpoint4_19Input) => Effect.Effect<Endpoint4_19Output, E>
|
||||
|
||||
type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.context"]>[0]
|
||||
type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.instructions.entry.list"]>[0]
|
||||
export type Endpoint4_20Input = { readonly sessionID: Endpoint4_20Request["params"]["sessionID"] }
|
||||
export type Endpoint4_20Output = EffectValue<ReturnType<RawClient["server.session"]["session.context"]>>["data"]
|
||||
export type SessionContextOperation<E = never> = (input: Endpoint4_20Input) => Effect.Effect<Endpoint4_20Output, E>
|
||||
|
||||
type Endpoint4_21Request = Parameters<RawClient["server.session"]["session.instructions.entry.list"]>[0]
|
||||
export type Endpoint4_21Input = { readonly sessionID: Endpoint4_21Request["params"]["sessionID"] }
|
||||
export type Endpoint4_21Output = EffectValue<
|
||||
export type Endpoint4_20Output = EffectValue<
|
||||
ReturnType<RawClient["server.session"]["session.instructions.entry.list"]>
|
||||
>["data"]
|
||||
export type SessionInstructionsEntryListOperation<E = never> = (
|
||||
input: Endpoint4_20Input,
|
||||
) => Effect.Effect<Endpoint4_20Output, E>
|
||||
|
||||
type Endpoint4_21Request = Parameters<RawClient["server.session"]["session.instructions.entry.put"]>[0]
|
||||
export type Endpoint4_21Input = {
|
||||
readonly sessionID: Endpoint4_21Request["params"]["sessionID"]
|
||||
readonly key: Endpoint4_21Request["params"]["key"]
|
||||
readonly value: Endpoint4_21Request["payload"]["value"]
|
||||
}
|
||||
export type Endpoint4_21Output = EffectValue<ReturnType<RawClient["server.session"]["session.instructions.entry.put"]>>
|
||||
export type SessionInstructionsEntryPutOperation<E = never> = (
|
||||
input: Endpoint4_21Input,
|
||||
) => Effect.Effect<Endpoint4_21Output, E>
|
||||
|
||||
type Endpoint4_22Request = Parameters<RawClient["server.session"]["session.instructions.entry.put"]>[0]
|
||||
type Endpoint4_22Request = Parameters<RawClient["server.session"]["session.instructions.entry.remove"]>[0]
|
||||
export type Endpoint4_22Input = {
|
||||
readonly sessionID: Endpoint4_22Request["params"]["sessionID"]
|
||||
readonly key: Endpoint4_22Request["params"]["key"]
|
||||
readonly value: Endpoint4_22Request["payload"]["value"]
|
||||
}
|
||||
export type Endpoint4_22Output = EffectValue<ReturnType<RawClient["server.session"]["session.instructions.entry.put"]>>
|
||||
export type SessionInstructionsEntryPutOperation<E = never> = (
|
||||
input: Endpoint4_22Input,
|
||||
) => Effect.Effect<Endpoint4_22Output, E>
|
||||
|
||||
type Endpoint4_23Request = Parameters<RawClient["server.session"]["session.instructions.entry.remove"]>[0]
|
||||
export type Endpoint4_23Input = {
|
||||
readonly sessionID: Endpoint4_23Request["params"]["sessionID"]
|
||||
readonly key: Endpoint4_23Request["params"]["key"]
|
||||
}
|
||||
export type Endpoint4_23Output = EffectValue<
|
||||
export type Endpoint4_22Output = EffectValue<
|
||||
ReturnType<RawClient["server.session"]["session.instructions.entry.remove"]>
|
||||
>
|
||||
export type SessionInstructionsEntryRemoveOperation<E = never> = (
|
||||
input: Endpoint4_23Input,
|
||||
) => Effect.Effect<Endpoint4_23Output, E>
|
||||
input: Endpoint4_22Input,
|
||||
) => Effect.Effect<Endpoint4_22Output, E>
|
||||
|
||||
type Endpoint4_24Request = Parameters<RawClient["server.session"]["session.log"]>[0]
|
||||
export type Endpoint4_24Input = {
|
||||
readonly sessionID: Endpoint4_24Request["params"]["sessionID"]
|
||||
readonly after?: Endpoint4_24Request["query"]["after"]
|
||||
readonly follow?: Endpoint4_24Request["query"]["follow"]
|
||||
type Endpoint4_23Request = Parameters<RawClient["server.session"]["session.log"]>[0]
|
||||
export type Endpoint4_23Input = {
|
||||
readonly sessionID: Endpoint4_23Request["params"]["sessionID"]
|
||||
readonly after?: Endpoint4_23Request["query"]["after"]
|
||||
readonly follow?: Endpoint4_23Request["query"]["follow"]
|
||||
}
|
||||
export type Endpoint4_24Output = StreamValue<EffectValue<ReturnType<RawClient["server.session"]["session.log"]>>>
|
||||
export type SessionLogOperation<E = never> = (input: Endpoint4_24Input) => Stream.Stream<Endpoint4_24Output, E>
|
||||
export type Endpoint4_23Output = StreamValue<EffectValue<ReturnType<RawClient["server.session"]["session.log"]>>>
|
||||
export type SessionLogOperation<E = never> = (input: Endpoint4_23Input) => Stream.Stream<Endpoint4_23Output, E>
|
||||
|
||||
type Endpoint4_25Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
|
||||
type Endpoint4_24Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
|
||||
export type Endpoint4_24Input = { readonly sessionID: Endpoint4_24Request["params"]["sessionID"] }
|
||||
export type Endpoint4_24Output = EffectValue<ReturnType<RawClient["server.session"]["session.interrupt"]>>
|
||||
export type SessionInterruptOperation<E = never> = (input: Endpoint4_24Input) => Effect.Effect<Endpoint4_24Output, E>
|
||||
|
||||
type Endpoint4_25Request = Parameters<RawClient["server.session"]["session.background"]>[0]
|
||||
export type Endpoint4_25Input = { readonly sessionID: Endpoint4_25Request["params"]["sessionID"] }
|
||||
export type Endpoint4_25Output = EffectValue<ReturnType<RawClient["server.session"]["session.interrupt"]>>
|
||||
export type SessionInterruptOperation<E = never> = (input: Endpoint4_25Input) => Effect.Effect<Endpoint4_25Output, E>
|
||||
export type Endpoint4_25Output = EffectValue<ReturnType<RawClient["server.session"]["session.background"]>>
|
||||
export type SessionBackgroundOperation<E = never> = (input: Endpoint4_25Input) => Effect.Effect<Endpoint4_25Output, E>
|
||||
|
||||
type Endpoint4_26Request = Parameters<RawClient["server.session"]["session.background"]>[0]
|
||||
export type Endpoint4_26Input = { readonly sessionID: Endpoint4_26Request["params"]["sessionID"] }
|
||||
export type Endpoint4_26Output = EffectValue<ReturnType<RawClient["server.session"]["session.background"]>>
|
||||
export type SessionBackgroundOperation<E = never> = (input: Endpoint4_26Input) => Effect.Effect<Endpoint4_26Output, E>
|
||||
|
||||
type Endpoint4_27Request = Parameters<RawClient["server.session"]["session.message"]>[0]
|
||||
export type Endpoint4_27Input = {
|
||||
readonly sessionID: Endpoint4_27Request["params"]["sessionID"]
|
||||
readonly messageID: Endpoint4_27Request["params"]["messageID"]
|
||||
type Endpoint4_26Request = Parameters<RawClient["server.session"]["session.message"]>[0]
|
||||
export type Endpoint4_26Input = {
|
||||
readonly sessionID: Endpoint4_26Request["params"]["sessionID"]
|
||||
readonly messageID: Endpoint4_26Request["params"]["messageID"]
|
||||
}
|
||||
export type Endpoint4_27Output = EffectValue<ReturnType<RawClient["server.session"]["session.message"]>>["data"]
|
||||
export type SessionMessageOperation<E = never> = (input: Endpoint4_27Input) => Effect.Effect<Endpoint4_27Output, E>
|
||||
export type Endpoint4_26Output = EffectValue<ReturnType<RawClient["server.session"]["session.message"]>>["data"]
|
||||
export type SessionMessageOperation<E = never> = (input: Endpoint4_26Input) => Effect.Effect<Endpoint4_26Output, E>
|
||||
|
||||
export interface SessionApi<E = never> {
|
||||
readonly list: SessionListOperation<E>
|
||||
@@ -283,7 +273,6 @@ export interface SessionApi<E = never> {
|
||||
readonly switchAgent: SessionSwitchAgentOperation<E>
|
||||
readonly switchModel: SessionSwitchModelOperation<E>
|
||||
readonly rename: SessionRenameOperation<E>
|
||||
readonly move: SessionMoveOperation<E>
|
||||
readonly prompt: SessionPromptOperation<E>
|
||||
readonly command: SessionCommandOperation<E>
|
||||
readonly skill: SessionSkillOperation<E>
|
||||
@@ -450,14 +439,8 @@ export type Endpoint10_0Input = { readonly location?: Endpoint10_0Request["query
|
||||
export type Endpoint10_0Output = EffectValue<ReturnType<RawClient["server.mcp"]["mcp.list"]>>
|
||||
export type ServerMcpListOperation<E = never> = (input?: Endpoint10_0Input) => Effect.Effect<Endpoint10_0Output, E>
|
||||
|
||||
type Endpoint10_1Request = Parameters<RawClient["server.mcp"]["mcp.resource.catalog"]>[0]
|
||||
export type Endpoint10_1Input = { readonly location?: Endpoint10_1Request["query"]["location"] }
|
||||
export type Endpoint10_1Output = EffectValue<ReturnType<RawClient["server.mcp"]["mcp.resource.catalog"]>>
|
||||
export type ServerMcpCatalogOperation<E = never> = (input?: Endpoint10_1Input) => Effect.Effect<Endpoint10_1Output, E>
|
||||
|
||||
export interface ServerMcpApi<E = never> {
|
||||
readonly list: ServerMcpListOperation<E>
|
||||
readonly catalog: ServerMcpCatalogOperation<E>
|
||||
}
|
||||
|
||||
type Endpoint11_0Request = Parameters<RawClient["server.credential"]["credential.update"]>[0]
|
||||
@@ -907,14 +890,8 @@ export interface VcsApi<E = never> {
|
||||
export type Endpoint25_0Output = EffectValue<ReturnType<RawClient["server.debug"]["debug.location"]>>
|
||||
export type DebugLocationOperation<E = never> = () => Effect.Effect<Endpoint25_0Output, E>
|
||||
|
||||
type Endpoint25_1Request = Parameters<RawClient["server.debug"]["debug.location.evict"]>[0]
|
||||
export type Endpoint25_1Input = { readonly location?: Endpoint25_1Request["query"]["location"] }
|
||||
export type Endpoint25_1Output = EffectValue<ReturnType<RawClient["server.debug"]["debug.location.evict"]>>
|
||||
export type DebugEvictLocationOperation<E = never> = (input?: Endpoint25_1Input) => Effect.Effect<Endpoint25_1Output, E>
|
||||
|
||||
export interface DebugApi<E = never> {
|
||||
readonly location: DebugLocationOperation<E>
|
||||
readonly evictLocation: DebugEvictLocationOperation<E>
|
||||
}
|
||||
|
||||
export interface AppApi<E = never> {
|
||||
|
||||
@@ -141,27 +141,15 @@ const Endpoint4_8 = (raw: RawClient["server.session"]) => (input: Endpoint4_8Inp
|
||||
Effect.mapError(mapClientError),
|
||||
)
|
||||
|
||||
type Endpoint4_9Request = Parameters<RawClient["server.session"]["session.move"]>[0]
|
||||
type Endpoint4_9Request = Parameters<RawClient["server.session"]["session.prompt"]>[0]
|
||||
type Endpoint4_9Input = {
|
||||
readonly sessionID: Endpoint4_9Request["params"]["sessionID"]
|
||||
readonly destination: Endpoint4_9Request["payload"]["destination"]
|
||||
readonly moveChanges?: Endpoint4_9Request["payload"]["moveChanges"]
|
||||
readonly id?: Endpoint4_9Request["payload"]["id"]
|
||||
readonly prompt: Endpoint4_9Request["payload"]["prompt"]
|
||||
readonly delivery?: Endpoint4_9Request["payload"]["delivery"]
|
||||
readonly resume?: Endpoint4_9Request["payload"]["resume"]
|
||||
}
|
||||
const Endpoint4_9 = (raw: RawClient["server.session"]) => (input: Endpoint4_9Input) =>
|
||||
raw["session.move"]({
|
||||
params: { sessionID: input["sessionID"] },
|
||||
payload: { destination: input["destination"], moveChanges: input["moveChanges"] },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint4_10Request = Parameters<RawClient["server.session"]["session.prompt"]>[0]
|
||||
type Endpoint4_10Input = {
|
||||
readonly sessionID: Endpoint4_10Request["params"]["sessionID"]
|
||||
readonly id?: Endpoint4_10Request["payload"]["id"]
|
||||
readonly prompt: Endpoint4_10Request["payload"]["prompt"]
|
||||
readonly delivery?: Endpoint4_10Request["payload"]["delivery"]
|
||||
readonly resume?: Endpoint4_10Request["payload"]["resume"]
|
||||
}
|
||||
const Endpoint4_10 = (raw: RawClient["server.session"]) => (input: Endpoint4_10Input) =>
|
||||
raw["session.prompt"]({
|
||||
params: { sessionID: input["sessionID"] },
|
||||
payload: { id: input["id"], prompt: input["prompt"], delivery: input["delivery"], resume: input["resume"] },
|
||||
@@ -170,20 +158,20 @@ const Endpoint4_10 = (raw: RawClient["server.session"]) => (input: Endpoint4_10I
|
||||
Effect.map((value) => value.data),
|
||||
)
|
||||
|
||||
type Endpoint4_11Request = Parameters<RawClient["server.session"]["session.command"]>[0]
|
||||
type Endpoint4_11Input = {
|
||||
readonly sessionID: Endpoint4_11Request["params"]["sessionID"]
|
||||
readonly id?: Endpoint4_11Request["payload"]["id"]
|
||||
readonly command: Endpoint4_11Request["payload"]["command"]
|
||||
readonly arguments?: Endpoint4_11Request["payload"]["arguments"]
|
||||
readonly agent?: Endpoint4_11Request["payload"]["agent"]
|
||||
readonly model?: Endpoint4_11Request["payload"]["model"]
|
||||
readonly files?: Endpoint4_11Request["payload"]["files"]
|
||||
readonly agents?: Endpoint4_11Request["payload"]["agents"]
|
||||
readonly delivery?: Endpoint4_11Request["payload"]["delivery"]
|
||||
readonly resume?: Endpoint4_11Request["payload"]["resume"]
|
||||
type Endpoint4_10Request = Parameters<RawClient["server.session"]["session.command"]>[0]
|
||||
type Endpoint4_10Input = {
|
||||
readonly sessionID: Endpoint4_10Request["params"]["sessionID"]
|
||||
readonly id?: Endpoint4_10Request["payload"]["id"]
|
||||
readonly command: Endpoint4_10Request["payload"]["command"]
|
||||
readonly arguments?: Endpoint4_10Request["payload"]["arguments"]
|
||||
readonly agent?: Endpoint4_10Request["payload"]["agent"]
|
||||
readonly model?: Endpoint4_10Request["payload"]["model"]
|
||||
readonly files?: Endpoint4_10Request["payload"]["files"]
|
||||
readonly agents?: Endpoint4_10Request["payload"]["agents"]
|
||||
readonly delivery?: Endpoint4_10Request["payload"]["delivery"]
|
||||
readonly resume?: Endpoint4_10Request["payload"]["resume"]
|
||||
}
|
||||
const Endpoint4_11 = (raw: RawClient["server.session"]) => (input: Endpoint4_11Input) =>
|
||||
const Endpoint4_10 = (raw: RawClient["server.session"]) => (input: Endpoint4_10Input) =>
|
||||
raw["session.command"]({
|
||||
params: { sessionID: input["sessionID"] },
|
||||
payload: {
|
||||
@@ -202,73 +190,67 @@ const Endpoint4_11 = (raw: RawClient["server.session"]) => (input: Endpoint4_11I
|
||||
Effect.map((value) => value.data),
|
||||
)
|
||||
|
||||
type Endpoint4_12Request = Parameters<RawClient["server.session"]["session.skill"]>[0]
|
||||
type Endpoint4_12Input = {
|
||||
readonly sessionID: Endpoint4_12Request["params"]["sessionID"]
|
||||
readonly id?: Endpoint4_12Request["payload"]["id"]
|
||||
readonly skill: Endpoint4_12Request["payload"]["skill"]
|
||||
readonly resume?: Endpoint4_12Request["payload"]["resume"]
|
||||
type Endpoint4_11Request = Parameters<RawClient["server.session"]["session.skill"]>[0]
|
||||
type Endpoint4_11Input = {
|
||||
readonly sessionID: Endpoint4_11Request["params"]["sessionID"]
|
||||
readonly id?: Endpoint4_11Request["payload"]["id"]
|
||||
readonly skill: Endpoint4_11Request["payload"]["skill"]
|
||||
readonly resume?: Endpoint4_11Request["payload"]["resume"]
|
||||
}
|
||||
const Endpoint4_12 = (raw: RawClient["server.session"]) => (input: Endpoint4_12Input) =>
|
||||
const Endpoint4_11 = (raw: RawClient["server.session"]) => (input: Endpoint4_11Input) =>
|
||||
raw["session.skill"]({
|
||||
params: { sessionID: input["sessionID"] },
|
||||
payload: { id: input["id"], skill: input["skill"], resume: input["resume"] },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint4_13Request = Parameters<RawClient["server.session"]["session.synthetic"]>[0]
|
||||
type Endpoint4_13Input = {
|
||||
readonly sessionID: Endpoint4_13Request["params"]["sessionID"]
|
||||
readonly text: Endpoint4_13Request["payload"]["text"]
|
||||
readonly description?: Endpoint4_13Request["payload"]["description"]
|
||||
readonly metadata?: Endpoint4_13Request["payload"]["metadata"]
|
||||
readonly resume?: Endpoint4_13Request["payload"]["resume"]
|
||||
type Endpoint4_12Request = Parameters<RawClient["server.session"]["session.synthetic"]>[0]
|
||||
type Endpoint4_12Input = {
|
||||
readonly sessionID: Endpoint4_12Request["params"]["sessionID"]
|
||||
readonly text: Endpoint4_12Request["payload"]["text"]
|
||||
readonly description?: Endpoint4_12Request["payload"]["description"]
|
||||
readonly metadata?: Endpoint4_12Request["payload"]["metadata"]
|
||||
}
|
||||
const Endpoint4_13 = (raw: RawClient["server.session"]) => (input: Endpoint4_13Input) =>
|
||||
const Endpoint4_12 = (raw: RawClient["server.session"]) => (input: Endpoint4_12Input) =>
|
||||
raw["session.synthetic"]({
|
||||
params: { sessionID: input["sessionID"] },
|
||||
payload: {
|
||||
text: input["text"],
|
||||
description: input["description"],
|
||||
metadata: input["metadata"],
|
||||
resume: input["resume"],
|
||||
},
|
||||
payload: { text: input["text"], description: input["description"], metadata: input["metadata"] },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint4_14Request = Parameters<RawClient["server.session"]["session.shell"]>[0]
|
||||
type Endpoint4_14Input = {
|
||||
readonly sessionID: Endpoint4_14Request["params"]["sessionID"]
|
||||
readonly id?: Endpoint4_14Request["payload"]["id"]
|
||||
readonly command: Endpoint4_14Request["payload"]["command"]
|
||||
type Endpoint4_13Request = Parameters<RawClient["server.session"]["session.shell"]>[0]
|
||||
type Endpoint4_13Input = {
|
||||
readonly sessionID: Endpoint4_13Request["params"]["sessionID"]
|
||||
readonly id?: Endpoint4_13Request["payload"]["id"]
|
||||
readonly command: Endpoint4_13Request["payload"]["command"]
|
||||
}
|
||||
const Endpoint4_14 = (raw: RawClient["server.session"]) => (input: Endpoint4_14Input) =>
|
||||
const Endpoint4_13 = (raw: RawClient["server.session"]) => (input: Endpoint4_13Input) =>
|
||||
raw["session.shell"]({
|
||||
params: { sessionID: input["sessionID"] },
|
||||
payload: { id: input["id"], command: input["command"] },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint4_15Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
|
||||
type Endpoint4_15Input = {
|
||||
readonly sessionID: Endpoint4_15Request["params"]["sessionID"]
|
||||
readonly id?: Endpoint4_15Request["payload"]["id"]
|
||||
type Endpoint4_14Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
|
||||
type Endpoint4_14Input = {
|
||||
readonly sessionID: Endpoint4_14Request["params"]["sessionID"]
|
||||
readonly id?: Endpoint4_14Request["payload"]["id"]
|
||||
}
|
||||
const Endpoint4_15 = (raw: RawClient["server.session"]) => (input: Endpoint4_15Input) =>
|
||||
const Endpoint4_14 = (raw: RawClient["server.session"]) => (input: Endpoint4_14Input) =>
|
||||
raw["session.compact"]({ params: { sessionID: input["sessionID"] }, payload: { id: input["id"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
)
|
||||
|
||||
type Endpoint4_16Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
|
||||
type Endpoint4_16Input = { readonly sessionID: Endpoint4_16Request["params"]["sessionID"] }
|
||||
const Endpoint4_16 = (raw: RawClient["server.session"]) => (input: Endpoint4_16Input) =>
|
||||
type Endpoint4_15Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
|
||||
type Endpoint4_15Input = { readonly sessionID: Endpoint4_15Request["params"]["sessionID"] }
|
||||
const Endpoint4_15 = (raw: RawClient["server.session"]) => (input: Endpoint4_15Input) =>
|
||||
raw["session.wait"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint4_17Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
|
||||
type Endpoint4_17Input = {
|
||||
readonly sessionID: Endpoint4_17Request["params"]["sessionID"]
|
||||
readonly messageID: Endpoint4_17Request["payload"]["messageID"]
|
||||
readonly files?: Endpoint4_17Request["payload"]["files"]
|
||||
type Endpoint4_16Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
|
||||
type Endpoint4_16Input = {
|
||||
readonly sessionID: Endpoint4_16Request["params"]["sessionID"]
|
||||
readonly messageID: Endpoint4_16Request["payload"]["messageID"]
|
||||
readonly files?: Endpoint4_16Request["payload"]["files"]
|
||||
}
|
||||
const Endpoint4_17 = (raw: RawClient["server.session"]) => (input: Endpoint4_17Input) =>
|
||||
const Endpoint4_16 = (raw: RawClient["server.session"]) => (input: Endpoint4_16Input) =>
|
||||
raw["session.revert.stage"]({
|
||||
params: { sessionID: input["sessionID"] },
|
||||
payload: { messageID: input["messageID"], files: input["files"] },
|
||||
@@ -277,61 +259,61 @@ const Endpoint4_17 = (raw: RawClient["server.session"]) => (input: Endpoint4_17I
|
||||
Effect.map((value) => value.data),
|
||||
)
|
||||
|
||||
type Endpoint4_18Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
|
||||
type Endpoint4_18Input = { readonly sessionID: Endpoint4_18Request["params"]["sessionID"] }
|
||||
const Endpoint4_18 = (raw: RawClient["server.session"]) => (input: Endpoint4_18Input) =>
|
||||
type Endpoint4_17Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
|
||||
type Endpoint4_17Input = { readonly sessionID: Endpoint4_17Request["params"]["sessionID"] }
|
||||
const Endpoint4_17 = (raw: RawClient["server.session"]) => (input: Endpoint4_17Input) =>
|
||||
raw["session.revert.clear"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
|
||||
type Endpoint4_19Input = { readonly sessionID: Endpoint4_19Request["params"]["sessionID"] }
|
||||
const Endpoint4_19 = (raw: RawClient["server.session"]) => (input: Endpoint4_19Input) =>
|
||||
type Endpoint4_18Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
|
||||
type Endpoint4_18Input = { readonly sessionID: Endpoint4_18Request["params"]["sessionID"] }
|
||||
const Endpoint4_18 = (raw: RawClient["server.session"]) => (input: Endpoint4_18Input) =>
|
||||
raw["session.revert.commit"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.context"]>[0]
|
||||
type Endpoint4_20Input = { readonly sessionID: Endpoint4_20Request["params"]["sessionID"] }
|
||||
const Endpoint4_20 = (raw: RawClient["server.session"]) => (input: Endpoint4_20Input) =>
|
||||
type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.context"]>[0]
|
||||
type Endpoint4_19Input = { readonly sessionID: Endpoint4_19Request["params"]["sessionID"] }
|
||||
const Endpoint4_19 = (raw: RawClient["server.session"]) => (input: Endpoint4_19Input) =>
|
||||
raw["session.context"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
)
|
||||
|
||||
type Endpoint4_21Request = Parameters<RawClient["server.session"]["session.instructions.entry.list"]>[0]
|
||||
type Endpoint4_21Input = { readonly sessionID: Endpoint4_21Request["params"]["sessionID"] }
|
||||
const Endpoint4_21 = (raw: RawClient["server.session"]) => (input: Endpoint4_21Input) =>
|
||||
type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.instructions.entry.list"]>[0]
|
||||
type Endpoint4_20Input = { readonly sessionID: Endpoint4_20Request["params"]["sessionID"] }
|
||||
const Endpoint4_20 = (raw: RawClient["server.session"]) => (input: Endpoint4_20Input) =>
|
||||
raw["session.instructions.entry.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
)
|
||||
|
||||
type Endpoint4_22Request = Parameters<RawClient["server.session"]["session.instructions.entry.put"]>[0]
|
||||
type Endpoint4_22Input = {
|
||||
readonly sessionID: Endpoint4_22Request["params"]["sessionID"]
|
||||
readonly key: Endpoint4_22Request["params"]["key"]
|
||||
readonly value: Endpoint4_22Request["payload"]["value"]
|
||||
type Endpoint4_21Request = Parameters<RawClient["server.session"]["session.instructions.entry.put"]>[0]
|
||||
type Endpoint4_21Input = {
|
||||
readonly sessionID: Endpoint4_21Request["params"]["sessionID"]
|
||||
readonly key: Endpoint4_21Request["params"]["key"]
|
||||
readonly value: Endpoint4_21Request["payload"]["value"]
|
||||
}
|
||||
const Endpoint4_22 = (raw: RawClient["server.session"]) => (input: Endpoint4_22Input) =>
|
||||
const Endpoint4_21 = (raw: RawClient["server.session"]) => (input: Endpoint4_21Input) =>
|
||||
raw["session.instructions.entry.put"]({
|
||||
params: { sessionID: input["sessionID"], key: input["key"] },
|
||||
payload: { value: input["value"] },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint4_23Request = Parameters<RawClient["server.session"]["session.instructions.entry.remove"]>[0]
|
||||
type Endpoint4_23Input = {
|
||||
readonly sessionID: Endpoint4_23Request["params"]["sessionID"]
|
||||
readonly key: Endpoint4_23Request["params"]["key"]
|
||||
type Endpoint4_22Request = Parameters<RawClient["server.session"]["session.instructions.entry.remove"]>[0]
|
||||
type Endpoint4_22Input = {
|
||||
readonly sessionID: Endpoint4_22Request["params"]["sessionID"]
|
||||
readonly key: Endpoint4_22Request["params"]["key"]
|
||||
}
|
||||
const Endpoint4_23 = (raw: RawClient["server.session"]) => (input: Endpoint4_23Input) =>
|
||||
const Endpoint4_22 = (raw: RawClient["server.session"]) => (input: Endpoint4_22Input) =>
|
||||
raw["session.instructions.entry.remove"]({ params: { sessionID: input["sessionID"], key: input["key"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
)
|
||||
|
||||
type Endpoint4_24Request = Parameters<RawClient["server.session"]["session.log"]>[0]
|
||||
type Endpoint4_24Input = {
|
||||
readonly sessionID: Endpoint4_24Request["params"]["sessionID"]
|
||||
readonly after?: Endpoint4_24Request["query"]["after"]
|
||||
readonly follow?: Endpoint4_24Request["query"]["follow"]
|
||||
type Endpoint4_23Request = Parameters<RawClient["server.session"]["session.log"]>[0]
|
||||
type Endpoint4_23Input = {
|
||||
readonly sessionID: Endpoint4_23Request["params"]["sessionID"]
|
||||
readonly after?: Endpoint4_23Request["query"]["after"]
|
||||
readonly follow?: Endpoint4_23Request["query"]["follow"]
|
||||
}
|
||||
const Endpoint4_24 = (raw: RawClient["server.session"]) => (input: Endpoint4_24Input) =>
|
||||
const Endpoint4_23 = (raw: RawClient["server.session"]) => (input: Endpoint4_23Input) =>
|
||||
Stream.unwrap(
|
||||
raw["session.log"]({
|
||||
params: { sessionID: input["sessionID"] },
|
||||
@@ -342,22 +324,22 @@ const Endpoint4_24 = (raw: RawClient["server.session"]) => (input: Endpoint4_24I
|
||||
),
|
||||
)
|
||||
|
||||
type Endpoint4_25Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
|
||||
type Endpoint4_25Input = { readonly sessionID: Endpoint4_25Request["params"]["sessionID"] }
|
||||
const Endpoint4_25 = (raw: RawClient["server.session"]) => (input: Endpoint4_25Input) =>
|
||||
type Endpoint4_24Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
|
||||
type Endpoint4_24Input = { readonly sessionID: Endpoint4_24Request["params"]["sessionID"] }
|
||||
const Endpoint4_24 = (raw: RawClient["server.session"]) => (input: Endpoint4_24Input) =>
|
||||
raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint4_26Request = Parameters<RawClient["server.session"]["session.background"]>[0]
|
||||
type Endpoint4_26Input = { readonly sessionID: Endpoint4_26Request["params"]["sessionID"] }
|
||||
const Endpoint4_26 = (raw: RawClient["server.session"]) => (input: Endpoint4_26Input) =>
|
||||
type Endpoint4_25Request = Parameters<RawClient["server.session"]["session.background"]>[0]
|
||||
type Endpoint4_25Input = { readonly sessionID: Endpoint4_25Request["params"]["sessionID"] }
|
||||
const Endpoint4_25 = (raw: RawClient["server.session"]) => (input: Endpoint4_25Input) =>
|
||||
raw["session.background"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint4_27Request = Parameters<RawClient["server.session"]["session.message"]>[0]
|
||||
type Endpoint4_27Input = {
|
||||
readonly sessionID: Endpoint4_27Request["params"]["sessionID"]
|
||||
readonly messageID: Endpoint4_27Request["params"]["messageID"]
|
||||
type Endpoint4_26Request = Parameters<RawClient["server.session"]["session.message"]>[0]
|
||||
type Endpoint4_26Input = {
|
||||
readonly sessionID: Endpoint4_26Request["params"]["sessionID"]
|
||||
readonly messageID: Endpoint4_26Request["params"]["messageID"]
|
||||
}
|
||||
const Endpoint4_27 = (raw: RawClient["server.session"]) => (input: Endpoint4_27Input) =>
|
||||
const Endpoint4_26 = (raw: RawClient["server.session"]) => (input: Endpoint4_26Input) =>
|
||||
raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
@@ -373,23 +355,22 @@ const adaptGroup4 = (raw: RawClient["server.session"]) => ({
|
||||
switchAgent: Endpoint4_6(raw),
|
||||
switchModel: Endpoint4_7(raw),
|
||||
rename: Endpoint4_8(raw),
|
||||
move: Endpoint4_9(raw),
|
||||
prompt: Endpoint4_10(raw),
|
||||
command: Endpoint4_11(raw),
|
||||
skill: Endpoint4_12(raw),
|
||||
synthetic: Endpoint4_13(raw),
|
||||
shell: Endpoint4_14(raw),
|
||||
compact: Endpoint4_15(raw),
|
||||
wait: Endpoint4_16(raw),
|
||||
revertStage: Endpoint4_17(raw),
|
||||
revertClear: Endpoint4_18(raw),
|
||||
revertCommit: Endpoint4_19(raw),
|
||||
context: Endpoint4_20(raw),
|
||||
instructions: { entry: { list: Endpoint4_21(raw), put: Endpoint4_22(raw), remove: Endpoint4_23(raw) } },
|
||||
log: Endpoint4_24(raw),
|
||||
interrupt: Endpoint4_25(raw),
|
||||
background: Endpoint4_26(raw),
|
||||
message: Endpoint4_27(raw),
|
||||
prompt: Endpoint4_9(raw),
|
||||
command: Endpoint4_10(raw),
|
||||
skill: Endpoint4_11(raw),
|
||||
synthetic: Endpoint4_12(raw),
|
||||
shell: Endpoint4_13(raw),
|
||||
compact: Endpoint4_14(raw),
|
||||
wait: Endpoint4_15(raw),
|
||||
revertStage: Endpoint4_16(raw),
|
||||
revertClear: Endpoint4_17(raw),
|
||||
revertCommit: Endpoint4_18(raw),
|
||||
context: Endpoint4_19(raw),
|
||||
instructions: { entry: { list: Endpoint4_20(raw), put: Endpoint4_21(raw), remove: Endpoint4_22(raw) } },
|
||||
log: Endpoint4_23(raw),
|
||||
interrupt: Endpoint4_24(raw),
|
||||
background: Endpoint4_25(raw),
|
||||
message: Endpoint4_26(raw),
|
||||
})
|
||||
|
||||
type Endpoint5_0Request = Parameters<RawClient["server.message"]["session.messages"]>[0]
|
||||
@@ -548,12 +529,7 @@ type Endpoint10_0Input = { readonly location?: Endpoint10_0Request["query"]["loc
|
||||
const Endpoint10_0 = (raw: RawClient["server.mcp"]) => (input?: Endpoint10_0Input) =>
|
||||
raw["mcp.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint10_1Request = Parameters<RawClient["server.mcp"]["mcp.resource.catalog"]>[0]
|
||||
type Endpoint10_1Input = { readonly location?: Endpoint10_1Request["query"]["location"] }
|
||||
const Endpoint10_1 = (raw: RawClient["server.mcp"]) => (input?: Endpoint10_1Input) =>
|
||||
raw["mcp.resource.catalog"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
const adaptGroup10 = (raw: RawClient["server.mcp"]) => ({ list: Endpoint10_0(raw), catalog: Endpoint10_1(raw) })
|
||||
const adaptGroup10 = (raw: RawClient["server.mcp"]) => ({ list: Endpoint10_0(raw) })
|
||||
|
||||
type Endpoint11_0Request = Parameters<RawClient["server.credential"]["credential.update"]>[0]
|
||||
type Endpoint11_0Input = {
|
||||
@@ -1093,15 +1069,7 @@ const adaptGroup24 = (raw: RawClient["server.vcs"]) => ({ status: Endpoint24_0(r
|
||||
const Endpoint25_0 = (raw: RawClient["server.debug"]) => () =>
|
||||
raw["debug.location"]({}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint25_1Request = Parameters<RawClient["server.debug"]["debug.location.evict"]>[0]
|
||||
type Endpoint25_1Input = { readonly location?: Endpoint25_1Request["query"]["location"] }
|
||||
const Endpoint25_1 = (raw: RawClient["server.debug"]) => (input?: Endpoint25_1Input) =>
|
||||
raw["debug.location.evict"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
const adaptGroup25 = (raw: RawClient["server.debug"]) => ({
|
||||
location: Endpoint25_0(raw),
|
||||
evictLocation: Endpoint25_1(raw),
|
||||
})
|
||||
const adaptGroup25 = (raw: RawClient["server.debug"]) => ({ location: Endpoint25_0(raw) })
|
||||
|
||||
const adaptClient = (raw: RawClient) => ({
|
||||
health: adaptGroup0(raw["server.health"]),
|
||||
|
||||
@@ -23,8 +23,6 @@ import type {
|
||||
SessionSwitchModelOutput,
|
||||
SessionRenameInput,
|
||||
SessionRenameOutput,
|
||||
SessionMoveInput,
|
||||
SessionMoveOutput,
|
||||
SessionPromptInput,
|
||||
SessionPromptOutput,
|
||||
SessionCommandInput,
|
||||
@@ -89,8 +87,6 @@ import type {
|
||||
IntegrationAttemptCancelOutput,
|
||||
ServerMcpListInput,
|
||||
ServerMcpListOutput,
|
||||
ServerMcpCatalogInput,
|
||||
ServerMcpCatalogOutput,
|
||||
CredentialUpdateInput,
|
||||
CredentialUpdateOutput,
|
||||
CredentialRemoveInput,
|
||||
@@ -182,8 +178,6 @@ import type {
|
||||
VcsDiffInput,
|
||||
VcsDiffOutput,
|
||||
DebugLocationOutput,
|
||||
DebugEvictLocationInput,
|
||||
DebugEvictLocationOutput,
|
||||
} from "./types"
|
||||
import { ClientError } from "./client-error"
|
||||
|
||||
@@ -491,18 +485,6 @@ export function make(options: ClientOptions) {
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
move: (input: SessionMoveInput, requestOptions?: RequestOptions) =>
|
||||
request<SessionMoveOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/move`,
|
||||
body: { destination: input["destination"], moveChanges: input["moveChanges"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [404, 400, 401],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
prompt: (input: SessionPromptInput, requestOptions?: RequestOptions) =>
|
||||
request<{ readonly data: SessionPromptOutput }>(
|
||||
{
|
||||
@@ -554,12 +536,7 @@ export function make(options: ClientOptions) {
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/synthetic`,
|
||||
body: {
|
||||
text: input["text"],
|
||||
description: input["description"],
|
||||
metadata: input["metadata"],
|
||||
resume: input["resume"],
|
||||
},
|
||||
body: { text: input["text"], description: input["description"], metadata: input["metadata"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [404, 400, 401],
|
||||
empty: true,
|
||||
@@ -913,18 +890,6 @@ export function make(options: ClientOptions) {
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
catalog: (input?: ServerMcpCatalogInput, requestOptions?: RequestOptions) =>
|
||||
request<ServerMcpCatalogOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/mcp/resource`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
credential: {
|
||||
update: (input: CredentialUpdateInput, requestOptions?: RequestOptions) =>
|
||||
@@ -1529,18 +1494,6 @@ export function make(options: ClientOptions) {
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
evictLocation: (input?: DebugEvictLocationInput, requestOptions?: RequestOptions) =>
|
||||
request<DebugEvictLocationOutput>(
|
||||
{
|
||||
method: "DELETE",
|
||||
path: `/api/debug/location`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -526,20 +526,6 @@ export type SessionRenameInput = {
|
||||
|
||||
export type SessionRenameOutput = void
|
||||
|
||||
export type SessionMoveInput = {
|
||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||
readonly destination: {
|
||||
readonly destination: { readonly directory: string }
|
||||
readonly moveChanges?: boolean | undefined
|
||||
}["destination"]
|
||||
readonly moveChanges?: {
|
||||
readonly destination: { readonly directory: string }
|
||||
readonly moveChanges?: boolean | undefined
|
||||
}["moveChanges"]
|
||||
}
|
||||
|
||||
export type SessionMoveOutput = void
|
||||
|
||||
export type SessionPromptInput = {
|
||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||
readonly id?: {
|
||||
@@ -870,26 +856,17 @@ export type SessionSyntheticInput = {
|
||||
readonly text: string
|
||||
readonly description?: string | null
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly resume?: boolean | null
|
||||
}["text"]
|
||||
readonly description?: {
|
||||
readonly text: string
|
||||
readonly description?: string | null
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly resume?: boolean | null
|
||||
}["description"]
|
||||
readonly metadata?: {
|
||||
readonly text: string
|
||||
readonly description?: string | null
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly resume?: boolean | null
|
||||
}["metadata"]
|
||||
readonly resume?: {
|
||||
readonly text: string
|
||||
readonly description?: string | null
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly resume?: boolean | null
|
||||
}["resume"]
|
||||
}
|
||||
|
||||
export type SessionSyntheticOutput = void
|
||||
@@ -2501,36 +2478,6 @@ export type ServerMcpListOutput = {
|
||||
}>
|
||||
}
|
||||
|
||||
export type ServerMcpCatalogInput = {
|
||||
readonly location?: {
|
||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||
}["location"]
|
||||
}
|
||||
|
||||
export type ServerMcpCatalogOutput = {
|
||||
readonly location: {
|
||||
readonly directory: string
|
||||
readonly workspaceID?: string
|
||||
readonly project: { readonly id: string; readonly directory: string }
|
||||
}
|
||||
readonly data: {
|
||||
readonly resources: ReadonlyArray<{
|
||||
readonly server: string
|
||||
readonly name: string
|
||||
readonly uri: string
|
||||
readonly description?: string
|
||||
readonly mimeType?: string
|
||||
}>
|
||||
readonly templates: ReadonlyArray<{
|
||||
readonly server: string
|
||||
readonly name: string
|
||||
readonly uriTemplate: string
|
||||
readonly description?: string
|
||||
readonly mimeType?: string
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export type CredentialUpdateInput = {
|
||||
readonly credentialID: { readonly credentialID: string }["credentialID"]
|
||||
readonly location?: {
|
||||
@@ -5561,14 +5508,6 @@ export type EventSubscribeOutput =
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: { readonly server: string }
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly created: number
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "mcp.resources.changed"
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: { readonly server: string }
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly created: number
|
||||
@@ -6191,11 +6130,3 @@ export type VcsDiffOutput = {
|
||||
}
|
||||
|
||||
export type DebugLocationOutput = ReadonlyArray<{ readonly directory: string; readonly workspaceID?: string }>
|
||||
|
||||
export type DebugEvictLocationInput = {
|
||||
readonly location?: {
|
||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||
}["location"]
|
||||
}
|
||||
|
||||
export type DebugEvictLocationOutput = void
|
||||
|
||||
@@ -50,29 +50,6 @@ test("exposes every standard HTTP API group", () => {
|
||||
expect(Object.keys(client.project)).toEqual(["list", "current", "directories"])
|
||||
})
|
||||
|
||||
test("MCP resource catalog uses the public HTTP contract", async () => {
|
||||
let request: Request | undefined
|
||||
const client = OpenCode.make({
|
||||
baseUrl: "http://localhost:3000",
|
||||
fetch: async (input) => {
|
||||
request = input instanceof Request ? input : new Request(input)
|
||||
return Response.json({
|
||||
location: { directory: "/tmp/project", project: { id: "proj_test", directory: "/tmp/project" } },
|
||||
data: {
|
||||
resources: [{ server: "docs", name: "Readme", uri: "docs://readme" }],
|
||||
templates: [{ server: "docs", name: "File", uriTemplate: "docs://{path}" }],
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const result = await client["server.mcp"].catalog({ location: { directory: "/tmp/project" } })
|
||||
|
||||
expect(result.data.resources[0]?.uri).toBe("docs://readme")
|
||||
expect(request?.method).toBe("GET")
|
||||
expect(request?.url).toBe("http://localhost:3000/api/mcp/resource?location%5Bdirectory%5D=%2Ftmp%2Fproject")
|
||||
})
|
||||
|
||||
test("file.read returns binary content from the public HTTP contract", async () => {
|
||||
let request: Request | undefined
|
||||
const client = OpenCode.make({
|
||||
|
||||
@@ -62,7 +62,7 @@ const result =
|
||||
|
||||
`result` is always a `CodeMode.Result`. Program, validation, limit, and tool failures are returned as diagnostics rather than failing the Effect. Host interruption remains interruption.
|
||||
|
||||
Successful result values are JSON-safe data. An explicit `return` produces the program result; when it is omitted, the final executable top-level expression is returned as a model-friendly REPL convenience. Otherwise reaching the end produces `null`. Returned `undefined` and nested `undefined` values are normalized to `null` as well.
|
||||
Successful result values are JSON-safe data. A program that returns `undefined`, including by reaching the end without `return`, produces `null`; nested `undefined` values are normalized to `null` as well.
|
||||
|
||||
## API
|
||||
|
||||
@@ -241,7 +241,7 @@ CodeMode executes a deliberately bounded JavaScript subset. It supports:
|
||||
- `if`, conditional expressions, `switch`, `for`, `for...of` (arrays, strings, Maps, Sets, including assignment-form destructuring such as `for ([key, value] of entries)`), `for...in` (own keys of plain objects, index strings of arrays, and namespace/tool names of `tools` references - anything else is an error suggesting `for...of` or `Object.keys`, rather than real JS's surprising behavior of indices for strings and zero iterations for Maps/Sets), `while`, and `do...while`.
|
||||
- Arrow functions and function declarations with closures, defaults, rest parameters, and destructuring.
|
||||
- Optional chaining, nullish coalescing, templates, spread (arrays, strings, Maps, Sets), and `try`/`catch`.
|
||||
- Common array, string, number, `Object`, `Math`, and `JSON` operations, including primitive-number `valueOf`, the standard non-finite `Number` constants, and host-backed `Math.random`. Mutating array methods include `push`/`pop`/`shift`/`unshift`/`splice` (removes in place and returns the removed elements)/`fill`/`copyWithin`; array `keys`/`values`/`entries` return **arrays** (matching the Map/Set convention) and work with `for...of` and spread. String methods include `localeCompare` (locale/options arguments ignored), `normalize`, and the `trimLeft`/`trimRight` aliases. `Object.keys` also accepts arrays (index strings, as in JS) and tool references: `Object.keys(tools)` lists the top-level namespaces, including `$codemode`, and `Object.keys(tools.ns)` lists the names at that node (a callable tool enumerates as `[]`; an unknown path is an `UnknownTool` diagnostic). `Object.values`/`Object.entries` on a tool reference fail with a pointer at `Object.keys(tools)` and `tools.$codemode.search`.
|
||||
- Common array, string, number, `Object`, `Math`, and `JSON` operations. Mutating array methods include `push`/`pop`/`shift`/`unshift`/`splice` (removes in place and returns the removed elements)/`fill`/`copyWithin`; array `keys`/`values`/`entries` return **arrays** (matching the Map/Set convention) and work with `for...of` and spread. String methods include `localeCompare` (locale/options arguments ignored), `normalize`, and the `trimLeft`/`trimRight` aliases. `Object.keys` also accepts arrays (index strings, as in JS) and tool references: `Object.keys(tools)` lists the top-level namespaces, including `$codemode`, and `Object.keys(tools.ns)` lists the names at that node (a callable tool enumerates as `[]`; an unknown path is an `UnknownTool` diagnostic). `Object.values`/`Object.entries` on a tool reference fail with a pointer at `Object.keys(tools)` and `tools.$codemode.search`.
|
||||
- `Date` - `Date.now()`/`Date.parse()`/`Date.UTC()`, `new Date(...)`, the getter methods, and date arithmetic/comparison via the time value. Dates stringify as ISO (`toString` included, for determinism across host timezones).
|
||||
- Regular expressions - `/literals/` and `new RegExp(...)` with `test`/`exec` (stateful `lastIndex` for `g`), plus string `match`/`matchAll`/`replace`/`replaceAll`/`split`/`search` with patterns. Match results are arrays carrying `index` and named `groups` as own properties (`input` is omitted). `replace` and `replaceAll` accept function replacers with captures, offset, input, and named groups; callbacks run sequentially, may await tool calls, and have their results coerced to strings. Invalid patterns, invalid flags, and missing-`g` calls fail with catchable errors that say what was wrong and how to fix it (escaping hints, the exact `/pattern/g` to write). Patterns run on the host engine, so pathological backtracking is bounded only by the execution timeout.
|
||||
- `Map` and `Set` - construction from entries/arrays/strings, `get`/`set`/`add`/`has`/`delete`/`clear`/`size`/`forEach`, and `keys`/`values`/`entries` returning **arrays** (not iterators).
|
||||
|
||||
@@ -158,7 +158,7 @@ current omissions to implement, not intentional product boundaries.
|
||||
- [ ] Add `Object.is` after runtime method and tool references have stable identity semantics.
|
||||
- [ ] Add deterministic modern collection conveniences where they improve orchestration: `Object.groupBy`, Set
|
||||
composition methods, and `Array.prototype.toSpliced`.
|
||||
- [ ] Decide whether iterable `Math.sumPrecise` belongs in the runtime.
|
||||
- [ ] Decide whether nondeterministic `Math.random` and iterable `Math.sumPrecise` belong in the runtime.
|
||||
- [ ] Refine diagnostics so user throws, expected tool failures, unexpected host/tool defects, and genuine interpreter
|
||||
defects are distinguishable without leaking private causes.
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ export type Binding = {
|
||||
|
||||
export type StatementResult =
|
||||
| { kind: "none" }
|
||||
| { kind: "value"; value: unknown }
|
||||
| { kind: "return"; value: unknown }
|
||||
| { kind: "break" }
|
||||
| { kind: "continue" }
|
||||
|
||||
@@ -530,29 +530,17 @@ const invokeArrayStatic = (name: string, args: Array<unknown>, node: AstNode): u
|
||||
if (args[0] instanceof SandboxURLSearchParams) {
|
||||
return Array.from(args[0].params.entries(), ([key, value]) => [key, value])
|
||||
}
|
||||
const source = args[0]
|
||||
if (source instanceof SandboxPromise) {
|
||||
throw new InterpreterRuntimeError(
|
||||
"Array.from received an un-awaited Promise; await it before creating the array.",
|
||||
node,
|
||||
"InvalidDataValue",
|
||||
)
|
||||
}
|
||||
const source = boundedData(args[0], "Array.from input")
|
||||
if (typeof source === "string") return Array.from(source)
|
||||
if (Array.isArray(source)) return [...source]
|
||||
if (
|
||||
source !== null &&
|
||||
typeof source === "object" &&
|
||||
(Object.getPrototypeOf(source) === Object.prototype || Object.getPrototypeOf(source) === null) &&
|
||||
typeof (source as { length?: unknown }).length === "number"
|
||||
) {
|
||||
return Array.from(source as ArrayLike<unknown>)
|
||||
}
|
||||
throw new InterpreterRuntimeError(
|
||||
"Array.from expects an array, string, Map, Set, or array-like value.",
|
||||
node,
|
||||
"InvalidDataValue",
|
||||
)
|
||||
throw new InterpreterRuntimeError("Array.from expects an array, string, Map, Set, or array-like value.", node)
|
||||
}
|
||||
default:
|
||||
throw new InterpreterRuntimeError(`Array.${name} is not available in CodeMode.`, node)
|
||||
@@ -618,6 +606,7 @@ class Interpreter<R> {
|
||||
// ToolRuntime.make like invokeTool: the interpreter never holds the tree itself.
|
||||
private readonly toolKeys: (path: ReadonlyArray<string>) => ReadonlyArray<string>
|
||||
private readonly logs: Array<string>
|
||||
private lastValue: unknown
|
||||
// Caps how many eagerly forked tool calls run at once (the parallel-call concurrency cap).
|
||||
private readonly callPermits: Semaphore.Semaphore
|
||||
// Fiber-backed promises whose settlement no program construct has observed yet. Successful
|
||||
@@ -636,6 +625,7 @@ class Interpreter<R> {
|
||||
this.invokeTool = invokeTool
|
||||
this.toolKeys = toolKeys
|
||||
this.logs = logs
|
||||
this.lastValue = undefined
|
||||
this.callPermits = shared?.callPermits ?? Semaphore.makeUnsafe(TOOL_CALL_CONCURRENCY)
|
||||
this.pendingSettlements = shared?.pendingSettlements ?? new Set<SandboxPromise>()
|
||||
globalScope.set("tools", { mutable: false, value: new ToolReference([]) })
|
||||
@@ -681,15 +671,13 @@ class Interpreter<R> {
|
||||
return Effect.gen(function* () {
|
||||
self.hoistFunctions(program.body)
|
||||
let value: unknown = undefined
|
||||
for (const [index, statement] of program.body.entries()) {
|
||||
if (index === program.body.length - 1 && statement.type === "ExpressionStatement") {
|
||||
value = yield* self.evaluateExpression(getNode(statement, "expression"))
|
||||
break
|
||||
}
|
||||
let returned = false
|
||||
for (const statement of program.body) {
|
||||
const result = yield* self.evaluateStatement(statement)
|
||||
|
||||
if (result.kind === "return") {
|
||||
value = result.value
|
||||
returned = true
|
||||
break
|
||||
}
|
||||
|
||||
@@ -697,7 +685,11 @@ class Interpreter<R> {
|
||||
throw new InterpreterRuntimeError(`Unexpected '${result.kind}' outside of a loop.`, statement)
|
||||
}
|
||||
|
||||
if (result.kind === "value") {
|
||||
self.lastValue = result.value
|
||||
}
|
||||
}
|
||||
if (!returned) value = self.lastValue
|
||||
|
||||
// The program body runs inside an implicit async function, so a returned promise
|
||||
// resolves before crossing the data boundary - `return tools.ns.tool(...)` works
|
||||
@@ -788,7 +780,7 @@ class Interpreter<R> {
|
||||
private evaluateStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> {
|
||||
switch (node.type) {
|
||||
case "ExpressionStatement":
|
||||
return Effect.as(this.evaluateExpression(getNode(node, "expression")), { kind: "none" })
|
||||
return Effect.map(this.evaluateExpression(getNode(node, "expression")), (value) => ({ kind: "value", value }))
|
||||
case "VariableDeclaration":
|
||||
return Effect.map(this.evaluateVariableDeclaration(node), () => ({ kind: "none" }))
|
||||
case "ReturnStatement": {
|
||||
@@ -841,6 +833,11 @@ class Interpreter<R> {
|
||||
const statement = asNode(statementValue, "body")
|
||||
const result = yield* self.evaluateStatement(statement)
|
||||
|
||||
if (result.kind === "value") {
|
||||
self.lastValue = result.value
|
||||
continue
|
||||
}
|
||||
|
||||
if (result.kind !== "none") {
|
||||
return result
|
||||
}
|
||||
@@ -932,6 +929,7 @@ class Interpreter<R> {
|
||||
const result = yield* self.evaluateStatement(asNode(statementValue, "consequent"))
|
||||
if (result.kind === "break") return { kind: "none" } satisfies StatementResult
|
||||
if (result.kind === "return" || result.kind === "continue") return result
|
||||
if (result.kind === "value") self.lastValue = result.value
|
||||
}
|
||||
}
|
||||
return { kind: "none" } satisfies StatementResult
|
||||
@@ -959,6 +957,9 @@ class Interpreter<R> {
|
||||
return result
|
||||
}
|
||||
|
||||
if (result.kind === "value") {
|
||||
self.lastValue = result.value
|
||||
}
|
||||
}
|
||||
|
||||
return { kind: "none" } satisfies StatementResult
|
||||
@@ -986,6 +987,9 @@ class Interpreter<R> {
|
||||
return result
|
||||
}
|
||||
|
||||
if (result.kind === "value") {
|
||||
self.lastValue = result.value
|
||||
}
|
||||
} while (yield* self.evaluateExpression(testNode))
|
||||
|
||||
return { kind: "none" } satisfies StatementResult
|
||||
@@ -1041,6 +1045,10 @@ class Interpreter<R> {
|
||||
return { kind: "none" } satisfies StatementResult
|
||||
}
|
||||
|
||||
if (result.kind === "value") {
|
||||
self.lastValue = result.value
|
||||
}
|
||||
|
||||
if (iterationScope) {
|
||||
const loopScope = self.currentScope()
|
||||
for (const name of perIterationBindings) {
|
||||
@@ -1125,6 +1133,10 @@ class Interpreter<R> {
|
||||
return { kind: "none" }
|
||||
}
|
||||
|
||||
if (result.kind === "value") {
|
||||
self.lastValue = result.value
|
||||
}
|
||||
|
||||
if (result.kind === "continue") {
|
||||
continue
|
||||
}
|
||||
@@ -1214,6 +1226,10 @@ class Interpreter<R> {
|
||||
return { kind: "none" }
|
||||
}
|
||||
|
||||
if (result.kind === "value") {
|
||||
self.lastValue = result.value
|
||||
}
|
||||
|
||||
if (result.kind === "continue") {
|
||||
continue
|
||||
}
|
||||
@@ -2017,9 +2033,6 @@ class Interpreter<R> {
|
||||
if (callable.namespace === "Object" && objectMethodsPreservingIdentity.has(callable.name)) {
|
||||
return invokeGlobalMethod(callable, args, node)
|
||||
}
|
||||
if (callable.namespace === "Array" && (callable.name === "from" || callable.name === "of")) {
|
||||
return invokeGlobalMethod(callable, args, node)
|
||||
}
|
||||
return boundedData(invokeGlobalMethod(callable, args, node), `${callable.namespace}.${callable.name} result`)
|
||||
}
|
||||
if (callable instanceof CoercionFunction) {
|
||||
@@ -2359,7 +2372,7 @@ class Interpreter<R> {
|
||||
|
||||
if (fn.body.type === "BlockStatement") {
|
||||
const result = yield* invocation.evaluateStatement(fn.body)
|
||||
return result.kind === "return" ? result.value : undefined
|
||||
return result.kind === "return" || result.kind === "value" ? result.value : undefined
|
||||
}
|
||||
|
||||
return yield* invocation.evaluateExpression(fn.body)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
export const mathConstants = new Set(["PI", "E", "LN2", "LN10", "LOG2E", "LOG10E", "SQRT2", "SQRT1_2"])
|
||||
|
||||
export const mathMethods = new Set([
|
||||
"random",
|
||||
"max",
|
||||
"min",
|
||||
"abs",
|
||||
@@ -41,7 +40,6 @@ export const mathMethods = new Set([
|
||||
|
||||
export const invokeMathMethod = (name: string, args: Array<unknown>, node: AstNode): number => {
|
||||
if (!mathMethods.has(name)) throw new InterpreterRuntimeError(`Math.${name} is not available in CodeMode.`, node)
|
||||
if (name === "random") return Math.random()
|
||||
const nums = args.map((arg) => {
|
||||
if (typeof arg !== "number") throw new InterpreterRuntimeError(`Math.${name} expects number arguments.`, node)
|
||||
return arg
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
export const numberMethods = new Set(["toFixed", "toPrecision", "toExponential", "toString", "valueOf"])
|
||||
export const numberMethods = new Set(["toFixed", "toPrecision", "toExponential", "toString"])
|
||||
|
||||
export const numberConstants = new Set([
|
||||
"MAX_SAFE_INTEGER",
|
||||
"MIN_SAFE_INTEGER",
|
||||
"MAX_VALUE",
|
||||
"MIN_VALUE",
|
||||
"EPSILON",
|
||||
"NaN",
|
||||
"POSITIVE_INFINITY",
|
||||
"NEGATIVE_INFINITY",
|
||||
])
|
||||
export const numberConstants = new Set(["MAX_SAFE_INTEGER", "MIN_SAFE_INTEGER", "MAX_VALUE", "MIN_VALUE", "EPSILON"])
|
||||
|
||||
export const numberStatics = new Set(["isInteger", "isFinite", "isNaN", "isSafeInteger", "parseInt", "parseFloat"])
|
||||
|
||||
@@ -41,9 +32,6 @@ export const invokeNumberMethod = (value: number, name: string, args: Array<unkn
|
||||
result = value.toString(radix)
|
||||
break
|
||||
}
|
||||
case "valueOf":
|
||||
result = value
|
||||
break
|
||||
default:
|
||||
throw new InterpreterRuntimeError(`Number method '${name}' is not available in CodeMode.`, node)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js"
|
||||
import { isBlockedMember } from "../tool-runtime.js"
|
||||
import { isSandboxValue, SandboxMap, SandboxPromise, SandboxSet, SandboxURLSearchParams } from "../values.js"
|
||||
import { isSandboxValue, SandboxMap, SandboxSet, SandboxURLSearchParams } from "../values.js"
|
||||
import { boundedData, coerceToString } from "./value.js"
|
||||
|
||||
export const objectStatics = new Set(["keys", "values", "entries", "hasOwn", "assign", "fromEntries"])
|
||||
@@ -10,23 +10,13 @@ export const invokeObjectMethod = (name: string, args: Array<unknown>, node: Ast
|
||||
if (!objectStatics.has(name)) throw new InterpreterRuntimeError(`Object.${name} is not available in CodeMode.`, node)
|
||||
const requireObject = (): Record<string, unknown> => {
|
||||
const input = args[0]
|
||||
const value = boundedData(args[0], `Object.${name} input`)
|
||||
if (Array.isArray(input)) return input as unknown as Record<string, unknown>
|
||||
if (isSandboxValue(input)) return {}
|
||||
if (input instanceof SandboxPromise) {
|
||||
throw new InterpreterRuntimeError(
|
||||
`Object.${name} received an un-awaited Promise; await it before inspecting the result.`,
|
||||
node,
|
||||
"InvalidDataValue",
|
||||
)
|
||||
if (isSandboxValue(value)) return {}
|
||||
if (value === null || typeof value !== "object") {
|
||||
throw new InterpreterRuntimeError(`Object.${name} expects a data object or array.`, node)
|
||||
}
|
||||
if (input === null || typeof input !== "object") {
|
||||
throw new InterpreterRuntimeError(`Object.${name} expects a data object or array.`, node, "InvalidDataValue")
|
||||
}
|
||||
const prototype = Object.getPrototypeOf(input)
|
||||
if (prototype !== null && prototype !== Object.prototype) {
|
||||
throw new InterpreterRuntimeError(`Object.${name} expects a data object or array.`, node, "InvalidDataValue")
|
||||
}
|
||||
return input as Record<string, unknown>
|
||||
return value as Record<string, unknown>
|
||||
}
|
||||
const guardedSet = (out: Record<string, unknown>, key: string, item: unknown): void => {
|
||||
if (isBlockedMember(key)) throw new InterpreterRuntimeError(`Property '${key}' is not available in CodeMode.`, node)
|
||||
@@ -38,8 +28,15 @@ export const invokeObjectMethod = (name: string, args: Array<unknown>, node: Ast
|
||||
guardedSet(out, coerceToString(key), item)
|
||||
}
|
||||
switch (name) {
|
||||
case "keys":
|
||||
return Object.keys(requireObject())
|
||||
case "keys": {
|
||||
const value = boundedData(args[0], "Object.keys input")
|
||||
if (isSandboxValue(value)) return []
|
||||
if (Array.isArray(value)) return Object.keys(value)
|
||||
if (value === null || typeof value !== "object") {
|
||||
throw new InterpreterRuntimeError("Object.keys expects a data object or array.", node)
|
||||
}
|
||||
return Object.keys(value)
|
||||
}
|
||||
case "values":
|
||||
return Object.values(requireObject())
|
||||
case "entries":
|
||||
|
||||
@@ -617,7 +617,6 @@ export const prepare = <R>(tools: HostTools<R>, catalogBudget = defaultCatalogBu
|
||||
"",
|
||||
"Use common JavaScript data operations, functions, control flow, selected standard-library methods, and awaited tool calls. Built-ins include Date, RegExp, Map, Set, URL, URLSearchParams, and URI encoding helpers.",
|
||||
"Modules/imports, classes, generators, timers, fetch, eval, prototype access, unlisted methods, and promise chaining are unavailable. Use Code Mode tools for external operations. Use await with try/catch.",
|
||||
"Prefer explicit `return`; otherwise only the final top-level expression becomes the result.",
|
||||
"Dates and URLs serialize to strings at data boundaries; Map/Set/RegExp/URLSearchParams serialize to `{}`.",
|
||||
]
|
||||
|
||||
|
||||
@@ -658,9 +658,6 @@ describe("CodeMode public contract", () => {
|
||||
expect(instructions).toContain("URL, URLSearchParams, and URI encoding helpers")
|
||||
expect(instructions).not.toContain("host globals")
|
||||
expect(instructions).toContain("Use Code Mode tools for external operations")
|
||||
expect(instructions).toContain(
|
||||
"Prefer explicit `return`; otherwise only the final top-level expression becomes the result.",
|
||||
)
|
||||
expect(instructions).toContain(
|
||||
"Dates and URLs serialize to strings at data boundaries; Map/Set/RegExp/URLSearchParams serialize to `{}`.",
|
||||
)
|
||||
@@ -1084,24 +1081,6 @@ describe("CodeMode public contract", () => {
|
||||
expect(Schema.decodeUnknownSync(CodeMode.Result)(JSON.parse(JSON.stringify(result)))).toStrictEqual(result)
|
||||
})
|
||||
|
||||
test("returns the final top-level expression when return is omitted", async () => {
|
||||
const result = await Effect.runPromise(CodeMode.execute({ code: `1; 2` }))
|
||||
|
||||
expect(result).toStrictEqual({ ok: true, value: 2, toolCalls: [] })
|
||||
})
|
||||
|
||||
test("does not implicitly return expressions nested in control flow", async () => {
|
||||
const result = await Effect.runPromise(CodeMode.execute({ code: `if (true) { 2 }` }))
|
||||
|
||||
expect(result).toStrictEqual({ ok: true, value: null, toolCalls: [] })
|
||||
})
|
||||
|
||||
test("returns null when the final top-level statement is not an expression", async () => {
|
||||
const result = await Effect.runPromise(CodeMode.execute({ code: `1; const value = 2` }))
|
||||
|
||||
expect(result).toStrictEqual({ ok: true, value: null, toolCalls: [] })
|
||||
})
|
||||
|
||||
test("rejects invalid configuration and discovery limits", async () => {
|
||||
expect(() => CodeMode.execute({ code: "return 1", limits: { timeoutMs: 0 } })).toThrow(RangeError)
|
||||
expect(() => CodeMode.execute({ code: "return 1", limits: { timeoutMs: Number.POSITIVE_INFINITY } })).toThrow(
|
||||
|
||||
@@ -245,12 +245,6 @@ describe("promises at data boundaries", () => {
|
||||
expect(diagnostic.message).toContain("await tools.ns.tool(...)")
|
||||
})
|
||||
|
||||
test("collection helpers do not let un-awaited promises cross the result boundary", async () => {
|
||||
const diagnostic = await error(`return Array.from([Promise.resolve(1)])`)
|
||||
expect(diagnostic.kind).toBe("InvalidDataValue")
|
||||
expect(diagnostic.message).toContain("un-awaited Promise")
|
||||
})
|
||||
|
||||
test("passing an un-awaited promise as a tool argument is a clear diagnostic", async () => {
|
||||
const diagnostic = await error(`return await tools.host.sleepy({ id: tools.host.sleepy({ id: 1 }) })`)
|
||||
expect(diagnostic.kind).toBe("InvalidDataValue")
|
||||
|
||||
@@ -19,28 +19,6 @@ const error = async (code: string) => {
|
||||
return result.error
|
||||
}
|
||||
|
||||
describe("Number and Math", () => {
|
||||
test("Math.random returns a number in [0, 1)", async () => {
|
||||
expect(await value(`const n = Math.random(); return typeof n === "number" && n >= 0 && n < 1`)).toBe(true)
|
||||
})
|
||||
|
||||
test("Number exposes native non-finite constants", async () => {
|
||||
expect(
|
||||
await value(
|
||||
`return [Number.isNaN(Number.NaN), Number.POSITIVE_INFINITY === Infinity, Number.NEGATIVE_INFINITY === -Infinity]`,
|
||||
),
|
||||
).toEqual([true, true, true])
|
||||
})
|
||||
|
||||
test("Number valueOf returns its primitive receiver", async () => {
|
||||
expect(await value(`return (42).valueOf()`)).toBe(42)
|
||||
})
|
||||
|
||||
test("Number valueOf does not enable boxed numbers", async () => {
|
||||
expect((await error(`return new Number(42)`)).kind).toBe("UnsupportedSyntax")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Date", () => {
|
||||
test("Date.now() returns a number", async () => {
|
||||
expect(await value(`return typeof Date.now()`)).toBe("number")
|
||||
@@ -773,43 +751,6 @@ describe("sandbox values at intra-sandbox checkpoints", () => {
|
||||
)
|
||||
})
|
||||
|
||||
test("Object.values/entries preserve nested object identity", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const child = { selected: false }
|
||||
const rows = { a: child }
|
||||
Object.values(rows)[0].selected = true
|
||||
return child.selected
|
||||
`),
|
||||
).toBe(true)
|
||||
expect(
|
||||
await value(`
|
||||
const child = { selected: false }
|
||||
const rows = { a: child }
|
||||
Object.entries(rows)[0][1].selected = true
|
||||
return child.selected
|
||||
`),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test("Object enumeration preserves promises and callable references", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const pending = Promise.resolve(1)
|
||||
const source = { pending }
|
||||
return [Object.keys(source), Object.hasOwn(source, "pending"), await Object.values(source)[0], await Object.entries(source)[0][1]]
|
||||
`),
|
||||
).toEqual([["pending"], true, 1, 1])
|
||||
expect(await value(`return Object.values({ max: Math.max })[0](1, 2)`)).toBe(2)
|
||||
})
|
||||
|
||||
test("Object enumeration rejects invalid receivers and gives promises an await hint", async () => {
|
||||
const diagnostic = await error(`return Object.keys(Promise.resolve({ a: 1 }))`)
|
||||
expect(diagnostic.kind).toBe("InvalidDataValue")
|
||||
expect(diagnostic.message).toContain("await")
|
||||
expect((await error(`return Object.keys(Math)`)).kind).toBe("InvalidDataValue")
|
||||
})
|
||||
|
||||
test("Object.assign keeps Maps usable", async () => {
|
||||
expect(await value(`const merged = Object.assign({}, { m: new Map([["a", 1]]) }); return merged.m.get("a")`)).toBe(
|
||||
1,
|
||||
@@ -832,53 +773,6 @@ describe("sandbox values at intra-sandbox checkpoints", () => {
|
||||
expect(await value(`return Array.from([new Date(5)])[0].getTime()`)).toBe(5)
|
||||
})
|
||||
|
||||
test("Array.from and Array.of preserve nested object identity", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const child = { selected: false }
|
||||
Array.from([child])[0].selected = true
|
||||
return child.selected
|
||||
`),
|
||||
).toBe(true)
|
||||
expect(
|
||||
await value(`
|
||||
const child = { selected: false }
|
||||
Array.of(child)[0].selected = true
|
||||
return child.selected
|
||||
`),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test("Array.from and Array.of preserve promises and callable references", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const pending = Promise.resolve(1)
|
||||
return [await Array.from([pending])[0], await Array.of(pending)[0]]
|
||||
`),
|
||||
).toEqual([1, 1])
|
||||
expect(await value(`return [Array.from([Math.max])[0](1, 2), Array.of(Math.max)[0](3, 4)]`)).toEqual([2, 4])
|
||||
})
|
||||
|
||||
test("Array.from preserves identity across supported collection shapes", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const child = { selected: false }
|
||||
const fromArrayLike = Array.from({ 0: child, length: 1 })
|
||||
const fromMap = Array.from(new Map([["child", child]]))
|
||||
const fromSet = Array.from(new Set([child]))
|
||||
fromArrayLike[0].selected = true
|
||||
return [fromMap[0][1] === child, fromSet[0] === child, child.selected]
|
||||
`),
|
||||
).toEqual([true, true, true])
|
||||
})
|
||||
|
||||
test("Array.from rejects invalid receivers and gives promises an await hint", async () => {
|
||||
const diagnostic = await error(`return Array.from(Promise.resolve([1]))`)
|
||||
expect(diagnostic.kind).toBe("InvalidDataValue")
|
||||
expect(diagnostic.message).toContain("await")
|
||||
expect((await error(`return Array.from(() => 1)`)).kind).toBe("InvalidDataValue")
|
||||
})
|
||||
|
||||
test("regexes stay callable through Object.values", async () => {
|
||||
expect(await value(`return Object.values({ r: /ab+/ })[0].test("abb")`)).toBe(true)
|
||||
})
|
||||
|
||||
@@ -150,44 +150,31 @@ export type LocationError = LayerNode.Error<typeof locationServices>
|
||||
export function buildLocationServiceMap(
|
||||
replacements: LayerNode.Replacements = [],
|
||||
): Layer.Layer<LocationServiceMap.Service> {
|
||||
// Structural Equal is own-key-set sensitive, so `{ directory }` (schema-decoded
|
||||
// payloads omit optional keys) and `{ directory, workspaceID: undefined }` are
|
||||
// different RcMap keys. The RcMap caches by the raw key before the build
|
||||
// callback runs, so canonicalize at the map boundary to the key-present shape.
|
||||
const canonical = (ref: Location.Ref) => Location.Ref.make({ directory: ref.directory, workspaceID: ref.workspaceID })
|
||||
return Layer.effect(
|
||||
LocationServiceMap.Service,
|
||||
Effect.map(
|
||||
LayerMap.make(
|
||||
(ref: Location.Ref) => {
|
||||
const startedAt = performance.now()
|
||||
const allReplacements = replacements.concat([[Location.node, Location.boundNode(ref)]])
|
||||
// Apply replacements during hoist, not afterward: replacements can
|
||||
// introduce new tagged dependencies (Location.boundNode depends on
|
||||
// Project), and the hoist walk is the only pass that can still slice
|
||||
// those back out.
|
||||
const location = LayerNode.hoist(locationServices, Node.tags.values.global, allReplacements)
|
||||
LayerMap.make(
|
||||
(ref: Location.Ref) => {
|
||||
const startedAt = performance.now()
|
||||
const allReplacements = replacements.concat([[Location.node, Location.boundNode(ref)]])
|
||||
// Apply replacements during hoist, not afterward: replacements can
|
||||
// introduce new tagged dependencies (Location.boundNode depends on
|
||||
// Project), and the hoist walk is the only pass that can still slice
|
||||
// those back out.
|
||||
const location = LayerNode.hoist(locationServices, Node.tags.values.global, allReplacements)
|
||||
|
||||
return LayerNode.compile(location.node).pipe(
|
||||
Layer.fresh,
|
||||
Layer.tap(() =>
|
||||
Effect.logInfo("location services booted", {
|
||||
directory: ref.directory,
|
||||
workspaceID: ref.workspaceID,
|
||||
durationMs: Math.round(performance.now() - startedAt),
|
||||
}),
|
||||
),
|
||||
Layer.provide(LayerNode.compile(location.hoisted)),
|
||||
)
|
||||
},
|
||||
{ idleTimeToLive: "60 minutes" },
|
||||
),
|
||||
(inner) => ({
|
||||
...inner,
|
||||
get: (ref: Location.Ref) => inner.get(canonical(ref)),
|
||||
contextEffect: (ref: Location.Ref) => inner.contextEffect(canonical(ref)),
|
||||
invalidate: (ref: Location.Ref) => inner.invalidate(canonical(ref)),
|
||||
}),
|
||||
return LayerNode.compile(location.node).pipe(
|
||||
Layer.fresh,
|
||||
Layer.tap(() =>
|
||||
Effect.logInfo("location services booted", {
|
||||
directory: ref.directory,
|
||||
workspaceID: ref.workspaceID,
|
||||
durationMs: Math.round(performance.now() - startedAt),
|
||||
}),
|
||||
),
|
||||
Layer.provide(LayerNode.compile(location.hoisted)),
|
||||
)
|
||||
},
|
||||
{ idleTimeToLive: "60 minutes" },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
+16
-118
@@ -3,7 +3,7 @@ export * as MCPClient from "./client"
|
||||
import path from "node:path"
|
||||
import { execFile } from "node:child_process"
|
||||
import { pathToFileURL } from "node:url"
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
|
||||
import { Client, type ClientOptions } from "@modelcontextprotocol/sdk/client/index.js"
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
|
||||
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
|
||||
import { UnauthorizedError, type OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js"
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
ListToolsResultSchema,
|
||||
PromptListChangedNotificationSchema,
|
||||
PromptSchema,
|
||||
ResourceListChangedNotificationSchema,
|
||||
type LoggingMessageNotification,
|
||||
LoggingMessageNotificationSchema,
|
||||
ToolListChangedNotificationSchema,
|
||||
@@ -69,13 +68,11 @@ export interface ToolDefinition {
|
||||
export interface PromptDefinition {
|
||||
readonly name: string
|
||||
readonly description: string | undefined
|
||||
readonly arguments:
|
||||
| ReadonlyArray<{
|
||||
readonly name: string
|
||||
readonly description: string | undefined
|
||||
readonly required: boolean | undefined
|
||||
}>
|
||||
| undefined
|
||||
readonly arguments: ReadonlyArray<{
|
||||
readonly name: string
|
||||
readonly description: string | undefined
|
||||
readonly required: boolean | undefined
|
||||
}> | undefined
|
||||
}
|
||||
|
||||
export interface PromptMessage {
|
||||
@@ -87,28 +84,6 @@ export interface PromptResult {
|
||||
readonly messages: ReadonlyArray<PromptMessage>
|
||||
}
|
||||
|
||||
export interface ResourceDefinition {
|
||||
readonly name: string
|
||||
readonly uri: string
|
||||
readonly description: string | undefined
|
||||
readonly mimeType: string | undefined
|
||||
}
|
||||
|
||||
export interface ResourceTemplateDefinition {
|
||||
readonly name: string
|
||||
readonly uriTemplate: string
|
||||
readonly description: string | undefined
|
||||
readonly mimeType: string | undefined
|
||||
}
|
||||
|
||||
export type ResourceContentPart =
|
||||
| { readonly type: "text"; readonly uri: string; readonly text: string; readonly mimeType: string | undefined }
|
||||
| { readonly type: "blob"; readonly uri: string; readonly blob: string; readonly mimeType: string | undefined }
|
||||
|
||||
export interface ReadResourceResult {
|
||||
readonly contents: ReadonlyArray<ResourceContentPart>
|
||||
}
|
||||
|
||||
export type CallToolContent =
|
||||
| { readonly type: "text"; readonly text: string }
|
||||
| { readonly type: "media"; readonly data: string; readonly mimeType: string }
|
||||
@@ -149,12 +124,6 @@ export interface Connection {
|
||||
readonly tools: () => Effect.Effect<ToolDefinition[], Error>
|
||||
/** Lists the server's prompts; returns [] when the server doesn't advertise prompt support, fails on a transport error. */
|
||||
readonly prompts: () => Effect.Effect<PromptDefinition[], Error>
|
||||
/** Lists the server's resources; returns [] when the server doesn't advertise resource support. */
|
||||
readonly resources: () => Effect.Effect<ResourceDefinition[], Error>
|
||||
/** Lists the server's resource templates; returns [] when the server doesn't advertise resource support. */
|
||||
readonly resourceTemplates: () => Effect.Effect<ResourceTemplateDefinition[], Error>
|
||||
/** Reads one resource; returns undefined when the server doesn't advertise resource support. */
|
||||
readonly readResource: (input: { readonly uri: string }) => Effect.Effect<ReadResourceResult | undefined, Error>
|
||||
/** Invokes a prompt on the server. Interruption aborts the in-flight request. */
|
||||
readonly prompt: (input: {
|
||||
readonly name: string
|
||||
@@ -172,8 +141,6 @@ export interface Connection {
|
||||
readonly onToolsChanged: (callback: () => void) => void
|
||||
/** Registers a callback fired when the server announces its prompt list changed; no-op if unsupported. */
|
||||
readonly onPromptsChanged: (callback: () => void) => void
|
||||
/** Registers a callback fired when the server announces its resource catalog changed. */
|
||||
readonly onResourcesChanged: (callback: () => void) => void
|
||||
}
|
||||
|
||||
/** Connects an MCP server; closing the calling scope tears down the transport and any spawned process. */
|
||||
@@ -201,8 +168,7 @@ export const connect = Effect.fnUntraced(function* (
|
||||
},
|
||||
})
|
||||
}
|
||||
if (!URL.canParse(config.url))
|
||||
return yield* new ConnectError({ server, message: `Invalid MCP URL for "${server}"` })
|
||||
if (!URL.canParse(config.url)) return yield* new ConnectError({ server, message: `Invalid MCP URL for "${server}"` })
|
||||
return new StreamableHTTPClientTransport(new URL(config.url), {
|
||||
requestInit: config.headers ? { headers: config.headers } : undefined,
|
||||
authProvider,
|
||||
@@ -236,7 +202,10 @@ export const connect = Effect.fnUntraced(function* (
|
||||
}).pipe(Effect.exit)
|
||||
if (Exit.isSuccess(exit)) {
|
||||
yield* Effect.addFinalizer(() =>
|
||||
cleanupStdioDescendants(transport).pipe(Effect.andThen(Effect.promise(() => client.close())), Effect.ignore),
|
||||
cleanupStdioDescendants(transport).pipe(
|
||||
Effect.andThen(Effect.promise(() => client.close())),
|
||||
Effect.ignore,
|
||||
),
|
||||
)
|
||||
const catalogTimeout = config.timeout?.catalog ?? DEFAULT_CATALOG_TIMEOUT
|
||||
const executionTimeout = config.timeout?.execution ?? DEFAULT_EXECUTION_TIMEOUT
|
||||
@@ -288,9 +257,7 @@ export const connect = Effect.fnUntraced(function* (
|
||||
),
|
||||
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
|
||||
}).pipe(
|
||||
Effect.tapError((error) =>
|
||||
Effect.logWarning("failed to list MCP prompts", { server, error: error.message }),
|
||||
),
|
||||
Effect.tapError((error) => Effect.logWarning("failed to list MCP prompts", { server, error: error.message })),
|
||||
)
|
||||
return prompts.map((prompt) => ({
|
||||
name: prompt.name,
|
||||
@@ -302,74 +269,6 @@ export const connect = Effect.fnUntraced(function* (
|
||||
})),
|
||||
}))
|
||||
}),
|
||||
resources: () =>
|
||||
Effect.gen(function* () {
|
||||
if (!client.getServerCapabilities()?.resources) return []
|
||||
const resources = yield* Effect.tryPromise({
|
||||
try: () =>
|
||||
paginate(
|
||||
(cursor) =>
|
||||
client.listResources(cursor === undefined ? undefined : { cursor }, { timeout: catalogTimeout }),
|
||||
(result) => result.resources,
|
||||
),
|
||||
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
|
||||
}).pipe(
|
||||
Effect.tapError((error) =>
|
||||
Effect.logWarning("failed to list MCP resources", { server, error: error.message }),
|
||||
),
|
||||
)
|
||||
return resources.map((resource) => ({
|
||||
name: resource.name,
|
||||
uri: resource.uri,
|
||||
description: resource.description,
|
||||
mimeType: resource.mimeType,
|
||||
}))
|
||||
}),
|
||||
resourceTemplates: () =>
|
||||
Effect.gen(function* () {
|
||||
if (!client.getServerCapabilities()?.resources) return []
|
||||
const templates = yield* Effect.tryPromise({
|
||||
try: () =>
|
||||
paginate(
|
||||
(cursor) =>
|
||||
client.listResourceTemplates(cursor === undefined ? undefined : { cursor }, {
|
||||
timeout: catalogTimeout,
|
||||
}),
|
||||
(result) => result.resourceTemplates,
|
||||
),
|
||||
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
|
||||
}).pipe(
|
||||
Effect.tapError((error) =>
|
||||
Effect.logWarning("failed to list MCP resource templates", { server, error: error.message }),
|
||||
),
|
||||
)
|
||||
return templates.map((template) => ({
|
||||
name: template.name,
|
||||
uriTemplate: template.uriTemplate,
|
||||
description: template.description,
|
||||
mimeType: template.mimeType,
|
||||
}))
|
||||
}),
|
||||
readResource: (input) =>
|
||||
Effect.gen(function* () {
|
||||
if (!client.getServerCapabilities()?.resources) return undefined
|
||||
const result = yield* Effect.tryPromise({
|
||||
try: (signal) => client.readResource({ uri: input.uri }, { signal, timeout: executionTimeout }),
|
||||
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
|
||||
}).pipe(
|
||||
Effect.tapError((error) =>
|
||||
Effect.logWarning("failed to read MCP resource", { server, uri: input.uri, error: error.message }),
|
||||
),
|
||||
)
|
||||
return {
|
||||
contents: result.contents.map(
|
||||
(part): ResourceContentPart =>
|
||||
"text" in part
|
||||
? { type: "text", uri: part.uri, text: part.text, mimeType: part.mimeType }
|
||||
: { type: "blob", uri: part.uri, blob: part.blob, mimeType: part.mimeType },
|
||||
),
|
||||
}
|
||||
}),
|
||||
prompt: (input) =>
|
||||
Effect.tryPromise({
|
||||
try: (signal) =>
|
||||
@@ -429,14 +328,13 @@ export const connect = Effect.fnUntraced(function* (
|
||||
if (!client.getServerCapabilities()?.prompts?.listChanged) return
|
||||
client.setNotificationHandler(PromptListChangedNotificationSchema, async () => callback())
|
||||
},
|
||||
onResourcesChanged: (callback) => {
|
||||
if (!client.getServerCapabilities()?.resources?.listChanged) return
|
||||
client.setNotificationHandler(ResourceListChangedNotificationSchema, async () => callback())
|
||||
},
|
||||
} satisfies Connection
|
||||
}
|
||||
|
||||
yield* cleanupStdioDescendants(transport).pipe(Effect.andThen(Effect.promise(() => transport.close())), Effect.ignore)
|
||||
yield* cleanupStdioDescendants(transport).pipe(
|
||||
Effect.andThen(Effect.promise(() => transport.close())),
|
||||
Effect.ignore,
|
||||
)
|
||||
const error = Cause.squash(exit.cause)
|
||||
if (error instanceof UnauthorizedError) return yield* new NeedsAuthError({ server })
|
||||
return yield* new ConnectError({ server, message: error instanceof Error ? error.message : String(error) })
|
||||
|
||||
@@ -83,16 +83,48 @@ export class PromptResult extends Schema.Class<PromptResult>("MCP.PromptResult")
|
||||
messages: Schema.Array(PromptMessage),
|
||||
}) {}
|
||||
|
||||
export const Resource = Mcp.Resource
|
||||
export type Resource = Mcp.Resource
|
||||
export const ResourceTemplate = Mcp.ResourceTemplate
|
||||
export type ResourceTemplate = Mcp.ResourceTemplate
|
||||
export const ResourceCatalog = Mcp.ResourceCatalog
|
||||
export type ResourceCatalog = Mcp.ResourceCatalog
|
||||
export const ResourceContentPart = Mcp.ResourceContentPart
|
||||
export type ResourceContentPart = Mcp.ResourceContentPart
|
||||
export const ResourceContent = Mcp.ResourceContent
|
||||
export type ResourceContent = Mcp.ResourceContent
|
||||
export class Resource extends Schema.Class<Resource>("MCP.Resource")({
|
||||
server: ServerName,
|
||||
name: Schema.String,
|
||||
uri: Schema.String,
|
||||
description: Schema.String.pipe(Schema.optional),
|
||||
mimeType: Schema.String.pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
export class ResourceTemplate extends Schema.Class<ResourceTemplate>("MCP.ResourceTemplate")({
|
||||
server: ServerName,
|
||||
name: Schema.String,
|
||||
uriTemplate: Schema.String,
|
||||
description: Schema.String.pipe(Schema.optional),
|
||||
mimeType: Schema.String.pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
export class ResourceCatalog extends Schema.Class<ResourceCatalog>("MCP.ResourceCatalog")({
|
||||
resources: Schema.Array(Resource),
|
||||
templates: Schema.Array(ResourceTemplate),
|
||||
}) {}
|
||||
|
||||
export const ResourceContentPart = Schema.Union([
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("text"),
|
||||
uri: Schema.String,
|
||||
text: Schema.String,
|
||||
mimeType: Schema.String.pipe(Schema.optional),
|
||||
}),
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("blob"),
|
||||
uri: Schema.String,
|
||||
blob: Schema.String,
|
||||
mimeType: Schema.String.pipe(Schema.optional),
|
||||
}),
|
||||
]).pipe(Schema.toTaggedUnion("type"))
|
||||
export type ResourceContentPart = typeof ResourceContentPart.Type
|
||||
|
||||
export class ResourceContent extends Schema.Class<ResourceContent>("MCP.ResourceContent")({
|
||||
server: ServerName,
|
||||
uri: Schema.String,
|
||||
contents: Schema.Array(ResourceContentPart),
|
||||
}) {}
|
||||
|
||||
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("MCP.NotFoundError", {
|
||||
server: ServerName,
|
||||
@@ -383,24 +415,6 @@ export const layer = Layer.effect(
|
||||
),
|
||||
})
|
||||
|
||||
const toResource = (server: ServerName, def: MCPClient.ResourceDefinition) =>
|
||||
Resource.make({
|
||||
server,
|
||||
name: def.name,
|
||||
uri: def.uri,
|
||||
description: def.description,
|
||||
mimeType: def.mimeType,
|
||||
})
|
||||
|
||||
const toResourceTemplate = (server: ServerName, def: MCPClient.ResourceTemplateDefinition) =>
|
||||
ResourceTemplate.make({
|
||||
server,
|
||||
name: def.name,
|
||||
uriTemplate: def.uriTemplate,
|
||||
description: def.description,
|
||||
mimeType: def.mimeType,
|
||||
})
|
||||
|
||||
const refreshTools = (name: ServerName, entry: ServerEntry, connection: MCPClient.Connection) =>
|
||||
connection.tools().pipe(
|
||||
Effect.map((defs) => {
|
||||
@@ -429,7 +443,6 @@ export const layer = Layer.effect(
|
||||
entry.prompts = undefined
|
||||
entry.status = { status: "failed", error: "Connection closed" }
|
||||
fork(events.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore))
|
||||
fork(events.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore))
|
||||
fork(events.publish(Command.Event.Updated, {}).pipe(Effect.ignore))
|
||||
fork(events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore))
|
||||
})
|
||||
@@ -445,10 +458,6 @@ export const layer = Layer.effect(
|
||||
connection.onPromptsChanged(() => {
|
||||
fork(refreshPrompts(name, entry, connection).pipe(Effect.ignore))
|
||||
})
|
||||
connection.onResourcesChanged(() => {
|
||||
if (entry.client !== connection) return
|
||||
fork(events.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore))
|
||||
})
|
||||
}
|
||||
|
||||
const serverLog = (server: ServerName, message: MCPClient.LogMessage) => {
|
||||
@@ -492,7 +501,6 @@ export const layer = Layer.effect(
|
||||
// after the initial registration sweep and emits no list-changed notification would otherwise
|
||||
// stay invisible to the model.
|
||||
yield* events.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore)
|
||||
yield* events.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore)
|
||||
yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
|
||||
fork(refreshPrompts(name, entry, result.value.connection).pipe(Effect.ignore))
|
||||
return
|
||||
@@ -549,6 +557,11 @@ export const layer = Layer.effect(
|
||||
concurrency: "unbounded",
|
||||
discard: true,
|
||||
})
|
||||
const gate = Effect.fnUntraced(function* (server: ServerName | string) {
|
||||
const target = yield* requireServer(server)
|
||||
yield* Deferred.await(target.entry.startup)
|
||||
})
|
||||
|
||||
return Service.of({
|
||||
servers: Effect.fn("MCP.servers")(function* () {
|
||||
const entries = Array.from(runtime).toSorted(([a], [b]) => a.localeCompare(b))
|
||||
@@ -624,54 +637,11 @@ export const layer = Layer.effect(
|
||||
}),
|
||||
resourceCatalog: Effect.fn("MCP.resourceCatalog")(function* () {
|
||||
yield* whenAllReady
|
||||
const catalogs = yield* Effect.forEach(
|
||||
Array.from(runtime),
|
||||
([name, entry]) => {
|
||||
if (!entry.client) return Effect.succeed({ resources: [], templates: [] })
|
||||
return Effect.all(
|
||||
{
|
||||
resources: entry.client.resources().pipe(Effect.catch(() => Effect.succeed([]))),
|
||||
templates: entry.client.resourceTemplates().pipe(Effect.catch(() => Effect.succeed([]))),
|
||||
},
|
||||
{ concurrency: "unbounded" },
|
||||
).pipe(
|
||||
Effect.map((catalog) => ({
|
||||
resources: catalog.resources.map((def) => toResource(name, def)),
|
||||
templates: catalog.templates.map((def) => toResourceTemplate(name, def)),
|
||||
})),
|
||||
)
|
||||
},
|
||||
{ concurrency: "unbounded" },
|
||||
)
|
||||
return ResourceCatalog.make({
|
||||
resources: catalogs
|
||||
.flatMap((catalog) => catalog.resources)
|
||||
.toSorted(
|
||||
(a, b) => a.server.localeCompare(b.server) || a.name.localeCompare(b.name) || a.uri.localeCompare(b.uri),
|
||||
),
|
||||
templates: catalogs
|
||||
.flatMap((catalog) => catalog.templates)
|
||||
.toSorted(
|
||||
(a, b) =>
|
||||
a.server.localeCompare(b.server) ||
|
||||
a.name.localeCompare(b.name) ||
|
||||
a.uriTemplate.localeCompare(b.uriTemplate),
|
||||
),
|
||||
})
|
||||
return new ResourceCatalog({ resources: [], templates: [] })
|
||||
}),
|
||||
readResource: Effect.fn("MCP.readResource")(function* (input) {
|
||||
const target = yield* requireServer(input.server)
|
||||
yield* Deferred.await(target.entry.startup)
|
||||
if (!target.entry.client) return undefined
|
||||
const result = yield* target.entry.client
|
||||
.readResource({ uri: input.uri })
|
||||
.pipe(Effect.catch(() => Effect.succeed(undefined)))
|
||||
if (!result) return undefined
|
||||
return ResourceContent.make({
|
||||
server: target.name,
|
||||
uri: input.uri,
|
||||
contents: result.contents,
|
||||
})
|
||||
yield* gate(input.server)
|
||||
return undefined
|
||||
}),
|
||||
})
|
||||
}),
|
||||
|
||||
@@ -7,6 +7,14 @@ import { EventV2 } from "../event"
|
||||
|
||||
export const Updated = EventV2.ephemeral({ type: "sdk.plugin.updated", schema: {} })
|
||||
|
||||
export interface Store {
|
||||
readonly plugins: Map<string, Plugin>
|
||||
}
|
||||
|
||||
export const makeStore = (): Store => ({ plugins: new Map() })
|
||||
|
||||
const defaultStore = makeStore()
|
||||
|
||||
/**
|
||||
* Holds the plugins an embedder (the `@opencode-ai/sdk-next` host) contributes,
|
||||
* so `PluginSupervisor` can add them on every Location boot through the ordinary
|
||||
@@ -14,9 +22,10 @@ export const Updated = EventV2.ephemeral({ type: "sdk.plugin.updated", schema: {
|
||||
* config. Registration publishes an unlocated update so every booted Location
|
||||
* reloads its plugin generation from the shared store.
|
||||
*
|
||||
* Each host-global layer owns one private store. Location graphs reuse that
|
||||
* layer through Effect's memoization, so separate hosts remain isolated while
|
||||
* every Location in one host sees the same registrations.
|
||||
* The store is shared explicitly between the SDK construction graph and the
|
||||
* embedded route graph because `LocationServiceMap` builds Location layers lazily
|
||||
* in a nested graph. Each embedded SDK creates its own store, so instances do not
|
||||
* see each other's contributions.
|
||||
*/
|
||||
export interface Interface {
|
||||
readonly register: (plugin: Plugin) => Effect.Effect<void>
|
||||
@@ -25,19 +34,26 @@ export interface Interface {
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/SdkPlugins") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const events = yield* EventV2.Service
|
||||
const plugins = new Map<string, Plugin>()
|
||||
return Service.of({
|
||||
register: (plugin) =>
|
||||
export const layerWithStore = (store: Store) =>
|
||||
Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const events = yield* EventV2.Service
|
||||
yield* Effect.addFinalizer(() =>
|
||||
Effect.sync(() => {
|
||||
plugins.set(plugin.id, plugin)
|
||||
}).pipe(Effect.andThen(events.publish(Updated, {})), Effect.asVoid),
|
||||
all: () => [...plugins.values()],
|
||||
})
|
||||
}),
|
||||
)
|
||||
store.plugins.clear()
|
||||
}),
|
||||
)
|
||||
return Service.of({
|
||||
register: (plugin) =>
|
||||
Effect.sync(() => {
|
||||
store.plugins.set(plugin.id, plugin)
|
||||
}).pipe(Effect.andThen(events.publish(Updated, {})), Effect.asVoid),
|
||||
all: () => [...store.plugins.values()],
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
export const layer = layerWithStore(defaultStore)
|
||||
|
||||
export const node = makeGlobalNode({ service: Service, layer, deps: [EventV2.node] })
|
||||
|
||||
+37
-158
@@ -11,7 +11,6 @@ import { Location } from "./location"
|
||||
import { SessionMessage } from "./session/message"
|
||||
import { Base64, FileAttachment, Prompt } from "@opencode-ai/schema/prompt"
|
||||
import { PromptInput } from "@opencode-ai/schema/prompt-input"
|
||||
import { Mcp } from "@opencode-ai/schema/mcp"
|
||||
import { EventV2 } from "./event"
|
||||
import { Database } from "./database/database"
|
||||
import { SessionProjector } from "./session/projector"
|
||||
@@ -46,7 +45,6 @@ import { Shell } from "./shell"
|
||||
import { Shell as ShellSchema } from "@opencode-ai/schema/shell"
|
||||
import { KeyedMutex } from "./effect/keyed-mutex"
|
||||
import { fileURLToPath } from "url"
|
||||
import { MCP } from "./mcp/index"
|
||||
|
||||
export const RevertState = Revert.State
|
||||
export type RevertState = Revert.State
|
||||
@@ -245,7 +243,6 @@ export interface Interface {
|
||||
text: string
|
||||
description?: string
|
||||
metadata?: Record<string, unknown>
|
||||
resume?: boolean
|
||||
}) => Effect.Effect<void, NotFoundError>
|
||||
readonly revert: {
|
||||
readonly stage: (input: {
|
||||
@@ -275,7 +272,6 @@ const layer = Layer.effect(
|
||||
const scope = yield* Scope.Scope
|
||||
const activeShells = new Set<SessionSchema.ID>()
|
||||
const shellLocks = KeyedMutex.makeUnsafe<SessionSchema.ID>()
|
||||
const promptLocks = KeyedMutex.makeUnsafe<SessionMessage.ID>()
|
||||
const decodeMessage = Schema.decodeUnknownEffect(SessionMessage.Message)
|
||||
const isDurableSessionEvent = Schema.is(SessionEvent.Durable)
|
||||
const decode = (row: typeof SessionMessageTable.$inferSelect) =>
|
||||
@@ -478,48 +474,40 @@ const layer = Layer.effect(
|
||||
EventV2.isSynced(item) || isDurableSessionEvent(item),
|
||||
),
|
||||
),
|
||||
prompt: Effect.fn("V2Session.prompt")((input) => {
|
||||
const admit = Effect.gen(function* () {
|
||||
const session = yield* result.get(input.sessionID)
|
||||
// A staged revert must be committed before admitting new input so the prompt
|
||||
// continues from the reverted boundary rather than stale post-boundary history.
|
||||
if (session.revert)
|
||||
yield* SessionRevert.commit(session).pipe(Effect.provideService(EventV2.Service, events))
|
||||
const messageID = input.id ?? SessionMessage.ID.create()
|
||||
const delivery = input.delivery ?? "steer"
|
||||
const recorded = input.id === undefined ? undefined : yield* SessionInput.find(db, input.id)
|
||||
const readMcpResource = (resource: Mcp.ResourceReference) =>
|
||||
Effect.gen(function* () {
|
||||
const mcp = yield* MCP.Service
|
||||
return yield* mcp.readResource(resource)
|
||||
}).pipe(Effect.provide(locations.get(session.location)))
|
||||
const prompt =
|
||||
recorded?.type === "prompt" && matchesResolvedMcpPrompt(recorded.prompt, input.prompt)
|
||||
? recorded.prompt
|
||||
: yield* resolvePrompt(input.prompt, fs, readMcpResource)
|
||||
const expected = { sessionID: input.sessionID, messageID, prompt, delivery }
|
||||
const admitted = yield* SessionInput.admit(db, events, {
|
||||
id: messageID,
|
||||
sessionID: input.sessionID,
|
||||
prompt,
|
||||
delivery,
|
||||
}).pipe(
|
||||
Effect.catchDefect((defect) =>
|
||||
defect instanceof SessionInput.LifecycleConflict
|
||||
? new PromptConflictError({ sessionID: input.sessionID, messageID })
|
||||
: Effect.die(defect),
|
||||
),
|
||||
)
|
||||
if (!SessionInput.equivalent(admitted, expected))
|
||||
return yield* new PromptConflictError({ sessionID: input.sessionID, messageID })
|
||||
if (input.resume !== false) {
|
||||
if (activeShells.has(admitted.sessionID)) return admitted
|
||||
yield* execution.wake(admitted.sessionID)
|
||||
}
|
||||
return admitted
|
||||
})
|
||||
return Effect.uninterruptible(input.id === undefined ? admit : promptLocks.withLock(input.id)(admit))
|
||||
}),
|
||||
prompt: Effect.fn("V2Session.prompt")((input) =>
|
||||
Effect.uninterruptible(
|
||||
Effect.gen(function* () {
|
||||
const session = yield* result.get(input.sessionID)
|
||||
// A staged revert must be committed before admitting new input so the prompt
|
||||
// continues from the reverted boundary rather than stale post-boundary history.
|
||||
if (session.revert)
|
||||
yield* SessionRevert.commit(session).pipe(Effect.provideService(EventV2.Service, events))
|
||||
const prompt = yield* resolvePrompt(input.prompt).pipe(Effect.provideService(FSUtil.Service, fs))
|
||||
const messageID = input.id ?? SessionMessage.ID.create()
|
||||
const delivery = input.delivery ?? "steer"
|
||||
const expected = { sessionID: input.sessionID, messageID, prompt, delivery }
|
||||
const admitted = yield* SessionInput.admit(db, events, {
|
||||
id: messageID,
|
||||
sessionID: input.sessionID,
|
||||
prompt,
|
||||
delivery,
|
||||
}).pipe(
|
||||
Effect.catchDefect((defect) =>
|
||||
defect instanceof SessionInput.LifecycleConflict
|
||||
? new PromptConflictError({ sessionID: input.sessionID, messageID })
|
||||
: Effect.die(defect),
|
||||
),
|
||||
)
|
||||
if (!SessionInput.equivalent(admitted, expected))
|
||||
return yield* new PromptConflictError({ sessionID: input.sessionID, messageID })
|
||||
if (input.resume !== false) {
|
||||
if (activeShells.has(admitted.sessionID)) return admitted
|
||||
yield* execution.wake(admitted.sessionID)
|
||||
}
|
||||
return admitted
|
||||
}),
|
||||
),
|
||||
),
|
||||
command: Effect.fn("V2Session.command")(function* (input) {
|
||||
const session = yield* result.get(input.sessionID)
|
||||
const commands = yield* CommandV2.Service.pipe(Effect.provide(locations.get(session.location)))
|
||||
@@ -694,7 +682,6 @@ const layer = Layer.effect(
|
||||
description: input.description,
|
||||
metadata: input.metadata,
|
||||
})
|
||||
if (input.resume === false) return
|
||||
yield* execution
|
||||
.resume(input.sessionID)
|
||||
.pipe(Effect.ignore, Effect.forkIn(scope, { startImmediately: true }), Effect.asVoid)
|
||||
@@ -753,75 +740,20 @@ function synthesizeTerminalShellInfo(started: ShellSchema.Info): ShellSchema.Inf
|
||||
}
|
||||
}
|
||||
|
||||
const resolvePrompt = Effect.fn("V2Session.resolvePrompt")(function* (
|
||||
input: PromptInput.Prompt,
|
||||
fs: FSUtil.Interface,
|
||||
readMcpResource: (
|
||||
input: Mcp.ResourceReference,
|
||||
) => Effect.Effect<MCP.ResourceContent | undefined, MCP.NotFoundError>,
|
||||
) {
|
||||
const resolvePrompt = Effect.fn("V2Session.resolvePrompt")(function* (input: PromptInput.Prompt) {
|
||||
const fs = yield* FSUtil.Service
|
||||
const files = input.files
|
||||
? (yield* Effect.forEach(input.files, (file) => materializeAttachment(fs, readMcpResource, file), {
|
||||
concurrency: 8,
|
||||
})).flat()
|
||||
? yield* Effect.forEach(input.files, (file) => materializeAttachment(fs, file), { concurrency: 8 })
|
||||
: undefined
|
||||
return Prompt.make({ text: input.text, agents: input.agents, files })
|
||||
})
|
||||
|
||||
const MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024
|
||||
const MAX_MCP_ATTACHMENT_PARTS = 100
|
||||
|
||||
const materializeAttachment = Effect.fn("V2Session.materializeAttachment")(function* (
|
||||
fs: FSUtil.Interface,
|
||||
readMcpResource: (
|
||||
input: Mcp.ResourceReference,
|
||||
) => Effect.Effect<MCP.ResourceContent | undefined, MCP.NotFoundError>,
|
||||
input: PromptInput.FileAttachment,
|
||||
) {
|
||||
const reference = Mcp.parseResourceUri(input.uri)
|
||||
if (reference) {
|
||||
const resource = yield* readMcpResource(reference).pipe(
|
||||
Effect.mapError(
|
||||
(error) => new AttachmentError({ uri: input.uri, message: `Unable to read MCP resource: ${error.message}` }),
|
||||
),
|
||||
)
|
||||
if (!resource || resource.contents.length === 0)
|
||||
return yield* new AttachmentError({ uri: input.uri, message: `Unable to read MCP resource: ${reference.uri}` })
|
||||
if (
|
||||
resource.contents.length > MAX_MCP_ATTACHMENT_PARTS ||
|
||||
resource.contents.reduce(
|
||||
(total, part) => total + (part.type === "text" ? Buffer.byteLength(part.text) : base64ByteLength(part.blob)),
|
||||
0,
|
||||
) > MAX_ATTACHMENT_BYTES
|
||||
)
|
||||
return yield* new AttachmentError({
|
||||
uri: input.uri,
|
||||
message: `MCP resource exceeds attachment limits: ${reference.uri}`,
|
||||
})
|
||||
return yield* Effect.forEach(resource.contents, (part, index) =>
|
||||
Effect.gen(function* () {
|
||||
const bytes = part.type === "text" ? Buffer.from(part.text) : yield* decodeMcpBlob(input.uri, part.blob)
|
||||
return yield* createAttachment(
|
||||
index === 0
|
||||
? input
|
||||
: {
|
||||
...input,
|
||||
name: `${input.name ?? part.uri}-${index + 1}`,
|
||||
mention: undefined,
|
||||
},
|
||||
{
|
||||
bytes,
|
||||
source: { type: "uri", uri: input.uri },
|
||||
start: undefined,
|
||||
end: undefined,
|
||||
name: undefined,
|
||||
mime: part.type === "text" ? "text/plain" : undefined,
|
||||
},
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const resolved = input.uri.startsWith("data:")
|
||||
? {
|
||||
bytes: yield* decodeDataURL(input.uri),
|
||||
@@ -832,20 +764,6 @@ const materializeAttachment = Effect.fn("V2Session.materializeAttachment")(funct
|
||||
mime: undefined,
|
||||
}
|
||||
: yield* readFileAttachment(fs, input.uri)
|
||||
return [yield* createAttachment(input, resolved)]
|
||||
})
|
||||
|
||||
const createAttachment = Effect.fnUntraced(function* (
|
||||
input: PromptInput.FileAttachment,
|
||||
resolved: {
|
||||
readonly bytes: Uint8Array
|
||||
readonly source: { readonly type: "inline" } | { readonly type: "uri"; readonly uri: string }
|
||||
readonly start: number | undefined
|
||||
readonly end: number | undefined
|
||||
readonly name: string | undefined
|
||||
readonly mime: string | undefined
|
||||
},
|
||||
) {
|
||||
if (resolved.bytes.byteLength > MAX_ATTACHMENT_BYTES)
|
||||
return yield* new AttachmentError({
|
||||
uri: input.uri,
|
||||
@@ -873,45 +791,6 @@ const createAttachment = Effect.fnUntraced(function* (
|
||||
})
|
||||
})
|
||||
|
||||
function decodeMcpBlob(uri: string, blob: string) {
|
||||
return Effect.try({
|
||||
try: () => {
|
||||
const bytes = Buffer.from(blob, "base64")
|
||||
if (bytes.toString("base64") !== blob) throw new Error("Non-canonical base64")
|
||||
return bytes
|
||||
},
|
||||
catch: () => new AttachmentError({ uri, message: `MCP resource returned invalid base64 content: ${uri}` }),
|
||||
})
|
||||
}
|
||||
|
||||
function base64ByteLength(value: string) {
|
||||
const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0
|
||||
return Math.floor(value.length * 0.75) - padding
|
||||
}
|
||||
|
||||
function matchesResolvedMcpPrompt(resolved: Prompt, input: PromptInput.Prompt) {
|
||||
if (resolved.text !== input.text || JSON.stringify(resolved.agents ?? []) !== JSON.stringify(input.agents ?? []))
|
||||
return false
|
||||
const files = input.files ?? []
|
||||
if (files.length === 0 || files.some((file) => Mcp.parseResourceUri(file.uri) === undefined)) return false
|
||||
const uris = files.map((file) => file.uri)
|
||||
if (new Set(uris).size !== uris.length) return false
|
||||
const resolvedFiles = resolved.files ?? []
|
||||
const sources = resolvedFiles.flatMap((file) => (file.source.type === "uri" ? [file.source.uri] : []))
|
||||
if (sources.length !== resolvedFiles.length) return false
|
||||
if (JSON.stringify(sources.filter((uri, index) => index === 0 || uri !== sources[index - 1])) !== JSON.stringify(uris))
|
||||
return false
|
||||
return files.every((file) => {
|
||||
const first = resolvedFiles.find((resolved) => resolved.source.type === "uri" && resolved.source.uri === file.uri)
|
||||
if (!first) return false
|
||||
return (
|
||||
first.name === file.name &&
|
||||
first.description === file.description &&
|
||||
JSON.stringify(first.mention) === JSON.stringify(file.mention)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const readFileAttachment = Effect.fn("V2Session.readFileAttachment")(function* (fs: FSUtil.Interface, uri: string) {
|
||||
const url = yield* Effect.try({
|
||||
try: () => new URL(uri),
|
||||
|
||||
@@ -25,27 +25,20 @@ const SUMMARY_TEMPLATE = `Output exactly the Markdown structure shown inside <te
|
||||
- [constraints/preferences, decisions and why, important facts/assumptions, exact context needed to continue, or "(none)"]
|
||||
|
||||
## Work State
|
||||
### Completed
|
||||
- [finished work, verified facts, or changes made; otherwise "(none)"]
|
||||
|
||||
### Active
|
||||
- [current work, partial changes, or investigation state; otherwise "(none)"]
|
||||
|
||||
### Blocked
|
||||
- [blockers, failing commands, or unknowns; otherwise "(none)"]
|
||||
- Completed: [finished work, verified facts, or changes made; otherwise "(none)"]
|
||||
- Active: [current work, partial changes, or investigation state; otherwise "(none)"]
|
||||
- Blocked: [blockers, failing commands, or unknowns; otherwise "(none)"]
|
||||
|
||||
## Next Move
|
||||
1. [immediate concrete action, or "(none)"]
|
||||
2. [next action if known, or "(none)"]
|
||||
|
||||
## Relevant Files
|
||||
- [file or directory path: why it matters, or "(none)"]
|
||||
</template>
|
||||
|
||||
Rules:
|
||||
- Keep every section, even when empty.
|
||||
- Use terse bullets, not prose paragraphs.
|
||||
- Preserve exact file paths, symbols, commands, error strings, URLs, and identifiers when known.
|
||||
- Put relevant files and symbols inside the section where they matter; do not add extra sections.
|
||||
- Do not mention the summary process or that context was compacted.`
|
||||
|
||||
type Settings = {
|
||||
|
||||
@@ -120,12 +120,12 @@ const assistant = (message: SessionMessage.Assistant, model: ModelV2.Ref) => {
|
||||
const content = message.content.flatMap((item): ContentPart[] => {
|
||||
if (item.type === "text") return [{ type: "text", text: item.text }]
|
||||
if (item.type === "reasoning")
|
||||
return reuseProviderMetadata
|
||||
return sameModel
|
||||
? [
|
||||
{
|
||||
type: "reasoning",
|
||||
text: item.text,
|
||||
providerMetadata: providerMetadata(model.providerID, item.state),
|
||||
providerMetadata: reuseProviderMetadata ? providerMetadata(model.providerID, item.state) : undefined,
|
||||
},
|
||||
]
|
||||
: item.text.length > 0
|
||||
|
||||
@@ -43,15 +43,20 @@ export interface Registration {
|
||||
readonly group?: string
|
||||
}
|
||||
|
||||
export interface CodeModeTools {
|
||||
[name: string]: Tool.Definition<never> | CodeModeTools
|
||||
}
|
||||
|
||||
export const create = (options: {
|
||||
readonly registrations: ReadonlyMap<string, Registration>
|
||||
readonly current: (name: string) => Registration | undefined
|
||||
readonly tools: CodeModeTools
|
||||
}) => {
|
||||
const runtime = (
|
||||
invoke: (name: string, registration: Registration, input: unknown) => Effect.Effect<unknown, unknown>,
|
||||
hooks?: CodeMode.ToolCallHooks,
|
||||
) => {
|
||||
const tools: Record<string, Tool.Definition<never> | Record<string, Tool.Definition<never>>> = {}
|
||||
const tools = cloneTools(options.tools)
|
||||
for (const [name, registration] of options.registrations) {
|
||||
const child = definition(name, registration.tool)
|
||||
const value = Tool.make({
|
||||
@@ -163,6 +168,15 @@ export const create = (options: {
|
||||
})
|
||||
}
|
||||
|
||||
function cloneTools(tools: CodeModeTools): CodeModeTools {
|
||||
return Object.assign(
|
||||
Object.create(null),
|
||||
Object.fromEntries(
|
||||
Object.entries(tools).map(([name, value]) => [name, Tool.isDefinition(value) ? value : cloneTools(value)]),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
function displayInput(input: unknown): Record<string, unknown> | undefined {
|
||||
if (input === null || input === undefined) return
|
||||
if (typeof input !== "object" || Array.isArray(input)) return { input }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * as ToolRegistry from "./registry"
|
||||
export type { CodeModeTools } from "./execute"
|
||||
|
||||
import { ToolOutput, type ToolCall, type ToolDefinition, type ToolResultValue } from "@opencode-ai/llm"
|
||||
import { Context, Effect, Layer, Scope } from "effect"
|
||||
@@ -9,11 +10,12 @@ import { SessionMessage } from "../session/message"
|
||||
import { SessionSchema } from "../session/schema"
|
||||
import { ToolOutputStore } from "../tool-output-store"
|
||||
import { Wildcard } from "../util/wildcard"
|
||||
import { ExecuteTool } from "./execute"
|
||||
import { ExecuteTool, type CodeModeTools } from "./execute"
|
||||
import { definition, permission, registrationEntries, RegistrationError, settle, type AnyTool } from "./tool"
|
||||
import { Tools } from "./tools"
|
||||
import { ToolHooks } from "./hooks"
|
||||
import { makeLocationNode } from "../effect/app-node"
|
||||
import { LayerNode } from "../effect/layer-node"
|
||||
import { SessionError } from "@opencode-ai/schema/session-error"
|
||||
import { toSessionError } from "../session/to-session-error"
|
||||
|
||||
@@ -51,10 +53,20 @@ export interface Settlement {
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/ToolRegistry") {}
|
||||
class CodeModeCatalog extends Context.Service<CodeModeCatalog, { readonly tools: CodeModeTools }>()(
|
||||
"@opencode/v2/CodeModeCatalog",
|
||||
) {}
|
||||
|
||||
const codeModeCatalogNode = makeLocationNode({
|
||||
service: CodeModeCatalog,
|
||||
layer: Layer.succeed(CodeModeCatalog, CodeModeCatalog.of({ tools: {} })),
|
||||
deps: [],
|
||||
})
|
||||
|
||||
const registryLayer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const codeModeTools = (yield* CodeModeCatalog).tools
|
||||
const resources = yield* ToolOutputStore.Service
|
||||
const toolHooks = yield* ToolHooks.Service
|
||||
type Registration = {
|
||||
@@ -204,11 +216,13 @@ const registryLayer = Layer.effect(
|
||||
}
|
||||
const direct = new Map(Array.from(registrations).filter(([, registration]) => !registration.deferred))
|
||||
const deferred = new Map(Array.from(registrations).filter(([, registration]) => registration.deferred))
|
||||
const tools = Flag.CODEMODE_ENABLED ? codeModeTools : {}
|
||||
const execute =
|
||||
deferred.size > 0 && !whollyDisabled("execute", input.permissions ?? [])
|
||||
(deferred.size > 0 || Object.keys(tools).length > 0) && !whollyDisabled("execute", input.permissions ?? [])
|
||||
? ExecuteTool.create({
|
||||
registrations: deferred,
|
||||
current: (name) => local.get(name)?.at(-1)?.registration,
|
||||
tools,
|
||||
})
|
||||
: undefined
|
||||
return {
|
||||
@@ -241,14 +255,18 @@ function whollyDisabled(action: string, rules: PermissionV2.Ruleset) {
|
||||
return rule?.resource === "*" && rule.effect === "deny"
|
||||
}
|
||||
|
||||
export function codeModeReplacement(tools: CodeModeTools): LayerNode.Replacement {
|
||||
return [codeModeCatalogNode, Layer.succeed(CodeModeCatalog, CodeModeCatalog.of({ tools }))]
|
||||
}
|
||||
|
||||
export const node = makeLocationNode({
|
||||
service: Service,
|
||||
layer,
|
||||
deps: [ToolOutputStore.node, ToolHooks.node],
|
||||
deps: [ToolOutputStore.node, ToolHooks.node, codeModeCatalogNode],
|
||||
})
|
||||
|
||||
export const toolsNode = makeLocationNode({
|
||||
service: Tools.Service,
|
||||
layer,
|
||||
deps: [ToolOutputStore.node, ToolHooks.node],
|
||||
deps: [ToolOutputStore.node, ToolHooks.node, codeModeCatalogNode],
|
||||
})
|
||||
|
||||
@@ -4,27 +4,16 @@ import {
|
||||
CallToolRequestSchema,
|
||||
GetPromptRequestSchema,
|
||||
ListPromptsRequestSchema,
|
||||
ListResourcesRequestSchema,
|
||||
ListResourceTemplatesRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
ReadResourceRequestSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js"
|
||||
|
||||
const server = new Server(
|
||||
{ name: "timeout", version: "1.0.0" },
|
||||
{ capabilities: { prompts: {}, resources: {}, tools: {} } },
|
||||
)
|
||||
const server = new Server({ name: "timeout", version: "1.0.0" }, { capabilities: { prompts: {}, tools: {} } })
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
if (process.env.MCP_TIMEOUT_TARGET === "catalog") await Bun.sleep(100)
|
||||
return { tools: [{ name: "slow", inputSchema: { type: "object" } }] }
|
||||
})
|
||||
server.setRequestHandler(ListPromptsRequestSchema, () => Promise.resolve({ prompts: [{ name: "slow" }] }))
|
||||
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
||||
if (process.env.MCP_TIMEOUT_TARGET === "resource-catalog") await Bun.sleep(100)
|
||||
return { resources: [{ name: "slow", uri: "test://slow" }] }
|
||||
})
|
||||
server.setRequestHandler(ListResourceTemplatesRequestSchema, () => Promise.resolve({ resourceTemplates: [] }))
|
||||
server.setRequestHandler(CallToolRequestSchema, async () => {
|
||||
await Bun.sleep(100)
|
||||
return { content: [] }
|
||||
@@ -33,9 +22,5 @@ server.setRequestHandler(GetPromptRequestSchema, async () => {
|
||||
await Bun.sleep(100)
|
||||
return { messages: [] }
|
||||
})
|
||||
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
||||
await Bun.sleep(100)
|
||||
return { contents: [{ uri: request.params.uri, text: "slow" }] }
|
||||
})
|
||||
|
||||
await server.connect(new StdioServerTransport())
|
||||
|
||||
@@ -14,12 +14,15 @@ export const emptyMcpLayer = Layer.succeed(
|
||||
instructions: () => Effect.succeed([]),
|
||||
prompts: () => Effect.succeed([]),
|
||||
prompt: () => Effect.succeed(undefined),
|
||||
resourceCatalog: () => Effect.succeed(MCP.ResourceCatalog.make({ resources: [], templates: [] })),
|
||||
resourceCatalog: () => Effect.succeed(new MCP.ResourceCatalog({ resources: [], templates: [] })),
|
||||
readResource: () => Effect.succeed(undefined),
|
||||
}),
|
||||
)
|
||||
|
||||
export const emptyConfigLayer = Layer.succeed(Config.Service, Config.Service.of({ entries: () => Effect.succeed([]) }))
|
||||
export const emptyConfigLayer = Layer.succeed(
|
||||
Config.Service,
|
||||
Config.Service.of({ entries: () => Effect.succeed([]) }),
|
||||
)
|
||||
|
||||
export const testLocationLayer = Layer.succeed(
|
||||
Location.Service,
|
||||
|
||||
@@ -3,7 +3,7 @@ import path from "path"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Config } from "@opencode-ai/schema/config"
|
||||
import { Plugin } from "@opencode-ai/schema/plugin"
|
||||
import { Context, DateTime, Effect, Equal, Hash, RcMap, Schema, Stream } from "effect"
|
||||
import { Context, DateTime, Effect, Equal, Hash, Schema, Stream } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||
import { Catalog } from "@opencode-ai/core/catalog"
|
||||
@@ -12,7 +12,6 @@ import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-services"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
import { SdkPlugins } from "@opencode-ai/core/plugin/sdk"
|
||||
import { PluginSupervisor } from "@opencode-ai/core/plugin/supervisor"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
@@ -29,43 +28,8 @@ import { Reference } from "../src/reference"
|
||||
import { ToolRegistry } from "../src/tool/registry"
|
||||
|
||||
const it = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, LocationServiceMap.node])))
|
||||
const itWithSdk = testEffect(
|
||||
AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, SdkPlugins.node, LocationServiceMap.node])),
|
||||
)
|
||||
|
||||
describe("LocationServiceMap", () => {
|
||||
itWithSdk.live("preserves embedded SDK plugins after Location eviction", () =>
|
||||
Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
|
||||
).pipe(
|
||||
Effect.flatMap((dir) =>
|
||||
Effect.gen(function* () {
|
||||
const sdk = yield* SdkPlugins.Service
|
||||
const locations = yield* LocationServiceMap.Service
|
||||
const id = AgentV2.ID.make("persistent-sdk-agent")
|
||||
const plugin = define({
|
||||
id: "persistent-sdk-plugin",
|
||||
effect: (ctx) => ctx.agent.transform((agents) => agents.update(id, () => {})),
|
||||
})
|
||||
yield* sdk.register(plugin)
|
||||
|
||||
const ref = Location.Ref.make({ directory: AbsolutePath.make(dir.path) })
|
||||
const read = Effect.gen(function* () {
|
||||
const supervisor = yield* PluginSupervisor.Service
|
||||
yield* supervisor.ready
|
||||
const agents = yield* AgentV2.Service
|
||||
return yield* agents.get(id)
|
||||
})
|
||||
|
||||
expect(yield* read.pipe(Effect.scoped, Effect.provide(locations.get(ref)))).toBeDefined()
|
||||
yield* locations.invalidate(ref)
|
||||
expect(yield* read.pipe(Effect.scoped, Effect.provide(locations.get(ref)))).toBeDefined()
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("applies ordered plugin config operations during boot", () =>
|
||||
Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
@@ -208,36 +172,6 @@ describe("LocationServiceMap", () => {
|
||||
),
|
||||
)
|
||||
|
||||
it.live("normalizes ref key shapes to one cached location graph", () =>
|
||||
Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
|
||||
).pipe(
|
||||
Effect.flatMap((dir) =>
|
||||
Effect.scoped(
|
||||
Effect.gen(function* () {
|
||||
const locations = yield* LocationServiceMap.Service
|
||||
const directory = AbsolutePath.make(dir.path)
|
||||
const absent = Location.Ref.make({ directory })
|
||||
const present = Location.Ref.make({ directory, workspaceID: undefined })
|
||||
// The two shapes are not structurally Equal: own-key sets differ.
|
||||
expect(Object.keys(absent)).toEqual(["directory"])
|
||||
expect(Object.keys(present)).toEqual(["directory", "workspaceID"])
|
||||
expect(Equal.equals(absent, present)).toBe(false)
|
||||
|
||||
const first = yield* locations.contextEffect(absent)
|
||||
expect(yield* locations.contextEffect(present)).toBe(first)
|
||||
expect(Array.from(yield* RcMap.keys(locations.rcMap))).toHaveLength(1)
|
||||
|
||||
// Invalidating with the shape opposite to the one that booted must evict.
|
||||
yield* locations.invalidate(present)
|
||||
expect(Array.from(yield* RcMap.keys(locations.rcMap))).toHaveLength(0)
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("isolates catalog state by location", () =>
|
||||
Effect.acquireRelease(
|
||||
Effect.promise(() => Promise.all([tmpdir(), tmpdir()])),
|
||||
|
||||
@@ -3,165 +3,26 @@ import { describe, expect, test } from "bun:test"
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
|
||||
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js"
|
||||
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js"
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListResourcesRequestSchema,
|
||||
ListResourceTemplatesRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
ReadResourceRequestSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js"
|
||||
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"
|
||||
import { ConfigMCP } from "@opencode-ai/core/config/mcp"
|
||||
import { Config } from "@opencode-ai/core/config"
|
||||
import { Credential } from "@opencode-ai/core/credential"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { Form } from "@opencode-ai/core/form"
|
||||
import { Integration } from "@opencode-ai/core/integration"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { MCP } from "@opencode-ai/core/mcp/index"
|
||||
import { MCPClient } from "@opencode-ai/core/mcp/client"
|
||||
import { PermissionV2 } from "@opencode-ai/core/permission"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { McpTool } from "@opencode-ai/core/tool/mcp"
|
||||
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
||||
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
|
||||
import { Deferred, Effect, Exit, Fiber, Layer, Stream } from "effect"
|
||||
import { Deferred, Effect, Fiber, Layer, Stream } from "effect"
|
||||
import { testEffect } from "./lib/effect"
|
||||
import { location } from "./fixture/location"
|
||||
import { settleTool, toolDefinitions, toolIdentity, waitForTool } from "./lib/tool"
|
||||
|
||||
let assertion: Deferred.Deferred<PermissionV2.AssertInput> | undefined
|
||||
let decision: Effect.Effect<void, PermissionV2.Error> = Effect.void
|
||||
let calls = 0
|
||||
|
||||
type ResourcePage = {
|
||||
items: Array<{ name: string; uri: string; description?: string; mimeType?: string }>
|
||||
nextCursor?: string
|
||||
}
|
||||
|
||||
type ResourceTemplatePage = {
|
||||
items: Array<{ name: string; uriTemplate: string; description?: string; mimeType?: string }>
|
||||
nextCursor?: string
|
||||
}
|
||||
|
||||
function resourceServer(input: { resources?: boolean; listChanged?: boolean } = {}) {
|
||||
return Effect.acquireRelease(
|
||||
Effect.promise(async () => {
|
||||
const state = {
|
||||
resources: [] as ResourcePage["items"],
|
||||
templates: [] as ResourceTemplatePage["items"],
|
||||
resourcePages: undefined as Record<string, ResourcePage> | undefined,
|
||||
templatePages: undefined as Record<string, ResourceTemplatePage> | undefined,
|
||||
contents: [
|
||||
{ uri: "docs://readme", text: "hello", mimeType: "text/plain" },
|
||||
{ uri: "docs://logo", blob: "aGVsbG8=", mimeType: "image/png" },
|
||||
] as Array<{ uri: string; text: string; mimeType?: string } | { uri: string; blob: string; mimeType?: string }>,
|
||||
resourceLists: 0,
|
||||
templateLists: 0,
|
||||
}
|
||||
const protocol = new Server(
|
||||
{ name: "mcp-resources", version: "1.0.0" },
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
...(input.resources === false ? {} : { resources: { listChanged: input.listChanged } }),
|
||||
},
|
||||
},
|
||||
)
|
||||
protocol.setRequestHandler(ListToolsRequestSchema, () => Promise.resolve({ tools: [] }))
|
||||
if (input.resources !== false) {
|
||||
protocol.setRequestHandler(ListResourcesRequestSchema, (request) => {
|
||||
state.resourceLists += 1
|
||||
const page = state.resourcePages?.[request.params?.cursor ?? "initial"]
|
||||
return Promise.resolve({ resources: page?.items ?? state.resources, nextCursor: page?.nextCursor })
|
||||
})
|
||||
protocol.setRequestHandler(ListResourceTemplatesRequestSchema, (request) => {
|
||||
state.templateLists += 1
|
||||
const page = state.templatePages?.[request.params?.cursor ?? "initial"]
|
||||
return Promise.resolve({ resourceTemplates: page?.items ?? state.templates, nextCursor: page?.nextCursor })
|
||||
})
|
||||
protocol.setRequestHandler(ReadResourceRequestSchema, () => Promise.resolve({ contents: state.contents }))
|
||||
}
|
||||
const transport = new WebStandardStreamableHTTPServerTransport({
|
||||
sessionIdGenerator: () => crypto.randomUUID(),
|
||||
enableJsonResponse: true,
|
||||
})
|
||||
await protocol.connect(transport)
|
||||
const http = Bun.serve({
|
||||
port: 0,
|
||||
fetch: (request) => transport.handleRequest(request),
|
||||
})
|
||||
return {
|
||||
state,
|
||||
url: http.url.toString(),
|
||||
sendResourceListChanged: () => protocol.sendResourceListChanged(),
|
||||
close: async () => {
|
||||
await protocol.close().catch(() => {})
|
||||
await http.stop(true)
|
||||
},
|
||||
}
|
||||
}),
|
||||
(server) => Effect.promise(server.close),
|
||||
)
|
||||
}
|
||||
|
||||
function resourceMcpLayer(url: string) {
|
||||
const directory = AbsolutePath.make(import.meta.dir)
|
||||
const unusedIntegration = () => Effect.die("unused integration service")
|
||||
return MCP.layer.pipe(
|
||||
Layer.provide(
|
||||
Layer.mergeAll(
|
||||
Layer.succeed(
|
||||
Config.Service,
|
||||
Config.Service.of({
|
||||
entries: () =>
|
||||
Effect.succeed([
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: new Config.Info({
|
||||
mcp: new ConfigMCP.Info({
|
||||
servers: { resources: new ConfigMCP.Remote({ type: "remote", url, oauth: false }) },
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
),
|
||||
Layer.succeed(Location.Service, Location.Service.of(location({ directory }))),
|
||||
Layer.mock(EventV2.Service, {
|
||||
subscribe: () => Stream.never,
|
||||
publish: (definition, data) =>
|
||||
Effect.succeed({
|
||||
id: EventV2.ID.create(),
|
||||
type: definition.type,
|
||||
data,
|
||||
} as EventV2.Payload<typeof definition>),
|
||||
}),
|
||||
Layer.mock(Form.Service, {}),
|
||||
Layer.mock(Integration.Service, {
|
||||
connection: {
|
||||
active: unusedIntegration,
|
||||
resolve: unusedIntegration,
|
||||
key: unusedIntegration,
|
||||
oauth: unusedIntegration,
|
||||
update: unusedIntegration,
|
||||
remove: unusedIntegration,
|
||||
},
|
||||
attempt: {
|
||||
status: unusedIntegration,
|
||||
complete: unusedIntegration,
|
||||
cancel: unusedIntegration,
|
||||
},
|
||||
}),
|
||||
Layer.mock(Credential.Service, {}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
const mcp = Layer.mock(MCP.Service, {
|
||||
tools: () =>
|
||||
Effect.succeed([
|
||||
@@ -380,163 +241,6 @@ test("applies the configured MCP execution timeout to prompts", async () => {
|
||||
await expect(result).rejects.toThrow("Request timed out")
|
||||
})
|
||||
|
||||
test("applies configured MCP timeouts to resource operations", async () => {
|
||||
const catalog = Effect.runPromise(
|
||||
Effect.scoped(
|
||||
Effect.gen(function* () {
|
||||
const connection = yield* MCPClient.connect(
|
||||
"resource-catalog-timeout",
|
||||
new ConfigMCP.Local({
|
||||
type: "local",
|
||||
command: [process.execPath, path.join(import.meta.dir, "fixture/mcp-timeout.ts")],
|
||||
environment: { MCP_TIMEOUT_TARGET: "resource-catalog" },
|
||||
timeout: new ConfigMCP.Timeout({ catalog: 10 }),
|
||||
}),
|
||||
import.meta.dir,
|
||||
)
|
||||
return yield* connection.resources()
|
||||
}),
|
||||
),
|
||||
)
|
||||
await expect(catalog).rejects.toThrow("Request timed out")
|
||||
|
||||
const read = Effect.runPromise(
|
||||
Effect.scoped(
|
||||
Effect.gen(function* () {
|
||||
const connection = yield* MCPClient.connect(
|
||||
"resource-read-timeout",
|
||||
new ConfigMCP.Local({
|
||||
type: "local",
|
||||
command: [process.execPath, path.join(import.meta.dir, "fixture/mcp-timeout.ts")],
|
||||
timeout: new ConfigMCP.Timeout({ execution: 10 }),
|
||||
}),
|
||||
import.meta.dir,
|
||||
)
|
||||
return yield* connection.readResource({ uri: "test://slow" })
|
||||
}),
|
||||
),
|
||||
)
|
||||
await expect(read).rejects.toThrow("Request timed out")
|
||||
})
|
||||
|
||||
test("lists, reads, and reports MCP resource changes", async () => {
|
||||
await Effect.runPromise(
|
||||
Effect.scoped(
|
||||
Effect.gen(function* () {
|
||||
const server = yield* resourceServer({ listChanged: true })
|
||||
server.state.resourcePages = {
|
||||
initial: {
|
||||
items: [{ name: "Readme", uri: "docs://readme", description: "Project docs" }],
|
||||
nextCursor: "resources-2",
|
||||
},
|
||||
"resources-2": { items: [{ name: "Logo", uri: "docs://logo", mimeType: "image/png" }] },
|
||||
}
|
||||
server.state.templatePages = {
|
||||
initial: {
|
||||
items: [{ name: "File", uriTemplate: "docs://{path}" }],
|
||||
nextCursor: "templates-2",
|
||||
},
|
||||
"templates-2": { items: [{ name: "Issue", uriTemplate: "issue://{id}", description: "Issue" }] },
|
||||
}
|
||||
const connection = yield* MCPClient.connect(
|
||||
"resources",
|
||||
new ConfigMCP.Remote({ type: "remote", url: server.url, oauth: false }),
|
||||
import.meta.dir,
|
||||
)
|
||||
|
||||
expect(yield* connection.resources()).toEqual([
|
||||
{ name: "Readme", uri: "docs://readme", description: "Project docs", mimeType: undefined },
|
||||
{ name: "Logo", uri: "docs://logo", description: undefined, mimeType: "image/png" },
|
||||
])
|
||||
expect(yield* connection.resourceTemplates()).toEqual([
|
||||
{ name: "File", uriTemplate: "docs://{path}", description: undefined, mimeType: undefined },
|
||||
{ name: "Issue", uriTemplate: "issue://{id}", description: "Issue", mimeType: undefined },
|
||||
])
|
||||
expect(yield* connection.readResource({ uri: "docs://readme" })).toEqual({
|
||||
contents: [
|
||||
{ type: "text", uri: "docs://readme", text: "hello", mimeType: "text/plain" },
|
||||
{ type: "blob", uri: "docs://logo", blob: "aGVsbG8=", mimeType: "image/png" },
|
||||
],
|
||||
})
|
||||
|
||||
const changed = yield* Deferred.make<void>()
|
||||
connection.onResourcesChanged(() => Deferred.doneUnsafe(changed, Exit.void))
|
||||
yield* Effect.promise(server.sendResourceListChanged)
|
||||
yield* Deferred.await(changed)
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
test("skips MCP resource requests when the capability is absent", async () => {
|
||||
await Effect.runPromise(
|
||||
Effect.scoped(
|
||||
Effect.gen(function* () {
|
||||
const server = yield* resourceServer({ resources: false })
|
||||
const connection = yield* MCPClient.connect(
|
||||
"resources",
|
||||
new ConfigMCP.Remote({ type: "remote", url: server.url, oauth: false }),
|
||||
import.meta.dir,
|
||||
)
|
||||
expect(yield* connection.resources()).toEqual([])
|
||||
expect(yield* connection.resourceTemplates()).toEqual([])
|
||||
expect(yield* connection.readResource({ uri: "docs://readme" })).toBeUndefined()
|
||||
expect({ resources: server.state.resourceLists, templates: server.state.templateLists }).toEqual({
|
||||
resources: 0,
|
||||
templates: 0,
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
test("loads and reads MCP resources", async () => {
|
||||
await Effect.runPromise(
|
||||
Effect.scoped(
|
||||
Effect.gen(function* () {
|
||||
const server = yield* resourceServer()
|
||||
server.state.resources = [{ name: "Readme", uri: "docs://readme" }]
|
||||
server.state.templates = [{ name: "File", uriTemplate: "docs://{path}" }]
|
||||
|
||||
yield* Effect.gen(function* () {
|
||||
const service = yield* MCP.Service
|
||||
expect(yield* service.resourceCatalog()).toEqual({
|
||||
resources: [
|
||||
{
|
||||
server: "resources",
|
||||
name: "Readme",
|
||||
uri: "docs://readme",
|
||||
description: undefined,
|
||||
mimeType: undefined,
|
||||
},
|
||||
],
|
||||
templates: [
|
||||
{
|
||||
server: "resources",
|
||||
name: "File",
|
||||
uriTemplate: "docs://{path}",
|
||||
description: undefined,
|
||||
mimeType: undefined,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
server.state.resources = [{ name: "Guide", uri: "docs://guide" }]
|
||||
expect((yield* service.resourceCatalog()).resources.map((resource) => resource.uri)).toEqual(["docs://guide"])
|
||||
expect(yield* service.readResource({ server: "resources", uri: "docs://readme" })).toEqual({
|
||||
server: "resources",
|
||||
uri: "docs://readme",
|
||||
contents: [
|
||||
{ type: "text", uri: "docs://readme", text: "hello", mimeType: "text/plain" },
|
||||
{ type: "blob", uri: "docs://logo", blob: "aGVsbG8=", mimeType: "image/png" },
|
||||
],
|
||||
})
|
||||
}).pipe(Effect.provide(resourceMcpLayer(server.url)))
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
it.effect("advertises MCP output schemas to Code Mode", () =>
|
||||
Effect.gen(function* () {
|
||||
const registry = yield* ToolRegistry.Service
|
||||
|
||||
@@ -52,15 +52,6 @@ const it = testEffect(
|
||||
),
|
||||
)
|
||||
|
||||
test("compaction prompt preserves detailed work state and relevant files", () => {
|
||||
const prompt = SessionCompaction.buildPrompt({ context: ["conversation history"] })
|
||||
|
||||
expect(prompt).toContain("## Work State\n### Completed")
|
||||
expect(prompt).toContain("### Active")
|
||||
expect(prompt).toContain("### Blocked")
|
||||
expect(prompt).toContain("## Relevant Files")
|
||||
})
|
||||
|
||||
test("compaction describes tool media without embedding base64", () => {
|
||||
const base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB"
|
||||
const serialized = SessionCompaction.serializeToolContent([
|
||||
@@ -83,14 +74,13 @@ test("compaction prompt requires the checkpoint headings in order", () => {
|
||||
"## Objective",
|
||||
"## Important Details",
|
||||
"## Work State",
|
||||
"### Completed",
|
||||
"### Active",
|
||||
"### Blocked",
|
||||
"## Next Move",
|
||||
"## Relevant Files",
|
||||
])
|
||||
expect(prompt).toContain("one or two brief sentences")
|
||||
expect(prompt).toContain("constraints/preferences, decisions and why")
|
||||
expect(prompt).toContain("Completed:")
|
||||
expect(prompt).toContain("Active:")
|
||||
expect(prompt).toContain("Blocked:")
|
||||
expect(prompt).toContain("immediate concrete action")
|
||||
expect(prompt).toContain("next action if known")
|
||||
expect(prompt).toContain("Keep every section, even when empty.")
|
||||
|
||||
@@ -24,8 +24,6 @@ import { SessionExecution } from "@opencode-ai/core/session/execution"
|
||||
import { SessionInput } from "@opencode-ai/core/session/input"
|
||||
import { SessionInputTable, SessionMessageTable, SessionTable } from "@opencode-ai/core/session/sql"
|
||||
import { SessionStore } from "@opencode-ai/core/session/store"
|
||||
import { MCP } from "@opencode-ai/core/mcp/index"
|
||||
import { Mcp } from "@opencode-ai/schema/mcp"
|
||||
import { testEffect } from "./lib/effect"
|
||||
|
||||
const executionCalls: SessionV2.ID[] = []
|
||||
@@ -51,62 +49,20 @@ const execution = Layer.succeed(
|
||||
awaitIdle: () => Effect.void,
|
||||
}),
|
||||
)
|
||||
let mcpResourcesAvailable = true
|
||||
let mcpResourceReads = 0
|
||||
const mcp = Layer.succeed(
|
||||
MCP.Service,
|
||||
MCP.Service.of({
|
||||
servers: () => Effect.succeed([]),
|
||||
tools: () => Effect.succeed([]),
|
||||
callTool: () => Effect.die("unused mcp.callTool"),
|
||||
instructions: () => Effect.succeed([]),
|
||||
prompts: () => Effect.succeed([]),
|
||||
prompt: () => Effect.succeed(undefined),
|
||||
resourceCatalog: () => Effect.succeed(MCP.ResourceCatalog.make({ resources: [], templates: [] })),
|
||||
readResource: (input) =>
|
||||
Effect.sync(() => {
|
||||
mcpResourceReads++
|
||||
if (!mcpResourcesAvailable || input.server !== "docs") return undefined
|
||||
if (input.uri === "docs://many")
|
||||
return MCP.ResourceContent.make({
|
||||
server: "docs",
|
||||
uri: input.uri,
|
||||
contents: Array.from({ length: 101 }, (_, index) => ({
|
||||
type: "text" as const,
|
||||
uri: `docs://many/${index}`,
|
||||
text: "",
|
||||
})),
|
||||
})
|
||||
if (input.uri !== "docs://readme") return undefined
|
||||
return MCP.ResourceContent.make({
|
||||
server: "docs",
|
||||
uri: input.uri,
|
||||
contents: [
|
||||
{ type: "text", uri: input.uri, text: '{"title":"Readme"}', mimeType: "application/json" },
|
||||
{ type: "blob", uri: "docs://logo", blob: "iVBORw0KGgo=", mimeType: "application/octet-stream" },
|
||||
],
|
||||
})
|
||||
}),
|
||||
}),
|
||||
)
|
||||
const it = testEffect(
|
||||
AppNodeBuilder.build(
|
||||
LayerNode.group([Database.node, EventV2.node, SessionProjector.node, SessionStore.node, SessionV2.node]),
|
||||
[
|
||||
[SessionExecution.node, execution],
|
||||
[MCP.node, mcp],
|
||||
],
|
||||
[[SessionExecution.node, execution]],
|
||||
),
|
||||
)
|
||||
const sessionID = SessionV2.ID.make("ses_prompt_test")
|
||||
const messageID = SessionMessage.ID.create()
|
||||
const directory = AbsolutePath.make(process.cwd())
|
||||
|
||||
const setup = Effect.gen(function* () {
|
||||
const { db } = yield* Database.Service
|
||||
yield* db
|
||||
.insert(ProjectTable)
|
||||
.values({ id: Project.ID.global, worktree: directory, sandboxes: [] })
|
||||
.values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] })
|
||||
.onConflictDoNothing()
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
@@ -116,7 +72,7 @@ const setup = Effect.gen(function* () {
|
||||
id: sessionID,
|
||||
project_id: Project.ID.global,
|
||||
slug: "test",
|
||||
directory,
|
||||
directory: "/project",
|
||||
title: "test",
|
||||
version: "test",
|
||||
})
|
||||
@@ -387,135 +343,6 @@ describe("SessionV2.prompt", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("materializes MCP resource content before admission", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
mcpResourcesAvailable = true
|
||||
const session = yield* SessionV2.Service
|
||||
const uri = Mcp.resourceUri({ server: "docs", uri: "docs://readme" })
|
||||
|
||||
const message = yield* session.prompt({
|
||||
sessionID,
|
||||
prompt: {
|
||||
text: "Inspect @Readme",
|
||||
files: [
|
||||
{
|
||||
uri,
|
||||
name: "Readme",
|
||||
description: "Project documentation",
|
||||
mention: { start: 8, end: 15, text: "@Readme" },
|
||||
},
|
||||
],
|
||||
},
|
||||
resume: false,
|
||||
})
|
||||
|
||||
expect(message.prompt.files).toEqual([
|
||||
{
|
||||
data: Buffer.from('{"title":"Readme"}').toString("base64"),
|
||||
mime: "text/plain",
|
||||
source: { type: "uri", uri },
|
||||
name: "Readme",
|
||||
description: "Project documentation",
|
||||
mention: { start: 8, end: 15, text: "@Readme" },
|
||||
},
|
||||
{
|
||||
data: "iVBORw0KGgo=",
|
||||
mime: "image/png",
|
||||
source: { type: "uri", uri },
|
||||
name: "Readme-2",
|
||||
description: "Project documentation",
|
||||
},
|
||||
])
|
||||
const stored = yield* admitted(message.id)
|
||||
expect(stored?.type === "prompt" ? stored.prompt.files : undefined).toEqual(message.prompt.files)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("rejects unavailable MCP resource attachments", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
mcpResourcesAvailable = true
|
||||
const session = yield* SessionV2.Service
|
||||
const uri = Mcp.resourceUri({ server: "docs", uri: "docs://missing" })
|
||||
|
||||
const error = yield* session
|
||||
.prompt({ sessionID, prompt: { text: "Inspect this", files: [{ uri }] }, resume: false })
|
||||
.pipe(Effect.flip)
|
||||
|
||||
expect(error).toMatchObject({
|
||||
_tag: "Session.AttachmentError",
|
||||
uri,
|
||||
message: "Unable to read MCP resource: docs://missing",
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("reuses durable MCP content for exact prompt retries", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
mcpResourcesAvailable = true
|
||||
mcpResourceReads = 0
|
||||
const session = yield* SessionV2.Service
|
||||
const uri = Mcp.resourceUri({ server: "docs", uri: "docs://readme" })
|
||||
const input = {
|
||||
id: messageID,
|
||||
sessionID,
|
||||
prompt: PromptInput.Prompt.make({ text: "Inspect this", files: [{ uri }] }),
|
||||
resume: false as const,
|
||||
}
|
||||
|
||||
const first = yield* session.prompt(input)
|
||||
mcpResourcesAvailable = false
|
||||
const retried = yield* session.prompt(input)
|
||||
|
||||
expect(retried).toEqual(first)
|
||||
expect(mcpResourceReads).toBe(1)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("coalesces concurrent MCP prompt retries before reading content", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
mcpResourcesAvailable = true
|
||||
mcpResourceReads = 0
|
||||
const session = yield* SessionV2.Service
|
||||
const input = {
|
||||
id: messageID,
|
||||
sessionID,
|
||||
prompt: PromptInput.Prompt.make({
|
||||
text: "Inspect this",
|
||||
files: [{ uri: Mcp.resourceUri({ server: "docs", uri: "docs://readme" }), name: "Readme" }],
|
||||
}),
|
||||
resume: false as const,
|
||||
}
|
||||
|
||||
const messages = yield* Effect.all([session.prompt(input), session.prompt(input)], { concurrency: "unbounded" })
|
||||
|
||||
expect(messages[1]).toEqual(messages[0])
|
||||
expect(mcpResourceReads).toBe(1)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("rejects MCP resources with too many content parts", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
mcpResourcesAvailable = true
|
||||
const session = yield* SessionV2.Service
|
||||
const uri = Mcp.resourceUri({ server: "docs", uri: "docs://many" })
|
||||
|
||||
const error = yield* session
|
||||
.prompt({ sessionID, prompt: { text: "Inspect this", files: [{ uri }] }, resume: false })
|
||||
.pipe(Effect.flip)
|
||||
|
||||
expect(error).toMatchObject({
|
||||
_tag: "Session.AttachmentError",
|
||||
uri,
|
||||
message: "MCP resource exceeds attachment limits: docs://many",
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("sniffs data URL content instead of trusting its declared MIME", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
@@ -821,7 +648,7 @@ describe("SessionV2.prompt", () => {
|
||||
id: other,
|
||||
project_id: Project.ID.global,
|
||||
slug: "other",
|
||||
directory,
|
||||
directory: "/project",
|
||||
title: "other",
|
||||
version: "test",
|
||||
})
|
||||
|
||||
@@ -461,7 +461,7 @@ Recent work
|
||||
])
|
||||
})
|
||||
|
||||
test("lowers failed assistant reasoning to text", () => {
|
||||
test("drops provider-native continuation metadata from failed assistant turns", () => {
|
||||
const messages = toLLMMessages(
|
||||
[
|
||||
SessionMessage.Assistant.make({
|
||||
@@ -501,7 +501,7 @@ Recent work
|
||||
)
|
||||
|
||||
expect(messages[0]?.content).toEqual([
|
||||
{ type: "text", text: "Partial thought" },
|
||||
{ type: "reasoning", text: "Partial thought", providerMetadata: undefined },
|
||||
{
|
||||
type: "tool-call",
|
||||
id: "hosted-failed",
|
||||
|
||||
@@ -30,6 +30,27 @@ const outputStore = Layer.mock(ToolOutputStore.Service, {
|
||||
})
|
||||
const registryLayer = AppNodeBuilder.build(ToolRegistry.node, [[ToolOutputStore.node, outputStore]])
|
||||
const it = testEffect(registryLayer)
|
||||
const codeModeTools: ToolRegistry.CodeModeTools = {
|
||||
opencode: {
|
||||
v2: {
|
||||
health: {
|
||||
get: {
|
||||
_tag: "CodeModeTool" as const,
|
||||
description: "Get server health",
|
||||
input: Schema.Struct({}),
|
||||
output: Schema.Struct({ healthy: Schema.Boolean }),
|
||||
run: () => Effect.succeed({ healthy: true }),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
const codeModeIt = testEffect(
|
||||
AppNodeBuilder.build(ToolRegistry.node, [
|
||||
[ToolOutputStore.node, outputStore],
|
||||
ToolRegistry.codeModeReplacement(codeModeTools),
|
||||
]),
|
||||
)
|
||||
const identity = {
|
||||
agent: AgentV2.ID.make("build"),
|
||||
assistantMessageID: SessionMessage.ID.make("msg_registry"),
|
||||
@@ -53,6 +74,50 @@ const make = (permission?: string) => {
|
||||
}
|
||||
|
||||
describe("ToolRegistry", () => {
|
||||
codeModeIt.effect("includes host Code Mode trees without hosted tool registration", () =>
|
||||
Effect.gen(function* () {
|
||||
const service = yield* ToolRegistry.Service
|
||||
const definitions = yield* toolDefinitions(service)
|
||||
expect(definitions.map((tool) => tool.name)).toEqual(["execute"])
|
||||
expect(definitions[0]?.description).toContain("tools.opencode.v2.health.get")
|
||||
|
||||
expect(
|
||||
yield* executeTool(service, {
|
||||
sessionID,
|
||||
...identity,
|
||||
call: {
|
||||
type: "tool-call",
|
||||
id: "call-opencode-health",
|
||||
name: "execute",
|
||||
input: { code: "return await tools.opencode.v2.health.get({})" },
|
||||
},
|
||||
}),
|
||||
).toEqual({ type: "text", value: '{\n "healthy": true\n}' })
|
||||
}),
|
||||
)
|
||||
|
||||
codeModeIt.effect("keeps host Code Mode trees immutable while merging deferred tools", () =>
|
||||
Effect.gen(function* () {
|
||||
const service = yield* ToolRegistry.Service
|
||||
yield* service.register({ echo: make() }, { group: "opencode", deferred: true })
|
||||
|
||||
expect((yield* toolDefinitions(service))[0]?.description).toContain("tools.opencode.echo")
|
||||
expect((yield* toolDefinitions(service))[0]?.description).toContain("tools.opencode.echo")
|
||||
expect(
|
||||
yield* executeTool(service, {
|
||||
sessionID,
|
||||
...identity,
|
||||
call: {
|
||||
type: "tool-call",
|
||||
id: "call-opencode-echo",
|
||||
name: "execute",
|
||||
input: { code: 'return await tools.opencode.echo({ text: "hello" })' },
|
||||
},
|
||||
}),
|
||||
).toEqual({ type: "text", value: '{\n "text": "hello"\n}' })
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("filters disabled tools with edit aliases and ordered wildcard precedence", () =>
|
||||
Effect.gen(function* () {
|
||||
const service = yield* ToolRegistry.Service
|
||||
|
||||
+14
-6
@@ -17,12 +17,7 @@
|
||||
"tabs": [
|
||||
{
|
||||
"tab": "Docs",
|
||||
"groups": [
|
||||
{
|
||||
"group": "Get started",
|
||||
"pages": ["index", "config", "plugins", "troubleshooting"]
|
||||
}
|
||||
]
|
||||
"pages": ["index", "config", "plugins", "troubleshooting"]
|
||||
},
|
||||
{
|
||||
"tab": "SDK",
|
||||
@@ -44,6 +39,19 @@
|
||||
],
|
||||
"global": {}
|
||||
},
|
||||
"navbar": {
|
||||
"links": [
|
||||
{
|
||||
"label": "Discord",
|
||||
"href": "https://opencode.ai/discord"
|
||||
}
|
||||
],
|
||||
"primary": {
|
||||
"type": "button",
|
||||
"label": "GitHub",
|
||||
"href": "https://github.com/anomalyco/opencode"
|
||||
}
|
||||
},
|
||||
"contextual": {
|
||||
"options": ["copy", "view", "chatgpt", "claude", "mcp", "cursor", "vscode"]
|
||||
},
|
||||
|
||||
@@ -6,7 +6,6 @@ const patterns = [
|
||||
/input is too long for requested model/i,
|
||||
/exceeds the context window/i,
|
||||
/input token count.*exceeds the maximum/i,
|
||||
/tokens in request more than max tokens allowed/i,
|
||||
/maximum prompt length is \d+/i,
|
||||
/reduce the length of the messages/i,
|
||||
/maximum context length is \d+ tokens/i,
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { isContextOverflow } from "../src"
|
||||
|
||||
describe("provider error classification", () => {
|
||||
test("classifies Z.AI GLM token limit messages as context overflow", () => {
|
||||
expect(isContextOverflow("tokens in request more than max tokens allowed")).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -1448,7 +1448,6 @@ describe("session.message-v2.fromError", () => {
|
||||
"prompt is too long: 213462 tokens > 200000 maximum",
|
||||
"Your input exceeds the context window of this model",
|
||||
"The input token count (1196265) exceeds the maximum number of tokens allowed (1048575)",
|
||||
"tokens in request more than max tokens allowed",
|
||||
"Please reduce the length of the messages or completion",
|
||||
"400 status code (no body)",
|
||||
"413 status code (no body)",
|
||||
|
||||
@@ -475,7 +475,6 @@ export type TuiHostSlotMap = {
|
||||
}
|
||||
sidebar_footer: {
|
||||
session_id: string
|
||||
directory: string
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,6 @@ export const groupNames = {
|
||||
} as const
|
||||
|
||||
export const endpointNames = {
|
||||
"debug.location.evict": "evictLocation",
|
||||
"session.messages": "list",
|
||||
"integration.connect.key": "connectKey",
|
||||
"integration.connect.oauth": "connectOauth",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Location } from "@opencode-ai/schema/location"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||
import { LocationQuery, locationQueryOpenApi } from "./location.js"
|
||||
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
|
||||
export const DebugGroup = HttpApiGroup.make("server.debug")
|
||||
.add(
|
||||
@@ -15,18 +14,4 @@ export const DebugGroup = HttpApiGroup.make("server.debug")
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.delete("debug.location.evict", "/api/debug/location", {
|
||||
query: LocationQuery,
|
||||
success: HttpApiSchema.NoContent,
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.debug.location.evict",
|
||||
summary: "Evict a loaded location",
|
||||
description: "Dispose the requested location's cached services so its next use boots them fresh.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(OpenApi.annotations({ title: "debug" }))
|
||||
|
||||
@@ -19,18 +19,4 @@ export const McpGroup = HttpApiGroup.make("server.mcp")
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.get("mcp.resource.catalog", "/api/mcp/resource", {
|
||||
query: LocationQuery,
|
||||
success: Location.response(Mcp.ResourceCatalog),
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.mcp.resource.catalog",
|
||||
summary: "List MCP resources",
|
||||
description: "Retrieve resources and resource templates from connected MCP servers.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(OpenApi.annotations({ title: "mcp", description: "MCP server and resource routes." }))
|
||||
.annotateMerge(OpenApi.annotations({ title: "mcp", description: "MCP server status routes." }))
|
||||
|
||||
@@ -269,25 +269,6 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("session.move", "/api/session/:sessionID/move", {
|
||||
params: { sessionID: Session.ID },
|
||||
payload: Schema.Struct({
|
||||
destination: Schema.Struct({ directory: AbsolutePath }),
|
||||
moveChanges: Schema.Boolean.pipe(Schema.optional),
|
||||
}),
|
||||
success: HttpApiSchema.NoContent,
|
||||
error: [SessionNotFoundError, InvalidRequestError],
|
||||
})
|
||||
.middleware(sessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.move",
|
||||
summary: "Move session",
|
||||
description: "Move a session to another project directory, optionally transferring local changes.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("session.prompt", "/api/session/:sessionID/prompt", {
|
||||
params: { sessionID: Session.ID },
|
||||
@@ -363,7 +344,6 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
||||
text: Schema.String,
|
||||
description: Schema.String.pipe(Schema.optional),
|
||||
metadata: SessionMessage.Synthetic.fields.metadata,
|
||||
resume: Schema.Boolean.pipe(Schema.optional),
|
||||
}),
|
||||
success: HttpApiSchema.NoContent,
|
||||
error: SessionNotFoundError,
|
||||
|
||||
@@ -4,6 +4,5 @@ import { isOpenCodeEvent } from "../src/groups/event.js"
|
||||
test("classifies public events by type", () => {
|
||||
expect(isOpenCodeEvent({ type: "server.connected" })).toBe(true)
|
||||
expect(isOpenCodeEvent({ type: "mcp.status.changed" })).toBe(true)
|
||||
expect(isOpenCodeEvent({ type: "mcp.resources.changed" })).toBe(true)
|
||||
expect(isOpenCodeEvent({ type: "mcp.tools.changed" })).toBe(false)
|
||||
})
|
||||
|
||||
@@ -80,7 +80,6 @@ export const ServerDefinitions = Event.inventory(
|
||||
...InstallationEvent.Definitions,
|
||||
...VcsEvent.Definitions,
|
||||
McpEvent.StatusChanged,
|
||||
McpEvent.ResourcesChanged,
|
||||
// Shared transitional: V1 contracts the current TUI still consumes during
|
||||
// the migration (permission.asked/replied, question.asked, session.error).
|
||||
// Remove when the TUI moves to the current permission/question surfaces.
|
||||
|
||||
@@ -9,7 +9,6 @@ export { Form } from "./form.js"
|
||||
export { Integration } from "./integration.js"
|
||||
export { LLM } from "./llm.js"
|
||||
export { Location } from "./location.js"
|
||||
export { Mcp } from "./mcp.js"
|
||||
export { Model } from "./model.js"
|
||||
export { Permission } from "./permission.js"
|
||||
export { PermissionSaved } from "./permission-saved.js"
|
||||
|
||||
@@ -10,13 +10,6 @@ export const ToolsChanged = Event.ephemeral({
|
||||
},
|
||||
})
|
||||
|
||||
export const ResourcesChanged = Event.ephemeral({
|
||||
type: "mcp.resources.changed",
|
||||
schema: {
|
||||
server: Schema.String,
|
||||
},
|
||||
})
|
||||
|
||||
export const BrowserOpenFailed = Event.ephemeral({
|
||||
type: "mcp.browser.open.failed",
|
||||
schema: {
|
||||
@@ -34,4 +27,4 @@ export const StatusChanged = Event.ephemeral({
|
||||
},
|
||||
})
|
||||
|
||||
export const Definitions = Event.inventory(ToolsChanged, ResourcesChanged, StatusChanged)
|
||||
export const Definitions = Event.inventory(ToolsChanged, StatusChanged)
|
||||
|
||||
@@ -25,9 +25,14 @@ const NeedsClientRegistration = Schema.Struct({
|
||||
}).annotate({ identifier: "Mcp.Status.NeedsClientRegistration" })
|
||||
|
||||
export type Status = typeof Status.Type
|
||||
export const Status = Schema.Union([Connected, Pending, Disabled, Failed, NeedsAuth, NeedsClientRegistration]).pipe(
|
||||
Schema.toTaggedUnion("status"),
|
||||
)
|
||||
export const Status = Schema.Union([
|
||||
Connected,
|
||||
Pending,
|
||||
Disabled,
|
||||
Failed,
|
||||
NeedsAuth,
|
||||
NeedsClientRegistration,
|
||||
]).pipe(Schema.toTaggedUnion("status"))
|
||||
|
||||
export interface Server extends Schema.Schema.Type<typeof Server> {}
|
||||
export const Server = Schema.Struct({
|
||||
@@ -37,77 +42,3 @@ export const Server = Schema.Struct({
|
||||
// without matching by name, which could collide with provider or plugin integrations.
|
||||
integrationID: optional(IntegrationID),
|
||||
}).annotate({ identifier: "Mcp.Server" })
|
||||
|
||||
export interface ResourceReference extends Schema.Schema.Type<typeof ResourceReference> {}
|
||||
export const ResourceReference = Schema.Struct({
|
||||
server: Schema.String,
|
||||
uri: Schema.String,
|
||||
}).annotate({ identifier: "Mcp.ResourceReference" })
|
||||
|
||||
export function resourceUri(input: ResourceReference) {
|
||||
const url = new URL("mcp://resource")
|
||||
url.searchParams.set("server", input.server)
|
||||
url.searchParams.set("uri", input.uri)
|
||||
return url.href
|
||||
}
|
||||
|
||||
export function parseResourceUri(input: string) {
|
||||
try {
|
||||
const url = new URL(input)
|
||||
if (url.protocol !== "mcp:" || url.hostname !== "resource" || url.pathname || url.hash) return
|
||||
const server = url.searchParams.get("server")
|
||||
const uri = url.searchParams.get("uri")
|
||||
if (!server || !uri) return
|
||||
const reference = ResourceReference.make({ server, uri })
|
||||
return resourceUri(reference) === input ? reference : undefined
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
export interface Resource extends Schema.Schema.Type<typeof Resource> {}
|
||||
export const Resource = Schema.Struct({
|
||||
server: Schema.String,
|
||||
name: Schema.String,
|
||||
uri: Schema.String,
|
||||
description: optional(Schema.String),
|
||||
mimeType: optional(Schema.String),
|
||||
}).annotate({ identifier: "Mcp.Resource" })
|
||||
|
||||
export interface ResourceTemplate extends Schema.Schema.Type<typeof ResourceTemplate> {}
|
||||
export const ResourceTemplate = Schema.Struct({
|
||||
server: Schema.String,
|
||||
name: Schema.String,
|
||||
uriTemplate: Schema.String,
|
||||
description: optional(Schema.String),
|
||||
mimeType: optional(Schema.String),
|
||||
}).annotate({ identifier: "Mcp.ResourceTemplate" })
|
||||
|
||||
export interface ResourceCatalog extends Schema.Schema.Type<typeof ResourceCatalog> {}
|
||||
export const ResourceCatalog = Schema.Struct({
|
||||
resources: Schema.Array(Resource),
|
||||
templates: Schema.Array(ResourceTemplate),
|
||||
}).annotate({ identifier: "Mcp.ResourceCatalog" })
|
||||
|
||||
export const ResourceContentPart = Schema.Union([
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("text"),
|
||||
uri: Schema.String,
|
||||
text: Schema.String,
|
||||
mimeType: optional(Schema.String),
|
||||
}),
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("blob"),
|
||||
uri: Schema.String,
|
||||
blob: Schema.String,
|
||||
mimeType: optional(Schema.String),
|
||||
}),
|
||||
]).pipe(Schema.toTaggedUnion("type"), Schema.annotate({ identifier: "Mcp.ResourceContentPart" }))
|
||||
export type ResourceContentPart = typeof ResourceContentPart.Type
|
||||
|
||||
export interface ResourceContent extends Schema.Schema.Type<typeof ResourceContent> {}
|
||||
export const ResourceContent = Schema.Struct({
|
||||
server: Schema.String,
|
||||
uri: Schema.String,
|
||||
contents: Schema.Array(ResourceContentPart),
|
||||
}).annotate({ identifier: "Mcp.ResourceContent" })
|
||||
|
||||
@@ -2,7 +2,6 @@ import { describe, expect, test } from "bun:test"
|
||||
import { DateTime, Schema } from "effect"
|
||||
import { Agent } from "../src/agent.js"
|
||||
import { FileSystem } from "../src/filesystem.js"
|
||||
import { Mcp } from "../src/mcp.js"
|
||||
import { Model } from "../src/model.js"
|
||||
import { Project } from "../src/project.js"
|
||||
import { Provider } from "../src/provider.js"
|
||||
@@ -52,11 +51,6 @@ describe("contract hygiene", () => {
|
||||
const identifiers = [
|
||||
Agent.Color,
|
||||
FileSystem.Submatch,
|
||||
Mcp.Resource,
|
||||
Mcp.ResourceTemplate,
|
||||
Mcp.ResourceCatalog,
|
||||
Mcp.ResourceContentPart,
|
||||
Mcp.ResourceContent,
|
||||
Model.Ref,
|
||||
Model.Capabilities,
|
||||
Model.Cost,
|
||||
|
||||
@@ -51,7 +51,6 @@ describe("public event manifest", () => {
|
||||
expect(EventManifest.Latest.get("agent.updated")).toBe(Agent.Event.Updated)
|
||||
expect(EventManifest.Latest.get("plugin.updated")).toBe(Plugin.Event.Updated)
|
||||
expect(EventManifest.Server.get("mcp.status.changed")).toBe(McpEvent.StatusChanged)
|
||||
expect(EventManifest.Server.get("mcp.resources.changed")).toBe(McpEvent.ResourcesChanged)
|
||||
expect(EventManifest.Server.get("session.deleted")).toBe(SessionEvent.Deleted)
|
||||
expect(EventManifest.Server.has("mcp.tools.changed")).toBe(false)
|
||||
expect(Agent.Event.Updated.durable).toBeUndefined()
|
||||
@@ -77,7 +76,7 @@ describe("public event manifest", () => {
|
||||
expect(Form.Event.Definitions).toEqual([Form.Event.Created, Form.Event.Replied, Form.Event.Cancelled])
|
||||
expect(Reference.Event.Definitions).toEqual([Reference.Event.Updated])
|
||||
expect(Plugin.Event.Definitions).toEqual([Plugin.Event.Added, Plugin.Event.Updated])
|
||||
expect(McpEvent.Definitions).toEqual([McpEvent.ToolsChanged, McpEvent.ResourcesChanged, McpEvent.StatusChanged])
|
||||
expect(McpEvent.Definitions).toEqual([McpEvent.ToolsChanged, McpEvent.StatusChanged])
|
||||
expect(EventManifest.Latest.has("mcp.browser.open.failed")).toBe(false)
|
||||
expect(EventManifest.Latest.has("ide.installed")).toBe(false)
|
||||
expect(IdeEvent.Definitions).toEqual([IdeEvent.Installed])
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Schema } from "effect"
|
||||
import { Mcp } from "../src/mcp.js"
|
||||
|
||||
describe("Mcp resources", () => {
|
||||
test("round trips canonical resource attachment URIs", () => {
|
||||
const reference = { server: "Docs & Search", uri: "docs://guide/chapter?q=one two" }
|
||||
const uri = Mcp.resourceUri(reference)
|
||||
|
||||
expect(uri).toBe("mcp://resource?server=Docs+%26+Search&uri=docs%3A%2F%2Fguide%2Fchapter%3Fq%3Done+two")
|
||||
expect(Mcp.parseResourceUri(uri)).toEqual(reference)
|
||||
expect(Mcp.parseResourceUri(`${uri}&extra=true`)).toBeUndefined()
|
||||
expect(Mcp.parseResourceUri("docs://guide")).toBeUndefined()
|
||||
})
|
||||
|
||||
test("decodes resource catalogs and omits absent metadata", () => {
|
||||
const value = Schema.decodeUnknownSync(Mcp.ResourceCatalog)({
|
||||
resources: [{ server: "docs", name: "Readme", uri: "docs://readme" }],
|
||||
templates: [{ server: "docs", name: "File", uriTemplate: "docs://{path}" }],
|
||||
})
|
||||
|
||||
expect(Schema.encodeSync(Mcp.ResourceCatalog)(value)).toEqual({
|
||||
resources: [{ server: "docs", name: "Readme", uri: "docs://readme" }],
|
||||
templates: [{ server: "docs", name: "File", uriTemplate: "docs://{path}" }],
|
||||
})
|
||||
})
|
||||
|
||||
test("preserves text and base64 blob contents", () => {
|
||||
expect(
|
||||
Schema.decodeUnknownSync(Mcp.ResourceContent)({
|
||||
server: "docs",
|
||||
uri: "docs://readme",
|
||||
contents: [
|
||||
{ type: "text", uri: "docs://readme", text: "hello", mimeType: "text/plain" },
|
||||
{ type: "blob", uri: "docs://logo", blob: "aGVsbG8=", mimeType: "image/png" },
|
||||
],
|
||||
}),
|
||||
).toEqual({
|
||||
server: "docs",
|
||||
uri: "docs://readme",
|
||||
contents: [
|
||||
{ type: "text", uri: "docs://readme", text: "hello", mimeType: "text/plain" },
|
||||
{ type: "blob", uri: "docs://logo", blob: "aGVsbG8=", mimeType: "image/png" },
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,23 +1,47 @@
|
||||
import { OpenCode } from "@opencode-ai/client/effect"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
||||
import { SdkPlugins } from "@opencode-ai/core/plugin/sdk"
|
||||
import { Project } from "@opencode-ai/core/project"
|
||||
import { createEmbeddedRoutes } from "@opencode-ai/server/routes"
|
||||
import { Context, Effect, Layer, ManagedRuntime } from "effect"
|
||||
import { FetchHttpClient, HttpEffect, HttpRouter, HttpServer } from "effect/unstable/http"
|
||||
import { Context, Effect, Layer, Scope } from "effect"
|
||||
import { FetchHttpClient, HttpRouter, HttpServer } from "effect/unstable/http"
|
||||
|
||||
export const create = Effect.fn("OpenCode.create")(function* () {
|
||||
const runtime = yield* Effect.acquireRelease(
|
||||
Effect.sync(() => ManagedRuntime.make(createEmbeddedRoutes().pipe(Layer.provide(HttpServer.layerServices)))),
|
||||
(runtime) => runtime.disposeEffect,
|
||||
const scope = yield* Scope.Scope
|
||||
const memoMap = yield* Layer.makeMemoMap
|
||||
const sdkPlugins = SdkPlugins.makeStore()
|
||||
const context = yield* Layer.buildWithMemoMap(
|
||||
AppNodeBuilder.build(LayerNode.group([EventV2.node, PermissionSaved.node, Project.node, SdkPlugins.node]), [
|
||||
[SdkPlugins.node, SdkPlugins.layerWithStore(sdkPlugins)],
|
||||
]),
|
||||
memoMap,
|
||||
scope,
|
||||
)
|
||||
const context = yield* runtime.contextEffect
|
||||
const plugins = Context.get(context, SdkPlugins.Service)
|
||||
const router = Context.get(context, HttpRouter.HttpRouter)
|
||||
const handler = HttpEffect.toWebHandler(router.asHttpEffect())
|
||||
const fetch = Object.assign((input: RequestInfo | URL, init?: RequestInit) => handler(new Request(input, init)), {
|
||||
const permissions = Context.get(context, PermissionSaved.Service)
|
||||
const project = Context.get(context, Project.Service)
|
||||
const web = yield* Effect.acquireRelease(
|
||||
Effect.sync(() =>
|
||||
HttpRouter.toWebHandler(
|
||||
createEmbeddedRoutes(sdkPlugins).pipe(
|
||||
HttpRouter.provideRequest(Layer.succeed(PermissionSaved.Service, permissions)),
|
||||
HttpRouter.provideRequest(Layer.succeed(Project.Service, project)),
|
||||
Layer.provide(HttpServer.layerServices),
|
||||
),
|
||||
{ disableLogger: true, memoMap },
|
||||
),
|
||||
),
|
||||
(web) => Effect.promise(web.dispose),
|
||||
)
|
||||
const fetch = Object.assign((input: RequestInfo | URL, init?: RequestInit) => web.handler(new Request(input, init)), {
|
||||
preconnect: () => undefined,
|
||||
}) satisfies typeof globalThis.fetch
|
||||
const client = yield* OpenCode.make({ baseUrl: "http://opencode.local" }).pipe(
|
||||
Effect.provide(FetchHttpClient.layer.pipe(Layer.provide(Layer.succeed(FetchHttpClient.Fetch, fetch)), Layer.fresh)),
|
||||
Effect.provide(FetchHttpClient.layer),
|
||||
Effect.provideService(FetchHttpClient.Fetch, fetch),
|
||||
)
|
||||
return {
|
||||
...client,
|
||||
|
||||
@@ -105,50 +105,6 @@ it.live(
|
||||
25_000,
|
||||
)
|
||||
|
||||
it.live(
|
||||
"preserves SDK plugins across Location eviction",
|
||||
() =>
|
||||
withEmbedded("opencode-embedded-plugin-eviction-", (fixture) =>
|
||||
Effect.gen(function* () {
|
||||
const opencode = yield* fixture.sdk.OpenCode.create()
|
||||
const ref = location(fixture)
|
||||
const connected = yield* Latch.make(false)
|
||||
const booted = yield* Deferred.make<void>()
|
||||
// The rebooted Location commits its second plugin generation.
|
||||
const recommitted = yield* Deferred.make<void>()
|
||||
const generations = yield* Ref.make(0)
|
||||
const id = `evicted-sdk-${crypto.randomUUID()}`
|
||||
|
||||
yield* opencode.events.subscribe().pipe(
|
||||
Stream.runForEach((event) => {
|
||||
if (event.type === "server.connected") return connected.open
|
||||
if (event.type !== "plugin.updated" || event.location?.directory !== fixture.directory) return Effect.void
|
||||
return Ref.updateAndGet(generations, (total) => total + 1).pipe(
|
||||
Effect.flatMap((total) => {
|
||||
if (total === 1) return Deferred.succeed(booted, undefined)
|
||||
if (total === 2) return Deferred.succeed(recommitted, undefined)
|
||||
return Effect.void
|
||||
}),
|
||||
Effect.asVoid,
|
||||
)
|
||||
}),
|
||||
Effect.forkScoped,
|
||||
)
|
||||
yield* connected.await
|
||||
yield* opencode.plugin({ id, effect: () => Effect.void })
|
||||
|
||||
yield* opencode.plugin.list({ location: ref })
|
||||
yield* Deferred.await(booted).pipe(Effect.timeout("5 seconds"))
|
||||
yield* opencode.debug.evictLocation({ location: ref })
|
||||
yield* opencode.plugin.list({ location: ref })
|
||||
yield* Deferred.await(recommitted).pipe(Effect.timeout("5 seconds"))
|
||||
|
||||
expect((yield* opencode.plugin.list({ location: ref })).data.map((plugin) => String(plugin.id))).toContain(id)
|
||||
}),
|
||||
),
|
||||
15_000,
|
||||
)
|
||||
|
||||
it.live(
|
||||
"keeps SDK plugin registration isolated between embedded hosts",
|
||||
() =>
|
||||
|
||||
@@ -277,8 +277,6 @@ import type {
|
||||
V2CredentialUpdateErrors,
|
||||
V2CredentialUpdateResponses,
|
||||
V2DebugLocationErrors,
|
||||
V2DebugLocationEvictErrors,
|
||||
V2DebugLocationEvictResponses,
|
||||
V2DebugLocationResponses,
|
||||
V2EventSubscribeErrors,
|
||||
V2EventSubscribeResponses,
|
||||
@@ -312,8 +310,6 @@ import type {
|
||||
V2LocationGetResponses,
|
||||
V2McpListErrors,
|
||||
V2McpListResponses,
|
||||
V2McpResourceCatalogErrors,
|
||||
V2McpResourceCatalogResponses,
|
||||
V2ModelDefaultErrors,
|
||||
V2ModelDefaultResponses,
|
||||
V2ModelListErrors,
|
||||
@@ -6243,7 +6239,6 @@ export class Session3 extends HeyApiClient {
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
resume?: boolean | null
|
||||
},
|
||||
options?: Options<never, ThrowOnError>,
|
||||
) {
|
||||
@@ -6256,7 +6251,6 @@ export class Session3 extends HeyApiClient {
|
||||
{ in: "body", key: "text" },
|
||||
{ in: "body", key: "description" },
|
||||
{ in: "body", key: "metadata" },
|
||||
{ in: "body", key: "resume" },
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -6975,34 +6969,6 @@ export class Integration extends HeyApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
export class Resource2 extends HeyApiClient {
|
||||
/**
|
||||
* List MCP resources
|
||||
*
|
||||
* Retrieve resources and resource templates from connected MCP servers.
|
||||
*/
|
||||
public catalog<ThrowOnError extends boolean = false>(
|
||||
parameters?: {
|
||||
location?: {
|
||||
directory?: string | null
|
||||
workspace?: string | null
|
||||
} | null
|
||||
},
|
||||
options?: Options<never, ThrowOnError>,
|
||||
) {
|
||||
const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }])
|
||||
return (options?.client ?? this.client).get<
|
||||
V2McpResourceCatalogResponses,
|
||||
V2McpResourceCatalogErrors,
|
||||
ThrowOnError
|
||||
>({
|
||||
url: "/api/mcp/resource",
|
||||
...options,
|
||||
...params,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class Mcp2 extends HeyApiClient {
|
||||
/**
|
||||
* List MCP servers
|
||||
@@ -7025,11 +6991,6 @@ export class Mcp2 extends HeyApiClient {
|
||||
...params,
|
||||
})
|
||||
}
|
||||
|
||||
private _resource?: Resource2
|
||||
get resource(): Resource2 {
|
||||
return (this._resource ??= new Resource2({ client: this.client }))
|
||||
}
|
||||
}
|
||||
|
||||
export class Credential extends HeyApiClient {
|
||||
@@ -8156,34 +8117,6 @@ export class Vcs2 extends HeyApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
export class Location2 extends HeyApiClient {
|
||||
/**
|
||||
* Evict a loaded location
|
||||
*
|
||||
* Dispose the requested location's cached services so its next use boots them fresh.
|
||||
*/
|
||||
public evict<ThrowOnError extends boolean = false>(
|
||||
parameters?: {
|
||||
location?: {
|
||||
directory?: string | null
|
||||
workspace?: string | null
|
||||
} | null
|
||||
},
|
||||
options?: Options<never, ThrowOnError>,
|
||||
) {
|
||||
const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }])
|
||||
return (options?.client ?? this.client).delete<
|
||||
V2DebugLocationEvictResponses,
|
||||
V2DebugLocationEvictErrors,
|
||||
ThrowOnError
|
||||
>({
|
||||
url: "/api/debug/location",
|
||||
...options,
|
||||
...params,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class Debug extends HeyApiClient {
|
||||
/**
|
||||
* List loaded locations
|
||||
@@ -8196,11 +8129,6 @@ export class Debug extends HeyApiClient {
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
private _location?: Location2
|
||||
get location2(): Location2 {
|
||||
return (this._location ??= new Location2({ client: this.client }))
|
||||
}
|
||||
}
|
||||
|
||||
export class V2 extends HeyApiClient {
|
||||
|
||||
@@ -95,7 +95,6 @@ export type Event =
|
||||
| EventTuiToastShow2
|
||||
| EventTuiSessionSelect2
|
||||
| EventMcpToolsChanged
|
||||
| EventMcpResourcesChanged
|
||||
| EventMcpStatusChanged
|
||||
| EventCommandExecuted
|
||||
| EventFileEdited
|
||||
@@ -1604,13 +1603,6 @@ export type GlobalEvent = {
|
||||
server: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
id: string
|
||||
type: "mcp.resources.changed"
|
||||
properties: {
|
||||
server: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
id: string
|
||||
type: "mcp.status.changed"
|
||||
@@ -2999,14 +2991,6 @@ export type ProviderNotFoundError = {
|
||||
message: string
|
||||
}
|
||||
|
||||
export type McpResource2 = {
|
||||
server: string
|
||||
name: string
|
||||
uri: string
|
||||
description?: string
|
||||
mimeType?: string
|
||||
}
|
||||
|
||||
export type FormNotFoundError = {
|
||||
_tag: "FormNotFoundError"
|
||||
id: string
|
||||
@@ -3169,7 +3153,6 @@ export type V2Event =
|
||||
| TuiToastShow
|
||||
| TuiSessionSelect
|
||||
| McpToolsChanged
|
||||
| McpResourcesChanged
|
||||
| McpStatusChanged
|
||||
| CommandExecuted
|
||||
| FileEdited
|
||||
@@ -5719,19 +5702,6 @@ export type McpServer = {
|
||||
integrationID?: string
|
||||
}
|
||||
|
||||
export type McpResourceTemplate = {
|
||||
server: string
|
||||
name: string
|
||||
uriTemplate: string
|
||||
description?: string
|
||||
mimeType?: string
|
||||
}
|
||||
|
||||
export type McpResourceCatalog = {
|
||||
resources: Array<McpResource2>
|
||||
templates: Array<McpResourceTemplate>
|
||||
}
|
||||
|
||||
export type ProjectCurrent = {
|
||||
id: string
|
||||
directory: string
|
||||
@@ -6638,19 +6608,6 @@ export type McpToolsChanged = {
|
||||
}
|
||||
}
|
||||
|
||||
export type McpResourcesChanged = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
type: "mcp.resources.changed"
|
||||
location?: LocationRef
|
||||
data: {
|
||||
server: string
|
||||
}
|
||||
}
|
||||
|
||||
export type McpStatusChanged = {
|
||||
id: string
|
||||
created: number
|
||||
@@ -7803,14 +7760,6 @@ export type EventMcpToolsChanged = {
|
||||
}
|
||||
}
|
||||
|
||||
export type EventMcpResourcesChanged = {
|
||||
id: string
|
||||
type: "mcp.resources.changed"
|
||||
properties: {
|
||||
server: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventMcpStatusChanged = {
|
||||
id: string
|
||||
type: "mcp.status.changed"
|
||||
@@ -8648,7 +8597,6 @@ export type V2EventV2 =
|
||||
| InstallationUpdateAvailableV2
|
||||
| VcsBranchUpdatedV2
|
||||
| McpStatusChangedV2
|
||||
| McpResourcesChangedV2
|
||||
| PermissionAskedV2
|
||||
| PermissionRepliedV2
|
||||
| QuestionAskedV2
|
||||
@@ -9738,19 +9686,6 @@ export type EventLogSyncedV2 = {
|
||||
seq?: number
|
||||
}
|
||||
|
||||
export type McpResourceV2 = {
|
||||
server: string
|
||||
name: string
|
||||
uri: string
|
||||
description?: string
|
||||
mimeType?: string
|
||||
}
|
||||
|
||||
export type McpResourceCatalogV2 = {
|
||||
resources: Array<McpResourceV2>
|
||||
templates: Array<McpResourceTemplate>
|
||||
}
|
||||
|
||||
export type ProjectTimeV2 = {
|
||||
created: number
|
||||
updated: number
|
||||
@@ -10680,19 +10615,6 @@ export type McpStatusChangedV2 = {
|
||||
}
|
||||
}
|
||||
|
||||
export type McpResourcesChangedV2 = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
type: "mcp.resources.changed"
|
||||
location?: LocationRefV2
|
||||
data: {
|
||||
server: string
|
||||
}
|
||||
}
|
||||
|
||||
export type PermissionAskedV2 = {
|
||||
id: string
|
||||
created: number
|
||||
@@ -15596,7 +15518,6 @@ export type V2SessionSyntheticData = {
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
resume?: boolean | null
|
||||
}
|
||||
path: {
|
||||
sessionID: string
|
||||
@@ -16764,43 +16685,6 @@ export type V2McpListResponses = {
|
||||
|
||||
export type V2McpListResponse = V2McpListResponses[keyof V2McpListResponses]
|
||||
|
||||
export type V2McpResourceCatalogData = {
|
||||
body?: never
|
||||
path?: never
|
||||
query?: {
|
||||
location?: {
|
||||
directory?: string | null
|
||||
workspace?: string | null
|
||||
} | null
|
||||
}
|
||||
url: "/api/mcp/resource"
|
||||
}
|
||||
|
||||
export type V2McpResourceCatalogErrors = {
|
||||
/**
|
||||
* InvalidRequestError
|
||||
*/
|
||||
400: InvalidRequestErrorV2
|
||||
/**
|
||||
* UnauthorizedError
|
||||
*/
|
||||
401: UnauthorizedError
|
||||
}
|
||||
|
||||
export type V2McpResourceCatalogError = V2McpResourceCatalogErrors[keyof V2McpResourceCatalogErrors]
|
||||
|
||||
export type V2McpResourceCatalogResponses = {
|
||||
/**
|
||||
* Success
|
||||
*/
|
||||
200: {
|
||||
location: LocationInfoV2
|
||||
data: McpResourceCatalogV2
|
||||
}
|
||||
}
|
||||
|
||||
export type V2McpResourceCatalogResponse = V2McpResourceCatalogResponses[keyof V2McpResourceCatalogResponses]
|
||||
|
||||
export type V2CredentialRemoveData = {
|
||||
body?: never
|
||||
path: {
|
||||
@@ -18665,40 +18549,6 @@ export type V2VcsDiffResponses = {
|
||||
|
||||
export type V2VcsDiffResponse = V2VcsDiffResponses[keyof V2VcsDiffResponses]
|
||||
|
||||
export type V2DebugLocationEvictData = {
|
||||
body?: never
|
||||
path?: never
|
||||
query?: {
|
||||
location?: {
|
||||
directory?: string | null
|
||||
workspace?: string | null
|
||||
} | null
|
||||
}
|
||||
url: "/api/debug/location"
|
||||
}
|
||||
|
||||
export type V2DebugLocationEvictErrors = {
|
||||
/**
|
||||
* InvalidRequestError
|
||||
*/
|
||||
400: InvalidRequestErrorV2
|
||||
/**
|
||||
* UnauthorizedError
|
||||
*/
|
||||
401: UnauthorizedError
|
||||
}
|
||||
|
||||
export type V2DebugLocationEvictError = V2DebugLocationEvictErrors[keyof V2DebugLocationEvictErrors]
|
||||
|
||||
export type V2DebugLocationEvictResponses = {
|
||||
/**
|
||||
* <No Content>
|
||||
*/
|
||||
204: void
|
||||
}
|
||||
|
||||
export type V2DebugLocationEvictResponse = V2DebugLocationEvictResponses[keyof V2DebugLocationEvictResponses]
|
||||
|
||||
export type V2DebugLocationData = {
|
||||
body?: never
|
||||
path?: never
|
||||
|
||||
@@ -2,24 +2,13 @@ import { LocationServiceMap } from "@opencode-ai/core/location-service-map"
|
||||
import { Effect, Option, RcMap } from "effect"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import { Api } from "../api"
|
||||
import { requestRef } from "../location"
|
||||
|
||||
export const DebugHandler = HttpApiBuilder.group(Api, "server.debug", (handlers) =>
|
||||
handlers
|
||||
.handle(
|
||||
"debug.location",
|
||||
Effect.fn(function* () {
|
||||
const locations = Option.getOrThrow(yield* Effect.serviceOption(LocationServiceMap.Service))
|
||||
return Array.from(yield* RcMap.keys(locations.rcMap))
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"debug.location.evict",
|
||||
Effect.fn(function* (ctx) {
|
||||
const locations = Option.getOrThrow(yield* Effect.serviceOption(LocationServiceMap.Service))
|
||||
// Resolve through requestRef so the key matches the shape the location
|
||||
// middleware cached the services under.
|
||||
yield* locations.invalidate(requestRef(ctx.request))
|
||||
}),
|
||||
),
|
||||
handlers.handle(
|
||||
"debug.location",
|
||||
Effect.fn(function* () {
|
||||
const locations = Option.getOrThrow(yield* Effect.serviceOption(LocationServiceMap.Service))
|
||||
return Array.from(yield* RcMap.keys(locations.rcMap))
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -6,28 +6,20 @@ import { response } from "../location"
|
||||
|
||||
export const McpHandler = HttpApiBuilder.group(Api, "server.mcp", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
return handlers
|
||||
.handle(
|
||||
"mcp.list",
|
||||
Effect.fn(function* () {
|
||||
const service = yield* MCP.Service
|
||||
return yield* response(
|
||||
service
|
||||
.servers()
|
||||
.pipe(
|
||||
Effect.map((servers) =>
|
||||
servers.map((info) => ({ name: info.name, status: info.status, integrationID: info.integrationID })),
|
||||
),
|
||||
return handlers.handle(
|
||||
"mcp.list",
|
||||
Effect.fn(function* () {
|
||||
const service = yield* MCP.Service
|
||||
return yield* response(
|
||||
service
|
||||
.servers()
|
||||
.pipe(
|
||||
Effect.map((servers) =>
|
||||
servers.map((info) => ({ name: info.name, status: info.status, integrationID: info.integrationID })),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"mcp.resource.catalog",
|
||||
Effect.fn(function* () {
|
||||
const service = yield* MCP.Service
|
||||
return yield* response(service.resourceCatalog())
|
||||
}),
|
||||
)
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { InstructionEntry } from "@opencode-ai/core/session/instruction-entry"
|
||||
import { MoveSession } from "@opencode-ai/core/control-plane/move-session"
|
||||
import { DateTime, Effect, Stream } from "effect"
|
||||
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
||||
import { Api } from "../api"
|
||||
@@ -9,8 +8,8 @@ import {
|
||||
ConflictError,
|
||||
CommandEvaluationError,
|
||||
CommandNotFoundError,
|
||||
InvalidRequestError,
|
||||
InvalidCursorError,
|
||||
InvalidRequestError,
|
||||
MessageNotFoundError,
|
||||
ServiceUnavailableError,
|
||||
SessionBusyError,
|
||||
@@ -25,7 +24,6 @@ const DefaultSessionsLimit = 50
|
||||
export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* SessionV2.Service
|
||||
const moveSession = yield* MoveSession.Service
|
||||
|
||||
return handlers
|
||||
.handle(
|
||||
@@ -203,43 +201,6 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"session.move",
|
||||
Effect.fn(function* (ctx) {
|
||||
yield* moveSession.moveSession({
|
||||
sessionID: ctx.params.sessionID,
|
||||
destination: ctx.payload.destination,
|
||||
moveChanges: ctx.payload.moveChanges,
|
||||
}).pipe(
|
||||
Effect.catchTag("Session.NotFoundError", (error) =>
|
||||
Effect.fail(
|
||||
new SessionNotFoundError({
|
||||
sessionID: error.sessionID,
|
||||
message: `Session not found: ${error.sessionID}`,
|
||||
}),
|
||||
),
|
||||
),
|
||||
Effect.catchTag("MoveSession.DestinationProjectMismatchError", () =>
|
||||
Effect.fail(new InvalidRequestError({ message: "Destination directory belongs to another project" })),
|
||||
),
|
||||
Effect.catchTag("MoveSession.ApplyChangesError", () =>
|
||||
Effect.fail(
|
||||
new InvalidRequestError({
|
||||
message:
|
||||
"Unable to apply your changes in the destination directory. The files may conflict with existing changes.",
|
||||
}),
|
||||
),
|
||||
),
|
||||
Effect.catchTag("MoveSession.CaptureChangesError", (error) =>
|
||||
Effect.fail(new InvalidRequestError({ message: error.message })),
|
||||
),
|
||||
Effect.catchTag("MoveSession.ResetSourceChangesError", (error) =>
|
||||
Effect.fail(new InvalidRequestError({ message: error.message })),
|
||||
),
|
||||
)
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"session.prompt",
|
||||
Effect.fn(function* (ctx) {
|
||||
@@ -368,7 +329,6 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
||||
text: ctx.payload.text,
|
||||
description: ctx.payload.description,
|
||||
metadata: ctx.payload.metadata,
|
||||
resume: ctx.payload.resume,
|
||||
})
|
||||
.pipe(
|
||||
Effect.catchTag("Session.NotFoundError", (error) =>
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
export * as ServerProcess from "./process"
|
||||
|
||||
import { NodeHttpClient, NodeHttpServer } from "@effect/platform-node"
|
||||
import { Credential } from "@opencode-ai/core/credential"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
||||
import { Project } from "@opencode-ai/core/project"
|
||||
import { HealthGroup } from "@opencode-ai/protocol/groups/health"
|
||||
import { Context, Effect, Layer, Option } from "effect"
|
||||
import { HttpClient, HttpClientRequest, HttpMiddleware, HttpRouter, HttpServer } from "effect/unstable/http"
|
||||
import { HttpClient, HttpClientRequest, HttpRouter, HttpServer } from "effect/unstable/http"
|
||||
import { HttpApi, HttpApiClient } from "effect/unstable/httpapi"
|
||||
import { createServer } from "node:http"
|
||||
import { createServer, type Server } from "node:http"
|
||||
import { ServerAuth } from "./auth"
|
||||
import { createRoutes } from "./routes"
|
||||
|
||||
@@ -13,6 +18,7 @@ export type Options = {
|
||||
readonly hostname: string
|
||||
readonly port: Option.Option<number>
|
||||
readonly password: string
|
||||
readonly replacements?: (server: Server) => LayerNode.Replacements
|
||||
}
|
||||
|
||||
const ReadinessApi = HttpApi.make("readiness").add(HealthGroup)
|
||||
@@ -37,22 +43,22 @@ export const start = Effect.fn("ServerProcess.start")(function* (options: Option
|
||||
})
|
||||
|
||||
function listen(options: Options) {
|
||||
if (Option.isSome(options.port)) return bind(options.hostname, options.port.value, options.password)
|
||||
if (Option.isSome(options.port)) return bind(options, options.port.value)
|
||||
const next = (port: number): ReturnType<typeof bind> =>
|
||||
bind(options.hostname, port, options.password).pipe(
|
||||
bind(options, port).pipe(
|
||||
Effect.catch((error) => (port === 65_535 ? Effect.fail(error) : next(port + 1))),
|
||||
)
|
||||
return next(4096)
|
||||
}
|
||||
|
||||
function bind(hostname: string, port: number, password: string) {
|
||||
function bind(options: Options, port: number) {
|
||||
const server = createServer()
|
||||
return Layer.build(
|
||||
createRoutes(password).pipe(
|
||||
Layer.flatMap((context) =>
|
||||
HttpServer.serve(Context.get(context, HttpRouter.HttpRouter).asHttpEffect(), HttpMiddleware.logger),
|
||||
),
|
||||
Layer.provideMerge(NodeHttpServer.layer(() => server, { port, host: hostname })),
|
||||
HttpRouter.serve(createRoutes(options.password, options.replacements?.(server)), {
|
||||
disableListenLog: true,
|
||||
}).pipe(
|
||||
Layer.provideMerge(NodeHttpServer.layer(() => server, { port, host: options.hostname })),
|
||||
Layer.provide(AppNodeBuilder.build(LayerNode.group([Credential.node, PermissionSaved.node, Project.node]))),
|
||||
),
|
||||
).pipe(
|
||||
Effect.tap(() => Effect.addFinalizer(() => Effect.sync(() => server.closeAllConnections()))),
|
||||
|
||||
@@ -8,7 +8,6 @@ import { Observability } from "@opencode-ai/core/observability"
|
||||
import { Credential } from "@opencode-ai/core/credential"
|
||||
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
||||
import { PtyTicket } from "@opencode-ai/core/pty/ticket"
|
||||
import { MoveSession } from "@opencode-ai/core/control-plane/move-session"
|
||||
import { Project } from "@opencode-ai/core/project"
|
||||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
||||
@@ -20,7 +19,7 @@ import { SdkPlugins } from "@opencode-ai/core/plugin/sdk"
|
||||
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
|
||||
import { HttpRouter, HttpServer } from "effect/unstable/http"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import { Context, Effect, Layer, Option } from "effect"
|
||||
import { Effect, Layer, Option } from "effect"
|
||||
import { Api } from "./api"
|
||||
import { ServerAuth } from "./auth"
|
||||
import { handlers } from "./handlers"
|
||||
@@ -38,11 +37,9 @@ const applicationServices = LayerNode.group([
|
||||
httpClient,
|
||||
ToolOutputStore.cleanupNode,
|
||||
Job.node,
|
||||
MoveSession.node,
|
||||
Project.node,
|
||||
SessionV2.node,
|
||||
PluginRuntime.providerNode,
|
||||
SdkPlugins.node,
|
||||
PermissionSaved.node,
|
||||
PtyTicket.node,
|
||||
Credential.node,
|
||||
@@ -50,30 +47,42 @@ const applicationServices = LayerNode.group([
|
||||
LocationServiceMap.node,
|
||||
])
|
||||
|
||||
export function createRoutes(password?: string) {
|
||||
export function createRoutes(password?: string, replacements: LayerNode.Replacements = []) {
|
||||
return makeRoutes(
|
||||
password
|
||||
? ServerAuth.Config.configLayer({ username: "opencode", password: Option.some(password) })
|
||||
: ServerAuth.Config.layer,
|
||||
undefined,
|
||||
replacements,
|
||||
)
|
||||
}
|
||||
|
||||
export function createEmbeddedRoutes() {
|
||||
return makeRoutes(ServerAuth.Config.configLayer({ username: "opencode", password: Option.none() }))
|
||||
export function createEmbeddedRoutes(sdkPlugins?: SdkPlugins.Store, replacements: LayerNode.Replacements = []) {
|
||||
return makeRoutes(
|
||||
ServerAuth.Config.configLayer({ username: "opencode", password: Option.none() }),
|
||||
sdkPlugins,
|
||||
replacements,
|
||||
)
|
||||
}
|
||||
|
||||
function makeRoutes<AuthError, AuthServices>(auth: Layer.Layer<ServerAuth.Config, AuthError, AuthServices>) {
|
||||
function makeRoutes<AuthError, AuthServices>(
|
||||
auth: Layer.Layer<ServerAuth.Config, AuthError, AuthServices>,
|
||||
sdkPlugins?: SdkPlugins.Store,
|
||||
hostReplacements: LayerNode.Replacements = [],
|
||||
) {
|
||||
const pluginRuntimeCell = PluginRuntime.makeCell()
|
||||
const replacements: LayerNode.Replacements = [
|
||||
[SessionExecution.node, SessionExecutionLocal.node],
|
||||
[PluginRuntime.node, PluginRuntime.layerWithCell(pluginRuntimeCell)],
|
||||
[PluginRuntime.providerNode, PluginRuntime.providerNodeWithCell(pluginRuntimeCell)],
|
||||
...(sdkPlugins ? [[SdkPlugins.node, SdkPlugins.layerWithStore(sdkPlugins)] as const] : []),
|
||||
...hostReplacements,
|
||||
]
|
||||
const serviceLayer = simulateEnabled()
|
||||
? Layer.unwrap(
|
||||
Effect.gen(function* () {
|
||||
const { simulationReplacements, startDriveServer } = yield* Effect.promise(
|
||||
() => import("@opencode-ai/simulation/backend"),
|
||||
const { simulationReplacements, startDriveServer } = yield* Effect.promise(() =>
|
||||
import("@opencode-ai/simulation/backend"),
|
||||
)
|
||||
if (driveEnabled()) startDriveServer()
|
||||
return AppNodeBuilder.build(applicationServices, [
|
||||
@@ -84,24 +93,16 @@ function makeRoutes<AuthError, AuthServices>(auth: Layer.Layer<ServerAuth.Config
|
||||
)
|
||||
: AppNodeBuilder.build(applicationServices, replacements)
|
||||
|
||||
return serviceLayer.pipe(
|
||||
Layer.flatMap((context) => {
|
||||
const services = Layer.succeedContext(context)
|
||||
const requestServices = Layer.succeedContext(Context.pick(PermissionSaved.Service, Project.Service)(context))
|
||||
return HttpApiBuilder.layer(Api, { openapiPath: "/openapi.json" }).pipe(
|
||||
Layer.provide(handlers.pipe(Layer.provide(services))),
|
||||
Layer.provide(formLocationLayer),
|
||||
Layer.provide(sessionLocationLayer),
|
||||
Layer.provide(layer),
|
||||
Layer.provide(authorizationLayer),
|
||||
Layer.provide(schemaErrorLayer),
|
||||
Layer.provide(auth),
|
||||
Layer.provide(Observability.layer),
|
||||
HttpRouter.provideRequest(requestServices),
|
||||
Layer.provideMerge(services),
|
||||
Layer.provideMerge(HttpRouter.layer),
|
||||
)
|
||||
}),
|
||||
return HttpApiBuilder.layer(Api, { openapiPath: "/openapi.json" }).pipe(
|
||||
Layer.provide(handlers.pipe(Layer.provide(serviceLayer))),
|
||||
Layer.provide(formLocationLayer),
|
||||
Layer.provide(sessionLocationLayer),
|
||||
Layer.provide(layer),
|
||||
Layer.provide(authorizationLayer),
|
||||
Layer.provide(schemaErrorLayer),
|
||||
Layer.provide(auth),
|
||||
Layer.provide(serviceLayer),
|
||||
Layer.provide(Observability.layer),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -115,4 +116,5 @@ function driveEnabled() {
|
||||
|
||||
export const routes = createRoutes()
|
||||
|
||||
export const webHandler = () => HttpRouter.toWebHandler(routes.pipe(Layer.provide(HttpServer.layerServices)))
|
||||
export const webHandler = () =>
|
||||
HttpRouter.toWebHandler(routes.pipe(Layer.provide(HttpServer.layerServices)))
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"typecheck": "tsgo --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@napi-rs/canvas": "1.0.2",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/llm": "workspace:*",
|
||||
"@opentui/core": "catalog:",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import { SimulationProtocol } from "../protocol"
|
||||
import { SimulationLLMExchange } from "./llm-exchange"
|
||||
import { SimulationNetwork } from "./network"
|
||||
|
||||
/**
|
||||
* Backend-hosted simulation control WebSocket.
|
||||
@@ -18,15 +19,16 @@ import { SimulationLLMExchange } from "./llm-exchange"
|
||||
* - `llm.finish` { id, reason? } finish an exchange
|
||||
* - `llm.disconnect` { id } abruptly terminate an exchange without a finish
|
||||
* - `llm.pending` list open exchanges
|
||||
* - `network.log` simulated network request log
|
||||
*/
|
||||
|
||||
type ControlSocket = Bun.ServerWebSocket<{ unsubscribe?: () => void }>
|
||||
|
||||
function parseRequest(input: string | Buffer) {
|
||||
return SimulationProtocol.Backend.decodeRequest(JSON.parse(typeof input === "string" ? input : input.toString()))
|
||||
return SimulationProtocol.JsonRpc.decodeRequest(JSON.parse(typeof input === "string" ? input : input.toString()))
|
||||
}
|
||||
|
||||
async function handle(socket: ControlSocket, request: SimulationProtocol.Backend.Request): Promise<unknown> {
|
||||
async function handle(socket: ControlSocket, request: SimulationProtocol.JsonRpc.Request): Promise<unknown> {
|
||||
switch (request.method) {
|
||||
case "llm.attach": {
|
||||
socket.data.unsubscribe?.()
|
||||
@@ -36,27 +38,31 @@ async function handle(socket: ControlSocket, request: SimulationProtocol.Backend
|
||||
return { attached: true }
|
||||
}
|
||||
case "llm.chunk": {
|
||||
const params = await SimulationProtocol.Backend.decodeChunkParams(request.params)
|
||||
await Effect.runPromise(
|
||||
SimulationLLMExchange.push(
|
||||
request.params.id,
|
||||
request.params.items.map((item) => ({ type: "item", item }) as const),
|
||||
params.id,
|
||||
params.items.map((item) => ({ type: "item", item }) as const),
|
||||
),
|
||||
)
|
||||
return { ok: true }
|
||||
}
|
||||
case "llm.finish": {
|
||||
await Effect.runPromise(
|
||||
SimulationLLMExchange.push(request.params.id, [{ type: "finish", reason: request.params.reason }]),
|
||||
)
|
||||
const params = await SimulationProtocol.Backend.decodeFinishParams(request.params)
|
||||
await Effect.runPromise(SimulationLLMExchange.push(params.id, [{ type: "finish", reason: params.reason }]))
|
||||
return { ok: true }
|
||||
}
|
||||
case "llm.disconnect": {
|
||||
await Effect.runPromise(SimulationLLMExchange.disconnect(request.params.id))
|
||||
const params = await SimulationProtocol.Backend.decodeDisconnectParams(request.params)
|
||||
await Effect.runPromise(SimulationLLMExchange.disconnect(params.id))
|
||||
return { ok: true }
|
||||
}
|
||||
case "llm.pending":
|
||||
return { exchanges: SimulationLLMExchange.pending() }
|
||||
case "network.log":
|
||||
return { entries: SimulationNetwork.log() }
|
||||
}
|
||||
throw new Error(`Unknown simulation control method: ${request.method}`)
|
||||
}
|
||||
|
||||
export function start(endpoint: string) {
|
||||
@@ -73,7 +79,7 @@ export function start(endpoint: string) {
|
||||
socket.data.unsubscribe?.()
|
||||
},
|
||||
async message(socket, message) {
|
||||
let request: SimulationProtocol.Backend.Request | undefined
|
||||
let request: SimulationProtocol.JsonRpc.Request | undefined
|
||||
try {
|
||||
request = parseRequest(message)
|
||||
const result = await handle(socket, request)
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { mkdtemp } from "node:fs/promises"
|
||||
import { tmpdir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import type { CapturedFrame, CliRenderer, Renderable } from "@opentui/core"
|
||||
import type { CliRenderer, Renderable } from "@opentui/core"
|
||||
import { createMockKeys, createMockMouse, type MockInput, type MockMouse } from "@opentui/core/testing"
|
||||
import type { SimulationProtocol } from "../protocol"
|
||||
import { SimulationRenderer } from "./renderer"
|
||||
import { SimulationPng } from "./png"
|
||||
import { SimulationTrace } from "./trace"
|
||||
|
||||
export type Action = SimulationProtocol.Frontend.Action
|
||||
export type Element = SimulationProtocol.Frontend.Element
|
||||
@@ -50,7 +47,7 @@ function hit(renderer: CliRenderer, renderable: Renderable) {
|
||||
/**
|
||||
* Builds the harness the simulation server drives.
|
||||
*
|
||||
* When the renderer is the headless simulation renderer, its TestRendererSetup
|
||||
* When the renderer is the fake simulation renderer, its TestRendererSetup
|
||||
* provides the supported testing APIs. For the visible terminal renderer the
|
||||
* harness falls back to `requestRender` + `idle` and reading the private
|
||||
* `currentRenderBuffer`.
|
||||
@@ -94,95 +91,57 @@ export function elements(renderer: CliRenderer): Element[] {
|
||||
.filter((element) => element.focusable || element.clickable || element.editor)
|
||||
}
|
||||
|
||||
export function actions(renderer: CliRenderer, options: { text?: string } = {}): Action[] {
|
||||
const items = elements(renderer)
|
||||
return [
|
||||
...(renderer.currentFocusedEditor
|
||||
? ([{ type: "typeText", text: options.text ?? "hello" }, { type: "pressEnter" }] satisfies Action[])
|
||||
: []),
|
||||
...items.filter((item) => item.focusable && !item.focused).map((item) => ({ type: "focus" as const, target: item.num })),
|
||||
...items
|
||||
.filter((item) => item.clickable)
|
||||
.map((item) => ({
|
||||
type: "click" as const,
|
||||
target: item.num,
|
||||
x: Math.floor(item.x + item.width / 2),
|
||||
y: Math.floor(item.y + item.height / 2),
|
||||
})),
|
||||
{ type: "pressArrow", direction: "down" },
|
||||
{ type: "pressArrow", direction: "up" },
|
||||
]
|
||||
}
|
||||
|
||||
export function state(harness: Harness) {
|
||||
return {
|
||||
screen: harness.screen(),
|
||||
focused: {
|
||||
renderable: harness.renderer.currentFocusedRenderable?.num,
|
||||
editor: Boolean(harness.renderer.currentFocusedEditor),
|
||||
},
|
||||
elements: elements(harness.renderer),
|
||||
actions: actions(harness.renderer),
|
||||
}
|
||||
}
|
||||
|
||||
export async function screenshot(harness: Harness) {
|
||||
await harness.renderOnce()
|
||||
const image = SimulationPng.screenshot(harness.renderer)
|
||||
const path = join(await mkdtemp(join(tmpdir(), "opencode-drive-")), "screenshot.png")
|
||||
await Bun.write(path, image.data)
|
||||
return path
|
||||
}
|
||||
|
||||
export function frame(harness: Harness): CapturedFrame {
|
||||
const buffer = harness.renderer.currentRenderBuffer
|
||||
return {
|
||||
cols: buffer.width,
|
||||
rows: buffer.height,
|
||||
cursor: [0, 0],
|
||||
lines: buffer.getSpanLines().map((line) => ({
|
||||
spans: line.spans.map((span) => ({
|
||||
text: span.text,
|
||||
fg: span.fg,
|
||||
bg: span.bg,
|
||||
attributes: span.attributes,
|
||||
width: span.width,
|
||||
})),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
export async function video(frames: CapturedFrame[]) {
|
||||
const directory = await mkdtemp(join(tmpdir(), "opencode-drive-recording-"))
|
||||
await Promise.all(
|
||||
frames.map((frame, index) =>
|
||||
Bun.write(
|
||||
join(directory, `frame-${index.toString().padStart(6, "0")}.png`),
|
||||
SimulationPng.screenshotFrame(frame).data,
|
||||
),
|
||||
),
|
||||
)
|
||||
const path = join(directory, "recording.mp4")
|
||||
const process = Bun.spawn(
|
||||
[
|
||||
"ffmpeg",
|
||||
"-loglevel",
|
||||
"error",
|
||||
"-framerate",
|
||||
"10",
|
||||
"-i",
|
||||
join(directory, "frame-%06d.png"),
|
||||
"-c:v",
|
||||
"libx264",
|
||||
"-pix_fmt",
|
||||
"yuv420p",
|
||||
"-movflags",
|
||||
"+faststart",
|
||||
"-y",
|
||||
path,
|
||||
],
|
||||
{ stderr: "pipe" },
|
||||
)
|
||||
if ((await process.exited) !== 0) throw new Error(`ffmpeg failed: ${await new Response(process.stderr).text()}`)
|
||||
return path
|
||||
}
|
||||
|
||||
export async function execute(harness: Harness, action: Action) {
|
||||
SimulationTrace.add("ui.action", { action })
|
||||
switch (action.type) {
|
||||
case "ui.type":
|
||||
case "typeText":
|
||||
await harness.mockInput.typeText(action.text)
|
||||
break
|
||||
case "ui.press":
|
||||
case "pressKey":
|
||||
harness.mockInput.pressKey(action.key, action.modifiers)
|
||||
break
|
||||
case "ui.enter":
|
||||
case "pressEnter":
|
||||
harness.mockInput.pressEnter()
|
||||
break
|
||||
case "ui.arrow":
|
||||
case "pressArrow":
|
||||
harness.mockInput.pressArrow(action.direction)
|
||||
break
|
||||
case "ui.focus":
|
||||
case "focus":
|
||||
all(harness.renderer.root).find((item) => item.num === action.target)?.focus()
|
||||
break
|
||||
case "ui.click":
|
||||
case "click":
|
||||
await harness.mockMouse.click(action.x, action.y)
|
||||
break
|
||||
}
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { GlobalFonts, createCanvas } from "@napi-rs/canvas"
|
||||
import { TextAttributes, type CapturedFrame, type CliRenderer, type RGBA } from "@opentui/core"
|
||||
|
||||
const CellWidth = 10
|
||||
const CellHeight = 20
|
||||
const FontSize = 16
|
||||
const FontFamily = "OpenCode Screenshot"
|
||||
|
||||
GlobalFonts.registerFromPath(
|
||||
fileURLToPath(new URL("../../../ui/src/assets/fonts/JetBrainsMonoNerdFontMono-Regular.woff2", import.meta.url)),
|
||||
FontFamily,
|
||||
)
|
||||
|
||||
export function screenshot(renderer: CliRenderer) {
|
||||
return screenshotFrame({
|
||||
cols: renderer.currentRenderBuffer.width,
|
||||
rows: renderer.currentRenderBuffer.height,
|
||||
cursor: [0, 0],
|
||||
lines: renderer.currentRenderBuffer.getSpanLines(),
|
||||
})
|
||||
}
|
||||
|
||||
export function screenshotFrame(frame: CapturedFrame) {
|
||||
const canvas = createCanvas(frame.cols * CellWidth, frame.rows * CellHeight)
|
||||
const context = canvas.getContext("2d")
|
||||
context.fillStyle = "#080808"
|
||||
context.fillRect(0, 0, canvas.width, canvas.height)
|
||||
context.textBaseline = "top"
|
||||
|
||||
frame.lines.forEach((line, row) => {
|
||||
let column = 0
|
||||
line.spans.forEach((span) => {
|
||||
const attributes = span.attributes & 0xff
|
||||
const inverse = Boolean(attributes & TextAttributes.INVERSE)
|
||||
const hidden = Boolean(attributes & TextAttributes.HIDDEN)
|
||||
const foreground = inverse ? span.bg : span.fg
|
||||
const background = inverse ? span.fg : span.bg
|
||||
const chars = [...span.text]
|
||||
let remaining = span.width
|
||||
|
||||
chars.forEach((char, index) => {
|
||||
const cells = Math.max(1, remaining - (chars.length - index - 1))
|
||||
if (background.a) {
|
||||
context.fillStyle = color(background)
|
||||
context.fillRect(column * CellWidth, row * CellHeight, cells * CellWidth, CellHeight)
|
||||
}
|
||||
if (!hidden && char.codePointAt(0) !== 0x0a00) {
|
||||
context.fillStyle = color(foreground, attributes & TextAttributes.DIM ? 0.55 : 1)
|
||||
context.font = `${attributes & TextAttributes.ITALIC ? "italic " : ""}${attributes & TextAttributes.BOLD ? "bold " : ""}${FontSize}px "${FontFamily}"`
|
||||
context.fillText(char, column * CellWidth, row * CellHeight + 1)
|
||||
if (attributes & TextAttributes.UNDERLINE) {
|
||||
context.fillRect(column * CellWidth, row * CellHeight + 17, cells * CellWidth, 1)
|
||||
}
|
||||
if (attributes & TextAttributes.STRIKETHROUGH) {
|
||||
context.fillRect(column * CellWidth, row * CellHeight + 10, cells * CellWidth, 1)
|
||||
}
|
||||
}
|
||||
column += cells
|
||||
remaining -= cells
|
||||
})
|
||||
while (remaining-- > 0) {
|
||||
if (background.a) {
|
||||
context.fillStyle = color(background)
|
||||
context.fillRect(column * CellWidth, row * CellHeight, CellWidth, CellHeight)
|
||||
}
|
||||
column++
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
width: canvas.width,
|
||||
height: canvas.height,
|
||||
data: canvas.toBuffer("image/png"),
|
||||
}
|
||||
}
|
||||
|
||||
function color(value: RGBA, opacity = 1) {
|
||||
const [red, green, blue, alpha] = value.toInts()
|
||||
return `rgba(${red}, ${green}, ${blue}, ${(alpha / 255) * opacity})`
|
||||
}
|
||||
|
||||
export * as SimulationPng from "./png"
|
||||
@@ -4,7 +4,7 @@ import { createTestRenderer, type TestRendererSetup } from "@opentui/core/testin
|
||||
const setups = new WeakMap<CliRenderer, TestRendererSetup>()
|
||||
|
||||
/**
|
||||
* Creates the headless simulation renderer: a real CliRenderer backed by an
|
||||
* Creates the fake simulation renderer: a real CliRenderer backed by an
|
||||
* in-memory screen buffer instead of a terminal. The TestRendererSetup is
|
||||
* kept module-side (keyed by renderer) so the harness can use the supported
|
||||
* testing APIs without app code carrying it around.
|
||||
|
||||
@@ -1,100 +1,67 @@
|
||||
import type { CapturedFrame } from "@opentui/core"
|
||||
import { SimulationProtocol } from "../protocol"
|
||||
import { SimulationActions, type Harness } from "./actions"
|
||||
import { SimulationTrace } from "./trace"
|
||||
|
||||
export interface Server {
|
||||
readonly url: string
|
||||
readonly stop: () => void
|
||||
}
|
||||
|
||||
function actionParam(params: unknown) {
|
||||
return SimulationProtocol.Frontend.decodeActionParams(params).action
|
||||
}
|
||||
|
||||
function parseRequest(input: string | Buffer) {
|
||||
return SimulationProtocol.Frontend.decodeRequest(JSON.parse(typeof input === "string" ? input : input.toString()))
|
||||
return SimulationProtocol.JsonRpc.decodeRequest(JSON.parse(typeof input === "string" ? input : input.toString()))
|
||||
}
|
||||
|
||||
interface Recording {
|
||||
readonly frames: CapturedFrame[]
|
||||
readonly timer: ReturnType<typeof setInterval>
|
||||
pending: Promise<void>
|
||||
}
|
||||
|
||||
async function handle(
|
||||
harness: Harness,
|
||||
request: SimulationProtocol.Frontend.Request,
|
||||
recording: { current?: Recording },
|
||||
headless: boolean,
|
||||
) {
|
||||
async function handle(harness: Harness, request: SimulationProtocol.JsonRpc.Request) {
|
||||
switch (request.method) {
|
||||
case "ui.screenshot":
|
||||
return SimulationActions.screenshot(harness)
|
||||
case "ui.state": {
|
||||
return SimulationActions.state(harness)
|
||||
const result = SimulationActions.state(harness)
|
||||
SimulationTrace.add("ui.state", { elements: result.elements.length, actions: result.actions.length })
|
||||
return result
|
||||
}
|
||||
case "ui.start-record": {
|
||||
if (recording.current) throw new Error("UI recording is already active")
|
||||
const frames = [SimulationActions.frame(harness)]
|
||||
const current: Recording = {
|
||||
frames,
|
||||
timer: setInterval(() => {
|
||||
current.pending = current.pending.then(async () => {
|
||||
if (headless) await harness.renderOnce()
|
||||
frames.push(SimulationActions.frame(harness))
|
||||
})
|
||||
}, 100),
|
||||
pending: Promise.resolve(),
|
||||
}
|
||||
recording.current = current
|
||||
return { recording: true }
|
||||
case "ui.action":
|
||||
return SimulationActions.execute(harness, actionParam(request.params))
|
||||
case "ui.render": {
|
||||
await harness.renderOnce()
|
||||
const result = SimulationActions.state(harness)
|
||||
SimulationTrace.add("ui.render", { elements: result.elements.length, actions: result.actions.length })
|
||||
return result
|
||||
}
|
||||
case "ui.end-record": {
|
||||
if (!recording.current) throw new Error("UI recording is not active")
|
||||
const current = recording.current
|
||||
clearInterval(current.timer)
|
||||
await current.pending
|
||||
if (headless) await harness.renderOnce()
|
||||
current.frames.push(SimulationActions.frame(harness))
|
||||
recording.current = undefined
|
||||
return SimulationActions.video(current.frames)
|
||||
}
|
||||
case "ui.type":
|
||||
return SimulationActions.execute(harness, { type: "ui.type", text: request.params.text })
|
||||
case "ui.enter":
|
||||
return SimulationActions.execute(harness, { type: "ui.enter" })
|
||||
case "ui.press":
|
||||
return SimulationActions.execute(harness, {
|
||||
type: "ui.press",
|
||||
key: request.params.key,
|
||||
modifiers: request.params.modifiers,
|
||||
})
|
||||
case "ui.arrow":
|
||||
return SimulationActions.execute(harness, { type: "ui.arrow", direction: request.params.direction })
|
||||
case "ui.focus":
|
||||
return SimulationActions.execute(harness, { type: "ui.focus", target: request.params.target })
|
||||
case "ui.click":
|
||||
return SimulationActions.execute(harness, {
|
||||
type: "ui.click",
|
||||
target: request.params.target,
|
||||
x: request.params.x,
|
||||
y: request.params.y,
|
||||
})
|
||||
case "trace.list":
|
||||
return { records: SimulationTrace.list() }
|
||||
case "trace.clear":
|
||||
SimulationTrace.clear()
|
||||
return { cleared: true }
|
||||
case "trace.export":
|
||||
return SimulationTrace.exportTrace()
|
||||
}
|
||||
throw new Error(`Unknown simulation method: ${request.method}`)
|
||||
}
|
||||
|
||||
export function start(harness: Harness, endpoint: string, headless: boolean): Server {
|
||||
export function start(harness: Harness, endpoint: string): Server {
|
||||
const url = new URL(endpoint)
|
||||
const recording: { current?: Recording } = {}
|
||||
const server = Bun.serve<{ readonly drive: true; readonly headless: boolean }>({
|
||||
const server = Bun.serve<{ readonly drive: true }>({
|
||||
hostname: url.hostname,
|
||||
port: Number(url.port),
|
||||
fetch(request, server) {
|
||||
if (server.upgrade(request, { data: { drive: true, headless } })) return undefined
|
||||
if (server.upgrade(request, { data: { drive: true } })) return undefined
|
||||
return new Response("opencode drive ui websocket", { status: 426 })
|
||||
},
|
||||
websocket: {
|
||||
open() {
|
||||
SimulationTrace.add("control.connect")
|
||||
},
|
||||
close() {
|
||||
SimulationTrace.add("control.disconnect")
|
||||
},
|
||||
async message(socket, message) {
|
||||
let request: SimulationProtocol.Frontend.Request | undefined
|
||||
let request: SimulationProtocol.JsonRpc.Request | undefined
|
||||
try {
|
||||
request = parseRequest(message)
|
||||
const result = await handle(harness, request, recording, headless)
|
||||
const result = await handle(harness, request)
|
||||
const next = SimulationProtocol.JsonRpc.success(request.id, result)
|
||||
if (next) socket.send(JSON.stringify(next))
|
||||
} catch (error) {
|
||||
@@ -103,10 +70,11 @@ export function start(harness: Harness, endpoint: string, headless: boolean): Se
|
||||
},
|
||||
},
|
||||
})
|
||||
SimulationTrace.add("control.start", { url: endpoint })
|
||||
return {
|
||||
url: endpoint,
|
||||
stop: () => {
|
||||
if (recording.current) clearInterval(recording.current.timer)
|
||||
SimulationTrace.add("control.stop", { url: endpoint })
|
||||
server.stop(true)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -7,18 +7,19 @@ import { SimulationServer } from "./server"
|
||||
/**
|
||||
* Drive-mode renderer entry point.
|
||||
*
|
||||
* Creates the renderer (headless when OPENCODE_DRIVE_RENDERER=headless, the normal
|
||||
* Creates the renderer (fake when OPENCODE_DRIVE_RENDERER=fake, the normal
|
||||
* visible renderer otherwise) and starts the UI control
|
||||
* server against it. The server stops when the renderer is destroyed, so the
|
||||
* caller only manages the renderer lifecycle.
|
||||
*/
|
||||
export async function create(options: CliRendererConfig): Promise<CliRenderer> {
|
||||
const headless = process.env.OPENCODE_DRIVE_RENDERER === "headless"
|
||||
const renderer = headless ? await SimulationRenderer.create(options) : await createCliRenderer(options)
|
||||
const renderer =
|
||||
process.env.OPENCODE_DRIVE_RENDERER === "fake"
|
||||
? await SimulationRenderer.create(options)
|
||||
: await createCliRenderer(options)
|
||||
const server = SimulationServer.start(
|
||||
SimulationActions.createHarness(renderer),
|
||||
DriveManifest.resolve().endpoints.ui,
|
||||
headless,
|
||||
)
|
||||
process.stderr.write(`opencode drive ui websocket: ${server.url}\n`)
|
||||
renderer.once("destroy", () => server.stop())
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
export type TraceRecord = {
|
||||
readonly id: number
|
||||
readonly time: string
|
||||
readonly type: string
|
||||
readonly data?: unknown
|
||||
}
|
||||
|
||||
const records: TraceRecord[] = []
|
||||
let nextID = 0
|
||||
|
||||
export function add(type: string, data?: unknown) {
|
||||
const record = {
|
||||
id: ++nextID,
|
||||
time: new Date().toISOString(),
|
||||
type,
|
||||
...(data === undefined ? {} : { data }),
|
||||
} satisfies TraceRecord
|
||||
records.push(record)
|
||||
return record
|
||||
}
|
||||
|
||||
export function list() {
|
||||
return [...records]
|
||||
}
|
||||
|
||||
export function clear() {
|
||||
records.length = 0
|
||||
nextID = 0
|
||||
}
|
||||
|
||||
export function exportTrace() {
|
||||
return {
|
||||
records: list(),
|
||||
}
|
||||
}
|
||||
|
||||
export * as SimulationTrace from "./trace"
|
||||
@@ -4,12 +4,9 @@ const JsonRpcID = Schema.Union([Schema.String, Schema.Number, Schema.Null])
|
||||
type Json = Schema.Schema.Type<typeof Schema.Json>
|
||||
|
||||
export namespace JsonRpc {
|
||||
export const RequestFields = {
|
||||
export const Request = Schema.Struct({
|
||||
jsonrpc: Schema.Literal("2.0"),
|
||||
id: Schema.optional(JsonRpcID),
|
||||
}
|
||||
export const Request = Schema.Struct({
|
||||
...RequestFields,
|
||||
method: Schema.String,
|
||||
params: Schema.optional(Schema.Json),
|
||||
})
|
||||
@@ -59,12 +56,12 @@ export namespace Frontend {
|
||||
export interface KeyModifiers extends Schema.Schema.Type<typeof KeyModifiers> {}
|
||||
|
||||
export const Action = Schema.Union([
|
||||
Schema.Struct({ type: Schema.Literal("ui.type"), text: Schema.String }),
|
||||
Schema.Struct({ type: Schema.Literal("ui.press"), key: Schema.String, modifiers: Schema.optional(KeyModifiers) }),
|
||||
Schema.Struct({ type: Schema.Literal("ui.enter") }),
|
||||
Schema.Struct({ type: Schema.Literal("ui.arrow"), direction: Schema.Literals(["up", "down", "left", "right"]) }),
|
||||
Schema.Struct({ type: Schema.Literal("ui.focus"), target: Schema.Number }),
|
||||
Schema.Struct({ type: Schema.Literal("ui.click"), target: Schema.Number, x: Schema.Number, y: Schema.Number }),
|
||||
Schema.Struct({ type: Schema.Literal("typeText"), text: Schema.String }),
|
||||
Schema.Struct({ type: Schema.Literal("pressKey"), key: Schema.String, modifiers: Schema.optional(KeyModifiers) }),
|
||||
Schema.Struct({ type: Schema.Literal("pressEnter") }),
|
||||
Schema.Struct({ type: Schema.Literal("pressArrow"), direction: Schema.Literals(["up", "down", "left", "right"]) }),
|
||||
Schema.Struct({ type: Schema.Literal("focus"), target: Schema.Number }),
|
||||
Schema.Struct({ type: Schema.Literal("click"), target: Schema.Number, x: Schema.Number, y: Schema.Number }),
|
||||
])
|
||||
export type Action = Schema.Schema.Type<typeof Action>
|
||||
|
||||
@@ -83,58 +80,30 @@ export namespace Frontend {
|
||||
export interface Element extends Schema.Schema.Type<typeof Element> {}
|
||||
|
||||
export const State = Schema.Struct({
|
||||
screen: Schema.String,
|
||||
focused: Schema.Struct({
|
||||
renderable: Schema.optional(Schema.Number),
|
||||
editor: Schema.Boolean,
|
||||
}),
|
||||
elements: Schema.Array(Element),
|
||||
actions: Schema.Array(Action),
|
||||
})
|
||||
export interface State extends Schema.Schema.Type<typeof State> {}
|
||||
|
||||
export const Screenshot = Schema.String
|
||||
export type Screenshot = Schema.Schema.Type<typeof Screenshot>
|
||||
export const ActionParams = Schema.Struct({ action: Action })
|
||||
export interface ActionParams extends Schema.Schema.Type<typeof ActionParams> {}
|
||||
export const decodeActionParams = Schema.decodeUnknownSync(ActionParams)
|
||||
|
||||
export const StartRecord = Schema.Struct({ recording: Schema.Literal(true) })
|
||||
export interface StartRecord extends Schema.Schema.Type<typeof StartRecord> {}
|
||||
|
||||
export const EndRecord = Schema.String
|
||||
export type EndRecord = Schema.Schema.Type<typeof EndRecord>
|
||||
|
||||
export const TypeParams = Schema.Struct({ text: Schema.String })
|
||||
export interface TypeParams extends Schema.Schema.Type<typeof TypeParams> {}
|
||||
|
||||
export const PressParams = Schema.Struct({ key: Schema.String, modifiers: Schema.optional(KeyModifiers) })
|
||||
export interface PressParams extends Schema.Schema.Type<typeof PressParams> {}
|
||||
|
||||
export const ArrowParams = Schema.Struct({ direction: Schema.Literals(["up", "down", "left", "right"]) })
|
||||
export interface ArrowParams extends Schema.Schema.Type<typeof ArrowParams> {}
|
||||
|
||||
export const FocusParams = Schema.Struct({ target: Schema.Number })
|
||||
export interface FocusParams extends Schema.Schema.Type<typeof FocusParams> {}
|
||||
|
||||
export const ClickParams = Schema.Struct({ target: Schema.Number, x: Schema.Number, y: Schema.Number })
|
||||
export interface ClickParams extends Schema.Schema.Type<typeof ClickParams> {}
|
||||
|
||||
export const Request = Schema.Union([
|
||||
Schema.Struct({ ...JsonRpc.RequestFields, method: Schema.Literal("ui.type"), params: TypeParams }),
|
||||
Schema.Struct({ ...JsonRpc.RequestFields, method: Schema.Literal("ui.press"), params: PressParams }),
|
||||
Schema.Struct({ ...JsonRpc.RequestFields, method: Schema.Literal("ui.arrow"), params: ArrowParams }),
|
||||
Schema.Struct({ ...JsonRpc.RequestFields, method: Schema.Literal("ui.focus"), params: FocusParams }),
|
||||
Schema.Struct({ ...JsonRpc.RequestFields, method: Schema.Literal("ui.click"), params: ClickParams }),
|
||||
Schema.Struct({
|
||||
...JsonRpc.RequestFields,
|
||||
method: Schema.Literals([
|
||||
"ui.enter",
|
||||
"ui.screenshot",
|
||||
"ui.state",
|
||||
"ui.start-record",
|
||||
"ui.end-record",
|
||||
]),
|
||||
}),
|
||||
])
|
||||
export type Request = Schema.Schema.Type<typeof Request>
|
||||
export const decodeRequest = Schema.decodeUnknownSync(Request)
|
||||
export const TraceRecord = Schema.Struct({
|
||||
id: Schema.Number,
|
||||
time: Schema.String,
|
||||
type: Schema.String,
|
||||
data: Schema.optional(Schema.Json),
|
||||
})
|
||||
export interface TraceRecord extends Schema.Schema.Type<typeof TraceRecord> {}
|
||||
|
||||
export const TraceList = Schema.Struct({ records: Schema.Array(TraceRecord) })
|
||||
export interface TraceList extends Schema.Schema.Type<typeof TraceList> {}
|
||||
}
|
||||
|
||||
export namespace Backend {
|
||||
@@ -161,18 +130,6 @@ export namespace Backend {
|
||||
export const DisconnectParams = Schema.Struct({ id: Schema.String })
|
||||
export interface DisconnectParams extends Schema.Schema.Type<typeof DisconnectParams> {}
|
||||
|
||||
export const Request = Schema.Union([
|
||||
Schema.Struct({ ...JsonRpc.RequestFields, method: Schema.Literal("llm.chunk"), params: ChunkParams }),
|
||||
Schema.Struct({ ...JsonRpc.RequestFields, method: Schema.Literal("llm.finish"), params: FinishParams }),
|
||||
Schema.Struct({ ...JsonRpc.RequestFields, method: Schema.Literal("llm.disconnect"), params: DisconnectParams }),
|
||||
Schema.Struct({
|
||||
...JsonRpc.RequestFields,
|
||||
method: Schema.Literals(["llm.attach", "llm.pending"]),
|
||||
}),
|
||||
])
|
||||
export type Request = Schema.Schema.Type<typeof Request>
|
||||
export const decodeRequest = Schema.decodeUnknownSync(Request)
|
||||
|
||||
export const OpenedExchange = Schema.Struct({ id: Schema.String, url: Schema.String, body: Schema.Json })
|
||||
export interface OpenedExchange extends Schema.Schema.Type<typeof OpenedExchange> {}
|
||||
|
||||
@@ -184,6 +141,9 @@ export namespace Backend {
|
||||
})
|
||||
export interface NetworkLogEntry extends Schema.Schema.Type<typeof NetworkLogEntry> {}
|
||||
|
||||
export const decodeChunkParams = Schema.decodeUnknownPromise(ChunkParams)
|
||||
export const decodeFinishParams = Schema.decodeUnknownPromise(FinishParams)
|
||||
export const decodeDisconnectParams = Schema.decodeUnknownPromise(DisconnectParams)
|
||||
}
|
||||
|
||||
export * as SimulationProtocol from "./index"
|
||||
|
||||
@@ -54,7 +54,6 @@
|
||||
"@opencode-ai/client": "workspace:*",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opencode-ai/simulation": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
|
||||
@@ -19,9 +19,8 @@ import { Spinner } from "./spinner"
|
||||
import { DialogWorkspaceFileChanges } from "./dialog-workspace-file-changes"
|
||||
import type { ProjectDirectoriesOutput } from "@opencode-ai/client/promise"
|
||||
import { useRoute } from "../context/route"
|
||||
import { DialogProjectCopyName } from "./dialog-project-copy-name"
|
||||
|
||||
export type MoveSessionSelection = { type: "directory"; directory: string; subdirectory: boolean } | { type: "new"; name: string }
|
||||
export type MoveSessionSelection = { type: "directory"; directory: string; subdirectory: boolean } | { type: "new" }
|
||||
type ProjectDirectory = ProjectDirectoriesOutput[number]
|
||||
|
||||
type DialogMoveSessionProps = {
|
||||
@@ -292,12 +291,6 @@ export function DialogMoveSession(props: DialogMoveSessionProps) {
|
||||
if (await removedCurrent(deletingCurrent)) return
|
||||
}
|
||||
|
||||
async function create() {
|
||||
const name = await DialogProjectCopyName.show(dialog)
|
||||
if (name === null) return
|
||||
props.onSelect({ type: "new", name })
|
||||
}
|
||||
|
||||
const fullHeight = createMemo(() =>
|
||||
Math.max(8, Math.min(16, dimensions().height - Math.floor(dimensions().height / 4) - 2)),
|
||||
)
|
||||
@@ -341,7 +334,7 @@ export function DialogMoveSession(props: DialogMoveSessionProps) {
|
||||
{
|
||||
command: "dialog.move_session.new",
|
||||
title: "new",
|
||||
onTrigger: () => void create(),
|
||||
onTrigger: () => props.onSelect({ type: "new" }),
|
||||
},
|
||||
{
|
||||
command: "dialog.move_session.delete",
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
import { InputRenderable, TextAttributes } from "@opentui/core"
|
||||
import { Slug } from "@opencode-ai/core/util/slug"
|
||||
import { createSignal, onMount } from "solid-js"
|
||||
import { useTuiConfig } from "../config"
|
||||
import { useTheme } from "../context/theme"
|
||||
import { useBindings, useCommandShortcut } from "../keymap"
|
||||
import { useDialog, type DialogContext } from "../ui/dialog"
|
||||
|
||||
export function DialogProjectCopyName(props: { onConfirm: (name: string) => void }) {
|
||||
const dialog = useDialog()
|
||||
const { theme } = useTheme()
|
||||
const tuiConfig = useTuiConfig()
|
||||
const generateShortcut = useCommandShortcut("dialog.project_copy.generate")
|
||||
const [inputTarget, setInputTarget] = createSignal<InputRenderable>()
|
||||
let input: InputRenderable
|
||||
|
||||
function generate() {
|
||||
input.value = Slug.create()
|
||||
input.gotoLineEnd()
|
||||
}
|
||||
|
||||
function confirm() {
|
||||
props.onConfirm(slugify(input.value) || Slug.create())
|
||||
}
|
||||
|
||||
useBindings(() => ({
|
||||
target: inputTarget,
|
||||
enabled: inputTarget() !== undefined,
|
||||
priority: 1,
|
||||
commands: [
|
||||
{
|
||||
name: "dialog.project_copy.generate",
|
||||
title: "Generate project copy name",
|
||||
category: "Dialog",
|
||||
run: generate,
|
||||
},
|
||||
],
|
||||
bindings: tuiConfig.keybinds.get("dialog.project_copy.generate"),
|
||||
}))
|
||||
|
||||
onMount(() => {
|
||||
dialog.setSize("medium")
|
||||
setTimeout(() => {
|
||||
if (!input || input.isDestroyed) return
|
||||
input.focus()
|
||||
}, 1)
|
||||
})
|
||||
|
||||
return (
|
||||
<box paddingLeft={2} paddingRight={2} gap={1}>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
<text attributes={TextAttributes.BOLD} fg={theme.text}>
|
||||
Name project copy
|
||||
</text>
|
||||
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
|
||||
esc
|
||||
</text>
|
||||
</box>
|
||||
<input
|
||||
ref={(value: InputRenderable) => {
|
||||
input = value
|
||||
setInputTarget(value)
|
||||
}}
|
||||
onSubmit={confirm}
|
||||
placeholder="Project copy name"
|
||||
placeholderColor={theme.textMuted}
|
||||
textColor={theme.text}
|
||||
focusedTextColor={theme.text}
|
||||
cursorColor={theme.text}
|
||||
/>
|
||||
<box paddingBottom={1} flexDirection="row" gap={2}>
|
||||
<text fg={theme.text}>
|
||||
enter <span style={{ fg: theme.textMuted }}>submit</span>
|
||||
</text>
|
||||
<text fg={theme.text}>
|
||||
{generateShortcut()} <span style={{ fg: theme.textMuted }}>generate one</span>
|
||||
</text>
|
||||
</box>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
DialogProjectCopyName.show = (dialog: DialogContext) =>
|
||||
new Promise<string | null>((resolve) => {
|
||||
dialog.replace(() => <DialogProjectCopyName onConfirm={resolve} />, () => resolve(null))
|
||||
})
|
||||
|
||||
function slugify(input: string) {
|
||||
return input
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+/, "")
|
||||
.replace(/-+$/, "")
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { pathToFileURL } from "bun"
|
||||
import fuzzysort from "fuzzysort"
|
||||
import path from "path"
|
||||
import { firstBy } from "remeda"
|
||||
import { createMemo, createResource, createEffect, onMount, onCleanup, Index, Show, createSignal, on } from "solid-js"
|
||||
import { createMemo, createResource, createEffect, onMount, onCleanup, Index, Show, createSignal } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useEditorContext } from "../../context/editor"
|
||||
import { useProject } from "../../context/project"
|
||||
@@ -23,7 +23,6 @@ import { useFrecency } from "../../prompt/frecency"
|
||||
import { useBindings, useCommandSlashes, useOpencodeModeStack } from "../../keymap"
|
||||
import { displayCharAt, mentionTriggerIndex } from "../../prompt/display"
|
||||
import type { FileSystemEntry } from "@opencode-ai/sdk/v2"
|
||||
import { Mcp } from "@opencode-ai/schema/mcp"
|
||||
|
||||
function removeLineRange(input: string) {
|
||||
const hashIndex = input.lastIndexOf("#")
|
||||
@@ -105,12 +104,6 @@ export function Autocomplete(props: {
|
||||
input: "keyboard" as "keyboard" | "mouse",
|
||||
})
|
||||
|
||||
createEffect(
|
||||
on(location, (location) => {
|
||||
void data.location.mcp.resource.refresh(location).catch(() => undefined)
|
||||
}),
|
||||
)
|
||||
|
||||
const [positionTick, setPositionTick] = createSignal(0)
|
||||
|
||||
createEffect(() => {
|
||||
@@ -370,7 +363,7 @@ export function Autocomplete(props: {
|
||||
const options: AutocompleteOption[] = []
|
||||
const width = props.anchor().width - 4
|
||||
|
||||
for (const res of data.location.mcp.resource.catalog(location())?.resources ?? []) {
|
||||
for (const res of Object.values(sync.data.mcp_resource)) {
|
||||
options.push({
|
||||
display: Locale.truncateMiddle(res.name, width),
|
||||
// Match the name only; matching the URI caused unrelated fuzzy hits.
|
||||
@@ -380,7 +373,7 @@ export function Autocomplete(props: {
|
||||
insertPart(res.name, {
|
||||
type: "file",
|
||||
value: {
|
||||
uri: Mcp.resourceUri({ server: res.server, uri: res.uri }),
|
||||
uri: res.uri,
|
||||
name: res.name,
|
||||
description: res.description,
|
||||
mention: { start: 0, end: 0, text: "" },
|
||||
|
||||
@@ -218,10 +218,7 @@ export function Prompt(props: PromptProps) {
|
||||
const editorContextLabelState = createMemo(() => editor.labelState())
|
||||
const [auto, setAuto] = createSignal<AutocompleteRef>()
|
||||
const workspace = usePromptWorkspace(props.sessionID)
|
||||
const move = usePromptMove({
|
||||
projectID: () => (props.sessionID ? data.session.get(props.sessionID)?.projectID : undefined) ?? project.project(),
|
||||
sessionID: () => props.sessionID,
|
||||
})
|
||||
const move = usePromptMove({ projectID: project.project, sessionID: () => props.sessionID })
|
||||
const [cursorVersion, setCursorVersion] = createSignal(0)
|
||||
const currentProviderLabel = createMemo(() => local.model.parsed().provider)
|
||||
const connected = useConnected()
|
||||
@@ -958,13 +955,13 @@ export function Prompt(props: PromptProps) {
|
||||
if (workspace.creating() || move.creating()) return false
|
||||
if (auto()?.visible) return false
|
||||
if (!store.prompt.text) return false
|
||||
const agent = local.agent.current()
|
||||
if (!agent) return false
|
||||
const trimmed = store.prompt.text.trim()
|
||||
if (trimmed === "exit" || trimmed === "quit" || trimmed === ":q") {
|
||||
void exit()
|
||||
return true
|
||||
}
|
||||
const agent = local.agent.current()
|
||||
if (!agent) return false
|
||||
const selectedModel = local.model.current()
|
||||
if (!selectedModel) {
|
||||
void promptModelWarning()
|
||||
@@ -994,7 +991,7 @@ export function Prompt(props: PromptProps) {
|
||||
const selectedWorkspace = workspace.selection()
|
||||
const workspaceID = selectedWorkspace?.type === "existing" ? selectedWorkspace.workspaceID : undefined
|
||||
|
||||
const directory = await move.getDirectory()
|
||||
const directory = await move.getDirectory(store.prompt.text)
|
||||
if (move.pending() && !directory) return false
|
||||
finishMoveProgress = Boolean(move.progress())
|
||||
const location = data.location.default()
|
||||
|
||||
@@ -4,12 +4,12 @@ import { useTuiPaths } from "../../context/runtime"
|
||||
import { errorMessage } from "../../util/error"
|
||||
import { useDialog } from "../../ui/dialog"
|
||||
import { useSDK } from "../../context/sdk"
|
||||
import { useSync } from "../../context/sync"
|
||||
import { useToast } from "../../ui/toast"
|
||||
import { DialogMoveSession, type MoveSessionSelection } from "../dialog-move-session"
|
||||
import { DialogWorkspaceFileChanges } from "../dialog-workspace-file-changes"
|
||||
import { useHomeSessionDestination } from "../../routes/home/session-destination"
|
||||
import { useProject } from "../../context/project"
|
||||
import { useData } from "../../context/data"
|
||||
|
||||
function moveReminderText(directory: string) {
|
||||
return `<system-reminder>The user has changed the current working directory to "${directory}". This is still the same project but at a possibly new location; take this into account when working with any files from now on.</system-reminder>`
|
||||
@@ -18,33 +18,38 @@ function moveReminderText(directory: string) {
|
||||
export function usePromptMove(input: { projectID: () => string | undefined; sessionID: () => string | undefined }) {
|
||||
const dialog = useDialog()
|
||||
const sdk = useSDK()
|
||||
const sync = useSync()
|
||||
const toast = useToast()
|
||||
const homeDestination = useHomeSessionDestination()
|
||||
const project = useProject()
|
||||
const data = useData()
|
||||
const paths = useTuiPaths()
|
||||
const [creating, setCreating] = createSignal(false)
|
||||
const [creatingDots, setCreatingDots] = createSignal(3)
|
||||
const [progress, setProgress] = createSignal<string>()
|
||||
|
||||
async function create(name: string) {
|
||||
const projectID = await resolveProjectID()
|
||||
async function create(context?: string) {
|
||||
const projectID = input.projectID()
|
||||
if (!projectID) return
|
||||
setCreating(true)
|
||||
setProgress("Creating copy")
|
||||
try {
|
||||
const generated = await sdk.client.experimental.projectCopy.generateName(
|
||||
{ projectID, context },
|
||||
{ throwOnError: true },
|
||||
)
|
||||
const result = await sdk.api.projectCopy.create({
|
||||
projectID,
|
||||
location: { directory: project.instance.directory() || paths.cwd },
|
||||
strategy: "git_worktree",
|
||||
directory: path.join(paths.worktree, projectID.slice(0, 6)),
|
||||
name,
|
||||
name: generated.data.name,
|
||||
})
|
||||
const directory = result.directory
|
||||
if (!directory) throw new Error("No project copy directory returned")
|
||||
|
||||
// Call a location-based route to make sure it's bootstrapped before moving on.
|
||||
await sdk.api.location.get({ location: { directory } })
|
||||
// Call a location-based route to make sure it's bootstrapped
|
||||
// before moving on
|
||||
await sdk.client.path.get({ directory }, { throwOnError: true })
|
||||
|
||||
setProgress("Creating session")
|
||||
return directory
|
||||
@@ -57,14 +62,11 @@ export function usePromptMove(input: { projectID: () => string | undefined; sess
|
||||
}
|
||||
}
|
||||
|
||||
async function open() {
|
||||
const projectID = await resolveProjectID()
|
||||
if (!projectID) {
|
||||
toast.show({ message: "Unable to determine current project", variant: "error" })
|
||||
return
|
||||
}
|
||||
function open() {
|
||||
const projectID = input.projectID()
|
||||
if (!projectID) return
|
||||
const sessionID = input.sessionID()
|
||||
const session = sessionID ? await resolveSession(sessionID) : undefined
|
||||
const session = sessionID ? sync.session.get(sessionID) : undefined
|
||||
dialog.replace(() => (
|
||||
<DialogMoveSession
|
||||
projectID={projectID}
|
||||
@@ -73,8 +75,8 @@ export function usePromptMove(input: { projectID: () => string | undefined; sess
|
||||
(session
|
||||
? {
|
||||
type: "directory",
|
||||
directory: session.location.directory,
|
||||
subdirectory: !!session.subpath,
|
||||
directory: session.directory,
|
||||
subdirectory: !!session.path,
|
||||
}
|
||||
: {
|
||||
type: "directory",
|
||||
@@ -96,13 +98,26 @@ export function usePromptMove(input: { projectID: () => string | undefined; sess
|
||||
))
|
||||
}
|
||||
|
||||
function sessionContext(sessionID: string) {
|
||||
const session = sync.session.get(sessionID)
|
||||
const messages = (sync.data.message[sessionID] ?? [])
|
||||
.slice(-6)
|
||||
.map((message) =>
|
||||
[
|
||||
message.role + ":",
|
||||
...(sync.data.part[message.id] ?? []).flatMap((part) => (part.type === "text" ? [part.text] : [])),
|
||||
].join(" "),
|
||||
)
|
||||
return [session?.title, ...messages].filter(Boolean).join("\n") || undefined
|
||||
}
|
||||
|
||||
async function moveExistingSession(sessionID: string, selection: MoveSessionSelection) {
|
||||
const session = await resolveSession(sessionID)
|
||||
const status = await sdk.client.vcs.status({ directory: session?.location.directory }).catch(() => undefined)
|
||||
const session = sync.session.get(sessionID)
|
||||
const status = await sdk.client.vcs.status({ directory: session?.directory }).catch(() => undefined)
|
||||
const choice = status?.data?.length ? await DialogWorkspaceFileChanges.show(dialog, status.data) : "no"
|
||||
if (!choice) return
|
||||
dialog.clear()
|
||||
const directory = selection.type === "new" ? await create(selection.name) : selection.directory
|
||||
const directory = selection.type === "new" ? await create(sessionContext(sessionID)) : selection.directory
|
||||
if (!directory) {
|
||||
setProgress(undefined)
|
||||
dialog.clear()
|
||||
@@ -110,9 +125,27 @@ export function usePromptMove(input: { projectID: () => string | undefined; sess
|
||||
}
|
||||
setProgress("Moving session")
|
||||
try {
|
||||
await sdk.api.session.move({ sessionID, destination: { directory }, moveChanges: choice === "yes" })
|
||||
await sdk.api.session
|
||||
.synthetic({ sessionID, text: moveReminderText(directory), resume: false })
|
||||
await sdk.client.experimental.controlPlane.moveSession(
|
||||
{
|
||||
sessionID,
|
||||
destination: { directory },
|
||||
moveChanges: choice === "yes",
|
||||
},
|
||||
{ throwOnError: true },
|
||||
)
|
||||
await sdk.client.session
|
||||
.promptAsync({
|
||||
sessionID,
|
||||
directory,
|
||||
noReply: true,
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: moveReminderText(directory),
|
||||
synthetic: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
.catch(() => undefined)
|
||||
dialog.clear()
|
||||
} catch (error) {
|
||||
@@ -124,34 +157,16 @@ export function usePromptMove(input: { projectID: () => string | undefined; sess
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveProjectID() {
|
||||
const projectID = input.projectID()
|
||||
if (projectID) return projectID
|
||||
const sessionID = input.sessionID()
|
||||
if (sessionID) return (await resolveSession(sessionID))?.projectID
|
||||
return sdk.api.project
|
||||
.current({ location: { directory: project.instance.directory() || paths.cwd } })
|
||||
.then((project) => project.id)
|
||||
.catch(() => undefined)
|
||||
}
|
||||
|
||||
async function resolveSession(sessionID: string) {
|
||||
const session = data.session.get(sessionID)
|
||||
if (session) return session
|
||||
await data.session.refresh(sessionID).catch(() => undefined)
|
||||
return data.session.get(sessionID)
|
||||
}
|
||||
|
||||
const pending = createMemo(() => Boolean(homeDestination?.destination()))
|
||||
const pendingNew = createMemo(() => homeDestination?.destination()?.type === "new")
|
||||
|
||||
async function getDirectory() {
|
||||
async function getDirectory(context?: string) {
|
||||
const value = homeDestination?.destination()
|
||||
if (!value) return
|
||||
if (value.type === "directory") {
|
||||
return value.directory
|
||||
}
|
||||
return await create(value.name)
|
||||
return await create(context)
|
||||
}
|
||||
|
||||
function startSubmit() {
|
||||
|
||||
@@ -207,7 +207,6 @@ export const Definitions = {
|
||||
"dialog.select.end": keybind("end", "Move to last dialog item"),
|
||||
"dialog.select.submit": keybind("return", "Submit selected dialog item"),
|
||||
"dialog.prompt.submit": keybind("return", "Submit dialog prompt"),
|
||||
"dialog.project_copy.generate": keybind("tab", "Generate project copy name"),
|
||||
"dialog.mcp.toggle": keybind("space", "Toggle MCP in MCP dialog"),
|
||||
"dialog.move_session.new": keybind("ctrl+m", "New project copy"),
|
||||
"dialog.move_session.delete": keybind("ctrl+d", "Delete project copy"),
|
||||
|
||||
@@ -21,7 +21,6 @@ import type {
|
||||
SkillV2Info,
|
||||
V2Event,
|
||||
} from "@opencode-ai/sdk/v2"
|
||||
import type { ServerMcpCatalogOutput } from "@opencode-ai/client/promise"
|
||||
import { createStore, produce, reconcile } from "solid-js/store"
|
||||
import { createSimpleContext } from "./helper"
|
||||
import { useSDK } from "./sdk"
|
||||
@@ -38,7 +37,6 @@ type LocationData = {
|
||||
command?: CommandV2Info[]
|
||||
integration?: IntegrationInfo[]
|
||||
mcp?: McpServer[]
|
||||
mcpResource?: ServerMcpCatalogOutput["data"]
|
||||
model?: ModelV2Info[]
|
||||
provider?: ProviderV2Info[]
|
||||
reference?: ReferenceInfo[]
|
||||
@@ -306,12 +304,6 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||
if (store.session.info[event.data.sessionID])
|
||||
setStore("session", "info", event.data.sessionID, "title", event.data.title)
|
||||
break
|
||||
case "session.moved":
|
||||
if (store.session.info[event.data.sessionID]) {
|
||||
setStore("session", "info", event.data.sessionID, "location", mutable(event.data.location))
|
||||
setStore("session", "info", event.data.sessionID, "subpath", event.data.subpath)
|
||||
}
|
||||
break
|
||||
case "session.prompt.promoted": {
|
||||
message.update(event.data.sessionID, (draft, index) => {
|
||||
const position = index.get(event.data.inputID)
|
||||
@@ -765,10 +757,6 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||
if (bootstrapping) break
|
||||
void result.location.mcp.refresh(event.location)
|
||||
break
|
||||
case "mcp.resources.changed":
|
||||
if (bootstrapping) break
|
||||
void result.location.mcp.resource.refresh(event.location)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -954,18 +942,6 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||
const key = locationKey(result.data.location)
|
||||
setStore("location", key, { ...store.location[key], mcp: result.data.data })
|
||||
},
|
||||
resource: {
|
||||
catalog(location?: LocationRef) {
|
||||
return store.location[locationKey(location ?? defaultLocation())]?.mcpResource
|
||||
},
|
||||
async refresh(ref?: LocationRef) {
|
||||
const result = await sdk.api["server.mcp"].catalog({
|
||||
location: locationQuery(ref ?? defaultLocation()),
|
||||
})
|
||||
const key = locationKey(result.location)
|
||||
setStore("location", key, { ...store.location[key], mcpResource: mutable(result.data) })
|
||||
},
|
||||
},
|
||||
},
|
||||
model: {
|
||||
list(location?: LocationRef) {
|
||||
@@ -1034,7 +1010,6 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||
result.location.agent.refresh(),
|
||||
result.location.integration.refresh(),
|
||||
result.location.mcp.refresh(),
|
||||
result.location.mcp.resource.refresh(),
|
||||
result.location.model.refresh(),
|
||||
result.location.provider.refresh(),
|
||||
result.location.reference.refresh(),
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useTuiPaths } from "../../context/runtime"
|
||||
|
||||
const id = "internal:sidebar-footer"
|
||||
|
||||
function View(props: { api: TuiPluginApi; directory: string }) {
|
||||
function View(props: { api: TuiPluginApi; sessionID: string }) {
|
||||
const paths = useTuiPaths()
|
||||
const theme = () => props.api.theme.current
|
||||
const has = createMemo(() =>
|
||||
@@ -17,8 +17,10 @@ function View(props: { api: TuiPluginApi; directory: string }) {
|
||||
const done = createMemo(() => props.api.kv.get("dismissed_getting_started", false))
|
||||
const show = createMemo(() => !has() && !done())
|
||||
const path = createMemo(() => {
|
||||
const out = abbreviateHome(props.directory, paths.home)
|
||||
const branch = props.directory === props.api.state.path.directory ? props.api.state.vcs?.branch : undefined
|
||||
const session = props.api.state.session.get(props.sessionID)
|
||||
const dir = session?.directory || props.api.state.path.directory || paths.cwd
|
||||
const out = abbreviateHome(dir, paths.home)
|
||||
const branch = session?.directory === props.api.state.path.directory ? props.api.state.vcs?.branch : undefined
|
||||
const text = branch ? out + ":" + branch : out
|
||||
const list = text.split("/")
|
||||
return {
|
||||
@@ -82,7 +84,7 @@ const tui: TuiPlugin = async (api) => {
|
||||
order: 100,
|
||||
slots: {
|
||||
sidebar_footer(_ctx, props) {
|
||||
return <View api={api} directory={props.directory} />
|
||||
return <View api={api} sessionID={props.session_id} />
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import { useSync } from "../../context/sync"
|
||||
import { useTuiPaths } from "../../context/runtime"
|
||||
|
||||
export type HomeSessionDestination = { type: "directory"; directory: string; subdirectory: boolean } | { type: "new"; name: string }
|
||||
export type HomeSessionDestination = { type: "directory"; directory: string; subdirectory: boolean } | { type: "new" }
|
||||
|
||||
type Context = {
|
||||
destination: Accessor<HomeSessionDestination | undefined>
|
||||
|
||||
@@ -85,12 +85,7 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
|
||||
</scrollbox>
|
||||
|
||||
<box flexShrink={0} gap={1} paddingTop={1}>
|
||||
<pluginRuntime.Slot
|
||||
name="sidebar_footer"
|
||||
mode="single_winner"
|
||||
session_id={props.sessionID}
|
||||
directory={session()?.location.directory ?? ""}
|
||||
>
|
||||
<pluginRuntime.Slot name="sidebar_footer" mode="single_winner" session_id={props.sessionID}>
|
||||
<text fg={theme.textMuted}>
|
||||
<span style={{ fg: theme.success }}>•</span> <b>Open</b>
|
||||
<span style={{ fg: theme.text }}>
|
||||
|
||||
@@ -108,116 +108,6 @@ test("refreshes resources into reactive getters", async () => {
|
||||
}
|
||||
})
|
||||
|
||||
test("refreshes MCP resource catalogs after change events", async () => {
|
||||
const events = createEventStream()
|
||||
let resources = [{ server: "docs", name: "Readme", uri: "docs://readme" }]
|
||||
let requests = 0
|
||||
const calls = createFetch((url) => {
|
||||
if (url.pathname !== "/api/mcp/resource") return
|
||||
requests++
|
||||
return json({
|
||||
location: { directory, project: { id: "proj_test", directory } },
|
||||
data: { resources, templates: [] },
|
||||
})
|
||||
}, events)
|
||||
let data!: ReturnType<typeof useData>
|
||||
|
||||
function Probe() {
|
||||
data = useData()
|
||||
return <text>{data.location.mcp.resource.catalog()?.resources[0]?.name ?? "missing"}</text>
|
||||
}
|
||||
|
||||
const app = await testRender(() => (
|
||||
<TestTuiContexts>
|
||||
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
|
||||
<ProjectProvider>
|
||||
<DataProvider>
|
||||
<Probe />
|
||||
</DataProvider>
|
||||
</ProjectProvider>
|
||||
</SDKProvider>
|
||||
</TestTuiContexts>
|
||||
))
|
||||
|
||||
try {
|
||||
await wait(() => requests === 1)
|
||||
expect(data.location.mcp.resource.catalog()?.resources[0]?.uri).toBe("docs://readme")
|
||||
|
||||
resources = [{ server: "docs", name: "Guide", uri: "docs://guide" }]
|
||||
emitEvent(events, {
|
||||
id: "evt_mcp_resources",
|
||||
created: 1,
|
||||
type: "mcp.resources.changed",
|
||||
data: { server: "docs" },
|
||||
})
|
||||
await wait(() => requests === 2 && data.location.mcp.resource.catalog()?.resources[0]?.name === "Guide")
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("updates session location when moved", async () => {
|
||||
const events = createEventStream()
|
||||
const destination = "/tmp/opencode-moved"
|
||||
const calls = createFetch((url) => {
|
||||
if (url.pathname === "/api/session/ses_test")
|
||||
return json({
|
||||
data: {
|
||||
id: "ses_test",
|
||||
projectID: "proj_test",
|
||||
cost: 0,
|
||||
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
time: { created: 0, updated: 0 },
|
||||
title: "Test session",
|
||||
location: { directory },
|
||||
},
|
||||
})
|
||||
}, events)
|
||||
let data!: ReturnType<typeof useData>
|
||||
let ready!: () => void
|
||||
const mounted = new Promise<void>((resolve) => {
|
||||
ready = resolve
|
||||
})
|
||||
|
||||
function Probe() {
|
||||
data = useData()
|
||||
onMount(ready)
|
||||
return <box />
|
||||
}
|
||||
|
||||
const app = await testRender(() => (
|
||||
<TestTuiContexts>
|
||||
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
|
||||
<ProjectProvider>
|
||||
<DataProvider>
|
||||
<Probe />
|
||||
</DataProvider>
|
||||
</ProjectProvider>
|
||||
</SDKProvider>
|
||||
</TestTuiContexts>
|
||||
))
|
||||
|
||||
try {
|
||||
await mounted
|
||||
await data.session.refresh("ses_test")
|
||||
emitEvent(events, {
|
||||
id: "evt_moved_1",
|
||||
created: 1,
|
||||
type: "session.moved",
|
||||
durable: durable("ses_test"),
|
||||
data: {
|
||||
sessionID: "ses_test",
|
||||
location: { directory: destination },
|
||||
subpath: "packages/cli",
|
||||
},
|
||||
})
|
||||
await wait(() => data.session.get("ses_test")?.location.directory === destination)
|
||||
expect(data.session.get("ses_test")?.subpath).toBe("packages/cli")
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("restores running manual compaction before applying live deltas", async () => {
|
||||
const events = createEventStream()
|
||||
const calls = createFetch((url) => {
|
||||
|
||||
@@ -99,11 +99,6 @@ export function createFetch(override?: FetchHandler, events?: ReturnType<typeof
|
||||
return json({ location: { directory, project: { id: "proj_test", directory: worktree } }, data: [] })
|
||||
if (url.pathname === "/api/mcp")
|
||||
return json({ location: { directory, project: { id: "proj_test", directory: worktree } }, data: [] })
|
||||
if (url.pathname === "/api/mcp/resource")
|
||||
return json({
|
||||
location: { directory, project: { id: "proj_test", directory: worktree } },
|
||||
data: { resources: [], templates: [] },
|
||||
})
|
||||
if (url.pathname === "/api/session") return json({ data: [], cursor: {} })
|
||||
if (url.pathname === "/api/session/active") return json({ data: {} })
|
||||
if (/^\/api\/session\/[^/]+\/form$/.test(url.pathname)) return json({ data: [] })
|
||||
|
||||
Reference in New Issue
Block a user