agentflow-dashboard 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -45,6 +45,422 @@ var import_agentflow_core3 = require("agentflow-core");
45
45
  var import_express = __toESM(require("express"), 1);
46
46
  var import_ws = require("ws");
47
47
 
48
+ // src/adapters/agentflow.ts
49
+ var SKIP_FILES = /* @__PURE__ */ new Set([
50
+ "workers.json",
51
+ "package.json",
52
+ "package-lock.json",
53
+ "tsconfig.json",
54
+ "biome.json",
55
+ "auth.json",
56
+ "models.json",
57
+ "config.json"
58
+ ]);
59
+ var SKIP_SUFFIXES = ["-state.json", "-config.json", "-watch-state.json", ".tmp", ".bak", ".backup"];
60
+ var AgentFlowAdapter = class {
61
+ name = "agentflow";
62
+ detect(_dirPath) {
63
+ return true;
64
+ }
65
+ canHandle(filePath) {
66
+ const filename = filePath.split("/").pop() ?? "";
67
+ if (SKIP_FILES.has(filename)) return false;
68
+ if (SKIP_SUFFIXES.some((s) => filename.endsWith(s))) return false;
69
+ return filename.endsWith(".json") || filename.endsWith(".jsonl") || filename.endsWith(".log") || filename.endsWith(".trace");
70
+ }
71
+ parse(_filePath) {
72
+ return [];
73
+ }
74
+ };
75
+
76
+ // src/adapters/openclaw.ts
77
+ var import_node_fs = require("fs");
78
+ var import_node_path = require("path");
79
+ var jobCache = /* @__PURE__ */ new Map();
80
+ function loadJobs(openclawDir) {
81
+ const cached = jobCache.get(openclawDir);
82
+ if (cached) return cached;
83
+ const jobsPath = (0, import_node_path.join)(openclawDir, "cron", "jobs.json");
84
+ const map = /* @__PURE__ */ new Map();
85
+ try {
86
+ if ((0, import_node_fs.existsSync)(jobsPath)) {
87
+ const data = JSON.parse((0, import_node_fs.readFileSync)(jobsPath, "utf-8"));
88
+ const jobs = Array.isArray(data) ? data : data.jobs ?? [];
89
+ for (const job of jobs) {
90
+ if (job.id) map.set(job.id, job);
91
+ }
92
+ }
93
+ } catch {
94
+ }
95
+ jobCache.set(openclawDir, map);
96
+ return map;
97
+ }
98
+ function findOpenClawRoot(filePath) {
99
+ let dir = (0, import_node_path.dirname)(filePath);
100
+ for (let i = 0; i < 5; i++) {
101
+ if ((0, import_node_fs.existsSync)((0, import_node_path.join)(dir, "cron", "jobs.json")) || (0, import_node_path.basename)(dir) === ".openclaw") {
102
+ return dir;
103
+ }
104
+ dir = (0, import_node_path.dirname)(dir);
105
+ }
106
+ return null;
107
+ }
108
+ var OpenClawAdapter = class {
109
+ name = "openclaw";
110
+ detect(dirPath) {
111
+ return (0, import_node_fs.existsSync)((0, import_node_path.join)(dirPath, "cron", "jobs.json")) || dirPath.includes(".openclaw") || (0, import_node_fs.existsSync)((0, import_node_path.join)(dirPath, "cron", "runs"));
112
+ }
113
+ canHandle(filePath) {
114
+ if (!filePath.endsWith(".jsonl")) return false;
115
+ return filePath.includes("/cron/runs/") || filePath.includes("\\cron\\runs\\");
116
+ }
117
+ parse(filePath) {
118
+ const traces = [];
119
+ try {
120
+ const content = (0, import_node_fs.readFileSync)(filePath, "utf-8");
121
+ const root = findOpenClawRoot(filePath);
122
+ const jobs = root ? loadJobs(root) : /* @__PURE__ */ new Map();
123
+ for (const line of content.split("\n")) {
124
+ if (!line.trim()) continue;
125
+ let entry;
126
+ try {
127
+ entry = JSON.parse(line);
128
+ } catch {
129
+ continue;
130
+ }
131
+ if (entry.action !== "finished") continue;
132
+ const jobId = entry.jobId ?? (0, import_node_path.basename)(filePath, ".jsonl");
133
+ const job = jobs.get(jobId);
134
+ const jobName = (job == null ? void 0 : job.name) ?? jobId;
135
+ const startTime = entry.runAtMs ?? entry.ts;
136
+ const duration = entry.durationMs ?? 0;
137
+ const trace = {
138
+ id: entry.sessionId ?? `${jobId}-${entry.ts}`,
139
+ agentId: `openclaw:${jobId}`,
140
+ name: jobName,
141
+ status: entry.status === "ok" ? "completed" : entry.status === "error" ? "failed" : "unknown",
142
+ startTime,
143
+ endTime: startTime + duration,
144
+ trigger: "cron",
145
+ source: "openclaw",
146
+ nodes: {
147
+ root: {
148
+ id: "root",
149
+ type: "cron-job",
150
+ name: jobName,
151
+ status: entry.status === "ok" ? "completed" : entry.status === "error" ? "failed" : "unknown",
152
+ startTime,
153
+ endTime: startTime + duration,
154
+ parentId: null,
155
+ children: [],
156
+ metadata: {
157
+ jobId,
158
+ summary: entry.summary,
159
+ error: entry.error,
160
+ delivered: entry.delivered,
161
+ deliveryStatus: entry.deliveryStatus
162
+ }
163
+ }
164
+ },
165
+ metadata: {
166
+ model: entry.model,
167
+ provider: entry.provider,
168
+ usage: entry.usage,
169
+ sessionId: entry.sessionId,
170
+ sessionKey: entry.sessionKey,
171
+ nextRunAtMs: entry.nextRunAtMs
172
+ },
173
+ filePath
174
+ };
175
+ traces.push(trace);
176
+ }
177
+ } catch {
178
+ }
179
+ return traces;
180
+ }
181
+ };
182
+
183
+ // src/adapters/otel.ts
184
+ var import_node_fs2 = require("fs");
185
+ var import_node_path2 = require("path");
186
+ var SPAN_TYPE_MAP = {
187
+ "gen_ai.chat": "llm",
188
+ "gen_ai.completion": "llm",
189
+ "gen_ai.embeddings": "embedding",
190
+ "gen_ai.content.prompt": "llm",
191
+ "gen_ai.content.completion": "llm"
192
+ };
193
+ function mapSpanType(spanName, attributes) {
194
+ for (const [prefix, type] of Object.entries(SPAN_TYPE_MAP)) {
195
+ if (spanName.startsWith(prefix)) return type;
196
+ }
197
+ if (attributes["tool.name"] || attributes["code.function"]) return "tool";
198
+ if (attributes["gen_ai.system"] || attributes["llm.vendor"]) return "llm";
199
+ if (attributes["db.system"]) return "database";
200
+ if (attributes["http.method"] || attributes["http.request.method"]) return "http";
201
+ return "span";
202
+ }
203
+ function extractAttributes(attrs) {
204
+ const result = {};
205
+ if (!Array.isArray(attrs)) return result;
206
+ for (const attr of attrs) {
207
+ const a = attr;
208
+ if (!a.key || !a.value) continue;
209
+ result[a.key] = a.value.stringValue ?? a.value.intValue ?? a.value.doubleValue ?? a.value.boolValue;
210
+ }
211
+ return result;
212
+ }
213
+ function parseOtlpPayload(payload) {
214
+ var _a, _b, _c;
215
+ const traceMap = /* @__PURE__ */ new Map();
216
+ for (const rs of payload.resourceSpans ?? []) {
217
+ const resourceAttrs = extractAttributes(((_a = rs.resource) == null ? void 0 : _a.attributes) ?? []);
218
+ for (const ss of rs.scopeSpans ?? []) {
219
+ for (const span of ss.spans ?? []) {
220
+ if (!span.traceId) continue;
221
+ let entry = traceMap.get(span.traceId);
222
+ if (!entry) {
223
+ entry = { spans: [], resource: resourceAttrs };
224
+ traceMap.set(span.traceId, entry);
225
+ }
226
+ entry.spans.push(span);
227
+ }
228
+ }
229
+ }
230
+ const traces = [];
231
+ for (const [traceId, { spans, resource }] of traceMap) {
232
+ const nodes = {};
233
+ const childMap = /* @__PURE__ */ new Map();
234
+ let traceStart = Number.MAX_SAFE_INTEGER;
235
+ let traceEnd = 0;
236
+ let hasFailed = false;
237
+ for (const span of spans) {
238
+ const attrs = extractAttributes(span.attributes ?? []);
239
+ const startNs = span.startTimeUnixNano ? Number(BigInt(span.startTimeUnixNano) / 1000000n) : 0;
240
+ const endNs = span.endTimeUnixNano ? Number(BigInt(span.endTimeUnixNano) / 1000000n) : null;
241
+ const failed = ((_b = span.status) == null ? void 0 : _b.code) === 2;
242
+ if (failed) hasFailed = true;
243
+ if (startNs < traceStart) traceStart = startNs;
244
+ if (endNs && endNs > traceEnd) traceEnd = endNs;
245
+ nodes[span.spanId] = {
246
+ id: span.spanId,
247
+ type: mapSpanType(span.name, attrs),
248
+ name: span.name,
249
+ status: failed ? "failed" : endNs ? "completed" : "running",
250
+ startTime: startNs,
251
+ endTime: endNs,
252
+ parentId: span.parentSpanId ?? null,
253
+ children: [],
254
+ metadata: {
255
+ ...attrs,
256
+ model: attrs["gen_ai.request.model"] ?? attrs["llm.request.model"],
257
+ inputTokens: attrs["gen_ai.usage.input_tokens"] ?? attrs["llm.usage.input_tokens"],
258
+ outputTokens: attrs["gen_ai.usage.output_tokens"] ?? attrs["llm.usage.output_tokens"]
259
+ }
260
+ };
261
+ if (span.parentSpanId) {
262
+ const siblings = childMap.get(span.parentSpanId) ?? [];
263
+ siblings.push(span.spanId);
264
+ childMap.set(span.parentSpanId, siblings);
265
+ }
266
+ }
267
+ for (const [parentId, children] of childMap) {
268
+ if (nodes[parentId]) {
269
+ nodes[parentId].children = children;
270
+ }
271
+ }
272
+ const serviceName = resource["service.name"] ?? "unknown-service";
273
+ traces.push({
274
+ id: traceId,
275
+ agentId: `otel:${serviceName}`,
276
+ name: ((_c = spans.find((s) => !s.parentSpanId)) == null ? void 0 : _c.name) ?? traceId,
277
+ status: hasFailed ? "failed" : "completed",
278
+ startTime: traceStart === Number.MAX_SAFE_INTEGER ? 0 : traceStart,
279
+ endTime: traceEnd,
280
+ trigger: "otel",
281
+ source: "otel",
282
+ nodes,
283
+ metadata: { ...resource }
284
+ });
285
+ }
286
+ return traces;
287
+ }
288
+ var OTelAdapter = class {
289
+ name = "otel";
290
+ detect(dirPath) {
291
+ try {
292
+ if ((0, import_node_fs2.existsSync)((0, import_node_path2.join)(dirPath, "otel-traces"))) return true;
293
+ const files = (0, import_node_fs2.readdirSync)(dirPath);
294
+ return files.some((f) => f.endsWith(".otlp.json"));
295
+ } catch {
296
+ return false;
297
+ }
298
+ }
299
+ canHandle(filePath) {
300
+ return filePath.endsWith(".otlp.json");
301
+ }
302
+ parse(filePath) {
303
+ try {
304
+ const content = (0, import_node_fs2.readFileSync)(filePath, "utf-8");
305
+ const payload = JSON.parse(content);
306
+ const traces = parseOtlpPayload(payload);
307
+ for (const t of traces) t.filePath = filePath;
308
+ return traces;
309
+ } catch {
310
+ return [];
311
+ }
312
+ }
313
+ };
314
+
315
+ // src/adapters/registry.ts
316
+ var adapters = [];
317
+ function registerAdapter(adapter) {
318
+ adapters.push(adapter);
319
+ }
320
+ function findAdapter(filePath) {
321
+ for (const adapter of adapters) {
322
+ if (adapter.canHandle(filePath)) return adapter;
323
+ }
324
+ return null;
325
+ }
326
+
327
+ // src/adapters/index.ts
328
+ registerAdapter(new OpenClawAdapter());
329
+ registerAdapter(new OTelAdapter());
330
+ registerAdapter(new AgentFlowAdapter());
331
+
332
+ // src/agent-clustering.ts
333
+ var PURPOSE_KEYWORDS = [
334
+ { keywords: ["email", "mail", "inbox", "smtp"], group: "Email Processors" },
335
+ { keywords: ["monitor", "watch", "alert", "surveillance"], group: "Monitors" },
336
+ { keywords: ["digest", "newsletter", "summary", "report", "briefing"], group: "Digests & Reports" },
337
+ { keywords: ["curator", "janitor", "distiller", "surveyor", "worker", "indexer"], group: "Workers" },
338
+ { keywords: ["cron", "schedule", "timer", "periodic"], group: "Scheduled Jobs" },
339
+ { keywords: ["search", "scrape", "crawl", "fetch"], group: "Data Collection" },
340
+ { keywords: ["embed", "vector", "index"], group: "Embeddings" }
341
+ ];
342
+ function extractSource(agentId) {
343
+ const colonIdx = agentId.indexOf(":");
344
+ if (colonIdx > 0 && colonIdx < 20) {
345
+ const prefix = agentId.slice(0, colonIdx);
346
+ if (["openclaw", "otel", "langchain", "crewai", "mastra"].includes(prefix)) {
347
+ return { source: prefix, localId: agentId.slice(colonIdx + 1) };
348
+ }
349
+ }
350
+ return { source: "agentflow", localId: agentId };
351
+ }
352
+ function extractSuffix(localId) {
353
+ const dashIdx = localId.indexOf("-");
354
+ if (dashIdx > 0 && dashIdx < localId.length - 1) {
355
+ const suffix = localId.slice(dashIdx + 1);
356
+ if (suffix.length >= 4) return suffix;
357
+ }
358
+ return null;
359
+ }
360
+ function findPurpose(name) {
361
+ const lower = name.toLowerCase();
362
+ for (const { keywords, group } of PURPOSE_KEYWORDS) {
363
+ if (keywords.some((kw) => lower.includes(kw))) return group;
364
+ }
365
+ return "General";
366
+ }
367
+ function capitalize(s) {
368
+ return s.charAt(0).toUpperCase() + s.slice(1);
369
+ }
370
+ function deduplicateAgents(agents) {
371
+ const tagged = agents.map((a) => ({
372
+ ...a,
373
+ ...extractSource(a.agentId)
374
+ }));
375
+ const suffixGroups = /* @__PURE__ */ new Map();
376
+ for (const a of tagged) {
377
+ const suffix = extractSuffix(a.localId);
378
+ if (!suffix) continue;
379
+ const group = suffixGroups.get(suffix) ?? [];
380
+ group.push(a);
381
+ suffixGroups.set(suffix, group);
382
+ }
383
+ const mergedIds = /* @__PURE__ */ new Set();
384
+ const mergedAgents = [];
385
+ for (const [suffix, group] of suffixGroups) {
386
+ if (group.length < 2) continue;
387
+ const prefixes = new Set(group.map((a) => a.localId.split("-")[0]));
388
+ if (prefixes.size < 2) continue;
389
+ const merged = {
390
+ agentId: group[0].source === "agentflow" ? suffix : `${group[0].source}:${suffix}`,
391
+ displayName: suffix,
392
+ totalExecutions: group.reduce((s, a) => s + a.totalExecutions, 0),
393
+ successfulExecutions: group.reduce((s, a) => s + a.successfulExecutions, 0),
394
+ failedExecutions: group.reduce((s, a) => s + a.failedExecutions, 0),
395
+ successRate: 0,
396
+ avgExecutionTime: 0,
397
+ lastExecution: Math.max(...group.map((a) => a.lastExecution)),
398
+ triggers: {},
399
+ recentActivity: group.flatMap((a) => a.recentActivity).sort((a, b) => b.timestamp - a.timestamp).slice(0, 50),
400
+ sources: group.map((a) => a.agentId),
401
+ adapterSource: group[0].source
402
+ };
403
+ merged.successRate = merged.totalExecutions > 0 ? merged.successfulExecutions / merged.totalExecutions * 100 : 0;
404
+ const totalExecTime = group.reduce((s, a) => s + a.avgExecutionTime * a.totalExecutions, 0);
405
+ merged.avgExecutionTime = merged.totalExecutions > 0 ? totalExecTime / merged.totalExecutions : 0;
406
+ for (const a of group) {
407
+ for (const [k, v] of Object.entries(a.triggers)) {
408
+ merged.triggers[k] = (merged.triggers[k] ?? 0) + v;
409
+ }
410
+ }
411
+ mergedAgents.push(merged);
412
+ for (const a of group) mergedIds.add(a.agentId);
413
+ }
414
+ const result = [];
415
+ for (const a of agents) {
416
+ if (mergedIds.has(a.agentId)) continue;
417
+ const { source, localId } = extractSource(a.agentId);
418
+ result.push({
419
+ ...a,
420
+ displayName: a.displayName ?? localId,
421
+ adapterSource: source
422
+ });
423
+ }
424
+ return [...result, ...mergedAgents];
425
+ }
426
+ function groupAgents(agents) {
427
+ const sourceMap = /* @__PURE__ */ new Map();
428
+ for (const a of agents) {
429
+ const source = a.adapterSource ?? extractSource(a.agentId).source;
430
+ const list = sourceMap.get(source) ?? [];
431
+ list.push(a);
432
+ sourceMap.set(source, list);
433
+ }
434
+ const SOURCE_DISPLAY = {
435
+ agentflow: "AgentFlow",
436
+ openclaw: "OpenClaw",
437
+ otel: "OpenTelemetry",
438
+ langchain: "LangChain",
439
+ crewai: "CrewAI"
440
+ };
441
+ const groups = [];
442
+ for (const [source, sourceAgents] of sourceMap) {
443
+ const subMap = /* @__PURE__ */ new Map();
444
+ for (const a of sourceAgents) {
445
+ const purpose = findPurpose(a.displayName ?? a.agentId);
446
+ const list = subMap.get(purpose) ?? [];
447
+ list.push(a.agentId);
448
+ subMap.set(purpose, list);
449
+ }
450
+ const subGroups = [...subMap.entries()].map(([name, agentIds]) => ({ name, agentIds })).sort((a, b) => b.agentIds.length - a.agentIds.length);
451
+ groups.push({
452
+ name: source,
453
+ displayName: SOURCE_DISPLAY[source] ?? capitalize(source),
454
+ totalExecutions: sourceAgents.reduce((s, a) => s + a.totalExecutions, 0),
455
+ failedExecutions: sourceAgents.reduce((s, a) => s + a.failedExecutions, 0),
456
+ agents: sourceAgents.sort((a, b) => b.totalExecutions - a.totalExecutions),
457
+ subGroups
458
+ });
459
+ }
460
+ groups.sort((a, b) => b.totalExecutions - a.totalExecutions);
461
+ return { groups };
462
+ }
463
+
48
464
  // src/stats.ts
