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/cli.cjs CHANGED
@@ -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) => {
@@ -1001,6 +1462,18 @@ var TraceWatcher = class extends import_events.EventEmitter {
1001
1462
  var import_meta = {};
1002
1463
  var __filename = (0, import_url.fileURLToPath)(import_meta.url);
1003
1464
  var __dirname = path2.dirname(__filename);
1465
+ function serializeTrace(trace) {
1466
+ if (!trace) return trace;
1467
+ const obj = { ...trace };
1468
+ if (obj.nodes instanceof Map) {
1469
+ const nodesObj = {};
1470
+ for (const [key, value] of obj.nodes) {
1471
+ nodesObj[key] = value;
1472
+ }
1473
+ obj.nodes = nodesObj;
1474
+ }
1475
+ return obj;
1476
+ }
1004
1477
  var DashboardServer = class {
1005
1478
  constructor(config) {
1006
1479
  this.config = config;
@@ -1012,6 +1485,10 @@ var DashboardServer = class {
1012
1485
  this.setupExpress();
1013
1486
  this.setupWebSocket();
1014
1487
  this.setupTraceWatcher();
1488
+ for (const trace of this.watcher.getAllTraces()) {
1489
+ this.stats.processTrace(trace);
1490
+ }
1491
+ console.log(`Processed ${this.watcher.getTraceCount()} existing traces for stats`);
1015
1492
  }
1016
1493
  app = (0, import_express.default)();
1017
1494
  server = (0, import_http.createServer)(this.app);
@@ -1036,7 +1513,7 @@ var DashboardServer = class {
1036
1513
  }
1037
1514
  this.app.get("/api/traces", (req, res) => {
1038
1515
  try {
1039
- const traces = this.watcher.getAllTraces();
1516
+ const traces = this.watcher.getAllTraces().map(serializeTrace);
1040
1517
  res.json(traces);
1041
1518
  } catch (error) {
1042
1519
  res.status(500).json({ error: "Failed to load traces" });
@@ -1048,7 +1525,7 @@ var DashboardServer = class {
1048
1525
  if (!trace) {
1049
1526
  return res.status(404).json({ error: "Trace not found" });
1050
1527
  }
1051
- res.json(trace);
1528
+ res.json(serializeTrace(trace));
1052
1529
  } catch (error) {
1053
1530
  res.status(500).json({ error: "Failed to load trace" });
1054
1531
  }
@@ -1110,7 +1587,55 @@ var DashboardServer = class {
1110
1587
  if (!processConfig) {
1111
1588
  return res.json(null);
1112
1589
  }
1113
- const result = (0, import_agentflow_core3.auditProcesses)(processConfig);
1590
+ const alfredResult = (0, import_agentflow_core3.auditProcesses)(processConfig);
1591
+ const openclawConfig = {
1592
+ processName: "openclaw",
1593
+ pidFile: void 0,
1594
+ workersFile: void 0,
1595
+ systemdUnit: null
1596
+ };
1597
+ const openclawResult = (0, import_agentflow_core3.auditProcesses)(openclawConfig);
1598
+ const clawmetryConfig = {
1599
+ processName: "clawmetry",
1600
+ pidFile: void 0,
1601
+ workersFile: void 0,
1602
+ systemdUnit: null
1603
+ };
1604
+ const clawmetryResult = (0, import_agentflow_core3.auditProcesses)(clawmetryConfig);
1605
+ const allOsProcesses = [
1606
+ ...alfredResult.osProcesses,
1607
+ ...openclawResult.osProcesses,
1608
+ ...clawmetryResult.osProcesses
1609
+ ];
1610
+ const uniqueProcesses = allOsProcesses.filter(
1611
+ (proc, index, arr) => arr.findIndex((p) => p.pid === proc.pid) === index
1612
+ );
1613
+ const result = {
1614
+ ...alfredResult,
1615
+ osProcesses: uniqueProcesses,
1616
+ // Recalculate orphans based on all processes
1617
+ orphans: uniqueProcesses.filter((p) => {
1618
+ var _a;
1619
+ const alfredKnownPids = /* @__PURE__ */ new Set();
1620
+ if (((_a = alfredResult.pidFile) == null ? void 0 : _a.pid) && !alfredResult.pidFile.stale) alfredKnownPids.add(alfredResult.pidFile.pid);
1621
+ if (alfredResult.workers) {
1622
+ if (alfredResult.workers.orchestratorPid) alfredKnownPids.add(alfredResult.workers.orchestratorPid);
1623
+ for (const w of alfredResult.workers.workers) {
1624
+ if (w.pid) alfredKnownPids.add(w.pid);
1625
+ }
1626
+ }
1627
+ const isOpenClawProcess = p.cmdline.includes("openclaw") || p.cmdline.includes("clawmetry");
1628
+ return !alfredKnownPids.has(p.pid) && !isOpenClawProcess && p.pid !== process.pid && p.pid !== process.ppid;
1629
+ })
1630
+ };
1631
+ const openclawProblems = [];
1632
+ if (openclawResult.osProcesses.length === 0) {
1633
+ openclawProblems.push("No OpenClaw gateway processes detected");
1634
+ }
1635
+ if (clawmetryResult.osProcesses.length === 0) {
1636
+ openclawProblems.push("No clawmetry processes detected");
1637
+ }
1638
+ result.problems = [...alfredResult.problems || [], ...openclawProblems];
1114
1639
  this.processHealthCache = { result, ts: now };
1115
1640
  res.json(result);
1116
1641
  } catch (error) {
@@ -1133,7 +1658,7 @@ var DashboardServer = class {
1133
1658
  JSON.stringify({
1134
1659
  type: "init",
1135
1660
  data: {
1136
- traces: this.watcher.getAllTraces(),
1661
+ traces: this.watcher.getAllTraces().map(serializeTrace),
1137
1662
  stats: this.stats.getGlobalStats()
1138
1663
  }
1139
1664
  })
@@ -1151,14 +1676,14 @@ var DashboardServer = class {
1151
1676
  this.stats.processTrace(trace);
1152
1677
  this.broadcast({
1153
1678
  type: "trace-added",
1154
- data: trace
1679
+ data: serializeTrace(trace)
1155
1680
  });
1156
1681
  });
1157
1682
  this.watcher.on("trace-updated", (trace) => {
1158
1683
  this.stats.processTrace(trace);
1159
1684
  this.broadcast({
1160
1685
  type: "trace-updated",
1161
- data: trace
1686
+ data: serializeTrace(trace)
1162
1687
  });
1163
1688
  });
1164
1689
  this.watcher.on("stats-updated", () => {
@@ -1201,9 +1726,12 @@ var DashboardServer = class {
1201
1726
  return this.watcher.getAllTraces();
1202
1727
  }
1203
1728
  };
1729
+ if (import_meta.url === `file://${process.argv[1]}`) {
1730
+ startDashboard().catch(console.error);
1731
+ }
1204
1732
 
1205
1733
  // src/cli.ts
1206
- var VERSION = "0.2.2";
1734
+ var VERSION = "0.4.0";
1207
1735
  function getLanAddress() {
1208
1736
  const interfaces = os.networkInterfaces();
1209
1737
  for (const name of Object.keys(interfaces)) {