From 1ffd528dfde1a289a2b00694b36ec565bcb7fb33 Mon Sep 17 00:00:00 2001 From: Kyle Mistele Date: Wed, 30 Sep 2026 13:07:58 -0700 Subject: [PATCH 1/6] feat(examples): add alchemy-cloudflare chat session example A fold chat host on Cloudflare deployed with Alchemy v2. Each session is a Durable Object named by its SessionId, with the fold event log stored in the object's SQLite. An alarm heartbeat keeps the object alive while a turn runs, and a turn cut off by a restart is nudged to continue (at most 3 times). HttpRouter routes reach the objects through a ChatSessions service. The model is OpenAI gpt-5.6-terra on medium reasoning, with the API key bound from the deploy environment. Co-Authored-By: Claude Opus 5.5 HumanLayer-Session: https://app.dev.codelayer.gg/sessions/01a0ef41-7f9a-7c78-83e6-b468fe8c440e --- bun.lock | 608 +++++++++++++++++- examples/alchemy-cloudflare/.gitignore | 2 + examples/alchemy-cloudflare/README.md | 35 + examples/alchemy-cloudflare/alchemy.run.ts | 14 + examples/alchemy-cloudflare/package.json | 28 + examples/alchemy-cloudflare/src/Api.ts | 48 ++ .../alchemy-cloudflare/src/ChatSession.ts | 140 ++++ .../alchemy-cloudflare/src/ChatSessions.ts | 50 ++ .../src/DurableObjectEventLog.ts | 147 +++++ examples/alchemy-cloudflare/src/Keepalive.ts | 44 ++ examples/alchemy-cloudflare/src/Worker.ts | 21 + examples/alchemy-cloudflare/test/Api.test.ts | 137 ++++ examples/alchemy-cloudflare/tsconfig.json | 4 + examples/alchemy-cloudflare/vitest.config.ts | 3 + package.json | 12 +- 15 files changed, 1273 insertions(+), 20 deletions(-) create mode 100644 examples/alchemy-cloudflare/.gitignore create mode 100644 examples/alchemy-cloudflare/README.md create mode 100644 examples/alchemy-cloudflare/alchemy.run.ts create mode 100644 examples/alchemy-cloudflare/package.json create mode 100644 examples/alchemy-cloudflare/src/Api.ts create mode 100644 examples/alchemy-cloudflare/src/ChatSession.ts create mode 100644 examples/alchemy-cloudflare/src/ChatSessions.ts create mode 100644 examples/alchemy-cloudflare/src/DurableObjectEventLog.ts create mode 100644 examples/alchemy-cloudflare/src/Keepalive.ts create mode 100644 examples/alchemy-cloudflare/src/Worker.ts create mode 100644 examples/alchemy-cloudflare/test/Api.test.ts create mode 100644 examples/alchemy-cloudflare/tsconfig.json create mode 100644 examples/alchemy-cloudflare/vitest.config.ts diff --git a/bun.lock b/bun.lock index 42bc127..18923e1 100644 --- a/bun.lock +++ b/bun.lock @@ -17,6 +17,23 @@ "vitest": "catalog:", }, }, + "examples/alchemy-cloudflare": { + "name": "@humanlayer/example-alchemy-cloudflare", + "version": "0.0.0", + "dependencies": { + "@effect/platform-bun": "catalog:", + "@effect/platform-node": "catalog:", + "@humanlayer/fold-core": "workspace:*", + "alchemy": "catalog:", + "effect": "catalog:", + }, + "devDependencies": { + "@effect/vitest": "catalog:", + "@humanlayer/fold-vitest-config": "workspace:*", + "typescript": "catalog:", + "vitest": "catalog:", + }, + }, "packages/effect-ai-anthropic": { "name": "@humanlayer/effect-ai-anthropic", "version": "0.0.0", @@ -244,6 +261,8 @@ }, }, "catalog": { + "@cloudflare/workers-types": "5.20260929.1", + "@effect/platform-bun": "4.0.0-rc.112", "@effect/platform-node": "4.0.0-rc.112", "@effect/vitest": "4.0.0-rc.112", "@kitlangton/terminal-control": "0.3.1", @@ -255,6 +274,7 @@ "@types/node": "26.1.0", "@types/turndown": "5.0.6", "@vitest/coverage-v8": "4.1.9", + "alchemy": "2.0.0-beta.76", "effect": "4.0.0-rc.112", "solid-js": "1.9.12", "turndown": "7.2.4", @@ -263,8 +283,54 @@ "yaml": "2.9.0", }, "packages": { + "@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.2.5", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-3NX/MpTdroi0aKz134A6RC2Gb2iXVECN4QaAXnvCIxxIm3C3AVB1mkUe8NaaiyvOpDfsrqWhYtj+Q6a62RrTsw=="], + + "@alchemy.run/cloudflare-runtime": ["@alchemy.run/cloudflare-runtime@2.0.0-beta.76", "", { "dependencies": { "@alchemy.run/node-utils": "2.0.0-beta.76", "@cloudflare/unenv-preset": "^2.16.0", "@puppeteer/browsers": "^3.2.0", "capnp-es": "^0.0.14", "magic-string": "^0.30.21", "sharp": "^0.35.3", "unenv": "^2.0.0-rc.24", "workerd": "1.20260704.1", "yauzl": "^3.4.0" }, "peerDependencies": { "@distilled.cloud/cloudflare": "1.0.0-rc.8", "@effect/platform-bun": ">=4.0.0-rc.112 || >=4.0.0", "@effect/platform-node": ">=4.0.0-rc.112 || >=4.0.0", "effect": ">=4.0.0-rc.112 || >=4.0.0", "rolldown": "1.2.5", "vite": "^7.0.0 || ^8.0.0" }, "optionalPeers": ["@effect/platform-bun", "@effect/platform-node", "rolldown", "vite"] }, "sha512-rH5BFh0Xq7aExpSpITyQbC+Jqfc9XgwJXidHVHTi1tNRC6/KAWg4tOzBfwiFIxljFLLYcApzlYFvLa/fTYcEjw=="], + + "@alchemy.run/floci": ["@alchemy.run/floci@2.0.0-beta.76", "", { "peerDependencies": { "effect": ">=4.0.0-rc.112 || >=4.0.0" } }, "sha512-UIKd+Hki3t5seDpLfyRBwWiNukKlbAqDCTOYBD+n49b/WQIgEWHhYXzGtnwC7OY/b3KAv5p7gsdn8IpPnRXR8g=="], + + "@alchemy.run/node-utils": ["@alchemy.run/node-utils@2.0.0-beta.76", "", {}, "sha512-KUAlGdoeWeMEToUP1V3I+mEf6FVzUjU+6Tc8savNvMsXN9t3uwZVqD1FeMzTMzMTJJijjxVljQTCtyoeE9W43w=="], + "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], + "@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="], + + "@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], + + "@aws-sdk/core": ["@aws-sdk/core@3.978.1", "", { "dependencies": { "@aws-sdk/types": "^3.974.6", "@aws-sdk/xml-builder": "^3.972.41", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.35.0", "@smithy/signature-v4": "^5.7.3", "@smithy/types": "^4.19.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-LbY9aGsEiznDWmUc30Nwv3aIX/+dbwTx8KfS0yOC3NPYMO+O91e6jkT1azf34FwjOndq8/Q+RcVVZz5xnerwdg=="], + + "@aws-sdk/credential-provider-cognito-identity": ["@aws-sdk/credential-provider-cognito-identity@3.972.71", "", { "dependencies": { "@aws-sdk/nested-clients": "^3.997.46", "@aws-sdk/types": "^3.974.6", "@smithy/core": "^3.35.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-rsxsi8tF3vMGdWdaHQNXceHBdb49442yR2Pe08K+Phoc/yICbptdsHwL25Wf0z5+hjhSS5a+hDJHP7Xul4zvHQ=="], + + "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.72", "", { "dependencies": { "@aws-sdk/core": "^3.978.1", "@aws-sdk/types": "^3.974.6", "@smithy/core": "^3.35.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-xTKO/FWJPozTIXbozVnVGoNBhaGba8TBcx+KyUjRVeOlXE+dUc7GTR1cLvu0uTdIdmemzaFbqqCshXeZA1fZew=="], + + "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.74", "", { "dependencies": { "@aws-sdk/core": "^3.978.1", "@aws-sdk/types": "^3.974.6", "@smithy/core": "^3.35.0", "@smithy/fetch-http-handler": "^5.8.0", "@smithy/node-http-handler": "^4.12.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-u91E/hT8f4d1xy0Jl7VG4nVKJ3lxbrZkoBTeSVoJdWBiSEUMwMS/9+e0H/aJVQV//Lt5wuzP+E69v4aRSsNTmw=="], + + "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.973.17", "", { "dependencies": { "@aws-sdk/core": "^3.978.1", "@aws-sdk/credential-provider-env": "^3.972.72", "@aws-sdk/credential-provider-http": "^3.972.74", "@aws-sdk/credential-provider-login": "^3.972.79", "@aws-sdk/credential-provider-process": "^3.972.72", "@aws-sdk/credential-provider-sso": "^3.973.16", "@aws-sdk/credential-provider-web-identity": "^3.972.78", "@aws-sdk/nested-clients": "^3.997.46", "@aws-sdk/types": "^3.974.6", "@smithy/core": "^3.35.0", "@smithy/credential-provider-imds": "^4.5.2", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-ged4KXdBkvIC81bLvNHHuQKdKak/VXhQTR1NWYTTqW0474nlmsxy9O/vlgTIohDDWH3xpBdtVMZRyjb+DnocDA=="], + + "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.79", "", { "dependencies": { "@aws-sdk/core": "^3.978.1", "@aws-sdk/nested-clients": "^3.997.46", "@aws-sdk/types": "^3.974.6", "@smithy/core": "^3.35.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-L+Z85anONJd8MaiuraO4wRxATCdEejBZ3K3eymzWI5JPXa9sOS9CkIm72PBKqXKX+Z9p9NGMX5AIMXm0LEflgw=="], + + "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.84", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.72", "@aws-sdk/credential-provider-http": "^3.972.74", "@aws-sdk/credential-provider-ini": "^3.973.17", "@aws-sdk/credential-provider-process": "^3.972.72", "@aws-sdk/credential-provider-sso": "^3.973.16", "@aws-sdk/credential-provider-web-identity": "^3.972.78", "@aws-sdk/types": "^3.974.6", "@smithy/core": "^3.35.0", "@smithy/credential-provider-imds": "^4.5.2", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-oHt854odINVwzwsh+c5x69j0ajm4DbqqqVJ+O1ECsCIZeMDAbzFpXItaqP7UZstJj/ATdTk/KFSH0LaNAgV+kA=="], + + "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.72", "", { "dependencies": { "@aws-sdk/core": "^3.978.1", "@aws-sdk/types": "^3.974.6", "@smithy/core": "^3.35.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-rLIp2xbMjX/k9/od7APpqq1ZgXXnV0pOL1Th3ZsL8Wu0TRtBsDTVS8iPqcfRFcHakFxPvR04OSTv2ka2qOb/2A=="], + + "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.973.16", "", { "dependencies": { "@aws-sdk/core": "^3.978.1", "@aws-sdk/nested-clients": "^3.997.46", "@aws-sdk/token-providers": "3.1138.0", "@aws-sdk/types": "^3.974.6", "@smithy/core": "^3.35.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-IGihaJfFZYacJJr/odqILCoK7W/mvrZ7cuK7ECn3sAu4vLC6u0V8bS7mCGbdugJ8Aum2tnvqmx0F2MRFp2rn9g=="], + + "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.78", "", { "dependencies": { "@aws-sdk/core": "^3.978.1", "@aws-sdk/nested-clients": "^3.997.46", "@aws-sdk/types": "^3.974.6", "@smithy/core": "^3.35.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-/y9WvNtlcPBGLR0qc1a+9J/xtYZfVczvLUOuXaVWylzttH7ewsxwHtjmiJSolNrVSDorIxHGHMU61CbonRkmwA=="], + + "@aws-sdk/credential-providers": ["@aws-sdk/credential-providers@3.1143.0", "", { "dependencies": { "@aws-sdk/core": "^3.978.1", "@aws-sdk/credential-provider-cognito-identity": "^3.972.71", "@aws-sdk/credential-provider-env": "^3.972.72", "@aws-sdk/credential-provider-http": "^3.972.74", "@aws-sdk/credential-provider-ini": "^3.973.17", "@aws-sdk/credential-provider-login": "^3.972.79", "@aws-sdk/credential-provider-node": "^3.972.84", "@aws-sdk/credential-provider-process": "^3.972.72", "@aws-sdk/credential-provider-sso": "^3.973.16", "@aws-sdk/credential-provider-web-identity": "^3.972.78", "@aws-sdk/nested-clients": "^3.997.46", "@aws-sdk/types": "^3.974.6", "@smithy/core": "^3.35.0", "@smithy/credential-provider-imds": "^4.5.2", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-1Tifq2jFMJy4JN6buq7fY8qlN8+3yZvD7aG9ZWRneRlwjvjurS56aftzMXTmBWSOa2eJHlLx4eNuFu6pLc6K5g=="], + + "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.46", "", { "dependencies": { "@aws-sdk/core": "^3.978.1", "@aws-sdk/signature-v4-multi-region": "^3.996.47", "@aws-sdk/types": "^3.974.6", "@smithy/core": "^3.35.0", "@smithy/fetch-http-handler": "^5.8.0", "@smithy/node-http-handler": "^4.12.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-oRxtBcka/JGHGs9l9p9IVajGoTP8vTPmoAzdHGy4Qcy9P5vPnDf6nhIeM/COQNY9k/OahImTRaLkHftoXvfcmQ=="], + + "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.47", "", { "dependencies": { "@aws-sdk/types": "^3.974.6", "@smithy/signature-v4": "^5.7.3", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-Zk08macMvQTHzQJCLJVkOlviVoqwYMrpXv4lmLN7b7sAbiMoOK7Go0NYdR5UeF+MW8LIbRmwrNy9u/5VvX1U5g=="], + + "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1138.0", "", { "dependencies": { "@aws-sdk/core": "^3.978.1", "@aws-sdk/nested-clients": "^3.997.46", "@aws-sdk/types": "^3.974.6", "@smithy/core": "^3.35.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-GpyAr0DD63YOEmYFM6Df+gJuIgC92MMTiBK4FTKfxii5MJ9ge20epR7LyroulscYlG89J+ZB2ivFDPjvfQhzdw=="], + + "@aws-sdk/types": ["@aws-sdk/types@3.974.6", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-v/clNZzZnDxGyvpHMOGpJKVXFAExJzUNAAjaWGdcx8QAcXLGwTaOkw33p5SHAi0YAioK32xB3hWwOekRVfmfKg=="], + + "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.41", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-ctjVSyCMegrWfXlx6VqzSBFI6UqmQ5ZlnfMhdLIiWmhoH8UAQxSCP5N3OpG7X3k4LnS7ou74C4mt20+bfTW2aQ=="], + + "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], + "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], "@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="], @@ -323,10 +389,60 @@ "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], + "@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@10.5.0", "", { "dependencies": { "@chevrotain/gast": "10.5.0", "@chevrotain/types": "10.5.0", "lodash": "4.17.21" } }, "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw=="], + + "@chevrotain/gast": ["@chevrotain/gast@10.5.0", "", { "dependencies": { "@chevrotain/types": "10.5.0", "lodash": "4.17.21" } }, "sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A=="], + + "@chevrotain/types": ["@chevrotain/types@10.5.0", "", {}, "sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A=="], + + "@chevrotain/utils": ["@chevrotain/utils@10.5.0", "", {}, "sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ=="], + + "@clack/core": ["@clack/core@1.5.1", "", { "dependencies": { "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-iHTrHA8MtVuLl2TfZySmcKv1qO2PoyC9Z7pfSDozEuV5vtY3/wcOPKJXlqJ5Oq2Cx5DDGQGAMVx6HZfRRoVEbQ=="], + + "@clack/prompts": ["@clack/prompts@1.8.1", "", { "dependencies": { "@clack/core": "1.5.1", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-dlT1m5e/0yUL0kRNcQn7yGLVThkgbB0Ga/1AmfDDC/8ik6AIiSf2QLQO2zPYvefsHP0aFgxO93cVLCCfDp7kzQ=="], + + "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.2", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": ">1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-JBP1+Z7ZSNG/d4mRP+y8VC5dka3tZVMLEZRvS+rzQ4DGV1EoxRFQckcJTTkXbHSQiTj0DtNI01Zwb/V2fX0mvQ=="], + + "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260704.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-XO+vvdhhTNZSsIWCkZ+JaE/JrFUmQAB0H0y/sVkAf32xa2TYBRvFUMwjSqf6WxtlxcKPQvmbLfpO/UQ0l/q4eQ=="], + + "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260704.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-6iI7nbOOO8PzEQ6UZVBZB/hv95m8jl0yvyjMuWrF5cJbiLb5zPw3KnpqvGi+aeOs2ZmUkc81i1FWKfXwLXzPRA=="], + + "@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260704.1", "", { "os": "linux", "cpu": "x64" }, "sha512-3mT0YHtxT7eLjghu3hKSJDUQoz+AYv8FM43nLPYhM0YiHOxr8OlmvnCb2Lp7v/U3bwd3Gnx7WEqjSppR583mGg=="], + + "@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260704.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Opo7cPTPg4x0WwK+eZSDszCyFLKQkOGNCWxF005HRTJ5SJH8h0K9KyrC1k4LBwx3haXSVi+E1eGqo1gZ1Hmhkg=="], + + "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260704.1", "", { "os": "win32", "cpu": "x64" }, "sha512-a97Ecnzhy04x3U052VKPCNp863F7Wf00WY7Ga5P0SbtYvaO1PDzylsHrvFMPKeiDFcOwqiredawmJ+v6bhIgeQ=="], + + "@cloudflare/workers-types": ["@cloudflare/workers-types@5.20260929.1", "", {}, "sha512-VlkFyzSqv2LeMK92eyyfnNvSTW4Nu37cR8T8OM4FF2xcNCsGy8/BxcNk9Jr4ZV0PlmKsCKvS49CfRZTVKBseUw=="], + + "@distilled.cloud/aws": ["@distilled.cloud/aws@1.0.0-rc.8", "", { "dependencies": { "@aws-crypto/crc32": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/credential-providers": "^3.994.0", "@aws-sdk/types": "^3.973.1", "@distilled.cloud/core": "1.0.0-rc.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "aws4fetch": "^1.0.20", "fast-xml-parser": "^5.3.4" }, "peerDependencies": { "effect": ">=4.0.0-rc.112 || >=4.0.0" } }, "sha512-eL4tRACU4TSykk/6+ItwUTZs93r3tRlrBn36ohqaUxqYYOfmssYlt/SLCzos4nlLgCxb+1T6BAstgYC848v1EQ=="], + + "@distilled.cloud/axiom": ["@distilled.cloud/axiom@1.0.0-rc.8", "", { "dependencies": { "@distilled.cloud/core": "1.0.0-rc.8" }, "peerDependencies": { "effect": ">=4.0.0-rc.112 || >=4.0.0" } }, "sha512-lczeM0FPuF1mLZu7V8vcj8Klm9b1VSo4A75PP2a4km/cjphDl9LKamUwrSGLEY9denmqd1nn4+g2Svw1UmGpEw=="], + + "@distilled.cloud/cloudflare": ["@distilled.cloud/cloudflare@1.0.0-rc.8", "", { "dependencies": { "@distilled.cloud/core": "1.0.0-rc.8" }, "peerDependencies": { "effect": ">=4.0.0-rc.112 || >=4.0.0" } }, "sha512-nhEyGOaupi/JmT0PS1DEMH5Ebwu004pvQv3Emzt9MJz3mF3LTdiuAnxP86rf3jsFtfa826TDokKArVMq55OKRA=="], + + "@distilled.cloud/core": ["@distilled.cloud/core@1.0.0-rc.8", "", { "peerDependencies": { "effect": ">=4.0.0-rc.112 || >=4.0.0" } }, "sha512-baZMSaBh9kEyAqdhO+UbkvVDK+wqyI8d0UW0paSp5CqVResOl7ydHU4tCbBUOLoxOrqLRWfmImKrhW9SY1R8qw=="], + + "@distilled.cloud/fly-io": ["@distilled.cloud/fly-io@1.0.0-rc.8", "", { "dependencies": { "@distilled.cloud/core": "1.0.0-rc.8" }, "peerDependencies": { "effect": ">=4.0.0-rc.112 || >=4.0.0" } }, "sha512-0HqVLIpBxmuvrIrnmziz8iK39F5cBxkHlz43WR2s+tFidbPlCzmnci6gxixxiwMBCzcmiRmv57xQxuQzgfDQMw=="], + + "@distilled.cloud/hetzner": ["@distilled.cloud/hetzner@1.0.0-rc.8", "", { "dependencies": { "@distilled.cloud/core": "1.0.0-rc.8" }, "peerDependencies": { "effect": ">=4.0.0-rc.112 || >=4.0.0" } }, "sha512-CAPj912qUQXMetyFvJEc7PJnSG4DR6RBvGXCOfQcJVgGTspt/IdT3iIeIInTUf6rgFRsqoGiRfS4ywTqWoqLFA=="], + + "@distilled.cloud/neon": ["@distilled.cloud/neon@1.0.0-rc.8", "", { "dependencies": { "@distilled.cloud/core": "1.0.0-rc.8" }, "peerDependencies": { "effect": ">=4.0.0-rc.112 || >=4.0.0" } }, "sha512-xIgxgoDyIs/G5lW8T2r4oyHpOcRzBO1wu7p46lU3ppuJ2rlJR4pmP9dF7KxStAVglr9yK3wmmmnraLoCAtd8Sg=="], + + "@distilled.cloud/planetscale": ["@distilled.cloud/planetscale@1.0.0-rc.8", "", { "dependencies": { "@distilled.cloud/core": "1.0.0-rc.8" }, "peerDependencies": { "effect": ">=4.0.0-rc.112 || >=4.0.0" } }, "sha512-D0mnkT2Gnk/SKWoz05rkf7+sngH8+5m5ugjQ1GycsFMfTxUVSIVUIHX9yrO5vFrTsMlNsmrMR+7a9R3ZMGCWkA=="], + + "@distilled.cloud/railway": ["@distilled.cloud/railway@1.0.0-rc.8", "", { "dependencies": { "@distilled.cloud/core": "1.0.0-rc.8" }, "peerDependencies": { "effect": ">=4.0.0-rc.112 || >=4.0.0" } }, "sha512-VW2YdbblVHhYww5JVtOjFlv3jBFl7xlt6/xTfkNxxgXgzJ5GYVlD3gPgBIpGy9Pcnv9rVRA6CL7QZeYeu47c9Q=="], + + "@effect/platform-bun": ["@effect/platform-bun@4.0.0-rc.112", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-rc.112" }, "peerDependencies": { "effect": "^4.0.0-rc.112" } }, "sha512-Y5n2HhV/vsbrJls9ukX42RL7zqXQxISHegWFsP9njsCyDmGTnq7QYSO82rglwrLPwtTrv5dkSRTuGw4+oXxgMg=="], + "@effect/platform-node": ["@effect/platform-node@4.0.0-rc.112", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-rc.112", "mime": "^4.1.0", "undici": "^8.10.0" }, "peerDependencies": { "effect": "^4.0.0-rc.112", "redis": ">=5.0.0 <7.0.0" } }, "sha512-/BMAcdNGQQskLmI0Zoa95KfTZkr9HV9N4NSxaSrusG6GeW6Ulp9KvZ+Rlaiw8lnOt43CXjFLdfll5/k5rxL4hQ=="], "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-rc.112", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.21.3" }, "peerDependencies": { "effect": "^4.0.0-rc.112" } }, "sha512-ttjz0xKamFN7vL8pNDYVwddJLjZvqKePc05djlz2VcdaKbLsnYbtMnL1rbOfHgEnIUSHGh7FkjaN4DM1Ov81sQ=="], + "@effect/sql-d1": ["@effect/sql-d1@4.0.0-rc.118", "", { "dependencies": { "@cloudflare/workers-types": "^5.20260926.1" }, "peerDependencies": { "effect": "^4.0.0-rc.118" } }, "sha512-V9Pma4c1ecRqQig3KaLTIV4AptUNgojWoIw55VgkDhQBtmrZhrd4V617/FNna11S7L1VePth5vUBNt/kFxs4iQ=="], + + "@effect/sql-sqlite-do": ["@effect/sql-sqlite-do@4.0.0-rc.118", "", { "peerDependencies": { "effect": "^4.0.0-rc.118" } }, "sha512-wuT52JOf5vqekrjprLDwNkjBk13xXB0rNUlbdPD645CajQmYgtAfpVlMv/329UNmsCUGoxUdYYglcl8xtpZlfQ=="], + "@effect/tsgo": ["@effect/tsgo@0.36.4", "", { "optionalDependencies": { "@effect/tsgo-darwin-arm64": "0.36.4", "@effect/tsgo-darwin-x64": "0.36.4", "@effect/tsgo-linux-arm": "0.36.4", "@effect/tsgo-linux-arm64": "0.36.4", "@effect/tsgo-linux-x64": "0.36.4", "@effect/tsgo-win32-arm64": "0.36.4", "@effect/tsgo-win32-x64": "0.36.4" }, "bin": { "effect-tsgo": "dist/effect-tsgo.cjs" } }, "sha512-fNmdUV6FgXnvIcG18AVS1SRXt7z9jsyBh5CKuJYmTsE+DgbLaWF0CevKHx7hQPcidDWRVmdJYfU66Jvc/ZEt6w=="], "@effect/tsgo-darwin-arm64": ["@effect/tsgo-darwin-arm64@0.36.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oOcWdKQucNch6G4CvidHEzmfgeKEPMvFcz+DXuKGonBLwVHX+i1VYGbZwccEOoiitOg7eWVl4e8S0qKMXg3/gg=="], @@ -345,12 +461,20 @@ "@effect/vitest": ["@effect/vitest@4.0.0-rc.112", "", { "peerDependencies": { "effect": "^4.0.0-rc.112", "vitest": ">=4.1.0 <5.0.0" } }, "sha512-mEKh/FI64mt8JK1/v9mpOrJYdnp+UFZdRUBMEdZMiKz7klg6NPqVgg/oeAGH6wOOQc2iAPcfc2H9BbAv1KyzMQ=="], + "@electric-sql/pglite": ["@electric-sql/pglite@0.3.15", "", {}, "sha512-Cj++n1Mekf9ETfdc16TlDi+cDDQF0W7EcbyRHYOAeZdsAe8M/FJg18itDTSwyHfar2WIezawM9o0EKaRGVKygQ=="], + + "@electric-sql/pglite-socket": ["@electric-sql/pglite-socket@0.0.20", "", { "peerDependencies": { "@electric-sql/pglite": "0.3.15" }, "bin": { "pglite-server": "dist/scripts/server.js" } }, "sha512-J5nLGsicnD9wJHnno9r+DGxfcZWh+YJMCe0q/aCgtG6XOm9Z7fKeite8IZSNXgZeGltSigM9U/vAWZQWdgcSFg=="], + + "@electric-sql/pglite-tools": ["@electric-sql/pglite-tools@0.2.20", "", { "peerDependencies": { "@electric-sql/pglite": "0.3.15" } }, "sha512-BK50ZnYa3IG7ztXhtgYf0Q7zijV32Iw1cYS8C+ThdQlwx12V5VZ9KRJ42y82Hyb4PkTxZQklVQA9JHyUlex33A=="], + "@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="], "@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], + "@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="], + "@humanlayer/effect-ai-anthropic": ["@humanlayer/effect-ai-anthropic@workspace:packages/effect-ai-anthropic"], "@humanlayer/effect-ai-openai": ["@humanlayer/effect-ai-openai@workspace:packages/effect-ai-openai"], @@ -359,6 +483,8 @@ "@humanlayer/effect-branded-id": ["@humanlayer/effect-branded-id@workspace:packages/effect-branded-id"], + "@humanlayer/example-alchemy-cloudflare": ["@humanlayer/example-alchemy-cloudflare@workspace:examples/alchemy-cloudflare"], + "@humanlayer/fold": ["@humanlayer/fold@workspace:packages/fold"], "@humanlayer/fold-agent": ["@humanlayer/fold-agent@workspace:packages/fold-agent"], @@ -377,6 +503,60 @@ "@humanlayer/fold-xai": ["@humanlayer/fold-xai@workspace:packages/fold-xai"], + "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], + + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg=="], + + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.4" }, "os": "darwin", "cpu": "x64" }, "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw=="], + + "@img/sharp-freebsd-wasm32": ["@img/sharp-freebsd-wasm32@0.35.5", "", { "dependencies": { "@img/sharp-wasm32": "0.35.5" }, "os": "freebsd" }, "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw=="], + + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw=="], + + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ=="], + + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.4", "", { "os": "linux", "cpu": "arm" }, "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw=="], + + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A=="], + + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.3.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ=="], + + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.3.4", "", { "os": "linux", "cpu": "none" }, "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q=="], + + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ=="], + + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.4", "", { "os": "linux", "cpu": "x64" }, "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw=="], + + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA=="], + + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.4", "", { "os": "linux", "cpu": "x64" }, "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg=="], + + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.4" }, "os": "linux", "cpu": "arm" }, "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ=="], + + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.4" }, "os": "linux", "cpu": "arm64" }, "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ=="], + + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA=="], + + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.4" }, "os": "linux", "cpu": "none" }, "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ=="], + + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.4" }, "os": "linux", "cpu": "s390x" }, "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA=="], + + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.4" }, "os": "linux", "cpu": "x64" }, "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog=="], + + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" }, "os": "linux", "cpu": "arm64" }, "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg=="], + + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.4" }, "os": "linux", "cpu": "x64" }, "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A=="], + + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.5", "", { "dependencies": { "@emnapi/runtime": "^1.11.3" } }, "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ=="], + + "@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.5", "", { "dependencies": { "@img/sharp-wasm32": "0.35.5" }, "cpu": "none" }, "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA=="], + + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.35.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q=="], + + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.5", "", { "os": "win32", "cpu": "x64" }, "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], @@ -395,8 +575,36 @@ "@kitlangton/terminal-control-linux-x64-gnu": ["@kitlangton/terminal-control-linux-x64-gnu@0.3.1", "", { "os": "linux", "cpu": "x64", "bin": { "termctrl": "bin/termctrl" } }, "sha512-09tVwkapRjgXddYPJ3L2PMMhV+49meD0KCuDnzh3/FpK9IuW7svbrvn830J8ztudfOThNIT91zxiddBAgA/aCw=="], + "@libsql/client": ["@libsql/client@0.17.4", "", { "dependencies": { "@libsql/core": "^0.17.4", "@libsql/hrana-client": "^0.10.0", "js-base64": "^3.7.5", "libsql": "^0.5.28", "promise-limit": "^2.7.0" } }, "sha512-lYayFWasDV78A+TjlEhr6ubb3odBV6OHjb+wdp8VQcyWWAEIjuwbCHaraEUS4m4yWoo0BvZo96It4VdzZRmRWw=="], + + "@libsql/core": ["@libsql/core@0.17.4", "", { "dependencies": { "js-base64": "^3.7.5" } }, "sha512-LqF9gIvnJ38nmAH1y/ChizHqDO/MO1wLgA96XrraulEEbqXxLjleSH92YWTolbuJKgPUmGu4aJk9W3UnAcxLOQ=="], + + "@libsql/darwin-arm64": ["@libsql/darwin-arm64@0.5.29", "", { "os": "darwin", "cpu": "arm64" }, "sha512-K+2RIB1OGFPYQbfay48GakLhqf3ArcbHqPFu7EZiaUcRgFcdw8RoltsMyvbj5ix2fY0HV3Q3Ioa/ByvQdaSM0A=="], + + "@libsql/darwin-x64": ["@libsql/darwin-x64@0.5.29", "", { "os": "darwin", "cpu": "x64" }, "sha512-OtT+KFHsKFy1R5FVadr8FJ2Bb1mghtXTyJkxv0trocq7NuHntSki1eUbxpO5ezJesDvBlqFjnWaYYY516QNLhQ=="], + + "@libsql/hrana-client": ["@libsql/hrana-client@0.10.0", "", { "dependencies": { "@libsql/isomorphic-ws": "^0.1.5", "js-base64": "^3.7.5" } }, "sha512-OoA4EMqRAC7kn7V2P6EQqRcpZf2W+AjsNIyCizBg339Tq/aMC7sRnzs3SklderhmQWAqEzvv8A2vhxVmWpkVvw=="], + + "@libsql/isomorphic-ws": ["@libsql/isomorphic-ws@0.1.5", "", { "dependencies": { "@types/ws": "^8.5.4", "ws": "^8.13.0" } }, "sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg=="], + + "@libsql/linux-arm-gnueabihf": ["@libsql/linux-arm-gnueabihf@0.5.29", "", { "os": "linux", "cpu": "arm" }, "sha512-CD4n4zj7SJTHso4nf5cuMoWoMSS7asn5hHygsDuhRl8jjjCTT3yE+xdUvI4J7zsyb53VO5ISh4cwwOtf6k2UhQ=="], + + "@libsql/linux-arm-musleabihf": ["@libsql/linux-arm-musleabihf@0.5.29", "", { "os": "linux", "cpu": "arm" }, "sha512-2Z9qBVpEJV7OeflzIR3+l5yAd4uTOLxklScYTwpZnkm2vDSGlC1PRlueLaufc4EFITkLKXK2MWBpexuNJfMVcg=="], + + "@libsql/linux-arm64-gnu": ["@libsql/linux-arm64-gnu@0.5.29", "", { "os": "linux", "cpu": "arm64" }, "sha512-gURBqaiXIGGwFNEaUj8Ldk7Hps4STtG+31aEidCk5evMMdtsdfL3HPCpvys+ZF/tkOs2MWlRWoSq7SOuCE9k3w=="], + + "@libsql/linux-arm64-musl": ["@libsql/linux-arm64-musl@0.5.29", "", { "os": "linux", "cpu": "arm64" }, "sha512-fwgYZ0H8mUkyVqXZHF3mT/92iIh1N94Owi/f66cPVNsk9BdGKq5gVpoKO+7UxaNzuEH1roJp2QEwsCZMvBLpqg=="], + + "@libsql/linux-x64-gnu": ["@libsql/linux-x64-gnu@0.5.29", "", { "os": "linux", "cpu": "x64" }, "sha512-y14V0vY0nmMC6G0pHeJcEarcnGU2H6cm21ZceRkacWHvQAEhAG0latQkCtoS2njFOXiYIg+JYPfAoWKbi82rkg=="], + + "@libsql/linux-x64-musl": ["@libsql/linux-x64-musl@0.5.29", "", { "os": "linux", "cpu": "x64" }, "sha512-gquqwA/39tH4pFl+J9n3SOMSymjX+6kZ3kWgY3b94nXFTwac9bnFNMffIomgvlFaC4ArVqMnOZD3nuJ3H3VO1w=="], + + "@libsql/win32-x64-msvc": ["@libsql/win32-x64-msvc@0.5.29", "", { "os": "win32", "cpu": "x64" }, "sha512-4/0CvEdhi6+KjMxMaVbFM2n2Z44escBRoEYpR+gZg64DdetzGnYm8mcNLcoySaDJZNaBd6wz5DNdgRmcI4hXcg=="], + "@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="], + "@mrleebo/prisma-ast": ["@mrleebo/prisma-ast@0.13.1", "", { "dependencies": { "chevrotain": "^10.5.0", "lilconfig": "^2.1.0" } }, "sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw=="], + "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ=="], "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w=="], @@ -411,8 +619,48 @@ "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="], + "@neon-rs/load": ["@neon-rs/load@0.0.4", "", {}, "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw=="], + "@noble/hashes": ["@noble/hashes@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="], + "@nodable/entities": ["@nodable/entities@3.1.0", "", {}, "sha512-LsS/DjHr+uDM647Gru/cA8+J3a3HfhttwCKLyuoyN7yXTFCxKBtSgMQBvrW9yNPa2/zDRUNuGPaH5QUFoa4arQ=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="], + + "@octokit/core": ["@octokit/core@7.0.8", "", { "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.5", "@octokit/request": "^10.0.16", "@octokit/request-error": "^7.1.2", "@octokit/types": "^18.0.0", "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-L7y8eYc+AwxGr2PWI4WFt1VG4TiJ66c26BD16mXpYIlXxG0SMigM1+m4aTSlYyBr5BlQsGAlz8uDCoZN4SEMcg=="], + + "@octokit/endpoint": ["@octokit/endpoint@11.0.5", "", { "dependencies": { "@octokit/types": "^18.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-iXa654H3yFafF/ieHkukfbgWo2rmXD2ceD0ZOtrPhw1bc3FDch1d9N/TNs0FQ1/cIbwb7kspUX8jzIs8nzb9DQ=="], + + "@octokit/graphql": ["@octokit/graphql@9.0.5", "", { "dependencies": { "@octokit/request": "^10.0.16", "@octokit/types": "^18.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-bt/hm03LeU6Vy7FwTrkkC9p3XGT/lBwClglMqxBSe5/q0E5CdJTXeAqEI0vlw89/LF/G6tryTIH8HirZ3prMVg=="], + + "@octokit/openapi-types": ["@octokit/openapi-types@29.0.1", "", {}, "sha512-9qWOMFNxxLokERcms42rU0PTLqQmVs7g5E41TI4mCOxmpFayD1rfC7XxOL55cG9MBZLFlC31BrR37myMKardwg=="], + + "@octokit/openapi-webhooks-types": ["@octokit/openapi-webhooks-types@12.1.0", "", {}, "sha512-WiuzhOsiOvb7W3Pvmhf8d2C6qaLHXrWiLBP4nJ/4kydu+wpagV5Fkz9RfQwV2afYzv3PB+3xYgp4mAdNGjDprA=="], + + "@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@14.0.0", "", { "dependencies": { "@octokit/types": "^16.0.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw=="], + + "@octokit/plugin-request-log": ["@octokit/plugin-request-log@6.0.0", "", { "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q=="], + + "@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@17.0.0", "", { "dependencies": { "@octokit/types": "^16.0.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw=="], + + "@octokit/request": ["@octokit/request@10.0.16", "", { "dependencies": { "@octokit/endpoint": "^11.0.5", "@octokit/request-error": "^7.1.2", "@octokit/types": "^18.0.0", "content-type": "^3.0.0", "json-with-bigint": "^3.5.12", "universal-user-agent": "^7.0.2" } }, "sha512-A0zWGjHzISIb+9ccG8s0dq7LKO5zVpJLRICjgUb+sJxEWqn8RUHB1rD3AE51+PECvXHIxqZ1VVvs4fHTSD9nUQ=="], + + "@octokit/request-error": ["@octokit/request-error@7.1.2", "", { "dependencies": { "@octokit/types": "^18.0.0" } }, "sha512-XZRuT3xZ84D3gYErI1DZvhJ33dCWVV6uzBtWkaBB4TvA/L6eOeTZodxLFVB44bBEEo3vEx7y00UfX1tBLrtLRg=="], + + "@octokit/rest": ["@octokit/rest@22.0.1", "", { "dependencies": { "@octokit/core": "^7.0.6", "@octokit/plugin-paginate-rest": "^14.0.0", "@octokit/plugin-request-log": "^6.0.0", "@octokit/plugin-rest-endpoint-methods": "^17.0.0" } }, "sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw=="], + + "@octokit/types": ["@octokit/types@18.0.0", "", { "dependencies": { "@octokit/openapi-types": "^29.0.1" } }, "sha512-l6bAF43PNxkJp6g+W4PjoUSSkxHomXw2nOum5CTftJz1NlV3vu93NImgOYtLf6CbBUb5j+fiuzW0PPQ5JTSvZA=="], + + "@octokit/webhooks": ["@octokit/webhooks@14.2.0", "", { "dependencies": { "@octokit/openapi-webhooks-types": "12.1.0", "@octokit/request-error": "^7.0.0", "@octokit/webhooks-methods": "^6.0.0" } }, "sha512-da6KbdNCV5sr1/txD896V+6W0iamFWrvVl8cHkBSPT+YlvmT3DwXa4jxZnQc+gnuTEqSWbBeoSZYTayXH9wXcw=="], + + "@octokit/webhooks-methods": ["@octokit/webhooks-methods@6.0.0", "", {}, "sha512-MFlzzoDJVw/GcbfzVC1RLR36QqkTLUf79vLVO3D+xn7r0QgxnFoLZgtrzxiQErAjFUOdH6fas2KeQJ1yr/qaXQ=="], + "@opentui/core": ["@opentui/core@0.4.3", "", { "dependencies": { "bun-ffi-structs": "0.2.4", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.4.3", "@opentui/core-darwin-x64": "0.4.3", "@opentui/core-linux-arm64": "0.4.3", "@opentui/core-linux-arm64-musl": "0.4.3", "@opentui/core-linux-x64": "0.4.3", "@opentui/core-linux-x64-musl": "0.4.3", "@opentui/core-win32-arm64": "0.4.3", "@opentui/core-win32-x64": "0.4.3" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-rrJfAk13tALDqldYjhc78eWQ+aKq1iknJgffIOg3OwyZoqQo+p6gtuqyhmWvXIfQzlNUbpgpCPcxbXlhMnlaHQ=="], "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.4.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-p5+7AAxpxGuDGagyQfewKtmTFnN7THvTVY4FyKqUtJomNaHdQXPHztapNNzMx0DGWbwOUbVKzpL+yc3CZY3chQ=="], @@ -437,7 +685,7 @@ "@opentui/solid": ["@opentui/solid@0.4.3", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.4.3", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-RcV0+S8HMdXOASyr7HmJUBuTUIaFPzAxMDa44VftS5C2JUgrmAuWo0Njv1q3TWRB1owjHnyKhEfWGKq7A82wxw=="], - "@oxc-project/types": ["@oxc-project/types@0.139.0", "", {}, "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw=="], + "@oxc-project/types": ["@oxc-project/types@0.146.0", "", {}, "sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA=="], "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.62.0", "", { "os": "android", "cpu": "arm" }, "sha512-pdsv0C4gPjJ8H1+sd8u0BDx+yLACTL+rgeMIOL1ln4ihSnhw8CWXtYWgvcSkyTfgGBIzFKab+d8rx9Xl4en/Kw=="], @@ -531,6 +779,16 @@ "@paralleldrive/cuid2": ["@paralleldrive/cuid2@3.3.0", "", { "dependencies": { "@noble/hashes": "^2.0.1", "bignumber.js": "^9.3.1", "error-causes": "^3.0.2" }, "bin": { "cuid2": "bin/cuid2.js" } }, "sha512-OqiFvSOF0dBSesELYY2CAMa4YINvlLpvKOz/rv6NeZEqiyttlHgv98Juwv4Ch+GrEV7IZ8jfI2VcEoYUjXXCjw=="], + "@prisma/debug": ["@prisma/debug@7.2.0", "", {}, "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw=="], + + "@prisma/dev": ["@prisma/dev@0.20.0", "", { "dependencies": { "@electric-sql/pglite": "0.3.15", "@electric-sql/pglite-socket": "0.0.20", "@electric-sql/pglite-tools": "0.2.20", "@hono/node-server": "1.19.9", "@mrleebo/prisma-ast": "0.13.1", "@prisma/get-platform": "7.2.0", "@prisma/query-plan-executor": "7.2.0", "foreground-child": "3.3.1", "get-port-please": "3.2.0", "hono": "4.11.4", "http-status-codes": "2.3.0", "pathe": "2.0.3", "proper-lockfile": "4.1.2", "remeda": "2.33.4", "std-env": "3.10.0", "valibot": "1.2.0", "zeptomatch": "2.1.0" } }, "sha512-ovlBYwWor0OzG+yH4J3Ot+AneD818BttLA+Ii7wjbcLHUrnC4tbUPVGyNd3c/+71KETPKZfjhkTSpdS15dmXNQ=="], + + "@prisma/get-platform": ["@prisma/get-platform@7.2.0", "", { "dependencies": { "@prisma/debug": "7.2.0" } }, "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA=="], + + "@prisma/query-plan-executor": ["@prisma/query-plan-executor@7.2.0", "", {}, "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ=="], + + "@puppeteer/browsers": ["@puppeteer/browsers@3.2.3", "", { "dependencies": { "modern-tar": "^0.8.4", "yargs": "^18.0.0" }, "peerDependencies": { "proxy-agent": ">=8.0.1", "yauzl": "^2.10.0 || ^3.4.0" }, "optionalPeers": ["proxy-agent", "yauzl"], "bin": { "browsers": "lib/main-cli.js" } }, "sha512-2Bt3m6dDAJqmZehn0wiXSYyQmuhyiHHZ7FjvrVaS1W2RoFGV6eCK71UvKYkNJjkwC1Q9nobjhvTMhLQztFVINA=="], + "@redis/bloom": ["@redis/bloom@6.2.1", "", { "peerDependencies": { "@redis/client": "^6.2.1" } }, "sha512-huQgNLaCIZfQ9SeLn4q9124uOUd8HbZDYHwwUzNcRgHqCHiHKl2dDxMqJCeWh8cMqZAoWuHR8XnWbDMIf+o7ag=="], "@redis/client": ["@redis/client@6.2.1", "", { "dependencies": { "cluster-key-slot": "1.1.2" }, "peerDependencies": { "@node-rs/xxhash": "^1.1.0", "@opentelemetry/api": ">=1 <2" }, "optionalPeers": ["@node-rs/xxhash", "@opentelemetry/api"] }, "sha512-LzxBY7SIBvvJiyCgcaJZZakE3fJrZZ++i24+EDW9fKpCl68D35uJcKFpZZwCfOoG9WZTbyZlMzMeM0gtOAMU9Q=="], @@ -541,44 +799,72 @@ "@redis/time-series": ["@redis/time-series@6.2.1", "", { "peerDependencies": { "@redis/client": "^6.2.1" } }, "sha512-kiYniph04dJOole+L359B6C9E+jYS2uDP7hca6Onj0xF38ZIpyxARO0Iq0W4ZRn1e8Q6vqW00QFZVSMRA/2Ijw=="], - "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.5", "", { "os": "android", "cpu": "arm64" }, "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ=="], + "@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.5", "", { "os": "android", "cpu": "arm" }, "sha512-DLe/i+l8ynIBY7XEQ191TeZvCoowIGa18R+dIV30GW7DiOtp74i/xX8hs8GUjW5ARV7VZuie3d6AumSmCwbeRA=="], + + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.5", "", { "os": "android", "cpu": "arm64" }, "sha512-zXcwKlQApYAOELHd8PwKDFkagYF9Wy4e0RJ+0qnzl9Pjnpj75TEG8ufv40p2J7kCEfwZAsNiuzRIyNNMWT38ig=="], - "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.1.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw=="], + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-dK4QakI42nzWgJT5sm4y4y/O//D4OxM75/cH28RLV+nzIN9AY+YsbuUVrUTjlLjXR6vpyxFbSsbmNuJ6BP9sww=="], - "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.1.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g=="], + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-fqSALaUu1Wjd1nK2uW2kJDWdLCc8lx1IcY+MTY26Aurfdx19anlzhqXOgCFbBFQnlFDTn4TC1/7Nz4Bl2mLP3A=="], - "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.1.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA=="], + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-/vCnNxlkxs9tKxNDcyWUePpJ/PgTzxIaVhoM5SmG8UV+GR/IcPam4VYxi7GIMo7PSDuNqlJqvprqii9NqqVCMw=="], - "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.1.5", "", { "os": "linux", "cpu": "arm" }, "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw=="], + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.5", "", { "os": "linux", "cpu": "arm" }, "sha512-abk0NLA519LxRCszmbE0jYKuQ9YPocOXTiOXOo6Yr+YAT95VH+PtqYAjOJvGKt3viEd/x4qzabAlwd5bHOOARg=="], - "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q=="], + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Y7eALiJ8lr0M2HH103Js+g7V34wf6snlpZLAsHI90uLhr3PVlNsbFVAXJC9d/V6BnPyKtpSwI+NcB/RLxsQxuA=="], - "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA=="], + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-xMvZgnbZg4YVnR/AX2b3oOPDTFYJvUVaJg5FedA/LuvexAtXibZQej4cnTkw3rjsJ/ggUROB64TdtETiim+FYA=="], - "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.1.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg=="], + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-GRjeqTUDHTo5GwntsLaAMcBahG3nlpjftXWZLN73HiYQlhwEowvarFgQnRnQZtIp4keXX7quXFbG38uPZBa2EA=="], - "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.1.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA=="], + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-vLNTR45F2Uwc8AufkNXPmB4VliaXs+FvcheEogIzOXzO4l+LzieXF5A/TWxLy5HtqpsRCHUfd0lPVrrdgXdLHQ=="], - "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ=="], + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.5", "", { "os": "linux", "cpu": "x64" }, "sha512-Mgj59/HTuYeK9Gz2MA+mBWKnHsAgkBSec15ZMb1st3oIfFbX7gCjOae7GydHhzcyQi9Z/7M1QuN9bR3oFqF0jQ=="], - "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg=="], + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.5", "", { "os": "linux", "cpu": "x64" }, "sha512-mY8AP0/ichsbhAxGnLa3d3+MwV0EfgrPND2bplI3Ym8T6R2pJ0N87bvrKVwNXmdy3jnr6eQBecdqx/HMknBmpA=="], - "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.1.5", "", { "os": "none", "cpu": "arm64" }, "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw=="], + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.5", "", { "os": "none", "cpu": "arm64" }, "sha512-8SLssA2oweAxyRgDp789ACfRb/3P+zNRJpzZxSizxF9m8NUDQ4+3xjo8ttjhVGGw6Qxb70oZiEtIjaKikCO7Yw=="], "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.1.5", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA=="], - "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.1.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw=="], + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-vGbruD5zquhoc8D9SViXgN2FBJtNdTyQ4DtG+SWiEGlJiAzoKcZ2xp+xuXCffhubVdt0NJlTZqkeRuERy7g8Cw=="], - "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.1.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA=="], + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.5", "", { "os": "win32", "cpu": "x64" }, "sha512-e/SXpgISz+IoqVcSSI0rx/d/he8zqLex+/rCWpnHpmVfmPIUjag9H6P7zotf0gJHwPUhQxZ/mF8tr6acebT9yw=="], "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], "@silvia-odwyer/photon-node": ["@silvia-odwyer/photon-node@0.3.4", "", {}, "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA=="], + "@smithy/core": ["@smithy/core@3.35.0", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-zRMhfkByhT2snNdr1si24vJitU6Cr9ix2MikUfWmkAgp4jrNP0GcKSP5YvwQ+TlI8AZXER5QOGJn3JsVtSD9/A=="], + + "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.5.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg=="], + + "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.8.0", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.18.0", "tslib": "^2.6.2" } }, "sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA=="], + + "@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + + "@smithy/node-config-provider": ["@smithy/node-config-provider@4.6.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "tslib": "^2.6.2" } }, "sha512-zMrXu/O5tPa7GLtra8L4wFG6DACcXT9QV4Ay+WEAjUhXm1dVq7c/q9Qv9gkJZNLY8hmQKg08778kDcxpKNMqOA=="], + + "@smithy/node-http-handler": ["@smithy/node-http-handler@4.12.1", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.18.0", "tslib": "^2.6.2" } }, "sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw=="], + + "@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.7.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "tslib": "^2.6.2" } }, "sha512-XsIDj5gVG4YRxGS4n4TiBDAogPWRHXKZyor6JW/sEuaa/7BvKADe9j45jI2dPYCnYbKkLBmbZ4qN9ns0sH+kMQ=="], + + "@smithy/signature-v4": ["@smithy/signature-v4@5.7.4", "", { "dependencies": { "@smithy/core": "^3.35.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ=="], + + "@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + + "@smithy/util-base64": ["@smithy/util-base64@4.6.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "tslib": "^2.6.2" } }, "sha512-wTQX1hPfElIqY6AzM/s6c4UgpMxCL4MwJ5u6340ksLPq78lur6Y+keheIDk5gMX4G3KFh4MERcDt6xhRq+ie1Q=="], + + "@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + + "@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="], + "@types/aws-lambda": ["@types/aws-lambda@8.10.164", "", {}, "sha512-XOnrazWcOd6yWPnR7DxqCBPPYciDjHq+NN8LPwwqBCJNXLyPJgKo47fZpJ113+oyAn9Zti0Cf6H4k6yPcE3WFg=="], + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], @@ -651,12 +937,24 @@ "@vitest/utils": ["@vitest/utils@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA=="], + "alchemy": ["alchemy@2.0.0-beta.76", "", { "dependencies": { "@alchemy.run/cloudflare-runtime": "2.0.0-beta.76", "@alchemy.run/floci": "2.0.0-beta.76", "@alchemy.run/node-utils": "2.0.0-beta.76", "@aws-sdk/credential-providers": "^3.0.0", "@clack/prompts": "^1.7.0", "@distilled.cloud/aws": "1.0.0-rc.8", "@distilled.cloud/axiom": "1.0.0-rc.8", "@distilled.cloud/cloudflare": "1.0.0-rc.8", "@distilled.cloud/core": "1.0.0-rc.8", "@distilled.cloud/fly-io": "1.0.0-rc.8", "@distilled.cloud/hetzner": "1.0.0-rc.8", "@distilled.cloud/neon": "1.0.0-rc.8", "@distilled.cloud/planetscale": "1.0.0-rc.8", "@distilled.cloud/railway": "1.0.0-rc.8", "@effect/sql-d1": ">=4.0.0-rc.112 || >=4.0.0", "@effect/sql-sqlite-do": ">=4.0.0-rc.112 || >=4.0.0", "@effect/vitest": ">=4.0.0-rc.112 || >=4.0.0", "@libsql/client": "^0.17.0", "@octokit/rest": "^22.0.1", "@octokit/webhooks": "^14.2.0", "@prisma/dev": "^0.20.0", "@smithy/node-config-provider": "^4.0.0", "@smithy/shared-ini-file-loader": "^4.3.4", "@smithy/types": "^4.8.1", "@types/aws-lambda": "^8.10.152", "aws4fetch": "^1.0.20", "capnweb": "^0.6.1", "fast-glob": "^3.3.2", "fast-xml-parser": "^5.3.4", "ink": "^6.3.1", "jszip": "^3.10.1", "libsodium-wrappers": "^0.8.3", "pathe": "^2.0.3", "picomatch": "^4.0.4", "react": "^19.2.0", "rolldown": "1.2.5", "undici": "^7.16.0", "yaml": "^2.0.0" }, "peerDependencies": { "@alchemy.run/frontend-frameworks": "2.0.0-beta.76", "@aws/durable-execution-sdk-js": "^2.1.0", "@effect/platform-bun": ">=4.0.0-rc.112 || >=4.0.0", "@effect/platform-node": ">=4.0.0-rc.112 || >=4.0.0", "@effect/sql-mysql2": ">=4.0.0-rc.112 || >=4.0.0", "@effect/sql-pg": ">=4.0.0-rc.112 || >=4.0.0", "@vercel/nft": "^1.10.2", "drizzle-kit": "1.0.0-rc.5-ab785fc", "drizzle-orm": "1.0.0-rc.5-ab785fc", "effect": ">=4.0.0-rc.112 || >=4.0.0", "mongodb": "^6.10.0", "mysql2": "^3.24.2", "pg": "^8.22.0", "vite": "^8.0.7", "ws": "^8.20.0" }, "optionalPeers": ["@alchemy.run/frontend-frameworks", "@aws/durable-execution-sdk-js", "@effect/platform-bun", "@effect/platform-node", "@effect/sql-mysql2", "@effect/sql-pg", "@vercel/nft", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "pg", "vite", "ws"], "bin": { "alchemy": "./bin/cli.js" } }, "sha512-XpcsXWk38YV9nO0v7s5PPjlMVtF8OQPeP8YqLu1iMzrq33bwViEOqkwQn3cgFiD7/QsnNcCju+hCXyeBwXrdtg=="], + + "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], + "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "anynum": ["anynum@1.0.1", "", {}, "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A=="], + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], "ast-v8-to-istanbul": ["ast-v8-to-istanbul@1.0.5", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^10.0.0" } }, "sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA=="], + "auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="], + + "aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="], + "babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.7", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-/O6JWUmjv03OI9lL2ry9bUjpD5S3PclM55RRJEyCdcFZ5W2SEA/59d+l2hNsk3gI6kiWRdRPdOtqZmsQzFN1pQ=="], "babel-plugin-module-resolver": ["babel-plugin-module-resolver@5.0.2", "", { "dependencies": { "find-babel-config": "^2.1.1", "glob": "^9.3.3", "pkg-up": "^3.1.0", "reselect": "^4.1.7", "resolve": "^1.22.8" } }, "sha512-9KtaCazHee2xc0ibfqsDeamwDps6FZNo5S0Q81dUqEuFzVwPhcT4J5jOqIVvgCA3Q/wO9hKYxN/Ds3tIsp5ygg=="], @@ -667,10 +965,16 @@ "baseline-browser-mapping": ["baseline-browser-mapping@2.10.43", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ=="], + "before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="], + "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], + "bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="], + "brace-expansion": ["brace-expansion@2.1.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA=="], + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + "browserslist": ["browserslist@4.28.6", "", { "dependencies": { "baseline-browser-mapping": "^2.10.42", "caniuse-lite": "^1.0.30001803", "electron-to-chromium": "^1.5.389", "node-releases": "^2.0.51", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw=="], "bun-ffi-structs": ["bun-ffi-structs@0.2.4", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-AJzsqoVFs1KBbJbWHIYrVZLDC3NhTqqh25awRXqzoLzmBAKr5oqk6+CwuYHAekKx+VBCYVohBoKuRq40dV+TYg=="], @@ -679,12 +983,38 @@ "caniuse-lite": ["caniuse-lite@1.0.30001805", "", {}, "sha512-52noaS3DubycKSXaU30TwPGIp+POyQSUVa5jBEq3vkRkY0kjyb3LQgvhU6WGyCcyXqVLWO0Cw0Q6BSdD0kUfVA=="], + "capnp-es": ["capnp-es@0.0.14", "", { "peerDependencies": { "typescript": "^5.7.3" }, "optionalPeers": ["typescript"], "bin": { "capnp-es": "dist/compiler/capnpc-js.mjs", "capnpc-js": "dist/compiler/capnpc-js.mjs", "capnpc-ts": "dist/compiler/capnpc-ts.mjs", "capnpc-dts": "dist/compiler/capnpc-dts.mjs" } }, "sha512-8lWj4GJISiqRSlAJGkWpI4Azib7QY5UDIkqxeHcI7aAnXVk9SFuWxl1Fme+2HhzNANV0WTJqwUZvvXvloc3sBA=="], + + "capnweb": ["capnweb@0.6.1", "", {}, "sha512-fmhV26QPd1ewf5R74h55oVZnGwIcSaRMzbfLQUy8+zOBjuTmT3KXoT8wxHvnp1m9Ht9BoUUS5ZwNLoVLfQTyBg=="], + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "chevrotain": ["chevrotain@10.5.0", "", { "dependencies": { "@chevrotain/cst-dts-gen": "10.5.0", "@chevrotain/gast": "10.5.0", "@chevrotain/types": "10.5.0", "@chevrotain/utils": "10.5.0", "lodash": "4.17.21", "regexp-to-ast": "0.5.0" } }, "sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A=="], + + "cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="], + + "cli-cursor": ["cli-cursor@4.0.0", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="], + + "cli-truncate": ["cli-truncate@5.2.0", "", { "dependencies": { "slice-ansi": "^8.0.0", "string-width": "^8.2.0" } }, "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw=="], + + "cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="], + "cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="], + "code-excerpt": ["code-excerpt@4.0.0", "", { "dependencies": { "convert-to-spaces": "^2.0.1" } }, "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA=="], + + "content-type": ["content-type@3.1.1", "", {}, "sha512-GW4qUsfFo59d0HbUibDlWv5wPz+vAAcaTWbKIuKCf0JkC7wWkSyf8f13IpXn5JkeMlB2P8iTSSIjwXljorg2vA=="], + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + "convert-to-spaces": ["convert-to-spaces@2.0.1", "", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="], + + "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], @@ -701,26 +1031,50 @@ "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], + "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], + "error-causes": ["error-causes@3.0.2", "", {}, "sha512-i0B8zq1dHL6mM85FGoxaJnVtx6LD5nL2v0hlpGdntg5FOSyzQ46c9lmz5qx0xRS2+PWHGOHcYxGIBC5Le2dRMw=="], "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], "es-module-lexer": ["es-module-lexer@2.3.1", "", {}, "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA=="], + "es-toolkit": ["es-toolkit@1.52.0", "", {}, "sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA=="], + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], "expect-type": ["expect-type@1.4.0", "", {}, "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA=="], "fast-check": ["fast-check@4.9.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg=="], + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="], + + "fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="], + + "fast-wrap-ansi": ["fast-wrap-ansi@0.2.2", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q=="], + + "fast-xml-builder": ["fast-xml-builder@1.3.1", "", { "dependencies": { "path-expression-matcher": "^1.6.2", "xml-naming": "^0.3.0" } }, "sha512-pIM/1n3ntFXKYrUZwW7QCK0gAW7XY+wzj1YMIV3tLDvPj/V+zTGJK5e3/4WJfwj0qWw2ElNXiTixda/R+3YSug=="], + + "fast-xml-parser": ["fast-xml-parser@5.11.2", "", { "dependencies": { "@nodable/entities": "^3.0.1", "fast-xml-builder": "^1.2.0", "is-unsafe": "^2.0.0", "path-expression-matcher": "^1.6.2", "strnum": "^2.4.2", "xml-naming": "^0.3.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-R9iDuNrQYeQut46cn2r2wHKn4HYzVDvDm5J1wW+koZewykv0yuO2HChTYeZtrULyHIDM9cj9TUXloCsueCQUog=="], + + "fastq": ["fastq@1.20.3", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + "find-babel-config": ["find-babel-config@2.1.2", "", { "dependencies": { "json5": "^2.2.3" } }, "sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg=="], "find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="], + "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], @@ -729,32 +1083,86 @@ "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="], + "get-port-please": ["get-port-please@3.2.0", "", {}, "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A=="], + "glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="], + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "grammex": ["grammex@3.1.13", "", {}, "sha512-LnPnhOBLEJEVKS8WFDVaA397L9Kq55Q9oSITJiVLHVdhAclfUkWzQv74KhvZHKL2Q09Pb1XdsrOsZ4LfTFFTEg=="], + + "graphmatch": ["graphmatch@1.1.1", "", {}, "sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg=="], + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], + "hono": ["hono@4.11.4", "", {}, "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA=="], + "html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="], "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], + "http-status-codes": ["http-status-codes@2.3.0", "", {}, "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA=="], + + "immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="], + + "indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ink": ["ink@6.8.0", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.4", "ansi-escapes": "^7.3.0", "ansi-styles": "^6.2.1", "auto-bind": "^5.0.1", "chalk": "^5.6.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", "cli-truncate": "^5.1.1", "code-excerpt": "^4.0.0", "es-toolkit": "^1.39.10", "indent-string": "^5.0.0", "is-in-ci": "^2.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.33.0", "scheduler": "^0.27.0", "signal-exit": "^3.0.7", "slice-ansi": "^8.0.0", "stack-utils": "^2.0.6", "string-width": "^8.1.1", "terminal-size": "^4.0.1", "type-fest": "^5.4.1", "widest-line": "^6.0.0", "wrap-ansi": "^9.0.0", "ws": "^8.18.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=19.0.0", "react": ">=19.0.0", "react-devtools-core": ">=6.1.2" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-sbl1RdLOgkO9isK42WCZlJCFN9hb++sX9dsklOvfd1YQ3bQ2AiFu12Q6tFlr0HvEUvzraJntQCCpfEoUe9DSzA=="], + "is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="], + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-in-ci": ["is-in-ci@2.0.0", "", { "bin": { "is-in-ci": "cli.js" } }, "sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-unsafe": ["is-unsafe@2.0.2", "", {}, "sha512-HgbIHPBH0KHHCcjLfGsCvhtPTVxjaAZlXjwdz7/GQC40SjSe4sfQsar8J5VFo8JOSbarkpV0OLG95bbaNd9aAQ=="], + + "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="], "istanbul-lib-report": ["istanbul-lib-report@3.0.1", "", { "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw=="], "istanbul-reports": ["istanbul-reports@3.2.0", "", { "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" } }, "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA=="], + "js-base64": ["js-base64@3.9.4", "", {}, "sha512-PtOMXpEGuP0RRiRXsjzHzl44dMHxSu2CPvAhinupR1tBa88me+1DPqsobl7eupn5UukjLkln1ZnAOAOXOrYG0Q=="], + "js-tokens": ["js-tokens@10.0.0", "", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="], "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + "json-with-bigint": ["json-with-bigint@3.5.12", "", {}, "sha512-uwbF/wSSuOgC7qqlq27Xp5B6a2MHVug3t0idZdTqu0JnlFvgJuH7ju+KAk/J06C7GfhoYy2gnb9wz2INqcne7w=="], + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + "jszip": ["jszip@3.10.2", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-3l+rb15IOWtUhU0H5MFqES/T6Kh7abYwjosBey/vD6hDt8zoEffkSC5Ws5SGtgVw3gBx2NEbhTeSW1+kWkpyTQ=="], + + "libsodium": ["libsodium@0.8.4", "", {}, "sha512-lMcYaRi0zcs7tarATsQUYC7rstliIXZuoq0c6zXSgNtSNtdvBgkSegjWhpMJAXzKX3SUSwIp7+zEsob+j3LuRw=="], + + "libsodium-wrappers": ["libsodium-wrappers@0.8.4", "", { "dependencies": { "libsodium": "^0.8.0" } }, "sha512-mu8aAWucZjTB5O/BtGXtW4e1agy7uHxNYG7zPthmmD1jU43LCDmSWZLN4JhflbdPXj3yDO4lxM1O9hLDgIOXDw=="], + + "libsql": ["libsql@0.5.29", "", { "dependencies": { "@neon-rs/load": "^0.0.4", "detect-libc": "2.0.2" }, "optionalDependencies": { "@libsql/darwin-arm64": "0.5.29", "@libsql/darwin-x64": "0.5.29", "@libsql/linux-arm-gnueabihf": "0.5.29", "@libsql/linux-arm-musleabihf": "0.5.29", "@libsql/linux-arm64-gnu": "0.5.29", "@libsql/linux-arm64-musl": "0.5.29", "@libsql/linux-x64-gnu": "0.5.29", "@libsql/linux-x64-musl": "0.5.29", "@libsql/win32-x64-msvc": "0.5.29" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "arm", "x64", "arm64", ] }, "sha512-8lMP8iMgiBzzoNbAPQ59qdVcj6UaE/Vnm+fiwX4doX4Narook0a4GPKWBEv+CR8a1OwbfkgL18uBfBjWdF0Fzg=="], + + "lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="], + "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], @@ -779,8 +1187,12 @@ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + "lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="], + "locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="], + "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], @@ -791,12 +1203,20 @@ "marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="], + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "mime": ["mime@4.1.0", "", { "bin": { "mime": "bin/cli.js" } }, "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw=="], + "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + "minimatch": ["minimatch@8.0.7", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg=="], "minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="], + "modern-tar": ["modern-tar@0.8.5", "", {}, "sha512-snEhs+6G5Tjd4I7tLCDOaoln2RgE0bD19RzEKgvgK2hZ5VKy3MpLhLTZ2fWpXSTg4K2cyPwp+VHATFJhxfnOeA=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "msgpackr": ["msgpackr@2.1.0", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-p/pBCVO63CsvvpkomUnNNag6+n38rULuDA6HHe70o2gtC8ODI52foF/4ko2qQcp6OiErJXTmrZeXmsGGHsIQNQ=="], @@ -811,6 +1231,8 @@ "obug": ["obug@2.1.3", "", {}, "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg=="], + "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + "oxfmt": ["oxfmt@0.62.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.62.0", "@oxfmt/binding-android-arm64": "0.62.0", "@oxfmt/binding-darwin-arm64": "0.62.0", "@oxfmt/binding-darwin-x64": "0.62.0", "@oxfmt/binding-freebsd-x64": "0.62.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.62.0", "@oxfmt/binding-linux-arm-musleabihf": "0.62.0", "@oxfmt/binding-linux-arm64-gnu": "0.62.0", "@oxfmt/binding-linux-arm64-musl": "0.62.0", "@oxfmt/binding-linux-ppc64-gnu": "0.62.0", "@oxfmt/binding-linux-riscv64-gnu": "0.62.0", "@oxfmt/binding-linux-riscv64-musl": "0.62.0", "@oxfmt/binding-linux-s390x-gnu": "0.62.0", "@oxfmt/binding-linux-x64-gnu": "0.62.0", "@oxfmt/binding-linux-x64-musl": "0.62.0", "@oxfmt/binding-openharmony-arm64": "0.62.0", "@oxfmt/binding-win32-arm64-msvc": "0.62.0", "@oxfmt/binding-win32-ia32-msvc": "0.62.0", "@oxfmt/binding-win32-x64-msvc": "0.62.0" }, "peerDependencies": { "svelte": "^5.0.0", "vite-plus": "*" }, "optionalPeers": ["svelte", "vite-plus"], "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-vxgGHTmnDU9j4CX7dDBLzxgmHxfda/yPcgJkGCMUSCwRmz+euo/V08xXLNgXTeqAB9Fhf3Pe2nO1RNKLCVgphQ=="], "oxlint": ["oxlint@1.77.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.77.0", "@oxlint/binding-android-arm64": "1.77.0", "@oxlint/binding-darwin-arm64": "1.77.0", "@oxlint/binding-darwin-x64": "1.77.0", "@oxlint/binding-freebsd-x64": "1.77.0", "@oxlint/binding-linux-arm-gnueabihf": "1.77.0", "@oxlint/binding-linux-arm-musleabihf": "1.77.0", "@oxlint/binding-linux-arm64-gnu": "1.77.0", "@oxlint/binding-linux-arm64-musl": "1.77.0", "@oxlint/binding-linux-ppc64-gnu": "1.77.0", "@oxlint/binding-linux-riscv64-gnu": "1.77.0", "@oxlint/binding-linux-riscv64-musl": "1.77.0", "@oxlint/binding-linux-s390x-gnu": "1.77.0", "@oxlint/binding-linux-x64-gnu": "1.77.0", "@oxlint/binding-linux-x64-musl": "1.77.0", "@oxlint/binding-openharmony-arm64": "1.77.0", "@oxlint/binding-win32-arm64-msvc": "1.77.0", "@oxlint/binding-win32-ia32-msvc": "1.77.0", "@oxlint/binding-win32-x64-msvc": "1.77.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2001", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-qnGh8XJHaQ0dprrDXNQZgS0FgjI6v+V3+X8DwmaV++5Aamy6jGKfDdQ1TUvhUxtmKFAbEf4/WeO5QZX+5WSngg=="], @@ -823,16 +1245,26 @@ "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], + "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + "patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="], + "path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="], + "path-expression-matcher": ["path-expression-matcher@1.6.2", "", {}, "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], "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=="], "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], @@ -841,24 +1273,48 @@ "postcss": ["postcss@8.5.19", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ=="], + "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], + + "promise-limit": ["promise-limit@2.7.0", "", {}, "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw=="], + + "proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="], + "pure-rand": ["pure-rand@8.4.2", "", {}, "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng=="], + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + "react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="], "react-devtools-core": ["react-devtools-core@7.0.1", "", { "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" } }, "sha512-C3yNvRHaizlpiASzy7b9vbnBGLrhvdhl1CbdU6EnZgxPNbai60szdLtl+VL76UNOt5bOoVTOz5rNWZxgGt+Gsw=="], "react-reconciler": ["react-reconciler@0.33.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA=="], + "readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + "redis": ["redis@6.2.1", "", { "dependencies": { "@redis/bloom": "6.2.1", "@redis/client": "6.2.1", "@redis/json": "6.2.1", "@redis/search": "6.2.1", "@redis/time-series": "6.2.1" } }, "sha512-Z9VHtgYs48PiQC77X9O2Er8Hj4T+5BtFjT91/vi5Is1D04N72cA946ZslM1ImJw8ZctFBZWAVjM7S5wJNeHMpg=="], + "regexp-to-ast": ["regexp-to-ast@0.5.0", "", {}, "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw=="], + + "remeda": ["remeda@2.33.4", "", {}, "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ=="], + "reselect": ["reselect@4.1.8", "", {}, "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ=="], "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], - "rolldown": ["rolldown@1.1.5", "", { "dependencies": { "@oxc-project/types": "=0.139.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.5", "@rolldown/binding-darwin-arm64": "1.1.5", "@rolldown/binding-darwin-x64": "1.1.5", "@rolldown/binding-freebsd-x64": "1.1.5", "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", "@rolldown/binding-linux-arm64-gnu": "1.1.5", "@rolldown/binding-linux-arm64-musl": "1.1.5", "@rolldown/binding-linux-ppc64-gnu": "1.1.5", "@rolldown/binding-linux-s390x-gnu": "1.1.5", "@rolldown/binding-linux-x64-gnu": "1.1.5", "@rolldown/binding-linux-x64-musl": "1.1.5", "@rolldown/binding-openharmony-arm64": "1.1.5", "@rolldown/binding-wasm32-wasi": "1.1.5", "@rolldown/binding-win32-arm64-msvc": "1.1.5", "@rolldown/binding-win32-x64-msvc": "1.1.5" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA=="], + "restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="], + + "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "rolldown": ["rolldown@1.2.5", "", { "dependencies": { "@oxc-project/types": "=0.146.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.5", "@rolldown/binding-android-arm64": "1.2.5", "@rolldown/binding-darwin-arm64": "1.2.5", "@rolldown/binding-darwin-x64": "1.2.5", "@rolldown/binding-freebsd-x64": "1.2.5", "@rolldown/binding-linux-arm-gnueabihf": "1.2.5", "@rolldown/binding-linux-arm64-gnu": "1.2.5", "@rolldown/binding-linux-arm64-musl": "1.2.5", "@rolldown/binding-linux-ppc64-gnu": "1.2.5", "@rolldown/binding-linux-s390x-gnu": "1.2.5", "@rolldown/binding-linux-x64-gnu": "1.2.5", "@rolldown/binding-linux-x64-musl": "1.2.5", "@rolldown/binding-openharmony-arm64": "1.2.5", "@rolldown/binding-win32-arm64-msvc": "1.2.5", "@rolldown/binding-win32-x64-msvc": "1.2.5" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-VD2IE5PUG4Oj8zz2VGykiYd5wbnjdIiSsNQb8Qu5B+noEp+A78mu2iVvpp27g8es14Tk9rofNs5Tku9iQCS4fA=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], "s-js": ["s-js@0.4.9", "", {}, "sha512-RtpOm+cM6O0sHg6IA70wH+UC3FZcND+rccBZpBAHzlUgNO2Bm5BN+FnM8+OBxzXdwpKWFwX11JGF0MFRkhSoIQ=="], + "safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], @@ -867,26 +1323,50 @@ "seroval-plugins": ["seroval-plugins@1.5.5", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-+BDhqYM6CEn3x09v44dpa9p6974FuUB2dxk+Ctn04k0cO1Zt6QODTXfmEZK0eBaTe/fJBvP4NMGuNJ+R8T+QMg=="], + "setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="], + + "sharp": ["sharp@0.35.5", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.5" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.5", "@img/sharp-darwin-x64": "0.35.5", "@img/sharp-freebsd-wasm32": "0.35.5", "@img/sharp-libvips-darwin-arm64": "1.3.4", "@img/sharp-libvips-darwin-x64": "1.3.4", "@img/sharp-libvips-linux-arm": "1.3.4", "@img/sharp-libvips-linux-arm64": "1.3.4", "@img/sharp-libvips-linux-ppc64": "1.3.4", "@img/sharp-libvips-linux-riscv64": "1.3.4", "@img/sharp-libvips-linux-s390x": "1.3.4", "@img/sharp-libvips-linux-x64": "1.3.4", "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", "@img/sharp-libvips-linuxmusl-x64": "1.3.4", "@img/sharp-linux-arm": "0.35.5", "@img/sharp-linux-arm64": "0.35.5", "@img/sharp-linux-ppc64": "0.35.5", "@img/sharp-linux-riscv64": "0.35.5", "@img/sharp-linux-s390x": "0.35.5", "@img/sharp-linux-x64": "0.35.5", "@img/sharp-linuxmusl-arm64": "0.35.5", "@img/sharp-linuxmusl-x64": "0.35.5", "@img/sharp-webcontainers-wasm32": "0.35.5", "@img/sharp-win32-arm64": "0.35.5", "@img/sharp-win32-ia32": "0.35.5", "@img/sharp-win32-x64": "0.35.5" }, "peerDependencies": { "@types/node": "*" }, "optionalPeers": ["@types/node"] }, "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "shell-quote": ["shell-quote@1.10.0", "", {}, "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA=="], "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + + "slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="], + "solid-js": ["solid-js@1.9.12", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-QzKaSJq2/iDrWR1As6MHZQ8fQkdOBf8GReYb7L5iKwMGceg7HxDcaOHk0at66tNgn9U2U7dXo8ZZpLIAmGMzgw=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="], + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], "std-env": ["std-env@4.2.0", "", {}, "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw=="], "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + "string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + "strnum": ["strnum@2.4.2", "", { "dependencies": { "anynum": "^1.0.1" } }, "sha512-rDG3Ah4TV0k1hWvLSzkZtMmLN9+eS+h3knq4MP6A42Y3Yh5qGNnOUs1jJkoSr8FG5dsL28c7KgkIBzSEykqtuw=="], + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], + + "terminal-size": ["terminal-size@4.0.1", "", {}, "sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ=="], + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], "tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="], @@ -897,38 +1377,92 @@ "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "turndown": ["turndown@7.2.4", "", { "dependencies": { "@mixmark-io/domino": "^2.2.0" } }, "sha512-I8yFsfRzmzK0WV1pNNOA4A7y4RDfFxPRxb3t+e3ui14qSGOxGtiSP6GjeX+Y6CHb7HYaFj7ECUD7VE5kQMZWGQ=="], + "type-fest": ["type-fest@5.10.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-NoSdpq/WEiAg5sjmBkmV/hfxv6HJH4NqPNrqjtSO5CwRmpsDfaf4begxW34KdJykH/l1yHtwBWQkCRdoXO8mPA=="], + "typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], "undici": ["undici@8.10.1", "", {}, "sha512-YQ3WlbqjYMmNpdvDH64jAgLjxuAR9+649calDWhbshYaeQGO2bR4nI94ORJmwI3J9YhoKQnpyGOK+0zlWS5N5Q=="], "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], + "unenv": ["unenv@2.0.0-rc.24", "", { "dependencies": { "pathe": "^2.0.3" } }, "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw=="], + + "universal-user-agent": ["universal-user-agent@7.0.3", "", {}, "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A=="], + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "valibot": ["valibot@1.2.0", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg=="], + "vite": ["vite@8.1.4", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", "postcss": "^8.5.16", "rolldown": "~1.1.4", "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-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ=="], "vitest": ["vitest@4.1.9", "", { "dependencies": { "@vitest/expect": "4.1.9", "@vitest/mocker": "4.1.9", "@vitest/pretty-format": "4.1.9", "@vitest/runner": "4.1.9", "@vitest/snapshot": "4.1.9", "@vitest/spy": "4.1.9", "@vitest/utils": "4.1.9", "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.9", "@vitest/browser-preview": "4.1.9", "@vitest/browser-webdriverio": "4.1.9", "@vitest/coverage-istanbul": "4.1.9", "@vitest/coverage-v8": "4.1.9", "@vitest/ui": "4.1.9", "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-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ=="], "web-tree-sitter": ["web-tree-sitter@0.25.10", "", { "peerDependencies": { "@types/emscripten": "^1.40.0" }, "optionalPeers": ["@types/emscripten"] }, "sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + "widest-line": ["widest-line@6.0.0", "", { "dependencies": { "string-width": "^8.1.0" } }, "sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA=="], + + "workerd": ["workerd@1.20260704.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260704.1", "@cloudflare/workerd-darwin-arm64": "1.20260704.1", "@cloudflare/workerd-linux-64": "1.20260704.1", "@cloudflare/workerd-linux-arm64": "1.20260704.1", "@cloudflare/workerd-windows-64": "1.20260704.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-GDZ0jzIYDYfN7rCt/oFJv4BG3QJ+4IS2kfRvxEMY9VKIfPhzo63PH69Ir7ug8LfORCgCtmfkiQVXrqbot7pZTQ=="], + + "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], + "ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], + "xml-naming": ["xml-naming@0.3.0", "", {}, "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ=="], + + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], + "yargs": ["yargs@18.2.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^8.2.1", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w=="], + + "yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], + + "yauzl": ["yauzl@3.4.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw=="], + + "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], + + "zeptomatch": ["zeptomatch@2.1.0", "", { "dependencies": { "grammex": "^3.1.11", "graphmatch": "^1.1.0" } }, "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA=="], + "@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + "@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], + + "@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], + + "@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], + + "@prisma/dev/std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + + "alchemy/undici": ["undici@7.30.0", "", {}, "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ=="], + "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], + "cli-truncate/string-width": ["string-width@8.3.0", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ=="], + + "foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "ink/string-width": ["string-width@8.3.0", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ=="], + + "libsql/detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="], + "make-dir/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], @@ -936,5 +1470,47 @@ "path-scurry/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], "react-devtools-core/ws": ["ws@7.5.12", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-1xGnbYN3zbog9CwuNDQULNRrTCLIn46/WmpR1f0w6PsCYQHkylZr5vkd6kfMZYV6pRnQkcPNRyiA8LsrNKyhpg=="], + + "sharp/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "vite/rolldown": ["rolldown@1.1.5", "", { "dependencies": { "@oxc-project/types": "=0.139.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.5", "@rolldown/binding-darwin-arm64": "1.1.5", "@rolldown/binding-darwin-x64": "1.1.5", "@rolldown/binding-freebsd-x64": "1.1.5", "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", "@rolldown/binding-linux-arm64-gnu": "1.1.5", "@rolldown/binding-linux-arm64-musl": "1.1.5", "@rolldown/binding-linux-ppc64-gnu": "1.1.5", "@rolldown/binding-linux-s390x-gnu": "1.1.5", "@rolldown/binding-linux-x64-gnu": "1.1.5", "@rolldown/binding-linux-x64-musl": "1.1.5", "@rolldown/binding-openharmony-arm64": "1.1.5", "@rolldown/binding-wasm32-wasi": "1.1.5", "@rolldown/binding-win32-arm64-msvc": "1.1.5", "@rolldown/binding-win32-x64-msvc": "1.1.5" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA=="], + + "widest-line/string-width": ["string-width@8.3.0", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ=="], + + "yargs/string-width": ["string-width@8.3.0", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ=="], + + "@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], + + "@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], + + "vite/rolldown/@oxc-project/types": ["@oxc-project/types@0.139.0", "", {}, "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw=="], + + "vite/rolldown/@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.5", "", { "os": "android", "cpu": "arm64" }, "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ=="], + + "vite/rolldown/@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.1.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw=="], + + "vite/rolldown/@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.1.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g=="], + + "vite/rolldown/@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.1.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA=="], + + "vite/rolldown/@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.1.5", "", { "os": "linux", "cpu": "arm" }, "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw=="], + + "vite/rolldown/@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q=="], + + "vite/rolldown/@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA=="], + + "vite/rolldown/@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.1.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg=="], + + "vite/rolldown/@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.1.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA=="], + + "vite/rolldown/@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ=="], + + "vite/rolldown/@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg=="], + + "vite/rolldown/@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.1.5", "", { "os": "none", "cpu": "arm64" }, "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw=="], + + "vite/rolldown/@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.1.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw=="], + + "vite/rolldown/@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.1.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA=="], } } diff --git a/examples/alchemy-cloudflare/.gitignore b/examples/alchemy-cloudflare/.gitignore new file mode 100644 index 0000000..e09c801 --- /dev/null +++ b/examples/alchemy-cloudflare/.gitignore @@ -0,0 +1,2 @@ +.alchemy/ +.env diff --git a/examples/alchemy-cloudflare/README.md b/examples/alchemy-cloudflare/README.md new file mode 100644 index 0000000..076c8b3 --- /dev/null +++ b/examples/alchemy-cloudflare/README.md @@ -0,0 +1,35 @@ +# alchemy-cloudflare + +A minimal fold chat host on Cloudflare, deployed with [Alchemy v2](https://alchemy.run/cloudflare/). Inference runs on OpenAI (`gpt-5.6-terra`, medium reasoning) through fold's `openaiModel`. Each fold session is one Durable Object, named by its `SessionId`. + +- `src/ChatSession.ts` — the Durable Object. It opens the fold log in its SQLite on activation; the first message starts the session, later activations resume it. +- `src/DurableObjectEventLog.ts` — the service that opens fold's `EventLogService` over the object's SQLite. +- `src/Keepalive.ts` — a 30s alarm heartbeat that keeps the object alive while a turn runs. +- `src/ChatSessions.ts` — the service the routes require; its `layer` reaches each session's Durable Object. +- `src/Api.ts` — the `HttpRouter` routes, encoding responses with fold's log schemas. +- `src/Worker.ts` — serves the routes, with `ChatSessions` as RPC to the Durable Objects. +- `alchemy.run.ts` — the stack. + +## Run + +```sh +echo 'OPENAI_API_KEY=sk-...' > .env # read at deploy time, stored on the Worker as a secret +bun alchemy profile edit --add Cloudflare +bun run dev # local workerd +bun run deploy # prints the Worker url +``` + +```sh +ID=sess_... # any new SessionId; the first message starts that session +curl -X POST "$URL/sessions/$ID/messages" -d '{"text":"hi"}' # the agent-finished entry +curl -X POST "$URL/sessions/$ID/messages" -d '{"text":"stop","whenRunning":"interrupt"}' # queue (default) | steer | interrupt +curl "$URL/sessions/$ID" # all log entries +``` + +`bun run test` checks the routes against an in-memory `ChatSessions` built on fold's in-memory log. + +## Limits + +- A turn cut off by a crash or deploy continues when the object next wakes, through a synthetic "continue" user message (at most 3 per cut-off turn). A subagent cut off with it is left for the root model to resume by id. + +- Replies arrive whole. `FoldSession.events` can stream deltas once a route forwards them. diff --git a/examples/alchemy-cloudflare/alchemy.run.ts b/examples/alchemy-cloudflare/alchemy.run.ts new file mode 100644 index 0000000..0a7f514 --- /dev/null +++ b/examples/alchemy-cloudflare/alchemy.run.ts @@ -0,0 +1,14 @@ +import * as Alchemy from 'alchemy' +import * as Cloudflare from 'alchemy/Cloudflare' +import { Effect } from 'effect' + +import Worker from './src/Worker' + +export default Alchemy.Stack( + 'FoldAlchemyCloudflare', + { providers: Cloudflare.providers(), state: Cloudflare.state() }, + Effect.gen(function* () { + const worker = yield* Worker + return { url: worker.url } + }), +) diff --git a/examples/alchemy-cloudflare/package.json b/examples/alchemy-cloudflare/package.json new file mode 100644 index 0000000..f6677ca --- /dev/null +++ b/examples/alchemy-cloudflare/package.json @@ -0,0 +1,28 @@ +{ + "name": "@humanlayer/example-alchemy-cloudflare", + "version": "0.0.0", + "private": true, + "license": "MIT", + "type": "module", + "scripts": { + "dev": "alchemy dev", + "deploy": "alchemy deploy", + "destroy": "alchemy destroy", + "typecheck": "tsc --noEmit", + "test": "bun vitest run", + "test:watch": "bun vitest" + }, + "dependencies": { + "@humanlayer/fold-core": "workspace:*", + "@effect/platform-bun": "catalog:", + "@effect/platform-node": "catalog:", + "alchemy": "catalog:", + "effect": "catalog:" + }, + "devDependencies": { + "@effect/vitest": "catalog:", + "@humanlayer/fold-vitest-config": "workspace:*", + "typescript": "catalog:", + "vitest": "catalog:" + } +} diff --git a/examples/alchemy-cloudflare/src/Api.ts b/examples/alchemy-cloudflare/src/Api.ts new file mode 100644 index 0000000..cbe489f --- /dev/null +++ b/examples/alchemy-cloudflare/src/Api.ts @@ -0,0 +1,48 @@ +/** + * The chat HTTP routes over {@link ChatSessions}, encoding responses with fold's log schemas. + * + * - `POST /sessions/:sessionId/messages` with `{ "text": string, "whenRunning"?: "queue" | "steer" | + * "interrupt" }` delivers one user message, starting the session if the id is new, and returns the + * `agent-finished` entry of the run that consumed it. `whenRunning` defaults to `queue`. + * - `GET /sessions/:sessionId` returns the session's log entries. + */ +import { AgentFinishedLogEntry, LogEntry, SessionId } from '@humanlayer/fold-core' +import { Effect, Schema } from 'effect' +import { HttpRouter, HttpServerRequest, HttpServerResponse } from 'effect/unstable/http' + +import { ChatSessions, WhenRunning } from './ChatSessions' + +const SessionParams = Schema.Struct({ sessionId: SessionId }) +const MessageBody = Schema.Struct({ + text: Schema.String, + whenRunning: WhenRunning.pipe(Schema.withDecodingDefaultKey(Effect.succeed('queue'))), +}) +const entriesJson = HttpServerResponse.schemaJson(Schema.Array(LogEntry)) +const finishedJson = HttpServerResponse.schemaJson(AgentFinishedLogEntry) + +const badRequest = () => Effect.succeed(HttpServerResponse.text('Bad request', { status: 400 })) + +export const ChatRoutes = HttpRouter.use((router) => + Effect.gen(function* () { + const sessions = yield* ChatSessions + + yield* router.add( + 'GET', + '/sessions/:sessionId', + Effect.gen(function* () { + const { sessionId } = yield* HttpRouter.schemaPathParams(SessionParams) + return yield* entriesJson(yield* sessions.entries(sessionId)) + }).pipe(Effect.catchTag('SchemaError', badRequest)), + ) + + yield* router.add( + 'POST', + '/sessions/:sessionId/messages', + Effect.gen(function* () { + const { sessionId } = yield* HttpRouter.schemaPathParams(SessionParams) + const { text, whenRunning } = yield* HttpServerRequest.schemaBodyJson(MessageBody) + return yield* finishedJson(yield* sessions.send(sessionId, text, whenRunning)) + }).pipe(Effect.catchTag('SchemaError', badRequest)), + ) + }), +) diff --git a/examples/alchemy-cloudflare/src/ChatSession.ts b/examples/alchemy-cloudflare/src/ChatSession.ts new file mode 100644 index 0000000..9360f49 --- /dev/null +++ b/examples/alchemy-cloudflare/src/ChatSession.ts @@ -0,0 +1,140 @@ +/** + * One fold session per Durable Object, named by its SessionId. The object's SQLite log is the session: + * addressing a new id creates the object with an empty log, and its first `send` starts the session + * under that id; a written log resumes on activation. Callers never create a session explicitly. + * + * Every turn runs under a {@link Keepalive} lease so the object is not evicted mid-run. A turn can still + * be cut off - a crash, a deploy - leaving the root's user message with no finished run after it. When a + * written log activates in that state, the object nudges the root to continue. + * Fold fills any tool call the cut left without a result, and the model can resume a cut-off subagent + * by id itself. A turn that keeps getting cut off stops being nudged after {@link MAX_RESTART_NUDGES}. + */ +import { + SessionId, + type AgentId, + type FoldSession, + type LogEntry, + type UserMessageLogEntry, + defineAgent, + eventLogSource, + openaiModel, + resumeSession, + startSession, +} from '@humanlayer/fold-core' +import * as Cloudflare from 'alchemy/Cloudflare' +import { Config, Effect, Match, Option, Predicate, Result, Schema, Scope, Stream } from 'effect' + +import type { WhenRunning } from './ChatSessions' +import { DurableObjectEventLog } from './DurableObjectEventLog' +import { Keepalive } from './Keepalive' + +const MODEL = 'gpt-5.6-terra' + +const RESTART_NUDGE = + 'A restart cut you off before you finished. Continue where you left off.' +const MAX_RESTART_NUDGES = 3 + +/** The root's user messages since its last finished run: any means a restart cut that run off. */ +const openRootMessages = (entries: ReadonlyArray, rootAgentId: AgentId) => { + const lastFinishedSeq = + entries.findLast((entry) => Predicate.isTagged(entry, 'agent-finished') && entry.agentId === rootAgentId) + ?.seq ?? -1 + return entries.filter( + (entry): entry is UserMessageLogEntry => + Predicate.isTagged(entry, 'user-message') && entry.agentId === rootAgentId && entry.seq > lastFinishedSeq, + ) +} + +const isRestartNudge = ({ message }: UserMessageLogEntry) => + typeof message.content === 'string' + ? message.content === RESTART_NUDGE + : message.content.some((part) => part.type === 'text' && part.text === RESTART_NUDGE) + +/** The root's next `agent-finished` at or after `fromSeq`: the end of the run a steered message joined. */ +const rootFinishedFrom = (session: FoldSession, fromSeq: number) => + session.events(fromSeq).pipe( + Stream.filterMap((event) => + event.kind === 'log' && + Predicate.isTagged(event.entry, 'agent-finished') && + event.entry.agentId === session.rootAgentId + ? Result.succeed(event.entry) + : Result.fail(event), + ), + Stream.runHead, + Effect.flatMap(Effect.fromOption), + Effect.orDie, + ) + +/** + * Deliver one message to the root agent per {@link WhenRunning}. Idle roots just start a run. Only a + * subagent target can be missing, so fold's `SubagentNotFoundError` cannot happen here. + */ +const deliver = (session: FoldSession, text: string, whenRunning: WhenRunning) => + Match.value(whenRunning).pipe( + Match.when('queue', () => session.send(text)), + Match.when('interrupt', () => Effect.andThen(session.interrupt(), session.send(text))), + Match.when('steer', () => + Effect.gen(function* () { + const fromSeq = (yield* session.entries).length + yield* session.steer(text) + return yield* rootFinishedFrom(session, fromSeq) + }).pipe(Effect.catchTag('AgentNotRunningError', () => session.send(text))), + ), + Match.exhaustive, + Effect.catchTag('SubagentNotFoundError', (error) => Effect.die(error)), + ) + +export default class ChatSession extends Cloudflare.DurableObject()( + 'ChatSession', + Effect.gen(function* () { + const state = yield* Cloudflare.DurableObjectState + // Read from the deploy environment and bound to the Worker as a secret. Missing, the deploy fails. + const apiKey = yield* Config.redacted('OPENAI_API_KEY').pipe(Effect.orDie) + const eventLogs = yield* DurableObjectEventLog + const keepalive = yield* Keepalive + + return Effect.gen(function* () { + const sessionId = yield* Schema.decodeUnknownEffect(SessionId)(state.id.name) + const eventLog = yield* eventLogs.open + const log = eventLogSource(Effect.succeed(eventLog)) + const isEmpty = Option.isNone(yield* Stream.runHead(eventLog.entries())) + + const agent = defineAgent({ + name: 'alchemy-cloudflare-chat', + systemPrompt: 'You are a helpful, concise assistant.', + model: openaiModel({ apiKey, model: MODEL, reasoning: 'medium' }), + }) + + // Never closed: the session lives as long as the object stays in memory, past every call's own + // scope. Opened on the first send, so reading an unknown id writes nothing. + const scope = yield* Scope.make() + const session = yield* Effect.cached( + (isEmpty ? startSession({ agent, log, sessionId }) : resumeSession({ agent, log })).pipe( + Scope.provide(scope), + ), + ) + + // A written log may hold a turn a restart cut off: nudge the root to continue it, in the background + // under a lease so the heartbeat keeps the object alive until it lands - even when that heartbeat's + // alarm is the only thing that woke the object. + if (!isEmpty) { + yield* Effect.forkIn( + Effect.gen(function* () { + const opened = yield* session + const open = openRootMessages(yield* opened.entries, opened.rootAgentId) + if (open.length === 0 || open.filter(isRestartNudge).length >= MAX_RESTART_NUDGES) return + yield* keepalive.whileRunning(deliver(opened, RESTART_NUDGE, 'queue')) + }), + scope, + ) + } + + return { + send: (text: string, whenRunning: WhenRunning) => + keepalive.whileRunning(Effect.flatMap(session, (opened) => deliver(opened, text, whenRunning))), + entries: () => Stream.runCollect(eventLog.entries()).pipe(Effect.orDie), + alarm: () => keepalive.alarm, + } + }).pipe(Effect.orDie) + }).pipe(Effect.provide([DurableObjectEventLog.layer, Keepalive.layer])), +) {} diff --git a/examples/alchemy-cloudflare/src/ChatSessions.ts b/examples/alchemy-cloudflare/src/ChatSessions.ts new file mode 100644 index 0000000..2d4b8a4 --- /dev/null +++ b/examples/alchemy-cloudflare/src/ChatSessions.ts @@ -0,0 +1,50 @@ +/** + * The fold chat sessions this host serves, each addressed by its SessionId. There is no create step: + * the first `send` to an id starts that session. The Worker implements this as RPC to the ChatSession + * Durable Object holding each session. + */ +import type { AgentFinishedLogEntry, LogEntry, SessionId } from '@humanlayer/fold-core' +import { Context, Effect, Layer, Schema } from 'effect' + +import ChatSession from './ChatSession' + +/** + * What `send` does when the session's root agent is already running. When it is idle, every mode just + * starts a run. + * + * - `queue`: fold's follow-up - the message joins the running run when it would otherwise finish. + * - `steer`: fold's steering - the message lands between the running run's turns. + * - `interrupt`: interrupt the running run (fold writes its interrupted markers), then start a new one. + */ +export const WhenRunning = Schema.Literals(['queue', 'steer', 'interrupt']) +export type WhenRunning = typeof WhenRunning.Type + +export class ChatSessions extends Context.Service< + ChatSessions, + { + /** Every durable log entry of the session so far; empty for an id nothing was sent to. */ + readonly entries: (sessionId: SessionId) => Effect.Effect> + /** + * Deliver one user message to the session's root agent, starting the session if it is new, and + * resolve with the `agent-finished` entry of the run that consumed it. + */ + readonly send: ( + sessionId: SessionId, + text: string, + whenRunning: WhenRunning, + ) => Effect.Effect + } +>()('alchemy-cloudflare/ChatSessions') { + /** Each session is the ChatSession Durable Object named by its id. Build it in a Worker's construction. */ + static readonly layer = Layer.effect( + ChatSessions, + Effect.gen(function* () { + const sessions = yield* ChatSession + + return ChatSessions.of({ + entries: (sessionId) => sessions.getByName(sessionId).entries(), + send: (sessionId, text, whenRunning) => sessions.getByName(sessionId).send(text, whenRunning), + }) + }), + ) +} diff --git a/examples/alchemy-cloudflare/src/DurableObjectEventLog.ts b/examples/alchemy-cloudflare/src/DurableObjectEventLog.ts new file mode 100644 index 0000000..b040c91 --- /dev/null +++ b/examples/alchemy-cloudflare/src/DurableObjectEventLog.ts @@ -0,0 +1,147 @@ +/** + * A fold event log in a Durable Object's SQLite: one object is one session's log. Opening the log loads + * every row, so `resumeSession` replays them; appends write through before they publish. This mirrors + * fold-agent's JSONL log with a table in place of the file. + * + * Queries run on the raw SQLite handle, which is synchronous and needs nothing at call time, so the log + * fold holds needs nothing either. + */ +import { + EventLogCorruptEntryError, + EventLogInvalidEntryError, + EventLogUnavailableError, + Ids, + LogEntry, + decodeStoredLogEntry, + layerLiveIdFactory, + makeStoredLogEntry, + type EventLogError, + type EventLogOperation, + type EventLogService, + type LogEntryInput, + type LogSeq, +} from '@humanlayer/fold-core' +import * as Cloudflare from 'alchemy/Cloudflare' +import { Context, Effect, Layer, PubSub, Ref, Schema, Semaphore, Stream } from 'effect' + +type LogRow = { readonly seq: number; readonly entry: string } + +const entriesFrom = (entries: ReadonlyArray, fromSeq: LogSeq) => + entries.filter((entry) => entry.seq >= fromSeq) + +const LogEntryJson = Schema.fromJsonString(LogEntry) + +/** Parse a row's JSON, then decode it by its stored version (older versions upcast to the current shape). */ +const decodeRow = (row: LogRow) => + Schema.decodeEffect(Schema.fromJsonString(Schema.Unknown))(row.entry).pipe( + Effect.mapError( + (cause) => + new EventLogCorruptEntryError({ + operation: 'entries', + message: `Invalid JSON in fold_log at seq ${row.seq}`, + seq: row.seq, + cause, + }), + ), + Effect.flatMap(decodeStoredLogEntry), + ) + +const encodeEntry = (entry: LogEntry) => + Schema.encodeEffect(LogEntryJson)(entry).pipe( + Effect.mapError( + (cause) => + new EventLogInvalidEntryError({ + operation: 'append', + message: 'Unable to encode EventLog entry', + cause, + }), + ), + ) + +export class DurableObjectEventLog extends Context.Service< + DurableObjectEventLog, + { + /** Open this object's log. Run it once the object is live: it reads the object's SQLite. */ + readonly open: Effect.Effect + } +>()('alchemy-cloudflare/DurableObjectEventLog') { + static readonly layer = Layer.effect( + DurableObjectEventLog, + Effect.gen(function* () { + const { raw } = yield* Cloudflare.DurableObjectState + const ids = yield* Ids + + const query = = Record>( + operation: EventLogOperation, + sql: string, + ...bindings: Array + ) => + Effect.try({ + try: () => raw.storage.sql.exec(sql, ...bindings).toArray(), + catch: (cause) => + new EventLogUnavailableError({ + operation, + message: 'fold_log query failed', + retryable: false, + cause, + }), + }) + + const open = Effect.gen(function* () { + yield* query( + 'entries', + 'CREATE TABLE IF NOT EXISTS fold_log (seq INTEGER PRIMARY KEY, entry TEXT NOT NULL)', + ) + const rows = yield* query('entries', 'SELECT seq, entry FROM fold_log ORDER BY seq') + const initialEntries = yield* Effect.forEach(rows, decodeRow) + + const entriesRef = yield* Ref.make>(initialEntries) + const pubsub = yield* PubSub.unbounded() + const appendLock = yield* Semaphore.make(1) + + const append = Effect.fn('alchemy_cloudflare.event_log.append')((input: LogEntryInput) => + appendLock.withPermit( + Effect.gen(function* () { + const current = yield* Ref.get(entriesRef) + const stored = yield* makeStoredLogEntry(input, current.length, ids) + const entry = yield* encodeEntry(stored) + + yield* query('append', 'INSERT INTO fold_log (seq, entry) VALUES (?, ?)', stored.seq, entry) + yield* Ref.set(entriesRef, [...current, stored]) + yield* PubSub.publish(pubsub, stored) + + return stored + }), + ), + ) + + const entries: EventLogService['entries'] = (fromSeq = 0) => + Stream.fromIterableEffect( + Ref.get(entriesRef).pipe(Effect.map((snapshot) => entriesFrom(snapshot, fromSeq))), + ) + + const subscribe: EventLogService['subscribe'] = (fromSeq = 0) => + Stream.unwrap( + appendLock.withPermit( + Effect.gen(function* () { + const subscription = yield* PubSub.subscribe(pubsub) + const snapshot = yield* Ref.get(entriesRef) + + return Stream.fromIterable(entriesFrom(snapshot, fromSeq)).pipe( + Stream.concat( + Stream.fromSubscription(subscription).pipe( + Stream.filter((entry) => entry.seq >= fromSeq), + ), + ), + ) + }), + ), + ) + + return { append, entries, subscribe } + }) + + return DurableObjectEventLog.of({ open }) + }), + ).pipe(Layer.provide(layerLiveIdFactory)) +} diff --git a/examples/alchemy-cloudflare/src/Keepalive.ts b/examples/alchemy-cloudflare/src/Keepalive.ts new file mode 100644 index 0000000..3014179 --- /dev/null +++ b/examples/alchemy-cloudflare/src/Keepalive.ts @@ -0,0 +1,44 @@ +/** + * Keeps a Durable Object from being evicted while work runs. A turn's model calls are outgoing fetches, + * which do not count as activity, so a turn that outlives its caller could be evicted mid-run. Like the + * Cloudflare Agents SDK's `keepAliveWhile`, each piece of work holds a lease, and while any lease is held + * the object's alarm fires every {@link HEARTBEAT_MILLIS} and re-arms itself. + */ +import * as Cloudflare from 'alchemy/Cloudflare' +import { Clock, Context, Effect, Layer, Ref } from 'effect' + +const HEARTBEAT_MILLIS = 30_000 + +export class Keepalive extends Context.Service< + Keepalive, + { + /** Run `effect` under a lease: the first lease arms the heartbeat. */ + readonly whileRunning: (effect: Effect.Effect) => Effect.Effect + /** The object's alarm handler: re-arm while any lease is held; once none are, let the alarm lapse. */ + readonly alarm: Effect.Effect + } +>()('alchemy-cloudflare/Keepalive') { + static readonly layer = Layer.effect( + Keepalive, + Effect.gen(function* () { + // The raw storage handle: its alarm call needs nothing at call time, so neither do these methods. + const { raw } = yield* Cloudflare.DurableObjectState + const leases = yield* Ref.make(0) + const armHeartbeat = Effect.flatMap(Clock.currentTimeMillis, (now) => + Effect.promise(() => raw.storage.setAlarm(now + HEARTBEAT_MILLIS)), + ) + + return Keepalive.of({ + whileRunning: (effect) => + Effect.acquireUseRelease( + Ref.updateAndGet(leases, (held) => held + 1).pipe( + Effect.tap((held) => (held === 1 ? armHeartbeat : Effect.void)), + ), + () => effect, + () => Ref.update(leases, (held) => held - 1), + ), + alarm: Effect.flatMap(Ref.get(leases), (held) => (held > 0 ? armHeartbeat : Effect.void)), + }) + }), + ) +} diff --git a/examples/alchemy-cloudflare/src/Worker.ts b/examples/alchemy-cloudflare/src/Worker.ts new file mode 100644 index 0000000..e5d95f8 --- /dev/null +++ b/examples/alchemy-cloudflare/src/Worker.ts @@ -0,0 +1,21 @@ +/** + * The chat Worker: {@link ChatRoutes} over {@link ChatSessions}, one ChatSession Durable Object per + * fold session. + */ +import * as Cloudflare from 'alchemy/Cloudflare' +import { Effect } from 'effect' +import { HttpRouter } from 'effect/unstable/http' + +import { ChatRoutes } from './Api' +import { ChatSessions } from './ChatSessions' + +export default Cloudflare.Worker( + 'ChatWorker', + { + main: import.meta.url, + compatibility: { flags: ['nodejs_compat'], date: '2026-08-31' }, + }, + Effect.gen(function* () { + return { fetch: yield* HttpRouter.toHttpEffect(ChatRoutes) } + }).pipe(Effect.provide(ChatSessions.layer)), +) diff --git a/examples/alchemy-cloudflare/test/Api.test.ts b/examples/alchemy-cloudflare/test/Api.test.ts new file mode 100644 index 0000000..ae3d585 --- /dev/null +++ b/examples/alchemy-cloudflare/test/Api.test.ts @@ -0,0 +1,137 @@ +/** + * The chat routes against an in-memory ChatSessions: each session is fold's in-memory EventLog, so the + * routes encode and decode real log entries without a Durable Object or a model. The fake mirrors the + * real contract's shape - the first send to an id starts its session - and echoes the delivery mode so + * tests can see what the routes passed. + */ +import { it } from '@effect/vitest' +import { + AgentId, + EventLog, + LogEntry, + SessionId, + layerInMemoryEventLog, + type EventLogService, +} from '@humanlayer/fold-core' +import { Context, Effect, Layer, Predicate, Ref, Schema, Scope, Stream } from 'effect' +import { HttpRouter } from 'effect/unstable/http' +import { expect } from 'vitest' + +import { ChatRoutes } from '../src/Api' +import { ChatSessions } from '../src/ChatSessions' + +const rootAgentId = AgentId.create() + +const layerInMemory = Layer.effect( + ChatSessions, + Effect.gen(function* () { + const scope = yield* Effect.scope + const logs = yield* Ref.make>(new Map()) + + const logFor = (sessionId: SessionId) => + Effect.gen(function* () { + const existing = (yield* Ref.get(logs)).get(sessionId) + if (existing !== undefined) return existing + const opened = yield* Layer.build(layerInMemoryEventLog).pipe( + Effect.map(Context.get(EventLog)), + Scope.provide(scope), + ) + yield* Ref.update(logs, (current) => new Map(current).set(sessionId, opened)) + return opened + }) + + const entriesOf = (log: EventLogService) => Stream.runCollect(log.entries()).pipe(Effect.orDie) + + return ChatSessions.of({ + entries: (sessionId) => Effect.flatMap(logFor(sessionId), entriesOf), + send: (sessionId, text, whenRunning) => + Effect.gen(function* () { + const log = yield* logFor(sessionId) + if ((yield* entriesOf(log)).length === 0) { + yield* log.append({ + _tag: 'session_started', + agentId: null, + parentAgentId: null, + toolCallId: null, + cwd: null, + sessionId, + rootAgentId, + meta: {}, + }) + } + const finished = yield* log.append({ + _tag: 'agent-finished', + agentId: rootAgentId, + parentAgentId: null, + toolCallId: null, + outcome: 'completed', + resultText: `${whenRunning}: ${text}`, + reason: null, + }) + if (Predicate.isTagged(finished, 'agent-finished')) return finished + return yield* Effect.die(`appended ${finished._tag} while appending agent-finished`) + }).pipe(Effect.orDie), + }) + }), +) + +/** One handler over a fresh in-memory ChatSessions, disposed with the test scope. */ +const serve = Effect.acquireRelease( + Effect.sync(() => HttpRouter.toWebHandler(ChatRoutes.pipe(Layer.provide(layerInMemory)), { disableLogger: true })), + ({ dispose }) => Effect.promise(dispose), +).pipe( + Effect.map(({ handler }) => (method: string, path: string, body?: unknown) => { + const init: RequestInit = { method } + if (body !== undefined) { + init.body = JSON.stringify(body) + init.headers = { 'content-type': 'application/json' } + } + return Effect.promise(() => handler(new Request(`http://chat.test${path}`, init))) + }), +) + +const decodeEntries = (response: Response) => + Effect.promise(() => response.json()).pipe(Effect.flatMap(Schema.decodeUnknownEffect(Schema.Array(LogEntry)))) + +it.effect('the first message to a new id starts its session; GET replays it', () => + Effect.gen(function* () { + const request = yield* serve + const sessionId = SessionId.create() + + expect(yield* request('GET', `/sessions/${sessionId}`).pipe(Effect.flatMap(decodeEntries))).toEqual([]) + + const sent = yield* request('POST', `/sessions/${sessionId}/messages`, { text: 'hi' }) + expect(sent.status).toBe(200) + expect(yield* Effect.promise(() => sent.json())).toMatchObject({ + _tag: 'agent-finished', + resultText: 'queue: hi', + }) + + const entries = yield* request('GET', `/sessions/${sessionId}`).pipe(Effect.flatMap(decodeEntries)) + expect(entries.map((entry) => entry._tag)).toEqual(['session_started', 'agent-finished']) + }).pipe(Effect.scoped), +) + +it.effect('passes an explicit whenRunning through', () => + Effect.gen(function* () { + const request = yield* serve + + const sent = yield* request('POST', `/sessions/${SessionId.create()}/messages`, { + text: 'stop that', + whenRunning: 'interrupt', + }) + expect(yield* Effect.promise(() => sent.json())).toMatchObject({ resultText: 'interrupt: stop that' }) + }).pipe(Effect.scoped), +) + +it.effect('rejects bad ids, bodies, and modes', () => + Effect.gen(function* () { + const request = yield* serve + const messages = `/sessions/${SessionId.create()}/messages` + + expect((yield* request('GET', '/sessions/not-a-session')).status).toBe(400) + expect((yield* request('POST', messages, { message: 'hi' })).status).toBe(400) + expect((yield* request('POST', messages, { text: 'hi', whenRunning: 'later' })).status).toBe(400) + expect((yield* request('GET', '/nope')).status).toBe(404) + }).pipe(Effect.scoped), +) diff --git a/examples/alchemy-cloudflare/tsconfig.json b/examples/alchemy-cloudflare/tsconfig.json new file mode 100644 index 0000000..cfce7ae --- /dev/null +++ b/examples/alchemy-cloudflare/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["alchemy.run.ts", "src", "test"] +} diff --git a/examples/alchemy-cloudflare/vitest.config.ts b/examples/alchemy-cloudflare/vitest.config.ts new file mode 100644 index 0000000..62216b1 --- /dev/null +++ b/examples/alchemy-cloudflare/vitest.config.ts @@ -0,0 +1,3 @@ +import { base } from '@humanlayer/fold-vitest-config' + +export default base diff --git a/package.json b/package.json index b432a5f..383a80c 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,9 @@ "release:prepare": "bun run scripts/release/prepare.ts", "release:validate": "bun run scripts/release/validate-manifest.ts", "release:publish": "bun run scripts/release/publish.ts", - "typecheck": "bun run build:providers && bun run --parallel --if-present --filter './packages/*' typecheck", - "test": "bun run --parallel --if-present --filter './packages/*' test", - "test:watch": "bun run --parallel --if-present --filter './packages/*' test:watch", + "typecheck": "bun run build:providers && bun run --parallel --if-present --filter './packages/*' --filter './examples/*' typecheck", + "test": "bun run --parallel --if-present --filter './packages/*' --filter './examples/*' test", + "test:watch": "bun run --parallel --if-present --filter './packages/*' --filter './examples/*' test:watch", "lint": "oxlint --type-aware", "lint:fix": "oxlint --type-aware --fix", "format": "oxfmt", @@ -25,16 +25,20 @@ }, "workspaces": { "packages": [ - "packages/*" + "packages/*", + "examples/*" ], "catalog": { "@kitlangton/terminal-control": "0.3.1", + "alchemy": "2.0.0-beta.76", + "@cloudflare/workers-types": "5.20260929.1", "@opentui/core": "0.4.3", "@opentui/keymap": "0.4.3", "@opentui/solid": "0.4.3", "solid-js": "1.9.12", "effect": "4.0.0-rc.112", "@effect/platform-node": "4.0.0-rc.112", + "@effect/platform-bun": "4.0.0-rc.112", "@effect/vitest": "4.0.0-rc.112", "@silvia-odwyer/photon-node": "0.3.4", "turndown": "7.2.4", From 93447c747969111f70e3a6924bc15317c2eff9b1 Mon Sep 17 00:00:00 2001 From: Kyle Mistele Date: Wed, 30 Sep 2026 16:10:40 -0700 Subject: [PATCH 2/6] fix(core): keep the session's platform services in tool handlers Effect AI runs a tool handler with the services in scope where the turn runs merged over the ones its toolkit was built with, so the turn's win. A host whose request handling carries its own FileSystem shadowed the one the session started with: on Cloudflare, Alchemy provides Node's services to every Durable Object call, so the file tools read the Worker's empty temporary disk instead of the session's workspace. Each tool handler now merges the session's platform services last. Co-Authored-By: Claude Opus 5.5 HumanLayer-Session: https://app.dev.codelayer.gg/sessions/01a0f3fc-13b0-758f-a343-fae04e83a7a9 --- packages/fold-core/src/Api/StartSession.ts | 18 +++++++++++- .../test/Api/StartSession.vi.test.ts | 28 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/packages/fold-core/src/Api/StartSession.ts b/packages/fold-core/src/Api/StartSession.ts index 17b9132..0ff0958 100644 --- a/packages/fold-core/src/Api/StartSession.ts +++ b/packages/fold-core/src/Api/StartSession.ts @@ -301,6 +301,20 @@ const providedPlatformServices: Effect.Effect> ) /** Lower the event log descriptor to its EventLog layer. */ +/** + * Run the tool's handler with the session's platform services over whatever is in scope where the turn + * runs. Effect AI merges the turn's services over the toolkit's, so a host whose request handling carries + * its own FileSystem (Alchemy's Worker runtime provides Node's) would otherwise shadow the session's. + */ +const withPlatformServices = ( + contribution: SessionToolContribution, + platformServices: Context.Context, +): SessionToolContribution => ({ + ...contribution, + handler: (params) => + contribution.handler(params).pipe(Effect.updateContext((context) => Context.merge(context, platformServices))), +}) + const eventLogLayerFor = ( log: FoldEventLog, platformServices: Context.Context, @@ -408,7 +422,9 @@ const assembleSessionGraph = (options: { (tool) => tool.init.pipe( Effect.provideContext(platformServices), - Effect.map((contribution) => toolContributions.set(tool, contribution)), + Effect.map((contribution) => + toolContributions.set(tool, withPlatformServices(contribution, platformServices)), + ), ), { discard: true }, ).pipe(Effect.asVoid) diff --git a/packages/fold-core/test/Api/StartSession.vi.test.ts b/packages/fold-core/test/Api/StartSession.vi.test.ts index 52bfbd6..75a7f05 100644 --- a/packages/fold-core/test/Api/StartSession.vi.test.ts +++ b/packages/fold-core/test/Api/StartSession.vi.test.ts @@ -388,3 +388,31 @@ it.effect('tool handlers that declare platform services read the FileSystem the ), ), ) + +it.effect('tool handlers keep the FileSystem the session started with when a turn runs under another', () => + Effect.gen(function* () { + const { model } = yield* scriptedModel(gptActiveModel, [ + toolCallTurn([{ id: 'provider-call-1', name: 'read_note', params: {} }]), + textTurn('done'), + ]) + const readNote = defineTool({ + name: 'read_note', + description: 'Reads the note file.', + success: Schema.String, + dependencies: platformToolDependencies, + handler: () => + Effect.flatMap(FileSystem.FileSystem, (fs) => fs.readFileString('/note.txt')).pipe(Effect.orDie), + }) + const noteFileSystem = (note: string) => FileSystem.makeNoop({ readFileString: () => Effect.succeed(note) }) + + const session = yield* startSession({ agent: defineAgent({ model, tools: [readNote] }) }).pipe( + Effect.provideService(FileSystem.FileSystem, noteFileSystem('from the session')), + ) + // A host whose request handling carries its own FileSystem, as Alchemy's Worker runtime does. + yield* session + .send('read the note') + .pipe(Effect.provideService(FileSystem.FileSystem, noteFileSystem('from the turn'))) + + expect(firstToolResultPart(yield* session.entries).result).toBe('from the session') + }).pipe(Effect.scoped), +) From a4bebf79bc97fcf6d869d277ade8bfe09268031f Mon Sep 17 00:00:00 2001 From: Kyle Mistele Date: Wed, 30 Sep 2026 16:10:40 -0700 Subject: [PATCH 3/6] feat(agent): add narrow entry points for file tools and disk skills `@humanlayer/fold-agent/tools/files` exports read, write, edit and apply_patch (with a `fileTools()` bundle that `codingTools` now uses), and `@humanlayer/fold-agent/skills` exports the disk skill loader. Hosts such as Cloudflare Workers can use them without pulling in bash, the provider packages, or config loading. The read tool's TextDecoder now passes `ignoreBOM: false`, its default, because Cloudflare's type definitions require the field. Co-Authored-By: Claude Opus 5.5 HumanLayer-Session: https://app.dev.codelayer.gg/sessions/01a0f3fc-13b0-758f-a343-fae04e83a7a9 --- packages/fold-agent/package.json | 4 +++- packages/fold-agent/src/Tools/CodingTools.ts | 10 ++------ packages/fold-agent/src/Tools/FileTools.ts | 24 ++++++++++++++++++++ packages/fold-agent/src/Tools/ReadTool.ts | 2 +- packages/fold-agent/src/index.ts | 1 + 5 files changed, 31 insertions(+), 10 deletions(-) create mode 100644 packages/fold-agent/src/Tools/FileTools.ts diff --git a/packages/fold-agent/package.json b/packages/fold-agent/package.json index 04b5f77..644fa2e 100644 --- a/packages/fold-agent/package.json +++ b/packages/fold-agent/package.json @@ -7,7 +7,9 @@ "module": "src/index.ts", "types": "src/index.ts", "exports": { - ".": "./src/index.ts" + ".": "./src/index.ts", + "./skills": "./src/Skills/DiskSkills.ts", + "./tools/files": "./src/Tools/FileTools.ts" }, "scripts": { "typecheck": "tsc --noEmit", diff --git a/packages/fold-agent/src/Tools/CodingTools.ts b/packages/fold-agent/src/Tools/CodingTools.ts index 20d295c..881635a 100644 --- a/packages/fold-agent/src/Tools/CodingTools.ts +++ b/packages/fold-agent/src/Tools/CodingTools.ts @@ -6,12 +6,9 @@ */ import type { FoldTool } from '@humanlayer/fold-core' -import { applyPatchTool } from './ApplyPatchTool' import { bashTool, type BashToolOptions } from './BashTool' -import { editTool } from './EditTool' -import { readTool } from './ReadTool' +import { fileTools } from './FileTools' import { webTools, type WebToolsOptions } from './WebTools' -import { writeTool } from './WriteTool' /** Options for {@link codingTools}: the shared cwd plus bash output-spill configuration. */ export type CodingToolsOptions = Pick & @@ -22,10 +19,7 @@ export type CodingToolsOptions = Pick => [ - readTool(options), - writeTool(options), - editTool(options), - applyPatchTool(options), + ...fileTools(options), bashTool(options), ...webTools(options), ] diff --git a/packages/fold-agent/src/Tools/FileTools.ts b/packages/fold-agent/src/Tools/FileTools.ts new file mode 100644 index 0000000..27111f4 --- /dev/null +++ b/packages/fold-agent/src/Tools/FileTools.ts @@ -0,0 +1,24 @@ +/** + * This file bundles the file tools - read, write, edit, and apply_patch - over the ambient FileSystem. + * It is also the `@humanlayer/fold-agent/tools/files` entry, for hosts such as Cloudflare Workers that + * want these tools without the rest of fold-agent (bash, providers, config). + */ +import type { FoldTool } from '@humanlayer/fold-core' + +import { applyPatchTool } from './ApplyPatchTool' +import { editTool } from './EditTool' +import { readTool } from './ReadTool' +import { writeTool } from './WriteTool' + +export { applyPatchTool } from './ApplyPatchTool' +export { editTool } from './EditTool' +export { readTool } from './ReadTool' +export { writeTool } from './WriteTool' + +/** read, write, edit, and apply_patch, resolving relative paths against `cwd`. */ +export const fileTools = (options?: { readonly cwd?: string }): ReadonlyArray => [ + readTool(options), + writeTool(options), + editTool(options), + applyPatchTool(options), +] diff --git a/packages/fold-agent/src/Tools/ReadTool.ts b/packages/fold-agent/src/Tools/ReadTool.ts index 1d90f07..c16d08a 100644 --- a/packages/fold-agent/src/Tools/ReadTool.ts +++ b/packages/fold-agent/src/Tools/ReadTool.ts @@ -82,7 +82,7 @@ const decodeTextFile = (path: string, bytes: Uint8Array): Effect.Effect new TextDecoder('utf-8', { fatal: true }).decode(bytes), + try: () => new TextDecoder('utf-8', { fatal: true, ignoreBOM: false }).decode(bytes), catch: () => ToolResultFailure.make({ text: `Cannot read file because it is not valid UTF-8: ${path}` }), }) } diff --git a/packages/fold-agent/src/index.ts b/packages/fold-agent/src/index.ts index fa20eef..01e35e1 100644 --- a/packages/fold-agent/src/index.ts +++ b/packages/fold-agent/src/index.ts @@ -41,6 +41,7 @@ export * from './Tools/ApplyPatchTool' export * from './Tools/BashTool' export * from './Tools/CodingTools' export * from './Tools/EditTool' +export * from './Tools/FileTools' export * from './Tools/Image/Mime' export * from './Tools/Image/Process' export * from './Tools/ReadTool' From e6804aaccadd04a34f9fe3d6582ce964b648c866 Mon Sep 17 00:00:00 2001 From: Kyle Mistele Date: Wed, 30 Sep 2026 16:10:40 -0700 Subject: [PATCH 4/6] feat(examples): give alchemy-cloudflare sessions a workspace of cloned repos Each session gets a @cloudflare/computer workspace: a filesystem with git in a Computer Durable Object, hosted by a separate plain Worker and named by the session's id. The first message's repos ([{ url, name?, ref? }]) clone into /workspace/ before the session starts; a failed clone is a 422 and starts nothing. The agent gets fold-agent's file tools and skill loader over the workspace, through an Effect FileSystem backed by the Computer's file methods. Skills load from each repo's .claude/skills and .agents/skills, and the system prompt lists the repos. The log route moves to GET /sessions/:id/log. Clones and file calls log through Effect's logger, visible with `bun alchemy tail`. Co-Authored-By: Claude Opus 5.5 HumanLayer-Session: https://app.dev.codelayer.gg/sessions/01a0f3fc-13b0-758f-a343-fae04e83a7a9 --- bun.lock | 152 +++++++++++++- examples/alchemy-cloudflare/README.md | 30 ++- examples/alchemy-cloudflare/alchemy.run.ts | 3 + examples/alchemy-cloudflare/package.json | 4 + examples/alchemy-cloudflare/src/Api.ts | 27 +-- .../alchemy-cloudflare/src/ChatSession.ts | 128 ++++++++++-- .../alchemy-cloudflare/src/ChatSessions.ts | 39 +++- examples/alchemy-cloudflare/src/Workspace.ts | 87 ++++++++ .../src/WorkspaceFileSystem.ts | 136 +++++++++++++ .../src/computer/Computer.ts | 125 ++++++++++++ .../src/computer/ComputerWorker.ts | 15 ++ .../src/computer/Contract.ts | 43 ++++ examples/alchemy-cloudflare/test/Api.test.ts | 85 ++++++-- .../test/WorkspaceFileSystem.test.ts | 189 ++++++++++++++++++ package.json | 2 + 15 files changed, 1000 insertions(+), 65 deletions(-) create mode 100644 examples/alchemy-cloudflare/src/Workspace.ts create mode 100644 examples/alchemy-cloudflare/src/WorkspaceFileSystem.ts create mode 100644 examples/alchemy-cloudflare/src/computer/Computer.ts create mode 100644 examples/alchemy-cloudflare/src/computer/ComputerWorker.ts create mode 100644 examples/alchemy-cloudflare/src/computer/Contract.ts create mode 100644 examples/alchemy-cloudflare/test/WorkspaceFileSystem.test.ts diff --git a/bun.lock b/bun.lock index 18923e1..a4943fa 100644 --- a/bun.lock +++ b/bun.lock @@ -21,13 +21,17 @@ "name": "@humanlayer/example-alchemy-cloudflare", "version": "0.0.0", "dependencies": { + "@cloudflare/computer": "catalog:", "@effect/platform-bun": "catalog:", "@effect/platform-node": "catalog:", + "@humanlayer/fold-agent": "workspace:*", "@humanlayer/fold-core": "workspace:*", + "@platformatic/vfs": "catalog:", "alchemy": "catalog:", "effect": "catalog:", }, "devDependencies": { + "@cloudflare/workers-types": "catalog:", "@effect/vitest": "catalog:", "@humanlayer/fold-vitest-config": "workspace:*", "typescript": "catalog:", @@ -261,6 +265,7 @@ }, }, "catalog": { + "@cloudflare/computer": "0.3.1", "@cloudflare/workers-types": "5.20260929.1", "@effect/platform-bun": "4.0.0-rc.112", "@effect/platform-node": "4.0.0-rc.112", @@ -269,6 +274,7 @@ "@opentui/core": "0.4.3", "@opentui/keymap": "0.4.3", "@opentui/solid": "0.4.3", + "@platformatic/vfs": "0.4.0", "@silvia-odwyer/photon-node": "0.3.4", "@types/bun": "1.3.14", "@types/node": "26.1.0", @@ -389,6 +395,8 @@ "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], + "@borewit/text-codec": ["@borewit/text-codec@0.2.2", "", {}, "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ=="], + "@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@10.5.0", "", { "dependencies": { "@chevrotain/gast": "10.5.0", "@chevrotain/types": "10.5.0", "lodash": "4.17.21" } }, "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw=="], "@chevrotain/gast": ["@chevrotain/gast@10.5.0", "", { "dependencies": { "@chevrotain/types": "10.5.0", "lodash": "4.17.21" } }, "sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A=="], @@ -401,6 +409,8 @@ "@clack/prompts": ["@clack/prompts@1.8.1", "", { "dependencies": { "@clack/core": "1.5.1", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-dlT1m5e/0yUL0kRNcQn7yGLVThkgbB0Ga/1AmfDDC/8ik6AIiSf2QLQO2zPYvefsHP0aFgxO93cVLCCfDp7kzQ=="], + "@cloudflare/computer": ["@cloudflare/computer@0.3.1", "", { "dependencies": { "acorn": "^8.17.0", "capnweb": "^0.10.0", "just-bash": "^3.0.1" }, "peerDependencies": { "@platformatic/vfs": "*", "ai": "^6.0.196 || ^7.0.0", "zod": "^4.4.3" }, "optionalPeers": ["@platformatic/vfs", "ai", "zod"] }, "sha512-am79tH9IukXidlJZTXeMAs7rvy4KgdlbBoOM1HgLBdxrlpeP1aQf2YLvXhxKGzSh6PkV/NmxSz0R2BcwSoRzPA=="], + "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.2", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": ">1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-JBP1+Z7ZSNG/d4mRP+y8VC5dka3tZVMLEZRvS+rzQ4DGV1EoxRFQckcJTTkXbHSQiTj0DtNI01Zwb/V2fX0mvQ=="], "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260704.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-XO+vvdhhTNZSsIWCkZ+JaE/JrFUmQAB0H0y/sVkAf32xa2TYBRvFUMwjSqf6WxtlxcKPQvmbLfpO/UQ0l/q4eQ=="], @@ -557,6 +567,16 @@ "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.5", "", { "os": "win32", "cpu": "x64" }, "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg=="], + "@jitl/quickjs-ffi-types": ["@jitl/quickjs-ffi-types@0.32.0", "", {}, "sha512-v9T+GQpmk43VDJ7d72sf0Nexhk+ArvtUihW27dy7lqAl0zBObFKtSBBIm5RBjwIhE8VwsPPm9PNuvPvNqLWUEg=="], + + "@jitl/quickjs-wasmfile-debug-asyncify": ["@jitl/quickjs-wasmfile-debug-asyncify@0.32.0", "", { "dependencies": { "@jitl/quickjs-ffi-types": "0.32.0" } }, "sha512-EX8zbXwGqCgAE764M+qvkHtyXDi/FUoMBea0JnES7vCM3P7a2+EOZOjGv85wtZ2sJhI1oJ+nekmqpOODFDY+hw=="], + + "@jitl/quickjs-wasmfile-debug-sync": ["@jitl/quickjs-wasmfile-debug-sync@0.32.0", "", { "dependencies": { "@jitl/quickjs-ffi-types": "0.32.0" } }, "sha512-LeYWrPGC1uNCTBWvibo3ZLJj0CSVNYUXvJpXMCmuQ5Sap2cCACc3uvGvYV4homHHBAzfw5akoTqMMS4YFRtw+Q=="], + + "@jitl/quickjs-wasmfile-release-asyncify": ["@jitl/quickjs-wasmfile-release-asyncify@0.32.0", "", { "dependencies": { "@jitl/quickjs-ffi-types": "0.32.0" } }, "sha512-3oSwPfja12ICz4aIblB58cuY8JlEq5Txt8Cut4VLo+LH47QN+mzCnSgnbB03hWzg1LBcc+VyyI9UOag7a1NF+Q=="], + + "@jitl/quickjs-wasmfile-release-sync": ["@jitl/quickjs-wasmfile-release-sync@0.32.0", "", { "dependencies": { "@jitl/quickjs-ffi-types": "0.32.0" } }, "sha512-BKNDI/TPBfGlLNGYpLrhcDGXmIk4xHm4MRAisOBnOzpXVn9HZWsfmMAc9WMBrAHjvvds6HOikKeaOBKdPdpVrg=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], @@ -603,6 +623,8 @@ "@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="], + "@mongodb-js/zstd": ["@mongodb-js/zstd@7.0.0", "", { "dependencies": { "node-addon-api": "^8.5.0", "prebuild-install": "^7.1.3" } }, "sha512-mQ2s0pYYiav+tzCDR05Zptem8Ey2v8s11lri5RKGhTtL4COVCvVCk5vtyRYNT+9L8qSfyOqqefF9UtnW8mC5jA=="], + "@mrleebo/prisma-ast": ["@mrleebo/prisma-ast@0.13.1", "", { "dependencies": { "chevrotain": "^10.5.0", "lilconfig": "^2.1.0" } }, "sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw=="], "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ=="], @@ -779,6 +801,8 @@ "@paralleldrive/cuid2": ["@paralleldrive/cuid2@3.3.0", "", { "dependencies": { "@noble/hashes": "^2.0.1", "bignumber.js": "^9.3.1", "error-causes": "^3.0.2" }, "bin": { "cuid2": "bin/cuid2.js" } }, "sha512-OqiFvSOF0dBSesELYY2CAMa4YINvlLpvKOz/rv6NeZEqiyttlHgv98Juwv4Ch+GrEV7IZ8jfI2VcEoYUjXXCjw=="], + "@platformatic/vfs": ["@platformatic/vfs@0.4.0", "", {}, "sha512-JwRxSIG63e/VaDSkYXkSBajySt5MJzxYIKuknYda28fhhmJMqyUGf5rbKrGW+vO9L83nIjQH3+YI12fp7EKICQ=="], + "@prisma/debug": ["@prisma/debug@7.2.0", "", {}, "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw=="], "@prisma/dev": ["@prisma/dev@0.20.0", "", { "dependencies": { "@electric-sql/pglite": "0.3.15", "@electric-sql/pglite-socket": "0.0.20", "@electric-sql/pglite-tools": "0.2.20", "@hono/node-server": "1.19.9", "@mrleebo/prisma-ast": "0.13.1", "@prisma/get-platform": "7.2.0", "@prisma/query-plan-executor": "7.2.0", "foreground-child": "3.3.1", "get-port-please": "3.2.0", "hono": "4.11.4", "http-status-codes": "2.3.0", "pathe": "2.0.3", "proper-lockfile": "4.1.2", "remeda": "2.33.4", "std-env": "3.10.0", "valibot": "1.2.0", "zeptomatch": "2.1.0" } }, "sha512-ovlBYwWor0OzG+yH4J3Ot+AneD818BttLA+Ii7wjbcLHUrnC4tbUPVGyNd3c/+71KETPKZfjhkTSpdS15dmXNQ=="], @@ -861,6 +885,10 @@ "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="], + + "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="], "@types/aws-lambda": ["@types/aws-lambda@8.10.164", "", {}, "sha512-XOnrazWcOd6yWPnR7DxqCBPPYciDjHq+NN8LPwwqBCJNXLyPJgKo47fZpJ113+oyAn9Zti0Cf6H4k6yPcE3WFg=="], @@ -937,6 +965,8 @@ "@vitest/utils": ["@vitest/utils@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA=="], + "acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="], + "alchemy": ["alchemy@2.0.0-beta.76", "", { "dependencies": { "@alchemy.run/cloudflare-runtime": "2.0.0-beta.76", "@alchemy.run/floci": "2.0.0-beta.76", "@alchemy.run/node-utils": "2.0.0-beta.76", "@aws-sdk/credential-providers": "^3.0.0", "@clack/prompts": "^1.7.0", "@distilled.cloud/aws": "1.0.0-rc.8", "@distilled.cloud/axiom": "1.0.0-rc.8", "@distilled.cloud/cloudflare": "1.0.0-rc.8", "@distilled.cloud/core": "1.0.0-rc.8", "@distilled.cloud/fly-io": "1.0.0-rc.8", "@distilled.cloud/hetzner": "1.0.0-rc.8", "@distilled.cloud/neon": "1.0.0-rc.8", "@distilled.cloud/planetscale": "1.0.0-rc.8", "@distilled.cloud/railway": "1.0.0-rc.8", "@effect/sql-d1": ">=4.0.0-rc.112 || >=4.0.0", "@effect/sql-sqlite-do": ">=4.0.0-rc.112 || >=4.0.0", "@effect/vitest": ">=4.0.0-rc.112 || >=4.0.0", "@libsql/client": "^0.17.0", "@octokit/rest": "^22.0.1", "@octokit/webhooks": "^14.2.0", "@prisma/dev": "^0.20.0", "@smithy/node-config-provider": "^4.0.0", "@smithy/shared-ini-file-loader": "^4.3.4", "@smithy/types": "^4.8.1", "@types/aws-lambda": "^8.10.152", "aws4fetch": "^1.0.20", "capnweb": "^0.6.1", "fast-glob": "^3.3.2", "fast-xml-parser": "^5.3.4", "ink": "^6.3.1", "jszip": "^3.10.1", "libsodium-wrappers": "^0.8.3", "pathe": "^2.0.3", "picomatch": "^4.0.4", "react": "^19.2.0", "rolldown": "1.2.5", "undici": "^7.16.0", "yaml": "^2.0.0" }, "peerDependencies": { "@alchemy.run/frontend-frameworks": "2.0.0-beta.76", "@aws/durable-execution-sdk-js": "^2.1.0", "@effect/platform-bun": ">=4.0.0-rc.112 || >=4.0.0", "@effect/platform-node": ">=4.0.0-rc.112 || >=4.0.0", "@effect/sql-mysql2": ">=4.0.0-rc.112 || >=4.0.0", "@effect/sql-pg": ">=4.0.0-rc.112 || >=4.0.0", "@vercel/nft": "^1.10.2", "drizzle-kit": "1.0.0-rc.5-ab785fc", "drizzle-orm": "1.0.0-rc.5-ab785fc", "effect": ">=4.0.0-rc.112 || >=4.0.0", "mongodb": "^6.10.0", "mysql2": "^3.24.2", "pg": "^8.22.0", "vite": "^8.0.7", "ws": "^8.20.0" }, "optionalPeers": ["@alchemy.run/frontend-frameworks", "@aws/durable-execution-sdk-js", "@effect/platform-bun", "@effect/platform-node", "@effect/sql-mysql2", "@effect/sql-pg", "@vercel/nft", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "pg", "vite", "ws"], "bin": { "alchemy": "./bin/cli.js" } }, "sha512-XpcsXWk38YV9nO0v7s5PPjlMVtF8OQPeP8YqLu1iMzrq33bwViEOqkwQn3cgFiD7/QsnNcCju+hCXyeBwXrdtg=="], "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], @@ -961,7 +991,9 @@ "babel-preset-solid": ["babel-preset-solid@1.9.12", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.6" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.12" }, "optionalPeers": ["solid-js"] }, "sha512-LLqnuKVDlKpyBlMPcH6qEvs/wmS9a+NczppxJ3ryS/c0O5IiSFOIBQi9GzyiGDSbcJpx4Gr87jyFTos1MyEuWg=="], - "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.10.43", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ=="], @@ -969,14 +1001,18 @@ "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + "bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="], - "brace-expansion": ["brace-expansion@2.1.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA=="], + "brace-expansion": ["brace-expansion@5.0.12", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], "browserslist": ["browserslist@4.28.6", "", { "dependencies": { "baseline-browser-mapping": "^2.10.42", "caniuse-lite": "^1.0.30001803", "electron-to-chromium": "^1.5.389", "node-releases": "^2.0.51", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw=="], + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + "bun-ffi-structs": ["bun-ffi-structs@0.2.4", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-AJzsqoVFs1KBbJbWHIYrVZLDC3NhTqqh25awRXqzoLzmBAKr5oqk6+CwuYHAekKx+VBCYVohBoKuRq40dV+TYg=="], "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], @@ -985,7 +1021,7 @@ "capnp-es": ["capnp-es@0.0.14", "", { "peerDependencies": { "typescript": "^5.7.3" }, "optionalPeers": ["typescript"], "bin": { "capnp-es": "dist/compiler/capnpc-js.mjs", "capnpc-js": "dist/compiler/capnpc-js.mjs", "capnpc-ts": "dist/compiler/capnpc-ts.mjs", "capnpc-dts": "dist/compiler/capnpc-dts.mjs" } }, "sha512-8lWj4GJISiqRSlAJGkWpI4Azib7QY5UDIkqxeHcI7aAnXVk9SFuWxl1Fme+2HhzNANV0WTJqwUZvvXvloc3sBA=="], - "capnweb": ["capnweb@0.6.1", "", {}, "sha512-fmhV26QPd1ewf5R74h55oVZnGwIcSaRMzbfLQUy8+zOBjuTmT3KXoT8wxHvnp1m9Ht9BoUUS5ZwNLoVLfQTyBg=="], + "capnweb": ["capnweb@0.10.0", "", {}, "sha512-lQ2nlzaifkqBeNfs8RLErn7NZ8FNf3vn1W7J3nSH73edWG+0B5nHZGR51S2S9HMZEuZcEypOO469dZRY/BluFA=="], "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], @@ -993,6 +1029,8 @@ "chevrotain": ["chevrotain@10.5.0", "", { "dependencies": { "@chevrotain/cst-dts-gen": "10.5.0", "@chevrotain/gast": "10.5.0", "@chevrotain/types": "10.5.0", "@chevrotain/utils": "10.5.0", "lodash": "4.17.21", "regexp-to-ast": "0.5.0" } }, "sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A=="], + "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], + "cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="], "cli-cursor": ["cli-cursor@4.0.0", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="], @@ -1005,6 +1043,8 @@ "code-excerpt": ["code-excerpt@4.0.0", "", { "dependencies": { "convert-to-spaces": "^2.0.1" } }, "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA=="], + "commander": ["commander@6.2.1", "", {}, "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA=="], + "content-type": ["content-type@3.1.1", "", {}, "sha512-GW4qUsfFo59d0HbUibDlWv5wPz+vAAcaTWbKIuKCf0JkC7wWkSyf8f13IpXn5JkeMlB2P8iTSSIjwXljorg2vA=="], "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], @@ -1019,6 +1059,10 @@ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], + + "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="], @@ -1029,6 +1073,8 @@ "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], + "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], @@ -1047,6 +1093,8 @@ "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], + "expect-type": ["expect-type@1.4.0", "", {}, "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA=="], "fast-check": ["fast-check@4.9.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg=="], @@ -1067,6 +1115,8 @@ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "file-type": ["file-type@21.3.4", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g=="], + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], "find-babel-config": ["find-babel-config@2.1.2", "", { "dependencies": { "json5": "^2.2.3" } }, "sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg=="], @@ -1075,6 +1125,8 @@ "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], @@ -1089,6 +1141,8 @@ "get-port-please": ["get-port-please@3.2.0", "", {}, "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A=="], + "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], + "glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="], "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -1111,12 +1165,16 @@ "http-status-codes": ["http-status-codes@2.3.0", "", {}, "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA=="], + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="], "indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="], "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + "ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], + "ink": ["ink@6.8.0", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.4", "ansi-escapes": "^7.3.0", "ansi-styles": "^6.2.1", "auto-bind": "^5.0.1", "chalk": "^5.6.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", "cli-truncate": "^5.1.1", "code-excerpt": "^4.0.0", "es-toolkit": "^1.39.10", "indent-string": "^5.0.0", "is-in-ci": "^2.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.33.0", "scheduler": "^0.27.0", "signal-exit": "^3.0.7", "slice-ansi": "^8.0.0", "stack-utils": "^2.0.6", "string-width": "^8.1.1", "terminal-size": "^4.0.1", "type-fest": "^5.4.1", "widest-line": "^6.0.0", "wrap-ansi": "^9.0.0", "ws": "^8.18.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=19.0.0", "react": ">=19.0.0", "react-devtools-core": ">=6.1.2" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-sbl1RdLOgkO9isK42WCZlJCFN9hb++sX9dsklOvfd1YQ3bQ2AiFu12Q6tFlr0HvEUvzraJntQCCpfEoUe9DSzA=="], "is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="], @@ -1155,6 +1213,8 @@ "jszip": ["jszip@3.10.2", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-3l+rb15IOWtUhU0H5MFqES/T6Kh7abYwjosBey/vD6hDt8zoEffkSC5Ws5SGtgVw3gBx2NEbhTeSW1+kWkpyTQ=="], + "just-bash": ["just-bash@3.4.2", "", { "dependencies": { "diff": "^8.0.4", "fast-xml-parser": "^5.10.1", "file-type": "^21.3.4", "ini": "^6.0.0", "minimatch": "^10.2.6", "modern-tar": "^0.7.7", "papaparse": "^5.6.0", "quickjs-emscripten": "^0.32.0", "re2js": "^1.3.3", "seek-bzip": "^2.0.0", "smol-toml": "^1.8.0", "sprintf-js": "^1.1.3", "sql.js": "^1.14.1", "turndown": "^7.2.4", "undici": "^7.29.0", "yaml": "^2.9.0" }, "optionalDependencies": { "@mongodb-js/zstd": "^7.0.0", "node-liblzma": "^2.2.0" }, "bin": { "just-bash": "dist/bin/just-bash.js", "just-bash-shell": "dist/bin/shell/shell.js" } }, "sha512-T0Vpy7YRgCjxJdqG3tkxn0ZnIDLJvVwb8hH4L+6NVdp+Te27jQxjxnszW9ODjEKbWxWujj83rP5S0GQxCSufgg=="], + "libsodium": ["libsodium@0.8.4", "", {}, "sha512-lMcYaRi0zcs7tarATsQUYC7rstliIXZuoq0c6zXSgNtSNtdvBgkSegjWhpMJAXzKX3SUSwIp7+zEsob+j3LuRw=="], "libsodium-wrappers": ["libsodium-wrappers@0.8.4", "", { "dependencies": { "libsodium": "^0.8.0" } }, "sha512-mu8aAWucZjTB5O/BtGXtW4e1agy7uHxNYG7zPthmmD1jU43LCDmSWZLN4JhflbdPXj3yDO4lxM1O9hLDgIOXDw=="], @@ -1211,11 +1271,17 @@ "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], - "minimatch": ["minimatch@8.0.7", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg=="], + "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], + + "minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], "minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="], - "modern-tar": ["modern-tar@0.8.5", "", {}, "sha512-snEhs+6G5Tjd4I7tLCDOaoln2RgE0bD19RzEKgvgK2hZ5VKy3MpLhLTZ2fWpXSTg4K2cyPwp+VHATFJhxfnOeA=="], + "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], + + "modern-tar": ["modern-tar@0.7.7", "", {}, "sha512-t9VmxaqrmANnEOBhpSDI6HD192Ge48k8vmWqQQL7hSFEqHEYwZbbsu49+aKLWZeRvFs3j1pMhXOqqF4kPlvjkQ=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -1225,12 +1291,24 @@ "nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="], + "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], + + "node-abi": ["node-abi@3.96.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-rebQ/lz7i0EkoLzUVSrKRzA69zMkwLp95kKMWoMDkkM00Suxz0D7zEQPwRml5fQum24mj7bPvmlgLAmu2JCiYg=="], + + "node-addon-api": ["node-addon-api@8.9.2", "", {}, "sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg=="], + + "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], + "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], + "node-liblzma": ["node-liblzma@2.2.0", "", { "dependencies": { "node-addon-api": "^8.5.0", "node-gyp-build": "^4.8.4" }, "bin": { "nxz": "lib/cli/nxz.js" } }, "sha512-s0KzNOWwOJJgPG6wxg6cKohnAl9Wk/oW1KrQaVzJBjQwVcUGPQCzpR46Ximygjqj/3KhOrtJXnYMp/xYAXp75g=="], + "node-releases": ["node-releases@2.0.51", "", {}, "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ=="], "obug": ["obug@2.1.3", "", {}, "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg=="], + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], "oxfmt": ["oxfmt@0.62.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.62.0", "@oxfmt/binding-android-arm64": "0.62.0", "@oxfmt/binding-darwin-arm64": "0.62.0", "@oxfmt/binding-darwin-x64": "0.62.0", "@oxfmt/binding-freebsd-x64": "0.62.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.62.0", "@oxfmt/binding-linux-arm-musleabihf": "0.62.0", "@oxfmt/binding-linux-arm64-gnu": "0.62.0", "@oxfmt/binding-linux-arm64-musl": "0.62.0", "@oxfmt/binding-linux-ppc64-gnu": "0.62.0", "@oxfmt/binding-linux-riscv64-gnu": "0.62.0", "@oxfmt/binding-linux-riscv64-musl": "0.62.0", "@oxfmt/binding-linux-s390x-gnu": "0.62.0", "@oxfmt/binding-linux-x64-gnu": "0.62.0", "@oxfmt/binding-linux-x64-musl": "0.62.0", "@oxfmt/binding-openharmony-arm64": "0.62.0", "@oxfmt/binding-win32-arm64-msvc": "0.62.0", "@oxfmt/binding-win32-ia32-msvc": "0.62.0", "@oxfmt/binding-win32-x64-msvc": "0.62.0" }, "peerDependencies": { "svelte": "^5.0.0", "vite-plus": "*" }, "optionalPeers": ["svelte", "vite-plus"], "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-vxgGHTmnDU9j4CX7dDBLzxgmHxfda/yPcgJkGCMUSCwRmz+euo/V08xXLNgXTeqAB9Fhf3Pe2nO1RNKLCVgphQ=="], @@ -1247,6 +1325,8 @@ "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], + "papaparse": ["papaparse@5.7.0", "", {}, "sha512-qBGxg/7Q3Kl9Wfhrz2Z74UnvnHTXLNG6jmKJFeBvP2+y4lV7So+7SR62+Zd47JvdrCkX+nDcnr0ObPzek/+6RA=="], + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], "patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="], @@ -1273,16 +1353,28 @@ "postcss": ["postcss@8.5.19", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ=="], + "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], + "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], "promise-limit": ["promise-limit@2.7.0", "", {}, "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw=="], "proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="], + "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], + "pure-rand": ["pure-rand@8.4.2", "", {}, "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng=="], "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + "quickjs-emscripten": ["quickjs-emscripten@0.32.0", "", { "dependencies": { "@jitl/quickjs-wasmfile-debug-asyncify": "0.32.0", "@jitl/quickjs-wasmfile-debug-sync": "0.32.0", "@jitl/quickjs-wasmfile-release-asyncify": "0.32.0", "@jitl/quickjs-wasmfile-release-sync": "0.32.0", "quickjs-emscripten-core": "0.32.0" } }, "sha512-So0Sqw869y/S2oE3Nuc0uT3Dhqgvsj8FSrwBdsuTosVsG8ME5/OcudU1GxsrIFdFABgy17GHnTVO9TYV/bLQcA=="], + + "quickjs-emscripten-core": ["quickjs-emscripten-core@0.32.0", "", { "dependencies": { "@jitl/quickjs-ffi-types": "0.32.0" } }, "sha512-QFnPfjFey8EqknSrSxe1hZrf1/8z7/6s1QzGOmKo6++02r7QRRX7ZoyNaZh7JuVjWsVW87KnQrbZqnHkOAzUyg=="], + + "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], + + "re2js": ["re2js@1.4.0", "", {}, "sha512-KTOIcZTSOpOxbu3i0+T6mFQ6tkxXKlTxfcMFs1trQbsMnG84qNq+DjXr8Afu+FEFjvF1NNlldpC7roPyazFI8g=="], + "react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="], "react-devtools-core": ["react-devtools-core@7.0.1", "", { "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" } }, "sha512-C3yNvRHaizlpiASzy7b9vbnBGLrhvdhl1CbdU6EnZgxPNbai60szdLtl+VL76UNOt5bOoVTOz5rNWZxgGt+Gsw=="], @@ -1317,6 +1409,8 @@ "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + "seek-bzip": ["seek-bzip@2.0.0", "", { "dependencies": { "commander": "^6.0.0" }, "bin": { "seek-bunzip": "bin/seek-bunzip", "seek-table": "bin/seek-bzip-table" } }, "sha512-SMguiTnYrhpLdk3PwfzHeotrcwi8bNV4iemL9tx9poR/yeaMYwB9VzR1w7b57DuWpuqR8n6oZboi0hj3AxZxQg=="], + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "seroval": ["seroval@1.5.5", "", {}, "sha512-bSjOuPcwPKLSJNhr9+bZxA20nQxVle5J5MNsYRVE6cIg7KpRLXGupymePavu0jrxlPiPsr4xGZSB8yUY2sH2sw=="], @@ -1337,14 +1431,24 @@ "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], + + "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], "slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="], + "smol-toml": ["smol-toml@1.9.0", "", {}, "sha512-hpd+HLON7HdZXqYchMM/+LaTTbdK0AU3NngIJ4KVyWbY9bfQqdL9cD+4yf6dUoU2Ap4VsU0JkQi6FxAI1B2mXQ=="], + "solid-js": ["solid-js@1.9.12", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-QzKaSJq2/iDrWR1As6MHZQ8fQkdOBf8GReYb7L5iKwMGceg7HxDcaOHk0at66tNgn9U2U7dXo8ZZpLIAmGMzgw=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], + + "sql.js": ["sql.js@1.14.2", "", {}, "sha512-3ZGPovObMFrdw79zrUHbfdE/DLIsy8jdNdssmMSQuRAymedU6q84asPt0kgiqrdMYlPegDItiIMfmIXzZnYFcw=="], + "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="], "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], @@ -1357,14 +1461,22 @@ "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], + "strnum": ["strnum@2.4.2", "", { "dependencies": { "anynum": "^1.0.1" } }, "sha512-rDG3Ah4TV0k1hWvLSzkZtMmLN9+eS+h3knq4MP6A42Y3Yh5qGNnOUs1jJkoSr8FG5dsL28c7KgkIBzSEykqtuw=="], + "strtok3": ["strtok3@10.3.5", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA=="], + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], + "tar-fs": ["tar-fs@2.1.5", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw=="], + + "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + "terminal-size": ["terminal-size@4.0.1", "", {}, "sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ=="], "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], @@ -1379,14 +1491,20 @@ "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + "token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], + "turndown": ["turndown@7.2.4", "", { "dependencies": { "@mixmark-io/domino": "^2.2.0" } }, "sha512-I8yFsfRzmzK0WV1pNNOA4A7y4RDfFxPRxb3t+e3ui14qSGOxGtiSP6GjeX+Y6CHb7HYaFj7ECUD7VE5kQMZWGQ=="], "type-fest": ["type-fest@5.10.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-NoSdpq/WEiAg5sjmBkmV/hfxv6HJH4NqPNrqjtSO5CwRmpsDfaf4begxW34KdJykH/l1yHtwBWQkCRdoXO8mPA=="], "typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], + "uint8array-extras": ["uint8array-extras@1.6.0", "", {}, "sha512-8iAasVS4wUx0gPLjH8Xtz2PeDzTSiy8QiLGu2DT3X5GU+egFEQhpnbtkehbAwjvDcxIERmCYcysiODFmE3Ud0Q=="], + "undici": ["undici@8.10.1", "", {}, "sha512-YQ3WlbqjYMmNpdvDH64jAgLjxuAR9+649calDWhbshYaeQGO2bR4nI94ORJmwI3J9YhoKQnpyGOK+0zlWS5N5Q=="], "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], @@ -1417,6 +1535,8 @@ "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + "ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], "xml-naming": ["xml-naming@0.3.0", "", {}, "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ=="], @@ -1447,32 +1567,50 @@ "@prisma/dev/std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + "@puppeteer/browsers/modern-tar": ["modern-tar@0.8.5", "", {}, "sha512-snEhs+6G5Tjd4I7tLCDOaoln2RgE0bD19RzEKgvgK2hZ5VKy3MpLhLTZ2fWpXSTg4K2cyPwp+VHATFJhxfnOeA=="], + + "alchemy/capnweb": ["capnweb@0.6.1", "", {}, "sha512-fmhV26QPd1ewf5R74h55oVZnGwIcSaRMzbfLQUy8+zOBjuTmT3KXoT8wxHvnp1m9Ht9BoUUS5ZwNLoVLfQTyBg=="], + "alchemy/undici": ["undici@7.30.0", "", {}, "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ=="], "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], + "bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "cli-truncate/string-width": ["string-width@8.3.0", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ=="], "foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "glob/minimatch": ["minimatch@8.0.7", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg=="], + "ink/string-width": ["string-width@8.3.0", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ=="], + "just-bash/diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="], + + "just-bash/undici": ["undici@7.30.0", "", {}, "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ=="], + "libsql/detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="], "make-dir/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + "node-abi/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "path-scurry/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + "rc/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], + "react-devtools-core/ws": ["ws@7.5.12", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-1xGnbYN3zbog9CwuNDQULNRrTCLIn46/WmpR1f0w6PsCYQHkylZr5vkd6kfMZYV6pRnQkcPNRyiA8LsrNKyhpg=="], "sharp/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + "tar-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "vite/rolldown": ["rolldown@1.1.5", "", { "dependencies": { "@oxc-project/types": "=0.139.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.5", "@rolldown/binding-darwin-arm64": "1.1.5", "@rolldown/binding-darwin-x64": "1.1.5", "@rolldown/binding-freebsd-x64": "1.1.5", "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", "@rolldown/binding-linux-arm64-gnu": "1.1.5", "@rolldown/binding-linux-arm64-musl": "1.1.5", "@rolldown/binding-linux-ppc64-gnu": "1.1.5", "@rolldown/binding-linux-s390x-gnu": "1.1.5", "@rolldown/binding-linux-x64-gnu": "1.1.5", "@rolldown/binding-linux-x64-musl": "1.1.5", "@rolldown/binding-openharmony-arm64": "1.1.5", "@rolldown/binding-wasm32-wasi": "1.1.5", "@rolldown/binding-win32-arm64-msvc": "1.1.5", "@rolldown/binding-win32-x64-msvc": "1.1.5" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA=="], "widest-line/string-width": ["string-width@8.3.0", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ=="], @@ -1483,6 +1621,8 @@ "@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], + "glob/minimatch/brace-expansion": ["brace-expansion@2.1.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA=="], + "vite/rolldown/@oxc-project/types": ["@oxc-project/types@0.139.0", "", {}, "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw=="], "vite/rolldown/@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.5", "", { "os": "android", "cpu": "arm64" }, "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ=="], @@ -1512,5 +1652,7 @@ "vite/rolldown/@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.1.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw=="], "vite/rolldown/@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.1.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA=="], + + "glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], } } diff --git a/examples/alchemy-cloudflare/README.md b/examples/alchemy-cloudflare/README.md index 076c8b3..2f2da27 100644 --- a/examples/alchemy-cloudflare/README.md +++ b/examples/alchemy-cloudflare/README.md @@ -1,14 +1,17 @@ # alchemy-cloudflare -A minimal fold chat host on Cloudflare, deployed with [Alchemy v2](https://alchemy.run/cloudflare/). Inference runs on OpenAI (`gpt-5.6-terra`, medium reasoning) through fold's `openaiModel`. Each fold session is one Durable Object, named by its `SessionId`. +A minimal fold chat host on Cloudflare, deployed with [Alchemy v2](https://alchemy.run/cloudflare/). Inference runs on OpenAI (`gpt-5.6-terra`, medium reasoning) through fold's `openaiModel`. Each fold session is one Durable Object, named by its `SessionId`, with a workspace of cloned repos in a second one of the same name. -- `src/ChatSession.ts` — the Durable Object. It opens the fold log in its SQLite on activation; the first message starts the session, later activations resume it. +- `src/ChatSession.ts` — the Durable Object. It opens the fold log in its SQLite on activation; the first message clones its repos and starts the session, later activations resume it. - `src/DurableObjectEventLog.ts` — the service that opens fold's `EventLogService` over the object's SQLite. - `src/Keepalive.ts` — a 30s alarm heartbeat that keeps the object alive while a turn runs. - `src/ChatSessions.ts` — the service the routes require; its `layer` reaches each session's Durable Object. +- `src/Workspace.ts` — the service ChatSession clones repos and reaches files through; its `layer` reaches the session's Computer. +- `src/WorkspaceFileSystem.ts` — Effect's `FileSystem` over the Computer's file methods, so fold-agent's file tools and skill loader run on the workspace unchanged. +- `src/computer/` — the Computer Worker: a plain Durable Object holding a [`@cloudflare/computer`](https://github.com/cloudflare/computer) workspace (a filesystem in its SQLite, with git) per session. A separate Worker, because `@cloudflare/computer` needs the plain `cloudflare:workers` class, and an Effect Worker's bundle keeps only its own exports. - `src/Api.ts` — the `HttpRouter` routes, encoding responses with fold's log schemas. - `src/Worker.ts` — serves the routes, with `ChatSessions` as RPC to the Durable Objects. -- `alchemy.run.ts` — the stack. +- `alchemy.run.ts` — the stack: the Computer Worker, then the chat Worker, which binds it by script name. ## Run @@ -20,16 +23,31 @@ bun run deploy # prints the Worker url ``` ```sh -ID=sess_... # any new SessionId; the first message starts that session -curl -X POST "$URL/sessions/$ID/messages" -d '{"text":"hi"}' # the agent-finished entry +ID=sess_... # any new SessionId; the first message clones its repos and starts that session +curl -X POST "$URL/sessions/$ID/messages" -d '{"text":"hi","repos":[{"url":"https://github.com/humanlayer/fold"}]}' # the agent-finished entry +curl -X POST "$URL/sessions/$ID/messages" -d '{"text":"hi"}' # later messages' repos are ignored curl -X POST "$URL/sessions/$ID/messages" -d '{"text":"stop","whenRunning":"interrupt"}' # queue (default) | steer | interrupt -curl "$URL/sessions/$ID" # all log entries +curl "$URL/sessions/$ID/log" # the fold event log ``` +`bun alchemy tail` streams both Workers' logs. Each clone logs `workspace.prepare`, and each file call from the agent's tools logs `workspace.file` with its method, path and result. + +Each repo is `{ "url": "https://...", "name"?: string, "ref"?: string }`: it clones into `/workspace/` (`name` defaults to the URL's last path segment) at `ref` (the default branch when absent), shallow. A repo that fails to clone makes the first message a 422 and starts nothing; the next message clones again from scratch. The log's `session_started` entry records each repo's commit. + +The agent gets fold-agent's file tools on the workspace: `read`, plus `write` and `edit` for Claude models or `apply_patch` for GPT models (fold picks by model). Relative paths resolve against `/workspace`. Skills load from each repo's `.claude/skills` and `.agents/skills` when the session opens. The system prompt lists the repos. + `bun run test` checks the routes against an in-memory `ChatSessions` built on fold's in-memory log. ## Limits - A turn cut off by a crash or deploy continues when the object next wakes, through a synthetic "continue" user message (at most 3 per cut-off turn). A subagent cut off with it is left for the root model to resume by id. +- The agent can't run commands, list directories, or search yet, so it must know or guess file paths. + +- The file tools read and write the workspace, not the repos' remotes: changes stay in the session's workspace. `read` can't process images in a Worker (its image library needs Node), so an image comes back as a short note, not the picture. + +- The Computer Worker's script name is fixed (`src/computer/Contract.ts`), so an account holds one deploy of this stack. + +- Clones go over HTTPS, public repos only. They run in the Durable Object through `isomorphic-git`, which fetches every file at the tip, so large repos clone slowly. + - Replies arrive whole. `FoldSession.events` can stream deltas once a route forwards them. diff --git a/examples/alchemy-cloudflare/alchemy.run.ts b/examples/alchemy-cloudflare/alchemy.run.ts index 0a7f514..94c1ded 100644 --- a/examples/alchemy-cloudflare/alchemy.run.ts +++ b/examples/alchemy-cloudflare/alchemy.run.ts @@ -2,12 +2,15 @@ import * as Alchemy from 'alchemy' import * as Cloudflare from 'alchemy/Cloudflare' import { Effect } from 'effect' +import { ComputerWorker } from './src/computer/ComputerWorker' import Worker from './src/Worker' export default Alchemy.Stack( 'FoldAlchemyCloudflare', { providers: Cloudflare.providers(), state: Cloudflare.state() }, Effect.gen(function* () { + // First: ChatWorker binds the Computer Durable Object by this Worker's script name. + yield* ComputerWorker const worker = yield* Worker return { url: worker.url } }), diff --git a/examples/alchemy-cloudflare/package.json b/examples/alchemy-cloudflare/package.json index f6677ca..a9b7528 100644 --- a/examples/alchemy-cloudflare/package.json +++ b/examples/alchemy-cloudflare/package.json @@ -13,13 +13,17 @@ "test:watch": "bun vitest" }, "dependencies": { + "@cloudflare/computer": "catalog:", + "@humanlayer/fold-agent": "workspace:*", "@humanlayer/fold-core": "workspace:*", + "@platformatic/vfs": "catalog:", "@effect/platform-bun": "catalog:", "@effect/platform-node": "catalog:", "alchemy": "catalog:", "effect": "catalog:" }, "devDependencies": { + "@cloudflare/workers-types": "catalog:", "@effect/vitest": "catalog:", "@humanlayer/fold-vitest-config": "workspace:*", "typescript": "catalog:", diff --git a/examples/alchemy-cloudflare/src/Api.ts b/examples/alchemy-cloudflare/src/Api.ts index cbe489f..9f8862c 100644 --- a/examples/alchemy-cloudflare/src/Api.ts +++ b/examples/alchemy-cloudflare/src/Api.ts @@ -2,21 +2,19 @@ * The chat HTTP routes over {@link ChatSessions}, encoding responses with fold's log schemas. * * - `POST /sessions/:sessionId/messages` with `{ "text": string, "whenRunning"?: "queue" | "steer" | - * "interrupt" }` delivers one user message, starting the session if the id is new, and returns the - * `agent-finished` entry of the run that consumed it. `whenRunning` defaults to `queue`. - * - `GET /sessions/:sessionId` returns the session's log entries. + * "interrupt", "repos"?: [{ "url": string, "name"?: string, "ref"?: string }] }` delivers one user + * message and returns the `agent-finished` entry of the run that consumed it. `whenRunning` defaults to + * `queue`. A message to a new id clones its `repos` into `/workspace/` and starts the session; + * if a repo fails to clone the response is a 422 and nothing starts. + * - `GET /sessions/:sessionId/log` returns the session's fold event log. */ import { AgentFinishedLogEntry, LogEntry, SessionId } from '@humanlayer/fold-core' import { Effect, Schema } from 'effect' import { HttpRouter, HttpServerRequest, HttpServerResponse } from 'effect/unstable/http' -import { ChatSessions, WhenRunning } from './ChatSessions' +import { ChatSessions, Message } from './ChatSessions' const SessionParams = Schema.Struct({ sessionId: SessionId }) -const MessageBody = Schema.Struct({ - text: Schema.String, - whenRunning: WhenRunning.pipe(Schema.withDecodingDefaultKey(Effect.succeed('queue'))), -}) const entriesJson = HttpServerResponse.schemaJson(Schema.Array(LogEntry)) const finishedJson = HttpServerResponse.schemaJson(AgentFinishedLogEntry) @@ -28,7 +26,7 @@ export const ChatRoutes = HttpRouter.use((router) => yield* router.add( 'GET', - '/sessions/:sessionId', + '/sessions/:sessionId/log', Effect.gen(function* () { const { sessionId } = yield* HttpRouter.schemaPathParams(SessionParams) return yield* entriesJson(yield* sessions.entries(sessionId)) @@ -40,9 +38,14 @@ export const ChatRoutes = HttpRouter.use((router) => '/sessions/:sessionId/messages', Effect.gen(function* () { const { sessionId } = yield* HttpRouter.schemaPathParams(SessionParams) - const { text, whenRunning } = yield* HttpServerRequest.schemaBodyJson(MessageBody) - return yield* finishedJson(yield* sessions.send(sessionId, text, whenRunning)) - }).pipe(Effect.catchTag('SchemaError', badRequest)), + const message = yield* HttpServerRequest.schemaBodyJson(Message) + return yield* finishedJson(yield* sessions.send(sessionId, message)) + }).pipe( + Effect.catchTags({ + SchemaError: badRequest, + RepoCloneError: ({ message }) => Effect.succeed(HttpServerResponse.text(message, { status: 422 })), + }), + ), ) }), ) diff --git a/examples/alchemy-cloudflare/src/ChatSession.ts b/examples/alchemy-cloudflare/src/ChatSession.ts index 9360f49..83f0589 100644 --- a/examples/alchemy-cloudflare/src/ChatSession.ts +++ b/examples/alchemy-cloudflare/src/ChatSession.ts @@ -1,7 +1,11 @@ /** * One fold session per Durable Object, named by its SessionId. The object's SQLite log is the session: - * addressing a new id creates the object with an empty log, and its first `send` starts the session - * under that id; a written log resumes on activation. Callers never create a session explicitly. + * addressing a new id creates the object with an empty log, and its first `send` clones the message's repos + * into the session's {@link Workspace} and starts the session under that id; a written log resumes on + * activation. Callers never create a session explicitly. + * + * The agent reads and changes the repos' files through fold's file tools, and loads skills from each repo's + * `.claude/skills` and `.agents/skills`, all on the workspace. * * Every turn runs under a {@link Keepalive} lease so the object is not evicted mid-run. A turn can still * be cut off - a crash, a deploy - leaving the root's user message with no finished run after it. When a @@ -9,6 +13,8 @@ * Fold fills any tool call the cut left without a result, and the model can resume a cut-off subagent * by id itself. A turn that keeps getting cut off stops being nudged after {@link MAX_RESTART_NUDGES}. */ +import { skillsFromDisk } from '@humanlayer/fold-agent/skills' +import { fileTools } from '@humanlayer/fold-agent/tools/files' import { SessionId, type AgentId, @@ -19,17 +25,58 @@ import { eventLogSource, openaiModel, resumeSession, + skillTool, startSession, } from '@humanlayer/fold-core' import * as Cloudflare from 'alchemy/Cloudflare' -import { Config, Effect, Match, Option, Predicate, Result, Schema, Scope, Stream } from 'effect' +import { + Config, + Effect, + FileSystem, + Match, + Option, + Path, + Predicate, + Result, + Schema, + Scope, + Stream, + SynchronizedRef, +} from 'effect' -import type { WhenRunning } from './ChatSessions' +import type { Message, WhenRunning } from './ChatSessions' +import { WORKSPACE_ROOT } from './computer/Contract' import { DurableObjectEventLog } from './DurableObjectEventLog' import { Keepalive } from './Keepalive' +import { type Repo, Workspace } from './Workspace' const MODEL = 'gpt-5.6-terra' +/** The agent's home directory. Nothing is there unless the agent puts it there. */ +const HOME = '/root' + +const systemPrompt = (repoNames: ReadonlyArray) => + [ + 'You are a helpful, concise coding assistant.', + repoNames.length === 0 + ? 'No repos are cloned for this session.' + : `This session's repos are cloned under ${WORKSPACE_ROOT}:\n${repoNames.map((name) => `- ${WORKSPACE_ROOT}/${name}`).join('\n')}`, + 'You can read and change their files. You cannot run commands yet.', + ].join('\n\n') + +/** Skills from each repo's `.claude/skills` and `.agents/skills`, scanned once when the session opens. */ +const repoSkills = (repoNames: ReadonlyArray) => + skillTool( + skillsFromDisk({ + cwd: WORKSPACE_ROOT, + home: HOME, + extraPaths: repoNames.flatMap((name) => [ + `${WORKSPACE_ROOT}/${name}/.claude/skills`, + `${WORKSPACE_ROOT}/${name}/.agents/skills`, + ]), + }), + ) + const RESTART_NUDGE = 'A restart cut you off before you finished. Continue where you left off.' const MAX_RESTART_NUDGES = 3 @@ -92,6 +139,7 @@ export default class ChatSession extends Cloudflare.DurableObject() const apiKey = yield* Config.redacted('OPENAI_API_KEY').pipe(Effect.orDie) const eventLogs = yield* DurableObjectEventLog const keepalive = yield* Keepalive + const workspace = yield* Workspace return Effect.gen(function* () { const sessionId = yield* Schema.decodeUnknownEffect(SessionId)(state.id.name) @@ -99,20 +147,56 @@ export default class ChatSession extends Cloudflare.DurableObject() const log = eventLogSource(Effect.succeed(eventLog)) const isEmpty = Option.isNone(yield* Stream.runHead(eventLog.entries())) - const agent = defineAgent({ - name: 'alchemy-cloudflare-chat', - systemPrompt: 'You are a helpful, concise assistant.', - model: openaiModel({ apiKey, model: MODEL, reasoning: 'medium' }), - }) + const fileSystem = workspace.fileSystem(sessionId) + const model = openaiModel({ apiKey, model: MODEL, reasoning: 'medium' }) + const agentFor = (repoNames: ReadonlyArray) => + defineAgent({ + name: 'alchemy-cloudflare-chat', + systemPrompt: systemPrompt(repoNames.toSorted()), + model, + tools: [...fileTools({ cwd: WORKSPACE_ROOT }), repoSkills(repoNames)], + }) // Never closed: the session lives as long as the object stays in memory, past every call's own // scope. Opened on the first send, so reading an unknown id writes nothing. const scope = yield* Scope.make() - const session = yield* Effect.cached( - (isEmpty ? startSession({ agent, log, sessionId }) : resumeSession({ agent, log })).pipe( - Scope.provide(scope), - ), - ) + + // A new session clones its repos, then starts; a cut-off start leaves the log empty, so the next + // send clones them again from scratch. + const start = (repos: ReadonlyArray) => + Effect.gen(function* () { + const cloned = yield* workspace.prepare(sessionId, repos) + return yield* startSession({ + agent: agentFor(cloned.map((repo) => repo.name)), + log, + sessionId, + cwd: WORKSPACE_ROOT, + meta: { repos: cloned }, + }) + }) + + // A written log's repos are the workspace's top-level directories. + const resume = Effect.gen(function* () { + const repoNames = yield* fileSystem.readDirectory(WORKSPACE_ROOT).pipe(Effect.orDie) + return yield* resumeSession({ agent: agentFor(repoNames), log }) + }) + + // Opened once, by whichever send or restart nudge comes first; `repos` only matter to a new session. + const opened = yield* SynchronizedRef.make(Option.none()) + const open = (repos: ReadonlyArray) => + SynchronizedRef.modifyEffect(opened, (current) => + Option.match(current, { + onSome: (session) => Effect.succeed([session, current] as const), + onNone: () => + (isEmpty ? start(repos) : resume).pipe( + // fold hands these to the file tools and the skill loader. + Effect.provideService(FileSystem.FileSystem, fileSystem), + Effect.provide(Path.layer), + Scope.provide(scope), + Effect.map((session) => [session, Option.some(session)] as const), + ), + }), + ) // A written log may hold a turn a restart cut off: nudge the root to continue it, in the background // under a lease so the heartbeat keeps the object alive until it lands - even when that heartbeat's @@ -120,21 +204,23 @@ export default class ChatSession extends Cloudflare.DurableObject() if (!isEmpty) { yield* Effect.forkIn( Effect.gen(function* () { - const opened = yield* session - const open = openRootMessages(yield* opened.entries, opened.rootAgentId) - if (open.length === 0 || open.filter(isRestartNudge).length >= MAX_RESTART_NUDGES) return - yield* keepalive.whileRunning(deliver(opened, RESTART_NUDGE, 'queue')) + const session = yield* open([]) + const cutOff = openRootMessages(yield* session.entries, session.rootAgentId) + if (cutOff.length === 0 || cutOff.filter(isRestartNudge).length >= MAX_RESTART_NUDGES) return + yield* keepalive.whileRunning(deliver(session, RESTART_NUDGE, 'queue')) }), scope, ) } return { - send: (text: string, whenRunning: WhenRunning) => - keepalive.whileRunning(Effect.flatMap(session, (opened) => deliver(opened, text, whenRunning))), + send: ({ text, whenRunning, repos }: Message) => + keepalive.whileRunning( + Effect.flatMap(open(repos ?? []), (session) => deliver(session, text, whenRunning)), + ), entries: () => Stream.runCollect(eventLog.entries()).pipe(Effect.orDie), alarm: () => keepalive.alarm, } }).pipe(Effect.orDie) - }).pipe(Effect.provide([DurableObjectEventLog.layer, Keepalive.layer])), + }).pipe(Effect.provide([DurableObjectEventLog.layer, Keepalive.layer, Workspace.layer])), ) {} diff --git a/examples/alchemy-cloudflare/src/ChatSessions.ts b/examples/alchemy-cloudflare/src/ChatSessions.ts index 2d4b8a4..a7330db 100644 --- a/examples/alchemy-cloudflare/src/ChatSessions.ts +++ b/examples/alchemy-cloudflare/src/ChatSessions.ts @@ -1,12 +1,13 @@ /** * The fold chat sessions this host serves, each addressed by its SessionId. There is no create step: - * the first `send` to an id starts that session. The Worker implements this as RPC to the ChatSession - * Durable Object holding each session. + * the first `send` to an id clones its repos and starts that session. The Worker implements this as RPC to + * the ChatSession Durable Object holding each session. */ import type { AgentFinishedLogEntry, LogEntry, SessionId } from '@humanlayer/fold-core' import { Context, Effect, Layer, Schema } from 'effect' import ChatSession from './ChatSession' +import { RepoCloneError, Repos } from './Workspace' /** * What `send` does when the session's root agent is already running. When it is idle, every mode just @@ -19,6 +20,17 @@ import ChatSession from './ChatSession' export const WhenRunning = Schema.Literals(['queue', 'steer', 'interrupt']) export type WhenRunning = typeof WhenRunning.Type +/** + * One user message. `repos` clone into the session's workspace when the message starts the session; later + * messages' `repos` are ignored. + */ +export const Message = Schema.Struct({ + text: Schema.String, + whenRunning: WhenRunning.pipe(Schema.withDecodingDefaultKey(Effect.succeed('queue'))), + repos: Schema.optionalKey(Repos), +}) +export type Message = typeof Message.Type + export class ChatSessions extends Context.Service< ChatSessions, { @@ -26,13 +38,10 @@ export class ChatSessions extends Context.Service< readonly entries: (sessionId: SessionId) => Effect.Effect> /** * Deliver one user message to the session's root agent, starting the session if it is new, and - * resolve with the `agent-finished` entry of the run that consumed it. + * resolve with the `agent-finished` entry of the run that consumed it. Fails, starting nothing, when a + * new session's repo does not clone. */ - readonly send: ( - sessionId: SessionId, - text: string, - whenRunning: WhenRunning, - ) => Effect.Effect + readonly send: (sessionId: SessionId, message: Message) => Effect.Effect } >()('alchemy-cloudflare/ChatSessions') { /** Each session is the ChatSession Durable Object named by its id. Build it in a Worker's construction. */ @@ -43,7 +52,19 @@ export class ChatSessions extends Context.Service< return ChatSessions.of({ entries: (sessionId) => sessions.getByName(sessionId).entries(), - send: (sessionId, text, whenRunning) => sessions.getByName(sessionId).send(text, whenRunning), + // The object's failures arrive encoded: decode the clone failure, and die on anything else. + send: (sessionId, message) => + sessions + .getByName(sessionId) + .send(message) + .pipe( + Effect.catch((error) => + Schema.decodeUnknownEffect(RepoCloneError)(error).pipe( + Effect.orDie, + Effect.flatMap(Effect.fail), + ), + ), + ), }) }), ) diff --git a/examples/alchemy-cloudflare/src/Workspace.ts b/examples/alchemy-cloudflare/src/Workspace.ts new file mode 100644 index 0000000..e99c615 --- /dev/null +++ b/examples/alchemy-cloudflare/src/Workspace.ts @@ -0,0 +1,87 @@ +/** + * Each session's workspace: the Computer Durable Object (see `computer/`) named by the session's id, + * reached across scripts from ChatSession. Repos clone into `/workspace/` when the session starts. + */ +import type { SessionId } from '@humanlayer/fold-core' +import type { RpcCallError } from 'alchemy' +import * as Cloudflare from 'alchemy/Cloudflare' +import { Context, Effect, type FileSystem, Layer, Schema } from 'effect' + +import { COMPUTER_WORKER_NAME, type ClonedRepo, type RepoSpec } from './computer/Contract' +import { type ComputerFiles, workspaceFileSystem } from './WorkspaceFileSystem' + +const REPO_NAME = /^(?!\.{1,2}$)[\w.-]+$/ + +/** + * One repo to clone: an https URL, a `ref` (the remote's default branch when absent), and the directory + * `name` under `/workspace` (the URL's last path segment when absent). + */ +export const Repo = Schema.Struct({ + url: Schema.String.check(Schema.isStartsWith('https://')), + name: Schema.optionalKey(Schema.String), + ref: Schema.optionalKey(Schema.String), +}) +export type Repo = typeof Repo.Type + +/** The directory a repo clones into, under `/workspace`. */ +export const repoName = (repo: Repo) => + repo.name ?? (repo.url.replace(/\/+$/, '').split('/').at(-1) ?? '').replace(/\.git$/, '') + +/** A session's repos: every directory name valid and distinct. */ +export const Repos = Schema.Array(Repo).check( + Schema.makeFilter((repos) => { + const names = repos.map(repoName) + const invalid = names.find((name) => !REPO_NAME.test(name)) + if (invalid !== undefined) return `repo directory name ${JSON.stringify(invalid)} is invalid; pass a "name"` + return new Set(names).size === names.length || 'repo directory names must be distinct' + }), +) + +/** A repo failed to clone, so the session did not start. Crosses the ChatSession RPC boundary encoded. */ +export class RepoCloneError extends Schema.TaggedError()('RepoCloneError', { + message: Schema.String, +}) {} + +/** The Computer Durable Object, as ChatSession sees it. Its class lives in the Computer Worker. */ +class Computer extends Cloudflare.DurableObject< + Computer, + ComputerFiles & { + readonly prepare: (repos: ReadonlyArray) => Effect.Effect, RpcCallError> + } +>()('Computer') {} + +export class Workspace extends Context.Service< + Workspace, + { + /** Clone the session's repos fresh, replacing any clone a cut-off attempt left behind. */ + readonly prepare: ( + sessionId: SessionId, + repos: ReadonlyArray, + ) => Effect.Effect, RepoCloneError> + /** The session's workspace as a `FileSystem`, for fold's file tools and skill loader. */ + readonly fileSystem: (sessionId: SessionId) => FileSystem.FileSystem + } +>()('alchemy-cloudflare/Workspace') { + /** Binds the Computer Durable Object; build it in a Worker's or Durable Object's construction. */ + static readonly layer = Layer.effect( + Workspace, + Effect.gen(function* () { + const computers = yield* Computer.from(COMPUTER_WORKER_NAME) + + return Workspace.of({ + prepare: Effect.fn('alchemy_cloudflare.workspace.prepare')((sessionId, repos) => + computers + .getByName(sessionId) + .prepare(repos.map((repo) => ({ name: repoName(repo), url: repo.url, ref: repo.ref ?? null }))) + .pipe( + Effect.tap((cloned) => + Effect.logInfo('workspace.prepare', JSON.stringify({ sessionId, cloned })), + ), + Effect.mapError((error) => new RepoCloneError({ message: error.message })), + ), + ), + fileSystem: (sessionId) => workspaceFileSystem(computers.getByName(sessionId)), + }) + }), + ) +} diff --git a/examples/alchemy-cloudflare/src/WorkspaceFileSystem.ts b/examples/alchemy-cloudflare/src/WorkspaceFileSystem.ts new file mode 100644 index 0000000..547526c --- /dev/null +++ b/examples/alchemy-cloudflare/src/WorkspaceFileSystem.ts @@ -0,0 +1,136 @@ +/** + * Effect's `FileSystem` over a session's Computer, so fold's file tools and skill loader work on the + * workspace unchanged. It covers what they call: reading, writing, making directories, removing, + * `stat`, `access`, `exists`, `realPath` and listing a directory. Every other method is Effect's no-op. + * + * The workspace has no permissions, so `access` only checks the path exists. It has symlinks, but + * `realPath` returns the path unchanged: fold only uses it to key its per-file write lock. + */ +import type { RpcCallError } from 'alchemy' +import { Effect, FileSystem, Option, PlatformError } from 'effect' + +import type { FileInfo, FileResult } from './computer/Contract' + +/** The Computer's file methods, as its RPC stub returns them. */ +export type ComputerFiles = { + readonly readFile: (path: string) => Effect.Effect, RpcCallError> + readonly writeFile: (path: string, content: string) => Effect.Effect, RpcCallError> + readonly mkdir: (path: string, recursive: boolean) => Effect.Effect, RpcCallError> + readonly rm: (path: string, recursive: boolean, force: boolean) => Effect.Effect, RpcCallError> + readonly stat: (path: string) => Effect.Effect, RpcCallError> + readonly readdir: (path: string) => Effect.Effect>, RpcCallError> +} + +const SYSTEM_ERROR_TAGS = new Map([ + ['ENOENT', 'NotFound'], + ['EEXIST', 'AlreadyExists'], + ['EACCES', 'PermissionDenied'], + ['EPERM', 'PermissionDenied'], + ['EISDIR', 'BadResource'], + ['ENOTDIR', 'BadResource'], + ['EINVAL', 'InvalidData'], +]) + +/** + * One Computer call as an Effect. A failed operation keeps its code in `cause.code`, where fold's tools + * read it; a failed RPC is `Unknown`, so the tool call fails and the session goes on. + */ +const run = ( + method: string, + path: string, + call: Effect.Effect, RpcCallError>, +): Effect.Effect => + call.pipe( + Effect.mapError((cause) => + PlatformError.systemError({ + _tag: 'Unknown', + module: 'FileSystem', + method, + pathOrDescriptor: path, + description: cause.message, + cause, + }), + ), + Effect.flatMap((result) => + result.ok + ? Effect.succeed(result.value) + : Effect.fail( + PlatformError.systemError({ + _tag: SYSTEM_ERROR_TAGS.get(result.code) ?? 'Unknown', + module: 'FileSystem', + method, + pathOrDescriptor: path, + description: result.message, + cause: { code: result.code }, + }), + ), + ), + Effect.tap(() => Effect.logInfo('workspace.file', JSON.stringify({ method, path, outcome: 'ok' }))), + Effect.tapError((error) => + Effect.logInfo( + 'workspace.file', + JSON.stringify({ method, path, outcome: error.reason._tag, error: error.message }), + ), + ), + ) + +const toInfo = (info: FileInfo): FileSystem.File.Info => ({ + type: info.type, + mtime: Option.some(new Date(info.mtime)), + atime: Option.none(), + birthtime: Option.none(), + dev: 0, + ino: Option.some(info.inode), + mode: info.mode, + nlink: Option.none(), + uid: Option.none(), + gid: Option.none(), + rdev: Option.none(), + size: FileSystem.Size(info.size), + blksize: Option.none(), + blocks: Option.none(), +}) + +export const workspaceFileSystem = (computer: ComputerFiles): FileSystem.FileSystem => { + const stat = (path: string) => run('stat', path, computer.stat(path)).pipe(Effect.map(toInfo)) + const readFile = (path: string) => run('readFile', path, computer.readFile(path)) + + return FileSystem.makeNoop({ + readFile, + readFileString: (path, encoding) => + Effect.flatMap(readFile(path), (bytes) => + Effect.try({ + try: () => new TextDecoder(encoding).decode(bytes), + catch: (cause) => + PlatformError.badArgument({ + module: 'FileSystem', + method: 'readFileString', + description: 'invalid encoding', + cause, + }), + }), + ), + writeFileString: (path, data) => run('writeFileString', path, computer.writeFile(path, data)), + makeDirectory: (path, options) => run('makeDirectory', path, computer.mkdir(path, options?.recursive ?? false)), + remove: (path, options) => + run('remove', path, computer.rm(path, options?.recursive ?? false, options?.force ?? false)), + stat, + access: (path) => Effect.asVoid(stat(path)), + exists: (path) => + stat(path).pipe( + Effect.as(true), + Effect.catchReasons('PlatformError', { NotFound: () => Effect.succeed(false) }), + ), + realPath: (path) => Effect.as(stat(path), path), + readDirectory: (path, options) => + options?.recursive === true + ? Effect.fail( + PlatformError.badArgument({ + module: 'FileSystem', + method: 'readDirectory', + description: 'recursive listing is not supported', + }), + ) + : run('readDirectory', path, computer.readdir(path)).pipe(Effect.map((names) => [...names])), + }) +} diff --git a/examples/alchemy-cloudflare/src/computer/Computer.ts b/examples/alchemy-cloudflare/src/computer/Computer.ts new file mode 100644 index 0000000..ee8deb1 --- /dev/null +++ b/examples/alchemy-cloudflare/src/computer/Computer.ts @@ -0,0 +1,125 @@ +/// +/** + * The Computer Durable Object: one `@cloudflare/computer` Workspace per fold session, named by its + * SessionId. The Workspace is a virtual filesystem in the object's SQLite, with git. A plain Durable Object + * rather than an Effect one, because `withWorkspace` must wrap the `cloudflare:workers` class. + */ +import { type DurableObjectStorageLike, getWorkspace, withWorkspace } from '@cloudflare/computer' +import { createGitClient, type GitClient, type GitCloneOptions } from '@cloudflare/computer/git' +import { DurableObject } from 'cloudflare:workers' + +import { type ClonedRepo, type FileInfo, type FileResult, type RepoSpec, WORKSPACE_ROOT } from './Contract' + +/** + * The object's storage as `@cloudflare/computer` types it. Its `exec` promises whatever row type the caller + * names, which a Durable Object's SQL cannot; the rows are the same at runtime. + */ +const workspaceStorage = (storage: DurableObjectStorage): DurableObjectStorageLike => { + function exec(query: string, ...bindings: Array): { toArray(): Array } + function exec(query: string, ...bindings: Array): { toArray(): Array } { + return storage.sql.exec(query, ...bindings) + } + return { + sql: { exec }, + transaction: (closure) => storage.transaction(async () => closure()), + transactionSync: (closure) => storage.transactionSync(closure), + } +} + +/** Run one file operation, returning a thrown workspace error as its code and message. */ +const attempt = async (operation: () => Promise): Promise> => { + try { + return { ok: true, value: await operation() } + } catch (cause) { + const code = + cause instanceof Error && 'code' in cause && typeof cause.code === 'string' ? cause.code : 'UNKNOWN' + return { ok: false, code, message: String(cause) } + } +} + +class ComputerBase extends DurableObject { + /** For withWorkspace's options, which see the instance but not `ctx`: DurableObject keeps it protected. */ + readonly storage = workspaceStorage(this.ctx.storage) +} + +export class Computer extends withWorkspace(ComputerBase, (self) => ({ + storage: self.storage, + git: createGitClient(), +})) { + /** + * Clone each repo into `/workspace/`, one at a time, replacing whatever a cut-off earlier attempt + * left there. Rejects with the first repo that fails. + */ + async prepare(repos: ReadonlyArray): Promise> { + using workspace = await getWorkspace(this) + const git: GitClient = workspace.git + await workspace.fs.mkdir(WORKSPACE_ROOT, { recursive: true }) + + const cloned: Array = [] + for (const repo of repos) { + const dir = `${WORKSPACE_ROOT}/${repo.name}` + try { + await workspace.fs.rm(dir, { recursive: true, force: true }) + const options: GitCloneOptions = { url: repo.url, dir } + if (repo.ref !== null) options.ref = repo.ref + await git.clone(options) + cloned.push({ ...repo, dir, commit: await git.revParse({ dir, ref: 'HEAD' }) }) + } catch (cause) { + throw new Error(`Cloning ${repo.name} from ${repo.url} failed: ${String(cause)}`, { cause }) + } + } + return cloned + } + + // The file methods fold's read, write, edit and apply_patch tools and its skill loader need. Paths are + // absolute. + + async readFile(path: string): Promise> { + using workspace = await getWorkspace(this) + return await attempt(async () => new Response(await workspace.fs.readFile(path)).bytes()) + } + + async writeFile(path: string, content: string): Promise> { + using workspace = await getWorkspace(this) + return await attempt(async () => { + await workspace.fs.writeFile(path, content) + return null + }) + } + + async mkdir(path: string, recursive: boolean): Promise> { + using workspace = await getWorkspace(this) + return await attempt(async () => { + await workspace.fs.mkdir(path, { recursive }) + return null + }) + } + + async rm(path: string, recursive: boolean, force: boolean): Promise> { + using workspace = await getWorkspace(this) + return await attempt(async () => { + await workspace.fs.rm(path, { recursive, force }) + return null + }) + } + + /** Follows symlinks. */ + async stat(path: string): Promise> { + using workspace = await getWorkspace(this) + return await attempt(async () => { + const stat = await workspace.fs.stat(path) + const type = stat.isDirectory ? 'Directory' : stat.isSymbolicLink ? 'SymbolicLink' : 'File' + return { type, size: stat.size, mtime: stat.mtime, mode: stat.mode, inode: stat.inode } + }) + } + + /** The directory's entry names. */ + async readdir(path: string): Promise>> { + using workspace = await getWorkspace(this) + return await attempt(async () => (await workspace.fs.readdir(path)).map((entry) => entry.name)) + } +} + +export default { + fetch: () => new Response('Not found', { status: 404 }), +} satisfies ExportedHandler diff --git a/examples/alchemy-cloudflare/src/computer/ComputerWorker.ts b/examples/alchemy-cloudflare/src/computer/ComputerWorker.ts new file mode 100644 index 0000000..a9a516e --- /dev/null +++ b/examples/alchemy-cloudflare/src/computer/ComputerWorker.ts @@ -0,0 +1,15 @@ +/** + * The Worker hosting the Computer Durable Object. An async Worker: an Effect Worker's bundle drops every + * export but its own, and `@cloudflare/computer` needs its classes exported from the Worker module. + */ +import * as Cloudflare from 'alchemy/Cloudflare' + +import type { Computer } from './Computer' +import { COMPUTER_WORKER_NAME } from './Contract' + +export const ComputerWorker = Cloudflare.Worker('ComputerWorker', { + name: COMPUTER_WORKER_NAME, + main: './src/computer/Computer.ts', + compatibility: { flags: ['nodejs_compat'], date: '2026-08-31' }, + env: { Computer: Cloudflare.DurableObject('Computer') }, +}) diff --git a/examples/alchemy-cloudflare/src/computer/Contract.ts b/examples/alchemy-cloudflare/src/computer/Contract.ts new file mode 100644 index 0000000..a996350 --- /dev/null +++ b/examples/alchemy-cloudflare/src/computer/Contract.ts @@ -0,0 +1,43 @@ +/** + * What both sides of the Computer boundary share: the Computer Worker's script name, where repos live, and + * the values its RPC methods take and return. Plain values only, so the Computer bundle stays free of Effect. + */ + +/** + * The Computer Worker's script name. Fixed, so ChatWorker can bind the Computer Durable Object across + * scripts; it also means one deploy of this stack per Cloudflare account. + */ +export const COMPUTER_WORKER_NAME = 'fold-alchemy-cloudflare-computer' + +/** Every repo clones into `${WORKSPACE_ROOT}/`. */ +export const WORKSPACE_ROOT = '/workspace' + +export type RepoSpec = { + readonly name: string + readonly url: string + /** Branch, tag, or commit; the remote's default branch when null. */ + readonly ref: string | null +} + +export type ClonedRepo = RepoSpec & { + readonly dir: string + /** The commit checked out. */ + readonly commit: string +} + +/** + * A file method's outcome. Failures come back as values, not thrown: an error thrown across RPC keeps its + * message but loses its `code` (`ENOENT`, `EISDIR`, ...), which the file tools need. + */ +export type FileResult = + | { readonly ok: true; readonly value: A } + | { readonly ok: false; readonly code: string; readonly message: string } + +export type FileInfo = { + readonly type: 'File' | 'Directory' | 'SymbolicLink' + readonly size: number + /** Milliseconds since the epoch. */ + readonly mtime: number + readonly mode: number + readonly inode: number +} diff --git a/examples/alchemy-cloudflare/test/Api.test.ts b/examples/alchemy-cloudflare/test/Api.test.ts index ae3d585..785de59 100644 --- a/examples/alchemy-cloudflare/test/Api.test.ts +++ b/examples/alchemy-cloudflare/test/Api.test.ts @@ -1,8 +1,8 @@ /** * The chat routes against an in-memory ChatSessions: each session is fold's in-memory EventLog, so the * routes encode and decode real log entries without a Durable Object or a model. The fake mirrors the - * real contract's shape - the first send to an id starts its session - and echoes the delivery mode so - * tests can see what the routes passed. + * real contract's shape - the first send to an id clones its repos and starts its session - and echoes the + * delivery mode so tests can see what the routes passed. A repo whose URL ends in `/missing` fails to clone. */ import { it } from '@effect/vitest' import { @@ -12,6 +12,7 @@ import { SessionId, layerInMemoryEventLog, type EventLogService, + type LogEntryInput, } from '@humanlayer/fold-core' import { Context, Effect, Layer, Predicate, Ref, Schema, Scope, Stream } from 'effect' import { HttpRouter } from 'effect/unstable/http' @@ -19,6 +20,7 @@ import { expect } from 'vitest' import { ChatRoutes } from '../src/Api' import { ChatSessions } from '../src/ChatSessions' +import { RepoCloneError, repoName } from '../src/Workspace' const rootAgentId = AgentId.create() @@ -41,25 +43,30 @@ const layerInMemory = Layer.effect( }) const entriesOf = (log: EventLogService) => Stream.runCollect(log.entries()).pipe(Effect.orDie) + const appendOrDie = (log: EventLogService, input: LogEntryInput) => Effect.orDie(log.append(input)) return ChatSessions.of({ entries: (sessionId) => Effect.flatMap(logFor(sessionId), entriesOf), - send: (sessionId, text, whenRunning) => + send: (sessionId, { text, whenRunning, repos = [] }) => Effect.gen(function* () { const log = yield* logFor(sessionId) if ((yield* entriesOf(log)).length === 0) { - yield* log.append({ + const missing = repos.find((repo) => repo.url.endsWith('/missing')) + if (missing !== undefined) { + return yield* new RepoCloneError({ message: `Cloning ${repoName(missing)} failed` }) + } + yield* appendOrDie(log, { _tag: 'session_started', agentId: null, parentAgentId: null, toolCallId: null, - cwd: null, + cwd: '/workspace', sessionId, rootAgentId, - meta: {}, + meta: { repos: repos.map(repoName) }, }) } - const finished = yield* log.append({ + const finished = yield* appendOrDie(log, { _tag: 'agent-finished', agentId: rootAgentId, parentAgentId: null, @@ -70,7 +77,7 @@ const layerInMemory = Layer.effect( }) if (Predicate.isTagged(finished, 'agent-finished')) return finished return yield* Effect.die(`appended ${finished._tag} while appending agent-finished`) - }).pipe(Effect.orDie), + }), }) }), ) @@ -93,12 +100,12 @@ const serve = Effect.acquireRelease( const decodeEntries = (response: Response) => Effect.promise(() => response.json()).pipe(Effect.flatMap(Schema.decodeUnknownEffect(Schema.Array(LogEntry)))) -it.effect('the first message to a new id starts its session; GET replays it', () => +it.effect('the first message to a new id starts its session; GET /log replays it', () => Effect.gen(function* () { const request = yield* serve const sessionId = SessionId.create() - expect(yield* request('GET', `/sessions/${sessionId}`).pipe(Effect.flatMap(decodeEntries))).toEqual([]) + expect(yield* request('GET', `/sessions/${sessionId}/log`).pipe(Effect.flatMap(decodeEntries))).toEqual([]) const sent = yield* request('POST', `/sessions/${sessionId}/messages`, { text: 'hi' }) expect(sent.status).toBe(200) @@ -107,7 +114,7 @@ it.effect('the first message to a new id starts its session; GET replays it', () resultText: 'queue: hi', }) - const entries = yield* request('GET', `/sessions/${sessionId}`).pipe(Effect.flatMap(decodeEntries)) + const entries = yield* request('GET', `/sessions/${sessionId}/log`).pipe(Effect.flatMap(decodeEntries)) expect(entries.map((entry) => entry._tag)).toEqual(['session_started', 'agent-finished']) }).pipe(Effect.scoped), ) @@ -129,9 +136,63 @@ it.effect('rejects bad ids, bodies, and modes', () => const request = yield* serve const messages = `/sessions/${SessionId.create()}/messages` - expect((yield* request('GET', '/sessions/not-a-session')).status).toBe(400) + expect((yield* request('GET', '/sessions/not-a-session/log')).status).toBe(400) expect((yield* request('POST', messages, { message: 'hi' })).status).toBe(400) expect((yield* request('POST', messages, { text: 'hi', whenRunning: 'later' })).status).toBe(400) expect((yield* request('GET', '/nope')).status).toBe(404) + expect((yield* request('GET', `/sessions/${SessionId.create()}`)).status).toBe(404) + }).pipe(Effect.scoped), +) + +it.effect('a new session clones the repos its first message names', () => + Effect.gen(function* () { + const request = yield* serve + const sessionId = SessionId.create() + + const sent = yield* request('POST', `/sessions/${sessionId}/messages`, { + text: 'hi', + repos: [ + { url: 'https://github.com/humanlayer/fold.git' }, + { url: 'https://github.com/x/y', name: 'other' }, + ], + }) + expect(sent.status).toBe(200) + + const [started] = yield* request('GET', `/sessions/${sessionId}/log`).pipe(Effect.flatMap(decodeEntries)) + expect(started).toMatchObject({ + _tag: 'session_started', + cwd: '/workspace', + meta: { repos: ['fold', 'other'] }, + }) + }).pipe(Effect.scoped), +) + +it.effect('a repo that fails to clone is a 422 and starts nothing', () => + Effect.gen(function* () { + const request = yield* serve + const sessionId = SessionId.create() + + const sent = yield* request('POST', `/sessions/${sessionId}/messages`, { + text: 'hi', + repos: [{ url: 'https://github.com/x/missing' }], + }) + expect(sent.status).toBe(422) + expect(yield* Effect.promise(() => sent.text())).toBe('Cloning missing failed') + expect(yield* request('GET', `/sessions/${sessionId}/log`).pipe(Effect.flatMap(decodeEntries))).toEqual([]) + }).pipe(Effect.scoped), +) + +it.effect('rejects repos that are not https or whose directory names are invalid or repeated', () => + Effect.gen(function* () { + const request = yield* serve + const send = (repos: ReadonlyArray) => + request('POST', `/sessions/${SessionId.create()}/messages`, { text: 'hi', repos }).pipe( + Effect.map((response) => response.status), + ) + + expect(yield* send([{ url: 'git@github.com:x/y.git' }])).toBe(400) + expect(yield* send([{ url: 'https://github.com/x/y', name: 'a b' }])).toBe(400) + expect(yield* send([{ url: 'https://github.com/x/y', name: '..' }])).toBe(400) + expect(yield* send([{ url: 'https://github.com/a/y' }, { url: 'https://github.com/b/y.git' }])).toBe(400) }).pipe(Effect.scoped), ) diff --git a/examples/alchemy-cloudflare/test/WorkspaceFileSystem.test.ts b/examples/alchemy-cloudflare/test/WorkspaceFileSystem.test.ts new file mode 100644 index 0000000..2356587 --- /dev/null +++ b/examples/alchemy-cloudflare/test/WorkspaceFileSystem.test.ts @@ -0,0 +1,189 @@ +/** + * The workspace FileSystem against an in-memory Computer that fails the way the real one does: with a + * code such as `ENOENT` in a result value. fold's disk skill loader then runs over it unchanged. + */ +import * as NodeServices from '@effect/platform-node/NodeServices' +import { it } from '@effect/vitest' +import { makeDiskSkillSource } from '@humanlayer/fold-agent/skills' +import { fileTools } from '@humanlayer/fold-agent/tools/files' +import { + AgentId, + CurrentAgent, + CurrentToolCall, + InterruptNote, + StopController, + Subagents, + ToolCallId, + ToolEvents, + ToolState, + type FoldTool, +} from '@humanlayer/fold-core' +import { Effect, FileSystem, Layer, type PlatformError } from 'effect' +import { expect } from 'vitest' + +import type { FileInfo, FileResult } from '../src/computer/Contract' +import { type ComputerFiles, workspaceFileSystem } from '../src/WorkspaceFileSystem' + +type Node = { readonly type: 'Directory' } | { readonly type: 'File'; readonly content: Uint8Array } + +const parentOf = (path: string) => path.slice(0, path.lastIndexOf('/')) || '/' + +const fakeComputer = (): ComputerFiles => { + const nodes = new Map([['/', { type: 'Directory' }]]) + const ok = (value: A): Effect.Effect> => Effect.succeed({ ok: true, value }) + const fail = (code: string, path: string): Effect.Effect> => + Effect.succeed({ ok: false, code, message: `${code}: ${path}` }) + const children = (path: string) => + [...nodes.keys()] + .filter((child) => child !== '/' && parentOf(child) === path) + .map((child) => child.slice(path.length).replace(/^\//, '')) + + return { + readFile: (path) => { + const node = nodes.get(path) + if (node === undefined) return fail('ENOENT', path) + return node.type === 'File' ? ok(node.content) : fail('EISDIR', path) + }, + writeFile: (path, content) => { + if (nodes.get(parentOf(path))?.type !== 'Directory') return fail('ENOENT', path) + nodes.set(path, { type: 'File', content: new TextEncoder().encode(content) }) + return ok(null) + }, + mkdir: (path, recursive) => { + if (nodes.has(path)) return recursive ? ok(null) : fail('EEXIST', path) + if (!nodes.has(parentOf(path)) && !recursive) return fail('ENOENT', path) + for (let dir = path; !nodes.has(dir); dir = parentOf(dir)) nodes.set(dir, { type: 'Directory' }) + return ok(null) + }, + rm: (path, recursive, force) => { + if (!nodes.has(path)) return force ? ok(null) : fail('ENOENT', path) + if (children(path).length > 0 && !recursive) return fail('ENOTEMPTY', path) + for (const key of nodes.keys()) if (key === path || key.startsWith(`${path}/`)) nodes.delete(key) + return ok(null) + }, + stat: (path) => { + const node = nodes.get(path) + if (node === undefined) return fail('ENOENT', path) + const info: FileInfo = { + type: node.type, + size: node.type === 'File' ? node.content.length : 0, + mtime: 0, + mode: 0o644, + inode: 1, + } + return ok(info) + }, + readdir: (path) => { + const node = nodes.get(path) + if (node === undefined) return fail('ENOENT', path) + return node.type === 'Directory' ? ok(children(path).sort()) : fail('ENOTDIR', path) + }, + } +} + +/** The services fold provides around every tool call, stubbed; the platform ones come from Node. */ +const toolCallServices = Layer.mergeAll( + NodeServices.layer, + Layer.succeed(ToolState, { get: () => Effect.succeed(null), set: () => Effect.void }), + Layer.succeed(ToolEvents, { emit: () => Effect.void }), + Layer.succeed(StopController, { requestStop: () => Effect.void, isStopRequested: Effect.succeed(false) }), + Layer.succeed(CurrentAgent, { agentId: AgentId.create(), parentAgentId: null }), + Layer.succeed(CurrentToolCall, { toolCallId: ToolCallId.create() }), + Layer.succeed(InterruptNote, { set: () => Effect.void }), + Layer.succeed(Subagents, { + dispatch: () => Effect.die('no subagents'), + fork: () => Effect.die('no subagents'), + resume: () => Effect.die('no subagents'), + continueSubagent: () => Effect.die('no subagents'), + }), +) + +/** Call one of fold's tools with the workspace as its FileSystem. */ +const call = (fs: FileSystem.FileSystem, tools: ReadonlyArray, name: string, params: unknown) => { + const tool = tools.find((candidate) => candidate.name === name) + if (tool === undefined) return Effect.die(`no ${name} tool`) + return tool.init.pipe( + Effect.flatMap((contribution) => contribution.handler(params)), + Effect.provideService(FileSystem.FileSystem, fs), + Effect.provide(toolCallServices), + ) +} + +const reasonTag = (error: PlatformError.PlatformError) => error.reason._tag + +it.effect('writes, reads, lists, and removes files', () => + Effect.gen(function* () { + const fs = workspaceFileSystem(fakeComputer()) + + yield* fs.makeDirectory('/workspace/app/src', { recursive: true }) + yield* fs.writeFileString('/workspace/app/src/index.ts', 'export {}\n') + + expect(yield* fs.readFileString('/workspace/app/src/index.ts')).toBe('export {}\n') + expect(yield* fs.readDirectory('/workspace/app')).toEqual(['src']) + expect((yield* fs.stat('/workspace/app/src')).type).toBe('Directory') + expect(yield* fs.exists('/workspace/app/src/index.ts')).toBe(true) + expect(yield* fs.realPath('/workspace/app/src/index.ts')).toBe('/workspace/app/src/index.ts') + + yield* fs.remove('/workspace/app', { recursive: true }) + expect(yield* fs.exists('/workspace/app')).toBe(false) + }), +) + +it.effect("maps the Computer's error codes to platform errors that keep the code", () => + Effect.gen(function* () { + const fs = workspaceFileSystem(fakeComputer()) + yield* fs.makeDirectory('/workspace') + + const missing = yield* Effect.flip(fs.readFile('/workspace/nope.txt')) + expect(reasonTag(missing)).toBe('NotFound') + expect(missing.reason.cause).toEqual({ code: 'ENOENT' }) + + expect(reasonTag(yield* Effect.flip(fs.readFile('/workspace')))).toBe('BadResource') + expect(reasonTag(yield* Effect.flip(fs.access('/workspace/nope.txt')))).toBe('NotFound') + expect(reasonTag(yield* Effect.flip(fs.writeFileString('/nowhere/file.txt', '')))).toBe('NotFound') + expect(reasonTag(yield* Effect.flip(fs.makeDirectory('/workspace')))).toBe('AlreadyExists') + }), +) + +it.effect("fold's skill loader finds a repo's skills on the workspace", () => + Effect.gen(function* () { + const fs = workspaceFileSystem(fakeComputer()) + yield* fs.makeDirectory('/workspace/app/.claude/skills/deploy', { recursive: true }) + yield* fs.writeFileString( + '/workspace/app/.claude/skills/deploy/SKILL.md', + '---\nname: deploy\ndescription: Ship the app.\n---\nRun the deploy.\n', + ) + + const skills = yield* makeDiskSkillSource({ + cwd: '/workspace', + home: '/root', + extraPaths: ['/workspace/app/.claude/skills', '/workspace/app/.agents/skills'], + }).pipe(Effect.provideService(FileSystem.FileSystem, fs)) + + expect(yield* skills.list).toEqual([{ name: 'deploy', description: 'Ship the app.' }]) + expect((yield* skills.load('deploy')).content).toBe('Run the deploy.') + }), +) + +it.effect("fold's file tools write, edit, patch and read files on the workspace", () => + Effect.gen(function* () { + const fs = workspaceFileSystem(fakeComputer()) + const tools = fileTools({ cwd: '/workspace' }) + yield* fs.makeDirectory('/workspace/app', { recursive: true }) + + yield* call(fs, tools, 'write', { path: 'app/notes.md', content: 'one\ntwo\n' }) + yield* call(fs, tools, 'edit', { path: 'app/notes.md', edits: [{ oldText: 'two', newText: 'three' }] }) + yield* call(fs, tools, 'apply_patch', { + patch_text: '*** Begin Patch\n*** Add File: app/new/added.md\n+added\n*** End Patch', + }) + + expect(yield* fs.readFileString('/workspace/app/notes.md')).toBe('one\nthree\n') + expect(yield* fs.readFileString('/workspace/app/new/added.md')).toBe('added\n') + expect(yield* call(fs, tools, 'read', { path: 'app/notes.md' })).toMatchObject({ + text: expect.stringContaining('three'), + }) + expect(yield* Effect.flip(call(fs, tools, 'read', { path: 'app/missing.md' }))).toMatchObject({ + text: expect.stringContaining('file not found'), + }) + }), +) diff --git a/package.json b/package.json index 383a80c..11a9237 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,8 @@ "catalog": { "@kitlangton/terminal-control": "0.3.1", "alchemy": "2.0.0-beta.76", + "@cloudflare/computer": "0.3.1", + "@platformatic/vfs": "0.4.0", "@cloudflare/workers-types": "5.20260929.1", "@opentui/core": "0.4.3", "@opentui/keymap": "0.4.3", From 00e9179e107b852ccd578fe689e6ce3d7082822b Mon Sep 17 00:00:00 2001 From: Kyle Mistele Date: Wed, 30 Sep 2026 17:07:28 -0700 Subject: [PATCH 5/6] feat(examples): add a bash tool and idle expiry to alchemy-cloudflare sessions bash: the Computer's workspace gets @cloudflare/computer's worker shell, just-bash in a Worker started through a Worker Loader binding, which reads and writes the same filesystem as the file tools. The agent's bash tool keeps fold-agent's parameters and output handling: stdout then stderr, trimmed to the last 2000 lines or 50KB, failing with the output on a non-zero exit or timeout. It covers the common text commands and git; it cannot install, build, or run tests. Idle expiry: a session with no messages for 14 days deletes its workspace, then its own log, and restarts empty, so its id starts a new session. The deadline shares the object's alarm with the keepalive heartbeat. The workspace also sets its own backup alarm a day later, so it is deleted even when its session never deletes it. A session that wakes past its deadline skips resuming and waits for the alarm, so it cannot reach into the workspace mid-delete or bring it back empty. Also: - A resumed session counts only folders in /workspace as repos. - ChatSessions passes through failures other than RepoCloneError instead of replacing them with a SchemaError. - Shell commands log `workspace.exec`. Co-Authored-By: Claude Opus 5.5 HumanLayer-Session: https://app.dev.codelayer.gg/sessions/01a0f3fc-13b0-758f-a343-fae04e83a7a9 --- examples/alchemy-cloudflare/README.md | 12 +- examples/alchemy-cloudflare/src/BashTool.ts | 93 ++++++++++++++ .../alchemy-cloudflare/src/ChatSession.ts | 49 ++++++-- .../alchemy-cloudflare/src/ChatSessions.ts | 2 +- examples/alchemy-cloudflare/src/Keepalive.ts | 11 +- .../alchemy-cloudflare/src/SessionExpiry.ts | 116 +++++++++++++++++ examples/alchemy-cloudflare/src/Workspace.ts | 68 +++++++++- .../src/WorkspaceFileSystem.ts | 16 +-- .../src/computer/Computer.ts | 99 +++++++++++++-- .../src/computer/ComputerWorker.ts | 6 +- .../src/computer/Contract.ts | 24 +++- .../alchemy-cloudflare/test/BashTool.test.ts | 113 +++++++++++++++++ .../test/SessionExpiry.test.ts | 119 ++++++++++++++++++ examples/alchemy-cloudflare/test/ToolCalls.ts | 42 +++++++ .../test/WorkspaceFileSystem.test.ts | 51 ++------ 15 files changed, 736 insertions(+), 85 deletions(-) create mode 100644 examples/alchemy-cloudflare/src/BashTool.ts create mode 100644 examples/alchemy-cloudflare/src/SessionExpiry.ts create mode 100644 examples/alchemy-cloudflare/test/BashTool.test.ts create mode 100644 examples/alchemy-cloudflare/test/SessionExpiry.test.ts create mode 100644 examples/alchemy-cloudflare/test/ToolCalls.ts diff --git a/examples/alchemy-cloudflare/README.md b/examples/alchemy-cloudflare/README.md index 2f2da27..5c23453 100644 --- a/examples/alchemy-cloudflare/README.md +++ b/examples/alchemy-cloudflare/README.md @@ -5,10 +5,12 @@ A minimal fold chat host on Cloudflare, deployed with [Alchemy v2](https://alche - `src/ChatSession.ts` — the Durable Object. It opens the fold log in its SQLite on activation; the first message clones its repos and starts the session, later activations resume it. - `src/DurableObjectEventLog.ts` — the service that opens fold's `EventLogService` over the object's SQLite. - `src/Keepalive.ts` — a 30s alarm heartbeat that keeps the object alive while a turn runs. +- `src/SessionExpiry.ts` — deletes an idle session and its workspace; see below. - `src/ChatSessions.ts` — the service the routes require; its `layer` reaches each session's Durable Object. - `src/Workspace.ts` — the service ChatSession clones repos and reaches files through; its `layer` reaches the session's Computer. +- `src/BashTool.ts` — the agent's `bash` tool, running commands in the workspace's shell. - `src/WorkspaceFileSystem.ts` — Effect's `FileSystem` over the Computer's file methods, so fold-agent's file tools and skill loader run on the workspace unchanged. -- `src/computer/` — the Computer Worker: a plain Durable Object holding a [`@cloudflare/computer`](https://github.com/cloudflare/computer) workspace (a filesystem in its SQLite, with git) per session. A separate Worker, because `@cloudflare/computer` needs the plain `cloudflare:workers` class, and an Effect Worker's bundle keeps only its own exports. +- `src/computer/` — the Computer Worker: a plain Durable Object holding a [`@cloudflare/computer`](https://github.com/cloudflare/computer) workspace (a filesystem in its SQLite, with git and a shell) per session. The shell is just-bash in a Worker that a Worker Loader starts, reading and writing the same filesystem. A separate Worker, because `@cloudflare/computer` needs the plain `cloudflare:workers` class, and an Effect Worker's bundle keeps only its own exports. - `src/Api.ts` — the `HttpRouter` routes, encoding responses with fold's log schemas. - `src/Worker.ts` — serves the routes, with `ChatSessions` as RPC to the Durable Objects. - `alchemy.run.ts` — the stack: the Computer Worker, then the chat Worker, which binds it by script name. @@ -30,11 +32,13 @@ curl -X POST "$URL/sessions/$ID/messages" -d '{"text":"stop","whenRunning":"inte curl "$URL/sessions/$ID/log" # the fold event log ``` -`bun alchemy tail` streams both Workers' logs. Each clone logs `workspace.prepare`, and each file call from the agent's tools logs `workspace.file` with its method, path and result. +A session with no messages for 14 days (`IDLE_TIME` in `src/SessionExpiry.ts`) deletes its workspace and its log; its id then starts a new session. The workspace also has its own deadline a day later, so it gets deleted even if its session never does it. + +`bun alchemy tail` streams both Workers' logs. Each clone logs `workspace.prepare`, each file call from the agent's tools logs `workspace.file` with its method, path and result, and each shell command logs `workspace.exec` with its exit code. Each repo is `{ "url": "https://...", "name"?: string, "ref"?: string }`: it clones into `/workspace/` (`name` defaults to the URL's last path segment) at `ref` (the default branch when absent), shallow. A repo that fails to clone makes the first message a 422 and starts nothing; the next message clones again from scratch. The log's `session_started` entry records each repo's commit. -The agent gets fold-agent's file tools on the workspace: `read`, plus `write` and `edit` for Claude models or `apply_patch` for GPT models (fold picks by model). Relative paths resolve against `/workspace`. Skills load from each repo's `.claude/skills` and `.agents/skills` when the session opens. The system prompt lists the repos. +The agent gets fold-agent's file tools on the workspace: `read`, plus `write` and `edit` for Claude models or `apply_patch` for GPT models (fold picks by model). Relative paths resolve against `/workspace`. It also gets `bash`, which runs in a lightweight shell: the common text commands (`ls`, `cat`, `grep`, `find`, `sed`, `awk`, `jq`, ...), pipes and redirects, and git. The shell and the file tools see the same files. Skills load from each repo's `.claude/skills` and `.agents/skills` when the session opens. The system prompt lists the repos. `bun run test` checks the routes against an in-memory `ChatSessions` built on fold's in-memory log. @@ -42,7 +46,7 @@ The agent gets fold-agent's file tools on the workspace: `read`, plus `write` an - A turn cut off by a crash or deploy continues when the object next wakes, through a synthetic "continue" user message (at most 3 per cut-off turn). A subagent cut off with it is left for the root model to resume by id. -- The agent can't run commands, list directories, or search yet, so it must know or guess file paths. +- The shell is not a full Linux machine: no package managers, no node or python, no compilers, and no network. It can't install, build, or run tests. - The file tools read and write the workspace, not the repos' remotes: changes stay in the session's workspace. `read` can't process images in a Worker (its image library needs Node), so an image comes back as a short note, not the picture. diff --git a/examples/alchemy-cloudflare/src/BashTool.ts b/examples/alchemy-cloudflare/src/BashTool.ts new file mode 100644 index 0000000..5bd2d31 --- /dev/null +++ b/examples/alchemy-cloudflare/src/BashTool.ts @@ -0,0 +1,93 @@ +/** + * The agent's `bash` tool, running commands in the session workspace's shell: just-bash, which covers the + * common text commands and git but no package managers or language runtimes. fold-agent's bash tool starts + * real processes, which a Worker cannot; this one keeps its parameters and its output handling - stdout and + * stderr trimmed to the last 2000 lines or 50KB, and a failure carrying the output when the command fails. + */ +import { + defaultMaxBytes, + defaultMaxLines, + defineTool, + formatSize, + ToolResultFailure, + ToolResultText, + truncateTail, + type FoldTool, +} from '@humanlayer/fold-core' +import { Effect, Schema } from 'effect' + +import { type CommandInput, type CommandOutput, WORKSPACE_ROOT } from './computer/Contract' +import type { ShellError } from './Workspace' + +const DEFAULT_TIMEOUT_MS = 120_000 +const MAX_TIMEOUT_MS = 600_000 + +const BashParameters = Schema.Struct({ + command: Schema.String.annotate({ description: 'Bash command to execute' }), + timeout_ms: Schema.optionalKey(Schema.Number).annotate({ + description: `Timeout in milliseconds (default ${DEFAULT_TIMEOUT_MS}, maximum ${MAX_TIMEOUT_MS})`, + }), + workdir: Schema.optionalKey(Schema.String).annotate({ + description: `Working directory for the command, absolute or relative to ${WORKSPACE_ROOT}. Use this instead of cd.`, + }), + description: Schema.optionalKey(Schema.String).annotate({ + description: 'Short (5-10 word) description of what this command does', + }), +}) + +const DESCRIPTION = + `Run a bash command in the session's workspace, where the repo(s) are cloned under ${WORKSPACE_ROOT}. ` + + 'Returns stdout then stderr, keeping the last ' + + `${defaultMaxLines} lines or ${formatSize(defaultMaxBytes)}. Commands start in ${WORKSPACE_ROOT} unless ` + + 'you pass workdir.\n\n' + + 'This is a lightweight shell (just-bash), not a full Linux machine. It has the common text commands - ' + + 'ls, cat, head, tail, grep, find, sed, awk, sort, uniq, wc, diff, cut, tr, xargs, jq and more - pipes, ' + + 'redirects, and git (clone, status, diff, log, add, commit, branch, checkout). It has no package managers, ' + + 'no node or python, no compilers, and no network access. Use it to explore and search the repos: list ' + + 'directories, grep for code, and inspect git history.' + +/** The command's output as the model sees it: stdout, then stderr, trimmed from the front. */ +const formatOutput = ({ stdout, stderr }: CommandOutput) => { + const text = stdout.length > 0 && stderr.length > 0 ? `${stdout.replace(/\n?$/, '\n')}${stderr}` : stdout + stderr + const truncation = truncateTail(text) + if (!truncation.truncated) return text + return `[Showing the last ${truncation.outputLines} of ${truncation.totalLines} lines]\n${truncation.content}` +} + +/** Build the bash tool over one session's shell. */ +export const bashTool = (run: (input: CommandInput) => Effect.Effect): FoldTool => + defineTool({ + name: 'bash', + description: DESCRIPTION, + parameters: BashParameters, + success: ToolResultText, + failure: ToolResultFailure, + handler: (params) => + Effect.gen(function* () { + const timeoutMs = params.timeout_ms ?? DEFAULT_TIMEOUT_MS + if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > MAX_TIMEOUT_MS) { + return yield* Effect.fail( + `Invalid timeout_ms: must be between 1 and ${MAX_TIMEOUT_MS} milliseconds`, + ) + } + const cwd = + params.workdir === undefined + ? WORKSPACE_ROOT + : params.workdir.startsWith('/') + ? params.workdir + : `${WORKSPACE_ROOT}/${params.workdir}` + + const output = yield* run({ command: params.command, cwd, timeoutMs }).pipe( + Effect.mapError((error) => `The shell could not run the command: ${error.message}`), + ) + const text = formatOutput(output).replace(/\n+$/, '') + + if (output.status === 'cancelled') { + return yield* Effect.fail(`${text}\n\nCommand timed out after ${timeoutMs} milliseconds`) + } + if (output.exitCode !== 0) { + return yield* Effect.fail(`${text}\n\nCommand exited with code ${output.exitCode}`) + } + return ToolResultText.make({ text: text.length === 0 ? '(no output)' : text }) + }).pipe(Effect.mapError((text) => ToolResultFailure.make({ text }))), + }) diff --git a/examples/alchemy-cloudflare/src/ChatSession.ts b/examples/alchemy-cloudflare/src/ChatSession.ts index 83f0589..e84edaf 100644 --- a/examples/alchemy-cloudflare/src/ChatSession.ts +++ b/examples/alchemy-cloudflare/src/ChatSession.ts @@ -4,14 +4,17 @@ * into the session's {@link Workspace} and starts the session under that id; a written log resumes on * activation. Callers never create a session explicitly. * - * The agent reads and changes the repos' files through fold's file tools, and loads skills from each repo's - * `.claude/skills` and `.agents/skills`, all on the workspace. + * The agent reads and changes the repos' files through fold's file tools, runs commands in the workspace's + * shell through {@link bashTool}, and loads skills from each repo's `.claude/skills` and `.agents/skills`. * * Every turn runs under a {@link Keepalive} lease so the object is not evicted mid-run. A turn can still * be cut off - a crash, a deploy - leaving the root's user message with no finished run after it. When a * written log activates in that state, the object nudges the root to continue. * Fold fills any tool call the cut left without a result, and the model can resume a cut-off subagent * by id itself. A turn that keeps getting cut off stops being nudged after {@link MAX_RESTART_NUDGES}. + * + * A session idle for 14 days deletes its workspace and itself; see + * {@link SessionExpiry}. Its id then starts a new session. */ import { skillsFromDisk } from '@humanlayer/fold-agent/skills' import { fileTools } from '@humanlayer/fold-agent/tools/files' @@ -44,10 +47,12 @@ import { SynchronizedRef, } from 'effect' +import { bashTool } from './BashTool' import type { Message, WhenRunning } from './ChatSessions' import { WORKSPACE_ROOT } from './computer/Contract' import { DurableObjectEventLog } from './DurableObjectEventLog' import { Keepalive } from './Keepalive' +import { SessionExpiry } from './SessionExpiry' import { type Repo, Workspace } from './Workspace' const MODEL = 'gpt-5.6-terra' @@ -61,7 +66,8 @@ const systemPrompt = (repoNames: ReadonlyArray) => repoNames.length === 0 ? 'No repos are cloned for this session.' : `This session's repos are cloned under ${WORKSPACE_ROOT}:\n${repoNames.map((name) => `- ${WORKSPACE_ROOT}/${name}`).join('\n')}`, - 'You can read and change their files. You cannot run commands yet.', + 'Use bash to list, search and inspect them, and the file tools to read and change files. The shell is ' + + 'lightweight: it cannot install packages, build, or run tests.', ].join('\n\n') /** Skills from each repo's `.claude/skills` and `.agents/skills`, scanned once when the session opens. */ @@ -81,6 +87,9 @@ const RESTART_NUDGE = 'A restart cut you off before you finished. Continue where you left off.' const MAX_RESTART_NUDGES = 3 +/** The workspace's backup deadline trails the session's by this much, so the session normally deletes both. */ +const WORKSPACE_GRACE_MILLIS = 24 * 60 * 60 * 1000 + /** The root's user messages since its last finished run: any means a restart cut that run off. */ const openRootMessages = (entries: ReadonlyArray, rootAgentId: AgentId) => { const lastFinishedSeq = @@ -139,6 +148,7 @@ export default class ChatSession extends Cloudflare.DurableObject() const apiKey = yield* Config.redacted('OPENAI_API_KEY').pipe(Effect.orDie) const eventLogs = yield* DurableObjectEventLog const keepalive = yield* Keepalive + const expiry = yield* SessionExpiry const workspace = yield* Workspace return Effect.gen(function* () { @@ -154,7 +164,11 @@ export default class ChatSession extends Cloudflare.DurableObject() name: 'alchemy-cloudflare-chat', systemPrompt: systemPrompt(repoNames.toSorted()), model, - tools: [...fileTools({ cwd: WORKSPACE_ROOT }), repoSkills(repoNames)], + tools: [ + ...fileTools({ cwd: WORKSPACE_ROOT }), + bashTool((input) => workspace.exec(sessionId, input)), + repoSkills(repoNames), + ], }) // Never closed: the session lives as long as the object stays in memory, past every call's own @@ -177,7 +191,13 @@ export default class ChatSession extends Cloudflare.DurableObject() // A written log's repos are the workspace's top-level directories. const resume = Effect.gen(function* () { - const repoNames = yield* fileSystem.readDirectory(WORKSPACE_ROOT).pipe(Effect.orDie) + const entries = yield* fileSystem.readDirectory(WORKSPACE_ROOT).pipe(Effect.orDie) + const repoNames = yield* Effect.filter(entries, (name) => + fileSystem.stat(`${WORKSPACE_ROOT}/${name}`).pipe( + Effect.map((info) => info.type === 'Directory'), + Effect.orDie, + ), + ) return yield* resumeSession({ agent: agentFor(repoNames), log }) }) @@ -201,7 +221,10 @@ export default class ChatSession extends Cloudflare.DurableObject() // A written log may hold a turn a restart cut off: nudge the root to continue it, in the background // under a lease so the heartbeat keeps the object alive until it lands - even when that heartbeat's // alarm is the only thing that woke the object. - if (!isEmpty) { + // An expired session skips this and waits for its alarm: resuming would reach into the workspace + // while it is being deleted, and could bring it back empty after. + if (!isEmpty && !(yield* expiry.expired)) { + yield* expiry.ensureScheduled yield* Effect.forkIn( Effect.gen(function* () { const session = yield* open([]) @@ -216,11 +239,19 @@ export default class ChatSession extends Cloudflare.DurableObject() return { send: ({ text, whenRunning, repos }: Message) => keepalive.whileRunning( - Effect.flatMap(open(repos ?? []), (session) => deliver(session, text, whenRunning)), + Effect.gen(function* () { + const deadline = yield* expiry.touch + yield* workspace.expireAt(sessionId, deadline + WORKSPACE_GRACE_MILLIS) + return yield* deliver(yield* open(repos ?? []), text, whenRunning) + }), ), entries: () => Stream.runCollect(eventLog.entries()).pipe(Effect.orDie), - alarm: () => keepalive.alarm, + // The heartbeat while a turn runs; after that, the idle deadline. + alarm: () => + Effect.flatMap(keepalive.alarm, (running) => + running ? Effect.void : expiry.alarm(workspace.destroy(sessionId)), + ), } }).pipe(Effect.orDie) - }).pipe(Effect.provide([DurableObjectEventLog.layer, Keepalive.layer, Workspace.layer])), + }).pipe(Effect.provide([DurableObjectEventLog.layer, Keepalive.layer, SessionExpiry.layer, Workspace.layer])), ) {} diff --git a/examples/alchemy-cloudflare/src/ChatSessions.ts b/examples/alchemy-cloudflare/src/ChatSessions.ts index a7330db..02dada6 100644 --- a/examples/alchemy-cloudflare/src/ChatSessions.ts +++ b/examples/alchemy-cloudflare/src/ChatSessions.ts @@ -60,7 +60,7 @@ export class ChatSessions extends Context.Service< .pipe( Effect.catch((error) => Schema.decodeUnknownEffect(RepoCloneError)(error).pipe( - Effect.orDie, + Effect.catch(() => Effect.die(error)), Effect.flatMap(Effect.fail), ), ), diff --git a/examples/alchemy-cloudflare/src/Keepalive.ts b/examples/alchemy-cloudflare/src/Keepalive.ts index 3014179..9ff09a6 100644 --- a/examples/alchemy-cloudflare/src/Keepalive.ts +++ b/examples/alchemy-cloudflare/src/Keepalive.ts @@ -14,8 +14,11 @@ export class Keepalive extends Context.Service< { /** Run `effect` under a lease: the first lease arms the heartbeat. */ readonly whileRunning: (effect: Effect.Effect) => Effect.Effect - /** The object's alarm handler: re-arm while any lease is held; once none are, let the alarm lapse. */ - readonly alarm: Effect.Effect + /** + * The object's alarm handler: re-arm while any lease is held. Returns whether it did; once no lease is + * held, the alarm is the caller's. + */ + readonly alarm: Effect.Effect } >()('alchemy-cloudflare/Keepalive') { static readonly layer = Layer.effect( @@ -37,7 +40,9 @@ export class Keepalive extends Context.Service< () => effect, () => Ref.update(leases, (held) => held - 1), ), - alarm: Effect.flatMap(Ref.get(leases), (held) => (held > 0 ? armHeartbeat : Effect.void)), + alarm: Effect.flatMap(Ref.get(leases), (held) => + held > 0 ? Effect.as(armHeartbeat, true) : Effect.succeed(false), + ), }) }), ) diff --git a/examples/alchemy-cloudflare/src/SessionExpiry.ts b/examples/alchemy-cloudflare/src/SessionExpiry.ts new file mode 100644 index 0000000..4824b33 --- /dev/null +++ b/examples/alchemy-cloudflare/src/SessionExpiry.ts @@ -0,0 +1,116 @@ +/** + * Deletes an idle session. Every message pushes the session's deadline to {@link IDLE_TIME} from now. Once no turn is running, the object's alarm is set for the deadline; when it fires past + * the deadline, the session's workspace is deleted, then the object's own storage, and the object restarts + * empty, so its id is new again. + * + * The alarm is shared with {@link Keepalive}: while a turn runs, Keepalive's heartbeat holds it, and the + * heartbeat that finds no turn running hands it back here. + */ +import * as Cloudflare from 'alchemy/Cloudflare' +import { Clock, Context, Duration, Effect, Layer } from 'effect' + +/** How long a session lasts after its last message. */ +export const IDLE_TIME = Duration.days(14) + +const DEADLINE_KEY = 'session_deadline' + +/** How long after deleting the session the object restarts. */ +const RESTART_DELAY_MILLIS = 1_000 + +/** What SessionExpiry needs from the Durable Object: its key-value storage, alarm, and a restart. */ +export type ExpiryStorage = { + readonly get: (key: string) => Effect.Effect + readonly put: (key: string, value: number) => Effect.Effect + readonly getAlarm: Effect.Effect + readonly setAlarm: (at: number) => Effect.Effect + /** Delete the alarm and every stored value. */ + readonly deleteAll: Effect.Effect + /** Throw away the object's in-memory state, so the next call starts from its (empty) storage. */ + readonly restart: Effect.Effect +} + +export class SessionExpiry extends Context.Service< + SessionExpiry, + { + /** Push the deadline back to the idle time from now. Returns the new deadline, in epoch milliseconds. */ + readonly touch: Effect.Effect + /** Whether the deadline has passed: the session is waiting for its alarm to delete it. */ + readonly expired: Effect.Effect + /** Give a written session an alarm if it has none, such as one written before expiry existed. */ + readonly ensureScheduled: Effect.Effect + /** + * The object's alarm when no turn holds it: past the deadline, run `deleteWorkspace` and delete the + * session; before it, set the alarm for the deadline. A session with no deadline was already deleted + * (or never written), so there is nothing to do. + */ + readonly alarm: (deleteWorkspace: Effect.Effect) => Effect.Effect + } +>()('alchemy-cloudflare/SessionExpiry') { + static readonly make = (storage: ExpiryStorage, idleTime: Duration.Duration) => { + const touch = Effect.gen(function* () { + const deadline = (yield* Clock.currentTimeMillis) + Duration.toMillis(idleTime) + yield* storage.put(DEADLINE_KEY, deadline) + return deadline + }) + + return SessionExpiry.of({ + touch, + expired: Effect.gen(function* () { + const deadline = yield* storage.get(DEADLINE_KEY) + return deadline !== undefined && (yield* Clock.currentTimeMillis) >= deadline + }), + ensureScheduled: Effect.gen(function* () { + if ((yield* storage.getAlarm) !== null) return + const deadline = (yield* storage.get(DEADLINE_KEY)) ?? (yield* touch) + yield* storage.setAlarm(deadline) + }), + alarm: (deleteWorkspace) => + Effect.gen(function* () { + const deadline = yield* storage.get(DEADLINE_KEY) + if (deadline === undefined) return + if ((yield* Clock.currentTimeMillis) < deadline) return yield* storage.setAlarm(deadline) + + yield* deleteWorkspace + yield* storage.deleteAll + yield* Effect.logInfo('session.expired') + yield* storage.restart + }), + }) + } + + /** Over the Durable Object's storage, with {@link IDLE_TIME}. */ + static readonly layer = Layer.effect( + SessionExpiry, + Effect.gen(function* () { + const { raw } = yield* Cloudflare.DurableObjectState + + return SessionExpiry.make( + { + get: (key) => + Effect.promise(() => raw.storage.get(key)).pipe( + Effect.map((value) => (typeof value === 'number' ? value : undefined)), + ), + put: (key, value) => Effect.promise(() => raw.storage.put(key, value)), + getAlarm: Effect.promise(() => raw.storage.getAlarm()), + setAlarm: (at) => Effect.promise(() => raw.storage.setAlarm(at)), + deleteAll: Effect.promise(async () => { + await raw.storage.deleteAlarm() + await raw.storage.deleteAll() + }), + // Once the alarm has finished: Cloudflare holds its completion until the delete is saved, + // and an abort before then fails the alarm, which Cloudflare retries. + restart: Effect.sync(() => { + setTimeout(() => { + try { + raw.abort('session expired') + } catch { + // abort throws to unwind; the object resets either way. + } + }, RESTART_DELAY_MILLIS) + }), + }, + IDLE_TIME, + ) + }), + ) +} diff --git a/examples/alchemy-cloudflare/src/Workspace.ts b/examples/alchemy-cloudflare/src/Workspace.ts index e99c615..7994079 100644 --- a/examples/alchemy-cloudflare/src/Workspace.ts +++ b/examples/alchemy-cloudflare/src/Workspace.ts @@ -5,9 +5,16 @@ import type { SessionId } from '@humanlayer/fold-core' import type { RpcCallError } from 'alchemy' import * as Cloudflare from 'alchemy/Cloudflare' -import { Context, Effect, type FileSystem, Layer, Schema } from 'effect' +import { Context, Data, Effect, type FileSystem, Layer, Schema } from 'effect' -import { COMPUTER_WORKER_NAME, type ClonedRepo, type RepoSpec } from './computer/Contract' +import { + COMPUTER_WORKER_NAME, + type ClonedRepo, + type CommandInput, + type CommandOutput, + type ComputerResult, + type RepoSpec, +} from './computer/Contract' import { type ComputerFiles, workspaceFileSystem } from './WorkspaceFileSystem' const REPO_NAME = /^(?!\.{1,2}$)[\w.-]+$/ @@ -42,11 +49,17 @@ export class RepoCloneError extends Schema.TaggedError()('RepoCl message: Schema.String, }) {} +/** The shell could not run a command at all. A command that runs and fails is a {@link CommandOutput}. */ +export class ShellError extends Data.TaggedError('ShellError')<{ readonly message: string }> {} + /** The Computer Durable Object, as ChatSession sees it. Its class lives in the Computer Worker. */ class Computer extends Cloudflare.DurableObject< Computer, ComputerFiles & { readonly prepare: (repos: ReadonlyArray) => Effect.Effect, RpcCallError> + readonly exec: (input: CommandInput) => Effect.Effect, RpcCallError> + readonly expireAt: (deleteAt: number) => Effect.Effect + readonly destroy: () => Effect.Effect } >()('Computer') {} @@ -60,6 +73,15 @@ export class Workspace extends Context.Service< ) => Effect.Effect, RepoCloneError> /** The session's workspace as a `FileSystem`, for fold's file tools and skill loader. */ readonly fileSystem: (sessionId: SessionId) => FileSystem.FileSystem + /** Run one command in the session's shell. */ + readonly exec: (sessionId: SessionId, input: CommandInput) => Effect.Effect + /** + * Have the workspace delete itself at `deleteAt` (epoch milliseconds) unless moved again: the backup + * for a session that never deletes it. A failure is logged, not raised. + */ + readonly expireAt: (sessionId: SessionId, deleteAt: number) => Effect.Effect + /** Delete the workspace now. A failure is logged, not raised: the workspace's own deadline still holds. */ + readonly destroy: (sessionId: SessionId) => Effect.Effect } >()('alchemy-cloudflare/Workspace') { /** Binds the Computer Durable Object; build it in a Worker's or Durable Object's construction. */ @@ -81,6 +103,48 @@ export class Workspace extends Context.Service< ), ), fileSystem: (sessionId) => workspaceFileSystem(computers.getByName(sessionId)), + expireAt: (sessionId, deleteAt) => + computers + .getByName(sessionId) + .expireAt(deleteAt) + .pipe( + Effect.catch((error) => + Effect.logWarning( + 'workspace.expireAt failed', + JSON.stringify({ sessionId, error: error.message }), + ), + ), + ), + destroy: (sessionId) => + computers + .getByName(sessionId) + .destroy() + .pipe( + Effect.tap(() => Effect.logInfo('workspace.destroyed', JSON.stringify({ sessionId }))), + Effect.catch((error) => + Effect.logWarning( + 'workspace.destroy failed', + JSON.stringify({ sessionId, error: error.message }), + ), + ), + ), + exec: Effect.fn('alchemy_cloudflare.workspace.exec')(function* (sessionId, input) { + const result = yield* computers + .getByName(sessionId) + .exec(input) + .pipe(Effect.mapError((error) => new ShellError({ message: error.message }))) + yield* Effect.logInfo( + 'workspace.exec', + JSON.stringify({ + command: input.command, + cwd: input.cwd, + outcome: result.ok + ? { status: result.value.status, exitCode: result.value.exitCode } + : result, + }), + ) + return result.ok ? result.value : yield* new ShellError({ message: result.message }) + }), }) }), ) diff --git a/examples/alchemy-cloudflare/src/WorkspaceFileSystem.ts b/examples/alchemy-cloudflare/src/WorkspaceFileSystem.ts index 547526c..718c240 100644 --- a/examples/alchemy-cloudflare/src/WorkspaceFileSystem.ts +++ b/examples/alchemy-cloudflare/src/WorkspaceFileSystem.ts @@ -9,16 +9,16 @@ import type { RpcCallError } from 'alchemy' import { Effect, FileSystem, Option, PlatformError } from 'effect' -import type { FileInfo, FileResult } from './computer/Contract' +import type { FileInfo, ComputerResult } from './computer/Contract' /** The Computer's file methods, as its RPC stub returns them. */ export type ComputerFiles = { - readonly readFile: (path: string) => Effect.Effect, RpcCallError> - readonly writeFile: (path: string, content: string) => Effect.Effect, RpcCallError> - readonly mkdir: (path: string, recursive: boolean) => Effect.Effect, RpcCallError> - readonly rm: (path: string, recursive: boolean, force: boolean) => Effect.Effect, RpcCallError> - readonly stat: (path: string) => Effect.Effect, RpcCallError> - readonly readdir: (path: string) => Effect.Effect>, RpcCallError> + readonly readFile: (path: string) => Effect.Effect, RpcCallError> + readonly writeFile: (path: string, content: string) => Effect.Effect, RpcCallError> + readonly mkdir: (path: string, recursive: boolean) => Effect.Effect, RpcCallError> + readonly rm: (path: string, recursive: boolean, force: boolean) => Effect.Effect, RpcCallError> + readonly stat: (path: string) => Effect.Effect, RpcCallError> + readonly readdir: (path: string) => Effect.Effect>, RpcCallError> } const SYSTEM_ERROR_TAGS = new Map([ @@ -38,7 +38,7 @@ const SYSTEM_ERROR_TAGS = new Map([ const run = ( method: string, path: string, - call: Effect.Effect, RpcCallError>, + call: Effect.Effect, RpcCallError>, ): Effect.Effect => call.pipe( Effect.mapError((cause) => diff --git a/examples/alchemy-cloudflare/src/computer/Computer.ts b/examples/alchemy-cloudflare/src/computer/Computer.ts index ee8deb1..d341295 100644 --- a/examples/alchemy-cloudflare/src/computer/Computer.ts +++ b/examples/alchemy-cloudflare/src/computer/Computer.ts @@ -1,14 +1,39 @@ /// /** * The Computer Durable Object: one `@cloudflare/computer` Workspace per fold session, named by its - * SessionId. The Workspace is a virtual filesystem in the object's SQLite, with git. A plain Durable Object + * SessionId. The Workspace is a virtual filesystem in the object's SQLite, with git, and a shell: just-bash + * in a Worker the Worker Loader starts, which reads and writes this same filesystem. A plain Durable Object * rather than an Effect one, because `withWorkspace` must wrap the `cloudflare:workers` class. */ import { type DurableObjectStorageLike, getWorkspace, withWorkspace } from '@cloudflare/computer' +import { WorkerShellBackend } from '@cloudflare/computer/backends/worker-shell' import { createGitClient, type GitClient, type GitCloneOptions } from '@cloudflare/computer/git' +import jq from '@cloudflare/computer/shell/jq' import { DurableObject } from 'cloudflare:workers' -import { type ClonedRepo, type FileInfo, type FileResult, type RepoSpec, WORKSPACE_ROOT } from './Contract' +import { + type ClonedRepo, + type CommandInput, + type CommandOutput, + COMPUTER_BINDING, + type ComputerResult, + type FileInfo, + type RepoSpec, + WORKSPACE_ROOT, +} from './Contract' + +// The shell reaches back into this object's workspace through this entrypoint, via `ctx.exports`. +export { WorkspaceServiceProxy } from '@cloudflare/computer' + +type Env = { + /** Starts the shell's Worker. */ + readonly LOADER: WorkerLoader +} + +const SHELL_BACKEND = 'shell' + +/** How long `destroy` waits after answering before the object restarts. */ +const RESTART_DELAY_MILLIS = 1_000 /** * The object's storage as `@cloudflare/computer` types it. Its `exec` promises whatever row type the caller @@ -27,7 +52,7 @@ const workspaceStorage = (storage: DurableObjectStorage): DurableObjectStorageLi } /** Run one file operation, returning a thrown workspace error as its code and message. */ -const attempt = async (operation: () => Promise): Promise> => { +const attempt = async (operation: () => Promise): Promise> => { try { return { ok: true, value: await operation() } } catch (cause) { @@ -37,14 +62,23 @@ const attempt = async (operation: () => Promise): Promise> = } } -class ComputerBase extends DurableObject { - /** For withWorkspace's options, which see the instance but not `ctx`: DurableObject keeps it protected. */ +class ComputerBase extends DurableObject { + // For withWorkspace's options, which see the instance but not `ctx` or `env`: DurableObject keeps them + // protected. readonly storage = workspaceStorage(this.ctx.storage) + readonly shell = new WorkerShellBackend({ + id: SHELL_BACKEND, + loader: this.env.LOADER, + workspace: { binding: COMPUTER_BINDING, id: this.ctx.id.toString() }, + ctx: this.ctx, + commands: [jq], + }) } export class Computer extends withWorkspace(ComputerBase, (self) => ({ storage: self.storage, git: createGitClient(), + backends: [self.shell], })) { /** * Clone each repo into `/workspace/`, one at a time, replacing whatever a cut-off earlier attempt @@ -74,12 +108,12 @@ export class Computer extends withWorkspace(ComputerBase, (self) => ({ // The file methods fold's read, write, edit and apply_patch tools and its skill loader need. Paths are // absolute. - async readFile(path: string): Promise> { + async readFile(path: string): Promise> { using workspace = await getWorkspace(this) return await attempt(async () => new Response(await workspace.fs.readFile(path)).bytes()) } - async writeFile(path: string, content: string): Promise> { + async writeFile(path: string, content: string): Promise> { using workspace = await getWorkspace(this) return await attempt(async () => { await workspace.fs.writeFile(path, content) @@ -87,7 +121,7 @@ export class Computer extends withWorkspace(ComputerBase, (self) => ({ }) } - async mkdir(path: string, recursive: boolean): Promise> { + async mkdir(path: string, recursive: boolean): Promise> { using workspace = await getWorkspace(this) return await attempt(async () => { await workspace.fs.mkdir(path, { recursive }) @@ -95,7 +129,7 @@ export class Computer extends withWorkspace(ComputerBase, (self) => ({ }) } - async rm(path: string, recursive: boolean, force: boolean): Promise> { + async rm(path: string, recursive: boolean, force: boolean): Promise> { using workspace = await getWorkspace(this) return await attempt(async () => { await workspace.fs.rm(path, { recursive, force }) @@ -104,7 +138,7 @@ export class Computer extends withWorkspace(ComputerBase, (self) => ({ } /** Follows symlinks. */ - async stat(path: string): Promise> { + async stat(path: string): Promise> { using workspace = await getWorkspace(this) return await attempt(async () => { const stat = await workspace.fs.stat(path) @@ -113,8 +147,51 @@ export class Computer extends withWorkspace(ComputerBase, (self) => ({ }) } + /** + * Delete this workspace at `deleteAt` (epoch milliseconds) unless moved again before then. Its session + * normally deletes it first; this is the backup for one that never does, such as a failed first clone. + */ + async expireAt(deleteAt: number): Promise { + await this.ctx.storage.setAlarm(deleteAt) + } + + /** Delete the workspace: files, git data, and the backup alarm. The object then restarts empty. */ + async destroy(): Promise { + await this.ctx.storage.deleteAlarm() + await this.ctx.storage.deleteAll() + // The in-memory workspace must go: its tables are gone. Wait until this call has answered: Cloudflare + // holds the answer until the delete is saved, and an abort before then fails the call. + setTimeout(() => { + try { + this.ctx.abort('workspace deleted') + } catch { + // abort throws to unwind; the object resets either way. + } + }, RESTART_DELAY_MILLIS) + } + + /** Only `expireAt` sets the alarm, and each call replaces it, so it fires at the latest deadline. */ + override async alarm(): Promise { + await this.destroy() + } + + /** Run one command in the shell. A command that fails still succeeds here, with its exit code. */ + async exec(input: CommandInput): Promise> { + using workspace = await getWorkspace(this) + return await attempt(async () => { + using handle = await workspace.runtime.exec(input.command, { + backend: SHELL_BACKEND, + cwd: input.cwd, + encoding: 'utf8', + timeoutMs: input.timeoutMs, + }) + const { status, exitCode, stdout, stderr } = await handle.result() + return { status, exitCode, stdout, stderr } + }) + } + /** The directory's entry names. */ - async readdir(path: string): Promise>> { + async readdir(path: string): Promise>> { using workspace = await getWorkspace(this) return await attempt(async () => (await workspace.fs.readdir(path)).map((entry) => entry.name)) } diff --git a/examples/alchemy-cloudflare/src/computer/ComputerWorker.ts b/examples/alchemy-cloudflare/src/computer/ComputerWorker.ts index a9a516e..91e01a1 100644 --- a/examples/alchemy-cloudflare/src/computer/ComputerWorker.ts +++ b/examples/alchemy-cloudflare/src/computer/ComputerWorker.ts @@ -11,5 +11,9 @@ export const ComputerWorker = Cloudflare.Worker('ComputerWorker', { name: COMPUTER_WORKER_NAME, main: './src/computer/Computer.ts', compatibility: { flags: ['nodejs_compat'], date: '2026-08-31' }, - env: { Computer: Cloudflare.DurableObject('Computer') }, + env: { + // Named COMPUTER_BINDING: the shell calls back into the Computer through this binding. + Computer: Cloudflare.DurableObject('Computer'), + LOADER: Cloudflare.WorkerLoader(), + }, }) diff --git a/examples/alchemy-cloudflare/src/computer/Contract.ts b/examples/alchemy-cloudflare/src/computer/Contract.ts index a996350..fc89a30 100644 --- a/examples/alchemy-cloudflare/src/computer/Contract.ts +++ b/examples/alchemy-cloudflare/src/computer/Contract.ts @@ -12,6 +12,9 @@ export const COMPUTER_WORKER_NAME = 'fold-alchemy-cloudflare-computer' /** Every repo clones into `${WORKSPACE_ROOT}/`. */ export const WORKSPACE_ROOT = '/workspace' +/** The Computer's binding name on its own Worker, which the shell uses to call back into it. */ +export const COMPUTER_BINDING = 'Computer' + export type RepoSpec = { readonly name: string readonly url: string @@ -26,10 +29,10 @@ export type ClonedRepo = RepoSpec & { } /** - * A file method's outcome. Failures come back as values, not thrown: an error thrown across RPC keeps its - * message but loses its `code` (`ENOENT`, `EISDIR`, ...), which the file tools need. + * A file or command method's outcome. Failures come back as values, not thrown: an error thrown across RPC + * keeps its message but loses its `code` (`ENOENT`, `EISDIR`, ...), which the file tools need. */ -export type FileResult = +export type ComputerResult = | { readonly ok: true; readonly value: A } | { readonly ok: false; readonly code: string; readonly message: string } @@ -41,3 +44,18 @@ export type FileInfo = { readonly mode: number readonly inode: number } + +/** One shell command, run in `cwd` and stopped after `timeoutMs`. */ +export type CommandInput = { + readonly command: string + readonly cwd: string + readonly timeoutMs: number +} + +export type CommandOutput = { + /** `cancelled` when the command hit its timeout. */ + readonly status: 'completed' | 'failed' | 'cancelled' + readonly exitCode: number + readonly stdout: string + readonly stderr: string +} diff --git a/examples/alchemy-cloudflare/test/BashTool.test.ts b/examples/alchemy-cloudflare/test/BashTool.test.ts new file mode 100644 index 0000000..4d2c9f9 --- /dev/null +++ b/examples/alchemy-cloudflare/test/BashTool.test.ts @@ -0,0 +1,113 @@ +/** + * The bash tool against a fake shell that records each command and answers with a scripted output. + */ +import { it } from '@effect/vitest' +import { Effect, FileSystem, Ref } from 'effect' +import { expect } from 'vitest' + +import { bashTool } from '../src/BashTool' +import type { CommandInput, CommandOutput } from '../src/computer/Contract' +import { ShellError } from '../src/Workspace' +import { callTool } from './ToolCalls' + +const output = (fields: Partial): CommandOutput => ({ + status: 'completed', + exitCode: 0, + stdout: '', + stderr: '', + ...fields, +}) + +/** A bash tool whose shell answers every command with `answer`, recording the inputs. */ +const fakeBash = (answer: Effect.Effect) => + Effect.gen(function* () { + const inputs = yield* Ref.make>([]) + const tool = bashTool((input) => + Effect.andThen( + Ref.update(inputs, (all) => [...all, input]), + answer, + ), + ) + // fold's tool setup may ask for a FileSystem; bash never does. + const bash = (params: unknown) => + callTool([tool], 'bash', params).pipe(Effect.provide(FileSystem.layerNoop({}))) + return { bash, inputs: Ref.get(inputs) } + }) + +it.effect('returns stdout then stderr, running in /workspace by default', () => + Effect.gen(function* () { + const { bash, inputs } = yield* fakeBash(Effect.succeed(output({ stdout: 'README\n', stderr: 'warning' }))) + + expect(yield* bash({ command: 'ls' })).toEqual({ _tag: 'text', text: 'README\nwarning' }) + expect(yield* inputs).toEqual([{ command: 'ls', cwd: '/workspace', timeoutMs: 120_000 }]) + }), +) + +it.effect('resolves workdir against /workspace and passes the timeout', () => + Effect.gen(function* () { + const { bash, inputs } = yield* fakeBash(Effect.succeed(output({}))) + + expect(yield* bash({ command: 'true', workdir: 'app/src', timeout_ms: 5_000 })).toEqual({ + _tag: 'text', + text: '(no output)', + }) + yield* bash({ command: 'true', workdir: '/tmp' }) + expect((yield* inputs).map(({ cwd, timeoutMs }) => ({ cwd, timeoutMs }))).toEqual([ + { cwd: '/workspace/app/src', timeoutMs: 5_000 }, + { cwd: '/tmp', timeoutMs: 120_000 }, + ]) + }), +) + +it.effect('fails with the output and exit code when the command fails', () => + Effect.gen(function* () { + const { bash } = yield* fakeBash(Effect.succeed(output({ exitCode: 2, stderr: 'grep: nope: No such file\n' }))) + + expect(yield* Effect.flip(bash({ command: 'grep x nope' }))).toEqual({ + _tag: 'failure', + text: 'grep: nope: No such file\n\nCommand exited with code 2', + }) + }), +) + +it.effect('fails when the command times out', () => + Effect.gen(function* () { + const { bash } = yield* fakeBash(Effect.succeed(output({ status: 'cancelled', exitCode: 130 }))) + + expect(yield* Effect.flip(bash({ command: 'sleep 1000', timeout_ms: 10 }))).toMatchObject({ + text: expect.stringContaining('Command timed out after 10 milliseconds'), + }) + }), +) + +it.effect('rejects a bad timeout without running anything', () => + Effect.gen(function* () { + const { bash, inputs } = yield* fakeBash(Effect.succeed(output({}))) + + expect(yield* Effect.flip(bash({ command: 'ls', timeout_ms: 0 }))).toMatchObject({ _tag: 'failure' }) + expect(yield* inputs).toEqual([]) + }), +) + +it.effect('keeps the end of long output', () => + Effect.gen(function* () { + const lines = Array.from({ length: 2_500 }, (_, index) => `line ${index + 1}`).join('\n') + const { bash } = yield* fakeBash(Effect.succeed(output({ stdout: lines }))) + + const result = yield* bash({ command: 'seq 2500' }) + expect(result).toMatchObject({ text: expect.stringMatching(/^\[Showing the last 2000 of 2500 lines\]\n/) }) + expect(result).toMatchObject({ text: expect.stringMatching(/line 2500$/) }) + expect(result).not.toMatchObject({ text: expect.stringContaining('line 500\n') }) + }), +) + +it.effect('fails the tool call when the shell cannot run the command', () => + Effect.gen(function* () { + const { bash } = yield* fakeBash(Effect.fail(new ShellError({ message: 'shell unavailable' }))) + + expect(yield* Effect.flip(bash({ command: 'ls' }))).toEqual({ + _tag: 'failure', + text: 'The shell could not run the command: shell unavailable', + }) + }), +) diff --git a/examples/alchemy-cloudflare/test/SessionExpiry.test.ts b/examples/alchemy-cloudflare/test/SessionExpiry.test.ts new file mode 100644 index 0000000..39912ba --- /dev/null +++ b/examples/alchemy-cloudflare/test/SessionExpiry.test.ts @@ -0,0 +1,119 @@ +/** + * SessionExpiry against an in-memory Durable Object store, with Effect's test clock standing in for time. + */ +import { it } from '@effect/vitest' +import { Duration, Effect, Ref } from 'effect' +import { TestClock } from 'effect/testing' +import { expect } from 'vitest' + +import { type ExpiryStorage, SessionExpiry } from '../src/SessionExpiry' + +const DAY = Duration.toMillis(Duration.days(1)) + +/** A fake object: its stored values, its alarm, and whether it was deleted or restarted. */ +const fakeObject = Effect.gen(function* () { + const values = yield* Ref.make>(new Map()) + const alarm = yield* Ref.make(null) + const events = yield* Ref.make>([]) + const record = (event: string) => Ref.update(events, (all) => [...all, event]) + + const storage: ExpiryStorage = { + get: (key) => Effect.map(Ref.get(values), (all) => all.get(key)), + put: (key, value) => Ref.update(values, (all) => new Map(all).set(key, value)), + getAlarm: Ref.get(alarm), + setAlarm: (at) => Ref.set(alarm, at), + deleteAll: Effect.andThen(Ref.set(values, new Map()), Ref.set(alarm, null)).pipe( + Effect.andThen(record('deleteAll')), + ), + restart: record('restart'), + } + const expiry = SessionExpiry.make(storage, Duration.days(14)) + return { expiry, alarm: Ref.get(alarm), events: Ref.get(events), deleteWorkspace: record('deleteWorkspace') } +}) + +it.effect('each message pushes the deadline back', () => + Effect.gen(function* () { + const { expiry } = yield* fakeObject + + expect(yield* expiry.touch).toBe(14 * DAY) + yield* TestClock.adjust(Duration.days(3)) + expect(yield* expiry.touch).toBe(17 * DAY) + }), +) + +it.effect('before the deadline, the alarm waits for it', () => + Effect.gen(function* () { + const { expiry, alarm, events, deleteWorkspace } = yield* fakeObject + yield* expiry.touch + yield* TestClock.adjust(Duration.days(1)) + + yield* expiry.alarm(deleteWorkspace) + expect(yield* alarm).toBe(14 * DAY) + expect(yield* events).toEqual([]) + }), +) + +it.effect('past the deadline, the alarm deletes the workspace, then the session, then restarts', () => + Effect.gen(function* () { + const { expiry, alarm, events, deleteWorkspace } = yield* fakeObject + yield* expiry.touch + yield* TestClock.adjust(Duration.days(14)) + + yield* expiry.alarm(deleteWorkspace) + expect(yield* events).toEqual(['deleteWorkspace', 'deleteAll', 'restart']) + expect(yield* alarm).toBeNull() + }), +) + +it.effect('a message after an earlier alarm keeps the session alive past the first deadline', () => + Effect.gen(function* () { + const { expiry, alarm, events, deleteWorkspace } = yield* fakeObject + yield* expiry.touch + yield* TestClock.adjust(Duration.days(10)) + yield* expiry.touch + yield* TestClock.adjust(Duration.days(4)) + + yield* expiry.alarm(deleteWorkspace) + expect(yield* alarm).toBe(24 * DAY) + expect(yield* events).toEqual([]) + }), +) + +it.effect('an alarm with no deadline stored does nothing: the session is already gone', () => + Effect.gen(function* () { + const { expiry, alarm, events, deleteWorkspace } = yield* fakeObject + + yield* expiry.alarm(deleteWorkspace) + expect(yield* alarm).toBeNull() + expect(yield* events).toEqual([]) + }), +) + +it.effect('a written session without an alarm gets one; an existing alarm is left alone', () => + Effect.gen(function* () { + const { expiry, alarm } = yield* fakeObject + + yield* expiry.ensureScheduled + expect(yield* alarm).toBe(14 * DAY) + + yield* TestClock.adjust(Duration.days(1)) + yield* expiry.ensureScheduled + expect(yield* alarm).toBe(14 * DAY) + }), +) + +it.effect('the session counts as expired from its deadline until it is deleted', () => + Effect.gen(function* () { + const { expiry, deleteWorkspace } = yield* fakeObject + expect(yield* expiry.expired).toBe(false) + + yield* expiry.touch + yield* TestClock.adjust(Duration.days(13)) + expect(yield* expiry.expired).toBe(false) + yield* TestClock.adjust(Duration.days(1)) + expect(yield* expiry.expired).toBe(true) + + yield* expiry.alarm(deleteWorkspace) + expect(yield* expiry.expired).toBe(false) + }), +) diff --git a/examples/alchemy-cloudflare/test/ToolCalls.ts b/examples/alchemy-cloudflare/test/ToolCalls.ts new file mode 100644 index 0000000..73c8f11 --- /dev/null +++ b/examples/alchemy-cloudflare/test/ToolCalls.ts @@ -0,0 +1,42 @@ +/** + * Call a fold tool's handler outside a session. fold provides these services around every tool call; here + * they are stubs. Platform services (FileSystem, Path) are the caller's to provide. + */ +import { + AgentId, + CurrentAgent, + CurrentToolCall, + InterruptNote, + StopController, + Subagents, + ToolCallId, + ToolEvents, + ToolState, + type FoldTool, +} from '@humanlayer/fold-core' +import { Effect, Layer } from 'effect' + +const toolCallServices = Layer.mergeAll( + Layer.succeed(ToolState, { get: () => Effect.succeed(null), set: () => Effect.void }), + Layer.succeed(ToolEvents, { emit: () => Effect.void }), + Layer.succeed(StopController, { requestStop: () => Effect.void, isStopRequested: Effect.succeed(false) }), + Layer.succeed(CurrentAgent, { agentId: AgentId.create(), parentAgentId: null }), + Layer.succeed(CurrentToolCall, { toolCallId: ToolCallId.create() }), + Layer.succeed(InterruptNote, { set: () => Effect.void }), + Layer.succeed(Subagents, { + dispatch: () => Effect.die('no subagents'), + fork: () => Effect.die('no subagents'), + resume: () => Effect.die('no subagents'), + continueSubagent: () => Effect.die('no subagents'), + }), +) + +/** Run the tool named `name` from `tools` with `params`, as the model would call it. */ +export const callTool = (tools: ReadonlyArray, name: string, params: unknown) => { + const tool = tools.find((candidate) => candidate.name === name) + if (tool === undefined) return Effect.die(`no ${name} tool`) + return tool.init.pipe( + Effect.flatMap((contribution) => contribution.handler(params)), + Effect.provide(toolCallServices), + ) +} diff --git a/examples/alchemy-cloudflare/test/WorkspaceFileSystem.test.ts b/examples/alchemy-cloudflare/test/WorkspaceFileSystem.test.ts index 2356587..f5687ae 100644 --- a/examples/alchemy-cloudflare/test/WorkspaceFileSystem.test.ts +++ b/examples/alchemy-cloudflare/test/WorkspaceFileSystem.test.ts @@ -2,27 +2,16 @@ * The workspace FileSystem against an in-memory Computer that fails the way the real one does: with a * code such as `ENOENT` in a result value. fold's disk skill loader then runs over it unchanged. */ -import * as NodeServices from '@effect/platform-node/NodeServices' import { it } from '@effect/vitest' import { makeDiskSkillSource } from '@humanlayer/fold-agent/skills' import { fileTools } from '@humanlayer/fold-agent/tools/files' -import { - AgentId, - CurrentAgent, - CurrentToolCall, - InterruptNote, - StopController, - Subagents, - ToolCallId, - ToolEvents, - ToolState, - type FoldTool, -} from '@humanlayer/fold-core' -import { Effect, FileSystem, Layer, type PlatformError } from 'effect' +import type { FoldTool } from '@humanlayer/fold-core' +import { Effect, FileSystem, Path, type PlatformError } from 'effect' import { expect } from 'vitest' -import type { FileInfo, FileResult } from '../src/computer/Contract' +import type { FileInfo, ComputerResult } from '../src/computer/Contract' import { type ComputerFiles, workspaceFileSystem } from '../src/WorkspaceFileSystem' +import { callTool } from './ToolCalls' type Node = { readonly type: 'Directory' } | { readonly type: 'File'; readonly content: Uint8Array } @@ -30,8 +19,8 @@ const parentOf = (path: string) => path.slice(0, path.lastIndexOf('/')) || '/' const fakeComputer = (): ComputerFiles => { const nodes = new Map([['/', { type: 'Directory' }]]) - const ok = (value: A): Effect.Effect> => Effect.succeed({ ok: true, value }) - const fail = (code: string, path: string): Effect.Effect> => + const ok = (value: A): Effect.Effect> => Effect.succeed({ ok: true, value }) + const fail = (code: string, path: string): Effect.Effect> => Effect.succeed({ ok: false, code, message: `${code}: ${path}` }) const children = (path: string) => [...nodes.keys()] @@ -81,33 +70,9 @@ const fakeComputer = (): ComputerFiles => { } } -/** The services fold provides around every tool call, stubbed; the platform ones come from Node. */ -const toolCallServices = Layer.mergeAll( - NodeServices.layer, - Layer.succeed(ToolState, { get: () => Effect.succeed(null), set: () => Effect.void }), - Layer.succeed(ToolEvents, { emit: () => Effect.void }), - Layer.succeed(StopController, { requestStop: () => Effect.void, isStopRequested: Effect.succeed(false) }), - Layer.succeed(CurrentAgent, { agentId: AgentId.create(), parentAgentId: null }), - Layer.succeed(CurrentToolCall, { toolCallId: ToolCallId.create() }), - Layer.succeed(InterruptNote, { set: () => Effect.void }), - Layer.succeed(Subagents, { - dispatch: () => Effect.die('no subagents'), - fork: () => Effect.die('no subagents'), - resume: () => Effect.die('no subagents'), - continueSubagent: () => Effect.die('no subagents'), - }), -) - /** Call one of fold's tools with the workspace as its FileSystem. */ -const call = (fs: FileSystem.FileSystem, tools: ReadonlyArray, name: string, params: unknown) => { - const tool = tools.find((candidate) => candidate.name === name) - if (tool === undefined) return Effect.die(`no ${name} tool`) - return tool.init.pipe( - Effect.flatMap((contribution) => contribution.handler(params)), - Effect.provideService(FileSystem.FileSystem, fs), - Effect.provide(toolCallServices), - ) -} +const call = (fs: FileSystem.FileSystem, tools: ReadonlyArray, name: string, params: unknown) => + callTool(tools, name, params).pipe(Effect.provideService(FileSystem.FileSystem, fs), Effect.provide(Path.layer)) const reasonTag = (error: PlatformError.PlatformError) => error.reason._tag From 5ecec52a0acc8cc989968fc88afd442e3aa358b7 Mon Sep 17 00:00:00 2001 From: Kyle Mistele Date: Thu, 1 Oct 2026 15:20:26 -0700 Subject: [PATCH 6/6] feat(examples): run alchemy-cloudflare bash commands in a Linux container The bash tool gains a backend: the in-Worker shell, as before, or a Debian container attached to the Computer, where commands run as root in a bash login shell with git, node, bun, python3, ripgrep, fd and tmux. The session starts the container in the background when it starts, and deleting the workspace stops it. The container runs computerd from a fork of cloudflare/computer (K-Mistele/computer), built from a pinned commit, and the Worker uses @cloudflare/computer from a release of the same fork. The fork keeps gitignored files in the container instead of sending them to the Computer, so installs and builds don't fill its storage, and fixes the slow path lookup, disk syncs and uncached "not found" answers that made installs and test runs slow on the mount. Co-Authored-By: Claude Opus 5.5 HumanLayer-Session: https://app.dev.codelayer.gg/sessions/01a0f3fc-13b0-758f-a343-fae04e83a7a9 --- bun.lock | 4 +- examples/alchemy-cloudflare/src/BashTool.ts | 45 ++++++++--- .../alchemy-cloudflare/src/ChatSession.ts | 10 ++- examples/alchemy-cloudflare/src/Workspace.ts | 29 ++++++- .../src/computer/Computer.ts | 77 ++++++++++++++++--- .../src/computer/ComputerWorker.ts | 9 ++- .../src/computer/Contract.ts | 9 ++- .../src/computer/container/Dockerfile | 66 ++++++++++++++++ .../alchemy-cloudflare/test/BashTool.test.ts | 14 +++- package.json | 2 +- 10 files changed, 228 insertions(+), 37 deletions(-) create mode 100644 examples/alchemy-cloudflare/src/computer/container/Dockerfile diff --git a/bun.lock b/bun.lock index a4943fa..2736fcb 100644 --- a/bun.lock +++ b/bun.lock @@ -265,7 +265,7 @@ }, }, "catalog": { - "@cloudflare/computer": "0.3.1", + "@cloudflare/computer": "https://github.com/K-Mistele/computer/releases/download/fold-91611593a1d7/cloudflare-computer-0.3.1.tgz", "@cloudflare/workers-types": "5.20260929.1", "@effect/platform-bun": "4.0.0-rc.112", "@effect/platform-node": "4.0.0-rc.112", @@ -409,7 +409,7 @@ "@clack/prompts": ["@clack/prompts@1.8.1", "", { "dependencies": { "@clack/core": "1.5.1", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-dlT1m5e/0yUL0kRNcQn7yGLVThkgbB0Ga/1AmfDDC/8ik6AIiSf2QLQO2zPYvefsHP0aFgxO93cVLCCfDp7kzQ=="], - "@cloudflare/computer": ["@cloudflare/computer@0.3.1", "", { "dependencies": { "acorn": "^8.17.0", "capnweb": "^0.10.0", "just-bash": "^3.0.1" }, "peerDependencies": { "@platformatic/vfs": "*", "ai": "^6.0.196 || ^7.0.0", "zod": "^4.4.3" }, "optionalPeers": ["@platformatic/vfs", "ai", "zod"] }, "sha512-am79tH9IukXidlJZTXeMAs7rvy4KgdlbBoOM1HgLBdxrlpeP1aQf2YLvXhxKGzSh6PkV/NmxSz0R2BcwSoRzPA=="], + "@cloudflare/computer": ["@cloudflare/computer@https://github.com/K-Mistele/computer/releases/download/fold-91611593a1d7/cloudflare-computer-0.3.1.tgz", { "dependencies": { "acorn": "^8.17.0", "capnweb": "^0.10.0", "just-bash": "^3.0.1" }, "peerDependencies": { "@platformatic/vfs": "*", "ai": "^6.0.196 || ^7.0.0", "zod": "^4.4.3" }, "optionalPeers": ["@platformatic/vfs", "ai", "zod"] }, "sha512-TC9Pqi3UtVbaA/jSW9ZOVUQcfnQaSJdJkTK1sc8oL0skyopVFtjxX6N3vrbqzavN50zKXpxTDPL6xRh6hyP2VQ=="], "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.2", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": ">1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-JBP1+Z7ZSNG/d4mRP+y8VC5dka3tZVMLEZRvS+rzQ4DGV1EoxRFQckcJTTkXbHSQiTj0DtNI01Zwb/V2fX0mvQ=="], diff --git a/examples/alchemy-cloudflare/src/BashTool.ts b/examples/alchemy-cloudflare/src/BashTool.ts index 5bd2d31..d119735 100644 --- a/examples/alchemy-cloudflare/src/BashTool.ts +++ b/examples/alchemy-cloudflare/src/BashTool.ts @@ -1,8 +1,9 @@ /** - * The agent's `bash` tool, running commands in the session workspace's shell: just-bash, which covers the - * common text commands and git but no package managers or language runtimes. fold-agent's bash tool starts - * real processes, which a Worker cannot; this one keeps its parameters and its output handling - stdout and - * stderr trimmed to the last 2000 lines or 50KB, and a failure carrying the output when the command fails. + * The agent's `bash` tool, running commands on the session's workspace in one of two places: the shell + * (just-bash, fast, with the common text commands and git) or the container (Linux, with package managers + * and language runtimes, slower to start). fold-agent's bash tool starts real processes, which a Worker + * cannot; this one keeps its parameters and its output handling - stdout and stderr trimmed to the last + * 2000 lines or 50KB, and a failure carrying the output when the command fails - and adds `backend`. */ import { defaultMaxBytes, @@ -24,6 +25,11 @@ const MAX_TIMEOUT_MS = 600_000 const BashParameters = Schema.Struct({ command: Schema.String.annotate({ description: 'Bash command to execute' }), + backend: Schema.optionalKey(Schema.Literals(['shell', 'container'])).annotate({ + description: + 'Where to run the command: "shell" (default; fast, text commands and git only) or "container" ' + + '(full Linux: installs, builds, tests)', + }), timeout_ms: Schema.optionalKey(Schema.Number).annotate({ description: `Timeout in milliseconds (default ${DEFAULT_TIMEOUT_MS}, maximum ${MAX_TIMEOUT_MS})`, }), @@ -40,11 +46,23 @@ const DESCRIPTION = 'Returns stdout then stderr, keeping the last ' + `${defaultMaxLines} lines or ${formatSize(defaultMaxBytes)}. Commands start in ${WORKSPACE_ROOT} unless ` + 'you pass workdir.\n\n' + - 'This is a lightweight shell (just-bash), not a full Linux machine. It has the common text commands - ' + - 'ls, cat, head, tail, grep, find, sed, awk, sort, uniq, wc, diff, cut, tr, xargs, jq and more - pipes, ' + - 'redirects, and git (clone, status, diff, log, add, commit, branch, checkout). It has no package managers, ' + - 'no node or python, no compilers, and no network access. Use it to explore and search the repos: list ' + - 'directories, grep for code, and inspect git history.' + 'Commands run in one of two places, which see the same files:\n' + + '- backend "shell" (the default): a lightweight shell (just-bash), not a full Linux machine. It has the ' + + 'common text commands - ls, cat, head, tail, grep, find, sed, awk, sort, uniq, wc, diff, cut, tr, xargs, ' + + 'jq and more - pipes, redirects, and git (clone, status, diff, log, add, commit, branch, checkout). It has ' + + 'no package managers, no node or python, no compilers, and no network access. It answers at once. Use it ' + + 'to explore and search the repos: list directories, grep for code, and inspect git history.\n' + + '- backend "container": a Debian Linux container where commands run as root in a bash login shell, with ' + + 'internet access, git, node 22 with npm, bun, ' + + 'python3, ripgrep, fd and tmux; install anything else with apt-get or a package manager. Use it to install ' + + 'dependencies, build, and run tests or scripts. It can take up to 30 seconds to start if it was stopped. ' + + 'Programs and files outside the workspace last only until it stops; files under the workspace are kept, ' + + "except those a repo's .gitignore lists (node_modules, build output, caches): they stay in the container " + + 'only, so the shell and the file tools cannot see them, and they are gone once the container stops, so ' + + 'reinstall or rebuild after a restart. Look at them with container commands. A gitignored file you made ' + + 'with the file tools, such as .env, does not receive changes made in the container; change it with the ' + + 'file tools.\n\n' + + 'Try the shell first, and switch to the container when a command is not found or needs a real machine.' /** The command's output as the model sees it: stdout, then stderr, trimmed from the front. */ const formatOutput = ({ stdout, stderr }: CommandOutput) => { @@ -77,9 +95,12 @@ export const bashTool = (run: (input: CommandInput) => Effect.Effect `The shell could not run the command: ${error.message}`), - ) + const output = yield* run({ + backend: params.backend ?? 'shell', + command: params.command, + cwd, + timeoutMs, + }).pipe(Effect.mapError((error) => `The shell could not run the command: ${error.message}`)) const text = formatOutput(output).replace(/\n+$/, '') if (output.status === 'cancelled') { diff --git a/examples/alchemy-cloudflare/src/ChatSession.ts b/examples/alchemy-cloudflare/src/ChatSession.ts index e84edaf..bb93ebc 100644 --- a/examples/alchemy-cloudflare/src/ChatSession.ts +++ b/examples/alchemy-cloudflare/src/ChatSession.ts @@ -66,8 +66,9 @@ const systemPrompt = (repoNames: ReadonlyArray) => repoNames.length === 0 ? 'No repos are cloned for this session.' : `This session's repos are cloned under ${WORKSPACE_ROOT}:\n${repoNames.map((name) => `- ${WORKSPACE_ROOT}/${name}`).join('\n')}`, - 'Use bash to list, search and inspect them, and the file tools to read and change files. The shell is ' + - 'lightweight: it cannot install packages, build, or run tests.', + 'Use bash to list, search and inspect them, and the file tools to read and change files. Run bash in ' + + "the container to install packages, build, and run tests. Files a repo's .gitignore lists, such as " + + 'node_modules and build output, exist only in the container.', ].join('\n\n') /** Skills from each repo's `.claude/skills` and `.agents/skills`, scanned once when the session opens. */ @@ -175,11 +176,12 @@ export default class ChatSession extends Cloudflare.DurableObject() // scope. Opened on the first send, so reading an unknown id writes nothing. const scope = yield* Scope.make() - // A new session clones its repos, then starts; a cut-off start leaves the log empty, so the next - // send clones them again from scratch. + // A new session clones its repos, starts its container in the background, then starts; a cut-off + // start leaves the log empty, so the next send clones them again from scratch. const start = (repos: ReadonlyArray) => Effect.gen(function* () { const cloned = yield* workspace.prepare(sessionId, repos) + yield* Effect.forkIn(workspace.startContainer(sessionId), scope) return yield* startSession({ agent: agentFor(cloned.map((repo) => repo.name)), log, diff --git a/examples/alchemy-cloudflare/src/Workspace.ts b/examples/alchemy-cloudflare/src/Workspace.ts index 7994079..cfe6a08 100644 --- a/examples/alchemy-cloudflare/src/Workspace.ts +++ b/examples/alchemy-cloudflare/src/Workspace.ts @@ -5,7 +5,7 @@ import type { SessionId } from '@humanlayer/fold-core' import type { RpcCallError } from 'alchemy' import * as Cloudflare from 'alchemy/Cloudflare' -import { Context, Data, Effect, type FileSystem, Layer, Schema } from 'effect' +import { Clock, Context, Data, Effect, type FileSystem, Layer, Schema } from 'effect' import { COMPUTER_WORKER_NAME, @@ -58,6 +58,7 @@ class Computer extends Cloudflare.DurableObject< ComputerFiles & { readonly prepare: (repos: ReadonlyArray) => Effect.Effect, RpcCallError> readonly exec: (input: CommandInput) => Effect.Effect, RpcCallError> + readonly startContainer: () => Effect.Effect, RpcCallError> readonly expireAt: (deleteAt: number) => Effect.Effect readonly destroy: () => Effect.Effect } @@ -73,8 +74,13 @@ export class Workspace extends Context.Service< ) => Effect.Effect, RepoCloneError> /** The session's workspace as a `FileSystem`, for fold's file tools and skill loader. */ readonly fileSystem: (sessionId: SessionId) => FileSystem.FileSystem - /** Run one command in the session's shell. */ + /** Run one command in the session's shell or container. */ readonly exec: (sessionId: SessionId, input: CommandInput) => Effect.Effect + /** + * Start the session's container, so its first command doesn't wait for it. A failure is logged, not + * raised: the first container command starts it again. + */ + readonly startContainer: (sessionId: SessionId) => Effect.Effect /** * Have the workspace delete itself at `deleteAt` (epoch milliseconds) unless moved again: the backup * for a session that never deletes it. A failure is logged, not raised. @@ -128,6 +134,24 @@ export class Workspace extends Context.Service< ), ), ), + startContainer: Effect.fn('alchemy_cloudflare.workspace.startContainer')(function* (sessionId) { + const started = yield* Clock.currentTimeMillis + const result = yield* computers + .getByName(sessionId) + .startContainer() + .pipe( + Effect.catch((error) => + Effect.succeed({ ok: false as const, code: 'RPC', message: error.message }), + ), + ) + const details = { sessionId, millis: (yield* Clock.currentTimeMillis) - started } + yield* result.ok + ? Effect.logInfo('workspace.container.started', JSON.stringify(details)) + : Effect.logWarning( + 'workspace.container.start failed', + JSON.stringify({ ...details, error: result.message }), + ) + }), exec: Effect.fn('alchemy_cloudflare.workspace.exec')(function* (sessionId, input) { const result = yield* computers .getByName(sessionId) @@ -136,6 +160,7 @@ export class Workspace extends Context.Service< yield* Effect.logInfo( 'workspace.exec', JSON.stringify({ + backend: input.backend, command: input.command, cwd: input.cwd, outcome: result.ok diff --git a/examples/alchemy-cloudflare/src/computer/Computer.ts b/examples/alchemy-cloudflare/src/computer/Computer.ts index d341295..f678a48 100644 --- a/examples/alchemy-cloudflare/src/computer/Computer.ts +++ b/examples/alchemy-cloudflare/src/computer/Computer.ts @@ -1,17 +1,30 @@ /// /** * The Computer Durable Object: one `@cloudflare/computer` Workspace per fold session, named by its - * SessionId. The Workspace is a virtual filesystem in the object's SQLite, with git, and a shell: just-bash - * in a Worker the Worker Loader starts, which reads and writes this same filesystem. A plain Durable Object - * rather than an Effect one, because `withWorkspace` must wrap the `cloudflare:workers` class. + * SessionId. The Workspace is a virtual filesystem in the object's SQLite, with git, and two places to run + * commands on it: + * + * - `shell`: just-bash in a Worker the Worker Loader starts, which reads and writes this same filesystem. + * - `container`: this object's Linux container (see `Dockerfile`), running `computerd`, which mounts a copy + * of the filesystem at `/workspace`. Each command first sends the container the files changed since the + * last one, then copies back what the command changed. + * + * A plain Durable Object rather than an Effect one, because `withWorkspace` must wrap the + * `cloudflare:workers` class. */ import { type DurableObjectStorageLike, getWorkspace, withWorkspace } from '@cloudflare/computer' +import { + CloudflareContainerBackend, + type CloudflareContainerBackendOptions, + withWorkspaceContainer, +} from '@cloudflare/computer/backends/container' import { WorkerShellBackend } from '@cloudflare/computer/backends/worker-shell' import { createGitClient, type GitClient, type GitCloneOptions } from '@cloudflare/computer/git' import jq from '@cloudflare/computer/shell/jq' import { DurableObject } from 'cloudflare:workers' import { + type Backend, type ClonedRepo, type CommandInput, type CommandOutput, @@ -22,15 +35,17 @@ import { WORKSPACE_ROOT, } from './Contract' -// The shell reaches back into this object's workspace through this entrypoint, via `ctx.exports`. -export { WorkspaceServiceProxy } from '@cloudflare/computer' +// Entrypoints the backends reach this object through, via `ctx.exports`: the shell calls back into the +// workspace through WorkspaceServiceProxy, and the container dials its connection in through WorkspaceProxy. +export { WorkspaceProxy, WorkspaceServiceProxy } from '@cloudflare/computer' type Env = { /** Starts the shell's Worker. */ readonly LOADER: WorkerLoader } -const SHELL_BACKEND = 'shell' +/** How long `startContainer` waits for the no-op command, past the container's own 30s start budget. */ +const CONTAINER_START_TIMEOUT_MILLIS = 10_000 /** How long `destroy` waits after answering before the object restarts. */ const RESTART_DELAY_MILLIS = 1_000 @@ -51,6 +66,18 @@ const workspaceStorage = (storage: DurableObjectStorage): DurableObjectStorageLi } } +/** What `withWorkspaceContainer` adds to the object. */ +type ContainerOwner = { getWorkspaceContainer(): unknown } + +/** + * The object as the container backend's host. `@cloudflare/computer` types the container API against its + * own copy of the Workers types, whose `Fetcher` has more methods than ours; the object is the same. + */ +function containerHost(self: ContainerOwner): Awaited> +function containerHost(self: ContainerOwner): ContainerOwner { + return self +} + /** Run one file operation, returning a thrown workspace error as its code and message. */ const attempt = async (operation: () => Promise): Promise> => { try { @@ -62,24 +89,49 @@ const attempt = async (operation: () => Promise): Promise { +class ComputerBase extends withWorkspaceContainer(class extends DurableObject {}) { // For withWorkspace's options, which see the instance but not `ctx` or `env`: DurableObject keeps them // protected. readonly storage = workspaceStorage(this.ctx.storage) readonly shell = new WorkerShellBackend({ - id: SHELL_BACKEND, + id: 'shell' satisfies Backend, loader: this.env.LOADER, workspace: { binding: COMPUTER_BINDING, id: this.ctx.id.toString() }, ctx: this.ctx, commands: [jq], }) + readonly container = new CloudflareContainerBackend({ + id: 'container' satisfies Backend, + container: () => containerHost(this), + workspace: { binding: COMPUTER_BINDING, id: this.ctx.id.toString() }, + // Commands may install packages. + egress: { mode: 'direct' }, + }) } export class Computer extends withWorkspace(ComputerBase, (self) => ({ storage: self.storage, git: createGitClient(), - backends: [self.shell], + backends: [self.shell, self.container], })) { + /** The container's connection, which `computerd` dials in through WorkspaceProxy. */ + override async fetch(request: Request): Promise { + return await this.container.handleFetch(request) + } + + /** + * Start the container and connect to it, so the first container command doesn't wait for it. Runs a + * no-op command: the workspace connects a backend on its first command. + */ + async startContainer(): Promise> { + return await this.exec({ + backend: 'container', + command: 'true', + cwd: '/', + timeoutMs: CONTAINER_START_TIMEOUT_MILLIS, + }).then((result) => (result.ok ? { ok: true, value: null } : result)) + } + /** * Clone each repo into `/workspace/`, one at a time, replacing whatever a cut-off earlier attempt * left there. Rejects with the first repo that fails. @@ -155,8 +207,9 @@ export class Computer extends withWorkspace(ComputerBase, (self) => ({ await this.ctx.storage.setAlarm(deleteAt) } - /** Delete the workspace: files, git data, and the backup alarm. The object then restarts empty. */ + /** Delete the workspace: the container, files, git data, and the backup alarm. The object then restarts empty. */ async destroy(): Promise { + if (this.ctx.container?.running === true) await this.ctx.container.destroy('workspace deleted') await this.ctx.storage.deleteAlarm() await this.ctx.storage.deleteAll() // The in-memory workspace must go: its tables are gone. Wait until this call has answered: Cloudflare @@ -175,12 +228,12 @@ export class Computer extends withWorkspace(ComputerBase, (self) => ({ await this.destroy() } - /** Run one command in the shell. A command that fails still succeeds here, with its exit code. */ + /** Run one command. A command that fails still succeeds here, with its exit code. */ async exec(input: CommandInput): Promise> { using workspace = await getWorkspace(this) return await attempt(async () => { using handle = await workspace.runtime.exec(input.command, { - backend: SHELL_BACKEND, + backend: input.backend, cwd: input.cwd, encoding: 'utf8', timeoutMs: input.timeoutMs, diff --git a/examples/alchemy-cloudflare/src/computer/ComputerWorker.ts b/examples/alchemy-cloudflare/src/computer/ComputerWorker.ts index 91e01a1..9a92ef2 100644 --- a/examples/alchemy-cloudflare/src/computer/ComputerWorker.ts +++ b/examples/alchemy-cloudflare/src/computer/ComputerWorker.ts @@ -12,8 +12,13 @@ export const ComputerWorker = Cloudflare.Worker('ComputerWorker', { main: './src/computer/Computer.ts', compatibility: { flags: ['nodejs_compat'], date: '2026-08-31' }, env: { - // Named COMPUTER_BINDING: the shell calls back into the Computer through this binding. - Computer: Cloudflare.DurableObject('Computer'), + // The Computer Durable Object with its container. Named COMPUTER_BINDING: the shell and the container + // call back into the Computer through this binding. + Computer: Cloudflare.Container('Computer', { + context: './src/computer/container', + + instanceType: 'standard-4', // standard-2 = 1 vCPU, standard-1 for 1/2 and standard-4 = 4 + }), LOADER: Cloudflare.WorkerLoader(), }, }) diff --git a/examples/alchemy-cloudflare/src/computer/Contract.ts b/examples/alchemy-cloudflare/src/computer/Contract.ts index fc89a30..6f329ab 100644 --- a/examples/alchemy-cloudflare/src/computer/Contract.ts +++ b/examples/alchemy-cloudflare/src/computer/Contract.ts @@ -45,8 +45,15 @@ export type FileInfo = { readonly inode: number } -/** One shell command, run in `cwd` and stopped after `timeoutMs`. */ +/** + * Where a command runs: `shell` is just-bash in a Worker, fast but with only text commands and git; + * `container` is a Linux container with node, bun, python and internet access, slow to start. + */ +export type Backend = 'shell' | 'container' + +/** One command, run on `backend` in `cwd` and stopped after `timeoutMs`. */ export type CommandInput = { + readonly backend: Backend readonly command: string readonly cwd: string readonly timeoutMs: number diff --git a/examples/alchemy-cloudflare/src/computer/container/Dockerfile b/examples/alchemy-cloudflare/src/computer/container/Dockerfile new file mode 100644 index 0000000..97efe38 --- /dev/null +++ b/examples/alchemy-cloudflare/src/computer/container/Dockerfile @@ -0,0 +1,66 @@ +# The Computer's Linux container: computerd, which mounts the session's workspace at /workspace and runs +# the agent's container commands, plus git, node, bun and python for installs, builds and tests. +# +# Based on @cloudflare/computer's examples/container. computerd comes from a fork of cloudflare/computer +# that keeps gitignored files in the container (SYNC_GITIGNORE below), so installs and builds never reach +# the Computer's storage. See https://github.com/cloudflare/computer/issues/179. To change it, push to +# the fork and update COMPUTER_COMMIT. + +FROM node:22-bookworm AS computerd +ARG COMPUTER_REPO=https://github.com/K-Mistele/computer +ARG COMPUTER_COMMIT=b8aa33f8d770dc3ab0c3f1156390a15c9f2d45da +RUN git clone --filter=blob:none "$COMPUTER_REPO" /opt/computer \ + && cd /opt/computer \ + && git checkout "$COMPUTER_COMMIT" \ + && npm ci \ + && npm run build --workspace @cloudflare/dofs --workspace @cloudflare/computer-rpc --workspace @cloudflare/computerd + +FROM oven/bun:1.3.14-debian AS bun + +FROM debian:stable-slim + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + fuse3 libfuse2t64 ca-certificates curl gnupg git jq ripgrep python3 tmux fd-find \ + && mkdir -p /etc/apt/keyrings \ + && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \ + | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \ + && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" \ + > /etc/apt/sources.list.d/nodesource.list \ + && apt-get update \ + && apt-get install -y --no-install-recommends nodejs \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=bun /usr/local/bin/bun /usr/local/bin/bun +RUN ln -s /usr/local/bin/bun /usr/local/bin/bunx \ + && ln -s /usr/bin/fdfind /usr/local/bin/fd + +COPY --from=computerd /opt/computer /opt/computer + +# computerd serves HTTP on PORT and mounts the workspace at MOUNT_POINT. FUSE_MOUNT=auto uses the kernel's +# FUSE, which Cloudflare Containers provide, and falls back to a userspace copy where it is missing. +ENV PORT=8080 +ENV MOUNT_POINT=/workspace +ENV FUSE_MOUNT=auto +# Run the agent's commands as root in a bash login shell, like a terminal: computerd calls +# `$EXEC_SHELL -c `, and the wrapper adds -l so /etc/profile and ~/.bashrc load. +ENV HOME=/root +ENV SHELL=/bin/bash +ENV EXEC_SHELL=/usr/local/bin/agent-bash +RUN printf '#!/bin/bash\nexec /bin/bash -l "$@"\n' > /usr/local/bin/agent-bash \ + && chmod +x /usr/local/bin/agent-bash +# Keep computerd's copy of the workspace on disk, not in memory, so a large install can't fill the +# container's memory. Skip SQLite's disk syncs: the disk goes away with the container, so a crash can't +# leave a damaged store behind, and syncing was most of the cost of saving each file. +ENV COMPUTERD_DB=/var/lib/computerd/state.db +ENV COMPUTERD_DB_SYNCHRONOUS=off +# Let the kernel remember "file not found" for a second. Module resolution looks for many files that +# don't exist, and without this every miss goes to computerd. computerd waits out the second after the +# Computer sends files, so a command never misses one. +ENV COMPUTERD_FUSE_NEGATIVE_TIMEOUT=1 +# Never send gitignored files back to the Computer. +ENV SYNC_GITIGNORE=1 +RUN mkdir -p /var/lib/computerd +EXPOSE 8080 + +ENTRYPOINT ["node", "/opt/computer/packages/computerd/dist/cli/computerd.cjs"] diff --git a/examples/alchemy-cloudflare/test/BashTool.test.ts b/examples/alchemy-cloudflare/test/BashTool.test.ts index 4d2c9f9..e3ea94f 100644 --- a/examples/alchemy-cloudflare/test/BashTool.test.ts +++ b/examples/alchemy-cloudflare/test/BashTool.test.ts @@ -39,7 +39,19 @@ it.effect('returns stdout then stderr, running in /workspace by default', () => const { bash, inputs } = yield* fakeBash(Effect.succeed(output({ stdout: 'README\n', stderr: 'warning' }))) expect(yield* bash({ command: 'ls' })).toEqual({ _tag: 'text', text: 'README\nwarning' }) - expect(yield* inputs).toEqual([{ command: 'ls', cwd: '/workspace', timeoutMs: 120_000 }]) + expect(yield* inputs).toEqual([{ backend: 'shell', command: 'ls', cwd: '/workspace', timeoutMs: 120_000 }]) + }), +) + +it.effect('runs in the container when asked', () => + Effect.gen(function* () { + const { bash, inputs } = yield* fakeBash(Effect.succeed(output({ stdout: 'v22.0.0\n' }))) + + expect(yield* bash({ command: 'node --version', backend: 'container' })).toEqual({ + _tag: 'text', + text: 'v22.0.0', + }) + expect((yield* inputs).map(({ backend }) => backend)).toEqual(['container']) }), ) diff --git a/package.json b/package.json index 11a9237..1f6ded9 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "catalog": { "@kitlangton/terminal-control": "0.3.1", "alchemy": "2.0.0-beta.76", - "@cloudflare/computer": "0.3.1", + "@cloudflare/computer": "https://github.com/K-Mistele/computer/releases/download/fold-91611593a1d7/cloudflare-computer-0.3.1.tgz", "@platformatic/vfs": "0.4.0", "@cloudflare/workers-types": "5.20260929.1", "@opentui/core": "0.4.3",