Files
nanoclaw/setup/add-telegram.sh
gavrielc dfcbab5364 feat(setup): optional WhatsApp wiring + cross-channel UX polish
WhatsApp (community/Baileys) joins the setup:auto channel picker, with
the same clack-native UX discipline as Telegram and Discord:

- setup/channels/whatsapp.ts — driver. Collects auth method (QR terminal
  or pairing code), runs the auth step, renders QR blocks in-place with
  ANSI cursor-rewind on rotation so the terminal doesn't fill up with
  stale codes, reads creds.me.id for the bot phone, restarts the service,
  asks for the operator's personal phone (defaulting to the authed
  number), writes ASSISTANT_HAS_OWN_NUMBER=true when they differ
  (dedicated mode), and hands off to init-first-agent.

- setup/whatsapp-auth.ts — forked standalone auth step. Channels-branch
  version had a browser-QR path with an HTTP server + <canvas> QR
  renderer; stripped entirely (headless/SSH users hit dead ends too
  often, and the extra deps complicate install). The remaining terminal
  QR emits raw QR strings in WHATSAPP_AUTH_QR blocks so the parent
  driver owns the rendering. Pairing-code path retained. Status blocks
  now use the runner's vocabulary (success/skipped/failed) so spawnStep
  sets ok correctly; WhatsApp-specific UI text ("WhatsApp linked", "You
  chat") lives in the driver.

- setup/add-whatsapp.sh — non-interactive installer, mirror of
  add-telegram.sh. Fetches the adapter + groups step from the channels
  branch (whatsapp-auth.ts stays local, pair-telegram.ts pattern),
  installs pinned baileys/qrcode/pino, registers the steps in
  setup/index.ts's STEPS map. No service restart (adapter factory
  returns null until creds exist).

Cross-channel fixes bundled:

- scripts/init-first-agent.ts: always addMember(user, agentGroup) for
  the target user so subsequent wirings (not the first) pass the access
  gate. Telegram wiring first → Discord/WhatsApp second was dropping
  every inbound with accessReason='not_member' because only the first
  user gets owner. namespacedPlatformId also passes through JID-format
  raws (contains '@') so WhatsApp's bare <phone>@s.whatsapp.net matches
  what the adapter stores.

- setup/service.ts: launchctl unload-then-load instead of bare load (bare
  load errors 'already loaded' when a prior plist was cached, keeping
  launchd on the OLD ProgramArguments even after the file on disk
  changed). systemctl start → restart (start is a no-op on an active
  unit, swallowing unit-file edits).

- setup/add-telegram.sh: removed the in-script open "tg://resolve"
  block. The driver (setup/channels/telegram.ts) now owns the deep-link,
  gated on a p.confirm so the browser can't steal focus unexpectedly.

- setup/channels/discord.ts + setup/channels/telegram.ts: every browser
  open goes through confirmThenOpen (new shared helper in
  setup/lib/browser.ts) — operator presses Enter before their browser
  takes focus. Telegram switched from tg://resolve?domain= to
  https://t.me/<bot> which works everywhere.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 12:39:48 +03:00

157 lines
5.1 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Install the Telegram adapter, persist the bot token to .env + data/env/env,
# restart the service, and open the bot's chat page in the local Telegram
# client. Non-interactive — the operator-facing "Create a bot" instructions
# and token paste live in setup/auto.ts. The token comes in via the
# TELEGRAM_BOT_TOKEN env var.
#
# Emits exactly one status block on stdout (ADD_TELEGRAM) at the end. All
# chatty progress messages go to stderr so setup:auto's raw-log capture
# sees the full story without cluttering the final block for the parser.
set -euo pipefail
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"
emit_status() {
local status=$1 error=${2:-}
local already=${ADAPTER_ALREADY_INSTALLED:-false}
local username=${BOT_USERNAME:-}
echo "=== NANOCLAW SETUP: ADD_TELEGRAM ==="
echo "STATUS: ${status}"
echo "ADAPTER_VERSION: ${ADAPTER_VERSION}"
echo "ADAPTER_ALREADY_INSTALLED: ${already}"
[ -n "$username" ] && echo "BOT_USERNAME: ${username}"
[ -n "$error" ] && echo "ERROR: ${error}"
echo "=== END ==="
}
log() { echo "[add-telegram] $*" >&2; }
if [ -z "${TELEGRAM_BOT_TOKEN:-}" ]; then
emit_status failed "TELEGRAM_BOT_TOKEN env var not set"
exit 1
fi
if ! [[ "$TELEGRAM_BOT_TOKEN" =~ ^[0-9]+:[A-Za-z0-9_-]{35,}$ ]]; then
emit_status failed "token format invalid (expected <digits>:<chars>)"
exit 1
fi
need_install() {
[ ! -f src/channels/telegram.ts ] && return 0
! grep -q "^import './telegram.js';" src/channels/index.ts 2>/dev/null && return 0
return 1
}
ADAPTER_ALREADY_INSTALLED=true
if need_install; then
ADAPTER_ALREADY_INSTALLED=false
log "Fetching channels branch…"
git fetch origin channels >&2 2>/dev/null || {
emit_status failed "git fetch origin channels failed"
exit 1
}
# pair-telegram.ts is maintained in this branch (setup-auto), so it's NOT
# in this list — do not overwrite the local version with the channels copy.
log "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
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);
}
'
log "Installing ${ADAPTER_VERSION}"
pnpm install "${ADAPTER_VERSION}" >&2 2>/dev/null || {
emit_status failed "pnpm install ${ADAPTER_VERSION} failed"
exit 1
}
log "Building…"
pnpm run build >&2 2>/dev/null || {
emit_status failed "pnpm run build failed"
exit 1
}
else
log "Adapter files already installed — skipping install phase."
fi
# Persist token. auto.ts validates before this point, so a bad token here
# would be an internal bug rather than operator input.
touch .env
if grep -q '^TELEGRAM_BOT_TOKEN=' .env; then
awk -v tok="$TELEGRAM_BOT_TOKEN" \
'/^TELEGRAM_BOT_TOKEN=/{print "TELEGRAM_BOT_TOKEN=" tok; next} {print}' \
.env > .env.tmp && mv .env.tmp .env
else
echo "TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}" >> .env
fi
# Look up the bot username (auto.ts already validated; we re-query here so
# standalone invocations still work — BOT_USERNAME is emitted in the status
# block for parent drivers to display).
INFO=$(curl -fsS --max-time 8 \
"https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getMe" 2>/dev/null || true)
BOT_USERNAME=""
if echo "$INFO" | grep -q '"ok":true'; then
BOT_USERNAME=$(echo "$INFO" | sed -nE 's/.*"username":"([^"]+)".*/\1/p')
fi
# Container reads from data/env/env (the host mounts it).
mkdir -p data/env
cp .env data/env/env
# Browser/app deep-link is done by the parent driver (setup/channels/telegram.ts)
# BEFORE this script runs — gated on a clack confirm so focus-stealing doesn't
# surprise the user. Keeping it out of here means this script stays pure
# non-interactive install.
log "Restarting service so the new adapter picks up the token…"
case "$(uname -s)" in
Darwin)
launchctl kickstart -k "gui/$(id -u)/com.nanoclaw" >&2 2>/dev/null || true
;;
Linux)
systemctl --user restart nanoclaw >&2 2>/dev/null \
|| sudo systemctl restart nanoclaw >&2 2>/dev/null \
|| true
;;
esac
# Give the Telegram adapter a moment to finish starting before pair-telegram
# begins polling for the user's code message.
sleep 5
emit_status success