agentflow-dashboard 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -38,9 +38,9 @@ module.exports = __toCommonJS(index_exports);
38
38
 
39
39
  // src/server.ts
40
40
  var import_express = __toESM(require("express"), 1);
41
- var fs2 = __toESM(require("fs"), 1);
41
+ var fs3 = __toESM(require("fs"), 1);
42
42
  var import_http = require("http");
43
- var path2 = __toESM(require("path"), 1);
43
+ var path3 = __toESM(require("path"), 1);
44
44
  var import_url = require("url");
45
45
  var import_ws = require("ws");
46
46
  var import_agentflow_core3 = require("agentflow-core");
@@ -226,7 +226,7 @@ var import_chokidar = __toESM(require("chokidar"), 1);
226
226
  var import_events = require("events");
227
227
  var fs = __toESM(require("fs"), 1);
228
228
  var path = __toESM(require("path"), 1);
229
- var TraceWatcher = class extends import_events.EventEmitter {
229
+ var TraceWatcher = class _TraceWatcher extends import_events.EventEmitter {
230
230
  watchers = [];
231
231
  traces = /* @__PURE__ */ new Map();
232
232
  tracesDir;
@@ -254,22 +254,65 @@ var TraceWatcher = class extends import_events.EventEmitter {
254
254
  }
255
255
  loadExistingFiles() {
256
256
  let totalFiles = 0;
257
+ let totalDirectories = 0;
257
258
  for (const dir of this.allWatchDirs) {
258
259
  if (!fs.existsSync(dir)) continue;
259
260
  try {
260
- const files = fs.readdirSync(dir).filter((f) => f.endsWith(".json") || f.endsWith(".jsonl"));
261
- totalFiles += files.length;
262
- for (const file of files) {
263
- this.loadFile(path.join(dir, file));
264
- }
261
+ totalDirectories++;
262
+ const loadedFiles = this.scanDirectoryRecursive(dir);
263
+ totalFiles += loadedFiles;
265
264
  } catch (error) {
266
265
  console.error(`Error scanning directory ${dir}:`, error);
267
266
  }
268
267
  }
269
- console.log(`Scanned ${this.allWatchDirs.length} directories, loaded ${this.traces.size} items from ${totalFiles} files`);
268
+ console.log(`Scanned ${totalDirectories} directories (recursive), loaded ${this.traces.size} items from ${totalFiles} files`);
269
+ }
270
+ /** Recursively scan directory for supported file types */
271
+ scanDirectoryRecursive(dir, depth = 0) {
272
+ if (depth > 10) return 0;
273
+ let fileCount = 0;
274
+ try {
275
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
276
+ for (const entry of entries) {
277
+ if (entry.name.startsWith(".")) continue;
278
+ const fullPath = path.join(dir, entry.name);
279
+ if (entry.isFile()) {
280
+ if (this.isSupportedFile(entry.name)) {
281
+ if (this.loadFile(fullPath)) {
282
+ fileCount++;
283
+ }
284
+ }
285
+ } else if (entry.isDirectory()) {
286
+ fileCount += this.scanDirectoryRecursive(fullPath, depth + 1);
287
+ }
288
+ }
289
+ } catch (error) {
290
+ console.warn(`Cannot read directory ${dir}:`, error.message);
291
+ }
292
+ return fileCount;
270
293
  }
294
+ /** Check if file type is supported */
295
+ isSupportedFile(filename) {
296
+ return filename.endsWith(".json") || filename.endsWith(".jsonl") || filename.endsWith(".log") || filename.endsWith(".trace");
297
+ }
298
+ /** File names that are config/state, not traces — skip them. */
299
+ static SKIP_FILES = /* @__PURE__ */ new Set([
300
+ "workers.json",
301
+ "package.json",
302
+ "package-lock.json",
303
+ "tsconfig.json",
304
+ "biome.json",
305
+ "jobs.json",
306
+ "auth.json",
307
+ "models.json",
308
+ "config.json"
309
+ ]);
310
+ static SKIP_SUFFIXES = ["-state.json", "-config.json", "-watch-state.json", ".tmp", ".bak", ".backup"];
271
311
  /** Load a .json trace, .jsonl session file, or .log file. */
272
312
  loadFile(filePath) {
313
+ const filename = path.basename(filePath);
314
+ if (_TraceWatcher.SKIP_FILES.has(filename)) return false;
315
+ if (_TraceWatcher.SKIP_SUFFIXES.some((s) => filename.endsWith(s))) return false;
273
316
  if (filePath.endsWith(".jsonl")) {
274
317
  return this.loadSessionFile(filePath);
275
318
  }
@@ -283,12 +326,23 @@ var TraceWatcher = class extends import_events.EventEmitter {
283
326
  const content = fs.readFileSync(filePath, "utf8");
284
327
  const filename = path.basename(filePath);
285
328
  const stats = fs.statSync(filePath);
329
+ if (filename.startsWith("openclaw-") || filePath.includes("openclaw")) {
330
+ const result = this.loadOpenClawLogFile(content, filename, filePath, stats);
331
+ if (result) return true;
332
+ }
286
333
  const traces = this.parseUniversalLog(content, filename, filePath);
287
334
  for (let i = 0; i < traces.length; i++) {
288
335
  const trace = traces[i];
336
+ if (trace.nodes && !(trace.nodes instanceof Map)) {
337
+ const nodeMap = /* @__PURE__ */ new Map();
338
+ for (const [key2, value] of Object.entries(trace.nodes)) {
339
+ nodeMap.set(key2, value);
340
+ }
341
+ trace.nodes = nodeMap;
342
+ }
289
343
  trace.filename = filename;
290
344
  trace.lastModified = stats.mtime.getTime();
291
- trace.sourceType = "trace";
345
+ trace.sourceType = trace.sourceType || "trace";
292
346
  trace.sourceDir = path.dirname(filePath);
293
347
  const key = traces.length === 1 ? this.traceKey(filePath) : `${this.traceKey(filePath)}-${i}`;
294
348
  this.traces.set(key, trace);
@@ -328,10 +382,26 @@ var TraceWatcher = class extends import_events.EventEmitter {
328
382
  if (activity.timestamp > session.endTime) {
329
383
  session.endTime = activity.timestamp;
330
384
  }
385
+ if (activity.level === "error" || activity.level === "fatal") {
386
+ session.status = "failed";
387
+ }
331
388
  }
332
389
  const traces = Array.from(activities.values()).filter(
333
390
  (session) => Object.keys(session.nodes).length > 0
334
391
  );
392
+ for (const trace of traces) {
393
+ const sortedNodes = Object.values(trace.nodes).sort((a, b) => a.startTime - b.startTime);
394
+ trace.sessionEvents = sortedNodes.map((node) => ({
395
+ type: node.status === "failed" ? "tool_result" : "system",
396
+ timestamp: node.startTime,
397
+ name: node.name,
398
+ content: node.metadata.count > 1 ? `${node.name} (${node.metadata.count} occurrences, ${node.metadata.errorCount || 0} errors)` : node.name,
399
+ duration: node.endTime - node.startTime,
400
+ toolError: node.status === "failed" ? `${node.metadata.errorCount || 1} error(s)` : void 0,
401
+ id: node.id
402
+ }));
403
+ trace.sourceType = "session";
404
+ }
335
405
  if (traces.length === 0) {
336
406
  const stats = fs.statSync(filePath);
337
407
  traces.push({
@@ -345,6 +415,7 @@ var TraceWatcher = class extends import_events.EventEmitter {
345
415
  status: "completed",
346
416
  startTime: stats.mtime.getTime(),
347
417
  endTime: stats.mtime.getTime(),
418
+ children: [],
348
419
  metadata: { lineCount: lines.length, path: filePath }
349
420
  }
350
421
  },
@@ -411,38 +482,37 @@ var TraceWatcher = class extends import_events.EventEmitter {
411
482
  ...kvPairs
412
483
  };
413
484
  }
485
+ /** Strip ANSI escape codes from a string. */
486
+ stripAnsi(str) {
487
+ return str.replace(/\x1b\[[0-9;]*m/g, "");
488
+ }
414
489
  extractTimestamp(line) {
415
- const coloredMatch = line.match(/^\[2m(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z)\[0m/);
416
- if (coloredMatch) return new Date(coloredMatch[1]).getTime();
417
- const isoMatch = line.match(/(\d{4}-\d{2}-\d{2}[T\s]\d{2}:\d{2}:\d{2}[.\d]*Z?)/);
490
+ const clean = this.stripAnsi(line);
491
+ const isoMatch = clean.match(/(\d{4}-\d{2}-\d{2}[T\s]\d{2}:\d{2}:\d{2}[.\d]*Z?)/);
418
492
  if (isoMatch) return new Date(isoMatch[1]).getTime();
419
493
  return null;
420
494
  }
421
495
  extractLogLevel(line) {
422
- const coloredMatch = line.match(/\[\[(\d+)m\[\[1m(\w+)\s*\[0m\]/);
423
- if (coloredMatch) return coloredMatch[2].toLowerCase();
424
- const levelMatch = line.match(/\b(debug|info|warn|warning|error|fatal|trace)\b/i);
425
- return levelMatch ? levelMatch[1].toLowerCase() : null;
496
+ const clean = this.stripAnsi(line);
497
+ const levelMatch = clean.match(/\b(debug|info|warn|warning|error|fatal|trace)\b/i);
498
+ return levelMatch ? levelMatch[1].trim().toLowerCase() : null;
426
499
  }
427
500
  extractAction(line) {
428
- const coloredMatch = line.match(/\[1m([^\[]+?)\s*\[0m/);
429
- if (coloredMatch) return coloredMatch[1].trim();
430
- const afterLevel = line.replace(/^.*?(debug|info|warn|warning|error|fatal|trace)\s*:?\s*/i, "");
431
- return afterLevel.split(" ")[0] || "";
501
+ const clean = this.stripAnsi(line);
502
+ const actionMatch = clean.match(/\]\s+(\S+)/);
503
+ if (actionMatch) return actionMatch[1].trim();
504
+ const afterLevel = clean.replace(/^.*?(debug|info|warn|warning|error|fatal|trace)\s*\]?\s*/i, "");
505
+ return afterLevel.split(/\s+/)[0] || "";
432
506
  }
433
507
  extractKeyValuePairs(line) {
434
508
  const pairs = {};
435
- const coloredRegex = /\[36m(\w+)\[0m=\[35m([^\[]+?)\[0m/g;
509
+ const clean = this.stripAnsi(line);
510
+ const kvRegex = /(\w+)=('(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|\S+)/g;
436
511
  let match;
437
- while ((match = coloredRegex.exec(line)) !== null) {
512
+ while ((match = kvRegex.exec(clean)) !== null) {
513
+ if (match[1] === "Z" || match[1] === "m") continue;
438
514
  pairs[match[1]] = this.parseValue(match[2]);
439
515
  }
440
- if (Object.keys(pairs).length === 0) {
441
- const kvRegex = /(\w+)=([^\s]+)/g;
442
- while ((match = kvRegex.exec(line)) !== null) {
443
- pairs[match[1]] = this.parseValue(match[2]);
444
- }
445
- }
446
516
  return pairs;
447
517
  }
448
518
  parseValue(value) {
@@ -466,30 +536,51 @@ var TraceWatcher = class extends import_events.EventEmitter {
466
536
  if (kvPairs.component) return kvPairs.component;
467
537
  if (kvPairs.service) return kvPairs.service;
468
538
  if (kvPairs.module) return kvPairs.module;
469
- return "unknown";
539
+ if (kvPairs.worker) return kvPairs.worker;
540
+ return action || "unknown";
470
541
  }
471
542
  detectOperation(action, kvPairs) {
472
543
  if (action.includes(".")) return action.split(".").slice(1).join(".");
473
544
  if (kvPairs.operation) return kvPairs.operation;
474
545
  if (kvPairs.method) return kvPairs.method;
546
+ if (kvPairs.action) return kvPairs.action;
475
547
  return action || "activity";
476
548
  }
477
549
  extractSessionIdentifier(activity) {
478
- return activity.run_id || activity.session_id || activity.request_id || activity.trace_id || activity.sweep_id || activity.transaction_id || "default";
550
+ return activity.session_id || activity.run_id || activity.request_id || activity.trace_id || activity.sweep_id || activity.transaction_id || "default";
479
551
  }
480
552
  detectAgentIdentifier(activity, filename, filePath) {
481
- if (activity.component !== "unknown") {
482
- const pathAgent = this.extractAgentFromPath(filePath);
483
- if (pathAgent !== activity.component) {
484
- return `${pathAgent}-${activity.component}`;
553
+ if (activity.agent_id) {
554
+ const agentId = activity.agent_id;
555
+ if (agentId.startsWith("vault-")) return agentId;
556
+ if (agentId === "main" && filePath.includes(".alfred/")) return "alfred-main";
557
+ return agentId;
558
+ }
559
+ const pathAgent = this.extractAgentFromPath(filePath);
560
+ if (filePath.includes(".alfred/") && !pathAgent.startsWith("alfred-")) {
561
+ const basename2 = path.basename(filePath, path.extname(filePath));
562
+ if (basename2.match(/^(janitor|curator|distiller|surveyor|alfred)$/)) {
563
+ return basename2 === "alfred" ? "alfred" : `alfred-${basename2}`;
485
564
  }
486
- return activity.component;
487
565
  }
488
- return this.extractAgentFromPath(filePath);
566
+ return pathAgent;
489
567
  }
490
568
  extractAgentFromPath(filePath) {
491
569
  const filename = path.basename(filePath, path.extname(filePath));
492
570
  const pathParts = filePath.split(path.sep);
571
+ if (filePath.includes(".openclaw/")) {
572
+ const agentsIndex = pathParts.lastIndexOf("agents");
573
+ if (agentsIndex !== -1 && agentsIndex + 1 < pathParts.length) {
574
+ return `openclaw-${pathParts[agentsIndex + 1]}`;
575
+ }
576
+ if (filename.startsWith("openclaw-")) {
577
+ return "openclaw-gateway";
578
+ }
579
+ return "openclaw";
580
+ }
581
+ if (filePath.includes(".alfred/") || filename.includes("alfred")) {
582
+ return "alfred";
583
+ }
493
584
  for (const part of pathParts.reverse()) {
494
585
  if (part.match(/agent|worker|service|daemon|bot|ai|llm/i)) {
495
586
  return part;
@@ -511,7 +602,18 @@ var TraceWatcher = class extends import_events.EventEmitter {
511
602
  return "event";
512
603
  }
513
604
  addActivityNode(session, activity) {
514
- const nodeId = `${activity.component}-${activity.operation}-${activity.timestamp}`;
605
+ const nodeId = `${activity.component}-${activity.operation}`;
606
+ if (session.nodes[nodeId]) {
607
+ const node2 = session.nodes[nodeId];
608
+ node2.endTime = Math.max(node2.endTime, activity.timestamp);
609
+ node2.startTime = Math.min(node2.startTime, activity.timestamp);
610
+ node2.metadata.count = (node2.metadata.count || 1) + 1;
611
+ if (activity.level === "error" || activity.level === "fatal") {
612
+ node2.status = "failed";
613
+ node2.metadata.errorCount = (node2.metadata.errorCount || 0) + 1;
614
+ }
615
+ return;
616
+ }
515
617
  const node = {
516
618
  id: nodeId,
517
619
  type: activity.component,
@@ -519,7 +621,8 @@ var TraceWatcher = class extends import_events.EventEmitter {
519
621
  status: this.getUniversalNodeStatus(activity),
520
622
  startTime: activity.timestamp,
521
623
  endTime: activity.timestamp,
522
- metadata: activity
624
+ children: [],
625
+ metadata: { ...activity, count: 1 }
523
626
  };
524
627
  session.nodes[nodeId] = node;
525
628
  if (!session.rootNodeId) {
@@ -534,22 +637,275 @@ var TraceWatcher = class extends import_events.EventEmitter {
534
637
  if ((_b = activity.operation) == null ? void 0 : _b.match(/complete|finish|end|done/i)) return "completed";
535
638
  return "completed";
536
639
  }
640
+ /** Parse OpenClaw tslog-format log files with session run results. */
641
+ loadOpenClawLogFile(content, filename, filePath, stats) {
642
+ var _a, _b, _c, _d;
643
+ const lines = content.split("\n").filter((l) => l.trim());
644
+ const sessions = /* @__PURE__ */ new Map();
645
+ for (const line of lines) {
646
+ try {
647
+ const parsed = JSON.parse(line);
648
+ if (parsed["0"] && typeof parsed["0"] === "string") {
649
+ try {
650
+ const inner = typeof parsed["0"] === "string" && parsed["0"].startsWith("{") ? JSON.parse(parsed["0"]) : null;
651
+ if ((inner == null ? void 0 : inner.payloads) && ((_a = inner == null ? void 0 : inner.meta) == null ? void 0 : _a.agentMeta)) {
652
+ const agentMeta = inner.meta.agentMeta;
653
+ const sessionId = agentMeta.sessionId || "unknown";
654
+ const agentName = this.openClawSessionIdToAgent(sessionId);
655
+ const timestamp = parsed.time ? new Date(parsed.time).getTime() : ((_b = parsed._meta) == null ? void 0 : _b.date) ? new Date(parsed._meta.date).getTime() : stats.mtime.getTime();
656
+ const texts = (inner.payloads || []).map((p) => p.text || "").filter(Boolean);
657
+ if (!sessions.has(sessionId)) {
658
+ sessions.set(sessionId, { entries: [] });
659
+ }
660
+ sessions.get(sessionId).entries.push({
661
+ text: texts.join("\n"),
662
+ timestamp,
663
+ sessionId,
664
+ provider: agentMeta.provider || "",
665
+ model: agentMeta.model || "",
666
+ usage: agentMeta.usage || {},
667
+ durationMs: inner.meta.durationMs || 0,
668
+ agentName
669
+ });
670
+ continue;
671
+ }
672
+ } catch {
673
+ }
674
+ }
675
+ if (parsed.payloads && ((_c = parsed.meta) == null ? void 0 : _c.agentMeta)) {
676
+ const agentMeta = parsed.meta.agentMeta;
677
+ const sessionId = agentMeta.sessionId || "unknown";
678
+ const agentName = this.openClawSessionIdToAgent(sessionId);
679
+ const timestamp = parsed.time ? new Date(parsed.time).getTime() : ((_d = parsed._meta) == null ? void 0 : _d.date) ? new Date(parsed._meta.date).getTime() : stats.mtime.getTime();
680
+ const texts = (parsed.payloads || []).map((p) => p.text || "").filter(Boolean);
681
+ if (!sessions.has(sessionId)) {
682
+ sessions.set(sessionId, { entries: [] });
683
+ }
684
+ sessions.get(sessionId).entries.push({
685
+ text: texts.join("\n"),
686
+ timestamp,
687
+ sessionId,
688
+ provider: agentMeta.provider || "",
689
+ model: agentMeta.model || "",
690
+ usage: agentMeta.usage || {},
691
+ durationMs: parsed.meta.durationMs || 0,
692
+ agentName
693
+ });
694
+ }
695
+ } catch {
696
+ }
697
+ }
698
+ if (sessions.size === 0) return false;
699
+ let traceIndex = 0;
700
+ for (const [sessionId, session] of sessions) {
701
+ const entries = session.entries;
702
+ if (entries.length === 0) continue;
703
+ const firstEntry = entries[0];
704
+ const lastEntry = entries[entries.length - 1];
705
+ const agentId = firstEntry.agentName;
706
+ let totalInput = 0, totalOutput = 0, totalTokens = 0;
707
+ let totalDuration = 0;
708
+ for (const entry of entries) {
709
+ totalInput += entry.usage.input || 0;
710
+ totalOutput += entry.usage.output || 0;
711
+ totalTokens += entry.usage.total || 0;
712
+ totalDuration += entry.durationMs;
713
+ }
714
+ const nodes = /* @__PURE__ */ new Map();
715
+ const rootId = `openclaw-${sessionId.slice(0, 12)}`;
716
+ for (let j = 0; j < entries.length; j++) {
717
+ const e = entries[j];
718
+ const nodeId = `entry-${j}`;
719
+ nodes.set(nodeId, {
720
+ id: nodeId,
721
+ type: "tool",
722
+ name: `${e.model}: ${e.sessionId}`,
723
+ startTime: e.timestamp - e.durationMs,
724
+ endTime: e.timestamp,
725
+ status: "completed",
726
+ parentId: rootId,
727
+ children: [],
728
+ metadata: {
729
+ provider: e.provider,
730
+ model: e.model,
731
+ durationMs: e.durationMs,
732
+ usage: e.usage,
733
+ preview: e.text.slice(0, 200)
734
+ }
735
+ });
736
+ }
737
+ nodes.set(rootId, {
738
+ id: rootId,
739
+ type: "agent",
740
+ name: sessionId,
741
+ startTime: firstEntry.timestamp - (firstEntry.durationMs || 0),
742
+ endTime: lastEntry.timestamp,
743
+ status: "completed",
744
+ parentId: void 0,
745
+ children: Array.from(nodes.keys()).filter((k) => k !== rootId),
746
+ metadata: {
747
+ provider: firstEntry.provider,
748
+ model: firstEntry.model,
749
+ sessionId,
750
+ totalTokens,
751
+ inputTokens: totalInput,
752
+ outputTokens: totalOutput,
753
+ durationMs: totalDuration
754
+ }
755
+ });
756
+ const sessionEvents = entries.map((e, idx) => ({
757
+ type: "assistant",
758
+ timestamp: e.timestamp,
759
+ name: e.model,
760
+ content: e.text,
761
+ model: e.model,
762
+ provider: e.provider,
763
+ tokens: { input: e.usage.input || 0, output: e.usage.output || 0, total: e.usage.total || 0 },
764
+ duration: e.durationMs,
765
+ id: `entry-${idx}`
766
+ }));
767
+ const trace = {
768
+ id: sessionId,
769
+ nodes,
770
+ edges: [],
771
+ events: [],
772
+ startTime: firstEntry.timestamp - (firstEntry.durationMs || 0),
773
+ endTime: lastEntry.timestamp,
774
+ agentId,
775
+ trigger: "cron",
776
+ name: sessionId,
777
+ traceId: sessionId,
778
+ spanId: sessionId,
779
+ filename,
780
+ lastModified: stats.mtime.getTime(),
781
+ sourceType: "session",
782
+ sourceDir: path.dirname(filePath),
783
+ sessionEvents,
784
+ tokenUsage: { input: totalInput, output: totalOutput, total: totalTokens || totalInput + totalOutput, cost: 0 },
785
+ metadata: {
786
+ provider: firstEntry.provider,
787
+ model: firstEntry.model,
788
+ durationMs: totalDuration,
789
+ source: "openclaw-log"
790
+ }
791
+ };
792
+ const key = sessions.size === 1 ? this.traceKey(filePath) : `${this.traceKey(filePath)}-${traceIndex}`;
793
+ this.traces.set(key, trace);
794
+ traceIndex++;
795
+ }
796
+ return traceIndex > 0;
797
+ }
798
+ /** Map OpenClaw sessionId prefix to agent name. */
799
+ openClawSessionIdToAgent(sessionId) {
800
+ if (sessionId.startsWith("janitor-")) return "vault-janitor";
801
+ if (sessionId.startsWith("curator-")) return "vault-curator";
802
+ if (sessionId.startsWith("distiller-")) return "vault-distiller";
803
+ if (sessionId.startsWith("main-")) return "main";
804
+ const firstSegment = sessionId.split("-")[0];
805
+ if (firstSegment) return firstSegment;
806
+ return "openclaw";
807
+ }
537
808
  loadTraceFile(filePath) {
538
809
  try {
539
810
  const content = fs.readFileSync(filePath, "utf8");
540
- const graph = (0, import_agentflow_core2.loadGraph)(content);
541
811
  const filename = path.basename(filePath);
812
+ if (filename === "sessions.json") {
813
+ return this.loadSessionsIndex(filePath, content);
814
+ }
815
+ const graph = (0, import_agentflow_core2.loadGraph)(content);
542
816
  const stats = fs.statSync(filePath);
543
817
  graph.filename = filename;
544
818
  graph.lastModified = stats.mtime.getTime();
545
819
  graph.sourceType = "trace";
546
820
  graph.sourceDir = path.dirname(filePath);
821
+ if (graph.nodes instanceof Map) {
822
+ for (const node of graph.nodes.values()) {
823
+ if (!node.children) node.children = [];
824
+ }
825
+ }
547
826
  this.traces.set(this.traceKey(filePath), graph);
548
827
  return true;
549
828
  } catch {
550
829
  return false;
551
830
  }
552
831
  }
832
+ /** Parse sessions.json index to discover agents and their sessions. */
833
+ loadSessionsIndex(filePath, content) {
834
+ try {
835
+ const data = JSON.parse(content);
836
+ if (typeof data !== "object" || data === null) return false;
837
+ const stats = fs.statSync(filePath);
838
+ const pathParts = filePath.split(path.sep);
839
+ const agentsIndex = pathParts.lastIndexOf("agents");
840
+ if (agentsIndex === -1 || agentsIndex + 1 >= pathParts.length) return false;
841
+ const agentName = pathParts[agentsIndex + 1];
842
+ const agentId = filePath.includes(".openclaw/") ? `openclaw-${agentName}` : agentName;
843
+ let loaded = 0;
844
+ for (const [sessionKey, sessionData] of Object.entries(data)) {
845
+ if (!sessionData || typeof sessionData !== "object") continue;
846
+ const session = sessionData;
847
+ const sessionId = session.sessionId;
848
+ if (!sessionId) continue;
849
+ const existingKey = Array.from(this.traces.keys()).find((k) => {
850
+ const t = this.traces.get(k);
851
+ return (t == null ? void 0 : t.id) === sessionId || (t == null ? void 0 : t.traceId) === sessionId;
852
+ });
853
+ if (existingKey) continue;
854
+ const updatedAt = session.updatedAt || stats.mtime.getTime();
855
+ const label = session.label || sessionKey.split(":").pop() || sessionId;
856
+ const chatType = session.chatType || (sessionKey.includes("cron") ? "cron" : "direct");
857
+ const trigger = sessionKey.includes("cron") ? "cron" : "message";
858
+ const rootId = `idx-${sessionId.slice(0, 12)}`;
859
+ const nodes = /* @__PURE__ */ new Map();
860
+ nodes.set(rootId, {
861
+ id: rootId,
862
+ type: "agent",
863
+ name: label,
864
+ startTime: updatedAt,
865
+ endTime: updatedAt,
866
+ status: "completed",
867
+ parentId: void 0,
868
+ children: [],
869
+ metadata: {
870
+ sessionId,
871
+ sessionKey,
872
+ chatType,
873
+ source: "sessions-index"
874
+ }
875
+ });
876
+ const trace = {
877
+ id: sessionId,
878
+ nodes,
879
+ edges: [],
880
+ events: [],
881
+ startTime: updatedAt,
882
+ agentId,
883
+ trigger,
884
+ name: label,
885
+ traceId: sessionId,
886
+ spanId: sessionId,
887
+ filename: `${agentName}-${sessionId.slice(0, 8)}.index`,
888
+ lastModified: updatedAt,
889
+ sourceType: "session",
890
+ sourceDir: path.dirname(filePath),
891
+ sessionEvents: [],
892
+ tokenUsage: { input: 0, output: 0, total: 0, cost: 0 },
893
+ metadata: {
894
+ sessionKey,
895
+ chatType,
896
+ source: "sessions-index",
897
+ agentName
898
+ }
899
+ };
900
+ const key = `${this.traceKey(filePath)}-${sessionId.slice(0, 12)}`;
901
+ this.traces.set(key, trace);
902
+ loaded++;
903
+ }
904
+ return loaded > 0;
905
+ } catch {
906
+ return false;
907
+ }
908
+ }
553
909
  /** Parse a JSONL session log into a WatchedTrace (best-effort). */
554
910
  loadSessionFile(filePath) {
555
911
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
@@ -565,6 +921,10 @@ var TraceWatcher = class extends import_events.EventEmitter {
565
921
  }
566
922
  }
567
923
  if (rawEvents.length === 0) return false;
924
+ const firstEvent = rawEvents[0];
925
+ if ((firstEvent == null ? void 0 : firstEvent.jobId) && (firstEvent == null ? void 0 : firstEvent.action) && !(firstEvent == null ? void 0 : firstEvent.type)) {
926
+ return this.loadCronRunFile(rawEvents, filePath);
927
+ }
568
928
  const sessionEvent = rawEvents.find((e) => e.type === "session");
569
929
  const sessionId = (sessionEvent == null ? void 0 : sessionEvent.id) || path.basename(filePath, ".jsonl");
570
930
  const sessionTimestamp = (sessionEvent == null ? void 0 : sessionEvent.timestamp) || ((_a = rawEvents[0]) == null ? void 0 : _a.timestamp);
@@ -572,7 +932,25 @@ var TraceWatcher = class extends import_events.EventEmitter {
572
932
  if (!startTime) return false;
573
933
  const parentDir = path.basename(path.dirname(filePath));
574
934
  const grandParentDir = path.basename(path.dirname(path.dirname(filePath)));
575
- const agentId = grandParentDir === "agents" ? parentDir : parentDir;
935
+ const greatGrandParentDir = path.basename(path.dirname(path.dirname(path.dirname(filePath))));
936
+ let agentId;
937
+ if (parentDir === "sessions" && greatGrandParentDir === "agents") {
938
+ agentId = grandParentDir;
939
+ } else if (grandParentDir === "agents") {
940
+ agentId = parentDir;
941
+ } else if (parentDir === "runs" && grandParentDir === "cron") {
942
+ agentId = "openclaw-cron";
943
+ } else {
944
+ agentId = parentDir;
945
+ }
946
+ if (filePath.includes(".openclaw/") && !agentId.startsWith("openclaw-")) {
947
+ agentId = `openclaw-${agentId}`;
948
+ }
949
+ if (filePath.includes(".alfred/") || filePath.includes("alfred")) {
950
+ if (!agentId.startsWith("alfred-")) {
951
+ agentId = `alfred-${agentId}`;
952
+ }
953
+ }
576
954
  const modelEvent = rawEvents.find((e) => e.type === "model_change");
577
955
  const provider = (modelEvent == null ? void 0 : modelEvent.provider) || "";
578
956
  const modelId = (modelEvent == null ? void 0 : modelEvent.modelId) || "";
@@ -885,11 +1263,75 @@ var TraceWatcher = class extends import_events.EventEmitter {
885
1263
  return false;
886
1264
  }
887
1265
  }
1266
+ /** Parse cron run JSONL files (ts, jobId, action, status format). */
1267
+ loadCronRunFile(rawEvents, filePath) {
1268
+ var _a, _b, _c;
1269
+ try {
1270
+ const filename = path.basename(filePath);
1271
+ const jobId = ((_a = rawEvents[0]) == null ? void 0 : _a.jobId) || path.basename(filePath, ".jsonl");
1272
+ const fileStat = fs.statSync(filePath);
1273
+ const sessionEvents = [];
1274
+ let lastStatus = "completed";
1275
+ for (const evt of rawEvents) {
1276
+ const ts = evt.ts || Date.now();
1277
+ const action = evt.action || "unknown";
1278
+ const status = evt.status || "ok";
1279
+ if (status !== "ok") lastStatus = "failed";
1280
+ sessionEvents.push({
1281
+ type: action === "finished" ? "assistant" : "system",
1282
+ timestamp: ts,
1283
+ name: `${jobId}: ${action}`,
1284
+ content: evt.summary || evt.error || `${action} (${status})`,
1285
+ id: `cron-${ts}`
1286
+ });
1287
+ }
1288
+ const firstTs = ((_b = rawEvents[0]) == null ? void 0 : _b.ts) || fileStat.mtime.getTime();
1289
+ const lastTs = ((_c = rawEvents[rawEvents.length - 1]) == null ? void 0 : _c.ts) || fileStat.mtime.getTime();
1290
+ const rootId = `cron-${jobId.slice(0, 12)}`;
1291
+ const nodes = /* @__PURE__ */ new Map();
1292
+ nodes.set(rootId, {
1293
+ id: rootId,
1294
+ type: "agent",
1295
+ name: jobId,
1296
+ startTime: firstTs,
1297
+ endTime: lastTs,
1298
+ status: lastStatus,
1299
+ parentId: void 0,
1300
+ children: [],
1301
+ metadata: { jobId, runs: rawEvents.length }
1302
+ });
1303
+ const trace = {
1304
+ id: jobId,
1305
+ nodes,
1306
+ edges: [],
1307
+ events: [],
1308
+ startTime: firstTs,
1309
+ agentId: "openclaw-cron",
1310
+ trigger: "cron",
1311
+ name: jobId,
1312
+ traceId: jobId,
1313
+ spanId: jobId,
1314
+ filename,
1315
+ lastModified: fileStat.mtime.getTime(),
1316
+ sourceType: "session",
1317
+ sourceDir: path.dirname(filePath),
1318
+ sessionEvents,
1319
+ tokenUsage: { input: 0, output: 0, total: 0, cost: 0 },
1320
+ metadata: { jobId, source: "cron-run" }
1321
+ };
1322
+ this.traces.set(this.traceKey(filePath), trace);
1323
+ return true;
1324
+ } catch {
1325
+ return false;
1326
+ }
1327
+ }
888
1328
  /** Unique key for a file across directories. */
889
1329
  traceKey(filePath) {
890
1330
  for (const dir of this.allWatchDirs) {
891
1331
  if (filePath.startsWith(dir)) {
892
- return path.relative(dir, filePath).replace(/\\/g, "/") + "@" + path.basename(dir);
1332
+ const dirParts = dir.split(path.sep).filter(Boolean);
1333
+ const dirSuffix = dirParts.slice(-2).join("/");
1334
+ return path.relative(dir, filePath).replace(/\\/g, "/") + "@" + dirSuffix;
893
1335
  }
894
1336
  }
895
1337
  return filePath;
@@ -897,16 +1339,35 @@ var TraceWatcher = class extends import_events.EventEmitter {
897
1339
  startWatching() {
898
1340
  for (const dir of this.allWatchDirs) {
899
1341
  if (!fs.existsSync(dir)) continue;
900
- const watcher = import_chokidar.default.watch(dir, {
901
- ignored: /^\./,
1342
+ const patterns = [
1343
+ path.join(dir, "**/*.json"),
1344
+ path.join(dir, "**/*.jsonl"),
1345
+ path.join(dir, "**/*.log"),
1346
+ path.join(dir, "**/*.trace")
1347
+ ];
1348
+ const watcher = import_chokidar.default.watch(patterns, {
1349
+ ignored: [
1350
+ /^\./,
1351
+ // Ignore hidden files
1352
+ /node_modules/,
1353
+ // Ignore node_modules
1354
+ /\.git/,
1355
+ // Ignore git directories
1356
+ /\.vscode/,
1357
+ // Ignore vscode
1358
+ /\.idea/
1359
+ // Ignore idea
1360
+ ],
902
1361
  persistent: true,
903
1362
  ignoreInitial: true,
904
- depth: 0
905
- // don't recurse into subdirectories
1363
+ followSymlinks: false,
1364
+ depth: 10
1365
+ // Allow deep nesting for OpenClaw agents/*/sessions/
906
1366
  });
907
1367
  watcher.on("add", (filePath) => {
908
- if (filePath.endsWith(".json") || filePath.endsWith(".jsonl") || filePath.endsWith(".log") || filePath.endsWith(".trace")) {
909
- console.log(`New file: ${path.basename(filePath)}`);
1368
+ if (this.isSupportedFile(path.basename(filePath))) {
1369
+ const relativePath = path.relative(dir, filePath);
1370
+ console.log(`New file: ${relativePath} (in ${path.basename(dir)})`);
910
1371
  if (this.loadFile(filePath)) {
911
1372
  const key = this.traceKey(filePath);
912
1373
  const trace = this.traces.get(key);
@@ -917,7 +1378,7 @@ var TraceWatcher = class extends import_events.EventEmitter {
917
1378
  }
918
1379
  });
919
1380
  watcher.on("change", (filePath) => {
920
- if (filePath.endsWith(".json") || filePath.endsWith(".jsonl") || filePath.endsWith(".log") || filePath.endsWith(".trace")) {
1381
+ if (this.isSupportedFile(path.basename(filePath))) {
921
1382
  if (this.loadFile(filePath)) {
922
1383
  const key = this.traceKey(filePath);
923
1384
  const trace = this.traces.get(key);
@@ -928,7 +1389,7 @@ var TraceWatcher = class extends import_events.EventEmitter {
928
1389
  }
929
1390
  });
930
1391
  watcher.on("unlink", (filePath) => {
931
- if (filePath.endsWith(".json") || filePath.endsWith(".jsonl") || filePath.endsWith(".log") || filePath.endsWith(".trace")) {
1392
+ if (this.isSupportedFile(path.basename(filePath))) {
932
1393
  const key = this.traceKey(filePath);
933
1394
  this.traces.delete(key);
934
1395
  this.emit("trace-removed", key);
@@ -939,7 +1400,7 @@ var TraceWatcher = class extends import_events.EventEmitter {
939
1400
  });
940
1401
  this.watchers.push(watcher);
941
1402
  }
942
- console.log(`Watching ${this.allWatchDirs.length} directories for JSON/JSONL files`);
1403
+ console.log(`Watching ${this.allWatchDirs.length} directories recursively for JSON/JSONL/LOG/TRACE files`);
943
1404
  }
944
1405
  getAllTraces() {
945
1406
  return Array.from(this.traces.values()).sort((a, b) => {
@@ -997,10 +1458,167 @@ var TraceWatcher = class extends import_events.EventEmitter {
997
1458
  }
998
1459
  };
999
1460
 
1461
+ // src/cli.ts
1462
+ var fs2 = __toESM(require("fs"), 1);
1463
+ var os = __toESM(require("os"), 1);
1464
+ var path2 = __toESM(require("path"), 1);
1465
+ var VERSION = "0.4.0";
1466
+ function getLanAddress() {
1467
+ const interfaces = os.networkInterfaces();
1468
+ for (const name of Object.keys(interfaces)) {
1469
+ for (const iface of interfaces[name] || []) {
1470
+ if (iface.family === "IPv4" && !iface.internal) {
1471
+ return iface.address;
1472
+ }
1473
+ }
1474
+ }
1475
+ return null;
1476
+ }
1477
+ function printBanner(config, traceCount, stats) {
1478
+ var _a;
1479
+ const lan = getLanAddress();
1480
+ const host = config.host || "localhost";
1481
+ const port = config.port;
1482
+ const isPublic = host === "0.0.0.0";
1483
+ console.log(`
1484
+ ___ _ _____ _
1485
+ / _ \\ __ _ ___ _ __ | |_| ___| | _____ __
1486
+ | |_| |/ _\` |/ _ \\ '_ \\| __| |_ | |/ _ \\ \\ /\\ / /
1487
+ | _ | (_| | __/ | | | |_| _| | | (_) \\ V V /
1488
+ |_| |_|\\__, |\\___|_| |_|\\__|_| |_|\\___/ \\_/\\_/
1489
+ |___/ dashboard v${VERSION}
1490
+
1491
+ See your agents think.
1492
+
1493
+ \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
1494
+ \u2502 \u{1F916} Agents \u2502 TRACE FILES \u2502 \u{1F4CA} AgentFlow \u2502 SHOWS YOU \u2502 \u{1F310} Your browser \u2502
1495
+ \u2502 Execute tasks, \u2502 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500> \u2502 Reads traces, \u2502 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500> \u2502 Interactive \u2502
1496
+ \u2502 write JSON \u2502 \u2502 builds graphs, \u2502 \u2502 graph, timeline, \u2502
1497
+ \u2502 trace files. \u2502 \u2502 serves dashboard.\u2502 \u2502 metrics, health. \u2502
1498
+ \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
1499
+
1500
+ Runs locally. Your data never leaves your machine.
1501
+
1502
+ Tabs: \u{1F3AF} Graph \xB7 \u23F1\uFE0F Timeline \xB7 \u{1F4CA} Metrics \xB7 \u{1F6E0}\uFE0F Process Health \xB7 \u26A0\uFE0F Errors
1503
+
1504
+ Traces: ${config.tracesDir}${((_a = config.dataDirs) == null ? void 0 : _a.length) ? "\n Data dirs: " + config.dataDirs.join("\n ") : ""}
1505
+ Loaded: ${traceCount} traces \xB7 ${stats.totalAgents} agents \xB7 ${stats.totalExecutions} executions
1506
+ Success: ${stats.globalSuccessRate.toFixed(1)}%${stats.activeAgents > 0 ? ` \xB7 ${stats.activeAgents} active now` : ""}
1507
+ CORS: ${config.enableCors ? "enabled" : "disabled"}
1508
+ WebSocket: live updates enabled
1509
+
1510
+ \u2192 http://localhost:${port}${isPublic && lan ? `
1511
+ \u2192 http://${lan}:${port} (LAN)` : ""}
1512
+ `);
1513
+ }
1514
+ async function startDashboard() {
1515
+ const args = process.argv.slice(2);
1516
+ const config = {
1517
+ port: 3e3,
1518
+ tracesDir: "./traces",
1519
+ host: "localhost",
1520
+ enableCors: false
1521
+ };
1522
+ for (let i = 0; i < args.length; i++) {
1523
+ switch (args[i]) {
1524
+ case "--port":
1525
+ case "-p":
1526
+ config.port = parseInt(args[++i]) || 3e3;
1527
+ break;
1528
+ case "--traces":
1529
+ case "-t":
1530
+ config.tracesDir = args[++i];
1531
+ break;
1532
+ case "--host":
1533
+ case "-h":
1534
+ config.host = args[++i];
1535
+ break;
1536
+ case "--data-dir":
1537
+ if (!config.dataDirs) config.dataDirs = [];
1538
+ config.dataDirs.push(args[++i]);
1539
+ break;
1540
+ case "--cors":
1541
+ config.enableCors = true;
1542
+ break;
1543
+ case "--help":
1544
+ printHelp();
1545
+ process.exit(0);
1546
+ }
1547
+ }
1548
+ const tracesPath = path2.resolve(config.tracesDir);
1549
+ if (!fs2.existsSync(tracesPath)) {
1550
+ fs2.mkdirSync(tracesPath, { recursive: true });
1551
+ }
1552
+ config.tracesDir = tracesPath;
1553
+ console.log("\nStarting AgentFlow Dashboard...\n");
1554
+ const dashboard = new DashboardServer(config);
1555
+ process.on("SIGINT", async () => {
1556
+ console.log("\n\u{1F6D1} Shutting down dashboard...");
1557
+ await dashboard.stop();
1558
+ process.exit(0);
1559
+ });
1560
+ process.on("SIGTERM", async () => {
1561
+ await dashboard.stop();
1562
+ process.exit(0);
1563
+ });
1564
+ try {
1565
+ await dashboard.start();
1566
+ setTimeout(() => {
1567
+ const stats = dashboard.getStats();
1568
+ const traces = dashboard.getTraces();
1569
+ printBanner(config, traces.length, stats);
1570
+ }, 1500);
1571
+ } catch (error) {
1572
+ console.error("\u274C Failed to start dashboard:", error);
1573
+ process.exit(1);
1574
+ }
1575
+ }
1576
+ function printHelp() {
1577
+ console.log(`
1578
+ \u{1F4CA} AgentFlow Dashboard v${VERSION} \u2014 See your agents think.
1579
+
1580
+ Usage:
1581
+ agentflow-dashboard [options]
1582
+ npx agentflow-dashboard [options]
1583
+
1584
+ Options:
1585
+ -p, --port <number> Server port (default: 3000)
1586
+ -t, --traces <path> Traces directory (default: ./traces)
1587
+ -h, --host <address> Host address (default: localhost)
1588
+ --data-dir <path> Extra data directory for process discovery (repeatable)
1589
+ --cors Enable CORS headers
1590
+ --help Show this help message
1591
+
1592
+ Examples:
1593
+ agentflow-dashboard --traces ./traces --host 0.0.0.0 --cors
1594
+ agentflow-dashboard -p 8080 -t /var/log/agentflow
1595
+ agentflow-dashboard --traces ./traces --data-dir ./workers --data-dir ./cron
1596
+
1597
+ Tabs:
1598
+ \u{1F3AF} Graph Interactive Cytoscape.js execution graph
1599
+ \u23F1\uFE0F Timeline Waterfall view of node durations
1600
+ \u{1F4CA} Metrics Success rates, durations, node breakdown
1601
+ \u{1F6E0}\uFE0F Process Health PID files, systemd, workers, orphans
1602
+ \u26A0\uFE0F Errors Failed and hung nodes with metadata
1603
+ `);
1604
+ }
1605
+
1000
1606
  // src/server.ts
1001
1607
  var import_meta = {};
1002
1608
  var __filename = (0, import_url.fileURLToPath)(import_meta.url);
1003
- var __dirname = path2.dirname(__filename);
1609
+ var __dirname = path3.dirname(__filename);
1610
+ function serializeTrace(trace) {
1611
+ if (!trace) return trace;
1612
+ const obj = { ...trace };
1613
+ if (obj.nodes instanceof Map) {
1614
+ const nodesObj = {};
1615
+ for (const [key, value] of obj.nodes) {
1616
+ nodesObj[key] = value;
1617
+ }
1618
+ obj.nodes = nodesObj;
1619
+ }
1620
+ return obj;
1621
+ }
1004
1622
  var DashboardServer = class {
1005
1623
  constructor(config) {
1006
1624
  this.config = config;
@@ -1012,6 +1630,10 @@ var DashboardServer = class {
1012
1630
  this.setupExpress();
1013
1631
  this.setupWebSocket();
1014
1632
  this.setupTraceWatcher();
1633
+ for (const trace of this.watcher.getAllTraces()) {
1634
+ this.stats.processTrace(trace);
1635
+ }
1636
+ console.log(`Processed ${this.watcher.getTraceCount()} existing traces for stats`);
1015
1637
  }
1016
1638
  app = (0, import_express.default)();
1017
1639
  server = (0, import_http.createServer)(this.app);
@@ -1030,13 +1652,13 @@ var DashboardServer = class {
1030
1652
  next();
1031
1653
  });
1032
1654
  }
1033
- const publicDir = path2.join(__dirname, "../public");
1034
- if (fs2.existsSync(publicDir)) {
1655
+ const publicDir = path3.join(__dirname, "../public");
1656
+ if (fs3.existsSync(publicDir)) {
1035
1657
  this.app.use(import_express.default.static(publicDir));
1036
1658
  }
1037
1659
  this.app.get("/api/traces", (req, res) => {
1038
1660
  try {
1039
- const traces = this.watcher.getAllTraces();
1661
+ const traces = this.watcher.getAllTraces().map(serializeTrace);
1040
1662
  res.json(traces);
1041
1663
  } catch (error) {
1042
1664
  res.status(500).json({ error: "Failed to load traces" });
@@ -1048,7 +1670,7 @@ var DashboardServer = class {
1048
1670
  if (!trace) {
1049
1671
  return res.status(404).json({ error: "Trace not found" });
1050
1672
  }
1051
- res.json(trace);
1673
+ res.json(serializeTrace(trace));
1052
1674
  } catch (error) {
1053
1675
  res.status(500).json({ error: "Failed to load trace" });
1054
1676
  }
@@ -1103,14 +1725,62 @@ var DashboardServer = class {
1103
1725
  }
1104
1726
  const discoveryDirs = [
1105
1727
  this.config.tracesDir,
1106
- path2.dirname(this.config.tracesDir),
1728
+ path3.dirname(this.config.tracesDir),
1107
1729
  ...this.config.dataDirs || []
1108
1730
  ];
1109
1731
  const processConfig = (0, import_agentflow_core3.discoverProcessConfig)(discoveryDirs);
1110
1732
  if (!processConfig) {
1111
1733
  return res.json(null);
1112
1734
  }
1113
- const result = (0, import_agentflow_core3.auditProcesses)(processConfig);
1735
+ const alfredResult = (0, import_agentflow_core3.auditProcesses)(processConfig);
1736
+ const openclawConfig = {
1737
+ processName: "openclaw",
1738
+ pidFile: void 0,
1739
+ workersFile: void 0,
1740
+ systemdUnit: null
1741
+ };
1742
+ const openclawResult = (0, import_agentflow_core3.auditProcesses)(openclawConfig);
1743
+ const clawmetryConfig = {
1744
+ processName: "clawmetry",
1745
+ pidFile: void 0,
1746
+ workersFile: void 0,
1747
+ systemdUnit: null
1748
+ };
1749
+ const clawmetryResult = (0, import_agentflow_core3.auditProcesses)(clawmetryConfig);
1750
+ const allOsProcesses = [
1751
+ ...alfredResult.osProcesses,
1752
+ ...openclawResult.osProcesses,
1753
+ ...clawmetryResult.osProcesses
1754
+ ];
1755
+ const uniqueProcesses = allOsProcesses.filter(
1756
+ (proc, index, arr) => arr.findIndex((p) => p.pid === proc.pid) === index
1757
+ );
1758
+ const result = {
1759
+ ...alfredResult,
1760
+ osProcesses: uniqueProcesses,
1761
+ // Recalculate orphans based on all processes
1762
+ orphans: uniqueProcesses.filter((p) => {
1763
+ var _a;
1764
+ const alfredKnownPids = /* @__PURE__ */ new Set();
1765
+ if (((_a = alfredResult.pidFile) == null ? void 0 : _a.pid) && !alfredResult.pidFile.stale) alfredKnownPids.add(alfredResult.pidFile.pid);
1766
+ if (alfredResult.workers) {
1767
+ if (alfredResult.workers.orchestratorPid) alfredKnownPids.add(alfredResult.workers.orchestratorPid);
1768
+ for (const w of alfredResult.workers.workers) {
1769
+ if (w.pid) alfredKnownPids.add(w.pid);
1770
+ }
1771
+ }
1772
+ const isOpenClawProcess = p.cmdline.includes("openclaw") || p.cmdline.includes("clawmetry");
1773
+ return !alfredKnownPids.has(p.pid) && !isOpenClawProcess && p.pid !== process.pid && p.pid !== process.ppid;
1774
+ })
1775
+ };
1776
+ const openclawProblems = [];
1777
+ if (openclawResult.osProcesses.length === 0) {
1778
+ openclawProblems.push("No OpenClaw gateway processes detected");
1779
+ }
1780
+ if (clawmetryResult.osProcesses.length === 0) {
1781
+ openclawProblems.push("No clawmetry processes detected");
1782
+ }
1783
+ result.problems = [...alfredResult.problems || [], ...openclawProblems];
1114
1784
  this.processHealthCache = { result, ts: now };
1115
1785
  res.json(result);
1116
1786
  } catch (error) {
@@ -1118,8 +1788,8 @@ var DashboardServer = class {
1118
1788
  }
1119
1789
  });
1120
1790
  this.app.get("*", (req, res) => {
1121
- const indexPath = path2.join(__dirname, "../public/index.html");
1122
- if (fs2.existsSync(indexPath)) {
1791
+ const indexPath = path3.join(__dirname, "../public/index.html");
1792
+ if (fs3.existsSync(indexPath)) {
1123
1793
  res.sendFile(indexPath);
1124
1794
  } else {
1125
1795
  res.status(404).send("Dashboard not found - public files may not be built");
@@ -1133,7 +1803,7 @@ var DashboardServer = class {
1133
1803
  JSON.stringify({
1134
1804
  type: "init",
1135
1805
  data: {
1136
- traces: this.watcher.getAllTraces(),
1806
+ traces: this.watcher.getAllTraces().map(serializeTrace),
1137
1807
  stats: this.stats.getGlobalStats()
1138
1808
  }
1139
1809
  })
@@ -1151,14 +1821,14 @@ var DashboardServer = class {
1151
1821
  this.stats.processTrace(trace);
1152
1822
  this.broadcast({
1153
1823
  type: "trace-added",
1154
- data: trace
1824
+ data: serializeTrace(trace)
1155
1825
  });
1156
1826
  });
1157
1827
  this.watcher.on("trace-updated", (trace) => {
1158
1828
  this.stats.processTrace(trace);
1159
1829
  this.broadcast({
1160
1830
  type: "trace-updated",
1161
- data: trace
1831
+ data: serializeTrace(trace)
1162
1832
  });
1163
1833
  });
1164
1834
  this.watcher.on("stats-updated", () => {
@@ -1176,21 +1846,21 @@ var DashboardServer = class {
1176
1846
  });
1177
1847
  }
1178
1848
  async start() {
1179
- return new Promise((resolve2) => {
1849
+ return new Promise((resolve3) => {
1180
1850
  const host = this.config.host || "localhost";
1181
1851
  this.server.listen(this.config.port, host, () => {
1182
1852
  console.log(`AgentFlow Dashboard running at http://${host}:${this.config.port}`);
1183
1853
  console.log(`Watching traces in: ${this.config.tracesDir}`);
1184
- resolve2();
1854
+ resolve3();
1185
1855
  });
1186
1856
  });
1187
1857
  }
1188
1858
  async stop() {
1189
- return new Promise((resolve2) => {
1859
+ return new Promise((resolve3) => {
1190
1860
  this.watcher.stop();
1191
1861
  this.server.close(() => {
1192
1862
  console.log("Dashboard server stopped");
1193
- resolve2();
1863
+ resolve3();
1194
1864
  });
1195
1865
  });
1196
1866
  }
@@ -1201,6 +1871,9 @@ var DashboardServer = class {
1201
1871
  return this.watcher.getAllTraces();
1202
1872
  }
1203
1873
  };
1874
+ if (import_meta.url === `file://${process.argv[1]}`) {
1875
+ startDashboard().catch(console.error);
1876
+ }
1204
1877
  // Annotate the CommonJS export names for ESM import in node:
1205
1878
  0 && (module.exports = {
1206
1879
  AgentStats,