agentflow-core 0.5.2 → 0.6.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/{chunk-YTMQBL3T.js → chunk-BOSYI5YM.js} +374 -48
- package/dist/cli.cjs +480 -58
- package/dist/cli.js +103 -3
- package/dist/index.cjs +382 -53
- package/dist/index.d.cts +109 -1
- package/dist/index.d.ts +109 -1
- package/dist/index.js +7 -1
- package/package.json +7 -3
|
@@ -206,10 +206,265 @@ function getTraceTree(trace) {
|
|
|
206
206
|
return result;
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
-
// src/
|
|
210
|
-
import { existsSync, readdirSync, readFileSync, statSync, watch } from "fs";
|
|
211
|
-
import { basename, join, resolve } from "path";
|
|
209
|
+
// src/process-audit.ts
|
|
212
210
|
import { execSync } from "child_process";
|
|
211
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
|
212
|
+
import { basename, join } from "path";
|
|
213
|
+
function isPidAlive(pid) {
|
|
214
|
+
try {
|
|
215
|
+
process.kill(pid, 0);
|
|
216
|
+
return true;
|
|
217
|
+
} catch {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function pidMatchesName(pid, name) {
|
|
222
|
+
try {
|
|
223
|
+
const cmdline = readFileSync(`/proc/${pid}/cmdline`, "utf8");
|
|
224
|
+
return cmdline.includes(name);
|
|
225
|
+
} catch {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function readPidFile(path) {
|
|
230
|
+
try {
|
|
231
|
+
const pid = parseInt(readFileSync(path, "utf8").trim(), 10);
|
|
232
|
+
return isNaN(pid) ? null : pid;
|
|
233
|
+
} catch {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function auditPidFile(config) {
|
|
238
|
+
if (!config.pidFile) return null;
|
|
239
|
+
const pid = readPidFile(config.pidFile);
|
|
240
|
+
if (pid === null) {
|
|
241
|
+
return {
|
|
242
|
+
path: config.pidFile,
|
|
243
|
+
pid: null,
|
|
244
|
+
alive: false,
|
|
245
|
+
matchesProcess: false,
|
|
246
|
+
stale: !existsSync(config.pidFile),
|
|
247
|
+
reason: existsSync(config.pidFile) ? "PID file exists but content is invalid" : "No PID file found"
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
const alive = isPidAlive(pid);
|
|
251
|
+
const matchesProcess = alive ? pidMatchesName(pid, config.processName) : false;
|
|
252
|
+
const stale = !alive || alive && !matchesProcess;
|
|
253
|
+
let reason;
|
|
254
|
+
if (alive && matchesProcess) {
|
|
255
|
+
reason = `PID ${pid} alive and matches ${config.processName}`;
|
|
256
|
+
} else if (alive && !matchesProcess) {
|
|
257
|
+
reason = `PID ${pid} alive but is NOT ${config.processName} (PID reused by another process)`;
|
|
258
|
+
} else {
|
|
259
|
+
reason = `PID ${pid} no longer exists`;
|
|
260
|
+
}
|
|
261
|
+
return { path: config.pidFile, pid, alive, matchesProcess, stale, reason };
|
|
262
|
+
}
|
|
263
|
+
function auditSystemd(config) {
|
|
264
|
+
if (config.systemdUnit === null || config.systemdUnit === void 0) return null;
|
|
265
|
+
const unit = config.systemdUnit;
|
|
266
|
+
try {
|
|
267
|
+
const raw = execSync(
|
|
268
|
+
`systemctl --user show ${unit} --property=ActiveState,SubState,MainPID,NRestarts,Result --no-pager 2>/dev/null`,
|
|
269
|
+
{ encoding: "utf8", timeout: 5e3 }
|
|
270
|
+
);
|
|
271
|
+
const props = {};
|
|
272
|
+
for (const line of raw.trim().split("\n")) {
|
|
273
|
+
const [k, ...v] = line.split("=");
|
|
274
|
+
if (k) props[k.trim()] = v.join("=").trim();
|
|
275
|
+
}
|
|
276
|
+
const activeState = props["ActiveState"] ?? "unknown";
|
|
277
|
+
const subState = props["SubState"] ?? "unknown";
|
|
278
|
+
const mainPid = parseInt(props["MainPID"] ?? "0", 10);
|
|
279
|
+
const restarts = parseInt(props["NRestarts"] ?? "0", 10);
|
|
280
|
+
const result = props["Result"] ?? "unknown";
|
|
281
|
+
return {
|
|
282
|
+
unit,
|
|
283
|
+
activeState,
|
|
284
|
+
subState,
|
|
285
|
+
mainPid,
|
|
286
|
+
restarts,
|
|
287
|
+
result,
|
|
288
|
+
crashLooping: activeState === "activating" && subState === "auto-restart",
|
|
289
|
+
failed: activeState === "failed"
|
|
290
|
+
};
|
|
291
|
+
} catch {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
function auditWorkers(config) {
|
|
296
|
+
if (!config.workersFile || !existsSync(config.workersFile)) return null;
|
|
297
|
+
try {
|
|
298
|
+
const data = JSON.parse(readFileSync(config.workersFile, "utf8"));
|
|
299
|
+
const orchPid = data.pid ?? null;
|
|
300
|
+
const orchAlive = orchPid ? isPidAlive(orchPid) : false;
|
|
301
|
+
const workers = [];
|
|
302
|
+
for (const [name, info] of Object.entries(data.tools ?? {})) {
|
|
303
|
+
const w = info;
|
|
304
|
+
const wPid = w.pid ?? null;
|
|
305
|
+
const wAlive = wPid ? isPidAlive(wPid) : false;
|
|
306
|
+
workers.push({
|
|
307
|
+
name,
|
|
308
|
+
pid: wPid,
|
|
309
|
+
declaredStatus: w.status ?? "unknown",
|
|
310
|
+
alive: wAlive,
|
|
311
|
+
stale: w.status === "running" && !wAlive
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
orchestratorPid: orchPid,
|
|
316
|
+
orchestratorAlive: orchAlive,
|
|
317
|
+
startedAt: data.started_at ?? "",
|
|
318
|
+
workers
|
|
319
|
+
};
|
|
320
|
+
} catch {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function getOsProcesses(processName) {
|
|
325
|
+
try {
|
|
326
|
+
const raw = execSync(`ps aux`, { encoding: "utf8", timeout: 5e3 });
|
|
327
|
+
return raw.split("\n").filter((line) => line.includes(processName) && !line.includes("process-audit") && !line.includes("grep")).map((line) => {
|
|
328
|
+
const parts = line.trim().split(/\s+/);
|
|
329
|
+
return {
|
|
330
|
+
pid: parseInt(parts[1] ?? "0", 10),
|
|
331
|
+
cpu: parts[2] ?? "0",
|
|
332
|
+
mem: parts[3] ?? "0",
|
|
333
|
+
command: parts.slice(10).join(" ")
|
|
334
|
+
};
|
|
335
|
+
}).filter((p) => !isNaN(p.pid) && p.pid > 0);
|
|
336
|
+
} catch {
|
|
337
|
+
return [];
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
function discoverProcessConfig(dirs) {
|
|
341
|
+
let pidFile;
|
|
342
|
+
let workersFile;
|
|
343
|
+
let processName = "";
|
|
344
|
+
for (const dir of dirs) {
|
|
345
|
+
if (!existsSync(dir)) continue;
|
|
346
|
+
let entries;
|
|
347
|
+
try {
|
|
348
|
+
entries = readdirSync(dir);
|
|
349
|
+
} catch {
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
for (const f of entries) {
|
|
353
|
+
const fp = join(dir, f);
|
|
354
|
+
try {
|
|
355
|
+
if (!statSync(fp).isFile()) continue;
|
|
356
|
+
} catch {
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (f.endsWith(".pid") && !pidFile) {
|
|
360
|
+
pidFile = fp;
|
|
361
|
+
if (!processName) {
|
|
362
|
+
processName = basename(f, ".pid");
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if ((f === "workers.json" || f.endsWith("-workers.json")) && !workersFile) {
|
|
366
|
+
workersFile = fp;
|
|
367
|
+
if (!processName && f !== "workers.json") {
|
|
368
|
+
processName = basename(f, "-workers.json");
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (!processName && !pidFile && !workersFile) return null;
|
|
374
|
+
if (!processName) processName = "agent";
|
|
375
|
+
return { processName, pidFile, workersFile };
|
|
376
|
+
}
|
|
377
|
+
function auditProcesses(config) {
|
|
378
|
+
const pidFile = auditPidFile(config);
|
|
379
|
+
const systemd = auditSystemd(config);
|
|
380
|
+
const workers = auditWorkers(config);
|
|
381
|
+
const osProcesses = getOsProcesses(config.processName);
|
|
382
|
+
const knownPids = /* @__PURE__ */ new Set();
|
|
383
|
+
if (pidFile?.pid && !pidFile.stale) knownPids.add(pidFile.pid);
|
|
384
|
+
if (workers) {
|
|
385
|
+
if (workers.orchestratorPid) knownPids.add(workers.orchestratorPid);
|
|
386
|
+
for (const w of workers.workers) {
|
|
387
|
+
if (w.pid) knownPids.add(w.pid);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (systemd?.mainPid) knownPids.add(systemd.mainPid);
|
|
391
|
+
const orphans = osProcesses.filter((p) => !knownPids.has(p.pid));
|
|
392
|
+
const problems = [];
|
|
393
|
+
if (pidFile?.stale) problems.push(`Stale PID file: ${pidFile.reason}`);
|
|
394
|
+
if (systemd?.crashLooping) problems.push("Systemd unit is crash-looping (auto-restart)");
|
|
395
|
+
if (systemd?.failed) problems.push("Systemd unit has failed");
|
|
396
|
+
if (systemd && systemd.restarts > 10) problems.push(`High systemd restart count: ${systemd.restarts}`);
|
|
397
|
+
if (pidFile?.pid && systemd?.mainPid && pidFile.pid !== systemd.mainPid) {
|
|
398
|
+
problems.push(`PID mismatch: file says ${pidFile.pid}, systemd says ${systemd.mainPid}`);
|
|
399
|
+
}
|
|
400
|
+
if (workers) {
|
|
401
|
+
for (const w of workers.workers) {
|
|
402
|
+
if (w.stale) problems.push(`Worker "${w.name}" (pid ${w.pid}) declares running but is dead`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (orphans.length > 0) problems.push(`${orphans.length} orphan process(es) not tracked by PID file or workers registry`);
|
|
406
|
+
return { pidFile, systemd, workers, osProcesses, orphans, problems };
|
|
407
|
+
}
|
|
408
|
+
function formatAuditReport(result) {
|
|
409
|
+
const lines = [];
|
|
410
|
+
lines.push("");
|
|
411
|
+
lines.push("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
|
|
412
|
+
lines.push("\u2551 \u{1F50D} P R O C E S S A U D I T \u2551");
|
|
413
|
+
lines.push("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
|
|
414
|
+
if (result.pidFile) {
|
|
415
|
+
const pf = result.pidFile;
|
|
416
|
+
const icon = pf.pid && pf.alive && pf.matchesProcess ? "\u2705" : pf.stale ? "\u26A0\uFE0F " : "\u2139\uFE0F ";
|
|
417
|
+
lines.push(`
|
|
418
|
+
PID File: ${pf.path}`);
|
|
419
|
+
lines.push(` ${icon} ${pf.reason}`);
|
|
420
|
+
}
|
|
421
|
+
if (result.systemd) {
|
|
422
|
+
const sd = result.systemd;
|
|
423
|
+
const icon = sd.activeState === "active" ? "\u{1F7E2}" : sd.crashLooping ? "\u{1F7E1}" : sd.failed ? "\u{1F534}" : "\u26AA";
|
|
424
|
+
lines.push(`
|
|
425
|
+
Systemd: ${sd.unit}`);
|
|
426
|
+
lines.push(` ${icon} State: ${sd.activeState} (${sd.subState}) Result: ${sd.result}`);
|
|
427
|
+
lines.push(` Main PID: ${sd.mainPid || "none"} Restarts: ${sd.restarts}`);
|
|
428
|
+
}
|
|
429
|
+
if (result.workers) {
|
|
430
|
+
const w = result.workers;
|
|
431
|
+
lines.push(`
|
|
432
|
+
Workers (orchestrator pid ${w.orchestratorPid ?? "unknown"} ${w.orchestratorAlive ? "\u2705" : "\u274C"})`);
|
|
433
|
+
for (const worker of w.workers) {
|
|
434
|
+
const icon = worker.declaredStatus === "running" && worker.alive ? "\u{1F7E2}" : worker.stale ? "\u{1F534} STALE" : "\u26AA";
|
|
435
|
+
lines.push(` ${icon} ${worker.name.padEnd(14)} pid=${String(worker.pid ?? "-").padEnd(8)} status=${worker.declaredStatus}`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (result.osProcesses.length > 0) {
|
|
439
|
+
lines.push(`
|
|
440
|
+
OS Processes (${result.osProcesses.length} total)`);
|
|
441
|
+
for (const p of result.osProcesses) {
|
|
442
|
+
lines.push(` PID ${String(p.pid).padEnd(8)} CPU=${p.cpu.padEnd(6)} MEM=${p.mem.padEnd(6)} ${p.command.substring(0, 55)}`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (result.orphans.length > 0) {
|
|
446
|
+
lines.push(`
|
|
447
|
+
\u26A0\uFE0F ${result.orphans.length} ORPHAN PROCESS(ES):`);
|
|
448
|
+
for (const p of result.orphans) {
|
|
449
|
+
lines.push(` PID ${p.pid} \u2014 not tracked by PID file or workers registry`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
lines.push("");
|
|
453
|
+
if (result.problems.length === 0) {
|
|
454
|
+
lines.push(" \u2705 All checks passed \u2014 no process issues detected.");
|
|
455
|
+
} else {
|
|
456
|
+
lines.push(` \u26A0\uFE0F ${result.problems.length} issue(s):`);
|
|
457
|
+
for (const p of result.problems) {
|
|
458
|
+
lines.push(` \u2022 ${p}`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
lines.push("");
|
|
462
|
+
return lines.join("\n");
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// src/live.ts
|
|
466
|
+
import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2, watch } from "fs";
|
|
467
|
+
import { basename as basename2, join as join2, resolve } from "path";
|
|
213
468
|
var C = {
|
|
214
469
|
reset: "\x1B[0m",
|
|
215
470
|
bold: "\x1B[1m",
|
|
@@ -284,7 +539,7 @@ function scanFiles(dirs, recursive) {
|
|
|
284
539
|
const seen = /* @__PURE__ */ new Set();
|
|
285
540
|
function scanDir(d, topLevel) {
|
|
286
541
|
try {
|
|
287
|
-
const dirStat =
|
|
542
|
+
const dirStat = statSync2(d);
|
|
288
543
|
const dirMtime = dirStat.mtime.getTime();
|
|
289
544
|
const cachedMtime = dirMtimeCache.get(d);
|
|
290
545
|
if (cachedMtime === dirMtime) {
|
|
@@ -300,13 +555,13 @@ function scanFiles(dirs, recursive) {
|
|
|
300
555
|
}
|
|
301
556
|
}
|
|
302
557
|
const dirResults = [];
|
|
303
|
-
for (const f of
|
|
558
|
+
for (const f of readdirSync2(d)) {
|
|
304
559
|
if (f.startsWith(".")) continue;
|
|
305
|
-
const fp =
|
|
560
|
+
const fp = join2(d, f);
|
|
306
561
|
if (seen.has(fp)) continue;
|
|
307
562
|
let stat;
|
|
308
563
|
try {
|
|
309
|
-
stat =
|
|
564
|
+
stat = statSync2(fp);
|
|
310
565
|
} catch {
|
|
311
566
|
continue;
|
|
312
567
|
}
|
|
@@ -338,13 +593,13 @@ function scanFiles(dirs, recursive) {
|
|
|
338
593
|
}
|
|
339
594
|
function safeReadJson(fp) {
|
|
340
595
|
try {
|
|
341
|
-
return JSON.parse(
|
|
596
|
+
return JSON.parse(readFileSync2(fp, "utf8"));
|
|
342
597
|
} catch {
|
|
343
598
|
return null;
|
|
344
599
|
}
|
|
345
600
|
}
|
|
346
601
|
function nameFromFile(filename) {
|
|
347
|
-
return
|
|
602
|
+
return basename2(filename).replace(/\.(json|jsonl)$/, "").replace(/-state$/, "");
|
|
348
603
|
}
|
|
349
604
|
function normalizeStatus(val) {
|
|
350
605
|
if (typeof val !== "string") return "unknown";
|
|
@@ -480,18 +735,20 @@ function processJsonFile(file) {
|
|
|
480
735
|
const w = info;
|
|
481
736
|
const status2 = findStatus(w);
|
|
482
737
|
const ts2 = findTimestamp(w) || findTimestamp(obj) || file.mtime;
|
|
483
|
-
const
|
|
738
|
+
const rawPid = w.pid;
|
|
739
|
+
const pid = typeof rawPid === "number" ? rawPid : Number(rawPid);
|
|
740
|
+
const validPid = Number.isFinite(pid) && pid > 0;
|
|
484
741
|
let validatedStatus = status2;
|
|
485
742
|
let pidAlive = true;
|
|
486
|
-
if (
|
|
743
|
+
if (validPid && (status2 === "running" || status2 === "ok")) {
|
|
487
744
|
try {
|
|
488
|
-
|
|
745
|
+
process.kill(pid, 0);
|
|
489
746
|
} catch {
|
|
490
747
|
pidAlive = false;
|
|
491
748
|
validatedStatus = "error";
|
|
492
749
|
}
|
|
493
750
|
}
|
|
494
|
-
const pidLabel =
|
|
751
|
+
const pidLabel = validPid ? pidAlive ? `pid: ${pid}` : `pid: ${pid} (dead)` : "";
|
|
495
752
|
const detail2 = pidLabel || extractDetail(w);
|
|
496
753
|
records.push({
|
|
497
754
|
id: name,
|
|
@@ -520,7 +777,7 @@ function processJsonFile(file) {
|
|
|
520
777
|
}
|
|
521
778
|
function processJsonlFile(file) {
|
|
522
779
|
try {
|
|
523
|
-
const content =
|
|
780
|
+
const content = readFileSync2(file.path, "utf8").trim();
|
|
524
781
|
if (!content) return [];
|
|
525
782
|
const lines = content.split("\n");
|
|
526
783
|
const lineCount = lines.length;
|
|
@@ -672,6 +929,9 @@ var prevFileCount = 0;
|
|
|
672
929
|
var newExecCount = 0;
|
|
673
930
|
var sessionStart = Date.now();
|
|
674
931
|
var firstRender = true;
|
|
932
|
+
var cachedAuditConfig = null;
|
|
933
|
+
var cachedAuditResult = null;
|
|
934
|
+
var lastAuditTime = 0;
|
|
675
935
|
var fileCache = /* @__PURE__ */ new Map();
|
|
676
936
|
function getRecordsCached(f) {
|
|
677
937
|
const cached = fileCache.get(f.path);
|
|
@@ -791,6 +1051,22 @@ function render(config) {
|
|
|
791
1051
|
const level = Math.round(v / maxBucket * 8);
|
|
792
1052
|
return (failBuckets[i] > 0 ? C.red : C.green) + sparkChars[level] + C.reset;
|
|
793
1053
|
}).join("");
|
|
1054
|
+
let auditResult = null;
|
|
1055
|
+
if (now - lastAuditTime > 1e4) {
|
|
1056
|
+
if (!cachedAuditConfig) {
|
|
1057
|
+
cachedAuditConfig = discoverProcessConfig(config.dirs);
|
|
1058
|
+
}
|
|
1059
|
+
if (cachedAuditConfig) {
|
|
1060
|
+
try {
|
|
1061
|
+
auditResult = auditProcesses(cachedAuditConfig);
|
|
1062
|
+
cachedAuditResult = auditResult;
|
|
1063
|
+
lastAuditTime = now;
|
|
1064
|
+
} catch {
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
} else {
|
|
1068
|
+
auditResult = cachedAuditResult;
|
|
1069
|
+
}
|
|
794
1070
|
const distributedTraces = [];
|
|
795
1071
|
if (allTraces.length > 1) {
|
|
796
1072
|
const traceGroups = groupByTraceId(allTraces);
|
|
@@ -871,6 +1147,41 @@ function render(config) {
|
|
|
871
1147
|
);
|
|
872
1148
|
writeLine(L, "");
|
|
873
1149
|
writeLine(L, ` ${C.bold}Activity (1h)${C.reset} ${spark} ${C.dim}\u2190 now${C.reset}`);
|
|
1150
|
+
if (auditResult) {
|
|
1151
|
+
const ar = auditResult;
|
|
1152
|
+
const healthy = ar.problems.length === 0;
|
|
1153
|
+
const healthIcon = healthy ? `${C.green}\u25CF${C.reset}` : `${C.red}\u25CF${C.reset}`;
|
|
1154
|
+
const healthLabel = healthy ? `${C.green}healthy${C.reset}` : `${C.red}${ar.problems.length} issue(s)${C.reset}`;
|
|
1155
|
+
const workerParts = [];
|
|
1156
|
+
if (ar.workers) {
|
|
1157
|
+
for (const w of ar.workers.workers) {
|
|
1158
|
+
const wIcon = w.declaredStatus === "running" && w.alive ? `${C.green}\u25CF${C.reset}` : w.stale ? `${C.red}\u25CF${C.reset}` : `${C.dim}\u25CB${C.reset}`;
|
|
1159
|
+
workerParts.push(`${wIcon} ${w.name}`);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
let sysdLabel = "";
|
|
1163
|
+
if (ar.systemd) {
|
|
1164
|
+
const si = ar.systemd.activeState === "active" ? `${C.green}\u25CF${C.reset}` : ar.systemd.crashLooping ? `${C.yellow}\u25CF${C.reset}` : ar.systemd.failed ? `${C.red}\u25CF${C.reset}` : `${C.dim}\u25CB${C.reset}`;
|
|
1165
|
+
sysdLabel = ` ${C.bold}Systemd${C.reset} ${si} ${ar.systemd.activeState}`;
|
|
1166
|
+
if (ar.systemd.restarts > 0) sysdLabel += ` ${C.dim}(${ar.systemd.restarts} restarts)${C.reset}`;
|
|
1167
|
+
}
|
|
1168
|
+
let pidLabel = "";
|
|
1169
|
+
if (ar.pidFile?.pid) {
|
|
1170
|
+
const pi = ar.pidFile.alive && ar.pidFile.matchesProcess ? `${C.green}\u25CF${C.reset}` : `${C.red}\u25CF${C.reset}`;
|
|
1171
|
+
pidLabel = ` ${C.bold}PID${C.reset} ${pi} ${ar.pidFile.pid}`;
|
|
1172
|
+
}
|
|
1173
|
+
writeLine(L, "");
|
|
1174
|
+
writeLine(L, ` ${C.bold}${C.under}Process Health${C.reset}`);
|
|
1175
|
+
writeLine(L, ` ${healthIcon} ${healthLabel}${pidLabel}${sysdLabel} ${C.bold}Procs${C.reset} ${C.dim}${ar.osProcesses.length}${C.reset} ${ar.orphans.length > 0 ? `${C.red}Orphans ${ar.orphans.length}${C.reset}` : `${C.dim}Orphans 0${C.reset}`}`);
|
|
1176
|
+
if (workerParts.length > 0) {
|
|
1177
|
+
writeLine(L, ` ${C.dim}Workers${C.reset} ${workerParts.join(" ")}`);
|
|
1178
|
+
}
|
|
1179
|
+
if (!healthy) {
|
|
1180
|
+
for (const p of ar.problems.slice(0, 3)) {
|
|
1181
|
+
writeLine(L, ` ${C.red}\u2022${C.reset} ${C.dim}${p}${C.reset}`);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
874
1185
|
writeLine(L, "");
|
|
875
1186
|
writeLine(
|
|
876
1187
|
L,
|
|
@@ -978,21 +1289,24 @@ function render(config) {
|
|
|
978
1289
|
writeLine(L, ` ${C.dim}Press Ctrl+C to exit${C.reset}`);
|
|
979
1290
|
flushLines(L);
|
|
980
1291
|
}
|
|
981
|
-
function getDistDepth(dt, spanId) {
|
|
1292
|
+
function getDistDepth(dt, spanId, visited) {
|
|
982
1293
|
if (!spanId) return 0;
|
|
1294
|
+
const seen = visited ?? /* @__PURE__ */ new Set();
|
|
1295
|
+
if (seen.has(spanId)) return 0;
|
|
1296
|
+
seen.add(spanId);
|
|
983
1297
|
const g = dt.graphs.get(spanId);
|
|
984
1298
|
if (!g || !g.parentSpanId) return 0;
|
|
985
|
-
return 1 + getDistDepth(dt, g.parentSpanId);
|
|
1299
|
+
return 1 + getDistDepth(dt, g.parentSpanId, seen);
|
|
986
1300
|
}
|
|
987
1301
|
function startLive(argv) {
|
|
988
1302
|
const config = parseArgs(argv);
|
|
989
|
-
const valid = config.dirs.filter((d) =>
|
|
1303
|
+
const valid = config.dirs.filter((d) => existsSync2(d));
|
|
990
1304
|
if (valid.length === 0) {
|
|
991
1305
|
console.error(`No valid directories found: ${config.dirs.join(", ")}`);
|
|
992
1306
|
console.error("Specify directories containing JSON/JSONL files: agentflow live <dir> [dir...]");
|
|
993
1307
|
process.exit(1);
|
|
994
1308
|
}
|
|
995
|
-
const invalid = config.dirs.filter((d) => !
|
|
1309
|
+
const invalid = config.dirs.filter((d) => !existsSync2(d));
|
|
996
1310
|
if (invalid.length > 0) {
|
|
997
1311
|
console.warn(`Skipping non-existent: ${invalid.join(", ")}`);
|
|
998
1312
|
}
|
|
@@ -1243,20 +1557,20 @@ function createGraphBuilder(config) {
|
|
|
1243
1557
|
|
|
1244
1558
|
// src/runner.ts
|
|
1245
1559
|
import { spawnSync } from "child_process";
|
|
1246
|
-
import { existsSync as
|
|
1247
|
-
import { basename as
|
|
1560
|
+
import { existsSync as existsSync3, mkdirSync, readdirSync as readdirSync3, statSync as statSync3, writeFileSync } from "fs";
|
|
1561
|
+
import { basename as basename3, join as join3, resolve as resolve2 } from "path";
|
|
1248
1562
|
function globToRegex(pattern) {
|
|
1249
1563
|
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
1250
1564
|
return new RegExp(`^${escaped}$`);
|
|
1251
1565
|
}
|
|
1252
1566
|
function snapshotDir(dir, patterns) {
|
|
1253
1567
|
const result = /* @__PURE__ */ new Map();
|
|
1254
|
-
if (!
|
|
1255
|
-
for (const entry of
|
|
1568
|
+
if (!existsSync3(dir)) return result;
|
|
1569
|
+
for (const entry of readdirSync3(dir)) {
|
|
1256
1570
|
if (!patterns.some((re) => re.test(entry))) continue;
|
|
1257
|
-
const full =
|
|
1571
|
+
const full = join3(dir, entry);
|
|
1258
1572
|
try {
|
|
1259
|
-
const stat =
|
|
1573
|
+
const stat = statSync3(full);
|
|
1260
1574
|
if (stat.isFile()) {
|
|
1261
1575
|
result.set(full, stat.mtimeMs);
|
|
1262
1576
|
}
|
|
@@ -1266,7 +1580,7 @@ function snapshotDir(dir, patterns) {
|
|
|
1266
1580
|
return result;
|
|
1267
1581
|
}
|
|
1268
1582
|
function agentIdFromFilename(filePath) {
|
|
1269
|
-
const base =
|
|
1583
|
+
const base = basename3(filePath, ".json");
|
|
1270
1584
|
const cleaned = base.replace(/-state$/, "");
|
|
1271
1585
|
return `alfred-${cleaned}`;
|
|
1272
1586
|
}
|
|
@@ -1371,14 +1685,18 @@ async function runTraced(config) {
|
|
|
1371
1685
|
childBuilder.endNode(childRootId);
|
|
1372
1686
|
allGraphs.push(childBuilder.build());
|
|
1373
1687
|
}
|
|
1374
|
-
if (!
|
|
1688
|
+
if (!existsSync3(resolvedTracesDir)) {
|
|
1375
1689
|
mkdirSync(resolvedTracesDir, { recursive: true });
|
|
1376
1690
|
}
|
|
1377
1691
|
const ts = fileTimestamp();
|
|
1378
1692
|
const tracePaths = [];
|
|
1379
1693
|
for (const graph of allGraphs) {
|
|
1380
1694
|
const filename = `${graph.agentId}-${ts}.json`;
|
|
1381
|
-
const outPath =
|
|
1695
|
+
const outPath = join3(resolvedTracesDir, filename);
|
|
1696
|
+
const resolvedOut = resolve2(outPath);
|
|
1697
|
+
if (!resolvedOut.startsWith(resolvedTracesDir + "/") && resolvedOut !== resolvedTracesDir) {
|
|
1698
|
+
throw new Error(`Path traversal detected: agentId "${graph.agentId}" escapes traces directory`);
|
|
1699
|
+
}
|
|
1382
1700
|
writeFileSync(outPath, JSON.stringify(graphToJson(graph), null, 2), "utf-8");
|
|
1383
1701
|
tracePaths.push(outPath);
|
|
1384
1702
|
}
|
|
@@ -1397,7 +1715,7 @@ async function runTraced(config) {
|
|
|
1397
1715
|
|
|
1398
1716
|
// src/trace-store.ts
|
|
1399
1717
|
import { mkdir, readdir, readFile, writeFile } from "fs/promises";
|
|
1400
|
-
import { join as
|
|
1718
|
+
import { join as join4, resolve as resolve3 } from "path";
|
|
1401
1719
|
function createTraceStore(dir) {
|
|
1402
1720
|
async function ensureDir() {
|
|
1403
1721
|
await mkdir(dir, { recursive: true });
|
|
@@ -1414,7 +1732,7 @@ function createTraceStore(dir) {
|
|
|
1414
1732
|
for (const file of files) {
|
|
1415
1733
|
if (!file.endsWith(".json")) continue;
|
|
1416
1734
|
try {
|
|
1417
|
-
const content = await readFile(
|
|
1735
|
+
const content = await readFile(join4(dir, file), "utf-8");
|
|
1418
1736
|
const graph = loadGraph(content);
|
|
1419
1737
|
graphs.push(graph);
|
|
1420
1738
|
} catch {
|
|
@@ -1426,13 +1744,18 @@ function createTraceStore(dir) {
|
|
|
1426
1744
|
async save(graph) {
|
|
1427
1745
|
await ensureDir();
|
|
1428
1746
|
const json = graphToJson(graph);
|
|
1429
|
-
const filePath =
|
|
1747
|
+
const filePath = join4(dir, `${graph.id}.json`);
|
|
1748
|
+
const resolvedBase = resolve3(dir);
|
|
1749
|
+
const resolvedPath = resolve3(filePath);
|
|
1750
|
+
if (!resolvedPath.startsWith(resolvedBase + "/") && resolvedPath !== resolvedBase) {
|
|
1751
|
+
throw new Error(`Path traversal detected: "${graph.id}" escapes base directory`);
|
|
1752
|
+
}
|
|
1430
1753
|
await writeFile(filePath, JSON.stringify(json, null, 2), "utf-8");
|
|
1431
1754
|
return filePath;
|
|
1432
1755
|
},
|
|
1433
1756
|
async get(graphId) {
|
|
1434
1757
|
await ensureDir();
|
|
1435
|
-
const filePath =
|
|
1758
|
+
const filePath = join4(dir, `${graphId}.json`);
|
|
1436
1759
|
try {
|
|
1437
1760
|
const content = await readFile(filePath, "utf-8");
|
|
1438
1761
|
return loadGraph(content);
|
|
@@ -1625,9 +1948,9 @@ function toTimeline(graph) {
|
|
|
1625
1948
|
}
|
|
1626
1949
|
|
|
1627
1950
|
// src/watch.ts
|
|
1628
|
-
import { existsSync as
|
|
1951
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1629
1952
|
import { hostname } from "os";
|
|
1630
|
-
import { join as
|
|
1953
|
+
import { join as join5, resolve as resolve4 } from "path";
|
|
1631
1954
|
|
|
1632
1955
|
// src/watch-alerts.ts
|
|
1633
1956
|
import { exec } from "child_process";
|
|
@@ -1685,7 +2008,7 @@ function sendTelegram(payload, botToken, chatId) {
|
|
|
1685
2008
|
text: formatTelegram(payload),
|
|
1686
2009
|
parse_mode: "Markdown"
|
|
1687
2010
|
});
|
|
1688
|
-
return new Promise((
|
|
2011
|
+
return new Promise((resolve5, reject) => {
|
|
1689
2012
|
const req = httpsRequest(
|
|
1690
2013
|
`https://api.telegram.org/bot${botToken}/sendMessage`,
|
|
1691
2014
|
{
|
|
@@ -1694,7 +2017,7 @@ function sendTelegram(payload, botToken, chatId) {
|
|
|
1694
2017
|
},
|
|
1695
2018
|
(res) => {
|
|
1696
2019
|
res.resume();
|
|
1697
|
-
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300)
|
|
2020
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) resolve5();
|
|
1698
2021
|
else reject(new Error(`Telegram API returned ${res.statusCode}`));
|
|
1699
2022
|
}
|
|
1700
2023
|
);
|
|
@@ -1707,7 +2030,7 @@ function sendWebhook(payload, url) {
|
|
|
1707
2030
|
const body = JSON.stringify(payload);
|
|
1708
2031
|
const isHttps = url.startsWith("https");
|
|
1709
2032
|
const doRequest = isHttps ? httpsRequest : httpRequest;
|
|
1710
|
-
return new Promise((
|
|
2033
|
+
return new Promise((resolve5, reject) => {
|
|
1711
2034
|
const req = doRequest(
|
|
1712
2035
|
url,
|
|
1713
2036
|
{
|
|
@@ -1716,7 +2039,7 @@ function sendWebhook(payload, url) {
|
|
|
1716
2039
|
},
|
|
1717
2040
|
(res) => {
|
|
1718
2041
|
res.resume();
|
|
1719
|
-
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300)
|
|
2042
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) resolve5();
|
|
1720
2043
|
else reject(new Error(`Webhook returned ${res.statusCode}`));
|
|
1721
2044
|
}
|
|
1722
2045
|
);
|
|
@@ -1729,7 +2052,7 @@ function sendWebhook(payload, url) {
|
|
|
1729
2052
|
});
|
|
1730
2053
|
}
|
|
1731
2054
|
function sendCommand(payload, cmd) {
|
|
1732
|
-
return new Promise((
|
|
2055
|
+
return new Promise((resolve5, reject) => {
|
|
1733
2056
|
const env = {
|
|
1734
2057
|
...process.env,
|
|
1735
2058
|
AGENTFLOW_ALERT_AGENT: payload.agentId,
|
|
@@ -1742,13 +2065,13 @@ function sendCommand(payload, cmd) {
|
|
|
1742
2065
|
};
|
|
1743
2066
|
exec(cmd, { env, timeout: 3e4 }, (err) => {
|
|
1744
2067
|
if (err) reject(err);
|
|
1745
|
-
else
|
|
2068
|
+
else resolve5();
|
|
1746
2069
|
});
|
|
1747
2070
|
});
|
|
1748
2071
|
}
|
|
1749
2072
|
|
|
1750
2073
|
// src/watch-state.ts
|
|
1751
|
-
import { existsSync as
|
|
2074
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, renameSync, writeFileSync as writeFileSync2 } from "fs";
|
|
1752
2075
|
function parseDuration(input) {
|
|
1753
2076
|
const match = input.match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)$/i);
|
|
1754
2077
|
if (!match) {
|
|
@@ -1773,9 +2096,9 @@ function emptyState() {
|
|
|
1773
2096
|
return { version: 1, agents: {}, lastPollTime: 0 };
|
|
1774
2097
|
}
|
|
1775
2098
|
function loadWatchState(filePath) {
|
|
1776
|
-
if (!
|
|
2099
|
+
if (!existsSync4(filePath)) return emptyState();
|
|
1777
2100
|
try {
|
|
1778
|
-
const raw = JSON.parse(
|
|
2101
|
+
const raw = JSON.parse(readFileSync3(filePath, "utf8"));
|
|
1779
2102
|
if (raw.version !== 1 || typeof raw.agents !== "object") return emptyState();
|
|
1780
2103
|
return raw;
|
|
1781
2104
|
} catch {
|
|
@@ -2017,20 +2340,20 @@ function parseWatchArgs(argv) {
|
|
|
2017
2340
|
recursive = true;
|
|
2018
2341
|
i++;
|
|
2019
2342
|
} else if (!arg.startsWith("-")) {
|
|
2020
|
-
dirs.push(
|
|
2343
|
+
dirs.push(resolve4(arg));
|
|
2021
2344
|
i++;
|
|
2022
2345
|
} else {
|
|
2023
2346
|
i++;
|
|
2024
2347
|
}
|
|
2025
2348
|
}
|
|
2026
|
-
if (dirs.length === 0) dirs.push(
|
|
2349
|
+
if (dirs.length === 0) dirs.push(resolve4("."));
|
|
2027
2350
|
if (alertConditions.length === 0) {
|
|
2028
2351
|
alertConditions.push({ type: "error" });
|
|
2029
2352
|
alertConditions.push({ type: "recovery" });
|
|
2030
2353
|
}
|
|
2031
2354
|
notifyChannels.unshift({ type: "stdout" });
|
|
2032
2355
|
if (!stateFilePath) {
|
|
2033
|
-
stateFilePath =
|
|
2356
|
+
stateFilePath = join5(dirs[0], ".agentflow-watch-state.json");
|
|
2034
2357
|
}
|
|
2035
2358
|
return {
|
|
2036
2359
|
dirs,
|
|
@@ -2038,7 +2361,7 @@ function parseWatchArgs(argv) {
|
|
|
2038
2361
|
pollIntervalMs,
|
|
2039
2362
|
alertConditions,
|
|
2040
2363
|
notifyChannels,
|
|
2041
|
-
stateFilePath:
|
|
2364
|
+
stateFilePath: resolve4(stateFilePath),
|
|
2042
2365
|
cooldownMs
|
|
2043
2366
|
};
|
|
2044
2367
|
}
|
|
@@ -2092,12 +2415,12 @@ Examples:
|
|
|
2092
2415
|
}
|
|
2093
2416
|
function startWatch(argv) {
|
|
2094
2417
|
const config = parseWatchArgs(argv);
|
|
2095
|
-
const valid = config.dirs.filter((d) =>
|
|
2418
|
+
const valid = config.dirs.filter((d) => existsSync5(d));
|
|
2096
2419
|
if (valid.length === 0) {
|
|
2097
2420
|
console.error(`No valid directories found: ${config.dirs.join(", ")}`);
|
|
2098
2421
|
process.exit(1);
|
|
2099
2422
|
}
|
|
2100
|
-
const invalid = config.dirs.filter((d) => !
|
|
2423
|
+
const invalid = config.dirs.filter((d) => !existsSync5(d));
|
|
2101
2424
|
if (invalid.length > 0) {
|
|
2102
2425
|
console.warn(`Skipping non-existent: ${invalid.join(", ")}`);
|
|
2103
2426
|
}
|
|
@@ -2190,6 +2513,9 @@ export {
|
|
|
2190
2513
|
groupByTraceId,
|
|
2191
2514
|
stitchTrace,
|
|
2192
2515
|
getTraceTree,
|
|
2516
|
+
discoverProcessConfig,
|
|
2517
|
+
auditProcesses,
|
|
2518
|
+
formatAuditReport,
|
|
2193
2519
|
startLive,
|
|
2194
2520
|
createGraphBuilder,
|
|
2195
2521
|
runTraced,
|