@inceptionstack/roundhouse 0.5.1 → 0.5.3
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/README.md +2 -1
- package/architecture.md +94 -31
- package/package.json +1 -1
- package/src/agents/kiro/kiro-adapter.ts +8 -1
- package/src/agents/pi/message-format.ts +87 -0
- package/src/agents/pi/pi-adapter.ts +9 -72
- package/src/cli/agent-command.ts +210 -0
- package/src/cli/cli.ts +69 -215
- package/src/cli/cron-commands.ts +258 -0
- package/src/cli/cron.ts +26 -267
- package/src/cli/launchd.ts +144 -0
- package/src/cli/service-manager.ts +192 -0
- package/src/cli/setup/args.ts +109 -0
- package/src/cli/setup/flows.ts +273 -0
- package/src/cli/setup/helpers.ts +66 -0
- package/src/cli/setup/index.ts +7 -0
- package/src/cli/setup/runtime.ts +109 -0
- package/src/cli/setup/steps.ts +617 -0
- package/src/cli/setup/types.ts +52 -0
- package/src/cli/setup.ts +79 -1231
- package/src/cli/shell.ts +49 -0
- package/src/cli/systemd.ts +6 -33
- package/src/config.ts +67 -53
- package/src/gateway/attachments.ts +147 -0
- package/src/gateway/commands.ts +371 -0
- package/src/gateway/helpers.ts +104 -0
- package/src/gateway/index.ts +11 -0
- package/src/gateway/streaming.ts +211 -0
- package/src/gateway.ts +212 -763
- package/src/types.ts +14 -0
package/src/cli/setup.ts
CHANGED
|
@@ -9,1193 +9,40 @@
|
|
|
9
9
|
* and starts the systemd service.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import {
|
|
13
|
-
import { resolve, dirname } from "node:path";
|
|
14
|
-
import { readFile, writeFile, mkdir, rename, unlink, realpath, stat } from "node:fs/promises";
|
|
15
|
-
import { execFileSync } from "node:child_process";
|
|
16
|
-
import { randomBytes } from "node:crypto";
|
|
12
|
+
import { readFile } from "node:fs/promises";
|
|
17
13
|
import { BOT_COMMANDS } from "../commands";
|
|
18
|
-
import {
|
|
14
|
+
import { atomicWriteJson, execSafe } from "./setup/helpers";
|
|
15
|
+
import { type SetupOptions } from "./setup/types";
|
|
16
|
+
import { parseSetupArgs } from "./setup/args";
|
|
17
|
+
export { parseSetupArgs } from "./setup/args";
|
|
19
18
|
import {
|
|
20
|
-
ROUNDHOUSE_DIR,
|
|
21
19
|
CONFIG_PATH,
|
|
22
20
|
ENV_FILE_PATH as ENV_PATH,
|
|
23
|
-
fileExists,
|
|
24
21
|
} from "../config";
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
whichSync,
|
|
28
|
-
systemctl,
|
|
29
|
-
isServiceActive,
|
|
30
|
-
systemctlShow,
|
|
31
|
-
resolveExecStart,
|
|
32
|
-
generateUnit,
|
|
33
|
-
writeServiceUnit,
|
|
34
|
-
hasSudoAccess,
|
|
35
|
-
} from "./systemd";
|
|
22
|
+
import { parseEnvFile, unquoteEnvValue } from "./env-file";
|
|
36
23
|
import {
|
|
37
24
|
getAgentDefinition,
|
|
38
25
|
listAvailableAgentTypes,
|
|
39
|
-
type AgentDefinition,
|
|
40
|
-
type AgentSetupContext,
|
|
41
26
|
} from "../agents/registry";
|
|
42
27
|
import {
|
|
43
28
|
validateBotToken,
|
|
44
|
-
checkWebhook,
|
|
45
|
-
registerBotCommands,
|
|
46
29
|
pairTelegram,
|
|
47
|
-
sendMessage,
|
|
48
|
-
type BotInfo,
|
|
49
|
-
type PairResult,
|
|
50
30
|
} from "./setup-telegram";
|
|
51
|
-
import { promptText, promptMasked } from "./setup-prompts";
|
|
52
|
-
import { createTextLogger, createJsonLogger, type SetupLogger, type SetupDiagnostics, printDiagnosticError } from "./setup-logger";
|
|
53
|
-
import { printQr } from "./qr";
|
|
54
31
|
import {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
model: string;
|
|
70
|
-
extensions: string[];
|
|
71
|
-
cwd: string;
|
|
72
|
-
notifyChatIds: number[];
|
|
73
|
-
systemd: boolean;
|
|
74
|
-
voice: boolean;
|
|
75
|
-
psst: boolean;
|
|
76
|
-
nonInteractive: boolean;
|
|
77
|
-
force: boolean;
|
|
78
|
-
dryRun: boolean;
|
|
79
|
-
/** Telegram-focused setup flow */
|
|
80
|
-
telegram: boolean;
|
|
81
|
-
/** Fully headless automation (no TTY prompts) */
|
|
82
|
-
headless: boolean;
|
|
83
|
-
/** QR code display mode */
|
|
84
|
-
qr: "auto" | "always" | "never";
|
|
85
|
-
/** Agent type (default: pi) */
|
|
86
|
-
agent: string;
|
|
87
|
-
/** Set by detection: skip agent package install if already configured */
|
|
88
|
-
_skipAgentInstall?: boolean;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
type StepStatus = "ok" | "warn" | "skip" | "fail";
|
|
92
|
-
|
|
93
|
-
// ── Constants ────────────────────────────────────────
|
|
94
|
-
|
|
95
|
-
const PI_SETTINGS_PATH = resolve(homedir(), ".pi", "agent", "settings.json");
|
|
96
|
-
|
|
97
|
-
const DEFAULT_PROVIDER = "amazon-bedrock";
|
|
98
|
-
const DEFAULT_MODEL = "us.anthropic.claude-opus-4-6-v1";
|
|
99
|
-
|
|
100
|
-
const EXTENSION_NAME_RE = /^@?[a-z0-9][\w.\-/]*$/i;
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Resolve agent definition and wire up setup-specific functions.
|
|
104
|
-
* Pi: configure writes ~/.pi/agent/settings.json, installExtension calls pi install.
|
|
105
|
-
*/
|
|
106
|
-
function resolveAgentForSetup(opts: SetupOptions): AgentDefinition {
|
|
107
|
-
const agent = { ...getAgentDefinition(opts.agent) };
|
|
108
|
-
|
|
109
|
-
if (agent.type === "pi") {
|
|
110
|
-
agent.configure = async (ctx: AgentSetupContext) => {
|
|
111
|
-
// Read existing settings if present
|
|
112
|
-
let existing: Record<string, unknown> = {};
|
|
113
|
-
try {
|
|
114
|
-
existing = JSON.parse(await readFile(PI_SETTINGS_PATH, "utf8"));
|
|
115
|
-
} catch {}
|
|
116
|
-
|
|
117
|
-
const settings: Record<string, unknown> = { ...existing };
|
|
118
|
-
|
|
119
|
-
if (ctx.force) {
|
|
120
|
-
settings.defaultProvider = ctx.provider;
|
|
121
|
-
settings.defaultModel = ctx.model;
|
|
122
|
-
} else {
|
|
123
|
-
if (existing.defaultProvider && existing.defaultProvider !== ctx.provider) {
|
|
124
|
-
warn(`Pi provider already set to '${existing.defaultProvider}' (keeping, use --force to override)`);
|
|
125
|
-
} else {
|
|
126
|
-
settings.defaultProvider = ctx.provider;
|
|
127
|
-
}
|
|
128
|
-
if (existing.defaultModel && existing.defaultModel !== ctx.model) {
|
|
129
|
-
warn(`Pi model already set to '${existing.defaultModel}' (keeping, use --force to override)`);
|
|
130
|
-
} else {
|
|
131
|
-
settings.defaultModel = ctx.model;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Ensure packages array exists
|
|
136
|
-
if (!Array.isArray(settings.packages)) settings.packages = [];
|
|
137
|
-
|
|
138
|
-
const pkgs = settings.packages as string[];
|
|
139
|
-
|
|
140
|
-
// Remove stale self-reference (roundhouse no longer ships pi extensions)
|
|
141
|
-
const selfPkg = "npm:@inceptionstack/roundhouse";
|
|
142
|
-
const selfIdx = pkgs.indexOf(selfPkg);
|
|
143
|
-
if (selfIdx !== -1) pkgs.splice(selfIdx, 1);
|
|
144
|
-
|
|
145
|
-
// Add code review + branch protection extensions
|
|
146
|
-
const coreExtensions = [
|
|
147
|
-
"npm:@inceptionstack/pi-hard-no",
|
|
148
|
-
"npm:@inceptionstack/pi-branch-enforcer",
|
|
149
|
-
];
|
|
150
|
-
for (const ext of coreExtensions) {
|
|
151
|
-
if (!pkgs.includes(ext)) pkgs.push(ext);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Add pi-psst if using psst
|
|
155
|
-
if (ctx.psst) {
|
|
156
|
-
const psstPkg = "npm:@miclivs/pi-psst";
|
|
157
|
-
if (!pkgs.includes(psstPkg)) pkgs.push(psstPkg);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
await mkdir(dirname(PI_SETTINGS_PATH), { recursive: true });
|
|
161
|
-
await atomicWriteJson(PI_SETTINGS_PATH, settings);
|
|
162
|
-
ok(`~/.pi/agent/settings.json (${settings.defaultProvider}, ${settings.defaultModel})`);
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
agent.installExtension = async (ext: string) => {
|
|
166
|
-
execOrFail("pi", ["install", `npm:${ext}`], `extension ${ext}`);
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return agent;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// ── Helpers ──────────────────────────────────────────
|
|
174
|
-
|
|
175
|
-
let log = (msg: string) => { console.log(msg); };
|
|
176
|
-
let step = (n: string, label: string) => { log(`\n${n} ${label}`); };
|
|
177
|
-
let ok = (msg: string) => { log(` ✓ ${msg}`); };
|
|
178
|
-
let warn = (msg: string) => { log(` ⚠ ${msg}`); };
|
|
179
|
-
let fail = (msg: string) => { log(` ✗ ${msg}`); };
|
|
180
|
-
|
|
181
|
-
async function atomicWriteJson(path: string, data: unknown): Promise<void> {
|
|
182
|
-
const tmp = `${path}.tmp.${randomBytes(4).toString("hex")}`;
|
|
183
|
-
try {
|
|
184
|
-
await writeFile(tmp, JSON.stringify(data, null, 2) + "\n", { mode: 0o600 });
|
|
185
|
-
await rename(tmp, path);
|
|
186
|
-
} catch (err) {
|
|
187
|
-
try { await unlink(tmp); } catch {}
|
|
188
|
-
throw err;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
async function atomicWriteText(path: string, content: string, mode = 0o600): Promise<void> {
|
|
193
|
-
const tmp = `${path}.tmp.${randomBytes(4).toString("hex")}`;
|
|
194
|
-
try {
|
|
195
|
-
await writeFile(tmp, content, { mode });
|
|
196
|
-
await rename(tmp, path);
|
|
197
|
-
} catch (err) {
|
|
198
|
-
try { await unlink(tmp); } catch {}
|
|
199
|
-
throw err;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function execSafe(cmd: string, args: string[], opts: { silent?: boolean; input?: string } = {}): string {
|
|
204
|
-
try {
|
|
205
|
-
const result = execFileSync(cmd, args, {
|
|
206
|
-
encoding: "utf8",
|
|
207
|
-
stdio: opts.silent ? "pipe" : opts.input ? ["pipe", "pipe", "pipe"] : "pipe",
|
|
208
|
-
input: opts.input,
|
|
209
|
-
timeout: 120_000,
|
|
210
|
-
});
|
|
211
|
-
return result.trim();
|
|
212
|
-
} catch {
|
|
213
|
-
return "";
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function execOrFail(cmd: string, args: string[], label: string): string {
|
|
218
|
-
try {
|
|
219
|
-
return execFileSync(cmd, args, { encoding: "utf8", stdio: "pipe", timeout: 120_000 }).trim();
|
|
220
|
-
} catch (err: any) {
|
|
221
|
-
throw new Error(`${label}: ${err.stderr?.trim() || err.message}`);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// ── Arg parser ───────────────────────────────────────
|
|
226
|
-
|
|
227
|
-
export function parseSetupArgs(argv: string[]): SetupOptions {
|
|
228
|
-
const opts: SetupOptions = {
|
|
229
|
-
botToken: "",
|
|
230
|
-
users: [],
|
|
231
|
-
provider: DEFAULT_PROVIDER,
|
|
232
|
-
model: DEFAULT_MODEL,
|
|
233
|
-
extensions: [],
|
|
234
|
-
cwd: homedir(),
|
|
235
|
-
notifyChatIds: [],
|
|
236
|
-
systemd: platform() === "linux",
|
|
237
|
-
voice: platform() === "linux", // Default off on macOS (whisper install is heavy)
|
|
238
|
-
psst: false,
|
|
239
|
-
nonInteractive: false,
|
|
240
|
-
force: false,
|
|
241
|
-
dryRun: false,
|
|
242
|
-
telegram: false,
|
|
243
|
-
headless: false,
|
|
244
|
-
qr: "auto",
|
|
245
|
-
agent: "pi",
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
for (let i = 0; i < argv.length; i++) {
|
|
249
|
-
const arg = argv[i];
|
|
250
|
-
const next = () => {
|
|
251
|
-
if (i + 1 >= argv.length) throw new Error(`Missing value for ${arg}`);
|
|
252
|
-
return argv[++i];
|
|
253
|
-
};
|
|
254
|
-
|
|
255
|
-
switch (arg) {
|
|
256
|
-
case "--bot-token": opts.botToken = next(); break;
|
|
257
|
-
case "--user": opts.users.push(next().replace(/^@/, "")); break;
|
|
258
|
-
case "--provider": opts.provider = next(); break;
|
|
259
|
-
case "--model": opts.model = next(); break;
|
|
260
|
-
case "--extension": opts.extensions.push(next()); break;
|
|
261
|
-
case "--cwd": opts.cwd = next(); break;
|
|
262
|
-
case "--notify-chat": opts.notifyChatIds.push(parseInt(next(), 10)); break;
|
|
263
|
-
case "--no-systemd": opts.systemd = false; break;
|
|
264
|
-
case "--no-voice": opts.voice = false; break;
|
|
265
|
-
case "--with-psst": opts.psst = true; break;
|
|
266
|
-
case "--non-interactive": opts.nonInteractive = true; break;
|
|
267
|
-
case "--telegram": opts.telegram = true; break;
|
|
268
|
-
case "--headless": opts.headless = true; opts.nonInteractive = true; break;
|
|
269
|
-
case "--agent": opts.agent = next().toLowerCase(); break;
|
|
270
|
-
case "--qr": opts.qr = "always"; break;
|
|
271
|
-
case "--no-qr": opts.qr = "never"; break;
|
|
272
|
-
case "--force": opts.force = true; break;
|
|
273
|
-
case "--dry-run": opts.dryRun = true; break;
|
|
274
|
-
default:
|
|
275
|
-
if (arg.startsWith("-")) throw new Error(`Unknown flag: ${arg}`);
|
|
276
|
-
throw new Error(`Unexpected argument: ${arg}`);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Token from env if not in flags
|
|
281
|
-
if (!opts.botToken) {
|
|
282
|
-
opts.botToken = process.env.TELEGRAM_BOT_TOKEN ?? "";
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Headless: reject --bot-token (argv visible in process listings)
|
|
286
|
-
if (opts.headless && argv.some((a) => a === "--bot-token")) {
|
|
287
|
-
throw new Error(
|
|
288
|
-
"--bot-token is not accepted in --headless mode (argv visible in process listings).\n" +
|
|
289
|
-
"Use: TELEGRAM_BOT_TOKEN=... roundhouse setup --telegram --headless --user USERNAME",
|
|
290
|
-
);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// Validate agent type
|
|
294
|
-
try {
|
|
295
|
-
getAgentDefinition(opts.agent);
|
|
296
|
-
} catch (err: any) {
|
|
297
|
-
throw new Error(err.message);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Interactive --telegram defers token/user prompting to the wizard
|
|
301
|
-
const isInteractiveTelegram = opts.telegram && !opts.headless && !opts.nonInteractive && process.stdin.isTTY;
|
|
302
|
-
|
|
303
|
-
// Validate
|
|
304
|
-
if (!opts.botToken && !opts.dryRun && !isInteractiveTelegram) {
|
|
305
|
-
throw new Error(
|
|
306
|
-
"Bot token required. Provide via:\n" +
|
|
307
|
-
" TELEGRAM_BOT_TOKEN=... roundhouse setup --user USERNAME\n" +
|
|
308
|
-
" roundhouse setup --bot-token TOKEN --user USERNAME",
|
|
309
|
-
);
|
|
310
|
-
}
|
|
311
|
-
if (opts.users.length === 0 && !isInteractiveTelegram) {
|
|
312
|
-
throw new Error(
|
|
313
|
-
"At least one --user USERNAME is required.\n" +
|
|
314
|
-
"This is your Telegram username (without @).",
|
|
315
|
-
);
|
|
316
|
-
}
|
|
317
|
-
for (const ext of opts.extensions) {
|
|
318
|
-
if (!EXTENSION_NAME_RE.test(ext)) {
|
|
319
|
-
throw new Error(`Invalid extension name: ${ext}`);
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
if (opts.notifyChatIds.some(isNaN)) {
|
|
323
|
-
throw new Error("--notify-chat must be a number");
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
return opts;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// ── Steps ────────────────────────────────────────────
|
|
330
|
-
|
|
331
|
-
async function stepPreflight(opts: SetupOptions, agent: AgentDefinition): Promise<void> {
|
|
332
|
-
step("①", "Preflight checks...");
|
|
333
|
-
|
|
334
|
-
// Node version
|
|
335
|
-
const nodeVer = process.version;
|
|
336
|
-
const major = parseInt(nodeVer.replace("v", ""));
|
|
337
|
-
if (major < 20) {
|
|
338
|
-
fail(`Node.js ${nodeVer} — version 20+ required`);
|
|
339
|
-
throw new Error("Node.js 20+ required");
|
|
340
|
-
}
|
|
341
|
-
ok(`Node.js ${nodeVer}`);
|
|
342
|
-
|
|
343
|
-
// npm
|
|
344
|
-
if (!whichSync("npm")) {
|
|
345
|
-
fail("npm not found on PATH");
|
|
346
|
-
throw new Error("npm required");
|
|
347
|
-
}
|
|
348
|
-
ok("npm available");
|
|
349
|
-
|
|
350
|
-
// Config dirs writable
|
|
351
|
-
const dirs = [ROUNDHOUSE_DIR, ...(agent.configDirs ?? [])];
|
|
352
|
-
for (const dir of dirs) {
|
|
353
|
-
try {
|
|
354
|
-
await mkdir(dir, { recursive: true });
|
|
355
|
-
ok(`Writable: ${dir.replace(homedir(), "~")}`);
|
|
356
|
-
} catch {
|
|
357
|
-
fail(`Cannot create: ${dir}`);
|
|
358
|
-
throw new Error(`Cannot write to ${dir}`);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// Seed .env with commented-out example if it doesn't exist yet
|
|
363
|
-
if (!(await fileExists(ENV_PATH))) {
|
|
364
|
-
const seed = [
|
|
365
|
-
"# Roundhouse environment file",
|
|
366
|
-
"# Uncomment and set values, or use: roundhouse setup",
|
|
367
|
-
"#",
|
|
368
|
-
"# TELEGRAM_BOT_TOKEN=\"your-bot-token\"",
|
|
369
|
-
"# BOT_USERNAME=\"your_bot_username\"",
|
|
370
|
-
"# ALLOWED_USERS=\"your_telegram_username\"",
|
|
371
|
-
"# AWS_PROFILE=\"default\"",
|
|
372
|
-
"# AWS_REGION=\"us-east-1\"",
|
|
373
|
-
"",
|
|
374
|
-
].join("\n");
|
|
375
|
-
await writeFile(ENV_PATH, seed, { mode: 0o600 });
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// Disk space (rough check)
|
|
379
|
-
try {
|
|
380
|
-
const dfOut = execSafe("df", ["-BG", "--output=avail", homedir()], { silent: true });
|
|
381
|
-
const match = dfOut.match(/(\d+)G/);
|
|
382
|
-
if (match) {
|
|
383
|
-
const freeGB = parseInt(match[1]);
|
|
384
|
-
if (freeGB < 1) {
|
|
385
|
-
fail(`Disk: ${freeGB} GB free (need >= 1 GB)`);
|
|
386
|
-
throw new Error("Insufficient disk space");
|
|
387
|
-
}
|
|
388
|
-
ok(`Disk: ${freeGB} GB free`);
|
|
389
|
-
}
|
|
390
|
-
} catch { /* non-fatal, df might not support these flags */ }
|
|
391
|
-
|
|
392
|
-
// Provider credentials (warn only)
|
|
393
|
-
if (opts.provider === "amazon-bedrock") {
|
|
394
|
-
const hasAws =
|
|
395
|
-
process.env.AWS_ACCESS_KEY_ID ||
|
|
396
|
-
process.env.AWS_PROFILE ||
|
|
397
|
-
await fileExists(resolve(homedir(), ".aws", "credentials")) ||
|
|
398
|
-
await fileExists(resolve(homedir(), ".aws", "config"));
|
|
399
|
-
|
|
400
|
-
// Also check instance metadata (EC2 IAM role)
|
|
401
|
-
let hasInstanceRole = false;
|
|
402
|
-
if (!hasAws) {
|
|
403
|
-
try {
|
|
404
|
-
const result = execSafe("curl", ["-sf", "--max-time", "2",
|
|
405
|
-
"http://169.254.169.254/latest/meta-data/iam/security-credentials/"], { silent: true });
|
|
406
|
-
hasInstanceRole = result.length > 0;
|
|
407
|
-
} catch {}
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
if (hasAws) {
|
|
411
|
-
ok("AWS credentials found");
|
|
412
|
-
} else if (hasInstanceRole) {
|
|
413
|
-
ok("AWS credentials found (instance IAM role)");
|
|
414
|
-
} else {
|
|
415
|
-
warn("AWS credentials not found — configure before first use");
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// --cwd validation
|
|
420
|
-
try {
|
|
421
|
-
const resolved = await realpath(opts.cwd);
|
|
422
|
-
const st = await stat(resolved);
|
|
423
|
-
if (!st.isDirectory()) throw new Error("not a directory");
|
|
424
|
-
opts.cwd = resolved;
|
|
425
|
-
ok(`Working directory: ${resolved.replace(homedir(), "~")}`);
|
|
426
|
-
} catch {
|
|
427
|
-
fail(`--cwd path invalid: ${opts.cwd}`);
|
|
428
|
-
throw new Error(`Invalid --cwd: ${opts.cwd}`);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
async function stepValidateToken(opts: SetupOptions): Promise<BotInfo> {
|
|
433
|
-
step("②", "Validating Telegram bot token...");
|
|
434
|
-
|
|
435
|
-
const botInfo = await validateBotToken(opts.botToken);
|
|
436
|
-
ok(`Bot: @${botInfo.username} (id: ${botInfo.id})`);
|
|
437
|
-
|
|
438
|
-
// Check for conflicting webhook
|
|
439
|
-
const webhook = await checkWebhook(opts.botToken);
|
|
440
|
-
if (webhook) {
|
|
441
|
-
warn(`Webhook active: ${webhook}`);
|
|
442
|
-
warn("Polling won't work while a webhook is set. Remove it or switch to webhook mode.");
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
return botInfo;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
async function stepStopGateway(): Promise<void> {
|
|
449
|
-
step("④", "Checking for running gateway...");
|
|
450
|
-
|
|
451
|
-
if (platform() !== "linux") {
|
|
452
|
-
ok("Not Linux — skipping service check");
|
|
453
|
-
return;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
if (isServiceActive()) {
|
|
457
|
-
log(" Stopping existing gateway...");
|
|
458
|
-
try {
|
|
459
|
-
systemctl("stop");
|
|
460
|
-
ok("Service stopped");
|
|
461
|
-
} catch {
|
|
462
|
-
warn("Could not stop service (may need sudo). Continuing anyway.");
|
|
463
|
-
}
|
|
464
|
-
} else {
|
|
465
|
-
ok("No running gateway");
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
async function stepInstallPackages(opts: SetupOptions, agent: AgentDefinition): Promise<void> {
|
|
470
|
-
step("⑤", "Installing packages...");
|
|
471
|
-
|
|
472
|
-
// Roundhouse
|
|
473
|
-
const rhInstalled = whichSync("roundhouse");
|
|
474
|
-
if (rhInstalled && !opts.force) {
|
|
475
|
-
ok(`@inceptionstack/roundhouse (already installed)`);
|
|
476
|
-
} else {
|
|
477
|
-
log(" Installing @inceptionstack/roundhouse...");
|
|
478
|
-
execOrFail("npm", ["install", "-g", "@inceptionstack/roundhouse"], "roundhouse install");
|
|
479
|
-
ok("@inceptionstack/roundhouse");
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
// Agent packages (driven by agent definition)
|
|
483
|
-
if (opts._skipAgentInstall) {
|
|
484
|
-
ok("Agent already configured — skipping package install");
|
|
485
|
-
} else {
|
|
486
|
-
for (const pkg of agent.packages) {
|
|
487
|
-
const label = pkg.name ?? pkg.packageName;
|
|
488
|
-
const installed = pkg.binary ? whichSync(pkg.binary) : false;
|
|
489
|
-
if (installed && !opts.force) {
|
|
490
|
-
ok(`${label} (already installed)`);
|
|
491
|
-
} else {
|
|
492
|
-
log(` Installing ${label}...`);
|
|
493
|
-
const args = pkg.install === "global"
|
|
494
|
-
? ["install", "-g", pkg.packageName]
|
|
495
|
-
: ["install", pkg.packageName];
|
|
496
|
-
execOrFail("npm", args, `${label} install`);
|
|
497
|
-
ok(label);
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
// psst-cli (requires bun runtime)
|
|
503
|
-
if (opts.psst) {
|
|
504
|
-
// Install bun if not present (psst-cli shebang is #!/usr/bin/env bun)
|
|
505
|
-
if (!whichSync("bun")) {
|
|
506
|
-
log(" Installing bun runtime (required by psst)...");
|
|
507
|
-
try {
|
|
508
|
-
execFileSync("bash", ["-c", "curl -fsSL https://bun.sh/install | bash"], {
|
|
509
|
-
encoding: "utf8", stdio: "pipe", timeout: 120_000,
|
|
510
|
-
env: { ...process.env, HOME: homedir() },
|
|
511
|
-
});
|
|
512
|
-
// bun installs to ~/.bun/bin/bun
|
|
513
|
-
const bunPath = resolve(homedir(), ".bun", "bin");
|
|
514
|
-
process.env.PATH = `${bunPath}:${process.env.PATH}`;
|
|
515
|
-
ok("bun runtime");
|
|
516
|
-
} catch (err: any) {
|
|
517
|
-
warn(`bun install failed: ${err.message}`);
|
|
518
|
-
warn("psst requires bun — install manually: curl -fsSL https://bun.sh/install | bash");
|
|
519
|
-
opts.psst = false;
|
|
520
|
-
}
|
|
521
|
-
} else {
|
|
522
|
-
ok("bun runtime (already installed)");
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// psst-cli
|
|
527
|
-
if (opts.psst) {
|
|
528
|
-
const psstInstalled = whichSync("psst");
|
|
529
|
-
if (psstInstalled && !opts.force) {
|
|
530
|
-
ok(`psst-cli (already installed)`);
|
|
531
|
-
} else {
|
|
532
|
-
log(" Installing psst-cli...");
|
|
533
|
-
try {
|
|
534
|
-
execFileSync("npm", ["install", "-g", "psst-cli"], {
|
|
535
|
-
encoding: "utf8", stdio: "pipe", timeout: 120_000,
|
|
536
|
-
});
|
|
537
|
-
} catch {
|
|
538
|
-
// npm may exit non-zero due to postinstall warnings — check if binary exists
|
|
539
|
-
}
|
|
540
|
-
if (whichSync("psst")) {
|
|
541
|
-
ok("psst-cli");
|
|
542
|
-
} else {
|
|
543
|
-
warn("psst-cli install failed");
|
|
544
|
-
opts.psst = false;
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
// Initialize psst vault
|
|
549
|
-
const vaultExists = await fileExists(resolve(homedir(), ".psst", "envs"));
|
|
550
|
-
if (vaultExists) {
|
|
551
|
-
ok("psst vault exists");
|
|
552
|
-
} else {
|
|
553
|
-
log(" Initializing psst vault...");
|
|
554
|
-
// On headless servers, no keychain is available — use PSST_PASSWORD
|
|
555
|
-
const psstEnv = { ...process.env };
|
|
556
|
-
if (!psstEnv.PSST_PASSWORD) {
|
|
557
|
-
// Generate a random password and store it for future use
|
|
558
|
-
const psstPw = randomBytes(32).toString("base64");
|
|
559
|
-
const pwFile = resolve(ROUNDHOUSE_DIR, ".psst-password");
|
|
560
|
-
await atomicWriteText(pwFile, psstPw + "\n", 0o600);
|
|
561
|
-
psstEnv.PSST_PASSWORD = psstPw;
|
|
562
|
-
// Also set for subsequent psst calls in this process
|
|
563
|
-
process.env.PSST_PASSWORD = psstPw;
|
|
564
|
-
}
|
|
565
|
-
try {
|
|
566
|
-
execFileSync("psst", ["init"], {
|
|
567
|
-
encoding: "utf8", stdio: "pipe", timeout: 30_000,
|
|
568
|
-
env: psstEnv,
|
|
569
|
-
});
|
|
570
|
-
ok("psst vault initialized");
|
|
571
|
-
} catch (err: any) {
|
|
572
|
-
warn(`psst vault init failed: ${err.stderr?.trim() || err.message}`);
|
|
573
|
-
// Clean up orphan password file
|
|
574
|
-
try { await unlink(resolve(ROUNDHOUSE_DIR, ".psst-password")); } catch {}
|
|
575
|
-
delete process.env.PSST_PASSWORD;
|
|
576
|
-
opts.psst = false;
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
// Install agent-specific psst extension (Pi: pi-psst)
|
|
581
|
-
if (agent.installExtension) {
|
|
582
|
-
log(" Installing agent psst extension...");
|
|
583
|
-
try {
|
|
584
|
-
await agent.installExtension("@miclivs/pi-psst");
|
|
585
|
-
ok("@miclivs/pi-psst extension");
|
|
586
|
-
} catch {
|
|
587
|
-
ok("@miclivs/pi-psst extension (already installed)");
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
// User extensions
|
|
593
|
-
for (const ext of opts.extensions) {
|
|
594
|
-
if (!agent.installExtension) {
|
|
595
|
-
fail(`--extension is not supported for agent "${agent.type}"`);
|
|
596
|
-
throw new Error(`Agent "${agent.type}" does not support extensions`);
|
|
597
|
-
}
|
|
598
|
-
log(` Installing extension: ${ext}...`);
|
|
599
|
-
await agent.installExtension(ext);
|
|
600
|
-
ok(ext);
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
async function stepStoreSecrets(opts: SetupOptions, botInfo: BotInfo): Promise<void> {
|
|
605
|
-
if (!opts.psst) {
|
|
606
|
-
step("⑧", "Storing secrets...");
|
|
607
|
-
ok("Skipped (default — use --with-psst to enable)");
|
|
608
|
-
return;
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
step("⑧", "Storing secrets in psst...");
|
|
612
|
-
|
|
613
|
-
const secrets: [string, string][] = [
|
|
614
|
-
["TELEGRAM_BOT_TOKEN", opts.botToken],
|
|
615
|
-
["BOT_USERNAME", botInfo.username],
|
|
616
|
-
["ALLOWED_USERS", opts.users.join(",")],
|
|
617
|
-
];
|
|
618
|
-
|
|
619
|
-
for (const [name, value] of secrets) {
|
|
620
|
-
try {
|
|
621
|
-
execFileSync("psst", ["set", name, "--stdin"], {
|
|
622
|
-
input: value,
|
|
623
|
-
encoding: "utf8",
|
|
624
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
625
|
-
timeout: 10_000,
|
|
626
|
-
});
|
|
627
|
-
ok(`${name} → psst vault`);
|
|
628
|
-
} catch {
|
|
629
|
-
// May already exist with same value
|
|
630
|
-
// Try overwrite
|
|
631
|
-
try {
|
|
632
|
-
execFileSync("psst", ["set", name, "--stdin"], {
|
|
633
|
-
input: value,
|
|
634
|
-
encoding: "utf8",
|
|
635
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
636
|
-
timeout: 10_000,
|
|
637
|
-
env: { ...process.env, PSST_FORCE: "1" },
|
|
638
|
-
});
|
|
639
|
-
ok(`${name} → psst vault (updated)`);
|
|
640
|
-
} catch (err: any) {
|
|
641
|
-
warn(`Failed to store ${name} in psst: ${err.message}`);
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
// ── Bundle install ──────────────────────────────────────────────────
|
|
648
|
-
|
|
649
|
-
async function stepInstallBundle(opts: SetupOptions): Promise<void> {
|
|
650
|
-
step("⑥", "Installing bundle (skills + CLI tools)...");
|
|
651
|
-
|
|
652
|
-
const bundleLog: ProvisionLog = {
|
|
653
|
-
info: (msg) => log(` ${msg}`),
|
|
654
|
-
warn: (msg) => warn(msg),
|
|
655
|
-
ok: (msg) => ok(msg),
|
|
656
|
-
};
|
|
657
|
-
|
|
658
|
-
provisionBundle({ force: opts.force, log: bundleLog });
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
async function stepConfigure(
|
|
662
|
-
opts: SetupOptions,
|
|
663
|
-
botInfo: BotInfo,
|
|
664
|
-
pairResult: PairResult | null,
|
|
665
|
-
agent: AgentDefinition,
|
|
666
|
-
): Promise<void> {
|
|
667
|
-
step("⑨", "Configuring...");
|
|
668
|
-
|
|
669
|
-
await mkdir(ROUNDHOUSE_DIR, { recursive: true });
|
|
670
|
-
|
|
671
|
-
// Agent-specific config (Pi: ~/.pi/agent/settings.json)
|
|
672
|
-
if (agent.configure) {
|
|
673
|
-
await agent.configure({
|
|
674
|
-
provider: opts.provider,
|
|
675
|
-
model: opts.model,
|
|
676
|
-
cwd: opts.cwd,
|
|
677
|
-
force: opts.force,
|
|
678
|
-
psst: opts.psst,
|
|
679
|
-
extensions: opts.extensions,
|
|
680
|
-
});
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
// ── Gateway config ──
|
|
684
|
-
let gatewayConfig: Record<string, any> = {};
|
|
685
|
-
if (!opts.force) {
|
|
686
|
-
try {
|
|
687
|
-
gatewayConfig = JSON.parse(await readFile(CONFIG_PATH, "utf8"));
|
|
688
|
-
} catch { /* new install */ }
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
// Merge users
|
|
692
|
-
const existingUsers: string[] = gatewayConfig.chat?.allowedUsers ?? [];
|
|
693
|
-
const existingUserIds: number[] = gatewayConfig.chat?.allowedUserIds ?? [];
|
|
694
|
-
const existingNotifyIds: number[] = (gatewayConfig.chat?.notifyChatIds ?? []).map(Number).filter((n) => !isNaN(n));
|
|
695
|
-
|
|
696
|
-
const mergedUsers = [...new Set([...existingUsers, ...opts.users])];
|
|
697
|
-
const mergedUserIds = [...existingUserIds];
|
|
698
|
-
const mergedNotifyIds = [...new Set([...existingNotifyIds, ...opts.notifyChatIds])];
|
|
699
|
-
|
|
700
|
-
// Add paired user data
|
|
701
|
-
if (pairResult) {
|
|
702
|
-
if (!mergedUserIds.includes(pairResult.userId)) {
|
|
703
|
-
mergedUserIds.push(pairResult.userId);
|
|
704
|
-
}
|
|
705
|
-
if (!mergedNotifyIds.includes(pairResult.chatId)) {
|
|
706
|
-
mergedNotifyIds.push(pairResult.chatId);
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
gatewayConfig = {
|
|
711
|
-
...gatewayConfig,
|
|
712
|
-
_version: 1, // Config schema version — for future migration support
|
|
713
|
-
agent: { ...gatewayConfig.agent, ...agent.configDefaults, type: agent.type, cwd: opts.cwd },
|
|
714
|
-
chat: {
|
|
715
|
-
...gatewayConfig.chat,
|
|
716
|
-
botUsername: botInfo.username,
|
|
717
|
-
allowedUsers: mergedUsers,
|
|
718
|
-
allowedUserIds: mergedUserIds,
|
|
719
|
-
notifyChatIds: mergedNotifyIds,
|
|
720
|
-
adapters: gatewayConfig.chat?.adapters ?? { telegram: { mode: "polling" } },
|
|
721
|
-
},
|
|
722
|
-
...(opts.voice === false ? { voice: { stt: { enabled: false } } } : {}),
|
|
723
|
-
};
|
|
724
|
-
|
|
725
|
-
await atomicWriteJson(CONFIG_PATH, gatewayConfig);
|
|
726
|
-
ok(`~/.roundhouse/gateway.config.json`);
|
|
727
|
-
|
|
728
|
-
// ── Env file ──
|
|
729
|
-
// With psst: only non-secret config
|
|
730
|
-
// Without psst: include secrets
|
|
731
|
-
const envLines: string[] = [];
|
|
732
|
-
|
|
733
|
-
if (!opts.psst) {
|
|
734
|
-
envLines.push(`TELEGRAM_BOT_TOKEN=${envQuote(opts.botToken)}`);
|
|
735
|
-
envLines.push(`BOT_USERNAME=${envQuote(botInfo.username)}`);
|
|
736
|
-
envLines.push(`ALLOWED_USERS=${envQuote(opts.users.join(","))}`);
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
// If psst uses a generated password (headless), include it in env for systemd.
|
|
740
|
-
// Threat model tradeoff: the vault key is plaintext in a 0600 file, but this is
|
|
741
|
-
// unavoidable on headless servers with no keychain. The benefit is that individual
|
|
742
|
-
// secrets are still managed centrally via psst and injected at runtime.
|
|
743
|
-
if (opts.psst) {
|
|
744
|
-
const pwFile = resolve(ROUNDHOUSE_DIR, ".psst-password");
|
|
745
|
-
if (await fileExists(pwFile)) {
|
|
746
|
-
const pw = (await readFile(pwFile, "utf8")).trim();
|
|
747
|
-
envLines.push(`PSST_PASSWORD=${envQuote(pw)}`);
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
if (opts.provider === "amazon-bedrock") {
|
|
752
|
-
// Preserve existing AWS config
|
|
753
|
-
let existingEnv = new Map<string, string>();
|
|
754
|
-
try {
|
|
755
|
-
existingEnv = parseEnvFile(await readFile(ENV_PATH, "utf8"));
|
|
756
|
-
} catch {}
|
|
757
|
-
const getExisting = (key: string) => existingEnv.get(key);
|
|
758
|
-
|
|
759
|
-
if (!envLines.some((l) => l.startsWith("AWS_PROFILE="))) {
|
|
760
|
-
envLines.push(`AWS_PROFILE=${getExisting("AWS_PROFILE") ?? '"default"'}`);
|
|
761
|
-
}
|
|
762
|
-
if (!envLines.some((l) => l.startsWith("AWS_DEFAULT_REGION="))) {
|
|
763
|
-
envLines.push(`AWS_DEFAULT_REGION=${getExisting("AWS_DEFAULT_REGION") ?? '"us-east-1"'}`);
|
|
764
|
-
}
|
|
765
|
-
// Pi agent requires AWS_REGION (not just AWS_DEFAULT_REGION) to discover Bedrock models
|
|
766
|
-
if (!envLines.some((l) => l.startsWith("AWS_REGION="))) {
|
|
767
|
-
envLines.push(`AWS_REGION=${getExisting("AWS_REGION") ?? getExisting("AWS_DEFAULT_REGION") ?? '"us-east-1"'}`);
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
await atomicWriteText(ENV_PATH, envLines.join("\n") + "\n");
|
|
772
|
-
ok(`~/.roundhouse/.env${opts.psst ? " (non-secret config only)" : ""}`);
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
async function stepPair(opts: SetupOptions, botInfo: BotInfo): Promise<PairResult | null> {
|
|
776
|
-
step("⑦", "Pairing with Telegram...");
|
|
777
|
-
|
|
778
|
-
// Skip if chat IDs already known
|
|
779
|
-
if (opts.notifyChatIds.length > 0) {
|
|
780
|
-
ok(`Using provided notify chat IDs: ${opts.notifyChatIds.join(", ")}`);
|
|
781
|
-
|
|
782
|
-
// Send test message
|
|
783
|
-
for (const chatId of opts.notifyChatIds) {
|
|
784
|
-
try {
|
|
785
|
-
await sendMessage(opts.botToken, chatId, "✅ Roundhouse setup complete! Gateway is starting.");
|
|
786
|
-
ok(`Sent test message to chat ${chatId}`);
|
|
787
|
-
} catch {
|
|
788
|
-
warn(`Could not send message to chat ${chatId}`);
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
return null;
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
// Skip if existing config already has notifyChatIds
|
|
795
|
-
if (!opts.force) {
|
|
796
|
-
try {
|
|
797
|
-
const existing = JSON.parse(await readFile(CONFIG_PATH, "utf8"));
|
|
798
|
-
const existingIds = existing.chat?.notifyChatIds ?? [];
|
|
799
|
-
if (existingIds.length > 0) {
|
|
800
|
-
ok(`Already paired (chat IDs: ${existingIds.join(", ")})`);
|
|
801
|
-
return null;
|
|
802
|
-
}
|
|
803
|
-
} catch {}
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
// Skip if non-interactive
|
|
807
|
-
if (opts.nonInteractive) {
|
|
808
|
-
warn("Skipping pairing (--non-interactive)");
|
|
809
|
-
warn("Startup notifications won't work until paired.");
|
|
810
|
-
warn("Run 'roundhouse pair' later to pair.");
|
|
811
|
-
return null;
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
const result = await pairTelegram(opts.botToken, botInfo.username, opts.users, 300_000, log);
|
|
815
|
-
|
|
816
|
-
if (result) {
|
|
817
|
-
ok(`Paired with @${result.username} (user id: ${result.userId}, chat: ${result.chatId})`);
|
|
818
|
-
// Add paired username to allowedUsers if not already present
|
|
819
|
-
const lcUsername = result.username.toLowerCase();
|
|
820
|
-
if (!opts.users.some((u) => u.toLowerCase() === lcUsername)) {
|
|
821
|
-
opts.users.push(result.username);
|
|
822
|
-
}
|
|
823
|
-
return result;
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
warn("Pairing timed out.");
|
|
827
|
-
warn("Run 'roundhouse pair' later to pair.");
|
|
828
|
-
return null;
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
async function stepRegisterCommands(opts: SetupOptions): Promise<void> {
|
|
832
|
-
step("⑩", "Registering bot commands...");
|
|
833
|
-
await registerBotCommands(opts.botToken);
|
|
834
|
-
ok(`${BOT_COMMANDS.length} commands registered with Telegram`);
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
async function stepInstallSystemd(opts: SetupOptions): Promise<void> {
|
|
838
|
-
step("⑩b", "Installing systemd service...");
|
|
839
|
-
|
|
840
|
-
if (!opts.systemd) {
|
|
841
|
-
ok("Skipped (--no-systemd)");
|
|
842
|
-
log(" Run manually: roundhouse start");
|
|
843
|
-
return;
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
if (platform() !== "linux") {
|
|
847
|
-
warn(`Systemd not available (${platform()})`);
|
|
848
|
-
log(" Run manually: roundhouse start");
|
|
849
|
-
return;
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
// Check sudo
|
|
853
|
-
if (!hasSudoAccess()) {
|
|
854
|
-
warn("No passwordless sudo — cannot install systemd service");
|
|
855
|
-
log(" Run manually: roundhouse start");
|
|
856
|
-
log(" Or install with: roundhouse setup --telegram");
|
|
857
|
-
return;
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
const user = process.env.USER || process.env.LOGNAME;
|
|
861
|
-
if (!user) {
|
|
862
|
-
warn("Cannot determine current user ($USER not set). Skipping systemd.");
|
|
863
|
-
log(" Run manually: roundhouse start");
|
|
864
|
-
return;
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
const psstBin = opts.psst ? whichSync("psst") : null;
|
|
868
|
-
const { execStart, nodeBinDir } = resolveExecStart({ psstBin });
|
|
869
|
-
const unit = generateUnit({ execStart, nodeBinDir, user });
|
|
870
|
-
|
|
871
|
-
try {
|
|
872
|
-
await writeServiceUnit(unit);
|
|
873
|
-
systemctl("enable");
|
|
874
|
-
systemctl("start");
|
|
875
|
-
ok("roundhouse.service enabled and started");
|
|
876
|
-
} catch (err: any) {
|
|
877
|
-
warn(`Systemd install failed: ${err.message}`);
|
|
878
|
-
log(" Run manually: roundhouse start");
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
async function stepPostflight(): Promise<void> {
|
|
883
|
-
step("⑪", "Postflight checks...");
|
|
884
|
-
|
|
885
|
-
if (platform() === "linux") {
|
|
886
|
-
if (isServiceActive()) {
|
|
887
|
-
const pid = systemctlShow("MainPID");
|
|
888
|
-
ok(`Service active (PID ${pid})`);
|
|
889
|
-
} else {
|
|
890
|
-
warn("Service not active — check: roundhouse logs");
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
if (await fileExists(CONFIG_PATH)) {
|
|
895
|
-
ok("Config readable");
|
|
896
|
-
} else {
|
|
897
|
-
warn(`Config missing: ${CONFIG_PATH}`);
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
// Optional checks
|
|
901
|
-
if (!whichSync("ffmpeg")) {
|
|
902
|
-
warn("ffmpeg not found (install for voice support)");
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
// Whisper STT check (only if voice is enabled)
|
|
906
|
-
if (platform() === "linux" || process.env.ROUNDHOUSE_VOICE === "1") {
|
|
907
|
-
if (!whichSync("whisper")) {
|
|
908
|
-
warn("whisper not found — STT will auto-install on first voice message");
|
|
909
|
-
log(" Pre-install: pip3 install openai-whisper");
|
|
910
|
-
} else {
|
|
911
|
-
ok("whisper available");
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
if (!process.env.TAVILY_API_KEY) {
|
|
916
|
-
warn("TAVILY_API_KEY not set — web search extension won't work");
|
|
917
|
-
log(" Get a free key at https://tavily.com and add to ~/.roundhouse/.env");
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
// ── BotFather Guide ──────────────────────────────────
|
|
922
|
-
|
|
923
|
-
function printBotFatherGuide(): void {
|
|
924
|
-
log("");
|
|
925
|
-
log(" 🤖 Create a Telegram Bot");
|
|
926
|
-
log(" ────────────────────────");
|
|
927
|
-
log(" 1. Open https://t.me/BotFather");
|
|
928
|
-
log(" 2. Send /newbot");
|
|
929
|
-
log(" 3. Choose a display name (e.g. 'My Roundhouse')");
|
|
930
|
-
log(" 4. Choose a username ending in 'bot' (e.g. 'my_roundhouse_bot')");
|
|
931
|
-
log(" 5. Copy the token BotFather returns");
|
|
932
|
-
log("");
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
// ── Interactive Telegram Setup ───────────────────────
|
|
936
|
-
|
|
937
|
-
async function runInteractiveTelegramSetup(opts: SetupOptions): Promise<void> {
|
|
938
|
-
const agent = resolveAgentForSetup(opts);
|
|
939
|
-
log("\n🔧 Roundhouse Telegram Setup");
|
|
940
|
-
log("━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
941
|
-
|
|
942
|
-
try {
|
|
943
|
-
// Step 1: Preflight
|
|
944
|
-
await stepPreflight(opts, agent);
|
|
945
|
-
|
|
946
|
-
// Detect existing agent installations
|
|
947
|
-
const env = detectEnvironment();
|
|
948
|
-
if (env.agents.length > 0) {
|
|
949
|
-
log("");
|
|
950
|
-
log(" 🔍 Agent detection:");
|
|
951
|
-
for (const line of formatDetectionResults(env)) {
|
|
952
|
-
ok(line);
|
|
953
|
-
}
|
|
954
|
-
// If the selected agent is already configured, skip package install
|
|
955
|
-
if (!opts.force) {
|
|
956
|
-
const selected = env.agents.find(a => a.type === opts.agent);
|
|
957
|
-
if (selected?.configured) {
|
|
958
|
-
opts._skipAgentInstall = true;
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
// Step 2: Get bot token (prompt if not provided)
|
|
964
|
-
if (!opts.botToken) {
|
|
965
|
-
log("");
|
|
966
|
-
printBotFatherGuide();
|
|
967
|
-
opts.botToken = await promptMasked(" Paste your bot token");
|
|
968
|
-
if (!opts.botToken) {
|
|
969
|
-
fail("No token provided");
|
|
970
|
-
process.exit(2);
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
const botInfo = await stepValidateToken(opts);
|
|
974
|
-
|
|
975
|
-
// Step 3: Get username (prompt if not provided)
|
|
976
|
-
if (opts.users.length === 0) {
|
|
977
|
-
step("③", "Telegram username...");
|
|
978
|
-
const username = await promptText(" Your Telegram username (without @)");
|
|
979
|
-
if (!username) {
|
|
980
|
-
fail("Username required");
|
|
981
|
-
process.exit(2);
|
|
982
|
-
}
|
|
983
|
-
opts.users.push(username.replace(/^@/, ""));
|
|
984
|
-
ok(`Allowed: ${opts.users.map(u => `@${u}`).join(", ")}`);
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
// Step 4: Stop existing gateway
|
|
988
|
-
await stepStopGateway();
|
|
989
|
-
|
|
990
|
-
// Step 5: Install packages
|
|
991
|
-
await stepInstallPackages(opts, agent);
|
|
992
|
-
|
|
993
|
-
// Step 6: Install bundle (skills + CLI tools)
|
|
994
|
-
await stepInstallBundle(opts);
|
|
995
|
-
|
|
996
|
-
// Step 7: Pair via Telegram
|
|
997
|
-
step("⑦", "Pairing with Telegram...");
|
|
998
|
-
const nonce = createPairingNonce();
|
|
999
|
-
const pairingLink = createPairingLink(botInfo.username, nonce);
|
|
1000
|
-
log(`\n Open this link to pair:\n`);
|
|
1001
|
-
log(` 🔗 ${pairingLink}\n`);
|
|
1002
|
-
printQr(pairingLink, opts.qr);
|
|
1003
|
-
log(` Or send /start ${nonce} to @${botInfo.username}`);
|
|
1004
|
-
log("");
|
|
1005
|
-
|
|
1006
|
-
// Auto-open the pairing link on macOS
|
|
1007
|
-
if (process.platform === "darwin") {
|
|
1008
|
-
try {
|
|
1009
|
-
execFileSync("open", [pairingLink], { stdio: "ignore" });
|
|
1010
|
-
log(" (Opened in Telegram — switch to the app to complete pairing)");
|
|
1011
|
-
} catch { /* ignore if open fails */ }
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
log(" Waiting for you to tap the link in Telegram...");
|
|
1015
|
-
|
|
1016
|
-
const pairResult = await pairTelegram(
|
|
1017
|
-
opts.botToken, botInfo.username, opts.users,
|
|
1018
|
-
300_000, log, { nonce, showLink: false },
|
|
1019
|
-
);
|
|
1020
|
-
if (!pairResult) {
|
|
1021
|
-
warn("Pairing timed out. Run 'roundhouse pair' later.");
|
|
1022
|
-
} else {
|
|
1023
|
-
ok(`Paired with @${pairResult.username} (chat: ${pairResult.chatId})`);
|
|
1024
|
-
if (!opts.notifyChatIds.includes(pairResult.chatId)) {
|
|
1025
|
-
opts.notifyChatIds.push(pairResult.chatId);
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
// Step 8: Store secrets
|
|
1030
|
-
await stepStoreSecrets(opts, botInfo);
|
|
1031
|
-
|
|
1032
|
-
// Step 9: Write config
|
|
1033
|
-
await stepConfigure(opts, botInfo, pairResult, agent);
|
|
1034
|
-
|
|
1035
|
-
// Step 10: Register commands + install service
|
|
1036
|
-
await stepRegisterCommands(opts);
|
|
1037
|
-
await stepInstallSystemd(opts);
|
|
1038
|
-
|
|
1039
|
-
// Step 11: Verify
|
|
1040
|
-
await stepPostflight();
|
|
1041
|
-
|
|
1042
|
-
// Done!
|
|
1043
|
-
log("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
1044
|
-
log("✅ Roundhouse is ready!");
|
|
1045
|
-
log(` Bot: @${botInfo.username}`);
|
|
1046
|
-
log(` Send /status to @${botInfo.username} on Telegram.\n`);
|
|
1047
|
-
} catch (err: any) {
|
|
1048
|
-
log("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
1049
|
-
log(`❌ Setup failed: ${err.message}`);
|
|
1050
|
-
log(" Re-run: roundhouse setup --telegram\n");
|
|
1051
|
-
process.exit(1);
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
// ── Headless Telegram Setup ─────────────────────────
|
|
1056
|
-
|
|
1057
|
-
async function runHeadlessTelegramSetup(opts: SetupOptions): Promise<void> {
|
|
1058
|
-
const agent = resolveAgentForSetup(opts);
|
|
1059
|
-
const logger = createJsonLogger();
|
|
1060
|
-
|
|
1061
|
-
// Override module-level log helpers to emit JSON instead of text
|
|
1062
|
-
const savedLog = log, savedStep = step, savedOk = ok, savedWarn = warn, savedFail = fail;
|
|
1063
|
-
log = (msg) => logger.info("log", msg);
|
|
1064
|
-
step = (_n, label) => logger.info("step", label);
|
|
1065
|
-
ok = (msg) => logger.ok(msg);
|
|
1066
|
-
warn = (msg) => logger.warn("warn", msg);
|
|
1067
|
-
fail = (msg) => logger.fail(msg);
|
|
1068
|
-
|
|
1069
|
-
try {
|
|
1070
|
-
// Validate required inputs
|
|
1071
|
-
if (!opts.botToken) {
|
|
1072
|
-
logger.error("validation.failed", "TELEGRAM_BOT_TOKEN env var required for --headless");
|
|
1073
|
-
process.exit(2);
|
|
1074
|
-
}
|
|
1075
|
-
if (opts.users.length === 0) {
|
|
1076
|
-
logger.error("validation.failed", "--user is required for --headless");
|
|
1077
|
-
process.exit(2);
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
// Step 1: Preflight
|
|
1081
|
-
logger.step(1, 9, "preflight.start", "Running preflight checks");
|
|
1082
|
-
await stepPreflight(opts, agent);
|
|
1083
|
-
logger.ok("Preflight passed");
|
|
1084
|
-
|
|
1085
|
-
// Step 2: Validate token
|
|
1086
|
-
logger.step(2, 9, "telegram.validate", "Validating Telegram bot token");
|
|
1087
|
-
const botInfo = await stepValidateToken(opts);
|
|
1088
|
-
logger.ok(`Bot: @${botInfo.username} (id: ${botInfo.id})`);
|
|
1089
|
-
|
|
1090
|
-
// Step 3: Stop existing gateway
|
|
1091
|
-
logger.step(3, 9, "gateway.stop", "Checking for running gateway");
|
|
1092
|
-
await stepStopGateway();
|
|
1093
|
-
|
|
1094
|
-
// Step 4: Install packages
|
|
1095
|
-
logger.step(4, 9, "packages.install", "Installing packages");
|
|
1096
|
-
await stepInstallPackages(opts, agent);
|
|
1097
|
-
logger.ok("Packages installed");
|
|
1098
|
-
|
|
1099
|
-
// Step 4b: Install bundle
|
|
1100
|
-
await stepInstallBundle(opts);
|
|
1101
|
-
|
|
1102
|
-
// Step 5: Create pending pairing
|
|
1103
|
-
logger.step(5, 9, "pairing.pending", "Creating pending pairing");
|
|
1104
|
-
let nonce: string;
|
|
1105
|
-
const existing = await readPendingPairing();
|
|
1106
|
-
if (existing?.status === "pending" && !opts.force) {
|
|
1107
|
-
nonce = existing.nonce;
|
|
1108
|
-
logger.info("pairing.reuse", `Reusing existing nonce: ${nonce}`);
|
|
1109
|
-
} else {
|
|
1110
|
-
nonce = createPairingNonce();
|
|
1111
|
-
}
|
|
1112
|
-
const pairingLink = createPairingLink(botInfo.username, nonce);
|
|
1113
|
-
const pendingPairing: PendingPairing = {
|
|
1114
|
-
version: 1,
|
|
1115
|
-
nonce,
|
|
1116
|
-
botUsername: botInfo.username,
|
|
1117
|
-
allowedUsers: opts.users,
|
|
1118
|
-
createdAt: new Date().toISOString(),
|
|
1119
|
-
status: "pending",
|
|
1120
|
-
};
|
|
1121
|
-
await writePendingPairing(pendingPairing);
|
|
1122
|
-
logger.info("pairing.link", `Pairing link: ${pairingLink}`, { pairingLink, nonce });
|
|
1123
|
-
|
|
1124
|
-
// Step 6: Store secrets
|
|
1125
|
-
logger.step(6, 9, "secrets.store", "Storing secrets");
|
|
1126
|
-
await stepStoreSecrets(opts, botInfo);
|
|
1127
|
-
|
|
1128
|
-
// Step 7: Write config (no pair result yet — gateway will complete pairing)
|
|
1129
|
-
logger.step(7, 9, "config.write", "Writing configuration");
|
|
1130
|
-
await stepConfigure(opts, botInfo, null, agent);
|
|
1131
|
-
logger.ok("Config written");
|
|
1132
|
-
|
|
1133
|
-
// Step 8: Register commands
|
|
1134
|
-
logger.step(8, 9, "commands.register", "Registering bot commands");
|
|
1135
|
-
await stepRegisterCommands(opts);
|
|
1136
|
-
logger.ok("Bot commands registered");
|
|
1137
|
-
|
|
1138
|
-
// Step 9: Install and start service
|
|
1139
|
-
logger.step(9, 9, "service.install", "Installing and starting service");
|
|
1140
|
-
if (!opts.systemd) {
|
|
1141
|
-
logger.warn("service.skip", "--no-systemd: service not installed. Start manually: roundhouse start");
|
|
1142
|
-
} else {
|
|
1143
|
-
await stepInstallSystemd(opts);
|
|
1144
|
-
logger.ok("Service installed and started");
|
|
1145
|
-
|
|
1146
|
-
// Verify service is active
|
|
1147
|
-
try {
|
|
1148
|
-
const { execFileSync } = await import("node:child_process");
|
|
1149
|
-
const state = execFileSync("systemctl", ["is-active", "roundhouse"], { encoding: "utf8" }).trim();
|
|
1150
|
-
if (state === "active") {
|
|
1151
|
-
logger.ok("Service is active");
|
|
1152
|
-
} else {
|
|
1153
|
-
logger.warn("service.state", `Service state: ${state}`);
|
|
1154
|
-
}
|
|
1155
|
-
} catch {
|
|
1156
|
-
logger.warn("service.state", "Could not verify service state");
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
// Success
|
|
1161
|
-
logger.info("setup.complete", "Headless setup complete", {
|
|
1162
|
-
botUsername: botInfo.username,
|
|
1163
|
-
pairingLink,
|
|
1164
|
-
pairingStatus: "pending",
|
|
1165
|
-
serviceInstalled: opts.systemd,
|
|
1166
|
-
});
|
|
1167
|
-
log("");
|
|
1168
|
-
log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
|
|
1169
|
-
log(`✅ Roundhouse installed and running!`);
|
|
1170
|
-
log(``);
|
|
1171
|
-
log(` Bot: @${botInfo.username}`);
|
|
1172
|
-
log(` Pairing: Open ${pairingLink} to complete setup`);
|
|
1173
|
-
log(` Gateway is running and will accept pairing automatically.`);
|
|
1174
|
-
log(``);
|
|
1175
|
-
} catch (err: any) {
|
|
1176
|
-
const diag: SetupDiagnostics = {
|
|
1177
|
-
node: process.version,
|
|
1178
|
-
platform: platform(),
|
|
1179
|
-
arch: process.arch,
|
|
1180
|
-
cwd: process.cwd(),
|
|
1181
|
-
roundhouseDir: ROUNDHOUSE_DIR,
|
|
1182
|
-
configExists: await fileExists(CONFIG_PATH).catch(() => false),
|
|
1183
|
-
envExists: await fileExists(ENV_PATH).catch(() => false),
|
|
1184
|
-
pairingStatus: (await readPendingPairing())?.status ?? "not found",
|
|
1185
|
-
serviceState: "unknown",
|
|
1186
|
-
error: { name: err.name, message: err.message, stack: err.stack },
|
|
1187
|
-
};
|
|
1188
|
-
try {
|
|
1189
|
-
const { execFileSync } = await import("node:child_process");
|
|
1190
|
-
diag.serviceState = execFileSync("systemctl", ["is-active", "roundhouse"], { encoding: "utf8" }).trim();
|
|
1191
|
-
} catch {}
|
|
1192
|
-
printDiagnosticError(diag, true);
|
|
1193
|
-
process.exit(1);
|
|
1194
|
-
} finally {
|
|
1195
|
-
// Restore module-level log helpers
|
|
1196
|
-
log = savedLog; step = savedStep; ok = savedOk; warn = savedWarn; fail = savedFail;
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
32
|
+
stepPreflight,
|
|
33
|
+
stepValidateToken,
|
|
34
|
+
stepStopGateway,
|
|
35
|
+
stepInstallPackages,
|
|
36
|
+
stepStoreSecrets,
|
|
37
|
+
stepInstallBundle,
|
|
38
|
+
stepConfigure,
|
|
39
|
+
stepPair,
|
|
40
|
+
stepRegisterCommands,
|
|
41
|
+
stepInstallSystemd,
|
|
42
|
+
stepPostflight,
|
|
43
|
+
} from "./setup/steps";
|
|
44
|
+
import { resolveAgentForSetup, textLog, textStepLog } from "./setup/runtime";
|
|
45
|
+
import { runInteractiveTelegramSetup, runHeadlessTelegramSetup } from "./setup/flows";
|
|
1199
46
|
|
|
1200
47
|
// ── Orchestrator ─────────────────────────────────────
|
|
1201
48
|
|
|
@@ -1225,58 +72,59 @@ export async function cmdSetup(argv: string[]): Promise<void> {
|
|
|
1225
72
|
}
|
|
1226
73
|
|
|
1227
74
|
// Legacy flow (no --telegram flag)
|
|
1228
|
-
const
|
|
1229
|
-
|
|
1230
|
-
|
|
75
|
+
const logger = textStepLog;
|
|
76
|
+
const agent = resolveAgentForSetup(opts, logger);
|
|
77
|
+
textLog("\n🔧 Roundhouse Setup");
|
|
78
|
+
textLog("━━━━━━━━━━━━━━━━━━━");
|
|
1231
79
|
|
|
1232
80
|
try {
|
|
1233
81
|
// Phase 1: Validate (no mutations)
|
|
1234
|
-
await stepPreflight(opts, agent);
|
|
1235
|
-
const botInfo = await stepValidateToken(opts);
|
|
1236
|
-
await stepStopGateway();
|
|
82
|
+
await stepPreflight(logger, opts, agent);
|
|
83
|
+
const botInfo = await stepValidateToken(logger, opts);
|
|
84
|
+
await stepStopGateway(logger);
|
|
1237
85
|
|
|
1238
86
|
// Phase 2: Install packages
|
|
1239
|
-
await stepInstallPackages(opts, agent);
|
|
87
|
+
await stepInstallPackages(logger, opts, agent);
|
|
1240
88
|
|
|
1241
89
|
// Phase 2b: Install bundle (skills + CLI tools)
|
|
1242
|
-
await stepInstallBundle(opts);
|
|
90
|
+
await stepInstallBundle(logger, opts);
|
|
1243
91
|
|
|
1244
92
|
// Phase 3: Pair (before secrets/config, so paired username is included)
|
|
1245
|
-
const pairResult = await stepPair(opts, botInfo);
|
|
93
|
+
const pairResult = await stepPair(logger, opts, botInfo);
|
|
1246
94
|
|
|
1247
95
|
// Phase 4: Store secrets (after pairing, so ALLOWED_USERS includes paired user)
|
|
1248
|
-
await stepStoreSecrets(opts, botInfo);
|
|
96
|
+
await stepStoreSecrets(logger, opts, botInfo);
|
|
1249
97
|
|
|
1250
98
|
// Phase 5: Write config (includes pair data)
|
|
1251
|
-
await stepConfigure(opts, botInfo, pairResult, agent);
|
|
99
|
+
await stepConfigure(logger, opts, botInfo, pairResult, agent);
|
|
1252
100
|
|
|
1253
101
|
// Phase 6: Remote setup
|
|
1254
|
-
await stepRegisterCommands(opts);
|
|
102
|
+
await stepRegisterCommands(logger, opts);
|
|
1255
103
|
|
|
1256
104
|
// Phase 7: Service
|
|
1257
|
-
await stepInstallSystemd(opts);
|
|
105
|
+
await stepInstallSystemd(logger, opts);
|
|
1258
106
|
|
|
1259
107
|
// Phase 8: Verify
|
|
1260
|
-
await stepPostflight();
|
|
108
|
+
await stepPostflight(logger);
|
|
1261
109
|
|
|
1262
110
|
// Final message
|
|
1263
111
|
const warnings = !opts.notifyChatIds.length && !pairResult;
|
|
1264
|
-
|
|
112
|
+
textLog("\n━━━━━━━━━━━━━━━━━━━");
|
|
1265
113
|
if (warnings) {
|
|
1266
|
-
|
|
1267
|
-
|
|
114
|
+
textLog("⚠️ Installed, action required:");
|
|
115
|
+
textLog(` • Not paired — run: roundhouse pair`);
|
|
1268
116
|
} else {
|
|
1269
|
-
|
|
117
|
+
textLog("✅ Roundhouse is running!");
|
|
1270
118
|
}
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
119
|
+
textLog(` Bot: @${botInfo.username}`);
|
|
120
|
+
textLog(` Memory: ${opts.extensions.some((e) => e.includes("pi-memory")) ? "agent-managed" : "roundhouse-managed"}`);
|
|
121
|
+
textLog(` Secrets: ${opts.psst ? "psst vault (encrypted)" : "~/.roundhouse/.env (plaintext)"}`);
|
|
122
|
+
textLog(` Send /status to @${botInfo.username} on Telegram.\n`);
|
|
1275
123
|
} catch (err: any) {
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
124
|
+
textLog("\n━━━━━━━━━━━━━━━━━━━");
|
|
125
|
+
textLog(`❌ Setup failed: ${err.message}`);
|
|
126
|
+
textLog(" Partial changes may have been applied.");
|
|
127
|
+
textLog(" Re-run setup to complete, or run: roundhouse doctor\n");
|
|
1280
128
|
process.exit(1);
|
|
1281
129
|
}
|
|
1282
130
|
}
|
|
@@ -1328,19 +176,19 @@ export async function cmdPair(argv: string[]): Promise<void> {
|
|
|
1328
176
|
process.exit(1);
|
|
1329
177
|
}
|
|
1330
178
|
|
|
1331
|
-
|
|
179
|
+
textLog("\n🔗 Roundhouse Pairing\n");
|
|
1332
180
|
|
|
1333
181
|
const botInfo = await validateBotToken(token);
|
|
1334
|
-
ok(`Bot: @${botInfo.username}`);
|
|
182
|
+
textStepLog.ok(`Bot: @${botInfo.username}`);
|
|
1335
183
|
|
|
1336
|
-
const result = await pairTelegram(token, botInfo.username, users, 300_000,
|
|
184
|
+
const result = await pairTelegram(token, botInfo.username, users, 300_000, textLog);
|
|
1337
185
|
|
|
1338
186
|
if (!result) {
|
|
1339
|
-
|
|
187
|
+
textLog("\n⚠ Pairing timed out. Try again: roundhouse pair\n");
|
|
1340
188
|
process.exit(1);
|
|
1341
189
|
}
|
|
1342
190
|
|
|
1343
|
-
ok(`Paired with @${result.username} (user id: ${result.userId}, chat: ${result.chatId})`);
|
|
191
|
+
textStepLog.ok(`Paired with @${result.username} (user id: ${result.userId}, chat: ${result.chatId})`);
|
|
1344
192
|
|
|
1345
193
|
// Update config
|
|
1346
194
|
try {
|
|
@@ -1356,52 +204,52 @@ export async function cmdPair(argv: string[]): Promise<void> {
|
|
|
1356
204
|
config.chat.notifyChatIds = existingNotifyIds;
|
|
1357
205
|
|
|
1358
206
|
await atomicWriteJson(CONFIG_PATH, config);
|
|
1359
|
-
ok("Config updated with chat ID");
|
|
207
|
+
textStepLog.ok("Config updated with chat ID");
|
|
1360
208
|
} catch {
|
|
1361
|
-
warn("Could not update config — add notifyChatIds manually");
|
|
209
|
+
textStepLog.warn("Could not update config — add notifyChatIds manually");
|
|
1362
210
|
}
|
|
1363
211
|
|
|
1364
|
-
|
|
212
|
+
textLog("\n✅ Paired! Restart gateway to apply: roundhouse restart\n");
|
|
1365
213
|
}
|
|
1366
214
|
|
|
1367
215
|
// ── Dry run ──────────────────────────────────────────
|
|
1368
216
|
|
|
1369
217
|
function printDryRun(opts: SetupOptions): void {
|
|
1370
218
|
const agent = getAgentDefinition(opts.agent);
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
219
|
+
textLog("\n🔧 Roundhouse Setup (DRY RUN)");
|
|
220
|
+
textLog("━━━━━━━━━━━━━━━━━━━\n");
|
|
221
|
+
textLog(`Agent: ${agent.name} (${agent.type})`);
|
|
222
|
+
textLog("Would validate Telegram token");
|
|
223
|
+
textLog("Would stop existing gateway (if running)");
|
|
224
|
+
textLog(`Would install: npm install -g @inceptionstack/roundhouse`);
|
|
1377
225
|
for (const pkg of agent.packages) {
|
|
1378
226
|
const scope = pkg.install === "global" ? "-g " : "";
|
|
1379
|
-
|
|
227
|
+
textLog(`Would install: npm install ${scope}${pkg.packageName}`);
|
|
1380
228
|
}
|
|
1381
229
|
if (opts.psst) {
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
230
|
+
textLog(`Would install: bun runtime (if not present)`);
|
|
231
|
+
textLog(`Would install: npm install -g psst-cli`);
|
|
232
|
+
textLog(`Would initialize psst vault`);
|
|
233
|
+
textLog(`Would install: pi-psst extension`);
|
|
1386
234
|
}
|
|
1387
|
-
for (const ext of opts.extensions)
|
|
235
|
+
for (const ext of opts.extensions) textLog(`Would install extension: ${ext}`);
|
|
1388
236
|
if (!opts.nonInteractive && opts.notifyChatIds.length === 0) {
|
|
1389
|
-
|
|
237
|
+
textLog(`Would pair via Telegram (interactive)`);
|
|
1390
238
|
}
|
|
1391
239
|
if (opts.psst) {
|
|
1392
|
-
|
|
240
|
+
textLog(`Would store TELEGRAM_BOT_TOKEN, BOT_USERNAME, ALLOWED_USERS in psst`);
|
|
1393
241
|
}
|
|
1394
242
|
if (agent.configDirs?.length) {
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
if (opts.systemd)
|
|
1404
|
-
|
|
243
|
+
textLog(`Would configure: agent-specific settings`);
|
|
244
|
+
textLog(` Agent: ${agent.name}`);
|
|
245
|
+
}
|
|
246
|
+
textLog(` Set defaultProvider: ${opts.provider}`);
|
|
247
|
+
textLog(` Set defaultModel: ${opts.model}`);
|
|
248
|
+
textLog(`Would write: ~/.roundhouse/gateway.config.json`);
|
|
249
|
+
textLog(`Would write: ~/.roundhouse/.env${opts.psst ? " (non-secret config only)" : ""}`);
|
|
250
|
+
textLog(`Would register ${BOT_COMMANDS.length} bot commands`);
|
|
251
|
+
if (opts.systemd) textLog(`Would install systemd service`);
|
|
252
|
+
textLog("\nNo changes made.\n");
|
|
1405
253
|
}
|
|
1406
254
|
|
|
1407
255
|
// ── Help ─────────────────────────────────────────────
|