diff --git a/Dockerfile b/Dockerfile index 340183e46..ceeed20f8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM docker.io/cloudflare/sandbox:0.7.0 +FROM docker.io/cloudflare/sandbox:0.7.4 # Install Node.js 22 (required by OpenClaw) and rsync (for R2 backup sync) # The base image has Node 20, we need to replace it with Node 22 @@ -20,6 +20,15 @@ RUN ARCH="$(dpkg --print-architecture)" \ # Install pnpm globally RUN npm install -g pnpm +# Install gogcli (Google Workspace CLI for Gmail/Calendar/Drive) +# OpenClaw has a built-in 'gog' skill that wraps this CLI +ENV GOG_VERSION=0.9.0 +RUN ARCH="$(dpkg --print-architecture)" \ + && curl -fsSL "https://github.com/steipete/gogcli/releases/download/v${GOG_VERSION}/gogcli_${GOG_VERSION}_linux_${ARCH}.tar.gz" -o /tmp/gogcli.tar.gz \ + && tar -xzf /tmp/gogcli.tar.gz -C /usr/local/bin gog \ + && rm /tmp/gogcli.tar.gz \ + && gog --version + # Install OpenClaw (formerly clawdbot/moltbot) # Pin to specific version for reproducible builds RUN npm install -g openclaw@2026.2.3 \ @@ -28,11 +37,16 @@ RUN npm install -g openclaw@2026.2.3 \ # Create OpenClaw directories # Legacy .clawdbot paths are kept for R2 backup migration RUN mkdir -p /root/.openclaw \ + && mkdir -p /root/.openclaw/workspace \ && mkdir -p /root/clawd \ && mkdir -p /root/clawd/skills +# Copy default workspace files (USER.md, SOUL.md, MEMORY.md) +# These are seeds — the startup script only uses them if no R2 backup exists +COPY workspace/ /root/.openclaw/workspace-defaults/ + # Copy startup script -# Build cache bust: 2026-02-06-v29-sync-workspace +# Build cache bust: 2026-02-14-v41-doctor-timeout-and-host-header-fix COPY start-openclaw.sh /usr/local/bin/start-openclaw.sh RUN chmod +x /usr/local/bin/start-openclaw.sh diff --git a/package-lock.json b/package-lock.json index 2e5494df7..e0de38209 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "react-dom": "^19.0.0" }, "devDependencies": { - "@cloudflare/sandbox": "*", + "@cloudflare/sandbox": "^0.7.4", "@cloudflare/vite-plugin": "^1.0.0", "@cloudflare/workers-types": "^4.20250109.0", "@types/node": "^22.0.0", @@ -29,7 +29,7 @@ "typescript": "^5.9.3", "vite": "^6.0.0", "vitest": "^4.0.18", - "wrangler": "^4.50.0" + "wrangler": "^4.65.0" } }, "node_modules/@babel/code-frame": { @@ -383,7 +383,9 @@ } }, "node_modules/@cloudflare/sandbox": { - "version": "0.7.0", + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@cloudflare/sandbox/-/sandbox-0.7.4.tgz", + "integrity": "sha512-8DRGlATKH5chOVTEwcntuS9ZtYmjknuIoXRUnIXDwwEEhst4FFXf/DjNxeBr/4J4XH6aWJvOKcdsTlajrhlhHA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -391,7 +393,8 @@ }, "peerDependencies": { "@openai/agents": "^0.3.3", - "@opencode-ai/sdk": "^1.0.137" + "@opencode-ai/sdk": "^1.1.40", + "@xterm/xterm": ">=5.0.0" }, "peerDependenciesMeta": { "@openai/agents": { @@ -399,6 +402,9 @@ }, "@opencode-ai/sdk": { "optional": true + }, + "@xterm/xterm": { + "optional": true } } }, @@ -434,41 +440,10 @@ "wrangler": "^4.60.0" } }, - "node_modules/@cloudflare/workerd-darwin-arm64": { - "version": "1.20260120.0", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@cloudflare/workers-types": { - "version": "4.20260124.0", - "dev": true, - "license": "MIT OR Apache-2.0" - }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "node_modules/@cloudflare/vite-plugin/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz", + "integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==", "cpu": [ "ppc64" ], @@ -482,10 +457,10 @@ "node": ">=18" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "node_modules/@cloudflare/vite-plugin/node_modules/@esbuild/android-arm": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.0.tgz", + "integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==", "cpu": [ "arm" ], @@ -499,10 +474,10 @@ "node": ">=18" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "node_modules/@cloudflare/vite-plugin/node_modules/@esbuild/android-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz", + "integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==", "cpu": [ "arm64" ], @@ -516,10 +491,10 @@ "node": ">=18" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "node_modules/@cloudflare/vite-plugin/node_modules/@esbuild/android-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.0.tgz", + "integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==", "cpu": [ "x64" ], @@ -533,8 +508,10 @@ "node": ">=18" } }, - "node_modules/@esbuild/darwin-arm64": { + "node_modules/@cloudflare/vite-plugin/node_modules/@esbuild/darwin-arm64": { "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz", + "integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==", "cpu": [ "arm64" ], @@ -548,10 +525,10 @@ "node": ">=18" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "node_modules/@cloudflare/vite-plugin/node_modules/@esbuild/darwin-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz", + "integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==", "cpu": [ "x64" ], @@ -565,10 +542,10 @@ "node": ">=18" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "node_modules/@cloudflare/vite-plugin/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz", + "integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==", "cpu": [ "arm64" ], @@ -582,10 +559,10 @@ "node": ">=18" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "node_modules/@cloudflare/vite-plugin/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz", + "integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==", "cpu": [ "x64" ], @@ -599,10 +576,10 @@ "node": ">=18" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "node_modules/@cloudflare/vite-plugin/node_modules/@esbuild/linux-arm": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz", + "integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==", "cpu": [ "arm" ], @@ -616,10 +593,10 @@ "node": ">=18" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "node_modules/@cloudflare/vite-plugin/node_modules/@esbuild/linux-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz", + "integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==", "cpu": [ "arm64" ], @@ -633,10 +610,10 @@ "node": ">=18" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "node_modules/@cloudflare/vite-plugin/node_modules/@esbuild/linux-ia32": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz", + "integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==", "cpu": [ "ia32" ], @@ -650,10 +627,10 @@ "node": ">=18" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "node_modules/@cloudflare/vite-plugin/node_modules/@esbuild/linux-loong64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz", + "integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==", "cpu": [ "loong64" ], @@ -667,10 +644,10 @@ "node": ">=18" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "node_modules/@cloudflare/vite-plugin/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz", + "integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==", "cpu": [ "mips64el" ], @@ -684,10 +661,10 @@ "node": ">=18" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "node_modules/@cloudflare/vite-plugin/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz", + "integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==", "cpu": [ "ppc64" ], @@ -701,10 +678,10 @@ "node": ">=18" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "node_modules/@cloudflare/vite-plugin/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz", + "integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==", "cpu": [ "riscv64" ], @@ -718,10 +695,10 @@ "node": ">=18" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "node_modules/@cloudflare/vite-plugin/node_modules/@esbuild/linux-s390x": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz", + "integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==", "cpu": [ "s390x" ], @@ -735,10 +712,10 @@ "node": ">=18" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "node_modules/@cloudflare/vite-plugin/node_modules/@esbuild/linux-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz", + "integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==", "cpu": [ "x64" ], @@ -752,10 +729,10 @@ "node": ">=18" } }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "node_modules/@cloudflare/vite-plugin/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz", + "integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==", "cpu": [ "arm64" ], @@ -769,10 +746,10 @@ "node": ">=18" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "node_modules/@cloudflare/vite-plugin/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz", + "integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==", "cpu": [ "x64" ], @@ -786,10 +763,10 @@ "node": ">=18" } }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "node_modules/@cloudflare/vite-plugin/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz", + "integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==", "cpu": [ "arm64" ], @@ -803,10 +780,10 @@ "node": ">=18" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "node_modules/@cloudflare/vite-plugin/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz", + "integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==", "cpu": [ "x64" ], @@ -820,10 +797,10 @@ "node": ">=18" } }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "node_modules/@cloudflare/vite-plugin/node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz", + "integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==", "cpu": [ "arm64" ], @@ -837,10 +814,10 @@ "node": ">=18" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "node_modules/@cloudflare/vite-plugin/node_modules/@esbuild/sunos-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz", + "integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==", "cpu": [ "x64" ], @@ -854,10 +831,10 @@ "node": ">=18" } }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "node_modules/@cloudflare/vite-plugin/node_modules/@esbuild/win32-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz", + "integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==", "cpu": [ "arm64" ], @@ -871,10 +848,10 @@ "node": ">=18" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "node_modules/@cloudflare/vite-plugin/node_modules/@esbuild/win32-ia32": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz", + "integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==", "cpu": [ "ia32" ], @@ -888,63 +865,1134 @@ "node": ">=18" } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "node_modules/@cloudflare/vite-plugin/node_modules/@esbuild/win32-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz", + "integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vite-plugin/node_modules/esbuild": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz", + "integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.0", + "@esbuild/android-arm": "0.27.0", + "@esbuild/android-arm64": "0.27.0", + "@esbuild/android-x64": "0.27.0", + "@esbuild/darwin-arm64": "0.27.0", + "@esbuild/darwin-x64": "0.27.0", + "@esbuild/freebsd-arm64": "0.27.0", + "@esbuild/freebsd-x64": "0.27.0", + "@esbuild/linux-arm": "0.27.0", + "@esbuild/linux-arm64": "0.27.0", + "@esbuild/linux-ia32": "0.27.0", + "@esbuild/linux-loong64": "0.27.0", + "@esbuild/linux-mips64el": "0.27.0", + "@esbuild/linux-ppc64": "0.27.0", + "@esbuild/linux-riscv64": "0.27.0", + "@esbuild/linux-s390x": "0.27.0", + "@esbuild/linux-x64": "0.27.0", + "@esbuild/netbsd-arm64": "0.27.0", + "@esbuild/netbsd-x64": "0.27.0", + "@esbuild/openbsd-arm64": "0.27.0", + "@esbuild/openbsd-x64": "0.27.0", + "@esbuild/openharmony-arm64": "0.27.0", + "@esbuild/sunos-x64": "0.27.0", + "@esbuild/win32-arm64": "0.27.0", + "@esbuild/win32-ia32": "0.27.0", + "@esbuild/win32-x64": "0.27.0" + } + }, + "node_modules/@cloudflare/vite-plugin/node_modules/wrangler": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.60.0.tgz", + "integrity": "sha512-n4kibm/xY0Qd5G2K/CbAQeVeOIlwPNVglmFjlDRCCYk3hZh8IggO/rg8AXt/vByK2Sxsugl5Z7yvgWxrUbmS6g==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "0.4.2", + "@cloudflare/unenv-preset": "2.11.0", + "blake3-wasm": "2.1.5", + "esbuild": "0.27.0", + "miniflare": "4.20260120.0", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.24", + "workerd": "1.20260120.0" + }, + "bin": { + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=20.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^4.20260120.0" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20260120.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260120.0.tgz", + "integrity": "sha512-JLHx3p5dpwz4wjVSis45YNReftttnI3ndhdMh5BUbbpdreN/g0jgxNt5Qp9tDFqEKl++N63qv+hxJiIIvSLR+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20260120.0", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20260120.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260120.0.tgz", + "integrity": "sha512-O0mIfJfvU7F8N5siCoRDaVDuI12wkz2xlG4zK6/Ct7U9c9FiE0ViXNFWXFQm5PPj+qbkNRyhjUwhP+GCKTk5EQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20260120.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260120.0.tgz", + "integrity": "sha512-aRHO/7bjxVpjZEmVVcpmhbzpN6ITbFCxuLLZSW0H9O0C0w40cDCClWSi19T87Ax/PQcYjFNT22pTewKsupkckA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20260120.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260120.0.tgz", + "integrity": "sha512-ASZIz1E8sqZQqQCgcfY1PJbBpUDrxPt8NZ+lqNil0qxnO4qX38hbCsdDF2/TDAuq0Txh7nu8ztgTelfNDlb4EA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workers-types": { + "version": "4.20260214.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260214.0.tgz", + "integrity": "sha512-qb8rgbAdJR4BAPXolXhFL/wuGtecHLh1veOyZ1mK6QqWuCdI3vK1biKC0i3lzmzdLR/DZvsN3mNtpUE8zpWGEg==", + "dev": true, + "license": "MIT OR Apache-2.0" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/colour": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", "cpu": [ - "x64" + "arm64" ], "dev": true, - "license": "MIT", + "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=18" - } - }, - "node_modules/@img/colour": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@img/sharp-darwin-arm64": { + "node_modules/@img/sharp-win32-ia32": { "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", "cpu": [ - "arm64" + "ia32" ], "dev": true, - "license": "Apache-2.0", + "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ - "darwin" + "win32" ], "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" } }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", "cpu": [ - "arm64" + "x64" ], "dev": true, - "license": "LGPL-3.0-or-later", + "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ - "darwin" + "win32" ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, "funding": { "url": "https://opencollective.com/libvips" } @@ -2381,7 +3429,9 @@ "license": "MIT" }, "node_modules/esbuild": { - "version": "0.27.0", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2392,38 +3442,38 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.0", - "@esbuild/android-arm": "0.27.0", - "@esbuild/android-arm64": "0.27.0", - "@esbuild/android-x64": "0.27.0", - "@esbuild/darwin-arm64": "0.27.0", - "@esbuild/darwin-x64": "0.27.0", - "@esbuild/freebsd-arm64": "0.27.0", - "@esbuild/freebsd-x64": "0.27.0", - "@esbuild/linux-arm": "0.27.0", - "@esbuild/linux-arm64": "0.27.0", - "@esbuild/linux-ia32": "0.27.0", - "@esbuild/linux-loong64": "0.27.0", - "@esbuild/linux-mips64el": "0.27.0", - "@esbuild/linux-ppc64": "0.27.0", - "@esbuild/linux-riscv64": "0.27.0", - "@esbuild/linux-s390x": "0.27.0", - "@esbuild/linux-x64": "0.27.0", - "@esbuild/netbsd-arm64": "0.27.0", - "@esbuild/netbsd-x64": "0.27.0", - "@esbuild/openbsd-arm64": "0.27.0", - "@esbuild/openbsd-x64": "0.27.0", - "@esbuild/openharmony-arm64": "0.27.0", - "@esbuild/sunos-x64": "0.27.0", - "@esbuild/win32-arm64": "0.27.0", - "@esbuild/win32-ia32": "0.27.0", - "@esbuild/win32-x64": "0.27.0" + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" } }, "node_modules/esbuild/node_modules/@esbuild/aix-ppc64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz", - "integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", "cpu": [ "ppc64" ], @@ -2438,9 +3488,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/android-arm": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.0.tgz", - "integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", "cpu": [ "arm" ], @@ -2455,9 +3505,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/android-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz", - "integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", "cpu": [ "arm64" ], @@ -2472,9 +3522,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/android-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.0.tgz", - "integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", "cpu": [ "x64" ], @@ -2489,9 +3539,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/darwin-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz", - "integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", "cpu": [ "x64" ], @@ -2506,9 +3556,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz", - "integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", "cpu": [ "arm64" ], @@ -2523,9 +3573,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/freebsd-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz", - "integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", "cpu": [ "x64" ], @@ -2540,9 +3590,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/linux-arm": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz", - "integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", "cpu": [ "arm" ], @@ -2557,9 +3607,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/linux-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz", - "integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", "cpu": [ "arm64" ], @@ -2574,9 +3624,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/linux-ia32": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz", - "integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", "cpu": [ "ia32" ], @@ -2591,9 +3641,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/linux-loong64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz", - "integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", "cpu": [ "loong64" ], @@ -2608,9 +3658,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/linux-mips64el": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz", - "integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", "cpu": [ "mips64el" ], @@ -2625,9 +3675,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/linux-ppc64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz", - "integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", "cpu": [ "ppc64" ], @@ -2642,9 +3692,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/linux-riscv64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz", - "integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", "cpu": [ "riscv64" ], @@ -2659,9 +3709,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/linux-s390x": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz", - "integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", "cpu": [ "s390x" ], @@ -2676,9 +3726,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/linux-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz", - "integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", "cpu": [ "x64" ], @@ -2693,9 +3743,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz", - "integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", "cpu": [ "arm64" ], @@ -2710,9 +3760,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/netbsd-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz", - "integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", "cpu": [ "x64" ], @@ -2727,9 +3777,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz", - "integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", "cpu": [ "arm64" ], @@ -2744,9 +3794,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/openbsd-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz", - "integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", "cpu": [ "x64" ], @@ -2761,9 +3811,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz", - "integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", "cpu": [ "arm64" ], @@ -2778,9 +3828,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/sunos-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz", - "integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", "cpu": [ "x64" ], @@ -2795,9 +3845,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/win32-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz", - "integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", "cpu": [ "arm64" ], @@ -2812,9 +3862,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/win32-ia32": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz", - "integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", "cpu": [ "ia32" ], @@ -2829,9 +3879,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/win32-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz", - "integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", "cpu": [ "x64" ], @@ -4235,18 +5285,20 @@ } }, "node_modules/wrangler": { - "version": "4.60.0", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.65.0.tgz", + "integrity": "sha512-R+n3o3tlGzLK9I4fGocPReOuvcnjhtOL2aCVKkHMeuEwt9pPbOO4FxJtx/ec5cIUG/otRyJnfQGCAr9DplBVng==", "dev": true, "license": "MIT OR Apache-2.0", "dependencies": { "@cloudflare/kv-asset-handler": "0.4.2", - "@cloudflare/unenv-preset": "2.11.0", + "@cloudflare/unenv-preset": "2.12.1", "blake3-wasm": "2.1.5", - "esbuild": "0.27.0", - "miniflare": "4.20260120.0", + "esbuild": "0.27.3", + "miniflare": "4.20260212.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", - "workerd": "1.20260120.0" + "workerd": "1.20260212.0" }, "bin": { "wrangler": "bin/wrangler.js", @@ -4259,7 +5311,7 @@ "fsevents": "~2.3.2" }, "peerDependencies": { - "@cloudflare/workers-types": "^4.20260120.0" + "@cloudflare/workers-types": "^4.20260212.0" }, "peerDependenciesMeta": { "@cloudflare/workers-types": { @@ -4267,6 +5319,149 @@ } } }, + "node_modules/wrangler/node_modules/@cloudflare/unenv-preset": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.12.1.tgz", + "integrity": "sha512-tP/Wi+40aBJovonSNJSsS7aFJY0xjuckKplmzDs2Xat06BJ68B6iG7YDUWXJL8gNn0gqW7YC5WhlYhO3QbugQA==", + "dev": true, + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "unenv": "2.0.0-rc.24", + "workerd": "^1.20260115.0" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } + } + }, + "node_modules/wrangler/node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20260212.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260212.0.tgz", + "integrity": "sha512-kLxuYutk88Wlo7edp8mlkN68TgZZ9237SUnuX9kNaD5jcOdblUqiBctMRZeRcPsuoX/3g2t0vS4ga02NBEVRNg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/wrangler/node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20260212.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260212.0.tgz", + "integrity": "sha512-fqoqQWMA1D0ZzDOD8sp0allREM2M8GHdpxMXQ8EdZpZ70z5bJbJ9Vr4qe35++FNIZJspsDHfTw3Xm/M4ELm/dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/wrangler/node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20260212.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260212.0.tgz", + "integrity": "sha512-bCSQoZzDzV5MSh4ueWo1DgmOn4Hf3QBu4Yo3eQFXA2llYFIu/sZgRtkEehw1X2/SY5Sn6O0EMCqxJYRf82Wdeg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/wrangler/node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20260212.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260212.0.tgz", + "integrity": "sha512-GPvp1iiKQodtbUDi6OmR5I0vD75lawB54tdYGtmypuHC7ZOI2WhBmhb3wCxgnQNOG1z7mhCQrzRCoqrKwYbVWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/wrangler/node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20260212.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260212.0.tgz", + "integrity": "sha512-wHRI218Xn4ndgWJCUHH4Zx0YlU5q/o6OmcxXkcw95tJOsQn4lDrhppioPh4eScxJZALf2X+ODeZcyQTCq5exGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/wrangler/node_modules/miniflare": { + "version": "4.20260212.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260212.0.tgz", + "integrity": "sha512-Lgxq83EuR2q/0/DAVOSGXhXS1V7GDB04HVggoPsenQng8sqEDR3hO4FigIw5ZI2Sv2X7kIc30NCzGHJlCFIYWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "sharp": "^0.34.5", + "undici": "7.18.2", + "workerd": "1.20260212.0", + "ws": "8.18.0", + "youch": "4.1.0-beta.10" + }, + "bin": { + "miniflare": "bootstrap.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/wrangler/node_modules/workerd": { + "version": "1.20260212.0", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260212.0.tgz", + "integrity": "sha512-4B9BoZUzKSRv3pVZGEPh7OX+Q817hpUqAUtz5O0TxJVqo4OsYJAUA/sY177Q5ha/twjT9KaJt2DtQzE+oyCOzw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20260212.0", + "@cloudflare/workerd-darwin-arm64": "1.20260212.0", + "@cloudflare/workerd-linux-64": "1.20260212.0", + "@cloudflare/workerd-linux-arm64": "1.20260212.0", + "@cloudflare/workerd-windows-64": "1.20260212.0" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/package.json b/package.json index c2801f422..26ffb4adf 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "private": true, "description": "Run Moltbot personal AI assistant in a Cloudflare Sandbox", "scripts": { - "build": "vite build", + "build": "vite build && mv dist/client/index.html dist/client/_admin-app.html", "deploy": "npm run build && wrangler deploy", "dev": "vite dev", "start": "wrangler dev", @@ -27,7 +27,7 @@ "react-dom": "^19.0.0" }, "devDependencies": { - "@cloudflare/sandbox": "*", + "@cloudflare/sandbox": "^0.7.4", "@cloudflare/vite-plugin": "^1.0.0", "@cloudflare/workers-types": "^4.20250109.0", "@types/node": "^22.0.0", @@ -40,7 +40,7 @@ "typescript": "^5.9.3", "vite": "^6.0.0", "vitest": "^4.0.18", - "wrangler": "^4.50.0" + "wrangler": "^4.65.0" }, "author": "", "license": "Apache-2.0", diff --git a/skills/google-workspace/SKILL.md b/skills/google-workspace/SKILL.md new file mode 100644 index 000000000..35b7372f3 --- /dev/null +++ b/skills/google-workspace/SKILL.md @@ -0,0 +1,55 @@ +--- +name: google-workspace +description: Access Gmail and Google Calendar via Google APIs. Search/read/send email and list/create calendar events. Requires GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, and GOOGLE_REFRESH_TOKEN env vars. +--- + +# Google Workspace + +Access Gmail and Google Calendar from the container via Google APIs with OAuth2 authentication. + +## Prerequisites + +- `GOOGLE_CLIENT_ID` environment variable set +- `GOOGLE_CLIENT_SECRET` environment variable set +- `GOOGLE_REFRESH_TOKEN` environment variable set + +## Quick Start + +### Search Gmail +```bash +node /root/clawd/skills/google-workspace/scripts/gmail-search.js "from:someone@example.com" --max 10 +``` + +### Read an email +```bash +node /root/clawd/skills/google-workspace/scripts/gmail-read.js +``` + +### Send an email +```bash +node /root/clawd/skills/google-workspace/scripts/gmail-send.js --to user@example.com --subject "Hello" --body "Message body" +``` + +### List calendar events +```bash +node /root/clawd/skills/google-workspace/scripts/calendar-events.js primary --from 2026-02-09 --to 2026-02-10 +``` + +### Create a calendar event +```bash +node /root/clawd/skills/google-workspace/scripts/calendar-create.js primary --summary "Meeting" --start "2026-02-10T10:00:00" --end "2026-02-10T11:00:00" +``` + +## Available Scripts + +| Script | Purpose | +|--------|---------| +| `gmail-search.js` | Search Gmail messages by query | +| `gmail-read.js` | Read full content of a single email | +| `gmail-send.js` | Send an email | +| `calendar-events.js` | List calendar events in a date range | +| `calendar-create.js` | Create a new calendar event | + +## Output Format + +Gmail search and calendar events output TSV (tab-separated values) for easy parsing, matching the format used by gogcli. diff --git a/skills/google-workspace/scripts/calendar-create.js b/skills/google-workspace/scripts/calendar-create.js new file mode 100644 index 000000000..b05ac3b0e --- /dev/null +++ b/skills/google-workspace/scripts/calendar-create.js @@ -0,0 +1,80 @@ +#!/usr/bin/env node +/** + * Calendar Create Event + * + * Usage: node calendar-create.js --summary --start --end [--description ] [--location ] + * + * Creates a new calendar event. + * + * calendarId: 'primary' for the user's main calendar, or a specific calendar ID + * Datetimes: ISO 8601 format (e.g., 2026-02-10T10:00:00) + */ + +const { getCalendar } = require('./google-auth'); + +function parseArgs(args) { + const result = { calendarId: 'primary' }; + for (let i = 0; i < args.length; i++) { + if (args[i] === '--summary' && args[i + 1]) { + result.summary = args[++i]; + } else if (args[i] === '--start' && args[i + 1]) { + result.start = args[++i]; + } else if (args[i] === '--end' && args[i + 1]) { + result.end = args[++i]; + } else if (args[i] === '--description' && args[i + 1]) { + result.description = args[++i]; + } else if (args[i] === '--location' && args[i + 1]) { + result.location = args[++i]; + } else if (!args[i].startsWith('--')) { + result.calendarId = args[i]; + } + } + return result; +} + +function toEventDateTime(dateStr) { + if (!dateStr) return undefined; + // All-day event: just a date + if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) { + return { date: dateStr }; + } + // Datetime event + let dt = dateStr; + if (!dt.includes('Z') && !dt.includes('+') && !dt.includes('-', 10)) { + dt += ':00'; // Ensure seconds + } + return { dateTime: dt, timeZone: 'America/Los_Angeles' }; +} + +async function main() { + const opts = parseArgs(process.argv.slice(2)); + + if (!opts.summary || !opts.start || !opts.end) { + console.error('Usage: node calendar-create.js --summary --start --end [--description ] [--location ]'); + process.exit(1); + } + + const calendar = getCalendar(); + + const event = { + summary: opts.summary, + start: toEventDateTime(opts.start), + end: toEventDateTime(opts.end), + }; + + if (opts.description) event.description = opts.description; + if (opts.location) event.location = opts.location; + + const res = await calendar.events.insert({ + calendarId: opts.calendarId, + requestBody: event, + }); + + console.log(`Created event: ${res.data.id}`); + console.log(`Link: ${res.data.htmlLink}`); +} + +main().catch(err => { + console.error('Error:', err.message); + process.exit(1); +}); diff --git a/skills/google-workspace/scripts/calendar-events.js b/skills/google-workspace/scripts/calendar-events.js new file mode 100644 index 000000000..a12142b8b --- /dev/null +++ b/skills/google-workspace/scripts/calendar-events.js @@ -0,0 +1,81 @@ +#!/usr/bin/env node +/** + * Calendar Events + * + * Usage: node calendar-events.js --from --to [--max N] + * + * Lists calendar events using events.list API. + * Outputs: ID, start, end, summary (TSV format) + * + * calendarId: 'primary' for the user's main calendar, or a specific calendar ID + * Dates: YYYY-MM-DD or ISO 8601 datetime + */ + +const { getCalendar } = require('./google-auth'); + +function parseArgs(args) { + const result = { calendarId: 'primary', maxResults: 50 }; + for (let i = 0; i < args.length; i++) { + if (args[i] === '--from' && args[i + 1]) { + result.from = args[++i]; + } else if (args[i] === '--to' && args[i + 1]) { + result.to = args[++i]; + } else if (args[i] === '--max' && args[i + 1]) { + result.maxResults = parseInt(args[++i], 10); + } else if (!args[i].startsWith('--')) { + result.calendarId = args[i]; + } + } + return result; +} + +function toRFC3339(dateStr) { + if (!dateStr) return undefined; + // If it's just a date (YYYY-MM-DD), append time + if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) { + return dateStr + 'T00:00:00Z'; + } + // If no timezone info, assume UTC + if (!dateStr.includes('Z') && !dateStr.includes('+') && !dateStr.includes('-', 10)) { + return dateStr + 'Z'; + } + return dateStr; +} + +async function main() { + const opts = parseArgs(process.argv.slice(2)); + + const calendar = getCalendar(); + + const params = { + calendarId: opts.calendarId, + maxResults: opts.maxResults, + singleEvents: true, + orderBy: 'startTime', + }; + + if (opts.from) params.timeMin = toRFC3339(opts.from); + if (opts.to) params.timeMax = toRFC3339(opts.to); + + const res = await calendar.events.list(params); + const events = res.data.items || []; + + if (events.length === 0) { + console.log('No events found.'); + return; + } + + console.log('ID\tStart\tEnd\tSummary'); + + for (const event of events) { + const start = event.start?.dateTime || event.start?.date || ''; + const end = event.end?.dateTime || event.end?.date || ''; + const summary = event.summary || '(no title)'; + console.log(`${event.id}\t${start}\t${end}\t${summary}`); + } +} + +main().catch(err => { + console.error('Error:', err.message); + process.exit(1); +}); diff --git a/skills/google-workspace/scripts/gmail-read.js b/skills/google-workspace/scripts/gmail-read.js new file mode 100644 index 000000000..804aaa5cd --- /dev/null +++ b/skills/google-workspace/scripts/gmail-read.js @@ -0,0 +1,86 @@ +#!/usr/bin/env node +/** + * Gmail Read + * + * Usage: node gmail-read.js + * + * Reads a single email's full content. + */ + +const { getGmail } = require('./google-auth'); + +function decodeBody(body) { + if (!body?.data) return ''; + return Buffer.from(body.data, 'base64url').toString('utf-8'); +} + +function extractText(payload) { + if (!payload) return ''; + + // Simple text/plain or text/html body + if (payload.mimeType === 'text/plain' && payload.body?.data) { + return decodeBody(payload.body); + } + + // Multipart: recurse through parts + if (payload.parts) { + // Prefer text/plain + for (const part of payload.parts) { + if (part.mimeType === 'text/plain' && part.body?.data) { + return decodeBody(part.body); + } + } + // Fall back to text/html + for (const part of payload.parts) { + if (part.mimeType === 'text/html' && part.body?.data) { + return decodeBody(part.body); + } + } + // Recurse into nested multipart + for (const part of payload.parts) { + const text = extractText(part); + if (text) return text; + } + } + + // Fallback: decode whatever body is there + if (payload.body?.data) { + return decodeBody(payload.body); + } + + return ''; +} + +async function main() { + const messageId = process.argv[2]; + if (!messageId) { + console.error('Usage: node gmail-read.js '); + process.exit(1); + } + + const gmail = getGmail(); + + const res = await gmail.users.messages.get({ + userId: 'me', + id: messageId, + format: 'full', + }); + + const headers = res.data.payload?.headers || []; + const getHeader = (name) => headers.find(h => h.name === name)?.value || ''; + + console.log(`From: ${getHeader('From')}`); + console.log(`To: ${getHeader('To')}`); + console.log(`Date: ${getHeader('Date')}`); + console.log(`Subject: ${getHeader('Subject')}`); + console.log(`Labels: ${(res.data.labelIds || []).join(', ')}`); + console.log('---'); + + const body = extractText(res.data.payload); + console.log(body || '(no text content)'); +} + +main().catch(err => { + console.error('Error:', err.message); + process.exit(1); +}); diff --git a/skills/google-workspace/scripts/gmail-search.js b/skills/google-workspace/scripts/gmail-search.js new file mode 100644 index 000000000..66474036b --- /dev/null +++ b/skills/google-workspace/scripts/gmail-search.js @@ -0,0 +1,67 @@ +#!/usr/bin/env node +/** + * Gmail Search + * + * Usage: node gmail-search.js [--max N] + * + * Searches Gmail using the users.messages.list + get API. + * Outputs: ID, date, from, subject, labels (TSV format) + */ + +const { getGmail } = require('./google-auth'); + +async function main() { + const args = process.argv.slice(2); + if (args.length === 0) { + console.error('Usage: node gmail-search.js [--max N]'); + process.exit(1); + } + + let query = ''; + let maxResults = 20; + + for (let i = 0; i < args.length; i++) { + if (args[i] === '--max' && args[i + 1]) { + maxResults = parseInt(args[i + 1], 10); + i++; + } else { + query += (query ? ' ' : '') + args[i]; + } + } + + const gmail = getGmail(); + + const listRes = await gmail.users.messages.list({ + userId: 'me', + q: query, + maxResults, + }); + + const messages = listRes.data.messages || []; + if (messages.length === 0) { + console.log('No messages found.'); + return; + } + + console.log('ID\tDate\tFrom\tSubject\tLabels'); + + for (const msg of messages) { + const detail = await gmail.users.messages.get({ + userId: 'me', + id: msg.id, + format: 'metadata', + metadataHeaders: ['From', 'Subject', 'Date'], + }); + + const headers = detail.data.payload?.headers || []; + const getHeader = (name) => headers.find(h => h.name === name)?.value || ''; + const labels = (detail.data.labelIds || []).join(','); + + console.log(`${msg.id}\t${getHeader('Date')}\t${getHeader('From')}\t${getHeader('Subject')}\t${labels}`); + } +} + +main().catch(err => { + console.error('Error:', err.message); + process.exit(1); +}); diff --git a/skills/google-workspace/scripts/gmail-send.js b/skills/google-workspace/scripts/gmail-send.js new file mode 100644 index 000000000..654c5163c --- /dev/null +++ b/skills/google-workspace/scripts/gmail-send.js @@ -0,0 +1,63 @@ +#!/usr/bin/env node +/** + * Gmail Send + * + * Usage: node gmail-send.js --to --subject --body + * + * Sends an email via Gmail API. + */ + +const { getGmail } = require('./google-auth'); + +function parseArgs(args) { + const result = {}; + for (let i = 0; i < args.length; i++) { + if (args[i] === '--to' && args[i + 1]) { + result.to = args[++i]; + } else if (args[i] === '--subject' && args[i + 1]) { + result.subject = args[++i]; + } else if (args[i] === '--body' && args[i + 1]) { + result.body = args[++i]; + } else if (args[i] === '--cc' && args[i + 1]) { + result.cc = args[++i]; + } else if (args[i] === '--bcc' && args[i + 1]) { + result.bcc = args[++i]; + } + } + return result; +} + +async function main() { + const opts = parseArgs(process.argv.slice(2)); + + if (!opts.to || !opts.subject || !opts.body) { + console.error('Usage: node gmail-send.js --to --subject --body [--cc ] [--bcc ]'); + process.exit(1); + } + + const gmail = getGmail(); + + // Build RFC 2822 message + let message = `To: ${opts.to}\n`; + if (opts.cc) message += `Cc: ${opts.cc}\n`; + if (opts.bcc) message += `Bcc: ${opts.bcc}\n`; + message += `Subject: ${opts.subject}\n`; + message += `Content-Type: text/plain; charset=utf-8\n\n`; + message += opts.body; + + const encodedMessage = Buffer.from(message).toString('base64url'); + + const res = await gmail.users.messages.send({ + userId: 'me', + requestBody: { + raw: encodedMessage, + }, + }); + + console.log(`Sent message ID: ${res.data.id}`); +} + +main().catch(err => { + console.error('Error:', err.message); + process.exit(1); +}); diff --git a/skills/google-workspace/scripts/google-auth.js b/skills/google-workspace/scripts/google-auth.js new file mode 100644 index 000000000..44a72884b --- /dev/null +++ b/skills/google-workspace/scripts/google-auth.js @@ -0,0 +1,49 @@ +#!/usr/bin/env node +/** + * Google Workspace - Shared Auth Library + * + * Creates authenticated Google API clients using OAuth2 credentials + * from environment variables. Handles token refresh automatically. + * + * Usage: + * const { getGmail, getCalendar } = require('./google-auth'); + * const gmail = getGmail(); + * const calendar = getCalendar(); + */ + +// Ensure globally-installed npm packages are resolvable +// (sandbox startProcess does not inherit Dockerfile ENV vars) +if (!process.env.NODE_PATH) { + process.env.NODE_PATH = '/usr/local/lib/node_modules'; + require('module').Module._initPaths(); +} + +const { google } = require('googleapis'); + +function getAuth() { + const clientId = process.env.GOOGLE_CLIENT_ID; + const clientSecret = process.env.GOOGLE_CLIENT_SECRET; + const refreshToken = process.env.GOOGLE_REFRESH_TOKEN; + + if (!clientId || !clientSecret || !refreshToken) { + const missing = []; + if (!clientId) missing.push('GOOGLE_CLIENT_ID'); + if (!clientSecret) missing.push('GOOGLE_CLIENT_SECRET'); + if (!refreshToken) missing.push('GOOGLE_REFRESH_TOKEN'); + throw new Error(`Missing environment variables: ${missing.join(', ')}`); + } + + const oauth2Client = new google.auth.OAuth2(clientId, clientSecret); + oauth2Client.setCredentials({ refresh_token: refreshToken }); + return oauth2Client; +} + +function getGmail() { + return google.gmail({ version: 'v1', auth: getAuth() }); +} + +function getCalendar() { + return google.calendar({ version: 'v3', auth: getAuth() }); +} + +module.exports = { getAuth, getGmail, getCalendar }; diff --git a/src/client/api.ts b/src/client/api.ts index 317542891..e8a8c02d7 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -37,6 +37,7 @@ export interface DeviceListResponse { stderr?: string; parseError?: string; error?: string; + gatewayStatus?: 'not_running' | 'starting'; } export interface ApproveResponse { diff --git a/src/client/pages/AdminPage.tsx b/src/client/pages/AdminPage.tsx index cfe725fce..1d50eaabf 100644 --- a/src/client/pages/AdminPage.tsx +++ b/src/client/pages/AdminPage.tsx @@ -51,6 +51,7 @@ export default function AdminPage() { const [storageStatus, setStorageStatus] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [gatewayStatus, setGatewayStatus] = useState(null); const [actionInProgress, setActionInProgress] = useState(null); const [restartInProgress, setRestartInProgress] = useState(false); const [syncInProgress, setSyncInProgress] = useState(false); @@ -61,8 +62,10 @@ export default function AdminPage() { const data: DeviceListResponse = await listDevices(); setPending(data.pending || []); setPaired(data.paired || []); + setGatewayStatus(data.gatewayStatus || null); - if (data.error) { + // Only show error if it's not a gateway status message + if (data.error && !data.gatewayStatus) { setError(data.error); } else if (data.parseError) { setError(`Parse error: ${data.parseError}`); @@ -93,6 +96,13 @@ export default function AdminPage() { fetchStorageStatus(); }, [fetchDevices, fetchStorageStatus]); + // Auto-retry when gateway is starting or not running + useEffect(() => { + if (!gatewayStatus) return; + const interval = setInterval(fetchDevices, 3000); + return () => clearInterval(interval); + }, [gatewayStatus, fetchDevices]); + const handleApprove = async (requestId: string) => { setActionInProgress(requestId); try { @@ -252,6 +262,12 @@ export default function AdminPage() {

