refactor(setup): own pair-telegram.ts in this branch with clean output
Previously setup:auto parsed pair-telegram's machine-readable status blocks and rendered a banner on top. Fork the script instead: check in setup/pair-telegram.ts with a focused 4-digit banner, a short wrong-attempt line, and a single final PAIR_TELEGRAM status block (kept so the parent driver still picks up PLATFORM_ID and PAIRED_USER_ID via parseStatus). Drop pair-telegram.ts from add-telegram.sh's copy list so the local version isn't overwritten on re-runs. The other adapter files (telegram.ts, telegram-pairing.ts, etc.) still come from the channels branch. Also fix a latent bug: auto.ts was reading ADMIN_USER_ID from the success block, but the actual field name is PAIRED_USER_ID — init-first-agent would have been called with --user-id "". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,7 +17,6 @@ CHANNELS_BRANCH="origin/channels"
|
|||||||
|
|
||||||
need_install() {
|
need_install() {
|
||||||
[[ ! -f src/channels/telegram.ts ]] && return 0
|
[[ ! -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
|
! grep -q "^import './telegram.js';" src/channels/index.ts 2>/dev/null && return 0
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
@@ -26,14 +25,15 @@ if need_install; then
|
|||||||
echo "[add-telegram] Fetching channels branch…"
|
echo "[add-telegram] Fetching channels branch…"
|
||||||
git fetch origin channels >/dev/null 2>&1
|
git fetch origin channels >/dev/null 2>&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.
|
||||||
echo "[add-telegram] Copying adapter files from $CHANNELS_BRANCH…"
|
echo "[add-telegram] Copying adapter files from $CHANNELS_BRANCH…"
|
||||||
for f in \
|
for f in \
|
||||||
src/channels/telegram.ts \
|
src/channels/telegram.ts \
|
||||||
src/channels/telegram-pairing.ts \
|
src/channels/telegram-pairing.ts \
|
||||||
src/channels/telegram-pairing.test.ts \
|
src/channels/telegram-pairing.test.ts \
|
||||||
src/channels/telegram-markdown-sanitize.ts \
|
src/channels/telegram-markdown-sanitize.ts \
|
||||||
src/channels/telegram-markdown-sanitize.test.ts \
|
src/channels/telegram-markdown-sanitize.test.ts
|
||||||
setup/pair-telegram.ts
|
|
||||||
do
|
do
|
||||||
git show "$CHANNELS_BRANCH:$f" > "$f"
|
git show "$CHANNELS_BRANCH:$f" > "$f"
|
||||||
done
|
done
|
||||||
|
|||||||
125
setup/auto.ts
125
setup/auto.ts
@@ -81,119 +81,6 @@ function runStep(name: string, extra: string[] = []): Promise<StepResult> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Variant of runStep for `pair-telegram`. The step emits machine-readable
|
|
||||||
* status blocks (PAIR_TELEGRAM_ISSUED, PAIR_TELEGRAM_ATTEMPT, etc.) meant
|
|
||||||
* for the /setup skill to parse and relay. Running it directly leaves the
|
|
||||||
* operator staring at noisy blocks — this filters them and renders a
|
|
||||||
* focused banner around the 4-digit code instead.
|
|
||||||
*/
|
|
||||||
function runPairTelegram(intent: string): Promise<StepResult> {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
console.log('\n── pair-telegram ───────────────────────────────');
|
|
||||||
const args = [
|
|
||||||
'exec', 'tsx', 'setup/index.ts',
|
|
||||||
'--step', 'pair-telegram',
|
|
||||||
'--', '--intent', intent,
|
|
||||||
];
|
|
||||||
const child = spawn('pnpm', args, { stdio: ['inherit', 'pipe', 'inherit'] });
|
|
||||||
|
|
||||||
let buf = '';
|
|
||||||
let partial = '';
|
|
||||||
let inBlock = false;
|
|
||||||
let blockType = '';
|
|
||||||
let blockFields: Record<string, string> = {};
|
|
||||||
|
|
||||||
function handleLine(line: string): void {
|
|
||||||
if (line.startsWith('=== NANOCLAW SETUP:')) {
|
|
||||||
inBlock = true;
|
|
||||||
blockType = line.replace('=== NANOCLAW SETUP:', '').replace('===', '').trim();
|
|
||||||
blockFields = {};
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (line.startsWith('=== END ===')) {
|
|
||||||
inBlock = false;
|
|
||||||
renderBlock(blockType, blockFields);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (inBlock) {
|
|
||||||
const idx = line.indexOf(':');
|
|
||||||
if (idx > -1) {
|
|
||||||
blockFields[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
process.stdout.write(line + '\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderBlock(type: string, fields: Record<string, string>): void {
|
|
||||||
switch (type) {
|
|
||||||
case 'PAIR_TELEGRAM_ISSUED':
|
|
||||||
printCodeBanner(fields.CODE ?? '????');
|
|
||||||
break;
|
|
||||||
case 'PAIR_TELEGRAM_NEW_CODE':
|
|
||||||
console.log('\n Previous code invalidated. New code:');
|
|
||||||
printCodeBanner(fields.CODE ?? '????');
|
|
||||||
break;
|
|
||||||
case 'PAIR_TELEGRAM_ATTEMPT':
|
|
||||||
console.log(
|
|
||||||
` Got "${fields.RECEIVED_CODE ?? '?'}" — doesn't match. A new code is on its way.`,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 'PAIR_TELEGRAM':
|
|
||||||
if (fields.STATUS === 'success') {
|
|
||||||
console.log('\n ✓ Telegram paired.');
|
|
||||||
} else if (fields.STATUS === 'failed') {
|
|
||||||
console.log(`\n ✗ Pairing failed: ${fields.ERROR ?? 'unknown'}`);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default: {
|
|
||||||
// Forward unknown blocks verbatim (forward-compat).
|
|
||||||
const lines = [`=== NANOCLAW SETUP: ${type} ===`];
|
|
||||||
for (const [k, v] of Object.entries(fields)) lines.push(`${k}: ${v}`);
|
|
||||||
lines.push('=== END ===');
|
|
||||||
process.stdout.write(lines.join('\n') + '\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
child.stdout.on('data', (chunk: Buffer) => {
|
|
||||||
const s = chunk.toString('utf-8');
|
|
||||||
buf += s;
|
|
||||||
partial += s;
|
|
||||||
const lines = partial.split('\n');
|
|
||||||
partial = lines.pop() ?? '';
|
|
||||||
for (const line of lines) handleLine(line);
|
|
||||||
});
|
|
||||||
child.on('close', (code) => {
|
|
||||||
if (partial) handleLine(partial);
|
|
||||||
const fields = parseStatus(buf);
|
|
||||||
resolve({
|
|
||||||
ok: code === 0 && fields.STATUS === 'success',
|
|
||||||
fields,
|
|
||||||
exitCode: code ?? 1,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function printCodeBanner(code: string): void {
|
|
||||||
// Double-space between digits for readability in a 4-digit code.
|
|
||||||
const digits = code.trim().split('').join(' ');
|
|
||||||
const content = [
|
|
||||||
'',
|
|
||||||
` PAIRING CODE: ${digits}`,
|
|
||||||
'',
|
|
||||||
' Send these digits from Telegram to your bot.',
|
|
||||||
'',
|
|
||||||
];
|
|
||||||
const width = Math.max(...content.map((l) => l.length));
|
|
||||||
const top = ' ╔' + '═'.repeat(width + 2) + '╗';
|
|
||||||
const bot = ' ╚' + '═'.repeat(width + 2) + '╝';
|
|
||||||
const mid = content.map((l) => ' ║ ' + l.padEnd(width) + ' ║');
|
|
||||||
console.log(['', top, ...mid, bot, ''].join('\n'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* After installing Docker, this process's supplementary groups are still
|
* After installing Docker, this process's supplementary groups are still
|
||||||
* frozen from login — subsequent steps that talk to /var/run/docker.sock
|
* frozen from login — subsequent steps that talk to /var/run/docker.sock
|
||||||
@@ -423,7 +310,7 @@ async function main(): Promise<void> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pair = await runPairTelegram('main');
|
const pair = await runStep('pair-telegram', ['--intent', 'main']);
|
||||||
if (!pair.ok) {
|
if (!pair.ok) {
|
||||||
fail(
|
fail(
|
||||||
'Telegram pairing failed.',
|
'Telegram pairing failed.',
|
||||||
@@ -432,10 +319,10 @@ async function main(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const platformId = pair.fields.PLATFORM_ID;
|
const platformId = pair.fields.PLATFORM_ID;
|
||||||
const adminUserId = pair.fields.ADMIN_USER_ID;
|
const pairedUserId = pair.fields.PAIRED_USER_ID;
|
||||||
if (!platformId || !adminUserId) {
|
if (!platformId || !pairedUserId) {
|
||||||
fail(
|
fail(
|
||||||
'pair-telegram succeeded but did not return PLATFORM_ID and ADMIN_USER_ID.',
|
'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.',
|
'Re-run `pnpm exec tsx setup/index.ts --step pair-telegram -- --intent main` and capture the success block.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -447,7 +334,7 @@ async function main(): Promise<void> {
|
|||||||
console.log('\n── wiring first agent ──────────────────────────');
|
console.log('\n── wiring first agent ──────────────────────────');
|
||||||
const initCode = await runTsxScript('scripts/init-first-agent.ts', [
|
const initCode = await runTsxScript('scripts/init-first-agent.ts', [
|
||||||
'--channel', 'telegram',
|
'--channel', 'telegram',
|
||||||
'--user-id', adminUserId,
|
'--user-id', pairedUserId,
|
||||||
'--platform-id', platformId,
|
'--platform-id', platformId,
|
||||||
'--display-name', displayName!,
|
'--display-name', displayName!,
|
||||||
'--agent-name', agentName,
|
'--agent-name', agentName,
|
||||||
@@ -455,7 +342,7 @@ async function main(): Promise<void> {
|
|||||||
if (initCode !== 0) {
|
if (initCode !== 0) {
|
||||||
fail(
|
fail(
|
||||||
'Wiring the Telegram agent failed.',
|
'Wiring the Telegram agent failed.',
|
||||||
`Re-run \`pnpm exec tsx scripts/init-first-agent.ts --channel telegram --user-id "${adminUserId}" --platform-id "${platformId}" --display-name "${displayName!}" --agent-name "${agentName}"\`.`,
|
`Re-run \`pnpm exec tsx scripts/init-first-agent.ts --channel telegram --user-id "${pairedUserId}" --platform-id "${platformId}" --display-name "${displayName!}" --agent-name "${agentName}"\`.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
124
setup/pair-telegram.ts
Normal file
124
setup/pair-telegram.ts
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
/**
|
||||||
|
* Step: pair-telegram — issue a one-time pairing code and wait for the
|
||||||
|
* operator to send the code from the chat they want to register.
|
||||||
|
*
|
||||||
|
* Used exclusively by `setup:auto` / `bash nanoclaw.sh` on this branch. Human-
|
||||||
|
* facing output is a focused banner for the code (no parseable block), plus a
|
||||||
|
* short line for wrong attempts / regenerations. A single machine-readable
|
||||||
|
* PAIR_TELEGRAM status block is still emitted at the end so the parent driver
|
||||||
|
* can pick up PLATFORM_ID / PAIRED_USER_ID / IS_GROUP.
|
||||||
|
*
|
||||||
|
* Depends on src/channels/telegram-pairing.js, which setup/add-telegram.sh
|
||||||
|
* copies in from the `channels` branch before this step runs. setup/ is
|
||||||
|
* excluded from the host tsconfig, so this file's import resolves only at
|
||||||
|
* runtime — tsc won't complain on branches that haven't run add-telegram yet.
|
||||||
|
*/
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
import {
|
||||||
|
createPairing,
|
||||||
|
waitForPairing,
|
||||||
|
type PairingIntent,
|
||||||
|
} from '../src/channels/telegram-pairing.js';
|
||||||
|
import { DATA_DIR } from '../src/config.js';
|
||||||
|
import { initDb } from '../src/db/connection.js';
|
||||||
|
import { runMigrations } from '../src/db/migrations/index.js';
|
||||||
|
|
||||||
|
import { emitStatus } from './status.js';
|
||||||
|
|
||||||
|
function parseArgs(args: string[]): PairingIntent {
|
||||||
|
let intent: PairingIntent = 'main';
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
if (args[i] === '--intent') {
|
||||||
|
const raw = args[++i] || 'main';
|
||||||
|
if (raw === 'main') {
|
||||||
|
intent = 'main';
|
||||||
|
} else if (raw.startsWith('wire-to:')) {
|
||||||
|
intent = { kind: 'wire-to', folder: raw.slice('wire-to:'.length) };
|
||||||
|
} else if (raw.startsWith('new-agent:')) {
|
||||||
|
intent = { kind: 'new-agent', folder: raw.slice('new-agent:'.length) };
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unknown intent: ${raw}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return intent;
|
||||||
|
}
|
||||||
|
|
||||||
|
function intentToString(intent: PairingIntent): string {
|
||||||
|
if (intent === 'main') return 'main';
|
||||||
|
return `${intent.kind}:${intent.folder}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function printCodeBanner(code: string): void {
|
||||||
|
const digits = code.split('').join(' ');
|
||||||
|
const content = [
|
||||||
|
'',
|
||||||
|
` PAIRING CODE: ${digits}`,
|
||||||
|
'',
|
||||||
|
' Send these digits from Telegram to your bot.',
|
||||||
|
'',
|
||||||
|
];
|
||||||
|
const width = Math.max(...content.map((l) => l.length));
|
||||||
|
const top = ' ╔' + '═'.repeat(width + 2) + '╗';
|
||||||
|
const bot = ' ╚' + '═'.repeat(width + 2) + '╝';
|
||||||
|
const mid = content.map((l) => ' ║ ' + l.padEnd(width) + ' ║');
|
||||||
|
console.log(['', top, ...mid, bot, ''].join('\n'));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function run(args: string[]): Promise<void> {
|
||||||
|
const intent = parseArgs(args);
|
||||||
|
|
||||||
|
// Pairing stores state under DATA_DIR; the DB isn't strictly needed for the
|
||||||
|
// pairing primitive itself, but the inbound interceptor running inside the
|
||||||
|
// live service needs migrations applied. Touch it here so a fresh install
|
||||||
|
// doesn't fail on the first code match.
|
||||||
|
const db = initDb(path.join(DATA_DIR, 'v2.db'));
|
||||||
|
runMigrations(db);
|
||||||
|
|
||||||
|
const MAX_REGENERATIONS = 5;
|
||||||
|
let record = await createPairing(intent);
|
||||||
|
printCodeBanner(record.code);
|
||||||
|
|
||||||
|
for (let regen = 0; regen <= MAX_REGENERATIONS; regen++) {
|
||||||
|
try {
|
||||||
|
const consumed = await waitForPairing(record.code, {
|
||||||
|
onAttempt: (a) => {
|
||||||
|
console.log(
|
||||||
|
` Got "${a.candidate}" — doesn't match. A new code is on its way.`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('\n ✓ Telegram paired.\n');
|
||||||
|
emitStatus('PAIR_TELEGRAM', {
|
||||||
|
STATUS: 'success',
|
||||||
|
CODE: record.code,
|
||||||
|
INTENT: intentToString(consumed.intent),
|
||||||
|
PLATFORM_ID: consumed.consumed!.platformId,
|
||||||
|
IS_GROUP: consumed.consumed!.isGroup,
|
||||||
|
PAIRED_USER_ID: consumed.consumed!.adminUserId
|
||||||
|
? `telegram:${consumed.consumed!.adminUserId}`
|
||||||
|
: '',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
const invalidated = /invalidated by wrong code/.test(message);
|
||||||
|
if (invalidated && regen < MAX_REGENERATIONS) {
|
||||||
|
record = await createPairing(intent);
|
||||||
|
console.log('\n Previous code invalidated. New code:');
|
||||||
|
printCodeBanner(record.code);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const reason = invalidated ? 'max-regenerations-exceeded' : message;
|
||||||
|
console.error(`\n ✗ Pairing failed: ${reason}`);
|
||||||
|
emitStatus('PAIR_TELEGRAM', {
|
||||||
|
STATUS: 'failed',
|
||||||
|
CODE: record.code,
|
||||||
|
ERROR: reason,
|
||||||
|
});
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user