agentflow-dashboard 0.2.0 → 0.3.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
@@ -33,6 +33,7 @@ __export(cli_exports, {
33
33
  });
34
34
  module.exports = __toCommonJS(cli_exports);
35
35
  var fs3 = __toESM(require("fs"), 1);
36
+ var os = __toESM(require("os"), 1);
36
37
  var path3 = __toESM(require("path"), 1);
37
38
 
38
39
  // src/server.ts
@@ -226,14 +227,23 @@ var import_events = require("events");
226
227
  var fs = __toESM(require("fs"), 1);
227
228
  var path = __toESM(require("path"), 1);
228
229
  var TraceWatcher = class extends import_events.EventEmitter {
229
- watcher;
230
+ watchers = [];
230
231
  traces = /* @__PURE__ */ new Map();
231
232
  tracesDir;
232
- constructor(tracesDir) {
233
+ dataDirs;
234
+ allWatchDirs;
235
+ constructor(tracesDirOrOptions) {
233
236
  super();
234
- this.tracesDir = path.resolve(tracesDir);
237
+ if (typeof tracesDirOrOptions === "string") {
238
+ this.tracesDir = path.resolve(tracesDirOrOptions);
239
+ this.dataDirs = [];
240
+ } else {
241
+ this.tracesDir = path.resolve(tracesDirOrOptions.tracesDir);
242
+ this.dataDirs = (tracesDirOrOptions.dataDirs || []).map((d) => path.resolve(d));
243
+ }
244
+ this.allWatchDirs = [this.tracesDir, ...this.dataDirs];
235
245
  this.ensureTracesDir();
236
- this.loadExistingTraces();
246
+ this.loadExistingFiles();
237
247
  this.startWatching();
238
248
  }
239
249
  ensureTracesDir() {
@@ -242,18 +252,288 @@ var TraceWatcher = class extends import_events.EventEmitter {
242
252
  console.log(`Created traces directory: ${this.tracesDir}`);
243
253
  }
244
254
  }
245
- loadExistingTraces() {
255
+ loadExistingFiles() {
256
+ let totalFiles = 0;
257
+ for (const dir of this.allWatchDirs) {
258
+ if (!fs.existsSync(dir)) continue;
259
+ 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
+ }
265
+ } catch (error) {
266
+ console.error(`Error scanning directory ${dir}:`, error);
267
+ }
268
+ }
269
+ console.log(`Scanned ${this.allWatchDirs.length} directories, loaded ${this.traces.size} items from ${totalFiles} files`);
270
+ }
271
+ /** Load a .json trace, .jsonl session file, or .log file. */
272
+ loadFile(filePath) {
273
+ if (filePath.endsWith(".jsonl")) {
274
+ return this.loadSessionFile(filePath);
275
+ }
276
+ if (filePath.endsWith(".log") || filePath.endsWith(".trace")) {
277
+ return this.loadLogFile(filePath);
278
+ }
279
+ return this.loadTraceFile(filePath);
280
+ }
281
+ loadLogFile(filePath) {
246
282
  try {
247
- const files = fs.readdirSync(this.tracesDir).filter((file) => file.endsWith(".json"));
248
- console.log(`Loading ${files.length} existing trace files...`);
249
- for (const file of files) {
250
- this.loadTraceFile(path.join(this.tracesDir, file));
283
+ const content = fs.readFileSync(filePath, "utf8");
284
+ const filename = path.basename(filePath);
285
+ const stats = fs.statSync(filePath);
286
+ const traces = this.parseUniversalLog(content, filename, filePath);
287
+ for (let i = 0; i < traces.length; i++) {
288
+ const trace = traces[i];
289
+ trace.filename = filename;
290
+ trace.lastModified = stats.mtime.getTime();
291
+ trace.sourceType = "trace";
292
+ trace.sourceDir = path.dirname(filePath);
293
+ const key = traces.length === 1 ? this.traceKey(filePath) : `${this.traceKey(filePath)}-${i}`;
294
+ this.traces.set(key, trace);
251
295
  }
252
- console.log(`Loaded ${this.traces.size} traces`);
296
+ return traces.length > 0;
253
297
  } catch (error) {
254
- console.error("Error loading existing traces:", error);
298
+ console.error(`Error loading log file ${filePath}:`, error);
299
+ return false;
255
300
  }
256
301
  }
302
+ /** Universal log parser - detects agent activities from any system */
303
+ parseUniversalLog(content, filename, filePath) {
304
+ const lines = content.split("\n").filter((line) => line.trim());
305
+ const activities = /* @__PURE__ */ new Map();
306
+ for (const line of lines) {
307
+ const activity = this.detectActivityPattern(line);
308
+ if (!activity) continue;
309
+ const sessionId = this.extractSessionIdentifier(activity);
310
+ if (!activities.has(sessionId)) {
311
+ activities.set(sessionId, {
312
+ id: sessionId,
313
+ rootNodeId: "",
314
+ agentId: this.detectAgentIdentifier(activity, filename, filePath),
315
+ name: this.generateActivityName(activity, sessionId),
316
+ trigger: this.detectTrigger(activity),
317
+ startTime: activity.timestamp,
318
+ endTime: activity.timestamp,
319
+ status: "completed",
320
+ nodes: {},
321
+ edges: [],
322
+ events: [],
323
+ metadata: { sessionId, source: filename }
324
+ });
325
+ }
326
+ const session = activities.get(sessionId);
327
+ this.addActivityNode(session, activity);
328
+ if (activity.timestamp > session.endTime) {
329
+ session.endTime = activity.timestamp;
330
+ }
331
+ }
332
+ const traces = Array.from(activities.values()).filter(
333
+ (session) => Object.keys(session.nodes).length > 0
334
+ );
335
+ if (traces.length === 0) {
336
+ const stats = fs.statSync(filePath);
337
+ traces.push({
338
+ id: "",
339
+ rootNodeId: "root",
340
+ nodes: {
341
+ "root": {
342
+ id: "root",
343
+ type: "log-file",
344
+ name: filename,
345
+ status: "completed",
346
+ startTime: stats.mtime.getTime(),
347
+ endTime: stats.mtime.getTime(),
348
+ metadata: { lineCount: lines.length, path: filePath }
349
+ }
350
+ },
351
+ edges: [],
352
+ startTime: stats.mtime.getTime(),
353
+ endTime: stats.mtime.getTime(),
354
+ status: "completed",
355
+ trigger: "file",
356
+ agentId: this.extractAgentFromPath(filePath),
357
+ events: [],
358
+ metadata: { type: "file-trace" }
359
+ });
360
+ }
361
+ return traces;
362
+ }
363
+ /** Detect activity patterns in log lines using universal heuristics */
364
+ detectActivityPattern(line) {
365
+ let timestamp = this.extractTimestamp(line);
366
+ let level = this.extractLogLevel(line);
367
+ let action = this.extractAction(line);
368
+ let kvPairs = this.extractKeyValuePairs(line);
369
+ if (!timestamp) {
370
+ const jsonMatch = line.match(/\{.*\}/);
371
+ if (jsonMatch) {
372
+ try {
373
+ const parsed = JSON.parse(jsonMatch[0]);
374
+ timestamp = this.parseTimestamp(parsed.timestamp || parsed.time || parsed.ts) || Date.now();
375
+ level = parsed.level || parsed.severity || "info";
376
+ action = parsed.action || parsed.event || parsed.message || "";
377
+ kvPairs = parsed;
378
+ } catch {
379
+ }
380
+ }
381
+ }
382
+ if (!timestamp) {
383
+ const kvMatches = line.match(/(\w+)=([^\s]+)/g);
384
+ if (kvMatches && kvMatches.length >= 2) {
385
+ const pairs = {};
386
+ kvMatches.forEach((match) => {
387
+ const [key, value] = match.split("=", 2);
388
+ pairs[key] = this.parseValue(value);
389
+ });
390
+ timestamp = this.parseTimestamp(pairs.timestamp || pairs.time) || Date.now();
391
+ level = pairs.level || "info";
392
+ action = pairs.action || pairs.event || "";
393
+ kvPairs = pairs;
394
+ }
395
+ }
396
+ if (!timestamp) {
397
+ const logMatch = line.match(/^(\d{4}-\d{2}-\d{2}[T\s]\d{2}:\d{2}:\d{2}[.\d]*Z?)\s+(\w+)?\s*:?\s*(.+)/);
398
+ if (logMatch) {
399
+ timestamp = new Date(logMatch[1]).getTime();
400
+ level = logMatch[2] || "info";
401
+ action = logMatch[3] || "";
402
+ }
403
+ }
404
+ if (!timestamp) return null;
405
+ return {
406
+ timestamp,
407
+ level: (level == null ? void 0 : level.toLowerCase()) || "info",
408
+ action,
409
+ component: this.detectComponent(action, kvPairs),
410
+ operation: this.detectOperation(action, kvPairs),
411
+ ...kvPairs
412
+ };
413
+ }
414
+ 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?)/);
418
+ if (isoMatch) return new Date(isoMatch[1]).getTime();
419
+ return null;
420
+ }
421
+ 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;
426
+ }
427
+ 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] || "";
432
+ }
433
+ extractKeyValuePairs(line) {
434
+ const pairs = {};
435
+ const coloredRegex = /\[36m(\w+)\[0m=\[35m([^\[]+?)\[0m/g;
436
+ let match;
437
+ while ((match = coloredRegex.exec(line)) !== null) {
438
+ pairs[match[1]] = this.parseValue(match[2]);
439
+ }
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
+ return pairs;
447
+ }
448
+ parseValue(value) {
449
+ if (value.match(/^\d+$/)) return parseInt(value);
450
+ if (value.match(/^\d+\.\d+$/)) return parseFloat(value);
451
+ if (value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
452
+ if (value.startsWith('"') && value.endsWith('"')) return value.slice(1, -1);
453
+ return value;
454
+ }
455
+ parseTimestamp(value) {
456
+ if (!value) return null;
457
+ if (typeof value === "number") return value;
458
+ try {
459
+ return new Date(value).getTime();
460
+ } catch {
461
+ return null;
462
+ }
463
+ }
464
+ detectComponent(action, kvPairs) {
465
+ if (action.includes(".")) return action.split(".")[0];
466
+ if (kvPairs.component) return kvPairs.component;
467
+ if (kvPairs.service) return kvPairs.service;
468
+ if (kvPairs.module) return kvPairs.module;
469
+ return "unknown";
470
+ }
471
+ detectOperation(action, kvPairs) {
472
+ if (action.includes(".")) return action.split(".").slice(1).join(".");
473
+ if (kvPairs.operation) return kvPairs.operation;
474
+ if (kvPairs.method) return kvPairs.method;
475
+ return action || "activity";
476
+ }
477
+ extractSessionIdentifier(activity) {
478
+ return activity.run_id || activity.session_id || activity.request_id || activity.trace_id || activity.sweep_id || activity.transaction_id || "default";
479
+ }
480
+ 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}`;
485
+ }
486
+ return activity.component;
487
+ }
488
+ return this.extractAgentFromPath(filePath);
489
+ }
490
+ extractAgentFromPath(filePath) {
491
+ const filename = path.basename(filePath, path.extname(filePath));
492
+ const pathParts = filePath.split(path.sep);
493
+ for (const part of pathParts.reverse()) {
494
+ if (part.match(/agent|worker|service|daemon|bot|ai|llm/i)) {
495
+ return part;
496
+ }
497
+ }
498
+ return filename;
499
+ }
500
+ generateActivityName(activity, sessionId) {
501
+ const component = activity.component !== "unknown" ? activity.component : "Activity";
502
+ const operation = activity.operation !== "activity" ? `: ${activity.operation}` : "";
503
+ return `${component}${operation} (${sessionId})`;
504
+ }
505
+ detectTrigger(activity) {
506
+ var _a, _b;
507
+ if (activity.trigger) return activity.trigger;
508
+ if (activity.method && activity.url) return "api-call";
509
+ if ((_a = activity.operation) == null ? void 0 : _a.includes("start")) return "startup";
510
+ if ((_b = activity.operation) == null ? void 0 : _b.includes("invoke")) return "invocation";
511
+ return "event";
512
+ }
513
+ addActivityNode(session, activity) {
514
+ const nodeId = `${activity.component}-${activity.operation}-${activity.timestamp}`;
515
+ const node = {
516
+ id: nodeId,
517
+ type: activity.component,
518
+ name: `${activity.component}: ${activity.operation}`,
519
+ status: this.getUniversalNodeStatus(activity),
520
+ startTime: activity.timestamp,
521
+ endTime: activity.timestamp,
522
+ metadata: activity
523
+ };
524
+ session.nodes[nodeId] = node;
525
+ if (!session.rootNodeId) {
526
+ session.rootNodeId = nodeId;
527
+ }
528
+ }
529
+ getUniversalNodeStatus(activity) {
530
+ var _a, _b;
531
+ if (activity.level === "error" || activity.level === "fatal") return "failed";
532
+ if (activity.level === "warn" || activity.level === "warning") return "warning";
533
+ if ((_a = activity.operation) == null ? void 0 : _a.match(/start|begin|init/i)) return "running";
534
+ if ((_b = activity.operation) == null ? void 0 : _b.match(/complete|finish|end|done/i)) return "completed";
535
+ return "completed";
536
+ }
257
537
  loadTraceFile(filePath) {
258
538
  try {
259
539
  const content = fs.readFileSync(filePath, "utf8");
@@ -262,55 +542,404 @@ var TraceWatcher = class extends import_events.EventEmitter {
262
542
  const stats = fs.statSync(filePath);
263
543
  graph.filename = filename;
264
544
  graph.lastModified = stats.mtime.getTime();
265
- this.traces.set(filename, graph);
545
+ graph.sourceType = "trace";
546
+ graph.sourceDir = path.dirname(filePath);
547
+ this.traces.set(this.traceKey(filePath), graph);
266
548
  return true;
267
- } catch (error) {
268
- console.error(`Error loading trace file ${filePath}:`, error);
549
+ } catch {
269
550
  return false;
270
551
  }
271
552
  }
272
- startWatching() {
273
- this.watcher = import_chokidar.default.watch(this.tracesDir, {
274
- ignored: /^\./,
275
- persistent: true,
276
- ignoreInitial: true
277
- });
278
- this.watcher.on("add", (filePath) => {
279
- if (filePath.endsWith(".json")) {
280
- console.log(`New trace file: ${path.basename(filePath)}`);
281
- if (this.loadTraceFile(filePath)) {
282
- const filename = path.basename(filePath);
283
- const trace = this.traces.get(filename);
284
- if (trace) {
285
- this.emit("trace-added", trace);
286
- }
553
+ /** Parse a JSONL session log into a WatchedTrace (best-effort). */
554
+ loadSessionFile(filePath) {
555
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
556
+ try {
557
+ const content = fs.readFileSync(filePath, "utf8");
558
+ const lines = content.split("\n").filter((l) => l.trim());
559
+ if (lines.length === 0) return false;
560
+ const rawEvents = [];
561
+ for (const line of lines) {
562
+ try {
563
+ rawEvents.push(JSON.parse(line));
564
+ } catch {
287
565
  }
288
566
  }
289
- });
290
- this.watcher.on("change", (filePath) => {
291
- if (filePath.endsWith(".json")) {
292
- console.log(`Trace file updated: ${path.basename(filePath)}`);
293
- if (this.loadTraceFile(filePath)) {
294
- const filename = path.basename(filePath);
295
- const trace = this.traces.get(filename);
296
- if (trace) {
297
- this.emit("trace-updated", trace);
567
+ if (rawEvents.length === 0) return false;
568
+ const sessionEvent = rawEvents.find((e) => e.type === "session");
569
+ const sessionId = (sessionEvent == null ? void 0 : sessionEvent.id) || path.basename(filePath, ".jsonl");
570
+ const sessionTimestamp = (sessionEvent == null ? void 0 : sessionEvent.timestamp) || ((_a = rawEvents[0]) == null ? void 0 : _a.timestamp);
571
+ const startTime = sessionTimestamp ? new Date(sessionTimestamp).getTime() : 0;
572
+ if (!startTime) return false;
573
+ const parentDir = path.basename(path.dirname(filePath));
574
+ const grandParentDir = path.basename(path.dirname(path.dirname(filePath)));
575
+ const agentId = grandParentDir === "agents" ? parentDir : parentDir;
576
+ const modelEvent = rawEvents.find((e) => e.type === "model_change");
577
+ const provider = (modelEvent == null ? void 0 : modelEvent.provider) || "";
578
+ const modelId = (modelEvent == null ? void 0 : modelEvent.modelId) || "";
579
+ const nodes = /* @__PURE__ */ new Map();
580
+ let lastTimestamp = startTime;
581
+ for (const evt of rawEvents) {
582
+ if (evt.timestamp) {
583
+ const ts = new Date(evt.timestamp).getTime();
584
+ if (ts > lastTimestamp) lastTimestamp = ts;
585
+ }
586
+ }
587
+ const firstMessage = rawEvents.find((e) => {
588
+ var _a2;
589
+ return e.type === "message" && ((_a2 = e.message) == null ? void 0 : _a2.role) === "user";
590
+ });
591
+ const userPrompt = ((_d = (_c = (_b = firstMessage == null ? void 0 : firstMessage.message) == null ? void 0 : _b.content) == null ? void 0 : _c[0]) == null ? void 0 : _d.text) || "";
592
+ const cronMatch = userPrompt.match(/\[cron:(\S+)\s+([^\]]+)\]/);
593
+ const triggerName = cronMatch ? cronMatch[2] : "";
594
+ const trigger = cronMatch ? "cron" : "message";
595
+ let totalInputTokens = 0;
596
+ let totalOutputTokens = 0;
597
+ let totalTokensSum = 0;
598
+ let totalCost = 0;
599
+ let userMessageCount = 0;
600
+ let assistantMessageCount = 0;
601
+ let toolCallCount = 0;
602
+ let thinkingBlockCount = 0;
603
+ const sessionEvents = [];
604
+ const toolCallMap = /* @__PURE__ */ new Map();
605
+ const rootId = `session-${sessionId.slice(0, 8)}`;
606
+ const rootName = triggerName || userPrompt.slice(0, 80) + (userPrompt.length > 80 ? "..." : "") || sessionId;
607
+ for (const evt of rawEvents) {
608
+ const evtTs = evt.timestamp ? new Date(evt.timestamp).getTime() : startTime;
609
+ if (evt.type === "session") {
610
+ sessionEvents.push({
611
+ type: "system",
612
+ timestamp: evtTs,
613
+ name: "Session Started",
614
+ content: `Version: ${evt.version || "unknown"}, CWD: ${evt.cwd || ""}`,
615
+ id: evt.id
616
+ });
617
+ continue;
618
+ }
619
+ if (evt.type === "model_change") {
620
+ sessionEvents.push({
621
+ type: "model_change",
622
+ timestamp: evtTs,
623
+ name: "Model Change",
624
+ model: evt.modelId,
625
+ provider: evt.provider,
626
+ content: `${evt.provider}/${evt.modelId}`,
627
+ id: evt.id
628
+ });
629
+ continue;
630
+ }
631
+ if (evt.type === "thinking_level_change") {
632
+ sessionEvents.push({
633
+ type: "system",
634
+ timestamp: evtTs,
635
+ name: "Thinking Level",
636
+ content: evt.thinkingLevel || "",
637
+ id: evt.id
638
+ });
639
+ continue;
640
+ }
641
+ if (evt.type === "custom" && evt.customType === "model-snapshot") {
642
+ sessionEvents.push({
643
+ type: "system",
644
+ timestamp: evtTs,
645
+ name: "Model Snapshot",
646
+ content: JSON.stringify(evt.data || {}).slice(0, 200),
647
+ id: evt.id
648
+ });
649
+ continue;
650
+ }
651
+ if (evt.type === "custom_message" && evt.customType === "openclaw.sessions_yield") {
652
+ sessionEvents.push({
653
+ type: "spawn",
654
+ timestamp: evtTs,
655
+ name: "Subagent Spawn",
656
+ content: ((_e = evt.data) == null ? void 0 : _e.sessionId) || "",
657
+ id: evt.id,
658
+ parentId: evt.parentId
659
+ });
660
+ const spawnId = `spawn-${toolCallCount + thinkingBlockCount + 1}`;
661
+ nodes.set(spawnId, {
662
+ id: spawnId,
663
+ type: "subagent",
664
+ name: "Subagent: " + (((_f = evt.data) == null ? void 0 : _f.sessionId) || "").slice(0, 12),
665
+ startTime: evtTs,
666
+ endTime: evtTs,
667
+ status: "completed",
668
+ parentId: rootId,
669
+ children: [],
670
+ metadata: { sessionId: (_g = evt.data) == null ? void 0 : _g.sessionId }
671
+ });
672
+ continue;
673
+ }
674
+ if (evt.type === "message" && evt.message) {
675
+ const msg = evt.message;
676
+ const role = msg.role;
677
+ const contentBlocks = Array.isArray(msg.content) ? msg.content : [];
678
+ if (role === "user") {
679
+ userMessageCount++;
680
+ const textContent = contentBlocks.filter((b) => b.type === "text").map((b) => b.text || "").join("\n");
681
+ sessionEvents.push({
682
+ type: "user",
683
+ timestamp: evtTs,
684
+ name: "User Message",
685
+ content: textContent,
686
+ id: evt.id,
687
+ parentId: evt.parentId
688
+ });
689
+ }
690
+ if (role === "assistant") {
691
+ assistantMessageCount++;
692
+ if (msg.usage) {
693
+ const u = msg.usage;
694
+ totalInputTokens += u.input || 0;
695
+ totalOutputTokens += u.output || 0;
696
+ totalTokensSum += u.totalTokens || 0;
697
+ if ((_h = u.cost) == null ? void 0 : _h.total) totalCost += u.cost.total;
698
+ }
699
+ for (const block of contentBlocks) {
700
+ if (block.type === "text" && block.text) {
701
+ sessionEvents.push({
702
+ type: "assistant",
703
+ timestamp: evtTs,
704
+ name: "Assistant",
705
+ content: block.text,
706
+ id: evt.id,
707
+ parentId: evt.parentId,
708
+ tokens: msg.usage ? {
709
+ input: msg.usage.input || 0,
710
+ output: msg.usage.output || 0,
711
+ total: msg.usage.totalTokens || 0,
712
+ cost: (_i = msg.usage.cost) == null ? void 0 : _i.total
713
+ } : void 0,
714
+ model: modelId,
715
+ provider
716
+ });
717
+ }
718
+ if (block.type === "thinking" && block.thinking) {
719
+ thinkingBlockCount++;
720
+ const thinkId = `thinking-${thinkingBlockCount}`;
721
+ sessionEvents.push({
722
+ type: "thinking",
723
+ timestamp: evtTs,
724
+ name: "Thinking",
725
+ content: block.thinking,
726
+ id: thinkId,
727
+ parentId: evt.id
728
+ });
729
+ nodes.set(thinkId, {
730
+ id: thinkId,
731
+ type: "decision",
732
+ name: "Thinking",
733
+ startTime: evtTs,
734
+ endTime: evtTs,
735
+ status: "completed",
736
+ parentId: rootId,
737
+ children: [],
738
+ metadata: { preview: block.thinking.slice(0, 100) }
739
+ });
740
+ }
741
+ if (block.type === "toolCall") {
742
+ toolCallCount++;
743
+ const toolName = block.name || "unknown";
744
+ const toolId = `tool-${toolCallCount}`;
745
+ const toolCallId = block.id || toolId;
746
+ sessionEvents.push({
747
+ type: "tool_call",
748
+ timestamp: evtTs,
749
+ name: toolName,
750
+ toolName,
751
+ toolArgs: block.arguments,
752
+ id: toolCallId,
753
+ parentId: evt.id
754
+ });
755
+ toolCallMap.set(toolCallId, sessionEvents.length - 1);
756
+ nodes.set(toolId, {
757
+ id: toolId,
758
+ type: "tool",
759
+ name: toolName,
760
+ startTime: evtTs,
761
+ endTime: evtTs,
762
+ // updated when result arrives
763
+ status: "running",
764
+ parentId: rootId,
765
+ children: [],
766
+ metadata: {
767
+ toolCallId,
768
+ args: block.arguments
769
+ }
770
+ });
771
+ }
772
+ }
773
+ }
774
+ if (role === "toolResult") {
775
+ const toolCallId = ((_j = contentBlocks[0]) == null ? void 0 : _j.toolCallId) || evt.parentId;
776
+ const resultContent = contentBlocks.map((b) => b.text || b.content || "").join("\n");
777
+ const hasError = contentBlocks.some((b) => b.isError || b.error);
778
+ const errorText = hasError ? resultContent : void 0;
779
+ sessionEvents.push({
780
+ type: "tool_result",
781
+ timestamp: evtTs,
782
+ name: "Tool Result",
783
+ toolResult: resultContent.slice(0, 2e3),
784
+ toolError: errorText == null ? void 0 : errorText.slice(0, 500),
785
+ id: evt.id,
786
+ parentId: toolCallId
787
+ });
788
+ for (const [nodeId, node] of nodes) {
789
+ if (node.type === "tool" && ((_k = node.metadata) == null ? void 0 : _k.toolCallId) === toolCallId) {
790
+ node.endTime = evtTs;
791
+ node.status = hasError ? "failed" : "completed";
792
+ if (hasError) node.metadata.error = errorText == null ? void 0 : errorText.slice(0, 500);
793
+ const callIdx = toolCallMap.get(toolCallId);
794
+ if (callIdx !== void 0 && sessionEvents[callIdx]) {
795
+ const callTs = sessionEvents[callIdx].timestamp;
796
+ sessionEvents[sessionEvents.length - 1].duration = evtTs - callTs;
797
+ sessionEvents[callIdx].duration = evtTs - callTs;
798
+ }
799
+ break;
800
+ }
801
+ }
298
802
  }
299
803
  }
300
804
  }
301
- });
302
- this.watcher.on("unlink", (filePath) => {
303
- if (filePath.endsWith(".json")) {
304
- const filename = path.basename(filePath);
305
- console.log(`Trace file removed: ${filename}`);
306
- this.traces.delete(filename);
307
- this.emit("trace-removed", filename);
805
+ const fileStat = fs.statSync(filePath);
806
+ const fileAge = Date.now() - fileStat.mtime.getTime();
807
+ const lastEvt = rawEvents[rawEvents.length - 1];
808
+ const hasToolError = sessionEvents.some((e) => e.type === "tool_result" && e.toolError);
809
+ const lastIsAssistant = (lastEvt == null ? void 0 : lastEvt.type) === "message" && ((_l = lastEvt == null ? void 0 : lastEvt.message) == null ? void 0 : _l.role) === "assistant";
810
+ const isRecentlyModified = fileAge < 5 * 60 * 1e3;
811
+ let status;
812
+ if (hasToolError) {
813
+ status = "failed";
814
+ } else if (lastIsAssistant) {
815
+ status = "completed";
816
+ } else if (isRecentlyModified) {
817
+ status = "running";
818
+ } else {
819
+ status = "completed";
308
820
  }
309
- });
310
- this.watcher.on("error", (error) => {
311
- console.error("Trace watcher error:", error);
312
- });
313
- console.log(`Started watching traces directory: ${this.tracesDir}`);
821
+ const tokenUsage = {
822
+ input: totalInputTokens,
823
+ output: totalOutputTokens,
824
+ total: totalTokensSum || totalInputTokens + totalOutputTokens,
825
+ cost: totalCost
826
+ };
827
+ nodes.set(rootId, {
828
+ id: rootId,
829
+ type: "agent",
830
+ name: rootName,
831
+ startTime,
832
+ endTime: lastTimestamp,
833
+ status,
834
+ parentId: void 0,
835
+ children: Array.from(nodes.keys()).filter((k) => k !== rootId),
836
+ metadata: {
837
+ provider,
838
+ model: modelId,
839
+ sessionId,
840
+ trigger,
841
+ totalTokens: tokenUsage.total,
842
+ inputTokens: tokenUsage.input,
843
+ outputTokens: tokenUsage.output,
844
+ cost: tokenUsage.cost,
845
+ userMessages: userMessageCount,
846
+ assistantMessages: assistantMessageCount,
847
+ toolCalls: toolCallCount,
848
+ thinkingBlocks: thinkingBlockCount,
849
+ "gen_ai.system": provider,
850
+ "gen_ai.request.model": modelId
851
+ }
852
+ });
853
+ const filename = path.basename(filePath);
854
+ const trace = {
855
+ id: sessionId,
856
+ nodes,
857
+ edges: [],
858
+ events: [],
859
+ startTime,
860
+ agentId,
861
+ trigger,
862
+ name: rootName,
863
+ traceId: sessionId,
864
+ spanId: sessionId,
865
+ filename,
866
+ lastModified: fileStat.mtime.getTime(),
867
+ sourceType: "session",
868
+ sourceDir: path.dirname(filePath),
869
+ sessionEvents,
870
+ tokenUsage,
871
+ metadata: {
872
+ provider,
873
+ model: modelId,
874
+ userMessages: userMessageCount,
875
+ assistantMessages: assistantMessageCount,
876
+ toolCalls: toolCallCount,
877
+ thinkingBlocks: thinkingBlockCount,
878
+ totalEvents: rawEvents.length,
879
+ sessionVersion: sessionEvent == null ? void 0 : sessionEvent.version
880
+ }
881
+ };
882
+ this.traces.set(this.traceKey(filePath), trace);
883
+ return true;
884
+ } catch {
885
+ return false;
886
+ }
887
+ }
888
+ /** Unique key for a file across directories. */
889
+ traceKey(filePath) {
890
+ for (const dir of this.allWatchDirs) {
891
+ if (filePath.startsWith(dir)) {
892
+ return path.relative(dir, filePath).replace(/\\/g, "/") + "@" + path.basename(dir);
893
+ }
894
+ }
895
+ return filePath;
896
+ }
897
+ startWatching() {
898
+ for (const dir of this.allWatchDirs) {
899
+ if (!fs.existsSync(dir)) continue;
900
+ const watcher = import_chokidar.default.watch(dir, {
901
+ ignored: /^\./,
902
+ persistent: true,
903
+ ignoreInitial: true,
904
+ depth: 0
905
+ // don't recurse into subdirectories
906
+ });
907
+ 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)}`);
910
+ if (this.loadFile(filePath)) {
911
+ const key = this.traceKey(filePath);
912
+ const trace = this.traces.get(key);
913
+ if (trace) {
914
+ this.emit("trace-added", trace);
915
+ }
916
+ }
917
+ }
918
+ });
919
+ watcher.on("change", (filePath) => {
920
+ if (filePath.endsWith(".json") || filePath.endsWith(".jsonl") || filePath.endsWith(".log") || filePath.endsWith(".trace")) {
921
+ if (this.loadFile(filePath)) {
922
+ const key = this.traceKey(filePath);
923
+ const trace = this.traces.get(key);
924
+ if (trace) {
925
+ this.emit("trace-updated", trace);
926
+ }
927
+ }
928
+ }
929
+ });
930
+ watcher.on("unlink", (filePath) => {
931
+ if (filePath.endsWith(".json") || filePath.endsWith(".jsonl") || filePath.endsWith(".log") || filePath.endsWith(".trace")) {
932
+ const key = this.traceKey(filePath);
933
+ this.traces.delete(key);
934
+ this.emit("trace-removed", key);
935
+ }
936
+ });
937
+ watcher.on("error", (error) => {
938
+ console.error(`Watcher error on ${dir}:`, error);
939
+ });
940
+ this.watchers.push(watcher);
941
+ }
942
+ console.log(`Watching ${this.allWatchDirs.length} directories for JSON/JSONL files`);
314
943
  }
