agentflow-core 0.2.3 → 0.3.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/dist/cli.cjs CHANGED
@@ -1109,6 +1109,522 @@ function startLive(argv) {
1109
1109
  });
1110
1110
  }
1111
1111
 
1112
+ // src/watch.ts
1113
+ var import_node_fs4 = require("fs");
1114
+ var import_node_path3 = require("path");
1115
+ var import_node_os = require("os");
1116
+
1117
+ // src/watch-state.ts
1118
+ var import_node_fs3 = require("fs");
1119
+ function parseDuration(input) {
1120
+ const match = input.match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)$/i);
1121
+ if (!match) {
1122
+ const n = parseInt(input, 10);
1123
+ return isNaN(n) ? 0 : n * 1e3;
1124
+ }
1125
+ const value = parseFloat(match[1]);
1126
+ switch (match[2].toLowerCase()) {
1127
+ case "s":
1128
+ return value * 1e3;
1129
+ case "m":
1130
+ return value * 6e4;
1131
+ case "h":
1132
+ return value * 36e5;
1133
+ case "d":
1134
+ return value * 864e5;
1135
+ default:
1136
+ return value * 1e3;
1137
+ }
1138
+ }
1139
+ function emptyState() {
1140
+ return { version: 1, agents: {}, lastPollTime: 0 };
1141
+ }
1142
+ function loadWatchState(filePath) {
1143
+ if (!(0, import_node_fs3.existsSync)(filePath)) return emptyState();
1144
+ try {
1145
+ const raw = JSON.parse((0, import_node_fs3.readFileSync)(filePath, "utf8"));
1146
+ if (raw.version !== 1 || typeof raw.agents !== "object") return emptyState();
1147
+ return raw;
1148
+ } catch {
1149
+ return emptyState();
1150
+ }
1151
+ }
1152
+ function saveWatchState(filePath, state) {
1153
+ const tmp = filePath + ".tmp";
1154
+ try {
1155
+ (0, import_node_fs3.writeFileSync)(tmp, JSON.stringify(state, null, 2), "utf8");
1156
+ (0, import_node_fs3.renameSync)(tmp, filePath);
1157
+ } catch {
1158
+ try {
1159
+ (0, import_node_fs3.writeFileSync)(filePath, JSON.stringify(state, null, 2), "utf8");
1160
+ } catch {
1161
+ }
1162
+ }
1163
+ }
1164
+ function estimateInterval(history) {
1165
+ if (history.length < 3) return 0;
1166
+ const sorted = [...history].sort((a, b) => a - b);
1167
+ const deltas = [];
1168
+ for (let i = 1; i < sorted.length; i++) {
1169
+ const d = sorted[i] - sorted[i - 1];
1170
+ if (d > 0) deltas.push(d);
1171
+ }
1172
+ if (deltas.length === 0) return 0;
1173
+ deltas.sort((a, b) => a - b);
1174
+ return deltas[Math.floor(deltas.length / 2)];
1175
+ }
1176
+ function detectTransitions(previous, currentRecords, config, now) {
1177
+ const alerts = [];
1178
+ const hasError = config.alertConditions.some((c) => c.type === "error");
1179
+ const hasRecovery = config.alertConditions.some((c) => c.type === "recovery");
1180
+ const staleConditions = config.alertConditions.filter((c) => c.type === "stale");
1181
+ const consecutiveConditions = config.alertConditions.filter((c) => c.type === "consecutive-errors");
1182
+ const byAgent = /* @__PURE__ */ new Map();
1183
+ for (const r of currentRecords) {
1184
+ const existing = byAgent.get(r.id);
1185
+ if (!existing || r.lastActive > existing.lastActive) {
1186
+ byAgent.set(r.id, r);
1187
+ }
1188
+ }
1189
+ for (const [agentId, record] of byAgent) {
1190
+ const prev = previous.agents[agentId];
1191
+ const prevStatus = prev?.lastStatus ?? "unknown";
1192
+ const currStatus = record.status;
1193
+ if (hasError && currStatus === "error" && prevStatus !== "error") {
1194
+ if (canAlert(prev, "error", config.cooldownMs, now)) {
1195
+ alerts.push(makePayload(agentId, "error", prevStatus, currStatus, record, config.dirs));
1196
+ }
1197
+ }
1198
+ if (hasRecovery && currStatus === "ok" && prevStatus === "error") {
1199
+ alerts.push(makePayload(agentId, "recovery", prevStatus, currStatus, record, config.dirs));
1200
+ }
1201
+ const newConsec = currStatus === "error" ? (prev?.consecutiveErrors ?? 0) + 1 : 0;
1202
+ for (const cond of consecutiveConditions) {
1203
+ if (newConsec === cond.threshold) {
1204
+ if (canAlert(prev, `consecutive-errors:${cond.threshold}`, config.cooldownMs, now)) {
1205
+ alerts.push(makePayload(
1206
+ agentId,
1207
+ `consecutive-errors (${cond.threshold})`,
1208
+ prevStatus,
1209
+ currStatus,
1210
+ { ...record, detail: `${newConsec} consecutive errors. ${record.detail}` },
1211
+ config.dirs
1212
+ ));
1213
+ }
1214
+ }
1215
+ }
1216
+ for (const cond of staleConditions) {
1217
+ const sinceActive = now - record.lastActive;
1218
+ if (sinceActive > cond.durationMs && record.lastActive > 0) {
1219
+ if (canAlert(prev, "stale", config.cooldownMs, now)) {
1220
+ const mins = Math.floor(sinceActive / 6e4);
1221
+ alerts.push(makePayload(
1222
+ agentId,
1223
+ "stale",
1224
+ prevStatus,
1225
+ currStatus,
1226
+ { ...record, detail: `No update for ${mins}m. ${record.detail}` },
1227
+ config.dirs
1228
+ ));
1229
+ }
1230
+ }
1231
+ }
1232
+ if (staleConditions.length === 0) {
1233
+ const history = prev?.mtimeHistory ?? [];
1234
+ const expectedInterval = estimateInterval(history);
1235
+ if (expectedInterval > 0) {
1236
+ const sinceActive = now - record.lastActive;
1237
+ if (sinceActive > expectedInterval * 3) {
1238
+ if (canAlert(prev, "stale-auto", config.cooldownMs, now)) {
1239
+ const mins = Math.floor(sinceActive / 6e4);
1240
+ const expectedMins = Math.floor(expectedInterval / 6e4);
1241
+ alerts.push(makePayload(
1242
+ agentId,
1243
+ "stale (auto)",
1244
+ prevStatus,
1245
+ currStatus,
1246
+ { ...record, detail: `No update for ${mins}m (expected every ~${expectedMins}m). ${record.detail}` },
1247
+ config.dirs
1248
+ ));
1249
+ }
1250
+ }
1251
+ }
1252
+ }
1253
+ }
1254
+ return alerts;
1255
+ }
1256
+ function updateWatchState(state, records, alerts, now) {
1257
+ const agents = { ...state.agents };
1258
+ const alertsByAgent = /* @__PURE__ */ new Map();
1259
+ for (const a of alerts) alertsByAgent.set(a.agentId, a);
1260
+ const byAgent = /* @__PURE__ */ new Map();
1261
+ for (const r of records) {
1262
+ const existing = byAgent.get(r.id);
1263
+ if (!existing || r.lastActive > existing.lastActive) {
1264
+ byAgent.set(r.id, r);
1265
+ }
1266
+ }
1267
+ for (const [agentId, record] of byAgent) {
1268
+ const prev = agents[agentId];
1269
+ const history = prev?.mtimeHistory ?? [];
1270
+ const newHistory = [...history];
1271
+ if (newHistory.length === 0 || newHistory[newHistory.length - 1] !== record.lastActive) {
1272
+ newHistory.push(record.lastActive);
1273
+ }
1274
+ while (newHistory.length > 10) newHistory.shift();
1275
+ const alert = alertsByAgent.get(agentId);
1276
+ const consecutiveErrors = record.status === "error" ? (prev?.consecutiveErrors ?? 0) + 1 : 0;
1277
+ agents[agentId] = {
1278
+ id: agentId,
1279
+ lastStatus: record.status,
1280
+ lastActive: record.lastActive,
1281
+ lastAlertTime: alert ? now : prev?.lastAlertTime ?? 0,
1282
+ lastAlertReason: alert ? alert.condition : prev?.lastAlertReason ?? "",
1283
+ consecutiveErrors,
1284
+ mtimeHistory: newHistory
1285
+ };
1286
+ }
1287
+ return { version: 1, agents, lastPollTime: now };
1288
+ }
1289
+ function canAlert(prev, reason, cooldownMs, now) {
1290
+ if (!prev) return true;
1291
+ if (prev.lastAlertReason !== reason) return true;
1292
+ return now - prev.lastAlertTime > cooldownMs;
1293
+ }
1294
+ function makePayload(agentId, condition, previousStatus, currentStatus, record, dirs) {
1295
+ return {
1296
+ agentId,
1297
+ condition,
1298
+ previousStatus,
1299
+ currentStatus,
1300
+ detail: record.detail,
1301
+ file: record.file,
1302
+ timestamp: Date.now(),
1303
+ dirs
1304
+ };
1305
+ }
1306
+
1307
+ // src/watch-alerts.ts
1308
+ var import_node_https = require("https");
1309
+ var import_node_http = require("http");
1310
+ var import_node_child_process2 = require("child_process");
1311
+ function formatAlertMessage(payload) {
1312
+ const time = new Date(payload.timestamp).toISOString();
1313
+ const arrow = `${payload.previousStatus} \u2192 ${payload.currentStatus}`;
1314
+ return [
1315
+ `[ALERT] ${payload.condition}: "${payload.agentId}"`,
1316
+ ` Status: ${arrow}`,
1317
+ payload.detail ? ` Detail: ${payload.detail}` : null,
1318
+ ` File: ${payload.file}`,
1319
+ ` Time: ${time}`
1320
+ ].filter(Boolean).join("\n");
1321
+ }
1322
+ function formatTelegram(payload) {
1323
+ const icon = payload.condition === "recovery" ? "\u2705" : "\u26A0\uFE0F";
1324
+ const time = new Date(payload.timestamp).toLocaleTimeString();
1325
+ return [
1326
+ `${icon} *AgentFlow Alert*`,
1327
+ `*${payload.condition}*: \`${payload.agentId}\``,
1328
+ `Status: ${payload.previousStatus} \u2192 ${payload.currentStatus}`,
1329
+ payload.detail ? `Detail: ${payload.detail.slice(0, 200)}` : null,
1330
+ `Time: ${time}`
1331
+ ].filter(Boolean).join("\n");
1332
+ }
1333
+ async function sendAlert(payload, channel) {
1334
+ try {
1335
+ switch (channel.type) {
1336
+ case "stdout":
1337
+ sendStdout(payload);
1338
+ break;
1339
+ case "telegram":
1340
+ await sendTelegram(payload, channel.botToken, channel.chatId);
1341
+ break;
1342
+ case "webhook":
1343
+ await sendWebhook(payload, channel.url);
1344
+ break;
1345
+ case "command":
1346
+ await sendCommand(payload, channel.cmd);
1347
+ break;
1348
+ }
1349
+ } catch (err) {
1350
+ const msg = err instanceof Error ? err.message : String(err);
1351
+ console.error(`[agentflow] Failed to send ${channel.type} alert: ${msg}`);
1352
+ }
1353
+ }
1354
+ function sendStdout(payload) {
1355
+ console.log(formatAlertMessage(payload));
1356
+ }
1357
+ function sendTelegram(payload, botToken, chatId) {
1358
+ const body = JSON.stringify({
1359
+ chat_id: chatId,
1360
+ text: formatTelegram(payload),
1361
+ parse_mode: "Markdown"
1362
+ });
1363
+ return new Promise((resolve5, reject) => {
1364
+ const req = (0, import_node_https.request)(
1365
+ `https://api.telegram.org/bot${botToken}/sendMessage`,
1366
+ { method: "POST", headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) } },
1367
+ (res) => {
1368
+ res.resume();
1369
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) resolve5();
1370
+ else reject(new Error(`Telegram API returned ${res.statusCode}`));
1371
+ }
1372
+ );
1373
+ req.on("error", reject);
1374
+ req.write(body);
1375
+ req.end();
1376
+ });
1377
+ }
1378
+ function sendWebhook(payload, url) {
1379
+ const body = JSON.stringify(payload);
1380
+ const isHttps = url.startsWith("https");
1381
+ const doRequest = isHttps ? import_node_https.request : import_node_http.request;
1382
+ return new Promise((resolve5, reject) => {
1383
+ const req = doRequest(
1384
+ url,
1385
+ { method: "POST", headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) } },
1386
+ (res) => {
1387
+ res.resume();
1388
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) resolve5();
1389
+ else reject(new Error(`Webhook returned ${res.statusCode}`));
1390
+ }
1391
+ );
1392
+ req.on("error", reject);
1393
+ req.setTimeout(1e4, () => {
1394
+ req.destroy(new Error("Webhook timeout"));
1395
+ });
1396
+ req.write(body);
1397
+ req.end();
1398
+ });
1399
+ }
1400
+ function sendCommand(payload, cmd) {
1401
+ return new Promise((resolve5, reject) => {
1402
+ const env = {
1403
+ ...process.env,
1404
+ AGENTFLOW_ALERT_AGENT: payload.agentId,
1405
+ AGENTFLOW_ALERT_CONDITION: payload.condition,
1406
+ AGENTFLOW_ALERT_STATUS: payload.currentStatus,
1407
+ AGENTFLOW_ALERT_PREVIOUS_STATUS: payload.previousStatus,
1408
+ AGENTFLOW_ALERT_DETAIL: payload.detail,
1409
+ AGENTFLOW_ALERT_FILE: payload.file,
1410
+ AGENTFLOW_ALERT_TIMESTAMP: String(payload.timestamp)
1411
+ };
1412
+ (0, import_node_child_process2.exec)(cmd, { env, timeout: 3e4 }, (err) => {
1413
+ if (err) reject(err);
1414
+ else resolve5();
1415
+ });
1416
+ });
1417
+ }
1418
+
1419
+ // src/watch.ts
1420
+ function parseWatchArgs(argv) {
1421
+ const dirs = [];
1422
+ const alertConditions = [];
1423
+ const notifyChannels = [];
1424
+ let recursive = false;
1425
+ let pollIntervalMs = 3e4;
1426
+ let cooldownMs = 30 * 6e4;
1427
+ let stateFilePath = "";
1428
+ const args = argv.slice(0);
1429
+ if (args[0] === "watch") args.shift();
1430
+ let i = 0;
1431
+ while (i < args.length) {
1432
+ const arg = args[i];
1433
+ if (arg === "--help" || arg === "-h") {
1434
+ printWatchUsage();
1435
+ process.exit(0);
1436
+ } else if (arg === "--alert-on") {
1437
+ i++;
1438
+ const val = args[i] ?? "";
1439
+ if (val === "error") {
1440
+ alertConditions.push({ type: "error" });
1441
+ } else if (val === "recovery") {
1442
+ alertConditions.push({ type: "recovery" });
1443
+ } else if (val.startsWith("stale:")) {
1444
+ const dur = parseDuration(val.slice(6));
1445
+ if (dur > 0) alertConditions.push({ type: "stale", durationMs: dur });
1446
+ } else if (val.startsWith("consecutive-errors:")) {
1447
+ const n = parseInt(val.slice(19), 10);
1448
+ if (n > 0) alertConditions.push({ type: "consecutive-errors", threshold: n });
1449
+ }
1450
+ i++;
1451
+ } else if (arg === "--notify") {
1452
+ i++;
1453
+ const val = args[i] ?? "";
1454
+ if (val === "telegram") {
1455
+ const botToken = process.env["AGENTFLOW_TELEGRAM_BOT_TOKEN"] ?? "";
1456
+ const chatId = process.env["AGENTFLOW_TELEGRAM_CHAT_ID"] ?? "";
1457
+ if (botToken && chatId) {
1458
+ notifyChannels.push({ type: "telegram", botToken, chatId });
1459
+ } else {
1460
+ console.error("Warning: --notify telegram requires AGENTFLOW_TELEGRAM_BOT_TOKEN and AGENTFLOW_TELEGRAM_CHAT_ID env vars");
1461
+ }
1462
+ } else if (val.startsWith("webhook:")) {
1463
+ notifyChannels.push({ type: "webhook", url: val.slice(8) });
1464
+ } else if (val.startsWith("command:")) {
1465
+ notifyChannels.push({ type: "command", cmd: val.slice(8) });
1466
+ }
1467
+ i++;
1468
+ } else if (arg === "--poll") {
1469
+ i++;
1470
+ const v = parseInt(args[i] ?? "", 10);
1471
+ if (!isNaN(v) && v > 0) pollIntervalMs = v * 1e3;
1472
+ i++;
1473
+ } else if (arg === "--cooldown") {
1474
+ i++;
1475
+ const dur = parseDuration(args[i] ?? "30m");
1476
+ if (dur > 0) cooldownMs = dur;
1477
+ i++;
1478
+ } else if (arg === "--state-file") {
1479
+ i++;
1480
+ stateFilePath = args[i] ?? "";
1481
+ i++;
1482
+ } else if (arg === "--recursive" || arg === "-R") {
1483
+ recursive = true;
1484
+ i++;
1485
+ } else if (!arg.startsWith("-")) {
1486
+ dirs.push((0, import_node_path3.resolve)(arg));
1487
+ i++;
1488
+ } else {
1489
+ i++;
1490
+ }
1491
+ }
1492
+ if (dirs.length === 0) dirs.push((0, import_node_path3.resolve)("."));
1493
+ if (alertConditions.length === 0) {
1494
+ alertConditions.push({ type: "error" });
1495
+ alertConditions.push({ type: "recovery" });
1496
+ }
1497
+ notifyChannels.unshift({ type: "stdout" });
1498
+ if (!stateFilePath) {
1499
+ stateFilePath = (0, import_node_path3.join)(dirs[0], ".agentflow-watch-state.json");
1500
+ }
1501
+ return {
1502
+ dirs,
1503
+ recursive,
1504
+ pollIntervalMs,
1505
+ alertConditions,
1506
+ notifyChannels,
1507
+ stateFilePath: (0, import_node_path3.resolve)(stateFilePath),
1508
+ cooldownMs
1509
+ };
1510
+ }
1511
+ function printWatchUsage() {
1512
+ console.log(`
1513
+ AgentFlow Watch \u2014 headless alert system for agent infrastructure.
1514
+
1515
+ Polls directories for JSON/JSONL files, detects failures and stale
1516
+ agents, sends alerts. Same auto-detection as \`agentflow live\`.
1517
+
1518
+ Usage:
1519
+ agentflow watch [dir...] [options]
1520
+
1521
+ Arguments:
1522
+ dir One or more directories to watch (default: .)
1523
+
1524
+ Alert conditions (--alert-on, repeatable):
1525
+ error Agent transitions to error status
1526
+ recovery Agent recovers from error to ok
1527
+ stale:DURATION No file update within duration (e.g. 15m, 1h)
1528
+ consecutive-errors:N N consecutive error observations
1529
+
1530
+ Default (if none specified): error + recovery
1531
+
1532
+ Notification channels (--notify, repeatable):
1533
+ telegram Telegram Bot API (needs env vars)
1534
+ webhook:URL POST JSON to any URL
1535
+ command:CMD Run shell command with alert env vars
1536
+
1537
+ Stdout alerts are always printed regardless of --notify flags.
1538
+
1539
+ Options:
1540
+ --poll <secs> Poll interval in seconds (default: 30)
1541
+ --cooldown <duration> Alert dedup cooldown (default: 30m)
1542
+ --state-file <path> Persistence file (default: <dir>/.agentflow-watch-state.json)
1543
+ -R, --recursive Scan subdirectories (1 level deep)
1544
+ -h, --help Show this help message
1545
+
1546
+ Environment variables:
1547
+ AGENTFLOW_TELEGRAM_BOT_TOKEN Telegram bot token (for --notify telegram)
1548
+ AGENTFLOW_TELEGRAM_CHAT_ID Telegram chat ID (for --notify telegram)
1549
+
1550
+ Examples:
1551
+ agentflow watch ./data --alert-on error --alert-on stale:15m
1552
+ agentflow watch ./data ./cron --notify telegram --poll 60
1553
+ agentflow watch ./traces --notify webhook:https://hooks.slack.com/... --alert-on consecutive-errors:3
1554
+ agentflow watch ./data --notify "command:curl -X POST https://my-pagerduty/alert"
1555
+ `.trim());
1556
+ }
1557
+ function startWatch(argv) {
1558
+ const config = parseWatchArgs(argv);
1559
+ const valid = config.dirs.filter((d) => (0, import_node_fs4.existsSync)(d));
1560
+ if (valid.length === 0) {
1561
+ console.error(`No valid directories found: ${config.dirs.join(", ")}`);
1562
+ process.exit(1);
1563
+ }
1564
+ const invalid = config.dirs.filter((d) => !(0, import_node_fs4.existsSync)(d));
1565
+ if (invalid.length > 0) {
1566
+ console.warn(`Skipping non-existent: ${invalid.join(", ")}`);
1567
+ }
1568
+ let state = loadWatchState(config.stateFilePath);
1569
+ const condLabels = config.alertConditions.map((c) => {
1570
+ if (c.type === "stale") return `stale:${Math.floor(c.durationMs / 6e4)}m`;
1571
+ if (c.type === "consecutive-errors") return `consecutive-errors:${c.threshold}`;
1572
+ return c.type;
1573
+ });
1574
+ const channelLabels = config.notifyChannels.filter((c) => c.type !== "stdout").map((c) => {
1575
+ if (c.type === "webhook") return `webhook:${c.url.slice(0, 40)}...`;
1576
+ if (c.type === "command") return `command:${c.cmd.slice(0, 40)}`;
1577
+ return c.type;
1578
+ });
1579
+ console.log(`
1580
+ agentflow watch started`);
1581
+ console.log(` Directories: ${valid.join(", ")}`);
1582
+ console.log(` Poll: ${config.pollIntervalMs / 1e3}s`);
1583
+ console.log(` Alert on: ${condLabels.join(", ")}`);
1584
+ console.log(` Notify: stdout${channelLabels.length > 0 ? ", " + channelLabels.join(", ") : ""}`);
1585
+ console.log(` Cooldown: ${Math.floor(config.cooldownMs / 6e4)}m`);
1586
+ console.log(` State: ${config.stateFilePath}`);
1587
+ console.log(` Hostname: ${(0, import_node_os.hostname)()}`);
1588
+ console.log("");
1589
+ let pollCount = 0;
1590
+ async function poll() {
1591
+ const now = Date.now();
1592
+ pollCount++;
1593
+ const files = scanFiles(valid, config.recursive);
1594
+ const records = [];
1595
+ for (const f of files.slice(0, 500)) {
1596
+ const recs = f.ext === ".jsonl" ? processJsonlFile(f) : processJsonFile(f);
1597
+ records.push(...recs);
1598
+ }
1599
+ const alerts = detectTransitions(state, records, config, now);
1600
+ for (const alert of alerts) {
1601
+ for (const channel of config.notifyChannels) {
1602
+ await sendAlert(alert, channel);
1603
+ }
1604
+ }
1605
+ state = updateWatchState(state, records, alerts, now);
1606
+ saveWatchState(config.stateFilePath, state);
1607
+ if (pollCount % 10 === 0) {
1608
+ const agentCount = Object.keys(state.agents).length;
1609
+ const errorCount = Object.values(state.agents).filter((a) => a.lastStatus === "error").length;
1610
+ const runningCount = Object.values(state.agents).filter((a) => a.lastStatus === "running").length;
1611
+ const time = (/* @__PURE__ */ new Date()).toLocaleTimeString();
1612
+ console.log(`[${time}] heartbeat: ${agentCount} agents, ${runningCount} running, ${errorCount} errors, ${files.length} files`);
1613
+ }
1614
+ }
1615
+ poll();
1616
+ setInterval(() => {
1617
+ poll();
1618
+ }, config.pollIntervalMs);
1619
+ function shutdown() {
1620
+ console.log("\nagentflow watch stopped.");
1621
+ saveWatchState(config.stateFilePath, state);
1622
+ process.exit(0);
1623
+ }
1624
+ process.on("SIGINT", shutdown);
1625
+ process.on("SIGTERM", shutdown);
1626
+ }
1627
+
1112
1628
  // src/cli.ts
