diff --git a/Dockerfile b/Dockerfile index 340183e46..3da1a01aa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,6 +20,15 @@ RUN ARCH="$(dpkg --print-architecture)" \ # Install pnpm globally RUN npm install -g pnpm +# Install gogcli (Google Workspace CLI for Gmail/Calendar/Drive) +# OpenClaw has a built-in 'gog' skill that wraps this CLI +ENV GOG_VERSION=0.9.0 +RUN ARCH="$(dpkg --print-architecture)" \ + && curl -fsSL "https://github.com/steipete/gogcli/releases/download/v${GOG_VERSION}/gogcli_${GOG_VERSION}_linux_${ARCH}.tar.gz" -o /tmp/gogcli.tar.gz \ + && tar -xzf /tmp/gogcli.tar.gz -C /usr/local/bin gog \ + && rm /tmp/gogcli.tar.gz \ + && gog --version + # Install OpenClaw (formerly clawdbot/moltbot) # Pin to specific version for reproducible builds RUN npm install -g openclaw@2026.2.3 \ @@ -32,7 +41,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-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/skills/google-workspace/SKILL.md b/skills/google-workspace/SKILL.md new file mode 100644 index 000000000..35b7372f3 --- /dev/null +++ b/skills/google-workspace/SKILL.md @@ -0,0 +1,55 @@ +--- +name: google-workspace +description: Access Gmail and Google Calendar via Google APIs. Search/read/send email and list/create calendar events. Requires GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, and GOOGLE_REFRESH_TOKEN env vars. +--- + +# Google Workspace + +Access Gmail and Google Calendar from the container via Google APIs with OAuth2 authentication. + +## Prerequisites + +- `GOOGLE_CLIENT_ID` environment variable set +- `GOOGLE_CLIENT_SECRET` environment variable set +- `GOOGLE_REFRESH_TOKEN` environment variable set + +## Quick Start + +### Search Gmail +```bash +node /root/clawd/skills/google-workspace/scripts/gmail-search.js "from:someone@example.com" --max 10 +``` + +### Read an email +```bash +node /root/clawd/skills/google-workspace/scripts/gmail-read.js +``` + +### Send an email +```bash +node /root/clawd/skills/google-workspace/scripts/gmail-send.js --to user@example.com --subject "Hello" --body "Message body" +``` + +### List calendar events +```bash +node /root/clawd/skills/google-workspace/scripts/calendar-events.js primary --from 2026-02-09 --to 2026-02-10 +``` + +### Create a calendar event +```bash +node /root/clawd/skills/google-workspace/scripts/calendar-create.js primary --summary "Meeting" --start "2026-02-10T10:00:00" --end "2026-02-10T11:00:00" +``` + +## Available Scripts + +| Script | Purpose | +|--------|---------| +| `gmail-search.js` | Search Gmail messages by query | +| `gmail-read.js` | Read full content of a single email | +| `gmail-send.js` | Send an email | +| `calendar-events.js` | List calendar events in a date range | +| `calendar-create.js` | Create a new calendar event | + +## Output Format + +Gmail search and calendar events output TSV (tab-separated values) for easy parsing, matching the format used by gogcli. diff --git a/skills/google-workspace/scripts/calendar-create.js b/skills/google-workspace/scripts/calendar-create.js new file mode 100644 index 000000000..b05ac3b0e --- /dev/null +++ b/skills/google-workspace/scripts/calendar-create.js @@ -0,0 +1,80 @@ +#!/usr/bin/env node +/** + * Calendar Create Event + * + * Usage: node calendar-create.js --summary --start --end [--description ] [--location ] + * + * Creates a new calendar event. + * + * calendarId: 'primary' for the user's main calendar, or a specific calendar ID + * Datetimes: ISO 8601 format (e.g., 2026-02-10T10:00:00) + */ + +const { getCalendar } = require('./google-auth'); + +function parseArgs(args) { + const result = { calendarId: 'primary' }; + for (let i = 0; i < args.length; i++) { + if (args[i] === '--summary' && args[i + 1]) { + result.summary = args[++i]; + } else if (args[i] === '--start' && args[i + 1]) { + result.start = args[++i]; + } else if (args[i] === '--end' && args[i + 1]) { + result.end = args[++i]; + } else if (args[i] === '--description' && args[i + 1]) { + result.description = args[++i]; + } else if (args[i] === '--location' && args[i + 1]) { + result.location = args[++i]; + } else if (!args[i].startsWith('--')) { + result.calendarId = args[i]; + } + } + return result; +} + +function toEventDateTime(dateStr) { + if (!dateStr) return undefined; + // All-day event: just a date + if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) { + return { date: dateStr }; + } + // Datetime event + let dt = dateStr; + if (!dt.includes('Z') && !dt.includes('+') && !dt.includes('-', 10)) { + dt += ':00'; // Ensure seconds + } + return { dateTime: dt, timeZone: 'America/Los_Angeles' }; +} + +async function main() { + const opts = parseArgs(process.argv.slice(2)); + + if (!opts.summary || !opts.start || !opts.end) { + console.error('Usage: node calendar-create.js --summary --start --end [--description ] [--location ]'); + process.exit(1); + } + + const calendar = getCalendar(); + + const event = { + summary: opts.summary, + start: toEventDateTime(opts.start), + end: toEventDateTime(opts.end), + }; + + if (opts.description) event.description = opts.description; + if (opts.location) event.location = opts.location; + + const res = await calendar.events.insert({ + calendarId: opts.calendarId, + requestBody: event, + }); + + console.log(`Created event: ${res.data.id}`); + console.log(`Link: ${res.data.htmlLink}`); +} + +main().catch(err => { + console.error('Error:', err.message); + process.exit(1); +}); diff --git a/skills/google-workspace/scripts/calendar-events.js b/skills/google-workspace/scripts/calendar-events.js new file mode 100644 index 000000000..a12142b8b --- /dev/null +++ b/skills/google-workspace/scripts/calendar-events.js @@ -0,0 +1,81 @@ +#!/usr/bin/env node +/** + * Calendar Events + * + * Usage: node calendar-events.js --from --to [--max N] + * + * Lists calendar events using events.list API. + * Outputs: ID, start, end, summary (TSV format) + * + * calendarId: 'primary' for the user's main calendar, or a specific calendar ID + * Dates: YYYY-MM-DD or ISO 8601 datetime + */ + +const { getCalendar } = require('./google-auth'); + +function parseArgs(args) { + const result = { calendarId: 'primary', maxResults: 50 }; + for (let i = 0; i < args.length; i++) { + if (args[i] === '--from' && args[i + 1]) { + result.from = args[++i]; + } else if (args[i] === '--to' && args[i + 1]) { + result.to = args[++i]; + } else if (args[i] === '--max' && args[i + 1]) { + result.maxResults = parseInt(args[++i], 10); + } else if (!args[i].startsWith('--')) { + result.calendarId = args[i]; + } + } + return result; +} + +function toRFC3339(dateStr) { + if (!dateStr) return undefined; + // If it's just a date (YYYY-MM-DD), append time + if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) { + return dateStr + 'T00:00:00Z'; + } + // If no timezone info, assume UTC + if (!dateStr.includes('Z') && !dateStr.includes('+') && !dateStr.includes('-', 10)) { + return dateStr + 'Z'; + } + return dateStr; +} + +async function main() { + const opts = parseArgs(process.argv.slice(2)); + + const calendar = getCalendar(); + + const params = { + calendarId: opts.calendarId, + maxResults: opts.maxResults, + singleEvents: true, + orderBy: 'startTime', + }; + + if (opts.from) params.timeMin = toRFC3339(opts.from); + if (opts.to) params.timeMax = toRFC3339(opts.to); + + const res = await calendar.events.list(params); + const events = res.data.items || []; + + if (events.length === 0) { + console.log('No events found.'); + return; + } + + console.log('ID\tStart\tEnd\tSummary'); + + for (const event of events) { + const start = event.start?.dateTime || event.start?.date || ''; + const end = event.end?.dateTime || event.end?.date || ''; + const summary = event.summary || '(no title)'; + console.log(`${event.id}\t${start}\t${end}\t${summary}`); + } +} + +main().catch(err => { + console.error('Error:', err.message); + process.exit(1); +}); diff --git a/skills/google-workspace/scripts/gmail-read.js b/skills/google-workspace/scripts/gmail-read.js new file mode 100644 index 000000000..804aaa5cd --- /dev/null +++ b/skills/google-workspace/scripts/gmail-read.js @@ -0,0 +1,86 @@ +#!/usr/bin/env node +/** + * Gmail Read + * + * Usage: node gmail-read.js + * + * Reads a single email's full content. + */ + +const { getGmail } = require('./google-auth'); + +function decodeBody(body) { + if (!body?.data) return ''; + return Buffer.from(body.data, 'base64url').toString('utf-8'); +} + +function extractText(payload) { + if (!payload) return ''; + + // Simple text/plain or text/html body + if (payload.mimeType === 'text/plain' && payload.body?.data) { + return decodeBody(payload.body); + } + + // Multipart: recurse through parts + if (payload.parts) { + // Prefer text/plain + for (const part of payload.parts) { + if (part.mimeType === 'text/plain' && part.body?.data) { + return decodeBody(part.body); + } + } + // Fall back to text/html + for (const part of payload.parts) { + if (part.mimeType === 'text/html' && part.body?.data) { + return decodeBody(part.body); + } + } + // Recurse into nested multipart + for (const part of payload.parts) { + const text = extractText(part); + if (text) return text; + } + } + + // Fallback: decode whatever body is there + if (payload.body?.data) { + return decodeBody(payload.body); + } + + return ''; +} + +async function main() { + const messageId = process.argv[2]; + if (!messageId) { + console.error('Usage: node gmail-read.js '); + process.exit(1); + } + + const gmail = getGmail(); + + const res = await gmail.users.messages.get({ + userId: 'me', + id: messageId, + format: 'full', + }); + + const headers = res.data.payload?.headers || []; + const getHeader = (name) => headers.find(h => h.name === name)?.value || ''; + + console.log(`From: ${getHeader('From')}`); + console.log(`To: ${getHeader('To')}`); + console.log(`Date: ${getHeader('Date')}`); + console.log(`Subject: ${getHeader('Subject')}`); + console.log(`Labels: ${(res.data.labelIds || []).join(', ')}`); + console.log('---'); + + const body = extractText(res.data.payload); + console.log(body || '(no text content)'); +} + +main().catch(err => { + console.error('Error:', err.message); + process.exit(1); +}); diff --git a/skills/google-workspace/scripts/gmail-search.js b/skills/google-workspace/scripts/gmail-search.js new file mode 100644 index 000000000..66474036b --- /dev/null +++ b/skills/google-workspace/scripts/gmail-search.js @@ -0,0 +1,67 @@ +#!/usr/bin/env node +/** + * Gmail Search + * + * Usage: node gmail-search.js [--max N] + * + * Searches Gmail using the users.messages.list + get API. + * Outputs: ID, date, from, subject, labels (TSV format) + */ + +const { getGmail } = require('./google-auth'); + +async function main() { + const args = process.argv.slice(2); + if (args.length === 0) { + console.error('Usage: node gmail-search.js [--max N]'); + process.exit(1); + } + + let query = ''; + let maxResults = 20; + + for (let i = 0; i < args.length; i++) { + if (args[i] === '--max' && args[i + 1]) { + maxResults = parseInt(args[i + 1], 10); + i++; + } else { + query += (query ? ' ' : '') + args[i]; + } + } + + const gmail = getGmail(); + + const listRes = await gmail.users.messages.list({ + userId: 'me', + q: query, + maxResults, + }); + + const messages = listRes.data.messages || []; + if (messages.length === 0) { + console.log('No messages found.'); + return; + } + + console.log('ID\tDate\tFrom\tSubject\tLabels'); + + for (const msg of messages) { + const detail = await gmail.users.messages.get({ + userId: 'me', + id: msg.id, + format: 'metadata', + metadataHeaders: ['From', 'Subject', 'Date'], + }); + + const headers = detail.data.payload?.headers || []; + const getHeader = (name) => headers.find(h => h.name === name)?.value || ''; + const labels = (detail.data.labelIds || []).join(','); + + console.log(`${msg.id}\t${getHeader('Date')}\t${getHeader('From')}\t${getHeader('Subject')}\t${labels}`); + } +} + +main().catch(err => { + console.error('Error:', err.message); + process.exit(1); +}); diff --git a/skills/google-workspace/scripts/gmail-send.js b/skills/google-workspace/scripts/gmail-send.js new file mode 100644 index 000000000..654c5163c --- /dev/null +++ b/skills/google-workspace/scripts/gmail-send.js @@ -0,0 +1,63 @@ +#!/usr/bin/env node +/** + * Gmail Send + * + * Usage: node gmail-send.js --to --subject --body + * + * Sends an email via Gmail API. + */ + +const { getGmail } = require('./google-auth'); + +function parseArgs(args) { + const result = {}; + for (let i = 0; i < args.length; i++) { + if (args[i] === '--to' && args[i + 1]) { + result.to = args[++i]; + } else if (args[i] === '--subject' && args[i + 1]) { + result.subject = args[++i]; + } else if (args[i] === '--body' && args[i + 1]) { + result.body = args[++i]; + } else if (args[i] === '--cc' && args[i + 1]) { + result.cc = args[++i]; + } else if (args[i] === '--bcc' && args[i + 1]) { + result.bcc = args[++i]; + } + } + return result; +} + +async function main() { + const opts = parseArgs(process.argv.slice(2)); + + if (!opts.to || !opts.subject || !opts.body) { + console.error('Usage: node gmail-send.js --to --subject --body [--cc ] [--bcc ]'); + process.exit(1); + } + + const gmail = getGmail(); + + // Build RFC 2822 message + let message = `To: ${opts.to}\n`; + if (opts.cc) message += `Cc: ${opts.cc}\n`; + if (opts.bcc) message += `Bcc: ${opts.bcc}\n`; + message += `Subject: ${opts.subject}\n`; + message += `Content-Type: text/plain; charset=utf-8\n\n`; + message += opts.body; + + const encodedMessage = Buffer.from(message).toString('base64url'); + + const res = await gmail.users.messages.send({ + userId: 'me', + requestBody: { + raw: encodedMessage, + }, + }); + + console.log(`Sent message ID: ${res.data.id}`); +} + +main().catch(err => { + console.error('Error:', err.message); + process.exit(1); +}); diff --git a/skills/google-workspace/scripts/google-auth.js b/skills/google-workspace/scripts/google-auth.js new file mode 100644 index 000000000..44a72884b --- /dev/null +++ b/skills/google-workspace/scripts/google-auth.js @@ -0,0 +1,49 @@ +#!/usr/bin/env node +/** + * Google Workspace - Shared Auth Library + * + * Creates authenticated Google API clients using OAuth2 credentials + * from environment variables. Handles token refresh automatically. + * + * Usage: + * const { getGmail, getCalendar } = require('./google-auth'); + * const gmail = getGmail(); + * const calendar = getCalendar(); + */ + +// Ensure globally-installed npm packages are resolvable +// (sandbox startProcess does not inherit Dockerfile ENV vars) +if (!process.env.NODE_PATH) { + process.env.NODE_PATH = '/usr/local/lib/node_modules'; + require('module').Module._initPaths(); +} + +const { google } = require('googleapis'); + +function getAuth() { + const clientId = process.env.GOOGLE_CLIENT_ID; + const clientSecret = process.env.GOOGLE_CLIENT_SECRET; + const refreshToken = process.env.GOOGLE_REFRESH_TOKEN; + + if (!clientId || !clientSecret || !refreshToken) { + const missing = []; + if (!clientId) missing.push('GOOGLE_CLIENT_ID'); + if (!clientSecret) missing.push('GOOGLE_CLIENT_SECRET'); + if (!refreshToken) missing.push('GOOGLE_REFRESH_TOKEN'); + throw new Error(`Missing environment variables: ${missing.join(', ')}`); + } + + const oauth2Client = new google.auth.OAuth2(clientId, clientSecret); + oauth2Client.setCredentials({ refresh_token: refreshToken }); + return oauth2Client; +} + +function getGmail() { + return google.gmail({ version: 'v1', auth: getAuth() }); +} + +function getCalendar() { + return google.calendar({ version: 'v3', auth: getAuth() }); +} + +module.exports = { getAuth, getGmail, getCalendar }; diff --git a/src/gateway/env.ts b/src/gateway/env.ts index 23dea539d..9c2c64520 100644 --- a/src/gateway/env.ts +++ b/src/gateway/env.ts @@ -49,6 +49,10 @@ 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; return envVars; } 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 { diff --git a/src/types.ts b/src/types.ts index a85d32da3..13edf6c1d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -42,6 +42,12 @@ export interface MoltbotEnv { BROWSER?: Fetcher; CDP_SECRET?: string; // Shared secret for CDP endpoint authentication WORKER_URL?: string; // Public URL of the worker (for CDP endpoint) + // OpenClaw agent model override (e.g. 'anthropic/claude-sonnet-4-5') + OPENCLAW_MODEL?: string; + // Google Workspace OAuth credentials (for Gmail/Calendar access) + GOOGLE_CLIENT_ID?: string; + GOOGLE_CLIENT_SECRET?: string; + GOOGLE_REFRESH_TOKEN?: string; } /** diff --git a/start-openclaw.sh b/start-openclaw.sh index dd9381d12..7fcbbba3a 100644 --- a/start-openclaw.sh +++ b/start-openclaw.sh @@ -189,6 +189,23 @@ if (process.env.OPENCLAW_DEV_MODE === 'true') { config.gateway.controlUi.allowInsecureAuth = true; } +// Agent model override (OPENCLAW_MODEL=anthropic/claude-sonnet-4-5) +if (process.env.OPENCLAW_MODEL) { + config.agents = config.agents || {}; + config.agents.defaults = config.agents.defaults || {}; + config.agents.defaults.model = { primary: process.env.OPENCLAW_MODEL }; + console.log('Model override:', process.env.OPENCLAW_MODEL); +} + +// Reduce concurrency to avoid Anthropic API rate limits +// Default is maxConcurrent=4 + subagents=8 which can fire 12 parallel requests +config.agents = config.agents || {}; +config.agents.defaults = config.agents.defaults || {}; +config.agents.defaults.maxConcurrent = 1; +config.agents.defaults.subagents = config.agents.defaults.subagents || {}; +config.agents.defaults.subagents.maxConcurrent = 2; +console.log('Concurrency: maxConcurrent=1, subagents.maxConcurrent=2'); + // Legacy AI Gateway base URL override: // ANTHROPIC_BASE_URL is picked up natively by the Anthropic SDK, // so we don't need to patch the provider config. Writing a provider @@ -284,6 +301,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 # ============================================================ 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