auto.ts had grown to 923 lines with ~10 interleaved responsibilities.
Split into three focused modules, keeping auto.ts as a pure step
sequencer:
- setup/lib/runner.ts (325 lines) — spawn + stream-parse + spinner-wrap
primitives. Exports: spawnStep, spawnQuiet, runQuietStep,
runQuietChild, runUnderSpinner (internal), StatusStream, types
(Fields, Block, StepResult, SpinnerLabels, QuietChildResult),
writeStepEntry, summariseTerminalFields, dumpTranscriptOnFailure,
fail(), ensureAnswer().
- setup/lib/theme.ts (39 lines) — brand palette (brand, brandBold,
brandChip) with USE_ANSI / TRUECOLOR gating, so both auto.ts and
channel flows can render the NanoClaw cyan without duplicating the
detection.
- setup/channels/telegram.ts (277 lines) — runTelegramChannel(displayName)
owns the full flow: BotFather instructions, token paste + validation
(via getMe), install script, pair-telegram streaming UI (code card +
attempt checkpoints), agent-name prompt, init-first-agent wiring.
auto.ts drops to 376 lines. main() reads as a clean sequence of
`if (!skip.has(X)) await Xstep(...)` blocks.
fail() now takes the step name explicitly — no module-level
failingStep state. Every call site is grep-friendly and self-contained
(fail('container', msg, hint)).
Typechecks clean. Smoke-tested end-to-end: intro, mounts step,
progression log, and outro all render the same as before the split.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
278 lines
8.6 KiB
TypeScript
278 lines
8.6 KiB
TypeScript
/**
|
|
* Telegram channel flow for setup:auto.
|
|
*
|
|
* `runTelegramChannel(displayName)` owns the full branch from the
|
|
* BotFather instructions through the welcome DM:
|
|
*
|
|
* 1. BotFather instructions (clack note)
|
|
* 2. Paste the bot token (clack password) — format-validated
|
|
* 3. getMe via the Bot API to resolve the bot's username
|
|
* 4. Install the adapter (setup/add-telegram.sh, non-interactive)
|
|
* 5. Run the pair-telegram step, rendering code events as clack notes
|
|
* 6. Ask for the messaging-agent name (defaulting to "Nano")
|
|
* 7. Wire the agent via scripts/init-first-agent.ts
|
|
*
|
|
* All output obeys the three-level contract: clack UI for the user,
|
|
* structured entries in logs/setup.log, full raw output in per-step files
|
|
* under logs/setup-steps/. See docs/setup-flow.md.
|
|
*/
|
|
import * as p from '@clack/prompts';
|
|
import k from 'kleur';
|
|
|
|
import * as setupLog from '../logs.js';
|
|
import {
|
|
type Block,
|
|
type StepResult,
|
|
dumpTranscriptOnFailure,
|
|
ensureAnswer,
|
|
fail,
|
|
runQuietChild,
|
|
spawnStep,
|
|
writeStepEntry,
|
|
} from '../lib/runner.js';
|
|
import { brandBold } from '../lib/theme.js';
|
|
|
|
const DEFAULT_AGENT_NAME = 'Nano';
|
|
|
|
export async function runTelegramChannel(displayName: string): Promise<void> {
|
|
const token = await collectTelegramToken();
|
|
const botUsername = await validateTelegramToken(token);
|
|
|
|
const install = await runQuietChild(
|
|
'telegram-install',
|
|
'bash',
|
|
['setup/add-telegram.sh'],
|
|
{
|
|
running: `Installing Telegram adapter and wiring @${botUsername}…`,
|
|
done: 'Telegram adapter ready.',
|
|
},
|
|
{
|
|
env: { TELEGRAM_BOT_TOKEN: token },
|
|
extraFields: { BOT_USERNAME: botUsername },
|
|
},
|
|
);
|
|
if (!install.ok) {
|
|
fail(
|
|
'telegram-install',
|
|
'Telegram install failed.',
|
|
'Check the raw log under logs/setup-steps/, then retry `pnpm run setup:auto`.',
|
|
);
|
|
}
|
|
|
|
const pair = await runPairTelegram();
|
|
if (!pair.ok) {
|
|
fail(
|
|
'pair-telegram',
|
|
'Telegram pairing failed.',
|
|
'Re-run `pnpm exec tsx setup/index.ts --step pair-telegram -- --intent main`.',
|
|
);
|
|
}
|
|
|
|
const platformId = pair.terminal?.fields.PLATFORM_ID;
|
|
const pairedUserId = pair.terminal?.fields.PAIRED_USER_ID;
|
|
if (!platformId || !pairedUserId) {
|
|
fail(
|
|
'pair-telegram',
|
|
'pair-telegram succeeded but did not return PLATFORM_ID and PAIRED_USER_ID.',
|
|
'Re-run `pnpm exec tsx setup/index.ts --step pair-telegram -- --intent main` and capture the success block.',
|
|
);
|
|
}
|
|
|
|
const agentName = await resolveAgentName();
|
|
|
|
const init = await runQuietChild(
|
|
'init-first-agent',
|
|
'pnpm',
|
|
[
|
|
'exec', 'tsx', 'scripts/init-first-agent.ts',
|
|
'--channel', 'telegram',
|
|
'--user-id', pairedUserId,
|
|
'--platform-id', platformId,
|
|
'--display-name', displayName,
|
|
'--agent-name', agentName,
|
|
],
|
|
{
|
|
running: `Wiring ${agentName} to your Telegram chat…`,
|
|
done: `${agentName} is wired — welcome DM incoming.`,
|
|
},
|
|
{
|
|
extraFields: { CHANNEL: 'telegram', AGENT_NAME: agentName, PLATFORM_ID: platformId },
|
|
},
|
|
);
|
|
if (!init.ok) {
|
|
fail(
|
|
'init-first-agent',
|
|
'Wiring the Telegram agent failed.',
|
|
`Re-run \`pnpm exec tsx scripts/init-first-agent.ts --channel telegram --user-id "${pairedUserId}" --platform-id "${platformId}" --display-name "${displayName}" --agent-name "${agentName}"\`.`,
|
|
);
|
|
}
|
|
}
|
|
|
|
async function collectTelegramToken(): Promise<string> {
|
|
p.note(
|
|
[
|
|
'1. Open Telegram and message @BotFather',
|
|
'2. Send: /newbot',
|
|
'3. Follow the prompts (name + username ending in "bot")',
|
|
'4. Copy the token it gives you (format: <digits>:<chars>)',
|
|
'',
|
|
k.dim('Optional, but recommended for groups:'),
|
|
k.dim(' @BotFather → /mybots → Bot Settings → Group Privacy → OFF'),
|
|
].join('\n'),
|
|
'Create a Telegram bot',
|
|
);
|
|
|
|
const answer = ensureAnswer(
|
|
await p.password({
|
|
message: 'Paste your bot token',
|
|
validate: (v) => {
|
|
if (!v || !v.trim()) return 'Token is required';
|
|
if (!/^[0-9]+:[A-Za-z0-9_-]{35,}$/.test(v.trim())) {
|
|
return 'Format looks wrong — expected <digits>:<chars>';
|
|
}
|
|
return undefined;
|
|
},
|
|
}),
|
|
);
|
|
const token = (answer as string).trim();
|
|
setupLog.userInput(
|
|
'telegram_token',
|
|
`${token.slice(0, 12)}…${token.slice(-4)}`,
|
|
);
|
|
return token;
|
|
}
|
|
|
|
async function validateTelegramToken(token: string): Promise<string> {
|
|
const s = p.spinner();
|
|
const start = Date.now();
|
|
s.start('Validating token with Telegram…');
|
|
try {
|
|
const res = await fetch(`https://api.telegram.org/bot${token}/getMe`);
|
|
const data = (await res.json()) as {
|
|
ok?: boolean;
|
|
result?: { username?: string; id?: number };
|
|
description?: string;
|
|
};
|
|
const elapsedS = Math.round((Date.now() - start) / 1000);
|
|
if (data.ok && data.result?.username) {
|
|
const username = data.result.username;
|
|
s.stop(`Bot is @${username}. ${k.dim(`(${elapsedS}s)`)}`);
|
|
setupLog.step('telegram-validate', 'success', Date.now() - start, {
|
|
BOT_USERNAME: username,
|
|
BOT_ID: data.result.id ?? '',
|
|
});
|
|
return username;
|
|
}
|
|
const reason = data.description ?? 'token rejected by Telegram';
|
|
s.stop(`Telegram rejected the token: ${reason}`, 1);
|
|
setupLog.step('telegram-validate', 'failed', Date.now() - start, {
|
|
ERROR: reason,
|
|
});
|
|
fail(
|
|
'telegram-validate',
|
|
'Telegram rejected the token.',
|
|
'Double-check the token (copy it again from @BotFather) and retry.',
|
|
);
|
|
} catch (err) {
|
|
const elapsedS = Math.round((Date.now() - start) / 1000);
|
|
s.stop(`Could not reach Telegram. ${k.dim(`(${elapsedS}s)`)}`, 1);
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
setupLog.step('telegram-validate', 'failed', Date.now() - start, {
|
|
ERROR: message,
|
|
});
|
|
fail(
|
|
'telegram-validate',
|
|
'Telegram API unreachable.',
|
|
'Check your network connection and retry.',
|
|
);
|
|
}
|
|
}
|
|
|
|
async function runPairTelegram(): Promise<
|
|
StepResult & { rawLog: string; durationMs: number }
|
|
> {
|
|
const rawLog = setupLog.stepRawLog('pair-telegram');
|
|
const start = Date.now();
|
|
const s = p.spinner();
|
|
s.start('Creating pairing code…');
|
|
let spinnerActive = true;
|
|
|
|
const stopSpinner = (msg: string, code?: number) => {
|
|
if (spinnerActive) {
|
|
s.stop(msg, code);
|
|
spinnerActive = false;
|
|
}
|
|
};
|
|
|
|
const result = await spawnStep(
|
|
'pair-telegram',
|
|
['--intent', 'main'],
|
|
(block: Block) => {
|
|
if (block.type === 'PAIR_TELEGRAM_CODE') {
|
|
const reason = block.fields.REASON ?? 'initial';
|
|
if (reason === 'initial') {
|
|
stopSpinner('Pairing code ready.');
|
|
} else {
|
|
stopSpinner('Previous code invalidated. New code below.');
|
|
}
|
|
p.note(formatCodeCard(block.fields.CODE ?? '????'), 'Pairing code');
|
|
s.start('Waiting for the code from Telegram…');
|
|
spinnerActive = true;
|
|
} else if (block.type === 'PAIR_TELEGRAM_ATTEMPT') {
|
|
stopSpinner(`Received "${block.fields.CANDIDATE ?? '?'}" — doesn't match.`);
|
|
s.start('Waiting for the correct code…');
|
|
spinnerActive = true;
|
|
} else if (block.type === 'PAIR_TELEGRAM') {
|
|
if (block.fields.STATUS === 'success') {
|
|
stopSpinner('Telegram paired.');
|
|
} else {
|
|
stopSpinner(`Pairing failed: ${block.fields.ERROR ?? 'unknown'}`, 1);
|
|
}
|
|
}
|
|
},
|
|
rawLog,
|
|
);
|
|
const durationMs = Date.now() - start;
|
|
|
|
// Safety net: if the child died without emitting a terminal block, make
|
|
// sure we don't leave the spinner running.
|
|
if (spinnerActive) {
|
|
stopSpinner(
|
|
result.ok ? 'Done.' : 'Pairing exited unexpectedly.',
|
|
result.ok ? 0 : 1,
|
|
);
|
|
if (!result.ok) dumpTranscriptOnFailure(result.transcript);
|
|
}
|
|
|
|
writeStepEntry('pair-telegram', result, durationMs, rawLog);
|
|
return { ...result, rawLog, durationMs };
|
|
}
|
|
|
|
function formatCodeCard(code: string): string {
|
|
const spaced = code.split('').join(' ');
|
|
return [
|
|
'',
|
|
` ${brandBold(spaced)}`,
|
|
'',
|
|
k.dim(' Send these digits from Telegram to your bot.'),
|
|
].join('\n');
|
|
}
|
|
|
|
async function resolveAgentName(): Promise<string> {
|
|
const preset = process.env.NANOCLAW_AGENT_NAME?.trim();
|
|
if (preset) {
|
|
setupLog.userInput('agent_name', preset);
|
|
return preset;
|
|
}
|
|
const answer = ensureAnswer(
|
|
await p.text({
|
|
message: 'What should your messaging agent be called?',
|
|
placeholder: DEFAULT_AGENT_NAME,
|
|
defaultValue: DEFAULT_AGENT_NAME,
|
|
}),
|
|
);
|
|
const value = (answer as string).trim() || DEFAULT_AGENT_NAME;
|
|
setupLog.userInput('agent_name', value);
|
|
return value;
|
|
}
|