49
465
  var import_agentflow_core = require("agentflow-core");
50
466
  var AgentStats = class {
@@ -369,7 +785,7 @@ function openClawSessionIdToAgent(sessionId) {
369
785
  if (sessionId.startsWith("janitor-")) return "vault-janitor";
370
786
  if (sessionId.startsWith("curator-")) return "vault-curator";
371
787
  if (sessionId.startsWith("distiller-")) return "vault-distiller";
372
- if (sessionId.startsWith("main-")) return "main";
788
+ if (sessionId.startsWith("main-")) return "alfred-main";
373
789
  const firstSegment = sessionId.split("-")[0];
374
790
  if (firstSegment) return firstSegment;
375
791
  return "openclaw";
@@ -454,10 +870,14 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
454
870
  "package-lock.json",
455
871
  "tsconfig.json",
456
872
  "biome.json",
457
- "jobs.json",
458
873
  "auth.json",
459
874
  "models.json",
460
- "config.json"
875
+ "config.json",
876
+ "runs.json",
877
+ "sessions.json",
878
+ "containers.json",
879
+ "update-check.json",
880
+ "exec-approvals.json"
461
881
  ]);
462
882
  static SKIP_SUFFIXES = [
463
883
  "-state.json",
@@ -467,11 +887,15 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
467
887
  ".bak",
468
888
  ".backup"
469
889
  ];