315
944
  getAllTraces() {
316
945
  return Array.from(this.traces.values()).sort((a, b) => {
@@ -318,7 +947,14 @@ var TraceWatcher = class extends import_events.EventEmitter {
318
947
  });
319
948
  }
320
949
  getTrace(filename) {
321
- return this.traces.get(filename);
950
+ const exact = this.traces.get(filename);
951
+ if (exact) return exact;
952
+ for (const [key, trace] of this.traces) {
953
+ if (trace.filename === filename || key.endsWith(filename)) {
954
+ return trace;
955
+ }
956
+ }
957
+ return void 0;
322
958
  }
323
959
  getTracesByAgent(agentId) {
324
960
  return this.getAllTraces().filter((trace) => trace.agentId === agentId);
@@ -337,11 +973,11 @@ var TraceWatcher = class extends import_events.EventEmitter {
337
973
  return Array.from(agentIds).sort();
338
974
  }
339
975
  stop() {
340
- if (this.watcher) {
341
- this.watcher.close();
342
- this.watcher = void 0;
343
- console.log("Stopped watching traces directory");
976
+ for (const w of this.watchers) {
977
+ w.close();
344
978
  }
979
+ this.watchers = [];
980
+ console.log("Stopped watching all directories");
345
981
  }
346
982
  getTraceStats() {
347
983
  const total = this.traces.size;
@@ -368,7 +1004,10 @@ var __dirname = path2.dirname(__filename);
368
1004
  var DashboardServer = class {
369
1005
  constructor(config) {
370
1006
  this.config = config;
371
- this.watcher = new TraceWatcher(config.tracesDir);
1007
+ this.watcher = new TraceWatcher({
1008
+ tracesDir: config.tracesDir,
1009
+ dataDirs: config.dataDirs
1010
+ });
372
1011
  this.stats = new AgentStats();
373
1012
  this.setupExpress();
374
1013
  this.setupWebSocket();
@@ -414,6 +1053,21 @@ var DashboardServer = class {
414
1053
  res.status(500).json({ error: "Failed to load trace" });
415
1054
  }
416
1055
  });
1056
+ this.app.get("/api/traces/:filename/events", (req, res) => {
1057
+ try {
1058
+ const trace = this.watcher.getTrace(req.params.filename);
1059
+ if (!trace) {
1060
+ return res.status(404).json({ error: "Trace not found" });
1061
+ }
1062
+ res.json({
1063
+ events: trace.sessionEvents || [],
1064
+ tokenUsage: trace.tokenUsage || null,
1065
+ sourceType: trace.sourceType || "trace"
1066
+ });
1067
+ } catch (error) {
1068
+ res.status(500).json({ error: "Failed to load trace events" });
1069
+ }
1070
+ });
417
1071
  this.app.get("/api/agents", (req, res) => {
418
1072
  try {
419
1073
  const agents = this.stats.getAgentsList();
@@ -549,8 +1203,56 @@ var DashboardServer = class {
549
1203
  };
550
1204
 
551
1205
  // src/cli.ts
552
- async function startDashboard() {
1206
+ var VERSION = "0.2.2";
1207
+ function getLanAddress() {
1208
+ const interfaces = os.networkInterfaces();
1209
+ for (const name of Object.keys(interfaces)) {
1210
+ for (const iface of interfaces[name] || []) {
1211
+ if (iface.family === "IPv4" && !iface.internal) {
1212
+ return iface.address;
1213
+ }
1214
+ }
1215
+ }
1216
+ return null;
1217
+ }
1218
+ function printBanner(config, traceCount, stats) {
553
1219
  var _a;
1220
+ const lan = getLanAddress();
1221
+ const host = config.host || "localhost";
1222
+ const port = config.port;
1223
+ const isPublic = host === "0.0.0.0";
1224
+ console.log(`
1225
+ ___ _ _____ _
1226
+ / _ \\ __ _ ___ _ __ | |_| ___| | _____ __
1227
+ | |_| |/ _\` |/ _ \\ '_ \\| __| |_ | |/ _ \\ \\ /\\ / /
1228
+ | _ | (_| | __/ | | | |_| _| | | (_) \\ V V /
1229
+ |_| |_|\\__, |\\___|_| |_|\\__|_| |_|\\___/ \\_/\\_/
1230
+ |___/ dashboard v${VERSION}
1231
+
1232
+ See your agents think.
1233
+
1234
+ \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
1235
+ \u2502 \u{1F916} Agents \u2502 TRACE FILES \u2502 \u{1F4CA} AgentFlow \u2502 SHOWS YOU \u2502 \u{1F310} Your browser \u2502
1236
+ \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
1237
+ \u2502 write JSON \u2502 \u2502 builds graphs, \u2502 \u2502 graph, timeline, \u2502
1238
+ \u2502 trace files. \u2502 \u2502 serves dashboard.\u2502 \u2502 metrics, health. \u2502
1239
+ \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
1240
+
1241
+ Runs locally. Your data never leaves your machine.
1242
+
1243
+ Tabs: \u{1F3AF} Graph \xB7 \u23F1\uFE0F Timeline \xB7 \u{1F4CA} Metrics \xB7 \u{1F6E0}\uFE0F Process Health \xB7 \u26A0\uFE0F Errors
1244
+
1245
+ Traces: ${config.tracesDir}${((_a = config.dataDirs) == null ? void 0 : _a.length) ? "\n Data dirs: " + config.dataDirs.join("\n ") : ""}
1246
+ Loaded: ${traceCount} traces \xB7 ${stats.totalAgents} agents \xB7 ${stats.totalExecutions} executions
1247
+ Success: ${stats.globalSuccessRate.toFixed(1)}%${stats.activeAgents > 0 ? ` \xB7 ${stats.activeAgents} active now` : ""}
1248
+ CORS: ${config.enableCors ? "enabled" : "disabled"}
1249
+ WebSocket: live updates enabled
1250
+
1251
+ \u2192 http://localhost:${port}${isPublic && lan ? `
1252
+ \u2192 http://${lan}:${port} (LAN)` : ""}
1253
+ `);
1254
+ }
1255
+ async function startDashboard() {
554
1256
  const args = process.argv.slice(2);
555
1257
  const config = {
556
1258
  port: 3e3,
@@ -586,43 +1288,27 @@ async function startDashboard() {
586
1288
  }
587
1289
  const tracesPath = path3.resolve(config.tracesDir);
588
1290
  if (!fs3.existsSync(tracesPath)) {
589
- console.log(`Traces directory doesn't exist: ${tracesPath}`);
590
- console.log("Creating traces directory...");
591
1291
  fs3.mkdirSync(tracesPath, { recursive: true });
592
1292
  }
593
1293
  config.tracesDir = tracesPath;
594
- console.log("\u{1F680} Starting AgentFlow Dashboard...");
595
- console.log(` Port: ${config.port}`);
596
- console.log(` Host: ${config.host}`);
597
- console.log(` Traces: ${config.tracesDir}`);
598
- console.log(` CORS: ${config.enableCors ? "enabled" : "disabled"}`);
599
- if ((_a = config.dataDirs) == null ? void 0 : _a.length) {
600
- console.log(` Data dirs: ${config.dataDirs.join(", ")}`);
601
- }
1294
+ console.log("\nStarting AgentFlow Dashboard...\n");
602
1295
  const dashboard = new DashboardServer(config);
603
1296
  process.on("SIGINT", async () => {
604
- console.log("\\n\u{1F6D1} Shutting down dashboard...");
1297
+ console.log("\n\u{1F6D1} Shutting down dashboard...");
605
1298
  await dashboard.stop();
606
1299
  process.exit(0);
607
1300
  });
608
1301
  process.on("SIGTERM", async () => {
609
- console.log("\\n\u{1F6D1} Received SIGTERM, shutting down...");
610
1302
  await dashboard.stop();
611
1303
  process.exit(0);
612
1304
  });
613
1305
  try {
614
1306
  await dashboard.start();
615
- console.log("\u2705 Dashboard started successfully!");
616
- console.log(` Open: http://${config.host}:${config.port}`);
617
1307
  setTimeout(() => {
618
1308
  const stats = dashboard.getStats();
619
1309
  const traces = dashboard.getTraces();
620
- console.log(`\\n\u{1F4CA} Dashboard Status:`);
621
- console.log(` Total Traces: ${traces.length}`);
622
- console.log(` Total Agents: ${stats.totalAgents}`);
623
- console.log(` Success Rate: ${stats.globalSuccessRate.toFixed(1)}%`);
624
- console.log(` Active Agents: ${stats.activeAgents}`);
625
- }, 1e3);
1310
+ printBanner(config, traces.length, stats);
1311
+ }, 1500);
626
1312
  } catch (error) {
627
1313
  console.error("\u274C Failed to start dashboard:", error);
628
1314
  process.exit(1);
@@ -630,7 +1316,7 @@ async function startDashboard() {
630
1316
  }
631
1317
  function printHelp() {
632
1318
  console.log(`
633
- \u{1F50D} AgentFlow Dashboard - Real-time monitoring for AI agent executions
1319
+ \u{1F4CA} AgentFlow Dashboard v${VERSION} \u2014 See your agents think.
634
1320
 
635
1321
  Usage:
636
1322
  agentflow-dashboard [options]
@@ -640,23 +1326,21 @@ Options:
640
1326
  -p, --port <number> Server port (default: 3000)
641
1327
  -t, --traces <path> Traces directory (default: ./traces)
642
1328
  -h, --host <address> Host address (default: localhost)
643
- --data-dir <path> Extra directory for process discovery (repeatable)
1329
+ --data-dir <path> Extra data directory for process discovery (repeatable)
644
1330
  --cors Enable CORS headers
645
1331
  --help Show this help message
646
1332
 
647
1333
  Examples:
648
- agentflow-dashboard --port 8080 --traces /var/log/agentflow
649
- agentflow-dashboard --host 0.0.0.0 --cors
650
- agentflow-dashboard --traces ./my-agent-traces
651
-
652
- Features:
653
- \u2728 Real-time trace monitoring
654
- \u{1F4CA} Agent performance analytics
655
- \u{1F3AF} Execution graph visualization
656
- \u{1F4C8} Success/failure tracking
657
- \u{1F50D} Multi-agent system overview
1334
+ agentflow-dashboard --traces ./traces --host 0.0.0.0 --cors
1335
+ agentflow-dashboard -p 8080 -t /var/log/agentflow
1336
+ agentflow-dashboard --traces ./traces --data-dir ./workers --data-dir ./cron
658
1337
 
659
- Visit: https://github.com/ClemenceChee/AgentFlow
1338
+ Tabs:
1339
+ \u{1F3AF} Graph Interactive Cytoscape.js execution graph
1340
+ \u23F1\uFE0F Timeline Waterfall view of node durations
1341
+ \u{1F4CA} Metrics Success rates, durations, node breakdown
1342
+ \u{1F6E0}\uFE0F Process Health PID files, systemd, workers, orphans
1343
+ \u26A0\uFE0F Errors Failed and hung nodes with metadata
660
1344
  `);
661
1345
  }
662
1346
  // Annotate the CommonJS export names for ESM import in node: