@inceptionstack/roundhouse 0.5.2 → 0.5.4

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/cli.ts CHANGED
@@ -5,7 +5,6 @@
5
5
  */
6
6
 
7
7
  import { resolve, dirname } from "node:path";
8
- import { homedir } from "node:os";
9
8
  import { readFile } from "node:fs/promises";
10
9
  import { readdirSync, statSync } from "node:fs";
11
10
  import { execSync, execFileSync, spawn } from "node:child_process";
@@ -15,7 +14,6 @@ import { performUpdate } from "../commands/update";
15
14
  import {
16
15
  CONFIG_PATH,
17
16
  SESSIONS_DIR,
18
- SERVICE_NAME,
19
17
  fileExists,
20
18
  loadConfig,
21
19
  resolveEnvFilePath,
@@ -23,14 +21,7 @@ import {
23
21
  import { getAgentSdkPackage } from "../agents/registry";
24
22
  import { threadIdToDir } from "../util";
25
23
  import { parseEnvFile, unquoteEnvValue } from "./env-file";
26
- import {
27
- SERVICE_PATH,
28
- systemctl,
29
- runSudo,
30
- isServiceInstalled,
31
- isServiceActive,
32
- systemctlShow,
33
- } from "./systemd";
24
+ import { getServiceManager } from "./service-manager";
34
25
 
35
26
  const __filename = fileURLToPath(import.meta.url);
36
27
  const __dirname = dirname(__filename);
@@ -42,7 +33,7 @@ const __dirname = dirname(__filename);
42
33
  * Only call with trusted/hardcoded strings. Any dynamic segments must be
43
34
  * validated (e.g. `/^\d+$/.test(pid)`) before interpolation.
44
35
  */
45
- function run(cmd: string, opts?: { silent?: boolean }): string {
36
+ function shellExec(cmd: string, opts?: { silent?: boolean }): string {
46
37
  try {
47
38
  const out = execSync(cmd, { encoding: "utf8", stdio: opts?.silent ? "pipe" : "inherit" });
48
39
  return (out ?? "").trim();
@@ -55,53 +46,32 @@ function run(cmd: string, opts?: { silent?: boolean }): string {
55
46
  // ── Commands ────────────────────────────────────────
56
47
 
57
48
  async function cmdStart() {
58
- // macOS: check launchd agent
59
- if (process.platform === "darwin") {
60
- const { isLaunchAgentInstalled, isLaunchAgentRunning, PLIST_PATH } = await import("./launchd.ts");
61
- if (isLaunchAgentInstalled()) {
62
- if (isLaunchAgentRunning()) {
63
- console.log("Roundhouse is already running (LaunchAgent).");
64
- console.log(" Logs: ~/.roundhouse/logs/roundhouse.log");
65
- console.log(" Stop: roundhouse stop");
66
- return;
67
- }
68
- // Load it
69
- try {
70
- execFileSync("launchctl", ["load", PLIST_PATH], { stdio: "pipe" });
71
- console.log("LaunchAgent started.");
72
- console.log(" Logs: ~/.roundhouse/logs/roundhouse.log");
73
- return;
74
- } catch {
75
- // Fall through to foreground
76
- }
77
- }
78
- }
49
+ const svc = getServiceManager();
50
+ const result = await svc.start();
79
51
 
80
- if (isServiceInstalled()) {
81
- if (isServiceActive()) {
82
- console.log("Roundhouse is already running.");
83
- console.log(" Use: roundhouse restart to restart");
84
- console.log(" roundhouse status to check status");
85
- console.log(" roundhouse logs to tail logs");
86
- return;
52
+ if (result.message === "no-service") {
53
+ // No service installed — fall back to foreground
54
+ if (!(await fileExists(CONFIG_PATH))) {
55
+ console.error("No config found. Run 'roundhouse setup --telegram' first.");
56
+ process.exit(1);
57
+ }
58
+ console.log("No service found. Running in foreground (use Ctrl+C to stop)...");
59
+ if (process.platform !== "darwin") {
60
+ console.log(" Tip: run 'roundhouse setup --telegram' to install as systemd daemon.\n");
61
+ } else {
62
+ console.log("");
87
63
  }
88
- systemctl("start", "Daemon started.");
64
+ await cmdRun();
89
65
  return;
90
66
  }
91
67
 
92
- // No systemd service — fall back to foreground. Check config before launching.
93
- if (!(await fileExists(CONFIG_PATH))) {
94
- console.error("No config found. Run 'roundhouse setup --telegram' first.");
95
- process.exit(1);
96
- }
97
-
98
- console.log("No systemd service found. Running in foreground (use Ctrl+C to stop)...");
99
- if (process.platform !== "darwin") {
100
- console.log(" Tip: run 'roundhouse setup --telegram' to install as systemd daemon.\n");
101
- } else {
102
- console.log("");
68
+ console.log(result.message);
69
+ if (result.started && process.platform === "darwin") {
70
+ console.log(" Logs: ~/.roundhouse/logs/roundhouse.log");
71
+ } else if (!result.started) {
72
+ console.log(" Logs: roundhouse logs");
73
+ console.log(" Stop: roundhouse stop");
103
74
  }
104
- await cmdRun();
105
75
  }
106
76
 
107
77
  async function cmdRun() {
@@ -128,7 +98,7 @@ async function cmdRun() {
128
98
  env: {
129
99
  ...process.env,
130
100
  ROUNDHOUSE_CONFIG: CONFIG_PATH,
131
- NODE_NO_WARNINGS: "1", // Suppress npm deprecation spam
101
+ NODE_NO_WARNINGS: "1",
132
102
  },
133
103
  });
134
104
  }
@@ -169,24 +139,11 @@ async function cmdInstall() {
169
139
  console.log(" This sets up config, installs packages, pairs Telegram,");
170
140
  console.log(" and installs the systemd service — all in one command.\n");
171
141
  }
172
- async function cmdUninstall() {
173
- if (process.platform === "darwin") {
174
- const { uninstallLaunchAgent, isLaunchAgentInstalled } = await import("./launchd.ts");
175
- if (isLaunchAgentInstalled()) {
176
- await uninstallLaunchAgent();
177
- console.log(" ✅ LaunchAgent removed. Config preserved at:", CONFIG_PATH);
178
- } else {
179
- console.log(" No LaunchAgent installed.");
180
- }
181
- return;
182
- }
183
142
 
184
- console.log("[roundhouse] Removing systemd daemon...");
185
- try { systemctl("stop"); } catch {}
186
- try { systemctl("disable"); } catch {}
187
- try { runSudo("rm", "-f", SERVICE_PATH); } catch {}
188
- runSudo("systemctl", "daemon-reload");
189
- console.log(" ✅ Daemon removed. Config preserved at:", CONFIG_PATH);
143
+ async function cmdUninstall() {
144
+ const svc = getServiceManager();
145
+ const result = await svc.uninstall();
146
+ console.log(` ✅ ${result.message} Config preserved at:`, CONFIG_PATH);
190
147
  }
191
148
 
192
149
  async function cmdUpdate() {
@@ -200,26 +157,16 @@ async function cmdUpdate() {
200
157
 
201
158
  console.log(`[roundhouse] Updated to v${result.latestVersion}`);
202
159
 
203
- if (process.platform === "darwin") {
204
- // Try to restart launchd agent
205
- const { isLaunchAgentInstalled, PLIST_PATH } = await import("./launchd.ts");
206
- if (isLaunchAgentInstalled()) {
207
- try {
208
- execFileSync("launchctl", ["unload", PLIST_PATH], { stdio: "pipe" });
209
- execFileSync("launchctl", ["load", PLIST_PATH], { stdio: "pipe" });
210
- console.log("\n ✅ Updated and restarted (LaunchAgent).");
211
- } catch {
212
- console.log("\n ✅ Update complete. Restart with: roundhouse start");
213
- }
214
- } else {
215
- console.log("\n ✅ Update complete. Restart with: roundhouse start");
216
- }
217
- } else if (!isServiceInstalled()) {
160
+ const svc = getServiceManager();
161
+ const status = await svc.status();
162
+
163
+ if (!status.installed) {
218
164
  console.log("\n ✅ Update complete. Restart with: roundhouse start");
219
165
  } else {
220
- console.log("\n[roundhouse] Restarting daemon...");
166
+ console.log("\n[roundhouse] Restarting service...");
221
167
  try {
222
- systemctl("restart", "Updated and restarted.");
168
+ const restartResult = await svc.restart();
169
+ console.log(` ✅ ${restartResult.message}`);
223
170
  } catch {
224
171
  console.log(" ⚠️ Could not restart. Run: roundhouse start");
225
172
  }
@@ -227,41 +174,34 @@ async function cmdUpdate() {
227
174
  }
228
175
 
229
176
  async function cmdStatus() {
230
- // macOS: check launchd
231
- if (process.platform === "darwin") {
232
- const { isLaunchAgentInstalled, isLaunchAgentRunning } = await import("./launchd.ts");
233
- if (isLaunchAgentRunning()) {
234
- console.log("\n ✅ Roundhouse is running (LaunchAgent).\n");
235
- console.log(" Logs: ~/.roundhouse/logs/roundhouse.log");
236
- console.log(" Stop: roundhouse stop\n");
237
- } else if (isLaunchAgentInstalled()) {
238
- console.log("\n ⚠️ LaunchAgent installed but not running.\n");
239
- console.log(" Start with: roundhouse start\n");
240
- } else {
241
- console.log("\n ❌ Roundhouse is not running.\n");
242
- console.log(" Start with: roundhouse start\n");
243
- }
177
+ const svc = getServiceManager();
178
+ const svcStatus = await svc.status();
179
+
180
+ if (!svcStatus.running) {
181
+ const icon = svcStatus.installed ? "⚠️" : "❌";
182
+ console.log(`\n ${icon} ${svcStatus.message}\n`);
183
+ console.log(" Start with: roundhouse start\n");
244
184
  return;
245
185
  }
246
186
 
247
- if (!isServiceActive()) {
248
- console.log("\n Roundhouse is not running.\n");
249
- console.log(" Start with: roundhouse start\n");
187
+ // macOS: simple status
188
+ if (process.platform === "darwin") {
189
+ console.log("\n Roundhouse is running (LaunchAgent).\n");
190
+ console.log(" Logs: ~/.roundhouse/logs/roundhouse.log");
191
+ console.log(" Stop: roundhouse stop\n");
250
192
  return;
251
193
  }
252
194
 
253
- // Load config for details
195
+ // Linux: detailed systemd status
196
+ const { systemctlShow } = await import("./systemd");
197
+
254
198
  let config: Awaited<ReturnType<typeof loadConfig>> | null = null;
255
- try {
256
- config = await loadConfig();
257
- } catch {}
199
+ try { config = await loadConfig(); } catch {}
258
200
 
259
- // Gather systemd info
260
201
  const pid = systemctlShow("MainPID");
261
202
  const activeState = systemctlShow("ActiveState");
262
203
  const startedAt = systemctlShow("ActiveEnterTimestamp");
263
204
 
264
- // Compute uptime
265
205
  let uptimeStr = "unknown";
266
206
  if (startedAt) {
267
207
  const startMs = new Date(startedAt).getTime();
@@ -272,17 +212,15 @@ async function cmdStatus() {
272
212
  }
273
213
  }
274
214
 
275
- // Memory from PID
276
215
  let memStr = "unknown";
277
216
  if (pid && pid !== "0" && /^\d+$/.test(pid)) {
278
- const rssKb = run(`ps -o rss= -p ${pid}`, { silent: true }).trim();
217
+ const rssKb = shellExec(`ps -o rss= -p ${pid}`, { silent: true }).trim();
279
218
  if (rssKb) {
280
219
  const parsed = parseInt(rssKb, 10);
281
220
  if (!isNaN(parsed)) memStr = `${(parsed / 1024).toFixed(1)} MB`;
282
221
  }
283
222
  }
284
223
 
285
- // Read env file for debug flags
286
224
  let debugStream = false;
287
225
  const statusEnvPath = await resolveEnvFilePath();
288
226
  try {
@@ -290,7 +228,6 @@ async function cmdStatus() {
290
228
  debugStream = envContent.includes("ROUNDHOUSE_DEBUG_STREAM=1") || envContent.includes('ROUNDHOUSE_DEBUG_STREAM="1"');
291
229
  } catch {}
292
230
 
293
- // Read versions
294
231
  let roundhouseVersion = "unknown";
295
232
  let agentVersion = "unknown";
296
233
  try {
@@ -299,7 +236,6 @@ async function cmdStatus() {
299
236
  roundhouseVersion = pkg.version;
300
237
  } catch {}
301
238
 
302
- // Resolve agent SDK version from registry
303
239
  const agentPkg = config ? getAgentSdkPackage(config.agent.type) : undefined;
304
240
  if (agentPkg) {
305
241
  try {
@@ -332,46 +268,21 @@ async function cmdStatus() {
332
268
  console.log();
333
269
  }
334
270
 
335
- async function cmdLogs() {
336
- if (process.platform === "darwin") {
337
- const logPath = resolve(homedir(), ".roundhouse", "logs", "roundhouse.log");
338
- const child = spawn("tail", ["-f", "-n", "100", logPath], { stdio: "inherit" });
339
- child.on("error", () => console.log("Could not read logs. Check ~/.roundhouse/logs/"));
340
- return;
341
- }
342
- const child = spawn("journalctl", ["-u", SERVICE_NAME, "-f", "--no-pager", "-n", "100"], {
343
- stdio: "inherit",
344
- });
345
- child.on("error", () => console.log("Could not read logs. Is the daemon installed?"));
346
- }
347
-
348
271
  async function cmdStop() {
349
- if (process.platform === "darwin") {
350
- const { isLaunchAgentInstalled, PLIST_PATH } = await import("./launchd.ts");
351
- if (isLaunchAgentInstalled()) {
352
- try { execFileSync("launchctl", ["unload", PLIST_PATH], { stdio: "pipe" }); } catch (e: any) { if (!e.message?.includes("Could not find")) console.warn(" (unload warning:", e.message?.split("\n")[0], ")"); }
353
- console.log("LaunchAgent stopped.");
354
- } else {
355
- console.log("No LaunchAgent installed. Nothing to stop.");
356
- }
357
- return;
358
- }
359
- systemctl("stop", "Daemon stopped.");
272
+ const svc = getServiceManager();
273
+ const result = await svc.stop();
274
+ console.log(result.message);
360
275
  }
361
276
 
362
277
  async function cmdRestart() {
363
- if (process.platform === "darwin") {
364
- const { isLaunchAgentInstalled, PLIST_PATH } = await import("./launchd.ts");
365
- if (isLaunchAgentInstalled()) {
366
- try { execFileSync("launchctl", ["unload", PLIST_PATH], { stdio: "pipe" }); } catch {}
367
- execFileSync("launchctl", ["load", PLIST_PATH], { stdio: "pipe" });
368
- console.log("LaunchAgent restarted.");
369
- } else {
370
- console.log("No LaunchAgent installed. Run: roundhouse setup --telegram");
371
- }
372
- return;
373
- }
374
- systemctl("restart", "Daemon restarted.");
278
+ const svc = getServiceManager();
279
+ const result = await svc.restart();
280
+ console.log(result.message);
281
+ }
282
+
283
+ async function cmdLogs() {
284
+ const svc = getServiceManager();
285
+ svc.logs();
375
286
  }
376
287
 
377
288
  async function cmdConfig() {
@@ -467,162 +378,9 @@ Environment:
467
378
 
468
379
  // ── Main ────────────────────────────────────────────
469
380
 
470
- async function cmdAgent() {
471
- // Usage: roundhouse agent <message>
472
- // roundhouse agent --thread <id> <message>
473
- // roundhouse agent --ephemeral <message>
474
- // echo "message" | roundhouse agent --stdin
475
- const args = process.argv.slice(3);
476
- let threadId = "";
477
- let messageText = "";
478
- let useStdin = false;
479
- let timeoutMs = 120_000;
480
- let verbose = false;
481
- let ephemeral = false;
482
-
483
- for (let i = 0; i < args.length; i++) {
484
- if (args[i] === "--thread" && args[i + 1]) {
485
- threadId = args[++i];
486
- } else if (args[i] === "--stdin") {
487
- useStdin = true;
488
- } else if (args[i] === "--timeout" && args[i + 1]) {
489
- const val = parseInt(args[++i], 10);
490
- if (isNaN(val) || val <= 0) { console.error("--timeout must be a positive number (seconds)"); process.exit(1); }
491
- timeoutMs = val * 1000;
492
- } else if (args[i] === "--no-timeout") {
493
- timeoutMs = 0;
494
- } else if (args[i] === "--verbose") {
495
- verbose = true;
496
- } else if (args[i] === "--ephemeral") {
497
- ephemeral = true;
498
- } else if (args[i].startsWith("-")) {
499
- console.error(`Unknown flag: ${args[i]}`);
500
- process.exit(1);
501
- } else {
502
- messageText = args.slice(i).join(" ");
503
- break;
504
- }
505
- }
506
-
507
- if (useStdin) {
508
- const chunks: Buffer[] = [];
509
- let totalBytes = 0;
510
- const MAX_INPUT = 1024 * 1024; // 1 MB
511
- for await (const chunk of process.stdin) {
512
- totalBytes += chunk.length;
513
- if (totalBytes > MAX_INPUT) {
514
- console.error(`Input exceeds ${MAX_INPUT / 1024}KB limit. Use a file instead.`);
515
- process.exit(1);
516
- }
517
- chunks.push(chunk);
518
- }
519
- // Strip single trailing newline (shell echo adds one)
520
- let raw = Buffer.concat(chunks).toString("utf8");
521
- if (raw.endsWith("\n")) raw = raw.slice(0, -1);
522
- messageText = raw;
523
- }
524
-
525
- if (!messageText) {
526
- console.error("Usage: roundhouse agent <message>");
527
- console.error(" roundhouse agent --thread <id> <message>");
528
- console.error(" echo \"message\" | roundhouse agent --stdin");
529
- console.error(" roundhouse agent --timeout 60 <message>");
530
- console.error(" roundhouse agent --verbose <message>");
531
- console.error(" roundhouse agent --ephemeral <message>");
532
- process.exit(1);
533
- }
534
-
535
- if (threadId && ephemeral) {
536
- console.error("--thread and --ephemeral cannot be used together");
537
- process.exit(1);
538
- }
539
-
540
- // Default: shared main session. --ephemeral restores one-off CLI behavior.
541
- if (!threadId) {
542
- threadId = ephemeral
543
- ? `cli-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
544
- : "main";
545
- }
546
-
547
- // Suppress debug/info logs unless --verbose
548
- const origLog = console.log;
549
- const origWarn = console.warn;
550
- const origError = console.error;
551
- if (!verbose) {
552
- console.log = () => {};
553
- console.warn = () => {};
554
- }
555
-
556
- let agent: import("../types").AgentAdapter | undefined;
557
- let aborted = false;
558
-
559
- // Clean abort on SIGINT/SIGTERM
560
- const handleSignal = async () => {
561
- if (aborted) return;
562
- aborted = true;
563
- console.log = origLog;
564
- console.warn = origWarn;
565
- console.error = origError;
566
- try { await agent?.abort?.(threadId); } catch {}
567
- try { await agent?.dispose(); } catch {}
568
- process.exit(130);
569
- };
570
- process.on("SIGINT", handleSignal);
571
- process.on("SIGTERM", handleSignal);
572
-
573
- // Timeout race
574
- let timer: ReturnType<typeof setTimeout> | undefined;
575
- const timeoutPromise = timeoutMs > 0
576
- ? new Promise<never>((_, reject) => {
577
- timer = setTimeout(async () => {
578
- aborted = true;
579
- try { await agent?.abort?.(threadId); } catch {}
580
- reject(new Error(`Timeout after ${timeoutMs / 1000}s`));
581
- }, timeoutMs);
582
- })
583
- : null;
584
-
585
- try {
586
- const config = await loadConfig();
587
- const { getAgentFactory } = await import("../agents/registry");
588
- const factory = getAgentFactory(config.agent.type);
589
- agent = factory(config.agent);
590
-
591
- const runAgent = async () => {
592
- if (agent!.promptStream) {
593
- for await (const event of agent!.promptStream(threadId, { text: messageText })) {
594
- if (event.type === "text_delta") {
595
- process.stdout.write(event.text);
596
- }
597
- }
598
- process.stdout.write("\n");
599
- } else {
600
- const response = await agent!.prompt(threadId, { text: messageText });
601
- origLog(response.text);
602
- }
603
- };
604
-
605
- if (timeoutPromise) {
606
- await Promise.race([runAgent(), timeoutPromise]);
607
- } else {
608
- await runAgent();
609
- }
610
- } catch (err: any) {
611
- console.error = origError;
612
- console.error(`Error: ${err.message}`);
613
- process.exit(aborted ? 124 : 1); // 124 = timeout (like coreutils)
614
- } finally {
615
- if (timer) clearTimeout(timer);
616
- process.off("SIGINT", handleSignal);
617
- process.off("SIGTERM", handleSignal);
618
- console.log = origLog;
619
- console.warn = origWarn;
620
- console.error = origError;
621
- if (!aborted) await agent?.dispose();
622
- }
623
- }
624
381
 
625
382
  import { cmdDoctor } from "./doctor";
383
+ import { cmdAgent } from "./agent-command";
626
384
  import { cmdCron } from "./cron";
627
385
  import { cmdSetup, cmdPair } from "./setup";
628
386