1113
1629
  function printHelp() {
1114
1630
  console.log(`
@@ -1118,8 +1634,9 @@ Usage:
1118
1634
  agentflow <command> [options]
1119
1635
 
1120
1636
  Commands:
1121
- run [options] -- <cmd> Wrap a command with automatic execution tracing
1122
- live [dir...] [options] Real-time terminal monitor (auto-detects any JSON/JSONL)
1637
+ run [options] -- <cmd> Wrap a command with automatic execution tracing
1638
+ live [dir...] [options] Real-time terminal monitor (auto-detects any JSON/JSONL)
1639
+ watch [dir...] [options] Headless alert system \u2014 detects failures, sends notifications
1123
1640
 
1124
1641
  Run \`agentflow <command> --help\` for command-specific options.
1125
1642
 
@@ -1127,7 +1644,8 @@ Examples:
1127
1644
  agentflow run --traces-dir ./traces -- python -m myagent process
1128
1645
  agentflow live ./data
1129
1646
  agentflow live ./traces ./cron ./workers -R
1130
- agentflow live ./data --refresh 5
1647
+ agentflow watch ./data --alert-on error --notify telegram
1648
+ agentflow watch ./data ./cron --alert-on stale:15m --notify webhook:https://...
1131
1649
  `.trim());
1132
1650
  }