470
- /** Load a .json trace, .jsonl session file, or .log file. */
890
+ /** Load a file using the adapter registry, falling back to built-in parsing. */
471
891
  loadFile(filePath) {
472
892
  const filename = path.basename(filePath);
473
893
  if (_TraceWatcher.SKIP_FILES.has(filename)) return false;
474
894
  if (_TraceWatcher.SKIP_SUFFIXES.some((s) => filename.endsWith(s))) return false;
895
+ const adapter = findAdapter(filePath);
896
+ if (adapter && adapter.name !== "agentflow") {
897
+ return this.loadViaAdapter(filePath, adapter.name);
898
+ }
475
899
  if (filePath.endsWith(".jsonl")) {
476
900
  return this.loadSessionFile(filePath);
477
901
  }
@@ -480,6 +904,57 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
480
904
  }
481
905
  return this.loadTraceFile(filePath);
482
906
  }
907
+ /** Load a file using a specific adapter and store normalized traces. */
908
+ loadViaAdapter(filePath, adapterName) {
909
+ try {
910
+ const adapter = findAdapter(filePath);
911
+ if (!adapter) return false;
912
+ const normalized = adapter.parse(filePath);
913
+ if (normalized.length === 0) return false;
914
+ for (const trace of normalized) {
915
+ const nodes = /* @__PURE__ */ new Map();
916
+ for (const [id, node] of Object.entries(trace.nodes)) {
917
+ nodes.set(id, {
918
+ id: node.id,
919
+ type: node.type,
920
+ name: node.name,
921
+ status: node.status,
922
+ startTime: node.startTime,
923
+ endTime: node.endTime,
924
+ parentId: node.parentId,
925
+ children: node.children,
926
+ metadata: node.metadata,
927
+ state: {}
928
+ });
929
+ }
930
+ const watched = {
931
+ id: trace.id,
932
+ rootNodeId: Object.keys(trace.nodes)[0] ?? "",
933
+ agentId: trace.agentId,
934
+ name: trace.name,
935
+ trigger: trace.trigger,
936
+ startTime: trace.startTime,
937
+ endTime: trace.endTime,
938
+ status: trace.status,
939
+ nodes,
940
+ edges: [],
941
+ events: [],
942
+ metadata: { ...trace.metadata, adapterSource: adapterName },
943
+ sessionEvents: trace.sessionEvents ?? [],
944
+ sourceType: "session",
945
+ filename: path.basename(filePath),
946
+ lastModified: Date.now(),
947
+ sourceDir: path.dirname(filePath)
948
+ };
949
+ const key = `${adapterName}:${trace.id}`;
950
+ this.traces.set(key, watched);
951
+ }
952
+ return true;
953
+ } catch (error) {
954
+ console.error(`Adapter ${adapterName} failed for ${filePath}:`, error);
955
+ return false;
956
+ }
957
+ }
483
958
  loadLogFile(filePath) {
484
959
  try {
485
960
  const content = fs.readFileSync(filePath, "utf8");
@@ -592,21 +1067,46 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
592
1067
  }
593
1068
  return traces;
594
1069
  }
