@inceptionstack/roundhouse 0.4.4 → 0.5.0
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 +4 -2
- package/package.json +1 -1
- package/pi/extensions/web-search.ts +7 -0
- package/src/agents/kiro/kiro-adapter.ts +2 -1
- package/src/cli/cli.ts +29 -67
- package/src/cli/detect.ts +157 -0
- package/src/cli/setup.ts +79 -30
- package/src/config.ts +13 -0
- package/src/gateway.ts +1 -5
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
|
|
@@ -178,7 +180,7 @@ Without a config file, defaults are used with env vars (`TELEGRAM_BOT_TOKEN`, `B
|
|
|
178
180
|
|
|
179
181
|
| Field | Description |
|
|
180
182
|
|-------|-------------|
|
|
181
|
-
| `agent.type` | Agent backend: `"pi"`
|
|
183
|
+
| `agent.type` | Agent backend: `"pi"`, `"kiro"` |
|
|
182
184
|
| `agent.cwd` | Working directory for the agent |
|
|
183
185
|
| `agent.sessionDir` | Override session storage path |
|
|
184
186
|
| `chat.botUsername` | Bot display name for Chat SDK |
|
|
@@ -492,7 +494,7 @@ No other changes needed — the gateway's unified handler covers all platforms.
|
|
|
492
494
|
| `src/agents/base-adapter.ts` | Abstract base class — adapter interface contract |
|
|
493
495
|
| `src/agents/registry.ts` | Agent type → factory registry |
|
|
494
496
|
| `src/config.ts` | Shared config loading, defaults, env overrides |
|
|
495
|
-
| `test/` | Unit tests (vitest,
|
|
497
|
+
| `test/` | Unit + integration tests (vitest, 311 passing) |
|
|
496
498
|
|
|
497
499
|
## CI/CD
|
|
498
500
|
|
package/package.json
CHANGED
|
@@ -46,6 +46,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
46
46
|
}),
|
|
47
47
|
signal: controller.signal,
|
|
48
48
|
});
|
|
49
|
+
} catch (err: any) {
|
|
50
|
+
clearTimeout(timeout);
|
|
51
|
+
const msg = err.name === "AbortError" ? "Request timed out (30s)" : `Network error: ${err.message}`;
|
|
52
|
+
return {
|
|
53
|
+
content: [{ type: "text", text: msg }],
|
|
54
|
+
details: { query: params.query, error: err.name },
|
|
55
|
+
};
|
|
49
56
|
} finally {
|
|
50
57
|
clearTimeout(timeout);
|
|
51
58
|
}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { homedir } from "node:os";
|
|
14
14
|
import { resolve } from "node:path";
|
|
15
15
|
import type { AgentAdapterFactory, AgentMessage, AgentResponse, AgentStreamEvent, AdapterInfo } from "../../types.js";
|
|
16
|
+
import { ROUNDHOUSE_VERSION } from "../../config.js";
|
|
16
17
|
import { BaseAdapter } from "../base-adapter.js";
|
|
17
18
|
import { spawnKiroCli, shutdownProcess, getKiroCliVersion, type AcpProcess, type InitializeResult, type SessionNewResult } from "./acp/index.js";
|
|
18
19
|
import { SessionStore, type SessionEntry } from "./session.js";
|
|
@@ -188,7 +189,7 @@ class KiroAdapter extends BaseAdapter {
|
|
|
188
189
|
|
|
189
190
|
await this.mainProcess.client.call<InitializeResult>("initialize", {
|
|
190
191
|
protocolVersion: "1.0",
|
|
191
|
-
clientInfo: { name: "roundhouse", version:
|
|
192
|
+
clientInfo: { name: "roundhouse", version: ROUNDHOUSE_VERSION },
|
|
192
193
|
});
|
|
193
194
|
|
|
194
195
|
if (!this.reaperInterval) {
|
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);
|
|
@@ -72,7 +66,12 @@ async function cmdStart() {
|
|
|
72
66
|
return;
|
|
73
67
|
}
|
|
74
68
|
|
|
75
|
-
// No systemd service — fall back to foreground
|
|
69
|
+
// No systemd service — fall back to foreground. Check config before launching.
|
|
70
|
+
if (!(await fileExists(CONFIG_PATH))) {
|
|
71
|
+
console.error("No config found. Run 'roundhouse setup --telegram' first.");
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
|
|
76
75
|
console.log("No systemd service found. Running in foreground (use Ctrl+C to stop)...");
|
|
77
76
|
if (process.platform !== "darwin") {
|
|
78
77
|
console.log(" Tip: run 'roundhouse install' to set up the systemd daemon.\n");
|
|
@@ -83,6 +82,12 @@ async function cmdStart() {
|
|
|
83
82
|
}
|
|
84
83
|
|
|
85
84
|
async function cmdRun() {
|
|
85
|
+
// Guard: check config exists before launching gateway
|
|
86
|
+
if (!(await fileExists(CONFIG_PATH))) {
|
|
87
|
+
console.error("No config found. Run 'roundhouse setup --telegram' first.");
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
86
91
|
process.env.ROUNDHOUSE_CONFIG = CONFIG_PATH;
|
|
87
92
|
|
|
88
93
|
// Load .env file so secrets (TELEGRAM_BOT_TOKEN, etc.) are available
|
|
@@ -97,7 +102,11 @@ async function cmdRun() {
|
|
|
97
102
|
const tsxPath = resolve(__dirname, "..", "..", "node_modules", "tsx", "dist", "cli.mjs");
|
|
98
103
|
execFileSync(process.execPath, [tsxPath, indexPath], {
|
|
99
104
|
stdio: "inherit",
|
|
100
|
-
env: {
|
|
105
|
+
env: {
|
|
106
|
+
...process.env,
|
|
107
|
+
ROUNDHOUSE_CONFIG: CONFIG_PATH,
|
|
108
|
+
NODE_NO_WARNINGS: "1", // Suppress npm deprecation spam
|
|
109
|
+
},
|
|
101
110
|
});
|
|
102
111
|
}
|
|
103
112
|
}
|
|
@@ -122,70 +131,23 @@ async function loadEnvFile(): Promise<void> {
|
|
|
122
131
|
}
|
|
123
132
|
|
|
124
133
|
async function cmdInstall() {
|
|
134
|
+
console.log("[roundhouse] 'install' is deprecated — use 'roundhouse setup --telegram' instead.\n");
|
|
135
|
+
|
|
125
136
|
if (process.platform === "darwin") {
|
|
126
|
-
console.log("
|
|
127
|
-
console.log("
|
|
128
|
-
console.log("
|
|
129
|
-
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");
|
|
130
140
|
process.exitCode = 1;
|
|
131
141
|
return;
|
|
132
142
|
}
|
|
133
143
|
|
|
134
|
-
console.log("
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
console.log(` Config exists: ${CONFIG_PATH}`);
|
|
139
|
-
} else {
|
|
140
|
-
await writeFile(CONFIG_PATH, JSON.stringify(DEFAULT_CONFIG, null, 2) + "\n");
|
|
141
|
-
console.log(` Created config: ${CONFIG_PATH}`);
|
|
142
|
-
console.log(` ⚠️ Edit this file to set allowedUsers and other settings.`);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Write environment file for secrets — merge with existing to preserve manually-added keys
|
|
146
|
-
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"];
|
|
147
|
-
const resolvedEnvPath = await resolveEnvFilePath();
|
|
148
|
-
const existing = await fileExists(resolvedEnvPath)
|
|
149
|
-
? parseEnvFile(await readFile(resolvedEnvPath, "utf8"))
|
|
150
|
-
: new Map<string, string>();
|
|
151
|
-
|
|
152
|
-
// Override with current env vars for known keys
|
|
153
|
-
let envChanged = false;
|
|
154
|
-
for (const key of ENV_KEYS) {
|
|
155
|
-
if (process.env[key]) {
|
|
156
|
-
existing.set(key, envQuote(process.env[key]));
|
|
157
|
-
envChanged = true;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
if (envChanged || !(await fileExists(ENV_FILE_PATH))) {
|
|
161
|
-
if (resolvedEnvPath !== ENV_FILE_PATH && await fileExists(resolvedEnvPath)) {
|
|
162
|
-
console.log(` Copying env file from ${resolvedEnvPath} to ${ENV_FILE_PATH}`);
|
|
163
|
-
}
|
|
164
|
-
await writeFile(ENV_FILE_PATH, serializeEnvFile(existing), { mode: 0o600 });
|
|
165
|
-
console.log(` Environment file: ${ENV_FILE_PATH}`);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Generate and install systemd unit
|
|
169
|
-
const { execStart, nodeBinDir } = resolveExecStart();
|
|
170
|
-
const unit = generateUnit({ execStart, nodeBinDir });
|
|
171
|
-
await writeServiceUnit(unit);
|
|
172
|
-
systemctl("enable");
|
|
173
|
-
systemctl("start", "Daemon installed and started.");
|
|
174
|
-
|
|
175
|
-
console.log(`\n Config: ${CONFIG_PATH}`);
|
|
176
|
-
console.log(` Env file: ${ENV_FILE_PATH}`);
|
|
177
|
-
console.log(` Service: ${SERVICE_PATH}`);
|
|
178
|
-
console.log(` Logs: roundhouse logs`);
|
|
179
|
-
console.log(` Status: roundhouse status`);
|
|
180
|
-
|
|
181
|
-
if (!envChanged) {
|
|
182
|
-
console.log(`\n ⚠️ No env vars detected. Edit ${ENV_FILE_PATH} with your secrets:`);
|
|
183
|
-
console.log(` TELEGRAM_BOT_TOKEN=...`);
|
|
184
|
-
console.log(` Then add your API keys and run: roundhouse restart`);
|
|
185
|
-
}
|
|
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");
|
|
186
148
|
}
|
|
187
|
-
|
|
188
149
|
async function cmdUninstall() {
|
|
150
|
+
|
|
189
151
|
console.log("[roundhouse] Removing systemd daemon...");
|
|
190
152
|
try { systemctl("stop"); } catch {}
|
|
191
153
|
try { systemctl("disable"); } catch {}
|
|
@@ -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
|
+
}
|
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,
|
|
@@ -443,7 +446,7 @@ async function stepValidateToken(opts: SetupOptions): Promise<BotInfo> {
|
|
|
443
446
|
}
|
|
444
447
|
|
|
445
448
|
async function stepStopGateway(): Promise<void> {
|
|
446
|
-
step("
|
|
449
|
+
step("④", "Checking for running gateway...");
|
|
447
450
|
|
|
448
451
|
if (platform() !== "linux") {
|
|
449
452
|
ok("Not Linux — skipping service check");
|
|
@@ -464,7 +467,7 @@ async function stepStopGateway(): Promise<void> {
|
|
|
464
467
|
}
|
|
465
468
|
|
|
466
469
|
async function stepInstallPackages(opts: SetupOptions, agent: AgentDefinition): Promise<void> {
|
|
467
|
-
step("
|
|
470
|
+
step("⑤", "Installing packages...");
|
|
468
471
|
|
|
469
472
|
// Roundhouse
|
|
470
473
|
const rhInstalled = whichSync("roundhouse");
|
|
@@ -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
|
|
|
@@ -596,12 +603,12 @@ async function stepInstallPackages(opts: SetupOptions, agent: AgentDefinition):
|
|
|
596
603
|
|
|
597
604
|
async function stepStoreSecrets(opts: SetupOptions, botInfo: BotInfo): Promise<void> {
|
|
598
605
|
if (!opts.psst) {
|
|
599
|
-
step("
|
|
606
|
+
step("⑧", "Storing secrets...");
|
|
600
607
|
ok("Skipped (default — use --with-psst to enable)");
|
|
601
608
|
return;
|
|
602
609
|
}
|
|
603
610
|
|
|
604
|
-
step("
|
|
611
|
+
step("⑧", "Storing secrets in psst...");
|
|
605
612
|
|
|
606
613
|
const secrets: [string, string][] = [
|
|
607
614
|
["TELEGRAM_BOT_TOKEN", opts.botToken],
|
|
@@ -640,7 +647,7 @@ async function stepStoreSecrets(opts: SetupOptions, botInfo: BotInfo): Promise<v
|
|
|
640
647
|
// ── Bundle install ──────────────────────────────────────────────────
|
|
641
648
|
|
|
642
649
|
async function stepInstallBundle(opts: SetupOptions): Promise<void> {
|
|
643
|
-
step("⑥
|
|
650
|
+
step("⑥", "Installing bundle (skills + CLI tools)...");
|
|
644
651
|
|
|
645
652
|
const bundleLog: ProvisionLog = {
|
|
646
653
|
info: (msg) => log(` ${msg}`),
|
|
@@ -657,7 +664,7 @@ async function stepConfigure(
|
|
|
657
664
|
pairResult: PairResult | null,
|
|
658
665
|
agent: AgentDefinition,
|
|
659
666
|
): Promise<void> {
|
|
660
|
-
step("
|
|
667
|
+
step("⑨", "Configuring...");
|
|
661
668
|
|
|
662
669
|
await mkdir(ROUNDHOUSE_DIR, { recursive: true });
|
|
663
670
|
|
|
@@ -766,7 +773,7 @@ async function stepConfigure(
|
|
|
766
773
|
}
|
|
767
774
|
|
|
768
775
|
async function stepPair(opts: SetupOptions, botInfo: BotInfo): Promise<PairResult | null> {
|
|
769
|
-
step("
|
|
776
|
+
step("⑦", "Pairing with Telegram...");
|
|
770
777
|
|
|
771
778
|
// Skip if chat IDs already known
|
|
772
779
|
if (opts.notifyChatIds.length > 0) {
|
|
@@ -822,13 +829,13 @@ async function stepPair(opts: SetupOptions, botInfo: BotInfo): Promise<PairResul
|
|
|
822
829
|
}
|
|
823
830
|
|
|
824
831
|
async function stepRegisterCommands(opts: SetupOptions): Promise<void> {
|
|
825
|
-
step("
|
|
832
|
+
step("⑩", "Registering bot commands...");
|
|
826
833
|
await registerBotCommands(opts.botToken);
|
|
827
834
|
ok(`${BOT_COMMANDS.length} commands registered with Telegram`);
|
|
828
835
|
}
|
|
829
836
|
|
|
830
837
|
async function stepInstallSystemd(opts: SetupOptions): Promise<void> {
|
|
831
|
-
step("
|
|
838
|
+
step("⑩b", "Installing systemd service...");
|
|
832
839
|
|
|
833
840
|
if (!opts.systemd) {
|
|
834
841
|
ok("Skipped (--no-systemd)");
|
|
@@ -873,7 +880,7 @@ async function stepInstallSystemd(opts: SetupOptions): Promise<void> {
|
|
|
873
880
|
}
|
|
874
881
|
|
|
875
882
|
async function stepPostflight(): Promise<void> {
|
|
876
|
-
step("
|
|
883
|
+
step("⑪", "Postflight checks...");
|
|
877
884
|
|
|
878
885
|
if (platform() === "linux") {
|
|
879
886
|
if (isServiceActive()) {
|
|
@@ -894,6 +901,21 @@ async function stepPostflight(): Promise<void> {
|
|
|
894
901
|
if (!whichSync("ffmpeg")) {
|
|
895
902
|
warn("ffmpeg not found (install for voice support)");
|
|
896
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
|
+
}
|
|
897
919
|
}
|
|
898
920
|
|
|
899
921
|
// ── BotFather Guide ──────────────────────────────────
|
|
@@ -921,6 +943,23 @@ async function runInteractiveTelegramSetup(opts: SetupOptions): Promise<void> {
|
|
|
921
943
|
// Step 1: Preflight
|
|
922
944
|
await stepPreflight(opts, agent);
|
|
923
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
|
+
|
|
924
963
|
// Step 2: Get bot token (prompt if not provided)
|
|
925
964
|
if (!opts.botToken) {
|
|
926
965
|
log("");
|
|
@@ -951,11 +990,11 @@ async function runInteractiveTelegramSetup(opts: SetupOptions): Promise<void> {
|
|
|
951
990
|
// Step 5: Install packages
|
|
952
991
|
await stepInstallPackages(opts, agent);
|
|
953
992
|
|
|
954
|
-
// Step
|
|
993
|
+
// Step 6: Install bundle (skills + CLI tools)
|
|
955
994
|
await stepInstallBundle(opts);
|
|
956
995
|
|
|
957
|
-
// Step
|
|
958
|
-
step("
|
|
996
|
+
// Step 7: Pair via Telegram
|
|
997
|
+
step("⑦", "Pairing with Telegram...");
|
|
959
998
|
const nonce = createPairingNonce();
|
|
960
999
|
const pairingLink = createPairingLink(botInfo.username, nonce);
|
|
961
1000
|
log(`\n Open this link to pair:\n`);
|
|
@@ -964,6 +1003,16 @@ async function runInteractiveTelegramSetup(opts: SetupOptions): Promise<void> {
|
|
|
964
1003
|
log(` Or send /start ${nonce} to @${botInfo.username}`);
|
|
965
1004
|
log("");
|
|
966
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
|
+
|
|
967
1016
|
const pairResult = await pairTelegram(
|
|
968
1017
|
opts.botToken, botInfo.username, opts.users,
|
|
969
1018
|
300_000, log, { nonce, showLink: false },
|
|
@@ -977,17 +1026,17 @@ async function runInteractiveTelegramSetup(opts: SetupOptions): Promise<void> {
|
|
|
977
1026
|
}
|
|
978
1027
|
}
|
|
979
1028
|
|
|
980
|
-
// Step
|
|
1029
|
+
// Step 8: Store secrets
|
|
981
1030
|
await stepStoreSecrets(opts, botInfo);
|
|
982
1031
|
|
|
983
|
-
// Step
|
|
1032
|
+
// Step 9: Write config
|
|
984
1033
|
await stepConfigure(opts, botInfo, pairResult, agent);
|
|
985
1034
|
|
|
986
|
-
// Step
|
|
1035
|
+
// Step 10: Register commands + install service
|
|
987
1036
|
await stepRegisterCommands(opts);
|
|
988
1037
|
await stepInstallSystemd(opts);
|
|
989
1038
|
|
|
990
|
-
// Step
|
|
1039
|
+
// Step 11: Verify
|
|
991
1040
|
await stepPostflight();
|
|
992
1041
|
|
|
993
1042
|
// Done!
|
package/src/config.ts
CHANGED
|
@@ -8,8 +8,21 @@
|
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
9
|
import { resolve } from "node:path";
|
|
10
10
|
import { readFile, access } from "node:fs/promises";
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
11
14
|
import type { GatewayConfig } from "./types";
|
|
12
15
|
|
|
16
|
+
// ── Version ──────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
const __configDir = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
|
|
20
|
+
/** Roundhouse package version (read from package.json at startup) */
|
|
21
|
+
export const ROUNDHOUSE_VERSION: string = (() => {
|
|
22
|
+
try { return JSON.parse(readFileSync(join(__configDir, "..", "package.json"), "utf8")).version; }
|
|
23
|
+
catch { return "unknown"; }
|
|
24
|
+
})();
|
|
25
|
+
|
|
13
26
|
// ── Path constants ───────────────────────────────────
|
|
14
27
|
|
|
15
28
|
/** New canonical config root */
|
package/src/gateway.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { isTelegramThread, postTelegramHtml, handleTelegramHtmlStream } from "./
|
|
|
13
13
|
import { SttService, enrichAttachmentsWithTranscripts, DEFAULT_STT_CONFIG } from "./voice/stt-service";
|
|
14
14
|
import { sendTelegramToMany } from "./notify/telegram";
|
|
15
15
|
import { runDoctor, formatDoctorTelegram, createDoctorContext } from "./cli/doctor/runner";
|
|
16
|
-
import { ROUNDHOUSE_DIR } from "./config";
|
|
16
|
+
import { ROUNDHOUSE_DIR, ROUNDHOUSE_VERSION } from "./config";
|
|
17
17
|
import { CronSchedulerService } from "./cron/scheduler";
|
|
18
18
|
import { isBuiltinJob } from "./cron/helpers";
|
|
19
19
|
import { formatSchedule, formatRunCounts, jobEnabledIcon } from "./cron/format";
|
|
@@ -65,10 +65,6 @@ import { join, dirname, basename } from "node:path";
|
|
|
65
65
|
import { fileURLToPath } from "node:url";
|
|
66
66
|
|
|
67
67
|
const __gatewayDir = dirname(fileURLToPath(import.meta.url));
|
|
68
|
-
const ROUNDHOUSE_VERSION: string = (() => {
|
|
69
|
-
try { return JSON.parse(readFileSync(join(__gatewayDir, "..", "package.json"), "utf8")).version; }
|
|
70
|
-
catch { return "unknown"; }
|
|
71
|
-
})();
|
|
72
68
|
|
|
73
69
|
function telegramChatIdFromThreadId(threadId: unknown): number | null {
|
|
74
70
|
if (typeof threadId !== "string") return null;
|