agentflow-dashboard 0.3.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.
@@ -0,0 +1,1350 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
+ // If the importer is in node compatibility mode or this is not an ESM
21
+ // file that has been converted to a CommonJS file using a Babel-
22
+ // compatible transform (i.e. "__esModule" has not been set), then set
23
+ // "default" to the CommonJS "module.exports" for node compatibility.
24
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
+ mod
26
+ ));
27
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
+
29
+ // src/server.ts
30
+ var server_exports = {};
31
+ __export(server_exports, {
32
+ DashboardServer: () => DashboardServer
33
+ });
34
+ module.exports = __toCommonJS(server_exports);
35
+ var import_express = __toESM(require("express"), 1);
36
+ var fs3 = __toESM(require("fs"), 1);
37
+ var import_http = require("http");
38
+ var path3 = __toESM(require("path"), 1);
39
+ var import_url = require("url");
40
+ var import_ws = require("ws");
41
+ var import_agentflow_core3 = require("agentflow-core");
42
+
43
+ // src/stats.ts
44
+ var import_agentflow_core = require("agentflow-core");
45
+ var AgentStats = class {
46
+ agentMetrics = /* @__PURE__ */ new Map();
47
+ processedTraces = /* @__PURE__ */ new Set();
48
+ processTrace(trace) {
49
+ const traceKey = `${trace.filename || trace.agentId}-${trace.startTime}`;
50
+ if (this.processedTraces.has(traceKey)) {
51
+ return;
52
+ }
53
+ this.processedTraces.add(traceKey);
54
+ const agentId = trace.agentId;
55
+ let metrics = this.agentMetrics.get(agentId);
56
+ if (!metrics) {
57
+ metrics = {
58
+ agentId,
59
+ totalExecutions: 0,
60
+ successfulExecutions: 0,
61
+ failedExecutions: 0,
62
+ successRate: 0,
63
+ avgExecutionTime: 0,
64
+ lastExecution: 0,
65
+ triggers: {},
66
+ recentActivity: []
67
+ };
68
+ this.agentMetrics.set(agentId, metrics);
69
+ }
70
+ const analysis = this.analyzeExecution(trace);
71
+ metrics.totalExecutions++;
72
+ metrics.lastExecution = Math.max(metrics.lastExecution, trace.startTime);
73
+ const trigger = trace.trigger || "unknown";
74
+ metrics.triggers[trigger] = (metrics.triggers[trigger] || 0) + 1;
75
+ if (analysis.success) {
76
+ metrics.successfulExecutions++;
77
+ } else {
78
+ metrics.failedExecutions++;
79
+ }
80
+ metrics.successRate = metrics.successfulExecutions / metrics.totalExecutions * 100;
81
+ if (analysis.executionTime > 0) {
82
+ const currentAvg = metrics.avgExecutionTime;
83
+ const count = metrics.totalExecutions;
84
+ metrics.avgExecutionTime = (currentAvg * (count - 1) + analysis.executionTime) / count;
85
+ }
86
+ metrics.recentActivity.push({
87
+ timestamp: trace.startTime,
88
+ success: analysis.success,
89
+ executionTime: analysis.executionTime,
90
+ trigger
91
+ });
92
+ if (metrics.recentActivity.length > 100) {
93
+ metrics.recentActivity = metrics.recentActivity.slice(-100);
94
+ }
95
+ metrics.recentActivity.sort((a, b) => b.timestamp - a.timestamp);
96
+ }
97
+ analyzeExecution(trace) {
98
+ try {
99
+ const stats = (0, import_agentflow_core.getStats)(trace);
100
+ const failures = (0, import_agentflow_core.getFailures)(trace);
101
+ const hungNodes = (0, import_agentflow_core.getHungNodes)(trace);
102
+ return {
103
+ success: failures.length === 0 && hungNodes.length === 0,
104
+ executionTime: stats.duration || 0,
105
+ nodeCount: stats.totalNodes || 0,
106
+ failureCount: failures.length,
107
+ hungCount: hungNodes.length
108
+ };
109
+ } catch (error) {
110
+ console.warn("Error analyzing trace with AgentFlow:", error);
111
+ const nodes = trace.nodes instanceof Map ? Array.from(trace.nodes.values()) : [];
112
+ const failedNodes = nodes.filter((node) => node.status === "failed").length;
113
+ return {
114
+ success: failedNodes === 0,
115
+ executionTime: 0,
116
+ nodeCount: nodes.length,
117
+ failureCount: failedNodes,
118
+ hungCount: 0
119
+ };
120
+ }
121
+ }
122
+ getAgentStats(agentId) {
123
+ return this.agentMetrics.get(agentId);
124
+ }
125
+ getAgentsList() {
126
+ return Array.from(this.agentMetrics.values()).sort((a, b) => b.lastExecution - a.lastExecution);
127
+ }
128
+ getGlobalStats() {
129
+ const agents = Array.from(this.agentMetrics.values());
130
+ const now = Date.now();
131
+ const oneHourAgo = now - 60 * 60 * 1e3;
132
+ const totalExecutions = agents.reduce((sum, agent) => sum + agent.totalExecutions, 0);
133
+ const totalSuccessful = agents.reduce((sum, agent) => sum + agent.successfulExecutions, 0);
134
+ const globalSuccessRate = totalExecutions > 0 ? totalSuccessful / totalExecutions * 100 : 0;
135
+ const activeAgents = agents.filter((agent) => agent.lastExecution > oneHourAgo).length;
136
+ const topAgents = agents.slice().sort((a, b) => b.totalExecutions - a.totalExecutions).slice(0, 10).map((agent) => ({
137
+ agentId: agent.agentId,
138
+ executionCount: agent.totalExecutions,
139
+ successRate: agent.successRate
140
+ }));
141
+ const recentActivity = [];
142
+ for (const agent of agents) {
143
+ for (const activity of agent.recentActivity.slice(0, 20)) {
144
+ recentActivity.push({
145
+ timestamp: activity.timestamp,
146
+ agentId: agent.agentId,
147
+ success: activity.success,
148
+ trigger: activity.trigger
149
+ });
150
+ }
151
+ }
152
+ recentActivity.sort((a, b) => b.timestamp - a.timestamp);
153
+ recentActivity.splice(200);
154
+ return {
155
+ totalAgents: agents.length,
156
+ totalExecutions,
157
+ globalSuccessRate,
158
+ activeAgents,
159
+ topAgents,
160
+ recentActivity
161
+ };
162
+ }
163
+ getPerformanceSummary() {
164
+ const global = this.getGlobalStats();
165
+ const agents = this.getAgentsList();
166
+ return {
167
+ overview: {
168
+ totalAgents: global.totalAgents,
169
+ totalExecutions: global.totalExecutions,
170
+ successRate: Math.round(global.globalSuccessRate * 100) / 100,
171
+ activeAgents: global.activeAgents
172
+ },
173
+ topPerformers: agents.slice(0, 5).map((agent) => ({
174
+ agentId: agent.agentId,
175
+ executions: agent.totalExecutions,
176
+ successRate: Math.round(agent.successRate * 100) / 100,
177
+ avgTime: Math.round(agent.avgExecutionTime * 100) / 100
178
+ })),
179
+ recentTrends: this.analyzeRecentTrends()
180
+ };
181
+ }
182
+ analyzeRecentTrends() {
183
+ const agents = Array.from(this.agentMetrics.values());
184
+ const now = Date.now();
185
+ const oneHourAgo = now - 60 * 60 * 1e3;
186
+ let recentExecutions = 0;
187
+ let recentFailures = 0;
188
+ for (const agent of agents) {
189
+ for (const activity of agent.recentActivity) {
190
+ if (activity.timestamp > oneHourAgo) {
191
+ recentExecutions++;
192
+ if (!activity.success) {
193
+ recentFailures++;
194
+ }
195
+ }
196
+ }
197
+ }
198
+ return {
199
+ hourlyExecutions: recentExecutions,
200
+ hourlyFailures: recentFailures,
201
+ hourlySuccessRate: recentExecutions > 0 ? Math.round((recentExecutions - recentFailures) / recentExecutions * 1e4) / 100 : 0
202
+ };
203
+ }
204
+ cleanup() {
205
+ const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1e3;
206
+ for (const [agentId, metrics] of this.agentMetrics.entries()) {
207
+ metrics.recentActivity = metrics.recentActivity.filter(
208
+ (activity) => activity.timestamp > cutoff
209
+ );
210
+ if (metrics.lastExecution < cutoff) {
211
+ this.agentMetrics.delete(agentId);
212
+ }
213
+ }
214
+ console.log(`Cleaned up old metrics, ${this.agentMetrics.size} agents remaining`);
215
+ }
216
+ };
217
+
218
+ // src/watcher.ts
219
+ var import_agentflow_core2 = require("agentflow-core");
220
+ var import_chokidar = __toESM(require("chokidar"), 1);
221
+ var import_events = require("events");
222
+ var fs = __toESM(require("fs"), 1);
223
+ var path = __toESM(require("path"), 1);
224
+ var TraceWatcher = class extends import_events.EventEmitter {
225
+ watchers = [];
226
+ traces = /* @__PURE__ */ new Map();
227
+ tracesDir;
228
+ dataDirs;
229
+ allWatchDirs;
230
+ constructor(tracesDirOrOptions) {
231
+ super();
232
+ if (typeof tracesDirOrOptions === "string") {
233
+ this.tracesDir = path.resolve(tracesDirOrOptions);
234
+ this.dataDirs = [];
235
+ } else {
236
+ this.tracesDir = path.resolve(tracesDirOrOptions.tracesDir);
237
+ this.dataDirs = (tracesDirOrOptions.dataDirs || []).map((d) => path.resolve(d));
238
+ }
239
+ this.allWatchDirs = [this.tracesDir, ...this.dataDirs];
240
+ this.ensureTracesDir();
241
+ this.loadExistingFiles();
242
+ this.startWatching();
243
+ }
244
+ ensureTracesDir() {
245
+ if (!fs.existsSync(this.tracesDir)) {
246
+ fs.mkdirSync(this.tracesDir, { recursive: true });
247
+ console.log(`Created traces directory: ${this.tracesDir}`);
248
+ }
249
+ }
250
+ loadExistingFiles() {
251
+ let totalFiles = 0;
252
+ for (const dir of this.allWatchDirs) {
253
+ if (!fs.existsSync(dir)) continue;
254
+ try {
255
+ const files = fs.readdirSync(dir).filter((f) => f.endsWith(".json") || f.endsWith(".jsonl"));
256
+ totalFiles += files.length;
257
+ for (const file of files) {
258
+ this.loadFile(path.join(dir, file));
259
+ }
260
+ } catch (error) {
261
+ console.error(`Error scanning directory ${dir}:`, error);
262
+ }
263
+ }
264
+ console.log(`Scanned ${this.allWatchDirs.length} directories, loaded ${this.traces.size} items from ${totalFiles} files`);
265
+ }
266
+ /** Load a .json trace, .jsonl session file, or .log file. */
267
+ loadFile(filePath) {
268
+ if (filePath.endsWith(".jsonl")) {
269
+ return this.loadSessionFile(filePath);
270
+ }
271
+ if (filePath.endsWith(".log") || filePath.endsWith(".trace")) {
272
+ return this.loadLogFile(filePath);
273
+ }
274
+ return this.loadTraceFile(filePath);
275
+ }
276
+ loadLogFile(filePath) {
277
+ try {
278
+ const content = fs.readFileSync(filePath, "utf8");
279
+ const filename = path.basename(filePath);
280
+ const stats = fs.statSync(filePath);
281
+ const traces = this.parseUniversalLog(content, filename, filePath);
282
+ for (let i = 0; i < traces.length; i++) {
283
+ const trace = traces[i];
284
+ trace.filename = filename;
285
+ trace.lastModified = stats.mtime.getTime();
286
+ trace.sourceType = "trace";
287
+ trace.sourceDir = path.dirname(filePath);
288
+ const key = traces.length === 1 ? this.traceKey(filePath) : `${this.traceKey(filePath)}-${i}`;
289
+ this.traces.set(key, trace);
290
+ }
291
+ return traces.length > 0;
292
+ } catch (error) {
293
+ console.error(`Error loading log file ${filePath}:`, error);
294
+ return false;
295
+ }
296
+ }
297
+ /** Universal log parser - detects agent activities from any system */
298
+ parseUniversalLog(content, filename, filePath) {
299
+ const lines = content.split("\n").filter((line) => line.trim());
300
+ const activities = /* @__PURE__ */ new Map();
301
+ for (const line of lines) {
302
+ const activity = this.detectActivityPattern(line);
303
+ if (!activity) continue;
304
+ const sessionId = this.extractSessionIdentifier(activity);
305
+ if (!activities.has(sessionId)) {
306
+ activities.set(sessionId, {
307
+ id: sessionId,
308
+ rootNodeId: "",
309
+ agentId: this.detectAgentIdentifier(activity, filename, filePath),
310
+ name: this.generateActivityName(activity, sessionId),
311
+ trigger: this.detectTrigger(activity),
312
+ startTime: activity.timestamp,
313
+ endTime: activity.timestamp,
314
+ status: "completed",
315
+ nodes: {},
316
+ edges: [],
317
+ events: [],
318
+ metadata: { sessionId, source: filename }
319
+ });
320
+ }
321
+ const session = activities.get(sessionId);
322
+ this.addActivityNode(session, activity);
323
+ if (activity.timestamp > session.endTime) {
324
+ session.endTime = activity.timestamp;
325
+ }
326
+ }
327
+ const traces = Array.from(activities.values()).filter(
328
+ (session) => Object.keys(session.nodes).length > 0
329
+ );
330
+ if (traces.length === 0) {
331
+ const stats = fs.statSync(filePath);
332
+ traces.push({
333
+ id: "",
334
+ rootNodeId: "root",
335
+ nodes: {
336
+ "root": {
337
+ id: "root",
338
+ type: "log-file",
339
+ name: filename,
340
+ status: "completed",
341
+ startTime: stats.mtime.getTime(),
342
+ endTime: stats.mtime.getTime(),
343
+ metadata: { lineCount: lines.length, path: filePath }
344
+ }
345
+ },
346
+ edges: [],
347
+ startTime: stats.mtime.getTime(),
348
+ endTime: stats.mtime.getTime(),
349
+ status: "completed",
350
+ trigger: "file",
351
+ agentId: this.extractAgentFromPath(filePath),
352
+ events: [],
353
+ metadata: { type: "file-trace" }
354
+ });
355
+ }
356
+ return traces;
357
+ }
358
+ /** Detect activity patterns in log lines using universal heuristics */
359
+ detectActivityPattern(line) {
360
+ let timestamp = this.extractTimestamp(line);
361
+ let level = this.extractLogLevel(line);
362
+ let action = this.extractAction(line);
363
+ let kvPairs = this.extractKeyValuePairs(line);
364
+ if (!timestamp) {
365
+ const jsonMatch = line.match(/\{.*\}/);
366
+ if (jsonMatch) {
367
+ try {
368
+ const parsed = JSON.parse(jsonMatch[0]);
369
+ timestamp = this.parseTimestamp(parsed.timestamp || parsed.time || parsed.ts) || Date.now();
370
+ level = parsed.level || parsed.severity || "info";
371
+ action = parsed.action || parsed.event || parsed.message || "";
372
+ kvPairs = parsed;
373
+ } catch {
374
+ }
375
+ }
376
+ }
377
+ if (!timestamp) {
378
+ const kvMatches = line.match(/(\w+)=([^\s]+)/g);
379
+ if (kvMatches && kvMatches.length >= 2) {
380
+ const pairs = {};
381
+ kvMatches.forEach((match) => {
382
+ const [key, value] = match.split("=", 2);
383
+ pairs[key] = this.parseValue(value);
384
+ });
385
+ timestamp = this.parseTimestamp(pairs.timestamp || pairs.time) || Date.now();
386
+ level = pairs.level || "info";
387
+ action = pairs.action || pairs.event || "";
388
+ kvPairs = pairs;
389
+ }
390
+ }
391
+ if (!timestamp) {
392
+ const logMatch = line.match(/^(\d{4}-\d{2}-\d{2}[T\s]\d{2}:\d{2}:\d{2}[.\d]*Z?)\s+(\w+)?\s*:?\s*(.+)/);
393
+ if (logMatch) {
394
+ timestamp = new Date(logMatch[1]).getTime();
395
+ level = logMatch[2] || "info";
396
+ action = logMatch[3] || "";
397
+ }
398
+ }
399
+ if (!timestamp) return null;
400
+ return {
401
+ timestamp,
402
+ level: (level == null ? void 0 : level.toLowerCase()) || "info",
403
+ action,
404
+ component: this.detectComponent(action, kvPairs),
405
+ operation: this.detectOperation(action, kvPairs),
406
+ ...kvPairs
407
+ };
408
+ }
409
+ extractTimestamp(line) {
410
+ const coloredMatch = line.match(/^\[2m(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z)\[0m/);
411
+ if (coloredMatch) return new Date(coloredMatch[1]).getTime();
412
+ const isoMatch = line.match(/(\d{4}-\d{2}-\d{2}[T\s]\d{2}:\d{2}:\d{2}[.\d]*Z?)/);
413
+ if (isoMatch) return new Date(isoMatch[1]).getTime();
414
+ return null;
415
+ }
416
+ extractLogLevel(line) {
417
+ const coloredMatch = line.match(/\[\[(\d+)m\[\[1m(\w+)\s*\[0m\]/);
418
+ if (coloredMatch) return coloredMatch[2].toLowerCase();
419
+ const levelMatch = line.match(/\b(debug|info|warn|warning|error|fatal|trace)\b/i);
420
+ return levelMatch ? levelMatch[1].toLowerCase() : null;
421
+ }
422
+ extractAction(line) {
423
+ const coloredMatch = line.match(/\[1m([^\[]+?)\s*\[0m/);
424
+ if (coloredMatch) return coloredMatch[1].trim();
425
+ const afterLevel = line.replace(/^.*?(debug|info|warn|warning|error|fatal|trace)\s*:?\s*/i, "");
426
+ return afterLevel.split(" ")[0] || "";
427
+ }
428
+ extractKeyValuePairs(line) {
429
+ const pairs = {};
430
+ const coloredRegex = /\[36m(\w+)\[0m=\[35m([^\[]+?)\[0m/g;
431
+ let match;
432
+ while ((match = coloredRegex.exec(line)) !== null) {
433
+ pairs[match[1]] = this.parseValue(match[2]);
434
+ }
435
+ if (Object.keys(pairs).length === 0) {
436
+ const kvRegex = /(\w+)=([^\s]+)/g;
437
+ while ((match = kvRegex.exec(line)) !== null) {
438
+ pairs[match[1]] = this.parseValue(match[2]);
439
+ }
440
+ }
441
+ return pairs;
442
+ }
443
+ parseValue(value) {
444
+ if (value.match(/^\d+$/)) return parseInt(value);
445
+ if (value.match(/^\d+\.\d+$/)) return parseFloat(value);
446
+ if (value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
447
+ if (value.startsWith('"') && value.endsWith('"')) return value.slice(1, -1);
448
+ return value;
449
+ }
450
+ parseTimestamp(value) {
451
+ if (!value) return null;
452
+ if (typeof value === "number") return value;
453
+ try {
454
+ return new Date(value).getTime();
455
+ } catch {
456
+ return null;
457
+ }
458
+ }
459
+ detectComponent(action, kvPairs) {
460
+ if (action.includes(".")) return action.split(".")[0];
461
+ if (kvPairs.component) return kvPairs.component;
462
+ if (kvPairs.service) return kvPairs.service;
463
+ if (kvPairs.module) return kvPairs.module;
464
+ return "unknown";
465
+ }
466
+ detectOperation(action, kvPairs) {
467
+ if (action.includes(".")) return action.split(".").slice(1).join(".");
468
+ if (kvPairs.operation) return kvPairs.operation;
469
+ if (kvPairs.method) return kvPairs.method;
470
+ return action || "activity";
471
+ }
472
+ extractSessionIdentifier(activity) {
473
+ return activity.run_id || activity.session_id || activity.request_id || activity.trace_id || activity.sweep_id || activity.transaction_id || "default";
474
+ }
475
+ detectAgentIdentifier(activity, filename, filePath) {
476
+ if (activity.component !== "unknown") {
477
+ const pathAgent = this.extractAgentFromPath(filePath);
478
+ if (pathAgent !== activity.component) {
479
+ return `${pathAgent}-${activity.component}`;
480
+ }
481
+ return activity.component;
482
+ }
483
+ return this.extractAgentFromPath(filePath);
484
+ }
485
+ extractAgentFromPath(filePath) {
486
+ const filename = path.basename(filePath, path.extname(filePath));
487
+ const pathParts = filePath.split(path.sep);
488
+ for (const part of pathParts.reverse()) {
489
+ if (part.match(/agent|worker|service|daemon|bot|ai|llm/i)) {
490
+ return part;
491
+ }
492
+ }
493
+ return filename;
494
+ }
495
+ generateActivityName(activity, sessionId) {
496
+ const component = activity.component !== "unknown" ? activity.component : "Activity";
497
+ const operation = activity.operation !== "activity" ? `: ${activity.operation}` : "";
498
+ return `${component}${operation} (${sessionId})`;
499
+ }
500
+ detectTrigger(activity) {
501
+ var _a, _b;
502
+ if (activity.trigger) return activity.trigger;
503
+ if (activity.method && activity.url) return "api-call";
504
+ if ((_a = activity.operation) == null ? void 0 : _a.includes("start")) return "startup";
505
+ if ((_b = activity.operation) == null ? void 0 : _b.includes("invoke")) return "invocation";
506
+ return "event";
507
+ }
508
+ addActivityNode(session, activity) {
509
+ const nodeId = `${activity.component}-${activity.operation}-${activity.timestamp}`;
510
+ const node = {
511
+ id: nodeId,
512
+ type: activity.component,
513
+ name: `${activity.component}: ${activity.operation}`,
514
+ status: this.getUniversalNodeStatus(activity),
515
+ startTime: activity.timestamp,
516
+ endTime: activity.timestamp,
517
+ metadata: activity
518
+ };
519
+ session.nodes[nodeId] = node;
520
+ if (!session.rootNodeId) {
521
+ session.rootNodeId = nodeId;
522
+ }
523
+ }
524
+ getUniversalNodeStatus(activity) {
525
+ var _a, _b;
526
+ if (activity.level === "error" || activity.level === "fatal") return "failed";
527
+ if (activity.level === "warn" || activity.level === "warning") return "warning";
528
+ if ((_a = activity.operation) == null ? void 0 : _a.match(/start|begin|init/i)) return "running";
529
+ if ((_b = activity.operation) == null ? void 0 : _b.match(/complete|finish|end|done/i)) return "completed";
530
+ return "completed";
531
+ }
532
+ loadTraceFile(filePath) {
533
+ try {
534
+ const content = fs.readFileSync(filePath, "utf8");
535
+ const graph = (0, import_agentflow_core2.loadGraph)(content);
536
+ const filename = path.basename(filePath);
537
+ const stats = fs.statSync(filePath);
538
+ graph.filename = filename;
539
+ graph.lastModified = stats.mtime.getTime();
540
+ graph.sourceType = "trace";
541
+ graph.sourceDir = path.dirname(filePath);
542
+ this.traces.set(this.traceKey(filePath), graph);
543
+ return true;
544
+ } catch {
545
+ return false;
546
+ }
547
+ }
548
+ /** Parse a JSONL session log into a WatchedTrace (best-effort). */
549
+ loadSessionFile(filePath) {
550
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
551
+ try {
552
+ const content = fs.readFileSync(filePath, "utf8");
553
+ const lines = content.split("\n").filter((l) => l.trim());
554
+ if (lines.length === 0) return false;
555
+ const rawEvents = [];
556
+ for (const line of lines) {
557
+ try {
558
+ rawEvents.push(JSON.parse(line));
559
+ } catch {
560
+ }
561
+ }
562
+ if (rawEvents.length === 0) return false;
563
+ const sessionEvent = rawEvents.find((e) => e.type === "session");
564
+ const sessionId = (sessionEvent == null ? void 0 : sessionEvent.id) || path.basename(filePath, ".jsonl");
565
+ const sessionTimestamp = (sessionEvent == null ? void 0 : sessionEvent.timestamp) || ((_a = rawEvents[0]) == null ? void 0 : _a.timestamp);
566
+ const startTime = sessionTimestamp ? new Date(sessionTimestamp).getTime() : 0;
567
+ if (!startTime) return false;
568
+ const parentDir = path.basename(path.dirname(filePath));
569
+ const grandParentDir = path.basename(path.dirname(path.dirname(filePath)));
570
+ const agentId = grandParentDir === "agents" ? parentDir : parentDir;
571
+ const modelEvent = rawEvents.find((e) => e.type === "model_change");
572
+ const provider = (modelEvent == null ? void 0 : modelEvent.provider) || "";
573
+ const modelId = (modelEvent == null ? void 0 : modelEvent.modelId) || "";
574
+ const nodes = /* @__PURE__ */ new Map();
575
+ let lastTimestamp = startTime;
576
+ for (const evt of rawEvents) {
577
+ if (evt.timestamp) {
578
+ const ts = new Date(evt.timestamp).getTime();
579
+ if (ts > lastTimestamp) lastTimestamp = ts;
580
+ }
581
+ }
582
+ const firstMessage = rawEvents.find((e) => {
583
+ var _a2;
584
+ return e.type === "message" && ((_a2 = e.message) == null ? void 0 : _a2.role) === "user";
585
+ });
586
+ 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) || "";
587
+ const cronMatch = userPrompt.match(/\[cron:(\S+)\s+([^\]]+)\]/);
588
+ const triggerName = cronMatch ? cronMatch[2] : "";
589
+ const trigger = cronMatch ? "cron" : "message";
590
+ let totalInputTokens = 0;
591
+ let totalOutputTokens = 0;
592
+ let totalTokensSum = 0;
593
+ let totalCost = 0;
594
+ let userMessageCount = 0;
595
+ let assistantMessageCount = 0;
596
+ let toolCallCount = 0;
597
+ let thinkingBlockCount = 0;
598
+ const sessionEvents = [];
599
+ const toolCallMap = /* @__PURE__ */ new Map();
600
+ const rootId = `session-${sessionId.slice(0, 8)}`;
601
+ const rootName = triggerName || userPrompt.slice(0, 80) + (userPrompt.length > 80 ? "..." : "") || sessionId;
602
+ for (const evt of rawEvents) {
603
+ const evtTs = evt.timestamp ? new Date(evt.timestamp).getTime() : startTime;
604
+ if (evt.type === "session") {
605
+ sessionEvents.push({
606
+ type: "system",
607
+ timestamp: evtTs,
608
+ name: "Session Started",
609
+ content: `Version: ${evt.version || "unknown"}, CWD: ${evt.cwd || ""}`,
610
+ id: evt.id
611
+ });
612
+ continue;
613
+ }
614
+ if (evt.type === "model_change") {
615
+ sessionEvents.push({
616
+ type: "model_change",
617
+ timestamp: evtTs,
618
+ name: "Model Change",
619
+ model: evt.modelId,
620
+ provider: evt.provider,
621
+ content: `${evt.provider}/${evt.modelId}`,
622
+ id: evt.id
623
+ });
624
+ continue;
625
+ }
626
+ if (evt.type === "thinking_level_change") {
627
+ sessionEvents.push({
628
+ type: "system",
629
+ timestamp: evtTs,
630
+ name: "Thinking Level",
631
+ content: evt.thinkingLevel || "",
632
+ id: evt.id
633
+ });
634
+ continue;
635
+ }
636
+ if (evt.type === "custom" && evt.customType === "model-snapshot") {
637
+ sessionEvents.push({
638
+ type: "system",
639
+ timestamp: evtTs,
640
+ name: "Model Snapshot",
641
+ content: JSON.stringify(evt.data || {}).slice(0, 200),
642
+ id: evt.id
643
+ });
644
+ continue;
645
+ }
646
+ if (evt.type === "custom_message" && evt.customType === "openclaw.sessions_yield") {
647
+ sessionEvents.push({
648
+ type: "spawn",
649
+ timestamp: evtTs,
650
+ name: "Subagent Spawn",
651
+ content: ((_e = evt.data) == null ? void 0 : _e.sessionId) || "",
652
+ id: evt.id,
653
+ parentId: evt.parentId
654
+ });
655
+ const spawnId = `spawn-${toolCallCount + thinkingBlockCount + 1}`;
656
+ nodes.set(spawnId, {
657
+ id: spawnId,
658
+ type: "subagent",
659
+ name: "Subagent: " + (((_f = evt.data) == null ? void 0 : _f.sessionId) || "").slice(0, 12),
660
+ startTime: evtTs,
661
+ endTime: evtTs,
662
+ status: "completed",
663
+ parentId: rootId,
664
+ children: [],
665
+ metadata: { sessionId: (_g = evt.data) == null ? void 0 : _g.sessionId }
666
+ });
667
+ continue;
668
+ }
669
+ if (evt.type === "message" && evt.message) {
670
+ const msg = evt.message;
671
+ const role = msg.role;
672
+ const contentBlocks = Array.isArray(msg.content) ? msg.content : [];
673
+ if (role === "user") {
674
+ userMessageCount++;
675
+ const textContent = contentBlocks.filter((b) => b.type === "text").map((b) => b.text || "").join("\n");
676
+ sessionEvents.push({
677
+ type: "user",
678
+ timestamp: evtTs,
679
+ name: "User Message",
680
+ content: textContent,
681
+ id: evt.id,
682
+ parentId: evt.parentId
683
+ });
684
+ }
685
+ if (role === "assistant") {
686
+ assistantMessageCount++;
687
+ if (msg.usage) {
688
+ const u = msg.usage;
689
+ totalInputTokens += u.input || 0;
690
+ totalOutputTokens += u.output || 0;
691
+ totalTokensSum += u.totalTokens || 0;
692
+ if ((_h = u.cost) == null ? void 0 : _h.total) totalCost += u.cost.total;
693
+ }
694
+ for (const block of contentBlocks) {
695
+ if (block.type === "text" && block.text) {
696
+ sessionEvents.push({
697
+ type: "assistant",
698
+ timestamp: evtTs,
699
+ name: "Assistant",
700
+ content: block.text,
701
+ id: evt.id,
702
+ parentId: evt.parentId,
703
+ tokens: msg.usage ? {
704
+ input: msg.usage.input || 0,
705
+ output: msg.usage.output || 0,
706
+ total: msg.usage.totalTokens || 0,
707
+ cost: (_i = msg.usage.cost) == null ? void 0 : _i.total
708
+ } : void 0,
709
+ model: modelId,
710
+ provider
711
+ });
712
+ }
713
+ if (block.type === "thinking" && block.thinking) {
714
+ thinkingBlockCount++;
715
+ const thinkId = `thinking-${thinkingBlockCount}`;
716
+ sessionEvents.push({
717
+ type: "thinking",
718
+ timestamp: evtTs,
719
+ name: "Thinking",
720
+ content: block.thinking,
721
+ id: thinkId,
722
+ parentId: evt.id
723
+ });
724
+ nodes.set(thinkId, {
725
+ id: thinkId,
726
+ type: "decision",
727
+ name: "Thinking",
728
+ startTime: evtTs,
729
+ endTime: evtTs,
730
+ status: "completed",
731
+ parentId: rootId,
732
+ children: [],
733
+ metadata: { preview: block.thinking.slice(0, 100) }
734
+ });
735
+ }
736
+ if (block.type === "toolCall") {
737
+ toolCallCount++;
738
+ const toolName = block.name || "unknown";
739
+ const toolId = `tool-${toolCallCount}`;
740
+ const toolCallId = block.id || toolId;
741
+ sessionEvents.push({
742
+ type: "tool_call",
743
+ timestamp: evtTs,
744
+ name: toolName,
745
+ toolName,
746
+ toolArgs: block.arguments,
747
+ id: toolCallId,
748
+ parentId: evt.id
749
+ });
750
+ toolCallMap.set(toolCallId, sessionEvents.length - 1);
751
+ nodes.set(toolId, {
752
+ id: toolId,
753
+ type: "tool",
754
+ name: toolName,
755
+ startTime: evtTs,
756
+ endTime: evtTs,
757
+ // updated when result arrives
758
+ status: "running",
759
+ parentId: rootId,
760
+ children: [],
761
+ metadata: {
762
+ toolCallId,
763
+ args: block.arguments
764
+ }
765
+ });
766
+ }
767
+ }
768
+ }
769
+ if (role === "toolResult") {
770
+ const toolCallId = ((_j = contentBlocks[0]) == null ? void 0 : _j.toolCallId) || evt.parentId;
771
+ const resultContent = contentBlocks.map((b) => b.text || b.content || "").join("\n");
772
+ const hasError = contentBlocks.some((b) => b.isError || b.error);
773
+ const errorText = hasError ? resultContent : void 0;
774
+ sessionEvents.push({
775
+ type: "tool_result",
776
+ timestamp: evtTs,
777
+ name: "Tool Result",
778
+ toolResult: resultContent.slice(0, 2e3),
779
+ toolError: errorText == null ? void 0 : errorText.slice(0, 500),
780
+ id: evt.id,
781
+ parentId: toolCallId
782
+ });
783
+ for (const [nodeId, node] of nodes) {
784
+ if (node.type === "tool" && ((_k = node.metadata) == null ? void 0 : _k.toolCallId) === toolCallId) {
785
+ node.endTime = evtTs;
786
+ node.status = hasError ? "failed" : "completed";
787
+ if (hasError) node.metadata.error = errorText == null ? void 0 : errorText.slice(0, 500);
788
+ const callIdx = toolCallMap.get(toolCallId);
789
+ if (callIdx !== void 0 && sessionEvents[callIdx]) {
790
+ const callTs = sessionEvents[callIdx].timestamp;
791
+ sessionEvents[sessionEvents.length - 1].duration = evtTs - callTs;
792
+ sessionEvents[callIdx].duration = evtTs - callTs;
793
+ }
794
+ break;
795
+ }
796
+ }
797
+ }
798
+ }
799
+ }
800
+ const fileStat = fs.statSync(filePath);
801
+ const fileAge = Date.now() - fileStat.mtime.getTime();
802
+ const lastEvt = rawEvents[rawEvents.length - 1];
803
+ const hasToolError = sessionEvents.some((e) => e.type === "tool_result" && e.toolError);
804
+ const lastIsAssistant = (lastEvt == null ? void 0 : lastEvt.type) === "message" && ((_l = lastEvt == null ? void 0 : lastEvt.message) == null ? void 0 : _l.role) === "assistant";
805
+ const isRecentlyModified = fileAge < 5 * 60 * 1e3;
806
+ let status;
807
+ if (hasToolError) {
808
+ status = "failed";
809
+ } else if (lastIsAssistant) {
810
+ status = "completed";
811
+ } else if (isRecentlyModified) {
812
+ status = "running";
813
+ } else {
814
+ status = "completed";
815
+ }
816
+ const tokenUsage = {
817
+ input: totalInputTokens,
818
+ output: totalOutputTokens,
819
+ total: totalTokensSum || totalInputTokens + totalOutputTokens,
820
+ cost: totalCost
821
+ };
822
+ nodes.set(rootId, {
823
+ id: rootId,
824
+ type: "agent",
825
+ name: rootName,
826
+ startTime,
827
+ endTime: lastTimestamp,
828
+ status,
829
+ parentId: void 0,
830
+ children: Array.from(nodes.keys()).filter((k) => k !== rootId),
831
+ metadata: {
832
+ provider,
833
+ model: modelId,
834
+ sessionId,
835
+ trigger,
836
+ totalTokens: tokenUsage.total,
837
+ inputTokens: tokenUsage.input,
838
+ outputTokens: tokenUsage.output,
839
+ cost: tokenUsage.cost,
840
+ userMessages: userMessageCount,
841
+ assistantMessages: assistantMessageCount,
842
+ toolCalls: toolCallCount,
843
+ thinkingBlocks: thinkingBlockCount,
844
+ "gen_ai.system": provider,
845
+ "gen_ai.request.model": modelId
846
+ }
847
+ });
848
+ const filename = path.basename(filePath);
849
+ const trace = {
850
+ id: sessionId,
851
+ nodes,
852
+ edges: [],
853
+ events: [],
854
+ startTime,
855
+ agentId,
856
+ trigger,
857
+ name: rootName,
858
+ traceId: sessionId,
859
+ spanId: sessionId,
860
+ filename,
861
+ lastModified: fileStat.mtime.getTime(),
862
+ sourceType: "session",
863
+ sourceDir: path.dirname(filePath),
864
+ sessionEvents,
865
+ tokenUsage,
866
+ metadata: {
867
+ provider,
868
+ model: modelId,
869
+ userMessages: userMessageCount,
870
+ assistantMessages: assistantMessageCount,
871
+ toolCalls: toolCallCount,
872
+ thinkingBlocks: thinkingBlockCount,
873
+ totalEvents: rawEvents.length,
874
+ sessionVersion: sessionEvent == null ? void 0 : sessionEvent.version
875
+ }
876
+ };
877
+ this.traces.set(this.traceKey(filePath), trace);
878
+ return true;
879
+ } catch {
880
+ return false;
881
+ }
882
+ }
883
+ /** Unique key for a file across directories. */
884
+ traceKey(filePath) {
885
+ for (const dir of this.allWatchDirs) {
886
+ if (filePath.startsWith(dir)) {
887
+ return path.relative(dir, filePath).replace(/\\/g, "/") + "@" + path.basename(dir);
888
+ }
889
+ }
890
+ return filePath;
891
+ }
892
+ startWatching() {
893
+ for (const dir of this.allWatchDirs) {
894
+ if (!fs.existsSync(dir)) continue;
895
+ const watcher = import_chokidar.default.watch(dir, {
896
+ ignored: /^\./,
897
+ persistent: true,
898
+ ignoreInitial: true,
899
+ depth: 0
900
+ // don't recurse into subdirectories
901
+ });
902
+ watcher.on("add", (filePath) => {
903
+ if (filePath.endsWith(".json") || filePath.endsWith(".jsonl") || filePath.endsWith(".log") || filePath.endsWith(".trace")) {
904
+ console.log(`New file: ${path.basename(filePath)}`);
905
+ if (this.loadFile(filePath)) {
906
+ const key = this.traceKey(filePath);
907
+ const trace = this.traces.get(key);
908
+ if (trace) {
909
+ this.emit("trace-added", trace);
910
+ }
911
+ }
912
+ }
913
+ });
914
+ watcher.on("change", (filePath) => {
915
+ if (filePath.endsWith(".json") || filePath.endsWith(".jsonl") || filePath.endsWith(".log") || filePath.endsWith(".trace")) {
916
+ if (this.loadFile(filePath)) {
917
+ const key = this.traceKey(filePath);
918
+ const trace = this.traces.get(key);
919
+ if (trace) {
920
+ this.emit("trace-updated", trace);
921
+ }
922
+ }
923
+ }
924
+ });
925
+ watcher.on("unlink", (filePath) => {
926
+ if (filePath.endsWith(".json") || filePath.endsWith(".jsonl") || filePath.endsWith(".log") || filePath.endsWith(".trace")) {
927
+ const key = this.traceKey(filePath);
928
+ this.traces.delete(key);
929
+ this.emit("trace-removed", key);
930
+ }
931
+ });
932
+ watcher.on("error", (error) => {
933
+ console.error(`Watcher error on ${dir}:`, error);
934
+ });
935
+ this.watchers.push(watcher);
936
+ }
937
+ console.log(`Watching ${this.allWatchDirs.length} directories for JSON/JSONL files`);
938
+ }
939
+ getAllTraces() {
940
+ return Array.from(this.traces.values()).sort((a, b) => {
941
+ return (b.lastModified || b.startTime) - (a.lastModified || a.startTime);
942
+ });
943
+ }
944
+ getTrace(filename) {
945
+ const exact = this.traces.get(filename);
946
+ if (exact) return exact;
947
+ for (const [key, trace] of this.traces) {
948
+ if (trace.filename === filename || key.endsWith(filename)) {
949
+ return trace;
950
+ }
951
+ }
952
+ return void 0;
953
+ }
954
+ getTracesByAgent(agentId) {
955
+ return this.getAllTraces().filter((trace) => trace.agentId === agentId);
956
+ }
957
+ getRecentTraces(limit = 50) {
958
+ return this.getAllTraces().slice(0, limit);
959
+ }
960
+ getTraceCount() {
961
+ return this.traces.size;
962
+ }
963
+ getAgentIds() {
964
+ const agentIds = /* @__PURE__ */ new Set();
965
+ for (const trace of this.traces.values()) {
966
+ agentIds.add(trace.agentId);
967
+ }
968
+ return Array.from(agentIds).sort();
969
+ }
970
+ stop() {
971
+ for (const w of this.watchers) {
972
+ w.close();
973
+ }
974
+ this.watchers = [];
975
+ console.log("Stopped watching all directories");
976
+ }
977
+ getTraceStats() {
978
+ const total = this.traces.size;
979
+ const agentCount = this.getAgentIds().length;
980
+ const recentCount = this.getRecentTraces(24).length;
981
+ const triggers = /* @__PURE__ */ new Map();
982
+ for (const trace of this.traces.values()) {
983
+ const trigger = trace.trigger || "unknown";
984
+ triggers.set(trigger, (triggers.get(trigger) || 0) + 1);
985
+ }
986
+ return {
987
+ total,
988
+ agentCount,
989
+ recentCount,
990
+ triggers: Object.fromEntries(triggers)
991
+ };
992
+ }
993
+ };
994
+
995
+ // src/cli.ts
996
+ var fs2 = __toESM(require("fs"), 1);
997
+ var os = __toESM(require("os"), 1);
998
+ var path2 = __toESM(require("path"), 1);
999
+ var VERSION = "0.3.1";
1000
+ function getLanAddress() {
1001
+ const interfaces = os.networkInterfaces();
1002
+ for (const name of Object.keys(interfaces)) {
1003
+ for (const iface of interfaces[name] || []) {
1004
+ if (iface.family === "IPv4" && !iface.internal) {
1005
+ return iface.address;
1006
+ }
1007
+ }
1008
+ }
1009
+ return null;
1010
+ }
1011
+ function printBanner(config, traceCount, stats) {
1012
+ var _a;
1013
+ const lan = getLanAddress();
1014
+ const host = config.host || "localhost";
1015
+ const port = config.port;
1016
+ const isPublic = host === "0.0.0.0";
1017
+ console.log(`
1018
+ ___ _ _____ _
1019
+ / _ \\ __ _ ___ _ __ | |_| ___| | _____ __
1020
+ | |_| |/ _\` |/ _ \\ '_ \\| __| |_ | |/ _ \\ \\ /\\ / /
1021
+ | _ | (_| | __/ | | | |_| _| | | (_) \\ V V /
1022
+ |_| |_|\\__, |\\___|_| |_|\\__|_| |_|\\___/ \\_/\\_/
1023
+ |___/ dashboard v${VERSION}
1024
+
1025
+ See your agents think.
1026
+
1027
+ \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
1028
+ \u2502 \u{1F916} Agents \u2502 TRACE FILES \u2502 \u{1F4CA} AgentFlow \u2502 SHOWS YOU \u2502 \u{1F310} Your browser \u2502
1029
+ \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
1030
+ \u2502 write JSON \u2502 \u2502 builds graphs, \u2502 \u2502 graph, timeline, \u2502
1031
+ \u2502 trace files. \u2502 \u2502 serves dashboard.\u2502 \u2502 metrics, health. \u2502
1032
+ \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
1033
+
1034
+ Runs locally. Your data never leaves your machine.
1035
+
1036
+ Tabs: \u{1F3AF} Graph \xB7 \u23F1\uFE0F Timeline \xB7 \u{1F4CA} Metrics \xB7 \u{1F6E0}\uFE0F Process Health \xB7 \u26A0\uFE0F Errors
1037
+
1038
+ Traces: ${config.tracesDir}${((_a = config.dataDirs) == null ? void 0 : _a.length) ? "\n Data dirs: " + config.dataDirs.join("\n ") : ""}
1039
+ Loaded: ${traceCount} traces \xB7 ${stats.totalAgents} agents \xB7 ${stats.totalExecutions} executions
1040
+ Success: ${stats.globalSuccessRate.toFixed(1)}%${stats.activeAgents > 0 ? ` \xB7 ${stats.activeAgents} active now` : ""}
1041
+ CORS: ${config.enableCors ? "enabled" : "disabled"}
1042
+ WebSocket: live updates enabled
1043
+
1044
+ \u2192 http://localhost:${port}${isPublic && lan ? `
1045
+ \u2192 http://${lan}:${port} (LAN)` : ""}
1046
+ `);
1047
+ }
1048
+ async function startDashboard() {
1049
+ const args = process.argv.slice(2);
1050
+ const config = {
1051
+ port: 3e3,
1052
+ tracesDir: "./traces",
1053
+ host: "localhost",
1054
+ enableCors: false
1055
+ };
1056
+ for (let i = 0; i < args.length; i++) {
1057
+ switch (args[i]) {
1058
+ case "--port":
1059
+ case "-p":
1060
+ config.port = parseInt(args[++i]) || 3e3;
1061
+ break;
1062
+ case "--traces":
1063
+ case "-t":
1064
+ config.tracesDir = args[++i];
1065
+ break;
1066
+ case "--host":
1067
+ case "-h":
1068
+ config.host = args[++i];
1069
+ break;
1070
+ case "--data-dir":
1071
+ if (!config.dataDirs) config.dataDirs = [];
1072
+ config.dataDirs.push(args[++i]);
1073
+ break;
1074
+ case "--cors":
1075
+ config.enableCors = true;
1076
+ break;
1077
+ case "--help":
1078
+ printHelp();
1079
+ process.exit(0);
1080
+ }
1081
+ }
1082
+ const tracesPath = path2.resolve(config.tracesDir);
1083
+ if (!fs2.existsSync(tracesPath)) {
1084
+ fs2.mkdirSync(tracesPath, { recursive: true });
1085
+ }
1086
+ config.tracesDir = tracesPath;
1087
+ console.log("\nStarting AgentFlow Dashboard...\n");
1088
+ const dashboard = new DashboardServer(config);
1089
+ process.on("SIGINT", async () => {
1090
+ console.log("\n\u{1F6D1} Shutting down dashboard...");
1091
+ await dashboard.stop();
1092
+ process.exit(0);
1093
+ });
1094
+ process.on("SIGTERM", async () => {
1095
+ await dashboard.stop();
1096
+ process.exit(0);
1097
+ });
1098
+ try {
1099
+ await dashboard.start();
1100
+ setTimeout(() => {
1101
+ const stats = dashboard.getStats();
1102
+ const traces = dashboard.getTraces();
1103
+ printBanner(config, traces.length, stats);
1104
+ }, 1500);
1105
+ } catch (error) {
1106
+ console.error("\u274C Failed to start dashboard:", error);
1107
+ process.exit(1);
1108
+ }
1109
+ }
1110
+ function printHelp() {
1111
+ console.log(`
1112
+ \u{1F4CA} AgentFlow Dashboard v${VERSION} \u2014 See your agents think.
1113
+
1114
+ Usage:
1115
+ agentflow-dashboard [options]
1116
+ npx agentflow-dashboard [options]
1117
+
1118
+ Options:
1119
+ -p, --port <number> Server port (default: 3000)
1120
+ -t, --traces <path> Traces directory (default: ./traces)
1121
+ -h, --host <address> Host address (default: localhost)
1122
+ --data-dir <path> Extra data directory for process discovery (repeatable)
1123
+ --cors Enable CORS headers
1124
+ --help Show this help message
1125
+
1126
+ Examples:
1127
+ agentflow-dashboard --traces ./traces --host 0.0.0.0 --cors
1128
+ agentflow-dashboard -p 8080 -t /var/log/agentflow
1129
+ agentflow-dashboard --traces ./traces --data-dir ./workers --data-dir ./cron
1130
+
1131
+ Tabs:
1132
+ \u{1F3AF} Graph Interactive Cytoscape.js execution graph
1133
+ \u23F1\uFE0F Timeline Waterfall view of node durations
1134
+ \u{1F4CA} Metrics Success rates, durations, node breakdown
1135
+ \u{1F6E0}\uFE0F Process Health PID files, systemd, workers, orphans
1136
+ \u26A0\uFE0F Errors Failed and hung nodes with metadata
1137
+ `);
1138
+ }
1139
+
1140
+ // src/server.ts
1141
+ var import_meta = {};
1142
+ var __filename = (0, import_url.fileURLToPath)(import_meta.url);
1143
+ var __dirname = path3.dirname(__filename);
1144
+ var DashboardServer = class {
1145
+ constructor(config) {
1146
+ this.config = config;
1147
+ this.watcher = new TraceWatcher({
1148
+ tracesDir: config.tracesDir,
1149
+ dataDirs: config.dataDirs
1150
+ });
1151
+ this.stats = new AgentStats();
1152
+ this.setupExpress();
1153
+ this.setupWebSocket();
1154
+ this.setupTraceWatcher();
1155
+ }
1156
+ app = (0, import_express.default)();
1157
+ server = (0, import_http.createServer)(this.app);
1158
+ wss = new import_ws.WebSocketServer({ server: this.server });
1159
+ watcher;
1160
+ stats;
1161
+ processHealthCache = { result: null, ts: 0 };
1162
+ setupExpress() {
1163
+ if (this.config.enableCors) {
1164
+ this.app.use((req, res, next) => {
1165
+ res.header("Access-Control-Allow-Origin", "*");
1166
+ res.header(
1167
+ "Access-Control-Allow-Headers",
1168
+ "Origin, X-Requested-With, Content-Type, Accept"
1169
+ );
1170
+ next();
1171
+ });
1172
+ }
1173
+ const publicDir = path3.join(__dirname, "../public");
1174
+ if (fs3.existsSync(publicDir)) {
1175
+ this.app.use(import_express.default.static(publicDir));
1176
+ }
1177
+ this.app.get("/api/traces", (req, res) => {
1178
+ try {
1179
+ const traces = this.watcher.getAllTraces();
1180
+ res.json(traces);
1181
+ } catch (error) {
1182
+ res.status(500).json({ error: "Failed to load traces" });
1183
+ }
1184
+ });
1185
+ this.app.get("/api/traces/:filename", (req, res) => {
1186
+ try {
1187
+ const trace = this.watcher.getTrace(req.params.filename);
1188
+ if (!trace) {
1189
+ return res.status(404).json({ error: "Trace not found" });
1190
+ }
1191
+ res.json(trace);
1192
+ } catch (error) {
1193
+ res.status(500).json({ error: "Failed to load trace" });
1194
+ }
1195
+ });
1196
+ this.app.get("/api/traces/:filename/events", (req, res) => {
1197
+ try {
1198
+ const trace = this.watcher.getTrace(req.params.filename);
1199
+ if (!trace) {
1200
+ return res.status(404).json({ error: "Trace not found" });
1201
+ }
1202
+ res.json({
1203
+ events: trace.sessionEvents || [],
1204
+ tokenUsage: trace.tokenUsage || null,
1205
+ sourceType: trace.sourceType || "trace"
1206
+ });
1207
+ } catch (error) {
1208
+ res.status(500).json({ error: "Failed to load trace events" });
1209
+ }
1210
+ });
1211
+ this.app.get("/api/agents", (req, res) => {
1212
+ try {
1213
+ const agents = this.stats.getAgentsList();
1214
+ res.json(agents);
1215
+ } catch (error) {
1216
+ res.status(500).json({ error: "Failed to load agents" });
1217
+ }
1218
+ });
1219
+ this.app.get("/api/stats", (req, res) => {
1220
+ try {
1221
+ const globalStats = this.stats.getGlobalStats();
1222
+ res.json(globalStats);
1223
+ } catch (error) {
1224
+ res.status(500).json({ error: "Failed to load statistics" });
1225
+ }
1226
+ });
1227
+ this.app.get("/api/stats/:agentId", (req, res) => {
1228
+ try {
1229
+ const agentStats = this.stats.getAgentStats(req.params.agentId);
1230
+ if (!agentStats) {
1231
+ return res.status(404).json({ error: "Agent not found" });
1232
+ }
1233
+ res.json(agentStats);
1234
+ } catch (error) {
1235
+ res.status(500).json({ error: "Failed to load agent statistics" });
1236
+ }
1237
+ });
1238
+ this.app.get("/api/process-health", (req, res) => {
1239
+ try {
1240
+ const now = Date.now();
1241
+ if (this.processHealthCache.result && now - this.processHealthCache.ts < 1e4) {
1242
+ return res.json(this.processHealthCache.result);
1243
+ }
1244
+ const discoveryDirs = [
1245
+ this.config.tracesDir,
1246
+ path3.dirname(this.config.tracesDir),
1247
+ ...this.config.dataDirs || []
1248
+ ];
1249
+ const processConfig = (0, import_agentflow_core3.discoverProcessConfig)(discoveryDirs);
1250
+ if (!processConfig) {
1251
+ return res.json(null);
1252
+ }
1253
+ const result = (0, import_agentflow_core3.auditProcesses)(processConfig);
1254
+ this.processHealthCache = { result, ts: now };
1255
+ res.json(result);
1256
+ } catch (error) {
1257
+ res.status(500).json({ error: "Failed to audit processes" });
1258
+ }
1259
+ });
1260
+ this.app.get("*", (req, res) => {
1261
+ const indexPath = path3.join(__dirname, "../public/index.html");
1262
+ if (fs3.existsSync(indexPath)) {
1263
+ res.sendFile(indexPath);
1264
+ } else {
1265
+ res.status(404).send("Dashboard not found - public files may not be built");
1266
+ }
1267
+ });
1268
+ }
1269
+ setupWebSocket() {
1270
+ this.wss.on("connection", (ws) => {
1271
+ console.log("Dashboard client connected");
1272
+ ws.send(
1273
+ JSON.stringify({
1274
+ type: "init",
1275
+ data: {
1276
+ traces: this.watcher.getAllTraces(),
1277
+ stats: this.stats.getGlobalStats()
1278
+ }
1279
+ })
1280
+ );
1281
+ ws.on("close", () => {
1282
+ console.log("Dashboard client disconnected");
1283
+ });
1284
+ ws.on("error", (error) => {
1285
+ console.error("WebSocket error:", error);
1286
+ });
1287
+ });
1288
+ }
1289
+ setupTraceWatcher() {
1290
+ this.watcher.on("trace-added", (trace) => {
1291
+ this.stats.processTrace(trace);
1292
+ this.broadcast({
1293
+ type: "trace-added",
1294
+ data: trace
1295
+ });
1296
+ });
1297
+ this.watcher.on("trace-updated", (trace) => {
1298
+ this.stats.processTrace(trace);
1299
+ this.broadcast({
1300
+ type: "trace-updated",
1301
+ data: trace
1302
+ });
1303
+ });
1304
+ this.watcher.on("stats-updated", () => {
1305
+ this.broadcast({
1306
+ type: "stats-updated",
1307
+ data: this.stats.getGlobalStats()
1308
+ });
1309
+ });
1310
+ }
1311
+ broadcast(message) {
1312
+ this.wss.clients.forEach((client) => {
1313
+ if (client.readyState === 1) {
1314
+ client.send(JSON.stringify(message));
1315
+ }
1316
+ });
1317
+ }
1318
+ async start() {
1319
+ return new Promise((resolve3) => {
1320
+ const host = this.config.host || "localhost";
1321
+ this.server.listen(this.config.port, host, () => {
1322
+ console.log(`AgentFlow Dashboard running at http://${host}:${this.config.port}`);
1323
+ console.log(`Watching traces in: ${this.config.tracesDir}`);
1324
+ resolve3();
1325
+ });
1326
+ });
1327
+ }
1328
+ async stop() {
1329
+ return new Promise((resolve3) => {
1330
+ this.watcher.stop();
1331
+ this.server.close(() => {
1332
+ console.log("Dashboard server stopped");
1333
+ resolve3();
1334
+ });
1335
+ });
1336
+ }
1337
+ getStats() {
1338
+ return this.stats.getGlobalStats();
1339
+ }
1340
+ getTraces() {
1341
+ return this.watcher.getAllTraces();
1342
+ }
1343
+ };
1344
+ if (import_meta.url === `file://${process.argv[1]}`) {
1345
+ startDashboard().catch(console.error);
1346
+ }
1347
+ // Annotate the CommonJS export names for ESM import in node:
1348
+ 0 && (module.exports = {
1349
+ DashboardServer
1350
+ });