1070
+ /**
1071
+ * Normalise agent identifiers so that the same worker is never shown
1072
+ * under two different names (e.g. "vault-curator" vs "openclaw-vault-curator").
1073
+ *
1074
+ * Canonical names: alfred-main, vault-curator, vault-janitor,
1075
+ * vault-distiller, vault-surveyor
1076
+ */
1077
+ static AGENT_ALIASES = {
1078
+ "openclaw-main": "alfred-main",
1079
+ "openclaw-vault-curator": "vault-curator",
1080
+ "openclaw-vault-janitor": "vault-janitor",
1081
+ "openclaw-vault-distiller": "vault-distiller",
1082
+ "openclaw-vault-surveyor": "vault-surveyor",
1083
+ "alfred-curator": "vault-curator",
1084
+ "alfred-janitor": "vault-janitor",
1085
+ "alfred-distiller": "vault-distiller",
1086
+ "alfred-surveyor": "vault-surveyor",
1087
+ curator: "vault-curator",
1088
+ janitor: "vault-janitor",
1089
+ distiller: "vault-distiller",
1090
+ surveyor: "vault-surveyor"
1091
+ };
1092
+ normaliseAgentId(raw) {
1093
+ return _TraceWatcher.AGENT_ALIASES[raw] ?? raw;
1094
+ }
595
1095
  detectAgentIdentifier(activity, _filename, filePath) {
596
1096
  if (activity.agent_id) {
597
1097
  const agentId = activity.agent_id;
598
- if (agentId.startsWith("vault-")) return agentId;
599
- if (agentId === "main" && filePath.includes(".alfred/")) return "alfred-main";
600
- return agentId;
1098
+ if (agentId === "main" && filePath.includes(".alfred/")) return this.normaliseAgentId("alfred-main");
1099
+ return this.normaliseAgentId(agentId);
601
1100
  }
602
1101
  const pathAgent = this.extractAgentFromPath(filePath);
603
1102
  if (filePath.includes(".alfred/") && !pathAgent.startsWith("alfred-")) {
604
- const basename2 = path.basename(filePath, path.extname(filePath));
605
- if (basename2.match(/^(janitor|curator|distiller|surveyor|alfred)$/)) {
606
- return basename2 === "alfred" ? "alfred" : `alfred-${basename2}`;
1103
+ const basename3 = path.basename(filePath, path.extname(filePath));
1104
+ if (basename3.match(/^(janitor|curator|distiller|surveyor|alfred)$/)) {
1105
+ const raw = basename3 === "alfred" ? "alfred" : `alfred-${basename3}`;
1106
+ return this.normaliseAgentId(raw);
607
1107
  }
608
1108
  }
609
- return pathAgent;
1109
+ return this.normaliseAgentId(pathAgent);
610
1110
  }
611
1111
  extractAgentFromPath(filePath) {
612
1112
  const filename = path.basename(filePath, path.extname(filePath));
@@ -1437,8 +1937,23 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
1437
1937
  getTrace(filename) {
1438
1938
  const exact = this.traces.get(filename);
1439
1939
  if (exact) return exact;
1940
+ if (filename.includes("::")) {
1941
+ const [fname, startTimeStr] = filename.split("::");
1942
+ const startTime = Number(startTimeStr);
1943
+ if (fname && !Number.isNaN(startTime)) {
1944
+ for (const trace of this.traces.values()) {
1945
+ if (trace.filename === fname && trace.startTime === startTime) {
1946
+ return trace;
1947
+ }
1948
+ }
1949
+ }
1950
+ }
1951
+ for (const prefix of ["openclaw:", "otel:", ""]) {
1952
+ const prefixed = this.traces.get(prefix + filename);
1953
+ if (prefixed) return prefixed;
1954
+ }
1440
1955
  for (const [key, trace] of this.traces) {
1441
- if (trace.filename === filename || key.endsWith(filename)) {
1956
+ if (trace.filename === filename || trace.id === filename || key.endsWith(filename)) {
1442
1957
  return trace;
1443
1958
  }
1444
1959
  }
@@ -1504,6 +2019,31 @@ function serializeTrace(trace) {
1504
2019
  var DashboardServer = class {
1505
2020
  constructor(config) {
1506
2021
  this.config = config;
2022
+ const home = process.env.HOME ?? "/home/trader";
2023
+ const configPath = path2.join(home, ".agentflow/dashboard-config.json");
2024
+ if (!config.dataDirs) config.dataDirs = [];
2025
+ try {
2026
+ if (fs2.existsSync(configPath)) {
2027
+ const saved = JSON.parse(fs2.readFileSync(configPath, "utf-8"));
2028
+ const extraDirs = saved.extraDirs ?? [];
2029
+ for (const d of extraDirs) {
2030
+ if (!config.dataDirs.includes(d)) config.dataDirs.push(d);
2031
+ }
2032
+ }
2033
+ } catch {
2034
+ }
2035
+ const autoDiscoverPaths = [
2036
+ path2.join(home, ".openclaw/cron/runs"),
2037
+ path2.join(home, ".openclaw/workspace/traces"),
2038
+ path2.join(home, ".openclaw/subagents"),
2039
+ path2.join(home, ".openclaw/agents/main/sessions"),
2040
+ path2.join(home, ".agentflow/traces")
2041
+ ];
2042
+ for (const p of autoDiscoverPaths) {
2043
+ if (fs2.existsSync(p) && !config.dataDirs.includes(p)) {
2044
+ config.dataDirs.push(p);
2045
+ }
2046
+ }
1507
2047
  this.watcher = new TraceWatcher({
1508
2048
  tracesDir: config.tracesDir,
1509
2049
  dataDirs: config.dataDirs
@@ -1552,9 +2092,13 @@ var DashboardServer = class {
1552
2092
  next();
1553
2093
  });
1554
2094
  }
2095
+ const clientDir = path2.join(__dirname, "../dist/client");
2096
+ if (fs2.existsSync(clientDir)) {
2097
+ this.app.use(import_express.default.static(clientDir));
2098
+ }
1555
2099
  const publicDir = path2.join(__dirname, "../public");
1556
2100
  if (fs2.existsSync(publicDir)) {
1557
- this.app.use(import_express.default.static(publicDir));
2101
+ this.app.use("/v1", import_express.default.static(publicDir));
1558
2102
  }
1559
2103
  this.app.get("/api/traces", (_req, res) => {
1560
2104
  try {
@@ -1590,10 +2134,28 @@ var DashboardServer = class {
1590
2134
  res.status(500).json({ error: "Failed to load trace events" });
1591
2135
  }
1592
2136
  });
1593
- this.app.get("/api/agents", (_req, res) => {
2137
+ this.app.get("/api/agents", (req, res) => {
1594
2138
  try {
1595
- const agents = this.stats.getAgentsList();
1596
- res.json(agents);
2139
+ const raw = this.stats.getAgentsList();
2140
+ for (const agent of raw) {
2141
+ if (!agent.displayName) {
2142
+ const traces = this.watcher.getTracesByAgent(agent.agentId);
2143
+ if (traces.length > 0) {
2144
+ const latest = traces[traces.length - 1];
2145
+ const name = latest == null ? void 0 : latest.name;
2146
+ if (name && name !== "default" && name !== agent.agentId && !name.startsWith("pipeline:") && name.length < 40) {
2147
+ agent.displayName = name;
2148
+ }
2149
+ }
2150
+ if (!agent.displayName) agent.displayName = agent.agentId;
2151
+ }
2152
+ }
2153
+ if (req.query.flat === "true") {
2154
+ return res.json(raw);
2155
+ }
2156
+ const deduped = deduplicateAgents(raw);
2157
+ const grouped = groupAgents(deduped);
2158
+ res.json(grouped);
1597
2159
  } catch (_error) {
1598
2160
  res.status(500).json({ error: "Failed to load agents" });
1599
2161
  }
@@ -1743,6 +2305,84 @@ var DashboardServer = class {
1743
2305
  res.status(500).json({ error: "Failed to load agent profile" });
1744
2306
  }
1745
2307
  });
2308
+ this.app.get("/api/process-model/:agentId", (req, res) => {
2309
+ try {
2310
+ const agentId = req.params.agentId;
2311
+ const allTraces = this.watcher.getTracesByAgent(agentId);
2312
+ if (allTraces.length === 0) {
2313
+ return res.status(404).json({ error: "No traces for agent" });
2314
+ }
2315
+ const transMap = /* @__PURE__ */ new Map();
2316
+ const nodeTypeMap = /* @__PURE__ */ new Map();
2317
+ const variantMap = /* @__PURE__ */ new Map();
2318
+ const durationMap = /* @__PURE__ */ new Map();
2319
+ for (const trace of allTraces) {
2320
+ const serialized = serializeTrace(trace);
2321
+ const nodes = serialized.nodes;
2322
+ if (!nodes || typeof nodes !== "object") continue;
2323
+ const nodeArr = Object.values(nodes);
2324
+ const sorted = nodeArr.filter((n) => n.name && typeof n.startTime === "number" && n.startTime > 0).sort((a, b) => (a.startTime ?? 0) - (b.startTime ?? 0));
2325
+ for (let i = 0; i < sorted.length - 1; i++) {
2326
+ const from = sorted[i].name;
2327
+ const to = sorted[i + 1].name;
2328
+ const key = `${from}|||${to}`;
2329
+ transMap.set(key, (transMap.get(key) ?? 0) + 1);
2330
+ }
2331
+ for (const n of sorted) {
2332
+ if (n.name && n.type) nodeTypeMap.set(n.name, n.type);
2333
+ }
2334
+ const sig = sorted.map((n) => n.name).join("\u2192");
2335
+ if (sig) variantMap.set(sig, (variantMap.get(sig) ?? 0) + 1);
2336
+ for (const n of sorted) {
2337
+ if (n.name && n.endTime && n.startTime) {
2338
+ const dur = n.endTime - n.startTime;
2339
+ if (dur > 0) {
2340
+ const arr = durationMap.get(n.name) ?? [];
2341
+ arr.push(dur);
2342
+ durationMap.set(n.name, arr);
2343
+ }
2344
+ }
2345
+ }
2346
+ }
2347
+ const model = {
2348
+ transitions: [...transMap.entries()].map(([key, count]) => {
2349
+ const [from, to] = key.split("|||");
2350
+ return { from, to, count };
2351
+ }),
2352
+ nodeTypes: Object.fromEntries(nodeTypeMap)
2353
+ };
2354
+ const totalTraces = allTraces.length;
2355
+ const variants = [...variantMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 20).map(([sig, count]) => ({
2356
+ pathSignature: sig,
2357
+ count,
2358
+ percentage: totalTraces > 0 ? count / totalTraces * 100 : 0
2359
+ }));
2360
+ const bottlenecks = [...durationMap.entries()].map(([name, durations]) => {
2361
+ const sorted = durations.sort((a, b) => a - b);
2362
+ const p95 = sorted[Math.floor(sorted.length * 0.95)] ?? 0;
2363
+ return { nodeName: name, nodeType: nodeTypeMap.get(name) ?? "unknown", p95 };
2364
+ }).sort((a, b) => b.p95 - a.p95).slice(0, 15);
2365
+ const graphs = this.getGraphTraces(agentId);
2366
+ if (graphs.length > 0) {
2367
+ try {
2368
+ const coreBottlenecks = (0, import_agentflow_core3.getBottlenecks)(graphs).map((b) => ({
2369
+ nodeName: b.nodeName,
2370
+ nodeType: b.nodeType,
2371
+ p95: b.durations.sort((a, b2) => a - b2)[Math.floor(b.durations.length * 0.95)] ?? 0
2372
+ }));
2373
+ if (coreBottlenecks.length > bottlenecks.length) {
2374
+ bottlenecks.length = 0;
2375
+ bottlenecks.push(...coreBottlenecks);
2376
+ }
2377
+ } catch {
2378
+ }
2379
+ }
2380
+ res.json({ model, variants, bottlenecks });
2381
+ } catch (error) {
2382
+ console.error("Process model error:", error);
2383
+ res.status(500).json({ error: "Failed to compute process model" });
2384
+ }
2385
+ });
1746
2386
  this.app.get("/api/stats/:agentId", (req, res) => {
1747
2387
  try {
1748
2388
  const agentStats = this.stats.getAgentStats(req.params.agentId);
@@ -1755,6 +2395,7 @@ var DashboardServer = class {
1755
2395
  }
1756
2396
  });
1757
2397
  this.app.get("/api/process-health", (_req, res) => {
2398
+ var _a, _b;
1758
2399
  try {
1759
2400
  const now = Date.now();
1760
2401
  if (this.processHealthCache.result && now - this.processHealthCache.ts < 1e4) {
@@ -1765,67 +2406,211 @@ var DashboardServer = class {
1765
2406
  path2.dirname(this.config.tracesDir),
1766
2407
  ...this.config.dataDirs || []
1767
2408
  ];
1768
- const processConfig = (0, import_agentflow_core3.discoverProcessConfig)(discoveryDirs);
1769
- if (!processConfig) {
2409
+ const configs = (0, import_agentflow_core3.discoverAllProcessConfigs)(discoveryDirs);
2410
+ if (configs.length === 0) {
1770
2411
  return res.json(null);
1771
2412
  }
1772
- const alfredResult = (0, import_agentflow_core3.auditProcesses)(processConfig);
1773
- const openclawConfig = {
1774
- processName: "openclaw",
1775
- pidFile: void 0,
1776
- workersFile: void 0,
1777
- systemdUnit: null
1778
- };
1779
- const openclawResult = (0, import_agentflow_core3.auditProcesses)(openclawConfig);
1780
- const clawmetryConfig = {
1781
- processName: "clawmetry",
1782
- pidFile: void 0,
1783
- workersFile: void 0,
1784
- systemdUnit: null
1785
- };
1786
- const clawmetryResult = (0, import_agentflow_core3.auditProcesses)(clawmetryConfig);
1787
- const allOsProcesses = [
1788
- ...alfredResult.osProcesses,
1789
- ...openclawResult.osProcesses,
1790
- ...clawmetryResult.osProcesses
1791
- ];
2413
+ const services = [];
2414
+ const allKnownPids = /* @__PURE__ */ new Set();
2415
+ for (const config of configs) {
2416
+ const audit = (0, import_agentflow_core3.auditProcesses)(config);
2417
+ services.push({ name: config.processName, audit });
2418
+ if (((_a = audit.pidFile) == null ? void 0 : _a.pid) && !audit.pidFile.stale) allKnownPids.add(audit.pidFile.pid);
2419
+ if ((_b = audit.systemd) == null ? void 0 : _b.mainPid) allKnownPids.add(audit.systemd.mainPid);
2420
+ if (audit.workers) {
2421
+ if (audit.workers.orchestratorPid) allKnownPids.add(audit.workers.orchestratorPid);
2422
+ for (const w of audit.workers.workers) {
2423
+ if (w.pid) allKnownPids.add(w.pid);
2424
+ }
2425
+ }
2426
+ for (const p of audit.osProcesses) allKnownPids.add(p.pid);
2427
+ }
2428
+ const primary = services.find((s) => s.audit.pidFile) ?? services[0];
2429
+ const allOsProcesses = services.flatMap((s) => s.audit.osProcesses);
1792
2430
  const uniqueProcesses = allOsProcesses.filter(
1793
2431
  (proc, index, arr) => arr.findIndex((p) => p.pid === proc.pid) === index
1794
2432
  );
2433
+ const orphans = uniqueProcesses.filter(
2434
+ (p) => !allKnownPids.has(p.pid) && p.pid !== process.pid && p.pid !== process.ppid
2435
+ );
2436
+ const problems = services.flatMap(
2437
+ (s) => s.audit.problems.map((p) => `[${s.name}] ${p}`)
2438
+ );
1795
2439
  const result = {
1796
- ...alfredResult,
2440
+ // Backward-compatible fields from primary service
2441
+ pidFile: (primary == null ? void 0 : primary.audit.pidFile) ?? null,
2442
+ systemd: (primary == null ? void 0 : primary.audit.systemd) ?? null,
2443
+ workers: (primary == null ? void 0 : primary.audit.workers) ?? null,
1797
2444
  osProcesses: uniqueProcesses,
1798
- // Recalculate orphans based on all processes
1799
- orphans: uniqueProcesses.filter((p) => {
1800
- var _a;
1801
- const alfredKnownPids = /* @__PURE__ */ new Set();
1802
- if (((_a = alfredResult.pidFile) == null ? void 0 : _a.pid) && !alfredResult.pidFile.stale)
1803
- alfredKnownPids.add(alfredResult.pidFile.pid);
1804
- if (alfredResult.workers) {
1805
- if (alfredResult.workers.orchestratorPid)
1806
- alfredKnownPids.add(alfredResult.workers.orchestratorPid);
1807
- for (const w of alfredResult.workers.workers) {
1808
- if (w.pid) alfredKnownPids.add(w.pid);
2445
+ orphans,
2446
+ problems,
2447
+ // All discovered services with their individual audit results + metrics
2448
+ services: services.map((s) => {
2449
+ var _a2, _b2;
2450
+ const mainPid = ((_a2 = s.audit.pidFile) == null ? void 0 : _a2.pid) ?? ((_b2 = s.audit.systemd) == null ? void 0 : _b2.mainPid);
2451
+ const osProc = mainPid ? uniqueProcesses.find((p) => p.pid === mainPid) : void 0;
2452
+ return {
2453
+ name: s.name,
2454
+ pidFile: s.audit.pidFile,
2455
+ systemd: s.audit.systemd,
2456
+ workers: s.audit.workers,
2457
+ problems: s.audit.problems,
2458
+ metrics: osProc ? { cpu: osProc.cpu, mem: osProc.mem, elapsed: osProc.elapsed } : void 0
2459
+ };
2460
+ }),
2461
+ // Topology edges: parent-child relationships from process ppid
2462
+ topology: uniqueProcesses.map((p) => {
2463
+ try {
2464
+ const statusContent = fs2.readFileSync(`/proc/${p.pid}/status`, "utf8");
2465
+ const ppidMatch = statusContent.match(/^PPid:\s+(\d+)/m);
2466
+ const ppid = ppidMatch ? parseInt(ppidMatch[1] ?? "0", 10) : 0;
2467
+ if (ppid > 1 && allKnownPids.has(ppid)) {
2468
+ return { source: ppid, target: p.pid };
1809
2469
  }
2470
+ } catch {
1810
2471
  }
1811
- const isOpenClawProcess = p.cmdline.includes("openclaw") || p.cmdline.includes("clawmetry");
1812
- return !alfredKnownPids.has(p.pid) && !isOpenClawProcess && p.pid !== process.pid && p.pid !== process.ppid;
1813
- })
2472
+ return null;
2473
+ }).filter(Boolean)
1814
2474
  };
1815
- const openclawProblems = [];
1816
- if (openclawResult.osProcesses.length === 0) {
1817
- openclawProblems.push("No OpenClaw gateway processes detected");
1818
- }
1819
- if (clawmetryResult.osProcesses.length === 0) {
1820
- openclawProblems.push("No clawmetry processes detected");
1821
- }
1822
- result.problems = [...alfredResult.problems || [], ...openclawProblems];
1823
2475
  this.processHealthCache = { result, ts: now };
1824
2476
  res.json(result);
1825
2477
  } catch (_error) {
1826
2478
  res.status(500).json({ error: "Failed to audit processes" });
1827
2479
  }
1828
2480
  });
2481
+ this.app.get("/api/directories", (_req, res) => {
2482
+ try {
2483
+ const home = process.env.HOME ?? "/home/trader";
2484
+ const configPath = path2.join(home, ".agentflow/dashboard-config.json");
2485
+ let extraDirs = [];
2486
+ try {
2487
+ if (fs2.existsSync(configPath)) {
2488
+ const cfg = JSON.parse(fs2.readFileSync(configPath, "utf-8"));
2489
+ extraDirs = cfg.extraDirs ?? [];
2490
+ }
2491
+ } catch {
2492
+ }
2493
+ const watched = [
2494
+ this.config.tracesDir,
2495
+ ...this.config.dataDirs || [],
2496
+ ...extraDirs
2497
+ ];
2498
+ const discovered = [];
2499
+ try {
2500
+ const { execSync } = require("child_process");
2501
+ const raw = execSync(
2502
+ "systemctl --user show --property=ExecStart --no-pager alfred.service openclaw-gateway.service 2>/dev/null",
2503
+ { encoding: "utf8", timeout: 5e3 }
2504
+ );
2505
+ for (const line of raw.split("\n")) {
2506
+ const match = line.match(/path=([^\s;]+)/);
2507
+ if (match == null ? void 0 : match[1]) {
2508
+ const dir = path2.dirname(match[1]);
2509
+ if (fs2.existsSync(dir)) discovered.push(dir);
2510
+ }
2511
+ }
2512
+ } catch {
2513
+ }
2514
+ const commonPaths = [
2515
+ path2.join(home, ".alfred/traces"),
2516
+ path2.join(home, ".alfred/data"),
2517
+ path2.join(home, ".openclaw/workspace/traces"),
2518
+ path2.join(home, ".openclaw/subagents"),
2519
+ path2.join(home, ".openclaw/cron/runs"),
2520
+ path2.join(home, ".openclaw/cron"),
2521
+ path2.join(home, ".openclaw/agents/main/sessions"),
2522
+ path2.join(home, ".agentflow/traces")
2523
+ ];
2524
+ for (const p of commonPaths) {
2525
+ if (fs2.existsSync(p) && !discovered.includes(p)) {
2526
+ discovered.push(p);
2527
+ }
2528
+ }
2529
+ const watchedSet = new Set(watched.map((w) => path2.resolve(w)));
2530
+ const suggested = discovered.filter((d) => !watchedSet.has(path2.resolve(d)));
2531
+ res.json({ watched, discovered, suggested });
2532
+ } catch (error) {
2533
+ console.error("Directory discovery error:", error);
2534
+ res.status(500).json({ error: "Failed to discover directories" });
2535
+ }
2536
+ });
2537
+ this.app.post("/api/directories", import_express.default.json(), (req, res) => {
2538
+ try {
2539
+ const { add, remove } = req.body;
2540
+ if (add && !fs2.existsSync(add)) {
2541
+ return res.status(400).json({ error: `Directory does not exist: ${add}` });
2542
+ }
2543
+ const configPath = path2.join(process.env.HOME ?? "/home/trader", ".agentflow/dashboard-config.json");
2544
+ let config = {};
2545
+ try {
2546
+ if (fs2.existsSync(configPath)) {
2547
+ config = JSON.parse(fs2.readFileSync(configPath, "utf-8"));
2548
+ }
2549
+ } catch {
2550
+ }
2551
+ if (!config.extraDirs) config.extraDirs = [];
2552
+ if (add && !config.extraDirs.includes(add)) {
2553
+ config.extraDirs.push(add);
2554
+ }
2555
+ if (remove) {
2556
+ config.extraDirs = config.extraDirs.filter((d) => d !== remove);
2557
+ }
2558
+ fs2.mkdirSync(path2.dirname(configPath), { recursive: true });
2559
+ fs2.writeFileSync(configPath, JSON.stringify(config, null, 2));
2560
+ res.json({ ok: true, extraDirs: config.extraDirs });
2561
+ } catch (error) {
2562
+ console.error("Directory config error:", error);
2563
+ res.status(500).json({ error: "Failed to update directory config" });
2564
+ }
2565
+ });
2566
+ if (this.config.enableCollector !== false) {
2567
+ this.app.post("/v1/traces", import_express.default.json({ limit: "10mb" }), (req, res) => {
2568
+ try {
2569
+ if (this.config.collectorAuthToken) {
2570
+ const auth = req.headers.authorization;
2571
+ if (!auth || auth !== `Bearer ${this.config.collectorAuthToken}`) {
2572
+ return res.status(401).json({ error: "Unauthorized \u2014 provide Authorization: Bearer <token>" });
2573
+ }
2574
+ }
2575
+ const traces = parseOtlpPayload(req.body);
2576
+ let ingested = 0;
2577
+ for (const trace of traces) {
2578
+ const nodes = /* @__PURE__ */ new Map();
2579
+ for (const [id, node] of Object.entries(trace.nodes)) {
2580
+ nodes.set(id, { ...node, state: {} });
2581
+ }
2582
+ const watched = {
2583
+ id: trace.id,
2584
+ rootNodeId: Object.keys(trace.nodes)[0] ?? "",
2585
+ agentId: trace.agentId,
2586
+ name: trace.name,
2587
+ trigger: trace.trigger,
2588
+ startTime: trace.startTime,
2589
+ endTime: trace.endTime,
2590
+ status: trace.status,
2591
+ nodes,
2592
+ edges: [],
2593
+ events: [],
2594
+ metadata: { ...trace.metadata, adapterSource: "otel" },
2595
+ sessionEvents: [],
2596
+ sourceType: "session",
2597
+ filename: `otel-${trace.id}`,
2598
+ lastModified: Date.now(),
2599
+ sourceDir: "http-collector"
2600
+ };
2601
+ this.watcher.traces.set(`otel:${trace.id}`, watched);
2602
+ ingested++;
2603
+ }
2604
+ if (ingested > 0) {
2605
+ this.broadcast({ type: "traces-updated", count: ingested });
2606
+ }
2607
+ res.json({ ok: true, tracesIngested: ingested });
2608
+ } catch (error) {
2609
+ console.error("OTLP collector error:", error);
2610
+ res.status(400).json({ error: "Failed to parse OTLP payload" });
2611
+ }
2612
+ });
2613
+ }
1829
2614
  this.app.get("/health", (_req, res) => {
1830
2615
  res.json({
1831
2616
  status: "ok",
@@ -1837,10 +2622,18 @@ var DashboardServer = class {
1837
2622
  this.app.get("/ready", (_req, res) => {
1838
2623
  res.json({ status: "ready" });
1839
2624
  });
2625
+ this.app.get("/v1/*", (_req, res) => {
2626
+ const legacyIndex = path2.join(__dirname, "../public/index.html");
2627
+ if (fs2.existsSync(legacyIndex)) {
2628
+ res.sendFile(legacyIndex);
2629
+ } else {
2630
+ res.status(404).send("Legacy dashboard not found");
2631
+ }
2632
+ });
1840
2633
  this.app.get("*", (_req, res) => {
1841
- const indexPath = path2.join(__dirname, "../public/index.html");
1842
- if (fs2.existsSync(indexPath)) {
1843
- res.sendFile(indexPath);
2634
+ const clientIndex = path2.join(__dirname, "../dist/client/index.html");
2635
+ if (fs2.existsSync(clientIndex)) {
2636
+ res.sendFile(clientIndex);
1844
2637
  } else {
1845
2638
  res.status(404).send("Dashboard not found - public files may not be built");
1846
2639
  }
@@ -2087,21 +2880,21 @@ var DashboardServer = class {
2087
2880
  });
2088
2881
  }
2089
2882
  async start() {
2090
- return new Promise((resolve3) => {
2883
+ return new Promise((resolve4) => {
2091
2884
  const host = this.config.host || "localhost";
2092
2885
  this.server.listen(this.config.port, host, () => {
2093
2886
  console.log(`AgentFlow Dashboard running at http://${host}:${this.config.port}`);
2094
2887
  console.log(`Watching traces in: ${this.config.tracesDir}`);
2095
- resolve3();
2888
+ resolve4();
2096
2889
  });
2097
2890
  });
2098
2891
  }
2099
2892
  async stop() {
2100
- return new Promise((resolve3) => {
2893
+ return new Promise((resolve4) => {
2101
2894
  this.watcher.stop();
2102
2895
  this.server.close(() => {
2103
2896
  console.log("Dashboard server stopped");
2104
- resolve3();
2897
+ resolve4();
2105
2898
  });
2106
2899
  });
2107
2900
  }
@@ -2196,11 +2989,23 @@ async function startDashboard() {
2196
2989
  case "--cors":
2197
2990
  config.enableCors = true;
2198
2991
  break;
2992
+ case "--no-collector":
2993
+ config.enableCollector = false;
2994
+ break;
2995
+ case "--collector-token":
2996
+ config.collectorAuthToken = args[++i];
2997
+ break;
2199
2998
  case "--help":
2200
2999
  printHelp();
2201
3000
  process.exit(0);
2202
3001
  }
2203
3002
  }
3003
+ if (!config.collectorAuthToken && process.env.AGENTFLOW_COLLECTOR_TOKEN) {
3004
+ config.collectorAuthToken = process.env.AGENTFLOW_COLLECTOR_TOKEN;
3005
+ }
3006
+ if (process.env.AGENTFLOW_NO_COLLECTOR === "true") {
3007
+ config.enableCollector = false;
3008
+ }
2204
3009
  const tracesPath = path3.resolve(config.tracesDir);
2205
3010
  if (!fs3.existsSync(tracesPath)) {
2206
3011
  fs3.mkdirSync(tracesPath, { recursive: true });
@@ -2243,6 +3048,8 @@ Options:
2243
3048
  -h, --host <address> Host address (default: localhost)
2244
3049
  --data-dir <path> Extra data directory for process discovery (repeatable)
2245
3050
  --cors Enable CORS headers
3051
+ --no-collector Disable OTLP trace collector (POST /v1/traces)
3052
+ --collector-token <tok> Require auth token for collector (or set AGENTFLOW_COLLECTOR_TOKEN)
2246
3053
  --help Show this help message
2247
3054
 
2248
3055
  Examples: