@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/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
- function log(msg: string) { console.log(msg); }
77
- function step(n: string, label: string) { log(`\n${n} ${label}`); }
78
- function ok(msg: string) { log(` ✓ ${msg}`); }
79
- function warn(msg: string) { log(` ⚠ ${msg}`); }
80
- function fail(msg: string) { log(` ✗ ${msg}`); }
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
- for (const dir of [ROUNDHOUSE_DIR, dirname(PI_SETTINGS_PATH)]) {
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
- // Pi agent
340
- const piInstalled = whichSync("pi");
341
- if (piInstalled && !opts.force) {
342
- ok(`@mariozechner/pi-coding-agent (already installed)`);
343
- } else {
344
- log(" Installing @mariozechner/pi-coding-agent...");
345
- execOrFail("npm", ["install", "-g", "@mariozechner/pi-coding-agent"], "pi install");
346
- ok("@mariozechner/pi-coding-agent");
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 pi-psst extension
428
- log(" Installing pi-psst extension...");
429
- try {
430
- execFileSync("pi", ["install", "npm:@miclivs/pi-psst"], { encoding: "utf8", stdio: "pipe", timeout: 120_000 });
431
- ok("@miclivs/pi-psst extension");
432
- } catch {
433
- // May already be installed
434
- ok("@miclivs/pi-psst extension (already installed)");
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
- execOrFail("pi", ["install", `npm:${ext}`], `extension ${ext}`);
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
- if (opts.force) {
506
- piSettings.defaultProvider = opts.provider;
507
- piSettings.defaultModel = opts.model;
508
- } else {
509
- const existingProvider = piSettings.defaultProvider;
510
- const existingModel = piSettings.defaultModel;
511
- if (existingProvider && existingProvider !== opts.provider) {
512
- warn(`Pi provider already set to '${existingProvider}' (keeping, use --force to override)`);
513
- } else {
514
- piSettings.defaultProvider = opts.provider;
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: "pi", cwd: opts.cwd },
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
- log(`Would install: npm install -g @mariozechner/pi-coding-agent`);
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
- log(`Would configure: ~/.pi/agent/settings.json`);
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
- TELEGRAM_BOT_TOKEN=... roundhouse setup --user USERNAME
945
- roundhouse setup --bot-token TOKEN --user USERNAME [options]
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
- Required:
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 (one required):
1336
+ Token:
951
1337
  TELEGRAM_BOT_TOKEN env Preferred — not in shell history
952
- --bot-token <token> Fallback for scripts
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> Pi extension (repeatable)
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