Compare commits

..

26 Commits

Author SHA1 Message Date
Kit Langton e6aaba8f4a fix(core): skip non-vcs location watcher
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2026-07-08 00:31:14 +00:00
Dax e1abf59c59 fix(core): provide PluginHooks in plugin test layer (#35811) 2026-07-07 20:03:41 -04:00
Dax 4f976bcf1a feat(plugin): add session request hook (#35794) 2026-07-07 19:56:47 -04:00
Kit Langton e25a724bc2 test(tui): cover patch diff display aliases (#35810) 2026-07-07 19:44:54 -04:00
Kit Langton 521ea87192 fix(core): retain reasoning delta state (#35758) 2026-07-07 19:44:40 -04:00
Aiden Cline 3100701488 fix(tui): restore prompt context usage (#35795) 2026-07-07 17:49:33 -05:00
Kit Langton 9c54291ae8 fix(tui): restore forms on reconnect
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2026-07-07 22:47:13 +00:00
Dax Raad 52ad916ba4 fix(tui): restore permissions on reconnect
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2026-07-07 22:44:26 +00:00
Dax Raad 5db320b02d refactor: rename apply_patch tool 2026-07-07 16:27:25 -04:00
Aiden Cline 910e37f6d8 fix: update v2 session usage metrics (#35468) 2026-07-07 14:31:54 -05:00
Kit Langton 81f6e06681 feat: enable background subagents by default 2026-07-07 15:18:46 -04:00
James Long da68e2865e feat(tui): wire move session command (#35203) 2026-07-07 14:49:11 -04:00
Aiden Cline f86e1cc1ff fix(codemode): preserve collection value identity (#35776) 2026-07-07 13:40:55 -05:00
Aiden Cline 947bbf9490 feat(mcp): expose resource catalog API (#35773) 2026-07-07 13:34:29 -05:00
James Long 3e57b43a4a feat(session): support admit-only synthetic messages (#35774) 2026-07-07 14:17:20 -04:00
opencode-agent[bot] cb18a42a47 docs(codemode): clarify implicit return guidance (#35763)
Co-authored-by: Kit Langton <kit.langton@gmail.com>
2026-07-07 12:48:05 -05:00
Aiden Cline 1fb2837fd3 feat(codemode): expand numeric standard library (#35749) 2026-07-07 12:11:57 -05:00
Aiden Cline 66a8d830aa feat(core): support MCP resources (#35656) 2026-07-07 11:55:06 -05:00
opencode-agent[bot] d0733f83bf fix(codemode): return final top-level expression (#35759)
Co-authored-by: Kit Langton <kit.langton@gmail.com>
2026-07-07 11:44:50 -05:00
Kit Langton b2ce195bda fix(core): normalize Location.Ref keys in LocationServiceMap (#35757) 2026-07-07 12:25:29 -04:00
James Long 6c065ca6a5 feat(simulation): render driven UI screenshots and videos (#35739) 2026-07-07 12:15:01 -04:00
Dax Raad dd1c007877 docs 2026-07-07 12:08:45 -04:00
Kit Langton 8deb0e5780 fix(core): preserve SDK plugins across location eviction (#35725) 2026-07-07 11:43:40 -04:00
Aiden Cline f488089179 fix: preserve compaction files and classify Z.AI overflow (#35747)
Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: 冯基魁 <56265583+fengjikui@users.noreply.github.com>
2026-07-07 09:54:31 -05:00
Kit Langton e3a39b214f fix(core): lower failed reasoning to text (#35735) 2026-07-07 09:49:17 -04:00
James Long b6a2912bb1 refactor(simulation): type RPC request payloads (#35734) 2026-07-07 09:21:19 -04:00
264 changed files with 10572 additions and 6157 deletions
+116 -6
View File
@@ -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)] : []),
+1 -1
View File
@@ -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
View File
-1
View File
@@ -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:*",
-66
View File
@@ -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),
]),
)
}
+2 -2
View File
@@ -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: [
-2
View File
@@ -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,
+1 -4
View File
@@ -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 }
-4
View File
@@ -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,
-5
View File
@@ -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,
+2 -2
View File
@@ -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,
-1
View File
@@ -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
} }
-11
View File
@@ -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)
-39
View File
@@ -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")}`,
},
])
})
+2 -3
View File
@@ -3,15 +3,14 @@ import { compile, emitEffectImported, emitEffectShape, emitPromise, write } from
import { import {
ClientApi, ClientApi,
effectOmitEndpoints, effectOmitEndpoints,
endpointNames,
groupNames, groupNames,
promiseOmitEndpoints, promiseOmitEndpoints,
} from "@opencode-ai/protocol/client" } from "@opencode-ai/protocol/client"
import { Effect } from "effect" import { Effect } from "effect"
import { fileURLToPath } from "url" import { fileURLToPath } from "url"
const promiseContract = compile(ClientApi, { groupNames, endpointNames, omitEndpoints: promiseOmitEndpoints }) const promiseContract = compile(ClientApi, { groupNames, omitEndpoints: promiseOmitEndpoints })
const effectContract = compile(ClientApi, { groupNames, endpointNames, omitEndpoints: effectOmitEndpoints }) const effectContract = compile(ClientApi, { groupNames, omitEndpoints: effectOmitEndpoints })
await Effect.runPromise( await Effect.runPromise(
Effect.all( Effect.all(
-1
View File
@@ -1,7 +1,6 @@
export { export {
ClientApi, ClientApi,
effectOmitEndpoints, effectOmitEndpoints,
endpointNames,
groupNames, groupNames,
promiseOmitEndpoints, promiseOmitEndpoints,
} from "@opencode-ai/protocol/client" } from "@opencode-ai/protocol/client"
+141 -112
View File
@@ -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>
@@ -280,9 +291,11 @@ export interface SessionApi<E = never> {
readonly shell: SessionShellOperation<E> readonly shell: SessionShellOperation<E>
readonly compact: SessionCompactOperation<E> readonly compact: SessionCompactOperation<E>
readonly wait: SessionWaitOperation<E> readonly wait: SessionWaitOperation<E>
readonly revertStage: SessionRevertStageOperation<E> readonly revert: {
readonly revertClear: SessionRevertClearOperation<E> readonly stage: SessionRevertStageOperation<E>
readonly revertCommit: SessionRevertCommitOperation<E> readonly clear: SessionRevertClearOperation<E>
readonly commit: SessionRevertCommitOperation<E>
}
readonly context: SessionContextOperation<E> readonly context: SessionContextOperation<E>
readonly instructions: { readonly instructions: {
readonly entry: { readonly entry: {
@@ -427,11 +440,15 @@ export type IntegrationAttemptCancelOperation<E = never> = (
export interface IntegrationApi<E = never> { export interface IntegrationApi<E = never> {
readonly list: IntegrationListOperation<E> readonly list: IntegrationListOperation<E>
readonly get: IntegrationGetOperation<E> readonly get: IntegrationGetOperation<E>
readonly connectKey: IntegrationConnectKeyOperation<E> readonly connect: {
readonly connectOauth: IntegrationConnectOauthOperation<E> readonly key: IntegrationConnectKeyOperation<E>
readonly attemptStatus: IntegrationAttemptStatusOperation<E> readonly oauth: IntegrationConnectOauthOperation<E>
readonly attemptComplete: IntegrationAttemptCompleteOperation<E> }
readonly attemptCancel: IntegrationAttemptCancelOperation<E> readonly attempt: {
readonly status: IntegrationAttemptStatusOperation<E>
readonly complete: IntegrationAttemptCompleteOperation<E>
readonly cancel: IntegrationAttemptCancelOperation<E>
}
} }
type Endpoint10_0Request = Parameters<RawClient["server.mcp"]["mcp.list"]>[0] type Endpoint10_0Request = Parameters<RawClient["server.mcp"]["mcp.list"]>[0]
@@ -439,8 +456,16 @@ 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 ServerMcpResourceCatalogOperation<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 resource: { readonly catalog: ServerMcpResourceCatalogOperation<E> }
} }
type Endpoint11_0Request = Parameters<RawClient["server.credential"]["credential.update"]>[0] type Endpoint11_0Request = Parameters<RawClient["server.credential"]["credential.update"]>[0]
@@ -490,7 +515,7 @@ export interface ProjectApi<E = never> {
type Endpoint13_0Request = Parameters<RawClient["server.form"]["form.request.list"]>[0] type Endpoint13_0Request = Parameters<RawClient["server.form"]["form.request.list"]>[0]
export type Endpoint13_0Input = { readonly location?: Endpoint13_0Request["query"]["location"] } export type Endpoint13_0Input = { readonly location?: Endpoint13_0Request["query"]["location"] }
export type Endpoint13_0Output = EffectValue<ReturnType<RawClient["server.form"]["form.request.list"]>> export type Endpoint13_0Output = EffectValue<ReturnType<RawClient["server.form"]["form.request.list"]>>
export type FormListRequestsOperation<E = never> = (input?: Endpoint13_0Input) => Effect.Effect<Endpoint13_0Output, E> export type FormRequestListOperation<E = never> = (input?: Endpoint13_0Input) => Effect.Effect<Endpoint13_0Output, E>
type Endpoint13_1Request = Parameters<RawClient["server.form"]["session.form.list"]>[0] type Endpoint13_1Request = Parameters<RawClient["server.form"]["session.form.list"]>[0]
export type Endpoint13_1Input = { readonly sessionID: Endpoint13_1Request["params"]["sessionID"] } export type Endpoint13_1Input = { readonly sessionID: Endpoint13_1Request["params"]["sessionID"] }
@@ -544,7 +569,7 @@ export type Endpoint13_6Output = EffectValue<ReturnType<RawClient["server.form"]
export type FormCancelOperation<E = never> = (input: Endpoint13_6Input) => Effect.Effect<Endpoint13_6Output, E> export type FormCancelOperation<E = never> = (input: Endpoint13_6Input) => Effect.Effect<Endpoint13_6Output, E>
export interface FormApi<E = never> { export interface FormApi<E = never> {
readonly listRequests: FormListRequestsOperation<E> readonly request: { readonly list: FormRequestListOperation<E> }
readonly list: FormListOperation<E> readonly list: FormListOperation<E>
readonly create: FormCreateOperation<E> readonly create: FormCreateOperation<E>
readonly get: FormGetOperation<E> readonly get: FormGetOperation<E>
@@ -556,7 +581,7 @@ export interface FormApi<E = never> {
type Endpoint14_0Request = Parameters<RawClient["server.permission"]["permission.request.list"]>[0] type Endpoint14_0Request = Parameters<RawClient["server.permission"]["permission.request.list"]>[0]
export type Endpoint14_0Input = { readonly location?: Endpoint14_0Request["query"]["location"] } export type Endpoint14_0Input = { readonly location?: Endpoint14_0Request["query"]["location"] }
export type Endpoint14_0Output = EffectValue<ReturnType<RawClient["server.permission"]["permission.request.list"]>> export type Endpoint14_0Output = EffectValue<ReturnType<RawClient["server.permission"]["permission.request.list"]>>
export type PermissionListRequestsOperation<E = never> = ( export type PermissionRequestListOperation<E = never> = (
input?: Endpoint14_0Input, input?: Endpoint14_0Input,
) => Effect.Effect<Endpoint14_0Output, E> ) => Effect.Effect<Endpoint14_0Output, E>
@@ -565,14 +590,14 @@ export type Endpoint14_1Input = { readonly projectID?: Endpoint14_1Request["quer
export type Endpoint14_1Output = EffectValue< export type Endpoint14_1Output = EffectValue<
ReturnType<RawClient["server.permission"]["permission.saved.list"]> ReturnType<RawClient["server.permission"]["permission.saved.list"]>
>["data"] >["data"]
export type PermissionListSavedOperation<E = never> = ( export type PermissionSavedListOperation<E = never> = (
input?: Endpoint14_1Input, input?: Endpoint14_1Input,
) => Effect.Effect<Endpoint14_1Output, E> ) => Effect.Effect<Endpoint14_1Output, E>
type Endpoint14_2Request = Parameters<RawClient["server.permission"]["permission.saved.remove"]>[0] type Endpoint14_2Request = Parameters<RawClient["server.permission"]["permission.saved.remove"]>[0]
export type Endpoint14_2Input = { readonly id: Endpoint14_2Request["params"]["id"] } export type Endpoint14_2Input = { readonly id: Endpoint14_2Request["params"]["id"] }
export type Endpoint14_2Output = EffectValue<ReturnType<RawClient["server.permission"]["permission.saved.remove"]>> export type Endpoint14_2Output = EffectValue<ReturnType<RawClient["server.permission"]["permission.saved.remove"]>>
export type PermissionRemoveSavedOperation<E = never> = ( export type PermissionSavedRemoveOperation<E = never> = (
input: Endpoint14_2Input, input: Endpoint14_2Input,
) => Effect.Effect<Endpoint14_2Output, E> ) => Effect.Effect<Endpoint14_2Output, E>
@@ -620,9 +645,8 @@ export type Endpoint14_6Output = EffectValue<ReturnType<RawClient["server.permis
export type PermissionReplyOperation<E = never> = (input: Endpoint14_6Input) => Effect.Effect<Endpoint14_6Output, E> export type PermissionReplyOperation<E = never> = (input: Endpoint14_6Input) => Effect.Effect<Endpoint14_6Output, E>
export interface PermissionApi<E = never> { export interface PermissionApi<E = never> {
readonly listRequests: PermissionListRequestsOperation<E> readonly request: { readonly list: PermissionRequestListOperation<E> }
readonly listSaved: PermissionListSavedOperation<E> readonly saved: { readonly list: PermissionSavedListOperation<E>; readonly remove: PermissionSavedRemoveOperation<E> }
readonly removeSaved: PermissionRemoveSavedOperation<E>
readonly create: PermissionCreateOperation<E> readonly create: PermissionCreateOperation<E>
readonly list: PermissionListOperation<E> readonly list: PermissionListOperation<E>
readonly get: PermissionGetOperation<E> readonly get: PermissionGetOperation<E>
@@ -791,7 +815,7 @@ export interface ShellApi<E = never> {
type Endpoint21_0Request = Parameters<RawClient["server.question"]["question.request.list"]>[0] type Endpoint21_0Request = Parameters<RawClient["server.question"]["question.request.list"]>[0]
export type Endpoint21_0Input = { readonly location?: Endpoint21_0Request["query"]["location"] } export type Endpoint21_0Input = { readonly location?: Endpoint21_0Request["query"]["location"] }
export type Endpoint21_0Output = EffectValue<ReturnType<RawClient["server.question"]["question.request.list"]>> export type Endpoint21_0Output = EffectValue<ReturnType<RawClient["server.question"]["question.request.list"]>>
export type QuestionListRequestsOperation<E = never> = ( export type QuestionRequestListOperation<E = never> = (
input?: Endpoint21_0Input, input?: Endpoint21_0Input,
) => Effect.Effect<Endpoint21_0Output, E> ) => Effect.Effect<Endpoint21_0Output, E>
@@ -818,7 +842,7 @@ export type Endpoint21_3Output = EffectValue<ReturnType<RawClient["server.questi
export type QuestionRejectOperation<E = never> = (input: Endpoint21_3Input) => Effect.Effect<Endpoint21_3Output, E> export type QuestionRejectOperation<E = never> = (input: Endpoint21_3Input) => Effect.Effect<Endpoint21_3Output, E>
export interface QuestionApi<E = never> { export interface QuestionApi<E = never> {
readonly listRequests: QuestionListRequestsOperation<E> readonly request: { readonly list: QuestionRequestListOperation<E> }
readonly list: QuestionListOperation<E> readonly list: QuestionListOperation<E>
readonly reply: QuestionReplyOperation<E> readonly reply: QuestionReplyOperation<E>
readonly reject: QuestionRejectOperation<E> readonly reject: QuestionRejectOperation<E>
@@ -888,10 +912,15 @@ 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 DebugLocationListOperation<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 DebugLocationEvictOperation<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: { readonly list: DebugLocationListOperation<E>; readonly evict: DebugLocationEvictOperation<E> }
} }
export interface AppApi<E = never> { export interface AppApi<E = never> {
+150 -122
View File
@@ -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,21 @@ 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), revert: { stage: Endpoint4_17(raw), clear: Endpoint4_18(raw), commit: Endpoint4_19(raw) },
revertCommit: Endpoint4_18(raw), context: Endpoint4_20(raw),
context: Endpoint4_19(raw), instructions: { entry: { list: Endpoint4_21(raw), put: Endpoint4_22(raw), remove: Endpoint4_23(raw) } },
instructions: { entry: { list: Endpoint4_20(raw), put: Endpoint4_21(raw), remove: Endpoint4_22(raw) } }, log: Endpoint4_24(raw),
log: Endpoint4_23(raw), interrupt: Endpoint4_25(raw),
interrupt: Endpoint4_24(raw), background: Endpoint4_26(raw),
background: Endpoint4_25(raw), message: Endpoint4_27(raw),
message: Endpoint4_26(raw),
}) })
type Endpoint5_0Request = Parameters<RawClient["server.message"]["session.messages"]>[0] type Endpoint5_0Request = Parameters<RawClient["server.message"]["session.messages"]>[0]
@@ -517,11 +534,8 @@ const Endpoint9_6 = (raw: RawClient["server.integration"]) => (input: Endpoint9_
const adaptGroup9 = (raw: RawClient["server.integration"]) => ({ const adaptGroup9 = (raw: RawClient["server.integration"]) => ({
list: Endpoint9_0(raw), list: Endpoint9_0(raw),
get: Endpoint9_1(raw), get: Endpoint9_1(raw),
connectKey: Endpoint9_2(raw), connect: { key: Endpoint9_2(raw), oauth: Endpoint9_3(raw) },
connectOauth: Endpoint9_3(raw), attempt: { status: Endpoint9_4(raw), complete: Endpoint9_5(raw), cancel: Endpoint9_6(raw) },
attemptStatus: Endpoint9_4(raw),
attemptComplete: Endpoint9_5(raw),
attemptCancel: Endpoint9_6(raw),
}) })
type Endpoint10_0Request = Parameters<RawClient["server.mcp"]["mcp.list"]>[0] type Endpoint10_0Request = Parameters<RawClient["server.mcp"]["mcp.list"]>[0]
@@ -529,7 +543,15 @@ 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),
resource: { 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 = {
@@ -666,7 +688,7 @@ const Endpoint13_6 = (raw: RawClient["server.form"]) => (input: Endpoint13_6Inpu
) )
const adaptGroup13 = (raw: RawClient["server.form"]) => ({ const adaptGroup13 = (raw: RawClient["server.form"]) => ({
listRequests: Endpoint13_0(raw), request: { list: Endpoint13_0(raw) },
list: Endpoint13_1(raw), list: Endpoint13_1(raw),
create: Endpoint13_2(raw), create: Endpoint13_2(raw),
get: Endpoint13_3(raw), get: Endpoint13_3(raw),
@@ -754,9 +776,8 @@ const Endpoint14_6 = (raw: RawClient["server.permission"]) => (input: Endpoint14
}).pipe(Effect.mapError(mapClientError)) }).pipe(Effect.mapError(mapClientError))
const adaptGroup14 = (raw: RawClient["server.permission"]) => ({ const adaptGroup14 = (raw: RawClient["server.permission"]) => ({
listRequests: Endpoint14_0(raw), request: { list: Endpoint14_0(raw) },
listSaved: Endpoint14_1(raw), saved: { list: Endpoint14_1(raw), remove: Endpoint14_2(raw) },
removeSaved: Endpoint14_2(raw),
create: Endpoint14_3(raw), create: Endpoint14_3(raw),
list: Endpoint14_4(raw), list: Endpoint14_4(raw),
get: Endpoint14_5(raw), get: Endpoint14_5(raw),
@@ -989,7 +1010,7 @@ const Endpoint21_3 = (raw: RawClient["server.question"]) => (input: Endpoint21_3
) )
const adaptGroup21 = (raw: RawClient["server.question"]) => ({ const adaptGroup21 = (raw: RawClient["server.question"]) => ({
listRequests: Endpoint21_0(raw), request: { list: Endpoint21_0(raw) },
list: Endpoint21_1(raw), list: Endpoint21_1(raw),
reply: Endpoint21_2(raw), reply: Endpoint21_2(raw),
reject: Endpoint21_3(raw), reject: Endpoint21_3(raw),
@@ -1069,7 +1090,14 @@ 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: { list: Endpoint25_0(raw), evict: Endpoint25_1(raw) },
})
const adaptClient = (raw: RawClient) => ({ const adaptClient = (raw: RawClient) => ({
health: adaptGroup0(raw["server.health"]), health: adaptGroup0(raw["server.health"]),
+244 -179
View File
@@ -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,
ServerMcpResourceCatalogInput,
ServerMcpResourceCatalogOutput,
CredentialUpdateInput, CredentialUpdateInput,
CredentialUpdateOutput, CredentialUpdateOutput,
CredentialRemoveInput, CredentialRemoveInput,
@@ -96,8 +100,8 @@ import type {
ProjectCurrentOutput, ProjectCurrentOutput,
ProjectDirectoriesInput, ProjectDirectoriesInput,
ProjectDirectoriesOutput, ProjectDirectoriesOutput,
FormListRequestsInput, FormRequestListInput,
FormListRequestsOutput, FormRequestListOutput,
FormListInput, FormListInput,
FormListOutput, FormListOutput,
FormCreateInput, FormCreateInput,
@@ -110,12 +114,12 @@ import type {
FormReplyOutput, FormReplyOutput,
FormCancelInput, FormCancelInput,
FormCancelOutput, FormCancelOutput,
PermissionListRequestsInput, PermissionRequestListInput,
PermissionListRequestsOutput, PermissionRequestListOutput,
PermissionListSavedInput, PermissionSavedListInput,
PermissionListSavedOutput, PermissionSavedListOutput,
PermissionRemoveSavedInput, PermissionSavedRemoveInput,
PermissionRemoveSavedOutput, PermissionSavedRemoveOutput,
PermissionCreateInput, PermissionCreateInput,
PermissionCreateOutput, PermissionCreateOutput,
PermissionListInput, PermissionListInput,
@@ -157,8 +161,8 @@ import type {
ShellOutputOutput, ShellOutputOutput,
ShellRemoveInput, ShellRemoveInput,
ShellRemoveOutput, ShellRemoveOutput,
QuestionListRequestsInput, QuestionRequestListInput,
QuestionListRequestsOutput, QuestionRequestListOutput,
QuestionListInput, QuestionListInput,
QuestionListOutput, QuestionListOutput,
QuestionReplyInput, QuestionReplyInput,
@@ -177,7 +181,9 @@ import type {
VcsStatusOutput, VcsStatusOutput,
VcsDiffInput, VcsDiffInput,
VcsDiffOutput, VcsDiffOutput,
DebugLocationOutput, DebugLocationListOutput,
DebugLocationEvictInput,
DebugLocationEvictOutput,
} 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,
@@ -578,40 +601,42 @@ export function make(options: ClientOptions) {
}, },
requestOptions, requestOptions,
), ),
revertStage: (input: SessionRevertStageInput, requestOptions?: RequestOptions) => revert: {
request<{ readonly data: SessionRevertStageOutput }>( stage: (input: SessionRevertStageInput, requestOptions?: RequestOptions) =>
{ request<{ readonly data: SessionRevertStageOutput }>(
method: "POST", {
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/stage`, method: "POST",
body: { messageID: input["messageID"], files: input["files"] }, path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/stage`,
successStatus: 200, body: { messageID: input["messageID"], files: input["files"] },
declaredStatuses: [404, 409, 500, 400, 401], successStatus: 200,
empty: false, declaredStatuses: [404, 409, 500, 400, 401],
}, empty: false,
requestOptions, },
).then((value) => value.data), requestOptions,
revertClear: (input: SessionRevertClearInput, requestOptions?: RequestOptions) => ).then((value) => value.data),
request<SessionRevertClearOutput>( clear: (input: SessionRevertClearInput, requestOptions?: RequestOptions) =>
{ request<SessionRevertClearOutput>(
method: "POST", {
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/clear`, method: "POST",
successStatus: 204, path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/clear`,
declaredStatuses: [404, 409, 500, 400, 401], successStatus: 204,
empty: true, declaredStatuses: [404, 409, 500, 400, 401],
}, empty: true,
requestOptions, },
), requestOptions,
revertCommit: (input: SessionRevertCommitInput, requestOptions?: RequestOptions) => ),
request<SessionRevertCommitOutput>( commit: (input: SessionRevertCommitInput, requestOptions?: RequestOptions) =>
{ request<SessionRevertCommitOutput>(
method: "POST", {
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/commit`, method: "POST",
successStatus: 204, path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/commit`,
declaredStatuses: [404, 409, 400, 401], successStatus: 204,
empty: true, declaredStatuses: [404, 409, 400, 401],
}, empty: true,
requestOptions, },
), requestOptions,
),
},
context: (input: SessionContextInput, requestOptions?: RequestOptions) => context: (input: SessionContextInput, requestOptions?: RequestOptions) =>
request<{ readonly data: SessionContextOutput }>( request<{ readonly data: SessionContextOutput }>(
{ {
@@ -813,69 +838,73 @@ export function make(options: ClientOptions) {
}, },
requestOptions, requestOptions,
), ),
connectKey: (input: IntegrationConnectKeyInput, requestOptions?: RequestOptions) => connect: {
request<IntegrationConnectKeyOutput>( key: (input: IntegrationConnectKeyInput, requestOptions?: RequestOptions) =>
{ request<IntegrationConnectKeyOutput>(
method: "POST", {
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/key`, method: "POST",
query: { location: input["location"] }, path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/key`,
body: { key: input["key"], label: input["label"] }, query: { location: input["location"] },
successStatus: 204, body: { key: input["key"], label: input["label"] },
declaredStatuses: [400, 401], successStatus: 204,
empty: true, declaredStatuses: [400, 401],
}, empty: true,
requestOptions, },
), requestOptions,
connectOauth: (input: IntegrationConnectOauthInput, requestOptions?: RequestOptions) => ),
request<IntegrationConnectOauthOutput>( oauth: (input: IntegrationConnectOauthInput, requestOptions?: RequestOptions) =>
{ request<IntegrationConnectOauthOutput>(
method: "POST", {
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth`, method: "POST",
query: { location: input["location"] }, path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth`,
body: { methodID: input["methodID"], inputs: input["inputs"], label: input["label"] }, query: { location: input["location"] },
successStatus: 200, body: { methodID: input["methodID"], inputs: input["inputs"], label: input["label"] },
declaredStatuses: [400, 401], successStatus: 200,
empty: false, declaredStatuses: [400, 401],
}, empty: false,
requestOptions, },
), requestOptions,
attemptStatus: (input: IntegrationAttemptStatusInput, requestOptions?: RequestOptions) => ),
request<IntegrationAttemptStatusOutput>( },
{ attempt: {
method: "GET", status: (input: IntegrationAttemptStatusInput, requestOptions?: RequestOptions) =>
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}`, request<IntegrationAttemptStatusOutput>(
query: { location: input["location"] }, {
successStatus: 200, method: "GET",
declaredStatuses: [401, 400], path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}`,
empty: false, query: { location: input["location"] },
}, successStatus: 200,
requestOptions, declaredStatuses: [401, 400],
), empty: false,
attemptComplete: (input: IntegrationAttemptCompleteInput, requestOptions?: RequestOptions) => },
request<IntegrationAttemptCompleteOutput>( requestOptions,
{ ),
method: "POST", complete: (input: IntegrationAttemptCompleteInput, requestOptions?: RequestOptions) =>
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}/complete`, request<IntegrationAttemptCompleteOutput>(
query: { location: input["location"] }, {
body: { code: input["code"] }, method: "POST",
successStatus: 204, path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}/complete`,
declaredStatuses: [400, 401], query: { location: input["location"] },
empty: true, body: { code: input["code"] },
}, successStatus: 204,
requestOptions, declaredStatuses: [400, 401],
), empty: true,
attemptCancel: (input: IntegrationAttemptCancelInput, requestOptions?: RequestOptions) => },
request<IntegrationAttemptCancelOutput>( requestOptions,
{ ),
method: "DELETE", cancel: (input: IntegrationAttemptCancelInput, requestOptions?: RequestOptions) =>
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}`, request<IntegrationAttemptCancelOutput>(
query: { location: input["location"] }, {
successStatus: 204, method: "DELETE",
declaredStatuses: [401, 400], path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}`,
empty: true, query: { location: input["location"] },
}, successStatus: 204,
requestOptions, declaredStatuses: [401, 400],
), empty: true,
},
requestOptions,
),
},
}, },
"server.mcp": { "server.mcp": {
list: (input?: ServerMcpListInput, requestOptions?: RequestOptions) => list: (input?: ServerMcpListInput, requestOptions?: RequestOptions) =>
@@ -890,6 +919,20 @@ export function make(options: ClientOptions) {
}, },
requestOptions, requestOptions,
), ),
resource: {
catalog: (input?: ServerMcpResourceCatalogInput, requestOptions?: RequestOptions) =>
request<ServerMcpResourceCatalogOutput>(
{
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) =>
@@ -950,18 +993,20 @@ export function make(options: ClientOptions) {
), ),
}, },
form: { form: {
listRequests: (input?: FormListRequestsInput, requestOptions?: RequestOptions) => request: {
request<FormListRequestsOutput>( list: (input?: FormRequestListInput, requestOptions?: RequestOptions) =>
{ request<FormRequestListOutput>(
method: "GET", {
path: `/api/form/request`, method: "GET",
query: { location: input?.["location"] }, path: `/api/form/request`,
successStatus: 200, query: { location: input?.["location"] },
declaredStatuses: [401, 400], successStatus: 200,
empty: false, declaredStatuses: [401, 400],
}, empty: false,
requestOptions, },
), requestOptions,
),
},
list: (input: FormListInput, requestOptions?: RequestOptions) => list: (input: FormListInput, requestOptions?: RequestOptions) =>
request<{ readonly data: FormListOutput }>( request<{ readonly data: FormListOutput }>(
{ {
@@ -1039,41 +1084,45 @@ export function make(options: ClientOptions) {
), ),
}, },
permission: { permission: {
listRequests: (input?: PermissionListRequestsInput, requestOptions?: RequestOptions) => request: {
request<PermissionListRequestsOutput>( list: (input?: PermissionRequestListInput, requestOptions?: RequestOptions) =>
{ request<PermissionRequestListOutput>(
method: "GET", {
path: `/api/permission/request`, method: "GET",
query: { location: input?.["location"] }, path: `/api/permission/request`,
successStatus: 200, query: { location: input?.["location"] },
declaredStatuses: [401, 400], successStatus: 200,
empty: false, declaredStatuses: [401, 400],
}, empty: false,
requestOptions, },
), requestOptions,
listSaved: (input?: PermissionListSavedInput, requestOptions?: RequestOptions) => ),
request<{ readonly data: PermissionListSavedOutput }>( },
{ saved: {
method: "GET", list: (input?: PermissionSavedListInput, requestOptions?: RequestOptions) =>
path: `/api/permission/saved`, request<{ readonly data: PermissionSavedListOutput }>(
query: { projectID: input?.["projectID"] }, {
successStatus: 200, method: "GET",
declaredStatuses: [401, 400], path: `/api/permission/saved`,
empty: false, query: { projectID: input?.["projectID"] },
}, successStatus: 200,
requestOptions, declaredStatuses: [401, 400],
).then((value) => value.data), empty: false,
removeSaved: (input: PermissionRemoveSavedInput, requestOptions?: RequestOptions) => },
request<PermissionRemoveSavedOutput>( requestOptions,
{ ).then((value) => value.data),
method: "DELETE", remove: (input: PermissionSavedRemoveInput, requestOptions?: RequestOptions) =>
path: `/api/permission/saved/${encodeURIComponent(input.id)}`, request<PermissionSavedRemoveOutput>(
successStatus: 204, {
declaredStatuses: [401, 400], method: "DELETE",
empty: true, path: `/api/permission/saved/${encodeURIComponent(input.id)}`,
}, successStatus: 204,
requestOptions, declaredStatuses: [401, 400],
), empty: true,
},
requestOptions,
),
},
create: (input: PermissionCreateInput, requestOptions?: RequestOptions) => create: (input: PermissionCreateInput, requestOptions?: RequestOptions) =>
request<{ readonly data: PermissionCreateOutput }>( request<{ readonly data: PermissionCreateOutput }>(
{ {
@@ -1355,18 +1404,20 @@ export function make(options: ClientOptions) {
), ),
}, },
question: { question: {
listRequests: (input?: QuestionListRequestsInput, requestOptions?: RequestOptions) => request: {
request<QuestionListRequestsOutput>( list: (input?: QuestionRequestListInput, requestOptions?: RequestOptions) =>
{ request<QuestionRequestListOutput>(
method: "GET", {
path: `/api/question/request`, method: "GET",
query: { location: input?.["location"] }, path: `/api/question/request`,
successStatus: 200, query: { location: input?.["location"] },
declaredStatuses: [401, 400], successStatus: 200,
empty: false, declaredStatuses: [401, 400],
}, empty: false,
requestOptions, },
), requestOptions,
),
},
list: (input: QuestionListInput, requestOptions?: RequestOptions) => list: (input: QuestionListInput, requestOptions?: RequestOptions) =>
request<{ readonly data: QuestionListOutput }>( request<{ readonly data: QuestionListOutput }>(
{ {
@@ -1483,17 +1534,31 @@ export function make(options: ClientOptions) {
), ),
}, },
debug: { debug: {
location: (requestOptions?: RequestOptions) => location: {
request<DebugLocationOutput>( list: (requestOptions?: RequestOptions) =>
{ request<DebugLocationListOutput>(
method: "GET", {
path: `/api/debug/location`, method: "GET",
successStatus: 200, path: `/api/debug/location`,
declaredStatuses: [401, 400], successStatus: 200,
empty: false, declaredStatuses: [401, 400],
}, empty: false,
requestOptions, },
), requestOptions,
),
evict: (input?: DebugLocationEvictInput, requestOptions?: RequestOptions) =>
request<DebugLocationEvictOutput>(
{
method: "DELETE",
path: `/api/debug/location`,
query: { location: input?.["location"] },
successStatus: 204,
declaredStatuses: [401, 400],
empty: true,
},
requestOptions,
),
},
}, },
} }
} }
+111 -11
View File
@@ -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 ServerMcpResourceCatalogInput = {
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
}
export type ServerMcpResourceCatalogOutput = {
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?: {
@@ -2525,13 +2585,13 @@ export type ProjectDirectoriesInput = {
export type ProjectDirectoriesOutput = ReadonlyArray<{ readonly directory: string; readonly strategy?: string }> export type ProjectDirectoriesOutput = ReadonlyArray<{ readonly directory: string; readonly strategy?: string }>
export type FormListRequestsInput = { export type FormRequestListInput = {
readonly location?: { readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"] }["location"]
} }
export type FormListRequestsOutput = { export type FormRequestListOutput = {
readonly location: { readonly location: {
readonly directory: string readonly directory: string
readonly workspaceID?: string readonly workspaceID?: string
@@ -3599,13 +3659,13 @@ export type FormCancelInput = {
export type FormCancelOutput = void export type FormCancelOutput = void
export type PermissionListRequestsInput = { export type PermissionRequestListInput = {
readonly location?: { readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"] }["location"]
} }
export type PermissionListRequestsOutput = { export type PermissionRequestListOutput = {
readonly location: { readonly location: {
readonly directory: string readonly directory: string
readonly workspaceID?: string readonly workspaceID?: string
@@ -3622,9 +3682,9 @@ export type PermissionListRequestsOutput = {
}> }>
} }
export type PermissionListSavedInput = { readonly projectID?: { readonly projectID?: string | undefined }["projectID"] } export type PermissionSavedListInput = { readonly projectID?: { readonly projectID?: string | undefined }["projectID"] }
export type PermissionListSavedOutput = { export type PermissionSavedListOutput = {
readonly data: ReadonlyArray<{ readonly data: ReadonlyArray<{
readonly id: string readonly id: string
readonly projectID: string readonly projectID: string
@@ -3633,9 +3693,9 @@ export type PermissionListSavedOutput = {
}> }>
}["data"] }["data"]
export type PermissionRemoveSavedInput = { readonly id: { readonly id: string }["id"] } export type PermissionSavedRemoveInput = { readonly id: { readonly id: string }["id"] }
export type PermissionRemoveSavedOutput = void export type PermissionSavedRemoveOutput = void
export type PermissionCreateInput = { export type PermissionCreateInput = {
readonly sessionID: { readonly sessionID: string }["sessionID"] readonly sessionID: { readonly sessionID: string }["sessionID"]
@@ -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
@@ -5957,13 +6049,13 @@ export type ShellRemoveInput = {
export type ShellRemoveOutput = void export type ShellRemoveOutput = void
export type QuestionListRequestsInput = { export type QuestionRequestListInput = {
readonly location?: { readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"] }["location"]
} }
export type QuestionListRequestsOutput = { export type QuestionRequestListOutput = {
readonly location: { readonly location: {
readonly directory: string readonly directory: string
readonly workspaceID?: string readonly workspaceID?: string
@@ -6129,4 +6221,12 @@ export type VcsDiffOutput = {
}> }>
} }
export type DebugLocationOutput = ReadonlyArray<{ readonly directory: string; readonly workspaceID?: string }> export type DebugLocationListOutput = ReadonlyArray<{ readonly directory: string; readonly workspaceID?: string }>
export type DebugLocationEvictInput = {
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
}
export type DebugLocationEvictOutput = void
@@ -19,7 +19,7 @@ import { SessionMessage } from "@opencode-ai/schema/session-message"
import { Workspace } from "@opencode-ai/schema/workspace" import { Workspace } from "@opencode-ai/schema/workspace"
import { Api } from "@opencode-ai/server/api" import { Api } from "@opencode-ai/server/api"
import { compile, emitPromise } from "@opencode-ai/httpapi-codegen" import { compile, emitPromise } from "@opencode-ai/httpapi-codegen"
import { ClientApi, endpointNames, groupNames, promiseOmitEndpoints } from "../src/contract" import { ClientApi, groupNames, promiseOmitEndpoints } from "../src/contract"
const Client = await import("../src/effect") const Client = await import("../src/effect")
@@ -49,8 +49,8 @@ test("Core and Server reuse the authoritative Schema and Protocol values", () =>
}) })
test("client and Server contracts generate identically", () => { test("client and Server contracts generate identically", () => {
const server = compile(Api, { groupNames, endpointNames, omitEndpoints: promiseOmitEndpoints }) const server = compile(Api, { groupNames, omitEndpoints: promiseOmitEndpoints })
const client = compile(ClientApi, { groupNames, endpointNames, omitEndpoints: promiseOmitEndpoints }) const client = compile(ClientApi, { groupNames, omitEndpoints: promiseOmitEndpoints })
expect(emitPromise(client)).toEqual(emitPromise(server)) expect(emitPromise(client)).toEqual(emitPromise(server))
}) })
+27 -9
View File
@@ -33,16 +33,11 @@ test("exposes every standard HTTP API group", () => {
"debug", "debug",
]) ])
expect(Object.keys(client.debug)).toEqual(["location"]) expect(Object.keys(client.debug)).toEqual(["location"])
expect(Object.keys(client.debug.location)).toEqual(["list", "evict"])
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", "get", "connect", "attempt"])
"list", expect(Object.keys(client.integration.connect)).toEqual(["key", "oauth"])
"get", expect(Object.keys(client.integration.attempt)).toEqual(["status", "complete", "cancel"])
"connectKey",
"connectOauth",
"attemptStatus",
"attemptComplete",
"attemptCancel",
])
expect(Object.keys(client.file)).toEqual(["read", "list", "find"]) expect(Object.keys(client.file)).toEqual(["read", "list", "find"])
expect(Object.keys(client.vcs)).toEqual(["status", "diff"]) expect(Object.keys(client.vcs)).toEqual(["status", "diff"])
expect(Object.keys(client.pty)).toEqual(["list", "create", "get", "update", "remove"]) expect(Object.keys(client.pty)).toEqual(["list", "create", "get", "update", "remove"])
@@ -50,6 +45,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"].resource.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({
+2 -2
View File
@@ -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).
+1 -1
View File
@@ -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" }
+24 -37
View File
@@ -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)
+2
View File
@@ -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
+14 -2
View File
@@ -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)
} }
+18 -15
View File
@@ -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":
+1
View File
@@ -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 `{}`.",
] ]
+21
View File
@@ -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(
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -177,13 +177,13 @@ describe("OpenAPI.fromSpec", () => {
const spec = await opencodeSpec() const spec = await opencodeSpec()
const result = OpenAPI.fromSpec({ spec, baseUrl }) const result = OpenAPI.fromSpec({ spec, baseUrl })
expect(result.skipped).toHaveLength(5) expect(result.skipped).toHaveLength(4)
expect(result.skipped).toContainEqual({ expect(result.skipped).toContainEqual({
method: "GET", method: "GET",
path: "/api/pty/{ptyID}/connect", path: "/api/pty/{ptyID}/connect",
reason: "WebSocket operations are not supported", reason: "WebSocket operations are not supported",
}) })
expect(result.skipped.filter((item) => item.reason === "SSE operations are not supported")).toHaveLength(3) expect(result.skipped.filter((item) => item.reason === "SSE operations are not supported")).toHaveLength(2)
expect(result.skipped).toContainEqual({ expect(result.skipped).toContainEqual({
method: "GET", method: "GET",
path: "/api/fs/read/*", path: "/api/fs/read/*",
@@ -210,11 +210,11 @@ describe("OpenAPI.fromSpec", () => {
if (!Tool.isDefinition(instructionPut)) throw new Error("v2.session.instructions.entry.put was not generated") if (!Tool.isDefinition(instructionPut)) throw new Error("v2.session.instructions.entry.put was not generated")
expect(inputTypeScript(instructionPut)).toBe("{ sessionID: string; key: string; value: unknown }") expect(inputTypeScript(instructionPut)).toBe("{ sessionID: string; key: string; value: unknown }")
expect(toolAt(result.tools, "v2_session_instructions_entry_put_2")).toBeUndefined() expect(toolAt(result.tools, "v2_session_instructions_entry_put_2")).toBeUndefined()
expect(toolAt(result.tools, "v2.pty.connect")).toBeUndefined() expect(Tool.isDefinition(toolAt(result.tools, "v2.pty.connect"))).toBe(false)
expect(toolAt(result.tools, "v2.session.log")).toBeUndefined() expect(toolAt(result.tools, "v2.session.log")).toBeUndefined()
expect(toolAt(result.tools, "v2.event.subscribe")).toBeUndefined() expect(toolAt(result.tools, "v2.event.subscribe")).toBeUndefined()
expect(toolAt(result.tools, "v2.fs.read")).toBeUndefined() expect(toolAt(result.tools, "v2.fs.read")).toBeUndefined()
expect(toolAt(result.tools, "v2.pty.connectToken")).not.toBeUndefined() expect(toolAt(result.tools, "v2.pty.connect.token")).not.toBeUndefined()
}) })
test("preserves operation path sanitization and collision handling", () => { test("preserves operation path sanitization and collision handling", () => {
+6
View File
@@ -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")
+106
View File
@@ -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)
}) })
+3 -2
View File
@@ -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) {
+1 -1
View File
@@ -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.
@@ -29,6 +29,7 @@ const layer = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const location = yield* Location.Service const location = yield* Location.Service
if (!location.vcs) return Service.of({})
const watcher = yield* Watcher.Service const watcher = yield* Watcher.Service
const events = yield* EventV2.Service const events = yield* EventV2.Service
const fs = yield* FSUtil.Service const fs = yield* FSUtil.Service
+35 -22
View File
@@ -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
View File
@@ -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) })
+80 -50
View File
@@ -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,
})
}), }),
}) })
}), }),
+13 -6
View File
@@ -1,6 +1,6 @@
export * as PluginV2 from "./plugin" export * as PluginV2 from "./plugin"
import type { Plugin } from "@opencode-ai/plugin/v2/effect" import type { Plugin } from "@opencode-ai/plugin/v2/effect/plugin"
import { Event, ID, type Info } from "@opencode-ai/schema/plugin" import { Event, ID, type Info } from "@opencode-ai/schema/plugin"
import { makeLocationNode } from "./effect/app-node" import { makeLocationNode } from "./effect/app-node"
import { Context, Effect, Exit, Layer, Scope, Semaphore } from "effect" import { Context, Effect, Exit, Layer, Scope, Semaphore } from "effect"
@@ -18,6 +18,7 @@ import { SkillV2 } from "./skill"
import { State } from "./state" import { State } from "./state"
import { ToolRegistry } from "./tool/registry" import { ToolRegistry } from "./tool/registry"
import { ToolHooks } from "./tool/hooks" import { ToolHooks } from "./tool/hooks"
import { PluginHooks } from "./plugin/hooks"
export interface Interface { export interface Interface {
readonly activate: (plugins: readonly { readonly plugin: Plugin; readonly version?: string }[]) => Effect.Effect<void> readonly activate: (plugins: readonly { readonly plugin: Plugin; readonly version?: string }[]) => Effect.Effect<void>
@@ -57,12 +58,13 @@ const layer = Layer.effect(
generation.length === definitions.length && generation.length === definitions.length &&
generation.every( generation.every(
(plugin, index) => plugin.id === definitions[index]?.id && plugin.version === definitions[index]?.version, (plugin, index) => plugin.id === definitions[index]?.id && plugin.version === definitions[index]?.version,
) ) &&
definitions.every((definition) => active.has(definition.id))
) { ) {
return return
} }
generation = undefined generation = undefined
const exit = yield* State.batch( yield* State.batch(
Effect.gen(function* () { Effect.gen(function* () {
const scopes = Array.from(active.values()).toReversed() const scopes = Array.from(active.values()).toReversed()
active.clear() active.clear()
@@ -81,13 +83,17 @@ const layer = Layer.effect(
Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(child, exit) : Effect.void)), Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(child, exit) : Effect.void)),
Effect.exit, Effect.exit,
) )
if (Exit.isFailure(loaded)) return loaded if (Exit.isFailure(loaded)) {
yield* Effect.logWarning("failed to load plugin", {
"plugin.id": definition.id,
cause: loaded.cause,
})
continue
}
active.set(definition.id, child) active.set(definition.id, child)
} }
return Exit.void
}), }),
) )
if (Exit.isFailure(exit)) return yield* exit
generation = definitions.map((definition) => ({ generation = definitions.map((definition) => ({
id: definition.id, id: definition.id,
...(definition.version === undefined ? {} : { version: definition.version }), ...(definition.version === undefined ? {} : { version: definition.version }),
@@ -131,6 +137,7 @@ export const node = makeLocationNode({
SkillV2.node, SkillV2.node,
ToolRegistry.toolsNode, ToolRegistry.toolsNode,
ToolHooks.node, ToolHooks.node,
PluginHooks.node,
PluginRuntime.node, PluginRuntime.node,
], ],
}) })
+67
View File
@@ -0,0 +1,67 @@
export * as PluginHooks from "./hooks"
import type { AISDKHooks } from "@opencode-ai/plugin/v2/effect/aisdk"
import type { SessionHooks } from "@opencode-ai/plugin/v2/effect/session"
import type { ToolHooks } from "@opencode-ai/plugin/v2/effect/tool"
import { Context, Effect, Layer, Scope } from "effect"
import { makeLocationNode } from "../effect/app-node"
import { State } from "../state"
export interface Domains {
readonly aisdk: AISDKHooks
readonly session: SessionHooks
readonly tool: ToolHooks
}
type Callback<Event> = (event: Event) => Effect.Effect<void>
export interface Interface {
readonly register: <Domain extends keyof Domains, Name extends keyof Domains[Domain]>(
domain: Domain,
name: Name,
callback: Callback<Domains[Domain][Name]>,
) => Effect.Effect<State.Registration, never, Scope.Scope>
readonly trigger: <Domain extends keyof Domains, Name extends keyof Domains[Domain]>(
domain: Domain,
name: Name,
event: Domains[Domain][Name],
) => Effect.Effect<Domains[Domain][Name]>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/PluginHooks") {}
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const callbacks = new Map<string, Function[]>()
const key = (domain: keyof Domains, name: PropertyKey) => `${domain}.${String(name)}`
const register: Interface["register"] = Effect.fn("PluginHooks.register")(function* (domain, name, callback) {
const scope = yield* Scope.Scope
const id = key(domain, name)
let active = true
callbacks.set(id, [...(callbacks.get(id) ?? []), callback])
const dispose = Effect.sync(() => {
if (!active) return
active = false
const next = (callbacks.get(id) ?? []).filter((item) => item !== callback)
if (next.length === 0) callbacks.delete(id)
else callbacks.set(id, next)
})
yield* Scope.addFinalizer(scope, dispose)
return { dispose }
})
const trigger: Interface["trigger"] = Effect.fn("PluginHooks.trigger")(function* (domain, name, event) {
for (const callback of callbacks.get(key(domain, name)) ?? []) {
const result: Effect.Effect<void> = Reflect.apply(callback, undefined, [event])
yield* result
}
return event
})
return Service.of({ register, trigger })
}),
)
export const node = makeLocationNode({ service: Service, layer, deps: [] })
+74 -68
View File
@@ -1,6 +1,6 @@
export * as PluginHost from "./host" export * as PluginHost from "./host"
import type { PluginContext } from "@opencode-ai/plugin/v2/effect" import type { Plugin } from "@opencode-ai/plugin/v2/effect"
import { EventManifest } from "@opencode-ai/schema/event-manifest" import { EventManifest } from "@opencode-ai/schema/event-manifest"
import { Effect, Schema, Stream } from "effect" import { Effect, Schema, Stream } from "effect"
import { AgentV2 } from "../agent" import { AgentV2 } from "../agent"
@@ -22,6 +22,7 @@ import { Tool } from "../tool/tool"
import { Tools } from "../tool/tools" import { Tools } from "../tool/tools"
import { ToolHooks } from "../tool/hooks" import { ToolHooks } from "../tool/hooks"
import { WorkspaceV2 } from "../workspace" import { WorkspaceV2 } from "../workspace"
import { PluginHooks } from "./hooks"
const mutable = <T>(value: T) => value as DeepMutable<T> const mutable = <T>(value: T) => value as DeepMutable<T>
export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Interface) { export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Interface) {
@@ -36,6 +37,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
const skill = yield* SkillV2.Service const skill = yield* SkillV2.Service
const tools = yield* Tools.Service const tools = yield* Tools.Service
const toolHooks = yield* ToolHooks.Service const toolHooks = yield* ToolHooks.Service
const hooks = yield* PluginHooks.Service
const runtime = yield* PluginRuntime.Service const runtime = yield* PluginRuntime.Service
const locationInfo = () => const locationInfo = () =>
new Location.Info({ new Location.Info({
@@ -43,7 +45,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
workspaceID: location.workspaceID, workspaceID: location.workspaceID,
project: location.project, project: location.project,
}) })
const locationRef = (input?: Parameters<PluginContext["agent"]["list"]>[0]) => const locationRef = (input?: Parameters<Plugin.Context["agent"]["list"]>[0]) =>
input?.location === undefined input?.location === undefined
? undefined ? undefined
: Location.Ref.make({ : Location.Ref.make({
@@ -79,32 +81,32 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
}), }),
}, },
aisdk: { aisdk: {
sdk: (callback) => hook: (name, callback) => {
aisdk.hook.sdk((event) => { if (name === "sdk") {
return aisdk.hook.sdk((event) => {
const output = {
model: mutable(event.model),
package: event.package,
options: event.options,
sdk: event.sdk,
}
return Reflect.apply(callback, undefined, [output]).pipe(
Effect.tap(() => Effect.sync(() => (event.sdk = output.sdk))),
)
})
}
return aisdk.hook.language((event) => {
const output = { const output = {
model: mutable(event.model), model: mutable(event.model),
package: event.package,
options: event.options, options: event.options,
sdk: event.sdk, sdk: event.sdk,
}
const result = callback(output)
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
Effect.tap(() => Effect.sync(() => (event.sdk = output.sdk))),
)
}),
language: (callback) =>
aisdk.hook.language((event) => {
const output = {
model: mutable(event.model),
sdk: event.sdk,
options: event.options,
language: event.language, language: event.language,
} }
const result = callback(output) return Reflect.apply(callback, undefined, [output]).pipe(
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
Effect.tap(() => Effect.sync(() => (event.language = output.language))), Effect.tap(() => Effect.sync(() => (event.language = output.language))),
) )
}), })
},
}, },
catalog: { catalog: {
provider: { provider: {
@@ -164,25 +166,29 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
integration: { integration: {
list: () => response(integration.list()), list: () => response(integration.list()),
get: (input) => response(integration.get(Integration.ID.make(input.integrationID))), get: (input) => response(integration.get(Integration.ID.make(input.integrationID))),
connectKey: (input) => connect: {
integration.connection.key({ key: (input) =>
integrationID: Integration.ID.make(input.integrationID), integration.connection.key({
key: input.key,
label: input.label,
}),
connectOauth: (input) =>
response(
integration.connection.oauth({
integrationID: Integration.ID.make(input.integrationID), integrationID: Integration.ID.make(input.integrationID),
methodID: Integration.MethodID.make(input.methodID), key: input.key,
inputs: input.inputs,
label: input.label, label: input.label,
}), }),
), oauth: (input) =>
attemptStatus: (input) => response(integration.attempt.status(Integration.AttemptID.make(input.attemptID))), response(
attemptComplete: (input) => integration.connection.oauth({
integration.attempt.complete({ attemptID: Integration.AttemptID.make(input.attemptID), code: input.code }), integrationID: Integration.ID.make(input.integrationID),
attemptCancel: (input) => integration.attempt.cancel(Integration.AttemptID.make(input.attemptID)), methodID: Integration.MethodID.make(input.methodID),
inputs: input.inputs,
label: input.label,
}),
),
},
attempt: {
status: (input) => response(integration.attempt.status(Integration.AttemptID.make(input.attemptID))),
complete: (input) =>
integration.attempt.complete({ attemptID: Integration.AttemptID.make(input.attemptID), code: input.code }),
cancel: (input) => integration.attempt.cancel(Integration.AttemptID.make(input.attemptID)),
},
reload: integration.reload, reload: integration.reload,
connection: { connection: {
active: (id) => integration.connection.active(Integration.ID.make(id)), active: (id) => integration.connection.active(Integration.ID.make(id)),
@@ -317,11 +323,12 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
registrations, registrations,
(registration) => tools.register({ [registration.name]: registration.tool }, registration.options), (registration) => tools.register({ [registration.name]: registration.tool }, registration.options),
{ discard: true }, { discard: true },
) ).pipe(Effect.orDie)
return { dispose: Effect.void }
}), }),
execute: { hook: (name, callback) => {
before: (callback) => if (name === "execute.before") {
toolHooks.hook.before((event) => { return toolHooks.hook.before((event) => {
const output = { const output = {
tool: event.tool, tool: event.tool,
sessionID: event.sessionID, sessionID: event.sessionID,
@@ -330,38 +337,37 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
toolCallID: event.toolCallID, toolCallID: event.toolCallID,
input: event.input, input: event.input,
} }
const result = callback(output) return Reflect.apply(callback, undefined, [output]).pipe(
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
Effect.tap(() => Effect.sync(() => (event.input = output.input))), Effect.tap(() => Effect.sync(() => (event.input = output.input))),
) )
}), })
after: (callback) => }
toolHooks.hook.after((event) => { return toolHooks.hook.after((event) => {
const output = { const output = {
tool: event.tool, tool: event.tool,
sessionID: event.sessionID, sessionID: event.sessionID,
agent: event.agent, agent: event.agent,
assistantMessageID: event.assistantMessageID, assistantMessageID: event.assistantMessageID,
toolCallID: event.toolCallID, toolCallID: event.toolCallID,
input: event.input, input: event.input,
result: event.result, result: event.result,
output: event.output, output: event.output,
outputPaths: event.outputPaths, outputPaths: event.outputPaths,
} }
const result = callback(output) return Reflect.apply(callback, undefined, [output]).pipe(
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe( Effect.tap(() =>
Effect.tap(() => Effect.sync(() => {
Effect.sync(() => { event.result = output.result
event.result = output.result event.output = output.output
event.output = output.output event.outputPaths = output.outputPaths
event.outputPaths = output.outputPaths }),
}), ),
), )
) })
}),
}, },
}, },
session: { session: {
hook: (name, callback) => hooks.register("session", name, callback),
create: (input) => create: (input) =>
runtime.session.create({ runtime.session.create({
id: input?.id, id: input?.id,
@@ -375,5 +381,5 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
command: runtime.session.command, command: runtime.session.command,
interrupt: (input) => runtime.session.interrupt(input.sessionID), interrupt: (input) => runtime.session.interrupt(input.sessionID),
}, },
} satisfies PluginContext } satisfies Plugin.Context
}) })
+1 -1
View File
@@ -1,6 +1,6 @@
export * as PluginInternal from "./internal" export * as PluginInternal from "./internal"
import type { Plugin } from "@opencode-ai/plugin/v2/effect" import type { Plugin } from "@opencode-ai/plugin/v2/effect/plugin"
import { Context, Effect, Scope } from "effect" import { Context, Effect, Scope } from "effect"
import { HttpClient } from "effect/unstable/http" import { HttpClient } from "effect/unstable/http"
import { AgentV2 } from "../agent" import { AgentV2 } from "../agent"
+24 -14
View File
@@ -1,11 +1,12 @@
export * as PluginPromise from "./promise" export * as PluginPromise from "./promise"
import { define } from "@opencode-ai/plugin/v2/effect" import { Plugin } from "@opencode-ai/plugin/v2/effect"
import type { Plugin, PluginContext } from "@opencode-ai/plugin/v2/promise"
import { Effect, Scope, Stream } from "effect" import { Effect, Scope, Stream } from "effect"
type HostRegistration = { readonly dispose: Effect.Effect<void> } type HostRegistration = { readonly dispose: Effect.Effect<void> }
type Registration = { readonly dispose: () => Promise<void> } type Registration = { readonly dispose: () => Promise<void> }
type PromisePlugin = import("@opencode-ai/plugin/v2/plugin").Plugin
type PromisePluginContext = import("@opencode-ai/plugin/v2/plugin").Context
/** /**
* Adapts a Promise plugin into an Effect plugin so the existing Effect-only * Adapts a Promise plugin into an Effect plugin so the existing Effect-only
@@ -16,8 +17,8 @@ type Registration = { readonly dispose: () => Promise<void> }
* preserves boot-time batching, so Promise-plugin transforms still coalesce * preserves boot-time batching, so Promise-plugin transforms still coalesce
* into one reload per domain. * into one reload per domain.
*/ */
export function fromPromise(plugin: Plugin) { export function fromPromise(plugin: PromisePlugin) {
return define({ return Plugin.define({
id: plugin.id, id: plugin.id,
effect: (host) => effect: (host) =>
Effect.gen(function* () { Effect.gen(function* () {
@@ -43,7 +44,7 @@ export function fromPromise(plugin: Plugin) {
}), }),
) )
const context2: PluginContext = { const context2: PromisePluginContext = {
options: host.options, options: host.options,
agent: { agent: {
list: (input) => run(host.agent.list(input)), list: (input) => run(host.agent.list(input)),
@@ -51,10 +52,8 @@ export function fromPromise(plugin: Plugin) {
reload: () => run(host.agent.reload()), reload: () => run(host.agent.reload()),
}, },
aisdk: { aisdk: {
sdk: (callback) => hook: (name, callback) =>
register(host.aisdk.sdk((event) => Effect.promise(() => Promise.resolve(callback(event))))), register(host.aisdk.hook(name, (event) => Effect.promise(() => Promise.resolve(callback(event))))),
language: (callback) =>
register(host.aisdk.language((event) => Effect.promise(() => Promise.resolve(callback(event))))),
}, },
catalog: { catalog: {
provider: { provider: {
@@ -79,11 +78,15 @@ export function fromPromise(plugin: Plugin) {
integration: { integration: {
list: (input) => run(host.integration.list(input)), list: (input) => run(host.integration.list(input)),
get: (input) => run(host.integration.get(input)), get: (input) => run(host.integration.get(input)),
connectKey: (input) => run(host.integration.connectKey(input)), connect: {
connectOauth: (input) => run(host.integration.connectOauth(input)), key: (input) => run(host.integration.connect.key(input)),
attemptStatus: (input) => run(host.integration.attemptStatus(input)), oauth: (input) => run(host.integration.connect.oauth(input)),
attemptComplete: (input) => run(host.integration.attemptComplete(input)), },
attemptCancel: (input) => run(host.integration.attemptCancel(input)), attempt: {
status: (input) => run(host.integration.attempt.status(input)),
complete: (input) => run(host.integration.attempt.complete(input)),
cancel: (input) => run(host.integration.attempt.cancel(input)),
},
transform: transform(host.integration), transform: transform(host.integration),
reload: () => run(host.integration.reload()), reload: () => run(host.integration.reload()),
connection: { connection: {
@@ -104,12 +107,19 @@ export function fromPromise(plugin: Plugin) {
transform: transform(host.skill), transform: transform(host.skill),
reload: () => run(host.skill.reload()), reload: () => run(host.skill.reload()),
}, },
tool: {
transform: transform(host.tool),
hook: (name, callback) =>
register(host.tool.hook(name, (event) => Effect.promise(() => Promise.resolve(callback(event))))),
},
session: { session: {
create: (input) => run(host.session.create(input)), create: (input) => run(host.session.create(input)),
get: (input) => run(host.session.get(input)), get: (input) => run(host.session.get(input)),
prompt: (input) => run(host.session.prompt(input)), prompt: (input) => run(host.session.prompt(input)),
command: (input) => run(host.session.command(input)), command: (input) => run(host.session.command(input)),
interrupt: (input) => run(host.session.interrupt(input)), interrupt: (input) => run(host.session.interrupt(input)),
hook: (name, callback) =>
register(host.session.hook(name, (event) => Effect.promise(() => Promise.resolve(callback(event))))),
}, },
} }
+2 -1
View File
@@ -4,7 +4,8 @@ import { define } from "@opencode-ai/plugin/v2/effect/plugin"
export const AlibabaPlugin = define({ export const AlibabaPlugin = define({
id: "opencode.provider.alibaba", id: "opencode.provider.alibaba",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.sdk( yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/alibaba") return if (evt.package !== "@ai-sdk/alibaba") return
const mod = yield* Effect.promise(() => import("@ai-sdk/alibaba")) const mod = yield* Effect.promise(() => import("@ai-sdk/alibaba"))
@@ -75,7 +75,8 @@ export const AmazonBedrockPlugin = define({
}) })
} }
}) })
yield* ctx.aisdk.sdk( yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (!["@ai-sdk/amazon-bedrock", "@ai-sdk/amazon-bedrock/mantle"].includes(evt.package)) return if (!["@ai-sdk/amazon-bedrock", "@ai-sdk/amazon-bedrock/mantle"].includes(evt.package)) return
const options = { ...evt.options } const options = { ...evt.options }
@@ -108,7 +109,8 @@ export const AmazonBedrockPlugin = define({
evt.sdk = mod.createAmazonBedrock(options) evt.sdk = mod.createAmazonBedrock(options)
}), }),
) )
yield* ctx.aisdk.language( yield* ctx.aisdk.hook(
"language",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.amazonBedrock) return if (evt.model.providerID !== ProviderV2.ID.amazonBedrock) return
if ( if (
@@ -17,7 +17,8 @@ export const AnthropicPlugin = define({
}) })
} }
}) })
yield* ctx.aisdk.sdk( yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/anthropic") return if (evt.package !== "@ai-sdk/anthropic") return
const mod = yield* Effect.promise(() => import("@ai-sdk/anthropic")) const mod = yield* Effect.promise(() => import("@ai-sdk/anthropic"))
+6 -3
View File
@@ -26,7 +26,8 @@ export const AzurePlugin = define({
}) })
} }
}) })
yield* ctx.aisdk.sdk( yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/azure") return if (evt.package !== "@ai-sdk/azure") return
if (evt.model.providerID === ProviderV2.ID.azure) { if (evt.model.providerID === ProviderV2.ID.azure) {
@@ -44,7 +45,8 @@ export const AzurePlugin = define({
evt.sdk = mod.createAzure(evt.options) evt.sdk = mod.createAzure(evt.options)
}), }),
) )
yield* ctx.aisdk.language( yield* ctx.aisdk.hook(
"language",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.azure) return if (evt.model.providerID !== ProviderV2.ID.azure) return
evt.language = selectLanguage( evt.language = selectLanguage(
@@ -75,7 +77,8 @@ export const AzureCognitiveServicesPlugin = define({
}) })
} }
}) })
yield* ctx.aisdk.language( yield* ctx.aisdk.hook(
"language",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("azure-cognitive-services")) return if (evt.model.providerID !== ProviderV2.ID.make("azure-cognitive-services")) return
evt.language = selectLanguage( evt.language = selectLanguage(
@@ -14,7 +14,8 @@ export const CerebrasPlugin = define({
}) })
} }
}) })
yield* ctx.aisdk.sdk( yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/cerebras") return if (evt.package !== "@ai-sdk/cerebras") return
const mod = yield* Effect.promise(() => import("@ai-sdk/cerebras")) const mod = yield* Effect.promise(() => import("@ai-sdk/cerebras"))
@@ -6,7 +6,8 @@ import { define } from "@opencode-ai/plugin/v2/effect/plugin"
export const CloudflareAIGatewayPlugin = define({ export const CloudflareAIGatewayPlugin = define({
id: "opencode.provider.cloudflare-ai-gateway", id: "opencode.provider.cloudflare-ai-gateway",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.sdk( yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "ai-gateway-provider") return if (evt.package !== "ai-gateway-provider") return
if (evt.options.baseURL) return if (evt.options.baseURL) return
@@ -19,7 +19,8 @@ export const CloudflareWorkersAIPlugin = define({
if (accountId) provider.settings = { ...provider.settings, baseURL: workersEndpoint(accountId) } if (accountId) provider.settings = { ...provider.settings, baseURL: workersEndpoint(accountId) }
}) })
}) })
yield* ctx.aisdk.sdk( yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID !== providerID) return if (evt.model.providerID !== providerID) return
if (evt.package !== "@ai-sdk/openai-compatible") return if (evt.package !== "@ai-sdk/openai-compatible") return
@@ -35,7 +36,8 @@ export const CloudflareWorkersAIPlugin = define({
) )
}), }),
) )
yield* ctx.aisdk.language( yield* ctx.aisdk.hook(
"language",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID !== providerID) return if (evt.model.providerID !== providerID) return
evt.language = evt.sdk.languageModel(evt.model.modelID ?? evt.model.id) evt.language = evt.sdk.languageModel(evt.model.modelID ?? evt.model.id)
+2 -1
View File
@@ -4,7 +4,8 @@ import { define } from "@opencode-ai/plugin/v2/effect/plugin"
export const CoherePlugin = define({ export const CoherePlugin = define({
id: "opencode.provider.cohere", id: "opencode.provider.cohere",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.sdk( yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/cohere") return if (evt.package !== "@ai-sdk/cohere") return
const mod = yield* Effect.promise(() => import("@ai-sdk/cohere")) const mod = yield* Effect.promise(() => import("@ai-sdk/cohere"))
@@ -4,7 +4,8 @@ import { define } from "@opencode-ai/plugin/v2/effect/plugin"
export const DeepInfraPlugin = define({ export const DeepInfraPlugin = define({
id: "opencode.provider.deepinfra", id: "opencode.provider.deepinfra",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.sdk( yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/deepinfra") return if (evt.package !== "@ai-sdk/deepinfra") return
const mod = yield* Effect.promise(() => import("@ai-sdk/deepinfra")) const mod = yield* Effect.promise(() => import("@ai-sdk/deepinfra"))
+2 -1
View File
@@ -7,7 +7,8 @@ export const DynamicProviderPlugin = define({
id: "opencode.provider.dynamic", id: "opencode.provider.dynamic",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
const npm = yield* Npm.Service const npm = yield* Npm.Service
yield* ctx.aisdk.sdk( yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.sdk) return if (evt.sdk) return
+2 -1
View File
@@ -4,7 +4,8 @@ import { define } from "@opencode-ai/plugin/v2/effect/plugin"
export const GatewayPlugin = define({ export const GatewayPlugin = define({
id: "opencode.provider.gateway", id: "opencode.provider.gateway",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.sdk( yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/gateway") return if (evt.package !== "@ai-sdk/gateway") return
const mod = yield* Effect.promise(() => import("@ai-sdk/gateway")) const mod = yield* Effect.promise(() => import("@ai-sdk/gateway"))
@@ -23,14 +23,16 @@ export const GithubCopilotPlugin = define({
model.enabled = false model.enabled = false
}) })
}) })
yield* ctx.aisdk.sdk( yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/github-copilot") return if (evt.package !== "@ai-sdk/github-copilot") return
const mod = yield* Effect.promise(() => import("../../github-copilot/copilot-provider")) const mod = yield* Effect.promise(() => import("../../github-copilot/copilot-provider"))
evt.sdk = mod.createOpenaiCompatible(evt.options) evt.sdk = mod.createOpenaiCompatible(evt.options)
}), }),
) )
yield* ctx.aisdk.language( yield* ctx.aisdk.hook(
"language",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.githubCopilot) return if (evt.model.providerID !== ProviderV2.ID.githubCopilot) return
if (evt.sdk.responses === undefined && evt.sdk.chat === undefined) { if (evt.sdk.responses === undefined && evt.sdk.chat === undefined) {
+4 -2
View File
@@ -7,7 +7,8 @@ import { ProviderV2 } from "../../provider"
export const GitLabPlugin = define({ export const GitLabPlugin = define({
id: "opencode.provider.gitlab", id: "opencode.provider.gitlab",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.sdk( yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "gitlab-ai-provider") return if (evt.package !== "gitlab-ai-provider") return
const mod = yield* Effect.promise(() => import("gitlab-ai-provider")) const mod = yield* Effect.promise(() => import("gitlab-ai-provider"))
@@ -31,7 +32,8 @@ export const GitLabPlugin = define({
}) })
}), }),
) )
yield* ctx.aisdk.language( yield* ctx.aisdk.hook(
"language",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.gitlab) return if (evt.model.providerID !== ProviderV2.ID.gitlab) return
const featureFlags = const featureFlags =
@@ -85,7 +85,8 @@ export const GoogleVertexPlugin = define({
}) })
} }
}) })
yield* ctx.aisdk.sdk( yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID === ProviderV2.ID.googleVertex && evt.package.includes("@ai-sdk/openai-compatible")) { if (evt.model.providerID === ProviderV2.ID.googleVertex && evt.package.includes("@ai-sdk/openai-compatible")) {
evt.options.fetch = authFetch(evt.options.fetch) evt.options.fetch = authFetch(evt.options.fetch)
@@ -104,7 +105,8 @@ export const GoogleVertexPlugin = define({
}) })
}), }),
) )
yield* ctx.aisdk.language( yield* ctx.aisdk.hook(
"language",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.googleVertex) return if (evt.model.providerID !== ProviderV2.ID.googleVertex) return
evt.language = evt.sdk.languageModel(String(evt.model.modelID ?? evt.model.id).trim()) evt.language = evt.sdk.languageModel(String(evt.model.modelID ?? evt.model.id).trim())
@@ -135,7 +137,8 @@ export const GoogleVertexAnthropicPlugin = define({
}) })
} }
}) })
yield* ctx.aisdk.sdk( yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/google-vertex/anthropic") return if (evt.package !== "@ai-sdk/google-vertex/anthropic") return
const mod = yield* Effect.promise(() => import("@ai-sdk/google-vertex/anthropic")) const mod = yield* Effect.promise(() => import("@ai-sdk/google-vertex/anthropic"))
@@ -161,7 +164,8 @@ export const GoogleVertexAnthropicPlugin = define({
}) })
}), }),
) )
yield* ctx.aisdk.language( yield* ctx.aisdk.hook(
"language",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("google-vertex-anthropic")) return if (evt.model.providerID !== ProviderV2.ID.make("google-vertex-anthropic")) return
evt.language = evt.sdk.languageModel(String(evt.model.modelID ?? evt.model.id).trim()) evt.language = evt.sdk.languageModel(String(evt.model.modelID ?? evt.model.id).trim())
+2 -1
View File
@@ -4,7 +4,8 @@ import { define } from "@opencode-ai/plugin/v2/effect/plugin"
export const GooglePlugin = define({ export const GooglePlugin = define({
id: "opencode.provider.google", id: "opencode.provider.google",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.sdk( yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/google") return if (evt.package !== "@ai-sdk/google") return
const mod = yield* Effect.promise(() => import("@ai-sdk/google")) const mod = yield* Effect.promise(() => import("@ai-sdk/google"))
+2 -1
View File
@@ -4,7 +4,8 @@ import { define } from "@opencode-ai/plugin/v2/effect/plugin"
export const GroqPlugin = define({ export const GroqPlugin = define({
id: "opencode.provider.groq", id: "opencode.provider.groq",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.sdk( yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/groq") return if (evt.package !== "@ai-sdk/groq") return
const mod = yield* Effect.promise(() => import("@ai-sdk/groq")) const mod = yield* Effect.promise(() => import("@ai-sdk/groq"))
+2 -1
View File
@@ -4,7 +4,8 @@ import { define } from "@opencode-ai/plugin/v2/effect/plugin"
export const MistralPlugin = define({ export const MistralPlugin = define({
id: "opencode.provider.mistral", id: "opencode.provider.mistral",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.sdk( yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/mistral") return if (evt.package !== "@ai-sdk/mistral") return
const mod = yield* Effect.promise(() => import("@ai-sdk/mistral")) const mod = yield* Effect.promise(() => import("@ai-sdk/mistral"))
@@ -4,7 +4,8 @@ import { define } from "@opencode-ai/plugin/v2/effect/plugin"
export const OpenAICompatiblePlugin = define({ export const OpenAICompatiblePlugin = define({
id: "opencode.provider.openai-compatible", id: "opencode.provider.openai-compatible",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.sdk( yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.sdk) return if (evt.sdk) return
if (!evt.package.includes("@ai-sdk/openai-compatible")) return if (!evt.package.includes("@ai-sdk/openai-compatible")) return
+4 -2
View File
@@ -210,14 +210,16 @@ export const OpenAIPlugin = define({
Effect.forkScoped({ startImmediately: true }), Effect.forkScoped({ startImmediately: true }),
) )
yield* refresh().pipe(Effect.forkScoped) yield* refresh().pipe(Effect.forkScoped)
yield* ctx.aisdk.sdk( yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/openai") return if (evt.package !== "@ai-sdk/openai") return
const mod = yield* Effect.promise(() => import("@ai-sdk/openai")) const mod = yield* Effect.promise(() => import("@ai-sdk/openai"))
evt.sdk = mod.createOpenAI(evt.options) evt.sdk = mod.createOpenAI(evt.options)
}), }),
) )
yield* ctx.aisdk.language( yield* ctx.aisdk.hook(
"language",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.openai) return if (evt.model.providerID !== ProviderV2.ID.openai) return
evt.language = evt.sdk.responses(evt.model.modelID ?? evt.model.id) evt.language = evt.sdk.responses(evt.model.modelID ?? evt.model.id)
@@ -23,7 +23,8 @@ export const OpenRouterPlugin = define({
} }
} }
}) })
yield* ctx.aisdk.sdk( yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@openrouter/ai-sdk-provider") return if (evt.package !== "@openrouter/ai-sdk-provider") return
const mod = yield* Effect.promise(() => import("@openrouter/ai-sdk-provider")) const mod = yield* Effect.promise(() => import("@openrouter/ai-sdk-provider"))
@@ -4,7 +4,8 @@ import { define } from "@opencode-ai/plugin/v2/effect/plugin"
export const PerplexityPlugin = define({ export const PerplexityPlugin = define({
id: "opencode.provider.perplexity", id: "opencode.provider.perplexity",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.sdk( yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/perplexity") return if (evt.package !== "@ai-sdk/perplexity") return
const mod = yield* Effect.promise(() => import("@ai-sdk/perplexity")) const mod = yield* Effect.promise(() => import("@ai-sdk/perplexity"))
@@ -8,7 +8,8 @@ export const SapAICorePlugin = define({
id: "opencode.provider.sap-ai-core", id: "opencode.provider.sap-ai-core",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
const npm = yield* Npm.Service const npm = yield* Npm.Service
yield* ctx.aisdk.sdk( yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return
const serviceKey = const serviceKey =
@@ -37,7 +38,8 @@ export const SapAICorePlugin = define({
) )
}), }),
) )
yield* ctx.aisdk.language( yield* ctx.aisdk.hook(
"language",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return
evt.language = evt.sdk(evt.model.modelID ?? evt.model.id) evt.language = evt.sdk(evt.model.modelID ?? evt.model.id)
@@ -67,7 +67,8 @@ export function cortexFetch(upstream: FetchLike = fetch) {
export const SnowflakeCortexPlugin = define({ export const SnowflakeCortexPlugin = define({
id: "opencode.provider.snowflake-cortex", id: "opencode.provider.snowflake-cortex",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.sdk( yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("snowflake-cortex")) return if (evt.model.providerID !== ProviderV2.ID.make("snowflake-cortex")) return
const token = const token =
@@ -4,7 +4,8 @@ import { define } from "@opencode-ai/plugin/v2/effect/plugin"
export const TogetherAIPlugin = define({ export const TogetherAIPlugin = define({
id: "opencode.provider.togetherai", id: "opencode.provider.togetherai",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.sdk( yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/togetherai") return if (evt.package !== "@ai-sdk/togetherai") return
const mod = yield* Effect.promise(() => import("@ai-sdk/togetherai")) const mod = yield* Effect.promise(() => import("@ai-sdk/togetherai"))
+2 -1
View File
@@ -4,7 +4,8 @@ import { define } from "@opencode-ai/plugin/v2/effect/plugin"
export const VenicePlugin = define({ export const VenicePlugin = define({
id: "opencode.provider.venice", id: "opencode.provider.venice",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.sdk( yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "venice-ai-sdk-provider") return if (evt.package !== "venice-ai-sdk-provider") return
const mod = yield* Effect.promise(() => import("venice-ai-sdk-provider")) const mod = yield* Effect.promise(() => import("venice-ai-sdk-provider"))
+2 -1
View File
@@ -14,7 +14,8 @@ export const VercelPlugin = define({
}) })
} }
}) })
yield* ctx.aisdk.sdk( yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/vercel") return if (evt.package !== "@ai-sdk/vercel") return
const mod = yield* Effect.promise(() => import("@ai-sdk/vercel")) const mod = yield* Effect.promise(() => import("@ai-sdk/vercel"))
+4 -2
View File
@@ -5,14 +5,16 @@ import { ProviderV2 } from "../../provider"
export const XAIPlugin = define({ export const XAIPlugin = define({
id: "opencode.provider.xai", id: "opencode.provider.xai",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.sdk( yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/xai") return if (evt.package !== "@ai-sdk/xai") return
const mod = yield* Effect.promise(() => import("@ai-sdk/xai")) const mod = yield* Effect.promise(() => import("@ai-sdk/xai"))
evt.sdk = mod.createXai(evt.options) evt.sdk = mod.createXai(evt.options)
}), }),
) )
yield* ctx.aisdk.language( yield* ctx.aisdk.hook(
"language",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("xai")) return if (evt.model.providerID !== ProviderV2.ID.make("xai")) return
evt.language = evt.sdk.responses(evt.model.modelID ?? evt.model.id) evt.language = evt.sdk.responses(evt.model.modelID ?? evt.model.id)
+17 -33
View File
@@ -1,20 +1,12 @@
export * as SdkPlugins from "./sdk" export * as SdkPlugins from "./sdk"
import type { Plugin } from "@opencode-ai/plugin/v2/effect" import type { Plugin } from "@opencode-ai/plugin/v2/effect/plugin"
import { Context, Effect, Layer } from "effect" import { Context, Effect, Layer } from "effect"
import { makeGlobalNode } from "../effect/app-node" import { makeGlobalNode } from "../effect/app-node"
import { EventV2 } from "../event" 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] })
+1 -1
View File
@@ -1,6 +1,6 @@
export * as PluginSupervisor from "./supervisor" export * as PluginSupervisor from "./supervisor"
import type { Plugin } from "@opencode-ai/plugin/v2/effect" import type { Plugin } from "@opencode-ai/plugin/v2/effect/plugin"
import { Event } from "@opencode-ai/schema/config" import { Event } from "@opencode-ai/schema/config"
import { Context, Effect, Fiber, Layer, Option, Schema, Semaphore, Stream } from "effect" import { Context, Effect, Fiber, Layer, Option, Schema, Semaphore, Stream } from "effect"
import path from "path" import path from "path"
+2
View File
@@ -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)
+11 -4
View File
@@ -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) => {
+60 -47
View File
@@ -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 }),
)
}), }),
) )
+65 -7
View File
@@ -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"
@@ -49,6 +50,32 @@ import { llmClient } from "../../effect/app-node-platform"
import { StepFailedError, UserInterruptedError } from "../error" 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"
import type { SessionHooks } from "@opencode-ai/plugin/v2/effect/session"
import { PluginHooks } from "../../plugin/hooks"
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.
@@ -107,6 +134,7 @@ const layer = Layer.effect(
const llm = yield* LLMClient.Service const llm = yield* LLMClient.Service
const agents = yield* AgentV2.Service const agents = yield* AgentV2.Service
const tools = yield* ToolRegistry.Service const tools = yield* ToolRegistry.Service
const hooks = yield* PluginHooks.Service
const models = yield* SessionRunnerModel.Service const models = yield* SessionRunnerModel.Service
const store = yield* SessionStore.Service const store = yield* SessionStore.Service
const location = yield* Location.Service const location = yield* Location.Service
@@ -227,10 +255,34 @@ const layer = Layer.effect(
tools: toolMaterialization?.definitions ?? [], tools: toolMaterialization?.definitions ?? [],
toolChoice: isLastStep ? "none" : undefined, toolChoice: isLastStep ? "none" : undefined,
}) })
const availableTools = new Map(request.tools.map((tool) => [tool.name, tool]))
const requestEvent: SessionHooks["request"] = {
sessionID: session.id,
agent: agent.id,
model: resolved.ref,
system: [...request.system],
messages: [...request.messages],
tools: Object.fromEntries(
request.tools.map((tool) => [tool.name, { description: tool.description, input: { ...tool.inputSchema } }]),
),
}
// Plugins may reshape the draft, but cannot advertise tools excluded earlier
// by permissions or registration state.
yield* hooks.trigger("session", "request", requestEvent)
const hookedRequest = LLM.updateRequest(request, {
system: requestEvent.system,
messages: requestEvent.messages,
tools: Object.entries(requestEvent.tools).flatMap(([name, tool]) => {
const registered = availableTools.get(name)
if (!registered) return []
return [{ ...registered, description: tool.description, inputSchema: tool.input }]
}),
})
const advertisedTools = new Set(hookedRequest.tools.map((tool) => tool.name))
// Automatic compaction completed; rebuild the request from compacted history. // Automatic compaction completed; rebuild the request from compacted history.
if ( if (
!(yield* SessionInput.pendingCompaction(db, session.id)) && !(yield* SessionInput.pendingCompaction(db, session.id)) &&
(yield* compaction.compactIfNeeded({ sessionID: session.id, messages: context, request })) (yield* compaction.compactIfNeeded({ sessionID: session.id, messages: context, request: hookedRequest }))
) )
return { _tag: "RestartAfterCompaction", step: currentStep } as const return { _tag: "RestartAfterCompaction", step: currentStep } as const
const startSnapshot = yield* snapshots.capture() const startSnapshot = yield* snapshots.capture()
@@ -251,7 +303,7 @@ const layer = Layer.effect(
const publish = (event: LLMEvent, outputPaths: ReadonlyArray<string> = [], error?: SessionError.Error) => const publish = (event: LLMEvent, outputPaths: ReadonlyArray<string> = [], error?: SessionError.Error) =>
serialized(publisher.publish(event, outputPaths, error)) serialized(publisher.publish(event, outputPaths, error))
let overflowFailure: ProviderErrorEvent | undefined let overflowFailure: ProviderErrorEvent | undefined
const providerStream = llm.stream(request).pipe( const providerStream = llm.stream(hookedRequest).pipe(
Stream.runForEach((event) => Stream.runForEach((event) =>
Effect.gen(function* () { Effect.gen(function* () {
if (overflowFailure || publisher.hasProviderError()) return if (overflowFailure || publisher.hasProviderError()) return
@@ -263,11 +315,11 @@ const layer = Layer.effect(
} }
yield* publish(event) yield* publish(event)
if (event.type !== "tool-call" || event.providerExecuted) return if (event.type !== "tool-call" || event.providerExecuted) return
if (!toolMaterialization) { if (!toolMaterialization || !advertisedTools.has(event.name)) {
yield* serialized( yield* serialized(
publisher.failUnsettledTools({ publisher.failUnsettledTools({
type: "tool.execution", type: "tool.execution",
message: "Tools are disabled after the maximum agent steps", message: `Tool is not available for this request: ${event.name}`,
}), }),
) )
return return
@@ -312,6 +364,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 +385,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 +508,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
@@ -595,6 +652,7 @@ export const node = makeLocationNode({
llmClient, llmClient,
AgentV2.node, AgentV2.node,
ToolRegistry.node, ToolRegistry.node,
PluginHooks.node,
SessionRunnerModel.node, SessionRunnerModel.node,
SessionStore.node, SessionStore.node,
Location.node, Location.node,
+5 -1
View File
@@ -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,
} }
}), }),
}) })
@@ -101,27 +101,36 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
ended: (id: string, value: string, ordinal: number, state?: Record<string, unknown>) => Effect.Effect<void>, ended: (id: string, value: string, ordinal: number, state?: Record<string, unknown>) => Effect.Effect<void>,
single = false, single = false,
) => { ) => {
const chunks = new Map<string, { readonly ordinal: number; readonly values: string[] }>() const chunks = new Map<
string,
{ readonly ordinal: number; readonly values: string[]; state?: Record<string, unknown> }
>()
let nextOrdinal = 0 let nextOrdinal = 0
const start = (id: string) => const start = (id: string, state?: Record<string, unknown>) =>
Effect.suspend(() => { Effect.suspend(() => {
if (chunks.has(id)) return Effect.die(new Error(`Duplicate ${name} start: ${id}`)) if (chunks.has(id)) return Effect.die(new Error(`Duplicate ${name} start: ${id}`))
if (single && chunks.size > 0) return Effect.die(new Error(`${name} start before end: ${id}`)) if (single && chunks.size > 0) return Effect.die(new Error(`${name} start before end: ${id}`))
const ordinal = nextOrdinal++ const ordinal = nextOrdinal++
chunks.set(id, { ordinal, values: [] }) chunks.set(id, { ordinal, values: [], state })
return Effect.succeed(ordinal) return Effect.succeed(ordinal)
}) })
const append = (id: string, value: string) => const append = (id: string, value: string, state?: Record<string, unknown>) =>
Effect.suspend(() => { Effect.suspend(() => {
const current = chunks.get(id) const current = chunks.get(id)
if (!current) return Effect.die(new Error(`${name} delta before start: ${id}`)) if (!current) return Effect.die(new Error(`${name} delta before start: ${id}`))
current.values.push(value) current.values.push(value)
if (state !== undefined) current.state = { ...current.state, ...state }
return Effect.succeed(current.ordinal) return Effect.succeed(current.ordinal)
}) })
const end = Effect.fnUntraced(function* (id: string, state?: Record<string, unknown>) { const end = Effect.fnUntraced(function* (id: string, state?: Record<string, unknown>) {
const current = chunks.get(id) const current = chunks.get(id)
if (!current) return yield* Effect.die(new Error(`${name} end before start: ${id}`)) if (!current) return yield* Effect.die(new Error(`${name} end before start: ${id}`))
yield* ended(id, current.values.join(""), current.ordinal, state) yield* ended(
id,
current.values.join(""),
current.ordinal,
state === undefined ? current.state : { ...current.state, ...state },
)
chunks.delete(id) chunks.delete(id)
}) })
const flush = Effect.fnUntraced(function* () { const flush = Effect.fnUntraced(function* () {
@@ -216,7 +225,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 +236,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
sessionID: input.sessionID, sessionID: input.sessionID,
assistantMessageID, assistantMessageID,
error: stepFailure, error: stepFailure,
...usage,
}) })
}) })
@@ -284,7 +297,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
return return
case "reasoning-start": case "reasoning-start":
retryEvidence = true retryEvidence = true
const startedReasoningOrdinal = yield* reasoning.start(event.id) const startedReasoningOrdinal = yield* reasoning.start(event.id, providerState(event.providerMetadata))
yield* events.publish(SessionEvent.Reasoning.Started, { yield* events.publish(SessionEvent.Reasoning.Started, {
sessionID: input.sessionID, sessionID: input.sessionID,
assistantMessageID: yield* startAssistant(), assistantMessageID: yield* startAssistant(),
@@ -293,7 +306,11 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
}) })
return return
case "reasoning-delta": case "reasoning-delta":
const deltaReasoningOrdinal = yield* reasoning.append(event.id, event.text) const deltaReasoningOrdinal = yield* reasoning.append(
event.id,
event.text,
providerState(event.providerMetadata),
)
yield* events.publish(SessionEvent.Reasoning.Delta, { yield* events.publish(SessionEvent.Reasoning.Delta, {
sessionID: input.sessionID, sessionID: input.sessionID,
assistantMessageID: yield* currentAssistantMessageID(), assistantMessageID: yield* currentAssistantMessageID(),
@@ -409,12 +426,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
+1 -1
View File
@@ -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.
+17 -4
View File
@@ -1,6 +1,6 @@
export * as ApplyPatchTool from "./apply-patch" export * as ApplyPatchTool from "./apply-patch"
import type { PluginContext } from "@opencode-ai/plugin/v2/effect" import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin"
import { ToolFailure } from "@opencode-ai/llm" import { ToolFailure } from "@opencode-ai/llm"
import { FileDiff } from "@opencode-ai/schema/file-diff" import { FileDiff } from "@opencode-ai/schema/file-diff"
import { createTwoFilesPatch, diffLines } from "diff" import { createTwoFilesPatch, diffLines } from "diff"
@@ -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)
@@ -194,6 +194,19 @@ export const Plugin = {
), ),
) )
.pipe(Effect.orDie) .pipe(Effect.orDie)
yield* ctx.session.hook("request", (event) =>
Effect.sync(() => {
const usePatch =
event.model.providerID.toLowerCase() === "openai" || event.model.id.toLowerCase().includes("gpt")
if (usePatch) {
delete event.tools.edit
delete event.tools.write
return
}
delete event.tools.patch
}),
)
}), }),
} }
+1 -1
View File
@@ -6,7 +6,7 @@
*/ */
export * as EditTool from "./edit" export * as EditTool from "./edit"
import type { PluginContext } from "@opencode-ai/plugin/v2/effect" import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin"
import { ToolFailure } from "@opencode-ai/llm" import { ToolFailure } from "@opencode-ai/llm"
import { FileDiff } from "@opencode-ai/schema/file-diff" import { FileDiff } from "@opencode-ai/schema/file-diff"
import { createTwoFilesPatch, diffLines } from "diff" import { createTwoFilesPatch, diffLines } from "diff"
+1 -15
View File
@@ -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 -1
View File
@@ -1,7 +1,7 @@
export * as GlobTool from "./glob" export * as GlobTool from "./glob"
import { ToolFailure } from "@opencode-ai/llm" import { ToolFailure } from "@opencode-ai/llm"
import type { PluginContext } from "@opencode-ai/plugin/v2/effect" import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin"
import { Effect, Schema } from "effect" import { Effect, Schema } from "effect"
import path from "path" import path from "path"
import { FileSystem } from "../filesystem" import { FileSystem } from "../filesystem"
+1 -1
View File
@@ -1,6 +1,6 @@
export * as GrepTool from "./grep" export * as GrepTool from "./grep"
import type { PluginContext } from "@opencode-ai/plugin/v2/effect" import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin"
import { ToolFailure } from "@opencode-ai/llm" import { ToolFailure } from "@opencode-ai/llm"
import { Effect, Schema } from "effect" import { Effect, Schema } from "effect"
import path from "path" import path from "path"

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