1133
1651
  function parseRunArgs(argv) {
@@ -1265,7 +1783,8 @@ async function runCommand(argv) {
1265
1783
  }
1266
1784
  async function main() {
1267
1785
  const argv = process.argv.slice(2);
1268
- if (argv.length === 0 || argv[0] !== "run" && argv[0] !== "live" && (argv.includes("--help") || argv.includes("-h"))) {
1786
+ const knownCommands = ["run", "live", "watch"];
1787
+ if (argv.length === 0 || !knownCommands.includes(argv[0]) && (argv.includes("--help") || argv.includes("-h"))) {
1269
1788
  printHelp();
1270
1789
  process.exit(0);
1271
1790
  }
@@ -1277,6 +1796,9 @@ async function main() {
1277
1796
  case "live":
1278
1797
  startLive(argv);
1279
1798
  break;
1799
+ case "watch":
1800
+ startWatch(argv);
1801
+ break;
1280
1802
  default:
1281
1803
  if (!subcommand?.startsWith("-")) {
1282
1804
  startLive(["live", ...argv]);
package/dist/cli.js CHANGED
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runTraced,
4
- startLive
5
- } from "./chunk-5SPZPOFN.js";
4
+ startLive,
5
+ startWatch
6
+ } from "./chunk-NPH34CAL.js";
6
7
 
7
8
  // src/cli.ts
8
9
  import { basename } from "path";
@@ -14,8 +15,9 @@ Usage:
14
15
  agentflow <command> [options]
15
16
 
16
17
  Commands:
17
- run [options] -- <cmd> Wrap a command with automatic execution tracing
18
- live [dir...] [options] Real-time terminal monitor (auto-detects any JSON/JSONL)
18
+ run [options] -- <cmd> Wrap a command with automatic execution tracing
19
+ live [dir...] [options] Real-time terminal monitor (auto-detects any JSON/JSONL)
20
+ watch [dir...] [options] Headless alert system \u2014 detects failures, sends notifications
19
21
 
20
22
  Run \`agentflow <command> --help\` for command-specific options.
21
23
 
@@ -23,7 +25,8 @@ Examples:
23
25
  agentflow run --traces-dir ./traces -- python -m myagent process
24
26
  agentflow live ./data
25
27
  agentflow live ./traces ./cron ./workers -R
26
- agentflow live ./data --refresh 5
28
+ agentflow watch ./data --alert-on error --notify telegram
29
+ agentflow watch ./data ./cron --alert-on stale:15m --notify webhook:https://...
27
30
  `.trim());
28
31
  }
29
32
  function parseRunArgs(argv) {
@@ -161,7 +164,8 @@ async function runCommand(argv) {
161
164
  }
162
165
  async function main() {
163
166
  const argv = process.argv.slice(2);
164
- if (argv.length === 0 || argv[0] !== "run" && argv[0] !== "live" && (argv.includes("--help") || argv.includes("-h"))) {
167
+ const knownCommands = ["run", "live", "watch"];
168
+ if (argv.length === 0 || !knownCommands.includes(argv[0]) && (argv.includes("--help") || argv.includes("-h"))) {
165
169
  printHelp();
166
170
  process.exit(0);
167
171
  }
@@ -173,6 +177,9 @@ async function main() {
173
177
  case "live":
174
178
  startLive(argv);
175
179
  break;
180
+ case "watch":
181
+ startWatch(argv);
182
+ break;
176
183
  default:
177
184
  if (!subcommand?.startsWith("-")) {
178
185
  startLive(["live", ...argv]);