mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-01 07:53:33 -04:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1dc54945d5 | |||
| 3100701488 | |||
| 9c54291ae8 | |||
| 52ad916ba4 | |||
| 5db320b02d | |||
| 910e37f6d8 | |||
| 81f6e06681 | |||
| da68e2865e | |||
| f86e1cc1ff | |||
| 947bbf9490 | |||
| 3e57b43a4a | |||
| cb18a42a47 | |||
| 1fb2837fd3 | |||
| 66a8d830aa | |||
| d0733f83bf | |||
| b2ce195bda | |||
| 6c065ca6a5 | |||
| dd1c007877 | |||
| 8deb0e5780 | |||
| f488089179 | |||
| e3a39b214f | |||
| b6a2912bb1 |
@@ -101,7 +101,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
"@opencode-ai/client": "workspace:*",
|
"@opencode-ai/client": "workspace:*",
|
||||||
"@opencode-ai/codemode": "workspace:*",
|
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/plugin": "workspace:*",
|
"@opencode-ai/plugin": "workspace:*",
|
||||||
"@opencode-ai/schema": "workspace:*",
|
"@opencode-ai/schema": "workspace:*",
|
||||||
@@ -874,6 +873,7 @@
|
|||||||
"name": "@opencode-ai/simulation",
|
"name": "@opencode-ai/simulation",
|
||||||
"version": "1.17.13",
|
"version": "1.17.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@napi-rs/canvas": "1.0.2",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/llm": "workspace:*",
|
"@opencode-ai/llm": "workspace:*",
|
||||||
"@opentui/core": "catalog:",
|
"@opentui/core": "catalog:",
|
||||||
@@ -1612,11 +1612,11 @@
|
|||||||
|
|
||||||
"@emmetio/stream-reader-utils": ["@emmetio/stream-reader-utils@0.1.0", "", {}, "sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A=="],
|
"@emmetio/stream-reader-utils": ["@emmetio/stream-reader-utils@0.1.0", "", {}, "sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A=="],
|
||||||
|
|
||||||
"@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/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="],
|
||||||
|
|
||||||
"@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="],
|
"@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="],
|
||||||
|
|
||||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
|
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
|
||||||
|
|
||||||
"@emotion/is-prop-valid": ["@emotion/is-prop-valid@0.8.8", "", { "dependencies": { "@emotion/memoize": "0.7.4" } }, "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA=="],
|
"@emotion/is-prop-valid": ["@emotion/is-prop-valid@0.8.8", "", { "dependencies": { "@emotion/memoize": "0.7.4" } }, "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA=="],
|
||||||
|
|
||||||
@@ -1984,6 +1984,30 @@
|
|||||||
|
|
||||||
"@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4", "", { "os": "win32", "cpu": "x64" }, "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ=="],
|
"@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=="],
|
"@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=="],
|
"@noble/hashes": ["@noble/hashes@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="],
|
||||||
@@ -2278,7 +2302,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-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.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="],
|
"@oxc-project/types": ["@oxc-project/types@0.138.0", "", {}, "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA=="],
|
||||||
|
|
||||||
"@oxc-resolver/binding-android-arm-eabi": ["@oxc-resolver/binding-android-arm-eabi@11.20.0", "", { "os": "android", "cpu": "arm" }, "sha512-IjfWOXRgJFNdORDl+Uf1aibNgZY2guOD3zmOhx1BGVb/MIiqlFTdmjpQNplSN58lhWehnX4UNqC3QwpUo8pjJg=="],
|
"@oxc-resolver/binding-android-arm-eabi": ["@oxc-resolver/binding-android-arm-eabi@11.20.0", "", { "os": "android", "cpu": "arm" }, "sha512-IjfWOXRgJFNdORDl+Uf1aibNgZY2guOD3zmOhx1BGVb/MIiqlFTdmjpQNplSN58lhWehnX4UNqC3QwpUo8pjJg=="],
|
||||||
|
|
||||||
@@ -2570,7 +2594,37 @@
|
|||||||
|
|
||||||
"@remix-run/router": ["@remix-run/router@1.9.0", "", {}, "sha512-bV63itrKBC0zdT27qYm6SDZHlkXwFL1xMBuhkn+X7l0+IIhNaH5wuuvZKp6eKhCD4KFhujhfhCT1YxXW6esUIA=="],
|
"@remix-run/router": ["@remix-run/router@1.9.0", "", {}, "sha512-bV63itrKBC0zdT27qYm6SDZHlkXwFL1xMBuhkn+X7l0+IIhNaH5wuuvZKp6eKhCD4KFhujhfhCT1YxXW6esUIA=="],
|
||||||
|
|
||||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
|
"@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=="],
|
||||||
|
|
||||||
"@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=="],
|
"@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=="],
|
||||||
|
|
||||||
@@ -3112,6 +3166,8 @@
|
|||||||
|
|
||||||
"@types/react": ["@types/react@18.0.25", "", { "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", "csstype": "^3.0.2" } }, "sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g=="],
|
"@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/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=="],
|
"@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="],
|
||||||
@@ -3184,6 +3240,12 @@
|
|||||||
|
|
||||||
"@valibot/to-json-schema": ["@valibot/to-json-schema@1.6.0", "", { "peerDependencies": { "valibot": "^1.3.0" } }, "sha512-d6rYyK5KVa2XdqamWgZ4/Nr+cXhxjy7lmpe6Iajw15J/jmU+gyxl2IEd1Otg1d7Rl3gOQL5reulnSypzBtYy1A=="],
|
"@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=="],
|
"@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=="],
|
"@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=="],
|
||||||
@@ -4988,6 +5050,8 @@
|
|||||||
|
|
||||||
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
||||||
@@ -5402,6 +5466,8 @@
|
|||||||
|
|
||||||
"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=="],
|
"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=="],
|
"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=="],
|
"rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="],
|
||||||
@@ -6022,8 +6088,12 @@
|
|||||||
|
|
||||||
"wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="],
|
"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-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=="],
|
"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=="],
|
"xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="],
|
||||||
@@ -6620,6 +6690,10 @@
|
|||||||
|
|
||||||
"@oslojs/jwt/@oslojs/encoding": ["@oslojs/encoding@0.4.1", "", {}, "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q=="],
|
"@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/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=="],
|
"@oxc-resolver/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
|
||||||
@@ -6640,6 +6714,8 @@
|
|||||||
|
|
||||||
"@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=="],
|
"@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=="],
|
"@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=="],
|
"@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=="],
|
||||||
@@ -6748,6 +6824,14 @@
|
|||||||
|
|
||||||
"@types/plist/xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="],
|
"@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/@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=="],
|
"@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=="],
|
||||||
@@ -7044,6 +7128,8 @@
|
|||||||
|
|
||||||
"openid-client/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
|
"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-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=="],
|
"p-locate/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
|
||||||
@@ -7708,6 +7794,10 @@
|
|||||||
|
|
||||||
"@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
"@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/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=="],
|
"@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=="],
|
||||||
@@ -7716,6 +7806,8 @@
|
|||||||
|
|
||||||
"@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=="],
|
"@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/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=="],
|
"@sentry/bundler-plugin-core/glob/minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="],
|
||||||
@@ -7786,6 +7878,22 @@
|
|||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="],
|
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="],
|
||||||
|
|
||||||
|
"@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/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=="],
|
"@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="],
|
||||||
@@ -8352,6 +8460,8 @@
|
|||||||
|
|
||||||
"@stoplight/spectral-core/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
"@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/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=="],
|
"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=="],
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const profiles = [
|
|||||||
{ name: "edit", tool: "edit", input: { filePath: "src/edit.ts" } },
|
{ name: "edit", tool: "edit", input: { filePath: "src/edit.ts" } },
|
||||||
{
|
{
|
||||||
name: "multi patch",
|
name: "multi patch",
|
||||||
tool: "apply_patch",
|
tool: "patch",
|
||||||
input: { files: ["src/a.ts", "src/b.ts", "src/old.ts", "src/moved.ts"] },
|
input: { files: ["src/a.ts", "src/b.ts", "src/old.ts", "src/moved.ts"] },
|
||||||
},
|
},
|
||||||
] as const
|
] as const
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ test("adds patch files incrementally without resetting outer expansion", async (
|
|||||||
userMessage(),
|
userMessage(),
|
||||||
assistantMessage(
|
assistantMessage(
|
||||||
[
|
[
|
||||||
toolPart(patchID, "apply_patch", "running", { files: [first.filePath] }, { metadata: { files: [first] } }),
|
toolPart(patchID, "patch", "running", { files: [first.filePath] }, { metadata: { files: [first] } }),
|
||||||
textPart(followingID, "Following incremental patch"),
|
textPart(followingID, "Following incremental patch"),
|
||||||
],
|
],
|
||||||
{ completed: false },
|
{ completed: false },
|
||||||
@@ -49,7 +49,7 @@ test("adds patch files incrementally without resetting outer expansion", async (
|
|||||||
partUpdated(
|
partUpdated(
|
||||||
toolPart(
|
toolPart(
|
||||||
patchID,
|
patchID,
|
||||||
"apply_patch",
|
"patch",
|
||||||
"running",
|
"running",
|
||||||
{ files: [first.filePath, second.filePath] },
|
{ files: [first.filePath, second.filePath] },
|
||||||
{ metadata: { files: [first, second] } },
|
{ metadata: { files: [first, second] } },
|
||||||
@@ -61,7 +61,7 @@ test("adds patch files incrementally without resetting outer expansion", async (
|
|||||||
partUpdated(
|
partUpdated(
|
||||||
toolPart(
|
toolPart(
|
||||||
patchID,
|
patchID,
|
||||||
"apply_patch",
|
"patch",
|
||||||
"completed",
|
"completed",
|
||||||
{ files: [first.filePath, second.filePath, third.filePath] },
|
{ files: [first.filePath, second.filePath, third.filePath] },
|
||||||
{ metadata: { files: [first, second, third] } },
|
{ metadata: { files: [first, second, third] } },
|
||||||
|
|||||||
@@ -295,7 +295,7 @@ function performanceTurn(index: number) {
|
|||||||
messageID: assistantID,
|
messageID: assistantID,
|
||||||
type: "tool",
|
type: "tool",
|
||||||
callID: `call_0000_${suffix}_patch`,
|
callID: `call_0000_${suffix}_patch`,
|
||||||
tool: "apply_patch",
|
tool: "patch",
|
||||||
state: {
|
state: {
|
||||||
status: "completed",
|
status: "completed",
|
||||||
input: { patchText: realisticPatch(index) },
|
input: { patchText: realisticPatch(index) },
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ function toolPart(
|
|||||||
): MessagePart {
|
): MessagePart {
|
||||||
const metadata =
|
const metadata =
|
||||||
metadataOverride ??
|
metadataOverride ??
|
||||||
(tool === "apply_patch"
|
(tool === "patch"
|
||||||
? { files: [patchFile(index, "update"), patchFile(index + 1, index % 2 === 0 ? "add" : "delete")] }
|
? { files: [patchFile(index, "update"), patchFile(index + 1, index % 2 === 0 ? "add" : "delete")] }
|
||||||
: tool === "edit" || tool === "write"
|
: tool === "edit" || tool === "write"
|
||||||
? {
|
? {
|
||||||
@@ -219,7 +219,7 @@ function turn(index: number): Message[] {
|
|||||||
? [toolPart(index, 7, "write", { filePath: `src/generated/write-${index}.ts`, content: code(index, 28) }, 560)]
|
? [toolPart(index, 7, "write", { filePath: `src/generated/write-${index}.ts`, content: code(index, 28) }, 560)]
|
||||||
: []),
|
: []),
|
||||||
...(index % 8 === 0
|
...(index % 8 === 0
|
||||||
? [toolPart(index, 8, "apply_patch", { files: [`src/generated/patch-${index}.ts`] }, 620)]
|
? [toolPart(index, 8, "patch", { files: [`src/generated/patch-${index}.ts`] }, 620)]
|
||||||
: []),
|
: []),
|
||||||
...(index % 7 === 0
|
...(index % 7 === 0
|
||||||
? [toolPart(index, 4, "bash", { command: "bun typecheck", description: "Verify generated output" }, 620)]
|
? [toolPart(index, 4, "bash", { command: "bun typecheck", description: "Verify generated output" }, 620)]
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ test("renders a completed single-file patch", async ({ page }) => {
|
|||||||
assistantMessage([
|
assistantMessage([
|
||||||
toolPart(
|
toolPart(
|
||||||
id,
|
id,
|
||||||
"apply_patch",
|
"patch",
|
||||||
"completed",
|
"completed",
|
||||||
{ files: ["src/a.ts"] },
|
{ files: ["src/a.ts"] },
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ test("preserves nested patch file state through outer collapse and reopen", asyn
|
|||||||
assistantMessage([
|
assistantMessage([
|
||||||
toolPart(
|
toolPart(
|
||||||
patchID,
|
patchID,
|
||||||
"apply_patch",
|
"patch",
|
||||||
"completed",
|
"completed",
|
||||||
{ files: files.map((file) => file.filePath) },
|
{ files: files.map((file) => file.filePath) },
|
||||||
{ metadata: { files } },
|
{ metadata: { files } },
|
||||||
|
|||||||
@@ -246,7 +246,7 @@ function editPart(id: string) {
|
|||||||
function patchPart(id: string) {
|
function patchPart(id: string) {
|
||||||
return toolPart(
|
return toolPart(
|
||||||
id,
|
id,
|
||||||
"apply_patch",
|
"patch",
|
||||||
"completed",
|
"completed",
|
||||||
{ files: ["src/a.ts", "src/b.ts"] },
|
{ files: ["src/a.ts", "src/b.ts"] },
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
} from "../performance/timeline-stability/fixture"
|
} from "../performance/timeline-stability/fixture"
|
||||||
|
|
||||||
test("renders every tool error outcome without leaking hidden tools", async ({ page }) => {
|
test("renders every tool error outcome without leaking hidden tools", async ({ page }) => {
|
||||||
const ordinary = ["bash", "edit", "write", "apply_patch", "webfetch", "websearch", "task", "skill", "mcp_probe"]
|
const ordinary = ["bash", "edit", "write", "patch", "webfetch", "websearch", "task", "skill", "mcp_probe"]
|
||||||
const parts = ordinary.map((tool, index) =>
|
const parts = ordinary.map((tool, index) =>
|
||||||
toolPart(`prt_error_${index}`, tool, "error", errorInput(tool), { error: `${tool} failed visibly` }),
|
toolPart(`prt_error_${index}`, tool, "error", errorInput(tool), { error: `${tool} failed visibly` }),
|
||||||
)
|
)
|
||||||
@@ -90,7 +90,7 @@ function questionInput() {
|
|||||||
function errorInput(tool: string) {
|
function errorInput(tool: string) {
|
||||||
if (tool === "bash") return { command: "exit 1" }
|
if (tool === "bash") return { command: "exit 1" }
|
||||||
if (["edit", "write"].includes(tool)) return { filePath: "src/error.ts", content: "" }
|
if (["edit", "write"].includes(tool)) return { filePath: "src/error.ts", content: "" }
|
||||||
if (tool === "apply_patch") return { files: ["src/error.ts"] }
|
if (tool === "patch") return { files: ["src/error.ts"] }
|
||||||
if (tool === "webfetch") return { url: "https://example.com" }
|
if (tool === "webfetch") return { url: "https://example.com" }
|
||||||
if (tool === "websearch") return { query: "failure" }
|
if (tool === "websearch") return { query: "failure" }
|
||||||
if (tool === "task") return { description: "Fail task", subagent_type: "explore" }
|
if (tool === "task") return { description: "Fail task", subagent_type: "explore" }
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ function toolPart(
|
|||||||
outputLength = 160,
|
outputLength = 160,
|
||||||
): MessagePart {
|
): MessagePart {
|
||||||
const metadata =
|
const metadata =
|
||||||
tool === "apply_patch"
|
tool === "patch"
|
||||||
? { files: [patchFile(index, "update"), patchFile(index + 1, index % 2 === 0 ? "add" : "delete")] }
|
? { files: [patchFile(index, "update"), patchFile(index + 1, index % 2 === 0 ? "add" : "delete")] }
|
||||||
: tool === "edit" || tool === "write"
|
: tool === "edit" || tool === "write"
|
||||||
? {
|
? {
|
||||||
@@ -199,7 +199,7 @@ function turn(index: number): Message[] {
|
|||||||
? [toolPart(index, 7, "write", { filePath: `src/generated/write-${index}.ts`, content: code(index, 28) }, 560)]
|
? [toolPart(index, 7, "write", { filePath: `src/generated/write-${index}.ts`, content: code(index, 28) }, 560)]
|
||||||
: []),
|
: []),
|
||||||
...(index % 8 === 0
|
...(index % 8 === 0
|
||||||
? [toolPart(index, 8, "apply_patch", { files: [`src/generated/patch-${index}.ts`] }, 620)]
|
? [toolPart(index, 8, "patch", { files: [`src/generated/patch-${index}.ts`] }, 620)]
|
||||||
: []),
|
: []),
|
||||||
...(index % 7 === 0 ? [toolPart(index, 4, "bash", { command: "bun typecheck" }, 620)] : []),
|
...(index % 7 === 0 ? [toolPart(index, 4, "bash", { command: "bun typecheck" }, 620)] : []),
|
||||||
...(index % 10 === 0 ? [toolPart(index, 9, "webfetch", { url: "https://example.com/docs/sample" }, 120)] : []),
|
...(index % 10 === 0 ? [toolPart(index, 9, "webfetch", { url: "https://example.com/docs/sample" }, 120)] : []),
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
|
|||||||
const path = url.pathname
|
const path = url.pathname
|
||||||
if (path === "/global/event" || path === "/event") return sse(route, config.events?.(), config.eventRetry)
|
if (path === "/global/event" || path === "/event") return sse(route, config.events?.(), config.eventRetry)
|
||||||
if (path === "/global/health") return json(route, { healthy: true })
|
if (path === "/global/health") return json(route, { healthy: true })
|
||||||
if (path === "/experimental/capabilities") return json(route, { backgroundSubagents: false })
|
if (path === "/experimental/capabilities") return json(route, { backgroundSubagents: true })
|
||||||
if (path === "/permission")
|
if (path === "/permission")
|
||||||
return json(route, typeof config.permissions === "function" ? config.permissions() : (config.permissions ?? []))
|
return json(route, typeof config.permissions === "function" ? config.permissions() : (config.permissions ?? []))
|
||||||
if (path === "/question")
|
if (path === "/question")
|
||||||
|
|||||||
@@ -1245,7 +1245,7 @@ export function MessageTimeline(props: {
|
|||||||
const value = row()
|
const value = row()
|
||||||
if (value._tag !== "AssistantPart" || value.group.type !== "part") return false
|
if (value._tag !== "AssistantPart" || value.group.type !== "part") return false
|
||||||
const part = getMsgPart(value.group.ref.messageID, value.group.ref.partID)
|
const part = getMsgPart(value.group.ref.messageID, value.group.ref.partID)
|
||||||
return part?.type === "tool" && ["edit", "write", "apply_patch"].includes(part.tool)
|
return part?.type === "tool" && ["edit", "write", "patch", "apply_patch"].includes(part.tool)
|
||||||
}
|
}
|
||||||
const [ready, setReady] = createSignal(initialItem.size <= timelineFallbackItemSize || !asyncFile())
|
const [ready, setReady] = createSignal(initialItem.size <= timelineFallbackItemSize || !asyncFile())
|
||||||
let contentMeasureFrame: number | undefined
|
let contentMeasureFrame: number | undefined
|
||||||
|
|||||||
Regular → Executable
@@ -33,7 +33,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
"@opencode-ai/client": "workspace:*",
|
"@opencode-ai/client": "workspace:*",
|
||||||
"@opencode-ai/codemode": "workspace:*",
|
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/plugin": "workspace:*",
|
"@opencode-ai/plugin": "workspace:*",
|
||||||
"@opencode-ai/schema": "workspace:*",
|
"@opencode-ai/schema": "workspace:*",
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
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),
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -628,11 +628,11 @@ function emitEdit(state: State): void {
|
|||||||
|
|
||||||
function emitPatch(state: State): void {
|
function emitPatch(state: State): void {
|
||||||
const file = path.join(process.cwd(), "src", "demo-format.ts")
|
const file = path.join(process.cwd(), "src", "demo-format.ts")
|
||||||
const ref = make(state, "apply_patch", {
|
const ref = make(state, "patch", {
|
||||||
patchText: "*** Begin Patch\n*** End Patch",
|
patchText: "*** Begin Patch\n*** End Patch",
|
||||||
})
|
})
|
||||||
doneTool(state, ref, {
|
doneTool(state, ref, {
|
||||||
title: "apply_patch",
|
title: "patch",
|
||||||
output: "",
|
output: "",
|
||||||
metadata: {
|
metadata: {
|
||||||
files: [
|
files: [
|
||||||
|
|||||||
@@ -84,7 +84,6 @@ type RunFooterOptions = {
|
|||||||
theme: RunTheme
|
theme: RunTheme
|
||||||
keymap: Keymap<Renderable, KeyEvent>
|
keymap: Keymap<Renderable, KeyEvent>
|
||||||
tuiConfig: RunTuiConfig
|
tuiConfig: RunTuiConfig
|
||||||
backgroundSubagents: boolean
|
|
||||||
diffStyle: RunDiffStyle
|
diffStyle: RunDiffStyle
|
||||||
onPermissionReply: (input: PermissionReply) => void | Promise<void>
|
onPermissionReply: (input: PermissionReply) => void | Promise<void>
|
||||||
onQuestionReply: (input: QuestionReply) => void | Promise<void>
|
onQuestionReply: (input: QuestionReply) => void | Promise<void>
|
||||||
@@ -326,7 +325,6 @@ export class RunFooter implements FooterApi {
|
|||||||
theme: footer.theme,
|
theme: footer.theme,
|
||||||
diffStyle: options.diffStyle,
|
diffStyle: options.diffStyle,
|
||||||
tuiConfig: options.tuiConfig,
|
tuiConfig: options.tuiConfig,
|
||||||
backgroundSubagents: options.backgroundSubagents,
|
|
||||||
history: footer.history,
|
history: footer.history,
|
||||||
agent: options.agentLabel,
|
agent: options.agentLabel,
|
||||||
onSubmit: footer.handlePrompt,
|
onSubmit: footer.handlePrompt,
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ type RunFooterViewProps = {
|
|||||||
theme: () => RunTheme
|
theme: () => RunTheme
|
||||||
diffStyle?: RunDiffStyle
|
diffStyle?: RunDiffStyle
|
||||||
tuiConfig: RunTuiConfig
|
tuiConfig: RunTuiConfig
|
||||||
backgroundSubagents: boolean
|
|
||||||
history?: () => RunPrompt[]
|
history?: () => RunPrompt[]
|
||||||
agent: string
|
agent: string
|
||||||
onSubmit: (input: RunPrompt) => boolean
|
onSubmit: (input: RunPrompt) => boolean
|
||||||
@@ -169,9 +168,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||||||
|
|
||||||
return tabs().findIndex((item) => item.sessionID === sessionID) + 1
|
return tabs().findIndex((item) => item.sessionID === sessionID) + 1
|
||||||
})
|
})
|
||||||
const foregroundSubagents = createMemo(
|
const foregroundSubagents = createMemo(() => activeTabs().some((item) => !item.background))
|
||||||
() => props.backgroundSubagents && activeTabs().some((item) => !item.background),
|
|
||||||
)
|
|
||||||
const model = createMemo(() => {
|
const model = createMemo(() => {
|
||||||
const current = props.currentModel()
|
const current = props.currentModel()
|
||||||
return current ? modelInfo(props.providers(), current) : { model: props.state().model, provider: undefined }
|
return current ? modelInfo(props.providers(), current) : { model: props.state().model, provider: undefined }
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { NodeFileSystem } from "@effect/platform-node"
|
import { NodeFileSystem } from "@effect/platform-node"
|
||||||
import { OpenCode, type OpenCodeClient } from "@opencode-ai/client/promise"
|
import { OpenCode, type OpenCodeClient } from "@opencode-ai/client/promise"
|
||||||
import { truthy } from "@opencode-ai/core/flag/flag"
|
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
@@ -84,9 +83,6 @@ export async function runMini(input: MiniCommandInput) {
|
|||||||
files: [],
|
files: [],
|
||||||
initialInput,
|
initialInput,
|
||||||
thinking: true,
|
thinking: true,
|
||||||
backgroundSubagents:
|
|
||||||
truthy("OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS") ||
|
|
||||||
(process.env.OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS === undefined && truthy("OPENCODE_EXPERIMENTAL")),
|
|
||||||
replay: input.replay ?? true,
|
replay: input.replay ?? true,
|
||||||
replayLimit: input.replayLimit,
|
replayLimit: input.replayLimit,
|
||||||
demo: input.demo,
|
demo: input.demo,
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ export type LifecycleInput = {
|
|||||||
model: RunInput["model"]
|
model: RunInput["model"]
|
||||||
variant: string | undefined
|
variant: string | undefined
|
||||||
tuiConfig: RunTuiConfig | Promise<RunTuiConfig>
|
tuiConfig: RunTuiConfig | Promise<RunTuiConfig>
|
||||||
backgroundSubagents: boolean
|
|
||||||
onPermissionReply: (input: PermissionReply) => void | Promise<void>
|
onPermissionReply: (input: PermissionReply) => void | Promise<void>
|
||||||
onQuestionReply: (input: QuestionReply) => void | Promise<void>
|
onQuestionReply: (input: QuestionReply) => void | Promise<void>
|
||||||
onQuestionReject: (input: QuestionReject) => void | Promise<void>
|
onQuestionReject: (input: QuestionReject) => void | Promise<void>
|
||||||
@@ -236,7 +235,6 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
|
|||||||
wrote,
|
wrote,
|
||||||
keymap,
|
keymap,
|
||||||
tuiConfig,
|
tuiConfig,
|
||||||
backgroundSubagents: input.backgroundSubagents,
|
|
||||||
diffStyle: tuiConfig.diff_style ?? "auto",
|
diffStyle: tuiConfig.diff_style ?? "auto",
|
||||||
onPermissionReply: input.onPermissionReply,
|
onPermissionReply: input.onPermissionReply,
|
||||||
onQuestionReply: input.onQuestionReply,
|
onQuestionReply: input.onQuestionReply,
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ type RunRuntimeInput = {
|
|||||||
files: RunInput["files"]
|
files: RunInput["files"]
|
||||||
initialInput?: string
|
initialInput?: string
|
||||||
thinking: boolean
|
thinking: boolean
|
||||||
backgroundSubagents: boolean
|
|
||||||
replay?: boolean
|
replay?: boolean
|
||||||
replayLimit?: number
|
replayLimit?: number
|
||||||
demo?: RunInput["demo"]
|
demo?: RunInput["demo"]
|
||||||
@@ -75,7 +74,6 @@ type RunDeferredInput = {
|
|||||||
files: RunInput["files"]
|
files: RunInput["files"]
|
||||||
initialInput?: string
|
initialInput?: string
|
||||||
thinking: boolean
|
thinking: boolean
|
||||||
backgroundSubagents: boolean
|
|
||||||
replay?: boolean
|
replay?: boolean
|
||||||
replayLimit?: number
|
replayLimit?: number
|
||||||
demo?: RunInput["demo"]
|
demo?: RunInput["demo"]
|
||||||
@@ -270,7 +268,6 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
|
|||||||
model: state.model,
|
model: state.model,
|
||||||
variant: state.activeVariant,
|
variant: state.activeVariant,
|
||||||
tuiConfig: tuiConfigTask,
|
tuiConfig: tuiConfigTask,
|
||||||
backgroundSubagents: input.backgroundSubagents,
|
|
||||||
onPermissionReply: async (next) => {
|
onPermissionReply: async (next) => {
|
||||||
if (state.demo?.permission(next)) {
|
if (state.demo?.permission(next)) {
|
||||||
return
|
return
|
||||||
@@ -876,7 +873,6 @@ export async function runInteractiveDeferredMode(input: RunDeferredInput, deps?:
|
|||||||
files: input.files,
|
files: input.files,
|
||||||
initialInput: input.initialInput,
|
initialInput: input.initialInput,
|
||||||
thinking: input.thinking,
|
thinking: input.thinking,
|
||||||
backgroundSubagents: input.backgroundSubagents,
|
|
||||||
replay: input.replay,
|
replay: input.replay,
|
||||||
replayLimit: input.replayLimit,
|
replayLimit: input.replayLimit,
|
||||||
demo: input.demo,
|
demo: input.demo,
|
||||||
@@ -928,7 +924,6 @@ export async function runInteractiveMode(
|
|||||||
files: input.files,
|
files: input.files,
|
||||||
initialInput: input.initialInput,
|
initialInput: input.initialInput,
|
||||||
thinking: input.thinking,
|
thinking: input.thinking,
|
||||||
backgroundSubagents: input.backgroundSubagents,
|
|
||||||
replay: input.replay,
|
replay: input.replay,
|
||||||
replayLimit: input.replayLimit,
|
replayLimit: input.replayLimit,
|
||||||
demo: input.demo,
|
demo: input.demo,
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ type ToolName =
|
|||||||
| "bash"
|
| "bash"
|
||||||
| "write"
|
| "write"
|
||||||
| "edit"
|
| "edit"
|
||||||
| "apply_patch"
|
| "patch"
|
||||||
| "batch"
|
| "batch"
|
||||||
| "task"
|
| "task"
|
||||||
| "todowrite"
|
| "todowrite"
|
||||||
@@ -1094,7 +1094,7 @@ const TOOL_RULES = {
|
|||||||
},
|
},
|
||||||
permission: permEdit,
|
permission: permEdit,
|
||||||
},
|
},
|
||||||
apply_patch: {
|
patch: {
|
||||||
view: {
|
view: {
|
||||||
output: false,
|
output: false,
|
||||||
final: true,
|
final: true,
|
||||||
|
|||||||
@@ -135,7 +135,6 @@ export type RunInput = {
|
|||||||
files: RunFilePart[]
|
files: RunFilePart[]
|
||||||
initialInput?: string
|
initialInput?: string
|
||||||
thinking: boolean
|
thinking: boolean
|
||||||
backgroundSubagents: boolean
|
|
||||||
demo?: boolean
|
demo?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,13 +7,11 @@ import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
|||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||||
import { AppProcess } from "@opencode-ai/core/process"
|
import { AppProcess } from "@opencode-ai/core/process"
|
||||||
import { Flock } from "@opencode-ai/core/util/flock"
|
|
||||||
import { start } from "@opencode-ai/server/process"
|
import { start } from "@opencode-ai/server/process"
|
||||||
import { randomBytes, randomUUID } from "node:crypto"
|
import { randomBytes, randomUUID } from "node:crypto"
|
||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
import { Effect, FileSystem, Logger, Option, Redacted, Schedule, Schema } from "effect"
|
import { Effect, FileSystem, Logger, Option, Redacted, Schedule, Schema } from "effect"
|
||||||
import { HttpServer } from "effect/unstable/http"
|
import { HttpServer } from "effect/unstable/http"
|
||||||
import { CodeModeHost } from "./code-mode"
|
|
||||||
import { Env } from "./env"
|
import { Env } from "./env"
|
||||||
import { ServiceConfig } from "./services/service-config"
|
import { ServiceConfig } from "./services/service-config"
|
||||||
import { Updater } from "./services/updater"
|
import { Updater } from "./services/updater"
|
||||||
@@ -38,14 +36,6 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
|
|||||||
if (options.mode === "service") yield* Effect.sync(() => process.chdir(Global.Path.home))
|
if (options.mode === "service") yield* Effect.sync(() => process.chdir(Global.Path.home))
|
||||||
return yield* Effect.scoped(
|
return yield* Effect.scoped(
|
||||||
Effect.gen(function* () {
|
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
|
const environmentPassword = yield* Env.password
|
||||||
// Keep the lease credential out of the environment inherited by tools.
|
// Keep the lease credential out of the environment inherited by tools.
|
||||||
if (options.mode === "stdio") {
|
if (options.mode === "stdio") {
|
||||||
@@ -64,7 +54,6 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
|
|||||||
hostname: options.hostname ?? config.hostname ?? "127.0.0.1",
|
hostname: options.hostname ?? config.hostname ?? "127.0.0.1",
|
||||||
port: Option.fromNullishOr(options.port ?? config.port),
|
port: Option.fromNullishOr(options.port ?? config.port),
|
||||||
password,
|
password,
|
||||||
replacements: (server) => CodeModeHost.replacements(server, password),
|
|
||||||
}).pipe(Effect.provide(Logger.layer([], { mergeWithExisting: false })))
|
}).pipe(Effect.provide(Logger.layer([], { mergeWithExisting: false })))
|
||||||
if (options.mode === "service") yield* register(address, password)
|
if (options.mode === "service") yield* register(address, password)
|
||||||
const url = HttpServer.formatAddress(address)
|
const url = HttpServer.formatAddress(address)
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
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,157 +111,167 @@ export type Endpoint4_8Input = {
|
|||||||
export type Endpoint4_8Output = EffectValue<ReturnType<RawClient["server.session"]["session.rename"]>>
|
export type Endpoint4_8Output = EffectValue<ReturnType<RawClient["server.session"]["session.rename"]>>
|
||||||
export type SessionRenameOperation<E = never> = (input: Endpoint4_8Input) => Effect.Effect<Endpoint4_8Output, E>
|
export type SessionRenameOperation<E = never> = (input: Endpoint4_8Input) => Effect.Effect<Endpoint4_8Output, E>
|
||||||
|
|
||||||
type Endpoint4_9Request = Parameters<RawClient["server.session"]["session.prompt"]>[0]
|
type Endpoint4_9Request = Parameters<RawClient["server.session"]["session.move"]>[0]
|
||||||
export type Endpoint4_9Input = {
|
export type Endpoint4_9Input = {
|
||||||
readonly sessionID: Endpoint4_9Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_9Request["params"]["sessionID"]
|
||||||
readonly id?: Endpoint4_9Request["payload"]["id"]
|
readonly destination: Endpoint4_9Request["payload"]["destination"]
|
||||||
readonly prompt: Endpoint4_9Request["payload"]["prompt"]
|
readonly moveChanges?: Endpoint4_9Request["payload"]["moveChanges"]
|
||||||
readonly delivery?: Endpoint4_9Request["payload"]["delivery"]
|
|
||||||
readonly resume?: Endpoint4_9Request["payload"]["resume"]
|
|
||||||
}
|
}
|
||||||
export type Endpoint4_9Output = EffectValue<ReturnType<RawClient["server.session"]["session.prompt"]>>["data"]
|
export type Endpoint4_9Output = EffectValue<ReturnType<RawClient["server.session"]["session.move"]>>
|
||||||
export type SessionPromptOperation<E = never> = (input: Endpoint4_9Input) => Effect.Effect<Endpoint4_9Output, E>
|
export type SessionMoveOperation<E = never> = (input: Endpoint4_9Input) => Effect.Effect<Endpoint4_9Output, E>
|
||||||
|
|
||||||
type Endpoint4_10Request = Parameters<RawClient["server.session"]["session.command"]>[0]
|
type Endpoint4_10Request = Parameters<RawClient["server.session"]["session.prompt"]>[0]
|
||||||
export type Endpoint4_10Input = {
|
export type Endpoint4_10Input = {
|
||||||
readonly sessionID: Endpoint4_10Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_10Request["params"]["sessionID"]
|
||||||
readonly id?: Endpoint4_10Request["payload"]["id"]
|
readonly id?: Endpoint4_10Request["payload"]["id"]
|
||||||
readonly command: Endpoint4_10Request["payload"]["command"]
|
readonly prompt: Endpoint4_10Request["payload"]["prompt"]
|
||||||
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 delivery?: Endpoint4_10Request["payload"]["delivery"]
|
||||||
readonly resume?: Endpoint4_10Request["payload"]["resume"]
|
readonly resume?: Endpoint4_10Request["payload"]["resume"]
|
||||||
}
|
}
|
||||||
export type Endpoint4_10Output = EffectValue<ReturnType<RawClient["server.session"]["session.command"]>>["data"]
|
export type Endpoint4_10Output = EffectValue<ReturnType<RawClient["server.session"]["session.prompt"]>>["data"]
|
||||||
export type SessionCommandOperation<E = never> = (input: Endpoint4_10Input) => Effect.Effect<Endpoint4_10Output, E>
|
export type SessionPromptOperation<E = never> = (input: Endpoint4_10Input) => Effect.Effect<Endpoint4_10Output, E>
|
||||||
|
|
||||||
type Endpoint4_11Request = Parameters<RawClient["server.session"]["session.skill"]>[0]
|
type Endpoint4_11Request = Parameters<RawClient["server.session"]["session.command"]>[0]
|
||||||
export type Endpoint4_11Input = {
|
export type Endpoint4_11Input = {
|
||||||
readonly sessionID: Endpoint4_11Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_11Request["params"]["sessionID"]
|
||||||
readonly id?: Endpoint4_11Request["payload"]["id"]
|
readonly id?: Endpoint4_11Request["payload"]["id"]
|
||||||
readonly skill: Endpoint4_11Request["payload"]["skill"]
|
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"]
|
readonly resume?: Endpoint4_11Request["payload"]["resume"]
|
||||||
}
|
}
|
||||||
export type Endpoint4_11Output = EffectValue<ReturnType<RawClient["server.session"]["session.skill"]>>
|
export type Endpoint4_11Output = EffectValue<ReturnType<RawClient["server.session"]["session.command"]>>["data"]
|
||||||
export type SessionSkillOperation<E = never> = (input: Endpoint4_11Input) => Effect.Effect<Endpoint4_11Output, E>
|
export type SessionCommandOperation<E = never> = (input: Endpoint4_11Input) => Effect.Effect<Endpoint4_11Output, E>
|
||||||
|
|
||||||
type Endpoint4_12Request = Parameters<RawClient["server.session"]["session.synthetic"]>[0]
|
type Endpoint4_12Request = Parameters<RawClient["server.session"]["session.skill"]>[0]
|
||||||
export type Endpoint4_12Input = {
|
export type Endpoint4_12Input = {
|
||||||
readonly sessionID: Endpoint4_12Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_12Request["params"]["sessionID"]
|
||||||
readonly text: Endpoint4_12Request["payload"]["text"]
|
readonly id?: Endpoint4_12Request["payload"]["id"]
|
||||||
readonly description?: Endpoint4_12Request["payload"]["description"]
|
readonly skill: Endpoint4_12Request["payload"]["skill"]
|
||||||
readonly metadata?: Endpoint4_12Request["payload"]["metadata"]
|
readonly resume?: Endpoint4_12Request["payload"]["resume"]
|
||||||
}
|
}
|
||||||
export type Endpoint4_12Output = EffectValue<ReturnType<RawClient["server.session"]["session.synthetic"]>>
|
export type Endpoint4_12Output = EffectValue<ReturnType<RawClient["server.session"]["session.skill"]>>
|
||||||
export type SessionSyntheticOperation<E = never> = (input: Endpoint4_12Input) => Effect.Effect<Endpoint4_12Output, E>
|
export type SessionSkillOperation<E = never> = (input: Endpoint4_12Input) => Effect.Effect<Endpoint4_12Output, E>
|
||||||
|
|
||||||
type Endpoint4_13Request = Parameters<RawClient["server.session"]["session.shell"]>[0]
|
type Endpoint4_13Request = Parameters<RawClient["server.session"]["session.synthetic"]>[0]
|
||||||
export type Endpoint4_13Input = {
|
export type Endpoint4_13Input = {
|
||||||
readonly sessionID: Endpoint4_13Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_13Request["params"]["sessionID"]
|
||||||
readonly id?: Endpoint4_13Request["payload"]["id"]
|
readonly text: Endpoint4_13Request["payload"]["text"]
|
||||||
readonly command: Endpoint4_13Request["payload"]["command"]
|
readonly description?: Endpoint4_13Request["payload"]["description"]
|
||||||
|
readonly metadata?: Endpoint4_13Request["payload"]["metadata"]
|
||||||
|
readonly resume?: Endpoint4_13Request["payload"]["resume"]
|
||||||
}
|
}
|
||||||
export type Endpoint4_13Output = EffectValue<ReturnType<RawClient["server.session"]["session.shell"]>>
|
export type Endpoint4_13Output = EffectValue<ReturnType<RawClient["server.session"]["session.synthetic"]>>
|
||||||
export type SessionShellOperation<E = never> = (input: Endpoint4_13Input) => Effect.Effect<Endpoint4_13Output, E>
|
export type SessionSyntheticOperation<E = never> = (input: Endpoint4_13Input) => Effect.Effect<Endpoint4_13Output, E>
|
||||||
|
|
||||||
type Endpoint4_14Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
|
type Endpoint4_14Request = Parameters<RawClient["server.session"]["session.shell"]>[0]
|
||||||
export type Endpoint4_14Input = {
|
export type Endpoint4_14Input = {
|
||||||
readonly sessionID: Endpoint4_14Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_14Request["params"]["sessionID"]
|
||||||
readonly id?: Endpoint4_14Request["payload"]["id"]
|
readonly id?: Endpoint4_14Request["payload"]["id"]
|
||||||
|
readonly command: Endpoint4_14Request["payload"]["command"]
|
||||||
}
|
}
|
||||||
export type Endpoint4_14Output = EffectValue<ReturnType<RawClient["server.session"]["session.compact"]>>["data"]
|
export type Endpoint4_14Output = EffectValue<ReturnType<RawClient["server.session"]["session.shell"]>>
|
||||||
export type SessionCompactOperation<E = never> = (input: Endpoint4_14Input) => Effect.Effect<Endpoint4_14Output, E>
|
export type SessionShellOperation<E = never> = (input: Endpoint4_14Input) => Effect.Effect<Endpoint4_14Output, E>
|
||||||
|
|
||||||
type Endpoint4_15Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
|
type Endpoint4_15Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
|
||||||
export type Endpoint4_15Input = { readonly sessionID: Endpoint4_15Request["params"]["sessionID"] }
|
export type Endpoint4_15Input = {
|
||||||
export type Endpoint4_15Output = EffectValue<ReturnType<RawClient["server.session"]["session.wait"]>>
|
readonly sessionID: Endpoint4_15Request["params"]["sessionID"]
|
||||||
export type SessionWaitOperation<E = never> = (input: Endpoint4_15Input) => Effect.Effect<Endpoint4_15Output, E>
|
readonly id?: Endpoint4_15Request["payload"]["id"]
|
||||||
|
|
||||||
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_16Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.stage"]>>["data"]
|
export type Endpoint4_15Output = EffectValue<ReturnType<RawClient["server.session"]["session.compact"]>>["data"]
|
||||||
export type SessionRevertStageOperation<E = never> = (input: Endpoint4_16Input) => Effect.Effect<Endpoint4_16Output, E>
|
export type SessionCompactOperation<E = never> = (input: Endpoint4_15Input) => Effect.Effect<Endpoint4_15Output, E>
|
||||||
|
|
||||||
type Endpoint4_17Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
|
type Endpoint4_16Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
|
||||||
export type Endpoint4_17Input = { readonly sessionID: Endpoint4_17Request["params"]["sessionID"] }
|
export type Endpoint4_16Input = { readonly sessionID: Endpoint4_16Request["params"]["sessionID"] }
|
||||||
export type Endpoint4_17Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.clear"]>>
|
export type Endpoint4_16Output = EffectValue<ReturnType<RawClient["server.session"]["session.wait"]>>
|
||||||
export type SessionRevertClearOperation<E = never> = (input: Endpoint4_17Input) => Effect.Effect<Endpoint4_17Output, E>
|
export type SessionWaitOperation<E = never> = (input: Endpoint4_16Input) => Effect.Effect<Endpoint4_16Output, E>
|
||||||
|
|
||||||
type Endpoint4_18Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
|
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]
|
||||||
export type Endpoint4_18Input = { readonly sessionID: Endpoint4_18Request["params"]["sessionID"] }
|
export type Endpoint4_18Input = { readonly sessionID: Endpoint4_18Request["params"]["sessionID"] }
|
||||||
export type Endpoint4_18Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.commit"]>>
|
export type Endpoint4_18Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.clear"]>>
|
||||||
export type SessionRevertCommitOperation<E = never> = (input: Endpoint4_18Input) => Effect.Effect<Endpoint4_18Output, E>
|
export type SessionRevertClearOperation<E = never> = (input: Endpoint4_18Input) => Effect.Effect<Endpoint4_18Output, E>
|
||||||
|
|
||||||
type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.context"]>[0]
|
type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
|
||||||
export type Endpoint4_19Input = { readonly sessionID: Endpoint4_19Request["params"]["sessionID"] }
|
export type Endpoint4_19Input = { readonly sessionID: Endpoint4_19Request["params"]["sessionID"] }
|
||||||
export type Endpoint4_19Output = EffectValue<ReturnType<RawClient["server.session"]["session.context"]>>["data"]
|
export type Endpoint4_19Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.commit"]>>
|
||||||
export type SessionContextOperation<E = never> = (input: Endpoint4_19Input) => Effect.Effect<Endpoint4_19Output, E>
|
export type SessionRevertCommitOperation<E = never> = (input: Endpoint4_19Input) => Effect.Effect<Endpoint4_19Output, E>
|
||||||
|
|
||||||
type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.instructions.entry.list"]>[0]
|
type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.context"]>[0]
|
||||||
export type Endpoint4_20Input = { readonly sessionID: Endpoint4_20Request["params"]["sessionID"] }
|
export type Endpoint4_20Input = { readonly sessionID: Endpoint4_20Request["params"]["sessionID"] }
|
||||||
export type Endpoint4_20Output = EffectValue<
|
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<
|
||||||
ReturnType<RawClient["server.session"]["session.instructions.entry.list"]>
|
ReturnType<RawClient["server.session"]["session.instructions.entry.list"]>
|
||||||
>["data"]
|
>["data"]
|
||||||
export type SessionInstructionsEntryListOperation<E = never> = (
|
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,
|
input: Endpoint4_21Input,
|
||||||
) => Effect.Effect<Endpoint4_21Output, E>
|
) => Effect.Effect<Endpoint4_21Output, E>
|
||||||
|
|
||||||
type Endpoint4_22Request = Parameters<RawClient["server.session"]["session.instructions.entry.remove"]>[0]
|
type Endpoint4_22Request = Parameters<RawClient["server.session"]["session.instructions.entry.put"]>[0]
|
||||||
export type Endpoint4_22Input = {
|
export type Endpoint4_22Input = {
|
||||||
readonly sessionID: Endpoint4_22Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_22Request["params"]["sessionID"]
|
||||||
readonly key: Endpoint4_22Request["params"]["key"]
|
readonly key: Endpoint4_22Request["params"]["key"]
|
||||||
|
readonly value: Endpoint4_22Request["payload"]["value"]
|
||||||
}
|
}
|
||||||
export type Endpoint4_22Output = EffectValue<
|
export type Endpoint4_22Output = EffectValue<ReturnType<RawClient["server.session"]["session.instructions.entry.put"]>>
|
||||||
ReturnType<RawClient["server.session"]["session.instructions.entry.remove"]>
|
export type SessionInstructionsEntryPutOperation<E = never> = (
|
||||||
>
|
|
||||||
export type SessionInstructionsEntryRemoveOperation<E = never> = (
|
|
||||||
input: Endpoint4_22Input,
|
input: Endpoint4_22Input,
|
||||||
) => Effect.Effect<Endpoint4_22Output, E>
|
) => Effect.Effect<Endpoint4_22Output, E>
|
||||||
|
|
||||||
type Endpoint4_23Request = Parameters<RawClient["server.session"]["session.log"]>[0]
|
type Endpoint4_23Request = Parameters<RawClient["server.session"]["session.instructions.entry.remove"]>[0]
|
||||||
export type Endpoint4_23Input = {
|
export type Endpoint4_23Input = {
|
||||||
readonly sessionID: Endpoint4_23Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_23Request["params"]["sessionID"]
|
||||||
readonly after?: Endpoint4_23Request["query"]["after"]
|
readonly key: Endpoint4_23Request["params"]["key"]
|
||||||
readonly follow?: Endpoint4_23Request["query"]["follow"]
|
|
||||||
}
|
}
|
||||||
export type Endpoint4_23Output = StreamValue<EffectValue<ReturnType<RawClient["server.session"]["session.log"]>>>
|
export type Endpoint4_23Output = EffectValue<
|
||||||
export type SessionLogOperation<E = never> = (input: Endpoint4_23Input) => Stream.Stream<Endpoint4_23Output, E>
|
ReturnType<RawClient["server.session"]["session.instructions.entry.remove"]>
|
||||||
|
>
|
||||||
|
export type SessionInstructionsEntryRemoveOperation<E = never> = (
|
||||||
|
input: Endpoint4_23Input,
|
||||||
|
) => Effect.Effect<Endpoint4_23Output, E>
|
||||||
|
|
||||||
type Endpoint4_24Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
|
type Endpoint4_24Request = Parameters<RawClient["server.session"]["session.log"]>[0]
|
||||||
export type Endpoint4_24Input = { readonly sessionID: Endpoint4_24Request["params"]["sessionID"] }
|
export type Endpoint4_24Input = {
|
||||||
export type Endpoint4_24Output = EffectValue<ReturnType<RawClient["server.session"]["session.interrupt"]>>
|
readonly sessionID: Endpoint4_24Request["params"]["sessionID"]
|
||||||
export type SessionInterruptOperation<E = never> = (input: Endpoint4_24Input) => Effect.Effect<Endpoint4_24Output, E>
|
readonly after?: Endpoint4_24Request["query"]["after"]
|
||||||
|
readonly follow?: Endpoint4_24Request["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>
|
||||||
|
|
||||||
type Endpoint4_25Request = Parameters<RawClient["server.session"]["session.background"]>[0]
|
type Endpoint4_25Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
|
||||||
export type Endpoint4_25Input = { readonly sessionID: Endpoint4_25Request["params"]["sessionID"] }
|
export type Endpoint4_25Input = { readonly sessionID: Endpoint4_25Request["params"]["sessionID"] }
|
||||||
export type Endpoint4_25Output = EffectValue<ReturnType<RawClient["server.session"]["session.background"]>>
|
export type Endpoint4_25Output = EffectValue<ReturnType<RawClient["server.session"]["session.interrupt"]>>
|
||||||
export type SessionBackgroundOperation<E = never> = (input: Endpoint4_25Input) => Effect.Effect<Endpoint4_25Output, E>
|
export type SessionInterruptOperation<E = never> = (input: Endpoint4_25Input) => Effect.Effect<Endpoint4_25Output, E>
|
||||||
|
|
||||||
type Endpoint4_26Request = Parameters<RawClient["server.session"]["session.message"]>[0]
|
type Endpoint4_26Request = Parameters<RawClient["server.session"]["session.background"]>[0]
|
||||||
export type Endpoint4_26Input = {
|
export type Endpoint4_26Input = { readonly sessionID: Endpoint4_26Request["params"]["sessionID"] }
|
||||||
readonly sessionID: Endpoint4_26Request["params"]["sessionID"]
|
export type Endpoint4_26Output = EffectValue<ReturnType<RawClient["server.session"]["session.background"]>>
|
||||||
readonly messageID: Endpoint4_26Request["params"]["messageID"]
|
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"]
|
||||||
}
|
}
|
||||||
export type Endpoint4_26Output = EffectValue<ReturnType<RawClient["server.session"]["session.message"]>>["data"]
|
export type Endpoint4_27Output = EffectValue<ReturnType<RawClient["server.session"]["session.message"]>>["data"]
|
||||||
export type SessionMessageOperation<E = never> = (input: Endpoint4_26Input) => Effect.Effect<Endpoint4_26Output, E>
|
export type SessionMessageOperation<E = never> = (input: Endpoint4_27Input) => Effect.Effect<Endpoint4_27Output, E>
|
||||||
|
|
||||||
export interface SessionApi<E = never> {
|
export interface SessionApi<E = never> {
|
||||||
readonly list: SessionListOperation<E>
|
readonly list: SessionListOperation<E>
|
||||||
@@ -273,6 +283,7 @@ export interface SessionApi<E = never> {
|
|||||||
readonly switchAgent: SessionSwitchAgentOperation<E>
|
readonly switchAgent: SessionSwitchAgentOperation<E>
|
||||||
readonly switchModel: SessionSwitchModelOperation<E>
|
readonly switchModel: SessionSwitchModelOperation<E>
|
||||||
readonly rename: SessionRenameOperation<E>
|
readonly rename: SessionRenameOperation<E>
|
||||||
|
readonly move: SessionMoveOperation<E>
|
||||||
readonly prompt: SessionPromptOperation<E>
|
readonly prompt: SessionPromptOperation<E>
|
||||||
readonly command: SessionCommandOperation<E>
|
readonly command: SessionCommandOperation<E>
|
||||||
readonly skill: SessionSkillOperation<E>
|
readonly skill: SessionSkillOperation<E>
|
||||||
@@ -439,8 +450,14 @@ export type Endpoint10_0Input = { readonly location?: Endpoint10_0Request["query
|
|||||||
export type Endpoint10_0Output = EffectValue<ReturnType<RawClient["server.mcp"]["mcp.list"]>>
|
export type Endpoint10_0Output = EffectValue<ReturnType<RawClient["server.mcp"]["mcp.list"]>>
|
||||||
export type ServerMcpListOperation<E = never> = (input?: Endpoint10_0Input) => Effect.Effect<Endpoint10_0Output, E>
|
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> {
|
export interface ServerMcpApi<E = never> {
|
||||||
readonly list: ServerMcpListOperation<E>
|
readonly list: ServerMcpListOperation<E>
|
||||||
|
readonly catalog: ServerMcpCatalogOperation<E>
|
||||||
}
|
}
|
||||||
|
|
||||||
type Endpoint11_0Request = Parameters<RawClient["server.credential"]["credential.update"]>[0]
|
type Endpoint11_0Request = Parameters<RawClient["server.credential"]["credential.update"]>[0]
|
||||||
@@ -890,8 +907,14 @@ export interface VcsApi<E = never> {
|
|||||||
export type Endpoint25_0Output = EffectValue<ReturnType<RawClient["server.debug"]["debug.location"]>>
|
export type Endpoint25_0Output = EffectValue<ReturnType<RawClient["server.debug"]["debug.location"]>>
|
||||||
export type DebugLocationOperation<E = never> = () => Effect.Effect<Endpoint25_0Output, E>
|
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> {
|
export interface DebugApi<E = never> {
|
||||||
readonly location: DebugLocationOperation<E>
|
readonly location: DebugLocationOperation<E>
|
||||||
|
readonly evictLocation: DebugEvictLocationOperation<E>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppApi<E = never> {
|
export interface AppApi<E = never> {
|
||||||
|
|||||||
@@ -141,15 +141,27 @@ const Endpoint4_8 = (raw: RawClient["server.session"]) => (input: Endpoint4_8Inp
|
|||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint4_9Request = Parameters<RawClient["server.session"]["session.prompt"]>[0]
|
type Endpoint4_9Request = Parameters<RawClient["server.session"]["session.move"]>[0]
|
||||||
type Endpoint4_9Input = {
|
type Endpoint4_9Input = {
|
||||||
readonly sessionID: Endpoint4_9Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_9Request["params"]["sessionID"]
|
||||||
readonly id?: Endpoint4_9Request["payload"]["id"]
|
readonly destination: Endpoint4_9Request["payload"]["destination"]
|
||||||
readonly prompt: Endpoint4_9Request["payload"]["prompt"]
|
readonly moveChanges?: Endpoint4_9Request["payload"]["moveChanges"]
|
||||||
readonly delivery?: Endpoint4_9Request["payload"]["delivery"]
|
|
||||||
readonly resume?: Endpoint4_9Request["payload"]["resume"]
|
|
||||||
}
|
}
|
||||||
const Endpoint4_9 = (raw: RawClient["server.session"]) => (input: Endpoint4_9Input) =>
|
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"]({
|
raw["session.prompt"]({
|
||||||
params: { sessionID: input["sessionID"] },
|
params: { sessionID: input["sessionID"] },
|
||||||
payload: { id: input["id"], prompt: input["prompt"], delivery: input["delivery"], resume: input["resume"] },
|
payload: { id: input["id"], prompt: input["prompt"], delivery: input["delivery"], resume: input["resume"] },
|
||||||
@@ -158,20 +170,20 @@ const Endpoint4_9 = (raw: RawClient["server.session"]) => (input: Endpoint4_9Inp
|
|||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint4_10Request = Parameters<RawClient["server.session"]["session.command"]>[0]
|
type Endpoint4_11Request = Parameters<RawClient["server.session"]["session.command"]>[0]
|
||||||
type Endpoint4_10Input = {
|
type Endpoint4_11Input = {
|
||||||
readonly sessionID: Endpoint4_10Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_11Request["params"]["sessionID"]
|
||||||
readonly id?: Endpoint4_10Request["payload"]["id"]
|
readonly id?: Endpoint4_11Request["payload"]["id"]
|
||||||
readonly command: Endpoint4_10Request["payload"]["command"]
|
readonly command: Endpoint4_11Request["payload"]["command"]
|
||||||
readonly arguments?: Endpoint4_10Request["payload"]["arguments"]
|
readonly arguments?: Endpoint4_11Request["payload"]["arguments"]
|
||||||
readonly agent?: Endpoint4_10Request["payload"]["agent"]
|
readonly agent?: Endpoint4_11Request["payload"]["agent"]
|
||||||
readonly model?: Endpoint4_10Request["payload"]["model"]
|
readonly model?: Endpoint4_11Request["payload"]["model"]
|
||||||
readonly files?: Endpoint4_10Request["payload"]["files"]
|
readonly files?: Endpoint4_11Request["payload"]["files"]
|
||||||
readonly agents?: Endpoint4_10Request["payload"]["agents"]
|
readonly agents?: Endpoint4_11Request["payload"]["agents"]
|
||||||
readonly delivery?: Endpoint4_10Request["payload"]["delivery"]
|
readonly delivery?: Endpoint4_11Request["payload"]["delivery"]
|
||||||
readonly resume?: Endpoint4_10Request["payload"]["resume"]
|
readonly resume?: Endpoint4_11Request["payload"]["resume"]
|
||||||
}
|
}
|
||||||
const Endpoint4_10 = (raw: RawClient["server.session"]) => (input: Endpoint4_10Input) =>
|
const Endpoint4_11 = (raw: RawClient["server.session"]) => (input: Endpoint4_11Input) =>
|
||||||
raw["session.command"]({
|
raw["session.command"]({
|
||||||
params: { sessionID: input["sessionID"] },
|
params: { sessionID: input["sessionID"] },
|
||||||
payload: {
|
payload: {
|
||||||
@@ -190,67 +202,73 @@ const Endpoint4_10 = (raw: RawClient["server.session"]) => (input: Endpoint4_10I
|
|||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint4_11Request = Parameters<RawClient["server.session"]["session.skill"]>[0]
|
type Endpoint4_12Request = Parameters<RawClient["server.session"]["session.skill"]>[0]
|
||||||
type Endpoint4_11Input = {
|
type Endpoint4_12Input = {
|
||||||
readonly sessionID: Endpoint4_11Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_12Request["params"]["sessionID"]
|
||||||
readonly id?: Endpoint4_11Request["payload"]["id"]
|
readonly id?: Endpoint4_12Request["payload"]["id"]
|
||||||
readonly skill: Endpoint4_11Request["payload"]["skill"]
|
readonly skill: Endpoint4_12Request["payload"]["skill"]
|
||||||
readonly resume?: Endpoint4_11Request["payload"]["resume"]
|
readonly resume?: Endpoint4_12Request["payload"]["resume"]
|
||||||
}
|
}
|
||||||
const Endpoint4_11 = (raw: RawClient["server.session"]) => (input: Endpoint4_11Input) =>
|
const Endpoint4_12 = (raw: RawClient["server.session"]) => (input: Endpoint4_12Input) =>
|
||||||
raw["session.skill"]({
|
raw["session.skill"]({
|
||||||
params: { sessionID: input["sessionID"] },
|
params: { sessionID: input["sessionID"] },
|
||||||
payload: { id: input["id"], skill: input["skill"], resume: input["resume"] },
|
payload: { id: input["id"], skill: input["skill"], resume: input["resume"] },
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
}).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint4_12Request = Parameters<RawClient["server.session"]["session.synthetic"]>[0]
|
type Endpoint4_13Request = 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_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"] },
|
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
|
||||||
|
|
||||||
type Endpoint4_13Request = Parameters<RawClient["server.session"]["session.shell"]>[0]
|
|
||||||
type Endpoint4_13Input = {
|
type Endpoint4_13Input = {
|
||||||
readonly sessionID: Endpoint4_13Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_13Request["params"]["sessionID"]
|
||||||
readonly id?: Endpoint4_13Request["payload"]["id"]
|
readonly text: Endpoint4_13Request["payload"]["text"]
|
||||||
readonly command: Endpoint4_13Request["payload"]["command"]
|
readonly description?: Endpoint4_13Request["payload"]["description"]
|
||||||
|
readonly metadata?: Endpoint4_13Request["payload"]["metadata"]
|
||||||
|
readonly resume?: Endpoint4_13Request["payload"]["resume"]
|
||||||
}
|
}
|
||||||
const Endpoint4_13 = (raw: RawClient["server.session"]) => (input: Endpoint4_13Input) =>
|
const Endpoint4_13 = (raw: RawClient["server.session"]) => (input: Endpoint4_13Input) =>
|
||||||
|
raw["session.synthetic"]({
|
||||||
|
params: { sessionID: input["sessionID"] },
|
||||||
|
payload: {
|
||||||
|
text: input["text"],
|
||||||
|
description: input["description"],
|
||||||
|
metadata: input["metadata"],
|
||||||
|
resume: input["resume"],
|
||||||
|
},
|
||||||
|
}).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"]
|
||||||
|
}
|
||||||
|
const Endpoint4_14 = (raw: RawClient["server.session"]) => (input: Endpoint4_14Input) =>
|
||||||
raw["session.shell"]({
|
raw["session.shell"]({
|
||||||
params: { sessionID: input["sessionID"] },
|
params: { sessionID: input["sessionID"] },
|
||||||
payload: { id: input["id"], command: input["command"] },
|
payload: { id: input["id"], command: input["command"] },
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
}).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint4_14Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
|
type Endpoint4_15Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
|
||||||
type Endpoint4_14Input = {
|
type Endpoint4_15Input = {
|
||||||
readonly sessionID: Endpoint4_14Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_15Request["params"]["sessionID"]
|
||||||
readonly id?: Endpoint4_14Request["payload"]["id"]
|
readonly id?: Endpoint4_15Request["payload"]["id"]
|
||||||
}
|
}
|
||||||
const Endpoint4_14 = (raw: RawClient["server.session"]) => (input: Endpoint4_14Input) =>
|
const Endpoint4_15 = (raw: RawClient["server.session"]) => (input: Endpoint4_15Input) =>
|
||||||
raw["session.compact"]({ params: { sessionID: input["sessionID"] }, payload: { id: input["id"] } }).pipe(
|
raw["session.compact"]({ params: { sessionID: input["sessionID"] }, payload: { id: input["id"] } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint4_15Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
|
type Endpoint4_16Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
|
||||||
type Endpoint4_15Input = { readonly sessionID: Endpoint4_15Request["params"]["sessionID"] }
|
type Endpoint4_16Input = { readonly sessionID: Endpoint4_16Request["params"]["sessionID"] }
|
||||||
const Endpoint4_15 = (raw: RawClient["server.session"]) => (input: Endpoint4_15Input) =>
|
const Endpoint4_16 = (raw: RawClient["server.session"]) => (input: Endpoint4_16Input) =>
|
||||||
raw["session.wait"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
raw["session.wait"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint4_16Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
|
type Endpoint4_17Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
|
||||||
type Endpoint4_16Input = {
|
type Endpoint4_17Input = {
|
||||||
readonly sessionID: Endpoint4_16Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_17Request["params"]["sessionID"]
|
||||||
readonly messageID: Endpoint4_16Request["payload"]["messageID"]
|
readonly messageID: Endpoint4_17Request["payload"]["messageID"]
|
||||||
readonly files?: Endpoint4_16Request["payload"]["files"]
|
readonly files?: Endpoint4_17Request["payload"]["files"]
|
||||||
}
|
}
|
||||||
const Endpoint4_16 = (raw: RawClient["server.session"]) => (input: Endpoint4_16Input) =>
|
const Endpoint4_17 = (raw: RawClient["server.session"]) => (input: Endpoint4_17Input) =>
|
||||||
raw["session.revert.stage"]({
|
raw["session.revert.stage"]({
|
||||||
params: { sessionID: input["sessionID"] },
|
params: { sessionID: input["sessionID"] },
|
||||||
payload: { messageID: input["messageID"], files: input["files"] },
|
payload: { messageID: input["messageID"], files: input["files"] },
|
||||||
@@ -259,61 +277,61 @@ const Endpoint4_16 = (raw: RawClient["server.session"]) => (input: Endpoint4_16I
|
|||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint4_17Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
|
type Endpoint4_18Request = 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_18Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
|
|
||||||
type Endpoint4_18Input = { readonly sessionID: Endpoint4_18Request["params"]["sessionID"] }
|
type Endpoint4_18Input = { readonly sessionID: Endpoint4_18Request["params"]["sessionID"] }
|
||||||
const Endpoint4_18 = (raw: RawClient["server.session"]) => (input: Endpoint4_18Input) =>
|
const Endpoint4_18 = (raw: RawClient["server.session"]) => (input: Endpoint4_18Input) =>
|
||||||
raw["session.revert.commit"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
raw["session.revert.clear"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.context"]>[0]
|
type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
|
||||||
type Endpoint4_19Input = { readonly sessionID: Endpoint4_19Request["params"]["sessionID"] }
|
type Endpoint4_19Input = { readonly sessionID: Endpoint4_19Request["params"]["sessionID"] }
|
||||||
const Endpoint4_19 = (raw: RawClient["server.session"]) => (input: Endpoint4_19Input) =>
|
const Endpoint4_19 = (raw: RawClient["server.session"]) => (input: Endpoint4_19Input) =>
|
||||||
|
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) =>
|
||||||
raw["session.context"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
raw["session.context"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.instructions.entry.list"]>[0]
|
type Endpoint4_21Request = Parameters<RawClient["server.session"]["session.instructions.entry.list"]>[0]
|
||||||
type Endpoint4_20Input = { readonly sessionID: Endpoint4_20Request["params"]["sessionID"] }
|
type Endpoint4_21Input = { readonly sessionID: Endpoint4_21Request["params"]["sessionID"] }
|
||||||
const Endpoint4_20 = (raw: RawClient["server.session"]) => (input: Endpoint4_20Input) =>
|
const Endpoint4_21 = (raw: RawClient["server.session"]) => (input: Endpoint4_21Input) =>
|
||||||
raw["session.instructions.entry.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
raw["session.instructions.entry.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint4_21Request = Parameters<RawClient["server.session"]["session.instructions.entry.put"]>[0]
|
type Endpoint4_22Request = Parameters<RawClient["server.session"]["session.instructions.entry.put"]>[0]
|
||||||
type Endpoint4_21Input = {
|
type Endpoint4_22Input = {
|
||||||
readonly sessionID: Endpoint4_21Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_22Request["params"]["sessionID"]
|
||||||
readonly key: Endpoint4_21Request["params"]["key"]
|
readonly key: Endpoint4_22Request["params"]["key"]
|
||||||
readonly value: Endpoint4_21Request["payload"]["value"]
|
readonly value: Endpoint4_22Request["payload"]["value"]
|
||||||
}
|
}
|
||||||
const Endpoint4_21 = (raw: RawClient["server.session"]) => (input: Endpoint4_21Input) =>
|
const Endpoint4_22 = (raw: RawClient["server.session"]) => (input: Endpoint4_22Input) =>
|
||||||
raw["session.instructions.entry.put"]({
|
raw["session.instructions.entry.put"]({
|
||||||
params: { sessionID: input["sessionID"], key: input["key"] },
|
params: { sessionID: input["sessionID"], key: input["key"] },
|
||||||
payload: { value: input["value"] },
|
payload: { value: input["value"] },
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
}).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint4_22Request = Parameters<RawClient["server.session"]["session.instructions.entry.remove"]>[0]
|
type Endpoint4_23Request = Parameters<RawClient["server.session"]["session.instructions.entry.remove"]>[0]
|
||||||
type Endpoint4_22Input = {
|
type Endpoint4_23Input = {
|
||||||
readonly sessionID: Endpoint4_22Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_23Request["params"]["sessionID"]
|
||||||
readonly key: Endpoint4_22Request["params"]["key"]
|
readonly key: Endpoint4_23Request["params"]["key"]
|
||||||
}
|
}
|
||||||
const Endpoint4_22 = (raw: RawClient["server.session"]) => (input: Endpoint4_22Input) =>
|
const Endpoint4_23 = (raw: RawClient["server.session"]) => (input: Endpoint4_23Input) =>
|
||||||
raw["session.instructions.entry.remove"]({ params: { sessionID: input["sessionID"], key: input["key"] } }).pipe(
|
raw["session.instructions.entry.remove"]({ params: { sessionID: input["sessionID"], key: input["key"] } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint4_23Request = Parameters<RawClient["server.session"]["session.log"]>[0]
|
type Endpoint4_24Request = Parameters<RawClient["server.session"]["session.log"]>[0]
|
||||||
type Endpoint4_23Input = {
|
type Endpoint4_24Input = {
|
||||||
readonly sessionID: Endpoint4_23Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_24Request["params"]["sessionID"]
|
||||||
readonly after?: Endpoint4_23Request["query"]["after"]
|
readonly after?: Endpoint4_24Request["query"]["after"]
|
||||||
readonly follow?: Endpoint4_23Request["query"]["follow"]
|
readonly follow?: Endpoint4_24Request["query"]["follow"]
|
||||||
}
|
}
|
||||||
const Endpoint4_23 = (raw: RawClient["server.session"]) => (input: Endpoint4_23Input) =>
|
const Endpoint4_24 = (raw: RawClient["server.session"]) => (input: Endpoint4_24Input) =>
|
||||||
Stream.unwrap(
|
Stream.unwrap(
|
||||||
raw["session.log"]({
|
raw["session.log"]({
|
||||||
params: { sessionID: input["sessionID"] },
|
params: { sessionID: input["sessionID"] },
|
||||||
@@ -324,22 +342,22 @@ const Endpoint4_23 = (raw: RawClient["server.session"]) => (input: Endpoint4_23I
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint4_24Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
|
type Endpoint4_25Request = 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_25Request = Parameters<RawClient["server.session"]["session.background"]>[0]
|
|
||||||
type Endpoint4_25Input = { readonly sessionID: Endpoint4_25Request["params"]["sessionID"] }
|
type Endpoint4_25Input = { readonly sessionID: Endpoint4_25Request["params"]["sessionID"] }
|
||||||
const Endpoint4_25 = (raw: RawClient["server.session"]) => (input: Endpoint4_25Input) =>
|
const Endpoint4_25 = (raw: RawClient["server.session"]) => (input: Endpoint4_25Input) =>
|
||||||
|
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) =>
|
||||||
raw["session.background"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
raw["session.background"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint4_26Request = Parameters<RawClient["server.session"]["session.message"]>[0]
|
type Endpoint4_27Request = Parameters<RawClient["server.session"]["session.message"]>[0]
|
||||||
type Endpoint4_26Input = {
|
type Endpoint4_27Input = {
|
||||||
readonly sessionID: Endpoint4_26Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_27Request["params"]["sessionID"]
|
||||||
readonly messageID: Endpoint4_26Request["params"]["messageID"]
|
readonly messageID: Endpoint4_27Request["params"]["messageID"]
|
||||||
}
|
}
|
||||||
const Endpoint4_26 = (raw: RawClient["server.session"]) => (input: Endpoint4_26Input) =>
|
const Endpoint4_27 = (raw: RawClient["server.session"]) => (input: Endpoint4_27Input) =>
|
||||||
raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe(
|
raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
@@ -355,22 +373,23 @@ const adaptGroup4 = (raw: RawClient["server.session"]) => ({
|
|||||||
switchAgent: Endpoint4_6(raw),
|
switchAgent: Endpoint4_6(raw),
|
||||||
switchModel: Endpoint4_7(raw),
|
switchModel: Endpoint4_7(raw),
|
||||||
rename: Endpoint4_8(raw),
|
rename: Endpoint4_8(raw),
|
||||||
prompt: Endpoint4_9(raw),
|
move: Endpoint4_9(raw),
|
||||||
command: Endpoint4_10(raw),
|
prompt: Endpoint4_10(raw),
|
||||||
skill: Endpoint4_11(raw),
|
command: Endpoint4_11(raw),
|
||||||
synthetic: Endpoint4_12(raw),
|
skill: Endpoint4_12(raw),
|
||||||
shell: Endpoint4_13(raw),
|
synthetic: Endpoint4_13(raw),
|
||||||
compact: Endpoint4_14(raw),
|
shell: Endpoint4_14(raw),
|
||||||
wait: Endpoint4_15(raw),
|
compact: Endpoint4_15(raw),
|
||||||
revertStage: Endpoint4_16(raw),
|
wait: Endpoint4_16(raw),
|
||||||
revertClear: Endpoint4_17(raw),
|
revertStage: Endpoint4_17(raw),
|
||||||
revertCommit: Endpoint4_18(raw),
|
revertClear: Endpoint4_18(raw),
|
||||||
context: Endpoint4_19(raw),
|
revertCommit: Endpoint4_19(raw),
|
||||||
instructions: { entry: { list: Endpoint4_20(raw), put: Endpoint4_21(raw), remove: Endpoint4_22(raw) } },
|
context: Endpoint4_20(raw),
|
||||||
log: Endpoint4_23(raw),
|
instructions: { entry: { list: Endpoint4_21(raw), put: Endpoint4_22(raw), remove: Endpoint4_23(raw) } },
|
||||||
interrupt: Endpoint4_24(raw),
|
log: Endpoint4_24(raw),
|
||||||
background: Endpoint4_25(raw),
|
interrupt: Endpoint4_25(raw),
|
||||||
message: Endpoint4_26(raw),
|
background: Endpoint4_26(raw),
|
||||||
|
message: Endpoint4_27(raw),
|
||||||
})
|
})
|
||||||
|
|
||||||
type Endpoint5_0Request = Parameters<RawClient["server.message"]["session.messages"]>[0]
|
type Endpoint5_0Request = Parameters<RawClient["server.message"]["session.messages"]>[0]
|
||||||
@@ -529,7 +548,12 @@ type Endpoint10_0Input = { readonly location?: Endpoint10_0Request["query"]["loc
|
|||||||
const Endpoint10_0 = (raw: RawClient["server.mcp"]) => (input?: Endpoint10_0Input) =>
|
const Endpoint10_0 = (raw: RawClient["server.mcp"]) => (input?: Endpoint10_0Input) =>
|
||||||
raw["mcp.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
raw["mcp.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
const adaptGroup10 = (raw: RawClient["server.mcp"]) => ({ list: Endpoint10_0(raw) })
|
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) })
|
||||||
|
|
||||||
type Endpoint11_0Request = Parameters<RawClient["server.credential"]["credential.update"]>[0]
|
type Endpoint11_0Request = Parameters<RawClient["server.credential"]["credential.update"]>[0]
|
||||||
type Endpoint11_0Input = {
|
type Endpoint11_0Input = {
|
||||||
@@ -1069,7 +1093,15 @@ const adaptGroup24 = (raw: RawClient["server.vcs"]) => ({ status: Endpoint24_0(r
|
|||||||
const Endpoint25_0 = (raw: RawClient["server.debug"]) => () =>
|
const Endpoint25_0 = (raw: RawClient["server.debug"]) => () =>
|
||||||
raw["debug.location"]({}).pipe(Effect.mapError(mapClientError))
|
raw["debug.location"]({}).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
const adaptGroup25 = (raw: RawClient["server.debug"]) => ({ location: Endpoint25_0(raw) })
|
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 adaptClient = (raw: RawClient) => ({
|
const adaptClient = (raw: RawClient) => ({
|
||||||
health: adaptGroup0(raw["server.health"]),
|
health: adaptGroup0(raw["server.health"]),
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import type {
|
|||||||
SessionSwitchModelOutput,
|
SessionSwitchModelOutput,
|
||||||
SessionRenameInput,
|
SessionRenameInput,
|
||||||
SessionRenameOutput,
|
SessionRenameOutput,
|
||||||
|
SessionMoveInput,
|
||||||
|
SessionMoveOutput,
|
||||||
SessionPromptInput,
|
SessionPromptInput,
|
||||||
SessionPromptOutput,
|
SessionPromptOutput,
|
||||||
SessionCommandInput,
|
SessionCommandInput,
|
||||||
@@ -87,6 +89,8 @@ import type {
|
|||||||
IntegrationAttemptCancelOutput,
|
IntegrationAttemptCancelOutput,
|
||||||
ServerMcpListInput,
|
ServerMcpListInput,
|
||||||
ServerMcpListOutput,
|
ServerMcpListOutput,
|
||||||
|
ServerMcpCatalogInput,
|
||||||
|
ServerMcpCatalogOutput,
|
||||||
CredentialUpdateInput,
|
CredentialUpdateInput,
|
||||||
CredentialUpdateOutput,
|
CredentialUpdateOutput,
|
||||||
CredentialRemoveInput,
|
CredentialRemoveInput,
|
||||||
@@ -178,6 +182,8 @@ import type {
|
|||||||
VcsDiffInput,
|
VcsDiffInput,
|
||||||
VcsDiffOutput,
|
VcsDiffOutput,
|
||||||
DebugLocationOutput,
|
DebugLocationOutput,
|
||||||
|
DebugEvictLocationInput,
|
||||||
|
DebugEvictLocationOutput,
|
||||||
} from "./types"
|
} from "./types"
|
||||||
import { ClientError } from "./client-error"
|
import { ClientError } from "./client-error"
|
||||||
|
|
||||||
@@ -485,6 +491,18 @@ export function make(options: ClientOptions) {
|
|||||||
},
|
},
|
||||||
requestOptions,
|
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) =>
|
prompt: (input: SessionPromptInput, requestOptions?: RequestOptions) =>
|
||||||
request<{ readonly data: SessionPromptOutput }>(
|
request<{ readonly data: SessionPromptOutput }>(
|
||||||
{
|
{
|
||||||
@@ -536,7 +554,12 @@ export function make(options: ClientOptions) {
|
|||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/synthetic`,
|
path: `/api/session/${encodeURIComponent(input.sessionID)}/synthetic`,
|
||||||
body: { text: input["text"], description: input["description"], metadata: input["metadata"] },
|
body: {
|
||||||
|
text: input["text"],
|
||||||
|
description: input["description"],
|
||||||
|
metadata: input["metadata"],
|
||||||
|
resume: input["resume"],
|
||||||
|
},
|
||||||
successStatus: 204,
|
successStatus: 204,
|
||||||
declaredStatuses: [404, 400, 401],
|
declaredStatuses: [404, 400, 401],
|
||||||
empty: true,
|
empty: true,
|
||||||
@@ -890,6 +913,18 @@ export function make(options: ClientOptions) {
|
|||||||
},
|
},
|
||||||
requestOptions,
|
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: {
|
credential: {
|
||||||
update: (input: CredentialUpdateInput, requestOptions?: RequestOptions) =>
|
update: (input: CredentialUpdateInput, requestOptions?: RequestOptions) =>
|
||||||
@@ -1494,6 +1529,18 @@ export function make(options: ClientOptions) {
|
|||||||
},
|
},
|
||||||
requestOptions,
|
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,6 +526,20 @@ export type SessionRenameInput = {
|
|||||||
|
|
||||||
export type SessionRenameOutput = void
|
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 = {
|
export type SessionPromptInput = {
|
||||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||||
readonly id?: {
|
readonly id?: {
|
||||||
@@ -856,17 +870,26 @@ export type SessionSyntheticInput = {
|
|||||||
readonly text: string
|
readonly text: string
|
||||||
readonly description?: string | null
|
readonly description?: string | null
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||||
|
readonly resume?: boolean | null
|
||||||
}["text"]
|
}["text"]
|
||||||
readonly description?: {
|
readonly description?: {
|
||||||
readonly text: string
|
readonly text: string
|
||||||
readonly description?: string | null
|
readonly description?: string | null
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||||
|
readonly resume?: boolean | null
|
||||||
}["description"]
|
}["description"]
|
||||||
readonly metadata?: {
|
readonly metadata?: {
|
||||||
readonly text: string
|
readonly text: string
|
||||||
readonly description?: string | null
|
readonly description?: string | null
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||||
|
readonly resume?: boolean | null
|
||||||
}["metadata"]
|
}["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
|
export type SessionSyntheticOutput = void
|
||||||
@@ -1416,6 +1439,13 @@ export type SessionLogOutput =
|
|||||||
readonly sessionID: string
|
readonly sessionID: string
|
||||||
readonly assistantMessageID: string
|
readonly assistantMessageID: string
|
||||||
readonly error: { readonly type: string; readonly message: string }
|
readonly error: { readonly type: string; readonly message: string }
|
||||||
|
readonly cost?: number
|
||||||
|
readonly tokens?: {
|
||||||
|
readonly input: number
|
||||||
|
readonly output: number
|
||||||
|
readonly reasoning: number
|
||||||
|
readonly cache: { readonly read: number; readonly write: number }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
@@ -2478,6 +2508,36 @@ 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 = {
|
export type CredentialUpdateInput = {
|
||||||
readonly credentialID: { readonly credentialID: string }["credentialID"]
|
readonly credentialID: { readonly credentialID: string }["credentialID"]
|
||||||
readonly location?: {
|
readonly location?: {
|
||||||
@@ -4489,6 +4549,23 @@ export type EventSubscribeOutput =
|
|||||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||||
readonly data: { readonly sessionID: string; readonly title: string }
|
readonly data: { readonly sessionID: string; readonly title: string }
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
readonly id: string
|
||||||
|
readonly created: number
|
||||||
|
readonly metadata?: { readonly [x: string]: unknown }
|
||||||
|
readonly type: "session.usage.updated"
|
||||||
|
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||||
|
readonly data: {
|
||||||
|
readonly sessionID: string
|
||||||
|
readonly cost: number
|
||||||
|
readonly tokens: {
|
||||||
|
readonly input: number
|
||||||
|
readonly output: number
|
||||||
|
readonly reasoning: number
|
||||||
|
readonly cache: { readonly read: number; readonly write: number }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
readonly id: string
|
readonly id: string
|
||||||
readonly created: number
|
readonly created: number
|
||||||
@@ -4712,6 +4789,13 @@ export type EventSubscribeOutput =
|
|||||||
readonly sessionID: string
|
readonly sessionID: string
|
||||||
readonly assistantMessageID: string
|
readonly assistantMessageID: string
|
||||||
readonly error: { readonly type: string; readonly message: string }
|
readonly error: { readonly type: string; readonly message: string }
|
||||||
|
readonly cost?: number
|
||||||
|
readonly tokens?: {
|
||||||
|
readonly input: number
|
||||||
|
readonly output: number
|
||||||
|
readonly reasoning: number
|
||||||
|
readonly cache: { readonly read: number; readonly write: number }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
@@ -5508,6 +5592,14 @@ export type EventSubscribeOutput =
|
|||||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||||
readonly data: { readonly server: 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 id: string
|
||||||
readonly created: number
|
readonly created: number
|
||||||
@@ -6130,3 +6222,11 @@ export type VcsDiffOutput = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type DebugLocationOutput = ReadonlyArray<{ readonly directory: string; readonly workspaceID?: string }>
|
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
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ test("exposes every standard HTTP API group", () => {
|
|||||||
"vcs",
|
"vcs",
|
||||||
"debug",
|
"debug",
|
||||||
])
|
])
|
||||||
expect(Object.keys(client.debug)).toEqual(["location"])
|
expect(Object.keys(client.debug)).toEqual(["location", "evictLocation"])
|
||||||
expect(Object.keys(client.message)).toEqual(["list"])
|
expect(Object.keys(client.message)).toEqual(["list"])
|
||||||
expect(Object.keys(client.integration)).toEqual([
|
expect(Object.keys(client.integration)).toEqual([
|
||||||
"list",
|
"list",
|
||||||
@@ -50,6 +50,29 @@ test("exposes every standard HTTP API group", () => {
|
|||||||
expect(Object.keys(client.project)).toEqual(["list", "current", "directories"])
|
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 () => {
|
test("file.read returns binary content from the public HTTP contract", async () => {
|
||||||
let request: Request | undefined
|
let request: Request | undefined
|
||||||
const client = OpenCode.make({
|
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.
|
`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. A program that returns `undefined`, including by reaching the end without `return`, produces `null`; nested `undefined` values are normalized to `null` as well.
|
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.
|
||||||
|
|
||||||
## API
|
## 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`.
|
- `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.
|
- 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`.
|
- Optional chaining, nullish coalescing, templates, spread (arrays, strings, Maps, Sets), and `try`/`catch`.
|
||||||
- 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`.
|
- 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`.
|
||||||
- `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).
|
- `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.
|
- 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).
|
- `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 `Object.is` after runtime method and tool references have stable identity semantics.
|
||||||
- [ ] Add deterministic modern collection conveniences where they improve orchestration: `Object.groupBy`, Set
|
- [ ] Add deterministic modern collection conveniences where they improve orchestration: `Object.groupBy`, Set
|
||||||
composition methods, and `Array.prototype.toSpliced`.
|
composition methods, and `Array.prototype.toSpliced`.
|
||||||
- [ ] Decide whether nondeterministic `Math.random` and iterable `Math.sumPrecise` belong in the runtime.
|
- [ ] Decide whether iterable `Math.sumPrecise` belongs in the runtime.
|
||||||
- [ ] Refine diagnostics so user throws, expected tool failures, unexpected host/tool defects, and genuine interpreter
|
- [ ] Refine diagnostics so user throws, expected tool failures, unexpected host/tool defects, and genuine interpreter
|
||||||
defects are distinguishable without leaking private causes.
|
defects are distinguishable without leaking private causes.
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ export type Binding = {
|
|||||||
|
|
||||||
export type StatementResult =
|
export type StatementResult =
|
||||||
| { kind: "none" }
|
| { kind: "none" }
|
||||||
| { kind: "value"; value: unknown }
|
|
||||||
| { kind: "return"; value: unknown }
|
| { kind: "return"; value: unknown }
|
||||||
| { kind: "break" }
|
| { kind: "break" }
|
||||||
| { kind: "continue" }
|
| { kind: "continue" }
|
||||||
|
|||||||
@@ -530,17 +530,29 @@ const invokeArrayStatic = (name: string, args: Array<unknown>, node: AstNode): u
|
|||||||
if (args[0] instanceof SandboxURLSearchParams) {
|
if (args[0] instanceof SandboxURLSearchParams) {
|
||||||
return Array.from(args[0].params.entries(), ([key, value]) => [key, value])
|
return Array.from(args[0].params.entries(), ([key, value]) => [key, value])
|
||||||
}
|
}
|
||||||
const source = boundedData(args[0], "Array.from input")
|
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",
|
||||||
|
)
|
||||||
|
}
|
||||||
if (typeof source === "string") return Array.from(source)
|
if (typeof source === "string") return Array.from(source)
|
||||||
if (Array.isArray(source)) return [...source]
|
if (Array.isArray(source)) return [...source]
|
||||||
if (
|
if (
|
||||||
source !== null &&
|
source !== null &&
|
||||||
typeof source === "object" &&
|
typeof source === "object" &&
|
||||||
|
(Object.getPrototypeOf(source) === Object.prototype || Object.getPrototypeOf(source) === null) &&
|
||||||
typeof (source as { length?: unknown }).length === "number"
|
typeof (source as { length?: unknown }).length === "number"
|
||||||
) {
|
) {
|
||||||
return Array.from(source as ArrayLike<unknown>)
|
return Array.from(source as ArrayLike<unknown>)
|
||||||
}
|
}
|
||||||
throw new InterpreterRuntimeError("Array.from expects an array, string, Map, Set, or array-like value.", node)
|
throw new InterpreterRuntimeError(
|
||||||
|
"Array.from expects an array, string, Map, Set, or array-like value.",
|
||||||
|
node,
|
||||||
|
"InvalidDataValue",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
throw new InterpreterRuntimeError(`Array.${name} is not available in CodeMode.`, node)
|
throw new InterpreterRuntimeError(`Array.${name} is not available in CodeMode.`, node)
|
||||||
@@ -606,7 +618,6 @@ class Interpreter<R> {
|
|||||||
// ToolRuntime.make like invokeTool: the interpreter never holds the tree itself.
|
// ToolRuntime.make like invokeTool: the interpreter never holds the tree itself.
|
||||||
private readonly toolKeys: (path: ReadonlyArray<string>) => ReadonlyArray<string>
|
private readonly toolKeys: (path: ReadonlyArray<string>) => ReadonlyArray<string>
|
||||||
private readonly logs: Array<string>
|
private readonly logs: Array<string>
|
||||||
private lastValue: unknown
|
|
||||||
// Caps how many eagerly forked tool calls run at once (the parallel-call concurrency cap).
|
// Caps how many eagerly forked tool calls run at once (the parallel-call concurrency cap).
|
||||||
private readonly callPermits: Semaphore.Semaphore
|
private readonly callPermits: Semaphore.Semaphore
|
||||||
// Fiber-backed promises whose settlement no program construct has observed yet. Successful
|
// Fiber-backed promises whose settlement no program construct has observed yet. Successful
|
||||||
@@ -625,7 +636,6 @@ class Interpreter<R> {
|
|||||||
this.invokeTool = invokeTool
|
this.invokeTool = invokeTool
|
||||||
this.toolKeys = toolKeys
|
this.toolKeys = toolKeys
|
||||||
this.logs = logs
|
this.logs = logs
|
||||||
this.lastValue = undefined
|
|
||||||
this.callPermits = shared?.callPermits ?? Semaphore.makeUnsafe(TOOL_CALL_CONCURRENCY)
|
this.callPermits = shared?.callPermits ?? Semaphore.makeUnsafe(TOOL_CALL_CONCURRENCY)
|
||||||
this.pendingSettlements = shared?.pendingSettlements ?? new Set<SandboxPromise>()
|
this.pendingSettlements = shared?.pendingSettlements ?? new Set<SandboxPromise>()
|
||||||
globalScope.set("tools", { mutable: false, value: new ToolReference([]) })
|
globalScope.set("tools", { mutable: false, value: new ToolReference([]) })
|
||||||
@@ -671,13 +681,15 @@ class Interpreter<R> {
|
|||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
self.hoistFunctions(program.body)
|
self.hoistFunctions(program.body)
|
||||||
let value: unknown = undefined
|
let value: unknown = undefined
|
||||||
let returned = false
|
for (const [index, statement] of program.body.entries()) {
|
||||||
for (const statement of program.body) {
|
if (index === program.body.length - 1 && statement.type === "ExpressionStatement") {
|
||||||
|
value = yield* self.evaluateExpression(getNode(statement, "expression"))
|
||||||
|
break
|
||||||
|
}
|
||||||
const result = yield* self.evaluateStatement(statement)
|
const result = yield* self.evaluateStatement(statement)
|
||||||
|
|
||||||
if (result.kind === "return") {
|
if (result.kind === "return") {
|
||||||
value = result.value
|
value = result.value
|
||||||
returned = true
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -685,11 +697,7 @@ class Interpreter<R> {
|
|||||||
throw new InterpreterRuntimeError(`Unexpected '${result.kind}' outside of a loop.`, statement)
|
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
|
// The program body runs inside an implicit async function, so a returned promise
|
||||||
// resolves before crossing the data boundary - `return tools.ns.tool(...)` works
|
// resolves before crossing the data boundary - `return tools.ns.tool(...)` works
|
||||||
@@ -780,7 +788,7 @@ class Interpreter<R> {
|
|||||||
private evaluateStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> {
|
private evaluateStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> {
|
||||||
switch (node.type) {
|
switch (node.type) {
|
||||||
case "ExpressionStatement":
|
case "ExpressionStatement":
|
||||||
return Effect.map(this.evaluateExpression(getNode(node, "expression")), (value) => ({ kind: "value", value }))
|
return Effect.as(this.evaluateExpression(getNode(node, "expression")), { kind: "none" })
|
||||||
case "VariableDeclaration":
|
case "VariableDeclaration":
|
||||||
return Effect.map(this.evaluateVariableDeclaration(node), () => ({ kind: "none" }))
|
return Effect.map(this.evaluateVariableDeclaration(node), () => ({ kind: "none" }))
|
||||||
case "ReturnStatement": {
|
case "ReturnStatement": {
|
||||||
@@ -833,11 +841,6 @@ class Interpreter<R> {
|
|||||||
const statement = asNode(statementValue, "body")
|
const statement = asNode(statementValue, "body")
|
||||||
const result = yield* self.evaluateStatement(statement)
|
const result = yield* self.evaluateStatement(statement)
|
||||||
|
|
||||||
if (result.kind === "value") {
|
|
||||||
self.lastValue = result.value
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.kind !== "none") {
|
if (result.kind !== "none") {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -929,7 +932,6 @@ class Interpreter<R> {
|
|||||||
const result = yield* self.evaluateStatement(asNode(statementValue, "consequent"))
|
const result = yield* self.evaluateStatement(asNode(statementValue, "consequent"))
|
||||||
if (result.kind === "break") return { kind: "none" } satisfies StatementResult
|
if (result.kind === "break") return { kind: "none" } satisfies StatementResult
|
||||||
if (result.kind === "return" || result.kind === "continue") return result
|
if (result.kind === "return" || result.kind === "continue") return result
|
||||||
if (result.kind === "value") self.lastValue = result.value
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { kind: "none" } satisfies StatementResult
|
return { kind: "none" } satisfies StatementResult
|
||||||
@@ -957,9 +959,6 @@ class Interpreter<R> {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.kind === "value") {
|
|
||||||
self.lastValue = result.value
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { kind: "none" } satisfies StatementResult
|
return { kind: "none" } satisfies StatementResult
|
||||||
@@ -987,9 +986,6 @@ class Interpreter<R> {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.kind === "value") {
|
|
||||||
self.lastValue = result.value
|
|
||||||
}
|
|
||||||
} while (yield* self.evaluateExpression(testNode))
|
} while (yield* self.evaluateExpression(testNode))
|
||||||
|
|
||||||
return { kind: "none" } satisfies StatementResult
|
return { kind: "none" } satisfies StatementResult
|
||||||
@@ -1045,10 +1041,6 @@ class Interpreter<R> {
|
|||||||
return { kind: "none" } satisfies StatementResult
|
return { kind: "none" } satisfies StatementResult
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.kind === "value") {
|
|
||||||
self.lastValue = result.value
|
|
||||||
}
|
|
||||||
|
|
||||||
if (iterationScope) {
|
if (iterationScope) {
|
||||||
const loopScope = self.currentScope()
|
const loopScope = self.currentScope()
|
||||||
for (const name of perIterationBindings) {
|
for (const name of perIterationBindings) {
|
||||||
@@ -1133,10 +1125,6 @@ class Interpreter<R> {
|
|||||||
return { kind: "none" }
|
return { kind: "none" }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.kind === "value") {
|
|
||||||
self.lastValue = result.value
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.kind === "continue") {
|
if (result.kind === "continue") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1226,10 +1214,6 @@ class Interpreter<R> {
|
|||||||
return { kind: "none" }
|
return { kind: "none" }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.kind === "value") {
|
|
||||||
self.lastValue = result.value
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.kind === "continue") {
|
if (result.kind === "continue") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -2033,6 +2017,9 @@ class Interpreter<R> {
|
|||||||
if (callable.namespace === "Object" && objectMethodsPreservingIdentity.has(callable.name)) {
|
if (callable.namespace === "Object" && objectMethodsPreservingIdentity.has(callable.name)) {
|
||||||
return invokeGlobalMethod(callable, args, node)
|
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`)
|
return boundedData(invokeGlobalMethod(callable, args, node), `${callable.namespace}.${callable.name} result`)
|
||||||
}
|
}
|
||||||
if (callable instanceof CoercionFunction) {
|
if (callable instanceof CoercionFunction) {
|
||||||
@@ -2372,7 +2359,7 @@ class Interpreter<R> {
|
|||||||
|
|
||||||
if (fn.body.type === "BlockStatement") {
|
if (fn.body.type === "BlockStatement") {
|
||||||
const result = yield* invocation.evaluateStatement(fn.body)
|
const result = yield* invocation.evaluateStatement(fn.body)
|
||||||
return result.kind === "return" || result.kind === "value" ? result.value : undefined
|
return result.kind === "return" ? result.value : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
return yield* invocation.evaluateExpression(fn.body)
|
return yield* invocation.evaluateExpression(fn.body)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export const mathConstants = new Set(["PI", "E", "LN2", "LN10", "LOG2E", "LOG10E", "SQRT2", "SQRT1_2"])
|
export const mathConstants = new Set(["PI", "E", "LN2", "LN10", "LOG2E", "LOG10E", "SQRT2", "SQRT1_2"])
|
||||||
|
|
||||||
export const mathMethods = new Set([
|
export const mathMethods = new Set([
|
||||||
|
"random",
|
||||||
"max",
|
"max",
|
||||||
"min",
|
"min",
|
||||||
"abs",
|
"abs",
|
||||||
@@ -40,6 +41,7 @@ export const mathMethods = new Set([
|
|||||||
|
|
||||||
export const invokeMathMethod = (name: string, args: Array<unknown>, node: AstNode): number => {
|
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 (!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) => {
|
const nums = args.map((arg) => {
|
||||||
if (typeof arg !== "number") throw new InterpreterRuntimeError(`Math.${name} expects number arguments.`, node)
|
if (typeof arg !== "number") throw new InterpreterRuntimeError(`Math.${name} expects number arguments.`, node)
|
||||||
return arg
|
return arg
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
export const numberMethods = new Set(["toFixed", "toPrecision", "toExponential", "toString"])
|
export const numberMethods = new Set(["toFixed", "toPrecision", "toExponential", "toString", "valueOf"])
|
||||||
|
|
||||||
export const numberConstants = new Set(["MAX_SAFE_INTEGER", "MIN_SAFE_INTEGER", "MAX_VALUE", "MIN_VALUE", "EPSILON"])
|
export const numberConstants = new Set([
|
||||||
|
"MAX_SAFE_INTEGER",
|
||||||
|
"MIN_SAFE_INTEGER",
|
||||||
|
"MAX_VALUE",
|
||||||
|
"MIN_VALUE",
|
||||||
|
"EPSILON",
|
||||||
|
"NaN",
|
||||||
|
"POSITIVE_INFINITY",
|
||||||
|
"NEGATIVE_INFINITY",
|
||||||
|
])
|
||||||
|
|
||||||
export const numberStatics = new Set(["isInteger", "isFinite", "isNaN", "isSafeInteger", "parseInt", "parseFloat"])
|
export const numberStatics = new Set(["isInteger", "isFinite", "isNaN", "isSafeInteger", "parseInt", "parseFloat"])
|
||||||
|
|
||||||
@@ -32,6 +41,9 @@ export const invokeNumberMethod = (value: number, name: string, args: Array<unkn
|
|||||||
result = value.toString(radix)
|
result = value.toString(radix)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
case "valueOf":
|
||||||
|
result = value
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
throw new InterpreterRuntimeError(`Number method '${name}' is not available in CodeMode.`, node)
|
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 { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js"
|
||||||
import { isBlockedMember } from "../tool-runtime.js"
|
import { isBlockedMember } from "../tool-runtime.js"
|
||||||
import { isSandboxValue, SandboxMap, SandboxSet, SandboxURLSearchParams } from "../values.js"
|
import { isSandboxValue, SandboxMap, SandboxPromise, SandboxSet, SandboxURLSearchParams } from "../values.js"
|
||||||
import { boundedData, coerceToString } from "./value.js"
|
import { boundedData, coerceToString } from "./value.js"
|
||||||
|
|
||||||
export const objectStatics = new Set(["keys", "values", "entries", "hasOwn", "assign", "fromEntries"])
|
export const objectStatics = new Set(["keys", "values", "entries", "hasOwn", "assign", "fromEntries"])
|
||||||
@@ -10,13 +10,23 @@ 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)
|
if (!objectStatics.has(name)) throw new InterpreterRuntimeError(`Object.${name} is not available in CodeMode.`, node)
|
||||||
const requireObject = (): Record<string, unknown> => {
|
const requireObject = (): Record<string, unknown> => {
|
||||||
const input = args[0]
|
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 (Array.isArray(input)) return input as unknown as Record<string, unknown>
|
||||||
if (isSandboxValue(value)) return {}
|
if (isSandboxValue(input)) return {}
|
||||||
if (value === null || typeof value !== "object") {
|
if (input instanceof SandboxPromise) {
|
||||||
throw new InterpreterRuntimeError(`Object.${name} expects a data object or array.`, node)
|
throw new InterpreterRuntimeError(
|
||||||
|
`Object.${name} received an un-awaited Promise; await it before inspecting the result.`,
|
||||||
|
node,
|
||||||
|
"InvalidDataValue",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return value as Record<string, unknown>
|
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>
|
||||||
}
|
}
|
||||||
const guardedSet = (out: Record<string, unknown>, key: string, item: unknown): void => {
|
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)
|
if (isBlockedMember(key)) throw new InterpreterRuntimeError(`Property '${key}' is not available in CodeMode.`, node)
|
||||||
@@ -28,15 +38,8 @@ export const invokeObjectMethod = (name: string, args: Array<unknown>, node: Ast
|
|||||||
guardedSet(out, coerceToString(key), item)
|
guardedSet(out, coerceToString(key), item)
|
||||||
}
|
}
|
||||||
switch (name) {
|
switch (name) {
|
||||||
case "keys": {
|
case "keys":
|
||||||
const value = boundedData(args[0], "Object.keys input")
|
return Object.keys(requireObject())
|
||||||
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":
|
case "values":
|
||||||
return Object.values(requireObject())
|
return Object.values(requireObject())
|
||||||
case "entries":
|
case "entries":
|
||||||
|
|||||||
@@ -617,6 +617,7 @@ 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.",
|
"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.",
|
"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 `{}`.",
|
"Dates and URLs serialize to strings at data boundaries; Map/Set/RegExp/URLSearchParams serialize to `{}`.",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -658,6 +658,9 @@ describe("CodeMode public contract", () => {
|
|||||||
expect(instructions).toContain("URL, URLSearchParams, and URI encoding helpers")
|
expect(instructions).toContain("URL, URLSearchParams, and URI encoding helpers")
|
||||||
expect(instructions).not.toContain("host globals")
|
expect(instructions).not.toContain("host globals")
|
||||||
expect(instructions).toContain("Use Code Mode tools for external operations")
|
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(
|
expect(instructions).toContain(
|
||||||
"Dates and URLs serialize to strings at data boundaries; Map/Set/RegExp/URLSearchParams serialize to `{}`.",
|
"Dates and URLs serialize to strings at data boundaries; Map/Set/RegExp/URLSearchParams serialize to `{}`.",
|
||||||
)
|
)
|
||||||
@@ -1081,6 +1084,24 @@ describe("CodeMode public contract", () => {
|
|||||||
expect(Schema.decodeUnknownSync(CodeMode.Result)(JSON.parse(JSON.stringify(result)))).toStrictEqual(result)
|
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 () => {
|
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: 0 } })).toThrow(RangeError)
|
||||||
expect(() => CodeMode.execute({ code: "return 1", limits: { timeoutMs: Number.POSITIVE_INFINITY } })).toThrow(
|
expect(() => CodeMode.execute({ code: "return 1", limits: { timeoutMs: Number.POSITIVE_INFINITY } })).toThrow(
|
||||||
|
|||||||
@@ -245,6 +245,12 @@ describe("promises at data boundaries", () => {
|
|||||||
expect(diagnostic.message).toContain("await tools.ns.tool(...)")
|
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 () => {
|
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 }) })`)
|
const diagnostic = await error(`return await tools.host.sleepy({ id: tools.host.sleepy({ id: 1 }) })`)
|
||||||
expect(diagnostic.kind).toBe("InvalidDataValue")
|
expect(diagnostic.kind).toBe("InvalidDataValue")
|
||||||
|
|||||||
@@ -19,6 +19,28 @@ const error = async (code: string) => {
|
|||||||
return result.error
|
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", () => {
|
describe("Date", () => {
|
||||||
test("Date.now() returns a number", async () => {
|
test("Date.now() returns a number", async () => {
|
||||||
expect(await value(`return typeof Date.now()`)).toBe("number")
|
expect(await value(`return typeof Date.now()`)).toBe("number")
|
||||||
@@ -751,6 +773,43 @@ 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 () => {
|
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(
|
expect(await value(`const merged = Object.assign({}, { m: new Map([["a", 1]]) }); return merged.m.get("a")`)).toBe(
|
||||||
1,
|
1,
|
||||||
@@ -773,6 +832,53 @@ describe("sandbox values at intra-sandbox checkpoints", () => {
|
|||||||
expect(await value(`return Array.from([new Date(5)])[0].getTime()`)).toBe(5)
|
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 () => {
|
test("regexes stay callable through Object.values", async () => {
|
||||||
expect(await value(`return Object.values({ r: /ab+/ })[0].test("abb")`)).toBe(true)
|
expect(await value(`return Object.values({ r: /ab+/ })[0].test("abb")`)).toBe(true)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type {
|
|||||||
JSONValue,
|
JSONValue,
|
||||||
LanguageModelV3,
|
LanguageModelV3,
|
||||||
LanguageModelV3CallOptions,
|
LanguageModelV3CallOptions,
|
||||||
|
LanguageModelV3FinishReason,
|
||||||
LanguageModelV3FunctionTool,
|
LanguageModelV3FunctionTool,
|
||||||
LanguageModelV3Message,
|
LanguageModelV3Message,
|
||||||
LanguageModelV3Prompt,
|
LanguageModelV3Prompt,
|
||||||
@@ -624,8 +625,8 @@ function usage(input: Extract<LanguageModelV3StreamPart, { type: "finish" }>["us
|
|||||||
return Object.values(output).some((value) => value !== undefined) ? output : undefined
|
return Object.values(output).some((value) => value !== undefined) ? output : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
function finishReason(value: unknown): FinishReason {
|
function finishReason(value: LanguageModelV3FinishReason): FinishReason {
|
||||||
return Schema.is(FinishReason)(value) ? value : "unknown"
|
return value.unified === "other" ? "unknown" : value.unified
|
||||||
}
|
}
|
||||||
|
|
||||||
function providerMetadata(value: unknown) {
|
function providerMetadata(value: unknown) {
|
||||||
|
|||||||
@@ -200,6 +200,6 @@ export const node = makeLocationNode({ service: Service, layer, deps: [FSUtil.no
|
|||||||
// TODO: Publish watcher/file-edit events after V2 watcher integration exists.
|
// TODO: Publish watcher/file-edit events after V2 watcher integration exists.
|
||||||
// TODO: Add snapshots / undo after V2 snapshot design exists.
|
// TODO: Add snapshots / undo after V2 snapshot design exists.
|
||||||
// TODO: Notify LSP and collect diagnostics after V2 LSP runtime exists.
|
// TODO: Notify LSP and collect diagnostics after V2 LSP runtime exists.
|
||||||
// TODO: Design multi-file transactions / rollback if apply_patch needs atomic edits.
|
// TODO: Design multi-file transactions / rollback if patch needs atomic edits.
|
||||||
// Until then, edits are sequential and report partial application.
|
// Until then, edits are sequential and report partial application.
|
||||||
// TODO: Define crash recovery and idempotency for side effects between Tool.Called and durable settlement.
|
// TODO: Define crash recovery and idempotency for side effects between Tool.Called and durable settlement.
|
||||||
|
|||||||
@@ -150,31 +150,44 @@ export type LocationError = LayerNode.Error<typeof locationServices>
|
|||||||
export function buildLocationServiceMap(
|
export function buildLocationServiceMap(
|
||||||
replacements: LayerNode.Replacements = [],
|
replacements: LayerNode.Replacements = [],
|
||||||
): Layer.Layer<LocationServiceMap.Service> {
|
): 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(
|
return Layer.effect(
|
||||||
LocationServiceMap.Service,
|
LocationServiceMap.Service,
|
||||||
LayerMap.make(
|
Effect.map(
|
||||||
(ref: Location.Ref) => {
|
LayerMap.make(
|
||||||
const startedAt = performance.now()
|
(ref: Location.Ref) => {
|
||||||
const allReplacements = replacements.concat([[Location.node, Location.boundNode(ref)]])
|
const startedAt = performance.now()
|
||||||
// Apply replacements during hoist, not afterward: replacements can
|
const allReplacements = replacements.concat([[Location.node, Location.boundNode(ref)]])
|
||||||
// introduce new tagged dependencies (Location.boundNode depends on
|
// Apply replacements during hoist, not afterward: replacements can
|
||||||
// Project), and the hoist walk is the only pass that can still slice
|
// introduce new tagged dependencies (Location.boundNode depends on
|
||||||
// those back out.
|
// Project), and the hoist walk is the only pass that can still slice
|
||||||
const location = LayerNode.hoist(locationServices, Node.tags.values.global, allReplacements)
|
// those back out.
|
||||||
|
const location = LayerNode.hoist(locationServices, Node.tags.values.global, allReplacements)
|
||||||
|
|
||||||
return LayerNode.compile(location.node).pipe(
|
return LayerNode.compile(location.node).pipe(
|
||||||
Layer.fresh,
|
Layer.fresh,
|
||||||
Layer.tap(() =>
|
Layer.tap(() =>
|
||||||
Effect.logInfo("location services booted", {
|
Effect.logInfo("location services booted", {
|
||||||
directory: ref.directory,
|
directory: ref.directory,
|
||||||
workspaceID: ref.workspaceID,
|
workspaceID: ref.workspaceID,
|
||||||
durationMs: Math.round(performance.now() - startedAt),
|
durationMs: Math.round(performance.now() - startedAt),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
Layer.provide(LayerNode.compile(location.hoisted)),
|
Layer.provide(LayerNode.compile(location.hoisted)),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{ idleTimeToLive: "60 minutes" },
|
{ 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)),
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+118
-16
@@ -3,7 +3,7 @@ export * as MCPClient from "./client"
|
|||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
import { execFile } from "node:child_process"
|
import { execFile } from "node:child_process"
|
||||||
import { pathToFileURL } from "node:url"
|
import { pathToFileURL } from "node:url"
|
||||||
import { Client, type ClientOptions } from "@modelcontextprotocol/sdk/client/index.js"
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
|
||||||
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
|
||||||
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
|
||||||
import { UnauthorizedError, type OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js"
|
import { UnauthorizedError, type OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js"
|
||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
ListToolsResultSchema,
|
ListToolsResultSchema,
|
||||||
PromptListChangedNotificationSchema,
|
PromptListChangedNotificationSchema,
|
||||||
PromptSchema,
|
PromptSchema,
|
||||||
|
ResourceListChangedNotificationSchema,
|
||||||
type LoggingMessageNotification,
|
type LoggingMessageNotification,
|
||||||
LoggingMessageNotificationSchema,
|
LoggingMessageNotificationSchema,
|
||||||
ToolListChangedNotificationSchema,
|
ToolListChangedNotificationSchema,
|
||||||
@@ -68,11 +69,13 @@ export interface ToolDefinition {
|
|||||||
export interface PromptDefinition {
|
export interface PromptDefinition {
|
||||||
readonly name: string
|
readonly name: string
|
||||||
readonly description: string | undefined
|
readonly description: string | undefined
|
||||||
readonly arguments: ReadonlyArray<{
|
readonly arguments:
|
||||||
readonly name: string
|
| ReadonlyArray<{
|
||||||
readonly description: string | undefined
|
readonly name: string
|
||||||
readonly required: boolean | undefined
|
readonly description: string | undefined
|
||||||
}> | undefined
|
readonly required: boolean | undefined
|
||||||
|
}>
|
||||||
|
| undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PromptMessage {
|
export interface PromptMessage {
|
||||||
@@ -84,6 +87,28 @@ export interface PromptResult {
|
|||||||
readonly messages: ReadonlyArray<PromptMessage>
|
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 =
|
export type CallToolContent =
|
||||||
| { readonly type: "text"; readonly text: string }
|
| { readonly type: "text"; readonly text: string }
|
||||||
| { readonly type: "media"; readonly data: string; readonly mimeType: string }
|
| { readonly type: "media"; readonly data: string; readonly mimeType: string }
|
||||||
@@ -124,6 +149,12 @@ export interface Connection {
|
|||||||
readonly tools: () => Effect.Effect<ToolDefinition[], Error>
|
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. */
|
/** 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>
|
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. */
|
/** Invokes a prompt on the server. Interruption aborts the in-flight request. */
|
||||||
readonly prompt: (input: {
|
readonly prompt: (input: {
|
||||||
readonly name: string
|
readonly name: string
|
||||||
@@ -141,6 +172,8 @@ export interface Connection {
|
|||||||
readonly onToolsChanged: (callback: () => void) => void
|
readonly onToolsChanged: (callback: () => void) => void
|
||||||
/** Registers a callback fired when the server announces its prompt list changed; no-op if unsupported. */
|
/** Registers a callback fired when the server announces its prompt list changed; no-op if unsupported. */
|
||||||
readonly onPromptsChanged: (callback: () => void) => void
|
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. */
|
/** Connects an MCP server; closing the calling scope tears down the transport and any spawned process. */
|
||||||
@@ -168,7 +201,8 @@ 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), {
|
return new StreamableHTTPClientTransport(new URL(config.url), {
|
||||||
requestInit: config.headers ? { headers: config.headers } : undefined,
|
requestInit: config.headers ? { headers: config.headers } : undefined,
|
||||||
authProvider,
|
authProvider,
|
||||||
@@ -202,10 +236,7 @@ export const connect = Effect.fnUntraced(function* (
|
|||||||
}).pipe(Effect.exit)
|
}).pipe(Effect.exit)
|
||||||
if (Exit.isSuccess(exit)) {
|
if (Exit.isSuccess(exit)) {
|
||||||
yield* Effect.addFinalizer(() =>
|
yield* Effect.addFinalizer(() =>
|
||||||
cleanupStdioDescendants(transport).pipe(
|
cleanupStdioDescendants(transport).pipe(Effect.andThen(Effect.promise(() => client.close())), Effect.ignore),
|
||||||
Effect.andThen(Effect.promise(() => client.close())),
|
|
||||||
Effect.ignore,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
const catalogTimeout = config.timeout?.catalog ?? DEFAULT_CATALOG_TIMEOUT
|
const catalogTimeout = config.timeout?.catalog ?? DEFAULT_CATALOG_TIMEOUT
|
||||||
const executionTimeout = config.timeout?.execution ?? DEFAULT_EXECUTION_TIMEOUT
|
const executionTimeout = config.timeout?.execution ?? DEFAULT_EXECUTION_TIMEOUT
|
||||||
@@ -257,7 +288,9 @@ export const connect = Effect.fnUntraced(function* (
|
|||||||
),
|
),
|
||||||
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
|
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
|
||||||
}).pipe(
|
}).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) => ({
|
return prompts.map((prompt) => ({
|
||||||
name: prompt.name,
|
name: prompt.name,
|
||||||
@@ -269,6 +302,74 @@ 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) =>
|
prompt: (input) =>
|
||||||
Effect.tryPromise({
|
Effect.tryPromise({
|
||||||
try: (signal) =>
|
try: (signal) =>
|
||||||
@@ -328,13 +429,14 @@ export const connect = Effect.fnUntraced(function* (
|
|||||||
if (!client.getServerCapabilities()?.prompts?.listChanged) return
|
if (!client.getServerCapabilities()?.prompts?.listChanged) return
|
||||||
client.setNotificationHandler(PromptListChangedNotificationSchema, async () => callback())
|
client.setNotificationHandler(PromptListChangedNotificationSchema, async () => callback())
|
||||||
},
|
},
|
||||||
|
onResourcesChanged: (callback) => {
|
||||||
|
if (!client.getServerCapabilities()?.resources?.listChanged) return
|
||||||
|
client.setNotificationHandler(ResourceListChangedNotificationSchema, async () => callback())
|
||||||
|
},
|
||||||
} satisfies Connection
|
} satisfies Connection
|
||||||
}
|
}
|
||||||
|
|
||||||
yield* cleanupStdioDescendants(transport).pipe(
|
yield* cleanupStdioDescendants(transport).pipe(Effect.andThen(Effect.promise(() => transport.close())), Effect.ignore)
|
||||||
Effect.andThen(Effect.promise(() => transport.close())),
|
|
||||||
Effect.ignore,
|
|
||||||
)
|
|
||||||
const error = Cause.squash(exit.cause)
|
const error = Cause.squash(exit.cause)
|
||||||
if (error instanceof UnauthorizedError) return yield* new NeedsAuthError({ server })
|
if (error instanceof UnauthorizedError) return yield* new NeedsAuthError({ server })
|
||||||
return yield* new ConnectError({ server, message: error instanceof Error ? error.message : String(error) })
|
return yield* new ConnectError({ server, message: error instanceof Error ? error.message : String(error) })
|
||||||
|
|||||||
@@ -83,48 +83,16 @@ export class PromptResult extends Schema.Class<PromptResult>("MCP.PromptResult")
|
|||||||
messages: Schema.Array(PromptMessage),
|
messages: Schema.Array(PromptMessage),
|
||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
export class Resource extends Schema.Class<Resource>("MCP.Resource")({
|
export const Resource = Mcp.Resource
|
||||||
server: ServerName,
|
export type Resource = Mcp.Resource
|
||||||
name: Schema.String,
|
export const ResourceTemplate = Mcp.ResourceTemplate
|
||||||
uri: Schema.String,
|
export type ResourceTemplate = Mcp.ResourceTemplate
|
||||||
description: Schema.String.pipe(Schema.optional),
|
export const ResourceCatalog = Mcp.ResourceCatalog
|
||||||
mimeType: Schema.String.pipe(Schema.optional),
|
export type ResourceCatalog = Mcp.ResourceCatalog
|
||||||
}) {}
|
export const ResourceContentPart = Mcp.ResourceContentPart
|
||||||
|
export type ResourceContentPart = Mcp.ResourceContentPart
|
||||||
export class ResourceTemplate extends Schema.Class<ResourceTemplate>("MCP.ResourceTemplate")({
|
export const ResourceContent = Mcp.ResourceContent
|
||||||
server: ServerName,
|
export type ResourceContent = Mcp.ResourceContent
|
||||||
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", {
|
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("MCP.NotFoundError", {
|
||||||
server: ServerName,
|
server: ServerName,
|
||||||
@@ -415,6 +383,24 @@ 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) =>
|
const refreshTools = (name: ServerName, entry: ServerEntry, connection: MCPClient.Connection) =>
|
||||||
connection.tools().pipe(
|
connection.tools().pipe(
|
||||||
Effect.map((defs) => {
|
Effect.map((defs) => {
|
||||||
@@ -443,6 +429,7 @@ export const layer = Layer.effect(
|
|||||||
entry.prompts = undefined
|
entry.prompts = undefined
|
||||||
entry.status = { status: "failed", error: "Connection closed" }
|
entry.status = { status: "failed", error: "Connection closed" }
|
||||||
fork(events.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore))
|
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(Command.Event.Updated, {}).pipe(Effect.ignore))
|
||||||
fork(events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore))
|
fork(events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore))
|
||||||
})
|
})
|
||||||
@@ -458,6 +445,10 @@ export const layer = Layer.effect(
|
|||||||
connection.onPromptsChanged(() => {
|
connection.onPromptsChanged(() => {
|
||||||
fork(refreshPrompts(name, entry, connection).pipe(Effect.ignore))
|
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) => {
|
const serverLog = (server: ServerName, message: MCPClient.LogMessage) => {
|
||||||
@@ -501,6 +492,7 @@ export const layer = Layer.effect(
|
|||||||
// after the initial registration sweep and emits no list-changed notification would otherwise
|
// after the initial registration sweep and emits no list-changed notification would otherwise
|
||||||
// stay invisible to the model.
|
// stay invisible to the model.
|
||||||
yield* events.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore)
|
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)
|
yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
|
||||||
fork(refreshPrompts(name, entry, result.value.connection).pipe(Effect.ignore))
|
fork(refreshPrompts(name, entry, result.value.connection).pipe(Effect.ignore))
|
||||||
return
|
return
|
||||||
@@ -557,11 +549,6 @@ export const layer = Layer.effect(
|
|||||||
concurrency: "unbounded",
|
concurrency: "unbounded",
|
||||||
discard: true,
|
discard: true,
|
||||||
})
|
})
|
||||||
const gate = Effect.fnUntraced(function* (server: ServerName | string) {
|
|
||||||
const target = yield* requireServer(server)
|
|
||||||
yield* Deferred.await(target.entry.startup)
|
|
||||||
})
|
|
||||||
|
|
||||||
return Service.of({
|
return Service.of({
|
||||||
servers: Effect.fn("MCP.servers")(function* () {
|
servers: Effect.fn("MCP.servers")(function* () {
|
||||||
const entries = Array.from(runtime).toSorted(([a], [b]) => a.localeCompare(b))
|
const entries = Array.from(runtime).toSorted(([a], [b]) => a.localeCompare(b))
|
||||||
@@ -637,11 +624,54 @@ export const layer = Layer.effect(
|
|||||||
}),
|
}),
|
||||||
resourceCatalog: Effect.fn("MCP.resourceCatalog")(function* () {
|
resourceCatalog: Effect.fn("MCP.resourceCatalog")(function* () {
|
||||||
yield* whenAllReady
|
yield* whenAllReady
|
||||||
return new ResourceCatalog({ resources: [], templates: [] })
|
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),
|
||||||
|
),
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
readResource: Effect.fn("MCP.readResource")(function* (input) {
|
readResource: Effect.fn("MCP.readResource")(function* (input) {
|
||||||
yield* gate(input.server)
|
const target = yield* requireServer(input.server)
|
||||||
return undefined
|
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,
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -7,14 +7,6 @@ import { EventV2 } from "../event"
|
|||||||
|
|
||||||
export const Updated = EventV2.ephemeral({ type: "sdk.plugin.updated", schema: {} })
|
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,
|
* Holds the plugins an embedder (the `@opencode-ai/sdk-next` host) contributes,
|
||||||
* so `PluginSupervisor` can add them on every Location boot through the ordinary
|
* so `PluginSupervisor` can add them on every Location boot through the ordinary
|
||||||
@@ -22,10 +14,9 @@ const defaultStore = makeStore()
|
|||||||
* config. Registration publishes an unlocated update so every booted Location
|
* config. Registration publishes an unlocated update so every booted Location
|
||||||
* reloads its plugin generation from the shared store.
|
* reloads its plugin generation from the shared store.
|
||||||
*
|
*
|
||||||
* The store is shared explicitly between the SDK construction graph and the
|
* Each host-global layer owns one private store. Location graphs reuse that
|
||||||
* embedded route graph because `LocationServiceMap` builds Location layers lazily
|
* layer through Effect's memoization, so separate hosts remain isolated while
|
||||||
* in a nested graph. Each embedded SDK creates its own store, so instances do not
|
* every Location in one host sees the same registrations.
|
||||||
* see each other's contributions.
|
|
||||||
*/
|
*/
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
readonly register: (plugin: Plugin) => Effect.Effect<void>
|
readonly register: (plugin: Plugin) => Effect.Effect<void>
|
||||||
@@ -34,26 +25,19 @@ export interface Interface {
|
|||||||
|
|
||||||
export class Service extends Context.Service<Service, Interface>()("@opencode/SdkPlugins") {}
|
export class Service extends Context.Service<Service, Interface>()("@opencode/SdkPlugins") {}
|
||||||
|
|
||||||
export const layerWithStore = (store: Store) =>
|
export const layer = Layer.effect(
|
||||||
Layer.effect(
|
Service,
|
||||||
Service,
|
Effect.gen(function* () {
|
||||||
Effect.gen(function* () {
|
const events = yield* EventV2.Service
|
||||||
const events = yield* EventV2.Service
|
const plugins = new Map<string, Plugin>()
|
||||||
yield* Effect.addFinalizer(() =>
|
return Service.of({
|
||||||
|
register: (plugin) =>
|
||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
store.plugins.clear()
|
plugins.set(plugin.id, plugin)
|
||||||
}),
|
}).pipe(Effect.andThen(events.publish(Updated, {})), Effect.asVoid),
|
||||||
)
|
all: () => [...plugins.values()],
|
||||||
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] })
|
export const node = makeGlobalNode({ service: Service, layer, deps: [EventV2.node] })
|
||||||
|
|||||||
@@ -243,6 +243,7 @@ export interface Interface {
|
|||||||
text: string
|
text: string
|
||||||
description?: string
|
description?: string
|
||||||
metadata?: Record<string, unknown>
|
metadata?: Record<string, unknown>
|
||||||
|
resume?: boolean
|
||||||
}) => Effect.Effect<void, NotFoundError>
|
}) => Effect.Effect<void, NotFoundError>
|
||||||
readonly revert: {
|
readonly revert: {
|
||||||
readonly stage: (input: {
|
readonly stage: (input: {
|
||||||
@@ -682,6 +683,7 @@ const layer = Layer.effect(
|
|||||||
description: input.description,
|
description: input.description,
|
||||||
metadata: input.metadata,
|
metadata: input.metadata,
|
||||||
})
|
})
|
||||||
|
if (input.resume === false) return
|
||||||
yield* execution
|
yield* execution
|
||||||
.resume(input.sessionID)
|
.resume(input.sessionID)
|
||||||
.pipe(Effect.ignore, Effect.forkIn(scope, { startImmediately: true }), Effect.asVoid)
|
.pipe(Effect.ignore, Effect.forkIn(scope, { startImmediately: true }), Effect.asVoid)
|
||||||
|
|||||||
@@ -25,20 +25,27 @@ 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)"]
|
- [constraints/preferences, decisions and why, important facts/assumptions, exact context needed to continue, or "(none)"]
|
||||||
|
|
||||||
## Work State
|
## Work State
|
||||||
- Completed: [finished work, verified facts, or changes made; otherwise "(none)"]
|
### Completed
|
||||||
- Active: [current work, partial changes, or investigation state; otherwise "(none)"]
|
- [finished work, verified facts, or changes made; otherwise "(none)"]
|
||||||
- Blocked: [blockers, failing commands, or unknowns; otherwise "(none)"]
|
|
||||||
|
### Active
|
||||||
|
- [current work, partial changes, or investigation state; otherwise "(none)"]
|
||||||
|
|
||||||
|
### Blocked
|
||||||
|
- [blockers, failing commands, or unknowns; otherwise "(none)"]
|
||||||
|
|
||||||
## Next Move
|
## Next Move
|
||||||
1. [immediate concrete action, or "(none)"]
|
1. [immediate concrete action, or "(none)"]
|
||||||
2. [next action if known, or "(none)"]
|
2. [next action if known, or "(none)"]
|
||||||
|
|
||||||
|
## Relevant Files
|
||||||
|
- [file or directory path: why it matters, or "(none)"]
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
- Keep every section, even when empty.
|
- Keep every section, even when empty.
|
||||||
- Use terse bullets, not prose paragraphs.
|
- Use terse bullets, not prose paragraphs.
|
||||||
- Preserve exact file paths, symbols, commands, error strings, URLs, and identifiers when known.
|
- 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.`
|
- Do not mention the summary process or that context was compacted.`
|
||||||
|
|
||||||
type Settings = {
|
type Settings = {
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
|||||||
|
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
yield* SessionEvent.All.match(event, {
|
yield* SessionEvent.All.match(event, {
|
||||||
|
"session.usage.updated": () => Effect.void,
|
||||||
"session.agent.selected": (event) => {
|
"session.agent.selected": (event) => {
|
||||||
return adapter.appendMessage(
|
return adapter.appendMessage(
|
||||||
SessionMessage.AgentSelected.make({
|
SessionMessage.AgentSelected.make({
|
||||||
@@ -296,6 +297,10 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
|||||||
draft.finish = "error"
|
draft.finish = "error"
|
||||||
draft.error = castDraft(event.data.error)
|
draft.error = castDraft(event.data.error)
|
||||||
draft.retry = undefined
|
draft.retry = undefined
|
||||||
|
if (event.data.cost !== undefined && event.data.tokens !== undefined) {
|
||||||
|
draft.cost = event.data.cost
|
||||||
|
draft.tokens = castDraft(event.data.tokens)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
"session.text.started": (event) => {
|
"session.text.started": (event) => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export * as SessionProjector from "./projector"
|
export * as SessionProjector from "./projector"
|
||||||
|
|
||||||
import { and, asc, desc, eq, gt, gte, inArray, lt, or, sql } from "drizzle-orm"
|
import { and, asc, desc, eq, gt, gte, inArray, lt, or, sql } from "drizzle-orm"
|
||||||
import { DateTime, Effect, Layer, Schema } from "effect"
|
import { DateTime, Effect, Layer, Schema, Stream } from "effect"
|
||||||
import { Database } from "../database/database"
|
import { Database } from "../database/database"
|
||||||
import { EventV2 } from "../event"
|
import { EventV2 } from "../event"
|
||||||
import { makeGlobalNode } from "../effect/app-node"
|
import { makeGlobalNode } from "../effect/app-node"
|
||||||
@@ -48,11 +48,6 @@ type Usage = {
|
|||||||
|
|
||||||
const ForkBatchSize = 500
|
const ForkBatchSize = 500
|
||||||
|
|
||||||
const emptyUsage = (): Usage => ({
|
|
||||||
cost: 0,
|
|
||||||
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
|
||||||
})
|
|
||||||
|
|
||||||
const forkTitle = (value: string) => {
|
const forkTitle = (value: string) => {
|
||||||
const match = value.match(/^(.+) \(fork #(\d+)\)$/)
|
const match = value.match(/^(.+) \(fork #(\d+)\)$/)
|
||||||
if (match) return `${match[1]} (fork #${Number.parseInt(match[2], 10) + 1})`
|
if (match) return `${match[1]} (fork #${Number.parseInt(match[2], 10) + 1})`
|
||||||
@@ -67,22 +62,6 @@ function usage(part: (typeof SessionV1.Event.PartUpdated.Type)["data"]["part"] |
|
|||||||
return { cost: value.cost as Usage["cost"], tokens: value.tokens as Usage["tokens"] }
|
return { cost: value.cost as Usage["cost"], tokens: value.tokens as Usage["tokens"] }
|
||||||
}
|
}
|
||||||
|
|
||||||
function addUsage(target: Usage, value: Usage) {
|
|
||||||
target.cost += value.cost
|
|
||||||
target.tokens.input += value.tokens.input
|
|
||||||
target.tokens.output += value.tokens.output
|
|
||||||
target.tokens.reasoning += value.tokens.reasoning
|
|
||||||
target.tokens.cache.read += value.tokens.cache.read
|
|
||||||
target.tokens.cache.write += value.tokens.cache.write
|
|
||||||
}
|
|
||||||
|
|
||||||
function messageUsage(row: typeof SessionMessageTable.$inferSelect): Usage | undefined {
|
|
||||||
if (row.type !== "assistant") return undefined
|
|
||||||
const message = decodeMessage({ ...row.data, id: row.id, type: row.type })
|
|
||||||
if (message.type !== "assistant" || message.cost === undefined || message.tokens === undefined) return undefined
|
|
||||||
return { cost: message.cost, tokens: message.tokens }
|
|
||||||
}
|
|
||||||
|
|
||||||
function sessionRow(info: SessionV1.SessionInfo): typeof SessionTable.$inferInsert {
|
function sessionRow(info: SessionV1.SessionInfo): typeof SessionTable.$inferInsert {
|
||||||
return {
|
return {
|
||||||
id: info.id,
|
id: info.id,
|
||||||
@@ -151,6 +130,37 @@ function applyUsage(
|
|||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const publishSessionUsage = Effect.fn("SessionProjector.publishUsage")(function* (
|
||||||
|
db: DatabaseService,
|
||||||
|
events: EventV2.Interface,
|
||||||
|
sessionID: (typeof SessionEvent.Step.Ended.Type)["data"]["sessionID"],
|
||||||
|
) {
|
||||||
|
const row = yield* db
|
||||||
|
.select({
|
||||||
|
cost: SessionTable.cost,
|
||||||
|
input: SessionTable.tokens_input,
|
||||||
|
output: SessionTable.tokens_output,
|
||||||
|
reasoning: SessionTable.tokens_reasoning,
|
||||||
|
cacheRead: SessionTable.tokens_cache_read,
|
||||||
|
cacheWrite: SessionTable.tokens_cache_write,
|
||||||
|
})
|
||||||
|
.from(SessionTable)
|
||||||
|
.where(eq(SessionTable.id, sessionID))
|
||||||
|
.get()
|
||||||
|
.pipe(Effect.orDie)
|
||||||
|
if (!row) return
|
||||||
|
yield* events.publish(SessionEvent.UsageUpdated, {
|
||||||
|
sessionID,
|
||||||
|
cost: row.cost,
|
||||||
|
tokens: {
|
||||||
|
input: row.input,
|
||||||
|
output: row.output,
|
||||||
|
reasoning: row.reasoning,
|
||||||
|
cache: { read: row.cacheRead, write: row.cacheWrite },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const projectFork = Effect.fn("SessionProjector.projectFork")(function* (
|
const projectFork = Effect.fn("SessionProjector.projectFork")(function* (
|
||||||
db: DatabaseService,
|
db: DatabaseService,
|
||||||
event: typeof SessionEvent.Forked.Type,
|
event: typeof SessionEvent.Forked.Type,
|
||||||
@@ -187,7 +197,7 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* (
|
|||||||
.limit(1)
|
.limit(1)
|
||||||
.get()
|
.get()
|
||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
const copiedSeq = copied?.seq ?? 0
|
const copiedSeq = copied?.seq
|
||||||
|
|
||||||
const stored = yield* db
|
const stored = yield* db
|
||||||
.insert(SessionTable)
|
.insert(SessionTable)
|
||||||
@@ -237,9 +247,8 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* (
|
|||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
}
|
}
|
||||||
|
|
||||||
const usage = emptyUsage()
|
|
||||||
let cursor = -1
|
let cursor = -1
|
||||||
while (true) {
|
while (copiedSeq !== undefined) {
|
||||||
const rows = yield* db
|
const rows = yield* db
|
||||||
.select()
|
.select()
|
||||||
.from(SessionMessageTable)
|
.from(SessionMessageTable)
|
||||||
@@ -247,7 +256,7 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* (
|
|||||||
and(
|
and(
|
||||||
eq(SessionMessageTable.session_id, event.data.parentID),
|
eq(SessionMessageTable.session_id, event.data.parentID),
|
||||||
gt(SessionMessageTable.seq, cursor),
|
gt(SessionMessageTable.seq, cursor),
|
||||||
copiedSeq === 0 ? undefined : lt(SessionMessageTable.seq, copiedSeq + 1),
|
lt(SessionMessageTable.seq, copiedSeq + 1),
|
||||||
sql`${SessionMessageTable.type} != 'compaction' or json_extract(${SessionMessageTable.data}, '$.status') not in ('queued', 'running')`,
|
sql`${SessionMessageTable.type} != 'compaction' or json_extract(${SessionMessageTable.data}, '$.status') not in ('queued', 'running')`,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -318,27 +327,9 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* (
|
|||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const row of rows) {
|
|
||||||
const value = messageUsage(row)
|
|
||||||
if (value) addUsage(usage, value)
|
|
||||||
}
|
|
||||||
cursor = rows.at(-1)!.seq
|
cursor = rows.at(-1)!.seq
|
||||||
}
|
}
|
||||||
|
if (copiedSeq !== undefined) yield* EventV2.reserveSequence(db, event.data.sessionID, copiedSeq)
|
||||||
yield* db
|
|
||||||
.update(SessionTable)
|
|
||||||
.set({
|
|
||||||
cost: usage.cost,
|
|
||||||
tokens_input: usage.tokens.input,
|
|
||||||
tokens_output: usage.tokens.output,
|
|
||||||
tokens_reasoning: usage.tokens.reasoning,
|
|
||||||
tokens_cache_read: usage.tokens.cache.read,
|
|
||||||
tokens_cache_write: usage.tokens.cache.write,
|
|
||||||
})
|
|
||||||
.where(eq(SessionTable.id, event.data.sessionID))
|
|
||||||
.run()
|
|
||||||
.pipe(Effect.orDie)
|
|
||||||
if (copiedSeq > 0) yield* EventV2.reserveSequence(db, event.data.sessionID, copiedSeq)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function run(db: DatabaseService, event: MessageEvent) {
|
function run(db: DatabaseService, event: MessageEvent) {
|
||||||
@@ -697,8 +688,19 @@ const layer = Layer.effectDiscard(
|
|||||||
yield* events.project(SessionEvent.Shell.Started, (event) => run(db, event))
|
yield* events.project(SessionEvent.Shell.Started, (event) => run(db, event))
|
||||||
yield* events.project(SessionEvent.Shell.Ended, (event) => run(db, event))
|
yield* events.project(SessionEvent.Shell.Ended, (event) => run(db, event))
|
||||||
yield* events.project(SessionEvent.Step.Started, (event) => run(db, event))
|
yield* events.project(SessionEvent.Step.Started, (event) => run(db, event))
|
||||||
yield* events.project(SessionEvent.Step.Ended, (event) => run(db, event))
|
yield* events.project(SessionEvent.Step.Ended, (event) =>
|
||||||
yield* events.project(SessionEvent.Step.Failed, (event) => run(db, event))
|
Effect.gen(function* () {
|
||||||
|
yield* run(db, event)
|
||||||
|
yield* applyUsage(db, event.data.sessionID, event.data)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
yield* events.project(SessionEvent.Step.Failed, (event) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* run(db, event)
|
||||||
|
if (event.data.cost !== undefined && event.data.tokens !== undefined)
|
||||||
|
yield* applyUsage(db, event.data.sessionID, { cost: event.data.cost, tokens: event.data.tokens })
|
||||||
|
}),
|
||||||
|
)
|
||||||
yield* events.project(SessionEvent.Text.Started, (event) => run(db, event))
|
yield* events.project(SessionEvent.Text.Started, (event) => run(db, event))
|
||||||
yield* events.project(SessionEvent.Text.Ended, (event) => run(db, event))
|
yield* events.project(SessionEvent.Text.Ended, (event) => run(db, event))
|
||||||
yield* events.project(SessionEvent.Tool.Input.Started, (event) => run(db, event))
|
yield* events.project(SessionEvent.Tool.Input.Started, (event) => run(db, event))
|
||||||
@@ -790,6 +792,17 @@ const layer = Layer.effectDiscard(
|
|||||||
yield* InstructionCheckpoint.reset(db, event.data.sessionID)
|
yield* InstructionCheckpoint.reset(db, event.data.sessionID)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
yield* events.subscribe([SessionEvent.Step.Ended, SessionEvent.Step.Failed]).pipe(
|
||||||
|
Stream.runForEach((event) => {
|
||||||
|
if (
|
||||||
|
event.type === SessionEvent.Step.Failed.type &&
|
||||||
|
(event.data.cost === undefined || event.data.tokens === undefined)
|
||||||
|
)
|
||||||
|
return Effect.void
|
||||||
|
return publishSessionUsage(db, events, event.data.sessionID)
|
||||||
|
}),
|
||||||
|
Effect.forkScoped({ startImmediately: true }),
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { Config } from "../../config"
|
|||||||
import { Database } from "../../database/database"
|
import { Database } from "../../database/database"
|
||||||
import { EventV2 } from "../../event"
|
import { EventV2 } from "../../event"
|
||||||
import { Location } from "../../location"
|
import { Location } from "../../location"
|
||||||
|
import { ModelV2 } from "../../model"
|
||||||
import { PermissionV2 } from "../../permission"
|
import { PermissionV2 } from "../../permission"
|
||||||
import { Instructions } from "../../instructions/index"
|
import { Instructions } from "../../instructions/index"
|
||||||
import { InstructionBuiltIns } from "../../instructions/builtins"
|
import { InstructionBuiltIns } from "../../instructions/builtins"
|
||||||
@@ -50,6 +51,30 @@ import { StepFailedError, UserInterruptedError } from "../error"
|
|||||||
import { toSessionError } from "../to-session-error"
|
import { toSessionError } from "../to-session-error"
|
||||||
import { SessionRunnerRetry } from "./retry"
|
import { SessionRunnerRetry } from "./retry"
|
||||||
|
|
||||||
|
type StepTokens = {
|
||||||
|
readonly input: number
|
||||||
|
readonly output: number
|
||||||
|
readonly reasoning: number
|
||||||
|
readonly cache: { readonly read: number; readonly write: number }
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(#35765): Use Copilot's reported billed amount once billing has a dedicated typed runtime contract.
|
||||||
|
export function calculateCost(costs: ModelV2.Info["cost"], tokens: StepTokens) {
|
||||||
|
const context = tokens.input + tokens.cache.read + tokens.cache.write
|
||||||
|
const tier = costs
|
||||||
|
.filter((cost) => cost.tier?.type === "context" && context > cost.tier.size)
|
||||||
|
.toSorted((a, b) => (b.tier?.size ?? 0) - (a.tier?.size ?? 0))[0]
|
||||||
|
const cost = tier ?? costs.find((cost) => cost.tier === undefined)
|
||||||
|
if (!cost) return 0
|
||||||
|
return (
|
||||||
|
(tokens.input * cost.input +
|
||||||
|
(tokens.output + tokens.reasoning) * cost.output +
|
||||||
|
tokens.cache.read * cost.cache.read +
|
||||||
|
tokens.cache.write * cost.cache.write) /
|
||||||
|
1_000_000
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs one durable coding-agent Session until it settles.
|
* Runs one durable coding-agent Session until it settles.
|
||||||
*
|
*
|
||||||
@@ -312,6 +337,11 @@ const layer = Layer.effect(
|
|||||||
Effect.ensuring(serialized(publisher.flush())),
|
Effect.ensuring(serialized(publisher.flush())),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const stepUsage = (settlement: NonNullable<ReturnType<typeof publisher.stepSettlement>>) => ({
|
||||||
|
cost: calculateCost(resolved.cost, settlement.tokens),
|
||||||
|
tokens: settlement.tokens,
|
||||||
|
})
|
||||||
|
|
||||||
// Captures the end snapshot, diffs it against the step's start, and durably ends the
|
// Captures the end snapshot, diffs it against the step's start, and durably ends the
|
||||||
// assistant step.
|
// assistant step.
|
||||||
const publishStepEnd = (settlement: NonNullable<ReturnType<typeof publisher.stepSettlement>>) =>
|
const publishStepEnd = (settlement: NonNullable<ReturnType<typeof publisher.stepSettlement>>) =>
|
||||||
@@ -328,8 +358,7 @@ const layer = Layer.effect(
|
|||||||
sessionID: session.id,
|
sessionID: session.id,
|
||||||
assistantMessageID: yield* publisher.startAssistant(),
|
assistantMessageID: yield* publisher.startAssistant(),
|
||||||
finish: settlement.finish,
|
finish: settlement.finish,
|
||||||
cost: 0,
|
...stepUsage(settlement),
|
||||||
tokens: settlement.tokens,
|
|
||||||
snapshot: endSnapshot,
|
snapshot: endSnapshot,
|
||||||
files,
|
files,
|
||||||
}),
|
}),
|
||||||
@@ -452,7 +481,8 @@ const layer = Layer.effect(
|
|||||||
const stepEndedCleanly =
|
const stepEndedCleanly =
|
||||||
!streamInterrupted && !toolsInterrupted && infraError === undefined && !providerFailed && !stepFailure
|
!streamInterrupted && !toolsInterrupted && infraError === undefined && !providerFailed && !stepFailure
|
||||||
if (stepSettlement && stepEndedCleanly) yield* publishStepEnd(stepSettlement)
|
if (stepSettlement && stepEndedCleanly) yield* publishStepEnd(stepSettlement)
|
||||||
if (stepFailure) yield* serialized(publisher.publishStepFailure())
|
if (stepFailure)
|
||||||
|
yield* serialized(publisher.publishStepFailure(stepSettlement ? stepUsage(stepSettlement) : undefined))
|
||||||
|
|
||||||
if (stream._tag === "Failure") return yield* Effect.failCause(stream.cause)
|
if (stream._tag === "Failure") return yield* Effect.failCause(stream.cause)
|
||||||
if (userDeclined) return yield* Effect.interrupt
|
if (userDeclined) return yield* Effect.interrupt
|
||||||
|
|||||||
@@ -82,6 +82,8 @@ export interface Resolved {
|
|||||||
readonly model: Model
|
readonly model: Model
|
||||||
/** Selected catalog identity. Durable records and displays must use this, never the API model id. */
|
/** Selected catalog identity. Durable records and displays must use this, never the API model id. */
|
||||||
readonly ref: ModelV2.Ref
|
readonly ref: ModelV2.Ref
|
||||||
|
/** Catalog pricing in dollars per million tokens. */
|
||||||
|
readonly cost: ModelV2.Info["cost"]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
@@ -94,13 +96,14 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
|
|||||||
export const layerWith = (resolve: Interface["resolve"]) => Layer.succeed(Service, Service.of({ resolve }))
|
export const layerWith = (resolve: Interface["resolve"]) => Layer.succeed(Service, Service.of({ resolve }))
|
||||||
|
|
||||||
/** Builds a Resolved whose catalog identity mirrors the route model. Test or embedding seam. */
|
/** Builds a Resolved whose catalog identity mirrors the route model. Test or embedding seam. */
|
||||||
export const resolved = (model: Model, variant?: ModelV2.VariantID): Resolved => ({
|
export const resolved = (model: Model, variant?: ModelV2.VariantID, cost: ModelV2.Info["cost"] = []): Resolved => ({
|
||||||
model,
|
model,
|
||||||
ref: ModelV2.Ref.make({
|
ref: ModelV2.Ref.make({
|
||||||
id: ModelV2.ID.make(model.id),
|
id: ModelV2.ID.make(model.id),
|
||||||
providerID: ProviderV2.ID.make(model.provider),
|
providerID: ProviderV2.ID.make(model.provider),
|
||||||
...(variant === undefined ? {} : { variant }),
|
...(variant === undefined ? {} : { variant }),
|
||||||
}),
|
}),
|
||||||
|
cost,
|
||||||
})
|
})
|
||||||
|
|
||||||
const apiKey = (model: ModelV2.Info, credential?: Credential.Value) => {
|
const apiKey = (model: ModelV2.Info, credential?: Credential.Value) => {
|
||||||
@@ -341,6 +344,7 @@ const layer = Layer.effect(
|
|||||||
providerID: selected.providerID,
|
providerID: selected.providerID,
|
||||||
...(session.model?.variant === undefined ? {} : { variant: session.model.variant }),
|
...(session.model?.variant === undefined ? {} : { variant: session.model.variant }),
|
||||||
}),
|
}),
|
||||||
|
cost: selected.cost,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -216,7 +216,10 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||||||
if (replace || stepFailure === undefined) stepFailure = error
|
if (replace || stepFailure === undefined) stepFailure = error
|
||||||
})
|
})
|
||||||
|
|
||||||
const publishStepFailure = Effect.fnUntraced(function* () {
|
const publishStepFailure = Effect.fnUntraced(function* (usage?: {
|
||||||
|
readonly cost: number
|
||||||
|
readonly tokens: ReturnType<typeof tokens>
|
||||||
|
}) {
|
||||||
if (stepFailed || stepFailure === undefined) return
|
if (stepFailed || stepFailure === undefined) return
|
||||||
const assistantMessageID = yield* startAssistant()
|
const assistantMessageID = yield* startAssistant()
|
||||||
stepFailed = true
|
stepFailed = true
|
||||||
@@ -224,6 +227,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
assistantMessageID,
|
assistantMessageID,
|
||||||
error: stepFailure,
|
error: stepFailure,
|
||||||
|
...usage,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -409,12 +413,12 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||||||
case "step-finish":
|
case "step-finish":
|
||||||
yield* flush()
|
yield* flush()
|
||||||
if (stepSettlement) return yield* Effect.die(new Error("Duplicate step finish"))
|
if (stepSettlement) return yield* Effect.die(new Error("Duplicate step finish"))
|
||||||
|
stepSettlement = { finish: event.reason, tokens: tokens(event.usage) }
|
||||||
if (event.reason === "content-filter") {
|
if (event.reason === "content-filter") {
|
||||||
providerFailed = true
|
providerFailed = true
|
||||||
yield* failAssistant({ type: "provider.content-filter", message: "Provider blocked the response" }, true)
|
yield* failAssistant({ type: "provider.content-filter", message: "Provider blocked the response" }, true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
stepSettlement = { finish: event.reason, tokens: tokens(event.usage) }
|
|
||||||
return
|
return
|
||||||
case "finish":
|
case "finish":
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -120,12 +120,12 @@ const assistant = (message: SessionMessage.Assistant, model: ModelV2.Ref) => {
|
|||||||
const content = message.content.flatMap((item): ContentPart[] => {
|
const content = message.content.flatMap((item): ContentPart[] => {
|
||||||
if (item.type === "text") return [{ type: "text", text: item.text }]
|
if (item.type === "text") return [{ type: "text", text: item.text }]
|
||||||
if (item.type === "reasoning")
|
if (item.type === "reasoning")
|
||||||
return sameModel
|
return reuseProviderMetadata
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
type: "reasoning",
|
type: "reasoning",
|
||||||
text: item.text,
|
text: item.text,
|
||||||
providerMetadata: reuseProviderMetadata ? providerMetadata(model.providerID, item.state) : undefined,
|
providerMetadata: providerMetadata(model.providerID, item.state),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: item.text.length > 0
|
: item.text.length > 0
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ Registrations are scoped:
|
|||||||
|
|
||||||
## Permissions
|
## Permissions
|
||||||
|
|
||||||
The registry has no `PermissionV2.Service` dependency and performs no execution authorization. An internal built-in-only operation attaches a permission action solely to preserve whole-tool definition filtering; it is not part of public `Tool.make`. Most tools default to their registered name; `edit`, `write`, and `apply_patch` declare the shared `edit` action.
|
The registry has no `PermissionV2.Service` dependency and performs no execution authorization. An internal built-in-only operation attaches a permission action solely to preserve whole-tool definition filtering; it is not part of public `Tool.make`. Most tools default to their registered name; `edit`, `write`, and `patch` declare the shared `edit` action.
|
||||||
|
|
||||||
Definition filtering is catalog visibility, not execution authorization. A call still executes the captured leaf policy if it reaches settlement.
|
Definition filtering is catalog visibility, not execution authorization. A call still executes the captured leaf policy if it reaches settlement.
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { Patch } from "../patch"
|
|||||||
import { PermissionV2 } from "../permission"
|
import { PermissionV2 } from "../permission"
|
||||||
import { Tool } from "./tool"
|
import { Tool } from "./tool"
|
||||||
|
|
||||||
export const name = "apply_patch"
|
export const name = "patch"
|
||||||
|
|
||||||
export const Input = Schema.Struct({
|
export const Input = Schema.Struct({
|
||||||
patchText: Schema.String.annotate({
|
patchText: Schema.String.annotate({
|
||||||
@@ -91,11 +91,11 @@ export const Plugin = {
|
|||||||
if (!input.patchText.trim()) return yield* new ToolFailure({ message: "patchText is required" })
|
if (!input.patchText.trim()) return yield* new ToolFailure({ message: "patchText is required" })
|
||||||
const hunks = yield* Effect.try({
|
const hunks = yield* Effect.try({
|
||||||
try: () => Patch.parse(input.patchText),
|
try: () => Patch.parse(input.patchText),
|
||||||
catch: (cause) => new ToolFailure({ message: `apply_patch verification failed: ${String(cause)}` }),
|
catch: (cause) => new ToolFailure({ message: `patch verification failed: ${String(cause)}` }),
|
||||||
})
|
})
|
||||||
if (hunks.length === 0) return yield* new ToolFailure({ message: "patch rejected: empty patch" })
|
if (hunks.length === 0) return yield* new ToolFailure({ message: "patch rejected: empty patch" })
|
||||||
const move = hunks.find((hunk) => hunk.type === "update" && hunk.movePath !== undefined)
|
const move = hunks.find((hunk) => hunk.type === "update" && hunk.movePath !== undefined)
|
||||||
if (move) return yield* new ToolFailure({ message: "apply_patch moves are not supported yet" })
|
if (move) return yield* new ToolFailure({ message: "patch moves are not supported yet" })
|
||||||
|
|
||||||
const targets: Array<{ readonly hunk: Patch.Hunk; readonly target: LocationMutation.Target }> = []
|
const targets: Array<{ readonly hunk: Patch.Hunk; readonly target: LocationMutation.Target }> = []
|
||||||
for (const hunk of hunks)
|
for (const hunk of hunks)
|
||||||
|
|||||||
@@ -43,20 +43,15 @@ export interface Registration {
|
|||||||
readonly group?: string
|
readonly group?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CodeModeTools {
|
|
||||||
[name: string]: Tool.Definition<never> | CodeModeTools
|
|
||||||
}
|
|
||||||
|
|
||||||
export const create = (options: {
|
export const create = (options: {
|
||||||
readonly registrations: ReadonlyMap<string, Registration>
|
readonly registrations: ReadonlyMap<string, Registration>
|
||||||
readonly current: (name: string) => Registration | undefined
|
readonly current: (name: string) => Registration | undefined
|
||||||
readonly tools: CodeModeTools
|
|
||||||
}) => {
|
}) => {
|
||||||
const runtime = (
|
const runtime = (
|
||||||
invoke: (name: string, registration: Registration, input: unknown) => Effect.Effect<unknown, unknown>,
|
invoke: (name: string, registration: Registration, input: unknown) => Effect.Effect<unknown, unknown>,
|
||||||
hooks?: CodeMode.ToolCallHooks,
|
hooks?: CodeMode.ToolCallHooks,
|
||||||
) => {
|
) => {
|
||||||
const tools = cloneTools(options.tools)
|
const tools: Record<string, Tool.Definition<never> | Record<string, Tool.Definition<never>>> = {}
|
||||||
for (const [name, registration] of options.registrations) {
|
for (const [name, registration] of options.registrations) {
|
||||||
const child = definition(name, registration.tool)
|
const child = definition(name, registration.tool)
|
||||||
const value = Tool.make({
|
const value = Tool.make({
|
||||||
@@ -168,15 +163,6 @@ 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 {
|
function displayInput(input: unknown): Record<string, unknown> | undefined {
|
||||||
if (input === null || input === undefined) return
|
if (input === null || input === undefined) return
|
||||||
if (typeof input !== "object" || Array.isArray(input)) return { input }
|
if (typeof input !== "object" || Array.isArray(input)) return { input }
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
export * as ToolRegistry from "./registry"
|
export * as ToolRegistry from "./registry"
|
||||||
export type { CodeModeTools } from "./execute"
|
|
||||||
|
|
||||||
import { ToolOutput, type ToolCall, type ToolDefinition, type ToolResultValue } from "@opencode-ai/llm"
|
import { ToolOutput, type ToolCall, type ToolDefinition, type ToolResultValue } from "@opencode-ai/llm"
|
||||||
import { Context, Effect, Layer, Scope } from "effect"
|
import { Context, Effect, Layer, Scope } from "effect"
|
||||||
@@ -10,12 +9,11 @@ import { SessionMessage } from "../session/message"
|
|||||||
import { SessionSchema } from "../session/schema"
|
import { SessionSchema } from "../session/schema"
|
||||||
import { ToolOutputStore } from "../tool-output-store"
|
import { ToolOutputStore } from "../tool-output-store"
|
||||||
import { Wildcard } from "../util/wildcard"
|
import { Wildcard } from "../util/wildcard"
|
||||||
import { ExecuteTool, type CodeModeTools } from "./execute"
|
import { ExecuteTool } from "./execute"
|
||||||
import { definition, permission, registrationEntries, RegistrationError, settle, type AnyTool } from "./tool"
|
import { definition, permission, registrationEntries, RegistrationError, settle, type AnyTool } from "./tool"
|
||||||
import { Tools } from "./tools"
|
import { Tools } from "./tools"
|
||||||
import { ToolHooks } from "./hooks"
|
import { ToolHooks } from "./hooks"
|
||||||
import { makeLocationNode } from "../effect/app-node"
|
import { makeLocationNode } from "../effect/app-node"
|
||||||
import { LayerNode } from "../effect/layer-node"
|
|
||||||
import { SessionError } from "@opencode-ai/schema/session-error"
|
import { SessionError } from "@opencode-ai/schema/session-error"
|
||||||
import { toSessionError } from "../session/to-session-error"
|
import { toSessionError } from "../session/to-session-error"
|
||||||
|
|
||||||
@@ -53,20 +51,10 @@ export interface Settlement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/ToolRegistry") {}
|
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(
|
const registryLayer = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const codeModeTools = (yield* CodeModeCatalog).tools
|
|
||||||
const resources = yield* ToolOutputStore.Service
|
const resources = yield* ToolOutputStore.Service
|
||||||
const toolHooks = yield* ToolHooks.Service
|
const toolHooks = yield* ToolHooks.Service
|
||||||
type Registration = {
|
type Registration = {
|
||||||
@@ -203,10 +191,10 @@ const registryLayer = Layer.effect(
|
|||||||
const registration = entries.at(-1)?.registration
|
const registration = entries.at(-1)?.registration
|
||||||
if (registration) registrations.set(name, registration)
|
if (registration) registrations.set(name, registration)
|
||||||
}
|
}
|
||||||
// OpenAI/GPT models use apply_patch; every other model uses edit and write.
|
// OpenAI/GPT models use patch; every other model uses edit and write.
|
||||||
const usePatch = input.model.provider.toLowerCase() === "openai" || input.model.id.toLowerCase().includes("gpt")
|
const usePatch = input.model.provider.toLowerCase() === "openai" || input.model.id.toLowerCase().includes("gpt")
|
||||||
for (const [name, registration] of registrations) {
|
for (const [name, registration] of registrations) {
|
||||||
const wrongEditTool = name === "apply_patch" ? !usePatch : (name === "edit" || name === "write") && usePatch
|
const wrongEditTool = name === "patch" ? !usePatch : (name === "edit" || name === "write") && usePatch
|
||||||
if (
|
if (
|
||||||
wrongEditTool ||
|
wrongEditTool ||
|
||||||
(registration.deferred && !Flag.CODEMODE_ENABLED) ||
|
(registration.deferred && !Flag.CODEMODE_ENABLED) ||
|
||||||
@@ -216,13 +204,11 @@ const registryLayer = Layer.effect(
|
|||||||
}
|
}
|
||||||
const direct = new Map(Array.from(registrations).filter(([, registration]) => !registration.deferred))
|
const direct = new Map(Array.from(registrations).filter(([, registration]) => !registration.deferred))
|
||||||
const deferred = 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 =
|
const execute =
|
||||||
(deferred.size > 0 || Object.keys(tools).length > 0) && !whollyDisabled("execute", input.permissions ?? [])
|
deferred.size > 0 && !whollyDisabled("execute", input.permissions ?? [])
|
||||||
? ExecuteTool.create({
|
? ExecuteTool.create({
|
||||||
registrations: deferred,
|
registrations: deferred,
|
||||||
current: (name) => local.get(name)?.at(-1)?.registration,
|
current: (name) => local.get(name)?.at(-1)?.registration,
|
||||||
tools,
|
|
||||||
})
|
})
|
||||||
: undefined
|
: undefined
|
||||||
return {
|
return {
|
||||||
@@ -255,18 +241,14 @@ function whollyDisabled(action: string, rules: PermissionV2.Ruleset) {
|
|||||||
return rule?.resource === "*" && rule.effect === "deny"
|
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({
|
export const node = makeLocationNode({
|
||||||
service: Service,
|
service: Service,
|
||||||
layer,
|
layer,
|
||||||
deps: [ToolOutputStore.node, ToolHooks.node, codeModeCatalogNode],
|
deps: [ToolOutputStore.node, ToolHooks.node],
|
||||||
})
|
})
|
||||||
|
|
||||||
export const toolsNode = makeLocationNode({
|
export const toolsNode = makeLocationNode({
|
||||||
service: Tools.Service,
|
service: Tools.Service,
|
||||||
layer,
|
layer,
|
||||||
deps: [ToolOutputStore.node, ToolHooks.node, codeModeCatalogNode],
|
deps: [ToolOutputStore.node, ToolHooks.node],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -51,13 +51,11 @@ it.effect("projects request settings, headers, and body overlays", () =>
|
|||||||
apiKey: "secret",
|
apiKey: "secret",
|
||||||
thinkingConfig: { thinkingBudget: 1024 },
|
thinkingConfig: { thinkingBudget: 1024 },
|
||||||
})
|
})
|
||||||
const resolved = yield* aisdk.model(
|
const resolved = yield* aisdk.model({
|
||||||
{
|
...input,
|
||||||
...input,
|
headers: { "x-test": "header" },
|
||||||
headers: { "x-test": "header" },
|
body: { safety_setting: "strict" },
|
||||||
body: { safety_setting: "strict" },
|
})
|
||||||
},
|
|
||||||
)
|
|
||||||
const prepared = yield* LLMClient.prepare<LanguageModelV3CallOptions>(
|
const prepared = yield* LLMClient.prepare<LanguageModelV3CallOptions>(
|
||||||
LLM.request({ model: resolved, prompt: "Hello" }),
|
LLM.request({ model: resolved, prompt: "Hello" }),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,16 +4,27 @@ import {
|
|||||||
CallToolRequestSchema,
|
CallToolRequestSchema,
|
||||||
GetPromptRequestSchema,
|
GetPromptRequestSchema,
|
||||||
ListPromptsRequestSchema,
|
ListPromptsRequestSchema,
|
||||||
|
ListResourcesRequestSchema,
|
||||||
|
ListResourceTemplatesRequestSchema,
|
||||||
ListToolsRequestSchema,
|
ListToolsRequestSchema,
|
||||||
|
ReadResourceRequestSchema,
|
||||||
} from "@modelcontextprotocol/sdk/types.js"
|
} from "@modelcontextprotocol/sdk/types.js"
|
||||||
|
|
||||||
const server = new Server({ name: "timeout", version: "1.0.0" }, { capabilities: { prompts: {}, tools: {} } })
|
const server = new Server(
|
||||||
|
{ name: "timeout", version: "1.0.0" },
|
||||||
|
{ capabilities: { prompts: {}, resources: {}, tools: {} } },
|
||||||
|
)
|
||||||
|
|
||||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||||
if (process.env.MCP_TIMEOUT_TARGET === "catalog") await Bun.sleep(100)
|
if (process.env.MCP_TIMEOUT_TARGET === "catalog") await Bun.sleep(100)
|
||||||
return { tools: [{ name: "slow", inputSchema: { type: "object" } }] }
|
return { tools: [{ name: "slow", inputSchema: { type: "object" } }] }
|
||||||
})
|
})
|
||||||
server.setRequestHandler(ListPromptsRequestSchema, () => Promise.resolve({ prompts: [{ name: "slow" }] }))
|
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 () => {
|
server.setRequestHandler(CallToolRequestSchema, async () => {
|
||||||
await Bun.sleep(100)
|
await Bun.sleep(100)
|
||||||
return { content: [] }
|
return { content: [] }
|
||||||
@@ -22,5 +33,9 @@ server.setRequestHandler(GetPromptRequestSchema, async () => {
|
|||||||
await Bun.sleep(100)
|
await Bun.sleep(100)
|
||||||
return { messages: [] }
|
return { messages: [] }
|
||||||
})
|
})
|
||||||
|
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
||||||
|
await Bun.sleep(100)
|
||||||
|
return { contents: [{ uri: request.params.uri, text: "slow" }] }
|
||||||
|
})
|
||||||
|
|
||||||
await server.connect(new StdioServerTransport())
|
await server.connect(new StdioServerTransport())
|
||||||
|
|||||||
@@ -14,15 +14,12 @@ export const emptyMcpLayer = Layer.succeed(
|
|||||||
instructions: () => Effect.succeed([]),
|
instructions: () => Effect.succeed([]),
|
||||||
prompts: () => Effect.succeed([]),
|
prompts: () => Effect.succeed([]),
|
||||||
prompt: () => Effect.succeed(undefined),
|
prompt: () => Effect.succeed(undefined),
|
||||||
resourceCatalog: () => Effect.succeed(new MCP.ResourceCatalog({ resources: [], templates: [] })),
|
resourceCatalog: () => Effect.succeed(MCP.ResourceCatalog.make({ resources: [], templates: [] })),
|
||||||
readResource: () => Effect.succeed(undefined),
|
readResource: () => Effect.succeed(undefined),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const emptyConfigLayer = Layer.succeed(
|
export const emptyConfigLayer = Layer.succeed(Config.Service, Config.Service.of({ entries: () => Effect.succeed([]) }))
|
||||||
Config.Service,
|
|
||||||
Config.Service.of({ entries: () => Effect.succeed([]) }),
|
|
||||||
)
|
|
||||||
|
|
||||||
export const testLocationLayer = Layer.succeed(
|
export const testLocationLayer = Layer.succeed(
|
||||||
Location.Service,
|
Location.Service,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import path from "path"
|
|||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { Config } from "@opencode-ai/schema/config"
|
import { Config } from "@opencode-ai/schema/config"
|
||||||
import { Plugin } from "@opencode-ai/schema/plugin"
|
import { Plugin } from "@opencode-ai/schema/plugin"
|
||||||
import { Context, DateTime, Effect, Equal, Hash, Schema, Stream } from "effect"
|
import { Context, DateTime, Effect, Equal, Hash, RcMap, Schema, Stream } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||||
import { Catalog } from "@opencode-ai/core/catalog"
|
import { Catalog } from "@opencode-ai/core/catalog"
|
||||||
@@ -12,6 +12,7 @@ import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
|||||||
import { LocationServiceMap } from "@opencode-ai/core/location-services"
|
import { LocationServiceMap } from "@opencode-ai/core/location-services"
|
||||||
import { Location } from "@opencode-ai/core/location"
|
import { Location } from "@opencode-ai/core/location"
|
||||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||||
|
import { SdkPlugins } from "@opencode-ai/core/plugin/sdk"
|
||||||
import { PluginSupervisor } from "@opencode-ai/core/plugin/supervisor"
|
import { PluginSupervisor } from "@opencode-ai/core/plugin/supervisor"
|
||||||
import { ModelV2 } from "@opencode-ai/core/model"
|
import { ModelV2 } from "@opencode-ai/core/model"
|
||||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||||
@@ -28,8 +29,43 @@ import { Reference } from "../src/reference"
|
|||||||
import { ToolRegistry } from "../src/tool/registry"
|
import { ToolRegistry } from "../src/tool/registry"
|
||||||
|
|
||||||
const it = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, LocationServiceMap.node])))
|
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", () => {
|
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", () =>
|
it.live("applies ordered plugin config operations during boot", () =>
|
||||||
Effect.acquireRelease(
|
Effect.acquireRelease(
|
||||||
Effect.promise(() => tmpdir()),
|
Effect.promise(() => tmpdir()),
|
||||||
@@ -172,6 +208,36 @@ 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", () =>
|
it.live("isolates catalog state by location", () =>
|
||||||
Effect.acquireRelease(
|
Effect.acquireRelease(
|
||||||
Effect.promise(() => Promise.all([tmpdir(), tmpdir()])),
|
Effect.promise(() => Promise.all([tmpdir(), tmpdir()])),
|
||||||
|
|||||||
@@ -3,26 +3,165 @@ import { describe, expect, test } from "bun:test"
|
|||||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
|
||||||
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"
|
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"
|
||||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js"
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js"
|
||||||
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js"
|
||||||
|
import {
|
||||||
|
CallToolRequestSchema,
|
||||||
|
ListResourcesRequestSchema,
|
||||||
|
ListResourceTemplatesRequestSchema,
|
||||||
|
ListToolsRequestSchema,
|
||||||
|
ReadResourceRequestSchema,
|
||||||
|
} from "@modelcontextprotocol/sdk/types.js"
|
||||||
import { ConfigMCP } from "@opencode-ai/core/config/mcp"
|
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 { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
import { EventV2 } from "@opencode-ai/core/event"
|
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 { MCP } from "@opencode-ai/core/mcp/index"
|
||||||
import { MCPClient } from "@opencode-ai/core/mcp/client"
|
import { MCPClient } from "@opencode-ai/core/mcp/client"
|
||||||
import { PermissionV2 } from "@opencode-ai/core/permission"
|
import { PermissionV2 } from "@opencode-ai/core/permission"
|
||||||
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
import { SessionV2 } from "@opencode-ai/core/session"
|
import { SessionV2 } from "@opencode-ai/core/session"
|
||||||
import { McpTool } from "@opencode-ai/core/tool/mcp"
|
import { McpTool } from "@opencode-ai/core/tool/mcp"
|
||||||
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
||||||
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
|
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
|
||||||
import { Deferred, Effect, Fiber, Layer, Stream } from "effect"
|
import { Deferred, Effect, Exit, Fiber, Layer, Stream } from "effect"
|
||||||
import { testEffect } from "./lib/effect"
|
import { testEffect } from "./lib/effect"
|
||||||
|
import { location } from "./fixture/location"
|
||||||
import { settleTool, toolDefinitions, toolIdentity, waitForTool } from "./lib/tool"
|
import { settleTool, toolDefinitions, toolIdentity, waitForTool } from "./lib/tool"
|
||||||
|
|
||||||
let assertion: Deferred.Deferred<PermissionV2.AssertInput> | undefined
|
let assertion: Deferred.Deferred<PermissionV2.AssertInput> | undefined
|
||||||
let decision: Effect.Effect<void, PermissionV2.Error> = Effect.void
|
let decision: Effect.Effect<void, PermissionV2.Error> = Effect.void
|
||||||
let calls = 0
|
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, {
|
const mcp = Layer.mock(MCP.Service, {
|
||||||
tools: () =>
|
tools: () =>
|
||||||
Effect.succeed([
|
Effect.succeed([
|
||||||
@@ -241,6 +380,163 @@ test("applies the configured MCP execution timeout to prompts", async () => {
|
|||||||
await expect(result).rejects.toThrow("Request timed out")
|
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", () =>
|
it.effect("advertises MCP output schemas to Code Mode", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const registry = yield* ToolRegistry.Service
|
const registry = yield* ToolRegistry.Service
|
||||||
|
|||||||
@@ -52,6 +52,15 @@ 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", () => {
|
test("compaction describes tool media without embedding base64", () => {
|
||||||
const base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB"
|
const base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB"
|
||||||
const serialized = SessionCompaction.serializeToolContent([
|
const serialized = SessionCompaction.serializeToolContent([
|
||||||
@@ -74,13 +83,14 @@ test("compaction prompt requires the checkpoint headings in order", () => {
|
|||||||
"## Objective",
|
"## Objective",
|
||||||
"## Important Details",
|
"## Important Details",
|
||||||
"## Work State",
|
"## Work State",
|
||||||
|
"### Completed",
|
||||||
|
"### Active",
|
||||||
|
"### Blocked",
|
||||||
"## Next Move",
|
"## Next Move",
|
||||||
|
"## Relevant Files",
|
||||||
])
|
])
|
||||||
expect(prompt).toContain("one or two brief sentences")
|
expect(prompt).toContain("one or two brief sentences")
|
||||||
expect(prompt).toContain("constraints/preferences, decisions and why")
|
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("immediate concrete action")
|
||||||
expect(prompt).toContain("next action if known")
|
expect(prompt).toContain("next action if known")
|
||||||
expect(prompt).toContain("Keep every section, even when empty.")
|
expect(prompt).toContain("Keep every section, even when empty.")
|
||||||
|
|||||||
@@ -225,9 +225,17 @@ describe("SessionV2.create", () => {
|
|||||||
promotedSeq: 2,
|
promotedSeq: 2,
|
||||||
})
|
})
|
||||||
|
|
||||||
yield* session.prompt({ sessionID: parent.id, prompt: PromptInput.Prompt.make({ text: "Parent changed" }), resume: false })
|
yield* session.prompt({
|
||||||
|
sessionID: parent.id,
|
||||||
|
prompt: PromptInput.Prompt.make({ text: "Parent changed" }),
|
||||||
|
resume: false,
|
||||||
|
})
|
||||||
yield* SessionInput.promoteSteers(db, events, parent.id)
|
yield* SessionInput.promoteSteers(db, events, parent.id)
|
||||||
yield* session.prompt({ sessionID: forked.id, prompt: PromptInput.Prompt.make({ text: "Child continues" }), resume: false })
|
yield* session.prompt({
|
||||||
|
sessionID: forked.id,
|
||||||
|
prompt: PromptInput.Prompt.make({ text: "Child continues" }),
|
||||||
|
resume: false,
|
||||||
|
})
|
||||||
yield* SessionInput.promoteSteers(db, events, forked.id)
|
yield* SessionInput.promoteSteers(db, events, forked.id)
|
||||||
|
|
||||||
expect((yield* session.context(parent.id)).map((message) => message.type)).toEqual(["user", "synthetic", "user"])
|
expect((yield* session.context(parent.id)).map((message) => message.type)).toEqual(["user", "synthetic", "user"])
|
||||||
@@ -260,8 +268,25 @@ describe("SessionV2.create", () => {
|
|||||||
resume: false,
|
resume: false,
|
||||||
})
|
})
|
||||||
yield* SessionInput.promoteSteers(db, events, parent.id)
|
yield* SessionInput.promoteSteers(db, events, parent.id)
|
||||||
|
const assistantMessageID = SessionMessage.ID.create()
|
||||||
|
const model = ModelV2.Ref.make({ id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") })
|
||||||
|
yield* events.publish(SessionEvent.Step.Started, {
|
||||||
|
sessionID: parent.id,
|
||||||
|
assistantMessageID,
|
||||||
|
agent: "build",
|
||||||
|
model,
|
||||||
|
})
|
||||||
|
yield* events.publish(SessionEvent.Step.Ended, {
|
||||||
|
sessionID: parent.id,
|
||||||
|
assistantMessageID,
|
||||||
|
finish: "stop",
|
||||||
|
cost: 0.75,
|
||||||
|
tokens: { input: 6, output: 3, reasoning: 1, cache: { read: 2, write: 1 } },
|
||||||
|
})
|
||||||
|
|
||||||
const forked = yield* session.fork({ sessionID: parent.id, messageID: second.id })
|
const forked = yield* session.fork({ sessionID: parent.id, messageID: second.id })
|
||||||
|
const beforeFirst = yield* session.fork({ sessionID: parent.id, messageID: first.id })
|
||||||
|
const complete = yield* session.fork({ sessionID: parent.id })
|
||||||
|
|
||||||
const context = yield* session.context(forked.id)
|
const context = yield* session.context(forked.id)
|
||||||
const history = Array.from(yield* Stream.runCollect(logEvents(session, forked.id)))
|
const history = Array.from(yield* Stream.runCollect(logEvents(session, forked.id)))
|
||||||
@@ -269,6 +294,13 @@ describe("SessionV2.create", () => {
|
|||||||
expect(context).toMatchObject([{ text: "First" }])
|
expect(context).toMatchObject([{ text: "First" }])
|
||||||
expect(context[0]?.id).not.toBe(first.id)
|
expect(context[0]?.id).not.toBe(first.id)
|
||||||
expect(history[0]).toMatchObject({ data: { from: second.id } })
|
expect(history[0]).toMatchObject({ data: { from: second.id } })
|
||||||
|
expect(forked).toMatchObject({ cost: 0, tokens: { input: 0, output: 0, reasoning: 0 } })
|
||||||
|
expect(yield* session.context(beforeFirst.id)).toEqual([])
|
||||||
|
expect(beforeFirst).toMatchObject({ cost: 0, tokens: { input: 0, output: 0, reasoning: 0 } })
|
||||||
|
expect(complete).toMatchObject({
|
||||||
|
cost: 0,
|
||||||
|
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -375,7 +407,11 @@ describe("SessionV2.create", () => {
|
|||||||
const events = yield* EventV2.Service
|
const events = yield* EventV2.Service
|
||||||
const { db } = yield* Database.Service
|
const { db } = yield* Database.Service
|
||||||
const created = yield* session.create({ location })
|
const created = yield* session.create({ location })
|
||||||
yield* session.prompt({ sessionID: created.id, prompt: PromptInput.Prompt.make({ text: "Hello" }), resume: false })
|
yield* session.prompt({
|
||||||
|
sessionID: created.id,
|
||||||
|
prompt: PromptInput.Prompt.make({ text: "Hello" }),
|
||||||
|
resume: false,
|
||||||
|
})
|
||||||
yield* SessionInput.promoteSteers(db, events, created.id)
|
yield* SessionInput.promoteSteers(db, events, created.id)
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { DateTime, Effect, Schema } from "effect"
|
import { DateTime, Effect, Fiber, Option, Schema, Stream } from "effect"
|
||||||
import { asc, eq } from "drizzle-orm"
|
import { asc, eq } from "drizzle-orm"
|
||||||
import { Database } from "@opencode-ai/core/database/database"
|
import { Database } from "@opencode-ai/core/database/database"
|
||||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
@@ -41,12 +41,15 @@ const assistantRow = (
|
|||||||
id: SessionMessage.ID,
|
id: SessionMessage.ID,
|
||||||
seq: number,
|
seq: number,
|
||||||
time: { created: DateTime.Utc; completed?: DateTime.Utc } = { created },
|
time: { created: DateTime.Utc; completed?: DateTime.Utc } = { created },
|
||||||
|
usage?: Pick<SessionMessage.Assistant, "cost" | "tokens">,
|
||||||
) => {
|
) => {
|
||||||
const {
|
const {
|
||||||
id: _,
|
id: _,
|
||||||
type,
|
type,
|
||||||
...data
|
...data
|
||||||
} = encodeMessage(SessionMessage.Assistant.make({ id, type: "assistant", agent: "build", model, content: [], time }))
|
} = encodeMessage(
|
||||||
|
SessionMessage.Assistant.make({ id, type: "assistant", agent: "build", model, content: [], time, ...usage }),
|
||||||
|
)
|
||||||
return { id, session_id: sessionID, type, seq, time_created: DateTime.toEpochMillis(time.created), data }
|
return { id, session_id: sessionID, type, seq, time_created: DateTime.toEpochMillis(time.created), data }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,6 +70,12 @@ describe("SessionProjector", () => {
|
|||||||
directory: "/project",
|
directory: "/project",
|
||||||
title: "test",
|
title: "test",
|
||||||
version: "test",
|
version: "test",
|
||||||
|
cost: 1.25,
|
||||||
|
tokens_input: 10,
|
||||||
|
tokens_output: 4,
|
||||||
|
tokens_reasoning: 2,
|
||||||
|
tokens_cache_read: 3,
|
||||||
|
tokens_cache_write: 1,
|
||||||
})
|
})
|
||||||
.run()
|
.run()
|
||||||
const boundary = SessionMessage.ID.make("msg_boundary")
|
const boundary = SessionMessage.ID.make("msg_boundary")
|
||||||
@@ -75,8 +84,24 @@ describe("SessionProjector", () => {
|
|||||||
.insert(SessionMessageTable)
|
.insert(SessionMessageTable)
|
||||||
.values([
|
.values([
|
||||||
assistantRow(earlier, 0),
|
assistantRow(earlier, 0),
|
||||||
assistantRow(boundary, 1),
|
assistantRow(
|
||||||
assistantRow(SessionMessage.ID.make("msg_later"), 2),
|
boundary,
|
||||||
|
1,
|
||||||
|
{ created },
|
||||||
|
{
|
||||||
|
cost: 0.5,
|
||||||
|
tokens: { input: 4, output: 1, reasoning: 1, cache: { read: 1, write: 0 } },
|
||||||
|
},
|
||||||
|
),
|
||||||
|
assistantRow(
|
||||||
|
SessionMessage.ID.make("msg_later"),
|
||||||
|
2,
|
||||||
|
{ created },
|
||||||
|
{
|
||||||
|
cost: 0.75,
|
||||||
|
tokens: { input: 6, output: 3, reasoning: 1, cache: { read: 2, write: 1 } },
|
||||||
|
},
|
||||||
|
),
|
||||||
])
|
])
|
||||||
.run()
|
.run()
|
||||||
yield* db
|
yield* db
|
||||||
@@ -106,6 +131,14 @@ describe("SessionProjector", () => {
|
|||||||
expect(
|
expect(
|
||||||
(yield* db.select({ id: SessionMessageTable.id }).from(SessionMessageTable).all()).map((row) => row.id),
|
(yield* db.select({ id: SessionMessageTable.id }).from(SessionMessageTable).all()).map((row) => row.id),
|
||||||
).toEqual([earlier])
|
).toEqual([earlier])
|
||||||
|
expect(yield* db.select().from(SessionTable).where(eq(SessionTable.id, sessionID)).get()).toMatchObject({
|
||||||
|
cost: 1.25,
|
||||||
|
tokens_input: 10,
|
||||||
|
tokens_output: 4,
|
||||||
|
tokens_reasoning: 2,
|
||||||
|
tokens_cache_read: 3,
|
||||||
|
tokens_cache_write: 1,
|
||||||
|
})
|
||||||
// A committed revert resets the context checkpoint so the next turn re-initializes.
|
// A committed revert resets the context checkpoint so the next turn re-initializes.
|
||||||
expect(yield* db.select().from(InstructionCheckpointTable).get().pipe(Effect.orDie)).toBeUndefined()
|
expect(yield* db.select().from(InstructionCheckpointTable).get().pipe(Effect.orDie)).toBeUndefined()
|
||||||
}),
|
}),
|
||||||
@@ -534,12 +567,15 @@ describe("SessionProjector", () => {
|
|||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
|
|
||||||
const service = yield* EventV2.Service
|
const service = yield* EventV2.Service
|
||||||
|
const usageUpdated = yield* service
|
||||||
|
.subscribe(SessionEvent.UsageUpdated)
|
||||||
|
.pipe(Stream.runHead, Effect.forkScoped({ startImmediately: true }))
|
||||||
yield* service.publish(SessionEvent.Step.Ended, {
|
yield* service.publish(SessionEvent.Step.Ended, {
|
||||||
sessionID,
|
sessionID,
|
||||||
assistantMessageID: SessionMessage.ID.make("msg_assistant_2"),
|
assistantMessageID: SessionMessage.ID.make("msg_assistant_2"),
|
||||||
finish: "stop",
|
finish: "stop",
|
||||||
cost: 0,
|
cost: 1.25,
|
||||||
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
tokens: { input: 10, output: 4, reasoning: 2, cache: { read: 3, write: 1 } },
|
||||||
})
|
})
|
||||||
|
|
||||||
const rows = yield* db
|
const rows = yield* db
|
||||||
@@ -556,8 +592,25 @@ describe("SessionProjector", () => {
|
|||||||
expect(messages[1]).toMatchObject({
|
expect(messages[1]).toMatchObject({
|
||||||
type: "assistant",
|
type: "assistant",
|
||||||
finish: "stop",
|
finish: "stop",
|
||||||
|
cost: 1.25,
|
||||||
|
tokens: { input: 10, output: 4, reasoning: 2, cache: { read: 3, write: 1 } },
|
||||||
time: { completed: DateTime.makeUnsafe(0) },
|
time: { completed: DateTime.makeUnsafe(0) },
|
||||||
})
|
})
|
||||||
|
expect(
|
||||||
|
yield* db.select().from(SessionTable).where(eq(SessionTable.id, sessionID)).get().pipe(Effect.orDie),
|
||||||
|
).toMatchObject({
|
||||||
|
cost: 1.25,
|
||||||
|
tokens_input: 10,
|
||||||
|
tokens_output: 4,
|
||||||
|
tokens_reasoning: 2,
|
||||||
|
tokens_cache_read: 3,
|
||||||
|
tokens_cache_write: 1,
|
||||||
|
})
|
||||||
|
expect(Option.getOrThrow(yield* Fiber.join(usageUpdated)).data).toEqual({
|
||||||
|
sessionID,
|
||||||
|
cost: 1.25,
|
||||||
|
tokens: { input: 10, output: 4, reasoning: 2, cache: { read: 3, write: 1 } },
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -461,7 +461,7 @@ Recent work
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
test("drops provider-native continuation metadata from failed assistant turns", () => {
|
test("lowers failed assistant reasoning to text", () => {
|
||||||
const messages = toLLMMessages(
|
const messages = toLLMMessages(
|
||||||
[
|
[
|
||||||
SessionMessage.Assistant.make({
|
SessionMessage.Assistant.make({
|
||||||
@@ -501,7 +501,7 @@ Recent work
|
|||||||
)
|
)
|
||||||
|
|
||||||
expect(messages[0]?.content).toEqual([
|
expect(messages[0]?.content).toEqual([
|
||||||
{ type: "reasoning", text: "Partial thought", providerMetadata: undefined },
|
{ type: "text", text: "Partial thought" },
|
||||||
{
|
{
|
||||||
type: "tool-call",
|
type: "tool-call",
|
||||||
id: "hosted-failed",
|
id: "hosted-failed",
|
||||||
|
|||||||
@@ -151,15 +151,39 @@ test("step finish records settlement without publishing step ended", async () =>
|
|||||||
test("content-filter finish retains failure evidence until step closeout", async () => {
|
test("content-filter finish retains failure evidence until step closeout", async () => {
|
||||||
const { published, publisher } = capture()
|
const { published, publisher } = capture()
|
||||||
await Effect.runPromise(publisher.publish(LLMEvent.stepStart({ index: 0 })))
|
await Effect.runPromise(publisher.publish(LLMEvent.stepStart({ index: 0 })))
|
||||||
await Effect.runPromise(publisher.publish(LLMEvent.stepFinish({ index: 0, reason: "content-filter" })))
|
await Effect.runPromise(
|
||||||
|
publisher.publish(
|
||||||
|
LLMEvent.stepFinish({
|
||||||
|
index: 0,
|
||||||
|
reason: "content-filter",
|
||||||
|
usage: {
|
||||||
|
nonCachedInputTokens: 8,
|
||||||
|
outputTokens: 3,
|
||||||
|
reasoningTokens: 1,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
expect(published.map((event) => event.type)).toEqual(["session.step.started.1"])
|
expect(published.map((event) => event.type)).toEqual(["session.step.started.1"])
|
||||||
await Effect.runPromise(publisher.publishStepFailure())
|
const settlement = publisher.stepSettlement()
|
||||||
|
expect(settlement).toMatchObject({
|
||||||
|
finish: "content-filter",
|
||||||
|
tokens: { input: 8, output: 2, reasoning: 1 },
|
||||||
|
})
|
||||||
|
if (!settlement) throw new Error("Expected content-filter settlement")
|
||||||
|
await Effect.runPromise(
|
||||||
|
publisher.publishStepFailure({
|
||||||
|
cost: 1.25,
|
||||||
|
tokens: settlement.tokens,
|
||||||
|
}),
|
||||||
|
)
|
||||||
expect(published.map((event) => event.type)).toEqual(["session.step.started.1", "session.step.failed.1"])
|
expect(published.map((event) => event.type)).toEqual(["session.step.started.1", "session.step.failed.1"])
|
||||||
expect(published.at(-1)?.data).toMatchObject({
|
expect(published.at(-1)?.data).toMatchObject({
|
||||||
error: { type: "provider.content-filter", message: "Provider blocked the response" },
|
error: { type: "provider.content-filter", message: "Provider blocked the response" },
|
||||||
|
cost: 1.25,
|
||||||
|
tokens: { input: 8, output: 2, reasoning: 1 },
|
||||||
})
|
})
|
||||||
expect(publisher.stepSettlement()).toBeUndefined()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("content-filter finish preserves partial streamed text and never ends the step successfully", async () => {
|
test("content-filter finish preserves partial streamed text and never ends the step successfully", async () => {
|
||||||
|
|||||||
@@ -30,27 +30,6 @@ const outputStore = Layer.mock(ToolOutputStore.Service, {
|
|||||||
})
|
})
|
||||||
const registryLayer = AppNodeBuilder.build(ToolRegistry.node, [[ToolOutputStore.node, outputStore]])
|
const registryLayer = AppNodeBuilder.build(ToolRegistry.node, [[ToolOutputStore.node, outputStore]])
|
||||||
const it = testEffect(registryLayer)
|
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 = {
|
const identity = {
|
||||||
agent: AgentV2.ID.make("build"),
|
agent: AgentV2.ID.make("build"),
|
||||||
assistantMessageID: SessionMessage.ID.make("msg_registry"),
|
assistantMessageID: SessionMessage.ID.make("msg_registry"),
|
||||||
@@ -74,50 +53,6 @@ const make = (permission?: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("ToolRegistry", () => {
|
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", () =>
|
it.effect("filters disabled tools with edit aliases and ordered wildcard precedence", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const service = yield* ToolRegistry.Service
|
const service = yield* ToolRegistry.Service
|
||||||
@@ -154,16 +89,16 @@ describe("ToolRegistry", () => {
|
|||||||
read: make(),
|
read: make(),
|
||||||
edit: make("edit"),
|
edit: make("edit"),
|
||||||
write: make("edit"),
|
write: make("edit"),
|
||||||
apply_patch: make("edit"),
|
patch: make("edit"),
|
||||||
})
|
})
|
||||||
const names = (model: ToolRegistry.MaterializeInput["model"]) =>
|
const names = (model: ToolRegistry.MaterializeInput["model"]) =>
|
||||||
service
|
service
|
||||||
.materialize({ model })
|
.materialize({ model })
|
||||||
.pipe(Effect.map((materialized) => materialized.definitions.map((tool) => tool.name)))
|
.pipe(Effect.map((materialized) => materialized.definitions.map((tool) => tool.name)))
|
||||||
|
|
||||||
expect(yield* names({ id: "gpt-5", provider: "openai" })).toEqual(["read", "apply_patch"])
|
expect(yield* names({ id: "gpt-5", provider: "openai" })).toEqual(["read", "patch"])
|
||||||
expect(yield* names({ id: "gpt-4o", provider: "opencode" })).toEqual(["read", "apply_patch"])
|
expect(yield* names({ id: "gpt-4o", provider: "opencode" })).toEqual(["read", "patch"])
|
||||||
expect(yield* names({ id: "computer-use-preview", provider: "openai" })).toEqual(["read", "apply_patch"])
|
expect(yield* names({ id: "computer-use-preview", provider: "openai" })).toEqual(["read", "patch"])
|
||||||
expect(yield* names({ id: "claude-sonnet-4", provider: "anthropic" })).toEqual(["read", "edit", "write"])
|
expect(yield* names({ id: "claude-sonnet-4", provider: "anthropic" })).toEqual(["read", "edit", "write"])
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, expect } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import {
|
import {
|
||||||
LLMClient,
|
LLMClient,
|
||||||
LLMError,
|
LLMError,
|
||||||
@@ -116,6 +116,28 @@ const recoveryModel = Model.make({
|
|||||||
provider: "fake",
|
provider: "fake",
|
||||||
route: OpenAIChat.route.with({ limits: { context: 20_000, output: 1_000 } }),
|
route: OpenAIChat.route.with({ limits: { context: 20_000, output: 1_000 } }),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("calculates step cost using the matching context tier", () => {
|
||||||
|
expect(
|
||||||
|
SessionRunnerLLM.calculateCost(
|
||||||
|
[
|
||||||
|
{ input: 1, output: 2, cache: { read: 0.1, write: 0.5 } },
|
||||||
|
{ tier: { type: "context", size: 100 }, input: 3, output: 4, cache: { read: 0.2, write: 0.6 } },
|
||||||
|
],
|
||||||
|
{ input: 80, output: 10, reasoning: 2, cache: { read: 20, write: 1 } },
|
||||||
|
),
|
||||||
|
).toBeCloseTo(0.0002926)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("does not apply an ineligible tier without base pricing", () => {
|
||||||
|
expect(
|
||||||
|
SessionRunnerLLM.calculateCost(
|
||||||
|
[{ tier: { type: "context", size: 100 }, input: 3, output: 4, cache: { read: 0.2, write: 0.6 } }],
|
||||||
|
{ input: 80, output: 10, reasoning: 2, cache: { read: 20, write: 0 } },
|
||||||
|
),
|
||||||
|
).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
const authorizations: Tool.Context[] = []
|
const authorizations: Tool.Context[] = []
|
||||||
const executions: string[] = []
|
const executions: string[] = []
|
||||||
const permission = Layer.succeed(
|
const permission = Layer.succeed(
|
||||||
@@ -1704,6 +1726,7 @@ describe("SessionRunnerLLM", () => {
|
|||||||
{
|
{
|
||||||
type: "assistant",
|
type: "assistant",
|
||||||
finish: "tool-calls",
|
finish: "tool-calls",
|
||||||
|
cost: 0,
|
||||||
tokens: { input: 8, output: 3, reasoning: 1, cache: { read: 2, write: 0 } },
|
tokens: { input: 8, output: 3, reasoning: 1, cache: { read: 2, write: 0 } },
|
||||||
content: [
|
content: [
|
||||||
{ type: "reasoning", text: "Think" },
|
{ type: "reasoning", text: "Think" },
|
||||||
@@ -3635,7 +3658,11 @@ describe("SessionRunnerLLM", () => {
|
|||||||
LLMEvent.stepStart({ index: 0 }),
|
LLMEvent.stepStart({ index: 0 }),
|
||||||
LLMEvent.textStart({ id: "partial" }),
|
LLMEvent.textStart({ id: "partial" }),
|
||||||
LLMEvent.textDelta({ id: "partial", text: "Partial" }),
|
LLMEvent.textDelta({ id: "partial", text: "Partial" }),
|
||||||
LLMEvent.stepFinish({ index: 0, reason: "content-filter" }),
|
LLMEvent.stepFinish({
|
||||||
|
index: 0,
|
||||||
|
reason: "content-filter",
|
||||||
|
usage: { nonCachedInputTokens: 8, outputTokens: 3, reasoningTokens: 1 },
|
||||||
|
}),
|
||||||
LLMEvent.finish({ reason: "content-filter" }),
|
LLMEvent.finish({ reason: "content-filter" }),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3646,9 +3673,15 @@ describe("SessionRunnerLLM", () => {
|
|||||||
type: "assistant",
|
type: "assistant",
|
||||||
finish: "error",
|
finish: "error",
|
||||||
error: { type: "provider.content-filter" },
|
error: { type: "provider.content-filter" },
|
||||||
|
cost: 0,
|
||||||
|
tokens: { input: 8, output: 2, reasoning: 1, cache: { read: 0, write: 0 } },
|
||||||
content: [{ type: "text", text: "Partial" }],
|
content: [{ type: "text", text: "Partial" }],
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
expect(yield* session.get(sessionID)).toMatchObject({
|
||||||
|
cost: 0,
|
||||||
|
tokens: { input: 8, output: 2, reasoning: 1, cache: { read: 0, write: 0 } },
|
||||||
|
})
|
||||||
expect(yield* recordedEventTypes(sessionID)).not.toContain("session.step.ended.1")
|
expect(yield* recordedEventTypes(sessionID)).not.toContain("session.step.ended.1")
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const applyPatchToolNode = makeLocationNode({
|
|||||||
deps: [ToolRegistry.toolsNode, LocationMutation.node, FileMutation.node, FSUtil.node, PermissionV2.node],
|
deps: [ToolRegistry.toolsNode, LocationMutation.node, FileMutation.node, FSUtil.node, PermissionV2.node],
|
||||||
})
|
})
|
||||||
|
|
||||||
const sessionID = SessionV2.ID.make("ses_apply_patch_tool_test")
|
const sessionID = SessionV2.ID.make("ses_patch_tool_test")
|
||||||
const assertions: PermissionV2.AssertInput[] = []
|
const assertions: PermissionV2.AssertInput[] = []
|
||||||
let denyAction: string | undefined
|
let denyAction: string | undefined
|
||||||
let failRemoveTarget: string | undefined
|
let failRemoveTarget: string | undefined
|
||||||
@@ -132,10 +132,10 @@ const withTool = <A, E, R>(directory: string, body: (registry: ToolRegistry.Inte
|
|||||||
const call = (patchText: string, id = "call-apply-patch") => ({
|
const call = (patchText: string, id = "call-apply-patch") => ({
|
||||||
sessionID,
|
sessionID,
|
||||||
...toolIdentity,
|
...toolIdentity,
|
||||||
call: { type: "tool-call" as const, id, name: "apply_patch", input: { patchText } },
|
call: { type: "tool-call" as const, id, name: "patch", input: { patchText } },
|
||||||
})
|
})
|
||||||
|
|
||||||
// apply_patch is only materialized for OpenAI/GPT models.
|
// patch is only materialized for OpenAI/GPT models.
|
||||||
const model = { id: "gpt-5", provider: "openai" }
|
const model = { id: "gpt-5", provider: "openai" }
|
||||||
|
|
||||||
const exists = (target: string) =>
|
const exists = (target: string) =>
|
||||||
@@ -162,7 +162,7 @@ describe("ApplyPatchTool", () => {
|
|||||||
withTool(tmp.path, (registry) =>
|
withTool(tmp.path, (registry) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
expect((yield* toolDefinitions(registry, undefined, model)).map((tool) => tool.name)).toEqual([
|
expect((yield* toolDefinitions(registry, undefined, model)).map((tool) => tool.name)).toEqual([
|
||||||
"apply_patch",
|
"patch",
|
||||||
])
|
])
|
||||||
const settled = yield* settleTool(
|
const settled = yield* settleTool(
|
||||||
registry,
|
registry,
|
||||||
@@ -241,7 +241,7 @@ describe("ApplyPatchTool", () => {
|
|||||||
),
|
),
|
||||||
model,
|
model,
|
||||||
),
|
),
|
||||||
).toEqual({ type: "error", value: "apply_patch moves are not supported yet" })
|
).toEqual({ type: "error", value: "patch moves are not supported yet" })
|
||||||
expect(yield* exists(path.join(tmp.path, "created.txt"))).toBe(false)
|
expect(yield* exists(path.join(tmp.path, "created.txt"))).toBe(false)
|
||||||
expect(assertions).toEqual([])
|
expect(assertions).toEqual([])
|
||||||
}),
|
}),
|
||||||
|
|||||||
+6
-14
@@ -17,7 +17,12 @@
|
|||||||
"tabs": [
|
"tabs": [
|
||||||
{
|
{
|
||||||
"tab": "Docs",
|
"tab": "Docs",
|
||||||
"pages": ["index", "config", "plugins", "troubleshooting"]
|
"groups": [
|
||||||
|
{
|
||||||
|
"group": "Get started",
|
||||||
|
"pages": ["index", "config", "plugins", "troubleshooting"]
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tab": "SDK",
|
"tab": "SDK",
|
||||||
@@ -39,19 +44,6 @@
|
|||||||
],
|
],
|
||||||
"global": {}
|
"global": {}
|
||||||
},
|
},
|
||||||
"navbar": {
|
|
||||||
"links": [
|
|
||||||
{
|
|
||||||
"label": "Discord",
|
|
||||||
"href": "https://opencode.ai/discord"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primary": {
|
|
||||||
"type": "button",
|
|
||||||
"label": "GitHub",
|
|
||||||
"href": "https://github.com/anomalyco/opencode"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"contextual": {
|
"contextual": {
|
||||||
"options": ["copy", "view", "chatgpt", "claude", "mcp", "cursor", "vscode"]
|
"options": ["copy", "view", "chatgpt", "claude", "mcp", "cursor", "vscode"]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const patterns = [
|
|||||||
/input is too long for requested model/i,
|
/input is too long for requested model/i,
|
||||||
/exceeds the context window/i,
|
/exceeds the context window/i,
|
||||||
/input token count.*exceeds the maximum/i,
|
/input token count.*exceeds the maximum/i,
|
||||||
|
/tokens in request more than max tokens allowed/i,
|
||||||
/maximum prompt length is \d+/i,
|
/maximum prompt length is \d+/i,
|
||||||
/reduce the length of the messages/i,
|
/reduce the length of the messages/i,
|
||||||
/maximum context length is \d+ tokens/i,
|
/maximum context length is \d+ tokens/i,
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -40,7 +40,6 @@ export class Service extends ConfigService.Service<Service>()("@opencode/Runtime
|
|||||||
enableExperimentalModels: bool("OPENCODE_ENABLE_EXPERIMENTAL_MODELS"),
|
enableExperimentalModels: bool("OPENCODE_ENABLE_EXPERIMENTAL_MODELS"),
|
||||||
enableQuestionTool: bool("OPENCODE_ENABLE_QUESTION_TOOL"),
|
enableQuestionTool: bool("OPENCODE_ENABLE_QUESTION_TOOL"),
|
||||||
experimentalReferences: enabledByExperimental("OPENCODE_EXPERIMENTAL_REFERENCES"),
|
experimentalReferences: enabledByExperimental("OPENCODE_EXPERIMENTAL_REFERENCES"),
|
||||||
experimentalBackgroundSubagents: enabledByExperimental("OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS"),
|
|
||||||
experimentalLspTy: bool("OPENCODE_EXPERIMENTAL_LSP_TY"),
|
experimentalLspTy: bool("OPENCODE_EXPERIMENTAL_LSP_TY"),
|
||||||
experimentalLspTool: enabledByExperimental("OPENCODE_EXPERIMENTAL_LSP_TOOL"),
|
experimentalLspTool: enabledByExperimental("OPENCODE_EXPERIMENTAL_LSP_TOOL"),
|
||||||
experimentalOxfmt: enabledByExperimental("OPENCODE_EXPERIMENTAL_OXFMT"),
|
experimentalOxfmt: enabledByExperimental("OPENCODE_EXPERIMENTAL_OXFMT"),
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { Agent } from "@/agent/agent"
|
|||||||
import { Job } from "@/job"
|
import { Job } from "@/job"
|
||||||
import { Config } from "@/config/config"
|
import { Config } from "@/config/config"
|
||||||
import { InstanceState } from "@/effect/instance-state"
|
import { InstanceState } from "@/effect/instance-state"
|
||||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
|
||||||
import { MCP } from "@/mcp"
|
import { MCP } from "@/mcp"
|
||||||
import { Project } from "@/project/project"
|
import { Project } from "@/project/project"
|
||||||
import { Session } from "@/session/session"
|
import { Session } from "@/session/session"
|
||||||
@@ -34,10 +33,9 @@ export const experimentalHandlers = HttpApiBuilder.group(InstanceHttpApi, "exper
|
|||||||
const worktreeSvc = yield* Worktree.Service
|
const worktreeSvc = yield* Worktree.Service
|
||||||
const sessions = yield* Session.Service
|
const sessions = yield* Session.Service
|
||||||
const jobs = yield* Job.Service
|
const jobs = yield* Job.Service
|
||||||
const flags = yield* RuntimeFlags.Service
|
|
||||||
|
|
||||||
const capabilities = Effect.fn("ExperimentalHttpApi.capabilities")(function* () {
|
const capabilities = Effect.fn("ExperimentalHttpApi.capabilities")(function* () {
|
||||||
return { backgroundSubagents: flags.experimentalBackgroundSubagents }
|
return { backgroundSubagents: true }
|
||||||
})
|
})
|
||||||
|
|
||||||
const getConsole = Effect.fn("ExperimentalHttpApi.console")(function* () {
|
const getConsole = Effect.fn("ExperimentalHttpApi.console")(function* () {
|
||||||
@@ -159,7 +157,6 @@ export const experimentalHandlers = HttpApiBuilder.group(InstanceHttpApi, "exper
|
|||||||
const sessionBackground = Effect.fn("ExperimentalHttpApi.sessionBackground")(function* (ctx: {
|
const sessionBackground = Effect.fn("ExperimentalHttpApi.sessionBackground")(function* (ctx: {
|
||||||
params: { sessionID: SessionID }
|
params: { sessionID: SessionID }
|
||||||
}) {
|
}) {
|
||||||
if (!flags.experimentalBackgroundSubagents) return false
|
|
||||||
return (yield* jobs.backgroundAll({ sessionID: ctx.params.sessionID, type: "task" })).length > 0
|
return (yield* jobs.backgroundAll({ sessionID: ctx.params.sessionID, type: "task" })).length > 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import * as Tool from "./tool"
|
import * as Tool from "./tool"
|
||||||
import DESCRIPTION from "./task.txt"
|
import DESCRIPTION from "./task.txt"
|
||||||
import { ToolJsonSchema } from "./json-schema"
|
|
||||||
import { SessionV1 } from "@opencode-ai/core/v1/session"
|
import { SessionV1 } from "@opencode-ai/core/v1/session"
|
||||||
import { Job } from "@/job"
|
import { Job } from "@/job"
|
||||||
import { Session } from "@/session/session"
|
import { Session } from "@/session/session"
|
||||||
@@ -12,7 +11,6 @@ import type { SessionPrompt } from "../session/prompt"
|
|||||||
import { Config } from "@/config/config"
|
import { Config } from "@/config/config"
|
||||||
import { Effect, Exit, Schema, Scope } from "effect"
|
import { Effect, Exit, Schema, Scope } from "effect"
|
||||||
import { EffectBridge } from "@/effect/bridge"
|
import { EffectBridge } from "@/effect/bridge"
|
||||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
|
||||||
import { Database } from "@opencode-ai/core/database/database"
|
import { Database } from "@opencode-ai/core/database/database"
|
||||||
|
|
||||||
export interface TaskPromptOps {
|
export interface TaskPromptOps {
|
||||||
@@ -51,8 +49,6 @@ const BaseParameterFields = {
|
|||||||
command: Schema.optional(Schema.String).annotate({ description: "The command that triggered this task" }),
|
command: Schema.optional(Schema.String).annotate({ description: "The command that triggered this task" }),
|
||||||
}
|
}
|
||||||
|
|
||||||
const BaseParameters = Schema.Struct(BaseParameterFields)
|
|
||||||
|
|
||||||
export const Parameters = Schema.Struct({
|
export const Parameters = Schema.Struct({
|
||||||
...BaseParameterFields,
|
...BaseParameterFields,
|
||||||
background: Schema.optional(Schema.Boolean).annotate({
|
background: Schema.optional(Schema.Boolean).annotate({
|
||||||
@@ -86,7 +82,6 @@ export const TaskTool = Tool.define(
|
|||||||
const config = yield* Config.Service
|
const config = yield* Config.Service
|
||||||
const sessions = yield* Session.Service
|
const sessions = yield* Session.Service
|
||||||
const scope = yield* Scope.Scope
|
const scope = yield* Scope.Scope
|
||||||
const flags = yield* RuntimeFlags.Service
|
|
||||||
const database = yield* Database.Service
|
const database = yield* Database.Service
|
||||||
|
|
||||||
const run = Effect.fn("TaskTool.execute")(function* (
|
const run = Effect.fn("TaskTool.execute")(function* (
|
||||||
@@ -95,11 +90,6 @@ export const TaskTool = Tool.define(
|
|||||||
) {
|
) {
|
||||||
const cfg = yield* config.get()
|
const cfg = yield* config.get()
|
||||||
const runInBackground = params.background === true
|
const runInBackground = params.background === true
|
||||||
if (runInBackground && !flags.experimentalBackgroundSubagents) {
|
|
||||||
return yield* Effect.fail(
|
|
||||||
new Error("Background subagents require OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ctx.extra?.bypassAgentCheck) {
|
if (!ctx.extra?.bypassAgentCheck) {
|
||||||
yield* ctx.ask({
|
yield* ctx.ask({
|
||||||
@@ -333,11 +323,8 @@ export const TaskTool = Tool.define(
|
|||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
description: flags.experimentalBackgroundSubagents
|
description: [DESCRIPTION, BACKGROUND_DESCRIPTION].join("\n\n"),
|
||||||
? [DESCRIPTION, BACKGROUND_DESCRIPTION].join("\n\n")
|
|
||||||
: DESCRIPTION,
|
|
||||||
parameters: Parameters,
|
parameters: Parameters,
|
||||||
jsonSchema: flags.experimentalBackgroundSubagents ? undefined : ToolJsonSchema.fromSchema(BaseParameters),
|
|
||||||
execute: (params: Schema.Schema.Type<typeof Parameters>, ctx: Tool.Context) =>
|
execute: (params: Schema.Schema.Type<typeof Parameters>, ctx: Tool.Context) =>
|
||||||
run(params, ctx).pipe(Effect.orDie),
|
run(params, ctx).pipe(Effect.orDie),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,7 +161,6 @@ async function renderFooter(
|
|||||||
currentModel?: RunInput["model"]
|
currentModel?: RunInput["model"]
|
||||||
currentVariant?: string
|
currentVariant?: string
|
||||||
subagents?: FooterSubagentState
|
subagents?: FooterSubagentState
|
||||||
backgroundSubagents?: boolean
|
|
||||||
width?: number
|
width?: number
|
||||||
height?: number
|
height?: number
|
||||||
state?: Partial<FooterState>
|
state?: Partial<FooterState>
|
||||||
@@ -199,7 +198,6 @@ async function renderFooter(
|
|||||||
subagent={subagents}
|
subagent={subagents}
|
||||||
theme={input.theme ?? (() => RUN_THEME_FALLBACK)}
|
theme={input.theme ?? (() => RUN_THEME_FALLBACK)}
|
||||||
tuiConfig={config}
|
tuiConfig={config}
|
||||||
backgroundSubagents={input.backgroundSubagents ?? true}
|
|
||||||
agent="opencode"
|
agent="opencode"
|
||||||
onSubmit={input.onSubmit ?? (() => true)}
|
onSubmit={input.onSubmit ?? (() => true)}
|
||||||
onPermissionReply={() => {}}
|
onPermissionReply={() => {}}
|
||||||
@@ -998,7 +996,6 @@ test("direct footer shows editable prompts and additional queued work while runn
|
|||||||
]}
|
]}
|
||||||
theme={() => RUN_THEME_FALLBACK}
|
theme={() => RUN_THEME_FALLBACK}
|
||||||
tuiConfig={tuiConfig}
|
tuiConfig={tuiConfig}
|
||||||
backgroundSubagents={true}
|
|
||||||
agent="opencode"
|
agent="opencode"
|
||||||
onSubmit={() => true}
|
onSubmit={() => true}
|
||||||
onPermissionReply={() => {}}
|
onPermissionReply={() => {}}
|
||||||
@@ -1071,7 +1068,7 @@ test("direct footer shows editable prompts and additional queued work while runn
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
test("direct footer separates a lone context hint from model and command hint", async () => {
|
test("direct footer always offers backgrounding for a foreground subagent", async () => {
|
||||||
const app = await renderFooter({
|
const app = await renderFooter({
|
||||||
providers: [provider()],
|
providers: [provider()],
|
||||||
currentModel: { providerID: "opencode", modelID: "gpt-5" },
|
currentModel: { providerID: "opencode", modelID: "gpt-5" },
|
||||||
@@ -1082,7 +1079,6 @@ test("direct footer separates a lone context hint from model and command hint",
|
|||||||
permissions: [],
|
permissions: [],
|
||||||
questions: [],
|
questions: [],
|
||||||
},
|
},
|
||||||
backgroundSubagents: false,
|
|
||||||
width: 160,
|
width: 160,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1091,8 +1087,8 @@ test("direct footer separates a lone context hint from model and command hint",
|
|||||||
const frame = app.captureCharFrame()
|
const frame = app.captureCharFrame()
|
||||||
|
|
||||||
expect(frame).toContain("GPT-5")
|
expect(frame).toContain("GPT-5")
|
||||||
expect(frame).toContain("xhigh · ctrl+x down subagents · ctrl+p cmd")
|
expect(frame).toContain("xhigh · ctrl+b background · ctrl+x down subagents · ctrl+p cmd")
|
||||||
expect(frame).not.toContain("ctrl+b background")
|
expect(frame).toContain("ctrl+b background")
|
||||||
expect(frame).not.toContain("queued")
|
expect(frame).not.toContain("queued")
|
||||||
} finally {
|
} finally {
|
||||||
app.cleanup()
|
app.cleanup()
|
||||||
@@ -1110,7 +1106,6 @@ test("direct footer hides the subagent hint when only completed subagents remain
|
|||||||
permissions: [],
|
permissions: [],
|
||||||
questions: [],
|
questions: [],
|
||||||
},
|
},
|
||||||
backgroundSubagents: false,
|
|
||||||
width: 160,
|
width: 160,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -134,7 +134,6 @@ describe("run interactive runtime", () => {
|
|||||||
variant: undefined,
|
variant: undefined,
|
||||||
files: [],
|
files: [],
|
||||||
thinking: false,
|
thinking: false,
|
||||||
backgroundSubagents: false,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
createRuntimeLifecycle: async () => {
|
createRuntimeLifecycle: async () => {
|
||||||
@@ -233,7 +232,6 @@ describe("run interactive runtime", () => {
|
|||||||
variant: undefined,
|
variant: undefined,
|
||||||
files: [],
|
files: [],
|
||||||
thinking: false,
|
thinking: false,
|
||||||
backgroundSubagents: false,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
createRuntimeLifecycle: async () => {
|
createRuntimeLifecycle: async () => {
|
||||||
@@ -406,7 +404,6 @@ describe("run interactive runtime", () => {
|
|||||||
variant: undefined,
|
variant: undefined,
|
||||||
files: [],
|
files: [],
|
||||||
thinking: true,
|
thinking: true,
|
||||||
backgroundSubagents: false,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
createRuntimeLifecycle: async (input) => {
|
createRuntimeLifecycle: async (input) => {
|
||||||
@@ -496,7 +493,6 @@ describe("run interactive runtime", () => {
|
|||||||
variant: undefined,
|
variant: undefined,
|
||||||
files: [],
|
files: [],
|
||||||
thinking: false,
|
thinking: false,
|
||||||
backgroundSubagents: false,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
createRuntimeLifecycle: async (input) => {
|
createRuntimeLifecycle: async (input) => {
|
||||||
@@ -557,7 +553,6 @@ describe("run interactive runtime", () => {
|
|||||||
variant: undefined,
|
variant: undefined,
|
||||||
files: [],
|
files: [],
|
||||||
thinking: false,
|
thinking: false,
|
||||||
backgroundSubagents: false,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
createRuntimeLifecycle: async () => {
|
createRuntimeLifecycle: async () => {
|
||||||
@@ -603,7 +598,6 @@ describe("run interactive runtime", () => {
|
|||||||
variant: undefined,
|
variant: undefined,
|
||||||
files: [],
|
files: [],
|
||||||
thinking: false,
|
thinking: false,
|
||||||
backgroundSubagents: false,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
createRuntimeLifecycle: async (input) => {
|
createRuntimeLifecycle: async (input) => {
|
||||||
@@ -716,7 +710,6 @@ describe("run interactive runtime", () => {
|
|||||||
variant: "low",
|
variant: "low",
|
||||||
files: [],
|
files: [],
|
||||||
thinking: false,
|
thinking: false,
|
||||||
backgroundSubagents: false,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
createRuntimeLifecycle: async (input) => {
|
createRuntimeLifecycle: async (input) => {
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ describe("RuntimeFlags", () => {
|
|||||||
expect(flags.enableExperimentalModels).toBe(true)
|
expect(flags.enableExperimentalModels).toBe(true)
|
||||||
expect(flags.enableQuestionTool).toBe(true)
|
expect(flags.enableQuestionTool).toBe(true)
|
||||||
expect(flags.experimentalReferences).toBe(true)
|
expect(flags.experimentalReferences).toBe(true)
|
||||||
expect(flags.experimentalBackgroundSubagents).toBe(true)
|
|
||||||
expect(flags.experimentalLspTy).toBe(false)
|
expect(flags.experimentalLspTy).toBe(false)
|
||||||
expect(flags.experimentalLspTool).toBe(true)
|
expect(flags.experimentalLspTool).toBe(true)
|
||||||
expect(flags.experimentalOxfmt).toBe(true)
|
expect(flags.experimentalOxfmt).toBe(true)
|
||||||
|
|||||||
@@ -579,8 +579,10 @@ const scenarios: Scenario[] = [
|
|||||||
.at((ctx) => ({ path: "/experimental/session?roots=false&archived=false", headers: ctx.headers() }))
|
.at((ctx) => ({ path: "/experimental/session?roots=false&archived=false", headers: ctx.headers() }))
|
||||||
.json(200, array),
|
.json(200, array),
|
||||||
http.protected.get("/experimental/capabilities", "experimental.capabilities.get").json(200, (body) => {
|
http.protected.get("/experimental/capabilities", "experimental.capabilities.get").json(200, (body) => {
|
||||||
check(typeof body === "object" && body !== null, "capabilities should be an object")
|
check(
|
||||||
check("backgroundSubagents" in body, "capabilities should report background subagents")
|
typeof body === "object" && body !== null && "backgroundSubagents" in body && body.backgroundSubagents === true,
|
||||||
|
"capabilities should report background subagents as available",
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
http.protected
|
http.protected
|
||||||
.post("/experimental/session/{sessionID}/background", "experimental.session.background")
|
.post("/experimental/session/{sessionID}/background", "experimental.session.background")
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { afterEach, describe, expect, mock } from "bun:test"
|
import { afterEach, describe, expect, mock } from "bun:test"
|
||||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
import { Effect, Layer } from "effect"
|
import { Effect, Fiber, Layer } from "effect"
|
||||||
import { Session as SessionNs } from "@/session/session"
|
import { Session as SessionNs } from "@/session/session"
|
||||||
|
import { Job } from "@/job"
|
||||||
import { disposeAllInstances, TestInstance } from "../fixture/fixture"
|
import { disposeAllInstances, TestInstance } from "../fixture/fixture"
|
||||||
import { testEffect } from "../lib/effect"
|
import { pollWithTimeout, testEffect } from "../lib/effect"
|
||||||
import { httpApiLayer, requestInDirectory } from "./httpapi-layer"
|
import { httpApiLayer, requestInDirectory } from "./httpapi-layer"
|
||||||
|
|
||||||
const it = testEffect(Layer.mergeAll(LayerNode.compile(SessionNs.node), httpApiLayer))
|
const it = testEffect(Layer.mergeAll(LayerNode.compile(SessionNs.node), LayerNode.compile(Job.node), httpApiLayer))
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
mock.restore()
|
mock.restore()
|
||||||
@@ -14,6 +15,19 @@ afterEach(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("session action routes", () => {
|
describe("session action routes", () => {
|
||||||
|
it.instance(
|
||||||
|
"reports background subagents as available",
|
||||||
|
() =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const test = yield* TestInstance
|
||||||
|
const res = yield* requestInDirectory("/experimental/capabilities", test.directory)
|
||||||
|
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
expect(yield* res.json).toEqual({ backgroundSubagents: true })
|
||||||
|
}),
|
||||||
|
{ git: true },
|
||||||
|
)
|
||||||
|
|
||||||
it.instance(
|
it.instance(
|
||||||
"session routes expose metadata on create, update, get, and fork",
|
"session routes expose metadata on create, update, get, and fork",
|
||||||
() =>
|
() =>
|
||||||
@@ -107,4 +121,33 @@ describe("session action routes", () => {
|
|||||||
}),
|
}),
|
||||||
{ git: true },
|
{ git: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.instance(
|
||||||
|
"experimental background route backgrounds a synchronous subagent",
|
||||||
|
() =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const test = yield* TestInstance
|
||||||
|
const session = yield* Effect.acquireRelease(SessionNs.use.create({}), (created) =>
|
||||||
|
SessionNs.use.remove(created.id).pipe(Effect.ignore),
|
||||||
|
)
|
||||||
|
const jobs = yield* Job.Service
|
||||||
|
const job = yield* jobs.start({ type: "task", run: Effect.never })
|
||||||
|
const waiting = yield* jobs.block({ id: job.id, sessionID: session.id }).pipe(Effect.forkChild)
|
||||||
|
|
||||||
|
const backgrounded = yield* pollWithTimeout(
|
||||||
|
requestInDirectory(`/experimental/session/${session.id}/background`, test.directory, {
|
||||||
|
method: "POST",
|
||||||
|
}).pipe(
|
||||||
|
Effect.flatMap((res) => res.json),
|
||||||
|
Effect.map((value) => (value === true ? true : undefined)),
|
||||||
|
),
|
||||||
|
"background route never released the synchronous subagent",
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(backgrounded).toBe(true)
|
||||||
|
expect(yield* Fiber.join(waiting)).toMatchObject({ type: "backgrounded", info: { id: job.id } })
|
||||||
|
yield* jobs.cancel(job.id)
|
||||||
|
}),
|
||||||
|
{ git: true },
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1448,6 +1448,7 @@ describe("session.message-v2.fromError", () => {
|
|||||||
"prompt is too long: 213462 tokens > 200000 maximum",
|
"prompt is too long: 213462 tokens > 200000 maximum",
|
||||||
"Your input exceeds the context window of this model",
|
"Your input exceeds the context window of this model",
|
||||||
"The input token count (1196265) exceeds the maximum number of tokens allowed (1048575)",
|
"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",
|
"Please reduce the length of the messages or completion",
|
||||||
"400 status code (no body)",
|
"400 status code (no body)",
|
||||||
"413 status code (no body)",
|
"413 status code (no body)",
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ describe("tool.registry", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.instance("hides task background parameter unless experimental background subagents are enabled", () =>
|
it.instance("exposes the task background parameter by default", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const registry = yield* ToolRegistry.Service
|
const registry = yield* ToolRegistry.Service
|
||||||
const agent = yield* Agent.Service
|
const agent = yield* Agent.Service
|
||||||
@@ -162,8 +162,9 @@ describe("tool.registry", () => {
|
|||||||
agent: build,
|
agent: build,
|
||||||
})).find((tool) => tool.id === "task")
|
})).find((tool) => tool.id === "task")
|
||||||
|
|
||||||
expect(task?.jsonSchema).toBeDefined()
|
if (!task) throw new Error("task tool not found")
|
||||||
expect((task?.jsonSchema?.properties as Record<string, unknown> | undefined)?.background).toBeUndefined()
|
const jsonSchema = ToolJsonSchema.fromTool(task)
|
||||||
|
expect((jsonSchema.properties as Record<string, unknown> | undefined)?.background).toBeDefined()
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ const ref = {
|
|||||||
modelID: ModelV2.ID.make("test-model"),
|
modelID: ModelV2.ID.make("test-model"),
|
||||||
}
|
}
|
||||||
|
|
||||||
const layer = (flags: Partial<RuntimeFlags.Info> = {}) =>
|
const layer = () =>
|
||||||
LayerNode.compile(
|
LayerNode.compile(
|
||||||
LayerNode.group([
|
LayerNode.group([
|
||||||
Agent.node,
|
Agent.node,
|
||||||
@@ -52,11 +52,10 @@ const layer = (flags: Partial<RuntimeFlags.Info> = {}) =>
|
|||||||
RuntimeFlags.node,
|
RuntimeFlags.node,
|
||||||
Ripgrep.node,
|
Ripgrep.node,
|
||||||
]),
|
]),
|
||||||
[[RuntimeFlags.node, RuntimeFlags.layer(flags)]],
|
[[RuntimeFlags.node, RuntimeFlags.layer()]],
|
||||||
)
|
)
|
||||||
|
|
||||||
const it = testEffect(layer())
|
const it = testEffect(layer())
|
||||||
const background = testEffect(layer({ experimentalBackgroundSubagents: true }))
|
|
||||||
|
|
||||||
function defer<T>() {
|
function defer<T>() {
|
||||||
let resolve!: (value: T | PromiseLike<T>) => void
|
let resolve!: (value: T | PromiseLike<T>) => void
|
||||||
@@ -456,37 +455,6 @@ describe("tool.task", () => {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
it.instance("rejects background execution when the experiment is disabled", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const { chat, assistant } = yield* seed()
|
|
||||||
const tool = yield* TaskTool
|
|
||||||
const def = yield* tool.init()
|
|
||||||
|
|
||||||
const exit = yield* def
|
|
||||||
.execute(
|
|
||||||
{
|
|
||||||
description: "inspect bug",
|
|
||||||
prompt: "look into the cache key path",
|
|
||||||
subagent_type: "general",
|
|
||||||
background: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sessionID: chat.id,
|
|
||||||
messageID: assistant.id,
|
|
||||||
agent: "build",
|
|
||||||
abort: new AbortController().signal,
|
|
||||||
extra: { promptOps: stubOps() },
|
|
||||||
messages: [],
|
|
||||||
metadata: () => Effect.void,
|
|
||||||
ask: () => Effect.void,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.pipe(Effect.exit)
|
|
||||||
|
|
||||||
expect(Exit.isFailure(exit)).toBe(true)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.instance("backgrounds a running foreground task without restarting it", () =>
|
it.instance("backgrounds a running foreground task without restarting it", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const jobs = yield* Job.Service
|
const jobs = yield* Job.Service
|
||||||
@@ -558,7 +526,7 @@ describe("tool.task", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
background.instance("execute launches background tasks without waiting for completion", () =>
|
it.instance("execute launches background tasks without waiting for completion", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const jobs = yield* Job.Service
|
const jobs = yield* Job.Service
|
||||||
const { chat, assistant } = yield* seed()
|
const { chat, assistant } = yield* seed()
|
||||||
@@ -596,7 +564,7 @@ describe("tool.task", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
background.instance("running task_id reports the existing background task", () =>
|
it.instance("running task_id reports the existing background task", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const jobs = yield* Job.Service
|
const jobs = yield* Job.Service
|
||||||
const { chat, assistant } = yield* seed()
|
const { chat, assistant } = yield* seed()
|
||||||
@@ -661,7 +629,7 @@ describe("tool.task", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
background.instance("background tasks complete through the job service", () =>
|
it.instance("background tasks complete through the job service", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const jobs = yield* Job.Service
|
const jobs = yield* Job.Service
|
||||||
const { chat, assistant } = yield* seed()
|
const { chat, assistant } = yield* seed()
|
||||||
@@ -694,7 +662,7 @@ describe("tool.task", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
background.instance("background task completion does not wait for the parent async prompt", () =>
|
it.instance("background task completion does not wait for the parent async prompt", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const jobs = yield* Job.Service
|
const jobs = yield* Job.Service
|
||||||
const { chat, assistant } = yield* seed()
|
const { chat, assistant } = yield* seed()
|
||||||
@@ -732,7 +700,7 @@ describe("tool.task", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
background.instance("removing the parent session cancels running background tasks", () =>
|
it.instance("removing the parent session cancels running background tasks", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const jobs = yield* Job.Service
|
const jobs = yield* Job.Service
|
||||||
const sessions = yield* Session.Service
|
const sessions = yield* Session.Service
|
||||||
@@ -771,7 +739,7 @@ describe("tool.task", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
background.instance("removing the child task session cancels its running background task", () =>
|
it.instance("removing the child task session cancels its running background task", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const jobs = yield* Job.Service
|
const jobs = yield* Job.Service
|
||||||
const sessions = yield* Session.Service
|
const sessions = yield* Session.Service
|
||||||
@@ -810,7 +778,7 @@ describe("tool.task", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
background.instance("cancelling the parent run cancels running background tasks", () =>
|
it.instance("cancelling the parent run cancels running background tasks", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const jobs = yield* Job.Service
|
const jobs = yield* Job.Service
|
||||||
const runState = yield* SessionRunState.Service
|
const runState = yield* SessionRunState.Service
|
||||||
|
|||||||
@@ -475,6 +475,7 @@ export type TuiHostSlotMap = {
|
|||||||
}
|
}
|
||||||
sidebar_footer: {
|
sidebar_footer: {
|
||||||
session_id: string
|
session_id: string
|
||||||
|
directory: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ export const groupNames = {
|
|||||||
} as const
|
} as const
|
||||||
|
|
||||||
export const endpointNames = {
|
export const endpointNames = {
|
||||||
|
"debug.location.evict": "evictLocation",
|
||||||
"session.messages": "list",
|
"session.messages": "list",
|
||||||
"integration.connect.key": "connectKey",
|
"integration.connect.key": "connectKey",
|
||||||
"integration.connect.oauth": "connectOauth",
|
"integration.connect.oauth": "connectOauth",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Location } from "@opencode-ai/schema/location"
|
import { Location } from "@opencode-ai/schema/location"
|
||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||||
|
import { LocationQuery, locationQueryOpenApi } from "./location.js"
|
||||||
|
|
||||||
export const DebugGroup = HttpApiGroup.make("server.debug")
|
export const DebugGroup = HttpApiGroup.make("server.debug")
|
||||||
.add(
|
.add(
|
||||||
@@ -14,4 +15,18 @@ 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" }))
|
.annotateMerge(OpenApi.annotations({ title: "debug" }))
|
||||||
|
|||||||
@@ -19,4 +19,18 @@ export const McpGroup = HttpApiGroup.make("server.mcp")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.annotateMerge(OpenApi.annotations({ title: "mcp", description: "MCP server status routes." }))
|
.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." }))
|
||||||
|
|||||||
@@ -269,6 +269,25 @@ 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(
|
.add(
|
||||||
HttpApiEndpoint.post("session.prompt", "/api/session/:sessionID/prompt", {
|
HttpApiEndpoint.post("session.prompt", "/api/session/:sessionID/prompt", {
|
||||||
params: { sessionID: Session.ID },
|
params: { sessionID: Session.ID },
|
||||||
@@ -344,6 +363,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
|||||||
text: Schema.String,
|
text: Schema.String,
|
||||||
description: Schema.String.pipe(Schema.optional),
|
description: Schema.String.pipe(Schema.optional),
|
||||||
metadata: SessionMessage.Synthetic.fields.metadata,
|
metadata: SessionMessage.Synthetic.fields.metadata,
|
||||||
|
resume: Schema.Boolean.pipe(Schema.optional),
|
||||||
}),
|
}),
|
||||||
success: HttpApiSchema.NoContent,
|
success: HttpApiSchema.NoContent,
|
||||||
error: SessionNotFoundError,
|
error: SessionNotFoundError,
|
||||||
|
|||||||
@@ -4,5 +4,6 @@ import { isOpenCodeEvent } from "../src/groups/event.js"
|
|||||||
test("classifies public events by type", () => {
|
test("classifies public events by type", () => {
|
||||||
expect(isOpenCodeEvent({ type: "server.connected" })).toBe(true)
|
expect(isOpenCodeEvent({ type: "server.connected" })).toBe(true)
|
||||||
expect(isOpenCodeEvent({ type: "mcp.status.changed" })).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)
|
expect(isOpenCodeEvent({ type: "mcp.tools.changed" })).toBe(false)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ export const ServerDefinitions = Event.inventory(
|
|||||||
...InstallationEvent.Definitions,
|
...InstallationEvent.Definitions,
|
||||||
...VcsEvent.Definitions,
|
...VcsEvent.Definitions,
|
||||||
McpEvent.StatusChanged,
|
McpEvent.StatusChanged,
|
||||||
|
McpEvent.ResourcesChanged,
|
||||||
// Shared transitional: V1 contracts the current TUI still consumes during
|
// Shared transitional: V1 contracts the current TUI still consumes during
|
||||||
// the migration (permission.asked/replied, question.asked, session.error).
|
// the migration (permission.asked/replied, question.asked, session.error).
|
||||||
// Remove when the TUI moves to the current permission/question surfaces.
|
// Remove when the TUI moves to the current permission/question surfaces.
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export { Form } from "./form.js"
|
|||||||
export { Integration } from "./integration.js"
|
export { Integration } from "./integration.js"
|
||||||
export { LLM } from "./llm.js"
|
export { LLM } from "./llm.js"
|
||||||
export { Location } from "./location.js"
|
export { Location } from "./location.js"
|
||||||
|
export { Mcp } from "./mcp.js"
|
||||||
export { Model } from "./model.js"
|
export { Model } from "./model.js"
|
||||||
export { Permission } from "./permission.js"
|
export { Permission } from "./permission.js"
|
||||||
export { PermissionSaved } from "./permission-saved.js"
|
export { PermissionSaved } from "./permission-saved.js"
|
||||||
|
|||||||
@@ -10,6 +10,13 @@ export const ToolsChanged = Event.ephemeral({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const ResourcesChanged = Event.ephemeral({
|
||||||
|
type: "mcp.resources.changed",
|
||||||
|
schema: {
|
||||||
|
server: Schema.String,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
export const BrowserOpenFailed = Event.ephemeral({
|
export const BrowserOpenFailed = Event.ephemeral({
|
||||||
type: "mcp.browser.open.failed",
|
type: "mcp.browser.open.failed",
|
||||||
schema: {
|
schema: {
|
||||||
@@ -27,4 +34,4 @@ export const StatusChanged = Event.ephemeral({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export const Definitions = Event.inventory(ToolsChanged, StatusChanged)
|
export const Definitions = Event.inventory(ToolsChanged, ResourcesChanged, StatusChanged)
|
||||||
|
|||||||
@@ -25,14 +25,9 @@ const NeedsClientRegistration = Schema.Struct({
|
|||||||
}).annotate({ identifier: "Mcp.Status.NeedsClientRegistration" })
|
}).annotate({ identifier: "Mcp.Status.NeedsClientRegistration" })
|
||||||
|
|
||||||
export type Status = typeof Status.Type
|
export type Status = typeof Status.Type
|
||||||
export const Status = Schema.Union([
|
export const Status = Schema.Union([Connected, Pending, Disabled, Failed, NeedsAuth, NeedsClientRegistration]).pipe(
|
||||||
Connected,
|
Schema.toTaggedUnion("status"),
|
||||||
Pending,
|
)
|
||||||
Disabled,
|
|
||||||
Failed,
|
|
||||||
NeedsAuth,
|
|
||||||
NeedsClientRegistration,
|
|
||||||
]).pipe(Schema.toTaggedUnion("status"))
|
|
||||||
|
|
||||||
export interface Server extends Schema.Schema.Type<typeof Server> {}
|
export interface Server extends Schema.Schema.Type<typeof Server> {}
|
||||||
export const Server = Schema.Struct({
|
export const Server = Schema.Struct({
|
||||||
@@ -42,3 +37,50 @@ export const Server = Schema.Struct({
|
|||||||
// without matching by name, which could collide with provider or plugin integrations.
|
// without matching by name, which could collide with provider or plugin integrations.
|
||||||
integrationID: optional(IntegrationID),
|
integrationID: optional(IntegrationID),
|
||||||
}).annotate({ identifier: "Mcp.Server" })
|
}).annotate({ identifier: "Mcp.Server" })
|
||||||
|
|
||||||
|
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" })
|
||||||
|
|||||||
@@ -30,6 +30,15 @@ export interface Source extends Schema.Schema.Type<typeof Source> {}
|
|||||||
const Base = {
|
const Base = {
|
||||||
sessionID: SessionID,
|
sessionID: SessionID,
|
||||||
}
|
}
|
||||||
|
const Tokens = Schema.Struct({
|
||||||
|
input: Schema.Finite,
|
||||||
|
output: Schema.Finite,
|
||||||
|
reasoning: Schema.Finite,
|
||||||
|
cache: Schema.Struct({
|
||||||
|
read: Schema.Finite,
|
||||||
|
write: Schema.Finite,
|
||||||
|
}),
|
||||||
|
})
|
||||||
const PromptFields = {
|
const PromptFields = {
|
||||||
...Base,
|
...Base,
|
||||||
inputID: SessionMessage.ID,
|
inputID: SessionMessage.ID,
|
||||||
@@ -84,6 +93,16 @@ export const Renamed = Event.durable({
|
|||||||
})
|
})
|
||||||
export type Renamed = typeof Renamed.Type
|
export type Renamed = typeof Renamed.Type
|
||||||
|
|
||||||
|
export const UsageUpdated = Event.ephemeral({
|
||||||
|
type: "session.usage.updated",
|
||||||
|
schema: {
|
||||||
|
...Base,
|
||||||
|
cost: Schema.Finite,
|
||||||
|
tokens: Tokens,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export type UsageUpdated = typeof UsageUpdated.Type
|
||||||
|
|
||||||
export const Deleted = Event.durable({
|
export const Deleted = Event.durable({
|
||||||
type: "session.deleted",
|
type: "session.deleted",
|
||||||
durable: {
|
durable: {
|
||||||
@@ -224,15 +243,7 @@ export namespace Step {
|
|||||||
assistantMessageID: SessionMessage.ID,
|
assistantMessageID: SessionMessage.ID,
|
||||||
finish: FinishReason,
|
finish: FinishReason,
|
||||||
cost: Schema.Finite,
|
cost: Schema.Finite,
|
||||||
tokens: Schema.Struct({
|
tokens: Tokens,
|
||||||
input: Schema.Finite,
|
|
||||||
output: Schema.Finite,
|
|
||||||
reasoning: Schema.Finite,
|
|
||||||
cache: Schema.Struct({
|
|
||||||
read: Schema.Finite,
|
|
||||||
write: Schema.Finite,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
snapshot: Schema.String.pipe(optional),
|
snapshot: Schema.String.pipe(optional),
|
||||||
files: Schema.Array(RelativePath).pipe(optional),
|
files: Schema.Array(RelativePath).pipe(optional),
|
||||||
},
|
},
|
||||||
@@ -246,6 +257,8 @@ export namespace Step {
|
|||||||
...Base,
|
...Base,
|
||||||
assistantMessageID: SessionMessage.ID,
|
assistantMessageID: SessionMessage.ID,
|
||||||
error: SessionError.Error,
|
error: SessionError.Error,
|
||||||
|
cost: Schema.Finite.pipe(optional),
|
||||||
|
tokens: Tokens.pipe(optional),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export type Failed = typeof Failed.Type
|
export type Failed = typeof Failed.Type
|
||||||
@@ -504,6 +517,7 @@ export const Definitions = Event.inventory(
|
|||||||
ModelSelected,
|
ModelSelected,
|
||||||
Moved,
|
Moved,
|
||||||
Renamed,
|
Renamed,
|
||||||
|
UsageUpdated,
|
||||||
Deleted,
|
Deleted,
|
||||||
Forked,
|
Forked,
|
||||||
PromptPromoted,
|
PromptPromoted,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"
|
|||||||
import { DateTime, Schema } from "effect"
|
import { DateTime, Schema } from "effect"
|
||||||
import { Agent } from "../src/agent.js"
|
import { Agent } from "../src/agent.js"
|
||||||
import { FileSystem } from "../src/filesystem.js"
|
import { FileSystem } from "../src/filesystem.js"
|
||||||
|
import { Mcp } from "../src/mcp.js"
|
||||||
import { Model } from "../src/model.js"
|
import { Model } from "../src/model.js"
|
||||||
import { Project } from "../src/project.js"
|
import { Project } from "../src/project.js"
|
||||||
import { Provider } from "../src/provider.js"
|
import { Provider } from "../src/provider.js"
|
||||||
@@ -51,6 +52,11 @@ describe("contract hygiene", () => {
|
|||||||
const identifiers = [
|
const identifiers = [
|
||||||
Agent.Color,
|
Agent.Color,
|
||||||
FileSystem.Submatch,
|
FileSystem.Submatch,
|
||||||
|
Mcp.Resource,
|
||||||
|
Mcp.ResourceTemplate,
|
||||||
|
Mcp.ResourceCatalog,
|
||||||
|
Mcp.ResourceContentPart,
|
||||||
|
Mcp.ResourceContent,
|
||||||
Model.Ref,
|
Model.Ref,
|
||||||
Model.Capabilities,
|
Model.Capabilities,
|
||||||
Model.Cost,
|
Model.Cost,
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ describe("public event manifest", () => {
|
|||||||
expect(EventManifest.Latest.get("agent.updated")).toBe(Agent.Event.Updated)
|
expect(EventManifest.Latest.get("agent.updated")).toBe(Agent.Event.Updated)
|
||||||
expect(EventManifest.Latest.get("plugin.updated")).toBe(Plugin.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.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.get("session.deleted")).toBe(SessionEvent.Deleted)
|
||||||
expect(EventManifest.Server.has("mcp.tools.changed")).toBe(false)
|
expect(EventManifest.Server.has("mcp.tools.changed")).toBe(false)
|
||||||
expect(Agent.Event.Updated.durable).toBeUndefined()
|
expect(Agent.Event.Updated.durable).toBeUndefined()
|
||||||
@@ -76,7 +77,7 @@ describe("public event manifest", () => {
|
|||||||
expect(Form.Event.Definitions).toEqual([Form.Event.Created, Form.Event.Replied, Form.Event.Cancelled])
|
expect(Form.Event.Definitions).toEqual([Form.Event.Created, Form.Event.Replied, Form.Event.Cancelled])
|
||||||
expect(Reference.Event.Definitions).toEqual([Reference.Event.Updated])
|
expect(Reference.Event.Definitions).toEqual([Reference.Event.Updated])
|
||||||
expect(Plugin.Event.Definitions).toEqual([Plugin.Event.Added, Plugin.Event.Updated])
|
expect(Plugin.Event.Definitions).toEqual([Plugin.Event.Added, Plugin.Event.Updated])
|
||||||
expect(McpEvent.Definitions).toEqual([McpEvent.ToolsChanged, McpEvent.StatusChanged])
|
expect(McpEvent.Definitions).toEqual([McpEvent.ToolsChanged, McpEvent.ResourcesChanged, McpEvent.StatusChanged])
|
||||||
expect(EventManifest.Latest.has("mcp.browser.open.failed")).toBe(false)
|
expect(EventManifest.Latest.has("mcp.browser.open.failed")).toBe(false)
|
||||||
expect(EventManifest.Latest.has("ide.installed")).toBe(false)
|
expect(EventManifest.Latest.has("ide.installed")).toBe(false)
|
||||||
expect(IdeEvent.Definitions).toEqual([IdeEvent.Installed])
|
expect(IdeEvent.Definitions).toEqual([IdeEvent.Installed])
|
||||||
@@ -143,6 +144,8 @@ describe("public event manifest", () => {
|
|||||||
expect(SessionEvent.DurableDefinitions).toEqual(
|
expect(SessionEvent.DurableDefinitions).toEqual(
|
||||||
SessionEvent.Definitions.filter((definition) => definition.durability === "durable"),
|
SessionEvent.Definitions.filter((definition) => definition.durability === "durable"),
|
||||||
)
|
)
|
||||||
|
expect(SessionEvent.UsageUpdated.durability).toBe("ephemeral")
|
||||||
|
expect(EventManifest.ServerDefinitions).toContain(SessionEvent.UsageUpdated)
|
||||||
expect(EventManifest.Definitions.every((definition) => definition.durability !== undefined)).toBe(true)
|
expect(EventManifest.Definitions.every((definition) => definition.durability !== undefined)).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { Schema } from "effect"
|
||||||
|
import { Mcp } from "../src/mcp.js"
|
||||||
|
|
||||||
|
describe("Mcp resources", () => {
|
||||||
|
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" },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user