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.
@@ -206,10 +206,265 @@ function getTraceTree(trace) {
206
206
  return result;
207
207
  }
208
208
 
209
- // src/live.ts
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 = statSync(d);
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 readdirSync(d)) {
558
+ for (const f of readdirSync2(d)) {
304
559
  if (f.startsWith(".")) continue;
305
- const fp = join(d, f);
560
+ const fp = join2(d, f);
306
561
  if (seen.has(fp)) continue;
307
562
  let stat;
308
563
  try {
309
- stat = statSync(fp);
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(readFileSync(fp, "utf8"));
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 basename(filename).replace(/\.(json|jsonl)$/, "").replace(/-state$/, "");
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 pid = w.pid;
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 (pid && (status2 === "running" || status2 === "ok")) {
743
+ if (validPid && (status2 === "running" || status2 === "ok")) {
487
744
  try {
488
- execSync(`kill -0 ${pid} 2>/dev/null`, { stdio: "ignore" });
745
+ process.kill(pid, 0);
489
746
  } catch {
490
747
  pidAlive = false;
491
748
  validatedStatus = "error";
492
749
  }
493
750
  }
494
- const pidLabel = pid ? pidAlive ? `pid: ${pid}` : `pid: ${pid} (dead)` : "";
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 = readFileSync(file.path, "utf8").trim();
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) => existsSync(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) => !existsSync(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 existsSync2, mkdirSync, readdirSync as readdirSync2, statSync as statSync2, writeFileSync } from "fs";
1247
- import { basename as basename2, join as join2, resolve as resolve2 } from "path";
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 (!existsSync2(dir)) return result;
1255
- for (const entry of readdirSync2(dir)) {
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 = join2(dir, entry);
1571
+ const full = join3(dir, entry);
1258
1572
  try {
1259
- const stat = statSync2(full);
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 = basename2(filePath, ".json");
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 (!existsSync2(resolvedTracesDir)) {
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 = join2(resolvedTracesDir, filename);
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 join3 } from "path";
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(join3(dir, file), "utf-8");
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 = join3(dir, `${graph.id}.json`);
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 = join3(dir, `${graphId}.json`);
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 existsSync4 } from "fs";
1951
+ import { existsSync as existsSync5 } from "fs";
1629
1952
  import { hostname } from "os";
1630
- import { join as join4, resolve as resolve3 } from "path";
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((resolve4, reject) => {
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) resolve4();
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((resolve4, reject) => {
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) resolve4();
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((resolve4, reject) => {
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 resolve4();
2068
+ else resolve5();
1746
2069
  });
1747
2070
  });
1748
2071
  }
1749
2072
 
1750
2073
  // src/watch-state.ts
1751
- import { existsSync as existsSync3, readFileSync as readFileSync2, renameSync, writeFileSync as writeFileSync2 } from "fs";
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 (!existsSync3(filePath)) return emptyState();
2099
+ if (!existsSync4(filePath)) return emptyState();
1777
2100
  try {
1778
- const raw = JSON.parse(readFileSync2(filePath, "utf8"));
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(resolve3(arg));
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(resolve3("."));
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 = join4(dirs[0], ".agentflow-watch-state.json");
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: resolve3(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) => existsSync4(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) => !existsSync4(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,