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