@inceptionstack/roundhouse 0.3.17 → 0.3.19
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/pi.ts +59 -68
- package/src/agents/registry.ts +109 -16
- package/src/cli/cli.ts +42 -82
- package/src/cli/doctor/checks/disk.ts +2 -2
- 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 +446 -71
- package/src/config.ts +3 -2
- package/src/cron/runner.ts +9 -8
- package/src/gateway.ts +179 -39
- package/src/memory/state.ts +3 -15
- package/src/pairing.ts +112 -0
- package/src/util.ts +0 -8
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(), "~")}`);
|
|
@@ -339,7 +447,7 @@ async function stepStopGateway(): Promise<void> {
|
|
|
339
447
|
}
|
|
340
448
|
}
|
|
341
449
|
|
|
342
|
-
async function stepInstallPackages(opts: SetupOptions): Promise<void> {
|
|
450
|
+
async function stepInstallPackages(opts: SetupOptions, agent: AgentDefinition): Promise<void> {
|
|
343
451
|
step("④", "Installing packages...");
|
|
344
452
|
|
|
345
453
|
// Roundhouse
|
|
@@ -352,14 +460,20 @@ async function stepInstallPackages(opts: SetupOptions): Promise<void> {
|
|
|
352
460
|
ok("@inceptionstack/roundhouse");
|
|
353
461
|
}
|
|
354
462
|
|
|
355
|
-
//
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
+
}
|
|
363
477
|
}
|
|
364
478
|
|
|
365
479
|
// psst-cli (requires bun runtime)
|
|
@@ -440,21 +554,26 @@ async function stepInstallPackages(opts: SetupOptions): Promise<void> {
|
|
|
440
554
|
}
|
|
441
555
|
}
|
|
442
556
|
|
|
443
|
-
// Install
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
+
}
|
|
451
566
|
}
|
|
452
567
|
}
|
|
453
568
|
|
|
454
569
|
// User extensions
|
|
455
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
|
+
}
|
|
456
575
|
log(` Installing extension: ${ext}...`);
|
|
457
|
-
|
|
576
|
+
await agent.installExtension(ext);
|
|
458
577
|
ok(ext);
|
|
459
578
|
}
|
|
460
579
|
}
|
|
@@ -506,45 +625,24 @@ async function stepConfigure(
|
|
|
506
625
|
opts: SetupOptions,
|
|
507
626
|
botInfo: BotInfo,
|
|
508
627
|
pairResult: PairResult | null,
|
|
628
|
+
agent: AgentDefinition,
|
|
509
629
|
): Promise<void> {
|
|
510
630
|
step("⑦", "Configuring...");
|
|
511
631
|
|
|
512
632
|
await mkdir(ROUNDHOUSE_DIR, { recursive: true });
|
|
513
|
-
await mkdir(dirname(PI_SETTINGS_PATH), { recursive: true });
|
|
514
|
-
|
|
515
|
-
// ── Pi settings ──
|
|
516
|
-
let piSettings: Record<string, any> = {};
|
|
517
|
-
try {
|
|
518
|
-
piSettings = JSON.parse(await readFile(PI_SETTINGS_PATH, "utf8"));
|
|
519
|
-
} catch { /* doesn't exist */ }
|
|
520
633
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
}
|
|
532
|
-
if (existingModel && existingModel !== opts.model) {
|
|
533
|
-
warn(`Pi model already set to '${existingModel}' (keeping, use --force to override)`);
|
|
534
|
-
} else {
|
|
535
|
-
piSettings.defaultModel = opts.model;
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
// Ensure packages array includes pi-psst if using psst
|
|
540
|
-
if (!piSettings.packages) piSettings.packages = [];
|
|
541
|
-
if (opts.psst && !piSettings.packages.includes("npm:@miclivs/pi-psst")) {
|
|
542
|
-
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
|
+
});
|
|
543
644
|
}
|
|
544
645
|
|
|
545
|
-
await atomicWriteJson(PI_SETTINGS_PATH, piSettings);
|
|
546
|
-
ok(`~/.pi/agent/settings.json (${piSettings.defaultProvider}, ${piSettings.defaultModel})`);
|
|
547
|
-
|
|
548
646
|
// ── Gateway config ──
|
|
549
647
|
let gatewayConfig: Record<string, any> = {};
|
|
550
648
|
if (!opts.force) {
|
|
@@ -575,7 +673,7 @@ async function stepConfigure(
|
|
|
575
673
|
gatewayConfig = {
|
|
576
674
|
...gatewayConfig,
|
|
577
675
|
_version: 1, // Config schema version — for future migration support
|
|
578
|
-
agent: { ...gatewayConfig.agent, type:
|
|
676
|
+
agent: { ...gatewayConfig.agent, ...agent.configDefaults, type: agent.type, cwd: opts.cwd },
|
|
579
677
|
chat: {
|
|
580
678
|
...gatewayConfig.chat,
|
|
581
679
|
botUsername: botInfo.username,
|
|
@@ -768,6 +866,252 @@ async function stepPostflight(): Promise<void> {
|
|
|
768
866
|
}
|
|
769
867
|
}
|
|
770
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
|
+
|
|
771
1115
|
// ── Orchestrator ─────────────────────────────────────
|
|
772
1116
|
|
|
773
1117
|
export async function cmdSetup(argv: string[]): Promise<void> {
|
|
@@ -785,17 +1129,29 @@ export async function cmdSetup(argv: string[]): Promise<void> {
|
|
|
785
1129
|
return;
|
|
786
1130
|
}
|
|
787
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);
|
|
788
1144
|
log("\n🔧 Roundhouse Setup");
|
|
789
1145
|
log("━━━━━━━━━━━━━━━━━━━");
|
|
790
1146
|
|
|
791
1147
|
try {
|
|
792
1148
|
// Phase 1: Validate (no mutations)
|
|
793
|
-
await stepPreflight(opts);
|
|
1149
|
+
await stepPreflight(opts, agent);
|
|
794
1150
|
const botInfo = await stepValidateToken(opts);
|
|
795
1151
|
await stepStopGateway();
|
|
796
1152
|
|
|
797
1153
|
// Phase 2: Install packages
|
|
798
|
-
await stepInstallPackages(opts);
|
|
1154
|
+
await stepInstallPackages(opts, agent);
|
|
799
1155
|
|
|
800
1156
|
// Phase 3: Pair (before secrets/config, so paired username is included)
|
|
801
1157
|
const pairResult = await stepPair(opts, botInfo);
|
|
@@ -804,7 +1160,7 @@ export async function cmdSetup(argv: string[]): Promise<void> {
|
|
|
804
1160
|
await stepStoreSecrets(opts, botInfo);
|
|
805
1161
|
|
|
806
1162
|
// Phase 5: Write config (includes pair data)
|
|
807
|
-
await stepConfigure(opts, botInfo, pairResult);
|
|
1163
|
+
await stepConfigure(opts, botInfo, pairResult, agent);
|
|
808
1164
|
|
|
809
1165
|
// Phase 6: Remote setup
|
|
810
1166
|
await stepRegisterCommands(opts);
|
|
@@ -923,12 +1279,17 @@ export async function cmdPair(argv: string[]): Promise<void> {
|
|
|
923
1279
|
// ── Dry run ──────────────────────────────────────────
|
|
924
1280
|
|
|
925
1281
|
function printDryRun(opts: SetupOptions): void {
|
|
1282
|
+
const agent = getAgentDefinition(opts.agent);
|
|
926
1283
|
log("\n🔧 Roundhouse Setup (DRY RUN)");
|
|
927
1284
|
log("━━━━━━━━━━━━━━━━━━━\n");
|
|
1285
|
+
log(`Agent: ${agent.name} (${agent.type})`);
|
|
928
1286
|
log("Would validate Telegram token");
|
|
929
1287
|
log("Would stop existing gateway (if running)");
|
|
930
1288
|
log(`Would install: npm install -g @inceptionstack/roundhouse`);
|
|
931
|
-
|
|
1289
|
+
for (const pkg of agent.packages) {
|
|
1290
|
+
const scope = pkg.install === "global" ? "-g " : "";
|
|
1291
|
+
log(`Would install: npm install ${scope}${pkg.packageName}`);
|
|
1292
|
+
}
|
|
932
1293
|
if (opts.psst) {
|
|
933
1294
|
log(`Would install: bun runtime (if not present)`);
|
|
934
1295
|
log(`Would install: npm install -g psst-cli`);
|
|
@@ -942,7 +1303,10 @@ function printDryRun(opts: SetupOptions): void {
|
|
|
942
1303
|
if (opts.psst) {
|
|
943
1304
|
log(`Would store TELEGRAM_BOT_TOKEN, BOT_USERNAME, ALLOWED_USERS in psst`);
|
|
944
1305
|
}
|
|
945
|
-
|
|
1306
|
+
if (agent.configDirs?.length) {
|
|
1307
|
+
log(`Would configure: agent-specific settings`);
|
|
1308
|
+
log(` Agent: ${agent.name}`);
|
|
1309
|
+
}
|
|
946
1310
|
log(` Set defaultProvider: ${opts.provider}`);
|
|
947
1311
|
log(` Set defaultModel: ${opts.model}`);
|
|
948
1312
|
log(`Would write: ~/.roundhouse/gateway.config.json`);
|
|
@@ -957,20 +1321,27 @@ function printDryRun(opts: SetupOptions): void {
|
|
|
957
1321
|
function printSetupHelp(): void {
|
|
958
1322
|
console.log(`
|
|
959
1323
|
Usage:
|
|
960
|
-
|
|
961
|
-
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
|
|
962
1327
|
|
|
963
|
-
|
|
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):
|
|
964
1334
|
--user <username> Telegram username (repeatable, strips @)
|
|
965
1335
|
|
|
966
|
-
Token
|
|
1336
|
+
Token:
|
|
967
1337
|
TELEGRAM_BOT_TOKEN env Preferred — not in shell history
|
|
968
|
-
--bot-token <token>
|
|
1338
|
+
--bot-token <token> Accepted in interactive mode only
|
|
969
1339
|
|
|
970
1340
|
Agent:
|
|
1341
|
+
--agent <type> Agent type (default: pi; available: ${listAvailableAgentTypes().join(", ")})
|
|
971
1342
|
--provider <provider> AI provider (default: amazon-bedrock)
|
|
972
1343
|
--model <model> AI model (default: us.anthropic.claude-opus-4-6-v1)
|
|
973
|
-
--extension <pkg>
|
|
1344
|
+
--extension <pkg> Agent extension (repeatable)
|
|
974
1345
|
--cwd <path> Agent working directory (default: ~)
|
|
975
1346
|
|
|
976
1347
|
Channel:
|
|
@@ -981,6 +1352,10 @@ Service:
|
|
|
981
1352
|
--no-voice Disable voice/STT
|
|
982
1353
|
--with-psst Use psst vault for secrets (default: .env file)
|
|
983
1354
|
|
|
1355
|
+
Display:
|
|
1356
|
+
--qr Force QR code display
|
|
1357
|
+
--no-qr Disable QR code display
|
|
1358
|
+
|
|
984
1359
|
Behavior:
|
|
985
1360
|
--non-interactive No pairing, no prompts
|
|
986
1361
|
--force Overwrite existing configs
|