diff --git a/nanoclaw.sh b/nanoclaw.sh index 2a98f98..2dc0f04 100755 --- a/nanoclaw.sh +++ b/nanoclaw.sh @@ -4,7 +4,7 @@ # # Runs `bash setup.sh` (bootstrap: Node check, pnpm install, native module # verify), then `pnpm run setup:auto` (environment → container → onecli → -# auth → mounts → service → cli-agent → verify). +# auth → mounts → service → cli-agent → channel → verify). # # Everything that can be scripted runs unattended; the one interactive pause # is the auth step (browser sign-in or paste token/API key). diff --git a/setup/add-telegram.sh b/setup/add-telegram.sh new file mode 100755 index 0000000..c822994 --- /dev/null +++ b/setup/add-telegram.sh @@ -0,0 +1,134 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Install the Telegram adapter (Phase A of the /add-telegram skill), collect +# the bot token, write .env + data/env/env, and restart the service so the +# new adapter is live. Idempotent. +# +# Pair-telegram (the interactive code-sending step) is run separately by the +# caller (setup/auto.ts) so it can stream status blocks to the user. + +PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$PROJECT_ROOT" + +# Keep in sync with .claude/skills/add-telegram/SKILL.md. +ADAPTER_VERSION="@chat-adapter/telegram@4.26.0" +CHANNELS_BRANCH="origin/channels" + +need_install() { + [[ ! -f src/channels/telegram.ts ]] && return 0 + [[ ! -f setup/pair-telegram.ts ]] && return 0 + ! grep -q "^import './telegram.js';" src/channels/index.ts 2>/dev/null && return 0 + return 1 +} + +if need_install; then + echo "[add-telegram] Fetching channels branch…" + git fetch origin channels >/dev/null 2>&1 + + echo "[add-telegram] Copying adapter files from $CHANNELS_BRANCH…" + for f in \ + src/channels/telegram.ts \ + src/channels/telegram-pairing.ts \ + src/channels/telegram-pairing.test.ts \ + src/channels/telegram-markdown-sanitize.ts \ + src/channels/telegram-markdown-sanitize.test.ts \ + setup/pair-telegram.ts + do + git show "$CHANNELS_BRANCH:$f" > "$f" + done + + # Append self-registration import if missing. + if ! grep -q "^import './telegram.js';" src/channels/index.ts; then + echo "import './telegram.js';" >> src/channels/index.ts + fi + + # Register pair-telegram step if not already in the STEPS map. + # Uses node (not sed) since sed's in-place + escape semantics differ + # between BSD (macOS) and GNU. + node -e ' + const fs = require("fs"); + const p = "setup/index.ts"; + let s = fs.readFileSync(p, "utf-8"); + if (!s.includes("\047pair-telegram\047")) { + s = s.replace( + /(register: \(\) => import\(\x27\.\/register\.js\x27\),)/, + "$1\n \x27pair-telegram\x27: () => import(\x27./pair-telegram.js\x27)," + ); + fs.writeFileSync(p, s); + } + ' + + echo "[add-telegram] Installing $ADAPTER_VERSION…" + pnpm install "$ADAPTER_VERSION" + + echo "[add-telegram] Building…" + pnpm run build >/dev/null +else + echo "[add-telegram] Adapter files already installed — skipping install phase." +fi + +# Token collection. +if grep -q '^TELEGRAM_BOT_TOKEN=.' .env 2>/dev/null; then + echo "[add-telegram] TELEGRAM_BOT_TOKEN already set in .env — skipping token prompt." +else + cat <<'EOF' + +── Create a Telegram bot ────────────────────────────────────── + + 1. Open Telegram and message @BotFather + 2. Send: /newbot + 3. Follow the prompts (bot name, username ending in "bot") + 4. Copy the token it gives you (format: :) + +Optional but recommended for groups: + 5. @BotFather → /mybots → your bot → Bot Settings → Group Privacy → OFF + +EOF + echo "Paste your TELEGRAM_BOT_TOKEN and press Enter." + echo "Nothing will appear on the screen as you paste — that's intentional." + echo "Paste once, then just press Enter to submit." + read -r -s -p "> " TOKEN &2 + exit 1 + fi + + # Telegram bot tokens: :<35+ base64url-ish chars>. + if [[ ! "$TOKEN" =~ ^[0-9]+:[A-Za-z0-9_-]{35,}$ ]]; then + echo "[add-telegram] Token format looks wrong (expected :). Aborting." >&2 + exit 1 + fi + + touch .env + if grep -q '^TELEGRAM_BOT_TOKEN=' .env; then + awk -v tok="$TOKEN" '/^TELEGRAM_BOT_TOKEN=/{print "TELEGRAM_BOT_TOKEN=" tok; next} {print}' \ + .env > .env.tmp && mv .env.tmp .env + else + echo "TELEGRAM_BOT_TOKEN=$TOKEN" >> .env + fi +fi + +# Container reads from data/env/env (the host mounts it). +mkdir -p data/env +cp .env data/env/env + +echo "[add-telegram] Restarting service so the new adapter picks up the token…" +case "$(uname -s)" in + Darwin) + launchctl kickstart -k "gui/$(id -u)/com.nanoclaw" >/dev/null 2>&1 || true + ;; + Linux) + systemctl --user restart nanoclaw >/dev/null 2>&1 \ + || sudo systemctl restart nanoclaw >/dev/null 2>&1 \ + || true + ;; +esac + +# Give the Telegram adapter a moment to finish starting before pair-telegram +# begins polling for the user's code message. +sleep 5 + +echo "[add-telegram] Install + credentials complete." diff --git a/setup/auto.ts b/setup/auto.ts index d3b8113..12a3070 100644 --- a/setup/auto.ts +++ b/setup/auto.ts @@ -11,8 +11,8 @@ * interactive prompt before cli-agent. If unset, * the driver asks, defaulting to $USER. * NANOCLAW_SKIP comma-separated step names to skip - * (environment|container|onecli|auth| - * mounts|service|cli-agent|verify) + * (environment|container|onecli|auth|mounts| + * service|cli-agent|channel|verify) * * Timezone is not configured here — it defaults to the host system's TZ. * Run `pnpm exec tsx setup/index.ts --step timezone -- --tz ` later @@ -130,6 +130,19 @@ async function askDisplayName(fallback: string): Promise { } } +async function askChannelChoice(): Promise<'telegram' | 'skip'> { + const rl = createInterface({ input: process.stdin, output: process.stdout }); + try { + console.log('\nConnect a messaging app so you can chat from your phone?'); + console.log(' 1) Telegram'); + console.log(' 2) Skip — just use the CLI for now'); + const answer = (await rl.question('Choose [1/2]: ')).trim(); + return answer === '1' ? 'telegram' : 'skip'; + } finally { + rl.close(); + } +} + function runBashScript(relPath: string): Promise { return new Promise((resolve) => { const child = spawn('bash', [relPath], { stdio: 'inherit' }); @@ -257,6 +270,34 @@ async function main(): Promise { } } + if (!skip.has('channel')) { + const choice = await askChannelChoice(); + if (choice === 'telegram') { + const installCode = await runBashScript('setup/add-telegram.sh'); + if (installCode !== 0) { + fail( + 'Telegram install failed.', + 'Re-run `bash setup/add-telegram.sh`, then `pnpm exec tsx setup/index.ts --step pair-telegram -- --intent main`.', + ); + } + + console.log( + '\n[setup:auto] Pairing Telegram. A 4-digit code will appear below.\n' + + ' From Telegram, send just those 4 digits to your bot\n' + + ' (DM the bot for a personal chat, or prefix with your\n' + + ' bot handle in a group with privacy on).\n', + ); + + const pair = await runStep('pair-telegram', ['--intent', 'main']); + if (!pair.ok) { + fail( + 'Telegram pairing failed.', + 'Re-run `pnpm exec tsx setup/index.ts --step pair-telegram -- --intent main`.', + ); + } + } + } + if (!skip.has('verify')) { const res = await runStep('verify'); if (!res.ok) {