@inceptionstack/roundhouse 0.2.2 → 0.3.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 +321 -9
- package/architecture.md +77 -8
- package/package.json +3 -1
- package/src/agents/pi.ts +433 -26
- package/src/agents/registry.ts +8 -0
- package/src/cli/cli.ts +384 -189
- package/src/cli/cron.ts +296 -0
- package/src/cli/doctor/checks/agent.ts +68 -0
- package/src/cli/doctor/checks/config.ts +88 -0
- package/src/cli/doctor/checks/credentials.ts +62 -0
- package/src/cli/doctor/checks/disk.ts +69 -0
- package/src/cli/doctor/checks/stt.ts +76 -0
- package/src/cli/doctor/checks/system.ts +86 -0
- package/src/cli/doctor/checks/systemd.ts +76 -0
- package/src/cli/doctor/output.ts +58 -0
- package/src/cli/doctor/runner.ts +142 -0
- package/src/cli/doctor/shell.ts +33 -0
- package/src/cli/doctor/types.ts +44 -0
- package/src/cli/doctor.ts +48 -0
- package/src/cli/setup-telegram.ts +148 -0
- package/src/cli/setup.ts +936 -0
- package/src/commands.ts +23 -0
- package/src/config.ts +188 -0
- package/src/cron/constants.ts +54 -0
- package/src/cron/durations.ts +33 -0
- package/src/cron/format.ts +139 -0
- package/src/cron/helpers.ts +30 -0
- package/src/cron/runner.ts +148 -0
- package/src/cron/schedule.ts +101 -0
- package/src/cron/scheduler.ts +295 -0
- package/src/cron/store.ts +125 -0
- package/src/cron/template.ts +89 -0
- package/src/cron/types.ts +76 -0
- package/src/gateway.ts +927 -18
- package/src/index.ts +1 -58
- package/src/memory/bootstrap.ts +98 -0
- package/src/memory/files.ts +100 -0
- package/src/memory/inject.ts +41 -0
- package/src/memory/lifecycle.ts +245 -0
- package/src/memory/policy.ts +122 -0
- package/src/memory/prompts.ts +42 -0
- package/src/memory/state.ts +43 -0
- package/src/memory/types.ts +90 -0
- package/src/notify/telegram.ts +48 -0
- package/src/types.ts +68 -1
- package/src/util.ts +28 -2
- package/src/voice/providers/whisper.ts +339 -0
- package/src/voice/stt-service.ts +284 -0
- package/src/voice/types.ts +63 -0
package/src/cli/cli.ts
CHANGED
|
@@ -2,46 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* roundhouse CLI entry point
|
|
5
|
-
*
|
|
6
|
-
* Commands:
|
|
7
|
-
* roundhouse start — start the gateway (foreground)
|
|
8
|
-
* roundhouse install — install as a systemd daemon
|
|
9
|
-
* roundhouse uninstall — remove the systemd daemon
|
|
10
|
-
* roundhouse update — update to latest version from npm + restart daemon
|
|
11
|
-
* roundhouse status — show daemon status
|
|
12
|
-
* roundhouse logs — tail daemon logs
|
|
13
|
-
* roundhouse stop — stop the daemon
|
|
14
|
-
* roundhouse restart — restart the daemon
|
|
15
|
-
* roundhouse config — show config path and current config
|
|
16
5
|
*/
|
|
17
6
|
|
|
18
7
|
import { resolve, dirname } from "node:path";
|
|
19
8
|
import { homedir } from "node:os";
|
|
20
|
-
import { readFile, writeFile, mkdir,
|
|
9
|
+
import { readFile, writeFile, mkdir, mkdtemp } from "node:fs/promises";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { readdirSync, statSync } from "node:fs";
|
|
21
12
|
import { execSync, spawn } from "node:child_process";
|
|
22
13
|
import { fileURLToPath } from "node:url";
|
|
23
14
|
|
|
15
|
+
import {
|
|
16
|
+
CONFIG_DIR,
|
|
17
|
+
CONFIG_PATH,
|
|
18
|
+
ENV_FILE_PATH,
|
|
19
|
+
DEFAULT_CONFIG,
|
|
20
|
+
SERVICE_NAME,
|
|
21
|
+
fileExists,
|
|
22
|
+
loadConfig,
|
|
23
|
+
resolveEnvFilePath,
|
|
24
|
+
} from "../config";
|
|
25
|
+
import { getAgentSdkPackage } from "../agents/registry";
|
|
26
|
+
|
|
24
27
|
const __filename = fileURLToPath(import.meta.url);
|
|
25
28
|
const __dirname = dirname(__filename);
|
|
26
29
|
|
|
27
|
-
const SERVICE_NAME = "roundhouse";
|
|
28
|
-
const CONFIG_DIR = resolve(homedir(), ".config", "roundhouse");
|
|
29
|
-
const CONFIG_PATH = resolve(CONFIG_DIR, "gateway.config.json");
|
|
30
30
|
const SERVICE_PATH = `/etc/systemd/system/${SERVICE_NAME}.service`;
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
agent: {
|
|
34
|
-
type: "pi",
|
|
35
|
-
cwd: homedir(),
|
|
36
|
-
},
|
|
37
|
-
chat: {
|
|
38
|
-
botUsername: "roundhouse_bot",
|
|
39
|
-
allowedUsers: [] as string[],
|
|
40
|
-
adapters: {
|
|
41
|
-
telegram: { mode: "polling" },
|
|
42
|
-
},
|
|
43
|
-
},
|
|
44
|
-
};
|
|
32
|
+
// ── Shell helpers ───────────────────────────────────
|
|
45
33
|
|
|
46
34
|
function run(cmd: string, opts?: { silent?: boolean }): string {
|
|
47
35
|
try {
|
|
@@ -56,40 +44,31 @@ function runSudo(cmd: string): void {
|
|
|
56
44
|
execSync(`sudo ${cmd}`, { stdio: "inherit" });
|
|
57
45
|
}
|
|
58
46
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return true;
|
|
63
|
-
} catch {
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
47
|
+
function systemctl(verb: string, message?: string): void {
|
|
48
|
+
runSudo(`systemctl ${verb} ${SERVICE_NAME}`);
|
|
49
|
+
if (message) console.log(` ✅ ${message}`);
|
|
66
50
|
}
|
|
67
51
|
|
|
68
52
|
// ── Commands ────────────────────────────────────────
|
|
69
53
|
|
|
70
54
|
async function cmdStart() {
|
|
71
|
-
// Import and run the gateway in-process (foreground)
|
|
72
55
|
process.env.ROUNDHOUSE_CONFIG = CONFIG_PATH;
|
|
73
|
-
const indexPath = resolve(__dirname, "..", "
|
|
74
|
-
|
|
75
|
-
// If running from installed npm package, use compiled JS
|
|
56
|
+
const indexPath = resolve(__dirname, "..", "index.ts");
|
|
76
57
|
const jsPath = resolve(__dirname, "..", "dist", "index.js");
|
|
58
|
+
|
|
77
59
|
if (await fileExists(jsPath)) {
|
|
78
60
|
await import(jsPath);
|
|
79
61
|
} else {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
env: { ...process.env, ROUNDHOUSE_CONFIG: CONFIG_PATH },
|
|
85
|
-
});
|
|
62
|
+
execSync(
|
|
63
|
+
`node ${resolve(__dirname, "..", "node_modules", "tsx", "dist", "cli.mjs")} ${indexPath}`,
|
|
64
|
+
{ stdio: "inherit", env: { ...process.env, ROUNDHOUSE_CONFIG: CONFIG_PATH } },
|
|
65
|
+
);
|
|
86
66
|
}
|
|
87
67
|
}
|
|
88
68
|
|
|
89
69
|
async function cmdInstall() {
|
|
90
70
|
console.log("[roundhouse] Installing as systemd daemon...\n");
|
|
91
71
|
|
|
92
|
-
// 1. Create config if missing
|
|
93
72
|
await mkdir(CONFIG_DIR, { recursive: true });
|
|
94
73
|
if (await fileExists(CONFIG_PATH)) {
|
|
95
74
|
console.log(` Config exists: ${CONFIG_PATH}`);
|
|
@@ -99,19 +78,55 @@ async function cmdInstall() {
|
|
|
99
78
|
console.log(` ⚠️ Edit this file to set allowedUsers and other settings.`);
|
|
100
79
|
}
|
|
101
80
|
|
|
102
|
-
//
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
81
|
+
// Write environment file for secrets — merge with existing to preserve manually-added keys
|
|
82
|
+
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"];
|
|
83
|
+
const existing = new Map<string, string>();
|
|
84
|
+
const resolvedEnvPath = await resolveEnvFilePath();
|
|
85
|
+
if (await fileExists(resolvedEnvPath)) {
|
|
86
|
+
const raw = await readFile(resolvedEnvPath, "utf8");
|
|
87
|
+
for (const line of raw.split("\n")) {
|
|
88
|
+
const trimmed = line.trim();
|
|
89
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
90
|
+
const eq = trimmed.indexOf("=");
|
|
91
|
+
if (eq > 0) existing.set(trimmed.slice(0, eq), trimmed.slice(eq + 1));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Override with current env vars for known keys
|
|
95
|
+
let envChanged = false;
|
|
96
|
+
for (const key of ENV_KEYS) {
|
|
109
97
|
if (process.env[key]) {
|
|
110
|
-
|
|
98
|
+
existing.set(key, `"${process.env[key].replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\$/g, "\\$$").replace(/`/g, "\\`").replace(/\n/g, "\\n")}"`);
|
|
99
|
+
envChanged = true;
|
|
111
100
|
}
|
|
112
101
|
}
|
|
102
|
+
if (envChanged || !(await fileExists(ENV_FILE_PATH))) {
|
|
103
|
+
if (resolvedEnvPath !== ENV_FILE_PATH && await fileExists(resolvedEnvPath)) {
|
|
104
|
+
console.log(` Copying env file from ${resolvedEnvPath} to ${ENV_FILE_PATH}`);
|
|
105
|
+
}
|
|
106
|
+
const envFileContent = [...existing.entries()].map(([k, v]) => `${k}=${v}`).join("\n") + "\n";
|
|
107
|
+
await writeFile(ENV_FILE_PATH, envFileContent, { mode: 0o600 });
|
|
108
|
+
console.log(` Environment file: ${ENV_FILE_PATH}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Resolve paths — prefer the installed bin, fall back to tsx + source
|
|
112
|
+
const binPath = run("which roundhouse", { silent: true });
|
|
113
|
+
const nodePath = run("which node", { silent: true }) || process.execPath;
|
|
114
|
+
const tsxPath = resolve(__dirname, "..", "node_modules", ".bin", "tsx");
|
|
115
|
+
const srcIndex = resolve(__dirname, "..", "index.ts");
|
|
116
|
+
|
|
117
|
+
let execStart: string;
|
|
118
|
+
if (binPath) {
|
|
119
|
+
execStart = `${nodePath} ${binPath} start`;
|
|
120
|
+
} else {
|
|
121
|
+
// No global install — use tsx directly
|
|
122
|
+
const tsxBin = run("which tsx", { silent: true }) || tsxPath;
|
|
123
|
+
execStart = `${tsxBin} ${srcIndex}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Compute PATH that includes node's bin dir (for mise/nvm setups)
|
|
127
|
+
const nodeBinDir = dirname(nodePath);
|
|
128
|
+
const pathValue = `${nodeBinDir}:/usr/local/bin:/usr/bin:/bin`;
|
|
113
129
|
|
|
114
|
-
// 4. Create systemd unit
|
|
115
130
|
const unit = `[Unit]
|
|
116
131
|
Description=Roundhouse Chat Gateway
|
|
117
132
|
After=network.target
|
|
@@ -120,47 +135,45 @@ After=network.target
|
|
|
120
135
|
Type=simple
|
|
121
136
|
User=${process.env.USER || "root"}
|
|
122
137
|
WorkingDirectory=${homedir()}
|
|
123
|
-
ExecStart=${
|
|
138
|
+
ExecStart=${execStart}
|
|
124
139
|
Restart=on-failure
|
|
125
140
|
RestartSec=5
|
|
126
|
-
|
|
141
|
+
EnvironmentFile=-${ENV_FILE_PATH}
|
|
127
142
|
Environment=ROUNDHOUSE_CONFIG=${CONFIG_PATH}
|
|
128
143
|
Environment=NODE_ENV=production
|
|
144
|
+
Environment=PATH=${pathValue}
|
|
129
145
|
|
|
130
146
|
[Install]
|
|
131
147
|
WantedBy=multi-user.target
|
|
132
148
|
`;
|
|
133
149
|
|
|
134
|
-
const
|
|
135
|
-
|
|
150
|
+
const tmpDir = await mkdtemp(resolve(tmpdir(), "roundhouse-"));
|
|
151
|
+
const tmpUnit = resolve(tmpDir, `${SERVICE_NAME}.service`);
|
|
152
|
+
await writeFile(tmpUnit, unit, { mode: 0o600 });
|
|
136
153
|
runSudo(`cp ${tmpUnit} ${SERVICE_PATH}`);
|
|
154
|
+
runSudo(`rm -rf -- ${tmpDir}`);
|
|
137
155
|
runSudo("systemctl daemon-reload");
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
console.log(`\n
|
|
142
|
-
console.log(
|
|
143
|
-
console.log(` Service:
|
|
144
|
-
console.log(` Logs:
|
|
145
|
-
console.log(` Status:
|
|
146
|
-
|
|
147
|
-
if (
|
|
148
|
-
console.log(`\n ⚠️ No env vars detected.
|
|
149
|
-
console.log(`
|
|
156
|
+
systemctl("enable");
|
|
157
|
+
systemctl("start", "Daemon installed and started.");
|
|
158
|
+
|
|
159
|
+
console.log(`\n Config: ${CONFIG_PATH}`);
|
|
160
|
+
console.log(` Env file: ${ENV_FILE_PATH}`);
|
|
161
|
+
console.log(` Service: ${SERVICE_PATH}`);
|
|
162
|
+
console.log(` Logs: roundhouse logs`);
|
|
163
|
+
console.log(` Status: roundhouse status`);
|
|
164
|
+
|
|
165
|
+
if (!envChanged) {
|
|
166
|
+
console.log(`\n ⚠️ No env vars detected. Edit ${ENV_FILE_PATH} with your secrets:`);
|
|
167
|
+
console.log(` TELEGRAM_BOT_TOKEN=...`);
|
|
168
|
+
console.log(` Then add your API keys and run: roundhouse restart`);
|
|
150
169
|
}
|
|
151
170
|
}
|
|
152
171
|
|
|
153
172
|
async function cmdUninstall() {
|
|
154
173
|
console.log("[roundhouse] Removing systemd daemon...");
|
|
155
|
-
try {
|
|
156
|
-
|
|
157
|
-
} catch {}
|
|
158
|
-
try {
|
|
159
|
-
runSudo(`systemctl disable ${SERVICE_NAME}`);
|
|
160
|
-
} catch {}
|
|
161
|
-
try {
|
|
162
|
-
runSudo(`rm -f ${SERVICE_PATH}`);
|
|
163
|
-
} catch {}
|
|
174
|
+
try { systemctl("stop"); } catch {}
|
|
175
|
+
try { systemctl("disable"); } catch {}
|
|
176
|
+
try { runSudo(`rm -f ${SERVICE_PATH}`); } catch {}
|
|
164
177
|
runSudo("systemctl daemon-reload");
|
|
165
178
|
console.log(" ✅ Daemon removed. Config preserved at:", CONFIG_PATH);
|
|
166
179
|
}
|
|
@@ -170,84 +183,143 @@ async function cmdUpdate() {
|
|
|
170
183
|
run("npm update -g roundhouse");
|
|
171
184
|
console.log("\n[roundhouse] Restarting daemon...");
|
|
172
185
|
try {
|
|
173
|
-
|
|
174
|
-
console.log(" ✅ Updated and restarted.");
|
|
186
|
+
systemctl("restart", "Updated and restarted.");
|
|
175
187
|
} catch {
|
|
176
188
|
console.log(" ⚠️ Daemon not running. Start with: roundhouse install");
|
|
177
189
|
}
|
|
178
190
|
}
|
|
179
191
|
|
|
180
|
-
function cmdStatus() {
|
|
192
|
+
async function cmdStatus() {
|
|
193
|
+
// Show systemd status
|
|
194
|
+
const isActive = run(`systemctl is-active ${SERVICE_NAME}`, { silent: true }) === "active";
|
|
195
|
+
|
|
196
|
+
if (!isActive) {
|
|
197
|
+
console.log("\n ❌ Roundhouse is not running.\n");
|
|
198
|
+
console.log(" Install with: roundhouse install");
|
|
199
|
+
console.log(" Or start foreground: roundhouse start\n");
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Load config for details
|
|
204
|
+
let config: Awaited<ReturnType<typeof loadConfig>> | null = null;
|
|
181
205
|
try {
|
|
182
|
-
|
|
183
|
-
} catch {
|
|
184
|
-
|
|
206
|
+
config = await loadConfig();
|
|
207
|
+
} catch {}
|
|
208
|
+
|
|
209
|
+
// Gather systemd info
|
|
210
|
+
const pid = run(`systemctl show -p MainPID --value ${SERVICE_NAME}`, { silent: true });
|
|
211
|
+
const activeState = run(`systemctl show -p ActiveState --value ${SERVICE_NAME}`, { silent: true });
|
|
212
|
+
const startedAt = run(`systemctl show -p ActiveEnterTimestamp --value ${SERVICE_NAME}`, { silent: true });
|
|
213
|
+
|
|
214
|
+
// Compute uptime
|
|
215
|
+
let uptimeStr = "unknown";
|
|
216
|
+
if (startedAt) {
|
|
217
|
+
const startMs = new Date(startedAt).getTime();
|
|
218
|
+
if (!isNaN(startMs)) {
|
|
219
|
+
const sec = Math.max(0, Math.floor((Date.now() - startMs) / 1000));
|
|
220
|
+
if (sec < 3600) uptimeStr = `${Math.floor(sec / 60)}m ${sec % 60}s`;
|
|
221
|
+
else uptimeStr = `${Math.floor(sec / 3600)}h ${Math.floor((sec % 3600) / 60)}m`;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Memory from PID
|
|
226
|
+
let memStr = "unknown";
|
|
227
|
+
if (pid && pid !== "0" && /^\d+$/.test(pid)) {
|
|
228
|
+
const rssKb = run(`ps -o rss= -p ${pid}`, { silent: true }).trim();
|
|
229
|
+
if (rssKb) {
|
|
230
|
+
const parsed = parseInt(rssKb, 10);
|
|
231
|
+
if (!isNaN(parsed)) memStr = `${(parsed / 1024).toFixed(1)} MB`;
|
|
232
|
+
}
|
|
185
233
|
}
|
|
234
|
+
|
|
235
|
+
// Read env file for debug flags
|
|
236
|
+
let debugStream = false;
|
|
237
|
+
const statusEnvPath = await resolveEnvFilePath();
|
|
238
|
+
try {
|
|
239
|
+
const envContent = await readFile(statusEnvPath, "utf8");
|
|
240
|
+
debugStream = envContent.includes("ROUNDHOUSE_DEBUG_STREAM=1") || envContent.includes('ROUNDHOUSE_DEBUG_STREAM="1"');
|
|
241
|
+
} catch {}
|
|
242
|
+
|
|
243
|
+
// Read versions
|
|
244
|
+
let roundhouseVersion = "unknown";
|
|
245
|
+
let agentVersion = "unknown";
|
|
246
|
+
try {
|
|
247
|
+
const pkgPath = resolve(__dirname, "..", "..", "package.json");
|
|
248
|
+
const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
|
|
249
|
+
roundhouseVersion = pkg.version;
|
|
250
|
+
} catch {}
|
|
251
|
+
|
|
252
|
+
// Resolve agent SDK version from registry
|
|
253
|
+
const agentPkg = config ? getAgentSdkPackage(config.agent.type) : undefined;
|
|
254
|
+
if (agentPkg) {
|
|
255
|
+
try {
|
|
256
|
+
const agentPkgPath = resolve(__dirname, "..", "..", "node_modules", ...agentPkg.split("/"), "package.json");
|
|
257
|
+
agentVersion = JSON.parse(await readFile(agentPkgPath, "utf8")).version;
|
|
258
|
+
} catch {}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
console.log("\n 🟢 Roundhouse is running\n");
|
|
262
|
+
console.log(` Version: v${roundhouseVersion}`);
|
|
263
|
+
console.log(` State: ${activeState}`);
|
|
264
|
+
console.log(` PID: ${pid}`);
|
|
265
|
+
console.log(` Uptime: ${uptimeStr}`);
|
|
266
|
+
console.log(` Memory: ${memStr}`);
|
|
267
|
+
|
|
268
|
+
if (config) {
|
|
269
|
+
const platforms = Object.keys(config.chat.adapters).join(", ");
|
|
270
|
+
const allowedCount = config.chat.allowedUsers?.length ?? 0;
|
|
271
|
+
console.log(` Agent: ${config.agent.type} (v${agentVersion})`);
|
|
272
|
+
console.log(` Agent CWD: ${config.agent.cwd ?? process.cwd()}`);
|
|
273
|
+
console.log(` Platforms: ${platforms}`);
|
|
274
|
+
console.log(` Bot: @${config.chat.botUsername}`);
|
|
275
|
+
console.log(` Allowed users: ${allowedCount === 0 ? "all (no allowlist)" : config.chat.allowedUsers!.join(", ")}`);
|
|
276
|
+
console.log(` Notify chats: ${config.chat.notifyChatIds?.join(", ") ?? "none"}`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
console.log(` Debug stream: ${debugStream ? "on" : "off"}`);
|
|
280
|
+
console.log(` Config: ${CONFIG_PATH}`);
|
|
281
|
+
console.log(` Env file: ${statusEnvPath}`);
|
|
282
|
+
console.log();
|
|
186
283
|
}
|
|
187
284
|
|
|
188
285
|
function cmdLogs() {
|
|
189
286
|
const child = spawn("journalctl", ["-u", SERVICE_NAME, "-f", "--no-pager", "-n", "100"], {
|
|
190
287
|
stdio: "inherit",
|
|
191
288
|
});
|
|
192
|
-
child.on("error", () =>
|
|
193
|
-
console.log("Could not read logs. Is the daemon installed?");
|
|
194
|
-
});
|
|
289
|
+
child.on("error", () => console.log("Could not read logs. Is the daemon installed?"));
|
|
195
290
|
}
|
|
196
291
|
|
|
197
|
-
function cmdStop() {
|
|
198
|
-
|
|
199
|
-
console.log(" ✅ Daemon stopped.");
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function cmdRestart() {
|
|
203
|
-
runSudo(`systemctl restart ${SERVICE_NAME}`);
|
|
204
|
-
console.log(" ✅ Daemon restarted.");
|
|
205
|
-
}
|
|
292
|
+
function cmdStop() { systemctl("stop", "Daemon stopped."); }
|
|
293
|
+
function cmdRestart() { systemctl("restart", "Daemon restarted."); }
|
|
206
294
|
|
|
207
295
|
async function cmdConfig() {
|
|
208
296
|
console.log(`Config path: ${CONFIG_PATH}\n`);
|
|
209
297
|
if (await fileExists(CONFIG_PATH)) {
|
|
210
|
-
|
|
211
|
-
console.log(content);
|
|
298
|
+
console.log(await readFile(CONFIG_PATH, "utf8"));
|
|
212
299
|
} else {
|
|
213
300
|
console.log("(no config file — defaults will be used)");
|
|
214
301
|
}
|
|
215
302
|
}
|
|
216
303
|
|
|
217
304
|
async function cmdTui() {
|
|
218
|
-
|
|
219
|
-
let config: any = DEFAULT_CONFIG;
|
|
220
|
-
if (await fileExists(CONFIG_PATH)) {
|
|
221
|
-
try {
|
|
222
|
-
config = JSON.parse(await readFile(CONFIG_PATH, "utf8"));
|
|
223
|
-
} catch {}
|
|
224
|
-
}
|
|
225
|
-
|
|
305
|
+
const config = await loadConfig();
|
|
226
306
|
const agentType = config.agent?.type ?? "pi";
|
|
227
307
|
|
|
228
308
|
if (agentType !== "pi") {
|
|
229
309
|
console.error(`roundhouse tui: agent type "${agentType}" does not support TUI yet.`);
|
|
230
|
-
console.error("Only \"pi\" is supported currently.");
|
|
231
310
|
process.exit(1);
|
|
232
311
|
}
|
|
233
312
|
|
|
234
|
-
|
|
235
|
-
|
|
313
|
+
const sessionsBase = (config.agent as any)?.sessionDir
|
|
314
|
+
?? resolve(homedir(), ".pi", "agent", "gateway-sessions");
|
|
315
|
+
|
|
236
316
|
let threadDirs: string[] = [];
|
|
237
317
|
try {
|
|
238
|
-
const { readdirSync, statSync } = await import("node:fs");
|
|
239
318
|
threadDirs = readdirSync(sessionsBase)
|
|
240
|
-
.filter((d
|
|
241
|
-
try {
|
|
242
|
-
return statSync(resolve(sessionsBase, d)).isDirectory();
|
|
243
|
-
} catch {
|
|
244
|
-
return false;
|
|
245
|
-
}
|
|
246
|
-
})
|
|
319
|
+
.filter((d) => { try { return statSync(resolve(sessionsBase, d)).isDirectory(); } catch { return false; } })
|
|
247
320
|
.sort();
|
|
248
321
|
} catch {
|
|
249
322
|
console.error(`No gateway sessions found at ${sessionsBase}`);
|
|
250
|
-
console.error("Send a message via Telegram/Slack first to create a session.");
|
|
251
323
|
process.exit(1);
|
|
252
324
|
}
|
|
253
325
|
|
|
@@ -256,26 +328,18 @@ async function cmdTui() {
|
|
|
256
328
|
process.exit(1);
|
|
257
329
|
}
|
|
258
330
|
|
|
259
|
-
|
|
260
|
-
const { readdirSync, statSync } = await import("node:fs");
|
|
261
|
-
const threadArg = process.argv[3]; // optional: roundhouse tui <thread>
|
|
331
|
+
const threadArg = process.argv[3];
|
|
262
332
|
|
|
263
|
-
interface SessionCandidate {
|
|
264
|
-
threadDir: string;
|
|
265
|
-
sessionFile: string;
|
|
266
|
-
mtime: number;
|
|
267
|
-
}
|
|
333
|
+
interface SessionCandidate { threadDir: string; sessionFile: string; mtime: number; }
|
|
268
334
|
|
|
269
335
|
const candidates: SessionCandidate[] = [];
|
|
270
336
|
for (const dir of threadDirs) {
|
|
271
337
|
if (threadArg && !dir.includes(threadArg)) continue;
|
|
272
338
|
const threadPath = resolve(sessionsBase, dir);
|
|
273
339
|
try {
|
|
274
|
-
const
|
|
275
|
-
for (const f of files) {
|
|
340
|
+
for (const f of readdirSync(threadPath).filter((f) => f.endsWith(".jsonl"))) {
|
|
276
341
|
const fullPath = resolve(threadPath, f);
|
|
277
|
-
|
|
278
|
-
candidates.push({ threadDir: dir, sessionFile: fullPath, mtime: st.mtimeMs });
|
|
342
|
+
candidates.push({ threadDir: dir, sessionFile: fullPath, mtime: statSync(fullPath).mtimeMs });
|
|
279
343
|
}
|
|
280
344
|
} catch {}
|
|
281
345
|
}
|
|
@@ -291,10 +355,8 @@ async function cmdTui() {
|
|
|
291
355
|
process.exit(1);
|
|
292
356
|
}
|
|
293
357
|
|
|
294
|
-
// Sort by most recently modified
|
|
295
358
|
candidates.sort((a, b) => b.mtime - a.mtime);
|
|
296
359
|
|
|
297
|
-
// If multiple threads and no filter, let user pick
|
|
298
360
|
let selected: SessionCandidate;
|
|
299
361
|
const uniqueThreads = [...new Set(candidates.map((c) => c.threadDir))];
|
|
300
362
|
|
|
@@ -316,14 +378,10 @@ async function cmdTui() {
|
|
|
316
378
|
}
|
|
317
379
|
console.log();
|
|
318
380
|
|
|
319
|
-
// Simple prompt
|
|
320
381
|
const readline = await import("node:readline");
|
|
321
382
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
322
|
-
const answer = await new Promise<string>((
|
|
323
|
-
rl.question("Pick a session [1]: ", (ans) => {
|
|
324
|
-
rl.close();
|
|
325
|
-
resolve(ans.trim() || "1");
|
|
326
|
-
});
|
|
383
|
+
const answer = await new Promise<string>((r) => {
|
|
384
|
+
rl.question("Pick a session [1]: ", (ans) => { rl.close(); r(ans.trim() || "1"); });
|
|
327
385
|
});
|
|
328
386
|
|
|
329
387
|
const idx = parseInt(answer, 10) - 1;
|
|
@@ -331,28 +389,17 @@ async function cmdTui() {
|
|
|
331
389
|
console.error("Invalid selection.");
|
|
332
390
|
process.exit(1);
|
|
333
391
|
}
|
|
334
|
-
// Find most recent session file for the selected thread
|
|
335
392
|
selected = candidates.find((c) => c.threadDir === shown[idx].threadDir)!;
|
|
336
393
|
}
|
|
337
394
|
|
|
338
395
|
console.log(`\nOpening: ${selected.sessionFile}\n`);
|
|
339
396
|
|
|
340
|
-
|
|
341
|
-
const piArgs = ["--resume", selected.sessionFile];
|
|
342
|
-
const child = spawn("pi", piArgs, { stdio: "inherit" });
|
|
343
|
-
|
|
397
|
+
const child = spawn("pi", ["--resume", selected.sessionFile], { stdio: "inherit" });
|
|
344
398
|
child.on("error", (err) => {
|
|
345
|
-
|
|
346
|
-
console.error("'pi' not found in PATH. Install pi first.");
|
|
347
|
-
} else {
|
|
348
|
-
console.error("Failed to launch pi:", err.message);
|
|
349
|
-
}
|
|
399
|
+
console.error((err as any).code === "ENOENT" ? "'pi' not found in PATH." : `Failed: ${err.message}`);
|
|
350
400
|
process.exit(1);
|
|
351
401
|
});
|
|
352
|
-
|
|
353
|
-
child.on("exit", (code) => {
|
|
354
|
-
process.exit(code ?? 0);
|
|
355
|
-
});
|
|
402
|
+
child.on("exit", (code) => process.exit(code ?? 0));
|
|
356
403
|
}
|
|
357
404
|
|
|
358
405
|
function printHelp() {
|
|
@@ -363,6 +410,8 @@ Usage:
|
|
|
363
410
|
roundhouse <command>
|
|
364
411
|
|
|
365
412
|
Commands:
|
|
413
|
+
setup One-command install & configure (also works via npx)
|
|
414
|
+
pair Pair Telegram account for notifications
|
|
366
415
|
start Start the gateway (foreground)
|
|
367
416
|
tui [thread] Open agent TUI on a gateway session
|
|
368
417
|
install Install as a systemd daemon (requires sudo)
|
|
@@ -373,9 +422,15 @@ Commands:
|
|
|
373
422
|
stop Stop the daemon
|
|
374
423
|
restart Restart the daemon
|
|
375
424
|
config Show config path and contents
|
|
425
|
+
agent <message> Send a message to the agent and print response
|
|
426
|
+
Options: --thread <id>, --stdin, --timeout <sec>,
|
|
427
|
+
--no-timeout, --verbose
|
|
428
|
+
doctor [--fix] Check system health and configuration
|
|
429
|
+
Options: --fix, --json, --verbose
|
|
430
|
+
cron <command> Manage scheduled jobs (add, list, trigger, etc.)
|
|
376
431
|
|
|
377
432
|
Config:
|
|
378
|
-
~/.
|
|
433
|
+
~/.roundhouse/gateway.config.json
|
|
379
434
|
|
|
380
435
|
Environment:
|
|
381
436
|
TELEGRAM_BOT_TOKEN Telegram bot token
|
|
@@ -386,40 +441,180 @@ Environment:
|
|
|
386
441
|
|
|
387
442
|
// ── Main ────────────────────────────────────────────
|
|
388
443
|
|
|
444
|
+
async function cmdAgent() {
|
|
445
|
+
// Usage: roundhouse agent <message>
|
|
446
|
+
// roundhouse agent --thread <id> <message>
|
|
447
|
+
// echo "message" | roundhouse agent --stdin
|
|
448
|
+
const args = process.argv.slice(3);
|
|
449
|
+
let threadId = "";
|
|
450
|
+
let messageText = "";
|
|
451
|
+
let useStdin = false;
|
|
452
|
+
let timeoutMs = 120_000;
|
|
453
|
+
let verbose = false;
|
|
454
|
+
|
|
455
|
+
for (let i = 0; i < args.length; i++) {
|
|
456
|
+
if (args[i] === "--thread" && args[i + 1]) {
|
|
457
|
+
threadId = args[++i];
|
|
458
|
+
} else if (args[i] === "--stdin") {
|
|
459
|
+
useStdin = true;
|
|
460
|
+
} else if (args[i] === "--timeout" && args[i + 1]) {
|
|
461
|
+
const val = parseInt(args[++i], 10);
|
|
462
|
+
if (isNaN(val) || val <= 0) { console.error("--timeout must be a positive number (seconds)"); process.exit(1); }
|
|
463
|
+
timeoutMs = val * 1000;
|
|
464
|
+
} else if (args[i] === "--no-timeout") {
|
|
465
|
+
timeoutMs = 0;
|
|
466
|
+
} else if (args[i] === "--verbose") {
|
|
467
|
+
verbose = true;
|
|
468
|
+
} else if (args[i].startsWith("-")) {
|
|
469
|
+
console.error(`Unknown flag: ${args[i]}`);
|
|
470
|
+
process.exit(1);
|
|
471
|
+
} else {
|
|
472
|
+
messageText = args.slice(i).join(" ");
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (useStdin) {
|
|
478
|
+
const chunks: Buffer[] = [];
|
|
479
|
+
let totalBytes = 0;
|
|
480
|
+
const MAX_INPUT = 1024 * 1024; // 1 MB
|
|
481
|
+
for await (const chunk of process.stdin) {
|
|
482
|
+
totalBytes += chunk.length;
|
|
483
|
+
if (totalBytes > MAX_INPUT) {
|
|
484
|
+
console.error(`Input exceeds ${MAX_INPUT / 1024}KB limit. Use a file instead.`);
|
|
485
|
+
process.exit(1);
|
|
486
|
+
}
|
|
487
|
+
chunks.push(chunk);
|
|
488
|
+
}
|
|
489
|
+
// Strip single trailing newline (shell echo adds one)
|
|
490
|
+
let raw = Buffer.concat(chunks).toString("utf8");
|
|
491
|
+
if (raw.endsWith("\n")) raw = raw.slice(0, -1);
|
|
492
|
+
messageText = raw;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (!messageText) {
|
|
496
|
+
console.error("Usage: roundhouse agent <message>");
|
|
497
|
+
console.error(" roundhouse agent --thread <id> <message>");
|
|
498
|
+
console.error(" echo \"message\" | roundhouse agent --stdin");
|
|
499
|
+
console.error(" roundhouse agent --timeout 60 <message>");
|
|
500
|
+
console.error(" roundhouse agent --verbose <message>");
|
|
501
|
+
process.exit(1);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Default: ephemeral thread ID (no session persistence across invocations)
|
|
505
|
+
// --thread <id> opts into persistent/shared sessions
|
|
506
|
+
if (!threadId) {
|
|
507
|
+
threadId = `cli-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Suppress debug/info logs unless --verbose
|
|
511
|
+
const origLog = console.log;
|
|
512
|
+
const origWarn = console.warn;
|
|
513
|
+
const origError = console.error;
|
|
514
|
+
if (!verbose) {
|
|
515
|
+
console.log = () => {};
|
|
516
|
+
console.warn = () => {};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
let agent: import("../types").AgentAdapter | undefined;
|
|
520
|
+
let aborted = false;
|
|
521
|
+
|
|
522
|
+
// Clean abort on SIGINT/SIGTERM
|
|
523
|
+
const handleSignal = async () => {
|
|
524
|
+
if (aborted) return;
|
|
525
|
+
aborted = true;
|
|
526
|
+
console.log = origLog;
|
|
527
|
+
console.warn = origWarn;
|
|
528
|
+
console.error = origError;
|
|
529
|
+
try { await agent?.abort?.(threadId); } catch {}
|
|
530
|
+
try { await agent?.dispose(); } catch {}
|
|
531
|
+
process.exit(130);
|
|
532
|
+
};
|
|
533
|
+
process.on("SIGINT", handleSignal);
|
|
534
|
+
process.on("SIGTERM", handleSignal);
|
|
535
|
+
|
|
536
|
+
// Timeout race
|
|
537
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
538
|
+
const timeoutPromise = timeoutMs > 0
|
|
539
|
+
? new Promise<never>((_, reject) => {
|
|
540
|
+
timer = setTimeout(async () => {
|
|
541
|
+
aborted = true;
|
|
542
|
+
try { await agent?.abort?.(threadId); } catch {}
|
|
543
|
+
reject(new Error(`Timeout after ${timeoutMs / 1000}s`));
|
|
544
|
+
}, timeoutMs);
|
|
545
|
+
})
|
|
546
|
+
: null;
|
|
547
|
+
|
|
548
|
+
try {
|
|
549
|
+
const config = await loadConfig();
|
|
550
|
+
const { getAgentFactory } = await import("../agents/registry");
|
|
551
|
+
const factory = getAgentFactory(config.agent.type);
|
|
552
|
+
agent = factory(config.agent);
|
|
553
|
+
|
|
554
|
+
const runAgent = async () => {
|
|
555
|
+
if (agent!.promptStream) {
|
|
556
|
+
for await (const event of agent!.promptStream(threadId, { text: messageText })) {
|
|
557
|
+
if (event.type === "text_delta") {
|
|
558
|
+
process.stdout.write(event.text);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
process.stdout.write("\n");
|
|
562
|
+
} else {
|
|
563
|
+
const response = await agent!.prompt(threadId, { text: messageText });
|
|
564
|
+
origLog(response.text);
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
if (timeoutPromise) {
|
|
569
|
+
await Promise.race([runAgent(), timeoutPromise]);
|
|
570
|
+
} else {
|
|
571
|
+
await runAgent();
|
|
572
|
+
}
|
|
573
|
+
} catch (err: any) {
|
|
574
|
+
console.error = origError;
|
|
575
|
+
console.error(`Error: ${err.message}`);
|
|
576
|
+
process.exit(aborted ? 124 : 1); // 124 = timeout (like coreutils)
|
|
577
|
+
} finally {
|
|
578
|
+
if (timer) clearTimeout(timer);
|
|
579
|
+
process.off("SIGINT", handleSignal);
|
|
580
|
+
process.off("SIGTERM", handleSignal);
|
|
581
|
+
console.log = origLog;
|
|
582
|
+
console.warn = origWarn;
|
|
583
|
+
console.error = origError;
|
|
584
|
+
if (!aborted) await agent?.dispose();
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
import { cmdDoctor } from "./doctor";
|
|
589
|
+
import { cmdCron } from "./cron";
|
|
590
|
+
import { cmdSetup, cmdPair } from "./setup";
|
|
591
|
+
|
|
389
592
|
const command = process.argv[2];
|
|
390
593
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
cmdConfig();
|
|
418
|
-
break;
|
|
419
|
-
case "tui":
|
|
420
|
-
cmdTui();
|
|
421
|
-
break;
|
|
422
|
-
default:
|
|
423
|
-
printHelp();
|
|
424
|
-
break;
|
|
594
|
+
const commands: Record<string, () => void | Promise<void>> = {
|
|
595
|
+
setup: () => cmdSetup(process.argv.slice(3)),
|
|
596
|
+
pair: () => cmdPair(process.argv.slice(3)),
|
|
597
|
+
start: cmdStart,
|
|
598
|
+
install: cmdInstall,
|
|
599
|
+
uninstall: cmdUninstall,
|
|
600
|
+
update: cmdUpdate,
|
|
601
|
+
status: cmdStatus,
|
|
602
|
+
logs: cmdLogs,
|
|
603
|
+
stop: cmdStop,
|
|
604
|
+
restart: cmdRestart,
|
|
605
|
+
config: cmdConfig,
|
|
606
|
+
tui: cmdTui,
|
|
607
|
+
doctor: () => cmdDoctor(process.argv.slice(3)),
|
|
608
|
+
cron: () => cmdCron(process.argv.slice(3)),
|
|
609
|
+
agent: cmdAgent,
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
const fn = command ? commands[command] : undefined;
|
|
613
|
+
if (fn) {
|
|
614
|
+
Promise.resolve(fn()).catch((err) => {
|
|
615
|
+
console.error(`[roundhouse] ${command} failed:`, err);
|
|
616
|
+
process.exit(1);
|
|
617
|
+
});
|
|
618
|
+
} else {
|
|
619
|
+
printHelp();
|
|
425
620
|
}
|