@inceptionstack/roundhouse 0.4.5 → 0.5.1

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 CHANGED
@@ -9,6 +9,8 @@ Multiple chat inputs (Telegram, Slack, Discord via [Vercel Chat SDK](https://cha
9
9
 
10
10
  ```bash
11
11
  npm install -g @inceptionstack/roundhouse
12
+ roundhouse setup --telegram
13
+ roundhouse start # macOS (foreground) — Linux auto-starts via systemd
12
14
  ```
13
15
 
14
16
  ## Architecture
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inceptionstack/roundhouse",
3
- "version": "0.4.5",
3
+ "version": "0.5.1",
4
4
  "type": "module",
5
5
  "description": "Multi-platform chat gateway that routes messages through a configured AI agent",
6
6
  "license": "MIT",
@@ -1,5 +1,5 @@
1
1
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
- import { Type } from "@sinclair/typebox";
2
+ import { Type } from "typebox";
3
3
 
4
4
  export default function (pi: ExtensionAPI) {
5
5
  pi.registerTool({
package/src/cli/cli.ts CHANGED
@@ -5,17 +5,14 @@
5
5
  */
6
6
 
7
7
  import { resolve, dirname } from "node:path";
8
- import { readFile, writeFile, mkdir } from "node:fs/promises";
8
+ import { readFile } from "node:fs/promises";
9
9
  import { readdirSync, statSync } from "node:fs";
10
10
  import { execSync, execFileSync, spawn } from "node:child_process";
11
11
  import { fileURLToPath } from "node:url";
12
12
  import { performUpdate } from "../commands/update";
13
13
 
14
14
  import {
15
- CONFIG_DIR,
16
15
  CONFIG_PATH,
17
- ENV_FILE_PATH,
18
- DEFAULT_CONFIG,
19
16
  SESSIONS_DIR,
20
17
  SERVICE_NAME,
21
18
  fileExists,
@@ -24,7 +21,7 @@ import {
24
21
  } from "../config";
25
22
  import { getAgentSdkPackage } from "../agents/registry";
26
23
  import { threadIdToDir } from "../util";
27
- import { parseEnvFile, serializeEnvFile, envQuote, unquoteEnvValue } from "./env-file";
24
+ import { parseEnvFile, unquoteEnvValue } from "./env-file";
28
25
  import {
29
26
  SERVICE_PATH,
30
27
  systemctl,
@@ -32,9 +29,6 @@ import {
32
29
  isServiceInstalled,
33
30
  isServiceActive,
34
31
  systemctlShow,
35
- resolveExecStart,
36
- generateUnit,
37
- writeServiceUnit,
38
32
  } from "./systemd";
39
33
 
40
34
  const __filename = fileURLToPath(import.meta.url);
@@ -80,7 +74,7 @@ async function cmdStart() {
80
74
 
81
75
  console.log("No systemd service found. Running in foreground (use Ctrl+C to stop)...");
82
76
  if (process.platform !== "darwin") {
83
- console.log(" Tip: run 'roundhouse install' to set up the systemd daemon.\n");
77
+ console.log(" Tip: run 'roundhouse setup --telegram' to install as systemd daemon.\n");
84
78
  } else {
85
79
  console.log("");
86
80
  }
@@ -108,7 +102,11 @@ async function cmdRun() {
108
102
  const tsxPath = resolve(__dirname, "..", "..", "node_modules", "tsx", "dist", "cli.mjs");
109
103
  execFileSync(process.execPath, [tsxPath, indexPath], {
110
104
  stdio: "inherit",
111
- env: { ...process.env, ROUNDHOUSE_CONFIG: CONFIG_PATH },
105
+ env: {
106
+ ...process.env,
107
+ ROUNDHOUSE_CONFIG: CONFIG_PATH,
108
+ NODE_NO_WARNINGS: "1", // Suppress npm deprecation spam
109
+ },
112
110
  });
113
111
  }
114
112
  }
@@ -133,70 +131,23 @@ async function loadEnvFile(): Promise<void> {
133
131
  }
134
132
 
135
133
  async function cmdInstall() {
134
+ console.log("[roundhouse] 'install' is deprecated — use 'roundhouse setup --telegram' instead.\n");
135
+
136
136
  if (process.platform === "darwin") {
137
- console.log("[roundhouse] macOS detected — systemd is not available.\n");
138
- console.log(" On macOS, use 'roundhouse start' to run in foreground,");
139
- console.log(" or set up a launchd plist manually.\n");
140
- console.log(" Tip: run 'roundhouse setup --telegram' to configure first.");
137
+ console.log(" On macOS:");
138
+ console.log(" 1. roundhouse setup --telegram");
139
+ console.log(" 2. roundhouse start\n");
141
140
  process.exitCode = 1;
142
141
  return;
143
142
  }
144
143
 
145
- console.log("[roundhouse] Installing as systemd daemon...\n");
146
-
147
- await mkdir(CONFIG_DIR, { recursive: true });
148
- if (await fileExists(CONFIG_PATH)) {
149
- console.log(` Config exists: ${CONFIG_PATH}`);
150
- } else {
151
- await writeFile(CONFIG_PATH, JSON.stringify(DEFAULT_CONFIG, null, 2) + "\n");
152
- console.log(` Created config: ${CONFIG_PATH}`);
153
- console.log(` ⚠️ Edit this file to set allowedUsers and other settings.`);
154
- }
155
-
156
- // Write environment file for secrets — merge with existing to preserve manually-added keys
157
- const ENV_KEYS = ["TELEGRAM_BOT_TOKEN", "ANTHROPIC_API_KEY", "OPENAI_API_KEY", "BOT_USERNAME", "ALLOWED_USERS", "NOTIFY_CHAT_IDS", "AWS_PROFILE", "AWS_DEFAULT_REGION", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN"];
158
- const resolvedEnvPath = await resolveEnvFilePath();
159
- const existing = await fileExists(resolvedEnvPath)
160
- ? parseEnvFile(await readFile(resolvedEnvPath, "utf8"))
161
- : new Map<string, string>();
162
-
163
- // Override with current env vars for known keys
164
- let envChanged = false;
165
- for (const key of ENV_KEYS) {
166
- if (process.env[key]) {
167
- existing.set(key, envQuote(process.env[key]));
168
- envChanged = true;
169
- }
170
- }
171
- if (envChanged || !(await fileExists(ENV_FILE_PATH))) {
172
- if (resolvedEnvPath !== ENV_FILE_PATH && await fileExists(resolvedEnvPath)) {
173
- console.log(` Copying env file from ${resolvedEnvPath} to ${ENV_FILE_PATH}`);
174
- }
175
- await writeFile(ENV_FILE_PATH, serializeEnvFile(existing), { mode: 0o600 });
176
- console.log(` Environment file: ${ENV_FILE_PATH}`);
177
- }
178
-
179
- // Generate and install systemd unit
180
- const { execStart, nodeBinDir } = resolveExecStart();
181
- const unit = generateUnit({ execStart, nodeBinDir });
182
- await writeServiceUnit(unit);
183
- systemctl("enable");
184
- systemctl("start", "Daemon installed and started.");
185
-
186
- console.log(`\n Config: ${CONFIG_PATH}`);
187
- console.log(` Env file: ${ENV_FILE_PATH}`);
188
- console.log(` Service: ${SERVICE_PATH}`);
189
- console.log(` Logs: roundhouse logs`);
190
- console.log(` Status: roundhouse status`);
191
-
192
- if (!envChanged) {
193
- console.log(`\n ⚠️ No env vars detected. Edit ${ENV_FILE_PATH} with your secrets:`);
194
- console.log(` TELEGRAM_BOT_TOKEN=...`);
195
- console.log(` Then add your API keys and run: roundhouse restart`);
196
- }
144
+ console.log(" Recommended:");
145
+ console.log(" roundhouse setup --telegram\n");
146
+ console.log(" This sets up config, installs packages, pairs Telegram,");
147
+ console.log(" and installs the systemd service — all in one command.\n");
197
148
  }
198
-
199
149
  async function cmdUninstall() {
150
+
200
151
  console.log("[roundhouse] Removing systemd daemon...");
201
152
  try { systemctl("stop"); } catch {}
202
153
  try { systemctl("disable"); } catch {}
@@ -215,19 +166,23 @@ async function cmdUpdate() {
215
166
  }
216
167
 
217
168
  console.log(`[roundhouse] Updated to v${result.latestVersion}`);
218
- console.log("\n[roundhouse] Restarting daemon...");
219
- try {
220
- systemctl("restart", "Updated and restarted.");
221
- } catch {
222
- console.log(" ⚠️ Daemon not running. Start with: roundhouse install");
169
+
170
+ if (process.platform === "darwin" || !isServiceInstalled()) {
171
+ console.log("\n ✅ Update complete. Restart with: roundhouse start");
172
+ } else {
173
+ console.log("\n[roundhouse] Restarting daemon...");
174
+ try {
175
+ systemctl("restart", "Updated and restarted.");
176
+ } catch {
177
+ console.log(" ⚠️ Could not restart. Run: roundhouse start");
178
+ }
223
179
  }
224
180
  }
225
181
 
226
182
  async function cmdStatus() {
227
183
  if (!isServiceActive()) {
228
184
  console.log("\n ❌ Roundhouse is not running.\n");
229
- console.log(" Install with: roundhouse install");
230
- console.log(" Or start foreground: roundhouse start\n");
185
+ console.log(" Start with: roundhouse start\n");
231
186
  return;
232
187
  }
233
188
 
@@ -0,0 +1,157 @@
1
+ /**
2
+ * detect.ts — Agent environment detection for setup wizard
3
+ *
4
+ * Detects which agent backends are available on the system
5
+ * so setup can skip unnecessary installs and offer smart defaults.
6
+ */
7
+
8
+ import { existsSync, readFileSync } from "node:fs";
9
+ import { execFileSync } from "node:child_process";
10
+ import { resolve } from "node:path";
11
+ import { homedir } from "node:os";
12
+ import { whichSync } from "./systemd";
13
+
14
+ // ── Types ────────────────────────────────────────────
15
+
16
+ export interface DetectedAgent {
17
+ type: "pi" | "kiro" | "openclaw";
18
+ binary: string | null; // Path to binary (null if not found)
19
+ version: string | null; // Version string (null if couldn't determine)
20
+ configured: boolean; // Has config/settings present
21
+ details: Record<string, string>; // Extra info (provider, model, etc.)
22
+ }
23
+
24
+ export interface DetectedEnvironment {
25
+ agents: DetectedAgent[];
26
+ recommended: "pi" | "kiro" | "openclaw" | null;
27
+ }
28
+
29
+ // ── Detection ────────────────────────────────────────
30
+
31
+ function detectPi(): DetectedAgent | null {
32
+ const binary = whichSync("pi");
33
+ if (!binary) return null;
34
+
35
+ let version: string | null = null;
36
+ try {
37
+
38
+ version = execFileSync("pi", ["--version"], { encoding: "utf8", timeout: 5000 }).trim();
39
+ } catch {}
40
+
41
+ const settingsPath = resolve(homedir(), ".pi", "agent", "settings.json");
42
+ let configured = false;
43
+ const details: Record<string, string> = {};
44
+
45
+ if (existsSync(settingsPath)) {
46
+ configured = true;
47
+ try {
48
+ const settings = JSON.parse(readFileSync(settingsPath, "utf8"));
49
+ if (settings.defaultProvider) details.provider = settings.defaultProvider;
50
+ if (settings.defaultModel) details.model = settings.defaultModel;
51
+ } catch {}
52
+ }
53
+
54
+ return { type: "pi", binary, version, configured, details };
55
+ }
56
+
57
+ function detectKiro(): DetectedAgent | null {
58
+ const binary = whichSync("kiro-cli");
59
+ if (!binary) return null;
60
+
61
+ let version: string | null = null;
62
+ try {
63
+
64
+ version = execFileSync("kiro-cli", ["--version"], { encoding: "utf8", timeout: 5000 }).trim();
65
+ } catch {}
66
+
67
+ // Check for kiro config directory
68
+ const configDir = resolve(homedir(), ".kiro");
69
+ const configured = existsSync(configDir);
70
+ return { type: "kiro", binary, version, configured, details: {} };
71
+ }
72
+
73
+ function detectOpenClaw(): DetectedAgent | null {
74
+ const binary = whichSync("oc");
75
+ if (!binary) return null;
76
+
77
+ let version: string | null = null;
78
+ try {
79
+
80
+ version = execFileSync("oc", ["--version"], { encoding: "utf8", timeout: 5000 }).trim();
81
+ } catch {}
82
+
83
+ const configPath = resolve(homedir(), ".openclaw", "openclaw.json");
84
+ let configured = false;
85
+ const details: Record<string, string> = {};
86
+
87
+ if (existsSync(configPath)) {
88
+ configured = true;
89
+ // Check if gateway is configured
90
+ try {
91
+ const config = JSON.parse(readFileSync(configPath, "utf8"));
92
+ if (config.gateway?.port) details.port = String(config.gateway.port);
93
+ } catch {}
94
+ }
95
+
96
+ return { type: "openclaw", binary, version, configured, details };
97
+ }
98
+
99
+ // ── Public API ───────────────────────────────────────
100
+
101
+ /**
102
+ * Detect which agent backends are available on the system.
103
+ * Returns all detected agents and a recommended default.
104
+ */
105
+ export function detectEnvironment(): DetectedEnvironment {
106
+ const agents: DetectedAgent[] = [];
107
+
108
+ const pi = detectPi();
109
+ if (pi) agents.push(pi);
110
+
111
+ const kiro = detectKiro();
112
+ if (kiro) agents.push(kiro);
113
+
114
+ const oc = detectOpenClaw();
115
+ if (oc) agents.push(oc);
116
+
117
+ // Recommendation: prefer configured agent, then Pi as default
118
+ let recommended: DetectedEnvironment["recommended"] = null;
119
+ const configured = agents.filter(a => a.configured);
120
+ if (configured.length === 1) {
121
+ recommended = configured[0].type;
122
+ } else if (configured.length > 1) {
123
+ // Multiple configured — prefer Pi (most common for roundhouse)
124
+ recommended = configured.find(a => a.type === "pi")?.type ?? configured[0].type;
125
+ } else if (agents.length === 1) {
126
+ recommended = agents[0].type;
127
+ }
128
+
129
+ return { agents, recommended };
130
+ }
131
+
132
+ /**
133
+ * Format detection results for display in setup output.
134
+ */
135
+ export function formatDetectionResults(env: DetectedEnvironment): string[] {
136
+ const lines: string[] = [];
137
+
138
+ if (env.agents.length === 0) {
139
+ lines.push("No agent backends detected (will install Pi)");
140
+ return lines;
141
+ }
142
+
143
+ for (const agent of env.agents) {
144
+ const ver = agent.version ? ` (${agent.version})` : "";
145
+ const status = agent.configured ? "configured" : "found";
146
+ let line = `${agent.type}${ver} — ${status}`;
147
+ if (agent.details.provider) line += ` [${agent.details.provider}]`;
148
+ if (agent.details.model) line += ` [${agent.details.model}]`;
149
+ lines.push(line);
150
+ }
151
+
152
+ if (env.recommended) {
153
+ lines.push(`→ Using: ${env.recommended}`);
154
+ }
155
+
156
+ return lines;
157
+ }
@@ -47,7 +47,7 @@ export const configChecks: DoctorCheck[] = [
47
47
  details: [`${ctx.configPath} does not exist`],
48
48
  fix: {
49
49
  description: "Create default config",
50
- command: `roundhouse install`,
50
+ command: `roundhouse setup --telegram`,
51
51
  run: async () => {
52
52
  const configDir = dirname(ctx.configPath);
53
53
  await mkdir(configDir, { recursive: true });
@@ -23,7 +23,7 @@ export const systemdChecks: DoctorCheck[] = [
23
23
  id: "systemd-unit", category: "systemd", name: "Service unit",
24
24
  status: result ? "pass" : "warn",
25
25
  summary: result ? "installed" : "not installed",
26
- details: !result ? ["Run: roundhouse install"] : undefined,
26
+ details: !result ? ["Run: roundhouse setup --telegram"] : undefined,
27
27
  };
28
28
  },
29
29
  },
package/src/cli/setup.ts CHANGED
@@ -58,6 +58,7 @@ import {
58
58
  writePendingPairing,
59
59
  type PendingPairing,
60
60
  } from "../pairing";
61
+ import { detectEnvironment, formatDetectionResults } from "./detect";
61
62
 
62
63
  // ── Types ────────────────────────────────────────────
63
64
 
@@ -83,6 +84,8 @@ interface SetupOptions {
83
84
  qr: "auto" | "always" | "never";
84
85
  /** Agent type (default: pi) */
85
86
  agent: string;
87
+ /** Set by detection: skip agent package install if already configured */
88
+ _skipAgentInstall?: boolean;
86
89
  }
87
90
 
88
91
  type StepStatus = "ok" | "warn" | "skip" | "fail";
@@ -231,7 +234,7 @@ export function parseSetupArgs(argv: string[]): SetupOptions {
231
234
  cwd: homedir(),
232
235
  notifyChatIds: [],
233
236
  systemd: platform() === "linux",
234
- voice: true,
237
+ voice: platform() === "linux", // Default off on macOS (whisper install is heavy)
235
238
  psst: false,
236
239
  nonInteractive: false,
237
240
  force: false,
@@ -477,18 +480,22 @@ async function stepInstallPackages(opts: SetupOptions, agent: AgentDefinition):
477
480
  }
478
481
 
479
482
  // Agent packages (driven by agent definition)
480
- for (const pkg of agent.packages) {
481
- const label = pkg.name ?? pkg.packageName;
482
- const installed = pkg.binary ? whichSync(pkg.binary) : false;
483
- if (installed && !opts.force) {
484
- ok(`${label} (already installed)`);
485
- } else {
486
- log(` Installing ${label}...`);
487
- const args = pkg.install === "global"
488
- ? ["install", "-g", pkg.packageName]
489
- : ["install", pkg.packageName];
490
- execOrFail("npm", args, `${label} install`);
491
- ok(label);
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
+ }
492
499
  }
493
500
  }
494
501
 
@@ -846,7 +853,7 @@ async function stepInstallSystemd(opts: SetupOptions): Promise<void> {
846
853
  if (!hasSudoAccess()) {
847
854
  warn("No passwordless sudo — cannot install systemd service");
848
855
  log(" Run manually: roundhouse start");
849
- log(" Or install with: sudo roundhouse install");
856
+ log(" Or install with: roundhouse setup --telegram");
850
857
  return;
851
858
  }
852
859
 
@@ -895,6 +902,16 @@ async function stepPostflight(): Promise<void> {
895
902
  warn("ffmpeg not found (install for voice support)");
896
903
  }
897
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
+
898
915
  if (!process.env.TAVILY_API_KEY) {
899
916
  warn("TAVILY_API_KEY not set — web search extension won't work");
900
917
  log(" Get a free key at https://tavily.com and add to ~/.roundhouse/.env");
@@ -926,6 +943,23 @@ async function runInteractiveTelegramSetup(opts: SetupOptions): Promise<void> {
926
943
  // Step 1: Preflight
927
944
  await stepPreflight(opts, agent);
928
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
+
929
963
  // Step 2: Get bot token (prompt if not provided)
930
964
  if (!opts.botToken) {
931
965
  log("");