From 4168709f0fd8cbfcd3722150d6c56a36678c3041 Mon Sep 17 00:00:00 2001 From: Nicholas Scarabosio Date: Mon, 9 Feb 2026 12:11:33 -0700 Subject: [PATCH 01/24] Add remote R2 bucket binding for local development Co-Authored-By: Claude Opus 4.6 --- wrangler.jsonc | 219 ++++++++++++++++++++++++++----------------------- 1 file changed, 117 insertions(+), 102 deletions(-) diff --git a/wrangler.jsonc b/wrangler.jsonc index 5d64e40e3..0c9524893 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -1,103 +1,118 @@ { - "$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, + }, + // 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", + }, + { + "bucket_name": "moltbot-data", + "binding": "moltbot_data", + "remote": true + }, + ], + // 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) +} \ No newline at end of file From 311b51bc47ca02ab8d92d9d6c35fda66e890e788 Mon Sep 17 00:00:00 2001 From: Nicholas Scarabosio Date: Mon, 9 Feb 2026 12:16:06 -0700 Subject: [PATCH 02/24] Update package-lock.json with resolved optional dependencies Co-Authored-By: Claude Opus 4.6 --- package-lock.json | 525 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 522 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2e5494df7..201a2c201 100644 --- a/package-lock.json +++ b/package-lock.json @@ -434,6 +434,23 @@ "wrangler": "^4.60.0" } }, + "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": [ @@ -449,6 +466,57 @@ "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.20260124.0", "dev": true, @@ -465,6 +533,17 @@ "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", @@ -934,6 +1013,29 @@ "@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": [ @@ -949,6 +1051,423 @@ "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": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "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": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "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": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -3051,9 +3570,9 @@ } }, "node_modules/hono": { - "version": "4.11.6", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.6.tgz", - "integrity": "sha512-ofIiiHyl34SV6AuhE3YT2mhO5HRWokce+eUYE82TsP6z0/H3JeJcjVWEMSIAiw2QkjDOEpES/lYsg8eEbsLtdw==", + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.9.tgz", + "integrity": "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==", "license": "MIT", "engines": { "node": ">=16.9.0" From 088b1828e6a4e73bb582b2dc7cfdbb9bdef793d7 Mon Sep 17 00:00:00 2001 From: Nicholas Scarabosio Date: Mon, 9 Feb 2026 12:18:49 -0700 Subject: [PATCH 03/24] Revert "Update package-lock.json with resolved optional dependencies" This reverts commit 311b51bc47ca02ab8d92d9d6c35fda66e890e788. --- package-lock.json | 525 +--------------------------------------------- 1 file changed, 3 insertions(+), 522 deletions(-) diff --git a/package-lock.json b/package-lock.json index 201a2c201..2e5494df7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -434,23 +434,6 @@ "wrangler": "^4.60.0" } }, - "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": [ @@ -466,57 +449,6 @@ "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.20260124.0", "dev": true, @@ -533,17 +465,6 @@ "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", @@ -1013,29 +934,6 @@ "@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": [ @@ -1051,423 +949,6 @@ "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": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "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": [ - "ia32" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "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": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -3570,9 +3051,9 @@ } }, "node_modules/hono": { - "version": "4.11.9", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.9.tgz", - "integrity": "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==", + "version": "4.11.6", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.6.tgz", + "integrity": "sha512-ofIiiHyl34SV6AuhE3YT2mhO5HRWokce+eUYE82TsP6z0/H3JeJcjVWEMSIAiw2QkjDOEpES/lYsg8eEbsLtdw==", "license": "MIT", "engines": { "node": ">=16.9.0" From 4ce85ea956b855e9b15d25beed090d65798b000c Mon Sep 17 00:00:00 2001 From: Nicholas Scarabosio Date: Mon, 9 Feb 2026 14:18:12 -0700 Subject: [PATCH 04/24] Add Google Workspace skill for Gmail and Calendar access Enables the OpenClaw agent to query Gmail and Google Calendar remotely via OAuth2 credentials passed as worker secrets. Includes scripts for searching/reading/sending email and listing/creating calendar events. Co-Authored-By: Claude Opus 4.6 --- Dockerfile | 5 +- skills/google-workspace/SKILL.md | 55 ++++++++++++ .../scripts/calendar-create.js | 80 +++++++++++++++++ .../scripts/calendar-events.js | 81 +++++++++++++++++ skills/google-workspace/scripts/gmail-read.js | 86 +++++++++++++++++++ .../google-workspace/scripts/gmail-search.js | 67 +++++++++++++++ skills/google-workspace/scripts/gmail-send.js | 63 ++++++++++++++ .../google-workspace/scripts/google-auth.js | 42 +++++++++ src/gateway/env.ts | 3 + src/types.ts | 4 + 10 files changed, 485 insertions(+), 1 deletion(-) create mode 100644 skills/google-workspace/SKILL.md create mode 100644 skills/google-workspace/scripts/calendar-create.js create mode 100644 skills/google-workspace/scripts/calendar-events.js create mode 100644 skills/google-workspace/scripts/gmail-read.js create mode 100644 skills/google-workspace/scripts/gmail-search.js create mode 100644 skills/google-workspace/scripts/gmail-send.js create mode 100644 skills/google-workspace/scripts/google-auth.js diff --git a/Dockerfile b/Dockerfile index 340183e46..2e2e87abe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,6 +20,9 @@ RUN ARCH="$(dpkg --print-architecture)" \ # Install pnpm globally RUN npm install -g pnpm +# Install Google APIs client (for Google Workspace skill) +RUN npm install -g googleapis google-auth-library + # Install OpenClaw (formerly clawdbot/moltbot) # Pin to specific version for reproducible builds RUN npm install -g openclaw@2026.2.3 \ @@ -32,7 +35,7 @@ RUN mkdir -p /root/.openclaw \ && mkdir -p /root/clawd/skills # Copy startup script -# Build cache bust: 2026-02-06-v29-sync-workspace +# Build cache bust: 2026-02-09-v30-google-workspace COPY start-openclaw.sh /usr/local/bin/start-openclaw.sh RUN chmod +x /usr/local/bin/start-openclaw.sh 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..7a9282e88 --- /dev/null +++ b/skills/google-workspace/scripts/google-auth.js @@ -0,0 +1,42 @@ +#!/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(); + */ + +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/gateway/env.ts b/src/gateway/env.ts index 23dea539d..a4064cead 100644 --- a/src/gateway/env.ts +++ b/src/gateway/env.ts @@ -49,6 +49,9 @@ 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.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; return envVars; } diff --git a/src/types.ts b/src/types.ts index a85d32da3..712de062e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -42,6 +42,10 @@ 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) + // Google Workspace OAuth credentials (for Gmail/Calendar access) + GOOGLE_CLIENT_ID?: string; + GOOGLE_CLIENT_SECRET?: string; + GOOGLE_REFRESH_TOKEN?: string; } /** From bc3ca0f4194ffcc75c3c1c2ddd5d40d5322ab055 Mon Sep 17 00:00:00 2001 From: Nicholas Scarabosio Date: Mon, 9 Feb 2026 15:10:28 -0700 Subject: [PATCH 05/24] Fix NODE_PATH for globally installed googleapis in sandbox The sandbox's startProcess does not inherit Dockerfile ENV vars, so globally installed npm packages aren't found by require(). Add NODE_PATH initialization directly in google-auth.js and as a Dockerfile ENV for other contexts. Co-Authored-By: Claude Opus 4.6 --- Dockerfile | 4 +++- skills/google-workspace/scripts/google-auth.js | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 2e2e87abe..51086036b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,6 +21,8 @@ RUN ARCH="$(dpkg --print-architecture)" \ RUN npm install -g pnpm # Install Google APIs client (for Google Workspace skill) +# NODE_PATH ensures globally installed modules are found by require() +ENV NODE_PATH=/usr/local/lib/node_modules RUN npm install -g googleapis google-auth-library # Install OpenClaw (formerly clawdbot/moltbot) @@ -35,7 +37,7 @@ RUN mkdir -p /root/.openclaw \ && mkdir -p /root/clawd/skills # Copy startup script -# Build cache bust: 2026-02-09-v30-google-workspace +# Build cache bust: 2026-02-09-v32-script-node-path COPY start-openclaw.sh /usr/local/bin/start-openclaw.sh RUN chmod +x /usr/local/bin/start-openclaw.sh diff --git a/skills/google-workspace/scripts/google-auth.js b/skills/google-workspace/scripts/google-auth.js index 7a9282e88..44a72884b 100644 --- a/skills/google-workspace/scripts/google-auth.js +++ b/skills/google-workspace/scripts/google-auth.js @@ -11,6 +11,13 @@ * 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() { From 359d70a3cff5a43de66ceba3a10ad4007b2f2275 Mon Sep 17 00:00:00 2001 From: Nicholas Scarabosio Date: Mon, 9 Feb 2026 15:47:28 -0700 Subject: [PATCH 06/24] Install gogcli and configure Google auth on container startup Replace googleapis npm package with the gogcli binary, which is OpenClaw's built-in Google Workspace skill. The startup script now configures gogcli credentials and imports the refresh token from env vars using the file-based keyring backend. Co-Authored-By: Claude Opus 4.6 --- Dockerfile | 14 +++++++++----- start-openclaw.sh | 31 +++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 51086036b..3da1a01aa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,10 +20,14 @@ RUN ARCH="$(dpkg --print-architecture)" \ # Install pnpm globally RUN npm install -g pnpm -# Install Google APIs client (for Google Workspace skill) -# NODE_PATH ensures globally installed modules are found by require() -ENV NODE_PATH=/usr/local/lib/node_modules -RUN npm install -g googleapis google-auth-library +# 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 @@ -37,7 +41,7 @@ RUN mkdir -p /root/.openclaw \ && mkdir -p /root/clawd/skills # Copy startup script -# Build cache bust: 2026-02-09-v32-script-node-path +# Build cache bust: 2026-02-09-v34-gogcli-auth-fix COPY start-openclaw.sh /usr/local/bin/start-openclaw.sh RUN chmod +x /usr/local/bin/start-openclaw.sh diff --git a/start-openclaw.sh b/start-openclaw.sh index dd9381d12..7b7ec7d9b 100644 --- a/start-openclaw.sh +++ b/start-openclaw.sh @@ -284,6 +284,37 @@ 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 + # ============================================================ # START GATEWAY # ============================================================ From bbb80151193605b599ae6fb867b596a18e1b79b0 Mon Sep 17 00:00:00 2001 From: Nicholas Scarabosio Date: Mon, 9 Feb 2026 17:09:36 -0700 Subject: [PATCH 07/24] Fix R2 sync config file detection using stdout instead of exitCode The sandbox SDK can have undefined exitCode for fast-exiting processes like `test -f`, causing the sync to falsely report "no config file found". Switch to ls + stdout parsing (FOUND/NOTFOUND) which is reliable regardless of exitCode timing. Co-Authored-By: Claude Opus 4.6 --- src/gateway/sync.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/gateway/sync.ts b/src/gateway/sync.ts index 63808c471..7e6bcb159 100644 --- a/src/gateway/sync.ts +++ b/src/gateway/sync.ts @@ -42,16 +42,23 @@ export async function syncToR2(sandbox: Sandbox, env: MoltbotEnv): Promise/dev/null && echo FOUND || echo NOTFOUND', + ); await waitForProcess(checkNew, 5000); - if (checkNew.exitCode !== 0) { - const checkLegacy = await sandbox.startProcess('test -f /root/.clawdbot/clawdbot.json'); + const checkNewLogs = await checkNew.getLogs(); + const newFound = checkNewLogs.stdout?.includes('FOUND') ?? false; + if (!newFound) { + const checkLegacy = await sandbox.startProcess( + 'ls /root/.clawdbot/clawdbot.json 2>/dev/null && echo FOUND || echo NOTFOUND', + ); await waitForProcess(checkLegacy, 5000); - if (checkLegacy.exitCode === 0) { + const checkLegacyLogs = await checkLegacy.getLogs(); + if (checkLegacyLogs.stdout?.includes('FOUND')) { configDir = '/root/.clawdbot'; } else { return { From 6f7c22f102e4ad21026af6b30ebd1ee53feac4f8 Mon Sep 17 00:00:00 2001 From: Nicholas Scarabosio Date: Mon, 9 Feb 2026 18:38:43 -0700 Subject: [PATCH 08/24] Add configurable agent model override via OPENCLAW_MODEL env var Allows switching the OpenClaw agent model (e.g. from Opus to Sonnet) without rebuilding the container, via wrangler secret. Co-Authored-By: Claude Opus 4.6 --- src/gateway/env.ts | 1 + src/types.ts | 2 ++ start-openclaw.sh | 8 ++++++++ 3 files changed, 11 insertions(+) diff --git a/src/gateway/env.ts b/src/gateway/env.ts index a4064cead..9c2c64520 100644 --- a/src/gateway/env.ts +++ b/src/gateway/env.ts @@ -49,6 +49,7 @@ 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; diff --git a/src/types.ts b/src/types.ts index 712de062e..13edf6c1d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -42,6 +42,8 @@ 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; diff --git a/start-openclaw.sh b/start-openclaw.sh index 7b7ec7d9b..0863ea823 100644 --- a/start-openclaw.sh +++ b/start-openclaw.sh @@ -189,6 +189,14 @@ 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); +} + // 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 From a2e4fb5269590b3244940736502c43059686f6a5 Mon Sep 17 00:00:00 2001 From: Nicholas Scarabosio Date: Mon, 9 Feb 2026 19:32:20 -0700 Subject: [PATCH 09/24] Reduce agent concurrency and route through Cloudflare AI Gateway Reduces maxConcurrent from 4 to 1 and subagents from 8 to 2 to avoid Anthropic API rate limits. AI Gateway adds caching, rate limit buffering, and request logging. Co-Authored-By: Claude Opus 4.6 --- start-openclaw.sh | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/start-openclaw.sh b/start-openclaw.sh index 0863ea823..7fcbbba3a 100644 --- a/start-openclaw.sh +++ b/start-openclaw.sh @@ -197,6 +197,15 @@ if (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 From 7e00a3f5dfb11ed920acc6d8318ad2545be20ad1 Mon Sep 17 00:00:00 2001 From: Nicholas Scarabosio Date: Thu, 12 Feb 2026 14:02:51 -0700 Subject: [PATCH 10/24] Add gateway token auth bypass for CF Access Allow ?token= query param to authenticate directly with the gateway token, bypassing CF Access JWT requirement. This enables direct URL access (e.g. browser bookmarks) without a CF Access self-hosted app. Extracts timingSafeEqual to shared src/utils/timing.ts utility. Co-Authored-By: Claude Opus 4.6 --- src/index.ts | 192 +++++++++++++++++++++++++++++++++++++++----- src/routes/cdp.ts | 111 +++++++++++++------------ src/utils/timing.ts | 14 ++++ 3 files changed, 246 insertions(+), 71 deletions(-) create mode 100644 src/utils/timing.ts diff --git a/src/index.ts b/src/index.ts index 53b06d3d2..cf111e1e1 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,9 +139,6 @@ 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(); }); @@ -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({ @@ -238,7 +328,9 @@ app.all('*', async (c) => { 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') !== undefined; const acceptsHtml = request.headers.get('Accept')?.includes('text/html'); if (!isGatewayReady && !isWebSocketRequest && acceptsHtml) { @@ -432,16 +524,7 @@ app.all('*', async (c) => { 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, - }); + return httpResponse; }); /** @@ -456,9 +539,67 @@ async function scheduled( const options = buildSandboxOptions(env); const sandbox = getSandbox(env.Sandbox, 'moltbot', 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'); + 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); + } return; } @@ -473,6 +614,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/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 Date: Thu, 12 Feb 2026 14:04:02 -0700 Subject: [PATCH 11/24] Improve process cleanup, gateway health checks, and container startup - Add runCommandWithCleanup utility to prevent zombie process accumulation - Add killAllGatewayProcesses to clear stuck gateway processes - Add health check to detect default Bun server (not real gateway) - Use rsync instead of cp for R2 restore (handles broken symlinks/.git) - Exclude .git dirs from R2 sync - Fix WebSocket detection (check Sec-WebSocket-Key for HTTP/2 edge) - Queue CDP messages during browser launch to prevent dropped commands - Fix build script to rename index.html to _admin-app.html (avoid asset collision) - Fix asset handling config (none instead of SPA/auto-trailing-slash) - Add Telegram webhook secret passthrough to container - Add browser profile config for CDP in container startup - Add openclaw doctor --fix to container startup validation - Add debug routes: /debug/net-test, /debug/cleanup - Change cron to every minute with process monitoring and cleanup - Remove debug headers from HTTP proxy responses Co-Authored-By: Claude Opus 4.6 --- Dockerfile | 2 +- package.json | 2 +- src/gateway/env.ts | 1 + src/gateway/index.ts | 4 +- src/gateway/process.test.ts | 66 +++++++++++++++++- src/gateway/process.ts | 130 +++++++++++++++++++++++++++++------- src/gateway/r2.ts | 19 +++--- src/gateway/sync.test.ts | 10 +-- src/gateway/sync.ts | 32 ++++----- src/gateway/utils.ts | 47 +++++++++++++ src/routes/admin-ui.ts | 2 +- src/routes/api.ts | 58 +++++++--------- src/routes/debug.ts | 109 ++++++++++++++++++------------ src/test-utils.ts | 16 ++++- src/types.ts | 1 + start-openclaw.sh | 36 +++++++--- wrangler.jsonc | 9 ++- 17 files changed, 386 insertions(+), 158 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3da1a01aa..7248af5ed 100644 --- a/Dockerfile +++ b/Dockerfile @@ -41,7 +41,7 @@ RUN mkdir -p /root/.openclaw \ && mkdir -p /root/clawd/skills # Copy startup script -# Build cache bust: 2026-02-09-v34-gogcli-auth-fix +# Build cache bust: 2026-02-12-v36-process-cleanup COPY start-openclaw.sh /usr/local/bin/start-openclaw.sh RUN chmod +x /usr/local/bin/start-openclaw.sh diff --git a/package.json b/package.json index c2801f422..5fa9e6fbf 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", diff --git a/src/gateway/env.ts b/src/gateway/env.ts index 9c2c64520..a91a8ebb6 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; 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..3be4c264b 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)); diff --git a/src/gateway/sync.ts b/src/gateway/sync.ts index 7e6bcb159..56d9814a1 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; @@ -46,19 +46,19 @@ export async function syncToR2(sandbox: Sandbox, env: MoltbotEnv): Promise/dev/null && echo FOUND || echo NOTFOUND', + 5000, ); - await waitForProcess(checkNew, 5000); - const checkNewLogs = await checkNew.getLogs(); - const newFound = checkNewLogs.stdout?.includes('FOUND') ?? false; + const newFound = checkNew.stdout.trim() === 'FOUND'; if (!newFound) { - const checkLegacy = await sandbox.startProcess( + const checkLegacy = await runCommandWithCleanup( + sandbox, 'ls /root/.clawdbot/clawdbot.json 2>/dev/null && echo FOUND || echo NOTFOUND', + 5000, ); - await waitForProcess(checkLegacy, 5000); - const checkLegacyLogs = await checkLegacy.getLogs(); - if (checkLegacyLogs.stdout?.includes('FOUND')) { + if (checkLegacy.stdout.trim() === 'FOUND') { configDir = '/root/.clawdbot'; } else { return { @@ -78,26 +78,22 @@ export async function syncToR2(sandbox: Sandbox, env: MoltbotEnv): Promise ${R2_MOUNT_PATH}/.last-sync`; + const syncCmd = `rsync -r --no-times --delete --exclude='*.lock' --exclude='*.log' --exclude='*.tmp' --exclude='.git' ${configDir}/ ${R2_MOUNT_PATH}/openclaw/ && rsync -r --no-times --delete --exclude='skills' --exclude='.git' /root/clawd/ ${R2_MOUNT_PATH}/workspace/ && rsync -r --no-times --delete --exclude='.git' /root/clawd/skills/ ${R2_MOUNT_PATH}/skills/ && date -Iseconds > ${R2_MOUNT_PATH}/.last-sync`; try { - const proc = await sandbox.startProcess(syncCmd); - await waitForProcess(proc, 30000); // 30 second timeout for sync + const syncResult = await runCommandWithCleanup(sandbox, syncCmd, 30000); // 30 second timeout for sync // 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(); + const timestampResult = await runCommandWithCleanup(sandbox, `cat ${R2_MOUNT_PATH}/.last-sync`, 5000); + const lastSync = timestampResult.stdout.trim(); if (lastSync && lastSync.match(/^\d{4}-\d{2}-\d{2}/)) { 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 || 'No timestamp file created', }; } } 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/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..d5bde00af 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -6,7 +6,7 @@ import { findExistingMoltbotProcess, mountR2Storage, syncToR2, - waitForProcess, + runCommandWithCleanup, } from '../gateway'; import { R2_MOUNT_PATH } from '../config'; @@ -41,14 +41,11 @@ adminApi.get('/devices', async (c) => { // 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 { @@ -63,8 +60,8 @@ adminApi.get('/devices', async (c) => { return c.json({ pending: [], paired: [], - raw: stdout, - stderr, + raw: result.stdout, + stderr: result.stderr, }); } catch { return c.json({ @@ -97,24 +94,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,13 +127,11 @@ 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 }> = []; @@ -150,7 +142,7 @@ adminApi.post('/devices/approve-all', async (c) => { 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 +155,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 +210,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/debug.ts b/src/routes/debug.ts index df89713d4..faef1e7fb 100644 --- a/src/routes/debug.ts +++ b/src/routes/debug.ts @@ -1,6 +1,6 @@ import { Hono } from 'hono'; import type { AppEnv } from '../types'; -import { findExistingMoltbotProcess } from '../gateway'; +import { findExistingMoltbotProcess, runCommandWithCleanup } from '../gateway'; /** * Debug routes for inspecting container state @@ -14,16 +14,12 @@ debug.get('/version', async (c) => { 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/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 13edf6c1d..b6c6c7154 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; diff --git a/start-openclaw.sh b/start-openclaw.sh index 7fcbbba3a..cb5b550f8 100644 --- a/start-openclaw.sh +++ b/start-openclaw.sh @@ -1,5 +1,5 @@ #!/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 @@ -8,11 +8,6 @@ set -e -if pgrep -f "openclaw gateway" > /dev/null 2>&1; then - echo "OpenClaw gateway is already running, exiting." - exit 0 -fi - CONFIG_DIR="/root/.openclaw" CONFIG_FILE="$CONFIG_DIR/openclaw.json" BACKUP_DIR="/data/moltbot" @@ -59,10 +54,11 @@ should_restore_from_r2() { } # Check for backup data in new openclaw/ prefix first, then legacy clawdbot/ prefix +# Use rsync instead of cp -a to handle broken symlinks/git objects in R2 (s3fs) 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/" + 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 R2 backup" fi @@ -70,7 +66,7 @@ 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/" + 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 # Rename the config file if it has the old name if [ -f "$CONFIG_DIR/clawdbot.json" ] && [ ! -f "$CONFIG_FILE" ]; then @@ -82,7 +78,7 @@ 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/" + rsync -r --no-times --exclude='.git' "$BACKUP_DIR/" "$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" @@ -102,7 +98,7 @@ if [ -d "$BACKUP_DIR/workspace" ] && [ "$(ls -A $BACKUP_DIR/workspace 2>/dev/nul if should_restore_from_r2; then echo "Restoring workspace from $BACKUP_DIR/workspace..." mkdir -p "$WORKSPACE_DIR" - cp -a "$BACKUP_DIR/workspace/." "$WORKSPACE_DIR/" + rsync -r --no-times --exclude='.git' "$BACKUP_DIR/workspace/" "$WORKSPACE_DIR/" 2>&1 || true echo "Restored workspace from R2 backup" fi fi @@ -113,7 +109,7 @@ if [ -d "$BACKUP_DIR/skills" ] && [ "$(ls -A $BACKUP_DIR/skills 2>/dev/null)" ]; if should_restore_from_r2; then echo "Restoring skills from $BACKUP_DIR/skills..." mkdir -p "$SKILLS_DIR" - cp -a "$BACKUP_DIR/skills/." "$SKILLS_DIR/" + rsync -r --no-times --exclude='.git' "$BACKUP_DIR/skills/" "$SKILLS_DIR/" 2>&1 || true echo "Restored skills from R2 backup" fi fi @@ -297,6 +293,17 @@ 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 @@ -332,6 +339,12 @@ 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..." +openclaw doctor --fix 2>&1 || true + # ============================================================ # START GATEWAY # ============================================================ @@ -350,3 +363,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/wrangler.jsonc b/wrangler.jsonc index 0c9524893..4b13cb5e1 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -9,11 +9,10 @@ "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", + "not_found_handling": "none", + "html_handling": "none", "binding": "ASSETS", "run_worker_first": true, }, @@ -75,10 +74,10 @@ "remote": true }, ], - // Cron trigger to sync moltbot data to R2 every 5 minutes + // Cron trigger: every minute to keep gateway alive + sync data to R2 "triggers": { "crons": [ - "*/5 * * * *" + "* * * * *" ], }, // Browser Rendering binding for CDP shim From 2ccb34a1120597274a59d2bf1d2ee95bee018230 Mon Sep 17 00:00:00 2001 From: Nicholas Scarabosio Date: Thu, 12 Feb 2026 14:11:46 -0700 Subject: [PATCH 12/24] Reset sandbox container by changing DO instance ID to moltbot-v2 The previous container was stuck serving the default Bun server instead of the OpenClaw gateway. Changing the stable ID forces Cloudflare to create a fresh DO instance. All data is preserved in R2 and restored automatically on startup. Co-Authored-By: Claude Opus 4.6 --- src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index cf111e1e1..e9f2648d9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -145,7 +145,7 @@ app.use('*', async (c, 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-v2', options); c.set('sandbox', sandbox); await next(); }); @@ -537,7 +537,7 @@ async function scheduled( _ctx: ExecutionContext, ): Promise { const options = buildSandboxOptions(env); - const sandbox = getSandbox(env.Sandbox, 'moltbot', options); + const sandbox = getSandbox(env.Sandbox, 'moltbot-v2', options); // Monitor total process count for leaks let processCount = 0; From 56d48936a13f7c45e4781f2e585b65ccf845ccb1 Mon Sep 17 00:00:00 2001 From: Nicholas Scarabosio Date: Sat, 14 Feb 2026 14:27:16 -0700 Subject: [PATCH 13/24] Fix catch-all proxy treating all requests as WebSocket due to null vs undefined check Headers.get() returns null for missing headers, but the WebSocket detection used `!== undefined` which is always truthy. This caused every HTTP request to enter the WebSocket branch and get proxied via wsConnect, returning the default Bun server response instead of the OpenClaw gateway UI. Also fixes: stdout reference bugs in api.ts, gateway.mode binding issue in start-openclaw.sh, adds FRESH_START env passthrough, adds doctor timeout, and simplifies R2 restore with timeouts for fresh containers. Co-Authored-By: Claude Opus 4.6 --- Dockerfile | 2 +- src/gateway/env.ts | 1 + src/index.ts | 72 ++++++++++------------------ src/routes/api.ts | 8 ++-- src/routes/public.ts | 3 +- src/types.ts | 1 + start-openclaw.sh | 112 +++++++++++++++++++------------------------ 7 files changed, 85 insertions(+), 114 deletions(-) diff --git a/Dockerfile b/Dockerfile index 7248af5ed..81bee3bbe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -41,7 +41,7 @@ RUN mkdir -p /root/.openclaw \ && mkdir -p /root/clawd/skills # Copy startup script -# Build cache bust: 2026-02-12-v36-process-cleanup +# 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/src/gateway/env.ts b/src/gateway/env.ts index a91a8ebb6..4e80d7e4d 100644 --- a/src/gateway/env.ts +++ b/src/gateway/env.ts @@ -54,6 +54,7 @@ export function buildEnvVars(env: MoltbotEnv): Record { 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/index.ts b/src/index.ts index e9f2648d9..6cede2692 100644 --- a/src/index.ts +++ b/src/index.ts @@ -323,52 +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' || - request.headers.get('Sec-WebSocket-Key') !== undefined; + 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); - return c.json( - { - error: 'Moltbot gateway failed to start', - details: errorMessage, - hint, - }, - 503, - ); + 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); + } } // Proxy to Moltbot with WebSocket message interception @@ -519,12 +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); - - return httpResponse; }); /** diff --git a/src/routes/api.ts b/src/routes/api.ts index d5bde00af..b9630e16a 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -50,7 +50,7 @@ adminApi.get('/devices', async (c) => { // 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); @@ -67,8 +67,8 @@ adminApi.get('/devices', async (c) => { return c.json({ pending: [], paired: [], - raw: stdout, - stderr, + raw: result.stdout, + stderr: result.stderr, parseError: 'Failed to parse CLI output', }); } @@ -136,7 +136,7 @@ adminApi.post('/devices/approve-all', async (c) => { // 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 || []; 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/types.ts b/src/types.ts index b6c6c7154..3f5d91c65 100644 --- a/src/types.ts +++ b/src/types.ts @@ -49,6 +49,7 @@ export interface MoltbotEnv { 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/start-openclaw.sh b/start-openclaw.sh index cb5b550f8..affd03ecd 100644 --- a/start-openclaw.sh +++ b/start-openclaw.sh @@ -6,7 +6,8 @@ # 3. Patches config for features onboard doesn't cover (channels, gateway auth) # 4. Starts the gateway -set -e +# 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" @@ -21,98 +22,80 @@ 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 + +# Check for backup data in new openclaw/ prefix first, then legacy clawdbot/ prefix +# Use rsync instead of cp -a to handle broken symlinks/git objects in R2 (s3fs) +# Wrap in timeout — s3fs file checks can hang on stale mounts in fresh containers +echo "Attempting R2 restore (30s timeout)..." +timeout 30 bash -c ' +BACKUP_DIR="/data/moltbot" +CONFIG_DIR="/root/.openclaw" +CONFIG_FILE="$CONFIG_DIR/openclaw.json" + +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 -# Use rsync instead of cp -a to handle broken symlinks/git objects in R2 (s3fs) if [ -f "$BACKUP_DIR/openclaw/openclaw.json" ]; then - if should_restore_from_r2; then + if should_restore; then echo "Restoring from R2 backup at $BACKUP_DIR/openclaw..." - rsync -r --no-times --exclude='.git' "$BACKUP_DIR/openclaw/" "$CONFIG_DIR/" 2>&1 || true + 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 R2 backup" fi elif [ -f "$BACKUP_DIR/clawdbot/clawdbot.json" ]; then - # Legacy backup format — migrate .clawdbot data into .openclaw - if should_restore_from_r2; then + if should_restore; then echo "Restoring from legacy R2 backup at $BACKUP_DIR/clawdbot..." - rsync -r --no-times --exclude='.git' "$BACKUP_DIR/clawdbot/" "$CONFIG_DIR/" 2>&1 || true + 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 - # 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..." - rsync -r --no-times --exclude='.git' "$BACKUP_DIR/" "$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 flat legacy R2 backup" - fi elif [ -d "$BACKUP_DIR" ]; then echo "R2 mounted at $BACKUP_DIR but no backup data found yet" else echo "R2 not mounted, starting fresh" fi +' || echo "WARNING: R2 restore timed out or failed — continuing without R2 data" -# Restore workspace from R2 backup if available (only if R2 is newer) -# This includes IDENTITY.md, USER.md, MEMORY.md, memory/, and assets/ +# Restore workspace and skills from R2 (also under timeout) +timeout 20 bash -c ' +BACKUP_DIR="/data/moltbot" WORKSPACE_DIR="/root/clawd" +SKILLS_DIR="/root/clawd/skills" + 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" - rsync -r --no-times --exclude='.git' "$BACKUP_DIR/workspace/" "$WORKSPACE_DIR/" 2>&1 || true - echo "Restored workspace from R2 backup" - fi + echo "Restoring workspace from $BACKUP_DIR/workspace..." + mkdir -p "$WORKSPACE_DIR" + rsync -r --no-times --exclude=".git" "$BACKUP_DIR/workspace/" "$WORKSPACE_DIR/" 2>&1 || true + echo "Restored workspace from R2 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" - rsync -r --no-times --exclude='.git' "$BACKUP_DIR/skills/" "$SKILLS_DIR/" 2>&1 || true - echo "Restored skills from R2 backup" - fi + echo "Restoring skills from $BACKUP_DIR/skills..." + mkdir -p "$SKILLS_DIR" + rsync -r --no-times --exclude=".git" "$BACKUP_DIR/skills/" "$SKILLS_DIR/" 2>&1 || true + echo "Restored skills from R2 backup" fi +' || echo "WARNING: Workspace/skills restore timed out or failed — continuing" + +fi # end FRESH_START skip # ============================================================ # ONBOARD (only if no config exists yet) @@ -132,16 +115,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 @@ -172,7 +157,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) { @@ -343,7 +331,7 @@ fi # VALIDATE & FIX CONFIG # ============================================================ echo "Running openclaw doctor --fix to validate config..." -openclaw doctor --fix 2>&1 || true +timeout 30 openclaw doctor --fix 2>&1 || echo "WARNING: openclaw doctor timed out or failed — continuing anyway" # ============================================================ # START GATEWAY From f214420a74eeb7ceab42af4c735985a55b8e5461 Mon Sep 17 00:00:00 2001 From: Nicholas Scarabosio Date: Sat, 14 Feb 2026 15:25:21 -0700 Subject: [PATCH 14/24] Fix admin UI spinning forever by replacing 3-min gateway wait with quick pre-check The /api/admin/devices endpoint called ensureMoltbotGateway() which blocked for up to 3 minutes waiting for the gateway port. Now does a 5-second port check and returns a gatewayStatus immediately if not ready. The frontend auto-retries every 3 seconds and shows "Gateway is starting up..." instead of an indefinite spinner. Co-Authored-By: Claude Opus 4.6 --- src/client/api.ts | 1 + src/client/pages/AdminPage.tsx | 18 +++++++++++++++++- src/routes/api.ts | 29 ++++++++++++++++++++++++++--- 3 files changed, 44 insertions(+), 4 deletions(-) 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/routes/api.ts b/src/routes/api.ts index b9630e16a..c5ff5c0f3 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -8,7 +8,7 @@ import { syncToR2, 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,14 +29,37 @@ 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; From 112ae1db232ebabb3866460f8809be08508d4f41 Mon Sep 17 00:00:00 2001 From: Nicholas Scarabosio Date: Sat, 14 Feb 2026 15:34:02 -0700 Subject: [PATCH 15/24] Reset sandbox container to moltbot-v3 to break DO reset loop after deploy Co-Authored-By: Claude Opus 4.6 --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 6cede2692..dfde4190a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -145,7 +145,7 @@ app.use('*', async (c, next) => { // Middleware: Initialize sandbox for all requests app.use('*', async (c, next) => { const options = buildSandboxOptions(c.env); - const sandbox = getSandbox(c.env.Sandbox, 'moltbot-v2', options); + const sandbox = getSandbox(c.env.Sandbox, 'moltbot-v3', options); c.set('sandbox', sandbox); await next(); }); From cc665b887906e1596223f00fac84467feba28392 Mon Sep 17 00:00:00 2001 From: Nicholas Scarabosio Date: Sat, 14 Feb 2026 16:10:03 -0700 Subject: [PATCH 16/24] Update @cloudflare/sandbox SDK and base image from 0.7.0 to 0.7.4 Fix DO reset loop that prevents container from starting after deploys. Co-Authored-By: Claude Opus 4.6 --- Dockerfile | 2 +- package-lock.json | 531 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 2 +- 3 files changed, 530 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 81bee3bbe..fae11719f 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 diff --git a/package-lock.json b/package-lock.json index 2e5494df7..47c140656 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", @@ -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,6 +440,23 @@ "wrangler": "^4.60.0" } }, + "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": [ @@ -449,6 +472,57 @@ "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.20260124.0", "dev": true, @@ -465,6 +539,17 @@ "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", @@ -934,6 +1019,29 @@ "@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": [ @@ -949,6 +1057,423 @@ "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": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "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": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "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": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", diff --git a/package.json b/package.json index 5fa9e6fbf..1ad20f8af 100644 --- a/package.json +++ b/package.json @@ -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", From d272a45c7414cb61a16d63f281abddfb60e4b727 Mon Sep 17 00:00:00 2001 From: Nicholas Scarabosio Date: Mon, 16 Feb 2026 06:36:09 -0700 Subject: [PATCH 17/24] Fix R2 backup: always sync after cron, use correct workspace path The cron handler returned early after gateway restarts, skipping the R2 backup sync entirely. If the gateway was frequently restarting, backups would never run. Also fixed the workspace sync/restore path from /root/clawd/ to /root/.openclaw/workspace/ where OpenClaw actually stores MEMORY.md, IDENTITY.md, and other workspace files. Co-Authored-By: Claude Opus 4.6 --- src/gateway/sync.ts | 6 +++--- src/index.ts | 2 +- start-openclaw.sh | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/gateway/sync.ts b/src/gateway/sync.ts index 56d9814a1..d6dea3bd5 100644 --- a/src/gateway/sync.ts +++ b/src/gateway/sync.ts @@ -22,7 +22,7 @@ export interface SyncResult { * * Syncs three directories: * - Config: /root/.openclaw/ (or /root/.clawdbot/) → R2:/openclaw/ - * - Workspace: /root/clawd/ → R2:/workspace/ (IDENTITY.md, MEMORY.md, memory/, assets/) + * - Workspace: /root/.openclaw/workspace/ → R2:/workspace/ (IDENTITY.md, MEMORY.md, memory/, assets/) * - Skills: /root/clawd/skills/ → R2:/skills/ * * @param sandbox - The sandbox instance @@ -77,8 +77,8 @@ export async function syncToR2(sandbox: Sandbox, env: MoltbotEnv): Promise ${R2_MOUNT_PATH}/.last-sync`; + // Also sync workspace directory (OpenClaw stores MEMORY.md, IDENTITY.md, etc. in /root/.openclaw/workspace/) + const syncCmd = `rsync -r --no-times --delete --exclude='*.lock' --exclude='*.log' --exclude='*.tmp' --exclude='.git' ${configDir}/ ${R2_MOUNT_PATH}/openclaw/ && rsync -r --no-times --delete --exclude='.git' /root/.openclaw/workspace/ ${R2_MOUNT_PATH}/workspace/ && rsync -r --no-times --delete --exclude='.git' /root/clawd/skills/ ${R2_MOUNT_PATH}/skills/ && date -Iseconds > ${R2_MOUNT_PATH}/.last-sync`; try { const syncResult = await runCommandWithCleanup(sandbox, syncCmd, 30000); // 30 second timeout for sync diff --git a/src/index.ts b/src/index.ts index dfde4190a..c19177521 100644 --- a/src/index.ts +++ b/src/index.ts @@ -580,7 +580,7 @@ async function scheduled( } catch (error) { console.error('[cron] Gateway restart failed:', error); } - return; + // Fall through to sync — don't skip backup just because gateway was restarted } console.log('[cron] Starting backup sync to R2...'); diff --git a/start-openclaw.sh b/start-openclaw.sh index affd03ecd..5b7ffa3c7 100644 --- a/start-openclaw.sh +++ b/start-openclaw.sh @@ -77,7 +77,7 @@ fi # Restore workspace and skills from R2 (also under timeout) timeout 20 bash -c ' BACKUP_DIR="/data/moltbot" -WORKSPACE_DIR="/root/clawd" +WORKSPACE_DIR="/root/.openclaw/workspace" SKILLS_DIR="/root/clawd/skills" if [ -d "$BACKUP_DIR/workspace" ] && [ "$(ls -A $BACKUP_DIR/workspace 2>/dev/null)" ]; then From aeb07488ed1c95c29752b30de7af2866103d7851 Mon Sep 17 00:00:00 2001 From: Nicholas Scarabosio Date: Mon, 16 Feb 2026 06:44:38 -0700 Subject: [PATCH 18/24] =?UTF-8?q?Fix=20cron=20handler=20using=20wrong=20sa?= =?UTF-8?q?ndbox=20instance=20ID=20(moltbot-v2=20=E2=86=92=20moltbot-v3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The request handler was updated to moltbot-v3 in 112ae1d but the cron handler was not, so the cron was starting the gateway in a different sandbox instance than requests were routed to. This caused the admin page to spin forever since it never found a running gateway. Co-Authored-By: Claude Opus 4.6 --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index c19177521..8e0654324 100644 --- a/src/index.ts +++ b/src/index.ts @@ -517,7 +517,7 @@ async function scheduled( _ctx: ExecutionContext, ): Promise { const options = buildSandboxOptions(env); - const sandbox = getSandbox(env.Sandbox, 'moltbot-v2', options); + const sandbox = getSandbox(env.Sandbox, 'moltbot-v3', options); // Monitor total process count for leaks let processCount = 0; From 8116c9ec12877bc28423d766b48fd62a0ecf233d Mon Sep 17 00:00:00 2001 From: Nicholas Scarabosio Date: Mon, 16 Feb 2026 09:53:17 -0700 Subject: [PATCH 19/24] Fix R2 sync config check: ls outputs filename before FOUND marker The check `stdout.trim() === 'FOUND'` always failed because `ls` outputs the filename to stdout before `echo FOUND`, producing multi-line output. Changed to `includes('FOUND') && !includes('NOTFOUND')` to correctly detect the marker. This was the root cause of backups not running. Co-Authored-By: Claude Opus 4.6 --- src/gateway/sync.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gateway/sync.ts b/src/gateway/sync.ts index d6dea3bd5..e5ab80b3e 100644 --- a/src/gateway/sync.ts +++ b/src/gateway/sync.ts @@ -51,14 +51,14 @@ export async function syncToR2(sandbox: Sandbox, env: MoltbotEnv): Promise/dev/null && echo FOUND || echo NOTFOUND', 5000, ); - const newFound = checkNew.stdout.trim() === 'FOUND'; + 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.trim() === 'FOUND') { + if (checkLegacy.stdout.includes('FOUND') && !checkLegacy.stdout.includes('NOTFOUND')) { configDir = '/root/.clawdbot'; } else { return { From 995921578c1e5962be666f48a5b8033a52fd40d5 Mon Sep 17 00:00:00 2001 From: Nicholas Scarabosio Date: Mon, 16 Feb 2026 10:09:02 -0700 Subject: [PATCH 20/24] Fix R2 sync failing when workspace directory doesn't exist The workspace rsync failed silently on fresh containers because /root/.openclaw/workspace/ didn't exist, breaking the && chain and leaving the old .last-sync timestamp. Added mkdir -p before rsync to ensure the directory exists. Co-Authored-By: Claude Opus 4.6 --- src/gateway/sync.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/gateway/sync.ts b/src/gateway/sync.ts index e5ab80b3e..1577ca745 100644 --- a/src/gateway/sync.ts +++ b/src/gateway/sync.ts @@ -76,9 +76,16 @@ export async function syncToR2(sandbox: Sandbox, env: MoltbotEnv): Promise ${R2_MOUNT_PATH}/.last-sync`; + // Ensure workspace directory exists (may not exist in fresh containers) + // mkdir -p is safe to run even if the directory already exists + const syncParts = [ + `mkdir -p /root/.openclaw/workspace`, + `rsync -r --no-times --delete --exclude='*.lock' --exclude='*.log' --exclude='*.tmp' --exclude='.git' ${configDir}/ ${R2_MOUNT_PATH}/openclaw/`, + `rsync -r --no-times --delete --exclude='.git' /root/.openclaw/workspace/ ${R2_MOUNT_PATH}/workspace/`, + `rsync -r --no-times --delete --exclude='.git' /root/clawd/skills/ ${R2_MOUNT_PATH}/skills/`, + `date -Iseconds > ${R2_MOUNT_PATH}/.last-sync`, + ]; + const syncCmd = syncParts.join(' && '); try { const syncResult = await runCommandWithCleanup(sandbox, syncCmd, 30000); // 30 second timeout for sync From 964e2c1f03df2f12949e74474813959360d58036 Mon Sep 17 00:00:00 2001 From: Nicholas Scarabosio Date: Mon, 16 Feb 2026 11:03:02 -0700 Subject: [PATCH 21/24] Switch R2 backup from rsync to tar archives for s3fs performance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rsync over s3fs does one HTTP request per file, making backups of even small directories take 30+ seconds and time out. Switched to tar archives — create compressed archive locally then copy a single file to R2 (3 writes total instead of hundreds). Restore falls back to legacy rsync-style directories for backward compatibility. Co-Authored-By: Claude Opus 4.6 --- src/gateway/sync.test.ts | 14 +++---- src/gateway/sync.ts | 46 ++++++++++++++-------- start-openclaw.sh | 84 ++++++++++++++++++++++------------------ 3 files changed, 83 insertions(+), 61 deletions(-) diff --git a/src/gateway/sync.test.ts b/src/gateway/sync.test.ts index 3be4c264b..8ffbccfd1 100644 --- a/src/gateway/sync.test.ts +++ b/src/gateway/sync.test.ts @@ -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 1577ca745..1a38f83c7 100644 --- a/src/gateway/sync.ts +++ b/src/gateway/sync.ts @@ -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/.openclaw/workspace/ → 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 @@ -76,31 +77,42 @@ export async function syncToR2(sandbox: Sandbox, env: MoltbotEnv): Promise ${R2_MOUNT_PATH}/.last-sync`, ]; const syncCmd = syncParts.join(' && '); try { - const syncResult = await runCommandWithCleanup(sandbox, syncCmd, 30000); // 30 second timeout for sync + const syncResult = await runCommandWithCleanup(sandbox, syncCmd, 60000); // 60s timeout - // Check for success by reading the timestamp file - const timestampResult = await runCommandWithCleanup(sandbox, `cat ${R2_MOUNT_PATH}/.last-sync`, 5000); + // 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 { return { success: false, error: 'Sync failed', - details: syncResult.stderr || syncResult.stdout || 'No timestamp file created', + details: syncResult.stderr || syncResult.stdout || 'Tar/copy chain failed — timestamp not written', }; } } catch (err) { diff --git a/start-openclaw.sh b/start-openclaw.sh index 5b7ffa3c7..73d9047eb 100644 --- a/start-openclaw.sh +++ b/start-openclaw.sh @@ -27,14 +27,15 @@ if [ "$FRESH_START" = "true" ]; then echo "FRESH_START=true — skipping R2 restore entirely" else -# Check for backup data in new openclaw/ prefix first, then legacy clawdbot/ prefix -# Use rsync instead of cp -a to handle broken symlinks/git objects in R2 (s3fs) -# Wrap in timeout — s3fs file checks can hang on stale mounts in fresh containers -echo "Attempting R2 restore (30s timeout)..." -timeout 30 bash -c ' +# 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" @@ -50,50 +51,59 @@ should_restore() { return 0 } -if [ -f "$BACKUP_DIR/openclaw/openclaw.json" ]; then - if should_restore; then - echo "Restoring from 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 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 - if should_restore; then - echo "Restoring from legacy R2 backup at $BACKUP_DIR/clawdbot..." - 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 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 -' || echo "WARNING: R2 restore timed out or failed — continuing without R2 data" -# Restore workspace and skills from R2 (also under timeout) -timeout 20 bash -c ' -BACKUP_DIR="/data/moltbot" -WORKSPACE_DIR="/root/.openclaw/workspace" -SKILLS_DIR="/root/clawd/skills" - -if [ -d "$BACKUP_DIR/workspace" ] && [ "$(ls -A $BACKUP_DIR/workspace 2>/dev/null)" ]; then - echo "Restoring workspace from $BACKUP_DIR/workspace..." - mkdir -p "$WORKSPACE_DIR" +# --- 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 R2 backup" + echo "Restored workspace from legacy rsync backup" fi -if [ -d "$BACKUP_DIR/skills" ] && [ "$(ls -A $BACKUP_DIR/skills 2>/dev/null)" ]; then - echo "Restoring skills from $BACKUP_DIR/skills..." - mkdir -p "$SKILLS_DIR" +# --- 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 R2 backup" + echo "Restored skills from legacy rsync backup" fi -' || echo "WARNING: Workspace/skills restore timed out or failed — continuing" +' || echo "WARNING: R2 restore timed out or failed — continuing without R2 data" fi # end FRESH_START skip From 18ec00dc29b0a6eef850b9a9ae7d898ce7bd9280 Mon Sep 17 00:00:00 2001 From: Nicholas Scarabosio Date: Mon, 16 Feb 2026 11:05:29 -0700 Subject: [PATCH 22/24] Update wrangler from 4.50 to 4.65 Co-Authored-By: Claude Opus 4.6 --- package-lock.json | 892 ++++++++++++++++++++++++++++++++++++++++------ package.json | 2 +- 2 files changed, 782 insertions(+), 112 deletions(-) diff --git a/package-lock.json b/package-lock.json index 47c140656..e0de38209 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": { @@ -440,6 +440,525 @@ "wrangler": "^4.60.0" } }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "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", @@ -524,7 +1043,9 @@ } }, "node_modules/@cloudflare/workers-types": { - "version": "4.20260124.0", + "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" }, @@ -619,7 +1140,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.0", + "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" ], @@ -2906,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", @@ -2917,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" ], @@ -2963,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" ], @@ -2980,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" ], @@ -2997,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" ], @@ -3014,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" ], @@ -3031,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" ], @@ -3048,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" ], @@ -3065,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" ], @@ -3082,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" ], @@ -3099,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" ], @@ -3116,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" ], @@ -3133,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" ], @@ -3150,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" ], @@ -3167,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" ], @@ -3184,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" ], @@ -3201,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" ], @@ -3218,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" ], @@ -3235,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" ], @@ -3252,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" ], @@ -3269,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" ], @@ -3286,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" ], @@ -3303,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" ], @@ -3320,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" ], @@ -3337,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" ], @@ -3354,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" ], @@ -4760,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", @@ -4784,7 +5311,7 @@ "fsevents": "~2.3.2" }, "peerDependencies": { - "@cloudflare/workers-types": "^4.20260120.0" + "@cloudflare/workers-types": "^4.20260212.0" }, "peerDependenciesMeta": { "@cloudflare/workers-types": { @@ -4792,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 1ad20f8af..26ffb4adf 100644 --- a/package.json +++ b/package.json @@ -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", From 427e837f5893b4e13cc9077c8ba4ea908b6dcd6f Mon Sep 17 00:00:00 2001 From: Nicholas Scarabosio Date: Mon, 16 Feb 2026 11:08:51 -0700 Subject: [PATCH 23/24] Revert instance_type to standard (matches current deploy config) Co-Authored-By: Claude Opus 4.6 --- wrangler.jsonc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrangler.jsonc b/wrangler.jsonc index 4b13cb5e1..ebd13d6c0 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -42,7 +42,7 @@ { "class_name": "Sandbox", "image": "./Dockerfile", - "instance_type": "standard-1", + "instance_type": "standard", "max_instances": 1, }, ], From 151ecd166ce07ceda574eb2f8c6a45c0ac04a809 Mon Sep 17 00:00:00 2001 From: Nicholas Scarabosio Date: Mon, 16 Feb 2026 11:25:11 -0700 Subject: [PATCH 24/24] Add default workspace files (USER.md, SOUL.md, MEMORY.md) to Docker image Bake workspace files into the container image as seeds. The startup script copies them to /root/.openclaw/workspace/ only if the files don't already exist from R2 restore, so bot-modified versions persist. Co-Authored-By: Claude Opus 4.6 --- Dockerfile | 5 + start-openclaw.sh | 19 ++ workspace/MEMORY.md | 568 ++++++++++++++++++++++++++++++++++++++++++++ workspace/SOUL.md | 71 ++++++ workspace/USER.md | 71 ++++++ 5 files changed, 734 insertions(+) create mode 100644 workspace/MEMORY.md create mode 100644 workspace/SOUL.md create mode 100644 workspace/USER.md diff --git a/Dockerfile b/Dockerfile index fae11719f..ceeed20f8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -37,9 +37,14 @@ 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-14-v41-doctor-timeout-and-host-header-fix COPY start-openclaw.sh /usr/local/bin/start-openclaw.sh diff --git a/start-openclaw.sh b/start-openclaw.sh index 73d9047eb..5ff76eeb0 100644 --- a/start-openclaw.sh +++ b/start-openclaw.sh @@ -107,6 +107,25 @@ fi 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 + # ============================================================ # ONBOARD (only if no config exists yet) # ============================================================ 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