@inceptionstack/roundhouse 0.5.1 → 0.5.3
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 -1
- package/architecture.md +94 -31
- package/package.json +1 -1
- package/src/agents/kiro/kiro-adapter.ts +8 -1
- package/src/agents/pi/message-format.ts +87 -0
- package/src/agents/pi/pi-adapter.ts +9 -72
- package/src/cli/agent-command.ts +210 -0
- package/src/cli/cli.ts +69 -215
- package/src/cli/cron-commands.ts +258 -0
- package/src/cli/cron.ts +26 -267
- package/src/cli/launchd.ts +144 -0
- package/src/cli/service-manager.ts +192 -0
- package/src/cli/setup/args.ts +109 -0
- package/src/cli/setup/flows.ts +273 -0
- package/src/cli/setup/helpers.ts +66 -0
- package/src/cli/setup/index.ts +7 -0
- package/src/cli/setup/runtime.ts +109 -0
- package/src/cli/setup/steps.ts +617 -0
- package/src/cli/setup/types.ts +52 -0
- package/src/cli/setup.ts +79 -1231
- package/src/cli/shell.ts +49 -0
- package/src/cli/systemd.ts +6 -33
- package/src/config.ts +67 -53
- package/src/gateway/attachments.ts +147 -0
- package/src/gateway/commands.ts +371 -0
- package/src/gateway/helpers.ts +104 -0
- package/src/gateway/index.ts +11 -0
- package/src/gateway/streaming.ts +211 -0
- package/src/gateway.ts +212 -763
- package/src/types.ts +14 -0
package/src/cli/cli.ts
CHANGED
|
@@ -14,7 +14,6 @@ import { performUpdate } from "../commands/update";
|
|
|
14
14
|
import {
|
|
15
15
|
CONFIG_PATH,
|
|
16
16
|
SESSIONS_DIR,
|
|
17
|
-
SERVICE_NAME,
|
|
18
17
|
fileExists,
|
|
19
18
|
loadConfig,
|
|
20
19
|
resolveEnvFilePath,
|
|
@@ -22,14 +21,7 @@ import {
|
|
|
22
21
|
import { getAgentSdkPackage } from "../agents/registry";
|
|
23
22
|
import { threadIdToDir } from "../util";
|
|
24
23
|
import { parseEnvFile, unquoteEnvValue } from "./env-file";
|
|
25
|
-
import {
|
|
26
|
-
SERVICE_PATH,
|
|
27
|
-
systemctl,
|
|
28
|
-
runSudo,
|
|
29
|
-
isServiceInstalled,
|
|
30
|
-
isServiceActive,
|
|
31
|
-
systemctlShow,
|
|
32
|
-
} from "./systemd";
|
|
24
|
+
import { getServiceManager } from "./service-manager";
|
|
33
25
|
|
|
34
26
|
const __filename = fileURLToPath(import.meta.url);
|
|
35
27
|
const __dirname = dirname(__filename);
|
|
@@ -41,7 +33,7 @@ const __dirname = dirname(__filename);
|
|
|
41
33
|
* Only call with trusted/hardcoded strings. Any dynamic segments must be
|
|
42
34
|
* validated (e.g. `/^\d+$/.test(pid)`) before interpolation.
|
|
43
35
|
*/
|
|
44
|
-
function
|
|
36
|
+
function shellExec(cmd: string, opts?: { silent?: boolean }): string {
|
|
45
37
|
try {
|
|
46
38
|
const out = execSync(cmd, { encoding: "utf8", stdio: opts?.silent ? "pipe" : "inherit" });
|
|
47
39
|
return (out ?? "").trim();
|
|
@@ -54,31 +46,32 @@ function run(cmd: string, opts?: { silent?: boolean }): string {
|
|
|
54
46
|
// ── Commands ────────────────────────────────────────
|
|
55
47
|
|
|
56
48
|
async function cmdStart() {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
49
|
+
const svc = getServiceManager();
|
|
50
|
+
const result = await svc.start();
|
|
51
|
+
|
|
52
|
+
if (result.message === "no-service") {
|
|
53
|
+
// No service installed — fall back to foreground
|
|
54
|
+
if (!(await fileExists(CONFIG_PATH))) {
|
|
55
|
+
console.error("No config found. Run 'roundhouse setup --telegram' first.");
|
|
56
|
+
process.exit(1);
|
|
64
57
|
}
|
|
65
|
-
|
|
58
|
+
console.log("No service found. Running in foreground (use Ctrl+C to stop)...");
|
|
59
|
+
if (process.platform !== "darwin") {
|
|
60
|
+
console.log(" Tip: run 'roundhouse setup --telegram' to install as systemd daemon.\n");
|
|
61
|
+
} else {
|
|
62
|
+
console.log("");
|
|
63
|
+
}
|
|
64
|
+
await cmdRun();
|
|
66
65
|
return;
|
|
67
66
|
}
|
|
68
67
|
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
console.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
console.log("No systemd service found. Running in foreground (use Ctrl+C to stop)...");
|
|
76
|
-
if (process.platform !== "darwin") {
|
|
77
|
-
console.log(" Tip: run 'roundhouse setup --telegram' to install as systemd daemon.\n");
|
|
78
|
-
} else {
|
|
79
|
-
console.log("");
|
|
68
|
+
console.log(result.message);
|
|
69
|
+
if (result.started && process.platform === "darwin") {
|
|
70
|
+
console.log(" Logs: ~/.roundhouse/logs/roundhouse.log");
|
|
71
|
+
} else if (!result.started) {
|
|
72
|
+
console.log(" Logs: roundhouse logs");
|
|
73
|
+
console.log(" Stop: roundhouse stop");
|
|
80
74
|
}
|
|
81
|
-
await cmdRun();
|
|
82
75
|
}
|
|
83
76
|
|
|
84
77
|
async function cmdRun() {
|
|
@@ -105,7 +98,7 @@ async function cmdRun() {
|
|
|
105
98
|
env: {
|
|
106
99
|
...process.env,
|
|
107
100
|
ROUNDHOUSE_CONFIG: CONFIG_PATH,
|
|
108
|
-
NODE_NO_WARNINGS: "1",
|
|
101
|
+
NODE_NO_WARNINGS: "1",
|
|
109
102
|
},
|
|
110
103
|
});
|
|
111
104
|
}
|
|
@@ -146,14 +139,11 @@ async function cmdInstall() {
|
|
|
146
139
|
console.log(" This sets up config, installs packages, pairs Telegram,");
|
|
147
140
|
console.log(" and installs the systemd service — all in one command.\n");
|
|
148
141
|
}
|
|
149
|
-
async function cmdUninstall() {
|
|
150
142
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
runSudo("systemctl", "daemon-reload");
|
|
156
|
-
console.log(" ✅ Daemon removed. Config preserved at:", CONFIG_PATH);
|
|
143
|
+
async function cmdUninstall() {
|
|
144
|
+
const svc = getServiceManager();
|
|
145
|
+
const result = await svc.uninstall();
|
|
146
|
+
console.log(` ✅ ${result.message} Config preserved at:`, CONFIG_PATH);
|
|
157
147
|
}
|
|
158
148
|
|
|
159
149
|
async function cmdUpdate() {
|
|
@@ -167,12 +157,16 @@ async function cmdUpdate() {
|
|
|
167
157
|
|
|
168
158
|
console.log(`[roundhouse] Updated to v${result.latestVersion}`);
|
|
169
159
|
|
|
170
|
-
|
|
160
|
+
const svc = getServiceManager();
|
|
161
|
+
const status = await svc.status();
|
|
162
|
+
|
|
163
|
+
if (!status.installed) {
|
|
171
164
|
console.log("\n ✅ Update complete. Restart with: roundhouse start");
|
|
172
165
|
} else {
|
|
173
|
-
console.log("\n[roundhouse] Restarting
|
|
166
|
+
console.log("\n[roundhouse] Restarting service...");
|
|
174
167
|
try {
|
|
175
|
-
|
|
168
|
+
const restartResult = await svc.restart();
|
|
169
|
+
console.log(` ✅ ${restartResult.message}`);
|
|
176
170
|
} catch {
|
|
177
171
|
console.log(" ⚠️ Could not restart. Run: roundhouse start");
|
|
178
172
|
}
|
|
@@ -180,24 +174,34 @@ async function cmdUpdate() {
|
|
|
180
174
|
}
|
|
181
175
|
|
|
182
176
|
async function cmdStatus() {
|
|
183
|
-
|
|
184
|
-
|
|
177
|
+
const svc = getServiceManager();
|
|
178
|
+
const svcStatus = await svc.status();
|
|
179
|
+
|
|
180
|
+
if (!svcStatus.running) {
|
|
181
|
+
const icon = svcStatus.installed ? "⚠️" : "❌";
|
|
182
|
+
console.log(`\n ${icon} ${svcStatus.message}\n`);
|
|
185
183
|
console.log(" Start with: roundhouse start\n");
|
|
186
184
|
return;
|
|
187
185
|
}
|
|
188
186
|
|
|
189
|
-
//
|
|
187
|
+
// macOS: simple status
|
|
188
|
+
if (process.platform === "darwin") {
|
|
189
|
+
console.log("\n ✅ Roundhouse is running (LaunchAgent).\n");
|
|
190
|
+
console.log(" Logs: ~/.roundhouse/logs/roundhouse.log");
|
|
191
|
+
console.log(" Stop: roundhouse stop\n");
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Linux: detailed systemd status
|
|
196
|
+
const { systemctlShow } = await import("./systemd");
|
|
197
|
+
|
|
190
198
|
let config: Awaited<ReturnType<typeof loadConfig>> | null = null;
|
|
191
|
-
try {
|
|
192
|
-
config = await loadConfig();
|
|
193
|
-
} catch {}
|
|
199
|
+
try { config = await loadConfig(); } catch {}
|
|
194
200
|
|
|
195
|
-
// Gather systemd info
|
|
196
201
|
const pid = systemctlShow("MainPID");
|
|
197
202
|
const activeState = systemctlShow("ActiveState");
|
|
198
203
|
const startedAt = systemctlShow("ActiveEnterTimestamp");
|
|
199
204
|
|
|
200
|
-
// Compute uptime
|
|
201
205
|
let uptimeStr = "unknown";
|
|
202
206
|
if (startedAt) {
|
|
203
207
|
const startMs = new Date(startedAt).getTime();
|
|
@@ -208,17 +212,15 @@ async function cmdStatus() {
|
|
|
208
212
|
}
|
|
209
213
|
}
|
|
210
214
|
|
|
211
|
-
// Memory from PID
|
|
212
215
|
let memStr = "unknown";
|
|
213
216
|
if (pid && pid !== "0" && /^\d+$/.test(pid)) {
|
|
214
|
-
const rssKb =
|
|
217
|
+
const rssKb = shellExec(`ps -o rss= -p ${pid}`, { silent: true }).trim();
|
|
215
218
|
if (rssKb) {
|
|
216
219
|
const parsed = parseInt(rssKb, 10);
|
|
217
220
|
if (!isNaN(parsed)) memStr = `${(parsed / 1024).toFixed(1)} MB`;
|
|
218
221
|
}
|
|
219
222
|
}
|
|
220
223
|
|
|
221
|
-
// Read env file for debug flags
|
|
222
224
|
let debugStream = false;
|
|
223
225
|
const statusEnvPath = await resolveEnvFilePath();
|
|
224
226
|
try {
|
|
@@ -226,7 +228,6 @@ async function cmdStatus() {
|
|
|
226
228
|
debugStream = envContent.includes("ROUNDHOUSE_DEBUG_STREAM=1") || envContent.includes('ROUNDHOUSE_DEBUG_STREAM="1"');
|
|
227
229
|
} catch {}
|
|
228
230
|
|
|
229
|
-
// Read versions
|
|
230
231
|
let roundhouseVersion = "unknown";
|
|
231
232
|
let agentVersion = "unknown";
|
|
232
233
|
try {
|
|
@@ -235,7 +236,6 @@ async function cmdStatus() {
|
|
|
235
236
|
roundhouseVersion = pkg.version;
|
|
236
237
|
} catch {}
|
|
237
238
|
|
|
238
|
-
// Resolve agent SDK version from registry
|
|
239
239
|
const agentPkg = config ? getAgentSdkPackage(config.agent.type) : undefined;
|
|
240
240
|
if (agentPkg) {
|
|
241
241
|
try {
|
|
@@ -268,15 +268,22 @@ async function cmdStatus() {
|
|
|
268
268
|
console.log();
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
-
function
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
271
|
+
async function cmdStop() {
|
|
272
|
+
const svc = getServiceManager();
|
|
273
|
+
const result = await svc.stop();
|
|
274
|
+
console.log(result.message);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function cmdRestart() {
|
|
278
|
+
const svc = getServiceManager();
|
|
279
|
+
const result = await svc.restart();
|
|
280
|
+
console.log(result.message);
|
|
276
281
|
}
|
|
277
282
|
|
|
278
|
-
function
|
|
279
|
-
|
|
283
|
+
async function cmdLogs() {
|
|
284
|
+
const svc = getServiceManager();
|
|
285
|
+
svc.logs();
|
|
286
|
+
}
|
|
280
287
|
|
|
281
288
|
async function cmdConfig() {
|
|
282
289
|
console.log(`Config path: ${CONFIG_PATH}\n`);
|
|
@@ -371,162 +378,9 @@ Environment:
|
|
|
371
378
|
|
|
372
379
|
// ── Main ────────────────────────────────────────────
|
|
373
380
|
|
|
374
|
-
async function cmdAgent() {
|
|
375
|
-
// Usage: roundhouse agent <message>
|
|
376
|
-
// roundhouse agent --thread <id> <message>
|
|
377
|
-
// roundhouse agent --ephemeral <message>
|
|
378
|
-
// echo "message" | roundhouse agent --stdin
|
|
379
|
-
const args = process.argv.slice(3);
|
|
380
|
-
let threadId = "";
|
|
381
|
-
let messageText = "";
|
|
382
|
-
let useStdin = false;
|
|
383
|
-
let timeoutMs = 120_000;
|
|
384
|
-
let verbose = false;
|
|
385
|
-
let ephemeral = false;
|
|
386
|
-
|
|
387
|
-
for (let i = 0; i < args.length; i++) {
|
|
388
|
-
if (args[i] === "--thread" && args[i + 1]) {
|
|
389
|
-
threadId = args[++i];
|
|
390
|
-
} else if (args[i] === "--stdin") {
|
|
391
|
-
useStdin = true;
|
|
392
|
-
} else if (args[i] === "--timeout" && args[i + 1]) {
|
|
393
|
-
const val = parseInt(args[++i], 10);
|
|
394
|
-
if (isNaN(val) || val <= 0) { console.error("--timeout must be a positive number (seconds)"); process.exit(1); }
|
|
395
|
-
timeoutMs = val * 1000;
|
|
396
|
-
} else if (args[i] === "--no-timeout") {
|
|
397
|
-
timeoutMs = 0;
|
|
398
|
-
} else if (args[i] === "--verbose") {
|
|
399
|
-
verbose = true;
|
|
400
|
-
} else if (args[i] === "--ephemeral") {
|
|
401
|
-
ephemeral = true;
|
|
402
|
-
} else if (args[i].startsWith("-")) {
|
|
403
|
-
console.error(`Unknown flag: ${args[i]}`);
|
|
404
|
-
process.exit(1);
|
|
405
|
-
} else {
|
|
406
|
-
messageText = args.slice(i).join(" ");
|
|
407
|
-
break;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
if (useStdin) {
|
|
412
|
-
const chunks: Buffer[] = [];
|
|
413
|
-
let totalBytes = 0;
|
|
414
|
-
const MAX_INPUT = 1024 * 1024; // 1 MB
|
|
415
|
-
for await (const chunk of process.stdin) {
|
|
416
|
-
totalBytes += chunk.length;
|
|
417
|
-
if (totalBytes > MAX_INPUT) {
|
|
418
|
-
console.error(`Input exceeds ${MAX_INPUT / 1024}KB limit. Use a file instead.`);
|
|
419
|
-
process.exit(1);
|
|
420
|
-
}
|
|
421
|
-
chunks.push(chunk);
|
|
422
|
-
}
|
|
423
|
-
// Strip single trailing newline (shell echo adds one)
|
|
424
|
-
let raw = Buffer.concat(chunks).toString("utf8");
|
|
425
|
-
if (raw.endsWith("\n")) raw = raw.slice(0, -1);
|
|
426
|
-
messageText = raw;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
if (!messageText) {
|
|
430
|
-
console.error("Usage: roundhouse agent <message>");
|
|
431
|
-
console.error(" roundhouse agent --thread <id> <message>");
|
|
432
|
-
console.error(" echo \"message\" | roundhouse agent --stdin");
|
|
433
|
-
console.error(" roundhouse agent --timeout 60 <message>");
|
|
434
|
-
console.error(" roundhouse agent --verbose <message>");
|
|
435
|
-
console.error(" roundhouse agent --ephemeral <message>");
|
|
436
|
-
process.exit(1);
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
if (threadId && ephemeral) {
|
|
440
|
-
console.error("--thread and --ephemeral cannot be used together");
|
|
441
|
-
process.exit(1);
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
// Default: shared main session. --ephemeral restores one-off CLI behavior.
|
|
445
|
-
if (!threadId) {
|
|
446
|
-
threadId = ephemeral
|
|
447
|
-
? `cli-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
|
448
|
-
: "main";
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// Suppress debug/info logs unless --verbose
|
|
452
|
-
const origLog = console.log;
|
|
453
|
-
const origWarn = console.warn;
|
|
454
|
-
const origError = console.error;
|
|
455
|
-
if (!verbose) {
|
|
456
|
-
console.log = () => {};
|
|
457
|
-
console.warn = () => {};
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
let agent: import("../types").AgentAdapter | undefined;
|
|
461
|
-
let aborted = false;
|
|
462
|
-
|
|
463
|
-
// Clean abort on SIGINT/SIGTERM
|
|
464
|
-
const handleSignal = async () => {
|
|
465
|
-
if (aborted) return;
|
|
466
|
-
aborted = true;
|
|
467
|
-
console.log = origLog;
|
|
468
|
-
console.warn = origWarn;
|
|
469
|
-
console.error = origError;
|
|
470
|
-
try { await agent?.abort?.(threadId); } catch {}
|
|
471
|
-
try { await agent?.dispose(); } catch {}
|
|
472
|
-
process.exit(130);
|
|
473
|
-
};
|
|
474
|
-
process.on("SIGINT", handleSignal);
|
|
475
|
-
process.on("SIGTERM", handleSignal);
|
|
476
|
-
|
|
477
|
-
// Timeout race
|
|
478
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
479
|
-
const timeoutPromise = timeoutMs > 0
|
|
480
|
-
? new Promise<never>((_, reject) => {
|
|
481
|
-
timer = setTimeout(async () => {
|
|
482
|
-
aborted = true;
|
|
483
|
-
try { await agent?.abort?.(threadId); } catch {}
|
|
484
|
-
reject(new Error(`Timeout after ${timeoutMs / 1000}s`));
|
|
485
|
-
}, timeoutMs);
|
|
486
|
-
})
|
|
487
|
-
: null;
|
|
488
|
-
|
|
489
|
-
try {
|
|
490
|
-
const config = await loadConfig();
|
|
491
|
-
const { getAgentFactory } = await import("../agents/registry");
|
|
492
|
-
const factory = getAgentFactory(config.agent.type);
|
|
493
|
-
agent = factory(config.agent);
|
|
494
|
-
|
|
495
|
-
const runAgent = async () => {
|
|
496
|
-
if (agent!.promptStream) {
|
|
497
|
-
for await (const event of agent!.promptStream(threadId, { text: messageText })) {
|
|
498
|
-
if (event.type === "text_delta") {
|
|
499
|
-
process.stdout.write(event.text);
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
process.stdout.write("\n");
|
|
503
|
-
} else {
|
|
504
|
-
const response = await agent!.prompt(threadId, { text: messageText });
|
|
505
|
-
origLog(response.text);
|
|
506
|
-
}
|
|
507
|
-
};
|
|
508
|
-
|
|
509
|
-
if (timeoutPromise) {
|
|
510
|
-
await Promise.race([runAgent(), timeoutPromise]);
|
|
511
|
-
} else {
|
|
512
|
-
await runAgent();
|
|
513
|
-
}
|
|
514
|
-
} catch (err: any) {
|
|
515
|
-
console.error = origError;
|
|
516
|
-
console.error(`Error: ${err.message}`);
|
|
517
|
-
process.exit(aborted ? 124 : 1); // 124 = timeout (like coreutils)
|
|
518
|
-
} finally {
|
|
519
|
-
if (timer) clearTimeout(timer);
|
|
520
|
-
process.off("SIGINT", handleSignal);
|
|
521
|
-
process.off("SIGTERM", handleSignal);
|
|
522
|
-
console.log = origLog;
|
|
523
|
-
console.warn = origWarn;
|
|
524
|
-
console.error = origError;
|
|
525
|
-
if (!aborted) await agent?.dispose();
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
381
|
|
|
529
382
|
import { cmdDoctor } from "./doctor";
|
|
383
|
+
import { cmdAgent } from "./agent-command";
|
|
530
384
|
import { cmdCron } from "./cron";
|
|
531
385
|
import { cmdSetup, cmdPair } from "./setup";
|
|
532
386
|
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli/cron-commands.ts — Individual cron subcommand handlers
|
|
3
|
+
*
|
|
4
|
+
* Each handler receives store, positional args, and flags.
|
|
5
|
+
* Extracted from the monolithic cmdCron switch statement.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { CronStore, validateJobId } from "../cron/store";
|
|
9
|
+
import { isBuiltinJob } from "../cron/helpers";
|
|
10
|
+
import { CronRunner } from "../cron/runner";
|
|
11
|
+
import { validateSchedule } from "../cron/schedule";
|
|
12
|
+
import { validateTemplate } from "../cron/template";
|
|
13
|
+
import { parseDuration } from "../cron/durations";
|
|
14
|
+
import type { CronJobConfig, CronSchedule } from "../cron/types";
|
|
15
|
+
import { DEFAULT_TIMEOUT_MS, DEFAULT_TIMEZONE, VALID_NOTIFY_ON, DEFAULT_RUNS_LIMIT } from "../cron/constants";
|
|
16
|
+
import { formatSchedule, formatRunCounts, formatJobSummary, formatJobDetail, formatRunLine, runStatusIcon, jobEnabledIcon } from "../cron/format";
|
|
17
|
+
|
|
18
|
+
function rejectBuiltin(id: string): void {
|
|
19
|
+
if (isBuiltinJob(id)) {
|
|
20
|
+
console.error(`Job ID "${id}" is reserved for built-in jobs.`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function validateNotifyOn(value?: string): "always" | "success" | "failure" {
|
|
26
|
+
const v = value ?? "always";
|
|
27
|
+
if (!(VALID_NOTIFY_ON as readonly string[]).includes(v)) {
|
|
28
|
+
console.error(`Invalid --notify-on: "${v}". Use ${VALID_NOTIFY_ON.join(", ")}.`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
return v as "always" | "success" | "failure";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function cronAdd(store: CronStore, positional: string[], flags: Record<string, string>): Promise<void> {
|
|
35
|
+
const id = positional[1];
|
|
36
|
+
if (!id) { console.error("Usage: roundhouse cron add <id> --prompt '...' --cron '...' --tz '...'"); process.exit(1); }
|
|
37
|
+
validateJobId(id);
|
|
38
|
+
rejectBuiltin(id);
|
|
39
|
+
|
|
40
|
+
const existing = await store.getJob(id);
|
|
41
|
+
if (existing && !flags.replace) {
|
|
42
|
+
console.error(`Job "${id}" already exists. Use --replace to overwrite.`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const prompt = flags.prompt;
|
|
47
|
+
if (!prompt) { console.error("--prompt is required"); process.exit(1); }
|
|
48
|
+
|
|
49
|
+
const schedCount = [flags.cron, flags.every, flags.at].filter(Boolean).length;
|
|
50
|
+
if (schedCount > 1) { console.error("Specify only one of --cron, --every, or --at"); process.exit(1); }
|
|
51
|
+
let schedule: CronSchedule;
|
|
52
|
+
if (flags.cron) {
|
|
53
|
+
schedule = { type: "cron", cron: flags.cron, tz: flags.tz ?? DEFAULT_TIMEZONE };
|
|
54
|
+
} else if (flags.every) {
|
|
55
|
+
schedule = { type: "interval", every: flags.every };
|
|
56
|
+
} else if (flags.at) {
|
|
57
|
+
schedule = { type: "once", at: flags.at, tz: flags.tz };
|
|
58
|
+
} else {
|
|
59
|
+
console.error("Schedule required: --cron '...', --every '...', or --at '...'");
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
validateSchedule(schedule);
|
|
64
|
+
|
|
65
|
+
const vars: Record<string, string> = {};
|
|
66
|
+
if (flags.var) {
|
|
67
|
+
for (const v of flags.var.split(",")) {
|
|
68
|
+
const [k, ...rest] = v.split("=");
|
|
69
|
+
if (k && rest.length) vars[k.trim()] = rest.join("=").trim();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const templateErrors = validateTemplate(prompt, new Set(Object.keys(vars)));
|
|
74
|
+
if (templateErrors.length) {
|
|
75
|
+
console.error("Template errors:");
|
|
76
|
+
templateErrors.forEach((e) => console.error(` ${e}`));
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const notify: CronJobConfig["notify"] = {};
|
|
81
|
+
if (flags.telegram) {
|
|
82
|
+
notify.telegram = {
|
|
83
|
+
chatIds: flags.telegram.split(",").map((s) => s.trim()),
|
|
84
|
+
onlyOn: validateNotifyOn(flags["notify-on"]),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const now = new Date().toISOString();
|
|
89
|
+
const job: CronJobConfig = {
|
|
90
|
+
id,
|
|
91
|
+
enabled: true,
|
|
92
|
+
description: flags.description,
|
|
93
|
+
createdAt: existing?.createdAt ?? now,
|
|
94
|
+
updatedAt: now,
|
|
95
|
+
schedule,
|
|
96
|
+
prompt,
|
|
97
|
+
vars: Object.keys(vars).length ? vars : undefined,
|
|
98
|
+
timeoutMs: flags.timeout ? parseDuration(flags.timeout) : undefined,
|
|
99
|
+
notify: Object.keys(notify).length ? notify : undefined,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
await store.writeJob(job);
|
|
103
|
+
console.log(`✅ Cron job "${id}" ${existing ? "updated" : "created"}.`);
|
|
104
|
+
if (flags.json) console.log(JSON.stringify(job, null, 2));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function cronList(store: CronStore, _positional: string[], flags: Record<string, string>): Promise<void> {
|
|
108
|
+
const jobs = await store.listJobs();
|
|
109
|
+
if (jobs.length === 0) {
|
|
110
|
+
console.log("No cron jobs configured.");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (flags.json) {
|
|
114
|
+
console.log(JSON.stringify(jobs, null, 2));
|
|
115
|
+
} else {
|
|
116
|
+
for (const j of jobs) {
|
|
117
|
+
const state = await store.getState(j.id);
|
|
118
|
+
console.log(` ${formatJobSummary(j, state)}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function cronShow(store: CronStore, positional: string[], flags: Record<string, string>): Promise<void> {
|
|
124
|
+
const id = positional[1];
|
|
125
|
+
if (!id) { console.error("Usage: roundhouse cron show <id>"); process.exit(1); }
|
|
126
|
+
const job = await store.getJob(id);
|
|
127
|
+
if (!job) { console.error(`Job not found: ${id}`); process.exit(1); }
|
|
128
|
+
const state = await store.getState(id);
|
|
129
|
+
const runs = await store.listRuns(id, 5);
|
|
130
|
+
if (flags.json) {
|
|
131
|
+
console.log(JSON.stringify({ job, state, recentRuns: runs }, null, 2));
|
|
132
|
+
} else {
|
|
133
|
+
console.log(`\n${formatJobDetail(job, state, runs)}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function cronTrigger(store: CronStore, positional: string[], _flags: Record<string, string>): Promise<void> {
|
|
138
|
+
const id = positional[1];
|
|
139
|
+
if (!id) { console.error("Usage: roundhouse cron trigger <id>"); process.exit(1); }
|
|
140
|
+
rejectBuiltin(id);
|
|
141
|
+
const job = await store.getJob(id);
|
|
142
|
+
if (!job) { console.error(`Job not found: ${id}`); process.exit(1); }
|
|
143
|
+
console.log(`Triggering ${id}...`);
|
|
144
|
+
const runner = new CronRunner(store);
|
|
145
|
+
const record = await runner.runJob(job, new Date(), "manual");
|
|
146
|
+
console.log(`\nResult: ${record.status} (${record.durationMs}ms)`);
|
|
147
|
+
if (record.responseText) console.log(`\n${record.responseText.slice(0, 2000)}`);
|
|
148
|
+
if (record.error) console.log(`\nError: ${record.error}`);
|
|
149
|
+
process.exit(record.status === "completed" ? 0 : 1);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function cronRuns(store: CronStore, positional: string[], flags: Record<string, string>): Promise<void> {
|
|
153
|
+
const id = positional[1];
|
|
154
|
+
if (!id) { console.error("Usage: roundhouse cron runs <id>"); process.exit(1); }
|
|
155
|
+
const runs = await store.listRuns(id, parseInt(flags.limit ?? String(DEFAULT_RUNS_LIMIT), 10));
|
|
156
|
+
if (runs.length === 0) {
|
|
157
|
+
console.log(`No runs for ${id}.`);
|
|
158
|
+
} else if (flags.json) {
|
|
159
|
+
console.log(JSON.stringify(runs, null, 2));
|
|
160
|
+
} else {
|
|
161
|
+
for (const r of runs) {
|
|
162
|
+
console.log(` ${formatRunLine(r)} (${r.kind})`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function cronPause(store: CronStore, positional: string[], _flags: Record<string, string>): Promise<void> {
|
|
168
|
+
const id = positional[1];
|
|
169
|
+
if (!id) { console.error("Usage: roundhouse cron pause <id>"); process.exit(1); }
|
|
170
|
+
rejectBuiltin(id);
|
|
171
|
+
const job = await store.getJob(id);
|
|
172
|
+
if (!job) { console.error(`Job not found: ${id}`); process.exit(1); }
|
|
173
|
+
job.enabled = false;
|
|
174
|
+
job.updatedAt = new Date().toISOString();
|
|
175
|
+
await store.writeJob(job);
|
|
176
|
+
console.log(`⏸️ Job "${id}" paused.`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function cronResume(store: CronStore, positional: string[], _flags: Record<string, string>): Promise<void> {
|
|
180
|
+
const id = positional[1];
|
|
181
|
+
if (!id) { console.error("Usage: roundhouse cron resume <id>"); process.exit(1); }
|
|
182
|
+
rejectBuiltin(id);
|
|
183
|
+
const job = await store.getJob(id);
|
|
184
|
+
if (!job) { console.error(`Job not found: ${id}`); process.exit(1); }
|
|
185
|
+
job.enabled = true;
|
|
186
|
+
job.updatedAt = new Date().toISOString();
|
|
187
|
+
await store.writeJob(job);
|
|
188
|
+
console.log(`▶️ Job "${id}" resumed.`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export async function cronEdit(store: CronStore, positional: string[], flags: Record<string, string>): Promise<void> {
|
|
192
|
+
const id = positional[1];
|
|
193
|
+
if (!id) { console.error("Usage: roundhouse cron edit <id> [--prompt '...'] [--cron '...'] ..."); process.exit(1); }
|
|
194
|
+
rejectBuiltin(id);
|
|
195
|
+
const job = await store.getJob(id);
|
|
196
|
+
if (!job) { console.error(`Job not found: ${id}`); process.exit(1); }
|
|
197
|
+
|
|
198
|
+
if (flags.prompt) job.prompt = flags.prompt;
|
|
199
|
+
if (flags.description) job.description = flags.description;
|
|
200
|
+
if (flags.timeout) job.timeoutMs = parseDuration(flags.timeout);
|
|
201
|
+
const editSchedCount = [flags.cron, flags.every, flags.at].filter(Boolean).length;
|
|
202
|
+
if (editSchedCount > 1) { console.error("Specify only one of --cron, --every, or --at"); process.exit(1); }
|
|
203
|
+
if (flags.cron) job.schedule = { type: "cron", cron: flags.cron, tz: flags.tz ?? (job.schedule.type === "cron" ? job.schedule.tz : DEFAULT_TIMEZONE) };
|
|
204
|
+
if (flags.every) job.schedule = { type: "interval", every: flags.every };
|
|
205
|
+
if (flags.at) job.schedule = { type: "once", at: flags.at, tz: flags.tz };
|
|
206
|
+
if (flags.telegram) {
|
|
207
|
+
job.notify = { ...job.notify, telegram: { chatIds: flags.telegram.split(",").map((s) => s.trim()), onlyOn: validateNotifyOn(flags["notify-on"]) } };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
validateSchedule(job.schedule);
|
|
211
|
+
const editVars = new Set(Object.keys(job.vars ?? {}));
|
|
212
|
+
const editErrors = validateTemplate(job.prompt, editVars);
|
|
213
|
+
if (editErrors.length) {
|
|
214
|
+
console.error("Template errors:");
|
|
215
|
+
editErrors.forEach((e) => console.error(` ${e}`));
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
job.updatedAt = new Date().toISOString();
|
|
219
|
+
await store.writeJob(job);
|
|
220
|
+
console.log(`✅ Job "${id}" updated.`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export async function cronDelete(store: CronStore, positional: string[], _flags: Record<string, string>): Promise<void> {
|
|
224
|
+
const id = positional[1];
|
|
225
|
+
if (!id) { console.error("Usage: roundhouse cron delete <id>"); process.exit(1); }
|
|
226
|
+
rejectBuiltin(id);
|
|
227
|
+
const job = await store.getJob(id);
|
|
228
|
+
if (!job) { console.error(`Job not found: ${id}`); process.exit(1); }
|
|
229
|
+
await store.deleteJob(id);
|
|
230
|
+
console.log(`🗑️ Job "${id}" deleted.`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function cronHelp(): void {
|
|
234
|
+
console.log(`roundhouse cron <command>
|
|
235
|
+
|
|
236
|
+
Commands:
|
|
237
|
+
add <id> [flags] Create a cron job
|
|
238
|
+
list List all jobs
|
|
239
|
+
show <id> Show job details
|
|
240
|
+
trigger <id> Run job now
|
|
241
|
+
runs <id> Show run history
|
|
242
|
+
edit <id> [flags] Edit a job
|
|
243
|
+
pause <id> Disable a job
|
|
244
|
+
resume <id> Enable a job
|
|
245
|
+
delete <id> Delete a job
|
|
246
|
+
|
|
247
|
+
Flags for add/edit:
|
|
248
|
+
--prompt "..." Prompt template (required for add)
|
|
249
|
+
--cron "..." Cron expression (e.g. "0 8 * * *")
|
|
250
|
+
--every "..." Interval (e.g. "6h")
|
|
251
|
+
--at "..." One-shot time (e.g. "30m" or ISO date)
|
|
252
|
+
--tz "..." Timezone (e.g. "Asia/Jerusalem")
|
|
253
|
+
--telegram "..." Telegram chat IDs (comma-separated)
|
|
254
|
+
--var "k=v,..." Template variables (comma-separated)
|
|
255
|
+
--timeout "..." Timeout (e.g. "30m")
|
|
256
|
+
--description "..." Job description
|
|
257
|
+
--json JSON output`);
|
|
258
|
+
}
|