mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-12 12:45:07 -04:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1036e82c6b | |||
| fcc8856f36 | |||
| 984e62502b |
@@ -345,6 +345,9 @@
|
||||
"packages/core": {
|
||||
"name": "@opencode-ai/core",
|
||||
"version": "1.18.4",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/alibaba": "1.0.17",
|
||||
"@ai-sdk/amazon-bedrock": "4.0.112",
|
||||
@@ -364,6 +367,8 @@
|
||||
"@ai-sdk/togetherai": "2.0.41",
|
||||
"@ai-sdk/vercel": "2.0.39",
|
||||
"@aws-sdk/credential-providers": "3.1057.0",
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@effect/sql-sqlite-bun": "catalog:",
|
||||
"@ff-labs/fff-bun": "0.10.1",
|
||||
"@ff-labs/fff-node": "0.10.1",
|
||||
"@lydell/node-pty": "catalog:",
|
||||
@@ -399,8 +404,6 @@
|
||||
"zod": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@effect/sql-sqlite-bun": "catalog:",
|
||||
"@opencode-ai/http-recorder": "workspace:*",
|
||||
"@parcel/watcher-darwin-arm64": "2.5.1",
|
||||
"@parcel/watcher-darwin-x64": "2.5.1",
|
||||
@@ -655,7 +658,6 @@
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@opencode-ai/server": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -1033,31 +1035,6 @@
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/workerd-spike": {
|
||||
"name": "@opencode-ai/workerd-spike",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/server": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/vitest-pool-workers": "0.12.6",
|
||||
"@cloudflare/workers-types": "^4.20250808.0",
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@vitest/expect": "3.2.7",
|
||||
"@vitest/mocker": "3.2.7",
|
||||
"@vitest/pretty-format": "3.2.7",
|
||||
"@vitest/runner": "3.2.7",
|
||||
"@vitest/snapshot": "3.2.7",
|
||||
"@vitest/spy": "3.2.7",
|
||||
"@vitest/utils": "3.2.7",
|
||||
"unenv": "2.0.0-rc.24",
|
||||
"vitest": "3.2.7",
|
||||
"wrangler": "4.28.0",
|
||||
"xdg-basedir": "5.1.0",
|
||||
},
|
||||
},
|
||||
"packages/www": {
|
||||
"name": "@opencode-ai/www",
|
||||
"dependencies": {
|
||||
@@ -1087,7 +1064,6 @@
|
||||
"effect@4.0.0-beta.101": "patches/effect@4.0.0-beta.101.patch",
|
||||
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
|
||||
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch",
|
||||
"@cloudflare/vitest-pool-workers@0.12.6": "patches/@cloudflare%2Fvitest-pool-workers@0.12.6.patch",
|
||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||
"@ai-sdk/mistral@3.0.51": "patches/@ai-sdk%2Fmistral@3.0.51.patch",
|
||||
"@ff-labs/fff-bun@0.10.1": "patches/@ff-labs%2Ffff-bun@0.10.1.patch",
|
||||
@@ -1561,8 +1537,6 @@
|
||||
|
||||
"@cloudflare/vite-plugin": ["@cloudflare/vite-plugin@1.15.2", "", { "dependencies": { "@cloudflare/unenv-preset": "2.7.11", "@remix-run/node-fetch-server": "^0.8.0", "get-port": "^7.1.0", "miniflare": "4.20251118.1", "picocolors": "^1.1.1", "tinyglobby": "^0.2.12", "unenv": "2.0.0-rc.24", "wrangler": "4.50.0", "ws": "8.18.0" }, "peerDependencies": { "vite": "^6.1.0 || ^7.0.0" } }, "sha512-SPMxsesbABOjzcAa4IzW+yM+fTIjx3GG1doh229Pg16FjSEZJhknyRpcld4gnaZioK3JKwG9FWdKsUhbplKY8w=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers": ["@cloudflare/vitest-pool-workers@0.12.6", "", { "dependencies": { "cjs-module-lexer": "^1.2.3", "esbuild": "0.27.0", "miniflare": "4.20260120.0", "wrangler": "4.60.0", "zod": "^3.25.76" }, "peerDependencies": { "@vitest/runner": "2.0.x - 3.2.x", "@vitest/snapshot": "2.0.x - 3.2.x", "vitest": "2.0.x - 3.2.x" } }, "sha512-smzhKzBdB4JYKo9bb8x3i70CtSj5GRhDfGvUhbLI7uPUv4QwMubddmZ+XYNekcy3C8/FG1fjLiwUqisITik8oQ=="],
|
||||
|
||||
"@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20251118.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-UmWmYEYS/LkK/4HFKN6xf3Hk8cw70PviR+ftr3hUvs9HYZS92IseZEp16pkL6ZBETrPRpZC7OrzoYF7ky6kHsg=="],
|
||||
|
||||
"@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20251118.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RockU7Qzf4rxNfY1lx3j4rvwutNLjTIX7rr2hogbQ4mzLo8Ea40/oZTzXVxl+on75joLBrt0YpenGW8o/r44QA=="],
|
||||
@@ -2149,8 +2123,6 @@
|
||||
|
||||
"@opencode-ai/web": ["@opencode-ai/web@workspace:packages/web"],
|
||||
|
||||
"@opencode-ai/workerd-spike": ["@opencode-ai/workerd-spike@workspace:packages/workerd-spike"],
|
||||
|
||||
"@opencode-ai/www": ["@opencode-ai/www@workspace:packages/www"],
|
||||
|
||||
"@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.9.0", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Seva+NCa0WUQnJIUE5GzHsUv1WTIeyqwz0ELl2VtS6NP+eF+77yCXGFVOMbvoCM7QMjlnhv7931e89R+8pJdcQ=="],
|
||||
@@ -3281,19 +3253,19 @@
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@vitest/expect": ["@vitest/expect@3.2.7", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.7", "@vitest/utils": "3.2.7", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w=="],
|
||||
"@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="],
|
||||
|
||||
"@vitest/mocker": ["@vitest/mocker@3.2.7", "", { "dependencies": { "@vitest/spy": "3.2.7", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA=="],
|
||||
"@vitest/mocker": ["@vitest/mocker@4.1.10", "", { "dependencies": { "@vitest/spy": "4.1.10", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow=="],
|
||||
|
||||
"@vitest/pretty-format": ["@vitest/pretty-format@3.2.7", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA=="],
|
||||
"@vitest/pretty-format": ["@vitest/pretty-format@4.1.10", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q=="],
|
||||
|
||||
"@vitest/runner": ["@vitest/runner@3.2.7", "", { "dependencies": { "@vitest/utils": "3.2.7", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA=="],
|
||||
"@vitest/runner": ["@vitest/runner@4.1.10", "", { "dependencies": { "@vitest/utils": "4.1.10", "pathe": "^2.0.3" } }, "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg=="],
|
||||
|
||||
"@vitest/snapshot": ["@vitest/snapshot@3.2.7", "", { "dependencies": { "@vitest/pretty-format": "3.2.7", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g=="],
|
||||
"@vitest/snapshot": ["@vitest/snapshot@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "@vitest/utils": "4.1.10", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw=="],
|
||||
|
||||
"@vitest/spy": ["@vitest/spy@3.2.7", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ=="],
|
||||
"@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="],
|
||||
|
||||
"@vitest/utils": ["@vitest/utils@3.2.7", "", { "dependencies": { "@vitest/pretty-format": "3.2.7", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw=="],
|
||||
"@vitest/utils": ["@vitest/utils@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA=="],
|
||||
|
||||
"@volar/kit": ["@volar/kit@2.4.28", "", { "dependencies": { "@volar/language-service": "2.4.28", "@volar/typescript": "2.4.28", "typesafe-path": "^0.2.2", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "typescript": "*" } }, "sha512-cKX4vK9dtZvDRaAzeoUdaAJEew6IdxHNCRrdp5Kvcl6zZOqb6jTOfk3kXkIkG3T7oTFXguEMt5+9ptyqYR84Pg=="],
|
||||
|
||||
@@ -3625,8 +3597,6 @@
|
||||
|
||||
"citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="],
|
||||
|
||||
"cjs-module-lexer": ["cjs-module-lexer@1.4.3", "", {}, "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="],
|
||||
|
||||
"classnames": ["classnames@2.3.2", "", {}, "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="],
|
||||
|
||||
"clean-css": ["clean-css@5.3.3", "", { "dependencies": { "source-map": "~0.6.0" } }, "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg=="],
|
||||
@@ -4067,8 +4037,6 @@
|
||||
|
||||
"expressive-code": ["expressive-code@0.41.7", "", { "dependencies": { "@expressive-code/core": "^0.41.7", "@expressive-code/plugin-frames": "^0.41.7", "@expressive-code/plugin-shiki": "^0.41.7", "@expressive-code/plugin-text-markers": "^0.41.7" } }, "sha512-2wZjC8OQ3TaVEMcBtYY4Va3lo6J+Ai9jf3d4dbhURMJcU4Pbqe6EcHe424MIZI0VHUA1bR6xdpoHYi3yxokWqA=="],
|
||||
|
||||
"exsolve": ["exsolve@1.1.1", "", {}, "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g=="],
|
||||
|
||||
"ext-list": ["ext-list@2.2.2", "", { "dependencies": { "mime-db": "^1.28.0" } }, "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA=="],
|
||||
|
||||
"ext-name": ["ext-name@5.0.0", "", { "dependencies": { "ext-list": "^2.0.0", "sort-keys-length": "^1.0.0" } }, "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ=="],
|
||||
@@ -5539,7 +5507,7 @@
|
||||
|
||||
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||
|
||||
"std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
|
||||
"std-env": ["std-env@4.2.0", "", {}, "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw=="],
|
||||
|
||||
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
|
||||
|
||||
@@ -5577,8 +5545,6 @@
|
||||
|
||||
"strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="],
|
||||
|
||||
"strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="],
|
||||
|
||||
"stripe": ["stripe@18.0.0", "", { "dependencies": { "@types/node": ">=8.1.0", "qs": "^6.11.0" } }, "sha512-3Fs33IzKUby//9kCkCa1uRpinAoTvj6rJgQ2jrBEysoxEvfsclvXdna1amyEYbA2EKkjynuB4+L/kleCCaWTpA=="],
|
||||
|
||||
"strnum": ["strnum@1.1.2", "", {}, "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA=="],
|
||||
@@ -5651,9 +5617,7 @@
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
|
||||
|
||||
"tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="],
|
||||
|
||||
"tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="],
|
||||
"tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="],
|
||||
|
||||
"tinyspy": ["tinyspy@4.0.4", "", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="],
|
||||
|
||||
@@ -5841,8 +5805,6 @@
|
||||
|
||||
"vite": ["vite@7.1.4", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-X5QFK4SGynAeeIt+A7ZWnApdUyHYm+pzv/8/A57LqSGcI88U6R6ipOs3uCesdc6yl7nl+zNO0t8LmqAdXcQihw=="],
|
||||
|
||||
"vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="],
|
||||
|
||||
"vite-plugin-dynamic-import": ["vite-plugin-dynamic-import@1.6.0", "", { "dependencies": { "acorn": "^8.12.1", "es-module-lexer": "^1.5.4", "fast-glob": "^3.3.2", "magic-string": "^0.30.11" } }, "sha512-TM0sz70wfzTIo9YCxVFwS8OA9lNREsh+0vMHGSkWDTZ7bgd1Yjs5RV8EgB634l/91IsXJReg0xtmuQqP0mf+rg=="],
|
||||
|
||||
"vite-plugin-icons-spritesheet": ["vite-plugin-icons-spritesheet@3.0.1", "", { "dependencies": { "chalk": "^5.4.1", "glob": "^11.0.1", "node-html-parser": "^7.0.1", "tinyexec": "^0.3.2" }, "peerDependencies": { "vite": ">=5.2.0" } }, "sha512-Cr0+Z6wRMwSwKisWW9PHeTjqmQFv0jwRQQMc3YgAhAgZEe03j21el0P/CA31KN/L5eiL1LhR14VTXl96LetonA=="],
|
||||
@@ -5851,7 +5813,7 @@
|
||||
|
||||
"vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="],
|
||||
|
||||
"vitest": ["vitest@3.2.7", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.7", "@vitest/mocker": "3.2.7", "@vitest/pretty-format": "^3.2.7", "@vitest/runner": "3.2.7", "@vitest/snapshot": "3.2.7", "@vitest/spy": "3.2.7", "@vitest/utils": "3.2.7", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.7", "@vitest/ui": "3.2.7", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg=="],
|
||||
"vitest": ["vitest@4.1.10", "", { "dependencies": { "@vitest/expect": "4.1.10", "@vitest/mocker": "4.1.10", "@vitest/pretty-format": "4.1.10", "@vitest/runner": "4.1.10", "@vitest/snapshot": "4.1.10", "@vitest/spy": "4.1.10", "@vitest/utils": "4.1.10", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.10", "@vitest/browser-preview": "4.1.10", "@vitest/browser-webdriverio": "4.1.10", "@vitest/coverage-istanbul": "4.1.10", "@vitest/coverage-v8": "4.1.10", "@vitest/ui": "4.1.10", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw=="],
|
||||
|
||||
"volar-service-css": ["volar-service-css@0.0.71", "", { "dependencies": { "vscode-css-languageservice": "^6.3.0", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-wRRFt9BpjMKCazcgOh67MSjUjiWUCAh99DyYSDIOTuxaRjEtDC7PpB0k1Y1wbJIW/pVtMUSVbpPo3UGSm0Byxw=="],
|
||||
|
||||
@@ -6265,14 +6227,6 @@
|
||||
|
||||
"@cloudflare/vite-plugin/ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/esbuild": ["esbuild@0.27.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.0", "@esbuild/android-arm": "0.27.0", "@esbuild/android-arm64": "0.27.0", "@esbuild/android-x64": "0.27.0", "@esbuild/darwin-arm64": "0.27.0", "@esbuild/darwin-x64": "0.27.0", "@esbuild/freebsd-arm64": "0.27.0", "@esbuild/freebsd-x64": "0.27.0", "@esbuild/linux-arm": "0.27.0", "@esbuild/linux-arm64": "0.27.0", "@esbuild/linux-ia32": "0.27.0", "@esbuild/linux-loong64": "0.27.0", "@esbuild/linux-mips64el": "0.27.0", "@esbuild/linux-ppc64": "0.27.0", "@esbuild/linux-riscv64": "0.27.0", "@esbuild/linux-s390x": "0.27.0", "@esbuild/linux-x64": "0.27.0", "@esbuild/netbsd-arm64": "0.27.0", "@esbuild/netbsd-x64": "0.27.0", "@esbuild/openbsd-arm64": "0.27.0", "@esbuild/openbsd-x64": "0.27.0", "@esbuild/openharmony-arm64": "0.27.0", "@esbuild/sunos-x64": "0.27.0", "@esbuild/win32-arm64": "0.27.0", "@esbuild/win32-ia32": "0.27.0", "@esbuild/win32-x64": "0.27.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/miniflare": ["miniflare@4.20260120.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "^0.34.5", "undici": "7.18.2", "workerd": "1.20260120.0", "ws": "8.18.0", "youch": "4.1.0-beta.10", "zod": "^3.25.76" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-XXZyE2pDKMtP5OLuv0LPHEAzIYhov4jrYjcqrhhqtxGGtXneWOHvXIPo+eV8sqwqWd3R7j4DlEKcyb+87BR49Q=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/wrangler": ["wrangler@4.60.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.2", "@cloudflare/unenv-preset": "2.11.0", "blake3-wasm": "2.1.5", "esbuild": "0.27.0", "miniflare": "4.20260120.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260120.0" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20260120.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-n4kibm/xY0Qd5G2K/CbAQeVeOIlwPNVglmFjlDRCCYk3hZh8IggO/rg8AXt/vByK2Sxsugl5Z7yvgWxrUbmS6g=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="],
|
||||
|
||||
"@dot/log/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
@@ -6419,8 +6373,6 @@
|
||||
|
||||
"@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="],
|
||||
|
||||
"@opencode-ai/workerd-spike/wrangler": ["wrangler@4.28.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.0", "@cloudflare/unenv-preset": "2.6.0", "blake3-wasm": "2.1.5", "esbuild": "0.25.4", "miniflare": "4.20250803.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.19", "workerd": "1.20250803.0" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20250803.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-y0yHIuScpok9oSErLqDbxkBChC2+/jZpvqMg2NxOto1JCyUtDUuKljOfcVMaI48d9GuhOCSoWSumYxLAHNxaLA=="],
|
||||
|
||||
"@opencode-ai/www/@astrojs/cloudflare": ["@astrojs/cloudflare@14.1.4", "", { "dependencies": { "@astrojs/internal-helpers": "0.10.1", "@astrojs/underscore-redirects": "1.0.3", "@cloudflare/vite-plugin": "^1.39.0", "piccolore": "^0.1.3", "vite": "^8.0.13" }, "peerDependencies": { "astro": "^7.0.0", "wrangler": "^4.83.0" } }, "sha512-Zyo1E/5/dmegmKODbwUzOd67euNd6oKcbllhAwe3uFFprA7mIFNkpF6yBBa78vIkpMNuU13MkBJHCLd+yJSuEA=="],
|
||||
|
||||
"@opencode-ai/www/astro": ["astro@7.1.3", "", { "dependencies": { "@astrojs/compiler-rs": "^0.3.1", "@astrojs/internal-helpers": "0.10.1", "@astrojs/markdown-satteri": "0.3.4", "@astrojs/telemetry": "3.3.3", "@capsizecss/unpack": "^4.0.0", "@clack/prompts": "^1.1.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.3.0", "am-i-vibing": "^0.4.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "ci-info": "^4.4.0", "clsx": "^2.1.1", "common-ancestor-path": "^2.0.0", "cookie": "^2.0.1", "devalue": "^5.8.1", "diff": "^8.0.3", "dset": "^3.1.4", "es-module-lexer": "^2.0.0", "esbuild": "^0.28.0", "flattie": "^1.1.1", "fontace": "~0.4.1", "get-tsconfig": "5.0.0-beta.4", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.2.0", "js-yaml": "^4.1.1", "jsonc-parser": "^3.3.1", "magic-string": "^0.30.21", "magicast": "^0.5.2", "mrmime": "^2.0.1", "neotraverse": "^1.0.1", "obug": "^2.1.1", "p-limit": "^7.3.0", "p-queue": "^9.1.0", "package-manager-detector": "^1.6.0", "piccolore": "^0.1.3", "picomatch": "^4.0.4", "semver": "^7.7.4", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "svgo": "^4.0.1", "tinyclip": "^0.1.12", "tinyexec": "^1.0.4", "tinyglobby": "^0.2.15", "ultrahtml": "^1.6.0", "unifont": "~0.7.4", "unstorage": "^1.17.5", "vite": "^8.0.13", "vitefu": "^1.1.2", "xxhash-wasm": "^1.1.0", "yargs-parser": "^22.0.0", "zod": "^4.3.6" }, "optionalDependencies": { "sharp": "^0.34.0 || ^0.35.0" }, "peerDependencies": { "@astrojs/markdown-remark": "7.2.1" }, "optionalPeers": ["@astrojs/markdown-remark"], "bin": { "astro": "./bin/astro.mjs" } }, "sha512-4dhPyAAXthf3xLEYnG8SeL7yr/nTPPABfY7e9YF0yuO+vK9Xp+8Q5j4xzsmL3GueukQv4oNwGNTBepLOiDGeJA=="],
|
||||
@@ -6525,8 +6477,6 @@
|
||||
|
||||
"@solidjs/start/vite": ["vite@7.1.10", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA=="],
|
||||
|
||||
"@solidjs/start/vitest": ["vitest@4.1.10", "", { "dependencies": { "@vitest/expect": "4.1.10", "@vitest/mocker": "4.1.10", "@vitest/pretty-format": "4.1.10", "@vitest/runner": "4.1.10", "@vitest/snapshot": "4.1.10", "@vitest/spy": "4.1.10", "@vitest/utils": "4.1.10", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.10", "@vitest/browser-preview": "4.1.10", "@vitest/browser-webdriverio": "4.1.10", "@vitest/coverage-istanbul": "4.1.10", "@vitest/coverage-v8": "4.1.10", "@vitest/ui": "4.1.10", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw=="],
|
||||
|
||||
"@storybook/addon-docs/react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="],
|
||||
|
||||
"@storybook/addon-docs/react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="],
|
||||
@@ -6573,6 +6523,12 @@
|
||||
|
||||
"@vitejs/plugin-react/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
|
||||
|
||||
"@vitest/expect/@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="],
|
||||
|
||||
"@vitest/expect/tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="],
|
||||
|
||||
"@vitest/mocker/@vitest/spy": ["@vitest/spy@4.1.10", "", {}, "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw=="],
|
||||
|
||||
"@vscode/emmet-helper/jsonc-parser": ["jsonc-parser@2.3.1", "", {}, "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg=="],
|
||||
|
||||
"@vscode/emmet-helper/vscode-languageserver-types": ["vscode-languageserver-types@3.18.0", "", {}, "sha512-8TsGPNMIMiiBdkORgRSvLjuiEIiAFtO+KssmYWxQ+uSVvlf7RjK8YKCOjPzZ+YA04jXEV7+7LvkSmHkhpNS99g=="],
|
||||
@@ -6887,10 +6843,6 @@
|
||||
|
||||
"sst/jose": ["jose@5.2.3", "", {}, "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA=="],
|
||||
|
||||
"storybook/@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="],
|
||||
|
||||
"storybook/@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="],
|
||||
|
||||
"storybook/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="],
|
||||
|
||||
"storybook/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
|
||||
@@ -6901,8 +6853,6 @@
|
||||
|
||||
"strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"strip-literal/js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
|
||||
|
||||
"sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
|
||||
|
||||
"svgo/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
|
||||
@@ -6939,11 +6889,17 @@
|
||||
|
||||
"venice-ai-sdk-provider/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.40", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-OL5IrpUm9Y8Dwy+w/vvFwPotS6m52O9W0op2oXgXdCROMJIBalBI0oro6OIBYkPxvm5Xg02GSkoQN25RlR0bnw=="],
|
||||
|
||||
"vite-node/vite": ["vite@7.1.10", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA=="],
|
||||
|
||||
"vite-plugin-icons-spritesheet/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="],
|
||||
|
||||
"vitest/vite": ["vite@7.1.10", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA=="],
|
||||
"vitest/@vitest/expect": ["@vitest/expect@4.1.10", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.10", "@vitest/utils": "4.1.10", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA=="],
|
||||
|
||||
"vitest/@vitest/spy": ["@vitest/spy@4.1.10", "", {}, "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw=="],
|
||||
|
||||
"vitest/es-module-lexer": ["es-module-lexer@2.3.1", "", {}, "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA=="],
|
||||
|
||||
"vitest/tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="],
|
||||
|
||||
"vitest/vite": ["vite@8.1.5", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", "postcss": "^8.5.17", "rolldown": "~1.1.5", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw=="],
|
||||
|
||||
"vscode-json-languageservice/vscode-languageserver-types": ["vscode-languageserver-types@3.18.0", "", {}, "sha512-8TsGPNMIMiiBdkORgRSvLjuiEIiAFtO+KssmYWxQ+uSVvlf7RjK8YKCOjPzZ+YA04jXEV7+7LvkSmHkhpNS99g=="],
|
||||
|
||||
@@ -7241,72 +7197,6 @@
|
||||
|
||||
"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.0", "", { "os": "android", "cpu": "arm" }, "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.0", "", { "os": "android", "cpu": "arm64" }, "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.0", "", { "os": "android", "cpu": "x64" }, "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.0", "", { "os": "linux", "cpu": "none" }, "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.0", "", { "os": "linux", "cpu": "none" }, "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.0", "", { "os": "linux", "cpu": "none" }, "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.0", "", { "os": "linux", "cpu": "x64" }, "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.0", "", { "os": "none", "cpu": "arm64" }, "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.0", "", { "os": "none", "cpu": "x64" }, "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.0", "", { "os": "none", "cpu": "arm64" }, "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.0", "", { "os": "sunos", "cpu": "x64" }, "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.0", "", { "os": "win32", "cpu": "x64" }, "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/miniflare/sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/miniflare/undici": ["undici@7.18.2", "", {}, "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/miniflare/workerd": ["workerd@1.20260120.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260120.0", "@cloudflare/workerd-darwin-arm64": "1.20260120.0", "@cloudflare/workerd-linux-64": "1.20260120.0", "@cloudflare/workerd-linux-arm64": "1.20260120.0", "@cloudflare/workerd-windows-64": "1.20260120.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-R6X/VQOkwLTBGLp4VRUwLQZZVxZ9T9J8pGiJ6GQUMaRkY7TVWrCSkVfoNMM1/YyFsY5UYhhPoQe5IehnhZ3Pdw=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/miniflare/ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/wrangler/@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.2", "", {}, "sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/wrangler/@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.11.0", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": "^1.20260115.0" }, "optionalPeers": ["workerd"] }, "sha512-z3hxFajL765VniNPGV0JRStZolNz63gU3B3AktwoGdDlnQvz5nP+Ah4RL04PONlZQjwmDdGHowEStJ94+RsaJg=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/wrangler/workerd": ["workerd@1.20260120.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260120.0", "@cloudflare/workerd-darwin-arm64": "1.20260120.0", "@cloudflare/workerd-linux-64": "1.20260120.0", "@cloudflare/workerd-linux-arm64": "1.20260120.0", "@cloudflare/workerd-windows-64": "1.20260120.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-R6X/VQOkwLTBGLp4VRUwLQZZVxZ9T9J8pGiJ6GQUMaRkY7TVWrCSkVfoNMM1/YyFsY5UYhhPoQe5IehnhZ3Pdw=="],
|
||||
|
||||
"@electron/asar/minimatch/brace-expansion": ["brace-expansion@1.1.16", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw=="],
|
||||
|
||||
"@electron/fuses/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
|
||||
@@ -7507,16 +7397,6 @@
|
||||
|
||||
"@opencode-ai/web/@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=="],
|
||||
|
||||
"@opencode-ai/workerd-spike/wrangler/@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.6.0", "", { "peerDependencies": { "unenv": "2.0.0-rc.19", "workerd": "^1.20250802.0" }, "optionalPeers": ["workerd"] }, "sha512-h7Txw0WbDuUbrvZwky6+x7ft+U/Gppfn/rWx6IdR+e9gjygozRJnV26Y2TOr3yrIFa6OsZqqR2lN+jWTrakHXg=="],
|
||||
|
||||
"@opencode-ai/workerd-spike/wrangler/esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="],
|
||||
|
||||
"@opencode-ai/workerd-spike/wrangler/miniflare": ["miniflare@4.20250803.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "sharp": "^0.33.5", "stoppable": "1.1.0", "undici": "^7.10.0", "workerd": "1.20250803.0", "ws": "8.18.0", "youch": "4.1.0-beta.10", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-1tmCLfmMw0SqRBF9PPII9CVLQRzOrO7uIBmSng8BMSmtgs2kos7OeoM0sg6KbR9FrvP/zAniLyZuCAMAjuu4fQ=="],
|
||||
|
||||
"@opencode-ai/workerd-spike/wrangler/unenv": ["unenv@2.0.0-rc.19", "", { "dependencies": { "defu": "^6.1.4", "exsolve": "^1.0.7", "ohash": "^2.0.11", "pathe": "^2.0.3", "ufo": "^1.6.1" } }, "sha512-t/OMHBNAkknVCI7bVB9OWjUUAwhVv9vsPIAGnNUxnu3FxPQN11rjh0sksLMzc3g7IlTgvHmOTl4JM7JHpcv5wA=="],
|
||||
|
||||
"@opencode-ai/workerd-spike/wrangler/workerd": ["workerd@1.20250803.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20250803.0", "@cloudflare/workerd-darwin-arm64": "1.20250803.0", "@cloudflare/workerd-linux-64": "1.20250803.0", "@cloudflare/workerd-linux-arm64": "1.20250803.0", "@cloudflare/workerd-windows-64": "1.20250803.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-oYH29mE/wNolPc32NHHQbySaNorj6+KASUtOvQHySxB5mO1NWdGuNv49woxNCF5971UYceGQndY+OLT+24C3wQ=="],
|
||||
|
||||
"@opencode-ai/www/@astrojs/cloudflare/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.10.1", "", { "dependencies": { "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", "js-yaml": "^4.1.1", "picomatch": "^4.0.4", "retext-smartypants": "^6.2.0", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "unified": "^11.0.5" } }, "sha512-5phcroT/vmOOrYuuAxtkbPixy5hePtlz9i8K4OeDv3dNK6/UQRuXPOSRTxIOBbUY5Sonw2UaxjbuVc43Mcir6Q=="],
|
||||
|
||||
"@opencode-ai/www/@astrojs/cloudflare/@astrojs/underscore-redirects": ["@astrojs/underscore-redirects@1.0.3", "", {}, "sha512-cxnGSw+sJigBLdX4TMSZKkzV6C3gMLJMucDk2W+n281Xhie68T2/9f1+1NMNDCZsc5i0FED7Qt5I10g2O9wtZg=="],
|
||||
@@ -7653,30 +7533,6 @@
|
||||
|
||||
"@solidjs/start/shiki/@shikijs/types": ["@shikijs/types@1.29.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw=="],
|
||||
|
||||
"@solidjs/start/vitest/@vitest/expect": ["@vitest/expect@4.1.10", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.10", "@vitest/utils": "4.1.10", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA=="],
|
||||
|
||||
"@solidjs/start/vitest/@vitest/mocker": ["@vitest/mocker@4.1.10", "", { "dependencies": { "@vitest/spy": "4.1.10", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow=="],
|
||||
|
||||
"@solidjs/start/vitest/@vitest/pretty-format": ["@vitest/pretty-format@4.1.10", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q=="],
|
||||
|
||||
"@solidjs/start/vitest/@vitest/runner": ["@vitest/runner@4.1.10", "", { "dependencies": { "@vitest/utils": "4.1.10", "pathe": "^2.0.3" } }, "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg=="],
|
||||
|
||||
"@solidjs/start/vitest/@vitest/snapshot": ["@vitest/snapshot@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "@vitest/utils": "4.1.10", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw=="],
|
||||
|
||||
"@solidjs/start/vitest/@vitest/spy": ["@vitest/spy@4.1.10", "", {}, "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw=="],
|
||||
|
||||
"@solidjs/start/vitest/@vitest/utils": ["@vitest/utils@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA=="],
|
||||
|
||||
"@solidjs/start/vitest/es-module-lexer": ["es-module-lexer@2.3.1", "", {}, "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA=="],
|
||||
|
||||
"@solidjs/start/vitest/std-env": ["std-env@4.2.0", "", {}, "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw=="],
|
||||
|
||||
"@solidjs/start/vitest/tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="],
|
||||
|
||||
"@solidjs/start/vitest/tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="],
|
||||
|
||||
"@solidjs/start/vitest/vite": ["vite@8.1.5", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", "postcss": "^8.5.17", "rolldown": "~1.1.5", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw=="],
|
||||
|
||||
"@storybook/addon-docs/react-dom/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||
|
||||
"@storybook/csf-plugin/unplugin/acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="],
|
||||
@@ -7689,6 +7545,8 @@
|
||||
|
||||
"@vercel/routing-utils/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||
|
||||
"@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="],
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/openai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.14", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-5X1k57JBJ4H7H1QjX7CnJYAB1I19r/trVZTMcSms7/kLNZ8RaU4Nt2agcwZzv82Hfx6Q7/TOLU7agAKeFfc8cA=="],
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.38", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-/HHGmtKllqjg1OLc023v9w9kK3laW7Z6TzfZukYQWCsGBbzB9p60zTvvpXFVcs44NZBVXL3viOa1HRKUbeee8g=="],
|
||||
@@ -7909,8 +7767,6 @@
|
||||
|
||||
"rimraf/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
|
||||
|
||||
"storybook/@vitest/expect/@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="],
|
||||
|
||||
"storybook/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
|
||||
|
||||
"storybook/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="],
|
||||
@@ -7985,6 +7841,12 @@
|
||||
|
||||
"venice-ai-sdk-provider/@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.35", "", { "dependencies": { "@ai-sdk/provider": "3.0.13", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bjYld/2KGPLt78kpqbya+fD4LYS7BqVQJyUjE3qAHrYB0FR2Q90BaWEVIBZaguTWXf/A8L6uG1zO1v9TxVlGWg=="],
|
||||
|
||||
"vitest/@vitest/expect/chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
|
||||
|
||||
"vitest/vite/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="],
|
||||
|
||||
"vitest/vite/lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="],
|
||||
|
||||
"vscode-languageserver/vscode-languageserver-protocol/vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="],
|
||||
|
||||
"wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="],
|
||||
@@ -8385,74 +8247,6 @@
|
||||
|
||||
"@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.933.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-o1GX0+IPlFi/D8ei9y/jj3yucJWNfPnbB5appVBWevAyUdZA5KzQ2nK/hDxiu9olTZlFEFpf1m1Rn3FaGxHqsw=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/miniflare/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260120.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-JLHx3p5dpwz4wjVSis45YNReftttnI3ndhdMh5BUbbpdreN/g0jgxNt5Qp9tDFqEKl++N63qv+hxJiIIvSLR+Q=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/miniflare/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260120.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-1Md2tCRhZjwajsZNOiBeOVGiS3zbpLPzUDjHr4+XGTXWOA6FzzwScJwQZLa0Doc28Cp4Nr1n7xGL0Dwiz1XuOA=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/miniflare/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260120.0", "", { "os": "linux", "cpu": "x64" }, "sha512-O0mIfJfvU7F8N5siCoRDaVDuI12wkz2xlG4zK6/Ct7U9c9FiE0ViXNFWXFQm5PPj+qbkNRyhjUwhP+GCKTk5EQ=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/miniflare/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260120.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-aRHO/7bjxVpjZEmVVcpmhbzpN6ITbFCxuLLZSW0H9O0C0w40cDCClWSi19T87Ax/PQcYjFNT22pTewKsupkckA=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/miniflare/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260120.0", "", { "os": "win32", "cpu": "x64" }, "sha512-ASZIz1E8sqZQqQCgcfY1PJbBpUDrxPt8NZ+lqNil0qxnO4qX38hbCsdDF2/TDAuq0Txh7nu8ztgTelfNDlb4EA=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/wrangler/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260120.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-JLHx3p5dpwz4wjVSis45YNReftttnI3ndhdMh5BUbbpdreN/g0jgxNt5Qp9tDFqEKl++N63qv+hxJiIIvSLR+Q=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/wrangler/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260120.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-1Md2tCRhZjwajsZNOiBeOVGiS3zbpLPzUDjHr4+XGTXWOA6FzzwScJwQZLa0Doc28Cp4Nr1n7xGL0Dwiz1XuOA=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/wrangler/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260120.0", "", { "os": "linux", "cpu": "x64" }, "sha512-O0mIfJfvU7F8N5siCoRDaVDuI12wkz2xlG4zK6/Ct7U9c9FiE0ViXNFWXFQm5PPj+qbkNRyhjUwhP+GCKTk5EQ=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/wrangler/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260120.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-aRHO/7bjxVpjZEmVVcpmhbzpN6ITbFCxuLLZSW0H9O0C0w40cDCClWSi19T87Ax/PQcYjFNT22pTewKsupkckA=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/wrangler/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260120.0", "", { "os": "win32", "cpu": "x64" }, "sha512-ASZIz1E8sqZQqQCgcfY1PJbBpUDrxPt8NZ+lqNil0qxnO4qX38hbCsdDF2/TDAuq0Txh7nu8ztgTelfNDlb4EA=="],
|
||||
|
||||
"@electron/asar/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"@electron/universal/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
@@ -8597,74 +8391,6 @@
|
||||
|
||||
"@opencode-ai/updates/wrangler/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260708.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bC/aSAwLy16Vjo24i9XU3aWH+eRgz7NeR5xPKavGbembO18ZywYTQbXh14eXtY6fAqN3RzRG8psijTdhX4xydA=="],
|
||||
|
||||
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="],
|
||||
|
||||
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.4", "", { "os": "android", "cpu": "arm" }, "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ=="],
|
||||
|
||||
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.4", "", { "os": "android", "cpu": "arm64" }, "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A=="],
|
||||
|
||||
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.4", "", { "os": "android", "cpu": "x64" }, "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ=="],
|
||||
|
||||
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g=="],
|
||||
|
||||
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A=="],
|
||||
|
||||
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ=="],
|
||||
|
||||
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ=="],
|
||||
|
||||
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.4", "", { "os": "linux", "cpu": "arm" }, "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ=="],
|
||||
|
||||
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ=="],
|
||||
|
||||
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.4", "", { "os": "linux", "cpu": "ia32" }, "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ=="],
|
||||
|
||||
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA=="],
|
||||
|
||||
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg=="],
|
||||
|
||||
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag=="],
|
||||
|
||||
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA=="],
|
||||
|
||||
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g=="],
|
||||
|
||||
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.4", "", { "os": "linux", "cpu": "x64" }, "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA=="],
|
||||
|
||||
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.4", "", { "os": "none", "cpu": "arm64" }, "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ=="],
|
||||
|
||||
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.4", "", { "os": "none", "cpu": "x64" }, "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw=="],
|
||||
|
||||
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.4", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A=="],
|
||||
|
||||
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw=="],
|
||||
|
||||
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.4", "", { "os": "sunos", "cpu": "x64" }, "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q=="],
|
||||
|
||||
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ=="],
|
||||
|
||||
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg=="],
|
||||
|
||||
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.4", "", { "os": "win32", "cpu": "x64" }, "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ=="],
|
||||
|
||||
"@opencode-ai/workerd-spike/wrangler/miniflare/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
|
||||
|
||||
"@opencode-ai/workerd-spike/wrangler/miniflare/undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="],
|
||||
|
||||
"@opencode-ai/workerd-spike/wrangler/miniflare/ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
|
||||
|
||||
"@opencode-ai/workerd-spike/wrangler/miniflare/zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="],
|
||||
|
||||
"@opencode-ai/workerd-spike/wrangler/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20250803.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-6QciMnJp1p3F1qUiN0LaLfmw7SuZA/gfUBOe8Ft81pw16JYZ3CyiqIKPJvc1SV8jgDx8r+gz/PRi1NwOMt329A=="],
|
||||
|
||||
"@opencode-ai/workerd-spike/wrangler/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20250803.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DoIgghDowtqoNhL6OoN/F92SKtrk7mRQKc4YSs/Dst8IwFZq+pCShOlWfB0MXqHKPSoiz5xLSrUKR9H6gQMPvw=="],
|
||||
|
||||
"@opencode-ai/workerd-spike/wrangler/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20250803.0", "", { "os": "linux", "cpu": "x64" }, "sha512-mYdz4vNWX3+PoqRjssepVQqgh42IBiSrl+wb7vbh7VVWUVzBnQKtW3G+UFiBF62hohCLexGIEi7L0cFfRlcKSQ=="],
|
||||
|
||||
"@opencode-ai/workerd-spike/wrangler/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20250803.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-RmrtUYLRUg6djKU7Z6yebS6YGJVnaDVY6bbXca+2s26vw4ibJDOTPLuBHFQF62Grw3fAfsNbjQh5i14vG2mqUg=="],
|
||||
|
||||
"@opencode-ai/workerd-spike/wrangler/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20250803.0", "", { "os": "win32", "cpu": "x64" }, "sha512-uLV8gdudz36o9sUaAKbBxxTwZwLFz1KyW7QpBvOo4+r3Ib8yVKXGiySIMWGD7A0urSMrjf3e5LlLcJKgZUOjMA=="],
|
||||
|
||||
"@opencode-ai/www/@astrojs/cloudflare/@astrojs/internal-helpers/js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="],
|
||||
|
||||
"@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.1", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": ">1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw=="],
|
||||
@@ -8875,12 +8601,6 @@
|
||||
|
||||
"@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="],
|
||||
|
||||
"@solidjs/start/vitest/@vitest/expect/chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
|
||||
|
||||
"@solidjs/start/vitest/vite/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="],
|
||||
|
||||
"@solidjs/start/vitest/vite/lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="],
|
||||
|
||||
"ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
|
||||
@@ -9039,8 +8759,6 @@
|
||||
|
||||
"rimraf/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||
|
||||
"storybook/@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="],
|
||||
|
||||
"temp/rimraf/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
|
||||
|
||||
"tw-to-css/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
@@ -9049,6 +8767,78 @@
|
||||
|
||||
"unplugin/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
||||
|
||||
"vitest/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
|
||||
|
||||
"vitest/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="],
|
||||
|
||||
"vitest/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="],
|
||||
|
||||
"vitest/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="],
|
||||
|
||||
"vitest/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="],
|
||||
|
||||
"vitest/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="],
|
||||
|
||||
"vitest/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="],
|
||||
|
||||
"vitest/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="],
|
||||
|
||||
"vitest/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="],
|
||||
|
||||
"vitest/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="],
|
||||
|
||||
"vitest/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="],
|
||||
|
||||
"vitest/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="],
|
||||
|
||||
"vitest/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="],
|
||||
|
||||
"vitest/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="],
|
||||
|
||||
"vitest/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="],
|
||||
|
||||
"vitest/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="],
|
||||
|
||||
"vitest/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="],
|
||||
|
||||
"vitest/vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="],
|
||||
|
||||
"vitest/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="],
|
||||
|
||||
"vitest/vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="],
|
||||
|
||||
"vitest/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="],
|
||||
|
||||
"vitest/vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="],
|
||||
|
||||
"vitest/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="],
|
||||
|
||||
"vitest/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="],
|
||||
|
||||
"vitest/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="],
|
||||
|
||||
"vitest/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="],
|
||||
|
||||
"vitest/vite/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="],
|
||||
|
||||
"vitest/vite/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="],
|
||||
|
||||
"vitest/vite/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="],
|
||||
|
||||
"vitest/vite/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="],
|
||||
|
||||
"vitest/vite/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="],
|
||||
|
||||
"vitest/vite/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="],
|
||||
|
||||
"vitest/vite/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="],
|
||||
|
||||
"vitest/vite/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="],
|
||||
|
||||
"vitest/vite/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="],
|
||||
|
||||
"vitest/vite/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="],
|
||||
|
||||
"yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"@astrojs/cloudflare/wrangler/miniflare/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
|
||||
@@ -9153,8 +8943,6 @@
|
||||
|
||||
"@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.782.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.775.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", "@aws-sdk/middleware-user-agent": "3.782.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.782.0", "@aws-sdk/util-user-agent-browser": "3.775.0", "@aws-sdk/util-user-agent-node": "3.782.0", "@smithy/config-resolver": "^4.1.0", "@smithy/core": "^3.2.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", "@smithy/middleware-endpoint": "^4.1.0", "@smithy/middleware-retry": "^4.1.0", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.8", "@smithy/util-defaults-mode-node": "^4.0.8", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-QOYC8q7luzHFXrP0xYAqBctoPkynjfV0r9dqntFu4/IWMTyC1vlo1UTxFAjIPyclYw92XJyEkVCVg9v/nQnsUA=="],
|
||||
|
||||
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
|
||||
|
||||
"@jsx-email/cli/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
||||
|
||||
"@opencode-ai/desktop/@actions/artifact/@actions/core/@actions/exec/@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="],
|
||||
@@ -9383,78 +9171,6 @@
|
||||
|
||||
"@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex-recursion": ["regex-recursion@5.1.1", "", { "dependencies": { "regex": "^5.1.1", "regex-utilities": "^2.3.0" } }, "sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w=="],
|
||||
|
||||
"@solidjs/start/vitest/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
|
||||
|
||||
"@solidjs/start/vitest/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="],
|
||||
|
||||
"@solidjs/start/vitest/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="],
|
||||
|
||||
"@solidjs/start/vitest/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="],
|
||||
|
||||
"@solidjs/start/vitest/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="],
|
||||
|
||||
"@solidjs/start/vitest/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="],
|
||||
|
||||
"@solidjs/start/vitest/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="],
|
||||
|
||||
"@solidjs/start/vitest/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="],
|
||||
|
||||
"@solidjs/start/vitest/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="],
|
||||
|
||||
"@solidjs/start/vitest/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="],
|
||||
|
||||
"@solidjs/start/vitest/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="],
|
||||
|
||||
"@solidjs/start/vitest/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="],
|
||||
|
||||
"@solidjs/start/vitest/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="],
|
||||
|
||||
"@solidjs/start/vitest/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="],
|
||||
|
||||
"@solidjs/start/vitest/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="],
|
||||
|
||||
"@solidjs/start/vitest/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="],
|
||||
|
||||
"@solidjs/start/vitest/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="],
|
||||
|
||||
"@solidjs/start/vitest/vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="],
|
||||
|
||||
"@solidjs/start/vitest/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="],
|
||||
|
||||
"@solidjs/start/vitest/vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="],
|
||||
|
||||
"@solidjs/start/vitest/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="],
|
||||
|
||||
"@solidjs/start/vitest/vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="],
|
||||
|
||||
"@solidjs/start/vitest/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="],
|
||||
|
||||
"@solidjs/start/vitest/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="],
|
||||
|
||||
"@solidjs/start/vitest/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="],
|
||||
|
||||
"@solidjs/start/vitest/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="],
|
||||
|
||||
"@solidjs/start/vitest/vite/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="],
|
||||
|
||||
"@solidjs/start/vitest/vite/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="],
|
||||
|
||||
"@solidjs/start/vitest/vite/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="],
|
||||
|
||||
"@solidjs/start/vitest/vite/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="],
|
||||
|
||||
"@solidjs/start/vitest/vite/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="],
|
||||
|
||||
"@solidjs/start/vitest/vite/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="],
|
||||
|
||||
"@solidjs/start/vitest/vite/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="],
|
||||
|
||||
"@solidjs/start/vitest/vite/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="],
|
||||
|
||||
"@solidjs/start/vitest/vite/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="],
|
||||
|
||||
"@solidjs/start/vitest/vite/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="],
|
||||
|
||||
"archiver-utils/glob/jackspeak/@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
|
||||
|
||||
"archiver-utils/glob/jackspeak/@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
|
||||
|
||||
@@ -172,7 +172,6 @@
|
||||
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
|
||||
"effect@4.0.0-beta.101": "patches/effect@4.0.0-beta.101.patch",
|
||||
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch",
|
||||
"@cloudflare/vitest-pool-workers@0.12.6": "patches/@cloudflare%2Fvitest-pool-workers@0.12.6.patch",
|
||||
"@ff-labs/fff-bun@0.10.1": "patches/@ff-labs%2Ffff-bun@0.10.1.patch"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,6 @@ export default Runtime.handler(Commands, (input) =>
|
||||
Effect.gen(function* () {
|
||||
const requestedDirectory = Option.getOrUndefined(input.directory)
|
||||
if (requestedDirectory !== undefined) process.chdir(requestedDirectory)
|
||||
const updater = yield* Updater.Service
|
||||
yield* updater.check().pipe(Effect.forkScoped)
|
||||
const preflight = UpdatePreflight.make()
|
||||
yield* Effect.addFinalizer(() => Effect.promise(() => preflight.close()))
|
||||
const server = yield* ServerConnection.resolve({
|
||||
@@ -36,6 +34,8 @@ export default Runtime.handler(Commands, (input) =>
|
||||
Effect.promise(() => preflight.fail("OpenCode update could not start the new background service")),
|
||||
),
|
||||
)
|
||||
const updater = yield* Updater.Service
|
||||
yield* updater.check().pipe(Effect.forkScoped)
|
||||
preflight.loading()
|
||||
const config = yield* Config.Service
|
||||
const npm = yield* Npm.Service
|
||||
|
||||
@@ -358,6 +358,15 @@ export type Endpoint5_31Output =
|
||||
readonly previous?: Model.Ref | undefined
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: Event.ID
|
||||
readonly created: DateTime.Utc
|
||||
readonly metadata?: { readonly [x: string]: unknown } | undefined
|
||||
readonly type: "session.move.admitted"
|
||||
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
|
||||
readonly location?: Location.Ref | undefined
|
||||
readonly data: { readonly sessionID: Session.ID; readonly move: SessionPending.MoveData }
|
||||
}
|
||||
| {
|
||||
readonly id: Event.ID
|
||||
readonly created: DateTime.Utc
|
||||
@@ -367,6 +376,7 @@ export type Endpoint5_31Output =
|
||||
readonly location?: Location.Ref | undefined
|
||||
readonly data: {
|
||||
readonly sessionID: Session.ID
|
||||
readonly moveID?: Event.ID | undefined
|
||||
readonly location: Location.Ref
|
||||
readonly projectID?: Project.ID | undefined
|
||||
readonly subpath?: RelativePath | undefined
|
||||
|
||||
@@ -420,6 +420,8 @@ export type SessionMessageLocationSwitched = {
|
||||
previous?: { location: LocationRef; projectID?: string; subpath?: string }
|
||||
}
|
||||
|
||||
export type SessionPendingMoveData = { location: LocationRef; projectID: string; subpath?: string }
|
||||
|
||||
export type SessionCreated = {
|
||||
id: string
|
||||
created: number
|
||||
@@ -468,7 +470,7 @@ export type SessionMoved = {
|
||||
type: "session.moved"
|
||||
durable: { aggregateID: string; seq: number; version: 1 }
|
||||
location?: LocationRef
|
||||
data: { sessionID: string; location: LocationRef; projectID?: string; subpath?: string }
|
||||
data: { sessionID: string; moveID?: string; location: LocationRef; projectID?: string; subpath?: string }
|
||||
}
|
||||
|
||||
export type SessionRenamed = {
|
||||
@@ -1527,6 +1529,24 @@ export type VcsInfo = { branch: VcsBranch }
|
||||
|
||||
export type PermissionRuleset = Array<PermissionRule>
|
||||
|
||||
export type SessionPendingMove = {
|
||||
id: string
|
||||
sessionID: string
|
||||
timeCreated: number
|
||||
type: "move"
|
||||
data: SessionPendingMoveData
|
||||
}
|
||||
|
||||
export type SessionMoveAdmitted = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: { [x: string]: any }
|
||||
type: "session.move.admitted"
|
||||
durable: { aggregateID: string; seq: number; version: 1 }
|
||||
location?: LocationRef
|
||||
data: { sessionID: string; move: SessionPendingMoveData }
|
||||
}
|
||||
|
||||
export type SessionInfo = {
|
||||
id: string
|
||||
parentID?: string
|
||||
@@ -1914,7 +1934,11 @@ export type FormFields = [FormField, ...Array<FormField>]
|
||||
|
||||
export type FormFields3 = [FormField1, ...Array<FormField1>]
|
||||
|
||||
export type SessionPendingInfo = SessionPendingUser | SessionPendingSynthetic | SessionPendingCompaction
|
||||
export type SessionPendingInfo =
|
||||
| SessionPendingUser
|
||||
| SessionPendingSynthetic
|
||||
| SessionPendingCompaction
|
||||
| SessionPendingMove
|
||||
|
||||
export type SessionPendingMessage = SessionPendingUserMessage | SessionPendingSyntheticMessage
|
||||
|
||||
@@ -1983,6 +2007,7 @@ export type SessionEventDurable =
|
||||
| SessionCreated
|
||||
| SessionAgentSelected
|
||||
| SessionModelSelected
|
||||
| SessionMoveAdmitted
|
||||
| SessionMoved
|
||||
| SessionRenamed
|
||||
| SessionDeleted
|
||||
@@ -2046,6 +2071,7 @@ export type V2Event =
|
||||
| SessionCreated
|
||||
| SessionAgentSelected
|
||||
| SessionModelSelected
|
||||
| SessionMoveAdmitted
|
||||
| SessionMoved
|
||||
| SessionRenamed
|
||||
| SessionUsageUpdated
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# @opencode-ai/core
|
||||
|
||||
Core runtime services for OpenCode.
|
||||
+13
-26
@@ -4,28 +4,24 @@
|
||||
"name": "@opencode-ai/core",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/anomalyco/opencode.git",
|
||||
"directory": "packages/core"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"db": "bun drizzle-kit",
|
||||
"migration": "bun run script/migration.ts",
|
||||
"fix-node-pty": "bun run script/fix-node-pty.ts",
|
||||
"benchmark:location": "bun run script/benchmark-location.ts",
|
||||
"build": "bun run script/build.ts",
|
||||
"update-models-snapshot": "bun run script/update-models-snapshot.ts",
|
||||
"test": "bun test --only-failures",
|
||||
"typecheck": "tsgo -b tsconfig.json tsconfig.tests.json"
|
||||
},
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode"
|
||||
},
|
||||
"exports": {
|
||||
"./environment": "./src/environment/index.ts",
|
||||
"./testing/environment-conformance": "./test/lib/environment-conformance.ts",
|
||||
"./session/runner": "./src/session/runner/index.ts",
|
||||
"./instructions": "./src/instructions/index.ts",
|
||||
"./*": "./src/*.ts"
|
||||
},
|
||||
"imports": {
|
||||
@@ -36,46 +32,32 @@
|
||||
"default": "./src/database/sqlite.node.ts"
|
||||
},
|
||||
"#pty": {
|
||||
"workerd": "./src/pty/pty.workerd.ts",
|
||||
"bun": "./src/pty/pty.bun.ts",
|
||||
"node": "./src/pty/pty.node.ts",
|
||||
"default": "./src/pty/pty.bun.ts"
|
||||
},
|
||||
"#fff": {
|
||||
"workerd": "./src/filesystem/fff.workerd.ts",
|
||||
"bun": "./src/filesystem/fff.bun.ts",
|
||||
"node": "./src/filesystem/fff.node.ts",
|
||||
"default": "./src/filesystem/fff.bun.ts"
|
||||
},
|
||||
"#photon-wasm": {
|
||||
"workerd": "./src/image/photon-wasm.workerd.ts",
|
||||
"bun": "./src/image/photon-wasm.bun.ts",
|
||||
"node": "./src/image/photon-wasm.node.ts",
|
||||
"default": "./src/image/photon-wasm.bun.ts"
|
||||
},
|
||||
"#shell-parser-wasm": {
|
||||
"workerd": "./src/shell/parser-wasm.workerd.ts",
|
||||
"bun": "./src/shell/parser-wasm.bun.ts",
|
||||
"node": "./src/shell/parser-wasm.node.ts",
|
||||
"default": "./src/shell/parser-wasm.bun.ts"
|
||||
},
|
||||
"#process-lock-ffi": {
|
||||
"workerd": "./src/util/process-lock-ffi.workerd.ts",
|
||||
"bun": "./src/util/process-lock-ffi.bun.ts",
|
||||
"node": "./src/util/process-lock-ffi.node.ts",
|
||||
"default": "./src/util/process-lock-ffi.bun.ts"
|
||||
},
|
||||
"#v1-migration": {
|
||||
"types": "./src/database/v1-migration.bun.ts",
|
||||
"bun": "./src/database/v1-migration.bun.ts",
|
||||
"node": "./src/database/v1-migration.noop.ts",
|
||||
"workerd": "./src/database/v1-migration.noop.ts",
|
||||
"default": "./src/database/v1-migration.noop.ts"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@effect/sql-sqlite-bun": "catalog:",
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
@@ -111,6 +93,8 @@
|
||||
"@ai-sdk/togetherai": "2.0.41",
|
||||
"@ai-sdk/vercel": "2.0.39",
|
||||
"@aws-sdk/credential-providers": "3.1057.0",
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@effect/sql-sqlite-bun": "catalog:",
|
||||
"@lydell/node-pty": "catalog:",
|
||||
"@modelcontextprotocol/sdk": "1.29.0",
|
||||
"@ff-labs/fff-bun": "0.10.1",
|
||||
@@ -144,5 +128,8 @@
|
||||
"web-tree-sitter": "0.25.10",
|
||||
"which": "6.0.1",
|
||||
"zod": "catalog:"
|
||||
},
|
||||
"overrides": {
|
||||
"drizzle-orm": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { $ } from "bun"
|
||||
import { rm } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
|
||||
process.chdir(fileURLToPath(new URL("..", import.meta.url)))
|
||||
|
||||
await rm("dist", { recursive: true, force: true })
|
||||
await $`bun tsc -p tsconfig.build.json`
|
||||
|
||||
const root = path.resolve("src")
|
||||
const files = await Array.fromAsync(new Bun.Glob("**/*.ts").scan({ cwd: root, absolute: true }))
|
||||
const result = await Bun.build({
|
||||
entrypoints: files.filter((file) => !file.endsWith(".d.ts")),
|
||||
root,
|
||||
outdir: "dist",
|
||||
target: "node",
|
||||
format: "esm",
|
||||
packages: "external",
|
||||
external: ["#sqlite", "#pty", "#fff", "#photon-wasm", "#shell-parser-wasm", "#process-lock-ffi", "#v1-migration"],
|
||||
splitting: true,
|
||||
loader: {
|
||||
".txt": "text",
|
||||
".md": "text",
|
||||
},
|
||||
naming: {
|
||||
entry: "[dir]/[name].[ext]",
|
||||
chunk: "chunks/[name]-[hash].[ext]",
|
||||
asset: "assets/[name]-[hash].[ext]",
|
||||
},
|
||||
})
|
||||
if (!result.success) throw new AggregateError(result.logs, "Failed to build Core")
|
||||
@@ -1,50 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { Script } from "@opencode-ai/script"
|
||||
import { $ } from "bun"
|
||||
import { rm } from "node:fs/promises"
|
||||
import { fileURLToPath } from "node:url"
|
||||
|
||||
process.chdir(fileURLToPath(new URL("..", import.meta.url)))
|
||||
|
||||
const originalText = await Bun.file("package.json").text()
|
||||
const pkg = JSON.parse(originalText) as {
|
||||
name: string
|
||||
version: string
|
||||
exports: Record<string, string | { import: string; types: string }>
|
||||
imports: Record<string, Record<string, string>>
|
||||
}
|
||||
const tarball = `${pkg.name.replace("@", "").replace("/", "-")}-${pkg.version}.tgz`
|
||||
const output = (value: string, types = false) =>
|
||||
value.replace("./src/", types ? "./dist/types/" : "./dist/").replace(/\.ts$/, types ? ".d.ts" : ".js")
|
||||
|
||||
if ((await $`npm view ${pkg.name}@${pkg.version} version`.nothrow()).exitCode === 0) {
|
||||
console.log(`already published ${pkg.name}@${pkg.version}`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
try {
|
||||
await $`bun run typecheck`
|
||||
await $`bun run build`
|
||||
pkg.exports = Object.fromEntries(
|
||||
Object.entries(pkg.exports).map(([key, value]) => {
|
||||
if (typeof value !== "string") return [key, value]
|
||||
return [key, { import: output(value), types: output(value, true) }]
|
||||
}),
|
||||
)
|
||||
pkg.imports = Object.fromEntries(
|
||||
Object.entries(pkg.imports).map(([key, conditions]) => [
|
||||
key,
|
||||
Object.fromEntries(
|
||||
Object.entries(conditions).map(([condition, value]) => [condition, output(value, condition === "types")]),
|
||||
),
|
||||
]),
|
||||
)
|
||||
await Bun.write("package.json", JSON.stringify(pkg, null, 2) + "\n")
|
||||
await rm(tarball, { force: true })
|
||||
await $`bun pm pack`
|
||||
await $`npm publish ${tarball} --tag ${Script.channel} --access public`
|
||||
} finally {
|
||||
await Bun.write("package.json", originalText)
|
||||
await rm(tarball, { force: true })
|
||||
}
|
||||
@@ -75,9 +75,7 @@ export const create = (
|
||||
const outputFileParts = outputFiles(content)
|
||||
if (outputFileParts.length > 0)
|
||||
yield* Ref.update(files, (items) => [...items, { index, files: outputFileParts }])
|
||||
if (executed.output !== undefined) return executed.output
|
||||
const text = content.flatMap((part) => (part.type === "text" ? [part.text] : [])).join("\n")
|
||||
return text === "" ? null : text
|
||||
return executed.output
|
||||
}),
|
||||
{
|
||||
onToolCallStart: ({ index, name, input }) => {
|
||||
@@ -157,7 +155,7 @@ function runtime(
|
||||
tools[path] = Tool.make({
|
||||
description: child.description,
|
||||
input: child.inputSchema,
|
||||
output: child.outputSchema ?? Schema.NullOr(Schema.String),
|
||||
output: child.outputSchema,
|
||||
execute: (input) => executeTool(name, registration, input),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -65,13 +65,4 @@ export function configured(options?: Options) {
|
||||
return makeGlobalNode({ service: Service, layer: layer(options), deps: [Global.node] })
|
||||
}
|
||||
|
||||
/** `configured`, but over an injected SqlClient layer instead of a filesystem path. */
|
||||
export function configuredClient(client: Layer.Layer<SqlClient.SqlClient>) {
|
||||
return makeGlobalNode({
|
||||
service: Service,
|
||||
layer: layerFromClient.pipe(Layer.provide(client)),
|
||||
deps: [Global.node],
|
||||
})
|
||||
}
|
||||
|
||||
export const node = configured({ path: ":memory:" })
|
||||
|
||||
@@ -28,20 +28,6 @@ export interface EffectSQLiteQueryEffectHKT extends QueryEffectHKTBase {
|
||||
readonly context: never
|
||||
}
|
||||
|
||||
/**
|
||||
* A SqlClient whose runtime rejects SQL transaction statements (BEGIN/COMMIT/SAVEPOINT) and
|
||||
* manages transactions natively instead — Cloudflare Durable Object SQLite. Producers set
|
||||
* `transactionStatements: false` (see `database/sqlite.workerd.ts`) and the session delegates
|
||||
* to the client's `withTransaction` rather than issuing transaction statements.
|
||||
*/
|
||||
export interface NativeTransactionSqlClient extends SqlClient {
|
||||
readonly transactionStatements: false
|
||||
}
|
||||
|
||||
export function managesTransactionsNatively(client: SqlClient): client is NativeTransactionSqlClient {
|
||||
return (client as SqlClient & { readonly transactionStatements?: boolean }).transactionStatements === false
|
||||
}
|
||||
|
||||
export type EffectSQLiteRunResult = readonly never[]
|
||||
|
||||
export interface EffectSQLiteSessionOptions {
|
||||
@@ -134,7 +120,6 @@ export class EffectSQLiteSession<TRelations extends AnyRelations> extends SQLite
|
||||
}
|
||||
|
||||
private withTransaction<A, E, R>(effect: Effect.Effect<A, E, R>, config: SQLiteTransactionConfig | undefined) {
|
||||
if (managesTransactionsNatively(this.client)) return this.client.withTransaction(effect)
|
||||
return Effect.uninterruptibleMask((restore) =>
|
||||
Effect.withFiber<A, E | SqlError, R>((fiber) => {
|
||||
const services = fiber.context
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Database, type SQLQueryBindings } from "bun:sqlite"
|
||||
import { drizzle } from "drizzle-orm/bun-sqlite"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import { Reactivity } from "effect/unstable/reactivity"
|
||||
import { SqlClient } from "effect/unstable/sql"
|
||||
@@ -97,7 +98,16 @@ const nativeLayer = (config: Config) =>
|
||||
|
||||
const clientLayer = (config: Config) => Layer.effect(SqlClient.SqlClient, make(config))
|
||||
|
||||
const drizzleLayer = Layer.effect(
|
||||
Sqlite.Drizzle,
|
||||
Effect.gen(function* () {
|
||||
return drizzle({ client: (yield* Sqlite.Native) as Database })
|
||||
}),
|
||||
)
|
||||
|
||||
export const sqliteLayer = (config: Config) => {
|
||||
const native = nativeLayer(config)
|
||||
return Layer.merge(native, clientLayer(config).pipe(Layer.provide(native))).pipe(Layer.provide(Reactivity.layer))
|
||||
return Layer.merge(native, Layer.merge(clientLayer(config), drizzleLayer).pipe(Layer.provide(native))).pipe(
|
||||
Layer.provide(Reactivity.layer),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { DatabaseSync, type SQLInputValue } from "node:sqlite"
|
||||
import { drizzle } from "drizzle-orm/node-sqlite"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import { Reactivity } from "effect/unstable/reactivity"
|
||||
import { SqlClient } from "effect/unstable/sql"
|
||||
@@ -94,7 +95,16 @@ const nativeLayer = (config: Config) =>
|
||||
|
||||
const clientLayer = (config: Config) => Layer.effect(SqlClient.SqlClient, make(config))
|
||||
|
||||
const drizzleLayer = Layer.effect(
|
||||
Sqlite.Drizzle,
|
||||
Effect.gen(function* () {
|
||||
return drizzle({ client: (yield* Sqlite.Native) as DatabaseSync }) as unknown as Sqlite.DrizzleClient
|
||||
}),
|
||||
)
|
||||
|
||||
export const sqliteLayer = (config: Config) => {
|
||||
const native = nativeLayer(config)
|
||||
return Layer.merge(native, clientLayer(config).pipe(Layer.provide(native))).pipe(Layer.provide(Reactivity.layer))
|
||||
return Layer.merge(native, Layer.merge(clientLayer(config), drizzleLayer).pipe(Layer.provide(native))).pipe(
|
||||
Layer.provide(Reactivity.layer),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,8 +5,11 @@ import { identity } from "effect/Function"
|
||||
import { SqlClient, Statement } from "effect/unstable/sql"
|
||||
import type { Connection } from "effect/unstable/sql/SqlConnection"
|
||||
import type { SqlError } from "effect/unstable/sql/SqlError"
|
||||
import type { drizzle } from "drizzle-orm/bun-sqlite"
|
||||
|
||||
export type DrizzleClient = ReturnType<typeof drizzle>
|
||||
export class Native extends Context.Service<Native, unknown>()("@opencode-ai/core/database/SqliteNative") {}
|
||||
export class Drizzle extends Context.Service<Drizzle, DrizzleClient>()("@opencode-ai/core/database/SqliteDrizzle") {}
|
||||
|
||||
export interface ClientConfig {
|
||||
readonly spanAttributes?: Record<string, unknown>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { drizzle } from "drizzle-orm/durable-sqlite"
|
||||
import { Context, Effect, Exit, Fiber, Layer, Scope, Semaphore, Stream } from "effect"
|
||||
import { identity } from "effect/Function"
|
||||
import { Reactivity } from "effect/unstable/reactivity"
|
||||
import { SqlClient, Statement } from "effect/unstable/sql"
|
||||
import type { Connection } from "effect/unstable/sql/SqlConnection"
|
||||
import { classifySqliteError, SqlError, UnknownError } from "effect/unstable/sql/SqlError"
|
||||
import type { NativeTransactionSqlClient } from "./drizzle/effect-sqlite/session.js"
|
||||
import { Sqlite } from "./sqlite.js"
|
||||
|
||||
const ATTR_DB_SYSTEM_NAME = "db.system.name"
|
||||
@@ -216,10 +216,10 @@ const make = (options: Config) =>
|
||||
config: options,
|
||||
withTransaction: makeWithTransaction(native, connection, semaphore),
|
||||
// Durable Object SQLite rejects BEGIN/COMMIT/SAVEPOINT; consumers such
|
||||
// as the drizzle session detect this and route through withTransaction.
|
||||
// as the drizzle session must route through withTransaction instead.
|
||||
transactionStatements: false,
|
||||
} as const,
|
||||
) satisfies NativeTransactionSqlClient
|
||||
},
|
||||
)
|
||||
|
||||
return client
|
||||
})
|
||||
@@ -238,7 +238,17 @@ const nativeLayer = (config: Config) =>
|
||||
|
||||
const clientLayer = (config: Config) => Layer.effect(SqlClient.SqlClient, make(config))
|
||||
|
||||
const drizzleLayer = Layer.effect(
|
||||
Sqlite.Drizzle,
|
||||
Effect.gen(function* () {
|
||||
const native = (yield* Sqlite.Native) as DurableObjectStorage
|
||||
return drizzle(native) as unknown as Sqlite.DrizzleClient
|
||||
}),
|
||||
)
|
||||
|
||||
export const sqliteLayer = (config: Config) => {
|
||||
const native = nativeLayer(config)
|
||||
return Layer.merge(native, clientLayer(config).pipe(Layer.provide(native))).pipe(Layer.provide(Reactivity.layer))
|
||||
return Layer.merge(native, Layer.merge(clientLayer(config), drizzleLayer).pipe(Layer.provide(native))).pipe(
|
||||
Layer.provide(Reactivity.layer),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,997 +0,0 @@
|
||||
export * as V1Migration from "./v1-migration.js"
|
||||
|
||||
import { Cause, Effect, Layer, Option, Schema, Semaphore } from "effect"
|
||||
import { Database } from "./database.js"
|
||||
import { SessionMessageTable, SessionTable } from "../session/sql.js"
|
||||
import { SessionV1 } from "@opencode-ai/schema/session-v1"
|
||||
import { SessionMessage } from "../session/message.js"
|
||||
import { SessionSchema } from "../session/schema.js"
|
||||
import { KVTable } from "../kv/sql.js"
|
||||
import { EventSequenceTable } from "../event/sql.js"
|
||||
import { eq, sql } from "drizzle-orm"
|
||||
import { Global } from "@opencode-ai/util/global"
|
||||
import { existsSync } from "node:fs"
|
||||
import path from "node:path"
|
||||
import type { Database as SQLiteDatabase } from "bun:sqlite"
|
||||
import { Project } from "@opencode-ai/schema/project"
|
||||
|
||||
export type SourceMessage = {
|
||||
readonly id: string
|
||||
readonly session_id: string
|
||||
readonly time_created: number
|
||||
readonly time_updated: number
|
||||
readonly data: string
|
||||
}
|
||||
|
||||
export type SourcePart = {
|
||||
readonly id: string
|
||||
readonly message_id: string
|
||||
readonly session_id: string
|
||||
readonly time_created: number
|
||||
readonly time_updated: number
|
||||
readonly data: string
|
||||
}
|
||||
|
||||
export type TransformInput = {
|
||||
readonly session: typeof SessionTable.$inferSelect
|
||||
readonly messages: ReadonlyArray<SourceMessage>
|
||||
readonly parts: ReadonlyArray<SourcePart>
|
||||
}
|
||||
|
||||
export type Warning = {
|
||||
readonly reason: string
|
||||
readonly sessionID: string
|
||||
readonly messageID?: string
|
||||
readonly partID?: string
|
||||
readonly observedType?: string
|
||||
}
|
||||
|
||||
export type TransformResult = {
|
||||
readonly messages: ReadonlyArray<{
|
||||
readonly id: string
|
||||
readonly session_id: string
|
||||
readonly type: SessionMessage.Type
|
||||
readonly seq: number
|
||||
readonly time_created: number
|
||||
readonly time_updated: number
|
||||
readonly data: Record<string, unknown>
|
||||
}>
|
||||
readonly session: Pick<
|
||||
typeof SessionTable.$inferInsert,
|
||||
| "agent"
|
||||
| "model"
|
||||
| "cost"
|
||||
| "tokens_input"
|
||||
| "tokens_output"
|
||||
| "tokens_reasoning"
|
||||
| "tokens_cache_read"
|
||||
| "tokens_cache_write"
|
||||
| "revert"
|
||||
| "time_compacting"
|
||||
>
|
||||
readonly watermark: number
|
||||
readonly warnings: ReadonlyArray<Warning>
|
||||
}
|
||||
|
||||
type Progress = {
|
||||
readonly label: string
|
||||
readonly numerator?: number
|
||||
readonly denominator?: number
|
||||
}
|
||||
|
||||
export type Status =
|
||||
| { readonly status: "required" | "completed" }
|
||||
| { readonly status: "running"; readonly progress: Progress }
|
||||
| { readonly status: "error"; readonly error: string }
|
||||
|
||||
type RunResult = {
|
||||
readonly status: "completed"
|
||||
}
|
||||
|
||||
type Options = {
|
||||
readonly nextDatabasePath?: string
|
||||
}
|
||||
|
||||
type MigrationState = { readonly phase: "sessions"; readonly cursor?: string } | { readonly phase: "completed" }
|
||||
|
||||
type RuntimeState =
|
||||
| { readonly status: "idle" }
|
||||
| { readonly status: "running"; readonly progress: Progress }
|
||||
| { readonly status: "error"; readonly error: string }
|
||||
|
||||
type NextProject = {
|
||||
readonly id: string
|
||||
readonly worktree: string
|
||||
readonly vcs: string | null
|
||||
readonly name: string | null
|
||||
readonly icon_url: string | null
|
||||
readonly icon_url_override: string | null
|
||||
readonly icon_color: string | null
|
||||
readonly time_created: number
|
||||
readonly time_updated: number
|
||||
readonly time_initialized: number | null
|
||||
readonly sandboxes: string
|
||||
readonly commands: string | null
|
||||
}
|
||||
|
||||
type NextSession = {
|
||||
readonly id: string
|
||||
readonly project_id: string
|
||||
readonly workspace_id: string | null
|
||||
readonly parent_id: string | null
|
||||
readonly fork_session_id: string | null
|
||||
readonly fork_boundary: string | null
|
||||
readonly slug: string
|
||||
readonly directory: string
|
||||
readonly path: string | null
|
||||
readonly title: string | null
|
||||
readonly version: string
|
||||
readonly share_url: string | null
|
||||
readonly summary_additions: number | null
|
||||
readonly summary_deletions: number | null
|
||||
readonly summary_files: number | null
|
||||
readonly summary_diffs: string | null
|
||||
readonly metadata: string | null
|
||||
readonly cost: number
|
||||
readonly tokens_input: number
|
||||
readonly tokens_output: number
|
||||
readonly tokens_reasoning: number
|
||||
readonly tokens_cache_read: number
|
||||
readonly tokens_cache_write: number
|
||||
readonly revert: string | null
|
||||
readonly permission: string | null
|
||||
readonly agent: string | null
|
||||
readonly model: string | null
|
||||
readonly time_created: number
|
||||
readonly time_updated: number
|
||||
readonly time_compacting: number | null
|
||||
readonly time_archived: number | null
|
||||
readonly time_suspended: number | null
|
||||
}
|
||||
|
||||
type NextMessage = {
|
||||
readonly id: string
|
||||
readonly session_id: string
|
||||
readonly type: string
|
||||
readonly seq: number
|
||||
readonly time_created: number
|
||||
readonly time_updated: number
|
||||
readonly data: string
|
||||
}
|
||||
|
||||
const lock = Semaphore.makeUnsafe(1)
|
||||
const MIGRATION_STATE_KEY = "migration.v1-v2"
|
||||
const EVENT_DELETE_BATCH_SIZE = 1_000
|
||||
const decodeJson = Schema.decodeUnknownOption(Schema.UnknownFromJsonString)
|
||||
const decodeMessage = Schema.decodeUnknownOption(SessionV1.Info)
|
||||
const decodePart = Schema.decodeUnknownOption(SessionV1.Part)
|
||||
let runtimeState: RuntimeState = { status: "idle" }
|
||||
|
||||
export function transformSession(input: TransformInput): TransformResult {
|
||||
const warnings: Warning[] = []
|
||||
const messages = input.messages
|
||||
.map((row) => {
|
||||
const value = Option.getOrUndefined(decodeJson(row.data))
|
||||
const decoded =
|
||||
value && typeof value === "object"
|
||||
? Option.getOrUndefined(decodeMessage({ ...value, id: row.id, sessionID: row.session_id }))
|
||||
: undefined
|
||||
if (decoded) return { row, value: decoded }
|
||||
warnings.push({ reason: "invalid-message", sessionID: input.session.id, messageID: row.id })
|
||||
return undefined
|
||||
})
|
||||
.filter((item): item is NonNullable<typeof item> => item !== undefined)
|
||||
.sort((a, b) => a.row.time_created - b.row.time_created || a.row.id.localeCompare(b.row.id))
|
||||
const messageIDs = new Set(input.messages.map((row) => row.id))
|
||||
const parts = input.parts
|
||||
.map((row) => {
|
||||
const value = Option.getOrUndefined(decodeJson(row.data))
|
||||
const observedType = value && typeof value === "object" && "type" in value ? String(value.type) : undefined
|
||||
if (!messageIDs.has(row.message_id)) {
|
||||
warnings.push({
|
||||
reason: "orphan-part",
|
||||
sessionID: input.session.id,
|
||||
messageID: row.message_id,
|
||||
partID: row.id,
|
||||
observedType,
|
||||
})
|
||||
return undefined
|
||||
}
|
||||
const decoded =
|
||||
value && typeof value === "object"
|
||||
? Option.getOrUndefined(
|
||||
decodePart({ ...value, id: row.id, messageID: row.message_id, sessionID: row.session_id }),
|
||||
)
|
||||
: undefined
|
||||
if (decoded) return { row, value: decoded }
|
||||
warnings.push({
|
||||
reason: "invalid-part",
|
||||
sessionID: input.session.id,
|
||||
messageID: row.message_id,
|
||||
partID: row.id,
|
||||
observedType,
|
||||
})
|
||||
return undefined
|
||||
})
|
||||
.filter((item): item is NonNullable<typeof item> => item !== undefined)
|
||||
.sort((a, b) => a.row.id.localeCompare(b.row.id))
|
||||
const byMessage = Map.groupBy(parts, (item) => item.row.message_id)
|
||||
const paired = new Set<string>()
|
||||
const used = new Set(messages.map((item) => item.row.id))
|
||||
const projected = messages
|
||||
.flatMap((item) => {
|
||||
if (paired.has(item.row.id)) return []
|
||||
const owned = byMessage.get(item.row.id)?.map((part) => part.value) ?? []
|
||||
if (item.value.role === "user") {
|
||||
const compaction = owned.find((part) => part.type === "compaction")
|
||||
if (compaction?.type === "compaction") {
|
||||
const pairedSummary = messages.find(
|
||||
(candidate) =>
|
||||
candidate.value.role === "assistant" &&
|
||||
candidate.value.parentID === item.row.id &&
|
||||
candidate.value.summary,
|
||||
)
|
||||
if (!pairedSummary || pairedSummary.value.role !== "assistant") return []
|
||||
paired.add(pairedSummary.row.id)
|
||||
if (pairedSummary.value.error || pairedSummary.value.time.completed === undefined) return []
|
||||
const summary = pairedSummary
|
||||
const summaryText = (byMessage.get(summary.row.id) ?? [])
|
||||
.map((part) => part.value)
|
||||
.filter((part) => part.type === "text" && part.text.length > 0)
|
||||
.map((part) => (part.type === "text" ? part.text : ""))
|
||||
.join("\n\n")
|
||||
const tailIndex = compaction.tail_start_id
|
||||
? messages.findIndex((candidate) => candidate.row.id === compaction.tail_start_id)
|
||||
: -1
|
||||
const compactionIndex = messages.findIndex((candidate) => candidate.row.id === item.row.id)
|
||||
const tail = tailIndex < 0 ? [] : messages.slice(tailIndex, compactionIndex)
|
||||
return [
|
||||
row(
|
||||
{ ...item.row, time_updated: Math.max(item.row.time_updated, summary.row.time_updated) },
|
||||
{
|
||||
id: item.row.id,
|
||||
type: "compaction",
|
||||
status: "completed",
|
||||
reason: compaction.auto ? "auto" : "manual",
|
||||
summary: summaryText,
|
||||
recent: serializeRecent(tail, byMessage),
|
||||
time: { created: item.row.time_created },
|
||||
},
|
||||
),
|
||||
]
|
||||
}
|
||||
const subtasks = owned.filter((part) => part.type === "subtask")
|
||||
const visible = owned.filter((part) => part.type === "text" && !part.ignored)
|
||||
const files = owned.filter((part) => part.type === "file")
|
||||
const agents = owned.filter((part) => part.type === "agent")
|
||||
if (subtasks.length > 0 && visible.length === 0 && files.length === 0 && agents.length === 0) return []
|
||||
const ordinary = visible.filter((part) => part.type === "text" && !part.synthetic)
|
||||
const synthetic = visible.filter((part) => part.type === "text" && part.synthetic)
|
||||
const attachments = files.flatMap((part) => (part.type === "file" ? migrateFile(part) : []))
|
||||
const unavailable = files.flatMap((part) =>
|
||||
part.type === "file" && !part.url.startsWith("data:") ? [unavailableFile(part)] : [],
|
||||
)
|
||||
const text = owned
|
||||
.flatMap((part) => {
|
||||
if (part.type === "text" && !part.ignored && !part.synthetic) return [part.text]
|
||||
if (part.type === "file" && !part.url.startsWith("data:")) return [unavailableFile(part)]
|
||||
return []
|
||||
})
|
||||
.join("\n\n")
|
||||
const agentAttachments = agents.map((part) =>
|
||||
part.type === "agent"
|
||||
? {
|
||||
name: part.name,
|
||||
...(part.source
|
||||
? { mention: { text: part.source.value, start: part.source.start, end: part.source.end } }
|
||||
: {}),
|
||||
}
|
||||
: { name: "" },
|
||||
)
|
||||
if (
|
||||
ordinary.length === 0 &&
|
||||
unavailable.length === 0 &&
|
||||
synthetic.length > 0 &&
|
||||
attachments.length === 0 &&
|
||||
agentAttachments.length === 0
|
||||
)
|
||||
return [
|
||||
row(item.row, {
|
||||
id: item.row.id,
|
||||
type: "synthetic",
|
||||
text: synthetic.map((part) => (part.type === "text" ? part.text : "")).join("\n\n"),
|
||||
time: { created: item.row.time_created },
|
||||
}),
|
||||
]
|
||||
const user = row(item.row, {
|
||||
id: item.row.id,
|
||||
type: "user",
|
||||
text,
|
||||
...(attachments.length ? { files: attachments } : {}),
|
||||
...(agentAttachments.length ? { agents: agentAttachments } : {}),
|
||||
time: { created: item.row.time_created },
|
||||
})
|
||||
if (synthetic.length === 0) return [user]
|
||||
return [
|
||||
user,
|
||||
row(item.row, {
|
||||
id: syntheticID(item.row.id, used),
|
||||
type: "synthetic",
|
||||
text: synthetic.map((part) => (part.type === "text" ? part.text : "")).join("\n\n"),
|
||||
time: { created: item.row.time_created },
|
||||
}),
|
||||
]
|
||||
}
|
||||
if (item.value.role !== "assistant") return []
|
||||
const assistant = item.value
|
||||
const parent = messages.find((candidate) => candidate.row.id === assistant.parentID)
|
||||
const parentParts = parent ? (byMessage.get(parent.row.id)?.map((part) => part.value) ?? []) : []
|
||||
if (
|
||||
parentParts.some((part) => part.type === "subtask") &&
|
||||
owned.some((part) => part.type === "tool" && part.tool === "task")
|
||||
)
|
||||
return []
|
||||
const content = owned.flatMap((part): Array<Record<string, unknown>> => {
|
||||
if (part.type === "text")
|
||||
return [{ type: "text", text: part.text, ...(part.metadata ? { state: part.metadata } : {}) }]
|
||||
if (part.type === "reasoning")
|
||||
return [
|
||||
{
|
||||
type: "reasoning",
|
||||
text: part.text,
|
||||
...(part.metadata ? { state: part.metadata } : {}),
|
||||
time: { created: part.time.start, ...(part.time.end === undefined ? {} : { completed: part.time.end }) },
|
||||
},
|
||||
]
|
||||
if (part.type !== "tool") return []
|
||||
return [migrateTool(part, item.row.time_created)]
|
||||
})
|
||||
const start =
|
||||
owned.flatMap((part) => (part.type === "step-start" && part.snapshot ? [part.snapshot] : []))[0] ??
|
||||
owned.flatMap((part) => (part.type === "snapshot" ? [part.snapshot] : []))[0] ??
|
||||
owned.flatMap((part) => (part.type === "patch" ? [part.hash] : []))[0]
|
||||
const end = owned.flatMap((part) => (part.type === "step-finish" && part.snapshot ? [part.snapshot] : [])).at(-1)
|
||||
const snapshotFiles = Array.from(new Set(owned.flatMap((part) => (part.type === "patch" ? part.files : []))))
|
||||
const finish = normalizeFinish(assistant.finish)
|
||||
return [
|
||||
row(item.row, {
|
||||
id: item.row.id,
|
||||
type: "assistant",
|
||||
agent: assistant.agent,
|
||||
model: {
|
||||
providerID: assistant.providerID,
|
||||
id: assistant.modelID,
|
||||
variant: assistant.variant ?? "default",
|
||||
},
|
||||
content,
|
||||
...(start || end || snapshotFiles.length
|
||||
? {
|
||||
snapshot: {
|
||||
...(start ? { start } : {}),
|
||||
...(end ? { end } : {}),
|
||||
...(snapshotFiles.length ? { files: snapshotFiles } : {}),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(finish ? { finish } : {}),
|
||||
cost: assistant.cost,
|
||||
tokens: {
|
||||
input: assistant.tokens.input,
|
||||
output: assistant.tokens.output,
|
||||
reasoning: assistant.tokens.reasoning,
|
||||
cache: assistant.tokens.cache,
|
||||
},
|
||||
...(assistant.error ? { error: migrateError(assistant.error) } : {}),
|
||||
time: {
|
||||
created: item.row.time_created,
|
||||
...(assistant.time.completed === undefined ? {} : { completed: item.row.time_updated }),
|
||||
},
|
||||
}),
|
||||
]
|
||||
})
|
||||
.map((item, seq) => ({ ...item, seq }))
|
||||
const assistants = messages
|
||||
.filter((item) => item.value.role === "assistant")
|
||||
.map((item) => item.value)
|
||||
.filter((item): item is SessionV1.Assistant => item.role === "assistant")
|
||||
const latestUser = messages.findLast((item) => {
|
||||
if (item.value.role !== "user") return false
|
||||
const owned = byMessage.get(item.row.id) ?? []
|
||||
if (owned.some((part) => part.value.type === "compaction")) return false
|
||||
return !owned.some((part) => part.value.type === "subtask") || !owned.every((part) => part.value.type === "subtask")
|
||||
})
|
||||
return {
|
||||
messages: projected,
|
||||
session: {
|
||||
agent: input.session.agent ?? (latestUser?.value.role === "user" ? latestUser.value.agent : null),
|
||||
model:
|
||||
input.session.model ??
|
||||
(latestUser?.value.role === "user"
|
||||
? {
|
||||
id: latestUser.value.model.modelID,
|
||||
providerID: latestUser.value.model.providerID,
|
||||
variant: latestUser.value.model.variant ?? "default",
|
||||
}
|
||||
: null),
|
||||
cost: assistants.reduce((total, item) => total + item.cost, 0),
|
||||
tokens_input: assistants.reduce((total, item) => total + item.tokens.input, 0),
|
||||
tokens_output: assistants.reduce((total, item) => total + item.tokens.output, 0),
|
||||
tokens_reasoning: assistants.reduce((total, item) => total + item.tokens.reasoning, 0),
|
||||
tokens_cache_read: assistants.reduce((total, item) => total + item.tokens.cache.read, 0),
|
||||
tokens_cache_write: assistants.reduce((total, item) => total + item.tokens.cache.write, 0),
|
||||
revert: null,
|
||||
time_compacting: null,
|
||||
},
|
||||
watermark: projected.length - 1,
|
||||
warnings,
|
||||
}
|
||||
}
|
||||
|
||||
export function status(): Effect.Effect<Status, never, Database.Service> {
|
||||
return Effect.gen(function* () {
|
||||
const { db } = yield* Database.Service
|
||||
if (!(yield* hasLegacySessions(db))) return { status: "completed" as const }
|
||||
const state = yield* readState(db)
|
||||
if (runtimeState.status === "running") return runtimeState
|
||||
if (runtimeState.status === "error") return runtimeState
|
||||
if (state?.phase === "completed") return { status: "completed" as const }
|
||||
return { status: "required" as const }
|
||||
}).pipe(Effect.orDie)
|
||||
}
|
||||
|
||||
export const layer = Layer.effectDiscard(
|
||||
Effect.gen(function* () {
|
||||
runtimeState = { status: "running", progress: { label: "Clearing old events" } }
|
||||
yield* run().pipe(
|
||||
Effect.matchCauseEffect({
|
||||
onFailure: (cause) =>
|
||||
Effect.sync(() => {
|
||||
runtimeState = { status: "error", error: errorText(Cause.squash(cause)) }
|
||||
}).pipe(Effect.andThen(Effect.logError("V1 migration failed", { cause }))),
|
||||
onSuccess: () =>
|
||||
Effect.sync(() => {
|
||||
runtimeState = { status: "idle" }
|
||||
}),
|
||||
}),
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
function errorText(input: unknown): string {
|
||||
if (!(input instanceof Error)) return String(input)
|
||||
const cause = input.cause
|
||||
return cause === undefined ? input.message : `${input.message}\nCaused by: ${errorText(cause)}`
|
||||
}
|
||||
|
||||
function updateProgress(progress: Progress) {
|
||||
if (runtimeState.status === "running") runtimeState = { status: "running", progress }
|
||||
}
|
||||
|
||||
export function run(options: Options = {}): Effect.Effect<RunResult, never, Database.Service | Global.Service> {
|
||||
return lock.withPermit(
|
||||
Effect.gen(function* () {
|
||||
const { db } = yield* Database.Service
|
||||
const global = yield* Global.Service
|
||||
const state = yield* readState(db)
|
||||
if (state?.phase === "completed") return { status: "completed" as const }
|
||||
if (!(yield* hasLegacySessions(db))) return { status: "completed" as const }
|
||||
const migrate = Effect.gen(function* () {
|
||||
const now = Date.now()
|
||||
yield* db.run(sql`
|
||||
INSERT OR IGNORE INTO project (id, worktree, time_created, time_updated, sandboxes)
|
||||
VALUES (${Project.ID.global}, ${path.parse(global.data).root}, ${now}, ${now}, '[]')
|
||||
`)
|
||||
if (state === undefined)
|
||||
yield* db
|
||||
.transaction((tx) =>
|
||||
Effect.gen(function* () {
|
||||
while (true) {
|
||||
yield* tx.run(sql`
|
||||
DELETE FROM event
|
||||
WHERE rowid IN (SELECT rowid FROM event LIMIT ${EVENT_DELETE_BATCH_SIZE})
|
||||
`)
|
||||
const deleted = (yield* tx.get<{ value: number }>(sql`SELECT changes() AS value`))?.value ?? 0
|
||||
if (deleted < EVENT_DELETE_BATCH_SIZE) break
|
||||
yield* Effect.yieldNow
|
||||
}
|
||||
yield* tx
|
||||
.insert(KVTable)
|
||||
.values({ key: MIGRATION_STATE_KEY, value: { phase: "sessions" } })
|
||||
.run()
|
||||
}),
|
||||
)
|
||||
.pipe(Effect.orDie)
|
||||
const sourceTotal = yield* countNextSessions(nextPath(options, global.data))
|
||||
const legacyTotal = (yield* db.get<{ value: number }>(sql`SELECT COUNT(*) AS value FROM session`))?.value ?? 0
|
||||
const cursor = state?.phase === "sessions" ? state.cursor : undefined
|
||||
const migrated =
|
||||
cursor !== undefined
|
||||
? ((yield* db.get<{ value: number }>(sql`SELECT COUNT(*) AS value FROM session WHERE id >= ${cursor}`))
|
||||
?.value ?? 0)
|
||||
: 0
|
||||
const denominator = sourceTotal + legacyTotal
|
||||
updateProgress({ label: "Migrating sessions", numerator: migrated, denominator })
|
||||
yield* importNextDatabase(db, nextPath(options, global.data), (completed) => {
|
||||
updateProgress({ label: "Migrating sessions", numerator: migrated + completed, denominator })
|
||||
})
|
||||
updateProgress({ label: "Migrating sessions", numerator: migrated + sourceTotal, denominator })
|
||||
const projects = new Set(
|
||||
(yield* db.all<{ id: string }>(sql`SELECT id FROM project`)).map((project) => project.id),
|
||||
)
|
||||
while (true) {
|
||||
const state = yield* readState(db)
|
||||
const cursorValue = state?.phase === "sessions" ? state.cursor : undefined
|
||||
const nextID = yield* db.get<{ id: string; project_id: string }>(
|
||||
cursorValue === undefined
|
||||
? sql`SELECT id, project_id FROM session ORDER BY id DESC LIMIT 1`
|
||||
: sql`SELECT id, project_id FROM session WHERE id < ${cursorValue} ORDER BY id DESC LIMIT 1`,
|
||||
)
|
||||
if (!nextID) break
|
||||
yield* db
|
||||
.transaction((tx) =>
|
||||
Effect.gen(function* () {
|
||||
yield* tx
|
||||
.insert(KVTable)
|
||||
.values({ key: MIGRATION_STATE_KEY, value: { phase: "sessions", cursor: nextID.id } })
|
||||
.onConflictDoUpdate({
|
||||
target: KVTable.key,
|
||||
set: { value: { phase: "sessions", cursor: nextID.id }, time_updated: Date.now() },
|
||||
})
|
||||
.run()
|
||||
const projectID = projects.has(nextID.project_id) ? nextID.project_id : Project.ID.global
|
||||
if (projectID !== nextID.project_id)
|
||||
yield* Effect.logWarning("Reassigned V1 session with missing project", {
|
||||
sessionID: nextID.id,
|
||||
projectID: nextID.project_id,
|
||||
})
|
||||
yield* tx.run(sql`
|
||||
INSERT OR IGNORE INTO session_v2 (
|
||||
id, project_id, workspace_id, parent_id, slug, directory, path, title, version, share_url,
|
||||
summary_additions, summary_deletions, summary_files, summary_diffs, metadata, cost,
|
||||
tokens_input, tokens_output, tokens_reasoning, tokens_cache_read, tokens_cache_write,
|
||||
revert, permission, agent, model, time_created, time_updated, time_compacting, time_archived
|
||||
)
|
||||
SELECT
|
||||
id, ${projectID}, workspace_id, parent_id, slug, directory, path, title, version, share_url,
|
||||
summary_additions, summary_deletions, summary_files, summary_diffs, metadata, cost,
|
||||
tokens_input, tokens_output, tokens_reasoning, tokens_cache_read, tokens_cache_write,
|
||||
revert, permission, agent, model, time_created, time_updated, time_compacting, time_archived
|
||||
FROM session
|
||||
WHERE id = ${nextID.id}
|
||||
`)
|
||||
const next = yield* tx
|
||||
.select()
|
||||
.from(SessionTable)
|
||||
.where(eq(SessionTable.id, SessionSchema.ID.make(nextID.id)))
|
||||
.get()
|
||||
if (!next) return yield* Effect.die(new Error(`Failed to copy V1 session ${nextID.id}`))
|
||||
const sourceMessages = yield* tx.all<SourceMessage>(
|
||||
sql`SELECT id, session_id, time_created, time_updated, data FROM message WHERE session_id = ${next.id}`,
|
||||
)
|
||||
const sourceParts = yield* tx.all<SourcePart>(
|
||||
sql`SELECT id, message_id, session_id, time_created, time_updated, data FROM part WHERE session_id = ${next.id}`,
|
||||
)
|
||||
const transformed = transformSession({ session: next, messages: sourceMessages, parts: sourceParts })
|
||||
yield* Effect.forEach(transformed.warnings, (warning) =>
|
||||
Effect.logWarning("Skipped V1 migration row", warning),
|
||||
)
|
||||
yield* tx.delete(SessionMessageTable).where(eq(SessionMessageTable.session_id, next.id)).run()
|
||||
yield* Effect.forEach(transformed.messages, (message) =>
|
||||
tx
|
||||
.insert(SessionMessageTable)
|
||||
.values({
|
||||
id: SessionMessage.ID.make(message.id),
|
||||
session_id: SessionSchema.ID.make(message.session_id),
|
||||
type: message.type,
|
||||
seq: message.seq,
|
||||
time_created: message.time_created,
|
||||
time_updated: message.time_updated,
|
||||
data: sql`${JSON.stringify(message.data)}`,
|
||||
})
|
||||
.run(),
|
||||
)
|
||||
yield* tx
|
||||
.update(SessionTable)
|
||||
.set({ ...transformed.session, time_updated: next.time_updated })
|
||||
.where(eq(SessionTable.id, next.id))
|
||||
.run()
|
||||
yield* tx
|
||||
.insert(EventSequenceTable)
|
||||
.values({ aggregate_id: next.id, seq: transformed.watermark })
|
||||
.onConflictDoUpdate({
|
||||
target: EventSequenceTable.aggregate_id,
|
||||
set: { seq: transformed.watermark, owner_id: null },
|
||||
})
|
||||
.run()
|
||||
}),
|
||||
)
|
||||
.pipe(Effect.orDie)
|
||||
if (runtimeState.status === "running")
|
||||
runtimeState = {
|
||||
status: "running",
|
||||
progress: {
|
||||
label: "Migrating sessions",
|
||||
numerator: (runtimeState.progress.numerator ?? 0) + 1,
|
||||
denominator,
|
||||
},
|
||||
}
|
||||
yield* Effect.yieldNow
|
||||
}
|
||||
yield* db
|
||||
.transaction((tx) =>
|
||||
Effect.gen(function* () {
|
||||
yield* tx
|
||||
.insert(KVTable)
|
||||
.values({ key: MIGRATION_STATE_KEY, value: { phase: "completed" } })
|
||||
.onConflictDoUpdate({
|
||||
target: KVTable.key,
|
||||
set: { value: { phase: "completed" }, time_updated: Date.now() },
|
||||
})
|
||||
.run()
|
||||
}),
|
||||
)
|
||||
.pipe(Effect.orDie)
|
||||
return { status: "completed" as const }
|
||||
})
|
||||
return yield* migrate
|
||||
}).pipe(Effect.orDie),
|
||||
)
|
||||
}
|
||||
|
||||
function nextPath(options: Options, data: string) {
|
||||
if (options.nextDatabasePath) return options.nextDatabasePath
|
||||
if (process.env.OPENCODE_DB === ":memory:") return undefined
|
||||
return path.join(data, "opencode-next.db")
|
||||
}
|
||||
|
||||
function openNextDatabase(sourcePath: string) {
|
||||
return Effect.acquireRelease(
|
||||
Effect.gen(function* () {
|
||||
const sqlite = yield* Effect.promise(() => import("bun:sqlite"))
|
||||
return new sqlite.Database(sourcePath, { readonly: true, strict: true })
|
||||
}),
|
||||
(source) => Effect.sync(() => source.close()),
|
||||
)
|
||||
}
|
||||
|
||||
function countNextSessions(sourcePath: string | undefined) {
|
||||
if (!sourcePath || !existsSync(sourcePath)) return Effect.succeed(0)
|
||||
return Effect.scoped(
|
||||
Effect.gen(function* () {
|
||||
const source = yield* openNextDatabase(sourcePath)
|
||||
if (!isNextDatabase(source)) return 0
|
||||
return source.query<{ value: number }, []>("SELECT COUNT(*) AS value FROM session").get()?.value ?? 0
|
||||
}),
|
||||
).pipe(Effect.orElseSucceed(() => 0))
|
||||
}
|
||||
|
||||
function importNextDatabase(
|
||||
db: Database.Interface["db"],
|
||||
sourcePath: string | undefined,
|
||||
onProgress: (completed: number) => void,
|
||||
): Effect.Effect<void, unknown> {
|
||||
if (!sourcePath || !existsSync(sourcePath)) return Effect.void
|
||||
return Effect.scoped(
|
||||
Effect.gen(function* () {
|
||||
const source = yield* openNextDatabase(sourcePath)
|
||||
if (!isNextDatabase(source)) {
|
||||
yield* Effect.logWarning("Skipped incompatible opencode-next.db", { path: sourcePath })
|
||||
return
|
||||
}
|
||||
source.run("BEGIN")
|
||||
yield* Effect.addFinalizer(() =>
|
||||
Effect.sync(() => {
|
||||
if (source.inTransaction) source.run("ROLLBACK")
|
||||
}),
|
||||
)
|
||||
const projects = new Map(
|
||||
source
|
||||
.query<NextProject, []>("SELECT * FROM project")
|
||||
.all()
|
||||
.map((project) => [project.id, project]),
|
||||
)
|
||||
const sessions = source.query<NextSession, []>("SELECT * FROM session ORDER BY id DESC").all()
|
||||
for (const [index, session] of sessions.entries()) {
|
||||
const project = projects.get(session.project_id)
|
||||
const projectID = project ? session.project_id : Project.ID.global
|
||||
if (!project) {
|
||||
yield* Effect.logWarning("Reassigned previous V2 session with missing project", {
|
||||
sessionID: session.id,
|
||||
projectID: session.project_id,
|
||||
})
|
||||
}
|
||||
const messages = source
|
||||
.query<
|
||||
NextMessage,
|
||||
[string]
|
||||
>("SELECT id, session_id, type, seq, time_created, time_updated, data FROM session_message WHERE session_id = ? ORDER BY seq")
|
||||
.all(session.id)
|
||||
yield* db
|
||||
.transaction((tx) =>
|
||||
Effect.gen(function* () {
|
||||
if (project)
|
||||
yield* tx.run(sql`
|
||||
INSERT OR IGNORE INTO project (
|
||||
id, worktree, vcs, name, icon_url, icon_url_override, icon_color,
|
||||
time_created, time_updated, time_initialized, sandboxes, commands
|
||||
) VALUES (
|
||||
${project.id}, ${project.worktree}, ${project.vcs}, ${project.name}, ${project.icon_url},
|
||||
${project.icon_url_override}, ${project.icon_color}, ${project.time_created}, ${project.time_updated},
|
||||
${project.time_initialized}, ${project.sandboxes}, ${project.commands}
|
||||
)
|
||||
`)
|
||||
const existing = yield* tx
|
||||
.select({ id: SessionTable.id })
|
||||
.from(SessionTable)
|
||||
.where(eq(SessionTable.id, SessionSchema.ID.make(session.id)))
|
||||
.get()
|
||||
if (existing) return
|
||||
yield* tx.run(sql`
|
||||
INSERT INTO session_v2 (
|
||||
id, project_id, workspace_id, parent_id, fork_session_id, fork_boundary, slug, directory,
|
||||
path, title, version, share_url, summary_additions, summary_deletions, summary_files,
|
||||
summary_diffs, metadata, cost, tokens_input, tokens_output, tokens_reasoning, tokens_cache_read,
|
||||
tokens_cache_write, revert, permission, agent, model, time_created, time_updated, time_compacting,
|
||||
time_archived, time_suspended
|
||||
) VALUES (
|
||||
${session.id}, ${projectID}, ${session.workspace_id}, ${session.parent_id},
|
||||
${session.fork_session_id}, ${session.fork_boundary}, ${session.slug}, ${session.directory},
|
||||
${session.path}, ${session.title}, ${session.version}, ${session.share_url},
|
||||
${session.summary_additions}, ${session.summary_deletions}, ${session.summary_files},
|
||||
${session.summary_diffs}, ${session.metadata}, ${session.cost}, ${session.tokens_input},
|
||||
${session.tokens_output}, ${session.tokens_reasoning}, ${session.tokens_cache_read},
|
||||
${session.tokens_cache_write}, ${session.revert}, ${session.permission}, ${session.agent},
|
||||
${session.model}, ${session.time_created}, ${session.time_updated}, ${session.time_compacting},
|
||||
${session.time_archived}, ${session.time_suspended}
|
||||
)
|
||||
`)
|
||||
yield* Effect.forEach(messages, (message) =>
|
||||
tx
|
||||
.insert(SessionMessageTable)
|
||||
.values({
|
||||
id: SessionMessage.ID.make(message.id),
|
||||
session_id: SessionSchema.ID.make(message.session_id),
|
||||
type: message.type as SessionMessage.Type,
|
||||
seq: message.seq,
|
||||
time_created: message.time_created,
|
||||
time_updated: message.time_updated,
|
||||
data: sql`${message.data}`,
|
||||
})
|
||||
.run(),
|
||||
)
|
||||
yield* tx
|
||||
.insert(EventSequenceTable)
|
||||
.values({ aggregate_id: session.id, seq: messages.at(-1)?.seq ?? -1 })
|
||||
.onConflictDoUpdate({
|
||||
target: EventSequenceTable.aggregate_id,
|
||||
set: { seq: messages.at(-1)?.seq ?? -1, owner_id: null },
|
||||
})
|
||||
.run()
|
||||
}),
|
||||
)
|
||||
.pipe(Effect.orDie)
|
||||
onProgress(index + 1)
|
||||
yield* Effect.yieldNow
|
||||
}
|
||||
source.run("COMMIT")
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function isNextDatabase(source: SQLiteDatabase) {
|
||||
const tables = new Set(
|
||||
source
|
||||
.query<{ name: string }, []>("SELECT name FROM sqlite_master WHERE type = 'table'")
|
||||
.all()
|
||||
.map((table) => table.name),
|
||||
)
|
||||
return tables.has("project") && tables.has("session") && tables.has("session_message")
|
||||
}
|
||||
|
||||
function row(
|
||||
source: SourceMessage,
|
||||
message: {
|
||||
readonly id: string
|
||||
readonly type: SessionMessage.Type
|
||||
readonly time: { readonly created: number }
|
||||
readonly [key: string]: unknown
|
||||
},
|
||||
): TransformResult["messages"][number] {
|
||||
const { id, type, ...data } = message
|
||||
return {
|
||||
id,
|
||||
session_id: source.session_id,
|
||||
type,
|
||||
seq: 0,
|
||||
time_created: source.time_created,
|
||||
time_updated: source.time_updated,
|
||||
data,
|
||||
}
|
||||
}
|
||||
|
||||
function migrateTool(part: typeof SessionV1.ToolPart.Type, fallback: number) {
|
||||
const base = {
|
||||
type: "tool" as const,
|
||||
id: part.callID,
|
||||
name: part.tool,
|
||||
...(part.metadata ? { providerState: part.metadata } : {}),
|
||||
}
|
||||
if (part.state.status === "completed")
|
||||
return {
|
||||
...base,
|
||||
state: {
|
||||
status: "completed",
|
||||
input: part.state.input,
|
||||
content:
|
||||
part.state.time.compacted === undefined
|
||||
? [
|
||||
{ type: "text", text: part.state.output },
|
||||
...(part.state.attachments ?? []).map((file) => ({
|
||||
type: "file" as const,
|
||||
uri: file.url,
|
||||
mime: file.mime,
|
||||
...(file.filename ? { name: file.filename } : {}),
|
||||
})),
|
||||
]
|
||||
: [{ type: "text", text: "[Old tool result content cleared]" }],
|
||||
metadata: part.state.metadata,
|
||||
},
|
||||
time: { created: part.state.time.start, completed: part.state.time.end },
|
||||
}
|
||||
if (part.state.status === "error")
|
||||
return {
|
||||
...base,
|
||||
state: {
|
||||
status: "error",
|
||||
input: part.state.input,
|
||||
error: { type: "tool.execution", message: part.state.error },
|
||||
...(typeof part.state.metadata?.output === "string"
|
||||
? { content: [{ type: "text", text: part.state.metadata.output }] }
|
||||
: {}),
|
||||
...(part.state.metadata ? { metadata: part.state.metadata } : {}),
|
||||
},
|
||||
time: { created: part.state.time.start, completed: part.state.time.end },
|
||||
}
|
||||
return {
|
||||
...base,
|
||||
state: {
|
||||
status: "error",
|
||||
input: part.state.input,
|
||||
error: { type: "tool.interrupted", message: "Tool execution was interrupted before V2 migration" },
|
||||
...(part.state.status === "running" && part.state.metadata ? { metadata: part.state.metadata } : {}),
|
||||
},
|
||||
time: { created: part.state.status === "running" ? part.state.time.start : fallback },
|
||||
}
|
||||
}
|
||||
|
||||
function migrateError(error: NonNullable<(typeof SessionV1.Assistant.Type)["error"]>) {
|
||||
const message =
|
||||
"message" in error.data
|
||||
? error.data.message
|
||||
: error.name === "MessageOutputLengthError"
|
||||
? "The model exceeded its output limit"
|
||||
: error.name
|
||||
const type =
|
||||
error.name === "ProviderAuthError"
|
||||
? "provider.auth"
|
||||
: error.name === "ContentFilterError"
|
||||
? "provider.content-filter"
|
||||
: error.name === "ContextOverflowError"
|
||||
? "provider.invalid-request"
|
||||
: error.name === "StructuredOutputError" || error.name === "MessageOutputLengthError"
|
||||
? "provider.invalid-output"
|
||||
: error.name === "MessageAbortedError"
|
||||
? "aborted"
|
||||
: error.name === "APIError"
|
||||
? "provider.error"
|
||||
: "unknown"
|
||||
return { type, message }
|
||||
}
|
||||
|
||||
function normalizeFinish(finish: string | undefined) {
|
||||
if (!finish) return undefined
|
||||
return (
|
||||
(["stop", "length", "tool-calls", "content-filter", "error", "unknown"] as const).find(
|
||||
(value) => value === finish,
|
||||
) ?? "unknown"
|
||||
)
|
||||
}
|
||||
|
||||
function migrateFile(part: SessionV1.FilePart) {
|
||||
if (!part.url.startsWith("data:")) return []
|
||||
const comma = part.url.indexOf(",")
|
||||
if (comma < 0) return []
|
||||
const header = part.url.slice(0, comma)
|
||||
const payload = part.url.slice(comma + 1)
|
||||
const data = header.endsWith(";base64")
|
||||
? Buffer.from(payload, "base64").toString("base64")
|
||||
: Buffer.from(decodeURIComponent(payload)).toString("base64")
|
||||
return [
|
||||
{
|
||||
data,
|
||||
mime: part.mime,
|
||||
source:
|
||||
part.source?.type === "resource" ? { type: "uri" as const, uri: part.source.uri } : { type: "inline" as const },
|
||||
...(part.filename ? { name: part.filename } : {}),
|
||||
...(part.source
|
||||
? { mention: { text: part.source.text.value, start: part.source.text.start, end: part.source.text.end } }
|
||||
: {}),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function unavailableFile(part: SessionV1.FilePart) {
|
||||
const label = part.filename ?? (part.source?.type === "resource" ? part.source.uri : part.url)
|
||||
return `[Attachment unavailable after migration: ${label} (${part.mime})]`
|
||||
}
|
||||
|
||||
function syntheticID(source: string, used: Set<string>) {
|
||||
const prefix = source.slice(0, 16)
|
||||
const alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||
for (let salt = 0; ; salt++) {
|
||||
const hex = new Bun.CryptoHasher("sha256").update(`v1-synthetic:${source}${salt ? `:${salt}` : ""}`).digest("hex")
|
||||
let value = BigInt(`0x${hex}`)
|
||||
let suffix = ""
|
||||
while (suffix.length < 14) {
|
||||
suffix = alphabet[Number(value % 62n)] + suffix
|
||||
value /= 62n
|
||||
}
|
||||
const id = prefix + suffix
|
||||
if (used.has(id)) continue
|
||||
used.add(id)
|
||||
return id
|
||||
}
|
||||
}
|
||||
|
||||
function serializeRecent(
|
||||
messages: ReadonlyArray<{ row: SourceMessage; value: typeof SessionV1.Info.Type }>,
|
||||
parts: Map<string, Array<{ row: SourcePart; value: typeof SessionV1.Part.Type }>>,
|
||||
) {
|
||||
return messages
|
||||
.flatMap((message) => {
|
||||
const owned = parts.get(message.row.id)?.map((part) => part.value) ?? []
|
||||
if (message.value.role === "user")
|
||||
return [
|
||||
`[User]: ${owned
|
||||
.filter((part) => part.type === "text" && !part.ignored)
|
||||
.map((part) => (part.type === "text" ? part.text : ""))
|
||||
.join("\n\n")}`,
|
||||
]
|
||||
return owned.flatMap((part) =>
|
||||
part.type === "text"
|
||||
? [`[Assistant]: ${part.text}`]
|
||||
: part.type === "reasoning" && part.text
|
||||
? [`[Assistant reasoning]: ${part.text}`]
|
||||
: [],
|
||||
)
|
||||
})
|
||||
.join("\n\n")
|
||||
}
|
||||
|
||||
function readState(db: Database.Interface["db"]): Effect.Effect<MigrationState | undefined> {
|
||||
return db
|
||||
.select({ value: KVTable.value })
|
||||
.from(KVTable)
|
||||
.where(eq(KVTable.key, MIGRATION_STATE_KEY))
|
||||
.get()
|
||||
.pipe(
|
||||
Effect.map((row) => parseState(row?.value)),
|
||||
Effect.orDie,
|
||||
)
|
||||
}
|
||||
|
||||
function parseState(input: unknown): MigrationState | undefined {
|
||||
if (!input || typeof input !== "object" || !("phase" in input)) return
|
||||
if (input.phase === "completed") return { phase: "completed" }
|
||||
if (input.phase !== "sessions") return
|
||||
if (!("cursor" in input) || input.cursor === undefined) return { phase: "sessions" }
|
||||
if (typeof input.cursor === "string") return { phase: "sessions", cursor: input.cursor }
|
||||
}
|
||||
|
||||
function hasLegacySessions(db: Database.Interface["db"]) {
|
||||
return db.get(sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'session'`).pipe(
|
||||
Effect.map((row) => row !== undefined),
|
||||
Effect.orDie,
|
||||
)
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Effect, Layer } from "effect"
|
||||
|
||||
export type Status = { readonly status: "completed" }
|
||||
|
||||
export const layer = Layer.empty
|
||||
export function status() {
|
||||
return Effect.succeed({ status: "completed" } as const)
|
||||
}
|
||||
export function run() {
|
||||
return Effect.succeed({ status: "completed" } as const)
|
||||
}
|
||||
@@ -1,2 +1,997 @@
|
||||
export * as V1Migration from "#v1-migration"
|
||||
export * from "#v1-migration"
|
||||
export * as V1Migration from "./v1-migration.js"
|
||||
|
||||
import { Cause, Effect, Layer, Option, Schema, Semaphore } from "effect"
|
||||
import { Database } from "./database.js"
|
||||
import { SessionMessageTable, SessionTable } from "../session/sql.js"
|
||||
import { SessionV1 } from "@opencode-ai/schema/session-v1"
|
||||
import { SessionMessage } from "../session/message.js"
|
||||
import { SessionSchema } from "../session/schema.js"
|
||||
import { KVTable } from "../kv/sql.js"
|
||||
import { EventSequenceTable } from "../event/sql.js"
|
||||
import { eq, sql } from "drizzle-orm"
|
||||
import { Global } from "@opencode-ai/util/global"
|
||||
import { existsSync } from "node:fs"
|
||||
import path from "node:path"
|
||||
import type { Database as SQLiteDatabase } from "bun:sqlite"
|
||||
import { Project } from "@opencode-ai/schema/project"
|
||||
|
||||
export type SourceMessage = {
|
||||
readonly id: string
|
||||
readonly session_id: string
|
||||
readonly time_created: number
|
||||
readonly time_updated: number
|
||||
readonly data: string
|
||||
}
|
||||
|
||||
export type SourcePart = {
|
||||
readonly id: string
|
||||
readonly message_id: string
|
||||
readonly session_id: string
|
||||
readonly time_created: number
|
||||
readonly time_updated: number
|
||||
readonly data: string
|
||||
}
|
||||
|
||||
export type TransformInput = {
|
||||
readonly session: typeof SessionTable.$inferSelect
|
||||
readonly messages: ReadonlyArray<SourceMessage>
|
||||
readonly parts: ReadonlyArray<SourcePart>
|
||||
}
|
||||
|
||||
export type Warning = {
|
||||
readonly reason: string
|
||||
readonly sessionID: string
|
||||
readonly messageID?: string
|
||||
readonly partID?: string
|
||||
readonly observedType?: string
|
||||
}
|
||||
|
||||
export type TransformResult = {
|
||||
readonly messages: ReadonlyArray<{
|
||||
readonly id: string
|
||||
readonly session_id: string
|
||||
readonly type: SessionMessage.Type
|
||||
readonly seq: number
|
||||
readonly time_created: number
|
||||
readonly time_updated: number
|
||||
readonly data: Record<string, unknown>
|
||||
}>
|
||||
readonly session: Pick<
|
||||
typeof SessionTable.$inferInsert,
|
||||
| "agent"
|
||||
| "model"
|
||||
| "cost"
|
||||
| "tokens_input"
|
||||
| "tokens_output"
|
||||
| "tokens_reasoning"
|
||||
| "tokens_cache_read"
|
||||
| "tokens_cache_write"
|
||||
| "revert"
|
||||
| "time_compacting"
|
||||
>
|
||||
readonly watermark: number
|
||||
readonly warnings: ReadonlyArray<Warning>
|
||||
}
|
||||
|
||||
type Progress = {
|
||||
readonly label: string
|
||||
readonly numerator?: number
|
||||
readonly denominator?: number
|
||||
}
|
||||
|
||||
export type Status =
|
||||
| { readonly status: "required" | "completed" }
|
||||
| { readonly status: "running"; readonly progress: Progress }
|
||||
| { readonly status: "error"; readonly error: string }
|
||||
|
||||
type RunResult = {
|
||||
readonly status: "completed"
|
||||
}
|
||||
|
||||
type Options = {
|
||||
readonly nextDatabasePath?: string
|
||||
}
|
||||
|
||||
type MigrationState = { readonly phase: "sessions"; readonly cursor?: string } | { readonly phase: "completed" }
|
||||
|
||||
type RuntimeState =
|
||||
| { readonly status: "idle" }
|
||||
| { readonly status: "running"; readonly progress: Progress }
|
||||
| { readonly status: "error"; readonly error: string }
|
||||
|
||||
type NextProject = {
|
||||
readonly id: string
|
||||
readonly worktree: string
|
||||
readonly vcs: string | null
|
||||
readonly name: string | null
|
||||
readonly icon_url: string | null
|
||||
readonly icon_url_override: string | null
|
||||
readonly icon_color: string | null
|
||||
readonly time_created: number
|
||||
readonly time_updated: number
|
||||
readonly time_initialized: number | null
|
||||
readonly sandboxes: string
|
||||
readonly commands: string | null
|
||||
}
|
||||
|
||||
type NextSession = {
|
||||
readonly id: string
|
||||
readonly project_id: string
|
||||
readonly workspace_id: string | null
|
||||
readonly parent_id: string | null
|
||||
readonly fork_session_id: string | null
|
||||
readonly fork_boundary: string | null
|
||||
readonly slug: string
|
||||
readonly directory: string
|
||||
readonly path: string | null
|
||||
readonly title: string | null
|
||||
readonly version: string
|
||||
readonly share_url: string | null
|
||||
readonly summary_additions: number | null
|
||||
readonly summary_deletions: number | null
|
||||
readonly summary_files: number | null
|
||||
readonly summary_diffs: string | null
|
||||
readonly metadata: string | null
|
||||
readonly cost: number
|
||||
readonly tokens_input: number
|
||||
readonly tokens_output: number
|
||||
readonly tokens_reasoning: number
|
||||
readonly tokens_cache_read: number
|
||||
readonly tokens_cache_write: number
|
||||
readonly revert: string | null
|
||||
readonly permission: string | null
|
||||
readonly agent: string | null
|
||||
readonly model: string | null
|
||||
readonly time_created: number
|
||||
readonly time_updated: number
|
||||
readonly time_compacting: number | null
|
||||
readonly time_archived: number | null
|
||||
readonly time_suspended: number | null
|
||||
}
|
||||
|
||||
type NextMessage = {
|
||||
readonly id: string
|
||||
readonly session_id: string
|
||||
readonly type: string
|
||||
readonly seq: number
|
||||
readonly time_created: number
|
||||
readonly time_updated: number
|
||||
readonly data: string
|
||||
}
|
||||
|
||||
const lock = Semaphore.makeUnsafe(1)
|
||||
const MIGRATION_STATE_KEY = "migration.v1-v2"
|
||||
const EVENT_DELETE_BATCH_SIZE = 1_000
|
||||
const decodeJson = Schema.decodeUnknownOption(Schema.UnknownFromJsonString)
|
||||
const decodeMessage = Schema.decodeUnknownOption(SessionV1.Info)
|
||||
const decodePart = Schema.decodeUnknownOption(SessionV1.Part)
|
||||
let runtimeState: RuntimeState = { status: "idle" }
|
||||
|
||||
export function transformSession(input: TransformInput): TransformResult {
|
||||
const warnings: Warning[] = []
|
||||
const messages = input.messages
|
||||
.map((row) => {
|
||||
const value = Option.getOrUndefined(decodeJson(row.data))
|
||||
const decoded =
|
||||
value && typeof value === "object"
|
||||
? Option.getOrUndefined(decodeMessage({ ...value, id: row.id, sessionID: row.session_id }))
|
||||
: undefined
|
||||
if (decoded) return { row, value: decoded }
|
||||
warnings.push({ reason: "invalid-message", sessionID: input.session.id, messageID: row.id })
|
||||
return undefined
|
||||
})
|
||||
.filter((item): item is NonNullable<typeof item> => item !== undefined)
|
||||
.sort((a, b) => a.row.time_created - b.row.time_created || a.row.id.localeCompare(b.row.id))
|
||||
const messageIDs = new Set(input.messages.map((row) => row.id))
|
||||
const parts = input.parts
|
||||
.map((row) => {
|
||||
const value = Option.getOrUndefined(decodeJson(row.data))
|
||||
const observedType = value && typeof value === "object" && "type" in value ? String(value.type) : undefined
|
||||
if (!messageIDs.has(row.message_id)) {
|
||||
warnings.push({
|
||||
reason: "orphan-part",
|
||||
sessionID: input.session.id,
|
||||
messageID: row.message_id,
|
||||
partID: row.id,
|
||||
observedType,
|
||||
})
|
||||
return undefined
|
||||
}
|
||||
const decoded =
|
||||
value && typeof value === "object"
|
||||
? Option.getOrUndefined(
|
||||
decodePart({ ...value, id: row.id, messageID: row.message_id, sessionID: row.session_id }),
|
||||
)
|
||||
: undefined
|
||||
if (decoded) return { row, value: decoded }
|
||||
warnings.push({
|
||||
reason: "invalid-part",
|
||||
sessionID: input.session.id,
|
||||
messageID: row.message_id,
|
||||
partID: row.id,
|
||||
observedType,
|
||||
})
|
||||
return undefined
|
||||
})
|
||||
.filter((item): item is NonNullable<typeof item> => item !== undefined)
|
||||
.sort((a, b) => a.row.id.localeCompare(b.row.id))
|
||||
const byMessage = Map.groupBy(parts, (item) => item.row.message_id)
|
||||
const paired = new Set<string>()
|
||||
const used = new Set(messages.map((item) => item.row.id))
|
||||
const projected = messages
|
||||
.flatMap((item) => {
|
||||
if (paired.has(item.row.id)) return []
|
||||
const owned = byMessage.get(item.row.id)?.map((part) => part.value) ?? []
|
||||
if (item.value.role === "user") {
|
||||
const compaction = owned.find((part) => part.type === "compaction")
|
||||
if (compaction?.type === "compaction") {
|
||||
const pairedSummary = messages.find(
|
||||
(candidate) =>
|
||||
candidate.value.role === "assistant" &&
|
||||
candidate.value.parentID === item.row.id &&
|
||||
candidate.value.summary,
|
||||
)
|
||||
if (!pairedSummary || pairedSummary.value.role !== "assistant") return []
|
||||
paired.add(pairedSummary.row.id)
|
||||
if (pairedSummary.value.error || pairedSummary.value.time.completed === undefined) return []
|
||||
const summary = pairedSummary
|
||||
const summaryText = (byMessage.get(summary.row.id) ?? [])
|
||||
.map((part) => part.value)
|
||||
.filter((part) => part.type === "text" && part.text.length > 0)
|
||||
.map((part) => (part.type === "text" ? part.text : ""))
|
||||
.join("\n\n")
|
||||
const tailIndex = compaction.tail_start_id
|
||||
? messages.findIndex((candidate) => candidate.row.id === compaction.tail_start_id)
|
||||
: -1
|
||||
const compactionIndex = messages.findIndex((candidate) => candidate.row.id === item.row.id)
|
||||
const tail = tailIndex < 0 ? [] : messages.slice(tailIndex, compactionIndex)
|
||||
return [
|
||||
row(
|
||||
{ ...item.row, time_updated: Math.max(item.row.time_updated, summary.row.time_updated) },
|
||||
{
|
||||
id: item.row.id,
|
||||
type: "compaction",
|
||||
status: "completed",
|
||||
reason: compaction.auto ? "auto" : "manual",
|
||||
summary: summaryText,
|
||||
recent: serializeRecent(tail, byMessage),
|
||||
time: { created: item.row.time_created },
|
||||
},
|
||||
),
|
||||
]
|
||||
}
|
||||
const subtasks = owned.filter((part) => part.type === "subtask")
|
||||
const visible = owned.filter((part) => part.type === "text" && !part.ignored)
|
||||
const files = owned.filter((part) => part.type === "file")
|
||||
const agents = owned.filter((part) => part.type === "agent")
|
||||
if (subtasks.length > 0 && visible.length === 0 && files.length === 0 && agents.length === 0) return []
|
||||
const ordinary = visible.filter((part) => part.type === "text" && !part.synthetic)
|
||||
const synthetic = visible.filter((part) => part.type === "text" && part.synthetic)
|
||||
const attachments = files.flatMap((part) => (part.type === "file" ? migrateFile(part) : []))
|
||||
const unavailable = files.flatMap((part) =>
|
||||
part.type === "file" && !part.url.startsWith("data:") ? [unavailableFile(part)] : [],
|
||||
)
|
||||
const text = owned
|
||||
.flatMap((part) => {
|
||||
if (part.type === "text" && !part.ignored && !part.synthetic) return [part.text]
|
||||
if (part.type === "file" && !part.url.startsWith("data:")) return [unavailableFile(part)]
|
||||
return []
|
||||
})
|
||||
.join("\n\n")
|
||||
const agentAttachments = agents.map((part) =>
|
||||
part.type === "agent"
|
||||
? {
|
||||
name: part.name,
|
||||
...(part.source
|
||||
? { mention: { text: part.source.value, start: part.source.start, end: part.source.end } }
|
||||
: {}),
|
||||
}
|
||||
: { name: "" },
|
||||
)
|
||||
if (
|
||||
ordinary.length === 0 &&
|
||||
unavailable.length === 0 &&
|
||||
synthetic.length > 0 &&
|
||||
attachments.length === 0 &&
|
||||
agentAttachments.length === 0
|
||||
)
|
||||
return [
|
||||
row(item.row, {
|
||||
id: item.row.id,
|
||||
type: "synthetic",
|
||||
text: synthetic.map((part) => (part.type === "text" ? part.text : "")).join("\n\n"),
|
||||
time: { created: item.row.time_created },
|
||||
}),
|
||||
]
|
||||
const user = row(item.row, {
|
||||
id: item.row.id,
|
||||
type: "user",
|
||||
text,
|
||||
...(attachments.length ? { files: attachments } : {}),
|
||||
...(agentAttachments.length ? { agents: agentAttachments } : {}),
|
||||
time: { created: item.row.time_created },
|
||||
})
|
||||
if (synthetic.length === 0) return [user]
|
||||
return [
|
||||
user,
|
||||
row(item.row, {
|
||||
id: syntheticID(item.row.id, used),
|
||||
type: "synthetic",
|
||||
text: synthetic.map((part) => (part.type === "text" ? part.text : "")).join("\n\n"),
|
||||
time: { created: item.row.time_created },
|
||||
}),
|
||||
]
|
||||
}
|
||||
if (item.value.role !== "assistant") return []
|
||||
const assistant = item.value
|
||||
const parent = messages.find((candidate) => candidate.row.id === assistant.parentID)
|
||||
const parentParts = parent ? (byMessage.get(parent.row.id)?.map((part) => part.value) ?? []) : []
|
||||
if (
|
||||
parentParts.some((part) => part.type === "subtask") &&
|
||||
owned.some((part) => part.type === "tool" && part.tool === "task")
|
||||
)
|
||||
return []
|
||||
const content = owned.flatMap((part): Array<Record<string, unknown>> => {
|
||||
if (part.type === "text")
|
||||
return [{ type: "text", text: part.text, ...(part.metadata ? { state: part.metadata } : {}) }]
|
||||
if (part.type === "reasoning")
|
||||
return [
|
||||
{
|
||||
type: "reasoning",
|
||||
text: part.text,
|
||||
...(part.metadata ? { state: part.metadata } : {}),
|
||||
time: { created: part.time.start, ...(part.time.end === undefined ? {} : { completed: part.time.end }) },
|
||||
},
|
||||
]
|
||||
if (part.type !== "tool") return []
|
||||
return [migrateTool(part, item.row.time_created)]
|
||||
})
|
||||
const start =
|
||||
owned.flatMap((part) => (part.type === "step-start" && part.snapshot ? [part.snapshot] : []))[0] ??
|
||||
owned.flatMap((part) => (part.type === "snapshot" ? [part.snapshot] : []))[0] ??
|
||||
owned.flatMap((part) => (part.type === "patch" ? [part.hash] : []))[0]
|
||||
const end = owned.flatMap((part) => (part.type === "step-finish" && part.snapshot ? [part.snapshot] : [])).at(-1)
|
||||
const snapshotFiles = Array.from(new Set(owned.flatMap((part) => (part.type === "patch" ? part.files : []))))
|
||||
const finish = normalizeFinish(assistant.finish)
|
||||
return [
|
||||
row(item.row, {
|
||||
id: item.row.id,
|
||||
type: "assistant",
|
||||
agent: assistant.agent,
|
||||
model: {
|
||||
providerID: assistant.providerID,
|
||||
id: assistant.modelID,
|
||||
variant: assistant.variant ?? "default",
|
||||
},
|
||||
content,
|
||||
...(start || end || snapshotFiles.length
|
||||
? {
|
||||
snapshot: {
|
||||
...(start ? { start } : {}),
|
||||
...(end ? { end } : {}),
|
||||
...(snapshotFiles.length ? { files: snapshotFiles } : {}),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(finish ? { finish } : {}),
|
||||
cost: assistant.cost,
|
||||
tokens: {
|
||||
input: assistant.tokens.input,
|
||||
output: assistant.tokens.output,
|
||||
reasoning: assistant.tokens.reasoning,
|
||||
cache: assistant.tokens.cache,
|
||||
},
|
||||
...(assistant.error ? { error: migrateError(assistant.error) } : {}),
|
||||
time: {
|
||||
created: item.row.time_created,
|
||||
...(assistant.time.completed === undefined ? {} : { completed: item.row.time_updated }),
|
||||
},
|
||||
}),
|
||||
]
|
||||
})
|
||||
.map((item, seq) => ({ ...item, seq }))
|
||||
const assistants = messages
|
||||
.filter((item) => item.value.role === "assistant")
|
||||
.map((item) => item.value)
|
||||
.filter((item): item is SessionV1.Assistant => item.role === "assistant")
|
||||
const latestUser = messages.findLast((item) => {
|
||||
if (item.value.role !== "user") return false
|
||||
const owned = byMessage.get(item.row.id) ?? []
|
||||
if (owned.some((part) => part.value.type === "compaction")) return false
|
||||
return !owned.some((part) => part.value.type === "subtask") || !owned.every((part) => part.value.type === "subtask")
|
||||
})
|
||||
return {
|
||||
messages: projected,
|
||||
session: {
|
||||
agent: input.session.agent ?? (latestUser?.value.role === "user" ? latestUser.value.agent : null),
|
||||
model:
|
||||
input.session.model ??
|
||||
(latestUser?.value.role === "user"
|
||||
? {
|
||||
id: latestUser.value.model.modelID,
|
||||
providerID: latestUser.value.model.providerID,
|
||||
variant: latestUser.value.model.variant ?? "default",
|
||||
}
|
||||
: null),
|
||||
cost: assistants.reduce((total, item) => total + item.cost, 0),
|
||||
tokens_input: assistants.reduce((total, item) => total + item.tokens.input, 0),
|
||||
tokens_output: assistants.reduce((total, item) => total + item.tokens.output, 0),
|
||||
tokens_reasoning: assistants.reduce((total, item) => total + item.tokens.reasoning, 0),
|
||||
tokens_cache_read: assistants.reduce((total, item) => total + item.tokens.cache.read, 0),
|
||||
tokens_cache_write: assistants.reduce((total, item) => total + item.tokens.cache.write, 0),
|
||||
revert: null,
|
||||
time_compacting: null,
|
||||
},
|
||||
watermark: projected.length - 1,
|
||||
warnings,
|
||||
}
|
||||
}
|
||||
|
||||
export function status(): Effect.Effect<Status, never, Database.Service> {
|
||||
return Effect.gen(function* () {
|
||||
const { db } = yield* Database.Service
|
||||
if (!(yield* hasLegacySessions(db))) return { status: "completed" as const }
|
||||
const state = yield* readState(db)
|
||||
if (runtimeState.status === "running") return runtimeState
|
||||
if (runtimeState.status === "error") return runtimeState
|
||||
if (state?.phase === "completed") return { status: "completed" as const }
|
||||
return { status: "required" as const }
|
||||
}).pipe(Effect.orDie)
|
||||
}
|
||||
|
||||
export const layer = Layer.effectDiscard(
|
||||
Effect.gen(function* () {
|
||||
runtimeState = { status: "running", progress: { label: "Clearing old events" } }
|
||||
yield* run().pipe(
|
||||
Effect.matchCauseEffect({
|
||||
onFailure: (cause) =>
|
||||
Effect.sync(() => {
|
||||
runtimeState = { status: "error", error: errorText(Cause.squash(cause)) }
|
||||
}).pipe(Effect.andThen(Effect.logError("V1 migration failed", { cause }))),
|
||||
onSuccess: () =>
|
||||
Effect.sync(() => {
|
||||
runtimeState = { status: "idle" }
|
||||
}),
|
||||
}),
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
function errorText(input: unknown): string {
|
||||
if (!(input instanceof Error)) return String(input)
|
||||
const cause = input.cause
|
||||
return cause === undefined ? input.message : `${input.message}\nCaused by: ${errorText(cause)}`
|
||||
}
|
||||
|
||||
function updateProgress(progress: Progress) {
|
||||
if (runtimeState.status === "running") runtimeState = { status: "running", progress }
|
||||
}
|
||||
|
||||
export function run(options: Options = {}): Effect.Effect<RunResult, never, Database.Service | Global.Service> {
|
||||
return lock.withPermit(
|
||||
Effect.gen(function* () {
|
||||
const { db } = yield* Database.Service
|
||||
const global = yield* Global.Service
|
||||
const state = yield* readState(db)
|
||||
if (state?.phase === "completed") return { status: "completed" as const }
|
||||
if (!(yield* hasLegacySessions(db))) return { status: "completed" as const }
|
||||
const migrate = Effect.gen(function* () {
|
||||
const now = Date.now()
|
||||
yield* db.run(sql`
|
||||
INSERT OR IGNORE INTO project (id, worktree, time_created, time_updated, sandboxes)
|
||||
VALUES (${Project.ID.global}, ${path.parse(global.data).root}, ${now}, ${now}, '[]')
|
||||
`)
|
||||
if (state === undefined)
|
||||
yield* db
|
||||
.transaction((tx) =>
|
||||
Effect.gen(function* () {
|
||||
while (true) {
|
||||
yield* tx.run(sql`
|
||||
DELETE FROM event
|
||||
WHERE rowid IN (SELECT rowid FROM event LIMIT ${EVENT_DELETE_BATCH_SIZE})
|
||||
`)
|
||||
const deleted = (yield* tx.get<{ value: number }>(sql`SELECT changes() AS value`))?.value ?? 0
|
||||
if (deleted < EVENT_DELETE_BATCH_SIZE) break
|
||||
yield* Effect.yieldNow
|
||||
}
|
||||
yield* tx
|
||||
.insert(KVTable)
|
||||
.values({ key: MIGRATION_STATE_KEY, value: { phase: "sessions" } })
|
||||
.run()
|
||||
}),
|
||||
)
|
||||
.pipe(Effect.orDie)
|
||||
const sourceTotal = yield* countNextSessions(nextPath(options, global.data))
|
||||
const legacyTotal = (yield* db.get<{ value: number }>(sql`SELECT COUNT(*) AS value FROM session`))?.value ?? 0
|
||||
const cursor = state?.phase === "sessions" ? state.cursor : undefined
|
||||
const migrated =
|
||||
cursor !== undefined
|
||||
? ((yield* db.get<{ value: number }>(sql`SELECT COUNT(*) AS value FROM session WHERE id >= ${cursor}`))
|
||||
?.value ?? 0)
|
||||
: 0
|
||||
const denominator = sourceTotal + legacyTotal
|
||||
updateProgress({ label: "Migrating sessions", numerator: migrated, denominator })
|
||||
yield* importNextDatabase(db, nextPath(options, global.data), (completed) => {
|
||||
updateProgress({ label: "Migrating sessions", numerator: migrated + completed, denominator })
|
||||
})
|
||||
updateProgress({ label: "Migrating sessions", numerator: migrated + sourceTotal, denominator })
|
||||
const projects = new Set(
|
||||
(yield* db.all<{ id: string }>(sql`SELECT id FROM project`)).map((project) => project.id),
|
||||
)
|
||||
while (true) {
|
||||
const state = yield* readState(db)
|
||||
const cursorValue = state?.phase === "sessions" ? state.cursor : undefined
|
||||
const nextID = yield* db.get<{ id: string; project_id: string }>(
|
||||
cursorValue === undefined
|
||||
? sql`SELECT id, project_id FROM session ORDER BY id DESC LIMIT 1`
|
||||
: sql`SELECT id, project_id FROM session WHERE id < ${cursorValue} ORDER BY id DESC LIMIT 1`,
|
||||
)
|
||||
if (!nextID) break
|
||||
yield* db
|
||||
.transaction((tx) =>
|
||||
Effect.gen(function* () {
|
||||
yield* tx
|
||||
.insert(KVTable)
|
||||
.values({ key: MIGRATION_STATE_KEY, value: { phase: "sessions", cursor: nextID.id } })
|
||||
.onConflictDoUpdate({
|
||||
target: KVTable.key,
|
||||
set: { value: { phase: "sessions", cursor: nextID.id }, time_updated: Date.now() },
|
||||
})
|
||||
.run()
|
||||
const projectID = projects.has(nextID.project_id) ? nextID.project_id : Project.ID.global
|
||||
if (projectID !== nextID.project_id)
|
||||
yield* Effect.logWarning("Reassigned V1 session with missing project", {
|
||||
sessionID: nextID.id,
|
||||
projectID: nextID.project_id,
|
||||
})
|
||||
yield* tx.run(sql`
|
||||
INSERT OR IGNORE INTO session_v2 (
|
||||
id, project_id, workspace_id, parent_id, slug, directory, path, title, version, share_url,
|
||||
summary_additions, summary_deletions, summary_files, summary_diffs, metadata, cost,
|
||||
tokens_input, tokens_output, tokens_reasoning, tokens_cache_read, tokens_cache_write,
|
||||
revert, permission, agent, model, time_created, time_updated, time_compacting, time_archived
|
||||
)
|
||||
SELECT
|
||||
id, ${projectID}, workspace_id, parent_id, slug, directory, path, title, version, share_url,
|
||||
summary_additions, summary_deletions, summary_files, summary_diffs, metadata, cost,
|
||||
tokens_input, tokens_output, tokens_reasoning, tokens_cache_read, tokens_cache_write,
|
||||
revert, permission, agent, model, time_created, time_updated, time_compacting, time_archived
|
||||
FROM session
|
||||
WHERE id = ${nextID.id}
|
||||
`)
|
||||
const next = yield* tx
|
||||
.select()
|
||||
.from(SessionTable)
|
||||
.where(eq(SessionTable.id, SessionSchema.ID.make(nextID.id)))
|
||||
.get()
|
||||
if (!next) return yield* Effect.die(new Error(`Failed to copy V1 session ${nextID.id}`))
|
||||
const sourceMessages = yield* tx.all<SourceMessage>(
|
||||
sql`SELECT id, session_id, time_created, time_updated, data FROM message WHERE session_id = ${next.id}`,
|
||||
)
|
||||
const sourceParts = yield* tx.all<SourcePart>(
|
||||
sql`SELECT id, message_id, session_id, time_created, time_updated, data FROM part WHERE session_id = ${next.id}`,
|
||||
)
|
||||
const transformed = transformSession({ session: next, messages: sourceMessages, parts: sourceParts })
|
||||
yield* Effect.forEach(transformed.warnings, (warning) =>
|
||||
Effect.logWarning("Skipped V1 migration row", warning),
|
||||
)
|
||||
yield* tx.delete(SessionMessageTable).where(eq(SessionMessageTable.session_id, next.id)).run()
|
||||
yield* Effect.forEach(transformed.messages, (message) =>
|
||||
tx
|
||||
.insert(SessionMessageTable)
|
||||
.values({
|
||||
id: SessionMessage.ID.make(message.id),
|
||||
session_id: SessionSchema.ID.make(message.session_id),
|
||||
type: message.type,
|
||||
seq: message.seq,
|
||||
time_created: message.time_created,
|
||||
time_updated: message.time_updated,
|
||||
data: sql`${JSON.stringify(message.data)}`,
|
||||
})
|
||||
.run(),
|
||||
)
|
||||
yield* tx
|
||||
.update(SessionTable)
|
||||
.set({ ...transformed.session, time_updated: next.time_updated })
|
||||
.where(eq(SessionTable.id, next.id))
|
||||
.run()
|
||||
yield* tx
|
||||
.insert(EventSequenceTable)
|
||||
.values({ aggregate_id: next.id, seq: transformed.watermark })
|
||||
.onConflictDoUpdate({
|
||||
target: EventSequenceTable.aggregate_id,
|
||||
set: { seq: transformed.watermark, owner_id: null },
|
||||
})
|
||||
.run()
|
||||
}),
|
||||
)
|
||||
.pipe(Effect.orDie)
|
||||
if (runtimeState.status === "running")
|
||||
runtimeState = {
|
||||
status: "running",
|
||||
progress: {
|
||||
label: "Migrating sessions",
|
||||
numerator: (runtimeState.progress.numerator ?? 0) + 1,
|
||||
denominator,
|
||||
},
|
||||
}
|
||||
yield* Effect.yieldNow
|
||||
}
|
||||
yield* db
|
||||
.transaction((tx) =>
|
||||
Effect.gen(function* () {
|
||||
yield* tx
|
||||
.insert(KVTable)
|
||||
.values({ key: MIGRATION_STATE_KEY, value: { phase: "completed" } })
|
||||
.onConflictDoUpdate({
|
||||
target: KVTable.key,
|
||||
set: { value: { phase: "completed" }, time_updated: Date.now() },
|
||||
})
|
||||
.run()
|
||||
}),
|
||||
)
|
||||
.pipe(Effect.orDie)
|
||||
return { status: "completed" as const }
|
||||
})
|
||||
return yield* migrate
|
||||
}).pipe(Effect.orDie),
|
||||
)
|
||||
}
|
||||
|
||||
function nextPath(options: Options, data: string) {
|
||||
if (options.nextDatabasePath) return options.nextDatabasePath
|
||||
if (process.env.OPENCODE_DB === ":memory:") return undefined
|
||||
return path.join(data, "opencode-next.db")
|
||||
}
|
||||
|
||||
function openNextDatabase(sourcePath: string) {
|
||||
return Effect.acquireRelease(
|
||||
Effect.gen(function* () {
|
||||
const sqlite = yield* Effect.promise(() => import("bun:sqlite"))
|
||||
return new sqlite.Database(sourcePath, { readonly: true, strict: true })
|
||||
}),
|
||||
(source) => Effect.sync(() => source.close()),
|
||||
)
|
||||
}
|
||||
|
||||
function countNextSessions(sourcePath: string | undefined) {
|
||||
if (!sourcePath || !existsSync(sourcePath)) return Effect.succeed(0)
|
||||
return Effect.scoped(
|
||||
Effect.gen(function* () {
|
||||
const source = yield* openNextDatabase(sourcePath)
|
||||
if (!isNextDatabase(source)) return 0
|
||||
return source.query<{ value: number }, []>("SELECT COUNT(*) AS value FROM session").get()?.value ?? 0
|
||||
}),
|
||||
).pipe(Effect.orElseSucceed(() => 0))
|
||||
}
|
||||
|
||||
function importNextDatabase(
|
||||
db: Database.Interface["db"],
|
||||
sourcePath: string | undefined,
|
||||
onProgress: (completed: number) => void,
|
||||
): Effect.Effect<void, unknown> {
|
||||
if (!sourcePath || !existsSync(sourcePath)) return Effect.void
|
||||
return Effect.scoped(
|
||||
Effect.gen(function* () {
|
||||
const source = yield* openNextDatabase(sourcePath)
|
||||
if (!isNextDatabase(source)) {
|
||||
yield* Effect.logWarning("Skipped incompatible opencode-next.db", { path: sourcePath })
|
||||
return
|
||||
}
|
||||
source.run("BEGIN")
|
||||
yield* Effect.addFinalizer(() =>
|
||||
Effect.sync(() => {
|
||||
if (source.inTransaction) source.run("ROLLBACK")
|
||||
}),
|
||||
)
|
||||
const projects = new Map(
|
||||
source
|
||||
.query<NextProject, []>("SELECT * FROM project")
|
||||
.all()
|
||||
.map((project) => [project.id, project]),
|
||||
)
|
||||
const sessions = source.query<NextSession, []>("SELECT * FROM session ORDER BY id DESC").all()
|
||||
for (const [index, session] of sessions.entries()) {
|
||||
const project = projects.get(session.project_id)
|
||||
const projectID = project ? session.project_id : Project.ID.global
|
||||
if (!project) {
|
||||
yield* Effect.logWarning("Reassigned previous V2 session with missing project", {
|
||||
sessionID: session.id,
|
||||
projectID: session.project_id,
|
||||
})
|
||||
}
|
||||
const messages = source
|
||||
.query<
|
||||
NextMessage,
|
||||
[string]
|
||||
>("SELECT id, session_id, type, seq, time_created, time_updated, data FROM session_message WHERE session_id = ? ORDER BY seq")
|
||||
.all(session.id)
|
||||
yield* db
|
||||
.transaction((tx) =>
|
||||
Effect.gen(function* () {
|
||||
if (project)
|
||||
yield* tx.run(sql`
|
||||
INSERT OR IGNORE INTO project (
|
||||
id, worktree, vcs, name, icon_url, icon_url_override, icon_color,
|
||||
time_created, time_updated, time_initialized, sandboxes, commands
|
||||
) VALUES (
|
||||
${project.id}, ${project.worktree}, ${project.vcs}, ${project.name}, ${project.icon_url},
|
||||
${project.icon_url_override}, ${project.icon_color}, ${project.time_created}, ${project.time_updated},
|
||||
${project.time_initialized}, ${project.sandboxes}, ${project.commands}
|
||||
)
|
||||
`)
|
||||
const existing = yield* tx
|
||||
.select({ id: SessionTable.id })
|
||||
.from(SessionTable)
|
||||
.where(eq(SessionTable.id, SessionSchema.ID.make(session.id)))
|
||||
.get()
|
||||
if (existing) return
|
||||
yield* tx.run(sql`
|
||||
INSERT INTO session_v2 (
|
||||
id, project_id, workspace_id, parent_id, fork_session_id, fork_boundary, slug, directory,
|
||||
path, title, version, share_url, summary_additions, summary_deletions, summary_files,
|
||||
summary_diffs, metadata, cost, tokens_input, tokens_output, tokens_reasoning, tokens_cache_read,
|
||||
tokens_cache_write, revert, permission, agent, model, time_created, time_updated, time_compacting,
|
||||
time_archived, time_suspended
|
||||
) VALUES (
|
||||
${session.id}, ${projectID}, ${session.workspace_id}, ${session.parent_id},
|
||||
${session.fork_session_id}, ${session.fork_boundary}, ${session.slug}, ${session.directory},
|
||||
${session.path}, ${session.title}, ${session.version}, ${session.share_url},
|
||||
${session.summary_additions}, ${session.summary_deletions}, ${session.summary_files},
|
||||
${session.summary_diffs}, ${session.metadata}, ${session.cost}, ${session.tokens_input},
|
||||
${session.tokens_output}, ${session.tokens_reasoning}, ${session.tokens_cache_read},
|
||||
${session.tokens_cache_write}, ${session.revert}, ${session.permission}, ${session.agent},
|
||||
${session.model}, ${session.time_created}, ${session.time_updated}, ${session.time_compacting},
|
||||
${session.time_archived}, ${session.time_suspended}
|
||||
)
|
||||
`)
|
||||
yield* Effect.forEach(messages, (message) =>
|
||||
tx
|
||||
.insert(SessionMessageTable)
|
||||
.values({
|
||||
id: SessionMessage.ID.make(message.id),
|
||||
session_id: SessionSchema.ID.make(message.session_id),
|
||||
type: message.type as SessionMessage.Type,
|
||||
seq: message.seq,
|
||||
time_created: message.time_created,
|
||||
time_updated: message.time_updated,
|
||||
data: sql`${message.data}`,
|
||||
})
|
||||
.run(),
|
||||
)
|
||||
yield* tx
|
||||
.insert(EventSequenceTable)
|
||||
.values({ aggregate_id: session.id, seq: messages.at(-1)?.seq ?? -1 })
|
||||
.onConflictDoUpdate({
|
||||
target: EventSequenceTable.aggregate_id,
|
||||
set: { seq: messages.at(-1)?.seq ?? -1, owner_id: null },
|
||||
})
|
||||
.run()
|
||||
}),
|
||||
)
|
||||
.pipe(Effect.orDie)
|
||||
onProgress(index + 1)
|
||||
yield* Effect.yieldNow
|
||||
}
|
||||
source.run("COMMIT")
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function isNextDatabase(source: SQLiteDatabase) {
|
||||
const tables = new Set(
|
||||
source
|
||||
.query<{ name: string }, []>("SELECT name FROM sqlite_master WHERE type = 'table'")
|
||||
.all()
|
||||
.map((table) => table.name),
|
||||
)
|
||||
return tables.has("project") && tables.has("session") && tables.has("session_message")
|
||||
}
|
||||
|
||||
function row(
|
||||
source: SourceMessage,
|
||||
message: {
|
||||
readonly id: string
|
||||
readonly type: SessionMessage.Type
|
||||
readonly time: { readonly created: number }
|
||||
readonly [key: string]: unknown
|
||||
},
|
||||
): TransformResult["messages"][number] {
|
||||
const { id, type, ...data } = message
|
||||
return {
|
||||
id,
|
||||
session_id: source.session_id,
|
||||
type,
|
||||
seq: 0,
|
||||
time_created: source.time_created,
|
||||
time_updated: source.time_updated,
|
||||
data,
|
||||
}
|
||||
}
|
||||
|
||||
function migrateTool(part: typeof SessionV1.ToolPart.Type, fallback: number) {
|
||||
const base = {
|
||||
type: "tool" as const,
|
||||
id: part.callID,
|
||||
name: part.tool,
|
||||
...(part.metadata ? { providerState: part.metadata } : {}),
|
||||
}
|
||||
if (part.state.status === "completed")
|
||||
return {
|
||||
...base,
|
||||
state: {
|
||||
status: "completed",
|
||||
input: part.state.input,
|
||||
content:
|
||||
part.state.time.compacted === undefined
|
||||
? [
|
||||
{ type: "text", text: part.state.output },
|
||||
...(part.state.attachments ?? []).map((file) => ({
|
||||
type: "file" as const,
|
||||
uri: file.url,
|
||||
mime: file.mime,
|
||||
...(file.filename ? { name: file.filename } : {}),
|
||||
})),
|
||||
]
|
||||
: [{ type: "text", text: "[Old tool result content cleared]" }],
|
||||
metadata: part.state.metadata,
|
||||
},
|
||||
time: { created: part.state.time.start, completed: part.state.time.end },
|
||||
}
|
||||
if (part.state.status === "error")
|
||||
return {
|
||||
...base,
|
||||
state: {
|
||||
status: "error",
|
||||
input: part.state.input,
|
||||
error: { type: "tool.execution", message: part.state.error },
|
||||
...(typeof part.state.metadata?.output === "string"
|
||||
? { content: [{ type: "text", text: part.state.metadata.output }] }
|
||||
: {}),
|
||||
...(part.state.metadata ? { metadata: part.state.metadata } : {}),
|
||||
},
|
||||
time: { created: part.state.time.start, completed: part.state.time.end },
|
||||
}
|
||||
return {
|
||||
...base,
|
||||
state: {
|
||||
status: "error",
|
||||
input: part.state.input,
|
||||
error: { type: "tool.interrupted", message: "Tool execution was interrupted before V2 migration" },
|
||||
...(part.state.status === "running" && part.state.metadata ? { metadata: part.state.metadata } : {}),
|
||||
},
|
||||
time: { created: part.state.status === "running" ? part.state.time.start : fallback },
|
||||
}
|
||||
}
|
||||
|
||||
function migrateError(error: NonNullable<(typeof SessionV1.Assistant.Type)["error"]>) {
|
||||
const message =
|
||||
"message" in error.data
|
||||
? error.data.message
|
||||
: error.name === "MessageOutputLengthError"
|
||||
? "The model exceeded its output limit"
|
||||
: error.name
|
||||
const type =
|
||||
error.name === "ProviderAuthError"
|
||||
? "provider.auth"
|
||||
: error.name === "ContentFilterError"
|
||||
? "provider.content-filter"
|
||||
: error.name === "ContextOverflowError"
|
||||
? "provider.invalid-request"
|
||||
: error.name === "StructuredOutputError" || error.name === "MessageOutputLengthError"
|
||||
? "provider.invalid-output"
|
||||
: error.name === "MessageAbortedError"
|
||||
? "aborted"
|
||||
: error.name === "APIError"
|
||||
? "provider.error"
|
||||
: "unknown"
|
||||
return { type, message }
|
||||
}
|
||||
|
||||
function normalizeFinish(finish: string | undefined) {
|
||||
if (!finish) return undefined
|
||||
return (
|
||||
(["stop", "length", "tool-calls", "content-filter", "error", "unknown"] as const).find(
|
||||
(value) => value === finish,
|
||||
) ?? "unknown"
|
||||
)
|
||||
}
|
||||
|
||||
function migrateFile(part: SessionV1.FilePart) {
|
||||
if (!part.url.startsWith("data:")) return []
|
||||
const comma = part.url.indexOf(",")
|
||||
if (comma < 0) return []
|
||||
const header = part.url.slice(0, comma)
|
||||
const payload = part.url.slice(comma + 1)
|
||||
const data = header.endsWith(";base64")
|
||||
? Buffer.from(payload, "base64").toString("base64")
|
||||
: Buffer.from(decodeURIComponent(payload)).toString("base64")
|
||||
return [
|
||||
{
|
||||
data,
|
||||
mime: part.mime,
|
||||
source:
|
||||
part.source?.type === "resource" ? { type: "uri" as const, uri: part.source.uri } : { type: "inline" as const },
|
||||
...(part.filename ? { name: part.filename } : {}),
|
||||
...(part.source
|
||||
? { mention: { text: part.source.text.value, start: part.source.text.start, end: part.source.text.end } }
|
||||
: {}),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function unavailableFile(part: SessionV1.FilePart) {
|
||||
const label = part.filename ?? (part.source?.type === "resource" ? part.source.uri : part.url)
|
||||
return `[Attachment unavailable after migration: ${label} (${part.mime})]`
|
||||
}
|
||||
|
||||
function syntheticID(source: string, used: Set<string>) {
|
||||
const prefix = source.slice(0, 16)
|
||||
const alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||
for (let salt = 0; ; salt++) {
|
||||
const hex = new Bun.CryptoHasher("sha256").update(`v1-synthetic:${source}${salt ? `:${salt}` : ""}`).digest("hex")
|
||||
let value = BigInt(`0x${hex}`)
|
||||
let suffix = ""
|
||||
while (suffix.length < 14) {
|
||||
suffix = alphabet[Number(value % 62n)] + suffix
|
||||
value /= 62n
|
||||
}
|
||||
const id = prefix + suffix
|
||||
if (used.has(id)) continue
|
||||
used.add(id)
|
||||
return id
|
||||
}
|
||||
}
|
||||
|
||||
function serializeRecent(
|
||||
messages: ReadonlyArray<{ row: SourceMessage; value: typeof SessionV1.Info.Type }>,
|
||||
parts: Map<string, Array<{ row: SourcePart; value: typeof SessionV1.Part.Type }>>,
|
||||
) {
|
||||
return messages
|
||||
.flatMap((message) => {
|
||||
const owned = parts.get(message.row.id)?.map((part) => part.value) ?? []
|
||||
if (message.value.role === "user")
|
||||
return [
|
||||
`[User]: ${owned
|
||||
.filter((part) => part.type === "text" && !part.ignored)
|
||||
.map((part) => (part.type === "text" ? part.text : ""))
|
||||
.join("\n\n")}`,
|
||||
]
|
||||
return owned.flatMap((part) =>
|
||||
part.type === "text"
|
||||
? [`[Assistant]: ${part.text}`]
|
||||
: part.type === "reasoning" && part.text
|
||||
? [`[Assistant reasoning]: ${part.text}`]
|
||||
: [],
|
||||
)
|
||||
})
|
||||
.join("\n\n")
|
||||
}
|
||||
|
||||
function readState(db: Database.Interface["db"]): Effect.Effect<MigrationState | undefined> {
|
||||
return db
|
||||
.select({ value: KVTable.value })
|
||||
.from(KVTable)
|
||||
.where(eq(KVTable.key, MIGRATION_STATE_KEY))
|
||||
.get()
|
||||
.pipe(
|
||||
Effect.map((row) => parseState(row?.value)),
|
||||
Effect.orDie,
|
||||
)
|
||||
}
|
||||
|
||||
function parseState(input: unknown): MigrationState | undefined {
|
||||
if (!input || typeof input !== "object" || !("phase" in input)) return
|
||||
if (input.phase === "completed") return { phase: "completed" }
|
||||
if (input.phase !== "sessions") return
|
||||
if (!("cursor" in input) || input.cursor === undefined) return { phase: "sessions" }
|
||||
if (typeof input.cursor === "string") return { phase: "sessions", cursor: input.cursor }
|
||||
}
|
||||
|
||||
function hasLegacySessions(db: Database.Interface["db"]) {
|
||||
return db.get(sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'session'`).pipe(
|
||||
Effect.map((row) => row !== undefined),
|
||||
Effect.orDie,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { bind } from "./fff.js"
|
||||
|
||||
export type { Directory, DirSearch, File, Init, Mixed, MixedSearch, Picker, Result, Search } from "./fff.js"
|
||||
|
||||
// No fff backend exists on workerd; every create reports unavailability and
|
||||
// FileSystemSearch degrades the same way it does on a runtime without the native module.
|
||||
const adapter = bind(undefined, "fff unavailable on workerd runtime")
|
||||
|
||||
export const available = adapter.available
|
||||
export const create = adapter.create
|
||||
|
||||
export * as Fff from "./fff.workerd.js"
|
||||
@@ -1,4 +0,0 @@
|
||||
// workerd has no filesystem path to a photon wasm artifact. Image.Photon only
|
||||
// reads this lazily and surfaces a typed ResizerUnavailableError when loading
|
||||
// fails, so an empty path degrades cleanly instead of breaking module load.
|
||||
export default ""
|
||||
@@ -164,8 +164,6 @@ export const Options = Schema.Struct({
|
||||
version: Schema.String,
|
||||
}),
|
||||
),
|
||||
/** Set false on runtimes that cannot spawn child processes; local (stdio) servers report failed instead of connecting. */
|
||||
stdio: Schema.optional(Schema.Boolean),
|
||||
})
|
||||
export type Options = typeof Options.Type
|
||||
|
||||
@@ -504,11 +502,6 @@ export const layer = (options?: Options) =>
|
||||
|
||||
const startServer = (name: ServerName, entry: ServerEntry) =>
|
||||
Effect.gen(function* () {
|
||||
if (options?.stdio === false && entry.config.type === "local") {
|
||||
entry.status = { status: "failed", error: "stdio MCP servers are unavailable in this runtime" }
|
||||
yield* bus.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
|
||||
return
|
||||
}
|
||||
// Announce the handshake so connect() and credential reconnects don't show a stale
|
||||
// disabled/failed status for the duration of the connection attempt.
|
||||
entry.status = { status: "pending" }
|
||||
|
||||
@@ -2,6 +2,7 @@ export * as MCPOAuth from "./oauth.js"
|
||||
|
||||
import { auth, type OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js"
|
||||
import type { OAuthClientInformationMixed, OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js"
|
||||
import { createServer } from "node:http"
|
||||
import { Deferred, Effect } from "effect"
|
||||
import { Credential } from "@opencode-ai/schema/credential"
|
||||
import { ConfigMCP } from "@opencode-ai/schema/config/mcp"
|
||||
@@ -151,8 +152,6 @@ export const authorize = (input: {
|
||||
const redirectPath = oauth?.redirect_uri ? new URL(oauth.redirect_uri).pathname : "/callback"
|
||||
const state = Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString("base64url")
|
||||
|
||||
// Lazy so runtimes without a loopback listener (workerd) never evaluate node:http.
|
||||
const { createServer } = yield* Effect.promise(() => import("node:http"))
|
||||
const server = createServer((request, response) => {
|
||||
const url = new URL(request.url ?? "/", "http://127.0.0.1")
|
||||
if (url.pathname !== redirectPath) {
|
||||
|
||||
@@ -544,24 +544,6 @@ const Cache = Schema.Struct({
|
||||
})
|
||||
const defaultSource = "https://models.opencode.ai"
|
||||
|
||||
// Bundled snapshot of https://models.opencode.ai/api.json, committed at
|
||||
// packages/core/src/models-dev/snapshot.txt and refreshed via
|
||||
// `bun run script/update-models-snapshot.ts`. Decoded and normalized once per
|
||||
// isolate: the snapshot is a multi-MB module-level constant and one isolate can
|
||||
// host many runtimes (Cloudflare colocates Durable Object instances), so
|
||||
// per-runtime decoding would multiply the cost.
|
||||
let bundledCache: readonly Snapshot[] | undefined
|
||||
const bundledSnapshot = Effect.suspend(() =>
|
||||
bundledCache
|
||||
? Effect.succeed(bundledCache)
|
||||
: decodeCatalog(snapshotText).pipe(
|
||||
Effect.map((catalog) => {
|
||||
bundledCache = normalize(catalog)
|
||||
return bundledCache
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
function cacheKey(source: string) {
|
||||
if (source === defaultSource) return "models-dev:catalog"
|
||||
return `models-dev:catalog:${Hash.fast(source)}`
|
||||
@@ -625,9 +607,11 @@ export const layer = (options?: Options) =>
|
||||
)
|
||||
: Effect.succeed(undefined)
|
||||
|
||||
// The bundled snapshot is the boot-time floor for the catalog; the
|
||||
// periodic fetch below still refreshes on top.
|
||||
const loadSnapshot = options?.snapshot === false ? Effect.succeed(undefined) : bundledSnapshot
|
||||
// Bundled snapshot of https://models.opencode.ai/api.json, committed at
|
||||
// packages/core/src/models-dev/snapshot.txt and refreshed via
|
||||
// `bun run script/update-models-snapshot.ts`. It is the boot-time floor
|
||||
// for the catalog; the periodic fetch below still refreshes on top.
|
||||
const loadSnapshot = options?.snapshot === false ? Effect.succeed(undefined) : decodeCatalog(snapshotText)
|
||||
|
||||
const fetchAndWrite = Effect.fn("ModelsDev.fetchAndWrite")(function* () {
|
||||
const text = yield* fetchApi()
|
||||
@@ -651,7 +635,7 @@ export const layer = (options?: Options) =>
|
||||
const cached = options?.file ? undefined : yield* loadFromCache()
|
||||
if (cached) return normalize(cached.catalog)
|
||||
const bundled = yield* loadSnapshot
|
||||
if (bundled) return bundled
|
||||
if (bundled) return normalize(bundled)
|
||||
if (!fetch) return []
|
||||
const catalog = yield* lock.withPermit(
|
||||
Effect.gen(function* () {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createServer } from "node:http"
|
||||
import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/effect/integration"
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import { Deferred, Effect, Option, Schema, Semaphore, Stream } from "effect"
|
||||
@@ -57,8 +58,6 @@ const browser = (app: App.Info) =>
|
||||
const state = base64UrlEncode(crypto.getRandomValues(new Uint8Array(32)).buffer)
|
||||
const code = yield* Deferred.make<string, Error>()
|
||||
const redirect = `http://localhost:${callbackPort}/auth/callback`
|
||||
// Lazy so runtimes without a loopback listener (workerd) never evaluate node:http.
|
||||
const { createServer } = yield* Effect.promise(() => import("node:http"))
|
||||
const server = createServer((request, response) => {
|
||||
const url = new URL(request.url ?? "/", `http://localhost:${callbackPort}`)
|
||||
if (url.pathname !== "/auth/callback") {
|
||||
|
||||
@@ -23,11 +23,7 @@ export const GooglePlugin = make("google", (id) => (id.includes("gemini-") ? PRO
|
||||
export const AnthropicPlugin = make("anthropic", (id) => (id.includes("claude") ? PROMPT_ANTHROPIC : undefined))
|
||||
export const KimiPlugin = make("kimi", (id) => (id.includes("kimi") ? PROMPT_KIMI : undefined))
|
||||
export const ArceePlugin = make("arcee", (id) => (id.includes("trinity") ? PROMPT_TRINITY : undefined))
|
||||
export const MetaPlugin = make("meta", (id) => {
|
||||
if (!id.includes("muse")) return
|
||||
const name = id.includes("muse-glimmer") ? "Muse Glimmer" : "Muse Spark"
|
||||
return PROMPT_META.replaceAll("{{MODEL_NAME}}", name)
|
||||
})
|
||||
export const MetaPlugin = make("meta", (id) => (id.includes("muse-spark") ? PROMPT_META : undefined))
|
||||
|
||||
export const Plugins = [OpenAIPlugin, GooglePlugin, AnthropicPlugin, KimiPlugin, ArceePlugin, MetaPlugin] as const
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
You are OpenCode, a coding agent that helps users with software engineering tasks. You are powered by {{MODEL_NAME}}, a large language model trained by Meta MSL.
|
||||
You are OpenCode, a coding agent that helps users with software engineering tasks. You are powered by Muse Spark, a large language model trained by Meta MSL.
|
||||
|
||||
Use the instructions below and the tools available to assist the user.
|
||||
|
||||
@@ -55,5 +55,5 @@ Use the instructions below and the tools available to assist the user.
|
||||
- NEVER use comments as a place for long-winded chain-of-thought. Long thinking texts must be generated as private reasoning. Comments in code must be appropriately concise.
|
||||
|
||||
# User Help & Feedback
|
||||
- Users can give feedback or report issues at https://github.com/anomalyco/opencode and mention that they are using Meta {{MODEL_NAME}}.
|
||||
- Users can give feedback or report issues at https://github.com/anomalyco/opencode and mention that they are using Meta Muse Spark.
|
||||
- When users ask directly about OpenCode (eg. "can OpenCode do...", "are you able to do...") or its features (eg. implement a hook, write a slash command, or install an MCP server), use the `webfetch` tool to gather information to answer the question from the V2 OpenCode docs at https://opencode.ai/v2/docs/.
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import type { Proc } from "./pty.js"
|
||||
|
||||
export type { Disp, Exit, Opts, Proc } from "./pty.js"
|
||||
|
||||
// workerd cannot spawn processes; the Pty service surfaces this as a defect if
|
||||
// a terminal is ever requested on this runtime.
|
||||
export function spawn(): Proc {
|
||||
throw new Error("Pseudo-terminals are unavailable on the workerd runtime")
|
||||
}
|
||||
@@ -185,8 +185,8 @@ export interface Interface {
|
||||
) => Effect.Effect<SessionMessage.Info[], NotFoundError | MessageDecodeError>
|
||||
/**
|
||||
* Durable admitted session work not yet visible in projected history,
|
||||
* ordered by admission. Includes unpromoted user and synthetic inputs and
|
||||
* unhandled compaction barriers.
|
||||
* ordered by admission. Includes unpromoted user and synthetic inputs,
|
||||
* unhandled compaction barriers, and deferred moves.
|
||||
*/
|
||||
readonly pending: (sessionID: SessionSchema.ID) => Effect.Effect<SessionPending.Info[], NotFoundError>
|
||||
readonly cancelPending: (input: PendingInputRef) => Effect.Effect<void, NotFoundError | PendingInputConflictError>
|
||||
@@ -738,23 +738,21 @@ const layer = Layer.effect(
|
||||
const info = yield* fs.stat(directory).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
||||
if (!info) return yield* new DestinationNotFoundError({ directory })
|
||||
if (info.type !== "Directory") return yield* new DestinationNotDirectoryError({ directory })
|
||||
if (current.location.directory === directory && current.location.workspaceID === input.workspaceID) return
|
||||
const pending = yield* SessionPending.move(db, input.sessionID)
|
||||
if (!pending && current.location.directory === directory && current.location.workspaceID === input.workspaceID)
|
||||
return
|
||||
const project = yield* projects.resolve(directory)
|
||||
yield* persistProject(project)
|
||||
if ((yield* execution.active).has(input.sessionID)) {
|
||||
yield* execution.interrupt(input.sessionID)
|
||||
yield* execution.awaitIdle(input.sessionID)
|
||||
}
|
||||
yield* bus.publish(
|
||||
SessionEvent.Moved,
|
||||
{
|
||||
sessionID: input.sessionID,
|
||||
yield* SessionPending.admitMove(db, bus, {
|
||||
sessionID: input.sessionID,
|
||||
source: current.location,
|
||||
data: {
|
||||
location: Location.Ref.make({ directory, workspaceID: input.workspaceID }),
|
||||
projectID: project.id,
|
||||
subpath: RelativePath.make(path.relative(project.directory, directory).replaceAll("\\", "/")),
|
||||
},
|
||||
{ location: current.location },
|
||||
)
|
||||
})
|
||||
yield* execution.wake(input.sessionID)
|
||||
}),
|
||||
compact: Effect.fn("Session.compact")(function* (input) {
|
||||
yield* result.get(input.sessionID)
|
||||
|
||||
@@ -11,6 +11,8 @@ import { SessionSchema } from "./schema.js"
|
||||
import { SessionStore } from "./store.js"
|
||||
import { toSessionError } from "./to-session-error.js"
|
||||
import { UserInterruptedError } from "./error.js"
|
||||
import { Database } from "../database/database.js"
|
||||
import { SessionPending } from "./pending.js"
|
||||
|
||||
export interface Interface {
|
||||
/** Snapshots active execution owned by this process. */
|
||||
@@ -45,6 +47,7 @@ export const layer = Layer.effect(
|
||||
const store = yield* SessionStore.Service
|
||||
const locations = yield* LocationServiceMap.Service
|
||||
const bus = yield* Bus.Service
|
||||
const db = (yield* Database.Service).db
|
||||
const reportLifecycle = <A>(sessionID: SessionSchema.ID, effect: Effect.Effect<A>) =>
|
||||
effect.pipe(
|
||||
Effect.tapCause((cause) =>
|
||||
@@ -54,7 +57,6 @@ export const layer = Layer.effect(
|
||||
Effect.annotateLogs({ sessionID }),
|
||||
),
|
||||
),
|
||||
Effect.asVoid,
|
||||
)
|
||||
// Write-ahead claim: starting records the durable intent that a turn is in flight, in the same
|
||||
// transaction as the started event. Terminals release it — except shutdown interruption, which
|
||||
@@ -72,7 +74,7 @@ export const layer = Layer.effect(
|
||||
reportLifecycle(
|
||||
sessionID,
|
||||
bus.publish(SessionEvent.Execution.Started, { sessionID }, claimOnCommit(sessionID)),
|
||||
),
|
||||
).pipe(Effect.asVoid),
|
||||
drain: Effect.fnUntraced(function* (sessionID: SessionSchema.ID, force) {
|
||||
const session = yield* store.get(sessionID)
|
||||
if (!session) return yield* Effect.die(new Error(`Session not found: ${sessionID}`))
|
||||
@@ -91,11 +93,9 @@ export const layer = Layer.effect(
|
||||
sessionID,
|
||||
Effect.gen(function* () {
|
||||
const outcome = terminal(exit, reason)
|
||||
if (outcome.type === "succeeded") {
|
||||
if (outcome.type === "succeeded")
|
||||
yield* bus.publish(SessionEvent.Execution.Succeeded, { sessionID }, releaseOnCommit(sessionID))
|
||||
return
|
||||
}
|
||||
if (outcome.type === "interrupted") {
|
||||
if (outcome.type === "interrupted")
|
||||
// A user cancel (or a superseding execution) releases the claim: the turn must not
|
||||
// resurrect at the next boot. Shutdown interruption keeps it for restart continuity.
|
||||
yield* bus.publish(
|
||||
@@ -103,16 +103,27 @@ export const layer = Layer.effect(
|
||||
{ sessionID, reason: outcome.reason },
|
||||
outcome.reason === "shutdown" ? undefined : releaseOnCommit(sessionID),
|
||||
)
|
||||
return
|
||||
}
|
||||
if (outcome.type === "failed")
|
||||
yield* bus.publish(
|
||||
SessionEvent.Execution.Failed,
|
||||
{
|
||||
sessionID,
|
||||
error: outcome.error,
|
||||
},
|
||||
releaseOnCommit(sessionID),
|
||||
)
|
||||
|
||||
if (outcome.type === "interrupted" && outcome.reason === "shutdown") return false
|
||||
const pending = yield* SessionPending.move(db, sessionID)
|
||||
if (!pending) return false
|
||||
const session = yield* store.get(sessionID)
|
||||
if (!session) return yield* Effect.die(new Error(`Session not found: ${sessionID}`))
|
||||
yield* bus.publish(
|
||||
SessionEvent.Execution.Failed,
|
||||
{
|
||||
sessionID,
|
||||
error: outcome.error,
|
||||
},
|
||||
releaseOnCommit(sessionID),
|
||||
SessionEvent.Moved,
|
||||
{ sessionID, moveID: pending.id, ...pending.data },
|
||||
{ location: session.location },
|
||||
)
|
||||
return yield* SessionPending.has(db, sessionID, "any")
|
||||
}),
|
||||
),
|
||||
})
|
||||
@@ -130,7 +141,7 @@ export const layer = Layer.effect(
|
||||
export const node = makeGlobalNode({
|
||||
service: Service,
|
||||
layer,
|
||||
deps: [SessionStore.node, LocationServiceMap.node, Bus.node],
|
||||
deps: [SessionStore.node, LocationServiceMap.node, Bus.node, Database.node],
|
||||
})
|
||||
|
||||
/** Low-level compatibility layer for callers that only need durable Session recording. */
|
||||
|
||||
@@ -7,6 +7,8 @@ import { SessionEvent } from "../event.js"
|
||||
import { SessionExecution } from "../execution.js"
|
||||
import { SessionSchema } from "../schema.js"
|
||||
import { SessionStore } from "../store.js"
|
||||
import { Database } from "../../database/database.js"
|
||||
import { SessionPending } from "../pending.js"
|
||||
|
||||
const CONTINUE_AFTER_SERVER_RESTART =
|
||||
"The server restarted while you were working. Continue from where you left off without repeating completed work."
|
||||
@@ -62,6 +64,7 @@ export const layer = (options?: Options) =>
|
||||
const store = yield* SessionStore.Service
|
||||
const execution = yield* SessionExecution.Service
|
||||
const bus = yield* Bus.Service
|
||||
const db = (yield* Database.Service).db
|
||||
const scope = yield* Effect.scope
|
||||
const maxAttempts = options?.maxAttempts ?? DEFAULT_MAX_ATTEMPTS
|
||||
|
||||
@@ -103,6 +106,14 @@ export const layer = (options?: Options) =>
|
||||
// them would only inject a stray continuation into a live turn.
|
||||
const orphaned = (yield* store.listSuspended()).filter((sessionID) => !active.has(sessionID))
|
||||
yield* Effect.forEach(orphaned, resumeOne, { concurrency: "unbounded", discard: true })
|
||||
const claimed = new Set(orphaned)
|
||||
yield* Effect.forEach(
|
||||
(yield* SessionPending.moveSessions(db)).filter(
|
||||
(sessionID) => !active.has(sessionID) && !claimed.has(sessionID),
|
||||
),
|
||||
execution.wake,
|
||||
{ concurrency: "unbounded", discard: true },
|
||||
)
|
||||
}),
|
||||
})
|
||||
}),
|
||||
@@ -111,5 +122,5 @@ export const layer = (options?: Options) =>
|
||||
export const node = makeGlobalNode({
|
||||
service: Service,
|
||||
layer: layer(),
|
||||
deps: [SessionStore.node, SessionExecution.node, Bus.node],
|
||||
deps: [SessionStore.node, SessionExecution.node, Bus.node, Database.node],
|
||||
})
|
||||
|
||||
@@ -106,6 +106,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
|
||||
)
|
||||
})
|
||||
},
|
||||
"session.move.admitted": () => Effect.void,
|
||||
"session.renamed": () => Effect.void,
|
||||
"session.deleted": () => Effect.void,
|
||||
"session.forked": () => Effect.void,
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
Delivery,
|
||||
Info,
|
||||
Message,
|
||||
Move,
|
||||
MoveData,
|
||||
Synthetic,
|
||||
SyntheticData,
|
||||
User,
|
||||
@@ -19,10 +21,11 @@ import { SessionEvent } from "./event.js"
|
||||
import { SessionMessage } from "./message.js"
|
||||
import { SessionSchema } from "./schema.js"
|
||||
import { SessionMessageTable, SessionPendingTable } from "./sql.js"
|
||||
import { Event } from "@opencode-ai/schema/event"
|
||||
|
||||
type DatabaseService = Database.Interface["db"]
|
||||
|
||||
export { Compaction, Delivery, Info, Message, Synthetic, SyntheticData, User, UserData }
|
||||
export { Compaction, Delivery, Info, Message, Move, MoveData, Synthetic, SyntheticData, User, UserData }
|
||||
|
||||
/**
|
||||
* Which pending input `promote` may consume: "steer" promotes steers only (a step
|
||||
@@ -35,6 +38,8 @@ const decodeUser = Schema.decodeUnknownSync(UserData)
|
||||
const encodeUser = Schema.encodeSync(UserData)
|
||||
const decodeSynthetic = Schema.decodeUnknownSync(SyntheticData)
|
||||
const encodeSynthetic = Schema.encodeSync(SyntheticData)
|
||||
const decodeMove = Schema.decodeUnknownSync(MoveData)
|
||||
const encodeMove = Schema.encodeSync(MoveData)
|
||||
const decodeMessage = Schema.decodeUnknownSync(SessionMessage.Info)
|
||||
const inboxLocks = KeyedMutex.makeUnsafe<SessionSchema.ID>()
|
||||
type PendingRef = { readonly id: SessionMessage.ID; readonly sessionID: SessionSchema.ID }
|
||||
@@ -42,21 +47,24 @@ type PendingRef = { readonly id: SessionMessage.ID; readonly sessionID: SessionS
|
||||
export class LifecycleConflict extends Schema.TaggedErrorClass<LifecycleConflict>()(
|
||||
"SessionPending.LifecycleConflict",
|
||||
{
|
||||
id: SessionMessage.ID,
|
||||
id: Schema.Union([SessionMessage.ID, Event.ID]),
|
||||
},
|
||||
) {}
|
||||
|
||||
const fromRow = (row: typeof SessionPendingTable.$inferSelect): Info => {
|
||||
const base = {
|
||||
id: SessionMessage.ID.make(row.id),
|
||||
sessionID: SessionSchema.ID.make(row.session_id),
|
||||
timeCreated: DateTime.makeUnsafe(row.time_created),
|
||||
}
|
||||
if (row.type === "compaction") return Compaction.make({ ...base, type: "compaction" })
|
||||
if (!row.delivery) throw new LifecycleConflict({ id: base.id })
|
||||
if (row.type === "move")
|
||||
return Move.make({ ...base, id: Event.ID.make(row.id), type: "move", data: decodeMove(row.data) })
|
||||
const id = SessionMessage.ID.make(row.id)
|
||||
if (row.type === "compaction") return Compaction.make({ ...base, id, type: "compaction" })
|
||||
if (!row.delivery) throw new LifecycleConflict({ id })
|
||||
if (row.type === "user")
|
||||
return User.make({
|
||||
...base,
|
||||
id,
|
||||
type: "user",
|
||||
data: decodeUser(row.data),
|
||||
delivery: row.delivery,
|
||||
@@ -64,11 +72,12 @@ const fromRow = (row: typeof SessionPendingTable.$inferSelect): Info => {
|
||||
if (row.type === "synthetic")
|
||||
return Synthetic.make({
|
||||
...base,
|
||||
id,
|
||||
type: "synthetic",
|
||||
data: decodeSynthetic(row.data),
|
||||
delivery: row.delivery,
|
||||
})
|
||||
throw new LifecycleConflict({ id: base.id })
|
||||
throw new LifecycleConflict({ id })
|
||||
}
|
||||
|
||||
export const find = Effect.fn("SessionPending.find")(function* (db: DatabaseService, id: SessionMessage.ID) {
|
||||
@@ -98,6 +107,44 @@ export const compaction = Effect.fn("SessionPending.compaction")(function* (
|
||||
return entry.type === "compaction" ? entry : undefined
|
||||
})
|
||||
|
||||
export const move = Effect.fn("SessionPending.move")(function* (db: DatabaseService, sessionID: SessionSchema.ID) {
|
||||
const row = yield* db
|
||||
.select()
|
||||
.from(SessionPendingTable)
|
||||
.where(and(eq(SessionPendingTable.session_id, sessionID), eq(SessionPendingTable.type, "move")))
|
||||
.orderBy(asc(SessionPendingTable.admitted_seq))
|
||||
.limit(1)
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!row) return
|
||||
const entry = fromRow(row)
|
||||
return entry.type === "move" ? entry : undefined
|
||||
})
|
||||
|
||||
export const admitMove = Effect.fn("SessionPending.admitMove")(function* (
|
||||
db: DatabaseService,
|
||||
bus: Bus.Interface,
|
||||
input: { readonly sessionID: SessionSchema.ID; readonly data: MoveData; readonly source: MoveData["location"] },
|
||||
) {
|
||||
return yield* inboxLocks.withLock(input.sessionID)(
|
||||
Effect.gen(function* () {
|
||||
const pending = yield* move(db, input.sessionID)
|
||||
if (pending && JSON.stringify(encodeMove(pending.data)) === JSON.stringify(encodeMove(input.data))) return pending
|
||||
const event = yield* bus.publish(
|
||||
SessionEvent.MoveAdmitted,
|
||||
{
|
||||
sessionID: input.sessionID,
|
||||
move: input.data,
|
||||
},
|
||||
{ location: input.source },
|
||||
)
|
||||
const stored = yield* move(db, input.sessionID)
|
||||
if (stored) return stored
|
||||
return yield* Effect.die(new LifecycleConflict({ id: event.id }))
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
const promotedFromMessage = Effect.fn("SessionPending.promotedFromMessage")(function* (
|
||||
db: DatabaseService,
|
||||
sessionID: SessionSchema.ID,
|
||||
@@ -288,6 +335,35 @@ export const projectCompactionAdmitted = Effect.fn("SessionPending.projectCompac
|
||||
return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||
})
|
||||
|
||||
export const projectMoveAdmitted = Effect.fn("SessionPending.projectMoveAdmitted")(function* (
|
||||
db: DatabaseService,
|
||||
input: {
|
||||
readonly admittedSeq: number
|
||||
readonly id: Event.ID
|
||||
readonly sessionID: SessionSchema.ID
|
||||
readonly data: MoveData
|
||||
readonly timeCreated: DateTime.Utc
|
||||
},
|
||||
) {
|
||||
yield* db
|
||||
.delete(SessionPendingTable)
|
||||
.where(and(eq(SessionPendingTable.session_id, input.sessionID), eq(SessionPendingTable.type, "move")))
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
yield* db
|
||||
.insert(SessionPendingTable)
|
||||
.values({
|
||||
id: input.id,
|
||||
session_id: input.sessionID,
|
||||
type: "move",
|
||||
data: input.data,
|
||||
admitted_seq: input.admittedSeq,
|
||||
time_created: DateTime.toEpochMillis(input.timeCreated),
|
||||
})
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
})
|
||||
|
||||
/**
|
||||
* Consume one pending row at promotion. The row's content feeds the projected
|
||||
* message insert inside the same event transaction; the deleted row is what
|
||||
@@ -297,7 +373,8 @@ export const projectPromoted = Effect.fn("SessionPending.projectPromoted")(funct
|
||||
db: DatabaseService,
|
||||
input: PendingRef,
|
||||
) {
|
||||
if (yield* compaction(db, input.sessionID)) return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||
if ((yield* compaction(db, input.sessionID)) || (yield* move(db, input.sessionID)))
|
||||
return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||
const deleted = yield* db
|
||||
.delete(SessionPendingTable)
|
||||
.where(and(eq(SessionPendingTable.id, input.id), eq(SessionPendingTable.session_id, input.sessionID)))
|
||||
@@ -306,7 +383,8 @@ export const projectPromoted = Effect.fn("SessionPending.projectPromoted")(funct
|
||||
.pipe(Effect.orDie)
|
||||
if (!deleted) return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||
const stored = fromRow(deleted)
|
||||
if (stored.type === "compaction") return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||
if (stored.type === "compaction" || stored.type === "move")
|
||||
return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||
return stored
|
||||
})
|
||||
|
||||
@@ -374,6 +452,33 @@ export const settleCompaction = Effect.fn("SessionPending.settleCompaction")(fun
|
||||
return undefined
|
||||
})
|
||||
|
||||
export const settleMove = Effect.fn("SessionPending.settleMove")(function* (
|
||||
db: DatabaseService,
|
||||
input: { readonly sessionID: SessionSchema.ID; readonly id: Event.ID },
|
||||
) {
|
||||
yield* db
|
||||
.delete(SessionPendingTable)
|
||||
.where(
|
||||
and(
|
||||
eq(SessionPendingTable.id, input.id),
|
||||
eq(SessionPendingTable.session_id, input.sessionID),
|
||||
eq(SessionPendingTable.type, "move"),
|
||||
),
|
||||
)
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
})
|
||||
|
||||
export const moveSessions = Effect.fn("SessionPending.moveSessions")(function* (db: DatabaseService) {
|
||||
const rows = yield* db
|
||||
.select({ sessionID: SessionPendingTable.session_id })
|
||||
.from(SessionPendingTable)
|
||||
.where(eq(SessionPendingTable.type, "move"))
|
||||
.all()
|
||||
.pipe(Effect.orDie)
|
||||
return [...new Set(rows.map((row) => row.sessionID))]
|
||||
})
|
||||
|
||||
export const list = Effect.fn("SessionPending.list")(function* (db: DatabaseService, sessionID: SessionSchema.ID) {
|
||||
const rows = yield* db
|
||||
.select()
|
||||
@@ -397,7 +502,7 @@ export const has = Effect.fn("SessionPending.has")(function* (
|
||||
sessionID: SessionSchema.ID,
|
||||
scope: Scope,
|
||||
) {
|
||||
if (scope !== "any" && (yield* compaction(db, sessionID))) return false
|
||||
if (scope !== "any" && ((yield* compaction(db, sessionID)) || (yield* move(db, sessionID)))) return false
|
||||
const row = yield* db
|
||||
.select({ id: SessionPendingTable.id })
|
||||
.from(SessionPendingTable)
|
||||
@@ -473,12 +578,13 @@ const publish = Effect.fn("SessionPending.publish")(function* (
|
||||
sessionID: SessionSchema.ID,
|
||||
rows: ReadonlyArray<typeof SessionPendingTable.$inferSelect>,
|
||||
) {
|
||||
if (yield* compaction(db, sessionID)) return 0
|
||||
if ((yield* compaction(db, sessionID)) || (yield* move(db, sessionID))) return 0
|
||||
yield* Effect.forEach(
|
||||
rows,
|
||||
(row) => {
|
||||
const entry = fromRow(row)
|
||||
if (entry.type === "compaction") return Effect.die(new LifecycleConflict({ id: entry.id }))
|
||||
if (entry.type === "compaction" || entry.type === "move")
|
||||
return Effect.die(new LifecycleConflict({ id: entry.id }))
|
||||
return bus
|
||||
.publish(SessionEvent.InputPromoted, {
|
||||
sessionID,
|
||||
@@ -512,7 +618,7 @@ export const promote = Effect.fn("SessionPending.promote")(function* (
|
||||
) {
|
||||
return yield* inboxLocks.withLock(sessionID)(
|
||||
Effect.gen(function* () {
|
||||
if (yield* compaction(db, sessionID)) return 0
|
||||
if ((yield* compaction(db, sessionID)) || (yield* move(db, sessionID))) return 0
|
||||
const steers = yield* db
|
||||
.select()
|
||||
.from(SessionPendingTable)
|
||||
|
||||
@@ -433,6 +433,8 @@ const layer = Layer.effectDiscard(
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
yield* InstructionState.reset(db, event.data.sessionID)
|
||||
if (event.data.moveID)
|
||||
yield* SessionPending.settleMove(db, { sessionID: event.data.sessionID, id: event.data.moveID })
|
||||
}),
|
||||
)
|
||||
yield* bus.project(SessionEvent.Deleted, (event) =>
|
||||
@@ -522,6 +524,19 @@ const layer = Layer.effectDiscard(
|
||||
.pipe(Effect.orDie)
|
||||
}),
|
||||
)
|
||||
yield* bus.project(SessionEvent.MoveAdmitted, (event) =>
|
||||
Effect.gen(function* () {
|
||||
if (event.durable === undefined)
|
||||
return yield* Effect.die(new Error("Durable Session event is missing aggregate sequence"))
|
||||
yield* SessionPending.projectMoveAdmitted(db, {
|
||||
admittedSeq: event.durable.seq,
|
||||
id: event.id,
|
||||
sessionID: event.data.sessionID,
|
||||
data: event.data.move,
|
||||
timeCreated: event.created,
|
||||
})
|
||||
}),
|
||||
)
|
||||
yield* bus.project(SessionEvent.InputCancelled, (event) =>
|
||||
SessionPending.projectCancelled(db, {
|
||||
id: event.data.inputID,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as SessionRunCoordinator from "./run-coordinator.js"
|
||||
|
||||
import { Deferred, Effect, Exit, Fiber, FiberSet, Scope } from "effect"
|
||||
import { Cause, Deferred, Effect, Exit, Fiber, FiberSet, Scope } from "effect"
|
||||
|
||||
/** Serializes execution for each key while allowing different keys to run concurrently. */
|
||||
export interface Coordinator<Key, E, Reason = never> {
|
||||
@@ -50,11 +50,17 @@ export const make = <Key, E, Reason = never>(options: {
|
||||
* Runs in the execution fiber for every exit, including interruption, after the final
|
||||
* drain and before the execution settles (waiters resolve after it completes).
|
||||
*/
|
||||
readonly settled?: (key: Key, exit: Exit.Exit<void, E>, reason?: Reason) => Effect.Effect<void>
|
||||
readonly settled?: (key: Key, exit: Exit.Exit<void, E>, reason?: Reason) => Effect.Effect<boolean | void>
|
||||
}): Effect.Effect<Coordinator<Key, E, Reason>, never, Scope.Scope> =>
|
||||
Effect.gen(function* () {
|
||||
const executions = new Map<Key, Execution<E, Reason>>()
|
||||
const fork = yield* FiberSet.makeRuntime<never, void, never>()
|
||||
const closing = { value: false }
|
||||
yield* Effect.addFinalizer(() =>
|
||||
Effect.sync(() => {
|
||||
closing.value = true
|
||||
}),
|
||||
)
|
||||
|
||||
const loop = (key: Key, execution: Execution<E, Reason>, force: boolean): Effect.Effect<void, E> =>
|
||||
Effect.suspend(() => options.drain(key, force)).pipe(
|
||||
@@ -85,7 +91,22 @@ export const make = <Key, E, Reason = never>(options: {
|
||||
Effect.onExit((exit) =>
|
||||
Effect.sync(() => {
|
||||
execution.owner = undefined
|
||||
}).pipe(Effect.andThen(options.settled?.(key, exit, execution.interruptionReason) ?? Effect.void)),
|
||||
if (closing.value && Exit.isFailure(exit) && Cause.hasInterrupts(exit.cause)) {
|
||||
execution.stopping = true
|
||||
execution.pendingWake = false
|
||||
}
|
||||
}).pipe(
|
||||
Effect.andThen(options.settled?.(key, exit, execution.interruptionReason) ?? Effect.void),
|
||||
Effect.map(Boolean),
|
||||
Effect.tap((restart) =>
|
||||
restart && !execution.stopping
|
||||
? Effect.sync(() => {
|
||||
execution.pendingWake = true
|
||||
})
|
||||
: Effect.void,
|
||||
),
|
||||
Effect.asVoid,
|
||||
),
|
||||
),
|
||||
Effect.onExit((exit) => Effect.sync(() => settle(key, execution, exit))),
|
||||
Effect.exit,
|
||||
|
||||
@@ -143,6 +143,7 @@ const layer = Layer.effect(
|
||||
let promotable: SessionPending.Promotable = "input"
|
||||
let step = 1
|
||||
while (true) {
|
||||
if (yield* SessionPending.move(db, sessionID)) return
|
||||
const result = yield* runStep(sessionID, promotable, step)
|
||||
if (step === 1) yield* startTitle(sessionID)
|
||||
yield* runPendingCompaction(sessionID)
|
||||
@@ -236,6 +237,8 @@ const layer = Layer.effect(
|
||||
// a blocked first step leaves pending inputs untouched.
|
||||
yield* InstructionState.prepare(db, bus, selected.instructions, selected.session.id)
|
||||
const promoted = promotable ? yield* SessionPending.promote(db, bus, selected.session.id, promotable) : 0
|
||||
if (promotable && promoted === 0 && (yield* SessionPending.move(db, sessionID)))
|
||||
return CallOutcome.Completed({ needsContinuation: false, step })
|
||||
// Promoted input opens a fresh step allowance.
|
||||
const currentStep = promoted > 0 ? 1 : step
|
||||
const loaded = yield* context.load(selected)
|
||||
@@ -482,6 +485,7 @@ const layer = Layer.effect(
|
||||
const runPendingCompaction = Effect.fn("SessionRunner.runPendingCompaction")(function* (
|
||||
sessionID: SessionSchema.ID,
|
||||
) {
|
||||
if (yield* SessionPending.move(db, sessionID)) return
|
||||
const pending = yield* SessionPending.compaction(db, sessionID)
|
||||
if (!pending) return
|
||||
const session = yield* getSession(sessionID)
|
||||
|
||||
@@ -96,13 +96,15 @@ export const SessionMessageTable = sqliteTable(
|
||||
export const SessionPendingTable = sqliteTable(
|
||||
"session_pending",
|
||||
{
|
||||
id: text().$type<SessionMessage.ID>().primaryKey(),
|
||||
id: text().$type<SessionPending.Info["id"]>().primaryKey(),
|
||||
session_id: text()
|
||||
.$type<SessionSchema.ID>()
|
||||
.notNull()
|
||||
.references(() => SessionTable.id, { onDelete: "cascade" }),
|
||||
type: text().$type<SessionPending.Info["type"]>().notNull(),
|
||||
data: text({ mode: "json" }).$type<UserData | SyntheticData | Record<string, never>>().notNull(),
|
||||
data: text({ mode: "json" })
|
||||
.$type<UserData | SyntheticData | SessionPending.MoveData | Record<string, never>>()
|
||||
.notNull(),
|
||||
delivery: text().$type<SessionPending.Delivery>(),
|
||||
admitted_seq: integer().notNull(),
|
||||
time_created: integer()
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
// workerd has no filesystem paths to tree-sitter wasm artifacts. ShellParse
|
||||
// loads these lazily and degrades when initialization fails, so empty paths
|
||||
// keep module load side-effect free instead of resolving from disk.
|
||||
export const shellParserWasm = { runtime: "", bash: "", powershell: "" }
|
||||
@@ -1,14 +0,0 @@
|
||||
export type LockResult =
|
||||
| { readonly acquired: true }
|
||||
| { readonly acquired: false; readonly held: true }
|
||||
| { readonly acquired: false; readonly held: false; readonly code: number }
|
||||
|
||||
// workerd has no FFI and no cross-process file locking; a Durable Object is
|
||||
// already single-threaded per instance, so nothing on this runtime should
|
||||
// reach these.
|
||||
const unavailable = (_fd: number): LockResult => {
|
||||
throw new Error("Process locks are unavailable on the workerd runtime")
|
||||
}
|
||||
|
||||
export const lockDarwin = unavailable
|
||||
export const lockLinux = unavailable
|
||||
@@ -5,7 +5,7 @@ import { Deferred, Effect, Fiber, Layer } from "effect"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
|
||||
import { FileMutation } from "@opencode-ai/core/file-mutation"
|
||||
import { Environment } from "@opencode-ai/core/environment/index"
|
||||
import { Environment } from "@opencode-ai/core/environment"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { LocationMutation } from "@opencode-ai/core/location-mutation"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import { Database } from "bun:sqlite"
|
||||
import type { DurableObjectStorage } from "@opencode-ai/core/database/sqlite.workerd"
|
||||
|
||||
// Emulates the Durable Object storage API over bun:sqlite so the workerd
|
||||
// adapter and the workerd server profile can be verified without workerd or
|
||||
// Cloudflare runtime dependencies. The real runtime is covered by the
|
||||
// workerd-spike package, which boots inside an actual isolate.
|
||||
export const makeDurableObjectStorage = (): DurableObjectStorage => {
|
||||
const native = new Database(":memory:")
|
||||
const toSqlStorageValue = (value: unknown) => {
|
||||
if (!(value instanceof Uint8Array)) return value as ArrayBuffer | string | number | null
|
||||
const buffer = new ArrayBuffer(value.byteLength)
|
||||
new Uint8Array(buffer).set(value)
|
||||
return buffer
|
||||
}
|
||||
return {
|
||||
sql: {
|
||||
exec(query: string, ...bindings: Array<unknown>) {
|
||||
const statement = native.query(query)
|
||||
const rows = (statement.values(...(bindings as never[])) ?? []).map((row) => row.map(toSqlStorageValue))
|
||||
const columnNames = statement.columnNames
|
||||
return {
|
||||
columnNames,
|
||||
raw: () => rows[Symbol.iterator](),
|
||||
toArray: () => rows.map((row) => Object.fromEntries(columnNames.map((name, i) => [name, row[i]]))),
|
||||
}
|
||||
},
|
||||
},
|
||||
transaction<T>(closure: (txn: { rollback(): void }) => Promise<T>): Promise<T> {
|
||||
native.run("BEGIN")
|
||||
let rolledBack = false
|
||||
return closure({ rollback: () => (rolledBack = true) }).then(
|
||||
(result) => {
|
||||
native.run(rolledBack ? "ROLLBACK" : "COMMIT")
|
||||
return result
|
||||
},
|
||||
(error) => {
|
||||
native.run("ROLLBACK")
|
||||
throw error
|
||||
},
|
||||
)
|
||||
},
|
||||
transactionSync<T>(closure: () => T): T {
|
||||
return native.transaction(closure)()
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { Environment } from "@opencode-ai/core/environment/index"
|
||||
import { Environment } from "@opencode-ai/core/environment"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { Effect, Layer } from "effect"
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Bus } from "@opencode-ai/core/bus"
|
||||
import { ConfigInstructionPlugin } from "@opencode-ai/core/config/plugin/instruction"
|
||||
import { Watcher } from "@opencode-ai/core/filesystem/watcher"
|
||||
import { InstructionDiscovery } from "@opencode-ai/core/instruction-discovery"
|
||||
import { Instructions } from "@opencode-ai/core/instructions/index"
|
||||
import { Instructions } from "@opencode-ai/core/instructions"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { FSUtil } from "@opencode-ai/util/fs-util"
|
||||
|
||||
@@ -7,7 +7,7 @@ import { LayerNode } from "@opencode-ai/util/effect/layer-node"
|
||||
import { Bus } from "@opencode-ai/core/bus"
|
||||
import { Event } from "@opencode-ai/schema/event"
|
||||
import { EventTable } from "@opencode-ai/core/event/sql"
|
||||
import { Instructions } from "@opencode-ai/core/instructions/index"
|
||||
import { Instructions } from "@opencode-ai/core/instructions"
|
||||
import { Project } from "@opencode-ai/core/project"
|
||||
import { ProjectTable } from "@opencode-ai/core/project/sql"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Cause, Effect, Exit, Option, Schema } from "effect"
|
||||
import { Instructions } from "@opencode-ai/core/instructions/index"
|
||||
import { Instructions } from "@opencode-ai/core/instructions"
|
||||
import { it } from "../lib/effect"
|
||||
|
||||
const key = (value: string) => Instructions.Key.make(value)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { Failed, NotFound, WrongKind, type Files } from "@opencode-ai/core/environment/index"
|
||||
import { Failed, NotFound, WrongKind, type Files } from "../../src/environment/index"
|
||||
import { it } from "./effect"
|
||||
|
||||
export interface EnvironmentHarness {
|
||||
readonly files: Files
|
||||
@@ -15,21 +16,17 @@ export const environmentConformance = <E>(
|
||||
skip = false,
|
||||
) => {
|
||||
const check = <A, E2>(title: string, body: (harness: EnvironmentHarness) => Effect.Effect<A, E2>) =>
|
||||
test(title, () =>
|
||||
Effect.runPromise(
|
||||
Effect.scoped(
|
||||
it.live(title, () =>
|
||||
Effect.gen(function* () {
|
||||
const harness = yield* Effect.acquireRelease(makeHarness(), (harness) =>
|
||||
Effect.gen(function* () {
|
||||
const harness = yield* Effect.acquireRelease(makeHarness(), (harness) =>
|
||||
Effect.gen(function* () {
|
||||
yield* Effect.ignore(harness.files.remove(harness.root))
|
||||
if (harness.dispose) yield* harness.dispose
|
||||
}),
|
||||
)
|
||||
yield* harness.files.mkdir(harness.root)
|
||||
return yield* body(harness)
|
||||
yield* Effect.ignore(harness.files.remove(harness.root))
|
||||
if (harness.dispose) yield* harness.dispose
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
yield* harness.files.mkdir(harness.root)
|
||||
return yield* body(harness)
|
||||
}),
|
||||
)
|
||||
|
||||
const bytes = (value: string) => new TextEncoder().encode(value)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect, Option, Schema } from "effect"
|
||||
import { Instructions } from "@opencode-ai/core/instructions/index"
|
||||
import { Instructions } from "@opencode-ai/core/instructions"
|
||||
|
||||
export interface State {
|
||||
readonly values: Readonly<Record<string, Schema.Json>>
|
||||
|
||||
@@ -248,12 +248,6 @@ const mcp = Layer.mock(MCP.Service, {
|
||||
required: ["ok"],
|
||||
},
|
||||
}),
|
||||
new MCP.Tool({
|
||||
server: MCP.ServerName.make("demo"),
|
||||
name: "status",
|
||||
description: "Status",
|
||||
inputSchema: { type: "object", properties: {} },
|
||||
}),
|
||||
new MCP.Tool({
|
||||
server: MCP.ServerName.make("direct"),
|
||||
name: "lookup",
|
||||
@@ -296,13 +290,6 @@ const mcp = Layer.mock(MCP.Service, {
|
||||
{ type: "media", data: "aGVsbG8=", mimeType: "image/png" },
|
||||
],
|
||||
})
|
||||
if (input.name === "status")
|
||||
return new MCP.ToolResult({
|
||||
server: MCP.ServerName.make(input.server),
|
||||
tool: input.name,
|
||||
isError: false,
|
||||
content: [{ type: "text", text: "hello" }],
|
||||
})
|
||||
return new MCP.ToolResult({
|
||||
server: MCP.ServerName.make(input.server),
|
||||
tool: input.name,
|
||||
@@ -997,31 +984,6 @@ it.effect("advertises MCP output schemas to Code Mode", () =>
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("returns content-only MCP results through Code Mode", () =>
|
||||
Effect.gen(function* () {
|
||||
assertion = yield* Deferred.make<Permission.AssertInput>()
|
||||
decision = Effect.void
|
||||
const registry = yield* Tool.Service
|
||||
const toolSet = yield* waitForCodeModeTool(registry, "demo.status")
|
||||
|
||||
const execution = yield* toolSet.execute({
|
||||
sessionID: Session.ID.make("ses_mcp_content_only"),
|
||||
...toolIdentity,
|
||||
call: {
|
||||
type: "tool-call",
|
||||
id: "call_mcp_content_only",
|
||||
name: "execute",
|
||||
input: { code: "return await tools.demo.status({})" },
|
||||
},
|
||||
})
|
||||
|
||||
expect(execution).toMatchObject({
|
||||
output: { output: "hello", toolCalls: [{ tool: "demo.status", status: "completed" }] },
|
||||
content: [{ type: "text", text: "hello" }],
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("advertises MCP tools directly when Code Mode is disabled for the server", () =>
|
||||
Effect.gen(function* () {
|
||||
const registry = yield* Tool.Service
|
||||
|
||||
@@ -393,45 +393,4 @@ describe("fromPromise", () => {
|
||||
expect(progress).toEqual([{ phase: "greeting" }])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("returns content-only plugin results through Code Mode", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugins = yield* Plugin.Service
|
||||
const registry = yield* Tool.Service
|
||||
const host = yield* PluginHost.make(plugins)
|
||||
const promisePlugin = define({
|
||||
id: "content-only-tool",
|
||||
setup: async (ctx) => {
|
||||
await ctx.tool.transform((tools) => {
|
||||
tools.add({
|
||||
name: "demo_status",
|
||||
description: "Returns a status string",
|
||||
input: Schema.Struct({}),
|
||||
execute: async () => ({ content: [{ type: "text", text: "hello" }] }),
|
||||
options: { codemode: true },
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
yield* PluginPromise.fromPromise(promisePlugin).effect(host)
|
||||
|
||||
const toolSet = yield* registry.snapshot()
|
||||
const throughCodeMode = yield* toolSet.execute({
|
||||
sessionID: Session.ID.make("ses_content_only_tool"),
|
||||
agent: Agent.ID.make("build"),
|
||||
messageID: SessionMessage.ID.make("msg_content_only_tool"),
|
||||
call: {
|
||||
type: "tool-call",
|
||||
id: "call_content_only_tool",
|
||||
name: "execute",
|
||||
input: { code: "return await tools.demo_status({})" },
|
||||
},
|
||||
})
|
||||
expect(throughCodeMode).toMatchObject({
|
||||
output: { output: "hello", toolCalls: [{ tool: "demo_status", status: "completed" }] },
|
||||
content: [{ type: "text", text: "hello" }],
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -90,36 +90,6 @@ describe("SystemPromptPlugin", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("selects the Meta prompt for Muse family model IDs", () =>
|
||||
Effect.gen(function* () {
|
||||
const hooks = yield* PluginHooks.Service
|
||||
const pluginHost = yield* makeHost
|
||||
yield* SystemPromptPlugin.MetaPlugin.effect(pluginHost)
|
||||
|
||||
yield* Effect.forEach(
|
||||
[
|
||||
["meta/muse-spark-preview", "Muse Spark"],
|
||||
["muse-spark-1.2", "Muse Spark"],
|
||||
["meta/muse-glimmer-30b", "Muse Glimmer"],
|
||||
["muse-glimmer-30b", "Muse Glimmer"],
|
||||
] as const,
|
||||
([id, name]) => {
|
||||
const event = context(id)
|
||||
return hooks.trigger("session", "context", event).pipe(
|
||||
Effect.tap(() =>
|
||||
Effect.sync(() => {
|
||||
expect(event.system[0]?.text).toContain(`powered by ${name},`)
|
||||
expect(event.system[0]?.text).toContain(`using Meta ${name}.`)
|
||||
expect(event.system[0]?.text).not.toContain("{{MODEL_NAME}}")
|
||||
}),
|
||||
),
|
||||
)
|
||||
},
|
||||
{ discard: true },
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("preserves an explicit agent system prompt", () =>
|
||||
Effect.gen(function* () {
|
||||
const agents = yield* Agent.Service
|
||||
|
||||
@@ -8,20 +8,25 @@ import { LocationServiceMap } from "@opencode-ai/core/location-service-map"
|
||||
import type { LocationServices } from "@opencode-ai/core/location-services"
|
||||
import { Project } from "@opencode-ai/core/project"
|
||||
import { ProjectTable } from "@opencode-ai/core/project/sql"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { AbsolutePath, RelativePath } from "@opencode-ai/core/schema"
|
||||
import { Session } from "@opencode-ai/core/session"
|
||||
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
||||
import { SessionRestart } from "@opencode-ai/core/session/execution/restart"
|
||||
import { UserInterruptedError } from "@opencode-ai/core/session/error"
|
||||
import { SessionEvent } from "@opencode-ai/core/session/event"
|
||||
import { SessionRunner } from "@opencode-ai/core/session/runner/index"
|
||||
import { SessionRunner } from "@opencode-ai/core/session/runner"
|
||||
import { SessionTable } from "@opencode-ai/core/session/sql"
|
||||
import { SessionStore } from "@opencode-ai/core/session/store"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { SessionPending } from "@opencode-ai/core/session/pending"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { Context, Deferred, Effect, Exit, Fiber, Layer, LayerMap, Scope } from "effect"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { testEffect } from "./lib/effect"
|
||||
|
||||
const it = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, Bus.node, SessionStore.node])))
|
||||
const it = testEffect(
|
||||
AppNodeBuilder.build(LayerNode.group([Database.node, Bus.node, SessionProjector.node, SessionStore.node])),
|
||||
)
|
||||
|
||||
describe("SessionExecution lifecycle", () => {
|
||||
test("classifies success and typed failure terminals", () => {
|
||||
@@ -133,6 +138,104 @@ describe("SessionExecution lifecycle", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("applies a deferred move only after the active execution settles", () =>
|
||||
Effect.gen(function* () {
|
||||
const database = yield* Database.Service
|
||||
const bus = yield* Bus.Service
|
||||
const store = yield* SessionStore.Service
|
||||
const sessionID = Session.ID.make("ses_deferred_move")
|
||||
yield* seedSessions(database, [sessionID])
|
||||
|
||||
const draining = yield* Deferred.make<void>()
|
||||
const release = yield* Deferred.make<void>()
|
||||
const scope = yield* Scope.make()
|
||||
yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void))
|
||||
const context = yield* buildExecution(scope, () =>
|
||||
Deferred.succeed(draining, undefined).pipe(Effect.andThen(Deferred.await(release))),
|
||||
)
|
||||
const execution = Context.get(context, SessionExecution.Service)
|
||||
yield* execution.resume(sessionID).pipe(Effect.forkIn(scope))
|
||||
yield* Deferred.await(draining)
|
||||
|
||||
yield* bus.publish(SessionEvent.MoveAdmitted, {
|
||||
sessionID,
|
||||
move: {
|
||||
location: Location.Ref.make({ directory: AbsolutePath.make("/destination") }),
|
||||
projectID: Project.ID.global,
|
||||
subpath: RelativePath.make(""),
|
||||
},
|
||||
})
|
||||
expect((yield* store.get(sessionID))?.location.directory).toBe(AbsolutePath.make("/project"))
|
||||
expect((yield* SessionPending.move(database.db, sessionID))?.data.location.directory).toBe(
|
||||
AbsolutePath.make("/destination"),
|
||||
)
|
||||
|
||||
yield* Deferred.succeed(release, undefined)
|
||||
yield* execution.awaitIdle(sessionID)
|
||||
|
||||
expect((yield* store.get(sessionID))?.location.directory).toBe(AbsolutePath.make("/destination"))
|
||||
expect(yield* SessionPending.move(database.db, sessionID)).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("settling one move preserves a newer admitted destination", () =>
|
||||
Effect.gen(function* () {
|
||||
const database = yield* Database.Service
|
||||
const bus = yield* Bus.Service
|
||||
const store = yield* SessionStore.Service
|
||||
const sessionID = Session.ID.make("ses_move_replacement")
|
||||
yield* seedSessions(database, [sessionID])
|
||||
const first = {
|
||||
location: Location.Ref.make({ directory: AbsolutePath.make("/first") }),
|
||||
projectID: Project.ID.global,
|
||||
subpath: RelativePath.make("first"),
|
||||
}
|
||||
const second = {
|
||||
location: Location.Ref.make({ directory: AbsolutePath.make("/second") }),
|
||||
projectID: Project.ID.global,
|
||||
subpath: RelativePath.make("second"),
|
||||
}
|
||||
|
||||
const admittedFirst = yield* bus.publish(SessionEvent.MoveAdmitted, { sessionID, move: first })
|
||||
const admittedSecond = yield* bus.publish(SessionEvent.MoveAdmitted, { sessionID, move: second })
|
||||
yield* bus.publish(SessionEvent.Moved, { sessionID, moveID: admittedFirst.id, ...first })
|
||||
|
||||
expect((yield* store.get(sessionID))?.location.directory).toBe(first.location.directory)
|
||||
expect((yield* SessionPending.move(database.db, sessionID))?.id).toBe(admittedSecond.id)
|
||||
|
||||
yield* bus.publish(SessionEvent.Moved, { sessionID, moveID: admittedSecond.id, ...second })
|
||||
expect((yield* store.get(sessionID))?.location.directory).toBe(second.location.directory)
|
||||
expect(yield* SessionPending.move(database.db, sessionID)).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("recovers an unclaimed deferred move on startup", () =>
|
||||
Effect.gen(function* () {
|
||||
const database = yield* Database.Service
|
||||
const bus = yield* Bus.Service
|
||||
const store = yield* SessionStore.Service
|
||||
const sessionID = Session.ID.make("ses_move_recovery")
|
||||
yield* seedSessions(database, [sessionID])
|
||||
yield* bus.publish(SessionEvent.MoveAdmitted, {
|
||||
sessionID,
|
||||
move: {
|
||||
location: Location.Ref.make({ directory: AbsolutePath.make("/recovered") }),
|
||||
projectID: Project.ID.global,
|
||||
},
|
||||
})
|
||||
|
||||
const scope = yield* Scope.make()
|
||||
yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void))
|
||||
const context = yield* buildExecution(scope, () => Effect.void)
|
||||
const execution = Context.get(context, SessionExecution.Service)
|
||||
yield* Context.get(context, SessionRestart.Service).resumeSuspendedSessions
|
||||
yield* execution.awaitIdle(sessionID)
|
||||
|
||||
expect((yield* store.get(sessionID))?.location.directory).toBe(AbsolutePath.make("/recovered"))
|
||||
expect(yield* SessionPending.move(database.db, sessionID)).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("starts every claimed execution without waiting for earlier drains to finish", () =>
|
||||
Effect.gen(function* () {
|
||||
const database = yield* Database.Service
|
||||
|
||||
@@ -17,7 +17,7 @@ import { LayerNode } from "@opencode-ai/util/effect/layer-node"
|
||||
import { Bus } from "@opencode-ai/core/bus"
|
||||
import { EventTable } from "@opencode-ai/core/event/sql"
|
||||
import { InstructionDiscovery } from "@opencode-ai/core/instruction-discovery"
|
||||
import { Instructions } from "@opencode-ai/core/instructions/index"
|
||||
import { Instructions } from "@opencode-ai/core/instructions"
|
||||
import { InstructionBuiltIns } from "@opencode-ai/core/instructions/builtins"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { McpInstructions } from "@opencode-ai/core/mcp/instructions"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { Bus } from "@opencode-ai/core/bus"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
@@ -34,7 +35,7 @@ const it = testEffect(
|
||||
)
|
||||
|
||||
describe("Session.move", () => {
|
||||
it.effect("moves a session whose source directory no longer exists", () =>
|
||||
it.effect("durably admits a move when the source directory no longer exists", () =>
|
||||
Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
@@ -49,24 +50,26 @@ describe("Session.move", () => {
|
||||
|
||||
yield* session.move({ sessionID: created.id, directory: destination })
|
||||
|
||||
expect((yield* session.get(created.id)).location.directory).toBe(destination)
|
||||
const messages = yield* session.messages({ sessionID: created.id, order: "asc" })
|
||||
expect(messages).toEqual([
|
||||
expect.objectContaining({
|
||||
type: "location-switched",
|
||||
location: { directory: destination },
|
||||
projectID: Project.ID.global,
|
||||
previous: {
|
||||
location: { directory: path.join(tmp.path, "deleted") },
|
||||
projectID: Project.ID.global,
|
||||
subpath: "",
|
||||
},
|
||||
subpath: "",
|
||||
}),
|
||||
expect((yield* session.get(created.id)).location.directory).toBe(
|
||||
AbsolutePath.make(path.join(tmp.path, "deleted")),
|
||||
)
|
||||
expect(yield* session.pending(created.id)).toMatchObject([
|
||||
{
|
||||
type: "move",
|
||||
data: { location: { directory: destination }, projectID: Project.ID.global },
|
||||
},
|
||||
])
|
||||
|
||||
yield* session.move({ sessionID: created.id, directory: destination })
|
||||
expect(yield* session.messages({ sessionID: created.id, order: "asc" })).toEqual(messages)
|
||||
const replacement = AbsolutePath.make(path.join(tmp.path, "replacement"))
|
||||
yield* Effect.promise(() => fs.mkdir(replacement))
|
||||
yield* session.move({ sessionID: created.id, directory: replacement })
|
||||
|
||||
expect(yield* session.pending(created.id)).toMatchObject([
|
||||
{
|
||||
type: "move",
|
||||
data: { location: { directory: replacement }, projectID: Project.ID.global },
|
||||
},
|
||||
])
|
||||
}),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -143,19 +143,26 @@ describe("SessionRunCoordinator", () => {
|
||||
it.effect("cleans active executions when its scope closes", () =>
|
||||
Effect.gen(function* () {
|
||||
const started = yield* Deferred.make<void>()
|
||||
let runs = 0
|
||||
const coordinator = yield* Effect.scoped(
|
||||
Effect.gen(function* () {
|
||||
const coordinator = yield* SessionRunCoordinator.make({
|
||||
drain: () => Deferred.succeed(started, undefined).pipe(Effect.andThen(Effect.never)),
|
||||
drain: () =>
|
||||
Effect.sync(() => runs++).pipe(
|
||||
Effect.andThen(Deferred.succeed(started, undefined)),
|
||||
Effect.andThen(Effect.never),
|
||||
),
|
||||
})
|
||||
yield* coordinator.wake("session")
|
||||
yield* Deferred.await(started)
|
||||
yield* coordinator.wake("session")
|
||||
expect(Array.from(yield* coordinator.active)).toEqual(["session"])
|
||||
return coordinator
|
||||
}),
|
||||
)
|
||||
|
||||
expect(Array.from(yield* coordinator.active)).toEqual([])
|
||||
expect(runs).toBe(1)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -517,6 +524,31 @@ describe("SessionRunCoordinator", () => {
|
||||
),
|
||||
)
|
||||
|
||||
it.effect("starts one successor when settlement requests it", () =>
|
||||
Effect.scoped(
|
||||
Effect.gen(function* () {
|
||||
const successor = yield* Deferred.make<void>()
|
||||
let drains = 0
|
||||
let settlements = 0
|
||||
const coordinator = yield* SessionRunCoordinator.make<string, never>({
|
||||
drain: () =>
|
||||
Effect.sync(() => {
|
||||
drains++
|
||||
if (drains === 2) Deferred.doneUnsafe(successor, Effect.void)
|
||||
}),
|
||||
settled: () => Effect.sync(() => ++settlements === 1),
|
||||
})
|
||||
|
||||
yield* coordinator.wake("session")
|
||||
yield* Deferred.await(successor)
|
||||
yield* coordinator.awaitIdle("session")
|
||||
|
||||
expect(drains).toBe(2)
|
||||
expect(settlements).toBe(2)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
it.effect("trampolines synchronous self-waking execution", () =>
|
||||
Effect.scoped(
|
||||
Effect.gen(function* () {
|
||||
|
||||
@@ -22,7 +22,7 @@ import { SessionTitle } from "@opencode-ai/core/session/title"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
||||
import { SessionRunCoordinator } from "@opencode-ai/core/session/run-coordinator"
|
||||
import { SessionRunner } from "@opencode-ai/core/session/runner/index"
|
||||
import { SessionRunner } from "@opencode-ai/core/session/runner"
|
||||
import * as SessionRunnerLLM from "@opencode-ai/core/session/runner/llm"
|
||||
import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
|
||||
import { Tool } from "@opencode-ai/core/tool"
|
||||
@@ -31,7 +31,7 @@ import { SessionStore } from "@opencode-ai/core/session/store"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { InstructionBuiltIns } from "@opencode-ai/core/instructions/builtins"
|
||||
import { InstructionDiscovery } from "@opencode-ai/core/instruction-discovery"
|
||||
import { Instructions } from "@opencode-ai/core/instructions/index"
|
||||
import { Instructions } from "@opencode-ai/core/instructions"
|
||||
import { SkillInstructions } from "@opencode-ai/core/skill/instructions"
|
||||
import { ReferenceInstructions } from "@opencode-ai/core/reference/instructions"
|
||||
import { McpInstructions } from "@opencode-ai/core/mcp/instructions"
|
||||
|
||||
@@ -38,7 +38,7 @@ import { Money } from "@opencode-ai/schema/money"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
||||
import { SessionRunCoordinator } from "@opencode-ai/core/session/run-coordinator"
|
||||
import { SessionRunner } from "@opencode-ai/core/session/runner/index"
|
||||
import { SessionRunner } from "@opencode-ai/core/session/runner"
|
||||
import * as SessionRunnerLLM from "@opencode-ai/core/session/runner/llm"
|
||||
import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
|
||||
import { PromptCacheDiagnostics } from "@opencode-ai/core/session/prompt-cache-diagnostics"
|
||||
@@ -61,7 +61,7 @@ import {
|
||||
} from "@opencode-ai/core/session/sql"
|
||||
import { InstructionEntry } from "@opencode-ai/core/session/instruction-entry"
|
||||
import { SessionStore } from "@opencode-ai/core/session/store"
|
||||
import { Instructions } from "@opencode-ai/core/instructions/index"
|
||||
import { Instructions } from "@opencode-ai/core/instructions"
|
||||
import { InstructionBuiltIns } from "@opencode-ai/core/instructions/builtins"
|
||||
import { InstructionDiscovery } from "@opencode-ai/core/instruction-discovery"
|
||||
import { SkillInstructions } from "@opencode-ai/core/skill/instructions"
|
||||
|
||||
@@ -1,19 +1,63 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Database } from "bun:sqlite"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { SqlClient } from "effect/unstable/sql"
|
||||
import { SqlError } from "effect/unstable/sql/SqlError"
|
||||
import { sqliteLayer } from "@opencode-ai/core/database/sqlite.workerd"
|
||||
import type { DurableObjectStorage } from "@opencode-ai/core/database/sqlite.workerd"
|
||||
import { makeDurableObjectStorage } from "./fixture/durable-object-storage"
|
||||
import { tempGlobalLayer } from "./fixture/global"
|
||||
|
||||
// Emulates the Durable Object storage API over bun:sqlite so the adapter can
|
||||
// be verified without workerd or Cloudflare runtime dependencies.
|
||||
const makeFakeStorage = () => {
|
||||
const native = new Database(":memory:")
|
||||
const toSqlStorageValue = (value: unknown) => {
|
||||
if (!(value instanceof Uint8Array)) return value as ArrayBuffer | string | number | null
|
||||
const buffer = new ArrayBuffer(value.byteLength)
|
||||
new Uint8Array(buffer).set(value)
|
||||
return buffer
|
||||
}
|
||||
const storage: DurableObjectStorage = {
|
||||
sql: {
|
||||
exec(query: string, ...bindings: Array<unknown>) {
|
||||
const statement = native.query(query)
|
||||
const rows = (statement.values(...(bindings as never[])) ?? []).map((row) => row.map(toSqlStorageValue))
|
||||
const columnNames = statement.columnNames
|
||||
return {
|
||||
columnNames,
|
||||
raw: () => rows[Symbol.iterator](),
|
||||
toArray: () => rows.map((row) => Object.fromEntries(columnNames.map((name, i) => [name, row[i]]))),
|
||||
}
|
||||
},
|
||||
},
|
||||
transaction<T>(closure: (txn: { rollback(): void }) => Promise<T>): Promise<T> {
|
||||
native.run("BEGIN")
|
||||
let rolledBack = false
|
||||
return closure({ rollback: () => (rolledBack = true) }).then(
|
||||
(result) => {
|
||||
native.run(rolledBack ? "ROLLBACK" : "COMMIT")
|
||||
return result
|
||||
},
|
||||
(error) => {
|
||||
native.run("ROLLBACK")
|
||||
throw error
|
||||
},
|
||||
)
|
||||
},
|
||||
transactionSync<T>(closure: () => T): T {
|
||||
return native.transaction(closure)()
|
||||
},
|
||||
}
|
||||
return storage
|
||||
}
|
||||
|
||||
const run = <A, E>(storage: DurableObjectStorage, effect: Effect.Effect<A, E, SqlClient.SqlClient>) =>
|
||||
Effect.runPromise(effect.pipe(Effect.provide(sqliteLayer({ storage })), Effect.scoped))
|
||||
|
||||
describe("sqlite.workerd", () => {
|
||||
test("executes statements with bindings and maps rows to records", async () => {
|
||||
const rows = await run(
|
||||
makeDurableObjectStorage(),
|
||||
makeFakeStorage(),
|
||||
Effect.gen(function* () {
|
||||
const sql = yield* SqlClient.SqlClient
|
||||
yield* sql`CREATE TABLE item (id INTEGER PRIMARY KEY, name TEXT NOT NULL)`
|
||||
@@ -29,7 +73,7 @@ describe("sqlite.workerd", () => {
|
||||
|
||||
test("normalizes ArrayBuffer blob values to Uint8Array", async () => {
|
||||
const rows = await run(
|
||||
makeDurableObjectStorage(),
|
||||
makeFakeStorage(),
|
||||
Effect.gen(function* () {
|
||||
const sql = yield* SqlClient.SqlClient
|
||||
yield* sql`CREATE TABLE blob (data BLOB NOT NULL)`
|
||||
@@ -42,7 +86,7 @@ describe("sqlite.workerd", () => {
|
||||
})
|
||||
|
||||
test("withTransaction commits on success and rolls back on failure", async () => {
|
||||
const storage = makeDurableObjectStorage()
|
||||
const storage = makeFakeStorage()
|
||||
const count = await run(
|
||||
storage,
|
||||
Effect.gen(function* () {
|
||||
@@ -65,7 +109,7 @@ describe("sqlite.workerd", () => {
|
||||
|
||||
test("nested withTransaction fails with SqlError", async () => {
|
||||
const error = await run(
|
||||
makeDurableObjectStorage(),
|
||||
makeFakeStorage(),
|
||||
Effect.gen(function* () {
|
||||
const sql = yield* SqlClient.SqlClient
|
||||
yield* sql`CREATE TABLE t (value TEXT NOT NULL)`
|
||||
@@ -78,7 +122,7 @@ describe("sqlite.workerd", () => {
|
||||
})
|
||||
|
||||
test("boots the full database layer with migrations over injected storage", async () => {
|
||||
const storage = makeDurableObjectStorage()
|
||||
const storage = makeFakeStorage()
|
||||
const core = await import("@opencode-ai/core/database/database")
|
||||
await Effect.runPromise(
|
||||
Effect.scoped(
|
||||
|
||||
@@ -4,7 +4,7 @@ import { describe, expect } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
|
||||
import { Environment } from "@opencode-ai/core/environment/index"
|
||||
import { Environment } from "@opencode-ai/core/environment"
|
||||
import { FileMutation } from "@opencode-ai/core/file-mutation"
|
||||
import { Formatter } from "@opencode-ai/core/formatter"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
|
||||
@@ -4,7 +4,7 @@ import { describe, expect } from "bun:test"
|
||||
import { Effect, Exit, Layer, Schema } from "effect"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
|
||||
import { Environment } from "@opencode-ai/core/environment/index"
|
||||
import { Environment } from "@opencode-ai/core/environment"
|
||||
import { FSUtil } from "@opencode-ai/util/fs-util"
|
||||
import { Formatter } from "@opencode-ai/core/formatter"
|
||||
import { FileMutation } from "@opencode-ai/core/file-mutation"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { Environment } from "@opencode-ai/core/environment/index"
|
||||
import { Environment } from "@opencode-ai/core/environment"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { ReadToolFileSystem } from "@opencode-ai/core/tool/read-filesystem"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/util/cross-spawn-spawner"
|
||||
|
||||
@@ -21,7 +21,7 @@ import { ReadTool } from "@opencode-ai/core/tool/plugin/read"
|
||||
import { ReadToolFileSystem } from "@opencode-ai/core/tool/read-filesystem"
|
||||
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { SessionInstructions } from "@opencode-ai/core/session/instructions"
|
||||
import { Environment } from "@opencode-ai/core/environment/index"
|
||||
import { Environment } from "@opencode-ai/core/environment"
|
||||
import { testEffect } from "./lib/effect"
|
||||
import { toolIdentity, executeTool, registerToolPlugin, toolDefinitions } from "./lib/tool"
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Effect, Layer } from "effect"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
|
||||
import { Environment } from "@opencode-ai/core/environment/index"
|
||||
import { Environment } from "@opencode-ai/core/environment"
|
||||
import { FileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { LocationMutation } from "@opencode-ai/core/location-mutation"
|
||||
|
||||
@@ -12,7 +12,7 @@ import { filesystem } from "@opencode-ai/util/effect/app-node-platform"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { Bus } from "@opencode-ai/core/bus"
|
||||
import { Config } from "@opencode-ai/core/config"
|
||||
import { Environment } from "@opencode-ai/core/environment/index"
|
||||
import { Environment } from "@opencode-ai/core/environment"
|
||||
import { FSUtil } from "@opencode-ai/util/fs-util"
|
||||
import { Global } from "@opencode-ai/util/global"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
|
||||
@@ -6,7 +6,7 @@ import { FileMutation } from "@opencode-ai/core/file-mutation"
|
||||
import { Formatter } from "@opencode-ai/core/formatter"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
|
||||
import { Environment } from "@opencode-ai/core/environment/index"
|
||||
import { Environment } from "@opencode-ai/core/environment"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { LocationMutation } from "@opencode-ai/core/location-mutation"
|
||||
import { Permission } from "@opencode-ai/core/permission"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { beforeEach, expect } from "bun:test"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { makeMemoryDriver } from "@opencode-ai/core/environment/index"
|
||||
import { makeMemoryDriver } from "@opencode-ai/core/environment"
|
||||
import { Workspace } from "@opencode-ai/core/workspace"
|
||||
import { WorkspaceDriver } from "@opencode-ai/core/workspace/driver"
|
||||
import { WorkspaceTable } from "@opencode-ai/core/workspace/sql"
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": false,
|
||||
"incremental": false,
|
||||
"outDir": "dist/types",
|
||||
"tsBuildInfoFile": null
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -5,8 +5,7 @@ export namespace ServiceStatus {
|
||||
export const Health = Schema.Struct({
|
||||
healthy: Schema.Literal(true),
|
||||
version: Schema.String,
|
||||
// 0 means the runtime has no OS process identity (e.g. workerd).
|
||||
pid: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
||||
pid: Schema.Int.check(Schema.isGreaterThan(0)),
|
||||
}).annotate({ identifier: "ServiceHealth" })
|
||||
export type Health = typeof Health.Type
|
||||
|
||||
|
||||
@@ -308,7 +308,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.move",
|
||||
summary: "Move session",
|
||||
description: "Move a session to another project directory, optionally transferring local changes.",
|
||||
description: "Move a session to another project directory after any active execution settles.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -90,6 +90,7 @@ export const Moved = Event.durable({
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
moveID: Event.ID.pipe(optional),
|
||||
location: Location.Ref,
|
||||
projectID: Project.ID.pipe(optional),
|
||||
subpath: RelativePath.pipe(optional),
|
||||
@@ -97,6 +98,16 @@ export const Moved = Event.durable({
|
||||
})
|
||||
export type Moved = typeof Moved.Type
|
||||
|
||||
export const MoveAdmitted = Event.durable({
|
||||
type: "session.move.admitted",
|
||||
...options,
|
||||
schema: {
|
||||
...Base,
|
||||
move: SessionPending.MoveData,
|
||||
},
|
||||
})
|
||||
export type MoveAdmitted = typeof MoveAdmitted.Type
|
||||
|
||||
export const Renamed = Event.durable({
|
||||
type: "session.renamed",
|
||||
...options,
|
||||
@@ -597,6 +608,7 @@ export const Definitions = Event.inventory(
|
||||
Created,
|
||||
AgentSelected,
|
||||
ModelSelected,
|
||||
MoveAdmitted,
|
||||
Moved,
|
||||
Renamed,
|
||||
UsageUpdated,
|
||||
|
||||
@@ -7,6 +7,10 @@ import { DateTimeUtcFromMillis } from "./schema.js"
|
||||
import { SessionDelivery } from "./session-delivery.js"
|
||||
import { SessionID } from "./session-id.js"
|
||||
import { SessionMessage } from "./session-message.js"
|
||||
import { Event } from "./event.js"
|
||||
import { Location } from "./location.js"
|
||||
import { Project } from "./project.js"
|
||||
import { RelativePath } from "./schema.js"
|
||||
|
||||
export const Delivery = SessionDelivery.Delivery
|
||||
export type Delivery = SessionDelivery.Delivery
|
||||
@@ -68,7 +72,23 @@ export const Compaction = Schema.Struct({
|
||||
type: Schema.tag("compaction"),
|
||||
}).annotate({ identifier: "SessionPending.Compaction" })
|
||||
|
||||
export const Info = Schema.Union([User, Synthetic, Compaction]).pipe(
|
||||
export interface MoveData extends Schema.Schema.Type<typeof MoveData> {}
|
||||
export const MoveData = Schema.Struct({
|
||||
location: Location.Ref,
|
||||
projectID: Project.ID,
|
||||
subpath: RelativePath.pipe(optional),
|
||||
}).annotate({ identifier: "SessionPending.MoveData" })
|
||||
|
||||
export interface Move extends Schema.Schema.Type<typeof Move> {}
|
||||
export const Move = Schema.Struct({
|
||||
id: Event.ID,
|
||||
sessionID: SessionID,
|
||||
timeCreated: DateTimeUtcFromMillis,
|
||||
type: Schema.tag("move"),
|
||||
data: MoveData,
|
||||
}).annotate({ identifier: "SessionPending.Move" })
|
||||
|
||||
export const Info = Schema.Union([User, Synthetic, Compaction, Move]).pipe(
|
||||
Schema.toTaggedUnion("type"),
|
||||
Schema.annotate({ identifier: "SessionPending.Info" }),
|
||||
)
|
||||
|
||||
@@ -90,6 +90,16 @@ describe("contract hygiene", () => {
|
||||
})
|
||||
})
|
||||
|
||||
test("pending moves omit absent placement details", () => {
|
||||
expect(
|
||||
Schema.encodeSync(SessionPending.MoveData)({
|
||||
location: { directory: AbsolutePath.make("/project"), workspaceID: undefined },
|
||||
projectID: Project.ID.global,
|
||||
subpath: undefined,
|
||||
}),
|
||||
).toEqual({ location: { directory: "/project" }, projectID: "global" })
|
||||
})
|
||||
|
||||
test("forms require at least one field", () => {
|
||||
expect(() =>
|
||||
Schema.decodeUnknownSync(Form.Info)({
|
||||
|
||||
@@ -78,6 +78,7 @@ describe("public event manifest", () => {
|
||||
"session.deleted.2",
|
||||
"session.agent.selected.1",
|
||||
"session.model.selected.1",
|
||||
"session.move.admitted.1",
|
||||
"session.moved.1",
|
||||
"session.renamed.1",
|
||||
"session.usage.recorded.1",
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./workerd": "./src/workerd.ts"
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "bun test --timeout 5000",
|
||||
@@ -18,7 +17,6 @@
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@opencode-ai/server": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"effect": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { OpenCode } from "@opencode-ai/client/effect"
|
||||
import { SdkPlugins } from "@opencode-ai/core/plugin/sdk"
|
||||
import { SessionRestart } from "@opencode-ai/core/session/execution/restart"
|
||||
import type { ServerFetch } from "@opencode-ai/server/fetch"
|
||||
import { createEmbeddedRoutes } from "@opencode-ai/server/routes"
|
||||
import type { ServerOptions } from "@opencode-ai/server/options"
|
||||
import { Context, Effect, Layer, ManagedRuntime, Scope } from "effect"
|
||||
@@ -15,32 +13,21 @@ export type CreateOptions = ServerOptions & {
|
||||
readonly log?: LogOptions
|
||||
}
|
||||
|
||||
/** Host hooks for embedding opencode on a non-default runtime profile (e.g. workerd). */
|
||||
export type EmbedOptions = ServerFetch.BootOptions
|
||||
|
||||
export const create = Effect.fn("OpenCode.create")(function* (options: CreateOptions = {}, embed: EmbedOptions = {}) {
|
||||
export const create = Effect.fn("OpenCode.create")(function* (options: CreateOptions = {}) {
|
||||
const { log, ...server } = options
|
||||
const runtime = yield* Effect.acquireRelease(
|
||||
Effect.sync(() =>
|
||||
ManagedRuntime.make(
|
||||
createEmbeddedRoutes(
|
||||
{
|
||||
...server,
|
||||
app: { ...server.app, name: server.app?.name ?? "sdk" },
|
||||
database: { path: ":memory:", ...server.database },
|
||||
},
|
||||
embed.overrides ?? [],
|
||||
).pipe(Layer.provide(HttpServer.layerServices), Layer.provideMerge(Logging.layer(log))),
|
||||
createEmbeddedRoutes({
|
||||
...server,
|
||||
app: { ...server.app, name: server.app?.name ?? "sdk" },
|
||||
database: { path: ":memory:", ...server.database },
|
||||
}).pipe(Layer.provide(HttpServer.layerServices), Layer.provideMerge(Logging.layer(log))),
|
||||
),
|
||||
),
|
||||
(runtime) => runtime.disposeEffect,
|
||||
)
|
||||
const context = yield* runtime.contextEffect
|
||||
// Unconditional, as on every runtime: the sweep is a no-op when nothing is
|
||||
// suspended (always, for the default in-memory database). Forked so the
|
||||
// returned client is never delayed; resumed drains are already logged and
|
||||
// durably recorded by the execution layer.
|
||||
yield* Effect.forkDetach(Context.get(context, SessionRestart.Service).resumeSuspendedSessions)
|
||||
const plugins = Context.get(context, SdkPlugins.Service)
|
||||
const router = Context.get(context, HttpRouter.HttpRouter)
|
||||
const handler = HttpEffect.toWebHandlerWith<never, HttpServerRequest.HttpServerRequest | Scope.Scope>(
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
export * as OpenCodeWorkerd from "./workerd"
|
||||
|
||||
import { ServerWorkerd } from "@opencode-ai/server/workerd"
|
||||
import { Layer } from "effect"
|
||||
import * as OpenCode from "./opencode"
|
||||
import type { LogOptions } from "./logging"
|
||||
|
||||
export type CreateOptions = ServerWorkerd.Options & {
|
||||
readonly log?: LogOptions
|
||||
}
|
||||
|
||||
/**
|
||||
* Boots the embedded opencode SDK on the workerd runtime profile: the full
|
||||
* application graph inside a Cloudflare Durable Object, with the database on
|
||||
* the injected `DurableObjectStorage` SQLite and every intentionally-local
|
||||
* service replaced or disabled (see `ServerWorkerd.replacements`).
|
||||
*
|
||||
* Suspended Sessions resume on boot (as on every runtime) because a Durable
|
||||
* Object can be evicted mid-turn with no teardown; the write-ahead execution
|
||||
* claim marks the turn and the boot-time sweep replays it.
|
||||
*
|
||||
* Returns the same typed `OpenCode.Interface` as `OpenCode.create` — typed
|
||||
* session operations plus the live `events.subscribe()` stream — served over
|
||||
* an in-process fetch transport, so no request leaves the isolate.
|
||||
*/
|
||||
export const create = ({ log, ...options }: CreateOptions) =>
|
||||
OpenCode.create({ ...ServerWorkerd.serverOptions(options), log }, { overrides: ServerWorkerd.replacements(options) })
|
||||
|
||||
export const layer = (options: CreateOptions) => Layer.effect(OpenCode.Service, create(options))
|
||||
@@ -10,8 +10,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"test": "bun test --only-failures",
|
||||
"typecheck": "tsgo -b",
|
||||
"probe:workerd": "bun run script/workerd-probe.ts"
|
||||
"typecheck": "tsgo -b"
|
||||
},
|
||||
"dependencies": {
|
||||
"@effect/platform-node": "catalog:",
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Bundle probe for the workerd profile: verifies the full module graph behind
|
||||
* src/workerd.ts resolves under the `workerd` condition without any `bun:`
|
||||
* builtins. `node:` builtins stay external (workerd provides them through
|
||||
* nodejs_compat); the probe prints the surviving externals so the A4 boot
|
||||
* spike knows exactly what the runtime must supply.
|
||||
*/
|
||||
import path from "node:path"
|
||||
import { mkdtempSync } from "node:fs"
|
||||
import os from "node:os"
|
||||
|
||||
const outdir = mkdtempSync(path.join(os.tmpdir(), "opencode-workerd-probe-"))
|
||||
const result = await Bun.build({
|
||||
entrypoints: [path.join(import.meta.dir, "../src/workerd.ts")],
|
||||
conditions: ["workerd"],
|
||||
target: "node",
|
||||
outdir,
|
||||
sourcemap: "none",
|
||||
throw: false,
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
console.error(`workerd bundle probe FAILED (${result.logs.length} issues)`)
|
||||
for (const log of result.logs) console.error(String(log))
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Everything in the graph is bundled, so any import specifier that survives in
|
||||
// the output is an external the runtime must provide. A specifier reached only
|
||||
// through `import()` is loaded lazily behind a runtime guard, so it costs
|
||||
// nothing on a runtime that never takes that branch; a static import is
|
||||
// evaluated on module load and must resolve.
|
||||
const transpiler = new Bun.Transpiler({ loader: "js" })
|
||||
const externals = new Map<string, { static: boolean }>()
|
||||
const record = (specifier: string, isStatic: boolean) => {
|
||||
const seen = externals.get(specifier)
|
||||
externals.set(specifier, { static: (seen?.static ?? false) || isStatic })
|
||||
}
|
||||
for (const artifact of result.outputs) {
|
||||
const text = await artifact.text()
|
||||
for (const imported of transpiler.scanImports(text)) record(imported.path, imported.kind !== "dynamic-import")
|
||||
for (const match of text.matchAll(/\brequire\(\s*"([^"]+)"\s*\)/g)) record(match[1], true)
|
||||
}
|
||||
const sorted = Array.from(externals.keys()).toSorted()
|
||||
const isBun = (specifier: string) => specifier === "bun" || specifier.startsWith("bun:")
|
||||
const leaked = sorted.filter((specifier) => isBun(specifier) && externals.get(specifier)!.static)
|
||||
const bytes = result.outputs.reduce((total, artifact) => total + artifact.size, 0)
|
||||
|
||||
console.log(`workerd bundle probe OK: ${result.outputs.length} artifacts, ${(bytes / 1024 / 1024).toFixed(1)} MiB`)
|
||||
console.log(`external builtins (${sorted.length}):`)
|
||||
for (const specifier of sorted) {
|
||||
const lazy = externals.get(specifier)!.static ? "" : " (lazy)"
|
||||
console.log(` ${specifier}${lazy}`)
|
||||
}
|
||||
if (leaked.length > 0) {
|
||||
console.error(`FAILED: bun builtins statically imported in the workerd graph: ${leaked.join(", ")}`)
|
||||
process.exit(1)
|
||||
}
|
||||
@@ -3,20 +3,10 @@ export * as ServerFetch from "./fetch"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import { HttpEffect, HttpMiddleware, HttpRouter, HttpServer } from "effect/unstable/http"
|
||||
import { SessionRestart } from "@opencode-ai/core/session/execution/restart"
|
||||
import type { LayerNode } from "@opencode-ai/util/effect/layer-node"
|
||||
import { isAllowedCorsOrigin } from "./cors"
|
||||
import { createRoutes } from "./routes"
|
||||
import type { ServerOptions } from "./options"
|
||||
|
||||
export interface BootOptions {
|
||||
/**
|
||||
* Runtime-profile service replacements, applied after the standard set so later entries
|
||||
* win — swaps services the standard graph assumes are local. See `ServerWorkerd.replacements`.
|
||||
*/
|
||||
readonly overrides?: LayerNode.Replacements
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Builds a web-standard fetch handler — `(request: Request) => Promise<Response>` — serving the
|
||||
* same HttpApi routes as the Node server process without binding a port, owning a listener, or
|
||||
@@ -39,10 +29,8 @@ export interface BootOptions {
|
||||
* Object leaves the same durable signature as a killed process — replays orphaned turns on the
|
||||
* next boot, and the sweep is a no-op when nothing is suspended.
|
||||
*/
|
||||
export const make = Effect.fn("ServerFetch.make")(function* (options: ServerOptions = {}, boot: BootOptions = {}) {
|
||||
const context = yield* Layer.build(
|
||||
createRoutes(options, () => [], boot.overrides ?? []).pipe(Layer.provide(HttpServer.layerServices)),
|
||||
)
|
||||
export const make = Effect.fn("ServerFetch.make")(function* (options: ServerOptions = {}) {
|
||||
const context = yield* Layer.build(createRoutes(options, () => []).pipe(Layer.provide(HttpServer.layerServices)))
|
||||
// Forked so the returned handler is never delayed; resumed drains are already
|
||||
// logged and durably recorded by the execution layer.
|
||||
yield* Effect.forkDetach(Context.get(context, SessionRestart.Service).resumeSuspendedSessions)
|
||||
|
||||
@@ -11,8 +11,7 @@ export const HealthHandler = HttpApiBuilder.group(Api, "server.health", (handler
|
||||
return {
|
||||
healthy: true as const,
|
||||
version: info.app.version ?? "unknown",
|
||||
// Runtimes without OS process identity (workerd) report 0.
|
||||
pid: process.pid ?? 0,
|
||||
pid: process.pid,
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -40,11 +40,5 @@ export const ServerOptions = Schema.Struct({
|
||||
fff: Schema.optional(Schema.Boolean),
|
||||
}),
|
||||
),
|
||||
mcp: Schema.optional(
|
||||
Schema.Struct({
|
||||
/** Set false on runtimes that cannot spawn child processes; local (stdio) MCP servers report failed instead of connecting. */
|
||||
stdio: Schema.optional(Schema.Boolean),
|
||||
}),
|
||||
),
|
||||
})
|
||||
export type ServerOptions = typeof ServerOptions.Type
|
||||
|
||||
@@ -66,34 +66,27 @@ const applicationServices = LayerNode.group([
|
||||
SessionRestart.node,
|
||||
])
|
||||
|
||||
export function createRoutes(
|
||||
options: ServerOptions = {},
|
||||
serviceURLs: () => ReadonlyArray<string> = () => [],
|
||||
overrides: LayerNode.Replacements = [],
|
||||
) {
|
||||
export function createRoutes(options: ServerOptions = {}, serviceURLs: () => ReadonlyArray<string> = () => []) {
|
||||
return makeRoutes(
|
||||
options.password
|
||||
? ServerAuth.Config.configLayer({ password: Option.some(options.password) })
|
||||
: ServerAuth.Config.layer,
|
||||
options,
|
||||
serviceURLs,
|
||||
overrides,
|
||||
)
|
||||
}
|
||||
|
||||
export function createEmbeddedRoutes(options: ServerOptions = {}, overrides: LayerNode.Replacements = []) {
|
||||
return makeRoutes(ServerAuth.Config.configLayer({ password: Option.none() }), options, () => [], overrides)
|
||||
export function createEmbeddedRoutes(options: ServerOptions = {}) {
|
||||
return makeRoutes(ServerAuth.Config.configLayer({ password: Option.none() }), options, () => [])
|
||||
}
|
||||
|
||||
function makeRoutes<AuthError, AuthServices>(
|
||||
auth: Layer.Layer<ServerAuth.Config, AuthError, AuthServices>,
|
||||
options: ServerOptions,
|
||||
serviceURLs: () => ReadonlyArray<string>,
|
||||
// Runtime-profile replacements (e.g. workerd) applied after the standard set, so later entries win.
|
||||
overrides: LayerNode.Replacements,
|
||||
) {
|
||||
const pluginRuntimeCell = PluginRuntime.makeCell()
|
||||
const standard: LayerNode.Replacements = [
|
||||
const replacements: LayerNode.Replacements = [
|
||||
[Database.node, Database.configured(options.database)],
|
||||
[Bus.node, Bus.configured({ persist: options.events?.persist })],
|
||||
[App.node, App.configured(options.app)],
|
||||
@@ -120,7 +113,6 @@ function makeRoutes<AuthError, AuthServices>(
|
||||
name: options.app?.name ?? "opencode",
|
||||
version: options.app?.version ?? "unknown",
|
||||
},
|
||||
stdio: options.mcp?.stdio,
|
||||
}),
|
||||
],
|
||||
[PluginRuntime.node, PluginRuntime.layerWithCell(pluginRuntimeCell)],
|
||||
@@ -130,7 +122,6 @@ function makeRoutes<AuthError, AuthServices>(
|
||||
WorkspaceDriver.registryNode({ [modalProvider]: modalWorkspaceDriver({ app: "opencode-workspaces" }) }),
|
||||
],
|
||||
]
|
||||
const replacements: LayerNode.Replacements = [...standard, ...overrides]
|
||||
const serviceLayer = options.simulation
|
||||
? Layer.unwrap(
|
||||
Effect.gen(function* () {
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
export * as ServerWorkerd from "./workerd"
|
||||
|
||||
import { Effect, Layer } from "effect"
|
||||
import { ConfigPluginSource } from "@opencode-ai/core/config/plugin/source"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { sqliteLayer } from "@opencode-ai/core/database/sqlite.workerd"
|
||||
import type { DurableObjectStorage } from "@opencode-ai/core/database/sqlite.workerd"
|
||||
import { FileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { FileSystemSearch } from "@opencode-ai/core/filesystem/search"
|
||||
import { Pty } from "@opencode-ai/core/pty"
|
||||
import { Shell } from "@opencode-ai/core/shell"
|
||||
import { Snapshot } from "@opencode-ai/core/snapshot"
|
||||
import { Vcs } from "@opencode-ai/core/vcs"
|
||||
import type { LayerNode } from "@opencode-ai/util/effect/layer-node"
|
||||
import { ServerFetch } from "./fetch"
|
||||
import type { ServerOptions } from "./options"
|
||||
|
||||
/**
|
||||
* The workerd runtime profile: boots opencode core and server inside a
|
||||
* Cloudflare Durable Object, with every intentionally-local service replaced
|
||||
* or disabled.
|
||||
*
|
||||
* - Database runs on the injected `DurableObjectStorage` SQLite.
|
||||
* - Watcher and fff are disabled through their existing option flags; pty, fff,
|
||||
* shell-parser, photon, and process-lock native modules resolve to inert
|
||||
* stubs under the `workerd` bundle condition.
|
||||
* - Shell, FileSystem, FileSystemSearch, and Pty fail with a clear defect until
|
||||
* a remote sandbox backs them; Snapshot and Vcs degrade to no-op results.
|
||||
* - Config is injected as a string (no filesystem); plugin discovery is
|
||||
* precompiled-only and MCP is restricted to remote transports.
|
||||
*
|
||||
* Bundle with the `workerd` condition, e.g.
|
||||
* `bun build src/workerd.ts --conditions=workerd --target=node`
|
||||
* (see `script/workerd-probe.ts`).
|
||||
*/
|
||||
export interface Options {
|
||||
/** Durable Object storage whose SQLite database backs the opencode database. */
|
||||
readonly storage: DurableObjectStorage
|
||||
readonly app?: ServerOptions["app"]
|
||||
readonly password?: string
|
||||
/** Inline opencode config content (JSON), same as `ServerOptions.config.content`. */
|
||||
readonly config?: { readonly content?: string }
|
||||
/** models.dev catalog options; the bundled snapshot is the boot-time floor either way. */
|
||||
readonly models?: ServerOptions["models"]
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the web-standard fetch handler for a Durable Object's `fetch()`. The
|
||||
* application layer builds eagerly in the caller's scope, so hold it in the
|
||||
* Durable Object instance rather than per request.
|
||||
*/
|
||||
export function create(options: Options) {
|
||||
// Eviction can kill the isolate between a turn's Started and terminal events with no
|
||||
// teardown. The write-ahead execution claim plus ServerFetch.make's boot-time resume
|
||||
// recovers such orphaned turns by replaying the drain from durable history on the next wake.
|
||||
return ServerFetch.make(serverOptions(options), { overrides: replacements(options) })
|
||||
}
|
||||
|
||||
export function serverOptions(options: Options): ServerOptions {
|
||||
return {
|
||||
app: options.app,
|
||||
password: options.password,
|
||||
fs: { filewatcher: false, fff: false },
|
||||
// Durable event history is how a turn orphaned by eviction is recovered:
|
||||
// the boot-time resume replays it. A runtime that dies without teardown
|
||||
// cannot opt out of it, so this is not exposed as an option.
|
||||
events: { persist: true },
|
||||
config: { content: options.config?.content },
|
||||
models: options.models,
|
||||
// No child processes on workerd: local (stdio) MCP servers report failed
|
||||
// instead of connecting; remote transports work unchanged.
|
||||
mcp: { stdio: false },
|
||||
}
|
||||
}
|
||||
|
||||
/** The workerd replacement graph, applied after the standard server replacements. */
|
||||
export function replacements(options: Options): LayerNode.Replacements {
|
||||
return [
|
||||
[Database.node, Database.configuredClient(sqliteLayer({ storage: options.storage }))],
|
||||
[Snapshot.node, Snapshot.noopLayer],
|
||||
[Vcs.node, vcsLayer],
|
||||
[Shell.node, shellLayer],
|
||||
[FileSystem.node, fileSystemLayer],
|
||||
[FileSystemSearch.node, fileSystemSearchLayer],
|
||||
[Pty.node, ptyLayer],
|
||||
// Precompiled (internal and SDK) plugins only: no plugin-directory scan, npm
|
||||
// install, or import of plugin code from disk.
|
||||
[ConfigPluginSource.node, ConfigPluginSource.empty],
|
||||
]
|
||||
}
|
||||
|
||||
const unavailable = (what: string) => Effect.die(new Error(`${what} is unavailable in the workerd profile`))
|
||||
|
||||
// Vcs degrades to empty results, matching its behavior for locations without a
|
||||
// supported VCS, so read-only clients never need to special-case this runtime.
|
||||
const vcsLayer = Layer.succeed(
|
||||
Vcs.Service,
|
||||
Vcs.Service.of({
|
||||
info: () => Effect.succeed({ branch: {} }),
|
||||
status: () => Effect.succeed([]),
|
||||
diff: () => Effect.succeed([]),
|
||||
}),
|
||||
)
|
||||
|
||||
// Shell commands need a real process; queries for unknown IDs stay typed while
|
||||
// creation is a defect until a remote sandbox backs them.
|
||||
const shellLayer = Layer.succeed(
|
||||
Shell.Service,
|
||||
Shell.Service.of({
|
||||
name: () => Effect.succeed("unsupported"),
|
||||
create: () => unavailable("Shell.create"),
|
||||
list: () => Effect.succeed([]),
|
||||
get: (id) => Effect.fail(new Shell.NotFoundError({ id })),
|
||||
wait: (id) => Effect.fail(new Shell.NotFoundError({ id })),
|
||||
timeout: (id) => Effect.fail(new Shell.NotFoundError({ id })),
|
||||
output: (id) => Effect.fail(new Shell.NotFoundError({ id })),
|
||||
remove: (id) => Effect.fail(new Shell.NotFoundError({ id })),
|
||||
}),
|
||||
)
|
||||
|
||||
// The Location-scoped filesystem has no local worktree to serve until a remote
|
||||
// sandbox backs it.
|
||||
const fileSystemLayer = Layer.succeed(
|
||||
FileSystem.Service,
|
||||
FileSystem.Service.of({
|
||||
read: () => unavailable("FileSystem.read"),
|
||||
list: () => unavailable("FileSystem.list"),
|
||||
find: () => unavailable("FileSystem.find"),
|
||||
}),
|
||||
)
|
||||
|
||||
const fileSystemSearchLayer = Layer.succeed(
|
||||
FileSystemSearch.Service,
|
||||
FileSystemSearch.Service.of({
|
||||
find: () => unavailable("FileSystemSearch.find"),
|
||||
}),
|
||||
)
|
||||
|
||||
const ptyLayer = Layer.succeed(
|
||||
Pty.Service,
|
||||
Pty.Service.of({
|
||||
list: () => Effect.succeed([]),
|
||||
get: (ptyID) => Effect.fail(new Pty.NotFoundError({ ptyID })),
|
||||
create: () => unavailable("Pty.create"),
|
||||
update: (ptyID) => Effect.fail(new Pty.NotFoundError({ ptyID })),
|
||||
remove: (ptyID) => Effect.fail(new Pty.NotFoundError({ ptyID })),
|
||||
write: (ptyID) => Effect.fail(new Pty.NotFoundError({ ptyID })),
|
||||
attach: (ptyID) => Effect.fail(new Pty.NotFoundError({ ptyID })),
|
||||
}),
|
||||
)
|
||||
@@ -2,7 +2,7 @@ import { Effect, Sink, Stream } from "effect"
|
||||
import { systemError } from "effect/PlatformError"
|
||||
import type { Command, KillOptions } from "effect/unstable/process/ChildProcess"
|
||||
import { ExitCode, make, makeHandle, ProcessId } from "effect/unstable/process/ChildProcessSpawner"
|
||||
import type { Driver } from "@opencode-ai/core/environment/index"
|
||||
import type { Driver } from "@opencode-ai/core/environment"
|
||||
import type { App, Image, ModalClient, ModalClientParams, Sandbox, SandboxCreateParams } from "modal"
|
||||
|
||||
const INNER_WRAPPER = `
|
||||
|
||||
@@ -4,8 +4,8 @@ import path from "node:path"
|
||||
import { afterAll, expect, test } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { ChildProcess } from "effect/unstable/process"
|
||||
import { Failed, makeFiles } from "@opencode-ai/core/environment/index"
|
||||
import { environmentConformance } from "../../core/test/lib/environment-conformance.js"
|
||||
import { Failed, makeFiles } from "@opencode-ai/core/environment"
|
||||
import { environmentConformance } from "@opencode-ai/core/testing/environment-conformance"
|
||||
import { createModalSandbox } from "../src/workspace/modal"
|
||||
|
||||
const enabled =
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { makeDurableObjectStorage } from "../../core/test/fixture/durable-object-storage"
|
||||
import { it } from "../../core/test/lib/effect"
|
||||
import { ServerWorkerd } from "../src/workerd"
|
||||
|
||||
// Covers the profile's replacement graph composing and the database booting
|
||||
// through the injected Durable Object storage. Verification inside a real
|
||||
// isolate lives in the workerd-spike package.
|
||||
it.live("boots the workerd profile over durable object storage", () =>
|
||||
Effect.gen(function* () {
|
||||
const handler = yield* ServerWorkerd.create({
|
||||
storage: makeDurableObjectStorage(),
|
||||
password: "secret",
|
||||
app: { version: "workerd-test" },
|
||||
config: { content: "{}" },
|
||||
})
|
||||
|
||||
const unauthorized = yield* Effect.promise(() => handler(new Request("http://opencode.local/api/health")))
|
||||
expect(unauthorized.status).toBe(401)
|
||||
|
||||
const health = yield* Effect.promise(() =>
|
||||
handler(
|
||||
new Request("http://opencode.local/api/health", {
|
||||
headers: { authorization: `Basic ${btoa("opencode:secret")}` },
|
||||
}),
|
||||
),
|
||||
)
|
||||
expect(health.status).toBe(200)
|
||||
|
||||
const body: unknown = yield* Effect.promise(() => health.json())
|
||||
expect(body).toMatchObject({ healthy: true, version: "workerd-test" })
|
||||
}).pipe(Effect.scoped),
|
||||
)
|
||||
@@ -3,7 +3,7 @@ import os from "node:os"
|
||||
import path from "node:path"
|
||||
import { expect, test } from "bun:test"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { makeFiles } from "@opencode-ai/core/environment/index"
|
||||
import { makeFiles } from "@opencode-ai/core/environment"
|
||||
import { Workspace } from "@opencode-ai/core/workspace"
|
||||
import { WorkspaceDriver } from "@opencode-ai/core/workspace/driver"
|
||||
import { Effect, Layer } from "effect"
|
||||
|
||||
@@ -501,7 +501,7 @@ function App(props: { pair?: DialogPairCredentials }) {
|
||||
toast.show({
|
||||
variant: "error",
|
||||
title: `MCP server failed: ${server.name}`,
|
||||
message: "Run /mcps to view details.",
|
||||
message: "Open MCP servers to view details.",
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
import { CliRenderEvents, TextAttributes, type ScrollBoxRenderable } from "@opentui/core"
|
||||
import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid"
|
||||
import { createEffect, createMemo, createSignal, onCleanup, onMount } from "solid-js"
|
||||
import { useConfig } from "../config"
|
||||
import { useClipboard } from "../context/clipboard"
|
||||
import { Keymap } from "../context/keymap"
|
||||
import { getScrollAcceleration } from "../util/scroll"
|
||||
import { useDialog } from "../ui/dialog"
|
||||
import { useTheme } from "../context/theme"
|
||||
import { useToast } from "../ui/toast"
|
||||
|
||||
export function DialogErrorDetails(props: { title: string; error: string; onBack: () => void }) {
|
||||
const dialog = useDialog()
|
||||
const clipboard = useClipboard()
|
||||
const toast = useToast()
|
||||
const theme = useTheme("elevated")
|
||||
const overlayTheme = useTheme("overlay")
|
||||
const renderer = useRenderer()
|
||||
const dimensions = useTerminalDimensions()
|
||||
const config = useConfig().data
|
||||
const [copied, setCopied] = createSignal(false)
|
||||
const [scrollable, setScrollable] = createSignal(false)
|
||||
const height = createMemo(() => Math.max(3, Math.floor(dimensions().height / 2) - 5))
|
||||
let scroll: ScrollBoxRenderable | undefined
|
||||
let measure: (() => void) | undefined
|
||||
|
||||
onMount(() => dialog.setSize("large"))
|
||||
|
||||
createEffect(() => {
|
||||
dimensions()
|
||||
props.error
|
||||
if (measure) renderer.off(CliRenderEvents.FRAME, measure)
|
||||
measure = () => {
|
||||
measure = undefined
|
||||
setScrollable(Boolean(scroll && scroll.scrollHeight > scroll.viewport.height))
|
||||
}
|
||||
renderer.once(CliRenderEvents.FRAME, measure)
|
||||
renderer.requestRender()
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
if (measure) renderer.off(CliRenderEvents.FRAME, measure)
|
||||
})
|
||||
|
||||
const copy = () => {
|
||||
void clipboard
|
||||
.write(props.error)
|
||||
.then(() => setCopied(true))
|
||||
.catch(toast.error)
|
||||
}
|
||||
|
||||
Keymap.createLayer(() => ({
|
||||
mode: "modal",
|
||||
commands: [
|
||||
{ bind: "escape", title: "Back", group: "Dialog", run: props.onBack },
|
||||
{ bind: "c", title: "Copy details", group: "Dialog", run: copy },
|
||||
],
|
||||
}))
|
||||
|
||||
useKeyboard((event) => {
|
||||
if (!scrollable()) return
|
||||
if (event.name === "up") return scroll?.scrollBy(-1)
|
||||
if (event.name === "down") return scroll?.scrollBy(1)
|
||||
if (event.name === "pageup") return scroll?.scrollBy(-height())
|
||||
if (event.name === "pagedown") return scroll?.scrollBy(height())
|
||||
if (event.name === "home") return scroll?.scrollTo(0)
|
||||
if (event.name === "end" && scroll) return scroll.scrollTo(scroll.scrollHeight)
|
||||
})
|
||||
|
||||
return (
|
||||
<box paddingLeft={4} paddingRight={4} paddingBottom={1} gap={1}>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
<text attributes={TextAttributes.BOLD} fg={theme.text.default}>
|
||||
{props.title}
|
||||
</text>
|
||||
<text fg={theme.text.subdued} onMouseUp={props.onBack}>
|
||||
esc
|
||||
</text>
|
||||
</box>
|
||||
<text fg={theme.text.feedback.error.default}>✗ Failed</text>
|
||||
<box
|
||||
backgroundColor={overlayTheme.background.default}
|
||||
paddingLeft={2}
|
||||
paddingRight={2}
|
||||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
>
|
||||
<scrollbox
|
||||
ref={(element: ScrollBoxRenderable) => (scroll = element)}
|
||||
height={height()}
|
||||
scrollbarOptions={{ visible: false }}
|
||||
scrollAcceleration={getScrollAcceleration(config)}
|
||||
>
|
||||
<text fg={overlayTheme.text.default} wrapMode="word">
|
||||
{props.error}
|
||||
</text>
|
||||
</scrollbox>
|
||||
</box>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
<text>
|
||||
<span style={{ fg: theme.text.default }}>
|
||||
<b>{scrollable() ? "↑/↓" : ""}</b>
|
||||
</span>
|
||||
<span style={{ fg: theme.text.subdued }}>{scrollable() ? " scroll" : ""}</span>
|
||||
</text>
|
||||
<text onMouseUp={copy}>
|
||||
<span style={{ fg: copied() ? theme.text.feedback.success.default : theme.text.default }}>
|
||||
<b>{copied() ? "✓ copied" : "c"}</b>
|
||||
</span>
|
||||
<span style={{ fg: theme.text.subdued }}>{copied() ? "" : " copy details"}</span>
|
||||
</text>
|
||||
</box>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createEffect, createMemo, createSignal, Show } from "solid-js"
|
||||
import { createEffect, createMemo, createSignal, onMount, Show } from "solid-js"
|
||||
import { useData } from "../context/data"
|
||||
import { useClient } from "../context/client"
|
||||
import { Keymap } from "../context/keymap"
|
||||
@@ -6,10 +6,13 @@ import { pipe, sortBy } from "remeda"
|
||||
import { DialogSelect } from "../ui/dialog-select"
|
||||
import { useDialog } from "../ui/dialog"
|
||||
import { useTheme } from "../context/theme"
|
||||
import { TextAttributes } from "@opentui/core"
|
||||
import { TextAttributes, type ScrollBoxRenderable } from "@opentui/core"
|
||||
import type { McpServer } from "@opencode-ai/client"
|
||||
import { useClipboard } from "../context/clipboard"
|
||||
import { useToast } from "../ui/toast"
|
||||
import { DialogErrorDetails } from "./dialog-error-details"
|
||||
import { useKeyboard, useTerminalDimensions } from "@opentui/solid"
|
||||
import { useConfig } from "../config"
|
||||
import { getScrollAcceleration } from "../util/scroll"
|
||||
|
||||
function statusError(status: McpServer["status"]) {
|
||||
if (status.status === "failed") return status.error
|
||||
@@ -140,9 +143,8 @@ export function DialogMcp() {
|
||||
}
|
||||
>
|
||||
{(server) => (
|
||||
<DialogErrorDetails
|
||||
title={`MCP server: ${server().name}`}
|
||||
error={statusError(server().status) ?? "Unknown MCP connection error"}
|
||||
<DialogMcpError
|
||||
server={server()}
|
||||
onBack={() => {
|
||||
setDetail()
|
||||
dialog.setSize("medium")
|
||||
@@ -153,3 +155,79 @@ export function DialogMcp() {
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogMcpError(props: { server: McpServer; onBack: () => void }) {
|
||||
const dialog = useDialog()
|
||||
const clipboard = useClipboard()
|
||||
const toast = useToast()
|
||||
const theme = useTheme("elevated")
|
||||
const overlayTheme = useTheme("overlay")
|
||||
const dimensions = useTerminalDimensions()
|
||||
const config = useConfig().data
|
||||
const [copied, setCopied] = createSignal(false)
|
||||
const error = () => statusError(props.server.status) ?? "Unknown MCP connection error"
|
||||
const height = createMemo(() => Math.max(3, Math.floor(dimensions().height / 2) - 5))
|
||||
let scroll: ScrollBoxRenderable | undefined
|
||||
|
||||
onMount(() => dialog.setSize("large"))
|
||||
|
||||
const copy = () => {
|
||||
void clipboard
|
||||
.write(error())
|
||||
.then(() => setCopied(true))
|
||||
.catch(toast.error)
|
||||
}
|
||||
|
||||
Keymap.createLayer(() => ({
|
||||
mode: "modal",
|
||||
commands: [{ bind: "escape", title: "Back to MCP servers", group: "Dialog", run: props.onBack }],
|
||||
}))
|
||||
|
||||
useKeyboard((event) => {
|
||||
if (event.name === "c") return copy()
|
||||
if (event.name === "up") return scroll?.scrollBy(-1)
|
||||
if (event.name === "down") return scroll?.scrollBy(1)
|
||||
if (event.name === "pageup") return scroll?.scrollBy(-height())
|
||||
if (event.name === "pagedown") return scroll?.scrollBy(height())
|
||||
if (event.name === "home") return scroll?.scrollTo(0)
|
||||
if (event.name === "end" && scroll) return scroll.scrollTo(scroll.scrollHeight)
|
||||
})
|
||||
|
||||
return (
|
||||
<box paddingLeft={4} paddingRight={4} paddingBottom={1} gap={1}>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
<text attributes={TextAttributes.BOLD} fg={theme.text.default}>
|
||||
MCP server: {props.server.name}
|
||||
</text>
|
||||
<text fg={theme.text.subdued} onMouseUp={props.onBack}>
|
||||
esc back
|
||||
</text>
|
||||
</box>
|
||||
<text fg={theme.text.feedback.error.default}>✗ Failed</text>
|
||||
<box
|
||||
backgroundColor={overlayTheme.background.default}
|
||||
paddingLeft={2}
|
||||
paddingRight={2}
|
||||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
>
|
||||
<scrollbox
|
||||
ref={(element: ScrollBoxRenderable) => (scroll = element)}
|
||||
height={height()}
|
||||
scrollbarOptions={{ visible: false }}
|
||||
scrollAcceleration={getScrollAcceleration(config)}
|
||||
>
|
||||
<text fg={overlayTheme.text.default} wrapMode="word">
|
||||
{error()}
|
||||
</text>
|
||||
</scrollbox>
|
||||
</box>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
<text fg={theme.text.subdued}>↑↓ scroll</text>
|
||||
<text fg={theme.text.subdued} onMouseUp={copy}>
|
||||
{copied() ? "✓ copied" : "c copy details"}
|
||||
</text>
|
||||
</box>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -201,7 +201,8 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||
function updatePending(sessionID: string, inputID: string, delivery: SessionPending.Delivery) {
|
||||
const index = store.session.pending[sessionID]?.findIndex((item) => item.id === inputID) ?? -1
|
||||
const item = store.session.pending[sessionID]?.[index]
|
||||
if (index < 0 || !item || item.type === "compaction" || item.delivery === delivery) return
|
||||
if (index < 0 || !item || (item.type !== "user" && item.type !== "synthetic") || item.delivery === delivery)
|
||||
return
|
||||
setStore("session", "pending", sessionID, index, { ...item, delivery })
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +1,10 @@
|
||||
import { Plugin } from "@opencode-ai/plugin/tui"
|
||||
import { createMemo, Match, Show, Switch } from "solid-js"
|
||||
import { useTerminalDimensions } from "@opentui/solid"
|
||||
import { usePlugin } from "../../plugin/context"
|
||||
|
||||
export function homeFooterVisibility(width: number) {
|
||||
return {
|
||||
mcpCommand: width >= 64,
|
||||
pluginCommand: width >= 80,
|
||||
version: width >= 64,
|
||||
}
|
||||
}
|
||||
|
||||
function Mcp(props: { context: Plugin.Context }) {
|
||||
const dimensions = useTerminalDimensions()
|
||||
const visibility = createMemo(() => homeFooterVisibility(dimensions().width))
|
||||
const list = createMemo(() => props.context.data.location.mcp.server.list(props.context.location) ?? [])
|
||||
const failed = createMemo(() => list().filter((item) => item.status.status === "failed").length)
|
||||
const failed = createMemo(() => list().some((item) => item.status.status === "failed"))
|
||||
const count = createMemo(() => list().filter((item) => item.status.status === "connected").length)
|
||||
|
||||
return (
|
||||
@@ -25,7 +14,6 @@ function Mcp(props: { context: Plugin.Context }) {
|
||||
<Switch>
|
||||
<Match when={failed()}>
|
||||
<span style={{ fg: props.context.theme.text.feedback.error.default }}>⊙ </span>
|
||||
{failed()} MCP failed
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<span
|
||||
@@ -36,34 +24,11 @@ function Mcp(props: { context: Plugin.Context }) {
|
||||
>
|
||||
⊙{" "}
|
||||
</span>
|
||||
{count()} MCP
|
||||
</Match>
|
||||
</Switch>
|
||||
{count()} MCP
|
||||
</text>
|
||||
<Show when={visibility().mcpCommand}>
|
||||
<text fg={props.context.theme.text.subdued}>/mcps</text>
|
||||
</Show>
|
||||
</box>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
function Plugins(props: { context: Plugin.Context }) {
|
||||
const dimensions = useTerminalDimensions()
|
||||
const visibility = createMemo(() => homeFooterVisibility(dimensions().width))
|
||||
const plugins = usePlugin()
|
||||
const failed = createMemo(() => plugins.list().filter((item) => item.status === "failed").length)
|
||||
|
||||
return (
|
||||
<Show when={failed()}>
|
||||
<box gap={1} flexDirection="row" flexShrink={0}>
|
||||
<text fg={props.context.theme.text.default}>
|
||||
<span style={{ fg: props.context.theme.text.feedback.error.default }}>⊙ </span>
|
||||
{failed()} plugin{failed() === 1 ? "" : "s"} failed
|
||||
</text>
|
||||
<Show when={visibility().pluginCommand}>
|
||||
<text fg={props.context.theme.text.subdued}>/plugins</text>
|
||||
</Show>
|
||||
<text fg={props.context.theme.text.subdued}>/status</text>
|
||||
</box>
|
||||
</Show>
|
||||
)
|
||||
@@ -71,7 +36,6 @@ function Plugins(props: { context: Plugin.Context }) {
|
||||
|
||||
function View(props: { context: Plugin.Context }) {
|
||||
const dimensions = useTerminalDimensions()
|
||||
const visibility = createMemo(() => homeFooterVisibility(dimensions().width))
|
||||
|
||||
return (
|
||||
<Show when={dimensions().height >= 12 && dimensions().width >= 44}>
|
||||
@@ -86,13 +50,10 @@ function View(props: { context: Plugin.Context }) {
|
||||
gap={2}
|
||||
>
|
||||
<Mcp context={props.context} />
|
||||
<Plugins context={props.context} />
|
||||
<box flexGrow={1} />
|
||||
<Show when={visibility().version}>
|
||||
<box flexShrink={0}>
|
||||
<text fg={props.context.theme.text.subdued}>{props.context.app.version}</text>
|
||||
</box>
|
||||
</Show>
|
||||
<box flexShrink={0}>
|
||||
<text fg={props.context.theme.text.subdued}>{props.context.app.version}</text>
|
||||
</box>
|
||||
</box>
|
||||
</Show>
|
||||
)
|
||||
|
||||
@@ -1,26 +1,22 @@
|
||||
import { Plugin } from "@opencode-ai/plugin/tui"
|
||||
import { createEffect, createMemo, createSignal, Show } from "solid-js"
|
||||
import { createMemo, createSignal } from "solid-js"
|
||||
import { usePlugin } from "../../plugin/context"
|
||||
import { DialogSelect, type DialogSelectOption } from "../../ui/dialog-select"
|
||||
import { useDialog } from "../../ui/dialog"
|
||||
import { DialogErrorDetails } from "../../component/dialog-error-details"
|
||||
|
||||
const id = "opencode.plugins"
|
||||
|
||||
function View(props: { context: Plugin.Context; plugins: ReturnType<typeof usePlugin> }) {
|
||||
const [locked, setLocked] = createSignal(false)
|
||||
const [focused, setFocused] = createSignal<string>()
|
||||
const [detail, setDetail] = createSignal<{ title: string; error: string }>()
|
||||
const dialog = useDialog()
|
||||
const options = createMemo(() => {
|
||||
const builtins = props.plugins
|
||||
const options = createMemo(() =>
|
||||
props.plugins
|
||||
.registered()
|
||||
.filter((plugin) => plugin.id !== id && plugin.source === "builtin")
|
||||
.filter((plugin) => plugin.id !== id)
|
||||
.sort((a, b) => a.id.localeCompare(b.id))
|
||||
.map(
|
||||
(plugin): DialogSelectOption<string> => ({
|
||||
title: plugin.id,
|
||||
value: plugin.id,
|
||||
category: "Built-in",
|
||||
category: plugin.source === "builtin" ? "Built-in" : "External",
|
||||
footer: (
|
||||
<span
|
||||
style={{
|
||||
@@ -33,46 +29,8 @@ function View(props: { context: Plugin.Context; plugins: ReturnType<typeof usePl
|
||||
</span>
|
||||
),
|
||||
}),
|
||||
)
|
||||
const external = props.plugins
|
||||
.list()
|
||||
.filter((plugin) => plugin.status !== "unsupported")
|
||||
.map(
|
||||
(plugin): DialogSelectOption<string> => ({
|
||||
title: plugin.id ?? plugin.target,
|
||||
value: plugin.id ?? plugin.target,
|
||||
category: "External",
|
||||
searchText: plugin.target,
|
||||
footer: (
|
||||
<span
|
||||
style={{
|
||||
fg:
|
||||
plugin.status === "active"
|
||||
? props.context.theme.text.feedback.success.default
|
||||
: plugin.status === "failed"
|
||||
? props.context.theme.text.feedback.error.default
|
||||
: props.context.theme.text.subdued,
|
||||
}}
|
||||
>
|
||||
{plugin.status}
|
||||
</span>
|
||||
),
|
||||
}),
|
||||
)
|
||||
return [...builtins, ...external].sort((a, b) => a.title.localeCompare(b.title))
|
||||
})
|
||||
|
||||
const failure = (value: string | undefined) =>
|
||||
props.plugins.list().find((plugin) => {
|
||||
if (plugin.status !== "failed") return false
|
||||
return (plugin.id ?? plugin.target) === value
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (focused()) return
|
||||
const first = options()[0]
|
||||
if (first) setFocused(first.value)
|
||||
})
|
||||
),
|
||||
)
|
||||
|
||||
const toggle = (plugin: DialogSelectOption<string>) => {
|
||||
if (locked()) return
|
||||
@@ -93,56 +51,15 @@ function View(props: { context: Plugin.Context; plugins: ReturnType<typeof usePl
|
||||
.finally(() => setLocked(false))
|
||||
}
|
||||
|
||||
const select = (plugin: DialogSelectOption<string>) => {
|
||||
const failed = failure(plugin.value)
|
||||
if (!failed || failed.status !== "failed") return toggle(plugin)
|
||||
setDetail({ title: failed.target, error: failed.error })
|
||||
}
|
||||
|
||||
return (
|
||||
<box>
|
||||
<Show
|
||||
when={detail()}
|
||||
fallback={
|
||||
<DialogSelect
|
||||
title="Plugins"
|
||||
options={options()}
|
||||
current={focused()}
|
||||
locked={locked()}
|
||||
preserveSelection={true}
|
||||
onMove={(option) => setFocused(option.value)}
|
||||
actions={[
|
||||
{
|
||||
title: "toggle",
|
||||
command: "plugins.toggle",
|
||||
disabled: (option) => {
|
||||
const failed = failure(option?.value)
|
||||
return Boolean(failed && !("id" in failed && failed.id))
|
||||
},
|
||||
onTrigger: toggle,
|
||||
},
|
||||
]}
|
||||
onSelect={select}
|
||||
footer={
|
||||
<Show when={failure(focused())}>
|
||||
<text fg={props.context.theme.text.subdued}>enter to view error</text>
|
||||
</Show>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{(item) => (
|
||||
<DialogErrorDetails
|
||||
title={`Plugin: ${item().title}`}
|
||||
error={item().error}
|
||||
onBack={() => {
|
||||
setDetail()
|
||||
dialog.setSize("medium")
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
</box>
|
||||
<DialogSelect
|
||||
title="Plugins"
|
||||
options={options()}
|
||||
locked={locked()}
|
||||
preserveSelection={true}
|
||||
actions={[{ title: "toggle", command: "plugins.toggle", onTrigger: toggle }]}
|
||||
onSelect={toggle}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -155,7 +72,6 @@ function Commands(props: { context: Plugin.Context }) {
|
||||
id: "plugins.list",
|
||||
title: "Plugins",
|
||||
group: "System",
|
||||
slash: { name: "plugins" },
|
||||
palette: true,
|
||||
run() {
|
||||
props.context.ui.dialog.show(() => <View context={props.context} plugins={plugins} />)
|
||||
|
||||
@@ -34,7 +34,7 @@ export interface PackageResolver {
|
||||
type State =
|
||||
| { readonly target: string; readonly id: string; readonly status: "active" | "inactive" }
|
||||
| { readonly target: string; readonly status: "unsupported" }
|
||||
| { readonly target: string; readonly id?: string; readonly status: "failed"; readonly error: string }
|
||||
| { readonly target: string; readonly status: "failed"; readonly error: string }
|
||||
|
||||
type RegisteredPlugin = {
|
||||
readonly id: string
|
||||
@@ -271,7 +271,6 @@ export function PluginProvider(props: ParentProps<{ packages: PackageResolver; d
|
||||
if (!local && !previous) npmFailures.set(target, resolved.error)
|
||||
failures.push({
|
||||
target,
|
||||
id: previous?.plugin.id,
|
||||
status: "failed",
|
||||
error: previous?.active ? `${resolved.error} (previous version still active)` : resolved.error,
|
||||
})
|
||||
@@ -377,7 +376,7 @@ export function PluginProvider(props: ParentProps<{ packages: PackageResolver; d
|
||||
// A failed reload keeps this item running; the failure entry covers it.
|
||||
if (failedTargets.has(item.target)) return []
|
||||
const error = errors.get(item.plugin.id)
|
||||
if (error) return [{ target: item.target, id: item.plugin.id, status: "failed", error }]
|
||||
if (error) return [{ target: item.target, status: "failed", error }]
|
||||
const status = store.registrations[item.plugin.id]?.active ? "active" : "inactive"
|
||||
return [{ target: item.target, id: item.plugin.id, status }]
|
||||
}),
|
||||
@@ -391,11 +390,7 @@ export function PluginProvider(props: ParentProps<{ packages: PackageResolver; d
|
||||
(prev) => prev.status === "failed" && prev.target === state.target && prev.error === state.error,
|
||||
)
|
||||
)
|
||||
host.toast.show({
|
||||
variant: "error",
|
||||
title: `Plugin failed: ${state.target}`,
|
||||
message: "Run /plugins to view details.",
|
||||
})
|
||||
host.toast.show({ variant: "error", title: "Plugin", message: `${state.target}: ${state.error}` })
|
||||
setStore("states", reconcileStore(states))
|
||||
}
|
||||
const slotItems = new WeakMap<SlotRender, Claim<SlotRender>>()
|
||||
|
||||
@@ -982,7 +982,12 @@ test("updates and removes queued inputs from durable lifecycle events", async ()
|
||||
await wait(() =>
|
||||
data.session.pending
|
||||
.list(sessionID)
|
||||
.some((item) => item.id === "message-queued" && item.type !== "compaction" && item.delivery === "steer"),
|
||||
.some(
|
||||
(item) =>
|
||||
item.id === "message-queued" &&
|
||||
(item.type === "user" || item.type === "synthetic") &&
|
||||
item.delivery === "steer",
|
||||
),
|
||||
)
|
||||
expect(rows).toContainEqual({ type: "message", messageID: "message-queued" })
|
||||
|
||||
@@ -996,7 +1001,12 @@ test("updates and removes queued inputs from durable lifecycle events", async ()
|
||||
await wait(() =>
|
||||
data.session.pending
|
||||
.list(sessionID)
|
||||
.some((item) => item.id === "message-queued" && item.type !== "compaction" && item.delivery === "queue"),
|
||||
.some(
|
||||
(item) =>
|
||||
item.id === "message-queued" &&
|
||||
(item.type === "user" || item.type === "synthetic") &&
|
||||
item.delivery === "queue",
|
||||
),
|
||||
)
|
||||
expect(rows).not.toContainEqual({ type: "message", messageID: "message-queued" })
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { homeFooterVisibility } from "../../src/feature-plugins/home/footer"
|
||||
|
||||
describe("home footer visibility", () => {
|
||||
test("keeps failure labels readable at the minimum supported width", () => {
|
||||
expect(homeFooterVisibility(44)).toEqual({ mcpCommand: false, pluginCommand: false, version: false })
|
||||
})
|
||||
|
||||
test("adds secondary hints as space becomes available", () => {
|
||||
expect(homeFooterVisibility(64)).toEqual({ mcpCommand: true, pluginCommand: false, version: true })
|
||||
expect(homeFooterVisibility(80)).toEqual({ mcpCommand: true, pluginCommand: true, version: true })
|
||||
})
|
||||
})
|
||||
@@ -20,12 +20,7 @@
|
||||
"./*": "./src/*.ts"
|
||||
},
|
||||
"imports": {
|
||||
"#global-roots": {
|
||||
"workerd": "./src/global-roots.workerd.ts",
|
||||
"default": "./src/global-roots.ts"
|
||||
},
|
||||
"#runtime-import": {
|
||||
"workerd": "./src/runtime/import.bun.ts",
|
||||
"bun": "./src/runtime/import.bun.ts",
|
||||
"node": "./src/runtime/import.node.ts",
|
||||
"default": "./src/runtime/import.bun.ts"
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import type { NonEmptyReadonlyArray } from "effect/Array"
|
||||
import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem"
|
||||
import * as NodePath from "@effect/platform-node/NodePath"
|
||||
import * as NodeSink from "@effect/platform-node/NodeSink"
|
||||
import * as NodeStream from "@effect/platform-node/NodeStream"
|
||||
import { NodeFileSystem, NodePath, NodeSink, NodeStream } from "@effect/platform-node"
|
||||
import { Deferred, Effect, Exit, FileSystem, Layer, Path, PlatformError, Predicate, Sink, Stream } from "effect"
|
||||
import type { Scope } from "effect"
|
||||
import { ChildProcess } from "effect/unstable/process"
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
// Deep imports: the @effect/platform-node barrel eagerly pulls in undici,
|
||||
// ioredis, and node:sqlite, which runtimes such as workerd cannot load.
|
||||
import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem"
|
||||
import * as NodePath from "@effect/platform-node/NodePath"
|
||||
import { NodeFileSystem, NodePath } from "@effect/platform-node"
|
||||
import { FileSystem, Path } from "effect"
|
||||
import { FetchHttpClient, HttpClient } from "effect/unstable/http"
|
||||
import { makeGlobalNode } from "./app-node.js"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem"
|
||||
import { NodeFileSystem } from "@effect/platform-node"
|
||||
import path, { dirname, isAbsolute, join, relative, sep } from "path"
|
||||
import { realpathSync } from "fs"
|
||||
import { readdir } from "fs/promises"
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
import { xdgCache, xdgConfig, xdgData, xdgState } from "xdg-basedir"
|
||||
|
||||
/** The XDG base directories that root opencode's global paths. */
|
||||
export function roots(app: string) {
|
||||
return {
|
||||
data: path.join(xdgData!, app),
|
||||
cache: path.join(xdgCache!, app),
|
||||
config: path.join(xdgConfig!, app),
|
||||
state: path.join(xdgState!, app),
|
||||
tmp: path.join(os.tmpdir(), app),
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user