@inceptionstack/roundhouse 0.3.16 → 0.3.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/agents/registry.ts +109 -16
- package/src/cli/qr.ts +24 -0
- package/src/cli/setup-logger.ts +142 -0
- package/src/cli/setup-prompts.ts +78 -0
- package/src/cli/setup-telegram.ts +12 -5
- package/src/cli/setup.ts +462 -71
- package/src/gateway.ts +88 -1
- package/src/pairing.ts +112 -0
package/src/cli/setup.ts
CHANGED
|
@@ -32,6 +32,12 @@ import {
|
|
|
32
32
|
writeServiceUnit,
|
|
33
33
|
hasSudoAccess,
|
|
34
34
|
} from "./systemd";
|
|
35
|
+
import {
|
|
36
|
+
getAgentDefinition,
|
|
37
|
+
listAvailableAgentTypes,
|
|
38
|
+
type AgentDefinition,
|
|
39
|
+
type AgentSetupContext,
|
|
40
|
+
} from "../agents/registry";
|
|
35
41
|
import {
|
|
36
42
|
validateBotToken,
|
|
37
43
|
checkWebhook,
|
|
@@ -41,6 +47,16 @@ import {
|
|
|
41
47
|
type BotInfo,
|
|
42
48
|
type PairResult,
|
|
43
49
|
} from "./setup-telegram";
|
|
50
|
+
import { promptText, promptMasked } from "./setup-prompts";
|
|
51
|
+
import { createTextLogger, createJsonLogger, type SetupLogger, type SetupDiagnostics, printDiagnosticError } from "./setup-logger";
|
|
52
|
+
import { printQr } from "./qr";
|
|
53
|
+
import {
|
|
54
|
+
createPairingNonce,
|
|
55
|
+
createPairingLink,
|
|
56
|
+
readPendingPairing,
|
|
57
|
+
writePendingPairing,
|
|
58
|
+
type PendingPairing,
|
|
59
|
+
} from "../pairing";
|
|
44
60
|
|
|
45
61
|
// ── Types ────────────────────────────────────────────
|
|
46
62
|
|
|
@@ -58,6 +74,14 @@ interface SetupOptions {
|
|
|
58
74
|
nonInteractive: boolean;
|
|
59
75
|
force: boolean;
|
|
60
76
|
dryRun: boolean;
|
|
77
|
+
/** Telegram-focused setup flow */
|
|
78
|
+
telegram: boolean;
|
|
79
|
+
/** Fully headless automation (no TTY prompts) */
|
|
80
|
+
headless: boolean;
|
|
81
|
+
/** QR code display mode */
|
|
82
|
+
qr: "auto" | "always" | "never";
|
|
83
|
+
/** Agent type (default: pi) */
|
|
84
|
+
agent: string;
|
|
61
85
|
}
|
|
62
86
|
|
|
63
87
|
type StepStatus = "ok" | "warn" | "skip" | "fail";
|
|
@@ -71,13 +95,69 @@ const DEFAULT_MODEL = "us.anthropic.claude-opus-4-6-v1";
|
|
|
71
95
|
|
|
72
96
|
const EXTENSION_NAME_RE = /^@?[a-z0-9][\w.\-/]*$/i;
|
|
73
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Resolve agent definition and wire up setup-specific functions.
|
|
100
|
+
* Pi: configure writes ~/.pi/agent/settings.json, installExtension calls pi install.
|
|
101
|
+
*/
|
|
102
|
+
function resolveAgentForSetup(opts: SetupOptions): AgentDefinition {
|
|
103
|
+
const agent = { ...getAgentDefinition(opts.agent) };
|
|
104
|
+
|
|
105
|
+
if (agent.type === "pi") {
|
|
106
|
+
agent.configure = async (ctx: AgentSetupContext) => {
|
|
107
|
+
// Read existing settings if present
|
|
108
|
+
let existing: Record<string, unknown> = {};
|
|
109
|
+
try {
|
|
110
|
+
existing = JSON.parse(await readFile(PI_SETTINGS_PATH, "utf8"));
|
|
111
|
+
} catch {}
|
|
112
|
+
|
|
113
|
+
const settings: Record<string, unknown> = { ...existing };
|
|
114
|
+
|
|
115
|
+
if (ctx.force) {
|
|
116
|
+
settings.defaultProvider = ctx.provider;
|
|
117
|
+
settings.defaultModel = ctx.model;
|
|
118
|
+
} else {
|
|
119
|
+
if (existing.defaultProvider && existing.defaultProvider !== ctx.provider) {
|
|
120
|
+
warn(`Pi provider already set to '${existing.defaultProvider}' (keeping, use --force to override)`);
|
|
121
|
+
} else {
|
|
122
|
+
settings.defaultProvider = ctx.provider;
|
|
123
|
+
}
|
|
124
|
+
if (existing.defaultModel && existing.defaultModel !== ctx.model) {
|
|
125
|
+
warn(`Pi model already set to '${existing.defaultModel}' (keeping, use --force to override)`);
|
|
126
|
+
} else {
|
|
127
|
+
settings.defaultModel = ctx.model;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Ensure packages array exists
|
|
132
|
+
if (!Array.isArray(settings.packages)) settings.packages = [];
|
|
133
|
+
|
|
134
|
+
// Add pi-psst if using psst
|
|
135
|
+
if (ctx.psst) {
|
|
136
|
+
const psstPkg = "npm:@miclivs/pi-psst";
|
|
137
|
+
const pkgs = settings.packages as string[];
|
|
138
|
+
if (!pkgs.includes(psstPkg)) pkgs.push(psstPkg);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
await mkdir(dirname(PI_SETTINGS_PATH), { recursive: true });
|
|
142
|
+
await atomicWriteJson(PI_SETTINGS_PATH, settings);
|
|
143
|
+
ok(`~/.pi/agent/settings.json (${settings.defaultProvider}, ${settings.defaultModel})`);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
agent.installExtension = async (ext: string) => {
|
|
147
|
+
execOrFail("pi", ["install", `npm:${ext}`], `extension ${ext}`);
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return agent;
|
|
152
|
+
}
|
|
153
|
+
|
|
74
154
|
// ── Helpers ──────────────────────────────────────────
|
|
75
155
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
156
|
+
let log = (msg: string) => { console.log(msg); };
|
|
157
|
+
let step = (n: string, label: string) => { log(`\n${n} ${label}`); };
|
|
158
|
+
let ok = (msg: string) => { log(` ✓ ${msg}`); };
|
|
159
|
+
let warn = (msg: string) => { log(` ⚠ ${msg}`); };
|
|
160
|
+
let fail = (msg: string) => { log(` ✗ ${msg}`); };
|
|
81
161
|
|
|
82
162
|
async function atomicWriteJson(path: string, data: unknown): Promise<void> {
|
|
83
163
|
const tmp = `${path}.tmp.${randomBytes(4).toString("hex")}`;
|
|
@@ -140,6 +220,10 @@ export function parseSetupArgs(argv: string[]): SetupOptions {
|
|
|
140
220
|
nonInteractive: false,
|
|
141
221
|
force: false,
|
|
142
222
|
dryRun: false,
|
|
223
|
+
telegram: false,
|
|
224
|
+
headless: false,
|
|
225
|
+
qr: "auto",
|
|
226
|
+
agent: "pi",
|
|
143
227
|
};
|
|
144
228
|
|
|
145
229
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -161,6 +245,11 @@ export function parseSetupArgs(argv: string[]): SetupOptions {
|
|
|
161
245
|
case "--no-voice": opts.voice = false; break;
|
|
162
246
|
case "--with-psst": opts.psst = true; break;
|
|
163
247
|
case "--non-interactive": opts.nonInteractive = true; break;
|
|
248
|
+
case "--telegram": opts.telegram = true; break;
|
|
249
|
+
case "--headless": opts.headless = true; opts.nonInteractive = true; break;
|
|
250
|
+
case "--agent": opts.agent = next().toLowerCase(); break;
|
|
251
|
+
case "--qr": opts.qr = "always"; break;
|
|
252
|
+
case "--no-qr": opts.qr = "never"; break;
|
|
164
253
|
case "--force": opts.force = true; break;
|
|
165
254
|
case "--dry-run": opts.dryRun = true; break;
|
|
166
255
|
default:
|
|
@@ -174,15 +263,33 @@ export function parseSetupArgs(argv: string[]): SetupOptions {
|
|
|
174
263
|
opts.botToken = process.env.TELEGRAM_BOT_TOKEN ?? "";
|
|
175
264
|
}
|
|
176
265
|
|
|
266
|
+
// Headless: reject --bot-token (argv visible in process listings)
|
|
267
|
+
if (opts.headless && argv.some((a) => a === "--bot-token")) {
|
|
268
|
+
throw new Error(
|
|
269
|
+
"--bot-token is not accepted in --headless mode (argv visible in process listings).\n" +
|
|
270
|
+
"Use: TELEGRAM_BOT_TOKEN=... roundhouse setup --telegram --headless --user USERNAME",
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Validate agent type
|
|
275
|
+
try {
|
|
276
|
+
getAgentDefinition(opts.agent);
|
|
277
|
+
} catch (err: any) {
|
|
278
|
+
throw new Error(err.message);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Interactive --telegram defers token/user prompting to the wizard
|
|
282
|
+
const isInteractiveTelegram = opts.telegram && !opts.headless && !opts.nonInteractive && process.stdin.isTTY;
|
|
283
|
+
|
|
177
284
|
// Validate
|
|
178
|
-
if (!opts.botToken && !opts.dryRun) {
|
|
285
|
+
if (!opts.botToken && !opts.dryRun && !isInteractiveTelegram) {
|
|
179
286
|
throw new Error(
|
|
180
287
|
"Bot token required. Provide via:\n" +
|
|
181
288
|
" TELEGRAM_BOT_TOKEN=... roundhouse setup --user USERNAME\n" +
|
|
182
289
|
" roundhouse setup --bot-token TOKEN --user USERNAME",
|
|
183
290
|
);
|
|
184
291
|
}
|
|
185
|
-
if (opts.users.length === 0) {
|
|
292
|
+
if (opts.users.length === 0 && !isInteractiveTelegram) {
|
|
186
293
|
throw new Error(
|
|
187
294
|
"At least one --user USERNAME is required.\n" +
|
|
188
295
|
"This is your Telegram username (without @).",
|
|
@@ -202,7 +309,7 @@ export function parseSetupArgs(argv: string[]): SetupOptions {
|
|
|
202
309
|
|
|
203
310
|
// ── Steps ────────────────────────────────────────────
|
|
204
311
|
|
|
205
|
-
async function stepPreflight(opts: SetupOptions): Promise<void> {
|
|
312
|
+
async function stepPreflight(opts: SetupOptions, agent: AgentDefinition): Promise<void> {
|
|
206
313
|
step("①", "Preflight checks...");
|
|
207
314
|
|
|
208
315
|
// Node version
|
|
@@ -222,7 +329,8 @@ async function stepPreflight(opts: SetupOptions): Promise<void> {
|
|
|
222
329
|
ok("npm available");
|
|
223
330
|
|
|
224
331
|
// Config dirs writable
|
|
225
|
-
|
|
332
|
+
const dirs = [ROUNDHOUSE_DIR, ...(agent.configDirs ?? [])];
|
|
333
|
+
for (const dir of dirs) {
|
|
226
334
|
try {
|
|
227
335
|
await mkdir(dir, { recursive: true });
|
|
228
336
|
ok(`Writable: ${dir.replace(homedir(), "~")}`);
|
|
@@ -232,6 +340,22 @@ async function stepPreflight(opts: SetupOptions): Promise<void> {
|
|
|
232
340
|
}
|
|
233
341
|
}
|
|
234
342
|
|
|
343
|
+
// Seed .env with commented-out example if it doesn't exist yet
|
|
344
|
+
if (!(await fileExists(ENV_PATH))) {
|
|
345
|
+
const seed = [
|
|
346
|
+
"# Roundhouse environment file",
|
|
347
|
+
"# Uncomment and set values, or use: roundhouse setup",
|
|
348
|
+
"#",
|
|
349
|
+
"# TELEGRAM_BOT_TOKEN=\"your-bot-token\"",
|
|
350
|
+
"# BOT_USERNAME=\"your_bot_username\"",
|
|
351
|
+
"# ALLOWED_USERS=\"your_telegram_username\"",
|
|
352
|
+
"# AWS_PROFILE=\"default\"",
|
|
353
|
+
"# AWS_REGION=\"us-east-1\"",
|
|
354
|
+
"",
|
|
355
|
+
].join("\n");
|
|
356
|
+
await writeFile(ENV_PATH, seed, { mode: 0o600 });
|
|
357
|
+
}
|
|
358
|
+
|
|
235
359
|
// Disk space (rough check)
|
|
236
360
|
try {
|
|
237
361
|
const dfOut = execSafe("df", ["-BG", "--output=avail", homedir()], { silent: true });
|
|
@@ -323,7 +447,7 @@ async function stepStopGateway(): Promise<void> {
|
|
|
323
447
|
}
|
|
324
448
|
}
|
|
325
449
|
|
|
326
|
-
async function stepInstallPackages(opts: SetupOptions): Promise<void> {
|
|
450
|
+
async function stepInstallPackages(opts: SetupOptions, agent: AgentDefinition): Promise<void> {
|
|
327
451
|
step("④", "Installing packages...");
|
|
328
452
|
|
|
329
453
|
// Roundhouse
|
|
@@ -336,14 +460,20 @@ async function stepInstallPackages(opts: SetupOptions): Promise<void> {
|
|
|
336
460
|
ok("@inceptionstack/roundhouse");
|
|
337
461
|
}
|
|
338
462
|
|
|
339
|
-
//
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
463
|
+
// Agent packages (driven by agent definition)
|
|
464
|
+
for (const pkg of agent.packages) {
|
|
465
|
+
const label = pkg.name ?? pkg.packageName;
|
|
466
|
+
const installed = pkg.binary ? whichSync(pkg.binary) : false;
|
|
467
|
+
if (installed && !opts.force) {
|
|
468
|
+
ok(`${label} (already installed)`);
|
|
469
|
+
} else {
|
|
470
|
+
log(` Installing ${label}...`);
|
|
471
|
+
const args = pkg.install === "global"
|
|
472
|
+
? ["install", "-g", pkg.packageName]
|
|
473
|
+
: ["install", pkg.packageName];
|
|
474
|
+
execOrFail("npm", args, `${label} install`);
|
|
475
|
+
ok(label);
|
|
476
|
+
}
|
|
347
477
|
}
|
|
348
478
|
|
|
349
479
|
// psst-cli (requires bun runtime)
|
|
@@ -424,21 +554,26 @@ async function stepInstallPackages(opts: SetupOptions): Promise<void> {
|
|
|
424
554
|
}
|
|
425
555
|
}
|
|
426
556
|
|
|
427
|
-
// Install
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
557
|
+
// Install agent-specific psst extension (Pi: pi-psst)
|
|
558
|
+
if (agent.installExtension) {
|
|
559
|
+
log(" Installing agent psst extension...");
|
|
560
|
+
try {
|
|
561
|
+
await agent.installExtension("@miclivs/pi-psst");
|
|
562
|
+
ok("@miclivs/pi-psst extension");
|
|
563
|
+
} catch {
|
|
564
|
+
ok("@miclivs/pi-psst extension (already installed)");
|
|
565
|
+
}
|
|
435
566
|
}
|
|
436
567
|
}
|
|
437
568
|
|
|
438
569
|
// User extensions
|
|
439
570
|
for (const ext of opts.extensions) {
|
|
571
|
+
if (!agent.installExtension) {
|
|
572
|
+
fail(`--extension is not supported for agent "${agent.type}"`);
|
|
573
|
+
throw new Error(`Agent "${agent.type}" does not support extensions`);
|
|
574
|
+
}
|
|
440
575
|
log(` Installing extension: ${ext}...`);
|
|
441
|
-
|
|
576
|
+
await agent.installExtension(ext);
|
|
442
577
|
ok(ext);
|
|
443
578
|
}
|
|
444
579
|
}
|
|
@@ -490,45 +625,24 @@ async function stepConfigure(
|
|
|
490
625
|
opts: SetupOptions,
|
|
491
626
|
botInfo: BotInfo,
|
|
492
627
|
pairResult: PairResult | null,
|
|
628
|
+
agent: AgentDefinition,
|
|
493
629
|
): Promise<void> {
|
|
494
630
|
step("⑦", "Configuring...");
|
|
495
631
|
|
|
496
632
|
await mkdir(ROUNDHOUSE_DIR, { recursive: true });
|
|
497
|
-
await mkdir(dirname(PI_SETTINGS_PATH), { recursive: true });
|
|
498
|
-
|
|
499
|
-
// ── Pi settings ──
|
|
500
|
-
let piSettings: Record<string, any> = {};
|
|
501
|
-
try {
|
|
502
|
-
piSettings = JSON.parse(await readFile(PI_SETTINGS_PATH, "utf8"));
|
|
503
|
-
} catch { /* doesn't exist */ }
|
|
504
633
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
}
|
|
516
|
-
if (existingModel && existingModel !== opts.model) {
|
|
517
|
-
warn(`Pi model already set to '${existingModel}' (keeping, use --force to override)`);
|
|
518
|
-
} else {
|
|
519
|
-
piSettings.defaultModel = opts.model;
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
// Ensure packages array includes pi-psst if using psst
|
|
524
|
-
if (!piSettings.packages) piSettings.packages = [];
|
|
525
|
-
if (opts.psst && !piSettings.packages.includes("npm:@miclivs/pi-psst")) {
|
|
526
|
-
piSettings.packages.push("npm:@miclivs/pi-psst");
|
|
634
|
+
// Agent-specific config (Pi: ~/.pi/agent/settings.json)
|
|
635
|
+
if (agent.configure) {
|
|
636
|
+
await agent.configure({
|
|
637
|
+
provider: opts.provider,
|
|
638
|
+
model: opts.model,
|
|
639
|
+
cwd: opts.cwd,
|
|
640
|
+
force: opts.force,
|
|
641
|
+
psst: opts.psst,
|
|
642
|
+
extensions: opts.extensions,
|
|
643
|
+
});
|
|
527
644
|
}
|
|
528
645
|
|
|
529
|
-
await atomicWriteJson(PI_SETTINGS_PATH, piSettings);
|
|
530
|
-
ok(`~/.pi/agent/settings.json (${piSettings.defaultProvider}, ${piSettings.defaultModel})`);
|
|
531
|
-
|
|
532
646
|
// ── Gateway config ──
|
|
533
647
|
let gatewayConfig: Record<string, any> = {};
|
|
534
648
|
if (!opts.force) {
|
|
@@ -559,7 +673,7 @@ async function stepConfigure(
|
|
|
559
673
|
gatewayConfig = {
|
|
560
674
|
...gatewayConfig,
|
|
561
675
|
_version: 1, // Config schema version — for future migration support
|
|
562
|
-
agent: { ...gatewayConfig.agent, type:
|
|
676
|
+
agent: { ...gatewayConfig.agent, ...agent.configDefaults, type: agent.type, cwd: opts.cwd },
|
|
563
677
|
chat: {
|
|
564
678
|
...gatewayConfig.chat,
|
|
565
679
|
botUsername: botInfo.username,
|
|
@@ -752,6 +866,252 @@ async function stepPostflight(): Promise<void> {
|
|
|
752
866
|
}
|
|
753
867
|
}
|
|
754
868
|
|
|
869
|
+
// ── BotFather Guide ──────────────────────────────────
|
|
870
|
+
|
|
871
|
+
function printBotFatherGuide(): void {
|
|
872
|
+
log("");
|
|
873
|
+
log(" 🤖 Create a Telegram Bot");
|
|
874
|
+
log(" ────────────────────────");
|
|
875
|
+
log(" 1. Open https://t.me/BotFather");
|
|
876
|
+
log(" 2. Send /newbot");
|
|
877
|
+
log(" 3. Choose a display name (e.g. 'My Roundhouse')");
|
|
878
|
+
log(" 4. Choose a username ending in 'bot' (e.g. 'my_roundhouse_bot')");
|
|
879
|
+
log(" 5. Copy the token BotFather returns");
|
|
880
|
+
log("");
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// ── Interactive Telegram Setup ───────────────────────
|
|
884
|
+
|
|
885
|
+
async function runInteractiveTelegramSetup(opts: SetupOptions): Promise<void> {
|
|
886
|
+
const agent = resolveAgentForSetup(opts);
|
|
887
|
+
log("\n🔧 Roundhouse Telegram Setup");
|
|
888
|
+
log("━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
889
|
+
|
|
890
|
+
try {
|
|
891
|
+
// Step 1: Preflight
|
|
892
|
+
await stepPreflight(opts, agent);
|
|
893
|
+
|
|
894
|
+
// Step 2: Get bot token (prompt if not provided)
|
|
895
|
+
if (!opts.botToken) {
|
|
896
|
+
log("");
|
|
897
|
+
printBotFatherGuide();
|
|
898
|
+
opts.botToken = await promptMasked(" Paste your bot token");
|
|
899
|
+
if (!opts.botToken) {
|
|
900
|
+
fail("No token provided");
|
|
901
|
+
process.exit(2);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
const botInfo = await stepValidateToken(opts);
|
|
905
|
+
|
|
906
|
+
// Step 3: Get username (prompt if not provided)
|
|
907
|
+
if (opts.users.length === 0) {
|
|
908
|
+
step("③", "Telegram username...");
|
|
909
|
+
const username = await promptText(" Your Telegram username (without @)");
|
|
910
|
+
if (!username) {
|
|
911
|
+
fail("Username required");
|
|
912
|
+
process.exit(2);
|
|
913
|
+
}
|
|
914
|
+
opts.users.push(username.replace(/^@/, ""));
|
|
915
|
+
ok(`Allowed: ${opts.users.map(u => `@${u}`).join(", ")}`);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// Step 4: Stop existing gateway
|
|
919
|
+
await stepStopGateway();
|
|
920
|
+
|
|
921
|
+
// Step 5: Install packages
|
|
922
|
+
await stepInstallPackages(opts, agent);
|
|
923
|
+
|
|
924
|
+
// Step 6: Pair via Telegram
|
|
925
|
+
step("⑥", "Pairing with Telegram...");
|
|
926
|
+
const nonce = createPairingNonce();
|
|
927
|
+
const pairingLink = createPairingLink(botInfo.username, nonce);
|
|
928
|
+
log(`\n Open this link to pair:\n`);
|
|
929
|
+
log(` 🔗 ${pairingLink}\n`);
|
|
930
|
+
printQr(pairingLink, opts.qr);
|
|
931
|
+
log(` Or send /start ${nonce} to @${botInfo.username}`);
|
|
932
|
+
log("");
|
|
933
|
+
|
|
934
|
+
const pairResult = await pairTelegram(
|
|
935
|
+
opts.botToken, botInfo.username, opts.users,
|
|
936
|
+
300_000, log, { nonce, showLink: false },
|
|
937
|
+
);
|
|
938
|
+
if (!pairResult) {
|
|
939
|
+
warn("Pairing timed out. Run 'roundhouse pair' later.");
|
|
940
|
+
} else {
|
|
941
|
+
ok(`Paired with @${pairResult.username} (chat: ${pairResult.chatId})`);
|
|
942
|
+
if (!opts.notifyChatIds.includes(pairResult.chatId)) {
|
|
943
|
+
opts.notifyChatIds.push(pairResult.chatId);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// Step 7: Store secrets
|
|
948
|
+
await stepStoreSecrets(opts, botInfo);
|
|
949
|
+
|
|
950
|
+
// Step 8: Write config
|
|
951
|
+
await stepConfigure(opts, botInfo, pairResult, agent);
|
|
952
|
+
|
|
953
|
+
// Step 9: Register commands + install service
|
|
954
|
+
await stepRegisterCommands(opts);
|
|
955
|
+
await stepInstallSystemd(opts);
|
|
956
|
+
|
|
957
|
+
// Step 10: Verify
|
|
958
|
+
await stepPostflight();
|
|
959
|
+
|
|
960
|
+
// Done!
|
|
961
|
+
log("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
962
|
+
log("✅ Roundhouse is ready!");
|
|
963
|
+
log(` Bot: @${botInfo.username}`);
|
|
964
|
+
log(` Send /status to @${botInfo.username} on Telegram.\n`);
|
|
965
|
+
} catch (err: any) {
|
|
966
|
+
log("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
967
|
+
log(`❌ Setup failed: ${err.message}`);
|
|
968
|
+
log(" Re-run: roundhouse setup --telegram\n");
|
|
969
|
+
process.exit(1);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// ── Headless Telegram Setup ─────────────────────────
|
|
974
|
+
|
|
975
|
+
async function runHeadlessTelegramSetup(opts: SetupOptions): Promise<void> {
|
|
976
|
+
const agent = resolveAgentForSetup(opts);
|
|
977
|
+
const logger = createJsonLogger();
|
|
978
|
+
|
|
979
|
+
// Override module-level log helpers to emit JSON instead of text
|
|
980
|
+
const savedLog = log, savedStep = step, savedOk = ok, savedWarn = warn, savedFail = fail;
|
|
981
|
+
log = (msg) => logger.info("log", msg);
|
|
982
|
+
step = (_n, label) => logger.info("step", label);
|
|
983
|
+
ok = (msg) => logger.ok(msg);
|
|
984
|
+
warn = (msg) => logger.warn("warn", msg);
|
|
985
|
+
fail = (msg) => logger.fail(msg);
|
|
986
|
+
|
|
987
|
+
try {
|
|
988
|
+
// Validate required inputs
|
|
989
|
+
if (!opts.botToken) {
|
|
990
|
+
logger.error("validation.failed", "TELEGRAM_BOT_TOKEN env var required for --headless");
|
|
991
|
+
process.exit(2);
|
|
992
|
+
}
|
|
993
|
+
if (opts.users.length === 0) {
|
|
994
|
+
logger.error("validation.failed", "--user is required for --headless");
|
|
995
|
+
process.exit(2);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// Step 1: Preflight
|
|
999
|
+
logger.step(1, 9, "preflight.start", "Running preflight checks");
|
|
1000
|
+
await stepPreflight(opts, agent);
|
|
1001
|
+
logger.ok("Preflight passed");
|
|
1002
|
+
|
|
1003
|
+
// Step 2: Validate token
|
|
1004
|
+
logger.step(2, 9, "telegram.validate", "Validating Telegram bot token");
|
|
1005
|
+
const botInfo = await stepValidateToken(opts);
|
|
1006
|
+
logger.ok(`Bot: @${botInfo.username} (id: ${botInfo.id})`);
|
|
1007
|
+
|
|
1008
|
+
// Step 3: Stop existing gateway
|
|
1009
|
+
logger.step(3, 9, "gateway.stop", "Checking for running gateway");
|
|
1010
|
+
await stepStopGateway();
|
|
1011
|
+
|
|
1012
|
+
// Step 4: Install packages
|
|
1013
|
+
logger.step(4, 9, "packages.install", "Installing packages");
|
|
1014
|
+
await stepInstallPackages(opts, agent);
|
|
1015
|
+
logger.ok("Packages installed");
|
|
1016
|
+
|
|
1017
|
+
// Step 5: Create pending pairing
|
|
1018
|
+
logger.step(5, 9, "pairing.pending", "Creating pending pairing");
|
|
1019
|
+
let nonce: string;
|
|
1020
|
+
const existing = await readPendingPairing();
|
|
1021
|
+
if (existing?.status === "pending" && !opts.force) {
|
|
1022
|
+
nonce = existing.nonce;
|
|
1023
|
+
logger.info("pairing.reuse", `Reusing existing nonce: ${nonce}`);
|
|
1024
|
+
} else {
|
|
1025
|
+
nonce = createPairingNonce();
|
|
1026
|
+
}
|
|
1027
|
+
const pairingLink = createPairingLink(botInfo.username, nonce);
|
|
1028
|
+
const pendingPairing: PendingPairing = {
|
|
1029
|
+
version: 1,
|
|
1030
|
+
nonce,
|
|
1031
|
+
botUsername: botInfo.username,
|
|
1032
|
+
allowedUsers: opts.users,
|
|
1033
|
+
createdAt: new Date().toISOString(),
|
|
1034
|
+
status: "pending",
|
|
1035
|
+
};
|
|
1036
|
+
await writePendingPairing(pendingPairing);
|
|
1037
|
+
logger.info("pairing.link", `Pairing link: ${pairingLink}`, { pairingLink, nonce });
|
|
1038
|
+
|
|
1039
|
+
// Step 6: Store secrets
|
|
1040
|
+
logger.step(6, 9, "secrets.store", "Storing secrets");
|
|
1041
|
+
await stepStoreSecrets(opts, botInfo);
|
|
1042
|
+
|
|
1043
|
+
// Step 7: Write config (no pair result yet — gateway will complete pairing)
|
|
1044
|
+
logger.step(7, 9, "config.write", "Writing configuration");
|
|
1045
|
+
await stepConfigure(opts, botInfo, null, agent);
|
|
1046
|
+
logger.ok("Config written");
|
|
1047
|
+
|
|
1048
|
+
// Step 8: Register commands
|
|
1049
|
+
logger.step(8, 9, "commands.register", "Registering bot commands");
|
|
1050
|
+
await stepRegisterCommands(opts);
|
|
1051
|
+
logger.ok("Bot commands registered");
|
|
1052
|
+
|
|
1053
|
+
// Step 9: Install and start service
|
|
1054
|
+
logger.step(9, 9, "service.install", "Installing and starting service");
|
|
1055
|
+
if (!opts.systemd) {
|
|
1056
|
+
logger.warn("service.skip", "--no-systemd: service not installed. Start manually: roundhouse start");
|
|
1057
|
+
} else {
|
|
1058
|
+
await stepInstallSystemd(opts);
|
|
1059
|
+
logger.ok("Service installed and started");
|
|
1060
|
+
|
|
1061
|
+
// Verify service is active
|
|
1062
|
+
try {
|
|
1063
|
+
const { execFileSync } = await import("node:child_process");
|
|
1064
|
+
const state = execFileSync("systemctl", ["is-active", "roundhouse"], { encoding: "utf8" }).trim();
|
|
1065
|
+
if (state === "active") {
|
|
1066
|
+
logger.ok("Service is active");
|
|
1067
|
+
} else {
|
|
1068
|
+
logger.warn("service.state", `Service state: ${state}`);
|
|
1069
|
+
}
|
|
1070
|
+
} catch {
|
|
1071
|
+
logger.warn("service.state", "Could not verify service state");
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// Success
|
|
1076
|
+
logger.info("setup.complete", "Headless setup complete", {
|
|
1077
|
+
botUsername: botInfo.username,
|
|
1078
|
+
pairingLink,
|
|
1079
|
+
pairingStatus: "pending",
|
|
1080
|
+
serviceInstalled: opts.systemd,
|
|
1081
|
+
});
|
|
1082
|
+
log("");
|
|
1083
|
+
log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
|
|
1084
|
+
log(`✅ Roundhouse installed and running!`);
|
|
1085
|
+
log(``);
|
|
1086
|
+
log(` Bot: @${botInfo.username}`);
|
|
1087
|
+
log(` Pairing: Open ${pairingLink} to complete setup`);
|
|
1088
|
+
log(` Gateway is running and will accept pairing automatically.`);
|
|
1089
|
+
log(``);
|
|
1090
|
+
} catch (err: any) {
|
|
1091
|
+
const diag: SetupDiagnostics = {
|
|
1092
|
+
node: process.version,
|
|
1093
|
+
platform: platform(),
|
|
1094
|
+
arch: process.arch,
|
|
1095
|
+
cwd: process.cwd(),
|
|
1096
|
+
roundhouseDir: ROUNDHOUSE_DIR,
|
|
1097
|
+
configExists: await fileExists(CONFIG_PATH).catch(() => false),
|
|
1098
|
+
envExists: await fileExists(ENV_PATH).catch(() => false),
|
|
1099
|
+
pairingStatus: (await readPendingPairing())?.status ?? "not found",
|
|
1100
|
+
serviceState: "unknown",
|
|
1101
|
+
error: { name: err.name, message: err.message, stack: err.stack },
|
|
1102
|
+
};
|
|
1103
|
+
try {
|
|
1104
|
+
const { execFileSync } = await import("node:child_process");
|
|
1105
|
+
diag.serviceState = execFileSync("systemctl", ["is-active", "roundhouse"], { encoding: "utf8" }).trim();
|
|
1106
|
+
} catch {}
|
|
1107
|
+
printDiagnosticError(diag, true);
|
|
1108
|
+
process.exit(1);
|
|
1109
|
+
} finally {
|
|
1110
|
+
// Restore module-level log helpers
|
|
1111
|
+
log = savedLog; step = savedStep; ok = savedOk; warn = savedWarn; fail = savedFail;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
755
1115
|
// ── Orchestrator ─────────────────────────────────────
|
|
756
1116
|
|
|
757
1117
|
export async function cmdSetup(argv: string[]): Promise<void> {
|
|
@@ -769,17 +1129,29 @@ export async function cmdSetup(argv: string[]): Promise<void> {
|
|
|
769
1129
|
return;
|
|
770
1130
|
}
|
|
771
1131
|
|
|
1132
|
+
// Route to --telegram flows
|
|
1133
|
+
if (opts.telegram) {
|
|
1134
|
+
if (opts.headless) {
|
|
1135
|
+
await runHeadlessTelegramSetup(opts);
|
|
1136
|
+
} else {
|
|
1137
|
+
await runInteractiveTelegramSetup(opts);
|
|
1138
|
+
}
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// Legacy flow (no --telegram flag)
|
|
1143
|
+
const agent = resolveAgentForSetup(opts);
|
|
772
1144
|
log("\n🔧 Roundhouse Setup");
|
|
773
1145
|
log("━━━━━━━━━━━━━━━━━━━");
|
|
774
1146
|
|
|
775
1147
|
try {
|
|
776
1148
|
// Phase 1: Validate (no mutations)
|
|
777
|
-
await stepPreflight(opts);
|
|
1149
|
+
await stepPreflight(opts, agent);
|
|
778
1150
|
const botInfo = await stepValidateToken(opts);
|
|
779
1151
|
await stepStopGateway();
|
|
780
1152
|
|
|
781
1153
|
// Phase 2: Install packages
|
|
782
|
-
await stepInstallPackages(opts);
|
|
1154
|
+
await stepInstallPackages(opts, agent);
|
|
783
1155
|
|
|
784
1156
|
// Phase 3: Pair (before secrets/config, so paired username is included)
|
|
785
1157
|
const pairResult = await stepPair(opts, botInfo);
|
|
@@ -788,7 +1160,7 @@ export async function cmdSetup(argv: string[]): Promise<void> {
|
|
|
788
1160
|
await stepStoreSecrets(opts, botInfo);
|
|
789
1161
|
|
|
790
1162
|
// Phase 5: Write config (includes pair data)
|
|
791
|
-
await stepConfigure(opts, botInfo, pairResult);
|
|
1163
|
+
await stepConfigure(opts, botInfo, pairResult, agent);
|
|
792
1164
|
|
|
793
1165
|
// Phase 6: Remote setup
|
|
794
1166
|
await stepRegisterCommands(opts);
|
|
@@ -907,12 +1279,17 @@ export async function cmdPair(argv: string[]): Promise<void> {
|
|
|
907
1279
|
// ── Dry run ──────────────────────────────────────────
|
|
908
1280
|
|
|
909
1281
|
function printDryRun(opts: SetupOptions): void {
|
|
1282
|
+
const agent = getAgentDefinition(opts.agent);
|
|
910
1283
|
log("\n🔧 Roundhouse Setup (DRY RUN)");
|
|
911
1284
|
log("━━━━━━━━━━━━━━━━━━━\n");
|
|
1285
|
+
log(`Agent: ${agent.name} (${agent.type})`);
|
|
912
1286
|
log("Would validate Telegram token");
|
|
913
1287
|
log("Would stop existing gateway (if running)");
|
|
914
1288
|
log(`Would install: npm install -g @inceptionstack/roundhouse`);
|
|
915
|
-
|
|
1289
|
+
for (const pkg of agent.packages) {
|
|
1290
|
+
const scope = pkg.install === "global" ? "-g " : "";
|
|
1291
|
+
log(`Would install: npm install ${scope}${pkg.packageName}`);
|
|
1292
|
+
}
|
|
916
1293
|
if (opts.psst) {
|
|
917
1294
|
log(`Would install: bun runtime (if not present)`);
|
|
918
1295
|
log(`Would install: npm install -g psst-cli`);
|
|
@@ -926,7 +1303,10 @@ function printDryRun(opts: SetupOptions): void {
|
|
|
926
1303
|
if (opts.psst) {
|
|
927
1304
|
log(`Would store TELEGRAM_BOT_TOKEN, BOT_USERNAME, ALLOWED_USERS in psst`);
|
|
928
1305
|
}
|
|
929
|
-
|
|
1306
|
+
if (agent.configDirs?.length) {
|
|
1307
|
+
log(`Would configure: agent-specific settings`);
|
|
1308
|
+
log(` Agent: ${agent.name}`);
|
|
1309
|
+
}
|
|
930
1310
|
log(` Set defaultProvider: ${opts.provider}`);
|
|
931
1311
|
log(` Set defaultModel: ${opts.model}`);
|
|
932
1312
|
log(`Would write: ~/.roundhouse/gateway.config.json`);
|
|
@@ -941,20 +1321,27 @@ function printDryRun(opts: SetupOptions): void {
|
|
|
941
1321
|
function printSetupHelp(): void {
|
|
942
1322
|
console.log(`
|
|
943
1323
|
Usage:
|
|
944
|
-
|
|
945
|
-
roundhouse setup --
|
|
1324
|
+
roundhouse setup --telegram Interactive wizard (recommended)
|
|
1325
|
+
TELEGRAM_BOT_TOKEN=... roundhouse setup \\\n --telegram --headless --user USERNAME Headless automation (SSM/cloud-init)
|
|
1326
|
+
TELEGRAM_BOT_TOKEN=... roundhouse setup \\\n --user USERNAME Legacy (non-wizard) setup
|
|
946
1327
|
|
|
947
|
-
|
|
1328
|
+
Modes:
|
|
1329
|
+
--telegram Telegram-focused setup (wizard or headless)
|
|
1330
|
+
--headless Non-interactive automation (implies --non-interactive)
|
|
1331
|
+
Requires TELEGRAM_BOT_TOKEN env var and --user
|
|
1332
|
+
|
|
1333
|
+
Required (or prompted in interactive --telegram):
|
|
948
1334
|
--user <username> Telegram username (repeatable, strips @)
|
|
949
1335
|
|
|
950
|
-
Token
|
|
1336
|
+
Token:
|
|
951
1337
|
TELEGRAM_BOT_TOKEN env Preferred — not in shell history
|
|
952
|
-
--bot-token <token>
|
|
1338
|
+
--bot-token <token> Accepted in interactive mode only
|
|
953
1339
|
|
|
954
1340
|
Agent:
|
|
1341
|
+
--agent <type> Agent type (default: pi; available: ${listAvailableAgentTypes().join(", ")})
|
|
955
1342
|
--provider <provider> AI provider (default: amazon-bedrock)
|
|
956
1343
|
--model <model> AI model (default: us.anthropic.claude-opus-4-6-v1)
|
|
957
|
-
--extension <pkg>
|
|
1344
|
+
--extension <pkg> Agent extension (repeatable)
|
|
958
1345
|
--cwd <path> Agent working directory (default: ~)
|
|
959
1346
|
|
|
960
1347
|
Channel:
|
|
@@ -965,6 +1352,10 @@ Service:
|
|
|
965
1352
|
--no-voice Disable voice/STT
|
|
966
1353
|
--with-psst Use psst vault for secrets (default: .env file)
|
|
967
1354
|
|
|
1355
|
+
Display:
|
|
1356
|
+
--qr Force QR code display
|
|
1357
|
+
--no-qr Disable QR code display
|
|
1358
|
+
|
|
968
1359
|
Behavior:
|
|
969
1360
|
--non-interactive No pairing, no prompts
|
|
970
1361
|
--force Overwrite existing configs
|