@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 +2 -0
- package/package.json +1 -1
- package/pi/extensions/web-search.ts +1 -1
- package/src/cli/cli.ts +29 -74
- package/src/cli/detect.ts +157 -0
- package/src/cli/doctor/checks/config.ts +1 -1
- package/src/cli/doctor/checks/systemd.ts +1 -1
- package/src/cli/setup.ts +48 -14
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
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
|
|
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,
|
|
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
|
|
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: {
|
|
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("
|
|
138
|
-
console.log("
|
|
139
|
-
console.log("
|
|
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("
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
222
|
-
console.log("
|
|
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("
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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:
|
|
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("");
|