agentflow-dashboard 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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");
@@ -227,14 +227,23 @@ var import_events = require("events");
227
227
  var fs = __toESM(require("fs"), 1);
228
228
  var path = __toESM(require("path"), 1);
229
229
  var TraceWatcher = class extends import_events.EventEmitter {
230
- watcher;
230
+ watchers = [];
231
231
  traces = /* @__PURE__ */ new Map();
232
232
  tracesDir;
233
- constructor(tracesDir) {
233
+ dataDirs;
234
+ allWatchDirs;
235
+ constructor(tracesDirOrOptions) {
234
236
  super();
235
- 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];
236
245
  this.ensureTracesDir();
237
- this.loadExistingTraces();
246
+ this.loadExistingFiles();
238
247
  this.startWatching();
239
248
  }
240
249
  ensureTracesDir() {
@@ -243,18 +252,288 @@ var TraceWatcher = class extends import_events.EventEmitter {
243
252
  console.log(`Created traces directory: ${this.tracesDir}`);
244
253
  }
245
254
  }
246
- 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) {
247
282
  try {
248
- const files = fs.readdirSync(this.tracesDir).filter((file) => file.endsWith(".json"));
249
- console.log(`Loading ${files.length} existing trace files...`);
250
- for (const file of files) {
251
- 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);
252
295
  }
253
- console.log(`Loaded ${this.traces.size} traces`);
296
+ return traces.length > 0;
254
297
  } catch (error) {
255
- console.error("Error loading existing traces:", error);
298
+ console.error(`Error loading log file ${filePath}:`, error);
299
+ return false;
300
+ }
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;
256
527
  }
