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:
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