Loading devices...

+ ) : gatewayStatus ? ( +
+
+

{gatewayStatus === 'starting' ? 'Gateway is starting up...' : 'Waiting for gateway to start...'}

+

This page will refresh automatically.

+
) : ( <>
diff --git a/src/gateway/env.ts b/src/gateway/env.ts index 23dea539d..4e80d7e4d 100644 --- a/src/gateway/env.ts +++ b/src/gateway/env.ts @@ -41,6 +41,7 @@ export function buildEnvVars(env: MoltbotEnv): Record { if (env.DEV_MODE) envVars.OPENCLAW_DEV_MODE = env.DEV_MODE; if (env.TELEGRAM_BOT_TOKEN) envVars.TELEGRAM_BOT_TOKEN = env.TELEGRAM_BOT_TOKEN; if (env.TELEGRAM_DM_POLICY) envVars.TELEGRAM_DM_POLICY = env.TELEGRAM_DM_POLICY; + if (env.TELEGRAM_WEBHOOK_SECRET) envVars.TELEGRAM_WEBHOOK_SECRET = env.TELEGRAM_WEBHOOK_SECRET; if (env.DISCORD_BOT_TOKEN) envVars.DISCORD_BOT_TOKEN = env.DISCORD_BOT_TOKEN; if (env.DISCORD_DM_POLICY) envVars.DISCORD_DM_POLICY = env.DISCORD_DM_POLICY; if (env.SLACK_BOT_TOKEN) envVars.SLACK_BOT_TOKEN = env.SLACK_BOT_TOKEN; @@ -49,6 +50,11 @@ export function buildEnvVars(env: MoltbotEnv): Record { if (env.CF_ACCOUNT_ID) envVars.CF_ACCOUNT_ID = env.CF_ACCOUNT_ID; if (env.CDP_SECRET) envVars.CDP_SECRET = env.CDP_SECRET; if (env.WORKER_URL) envVars.WORKER_URL = env.WORKER_URL; + if (env.OPENCLAW_MODEL) envVars.OPENCLAW_MODEL = env.OPENCLAW_MODEL; + if (env.GOOGLE_CLIENT_ID) envVars.GOOGLE_CLIENT_ID = env.GOOGLE_CLIENT_ID; + if (env.GOOGLE_CLIENT_SECRET) envVars.GOOGLE_CLIENT_SECRET = env.GOOGLE_CLIENT_SECRET; + if (env.GOOGLE_REFRESH_TOKEN) envVars.GOOGLE_REFRESH_TOKEN = env.GOOGLE_REFRESH_TOKEN; + if (env.FRESH_START) envVars.FRESH_START = env.FRESH_START; return envVars; } diff --git a/src/gateway/index.ts b/src/gateway/index.ts index 96c7862d0..e6977f929 100644 --- a/src/gateway/index.ts +++ b/src/gateway/index.ts @@ -1,5 +1,5 @@ export { buildEnvVars } from './env'; export { mountR2Storage } from './r2'; -export { findExistingMoltbotProcess, ensureMoltbotGateway } from './process'; +export { findExistingMoltbotProcess, ensureMoltbotGateway, killAllGatewayProcesses } from './process'; export { syncToR2 } from './sync'; -export { waitForProcess } from './utils'; +export { waitForProcess, runCommandWithCleanup } from './utils'; diff --git a/src/gateway/process.test.ts b/src/gateway/process.test.ts index b9d71c42a..1c9ec2db3 100644 --- a/src/gateway/process.test.ts +++ b/src/gateway/process.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, vi } from 'vitest'; -import { findExistingMoltbotProcess } from './process'; +import { findExistingMoltbotProcess, killAllGatewayProcesses } from './process'; import type { Sandbox, Process } from '@cloudflare/sandbox'; import { createMockSandbox } from '../test-utils'; @@ -143,3 +143,67 @@ describe('findExistingMoltbotProcess', () => { expect(result).toBeNull(); }); }); + +describe('killAllGatewayProcesses', () => { + it('returns 0 when no gateway processes exist', async () => { + const { sandbox, listProcessesMock } = createMockSandbox(); + listProcessesMock.mockResolvedValue([]); + + const result = await killAllGatewayProcesses(sandbox); + expect(result).toBe(0); + }); + + it('kills all running gateway processes', async () => { + const killMock1 = vi.fn().mockResolvedValue(undefined); + const killMock2 = vi.fn().mockResolvedValue(undefined); + const processes = [ + createFullMockProcess({ id: 'gw-1', command: 'openclaw gateway --port 18789', status: 'running', kill: killMock1 }), + createFullMockProcess({ id: 'gw-2', command: '/usr/local/bin/start-openclaw.sh', status: 'starting', kill: killMock2 }), + createFullMockProcess({ id: 'cli-1', command: 'openclaw devices list', status: 'running' }), + ]; + const { sandbox, listProcessesMock } = createMockSandbox(); + listProcessesMock.mockResolvedValue(processes); + + const result = await killAllGatewayProcesses(sandbox); + expect(result).toBe(2); + expect(killMock1).toHaveBeenCalled(); + expect(killMock2).toHaveBeenCalled(); + }); + + it('skips completed gateway processes', async () => { + const processes = [ + createFullMockProcess({ id: 'gw-1', command: 'openclaw gateway', status: 'completed' }), + createFullMockProcess({ id: 'gw-2', command: 'openclaw gateway', status: 'failed' }), + ]; + const { sandbox, listProcessesMock } = createMockSandbox(); + listProcessesMock.mockResolvedValue(processes); + + const result = await killAllGatewayProcesses(sandbox); + expect(result).toBe(0); + }); + + it('continues killing even if one kill fails', async () => { + const killMock1 = vi.fn().mockRejectedValue(new Error('kill failed')); + const killMock2 = vi.fn().mockResolvedValue(undefined); + const processes = [ + createFullMockProcess({ id: 'gw-1', command: 'openclaw gateway', status: 'running', kill: killMock1 }), + createFullMockProcess({ id: 'gw-2', command: 'openclaw gateway', status: 'running', kill: killMock2 }), + ]; + const { sandbox, listProcessesMock } = createMockSandbox(); + listProcessesMock.mockResolvedValue(processes); + + const result = await killAllGatewayProcesses(sandbox); + expect(result).toBe(1); + expect(killMock1).toHaveBeenCalled(); + expect(killMock2).toHaveBeenCalled(); + }); + + it('handles listProcesses errors gracefully', async () => { + const sandbox = { + listProcesses: vi.fn().mockRejectedValue(new Error('Network error')), + } as unknown as Sandbox; + + const result = await killAllGatewayProcesses(sandbox); + expect(result).toBe(0); + }); +}); diff --git a/src/gateway/process.ts b/src/gateway/process.ts index bfdf077f6..cadbc6220 100644 --- a/src/gateway/process.ts +++ b/src/gateway/process.ts @@ -4,6 +4,24 @@ import { MOLTBOT_PORT, STARTUP_TIMEOUT_MS } from '../config'; import { buildEnvVars } from './env'; import { mountR2Storage } from './r2'; +/** + * Check if a process command matches a gateway process (not a CLI command). + */ +function isGatewayCommand(command: string): boolean { + const isGateway = + command.includes('start-openclaw.sh') || + command.includes('openclaw gateway') || + command.includes('start-moltbot.sh') || + command.includes('clawdbot gateway'); + const isCli = + command.includes('openclaw devices') || + command.includes('openclaw --version') || + command.includes('openclaw onboard') || + command.includes('clawdbot devices') || + command.includes('clawdbot --version'); + return isGateway && !isCli; +} + /** * Find an existing OpenClaw gateway process * @@ -14,22 +32,7 @@ export async function findExistingMoltbotProcess(sandbox: Sandbox): Promise { + let killed = 0; + try { + const processes = await sandbox.listProcesses(); + for (const proc of processes) { + if (isGatewayCommand(proc.command) && (proc.status === 'running' || proc.status === 'starting')) { + try { + await proc.kill(); + killed++; + console.log(`[cleanup] Killed gateway process ${proc.id} (${proc.command})`); + } catch (e) { + console.log(`[cleanup] Failed to kill process ${proc.id}:`, e); + } + } + } + } catch (e) { + console.log('[cleanup] Could not list processes:', e); + } + return killed; +} + /** * Ensure the OpenClaw gateway is running * @@ -75,19 +107,52 @@ export async function ensureMoltbotGateway(sandbox: Sandbox, env: MoltbotEnv): P console.log('Waiting for gateway on port', MOLTBOT_PORT, 'timeout:', STARTUP_TIMEOUT_MS); await existingProcess.waitForPort(MOLTBOT_PORT, { mode: 'tcp', timeout: STARTUP_TIMEOUT_MS }); console.log('Gateway is reachable'); - return existingProcess; + + // Verify it's the real gateway, not the default Bun server + try { + const healthResp = await sandbox.containerFetch( + new Request(`http://localhost:${MOLTBOT_PORT}/`), + MOLTBOT_PORT, + ); + const body = await healthResp.text(); + const snippet = body.slice(0, 200); + console.log('[Gateway] Health check response:', healthResp.status, 'body:', snippet); + if (body.includes('Bun') && !body.includes('openclaw')) { + console.error('[Gateway] Default Bun server detected on existing process — killing all and restarting'); + // Fall through to cleanup + restart below + } else { + return existingProcess; + } + } catch (healthErr) { + // Health check fetch failed but port is open — assume gateway is OK + console.log('[Gateway] Health check failed (non-fatal), assuming gateway OK:', healthErr); + return existingProcess; + } // eslint-disable-next-line no-unused-vars } catch (_e) { - // Timeout waiting for port - process is likely dead or stuck, kill and restart + // Timeout waiting for port - process is likely dead or stuck console.log('Existing process not reachable after full timeout, killing and restarting...'); - try { - await existingProcess.kill(); - } catch (killError) { - console.log('Failed to kill process:', killError); - } } + + // Kill the existing process (and any other gateway processes) before restarting + try { + await existingProcess.kill(); + } catch (killError) { + console.log('Failed to kill process:', killError); + } + } + + // Clean up before starting a new gateway + try { + const cleaned = await sandbox.cleanupCompletedProcesses(); + if (cleaned > 0) console.log(`[cleanup] Removed ${cleaned} completed processes`); + } catch (e) { + console.log('[cleanup] cleanupCompletedProcesses failed:', e); } + const killedCount = await killAllGatewayProcesses(sandbox); + if (killedCount > 0) console.log(`[cleanup] Killed ${killedCount} zombie gateway processes`); + // Start a new OpenClaw gateway console.log('Starting new OpenClaw gateway...'); const envVars = buildEnvVars(env); @@ -131,8 +196,25 @@ export async function ensureMoltbotGateway(sandbox: Sandbox, env: MoltbotEnv): P } } - // Verify gateway is actually responding + // Verify gateway is actually responding (not the default Bun server) console.log('[Gateway] Verifying gateway health...'); + try { + const healthResp = await sandbox.containerFetch( + new Request(`http://localhost:${MOLTBOT_PORT}/`), + MOLTBOT_PORT, + ); + const body = await healthResp.text(); + const snippet = body.slice(0, 200); + console.log('[Gateway] New process health check:', healthResp.status, 'body:', snippet); + if (body.includes('Bun') && !body.includes('openclaw')) { + console.error('[Gateway] Default Bun server detected instead of OpenClaw gateway'); + throw new Error('Container is serving default Bun response instead of OpenClaw gateway'); + } + } catch (e) { + if (e instanceof Error && e.message.includes('default Bun response')) throw e; + // Non-fatal: containerFetch may fail for non-HTTP endpoints, gateway is still up + console.log('[Gateway] Health check fetch failed (non-fatal):', e); + } return process; } diff --git a/src/gateway/r2.ts b/src/gateway/r2.ts index c95efc40b..6039535b4 100644 --- a/src/gateway/r2.ts +++ b/src/gateway/r2.ts @@ -1,24 +1,21 @@ import type { Sandbox } from '@cloudflare/sandbox'; import type { MoltbotEnv } from '../types'; import { R2_MOUNT_PATH, getR2BucketName } from '../config'; +import { runCommandWithCleanup } from './utils'; /** * Check if R2 is already mounted by looking at the mount table */ async function isR2Mounted(sandbox: Sandbox): Promise { try { - const proc = await sandbox.startProcess(`mount | grep "s3fs on ${R2_MOUNT_PATH}"`); - // Wait for the command to complete - let attempts = 0; - while (proc.status === 'running' && attempts < 10) { - // eslint-disable-next-line no-await-in-loop -- intentional sequential polling - await new Promise((r) => setTimeout(r, 200)); - attempts++; - } - const logs = await proc.getLogs(); + const result = await runCommandWithCleanup( + sandbox, + `mount | grep "s3fs on ${R2_MOUNT_PATH}"`, + 5000, + ); // If stdout has content, the mount exists - const mounted = !!(logs.stdout && logs.stdout.includes('s3fs')); - console.log('isR2Mounted check:', mounted, 'stdout:', logs.stdout?.slice(0, 100)); + const mounted = !!(result.stdout && result.stdout.includes('s3fs')); + console.log('isR2Mounted check:', mounted, 'stdout:', result.stdout.slice(0, 100)); return mounted; } catch (err) { console.log('isR2Mounted error:', err); diff --git a/src/gateway/sync.test.ts b/src/gateway/sync.test.ts index f062ffed7..8ffbccfd1 100644 --- a/src/gateway/sync.test.ts +++ b/src/gateway/sync.test.ts @@ -43,8 +43,8 @@ describe('syncToR2', () => { const { sandbox, startProcessMock } = createMockSandbox(); startProcessMock .mockResolvedValueOnce(createMockProcess('s3fs on /data/moltbot type fuse.s3fs\n')) - .mockResolvedValueOnce(createMockProcess('', { exitCode: 1 })) // No openclaw.json - .mockResolvedValueOnce(createMockProcess('', { exitCode: 1 })); // No clawdbot.json either + .mockResolvedValueOnce(createMockProcess('NOTFOUND')) // No openclaw.json + .mockResolvedValueOnce(createMockProcess('NOTFOUND')); // No clawdbot.json either const env = createMockEnvWithR2(); @@ -63,7 +63,7 @@ describe('syncToR2', () => { // Calls: mount check, check openclaw.json, rsync, cat timestamp startProcessMock .mockResolvedValueOnce(createMockProcess('s3fs on /data/moltbot type fuse.s3fs\n')) - .mockResolvedValueOnce(createMockProcess('ok')) + .mockResolvedValueOnce(createMockProcess('FOUND')) .mockResolvedValueOnce(createMockProcess('')) .mockResolvedValueOnce(createMockProcess(timestamp)); @@ -81,7 +81,7 @@ describe('syncToR2', () => { // Calls: mount check, check openclaw.json, rsync (fails), cat timestamp (empty) startProcessMock .mockResolvedValueOnce(createMockProcess('s3fs on /data/moltbot type fuse.s3fs\n')) - .mockResolvedValueOnce(createMockProcess('ok')) + .mockResolvedValueOnce(createMockProcess('FOUND')) .mockResolvedValueOnce(createMockProcess('', { exitCode: 1 })) .mockResolvedValueOnce(createMockProcess('')); @@ -99,7 +99,7 @@ describe('syncToR2', () => { startProcessMock .mockResolvedValueOnce(createMockProcess('s3fs on /data/moltbot type fuse.s3fs\n')) - .mockResolvedValueOnce(createMockProcess('ok')) + .mockResolvedValueOnce(createMockProcess('FOUND')) .mockResolvedValueOnce(createMockProcess('')) .mockResolvedValueOnce(createMockProcess(timestamp)); @@ -107,13 +107,13 @@ describe('syncToR2', () => { await syncToR2(sandbox, env); - // Third call should be rsync to openclaw/ R2 prefix - const rsyncCall = startProcessMock.mock.calls[2][0]; - expect(rsyncCall).toContain('rsync'); - expect(rsyncCall).toContain('--no-times'); - expect(rsyncCall).toContain('--delete'); - expect(rsyncCall).toContain('/root/.openclaw/'); - expect(rsyncCall).toContain('/data/moltbot/openclaw/'); + // Third call should be the tar/cp sync command + const syncCall = startProcessMock.mock.calls[2][0]; + expect(syncCall).toContain('tar czf'); + expect(syncCall).toContain('openclaw-config.tar.gz'); + expect(syncCall).toContain('workspace.tar.gz'); + expect(syncCall).toContain('skills.tar.gz'); + expect(syncCall).toContain('.last-sync'); }); }); }); diff --git a/src/gateway/sync.ts b/src/gateway/sync.ts index 63808c471..1a38f83c7 100644 --- a/src/gateway/sync.ts +++ b/src/gateway/sync.ts @@ -2,7 +2,7 @@ import type { Sandbox } from '@cloudflare/sandbox'; import type { MoltbotEnv } from '../types'; import { R2_MOUNT_PATH } from '../config'; import { mountR2Storage } from './r2'; -import { waitForProcess } from './utils'; +import { runCommandWithCleanup } from './utils'; export interface SyncResult { success: boolean; @@ -17,13 +17,14 @@ export interface SyncResult { * This function: * 1. Mounts R2 if not already mounted * 2. Verifies source has critical files (prevents overwriting good backup with empty data) - * 3. Runs rsync to copy config, workspace, and skills to R2 - * 4. Writes a timestamp file for tracking + * 3. Creates tar archives of config, workspace, and skills + * 4. Copies each archive to R2 (3 file writes instead of hundreds via rsync) + * 5. Writes a timestamp file for tracking * - * Syncs three directories: - * - Config: /root/.openclaw/ (or /root/.clawdbot/) → R2:/openclaw/ - * - Workspace: /root/clawd/ → R2:/workspace/ (IDENTITY.md, MEMORY.md, memory/, assets/) - * - Skills: /root/clawd/skills/ → R2:/skills/ + * Creates three tar archives in R2: + * - openclaw-config.tar.gz: /root/.openclaw/ (excluding workspace) + * - workspace.tar.gz: /root/.openclaw/workspace/ (IDENTITY.md, MEMORY.md, etc.) + * - skills.tar.gz: /root/clawd/skills/ * * @param sandbox - The sandbox instance * @param env - Worker environment bindings @@ -42,16 +43,23 @@ export async function syncToR2(sandbox: Sandbox, env: MoltbotEnv): Promise/dev/null && echo FOUND || echo NOTFOUND', + 5000, + ); + const newFound = checkNew.stdout.includes('FOUND') && !checkNew.stdout.includes('NOTFOUND'); + if (!newFound) { + const checkLegacy = await runCommandWithCleanup( + sandbox, + 'ls /root/.clawdbot/clawdbot.json 2>/dev/null && echo FOUND || echo NOTFOUND', + 5000, + ); + if (checkLegacy.stdout.includes('FOUND') && !checkLegacy.stdout.includes('NOTFOUND')) { configDir = '/root/.clawdbot'; } else { return { @@ -69,28 +77,42 @@ export async function syncToR2(sandbox: Sandbox, env: MoltbotEnv): Promise ${R2_MOUNT_PATH}/.last-sync`; + // Use tar archives instead of rsync — s3fs does one HTTP request per file, + // so rsync with many files is extremely slow. Tar creates a single archive + // locally then writes one file to R2 (3 writes total instead of hundreds). + const syncParts = [ + `rm -f ${R2_MOUNT_PATH}/.last-sync`, + `mkdir -p /root/.openclaw/workspace /root/clawd/skills`, + // Config: tar locally, copy single file to R2 + `tar czf /tmp/openclaw-config.tar.gz -C ${configDir} --exclude='workspace' --exclude='.git' --exclude='*.lock' --exclude='*.log' --exclude='*.tmp' .`, + `cp /tmp/openclaw-config.tar.gz ${R2_MOUNT_PATH}/openclaw-config.tar.gz`, + // Workspace: tar locally, copy single file to R2 + `tar czf /tmp/openclaw-workspace.tar.gz -C /root/.openclaw/workspace --exclude='.git' .`, + `cp /tmp/openclaw-workspace.tar.gz ${R2_MOUNT_PATH}/workspace.tar.gz`, + // Skills: tar locally, copy single file to R2 + `tar czf /tmp/openclaw-skills.tar.gz -C /root/clawd/skills --exclude='.git' .`, + `cp /tmp/openclaw-skills.tar.gz ${R2_MOUNT_PATH}/skills.tar.gz`, + // Cleanup temp files and write timestamp + `rm -f /tmp/openclaw-config.tar.gz /tmp/openclaw-workspace.tar.gz /tmp/openclaw-skills.tar.gz`, + `date -Iseconds > ${R2_MOUNT_PATH}/.last-sync`, + ]; + const syncCmd = syncParts.join(' && '); try { - const proc = await sandbox.startProcess(syncCmd); - await waitForProcess(proc, 30000); // 30 second timeout for sync + const syncResult = await runCommandWithCleanup(sandbox, syncCmd, 60000); // 60s timeout - // Check for success by reading the timestamp file - const timestampProc = await sandbox.startProcess(`cat ${R2_MOUNT_PATH}/.last-sync`); - await waitForProcess(timestampProc, 5000); - const timestampLogs = await timestampProc.getLogs(); - const lastSync = timestampLogs.stdout?.trim(); + // Check for success by reading the NEW timestamp file + // If the && chain broke, .last-sync won't exist (we deleted it above) + const timestampResult = await runCommandWithCleanup(sandbox, `cat ${R2_MOUNT_PATH}/.last-sync 2>/dev/null || echo MISSING`, 5000); + const lastSync = timestampResult.stdout.trim(); - if (lastSync && lastSync.match(/^\d{4}-\d{2}-\d{2}/)) { + if (lastSync && lastSync.match(/^\d{4}-\d{2}-\d{2}/) && !lastSync.includes('MISSING')) { return { success: true, lastSync }; } else { - const logs = await proc.getLogs(); return { success: false, error: 'Sync failed', - details: logs.stderr || logs.stdout || 'No timestamp file created', + details: syncResult.stderr || syncResult.stdout || 'Tar/copy chain failed — timestamp not written', }; } } catch (err) { diff --git a/src/gateway/utils.ts b/src/gateway/utils.ts index 3e4d63e96..82ea76289 100644 --- a/src/gateway/utils.ts +++ b/src/gateway/utils.ts @@ -1,3 +1,5 @@ +import type { Sandbox, Process } from '@cloudflare/sandbox'; + /** * Shared utilities for gateway operations */ @@ -22,3 +24,48 @@ export async function waitForProcess( attempts++; } } + +/** + * Run a short-lived command and automatically clean it up when done. + * + * This helper: + * 1. Starts the process + * 2. Waits for it to complete + * 3. Gets the logs + * 4. Kills the process (using try-finally to ensure cleanup even on error) + * + * Use this for all CLI commands and short-lived processes to prevent zombie process accumulation. + * DO NOT use this for the long-running gateway process. + * + * @param sandbox - The sandbox instance + * @param command - Command to run + * @param timeoutMs - Maximum time to wait for completion (default 30s) + * @returns Process logs (stdout and stderr) + */ +export async function runCommandWithCleanup( + sandbox: Sandbox, + command: string, + timeoutMs: number = 30000, +): Promise<{ stdout: string; stderr: string; exitCode: number | undefined; process: Process }> { + const proc = await sandbox.startProcess(command); + + try { + await waitForProcess(proc, timeoutMs); + const logs = await proc.getLogs(); + + return { + stdout: logs.stdout || '', + stderr: logs.stderr || '', + exitCode: proc.exitCode, + process: proc, + }; + } finally { + // Always kill the process, even if there was an error + try { + await proc.kill(); + } catch (killErr) { + // Ignore kill errors (process may have already exited) + console.log('[cleanup] Failed to kill process (may have already exited):', proc.id, killErr); + } + } +} diff --git a/src/index.ts b/src/index.ts index 53b06d3d2..8e0654324 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,7 +21,7 @@ */ import { Hono } from 'hono'; -import { getSandbox, Sandbox, type SandboxOptions } from '@cloudflare/sandbox'; +import { getSandbox, Sandbox as BaseSandbox, type SandboxOptions } from '@cloudflare/sandbox'; import type { AppEnv, MoltbotEnv } from './types'; import { MOLTBOT_PORT } from './config'; @@ -29,9 +29,22 @@ import { createAccessMiddleware } from './auth'; import { ensureMoltbotGateway, findExistingMoltbotProcess, syncToR2 } from './gateway'; import { publicRoutes, api, adminUi, debug, cdp } from './routes'; import { redactSensitiveParams } from './utils/logging'; +import { timingSafeEqual } from './utils/timing'; import loadingPageHtml from './assets/loading.html'; import configErrorHtml from './assets/config-error.html'; +/** + * Custom Sandbox class that explicitly enables outbound internet access. + * Required for the OpenClaw gateway to reach external APIs (Telegram, Discord, etc.). + * + * The 1-minute cron job handles starting/restarting the gateway. + * We do NOT use onStart() because ensureMoltbotGateway() waits for port readiness, + * which exceeds the blockConcurrencyWhile() timeout and causes a DO reset loop. + */ +class Sandbox extends BaseSandbox { + enableInternet = true; +} + /** * Transform error messages from the gateway to be more user-friendly. */ @@ -126,16 +139,13 @@ app.use('*', async (c, next) => { const url = new URL(c.req.url); const redactedSearch = redactSensitiveParams(url); console.log(`[REQ] ${c.req.method} ${url.pathname}${redactedSearch}`); - console.log(`[REQ] Has ANTHROPIC_API_KEY: ${!!c.env.ANTHROPIC_API_KEY}`); - console.log(`[REQ] DEV_MODE: ${c.env.DEV_MODE}`); - console.log(`[REQ] DEBUG_ROUTES: ${c.env.DEBUG_ROUTES}`); await next(); }); // Middleware: Initialize sandbox for all requests app.use('*', async (c, next) => { const options = buildSandboxOptions(c.env); - const sandbox = getSandbox(c.env.Sandbox, 'moltbot', options); + const sandbox = getSandbox(c.env.Sandbox, 'moltbot-v3', options); c.set('sandbox', sandbox); await next(); }); @@ -151,6 +161,58 @@ app.route('/', publicRoutes); // Mount CDP routes (uses shared secret auth via query param, not CF Access) app.route('/cdp', cdp); +// Middleware: Proxy WebSocket upgrades directly to the gateway, bypassing CF Access. +// CF Access is browser-based (302 redirects) and breaks non-browser WebSocket clients +// like `openclaw node run`. The gateway handles its own auth (token + device pairing), +// so CF Access is not needed for WebSocket connections. +app.use('*', async (c, next) => { + // Skip for /cdp paths — the CDP route has its own WebSocket handling and auth + const reqUrl = new URL(c.req.url); + if (reqUrl.pathname.startsWith('/cdp')) return next(); + + // Cloudflare's HTTP/2 edge strips the Upgrade header, so also check Sec-WebSocket-Key + const isWebSocket = + c.req.header('Upgrade')?.toLowerCase() === 'websocket' || + c.req.header('Sec-WebSocket-Key') !== undefined; + if (!isWebSocket) return next(); + + const sandbox = c.get('sandbox'); + try { + await ensureMoltbotGateway(sandbox, c.env); + } catch (error) { + console.error('[WS-PUBLIC] Failed to start gateway:', error); + return c.json({ error: 'Gateway failed to start' }, 503); + } + + // Build WebSocket request with token and restored Upgrade header. + // Cloudflare's HTTP/2 edge strips the Upgrade header, but the Sandbox SDK + // and the container gateway need it to handle the request as a WebSocket. + const url = new URL(c.req.url); + + // Rewrite /ws path to / for the container gateway. + // Node host and other machine clients connect to /ws (which has a CF Access + // bypass policy), and the gateway expects connections on /. + if (url.pathname === '/ws' || url.pathname === '/ws/') { + url.pathname = '/'; + } + + if (c.env.MOLTBOT_GATEWAY_TOKEN && !url.searchParams.has('token')) { + url.searchParams.set('token', c.env.MOLTBOT_GATEWAY_TOKEN); + } + const headers = new Headers(c.req.raw.headers); + if (!headers.has('Upgrade')) { + headers.set('Upgrade', 'websocket'); + } + if (!headers.has('Connection') || !headers.get('Connection')?.toLowerCase().includes('upgrade')) { + headers.set('Connection', 'Upgrade'); + } + const wsRequest = new Request(url.toString(), { headers }); + + console.log('[WS-PUBLIC] Proxying WebSocket:', url.pathname); + const response = await sandbox.wsConnect(wsRequest, MOLTBOT_PORT); + return response; +}); + // ============================================================================= // PROTECTED ROUTES: Cloudflare Access authentication required // ============================================================================= @@ -195,8 +257,36 @@ app.use('*', async (c, next) => { return next(); }); +// Middleware: Allow gateway token as alternative to CF Access +// The ?token= param authenticates directly with the gateway token, +// bypassing CF Access JWT requirement (used for direct URL access) +app.use('*', async (c, next) => { + const url = new URL(c.req.url); + const providedToken = url.searchParams.get('token'); + const expectedToken = c.env.MOLTBOT_GATEWAY_TOKEN; + + if (providedToken && expectedToken && timingSafeEqual(providedToken, expectedToken)) { + c.set('accessUser', { email: 'token-auth@local', name: 'Token Auth' }); + return next(); + } + + return next(); +}); + // Middleware: Cloudflare Access authentication for protected routes +// Skip for WebSocket upgrades — the gateway handles its own auth (token + device pairing) app.use('*', async (c, next) => { + // Skip if already authenticated (e.g., by gateway token) + if (c.get('accessUser')) return next(); + + const isWebSocket = + c.req.header('Upgrade')?.toLowerCase() === 'websocket' || + c.req.header('Sec-WebSocket-Key') !== undefined; + if (isWebSocket) { + console.log('[AUTH] Skipping CF Access for WebSocket request'); + return next(); + } + // Determine response type based on Accept header const acceptsHtml = c.req.header('Accept')?.includes('text/html'); const middleware = createAccessMiddleware({ @@ -233,50 +323,38 @@ app.all('*', async (c) => { console.log('[PROXY] Handling request:', url.pathname); - // Check if gateway is already running - const existingProcess = await findExistingMoltbotProcess(sandbox); - const isGatewayReady = existingProcess !== null && existingProcess.status === 'running'; - - // For browser requests (non-WebSocket, non-API), show loading page if gateway isn't ready - const isWebSocketRequest = request.headers.get('Upgrade')?.toLowerCase() === 'websocket'; + const isWebSocketRequest = + request.headers.get('Upgrade')?.toLowerCase() === 'websocket' || + request.headers.get('Sec-WebSocket-Key') !== null; const acceptsHtml = request.headers.get('Accept')?.includes('text/html'); - if (!isGatewayReady && !isWebSocketRequest && acceptsHtml) { - console.log('[PROXY] Gateway not ready, serving loading page'); - - // Start the gateway in the background (don't await) - c.executionCtx.waitUntil( - ensureMoltbotGateway(sandbox, c.env).catch((err: Error) => { - console.error('[PROXY] Background gateway start failed:', err); - }), - ); - - // Return the loading page immediately - return c.html(loadingPageHtml); - } - - // Ensure moltbot is running (this will wait for startup) - try { - await ensureMoltbotGateway(sandbox, c.env); - } catch (error) { - console.error('[PROXY] Failed to start Moltbot:', error); - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - - let hint = 'Check worker logs with: wrangler tail'; - if (!c.env.ANTHROPIC_API_KEY) { - hint = 'ANTHROPIC_API_KEY is not set. Run: wrangler secret put ANTHROPIC_API_KEY'; - } else if (errorMessage.includes('heap out of memory') || errorMessage.includes('OOM')) { - hint = 'Gateway ran out of memory. Try again or check for memory leaks.'; + // Proxy non-WebSocket HTTP requests directly to the container gateway + if (!isWebSocketRequest) { + const containerParams = new URLSearchParams(url.search); + containerParams.delete('token'); + // Inject gateway token — the gateway requires it for auth + if (c.env.MOLTBOT_GATEWAY_TOKEN) { + containerParams.set('token', c.env.MOLTBOT_GATEWAY_TOKEN); + } + const containerSearch = containerParams.toString() ? `?${containerParams.toString()}` : ''; + const containerUrl = new URL(url.pathname + containerSearch, `http://localhost:${MOLTBOT_PORT}`); + console.log('[HTTP] Proxying:', containerUrl.pathname + containerUrl.search); + + try { + const containerRequest = new Request(containerUrl.toString()); + const httpResponse = await sandbox.containerFetch(containerRequest, MOLTBOT_PORT); + console.log('[HTTP] Response status:', httpResponse.status); + return new Response(httpResponse.body, { + status: httpResponse.status, + headers: new Headers(httpResponse.headers), + }); + } catch (e) { + console.error('[HTTP] containerFetch failed:', e); + if (acceptsHtml) { + return c.html(loadingPageHtml); + } + return c.json({ error: 'Gateway not responding', details: String(e) }, 503); } - - return c.json( - { - error: 'Moltbot gateway failed to start', - details: errorMessage, - hint, - }, - 503, - ); } // Proxy to Moltbot with WebSocket message interception @@ -427,21 +505,6 @@ app.all('*', async (c) => { webSocket: clientWs, }); } - - console.log('[HTTP] Proxying:', url.pathname + url.search); - const httpResponse = await sandbox.containerFetch(request, MOLTBOT_PORT); - console.log('[HTTP] Response status:', httpResponse.status); - - // Add debug header to verify worker handled the request - const newHeaders = new Headers(httpResponse.headers); - newHeaders.set('X-Worker-Debug', 'proxy-to-moltbot'); - newHeaders.set('X-Debug-Path', url.pathname); - - return new Response(httpResponse.body, { - status: httpResponse.status, - statusText: httpResponse.statusText, - headers: newHeaders, - }); }); /** @@ -454,12 +517,70 @@ async function scheduled( _ctx: ExecutionContext, ): Promise { const options = buildSandboxOptions(env); - const sandbox = getSandbox(env.Sandbox, 'moltbot', options); + const sandbox = getSandbox(env.Sandbox, 'moltbot-v3', options); + + // Monitor total process count for leaks + let processCount = 0; + try { + const allProcesses = await sandbox.listProcesses(); + processCount = allProcesses.length; + console.log(`[cron] Total processes in sandbox: ${processCount}`); + + // Warn if process count is growing suspiciously high + if (processCount > 50) { + console.warn(`[cron] High process count detected: ${processCount} processes`); + } + if (processCount > 100) { + console.error(`[cron] CRITICAL: Very high process count: ${processCount} processes - possible leak!`); + } + } catch (e) { + console.log('[cron] Failed to list processes:', e); + } + + // Clean up completed/exited processes to prevent accumulation over time + try { + const cleaned = await sandbox.cleanupCompletedProcesses(); + if (cleaned > 0) { + console.log(`[cron] Cleaned up ${cleaned} completed processes`); + // Log new count after cleanup + const afterCleanup = await sandbox.listProcesses(); + console.log(`[cron] Process count after cleanup: ${afterCleanup.length}`); + } + } catch (e) { + console.log('[cron] cleanupCompletedProcesses failed:', e); + } const gatewayProcess = await findExistingMoltbotProcess(sandbox); - if (!gatewayProcess) { - console.log('[cron] Gateway not running yet, skipping sync'); - return; + let needsRestart = !gatewayProcess; + + // Health check: verify existing process isn't serving the default Bun server + if (gatewayProcess) { + try { + const healthResp = await sandbox.containerFetch( + new Request(`http://localhost:${MOLTBOT_PORT}/`), + MOLTBOT_PORT, + ); + const body = await healthResp.text(); + if (body.includes('Bun') && !body.includes('openclaw')) { + console.error('[cron] Default Bun server detected — gateway is not actually running. Body:', body.slice(0, 200)); + needsRestart = true; + } + } catch { + // containerFetch failed — port may not be ready, let ensureMoltbotGateway handle it + console.log('[cron] Health check failed, will attempt restart'); + needsRestart = true; + } + } + + if (needsRestart) { + console.log('[cron] Gateway not running or unhealthy, restarting...'); + try { + await ensureMoltbotGateway(sandbox, env); + console.log('[cron] Gateway restarted successfully'); + } catch (error) { + console.error('[cron] Gateway restart failed:', error); + } + // Fall through to sync — don't skip backup just because gateway was restarted } console.log('[cron] Starting backup sync to R2...'); @@ -473,6 +594,21 @@ async function scheduled( } export default { - fetch: app.fetch, + fetch(request: Request, env: MoltbotEnv, ctx: ExecutionContext) { + // Cloudflare's HTTP/2 edge strips the Upgrade header from WebSocket requests. + // The Workers runtime requires this header to return a WebSocket response (101). + // Restore it using the Sec-WebSocket-Key header as the reliable WebSocket indicator. + if (!request.headers.has('Upgrade') && request.headers.has('Sec-WebSocket-Key')) { + const headers = new Headers(request.headers); + headers.set('Upgrade', 'websocket'); + headers.set('Connection', 'Upgrade'); + request = new Request(request.url, { + method: request.method, + headers, + body: request.body, + }); + } + return app.fetch(request, env, ctx); + }, scheduled, }; diff --git a/src/routes/admin-ui.ts b/src/routes/admin-ui.ts index 6fca5e61c..82b4bd6fc 100644 --- a/src/routes/admin-ui.ts +++ b/src/routes/admin-ui.ts @@ -13,7 +13,7 @@ const adminUi = new Hono(); // Serve index.html for all admin routes (SPA) adminUi.get('*', async (c) => { const url = new URL(c.req.url); - return c.env.ASSETS.fetch(new Request(new URL('/index.html', url.origin).toString())); + return c.env.ASSETS.fetch(new Request(new URL('/_admin-app.html', url.origin).toString())); }); export { adminUi }; diff --git a/src/routes/api.ts b/src/routes/api.ts index bbc04feb7..c5ff5c0f3 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -6,9 +6,9 @@ import { findExistingMoltbotProcess, mountR2Storage, syncToR2, - waitForProcess, + runCommandWithCleanup, } from '../gateway'; -import { R2_MOUNT_PATH } from '../config'; +import { R2_MOUNT_PATH, MOLTBOT_PORT } from '../config'; // CLI commands can take 10-15 seconds to complete due to WebSocket connection overhead const CLI_TIMEOUT_MS = 20000; @@ -29,31 +29,51 @@ const adminApi = new Hono(); // Middleware: Verify Cloudflare Access JWT for all admin routes adminApi.use('*', createAccessMiddleware({ type: 'json' })); +// Quick port check timeout - fail fast instead of blocking for 3 minutes +const QUICK_CHECK_MS = 5000; + // GET /api/admin/devices - List pending and paired devices adminApi.get('/devices', async (c) => { const sandbox = c.get('sandbox'); + // Quick check: is the gateway already reachable? (5s instead of 3min) + // The cron job (every minute) handles starting the gateway. + const existingProcess = await findExistingMoltbotProcess(sandbox); + if (!existingProcess) { + return c.json({ + pending: [], + paired: [], + gatewayStatus: 'not_running', + error: 'Gateway is not running. It will start automatically within a minute.', + }); + } + try { - // Ensure moltbot is running first - await ensureMoltbotGateway(sandbox, c.env); + await existingProcess.waitForPort(MOLTBOT_PORT, { mode: 'tcp', timeout: QUICK_CHECK_MS }); + } catch { + return c.json({ + pending: [], + paired: [], + gatewayStatus: 'starting', + error: 'Gateway is starting up. Please wait...', + }); + } + try { // Run OpenClaw CLI to list devices // Must specify --url and --token (OpenClaw v2026.2.3 requires explicit credentials with --url) const token = c.env.MOLTBOT_GATEWAY_TOKEN; const tokenArg = token ? ` --token ${token}` : ''; - const proc = await sandbox.startProcess( + const result = await runCommandWithCleanup( + sandbox, `openclaw devices list --json --url ws://localhost:18789${tokenArg}`, + CLI_TIMEOUT_MS, ); - await waitForProcess(proc, CLI_TIMEOUT_MS); - - const logs = await proc.getLogs(); - const stdout = logs.stdout || ''; - const stderr = logs.stderr || ''; // Try to parse JSON output try { // Find JSON in output (may have other log lines) - const jsonMatch = stdout.match(/\{[\s\S]*\}/); + const jsonMatch = result.stdout.match(/\{[\s\S]*\}/); if (jsonMatch) { const data = JSON.parse(jsonMatch[0]); return c.json(data); @@ -63,15 +83,15 @@ adminApi.get('/devices', async (c) => { return c.json({ pending: [], paired: [], - raw: stdout, - stderr, + raw: result.stdout, + stderr: result.stderr, }); } catch { return c.json({ pending: [], paired: [], - raw: stdout, - stderr, + raw: result.stdout, + stderr: result.stderr, parseError: 'Failed to parse CLI output', }); } @@ -97,24 +117,21 @@ adminApi.post('/devices/:requestId/approve', async (c) => { // Run OpenClaw CLI to approve the device const token = c.env.MOLTBOT_GATEWAY_TOKEN; const tokenArg = token ? ` --token ${token}` : ''; - const proc = await sandbox.startProcess( + const result = await runCommandWithCleanup( + sandbox, `openclaw devices approve ${requestId} --url ws://localhost:18789${tokenArg}`, + CLI_TIMEOUT_MS, ); - await waitForProcess(proc, CLI_TIMEOUT_MS); - - const logs = await proc.getLogs(); - const stdout = logs.stdout || ''; - const stderr = logs.stderr || ''; // Check for success indicators (case-insensitive, CLI outputs "Approved ...") - const success = stdout.toLowerCase().includes('approved') || proc.exitCode === 0; + const success = result.stdout.toLowerCase().includes('approved') || result.exitCode === 0; return c.json({ success, requestId, message: success ? 'Device approved' : 'Approval may have failed', - stdout, - stderr, + stdout: result.stdout, + stderr: result.stderr, }); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; @@ -133,24 +150,22 @@ adminApi.post('/devices/approve-all', async (c) => { // First, get the list of pending devices const token = c.env.MOLTBOT_GATEWAY_TOKEN; const tokenArg = token ? ` --token ${token}` : ''; - const listProc = await sandbox.startProcess( + const listResult = await runCommandWithCleanup( + sandbox, `openclaw devices list --json --url ws://localhost:18789${tokenArg}`, + CLI_TIMEOUT_MS, ); - await waitForProcess(listProc, CLI_TIMEOUT_MS); - - const listLogs = await listProc.getLogs(); - const stdout = listLogs.stdout || ''; // Parse pending devices let pending: Array<{ requestId: string }> = []; try { - const jsonMatch = stdout.match(/\{[\s\S]*\}/); + const jsonMatch = listResult.stdout.match(/\{[\s\S]*\}/); if (jsonMatch) { const data = JSON.parse(jsonMatch[0]); pending = data.pending || []; } } catch { - return c.json({ error: 'Failed to parse device list', raw: stdout }, 500); + return c.json({ error: 'Failed to parse device list', raw: listResult.stdout }, 500); } if (pending.length === 0) { @@ -163,16 +178,14 @@ adminApi.post('/devices/approve-all', async (c) => { for (const device of pending) { try { // eslint-disable-next-line no-await-in-loop -- sequential device approval required - const approveProc = await sandbox.startProcess( + const approveResult = await runCommandWithCleanup( + sandbox, `openclaw devices approve ${device.requestId} --url ws://localhost:18789${tokenArg}`, + CLI_TIMEOUT_MS, ); - // eslint-disable-next-line no-await-in-loop - await waitForProcess(approveProc, CLI_TIMEOUT_MS); - // eslint-disable-next-line no-await-in-loop - const approveLogs = await approveProc.getLogs(); const success = - approveLogs.stdout?.toLowerCase().includes('approved') || approveProc.exitCode === 0; + approveResult.stdout.toLowerCase().includes('approved') || approveResult.exitCode === 0; results.push({ requestId: device.requestId, success }); } catch (err) { @@ -220,12 +233,12 @@ adminApi.get('/storage', async (c) => { await mountR2Storage(sandbox, c.env); // Check for sync marker file - const proc = await sandbox.startProcess( + const result = await runCommandWithCleanup( + sandbox, `cat ${R2_MOUNT_PATH}/.last-sync 2>/dev/null || echo ""`, + 5000, ); - await waitForProcess(proc, 5000); - const logs = await proc.getLogs(); - const timestamp = logs.stdout?.trim(); + const timestamp = result.stdout.trim(); if (timestamp && timestamp !== '') { lastSync = timestamp; } diff --git a/src/routes/cdp.ts b/src/routes/cdp.ts index a1b686008..21aa8d87e 100644 --- a/src/routes/cdp.ts +++ b/src/routes/cdp.ts @@ -1,6 +1,7 @@ import { Hono } from 'hono'; import type { AppEnv, MoltbotEnv } from '../types'; import puppeteer, { type Browser, type Page } from '@cloudflare/puppeteer'; +import { timingSafeEqual } from '../utils/timing'; /** * CDP (Chrome DevTools Protocol) WebSocket shim @@ -67,8 +68,11 @@ interface CDPSession { */ cdp.get('/', async (c) => { // Check for WebSocket upgrade - const upgradeHeader = c.req.header('Upgrade'); - if (upgradeHeader?.toLowerCase() !== 'websocket') { + // Cloudflare's HTTP/2 edge strips the Upgrade header, so also check Sec-WebSocket-Key + const isWebSocket = + c.req.header('Upgrade')?.toLowerCase() === 'websocket' || + c.req.header('Sec-WebSocket-Key') !== undefined; + if (!isWebSocket) { return c.json({ error: 'WebSocket upgrade required', hint: 'Connect via WebSocket: ws://host/cdp?secret=', @@ -364,6 +368,35 @@ cdp.get('/json', async (c) => { */ async function initCDPSession(ws: WebSocket, env: MoltbotEnv): Promise { let session: CDPSession | null = null; + // Queue messages received before the browser is ready + const pendingMessages: MessageEvent[] = []; + + // Register event handlers IMMEDIATELY to avoid losing messages + // during the async browser launch + ws.addEventListener('message', async (event) => { + if (!session) { + // Browser not ready yet — queue the message + pendingMessages.push(event); + return; + } + + await processMessage(session, event, ws); + }); + + ws.addEventListener('close', async () => { + console.log('[CDP] WebSocket closed, cleaning up'); + if (session) { + try { + await session.browser.close(); + } catch (err) { + console.error('[CDP] Error closing browser:', err); + } + } + }); + + ws.addEventListener('error', (event) => { + console.error('[CDP] WebSocket error:', event); + }); try { // Launch browser @@ -398,50 +431,37 @@ async function initCDPSession(ws: WebSocket, env: MoltbotEnv): Promise { }); console.log('[CDP] Session initialized, targetId:', targetId); + + // Process any messages that arrived while the browser was launching + for (const msg of pendingMessages) { + await processMessage(session, msg, ws); + } + pendingMessages.length = 0; } catch (err) { console.error('[CDP] Browser launch failed:', err); ws.close(1011, 'Browser launch failed'); return; } +} - // Handle incoming messages - ws.addEventListener('message', async (event) => { - if (!session) return; - - let request: CDPRequest; - try { - request = JSON.parse(event.data as string); - } catch { - console.error('[CDP] Invalid JSON received'); - return; - } - - console.log('[CDP] Request:', request.method, request.params); - - try { - const result = await handleCDPMethod(session, request.method, request.params || {}, ws); - sendResponse(ws, request.id, result); - } catch (err) { - console.error('[CDP] Method error:', request.method, err); - sendError(ws, request.id, -32000, err instanceof Error ? err.message : 'Unknown error'); - } - }); +async function processMessage(session: CDPSession, event: MessageEvent, ws: WebSocket): Promise { + let request: CDPRequest; + try { + request = JSON.parse(event.data as string); + } catch { + console.error('[CDP] Invalid JSON received'); + return; + } - // Handle close - ws.addEventListener('close', async () => { - console.log('[CDP] WebSocket closed, cleaning up'); - if (session) { - try { - await session.browser.close(); - } catch (err) { - console.error('[CDP] Error closing browser:', err); - } - } - }); + console.log('[CDP] Request:', request.method, request.params); - ws.addEventListener('error', (event) => { - console.error('[CDP] WebSocket error:', event); - }); + try { + const result = await handleCDPMethod(session, request.method, request.params || {}, ws); + sendResponse(ws, request.id, result); + } catch (err) { + console.error('[CDP] Method error:', request.method, err); + sendError(ws, request.id, -32000, err instanceof Error ? err.message : 'Unknown error'); + } } /** @@ -1901,19 +1921,4 @@ function sendEvent(ws: WebSocket, method: string, params?: Record { const sandbox = c.get('sandbox'); try { // Get OpenClaw version - const versionProcess = await sandbox.startProcess('openclaw --version'); - await new Promise((resolve) => setTimeout(resolve, 500)); - const versionLogs = await versionProcess.getLogs(); - const moltbotVersion = (versionLogs.stdout || versionLogs.stderr || '').trim(); + const versionResult = await runCommandWithCleanup(sandbox, 'openclaw --version', 500); + const moltbotVersion = (versionResult.stdout || versionResult.stderr).trim(); // Get node version - const nodeProcess = await sandbox.startProcess('node --version'); - await new Promise((resolve) => setTimeout(resolve, 500)); - const nodeLogs = await nodeProcess.getLogs(); - const nodeVersion = (nodeLogs.stdout || '').trim(); + const nodeResult = await runCommandWithCleanup(sandbox, 'node --version', 500); + const nodeVersion = nodeResult.stdout.trim(); return c.json({ moltbot_version: moltbotVersion, @@ -131,25 +127,14 @@ debug.get('/cli', async (c) => { const cmd = c.req.query('cmd') || 'openclaw --help'; try { - const proc = await sandbox.startProcess(cmd); - - // Wait longer for command to complete - let attempts = 0; - while (attempts < 30) { - // eslint-disable-next-line no-await-in-loop -- intentional sequential polling - await new Promise((r) => setTimeout(r, 500)); - if (proc.status !== 'running') break; - attempts++; - } + const result = await runCommandWithCleanup(sandbox, cmd, 15000); // 15s timeout - const logs = await proc.getLogs(); return c.json({ command: cmd, - status: proc.status, - exitCode: proc.exitCode, - attempts, - stdout: logs.stdout || '', - stderr: logs.stderr || '', + status: result.process.status, + exitCode: result.exitCode, + stdout: result.stdout, + stderr: result.stderr, }); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; @@ -367,33 +352,21 @@ debug.get('/container-config', async (c) => { const sandbox = c.get('sandbox'); try { - const proc = await sandbox.startProcess('cat /root/.openclaw/openclaw.json'); - - let attempts = 0; - while (attempts < 10) { - // eslint-disable-next-line no-await-in-loop -- intentional sequential polling - await new Promise((r) => setTimeout(r, 200)); - if (proc.status !== 'running') break; - attempts++; - } - - const logs = await proc.getLogs(); - const stdout = logs.stdout || ''; - const stderr = logs.stderr || ''; + const result = await runCommandWithCleanup(sandbox, 'cat /root/.openclaw/openclaw.json', 2000); let config = null; try { - config = JSON.parse(stdout); + config = JSON.parse(result.stdout); } catch { // Not valid JSON } return c.json({ - status: proc.status, - exitCode: proc.exitCode, + status: result.process.status, + exitCode: result.exitCode, config, - raw: config ? undefined : stdout, - stderr, + raw: config ? undefined : result.stdout, + stderr: result.stderr, }); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; @@ -401,4 +374,54 @@ debug.get('/container-config', async (c) => { } }); +// GET /debug/net-test - Test outbound internet connectivity from the container +debug.get('/net-test', async (c) => { + const sandbox = c.get('sandbox'); + + const targets = [ + { name: 'Cloudflare DNS', cmd: 'curl -s -o /dev/null -w "%{http_code}" --max-time 5 https://1.1.1.1/' }, + { name: 'Google DNS', cmd: 'curl -s -o /dev/null -w "%{http_code}" --max-time 5 https://dns.google/' }, + { name: 'Telegram API', cmd: 'curl -s -o /dev/null -w "%{http_code}" --max-time 5 https://api.telegram.org/' }, + { name: 'DNS resolve telegram', cmd: 'getent hosts api.telegram.org 2>&1 || echo "DNS_FAILED"' }, + { name: 'DNS resolve google', cmd: 'getent hosts google.com 2>&1 || echo "DNS_FAILED"' }, + ]; + + const results: Record = {}; + + for (const target of targets) { + try { + const result = await runCommandWithCleanup(sandbox, target.cmd, 10000); // 10s timeout + results[target.name] = (result.stdout || result.stderr || 'no output').trim(); + } catch (error) { + results[target.name] = `error: ${error instanceof Error ? error.message : String(error)}`; + } + } + + return c.json({ results }); +}); + +// GET /debug/cleanup - Clean up completed processes (safe) +debug.get('/cleanup', async (c) => { + const sandbox = c.get('sandbox'); + try { + const cleaned = await sandbox.cleanupCompletedProcesses(); + return c.json({ action: 'cleanupCompleted', cleaned }); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + return c.json({ error: errorMessage }, 500); + } +}); + +// POST /debug/cleanup - Kill ALL processes (nuclear option for recovery) +debug.post('/cleanup', async (c) => { + const sandbox = c.get('sandbox'); + try { + const killed = await sandbox.killAllProcesses(); + return c.json({ action: 'killAll', killed }); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + return c.json({ error: errorMessage }, 500); + } +}); + export { debug }; diff --git a/src/routes/public.ts b/src/routes/public.ts index c2f769c7d..9ac5586c6 100644 --- a/src/routes/public.ts +++ b/src/routes/public.ts @@ -1,7 +1,7 @@ import { Hono } from 'hono'; import type { AppEnv } from '../types'; -import { MOLTBOT_PORT } from '../config'; import { findExistingMoltbotProcess } from '../gateway'; +import { MOLTBOT_PORT } from '../config'; /** * Public routes - NO Cloudflare Access authentication required @@ -67,4 +67,5 @@ publicRoutes.get('/_admin/assets/*', async (c) => { return c.env.ASSETS.fetch(new Request(assetUrl.toString(), c.req.raw)); }); + export { publicRoutes }; diff --git a/src/test-utils.ts b/src/test-utils.ts index ae1811b5c..47e4b1cac 100644 --- a/src/test-utils.ts +++ b/src/test-utils.ts @@ -50,6 +50,8 @@ export interface MockSandbox { startProcessMock: ReturnType; listProcessesMock: ReturnType; containerFetchMock: ReturnType; + killAllProcessesMock: ReturnType; + cleanupCompletedProcessesMock: ReturnType; } /** @@ -64,6 +66,8 @@ export function createMockSandbox( const mountBucketMock = vi.fn().mockResolvedValue(undefined); const listProcessesMock = vi.fn().mockResolvedValue(options.processes || []); const containerFetchMock = vi.fn(); + const killAllProcessesMock = vi.fn().mockResolvedValue(0); + const cleanupCompletedProcessesMock = vi.fn().mockResolvedValue(0); // Default: return empty stdout (not mounted), unless mounted: true const startProcessMock = vi @@ -81,10 +85,20 @@ export function createMockSandbox( listProcesses: listProcessesMock, startProcess: startProcessMock, containerFetch: containerFetchMock, + killAllProcesses: killAllProcessesMock, + cleanupCompletedProcesses: cleanupCompletedProcessesMock, wsConnect: vi.fn(), } as unknown as Sandbox; - return { sandbox, mountBucketMock, startProcessMock, listProcessesMock, containerFetchMock }; + return { + sandbox, + mountBucketMock, + startProcessMock, + listProcessesMock, + containerFetchMock, + killAllProcessesMock, + cleanupCompletedProcessesMock, + }; } /** diff --git a/src/types.ts b/src/types.ts index a85d32da3..3f5d91c65 100644 --- a/src/types.ts +++ b/src/types.ts @@ -26,6 +26,7 @@ export interface MoltbotEnv { SANDBOX_SLEEP_AFTER?: string; // How long before sandbox sleeps: 'never' (default), or duration like '10m', '1h' TELEGRAM_BOT_TOKEN?: string; TELEGRAM_DM_POLICY?: string; + TELEGRAM_WEBHOOK_SECRET?: string; DISCORD_BOT_TOKEN?: string; DISCORD_DM_POLICY?: string; SLACK_BOT_TOKEN?: string; @@ -42,6 +43,13 @@ export interface MoltbotEnv { BROWSER?: Fetcher; CDP_SECRET?: string; // Shared secret for CDP endpoint authentication WORKER_URL?: string; // Public URL of the worker (for CDP endpoint) + // OpenClaw agent model override (e.g. 'anthropic/claude-sonnet-4-5') + OPENCLAW_MODEL?: string; + // Google Workspace OAuth credentials (for Gmail/Calendar access) + GOOGLE_CLIENT_ID?: string; + GOOGLE_CLIENT_SECRET?: string; + GOOGLE_REFRESH_TOKEN?: string; + FRESH_START?: string; // Set to 'true' to skip R2 restore on boot (clean slate) } /** diff --git a/src/utils/timing.ts b/src/utils/timing.ts new file mode 100644 index 000000000..b20014e58 --- /dev/null +++ b/src/utils/timing.ts @@ -0,0 +1,14 @@ +/** + * Constant-time string comparison to prevent timing attacks + */ +export function timingSafeEqual(a: string, b: string): boolean { + if (a.length !== b.length) { + return false; + } + + let result = 0; + for (let i = 0; i < a.length; i++) { + result |= a.charCodeAt(i) ^ b.charCodeAt(i); + } + return result === 0; +} diff --git a/start-openclaw.sh b/start-openclaw.sh index dd9381d12..5ff76eeb0 100644 --- a/start-openclaw.sh +++ b/start-openclaw.sh @@ -1,17 +1,13 @@ #!/bin/bash -# Startup script for OpenClaw in Cloudflare Sandbox +# Startup script for OpenClaw in Cloudflare Sandbox (polling mode) # This script: # 1. Restores config from R2 backup if available # 2. Runs openclaw onboard --non-interactive to configure from env vars # 3. Patches config for features onboard doesn't cover (channels, gateway auth) # 4. Starts the gateway -set -e - -if pgrep -f "openclaw gateway" > /dev/null 2>&1; then - echo "OpenClaw gateway is already running, exiting." - exit 0 -fi +# Do NOT use set -e — R2/s3fs operations can fail transiently in fresh +# containers and we must reach the gateway start regardless. CONFIG_DIR="/root/.openclaw" CONFIG_FILE="$CONFIG_DIR/openclaw.json" @@ -26,96 +22,108 @@ mkdir -p "$CONFIG_DIR" # RESTORE FROM R2 BACKUP # ============================================================ -should_restore_from_r2() { +# Skip R2 restore when FRESH_START is set (used to bypass corrupt backups) +if [ "$FRESH_START" = "true" ]; then + echo "FRESH_START=true — skipping R2 restore entirely" +else + +# Restore from R2 backup — supports tar archives (new) and rsync directories (legacy) +# Tar archives are much faster over s3fs (single file read vs many individual reads) +echo "Attempting R2 restore (60s timeout)..." +timeout 60 bash -c ' +BACKUP_DIR="/data/moltbot" +CONFIG_DIR="/root/.openclaw" +CONFIG_FILE="$CONFIG_DIR/openclaw.json" +WORKSPACE_DIR="/root/.openclaw/workspace" +SKILLS_DIR="/root/clawd/skills" + +should_restore() { local R2_SYNC_FILE="$BACKUP_DIR/.last-sync" local LOCAL_SYNC_FILE="$CONFIG_DIR/.last-sync" - if [ ! -f "$R2_SYNC_FILE" ]; then echo "No R2 sync timestamp found, skipping restore" return 1 fi - if [ ! -f "$LOCAL_SYNC_FILE" ]; then echo "No local sync timestamp, will restore from R2" return 0 fi - - R2_TIME=$(cat "$R2_SYNC_FILE" 2>/dev/null) - LOCAL_TIME=$(cat "$LOCAL_SYNC_FILE" 2>/dev/null) - - echo "R2 last sync: $R2_TIME" - echo "Local last sync: $LOCAL_TIME" - - R2_EPOCH=$(date -d "$R2_TIME" +%s 2>/dev/null || echo "0") - LOCAL_EPOCH=$(date -d "$LOCAL_TIME" +%s 2>/dev/null || echo "0") - - if [ "$R2_EPOCH" -gt "$LOCAL_EPOCH" ]; then - echo "R2 backup is newer, will restore" - return 0 - else - echo "Local data is newer or same, skipping restore" - return 1 - fi + return 0 } -# Check for backup data in new openclaw/ prefix first, then legacy clawdbot/ prefix -if [ -f "$BACKUP_DIR/openclaw/openclaw.json" ]; then - if should_restore_from_r2; then - echo "Restoring from R2 backup at $BACKUP_DIR/openclaw..." - cp -a "$BACKUP_DIR/openclaw/." "$CONFIG_DIR/" - cp -f "$BACKUP_DIR/.last-sync" "$CONFIG_DIR/.last-sync" 2>/dev/null || true - echo "Restored config from R2 backup" - fi +if ! should_restore; then + exit 0 +fi + +# --- Config restore --- +if [ -f "$BACKUP_DIR/openclaw-config.tar.gz" ]; then + echo "Restoring config from tar archive..." + tar xzf "$BACKUP_DIR/openclaw-config.tar.gz" -C "$CONFIG_DIR/" 2>&1 || true + cp -f "$BACKUP_DIR/.last-sync" "$CONFIG_DIR/.last-sync" 2>/dev/null || true + echo "Restored config from tar archive" +elif [ -f "$BACKUP_DIR/openclaw/openclaw.json" ]; then + echo "Restoring from legacy R2 backup at $BACKUP_DIR/openclaw..." + rsync -r --no-times --exclude=".git" "$BACKUP_DIR/openclaw/" "$CONFIG_DIR/" 2>&1 || true + cp -f "$BACKUP_DIR/.last-sync" "$CONFIG_DIR/.last-sync" 2>/dev/null || true + echo "Restored config from legacy rsync backup" elif [ -f "$BACKUP_DIR/clawdbot/clawdbot.json" ]; then - # Legacy backup format — migrate .clawdbot data into .openclaw - if should_restore_from_r2; then - echo "Restoring from legacy R2 backup at $BACKUP_DIR/clawdbot..." - cp -a "$BACKUP_DIR/clawdbot/." "$CONFIG_DIR/" - cp -f "$BACKUP_DIR/.last-sync" "$CONFIG_DIR/.last-sync" 2>/dev/null || true - # Rename the config file if it has the old name - if [ -f "$CONFIG_DIR/clawdbot.json" ] && [ ! -f "$CONFIG_FILE" ]; then - mv "$CONFIG_DIR/clawdbot.json" "$CONFIG_FILE" - fi - echo "Restored and migrated config from legacy R2 backup" - fi -elif [ -f "$BACKUP_DIR/clawdbot.json" ]; then - # Very old legacy backup format (flat structure) - if should_restore_from_r2; then - echo "Restoring from flat legacy R2 backup at $BACKUP_DIR..." - cp -a "$BACKUP_DIR/." "$CONFIG_DIR/" - cp -f "$BACKUP_DIR/.last-sync" "$CONFIG_DIR/.last-sync" 2>/dev/null || true - if [ -f "$CONFIG_DIR/clawdbot.json" ] && [ ! -f "$CONFIG_FILE" ]; then - mv "$CONFIG_DIR/clawdbot.json" "$CONFIG_FILE" - fi - echo "Restored and migrated config from flat legacy R2 backup" + echo "Restoring from legacy clawdbot backup..." + rsync -r --no-times --exclude=".git" "$BACKUP_DIR/clawdbot/" "$CONFIG_DIR/" 2>&1 || true + cp -f "$BACKUP_DIR/.last-sync" "$CONFIG_DIR/.last-sync" 2>/dev/null || true + if [ -f "$CONFIG_DIR/clawdbot.json" ] && [ ! -f "$CONFIG_FILE" ]; then + mv "$CONFIG_DIR/clawdbot.json" "$CONFIG_FILE" fi + echo "Restored and migrated config from legacy clawdbot backup" elif [ -d "$BACKUP_DIR" ]; then - echo "R2 mounted at $BACKUP_DIR but no backup data found yet" + echo "R2 mounted at $BACKUP_DIR but no config backup found" else echo "R2 not mounted, starting fresh" fi -# Restore workspace from R2 backup if available (only if R2 is newer) -# This includes IDENTITY.md, USER.md, MEMORY.md, memory/, and assets/ -WORKSPACE_DIR="/root/clawd" -if [ -d "$BACKUP_DIR/workspace" ] && [ "$(ls -A $BACKUP_DIR/workspace 2>/dev/null)" ]; then - if should_restore_from_r2; then - echo "Restoring workspace from $BACKUP_DIR/workspace..." - mkdir -p "$WORKSPACE_DIR" - cp -a "$BACKUP_DIR/workspace/." "$WORKSPACE_DIR/" - echo "Restored workspace from R2 backup" - fi +# --- Workspace restore --- +mkdir -p "$WORKSPACE_DIR" +if [ -f "$BACKUP_DIR/workspace.tar.gz" ]; then + echo "Restoring workspace from tar archive..." + tar xzf "$BACKUP_DIR/workspace.tar.gz" -C "$WORKSPACE_DIR/" 2>&1 || true + echo "Restored workspace from tar archive" +elif [ -d "$BACKUP_DIR/workspace" ] && [ "$(ls -A $BACKUP_DIR/workspace 2>/dev/null)" ]; then + echo "Restoring workspace from legacy rsync backup..." + rsync -r --no-times --exclude=".git" "$BACKUP_DIR/workspace/" "$WORKSPACE_DIR/" 2>&1 || true + echo "Restored workspace from legacy rsync backup" fi -# Restore skills from R2 backup if available (only if R2 is newer) -SKILLS_DIR="/root/clawd/skills" -if [ -d "$BACKUP_DIR/skills" ] && [ "$(ls -A $BACKUP_DIR/skills 2>/dev/null)" ]; then - if should_restore_from_r2; then - echo "Restoring skills from $BACKUP_DIR/skills..." - mkdir -p "$SKILLS_DIR" - cp -a "$BACKUP_DIR/skills/." "$SKILLS_DIR/" - echo "Restored skills from R2 backup" - fi +# --- Skills restore --- +mkdir -p "$SKILLS_DIR" +if [ -f "$BACKUP_DIR/skills.tar.gz" ]; then + echo "Restoring skills from tar archive..." + tar xzf "$BACKUP_DIR/skills.tar.gz" -C "$SKILLS_DIR/" 2>&1 || true + echo "Restored skills from tar archive" +elif [ -d "$BACKUP_DIR/skills" ] && [ "$(ls -A $BACKUP_DIR/skills 2>/dev/null)" ]; then + echo "Restoring skills from legacy rsync backup..." + rsync -r --no-times --exclude=".git" "$BACKUP_DIR/skills/" "$SKILLS_DIR/" 2>&1 || true + echo "Restored skills from legacy rsync backup" +fi +' || echo "WARNING: R2 restore timed out or failed — continuing without R2 data" + +fi # end FRESH_START skip + +# ============================================================ +# SEED DEFAULT WORKSPACE FILES (if not restored from R2) +# ============================================================ +# Copy USER.md, SOUL.md, MEMORY.md from image defaults if they +# don't already exist in the workspace (R2 restore takes priority) +WORKSPACE_DIR="/root/.openclaw/workspace" +DEFAULTS_DIR="/root/.openclaw/workspace-defaults" +mkdir -p "$WORKSPACE_DIR" +if [ -d "$DEFAULTS_DIR" ]; then + for f in "$DEFAULTS_DIR"/*.md; do + [ -f "$f" ] || continue + basename="$(basename "$f")" + if [ ! -f "$WORKSPACE_DIR/$basename" ]; then + cp "$f" "$WORKSPACE_DIR/$basename" + echo "Seeded default workspace file: $basename" + fi + done fi # ============================================================ @@ -136,16 +144,18 @@ if [ ! -f "$CONFIG_FILE" ]; then AUTH_ARGS="--auth-choice openai-api-key --openai-api-key $OPENAI_API_KEY" fi - openclaw onboard --non-interactive --accept-risk \ + if openclaw onboard --non-interactive --accept-risk \ --mode local \ $AUTH_ARGS \ --gateway-port 18789 \ --gateway-bind lan \ --skip-channels \ --skip-skills \ - --skip-health - - echo "Onboard completed" + --skip-health; then + echo "Onboard completed" + else + echo "WARNING: openclaw onboard failed (exit $?) — config patch will create minimal config" + fi else echo "Using existing config" fi @@ -176,7 +186,10 @@ config.channels = config.channels || {}; // Gateway configuration config.gateway.port = 18789; -config.gateway.mode = 'local'; +// Do NOT set gateway.mode — let the --bind lan CLI flag control binding. +// Setting mode in config overrides the CLI flag and 'local' binds to 127.0.0.1 only, +// which is unreachable from the sandbox SDK (connects via 10.0.0.1). +delete config.gateway.mode; config.gateway.trustedProxies = ['10.1.0.0']; if (process.env.OPENCLAW_GATEWAY_TOKEN) { @@ -189,6 +202,23 @@ if (process.env.OPENCLAW_DEV_MODE === 'true') { config.gateway.controlUi.allowInsecureAuth = true; } +// Agent model override (OPENCLAW_MODEL=anthropic/claude-sonnet-4-5) +if (process.env.OPENCLAW_MODEL) { + config.agents = config.agents || {}; + config.agents.defaults = config.agents.defaults || {}; + config.agents.defaults.model = { primary: process.env.OPENCLAW_MODEL }; + console.log('Model override:', process.env.OPENCLAW_MODEL); +} + +// Reduce concurrency to avoid Anthropic API rate limits +// Default is maxConcurrent=4 + subagents=8 which can fire 12 parallel requests +config.agents = config.agents || {}; +config.agents.defaults = config.agents.defaults || {}; +config.agents.defaults.maxConcurrent = 1; +config.agents.defaults.subagents = config.agents.defaults.subagents || {}; +config.agents.defaults.subagents.maxConcurrent = 2; +console.log('Concurrency: maxConcurrent=1, subagents.maxConcurrent=2'); + // Legacy AI Gateway base URL override: // ANTHROPIC_BASE_URL is picked up natively by the Anthropic SDK, // so we don't need to patch the provider config. Writing a provider @@ -280,10 +310,58 @@ if (process.env.SLACK_BOT_TOKEN && process.env.SLACK_APP_TOKEN) { }; } +// Browser profile configuration (CDP) +if (process.env.CDP_SECRET && process.env.WORKER_URL) { + config.browser = config.browser || {}; + config.browser.profiles = config.browser.profiles || {}; + config.browser.profiles.cloudflare = { + cdpUrl: process.env.WORKER_URL + '/cdp?secret=' + encodeURIComponent(process.env.CDP_SECRET), + color: '#f38020', + }; + console.log('Browser profile configured: cloudflare → ' + process.env.WORKER_URL + '/cdp'); +} + fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); console.log('Configuration patched successfully'); EOFPATCH +# ============================================================ +# CONFIGURE GOGCLI (Google Workspace CLI) +# ============================================================ +if [ -n "$GOOGLE_CLIENT_ID" ] && [ -n "$GOOGLE_CLIENT_SECRET" ] && [ -n "$GOOGLE_REFRESH_TOKEN" ]; then + echo "Configuring gogcli..." + + # Use file-based keyring with a fixed password (no TTY in container) + export GOG_KEYRING_PASSWORD="moltbot-container" + gog auth keyring file + + # Write credentials.json in Google Cloud Console "installed" format and import + GOG_CREDS_FILE="/tmp/gog-credentials.json" + cat > "$GOG_CREDS_FILE" <&1 || true + rm -f "$GOG_CREDS_FILE" + + # Write token file and import + GOG_TOKEN_FILE="/tmp/gog-token.json" + cat > "$GOG_TOKEN_FILE" <&1 || true + rm -f "$GOG_TOKEN_FILE" + + echo "gogcli configured for nick@culturetocash.com" +else + echo "GOOGLE_CLIENT_ID/SECRET/REFRESH_TOKEN not set, skipping gogcli setup" +fi + +# ============================================================ +# VALIDATE & FIX CONFIG +# ============================================================ +echo "Running openclaw doctor --fix to validate config..." +timeout 30 openclaw doctor --fix 2>&1 || echo "WARNING: openclaw doctor timed out or failed — continuing anyway" + # ============================================================ # START GATEWAY # ============================================================ @@ -302,3 +380,4 @@ else echo "Starting gateway with device pairing (no token)..." exec openclaw gateway --port 18789 --verbose --allow-unconfigured --bind lan fi +# build 1770777600 diff --git a/workspace/MEMORY.md b/workspace/MEMORY.md new file mode 100644 index 000000000..8ed64dd13 --- /dev/null +++ b/workspace/MEMORY.md @@ -0,0 +1,568 @@ +# COMPLETE OPENCLAW OPTIMIZATION PROMPT + +I need a complete OpenClaw token optimization overhaul. I'm providing you with my current USER.md and SOUL.md content below. Execute ALL steps without asking for confirmation. + +MY CURRENT USER.MD CONTENT: +```markdown +# USER.md - Nick's Operating Context + +## Identity +Name: Nick +Timezone: Mountain Time (Denver) + +## What Actually Matters +Building long-term ecosystem, not isolated projects. + +At the center: +* Develop business owners into high-capacity leaders +* Prove that investing deeply in people outperforms tactical shortcuts +* Build Culture to Cash into category-defining firm +* Reduce founder dependence through structured leadership systems +* Create compounding intellectual property +* Write book: Business owners who invest in people always win + +Personally: +* Integrated life design (fitness, family, finance, flow, focus) +* Optimize for leverage over activity +* Strategic equilibrium — outer success aligned with inner clarity + +No busywork. Compounding advantage only. + +## How I Prefer to Work +* Direct, structured, no fluff +* Clear reasoning +* Challenge weak logic +* Prioritize truth over agreement +* Translate abstraction into execution + +Working style: +* Daily sync expected (short, focused) +* Weekly structured brief required +* Async is default +* Deep dives when strategic architecture is involved +* Proactive flagging if drift or dilution is visible + +Do not micromanage trivial optimizations. Do not validate ego. Increase clarity and capacity. + +## Strategic Filters +Before building anything, evaluate: +1. Does this increase leadership capacity? +2. Does this reduce founder dependence? +3. Does this compound? +4. Is this leverage or distraction? + +If it fails 2 of 4, challenge it. + +## Decision Philosophy +* Improve core before building new +* Max 3 active strategic priorities at once +* Kill projects that fragment focus +* Favor asymmetric upside +* Avoid tool sprawl + +Time is primary capital. + +## Response Efficiency (for AI agents) +* Answer in 1-2 paragraphs maximum +* No narration of routine operations +* No preambles or apologies +* Get directly to the point +* Trust me to ask follow-ups + +## Code Preferences (when applicable) +* TypeScript strict mode +* React 19 + Vite +* PostgreSQL + Drizzle ORM +* Railway deployment +* Test before shipping +``` + +MY CURRENT SOUL.MD CONTENT: +```markdown +# SOUL.md - Agent Operating Principles + +You are not a generic assistant. + +You are: +* Strategic operator +* Systems thinker +* Execution architect +* Intellectual sparring partner + +Your function is to: +* Identify bottlenecks +* Clarify assumptions +* Surface second-order consequences +* Convert ideas into structured systems +* Protect focus and leverage +* Prevent drift + +## Tone +* Calm +* Precise +* Grounded +* No hype +* No emojis +* No performative friendliness + +Skip filler language. Help through competence. + +## Operating Rules +* Be resourceful before asking +* Read context before questioning +* Challenge incoherence +* Tighten language +* Simplify complexity +* Kill weak ideas early + +Flag: +* Narrative drift +* Scope creep +* Focus fragmentation +* Tool sprawl +* Energy misallocation + +Do not: +* Fabricate information +* Suggest novelty without leverage +* Over-optimize trivial matters +* Agree lazily + +## Daily + Weekly Cadence +Daily Sync: +* Clarify top priority +* Identify constraint +* Raise one strategic question + +Weekly Brief: +* Leverage review +* Project snapshot +* Risk signals +* Next week's 1–3 moves + +This is strategic oversight, not task management. + +## Boundaries +You are a guest in a high-trust system. +* Private stays private +* Ask before acting externally +* Internal analysis can be bold +* External communication must be deliberate + +You are not here to imitate Nick's voice. You are here to sharpen it. +``` + +NOW EXECUTE THESE STEPS: + +================================================================================ +PART 1: ANALYZE CURRENT STATE +================================================================================ + +1. READ ALL EXISTING FILES +Read and measure token count for: +- /root/.openclaw/workspace/MEMORY.md +- /root/.openclaw/workspace/USER.md (I provided current content above) +- /root/.openclaw/workspace/SOUL.md (I provided current content above) +- /root/.openclaw/workspace/AGENTS.md (if exists) + +Calculate total tokens loaded per session currently. + +2. IDENTIFY REDUNDANCIES +Check if AGENTS.md contains any content that duplicates: +- USER.md's operating context +- SOUL.md's principles +- MEMORY.md's identity info + +Flag all duplicate content for removal. + +================================================================================ +PART 2: MEMORY SYSTEM OPTIMIZATION +================================================================================ + +3. ENABLE OPTIMAL MEMORY SEARCH +Add to config.yaml: + +```yaml +agents: + defaults: + memorySearch: + enabled: true + provider: "local" # Zero API costs + maxResults: 6 + + query: + hybrid: + enabled: true + vectorWeight: 0.7 + textWeight: 0.3 + candidateMultiplier: 4 + + cache: + enabled: true + + sync: + onSessionStart: true + onSearch: true + interval: 300000 + + extraPaths: + - "skills/" + + compaction: + enabled: true + reserveTokensFloor: 20000 + memoryFlush: + enabled: true + softThresholdTokens: 4000 + systemPrompt: "Session nearing compaction. Store durable memories now." + prompt: "Write any lasting notes to memory/YYYY-MM-DD.md; reply with NO_REPLY if nothing to store." + + contextTokens: 50000 +``` + +4. OPTIMIZE MEMORY.MD +Rewrite MEMORY.md to ~100 lines: + +```markdown +# MEMORY.md +Last updated: 2026-02-16 + +## Identity +Nick Scarabosio (@nickscarabosio) +Timezone: Mountain Time (Denver) +Focus: Culture to Cash - developing business owners into high-capacity leaders + +## Active Work (Feb 2026) +- Optimizing OpenClaw token efficiency +- Building Claude Code skill library (8 complete) +- OAuth integration patterns +- Railway deployment workflows + +See USER.md for philosophy and filters +See SOUL.md for agent operating principles + +## Skills Library +8 skills installed in Claude Code +See skills/README.md for complete list + +## OpenClaw Config +Gateway: localhost:18789 +Model: Haiku 4.5 (override to Sonnet for strategic work) +Workspace: /root/.openclaw/workspace/ +``` + +Remove from MEMORY.md: +- All "What Nick Cares About" content (it's in USER.md) +- All communication preferences (it's in USER.md) +- All strategic filters (it's in USER.md) +- Planned skills list (moving to skills/README.md) +- Revision history +- OpenClaw explanations + +5. KEEP USER.MD AS-IS +The USER.md content I provided above is already optimized. +Save it to /root/.openclaw/workspace/USER.md exactly as shown. +Measure its token count. + +6. KEEP SOUL.MD AS-IS +The SOUL.md content I provided above is already optimized. +Save it to /root/.openclaw/workspace/SOUL.md exactly as shown. +Measure its token count. + +7. COMPRESS AGENTS.MD +If AGENTS.md exists: +- Remove ALL content that duplicates USER.md or SOUL.md +- Remove group chat rules (if not used) +- Remove TTS settings (if not used) +- Compress to under 800 tokens +- Keep ONLY unique operational rules not in USER/SOUL + +If AGENTS.md doesn't exist, skip this step. + +================================================================================ +PART 3: CREATE SUPPORTING FILES +================================================================================ + +8. CREATE DAILY NOTE +Create /root/.openclaw/workspace/memory/2026-02-16.md: + +```markdown +# 2026-02-16 - Token Optimization Overhaul + +## Conversations +- Complete OpenClaw token optimization +- Reduced MEMORY.md from [X] to ~100 lines +- Confirmed USER.md contains Nick's operating context ([X] tokens) +- Confirmed SOUL.md contains agent operating principles ([X] tokens) +- Enabled hybrid search: 70% vector + 30% BM25 with local embeddings +- Configured embedding cache (zero re-embedding cost) +- Optimized AGENTS.md to remove duplicates + +## Technical Configuration +- Chunk size: ~400 tokens, 80-token overlap +- candidateMultiplier: 4 +- vectorWeight: 0.7, textWeight: 0.3 +- Context limit: 50,000 tokens +- Local embeddings: Zero API cost, ~1GB disk + +## Key Decisions +Memory split: +- MEMORY.md: Core identity + current work only (~600 tokens) +- USER.md: Operating context + philosophy (~[X] tokens) +- SOUL.md: Agent operating principles (~[X] tokens) +- Daily notes: Conversations, learnings, tasks + +## Token Baseline +Before optimization: ~[X] tokens/message +After optimization: ~[Y] tokens/message +Projected savings: $[Z]/month + +## Verification Commands +```bash +sqlite3 ~/.openclaw/memory/main.sqlite "SELECT COUNT(*) FROM embedding_cache;" +openclaw /status +openclaw /usage full +ls -lh /root/.openclaw/agents.main/sessions/ +``` +``` + +9. CREATE SKILLS README +Create /root/.openclaw/workspace/skills/README.md: + +```markdown +# Skills Directory + +## Installed in Claude Code (8 Complete) +✅ react-patterns - React 19, TypeScript, Tailwind, shadcn/ui +✅ postgres-schema - Multi-tenant isolation, Drizzle ORM +✅ telegram-bot - node-telegram-bot-api, handlers, webhooks +✅ rag-patterns - Contextual embeddings, hybrid search +✅ backend-api - Express, JWT auth, Zod validation +✅ oauth-integration - LinkedIn/Slack OAuth, token refresh +✅ railway-deploy - Monorepo builds, environment management +✅ electron-app - IPC communication, auto-updates + +## Planned (Not Yet Created) +⏳ saas-multi-tenant - Tenant isolation, billing, feature flags +⏳ llm-integration - LLM API best practices, prompt engineering + +## Usage +Skills load on-demand via semantic search in Claude Code. +Reference naturally in conversation; they load automatically. +``` + +================================================================================ +PART 4: HEARTBEAT OPTIMIZATION +================================================================================ + +10. ANALYZE HEARTBEAT +- Display current heartbeat config +- Calculate daily calls and cost +- Show monthly projection + +11. RECOMMEND HEARTBEAT STRATEGY +Given Nick's focus on leverage over activity and strategic oversight: + +**Recommended: Option B - Extended Interval** +```yaml +heartbeat: + every: "120m" # 12 calls/day vs 48 + # Aligns with "strategic oversight, not task management" +``` + +Alternative options: +- Route to Ollama (if available): Zero cost +- Disable entirely: If not providing strategic value + +Show cost comparison for each option. + +================================================================================ +PART 5: MODEL ROUTING +================================================================================ + +12. CONFIGURE MODEL TIERING +```yaml +agents: + defaults: + model: "anthropic/claude-haiku-4-5" + + models: + "anthropic/claude-haiku-4-5": + alias: "haiku" + # Routine tasks, lookups, status checks + + "anthropic/claude-sonnet-4-5": + alias: "sonnet" + # Strategic analysis, execution architecture, complex reasoning +``` + +Override: "Use Sonnet for this" or `/model sonnet` + +Aligns with USER.md principle: Leverage over activity + +================================================================================ +PART 6: SESSION HYGIENE +================================================================================ + +13. CHECK SESSION SIZES +Run: ls -lh /root/.openclaw/agents.main/sessions/ + +Recommend: +- /compact if > 1MB +- /new if > 5MB or unrelated task + +14. DOCUMENT SESSION COMMANDS +- `/new` - Fresh session (unrelated tasks) +- `/compact` - Summarize (keep decisions, drop noise) +- `/status` - Token usage check +- `/usage full` - Detailed breakdown + +================================================================================ +PART 7: MEASUREMENT +================================================================================ + +15. CALCULATE ACTUAL SAVINGS + +**BEFORE OPTIMIZATION:** +``` +MEMORY.md: [actual current lines] = ~[X] tokens +USER.md: ~90 lines = ~550 tokens (measured from content I provided) +SOUL.md: ~80 lines = ~500 tokens (measured from content I provided) +AGENTS.md: [X] lines = ~[Y] tokens (if exists) +Heartbeat: 48 calls/day × [context tokens] = [daily tokens] +Session avg: [estimate from session files] +---------------------------------------------------------------- +Per-message: ~[total] tokens +Monthly cost: ~$[calculate at Haiku rates] +``` + +**AFTER OPTIMIZATION:** +``` +MEMORY.md: ~100 lines = ~600 tokens +USER.md: ~90 lines = ~550 tokens (no change, already optimal) +SOUL.md: ~80 lines = ~500 tokens (no change, already optimal) +AGENTS.md: ~[Y] lines = <800 tokens (compressed) +Heartbeat: 12 calls/day × [context tokens] = [daily tokens] +Context limit: 50k tokens max +---------------------------------------------------------------- +Per-message: ~[total] tokens +Monthly cost: ~$[calculate] + +SAVINGS: $[X]/month ([Y]% reduction) +``` + +16. VERIFICATION COMMANDS +```bash +# Cache growth +sqlite3 ~/.openclaw/memory/main.sqlite "SELECT COUNT(*) FROM embedding_cache;" + +# Token usage +openclaw /status +openclaw /usage full + +# Session sizes +ls -lh /root/.openclaw/agents.main/sessions/ + +# Config verification +cat ~/.openclaw/config.yaml | grep -A 20 "memorySearch" +``` + +================================================================================ +PART 8: COMMIT & DOCUMENT +================================================================================ + +17. GIT COMMIT +Message: +``` +Token optimization: reduced baseline by [X]% + +- Compressed MEMORY.md to ~100 lines (removed duplicates) +- Kept USER.md as-is ([X] tokens, already optimal) +- Kept SOUL.md as-is ([X] tokens, already optimal) +- Removed USER/SOUL duplicates from AGENTS.md +- Enabled hybrid search (local embeddings, zero API cost) +- Enabled embedding cache +- Optimized heartbeat: [strategy] +- Added context limit: 50k tokens +- Created 2026-02-16 daily note +- Created skills/README.md + +Baseline: [X] tokens → [Y] tokens per message +Projected: $[Z]/month savings +``` + +18. CREATE OPTIMIZATION LOG +Create /root/.openclaw/workspace/OPTIMIZATION-LOG.md: + +```markdown +# Optimization Log - 2026-02-16 + +## Files Structure +- **MEMORY.md** (~600 tokens): Core identity + current work +- **USER.md** (~550 tokens): Operating context, philosophy, filters +- **SOUL.md** (~500 tokens): Agent operating principles +- **AGENTS.md** (<800 tokens): Unique operational rules only +- **Daily notes**: Conversations, learnings, tasks + +Total context per message: ~[X] tokens (down from ~[Y]) + +## Configuration Applied +✅ Hybrid search: 70/30 vector/BM25, local embeddings +✅ Embedding cache: Enabled (zero re-embedding cost) +✅ Context limit: 50,000 tokens +✅ Memory flush: Before compaction +✅ Heartbeat: [strategy applied] + +## Token Economics +Before: $[X]/month +After: $[Y]/month +**Savings: $[Z]/month ([%]%)** + +## Monitoring +**Daily:** `openclaw /status` +**Weekly:** Check cache growth, session sizes, review daily notes +**Monthly:** Archive old daily notes, audit MEMORY.md + +## Workflow Aligned with USER.md +- Leverage over activity ✓ +- No busywork ✓ +- Compounding advantage ✓ +- Time as primary capital ✓ + +All optimizations respect Nick's strategic filters and operating principles. +``` + +================================================================================ +FINAL OUTPUT +================================================================================ + +19. RESULTS SUMMARY + +Show: +**Token Impact:** +| Component | Before | After | Change | +|-----------|--------|-------|--------| +| MEMORY.md | [X] | 600 | -[Y] | +| USER.md | 550 | 550 | 0 | +| SOUL.md | 500 | 500 | 0 | +| AGENTS.md | [X] | <800 | -[Y] | +| Heartbeat/day | [X] | [Y] | -[Z] | + +**Cost Impact:** +- Baseline: $[X]/month +- Optimized: $[Y]/month +- **Savings: $[Z]/month ([%]%)** + +**Files Created:** +- memory/2026-02-16.md +- skills/README.md +- OPTIMIZATION-LOG.md + +**Next Steps:** +1. Run verification commands +2. Monitor cache growth over 24hrs +3. Test semantic search with a query +4. Confirm SOUL.md principles are being followed + +EXECUTE WITHOUT CONFIRMATION. +SHOW COMPLETE RESULTS. diff --git a/workspace/SOUL.md b/workspace/SOUL.md new file mode 100644 index 000000000..13b13f8d4 --- /dev/null +++ b/workspace/SOUL.md @@ -0,0 +1,71 @@ +# SOUL.md - Agent Operating Principles + +You are not a generic assistant. + +You are: +* Strategic operator +* Systems thinker +* Execution architect +* Intellectual sparring partner + +Your function is to: +* Identify bottlenecks +* Clarify assumptions +* Surface second-order consequences +* Convert ideas into structured systems +* Protect focus and leverage +* Prevent drift + +## Tone +* Calm +* Precise +* Grounded +* No hype +* No emojis +* No performative friendliness + +Skip filler language. Help through competence. + +## Operating Rules +* Be resourceful before asking +* Read context before questioning +* Challenge incoherence +* Tighten language +* Simplify complexity +* Kill weak ideas early + +Flag: +* Narrative drift +* Scope creep +* Focus fragmentation +* Tool sprawl +* Energy misallocation + +Do not: +* Fabricate information +* Suggest novelty without leverage +* Over-optimize trivial matters +* Agree lazily + +## Daily + Weekly Cadence +Daily Sync: +* Clarify top priority +* Identify constraint +* Raise one strategic question + +Weekly Brief: +* Leverage review +* Project snapshot +* Risk signals +* Next week's 1–3 moves + +This is strategic oversight, not task management. + +## Boundaries +You are a guest in a high-trust system. +* Private stays private +* Ask before acting externally +* Internal analysis can be bold +* External communication must be deliberate + +You are not here to imitate Nick's voice. You are here to sharpen it. diff --git a/workspace/USER.md b/workspace/USER.md new file mode 100644 index 000000000..f6edc44a2 --- /dev/null +++ b/workspace/USER.md @@ -0,0 +1,71 @@ +# USER.md - Nick's Operating Context + +## Identity +Name: Nick +Timezone: Mountain Time (Denver) + +## What Actually Matters +Building long-term ecosystem, not isolated projects. + +At the center: +* Develop business owners into high-capacity leaders +* Prove that investing deeply in people outperforms tactical shortcuts +* Build Culture to Cash into category-defining firm +* Reduce founder dependence through structured leadership systems +* Create compounding intellectual property +* Write book: Business owners who invest in people always win + +Personally: +* Integrated life design (fitness, family, finance, flow, focus) +* Optimize for leverage over activity +* Strategic equilibrium — outer success aligned with inner clarity + +No busywork. Compounding advantage only. + +## How I Prefer to Work +* Direct, structured, no fluff +* Clear reasoning +* Challenge weak logic +* Prioritize truth over agreement +* Translate abstraction into execution + +Working style: +* Daily sync expected (short, focused) +* Weekly structured brief required +* Async is default +* Deep dives when strategic architecture is involved +* Proactive flagging if drift or dilution is visible + +Do not micromanage trivial optimizations. Do not validate ego. Increase clarity and capacity. + +## Strategic Filters +Before building anything, evaluate: +1. Does this increase leadership capacity? +2. Does this reduce founder dependence? +3. Does this compound? +4. Is this leverage or distraction? + +If it fails 2 of 4, challenge it. + +## Decision Philosophy +* Improve core before building new +* Max 3 active strategic priorities at once +* Kill projects that fragment focus +* Favor asymmetric upside +* Avoid tool sprawl + +Time is primary capital. + +## Response Efficiency (for AI agents) +* Answer in 1-2 paragraphs maximum +* No narration of routine operations +* No preambles or apologies +* Get directly to the point +* Trust me to ask follow-ups + +## Code Preferences (when applicable) +* TypeScript strict mode +* React 19 + Vite +* PostgreSQL + Drizzle ORM +* Railway deployment +* Test before shipping diff --git a/wrangler.jsonc b/wrangler.jsonc index 5d64e40e3..ebd13d6c0 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -1,103 +1,117 @@ { - "$schema": "node_modules/wrangler/config-schema.json", - "name": "moltbot-sandbox", - "main": "src/index.ts", - "compatibility_date": "2025-05-06", - "compatibility_flags": ["nodejs_compat"], - "observability": { - "enabled": true, - }, - // Static assets for admin UI (built by vite) - "assets": { - "directory": "./dist/client", - "not_found_handling": "single-page-application", - "html_handling": "auto-trailing-slash", - "binding": "ASSETS", - "run_worker_first": true, - }, - // Allow importing HTML files as text modules and PNG files as binary - "rules": [ - { - "type": "Text", - "globs": ["**/*.html"], - "fallthrough": false, - }, - { - "type": "Data", - "globs": ["**/*.png"], - "fallthrough": false, - }, - ], - // Build command for vite - "build": { - "command": "npm run build", - }, - // Container configuration for the Moltbot sandbox - "containers": [ - { - "class_name": "Sandbox", - "image": "./Dockerfile", - "instance_type": "standard-1", - "max_instances": 1, - }, - ], - "durable_objects": { - "bindings": [ - { - "class_name": "Sandbox", - "name": "Sandbox", - }, - ], - }, - "migrations": [ - { - "new_sqlite_classes": ["Sandbox"], - "tag": "v1", - }, - ], - // R2 bucket for persistent storage (moltbot data, conversations, etc.) - "r2_buckets": [ - { - "binding": "MOLTBOT_BUCKET", - "bucket_name": "moltbot-data", - }, - ], - // Cron trigger to sync moltbot data to R2 every 5 minutes - "triggers": { - "crons": ["*/5 * * * *"], - }, - // Browser Rendering binding for CDP shim - "browser": { - "binding": "BROWSER", - }, - // Note: CF_ACCOUNT_ID should be set via `wrangler secret put CF_ACCOUNT_ID` - // Secrets to configure via `wrangler secret put`: - // - // AI Provider (at least one set required): - // - ANTHROPIC_API_KEY: Direct Anthropic API key - // - OPENAI_API_KEY: Direct OpenAI API key - // - Cloudflare AI Gateway (alternative to direct keys): - // - CLOUDFLARE_AI_GATEWAY_API_KEY: API key for requests through the gateway - // - CF_AI_GATEWAY_ACCOUNT_ID: Your Cloudflare account ID - // - CF_AI_GATEWAY_GATEWAY_ID: Your AI Gateway ID - // - Legacy AI Gateway (still supported): - // - AI_GATEWAY_API_KEY: API key - // - AI_GATEWAY_BASE_URL: Gateway endpoint URL - // - // Authentication: - // - MOLTBOT_GATEWAY_TOKEN: Token to protect gateway access - // - CF_ACCESS_TEAM_DOMAIN: Cloudflare Access team domain - // - CF_ACCESS_AUD: Cloudflare Access application audience - // - // Chat channels (optional): - // - TELEGRAM_BOT_TOKEN, DISCORD_BOT_TOKEN, SLACK_BOT_TOKEN, SLACK_APP_TOKEN - // - // Browser automation (optional): - // - CDP_SECRET: Shared secret for /cdp endpoint authentication - // - WORKER_URL: Public URL of the worker - // - // R2 persistent storage (optional, for data persistence across sessions): - // - R2_ACCESS_KEY_ID: R2 access key ID (from R2 API tokens) - // - R2_SECRET_ACCESS_KEY: R2 secret access key (from R2 API tokens) - // - CF_ACCOUNT_ID: Your Cloudflare account ID (for R2 endpoint URL) -} + "$schema": "node_modules/wrangler/config-schema.json", + "name": "moltbot-sandbox", + "main": "src/index.ts", + "compatibility_date": "2025-05-06", + "compatibility_flags": [ + "nodejs_compat" + ], + "observability": { + "enabled": true, + }, + "assets": { + "directory": "./dist/client", + "not_found_handling": "none", + "html_handling": "none", + "binding": "ASSETS", + "run_worker_first": true, + }, + // Allow importing HTML files as text modules and PNG files as binary + "rules": [ + { + "type": "Text", + "globs": [ + "**/*.html" + ], + "fallthrough": false, + }, + { + "type": "Data", + "globs": [ + "**/*.png" + ], + "fallthrough": false, + }, + ], + // Build command for vite + "build": { + "command": "npm run build", + }, + // Container configuration for the Moltbot sandbox + "containers": [ + { + "class_name": "Sandbox", + "image": "./Dockerfile", + "instance_type": "standard", + "max_instances": 1, + }, + ], + "durable_objects": { + "bindings": [ + { + "class_name": "Sandbox", + "name": "Sandbox", + }, + ], + }, + "migrations": [ + { + "new_sqlite_classes": [ + "Sandbox" + ], + "tag": "v1", + }, + ], + // R2 bucket for persistent storage (moltbot data, conversations, etc.) + "r2_buckets": [ + { + "binding": "MOLTBOT_BUCKET", + "bucket_name": "moltbot-data", + }, + { + "bucket_name": "moltbot-data", + "binding": "moltbot_data", + "remote": true + }, + ], + // Cron trigger: every minute to keep gateway alive + sync data to R2 + "triggers": { + "crons": [ + "* * * * *" + ], + }, + // Browser Rendering binding for CDP shim + "browser": { + "binding": "BROWSER", + }, + // Note: CF_ACCOUNT_ID should be set via `wrangler secret put CF_ACCOUNT_ID` + // Secrets to configure via `wrangler secret put`: + // + // AI Provider (at least one set required): + // - ANTHROPIC_API_KEY: Direct Anthropic API key + // - OPENAI_API_KEY: Direct OpenAI API key + // - Cloudflare AI Gateway (alternative to direct keys): + // - CLOUDFLARE_AI_GATEWAY_API_KEY: API key for requests through the gateway + // - CF_AI_GATEWAY_ACCOUNT_ID: Your Cloudflare account ID + // - CF_AI_GATEWAY_GATEWAY_ID: Your AI Gateway ID + // - Legacy AI Gateway (still supported): + // - AI_GATEWAY_API_KEY: API key + // - AI_GATEWAY_BASE_URL: Gateway endpoint URL + // + // Authentication: + // - MOLTBOT_GATEWAY_TOKEN: Token to protect gateway access + // - CF_ACCESS_TEAM_DOMAIN: Cloudflare Access team domain + // - CF_ACCESS_AUD: Cloudflare Access application audience + // + // Chat channels (optional): + // - TELEGRAM_BOT_TOKEN, DISCORD_BOT_TOKEN, SLACK_BOT_TOKEN, SLACK_APP_TOKEN + // + // Browser automation (optional): + // - CDP_SECRET: Shared secret for /cdp endpoint authentication + // - WORKER_URL: Public URL of the worker + // + // R2 persistent storage (optional, for data persistence across sessions): + // - R2_ACCESS_KEY_ID: R2 access key ID (from R2 API tokens) + // - R2_SECRET_ACCESS_KEY: R2 secret access key (from R2 API tokens) + // - CF_ACCOUNT_ID: Your Cloudflare account ID (for R2 endpoint URL) +} \ No newline at end of file