257
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
+ }
258
537
  loadTraceFile(filePath) {
259
538
  try {
260
539
  const content = fs.readFileSync(filePath, "utf8");
@@ -263,55 +542,404 @@ var TraceWatcher = class extends import_events.EventEmitter {
263
542
  const stats = fs.statSync(filePath);
264
543
  graph.filename = filename;
265
544
  graph.lastModified = stats.mtime.getTime();
266
- this.traces.set(filename, graph);
545
+ graph.sourceType = "trace";
546
+ graph.sourceDir = path.dirname(filePath);
547
+ this.traces.set(this.traceKey(filePath), graph);
267
548
  return true;
268
- } catch (error) {
269
- console.error(`Error loading trace file ${filePath}:`, error);
549
+ } catch {
270
550
  return false;
271
551
  }
272
552
  }
273
- startWatching() {
274
- this.watcher = import_chokidar.default.watch(this.tracesDir, {
275
- ignored: /^\./,
276
- persistent: true,
277
- ignoreInitial: true
278
- });
279
- this.watcher.on("add", (filePath) => {
280
- if (filePath.endsWith(".json")) {
281
- console.log(`New trace file: ${path.basename(filePath)}`);
282
- if (this.loadTraceFile(filePath)) {
283
- const filename = path.basename(filePath);
284
- const trace = this.traces.get(filename);
285
- if (trace) {
286
- this.emit("trace-added", trace);
287
- }
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 {
288
565
  }
289
566
  }
290
- });
291
- this.watcher.on("change", (filePath) => {
292
- if (filePath.endsWith(".json")) {
293
- console.log(`Trace file updated: ${path.basename(filePath)}`);
294
- if (this.loadTraceFile(filePath)) {
295
- const filename = path.basename(filePath);
296
- const trace = this.traces.get(filename);
297
- if (trace) {
298
- 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
+ }
299
802
  }
300
803
  }
301
804
  }
302
- });
303
- this.watcher.on("unlink", (filePath) => {
304
- if (filePath.endsWith(".json")) {
305
- const filename = path.basename(filePath);
306
- console.log(`Trace file removed: ${filename}`);
307
- this.traces.delete(filename);
308
- 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";
309
820
  }
310
- });
311
- this.watcher.on("error", (error) => {
312
- console.error("Trace watcher error:", error);
313
- });
314
- 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`);
315
943
  }
316
944
  getAllTraces() {
317
945
  return Array.from(this.traces.values()).sort((a, b) => {
@@ -319,7 +947,14 @@ var TraceWatcher = class extends import_events.EventEmitter {
319
947
  });
320
948
  }
321
949
  getTrace(filename) {
322
- 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;
323
958
  }
324
959
  getTracesByAgent(agentId) {
325
960
  return this.getAllTraces().filter((trace) => trace.agentId === agentId);
@@ -338,11 +973,11 @@ var TraceWatcher = class extends import_events.EventEmitter {
338
973
  return Array.from(agentIds).sort();
339
974
  }
340
975
  stop() {
341
- if (this.watcher) {
342
- this.watcher.close();
343
- this.watcher = void 0;
344
- console.log("Stopped watching traces directory");
976
+ for (const w of this.watchers) {
977
+ w.close();
345
978
  }
979
+ this.watchers = [];
980
+ console.log("Stopped watching all directories");
346
981
  }
347
982
  getTraceStats() {
348
983
  const total = this.traces.size;
@@ -362,14 +997,162 @@ var TraceWatcher = class extends import_events.EventEmitter {
362
997
  }
363
998
  };
364
999
 
1000
+ // src/cli.ts
1001
+ var fs2 = __toESM(require("fs"), 1);
1002
+ var os = __toESM(require("os"), 1);
1003
+ var path2 = __toESM(require("path"), 1);
1004
+ var VERSION = "0.3.1";
1005
+ function getLanAddress() {
1006
+ const interfaces = os.networkInterfaces();
1007
+ for (const name of Object.keys(interfaces)) {
1008
+ for (const iface of interfaces[name] || []) {
1009
+ if (iface.family === "IPv4" && !iface.internal) {
1010
+ return iface.address;
1011
+ }
1012
+ }
1013
+ }
1014
+ return null;
1015
+ }
1016
+ function printBanner(config, traceCount, stats) {
1017
+ var _a;
1018
+ const lan = getLanAddress();
1019
+ const host = config.host || "localhost";
1020
+ const port = config.port;
1021
+ const isPublic = host === "0.0.0.0";
1022
+ console.log(`
1023
+ ___ _ _____ _
1024
+ / _ \\ __ _ ___ _ __ | |_| ___| | _____ __
1025
+ | |_| |/ _\` |/ _ \\ '_ \\| __| |_ | |/ _ \\ \\ /\\ / /
1026
+ | _ | (_| | __/ | | | |_| _| | | (_) \\ V V /
1027
+ |_| |_|\\__, |\\___|_| |_|\\__|_| |_|\\___/ \\_/\\_/
1028
+ |___/ dashboard v${VERSION}
1029
+
1030
+ See your agents think.
1031
+
1032
+ \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
1033
+ \u2502 \u{1F916} Agents \u2502 TRACE FILES \u2502 \u{1F4CA} AgentFlow \u2502 SHOWS YOU \u2502 \u{1F310} Your browser \u2502
1034
+ \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
1035
+ \u2502 write JSON \u2502 \u2502 builds graphs, \u2502 \u2502 graph, timeline, \u2502
1036
+ \u2502 trace files. \u2502 \u2502 serves dashboard.\u2502 \u2502 metrics, health. \u2502
1037
+ \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
1038
+
1039
+ Runs locally. Your data never leaves your machine.
1040
+
1041
+ Tabs: \u{1F3AF} Graph \xB7 \u23F1\uFE0F Timeline \xB7 \u{1F4CA} Metrics \xB7 \u{1F6E0}\uFE0F Process Health \xB7 \u26A0\uFE0F Errors
1042
+
1043
+ Traces: ${config.tracesDir}${((_a = config.dataDirs) == null ? void 0 : _a.length) ? "\n Data dirs: " + config.dataDirs.join("\n ") : ""}
1044
+ Loaded: ${traceCount} traces \xB7 ${stats.totalAgents} agents \xB7 ${stats.totalExecutions} executions
1045
+ Success: ${stats.globalSuccessRate.toFixed(1)}%${stats.activeAgents > 0 ? ` \xB7 ${stats.activeAgents} active now` : ""}
1046
+ CORS: ${config.enableCors ? "enabled" : "disabled"}
1047
+ WebSocket: live updates enabled
1048
+
1049
+ \u2192 http://localhost:${port}${isPublic && lan ? `
1050
+ \u2192 http://${lan}:${port} (LAN)` : ""}
1051
+ `);
1052
+ }
1053
+ async function startDashboard() {
1054
+ const args = process.argv.slice(2);
1055
+ const config = {
1056
+ port: 3e3,
1057
+ tracesDir: "./traces",
1058
+ host: "localhost",
1059
+ enableCors: false
1060
+ };
1061
+ for (let i = 0; i < args.length; i++) {
1062
+ switch (args[i]) {
1063
+ case "--port":
1064
+ case "-p":
1065
+ config.port = parseInt(args[++i]) || 3e3;
1066
+ break;
1067
+ case "--traces":
1068
+ case "-t":
1069
+ config.tracesDir = args[++i];
1070
+ break;
1071
+ case "--host":
1072
+ case "-h":
1073
+ config.host = args[++i];
1074
+ break;
1075
+ case "--data-dir":
1076
+ if (!config.dataDirs) config.dataDirs = [];
1077
+ config.dataDirs.push(args[++i]);
1078
+ break;
1079
+ case "--cors":
1080
+ config.enableCors = true;
1081
+ break;
1082
+ case "--help":
1083
+ printHelp();
1084
+ process.exit(0);
1085
+ }
1086
+ }
1087
+ const tracesPath = path2.resolve(config.tracesDir);
1088
+ if (!fs2.existsSync(tracesPath)) {
1089
+ fs2.mkdirSync(tracesPath, { recursive: true });
1090
+ }
1091
+ config.tracesDir = tracesPath;
1092
+ console.log("\nStarting AgentFlow Dashboard...\n");
1093
+ const dashboard = new DashboardServer(config);
1094
+ process.on("SIGINT", async () => {
1095
+ console.log("\n\u{1F6D1} Shutting down dashboard...");
1096
+ await dashboard.stop();
1097
+ process.exit(0);
1098
+ });
1099
+ process.on("SIGTERM", async () => {
1100
+ await dashboard.stop();
1101
+ process.exit(0);
1102
+ });
1103
+ try {
1104
+ await dashboard.start();
1105
+ setTimeout(() => {
1106
+ const stats = dashboard.getStats();
1107
+ const traces = dashboard.getTraces();
1108
+ printBanner(config, traces.length, stats);
1109
+ }, 1500);
1110
+ } catch (error) {
1111
+ console.error("\u274C Failed to start dashboard:", error);
1112
+ process.exit(1);
1113
+ }
1114
+ }
1115
+ function printHelp() {
1116
+ console.log(`
1117
+ \u{1F4CA} AgentFlow Dashboard v${VERSION} \u2014 See your agents think.
1118
+
1119
+ Usage:
1120
+ agentflow-dashboard [options]
1121
+ npx agentflow-dashboard [options]
1122
+
1123
+ Options:
1124
+ -p, --port <number> Server port (default: 3000)
1125
+ -t, --traces <path> Traces directory (default: ./traces)
1126
+ -h, --host <address> Host address (default: localhost)
1127
+ --data-dir <path> Extra data directory for process discovery (repeatable)
1128
+ --cors Enable CORS headers
1129
+ --help Show this help message
1130
+
1131
+ Examples:
1132
+ agentflow-dashboard --traces ./traces --host 0.0.0.0 --cors
1133
+ agentflow-dashboard -p 8080 -t /var/log/agentflow
1134
+ agentflow-dashboard --traces ./traces --data-dir ./workers --data-dir ./cron
1135
+
1136
+ Tabs:
1137
+ \u{1F3AF} Graph Interactive Cytoscape.js execution graph
1138
+ \u23F1\uFE0F Timeline Waterfall view of node durations
1139
+ \u{1F4CA} Metrics Success rates, durations, node breakdown
1140
+ \u{1F6E0}\uFE0F Process Health PID files, systemd, workers, orphans
1141
+ \u26A0\uFE0F Errors Failed and hung nodes with metadata
1142
+ `);
1143
+ }
1144
+
365
1145
  // src/server.ts
366
1146
  var import_meta = {};
367
1147
  var __filename = (0, import_url.fileURLToPath)(import_meta.url);
368
- var __dirname = path2.dirname(__filename);
1148
+ var __dirname = path3.dirname(__filename);
369
1149
  var DashboardServer = class {
370
1150
  constructor(config) {
371
1151
  this.config = config;
372
- this.watcher = new TraceWatcher(config.tracesDir);
1152
+ this.watcher = new TraceWatcher({
1153
+ tracesDir: config.tracesDir,
1154
+ dataDirs: config.dataDirs
1155
+ });
373
1156
  this.stats = new AgentStats();
374
1157
  this.setupExpress();
375
1158
  this.setupWebSocket();
@@ -392,8 +1175,8 @@ var DashboardServer = class {
392
1175
  next();
393
1176
  });
394
1177
  }
395
- const publicDir = path2.join(__dirname, "../public");
396
- if (fs2.existsSync(publicDir)) {
1178
+ const publicDir = path3.join(__dirname, "../public");
1179
+ if (fs3.existsSync(publicDir)) {
397
1180
  this.app.use(import_express.default.static(publicDir));
398
1181
  }
399
1182
  this.app.get("/api/traces", (req, res) => {
@@ -415,6 +1198,21 @@ var DashboardServer = class {
415
1198
  res.status(500).json({ error: "Failed to load trace" });
416
1199
  }
417
1200
  });
1201
+ this.app.get("/api/traces/:filename/events", (req, res) => {
1202
+ try {
1203
+ const trace = this.watcher.getTrace(req.params.filename);
1204
+ if (!trace) {
1205
+ return res.status(404).json({ error: "Trace not found" });
1206
+ }
1207
+ res.json({
1208
+ events: trace.sessionEvents || [],
1209
+ tokenUsage: trace.tokenUsage || null,
1210
+ sourceType: trace.sourceType || "trace"
1211
+ });
1212
+ } catch (error) {
1213
+ res.status(500).json({ error: "Failed to load trace events" });
1214
+ }
1215
+ });
418
1216
  this.app.get("/api/agents", (req, res) => {
419
1217
  try {
420
1218
  const agents = this.stats.getAgentsList();
@@ -450,7 +1248,7 @@ var DashboardServer = class {
450
1248
  }
451
1249
  const discoveryDirs = [
452
1250
  this.config.tracesDir,
453
- path2.dirname(this.config.tracesDir),
1251
+ path3.dirname(this.config.tracesDir),
454
1252
  ...this.config.dataDirs || []
455
1253
  ];
456
1254
  const processConfig = (0, import_agentflow_core3.discoverProcessConfig)(discoveryDirs);
@@ -465,8 +1263,8 @@ var DashboardServer = class {
465
1263
  }
466
1264
  });
467
1265
  this.app.get("*", (req, res) => {
468
- const indexPath = path2.join(__dirname, "../public/index.html");
469
- if (fs2.existsSync(indexPath)) {
1266
+ const indexPath = path3.join(__dirname, "../public/index.html");
1267
+ if (fs3.existsSync(indexPath)) {
470
1268
  res.sendFile(indexPath);
471
1269
  } else {
472
1270
  res.status(404).send("Dashboard not found - public files may not be built");
@@ -523,21 +1321,21 @@ var DashboardServer = class {
523
1321
  });
524
1322
  }
525
1323
  async start() {
526
- return new Promise((resolve2) => {
1324
+ return new Promise((resolve3) => {
527
1325
  const host = this.config.host || "localhost";
528
1326
  this.server.listen(this.config.port, host, () => {
529
1327
  console.log(`AgentFlow Dashboard running at http://${host}:${this.config.port}`);
530
1328
  console.log(`Watching traces in: ${this.config.tracesDir}`);
531
- resolve2();
1329
+ resolve3();
532
1330
  });
533
1331
  });
534
1332
  }
535
1333
  async stop() {
536
- return new Promise((resolve2) => {
1334
+ return new Promise((resolve3) => {
537
1335
  this.watcher.stop();
538
1336
  this.server.close(() => {
539
1337
  console.log("Dashboard server stopped");
540
- resolve2();
1338
+ resolve3();
541
1339
  });
542
1340
  });
543
1341
  }
@@ -548,6 +1346,9 @@ var DashboardServer = class {
548
1346
  return this.watcher.getAllTraces();
549
1347
  }
550
1348
  };
1349
+ if (import_meta.url === `file://${process.argv[1]}`) {
1350
+ startDashboard().catch(console.error);
1351
+ }
551
1352
  // Annotate the CommonJS export names for ESM import in node:
552
1353
  0 && (module.exports = {
553
1354
  AgentStats,