diff --git a/packages/cli/package.json b/packages/cli/package.json index f3deee55..a5e35832 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@openrouter/spawn", - "version": "0.17.15", + "version": "0.17.16", "type": "module", "bin": { "spawn": "cli.js" diff --git a/packages/cli/src/shared/agent-setup.ts b/packages/cli/src/shared/agent-setup.ts index 3a502b0d..d475b23e 100644 --- a/packages/cli/src/shared/agent-setup.ts +++ b/packages/cli/src/shared/agent-setup.ts @@ -9,7 +9,7 @@ import { join } from "node:path"; import { getTmpDir } from "./paths"; import { asyncTryCatch, asyncTryCatchIf, isOperationalError, tryCatchIf } from "./result.js"; import { getErrorMessage } from "./type-guards"; -import { Err, jsonEscape, logError, logInfo, logStep, logWarn, Ok, shellQuote, withRetry } from "./ui"; +import { Err, jsonEscape, logError, logInfo, logStep, logWarn, Ok, prompt, shellQuote, withRetry } from "./ui"; /** * Wrap an SSH-based async operation into a Result for use with withRetry. @@ -324,10 +324,28 @@ async function setupOpenclawConfig( await installChromeBrowser(runner); } + // Prompt for Telegram bot token before building the config JSON so we can + // include it in a single atomic write. + let telegramBotToken = ""; + if (enabledSteps?.has("telegram")) { + logStep("Setting up Telegram..."); + const envToken = process.env.TELEGRAM_BOT_TOKEN ?? process.env.SPAWN_TELEGRAM_BOT_TOKEN ?? ""; + if (!envToken) { + logInfo("To get a bot token:"); + logInfo(" 1. Open Telegram and search for @BotFather"); + logInfo(" 2. Send /newbot and follow the prompts"); + logInfo(" 3. Copy the token (looks like 123456:ABC-DEF...)"); + logInfo(" Press Enter to skip if you don't have one yet."); + } + telegramBotToken = (envToken || (await prompt("Telegram bot token: "))).trim(); + if (!telegramBotToken) { + logInfo("No token entered — set up Telegram via the web dashboard after launch"); + } + } + const gatewayToken = token ?? crypto.randomUUID().replace(/-/g, ""); - // Build config object for atomic JSON write — base config only (API key, gateway, model). - // Channel setup (Telegram, WhatsApp) is handled by `openclaw onboard` in orchestrate.ts. + // Build config object for atomic JSON write const configObj: Record = { env: { OPENROUTER_API_KEY: apiKey, @@ -347,6 +365,36 @@ async function setupOpenclawConfig( }, }; + // Channel config — written directly to the config file. + // Both use dmPolicy "pairing" so users must approve new senders. + const channels: Record = {}; + + if (telegramBotToken) { + channels.telegram = { + enabled: true, + botToken: telegramBotToken, + dmPolicy: "pairing", + groups: { + "*": { + requireMention: true, + }, + }, + }; + logInfo("Telegram bot token configured"); + } + + if (enabledSteps?.has("whatsapp")) { + channels.whatsapp = { + dmPolicy: "pairing", + groupPolicy: "allowlist", + sendReadReceipts: true, + }; + } + + if (Object.keys(channels).length > 0) { + configObj.channels = channels; + } + const config = JSON.stringify(configObj, null, 2); await uploadConfigFile(runner, config, "$HOME/.openclaw/openclaw.json"); @@ -378,7 +426,7 @@ async function setupOpenclawConfig( logWarn("Gateway token re-assertion failed (non-fatal) — dashboard may show Unauthorized"); } - // Channel setup (Telegram, WhatsApp) is handled by `openclaw onboard` in orchestrate.ts. + // Channel pairing (Telegram/WhatsApp) happens in orchestrate.ts after the gateway starts. // Write USER.md bootstrap file — guides users to the web dashboard for // visual tasks like WhatsApp QR code scanning that don't work in the TUI. diff --git a/packages/cli/src/shared/orchestrate.ts b/packages/cli/src/shared/orchestrate.ts index 7436fd0a..949f6968 100644 --- a/packages/cli/src/shared/orchestrate.ts +++ b/packages/cli/src/shared/orchestrate.ts @@ -25,6 +25,8 @@ import { logWarn, openBrowser, prepareStdinForHandoff, + prompt, + shellQuote, validateModelId, withRetry, } from "./ui"; @@ -291,14 +293,61 @@ export async function runOrchestration( } } - // 11c. Channel setup — delegate to OpenClaw's built-in onboard wizard. - // `openclaw onboard` interactively guides the user through Telegram, WhatsApp, - // and other channel configuration. Runs after the gateway starts. - if (enabledSteps?.has("telegram") || enabledSteps?.has("whatsapp")) { - const ocPath = "export PATH=$HOME/.npm-global/bin:$HOME/.bun/bin:$HOME/.local/bin:$PATH"; - logStep("Running OpenClaw channel setup..."); + // 11c. Channel setup (runs after gateway is up) + const ocPath = "export PATH=$HOME/.npm-global/bin:$HOME/.bun/bin:$HOME/.local/bin:$PATH"; + + if (enabledSteps?.has("telegram")) { + logStep("Telegram pairing..."); + logInfo("DM your Telegram bot to get a pairing code, then enter it below."); + logInfo("Waiting for pairing code..."); + process.stderr.write("\n"); + const pairingCode = (await prompt("Telegram pairing code: ")).trim(); + if (pairingCode) { + const escaped = shellQuote(pairingCode); + const result = await asyncTryCatchIf(isOperationalError, () => + cloud.runner.runServer( + `source ~/.spawnrc 2>/dev/null; ${ocPath}; openclaw pairing approve telegram ${escaped}`, + ), + ); + if (result.ok) { + logInfo("Telegram paired successfully"); + } else { + logWarn("Pairing failed — you can pair later via: openclaw pairing approve telegram "); + } + } else { + logInfo("No code entered — pair later via: openclaw pairing approve telegram "); + } + } + + if (enabledSteps?.has("whatsapp")) { + // Step 1: QR code scan to link the WhatsApp device + logStep("Linking WhatsApp — scan the QR code with your phone..."); + logInfo("Open WhatsApp > Settings > Linked Devices > Link a Device"); + process.stderr.write("\n"); + const whatsappCmd = `source ~/.spawnrc 2>/dev/null; ${ocPath}; openclaw channels login --channel whatsapp`; prepareStdinForHandoff(); - await cloud.interactiveSession(`source ~/.spawnrc 2>/dev/null; ${ocPath}; openclaw onboard`); + await cloud.interactiveSession(whatsappCmd); + + // Step 2: Pairing — approve your own number so the bot responds to you + logStep("WhatsApp pairing..."); + logInfo("Send a message to your bot on WhatsApp to get a pairing code, then enter it below."); + process.stderr.write("\n"); + const pairingCode = (await prompt("WhatsApp pairing code: ")).trim(); + if (pairingCode) { + const escaped = shellQuote(pairingCode); + const result = await asyncTryCatchIf(isOperationalError, () => + cloud.runner.runServer( + `source ~/.spawnrc 2>/dev/null; ${ocPath}; openclaw pairing approve whatsapp ${escaped}`, + ), + ); + if (result.ok) { + logInfo("WhatsApp paired successfully"); + } else { + logWarn("Pairing failed — you can pair later via: openclaw pairing approve whatsapp "); + } + } else { + logInfo("No code entered — pair later via: openclaw pairing approve whatsapp "); + } } // 11d. Agent-specific pre-launch tip (e.g. channel setup ordering hint)