agentflow-dashboard 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.cjs CHANGED
@@ -32,10 +32,84 @@ __export(server_exports, {
32
32
  DashboardServer: () => DashboardServer
33
33
  });
34
34
  module.exports = __toCommonJS(server_exports);
35
+ var import_node_child_process = require("child_process");
35
36
  var fs3 = __toESM(require("fs"), 1);
36
37
  var import_node_http = require("http");
37
38
  var path3 = __toESM(require("path"), 1);
38
39
  var import_node_url = require("url");
40
+
41
+ // src/config.ts
42
+ var import_node_fs = require("fs");
43
+ var import_node_os = require("os");
44
+ var import_node_path = require("path");
45
+ var EMPTY_CONFIG = {};
46
+ function expandTilde(p) {
47
+ if (p.startsWith("~/") || p === "~") {
48
+ return (0, import_node_path.join)((0, import_node_os.homedir)(), p.slice(1));
49
+ }
50
+ return p;
51
+ }
52
+ function loadConfig(explicitPath) {
53
+ const candidates = [];
54
+ if (explicitPath) {
55
+ candidates.push((0, import_node_path.resolve)(explicitPath));
56
+ }
57
+ if (process.env.AGENTFLOW_CONFIG) {
58
+ candidates.push((0, import_node_path.resolve)(process.env.AGENTFLOW_CONFIG));
59
+ }
60
+ candidates.push((0, import_node_path.resolve)("agentflow.config.json"));
61
+ candidates.push((0, import_node_path.join)((0, import_node_os.homedir)(), ".config", "agentflow", "config.json"));
62
+ for (const candidate of candidates) {
63
+ if (!(0, import_node_fs.existsSync)(candidate)) continue;
64
+ try {
65
+ const raw = (0, import_node_fs.readFileSync)(candidate, "utf-8");
66
+ const parsed = JSON.parse(raw);
67
+ const cleaned = stripCommentKeys(parsed);
68
+ console.log(`Loaded config: ${candidate}`);
69
+ return { config: cleaned, configPath: candidate };
70
+ } catch (err) {
71
+ console.warn(`Warning: Failed to load config from ${candidate}: ${err.message}`);
72
+ console.warn("Continuing with empty defaults.");
73
+ return { config: EMPTY_CONFIG, configPath: null };
74
+ }
75
+ }
76
+ return { config: EMPTY_CONFIG, configPath: null };
77
+ }
78
+ function stripCommentKeys(obj) {
79
+ if (Array.isArray(obj)) return obj.map(stripCommentKeys);
80
+ if (obj && typeof obj === "object") {
81
+ const result = {};
82
+ for (const [key, value] of Object.entries(obj)) {
83
+ if (key.startsWith("//")) continue;
84
+ result[key] = stripCommentKeys(value);
85
+ }
86
+ return result;
87
+ }
88
+ return obj;
89
+ }
90
+ function getAliases(config) {
91
+ return config.aliases ?? {};
92
+ }
93
+ function getSkipFiles(config) {
94
+ return config.skipFiles ?? [];
95
+ }
96
+ function getSkipDirectories(config) {
97
+ return config.skipDirectories ?? [];
98
+ }
99
+ function getDiscoveryPaths(config) {
100
+ return (config.discoveryPaths ?? []).map(expandTilde);
101
+ }
102
+ function getSystemdServices(config) {
103
+ return config.systemdServices ?? [];
104
+ }
105
+ function getAgentDetection(config) {
106
+ return config.agentDetection ?? {};
107
+ }
108
+ function getProcessPreference(config) {
109
+ return config.processPreference ?? null;
110
+ }
111
+
112
+ // src/server.ts
39
113
  var import_agentflow_core3 = require("agentflow-core");
40
114
  var import_express = __toESM(require("express"), 1);
41
115
  var import_ws = require("ws");
@@ -69,17 +143,17 @@ var AgentFlowAdapter = class {
69
143
  };
70
144
 
71
145
  // src/adapters/openclaw.ts
72
- var import_node_fs = require("fs");
73
- var import_node_path = require("path");
146
+ var import_node_fs2 = require("fs");
147
+ var import_node_path2 = require("path");
74
148
  var jobCache = /* @__PURE__ */ new Map();
75
149
  function loadJobs(openclawDir) {
76
150
  const cached = jobCache.get(openclawDir);
77
151
  if (cached) return cached;
78
- const jobsPath = (0, import_node_path.join)(openclawDir, "cron", "jobs.json");
152
+ const jobsPath = (0, import_node_path2.join)(openclawDir, "cron", "jobs.json");
79
153
  const map = /* @__PURE__ */ new Map();
80
154
  try {
81
- if ((0, import_node_fs.existsSync)(jobsPath)) {
82
- const data = JSON.parse((0, import_node_fs.readFileSync)(jobsPath, "utf-8"));
155
+ if ((0, import_node_fs2.existsSync)(jobsPath)) {
156
+ const data = JSON.parse((0, import_node_fs2.readFileSync)(jobsPath, "utf-8"));
83
157
  const jobs = Array.isArray(data) ? data : data.jobs ?? [];
84
158
  for (const job of jobs) {
85
159
  if (job.id) map.set(job.id, job);
@@ -91,19 +165,19 @@ function loadJobs(openclawDir) {
91
165
  return map;
92
166
  }
93
167
  function findOpenClawRoot(filePath) {
94
- let dir = (0, import_node_path.dirname)(filePath);
168
+ let dir = (0, import_node_path2.dirname)(filePath);
95
169
  for (let i = 0; i < 5; i++) {
96
- if ((0, import_node_fs.existsSync)((0, import_node_path.join)(dir, "cron", "jobs.json")) || (0, import_node_path.basename)(dir) === ".openclaw") {
170
+ if ((0, import_node_fs2.existsSync)((0, import_node_path2.join)(dir, "cron", "jobs.json")) || (0, import_node_path2.basename)(dir) === ".openclaw") {
97
171
  return dir;
98
172
  }
99
- dir = (0, import_node_path.dirname)(dir);
173
+ dir = (0, import_node_path2.dirname)(dir);
100
174
  }
101
175
  return null;
102
176
  }
103
177
  var OpenClawAdapter = class {
104
178
  name = "openclaw";
105
179
  detect(dirPath) {
106
- 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"));
180
+ return (0, import_node_fs2.existsSync)((0, import_node_path2.join)(dirPath, "cron", "jobs.json")) || dirPath.includes(".openclaw") || (0, import_node_fs2.existsSync)((0, import_node_path2.join)(dirPath, "cron", "runs"));
107
181
  }
108
182
  canHandle(filePath) {
109
183
  if (!filePath.endsWith(".jsonl")) return false;
@@ -112,7 +186,7 @@ var OpenClawAdapter = class {
112
186
  parse(filePath) {
113
187
  const traces = [];
114
188
  try {
115
- const content = (0, import_node_fs.readFileSync)(filePath, "utf-8");
189
+ const content = (0, import_node_fs2.readFileSync)(filePath, "utf-8");
116
190
  const root = findOpenClawRoot(filePath);
117
191
  const jobs = root ? loadJobs(root) : /* @__PURE__ */ new Map();
118
192
  for (const line of content.split("\n")) {
@@ -124,7 +198,7 @@ var OpenClawAdapter = class {
124
198
  continue;
125
199
  }
126
200
  if (entry.action !== "finished") continue;
127
- const jobId = entry.jobId ?? (0, import_node_path.basename)(filePath, ".jsonl");
201
+ const jobId = entry.jobId ?? (0, import_node_path2.basename)(filePath, ".jsonl");
128
202
  const job = jobs.get(jobId);
129
203
  const jobName = (job == null ? void 0 : job.name) ?? jobId;
130
204
  const startTime = entry.runAtMs ?? entry.ts;
@@ -176,8 +250,8 @@ var OpenClawAdapter = class {
176
250
  };
177
251
 
178
252
  // src/adapters/otel.ts
179
- var import_node_fs2 = require("fs");
180
- var import_node_path2 = require("path");
253
+ var import_node_fs3 = require("fs");
254
+ var import_node_path3 = require("path");
181
255
  var SPAN_TYPE_MAP = {
182
256
  "gen_ai.chat": "llm",
183
257
  "gen_ai.completion": "llm",
@@ -284,8 +358,8 @@ var OTelAdapter = class {
284
358
  name = "otel";
285
359
  detect(dirPath) {
286
360
  try {
287
- if ((0, import_node_fs2.existsSync)((0, import_node_path2.join)(dirPath, "otel-traces"))) return true;
288
- const files = (0, import_node_fs2.readdirSync)(dirPath);
361
+ if ((0, import_node_fs3.existsSync)((0, import_node_path3.join)(dirPath, "otel-traces"))) return true;
362
+ const files = (0, import_node_fs3.readdirSync)(dirPath);
289
363
  return files.some((f) => f.endsWith(".otlp.json"));
290
364
  } catch {
291
365
  return false;
@@ -296,7 +370,7 @@ var OTelAdapter = class {
296
370
  }
297
371
  parse(filePath) {
298
372
  try {
299
- const content = (0, import_node_fs2.readFileSync)(filePath, "utf-8");
373
+ const content = (0, import_node_fs3.readFileSync)(filePath, "utf-8");
300
374
  const payload = JSON.parse(content);
301
375
  const traces = parseOtlpPayload(payload);
302
376
  for (const t of traces) t.filePath = filePath;
@@ -338,9 +412,7 @@ function extractSource(agentId) {
338
412
  const colonIdx = agentId.indexOf(":");
339
413
  if (colonIdx > 0 && colonIdx < 20) {
340
414
  const prefix = agentId.slice(0, colonIdx);
341
- if (["openclaw", "otel", "langchain", "crewai", "mastra"].includes(prefix)) {
342
- return { source: prefix, localId: agentId.slice(colonIdx + 1) };
343
- }
415
+ return { source: prefix, localId: agentId.slice(colonIdx + 1) };
344
416
  }
345
417
  return { source: "agentflow", localId: agentId };
346
418
  }
@@ -371,16 +443,20 @@ function deduplicateAgents(agents) {
371
443
  for (const a of tagged) {
372
444
  const suffix = extractSuffix(a.localId);
373
445
  if (!suffix) continue;
374
- const group = suffixGroups.get(suffix) ?? [];
446
+ const key = `${a.source}:${suffix}`;
447
+ const group = suffixGroups.get(key) ?? [];
375
448
  group.push(a);
376
- suffixGroups.set(suffix, group);
449
+ suffixGroups.set(key, group);
377
450
  }
378
451
  const mergedIds = /* @__PURE__ */ new Set();
379
452
  const mergedAgents = [];
380
- for (const [suffix, group] of suffixGroups) {
453
+ for (const [_key, group] of suffixGroups) {
454
+ const suffix = extractSuffix(group[0].localId);
381
455
  if (group.length < 2) continue;
382
456
  const prefixes = new Set(group.map((a) => a.localId.split("-")[0]));
383
457
  if (prefixes.size < 2) continue;
458
+ const longPrefixes = [...prefixes].filter((p) => p !== suffix && p.length > 2);
459
+ if (longPrefixes.length >= 2) continue;
384
460
  const merged = {
385
461
  agentId: group[0].source === "agentflow" ? suffix : `${group[0].source}:${suffix}`,
386
462
  displayName: suffix,
@@ -428,10 +504,7 @@ function groupAgents(agents) {
428
504
  }
429
505
  const SOURCE_DISPLAY = {
430
506
  agentflow: "AgentFlow",
431
- openclaw: "OpenClaw",
432
- otel: "OpenTelemetry",
433
- langchain: "LangChain",
434
- crewai: "CrewAI"
507
+ otel: "OpenTelemetry"
435
508
  };
436
509
  const groups = [];
437
510
  for (const [source, sourceAgents] of sourceMap) {
@@ -777,10 +850,6 @@ function getUniversalNodeStatus(activity) {
777
850
  return "completed";
778
851
  }
779
852
  function openClawSessionIdToAgent(sessionId) {
780
- if (sessionId.startsWith("janitor-")) return "vault-janitor";
781
- if (sessionId.startsWith("curator-")) return "vault-curator";
782
- if (sessionId.startsWith("distiller-")) return "vault-distiller";
783
- if (sessionId.startsWith("main-")) return "alfred-main";
784
853
  const firstSegment = sessionId.split("-")[0];
785
854
  if (firstSegment) return firstSegment;
786
855
  return "openclaw";
@@ -793,19 +862,81 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
793
862
  tracesDir;
794
863
  dataDirs;
795
864
  allWatchDirs;
865
+ maxAgeMs;
866
+ userConfig;
796
867
  constructor(tracesDirOrOptions) {
797
868
  super();
869
+ const defaultMaxAgeMs = 48 * 60 * 60 * 1e3;
870
+ const envHours = process.env.AGENTFLOW_TRACE_WINDOW_HOURS;
871
+ const envMaxAgeMs = envHours ? parseFloat(envHours) * 60 * 60 * 1e3 : void 0;
798
872
  if (typeof tracesDirOrOptions === "string") {
799
873
  this.tracesDir = path.resolve(tracesDirOrOptions);
800
874
  this.dataDirs = [];
875
+ this.maxAgeMs = envMaxAgeMs ?? defaultMaxAgeMs;
876
+ this.userConfig = {};
801
877
  } else {
802
878
  this.tracesDir = path.resolve(tracesDirOrOptions.tracesDir);
803
879
  this.dataDirs = (tracesDirOrOptions.dataDirs || []).map((d) => path.resolve(d));
804
- }
880
+ this.maxAgeMs = envMaxAgeMs ?? tracesDirOrOptions.maxAgeMs ?? defaultMaxAgeMs;
881
+ this.userConfig = tracesDirOrOptions.userConfig ?? {};
882
+ }
883
+ this.skipFiles = /* @__PURE__ */ new Set([
884
+ ..._TraceWatcher.STRUCTURAL_SKIP_FILES,
885
+ ...getSkipFiles(this.userConfig)
886
+ ]);
887
+ this.userSkipDirs = new Set(getSkipDirectories(this.userConfig));
805
888
  this.allWatchDirs = [this.tracesDir, ...this.dataDirs];
806
889
  this.ensureTracesDir();
807
890
  this.loadExistingFiles();
891
+ this.archiveOldTraces();
808
892
  this.startWatching();
893
+ setInterval(() => this.archiveOldTraces(), 6 * 60 * 60 * 1e3);
894
+ }
895
+ /** Move trace files older than maxAgeMs into archive/YYYY-MM/ subdirectories. */
896
+ archiveOldTraces() {
897
+ const cutoff = Date.now() - this.maxAgeMs;
898
+ let archived = 0;
899
+ for (const dir of this.allWatchDirs) {
900
+ if (!fs.existsSync(dir)) continue;
901
+ try {
902
+ this.archiveDirectory(dir, cutoff, 0);
903
+ } catch (error) {
904
+ console.warn(`Archival error in ${dir}:`, error.message);
905
+ }
906
+ }
907
+ }
908
+ archiveDirectory(dir, cutoff, depth) {
909
+ if (depth > 10) return 0;
910
+ if (path.basename(dir) === "archive") return 0;
911
+ let archived = 0;
912
+ try {
913
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
914
+ for (const entry of entries) {
915
+ if (entry.name.startsWith(".") || entry.name === "archive" || this.userSkipDirs.has(entry.name)) continue;
916
+ const fullPath = path.join(dir, entry.name);
917
+ if (entry.isDirectory()) {
918
+ archived += this.archiveDirectory(fullPath, cutoff, depth + 1);
919
+ continue;
920
+ }
921
+ if (!entry.isFile() || !this.isSupportedFile(entry.name)) continue;
922
+ try {
923
+ const stats = fs.statSync(fullPath);
924
+ if (stats.mtimeMs >= cutoff) continue;
925
+ const mtime = new Date(stats.mtimeMs);
926
+ const yearMonth = `${mtime.getFullYear()}-${String(mtime.getMonth() + 1).padStart(2, "0")}`;
927
+ const archiveDir = path.join(this.tracesDir, "archive", yearMonth);
928
+ fs.mkdirSync(archiveDir, { recursive: true });
929
+ const dest = path.join(archiveDir, entry.name);
930
+ fs.renameSync(fullPath, dest);
931
+ const key = this.traceKey(fullPath);
932
+ this.traces.delete(key);
933
+ archived++;
934
+ } catch {
935
+ }
936
+ }
937
+ } catch {
938
+ }
939
+ return archived;
809
940
  }
810
941
  ensureTracesDir() {
811
942
  if (!fs.existsSync(this.tracesDir)) {
@@ -838,9 +969,17 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
838
969
  const entries = fs.readdirSync(dir, { withFileTypes: true });
839
970
  for (const entry of entries) {
840
971
  if (entry.name.startsWith(".")) continue;
972
+ if (entry.name === "archive") continue;
973
+ if (this.userSkipDirs.has(entry.name)) continue;
841
974
  const fullPath = path.join(dir, entry.name);
842
975
  if (entry.isFile()) {
843
976
  if (this.isSupportedFile(entry.name)) {
977
+ try {
978
+ const mtime = fs.statSync(fullPath).mtimeMs;
979
+ if (Date.now() - mtime > this.maxAgeMs) continue;
980
+ } catch {
981
+ continue;
982
+ }
844
983
  if (this.loadFile(fullPath)) {
845
984
  fileCount++;
846
985
  }
@@ -858,8 +997,8 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
858
997
  isSupportedFile(filename) {
859
998
  return filename.endsWith(".json") || filename.endsWith(".jsonl") || filename.endsWith(".log") || filename.endsWith(".trace");
860
999
  }
861
- /** File names that are config/state, not tracesskip them. */
862
- static SKIP_FILES = /* @__PURE__ */ new Set([
1000
+ /** Structural file names that are never trace dataalways skipped. */
1001
+ static STRUCTURAL_SKIP_FILES = /* @__PURE__ */ new Set([
863
1002
  "workers.json",
864
1003
  "package.json",
865
1004
  "package-lock.json",
@@ -874,6 +1013,10 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
874
1013
  "update-check.json",
875
1014
  "exec-approvals.json"
876
1015
  ]);
1016
+ /** Skip files = structural + user config */
1017
+ skipFiles;
1018
+ /** Skip directories from user config */
1019
+ userSkipDirs;
877
1020
  static SKIP_SUFFIXES = [
878
1021
  "-state.json",
879
1022
  "-config.json",
@@ -885,7 +1028,7 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
885
1028
  /** Load a file using the adapter registry, falling back to built-in parsing. */
886
1029
  loadFile(filePath) {
887
1030
  const filename = path.basename(filePath);
888
- if (_TraceWatcher.SKIP_FILES.has(filename)) return false;
1031
+ if (this.skipFiles.has(filename)) return false;
889
1032
  if (_TraceWatcher.SKIP_SUFFIXES.some((s) => filename.endsWith(s))) return false;
890
1033
  const adapter = findAdapter(filePath);
891
1034
  if (adapter && adapter.name !== "agentflow") {
@@ -1062,43 +1205,26 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
1062
1205
  }
1063
1206
  return traces;
1064
1207
  }
1065
- /**
1066
- * Normalise agent identifiers so that the same worker is never shown
1067
- * under two different names (e.g. "vault-curator" vs "openclaw-vault-curator").
1068
- *
1069
- * Canonical names: alfred-main, vault-curator, vault-janitor,
1070
- * vault-distiller, vault-surveyor
1071
- */
1072
- static AGENT_ALIASES = {
1073
- "openclaw-main": "alfred-main",
1074
- "openclaw-vault-curator": "vault-curator",
1075
- "openclaw-vault-janitor": "vault-janitor",
1076
- "openclaw-vault-distiller": "vault-distiller",
1077
- "openclaw-vault-surveyor": "vault-surveyor",
1078
- "alfred-curator": "vault-curator",
1079
- "alfred-janitor": "vault-janitor",
1080
- "alfred-distiller": "vault-distiller",
1081
- "alfred-surveyor": "vault-surveyor",
1082
- curator: "vault-curator",
1083
- janitor: "vault-janitor",
1084
- distiller: "vault-distiller",
1085
- surveyor: "vault-surveyor"
1086
- };
1208
+ /** Normalise agent identifiers using config-driven alias map. */
1087
1209
  normaliseAgentId(raw) {
1088
- return _TraceWatcher.AGENT_ALIASES[raw] ?? raw;
1210
+ const aliases = getAliases(this.userConfig);
1211
+ return aliases[raw] ?? raw;
1089
1212
  }
1090
1213
  detectAgentIdentifier(activity, _filename, filePath) {
1091
1214
  if (activity.agent_id) {
1092
- const agentId = activity.agent_id;
1093
- if (agentId === "main" && filePath.includes(".alfred/")) return this.normaliseAgentId("alfred-main");
1094
- return this.normaliseAgentId(agentId);
1215
+ return this.normaliseAgentId(activity.agent_id);
1095
1216
  }
1096
1217
  const pathAgent = this.extractAgentFromPath(filePath);
1097
- if (filePath.includes(".alfred/") && !pathAgent.startsWith("alfred-")) {
1218
+ const detection = getAgentDetection(this.userConfig);
1219
+ if (detection.filePatterns) {
1098
1220
  const basename3 = path.basename(filePath, path.extname(filePath));
1099
- if (basename3.match(/^(janitor|curator|distiller|surveyor|alfred)$/)) {
1100
- const raw = basename3 === "alfred" ? "alfred" : `alfred-${basename3}`;
1101
- return this.normaliseAgentId(raw);
1221
+ for (const [pattern, template] of Object.entries(detection.filePatterns)) {
1222
+ const re = new RegExp(`^(${pattern})$`);
1223
+ const match = basename3.match(re);
1224
+ if (match) {
1225
+ const resolved = template.replace("${match}", match[1]);
1226
+ return this.normaliseAgentId(resolved);
1227
+ }
1102
1228
  }
1103
1229
  }
1104
1230
  return this.normaliseAgentId(pathAgent);
@@ -1106,20 +1232,23 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
1106
1232
  extractAgentFromPath(filePath) {
1107
1233
  const filename = path.basename(filePath, path.extname(filePath));
1108
1234
  const pathParts = filePath.split(path.sep);
1109
- if (filePath.includes(".openclaw/")) {
1110
- const agentsIndex = pathParts.lastIndexOf("agents");
1111
- if (agentsIndex !== -1 && agentsIndex + 1 < pathParts.length) {
1112
- return `openclaw-${pathParts[agentsIndex + 1]}`;
1113
- }
1114
- if (filename.startsWith("openclaw-")) {
1115
- return "openclaw-gateway";
1235
+ const detection = getAgentDetection(this.userConfig);
1236
+ let pathPrefix = "";
1237
+ if (detection.pathPatterns) {
1238
+ for (const [pathSubstring, agentId] of Object.entries(detection.pathPatterns)) {
1239
+ if (filePath.includes(pathSubstring)) {
1240
+ pathPrefix = agentId;
1241
+ break;
1242
+ }
1116
1243
  }
1117
- return "openclaw";
1118
1244
  }
1119
- if (filePath.includes(".alfred/") || filename.includes("alfred")) {
1120
- return "alfred";
1245
+ const agentsIndex = pathParts.lastIndexOf("agents");
1246
+ if (agentsIndex !== -1 && agentsIndex + 1 < pathParts.length) {
1247
+ const agentName = pathParts[agentsIndex + 1];
1248
+ return pathPrefix ? `${pathPrefix}-${agentName}` : agentName;
1121
1249
  }
1122
- for (const part of pathParts.reverse()) {
1250
+ if (pathPrefix) return pathPrefix;
1251
+ for (const part of [...pathParts].reverse()) {
1123
1252
  if (part.match(/agent|worker|service|daemon|bot|ai|llm/i)) {
1124
1253
  return part;
1125
1254
  }
@@ -1454,19 +1583,22 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
1454
1583
  const parentDir = path.basename(path.dirname(filePath));
1455
1584
  const grandParentDir = path.basename(path.dirname(path.dirname(filePath)));
1456
1585
  const greatGrandParentDir = path.basename(path.dirname(path.dirname(path.dirname(filePath))));
1457
- let agentId;
1586
+ let agentName;
1458
1587
  if (parentDir === "sessions" && greatGrandParentDir === "agents") {
1459
- agentId = grandParentDir;
1588
+ agentName = grandParentDir;
1460
1589
  } else if (grandParentDir === "agents") {
1461
- agentId = parentDir;
1462
- } else if (parentDir === "runs" && grandParentDir === "cron") {
1463
- agentId = "openclaw-cron";
1590
+ agentName = parentDir;
1464
1591
  } else {
1465
- agentId = parentDir;
1466
- }
1467
- if (filePath.includes(".alfred/") || filePath.includes("alfred")) {
1468
- if (!agentId.startsWith("alfred-")) {
1469
- agentId = `alfred-${agentId}`;
1592
+ agentName = parentDir;
1593
+ }
1594
+ let agentId = agentName;
1595
+ const detection = getAgentDetection(this.userConfig);
1596
+ if (detection.pathPatterns) {
1597
+ for (const [pathSubstring, prefix] of Object.entries(detection.pathPatterns)) {
1598
+ if (filePath.includes(pathSubstring)) {
1599
+ agentId = `${prefix}-${agentName}`;
1600
+ break;
1601
+ }
1470
1602
  }
1471
1603
  }
1472
1604
  const modelEvent = rawEvents.find((e) => e.type === "model_change");
@@ -1755,6 +1887,7 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
1755
1887
  edges: [],
1756
1888
  events: [],
1757
1889
  startTime,
1890
+ status,
1758
1891
  agentId,
1759
1892
  trigger,
1760
1893
  name: rootName,
@@ -1875,8 +2008,12 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
1875
2008
  // Ignore git directories
1876
2009
  /\.vscode/,
1877
2010
  // Ignore vscode
1878
- /\.idea/
2011
+ /\.idea/,
1879
2012
  // Ignore idea
2013
+ /\/archive\//,
2014
+ // Ignore archived trace files
2015
+ // Ignore user-configured skip directories
2016
+ ...getSkipDirectories(this.userConfig).map((d) => new RegExp(`/${d}/`))
1880
2017
  ],
1881
2018
  persistent: true,
1882
2019
  ignoreInitial: true,
@@ -1930,29 +2067,42 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
1930
2067
  });
1931
2068
  }
1932
2069
  getTrace(filename) {
2070
+ const candidates = [];
1933
2071
  const exact = this.traces.get(filename);
1934
- if (exact) return exact;
2072
+ if (exact) candidates.push(exact);
1935
2073
  if (filename.includes("::")) {
1936
2074
  const [fname, startTimeStr] = filename.split("::");
1937
2075
  const startTime = Number(startTimeStr);
1938
2076
  if (fname && !Number.isNaN(startTime)) {
1939
2077
  for (const trace of this.traces.values()) {
1940
2078
  if (trace.filename === fname && trace.startTime === startTime) {
1941
- return trace;
2079
+ candidates.push(trace);
1942
2080
  }
1943
2081
  }
1944
2082
  }
1945
2083
  }
1946
2084
  for (const prefix of ["openclaw:", "otel:", ""]) {
1947
2085
  const prefixed = this.traces.get(prefix + filename);
1948
- if (prefixed) return prefixed;
2086
+ if (prefixed) candidates.push(prefixed);
1949
2087
  }
1950
2088
  for (const [key, trace] of this.traces) {
1951
2089
  if (trace.filename === filename || trace.id === filename || key.endsWith(filename)) {
1952
- return trace;
2090
+ candidates.push(trace);
2091
+ }
2092
+ }
2093
+ if (candidates.length === 0) return void 0;
2094
+ if (candidates.length === 1) return candidates[0];
2095
+ let best = candidates[0];
2096
+ let bestNodeCount = best.nodes instanceof Map ? best.nodes.size : Object.keys(best.nodes ?? {}).length;
2097
+ for (let i = 1; i < candidates.length; i++) {
2098
+ const c = candidates[i];
2099
+ const nc = c.nodes instanceof Map ? c.nodes.size : Object.keys(c.nodes ?? {}).length;
2100
+ if (nc > bestNodeCount) {
2101
+ best = c;
2102
+ bestNodeCount = nc;
1953
2103
  }
1954
2104
  }
1955
- return void 0;
2105
+ return best;
1956
2106
  }
1957
2107
  getTracesByAgent(agentId) {
1958
2108
  return this.getAllTraces().filter((trace) => trace.agentId === agentId);
@@ -1999,7 +2149,7 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
1999
2149
  var fs2 = __toESM(require("fs"), 1);
2000
2150
  var os = __toESM(require("os"), 1);
2001
2151
  var path2 = __toESM(require("path"), 1);
2002
- var VERSION = "0.4.0";
2152
+ var VERSION = "0.8.0";
2003
2153
  function getLanAddress() {
2004
2154
  const interfaces = os.networkInterfaces();
2005
2155
  for (const name of Object.keys(interfaces)) {
@@ -2011,7 +2161,7 @@ function getLanAddress() {
2011
2161
  }
2012
2162
  return null;
2013
2163
  }
2014
- function printBanner(config, traceCount, stats) {
2164
+ function printBanner(config, traceCount, stats, configPath) {
2015
2165
  var _a;
2016
2166
  const lan = getLanAddress();
2017
2167
  const host = config.host || "localhost";
@@ -2027,26 +2177,23 @@ function printBanner(config, traceCount, stats) {
2027
2177
 
2028
2178
  See your agents think.
2029
2179
 
2030
- \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
2031
- \u2502 \u{1F916} Agents \u2502 TRACE FILES \u2502 \u{1F4CA} AgentFlow \u2502 SHOWS YOU \u2502 \u{1F310} Your browser \u2502
2032
- \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
2033
- \u2502 write JSON \u2502 \u2502 builds graphs, \u2502 \u2502 graph, timeline, \u2502
2034
- \u2502 trace files. \u2502 \u2502 serves dashboard.\u2502 \u2502 metrics, health. \u2502
2035
- \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
2036
-
2037
- Runs locally. Your data never leaves your machine.
2038
-
2039
- Tabs: \u{1F3AF} Graph \xB7 \u23F1\uFE0F Timeline \xB7 \u{1F4CA} Metrics \xB7 \u{1F6E0}\uFE0F Process Health \xB7 \u26A0\uFE0F Errors
2040
-
2041
2180
  Traces: ${config.tracesDir}${((_a = config.dataDirs) == null ? void 0 : _a.length) ? `
2042
2181
  Data dirs: ${config.dataDirs.join("\n ")}` : ""}
2043
2182
  Loaded: ${traceCount} traces \xB7 ${stats.totalAgents} agents \xB7 ${stats.totalExecutions} executions
2044
2183
  Success: ${stats.globalSuccessRate.toFixed(1)}%${stats.activeAgents > 0 ? ` \xB7 ${stats.activeAgents} active now` : ""}
2184
+ Config: ${configPath ?? "none (using defaults)"}
2045
2185
  CORS: ${config.enableCors ? "enabled" : "disabled"}
2046
2186
  WebSocket: live updates enabled
2187
+ Window: ${process.env.AGENTFLOW_TRACE_WINDOW_HOURS ?? "48"}h (set AGENTFLOW_TRACE_WINDOW_HOURS to change)
2047
2188
 
2048
2189
  \u2192 http://localhost:${port}${isPublic && lan ? `
2049
2190
  \u2192 http://${lan}:${port} (LAN)` : ""}
2191
+
2192
+ Views: Agent Profile \xB7 Execution Detail \xB7 Governance
2193
+ Tabs: Flame Chart \xB7 Agent Flow \xB7 Metrics \xB7 Dependencies
2194
+ State Machine \xB7 Summary \xB7 Transcript
2195
+
2196
+ Runs locally. Your data never leaves your machine.
2050
2197
  `);
2051
2198
  }
2052
2199
  async function startDashboard() {
@@ -2078,11 +2225,32 @@ async function startDashboard() {
2078
2225
  case "--cors":
2079
2226
  config.enableCors = true;
2080
2227
  break;
2228
+ case "--no-collector":
2229
+ config.enableCollector = false;
2230
+ break;
2231
+ case "--collector-token":
2232
+ config.collectorAuthToken = args[++i];
2233
+ break;
2234
+ case "--soma-vault":
2235
+ config.somaVault = args[++i];
2236
+ break;
2237
+ case "--config":
2238
+ config.configPath = args[++i];
2239
+ break;
2081
2240
  case "--help":
2082
2241
  printHelp();
2083
2242
  process.exit(0);
2084
2243
  }
2085
2244
  }
2245
+ if (!config.collectorAuthToken && process.env.AGENTFLOW_COLLECTOR_TOKEN) {
2246
+ config.collectorAuthToken = process.env.AGENTFLOW_COLLECTOR_TOKEN;
2247
+ }
2248
+ if (process.env.AGENTFLOW_NO_COLLECTOR === "true") {
2249
+ config.enableCollector = false;
2250
+ }
2251
+ if (!config.somaVault && process.env.SOMA_VAULT) {
2252
+ config.somaVault = process.env.SOMA_VAULT;
2253
+ }
2086
2254
  const tracesPath = path2.resolve(config.tracesDir);
2087
2255
  if (!fs2.existsSync(tracesPath)) {
2088
2256
  fs2.mkdirSync(tracesPath, { recursive: true });
@@ -2104,7 +2272,7 @@ async function startDashboard() {
2104
2272
  setTimeout(() => {
2105
2273
  const stats = dashboard.getStats();
2106
2274
  const traces = dashboard.getTraces();
2107
- printBanner(config, traces.length, stats);
2275
+ printBanner(config, traces.length, stats, dashboard.getConfigPath());
2108
2276
  }, 1500);
2109
2277
  } catch (error) {
2110
2278
  console.error("\u274C Failed to start dashboard:", error);
@@ -2113,7 +2281,7 @@ async function startDashboard() {
2113
2281
  }
2114
2282
  function printHelp() {
2115
2283
  console.log(`
2116
- \u{1F4CA} AgentFlow Dashboard v${VERSION} \u2014 See your agents think.
2284
+ AgentFlow Dashboard v${VERSION} \u2014 See your agents think.
2117
2285
 
2118
2286
  Usage:
2119
2287
  agentflow-dashboard [options]
@@ -2124,20 +2292,34 @@ Options:
2124
2292
  -t, --traces <path> Traces directory (default: ./traces)
2125
2293
  -h, --host <address> Host address (default: localhost)
2126
2294
  --data-dir <path> Extra data directory for process discovery (repeatable)
2295
+ --config <path> Path to agentflow.config.json (aliases, skip files, etc.)
2296
+ --soma-vault <path> SOMA vault directory for intelligence data
2127
2297
  --cors Enable CORS headers
2298
+ --no-collector Disable OTLP trace collector (POST /v1/traces)
2299
+ --collector-token <tok> Require auth token for collector (or set AGENTFLOW_COLLECTOR_TOKEN)
2128
2300
  --help Show this help message
2129
2301
 
2130
- Examples:
2131
- agentflow-dashboard --traces ./traces --host 0.0.0.0 --cors
2132
- agentflow-dashboard -p 8080 -t /var/log/agentflow
2133
- agentflow-dashboard --traces ./traces --data-dir ./workers --data-dir ./cron
2302
+ Config file:
2303
+ The dashboard loads agentflow.config.json for agent aliases, skip files,
2304
+ discovery paths, and systemd services. Resolution order:
2305
+ 1. --config flag
2306
+ 2. AGENTFLOW_CONFIG env var
2307
+ 3. ./agentflow.config.json
2308
+ 4. ~/.config/agentflow/config.json
2309
+
2310
+ See agentflow.config.example.json for a complete reference.
2311
+
2312
+ Environment:
2313
+ AGENTFLOW_CONFIG Path to config file
2314
+ AGENTFLOW_TRACE_WINDOW_HOURS Max age of traces to load (default: 48)
2315
+ AGENTFLOW_COLLECTOR_TOKEN Auth token for OTLP collector
2316
+ AGENTFLOW_NO_COLLECTOR=true Disable OTLP collector
2317
+ SOMA_VAULT SOMA vault directory
2134
2318
 
2135
- Tabs:
2136
- \u{1F3AF} Graph Interactive Cytoscape.js execution graph
2137
- \u23F1\uFE0F Timeline Waterfall view of node durations
2138
- \u{1F4CA} Metrics Success rates, durations, node breakdown
2139
- \u{1F6E0}\uFE0F Process Health PID files, systemd, workers, orphans
2140
- \u26A0\uFE0F Errors Failed and hung nodes with metadata
2319
+ Examples:
2320
+ agentflow-dashboard --traces ./traces --host 0.0.0.0
2321
+ agentflow-dashboard --traces ./traces --config ./agentflow.config.json
2322
+ agentflow-dashboard -p 8080 -t /var/log/agentflow --cors
2141
2323
  `);
2142
2324
  }
2143
2325
 
@@ -2160,12 +2342,15 @@ function serializeTrace(trace) {
2160
2342
  var DashboardServer = class {
2161
2343
  constructor(config) {
2162
2344
  this.config = config;
2163
- const home = process.env.HOME ?? "/home/trader";
2164
- const configPath = path3.join(home, ".agentflow/dashboard-config.json");
2345
+ const { config: userCfg, configPath: cfgPath } = loadConfig(config.configPath);
2346
+ this.userConfig = userCfg;
2347
+ this.configPath = cfgPath;
2348
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
2349
+ const dashConfigPath = path3.join(home, ".agentflow/dashboard-config.json");
2165
2350
  if (!config.dataDirs) config.dataDirs = [];
2166
2351
  try {
2167
- if (fs3.existsSync(configPath)) {
2168
- const saved = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
2352
+ if (fs3.existsSync(dashConfigPath)) {
2353
+ const saved = JSON.parse(fs3.readFileSync(dashConfigPath, "utf-8"));
2169
2354
  const extraDirs = saved.extraDirs ?? [];
2170
2355
  for (const d of extraDirs) {
2171
2356
  if (!config.dataDirs.includes(d)) config.dataDirs.push(d);
@@ -2173,21 +2358,15 @@ var DashboardServer = class {
2173
2358
  }
2174
2359
  } catch {
2175
2360
  }
2176
- const autoDiscoverPaths = [
2177
- path3.join(home, ".openclaw/cron/runs"),
2178
- path3.join(home, ".openclaw/workspace/traces"),
2179
- path3.join(home, ".openclaw/subagents"),
2180
- path3.join(home, ".openclaw/agents/main/sessions"),
2181
- path3.join(home, ".agentflow/traces")
2182
- ];
2183
- for (const p of autoDiscoverPaths) {
2361
+ for (const p of getDiscoveryPaths(this.userConfig)) {
2184
2362
  if (fs3.existsSync(p) && !config.dataDirs.includes(p)) {
2185
2363
  config.dataDirs.push(p);
2186
2364
  }
2187
2365
  }
2188
2366
  this.watcher = new TraceWatcher({
2189
2367
  tracesDir: config.tracesDir,
2190
- dataDirs: config.dataDirs
2368
+ dataDirs: config.dataDirs,
2369
+ userConfig: this.userConfig
2191
2370
  });
2192
2371
  this.stats = new AgentStats();
2193
2372
  this.knowledgeStore = (0, import_agentflow_core3.createKnowledgeStore)({
@@ -2222,6 +2401,8 @@ var DashboardServer = class {
2222
2401
  ts: 0
2223
2402
  };
2224
2403
  knowledgeStore;
2404
+ userConfig;
2405
+ configPath;
2225
2406
  setupExpress() {
2226
2407
  if (this.config.enableCors) {
2227
2408
  this.app.use((_req, res, next) => {
@@ -2233,18 +2414,35 @@ var DashboardServer = class {
2233
2414
  next();
2234
2415
  });
2235
2416
  }
2236
- const clientDir = path3.join(__dirname, "../dist/client");
2417
+ const pkgDir = path3.join(__dirname, "..");
2418
+ const clientDir = path3.join(pkgDir, "dist/client");
2419
+ const clientIndex = path3.join(clientDir, "index.html");
2420
+ const srcDir = path3.join(pkgDir, "src/client");
2421
+ const needsBuild = !fs3.existsSync(clientIndex) || fs3.existsSync(srcDir) && this.isClientStale(srcDir, clientDir);
2422
+ if (needsBuild) {
2423
+ try {
2424
+ console.log("Building dashboard client...");
2425
+ (0, import_node_child_process.execSync)("npm run build:client", { cwd: pkgDir, stdio: "inherit", timeout: 3e4 });
2426
+ } catch (err) {
2427
+ console.warn("Client build failed \u2014 dashboard UI may be stale:", err.message);
2428
+ }
2429
+ }
2237
2430
  if (fs3.existsSync(clientDir)) {
2238
2431
  this.app.use(import_express.default.static(clientDir));
2239
2432
  }
2240
- const publicDir = path3.join(__dirname, "../public");
2241
- if (fs3.existsSync(publicDir)) {
2242
- this.app.use("/v1", import_express.default.static(publicDir));
2243
- }
2244
- this.app.get("/api/traces", (_req, res) => {
2433
+ this.app.get("/api/traces", (req, res) => {
2245
2434
  try {
2246
- const traces = this.watcher.getAllTraces().map(serializeTrace);
2247
- res.json(traces);
2435
+ const limit = Math.min(Math.max(parseInt(req.query.limit, 10) || 50, 1), 200);
2436
+ const cursor = req.query.cursor ? parseFloat(req.query.cursor) : void 0;
2437
+ let allTraces = this.watcher.getAllTraces();
2438
+ if (cursor) {
2439
+ allTraces = allTraces.filter((t) => (t.lastModified || t.startTime) < cursor);
2440
+ }
2441
+ const page = allTraces.slice(0, limit);
2442
+ const serialized = page.map(serializeTrace);
2443
+ const lastTrace = page[page.length - 1];
2444
+ const nextCursor = page.length === limit && lastTrace ? lastTrace.lastModified || lastTrace.startTime : null;
2445
+ res.json({ traces: serialized, nextCursor });
2248
2446
  } catch (_error) {
2249
2447
  res.status(500).json({ error: "Failed to load traces" });
2250
2448
  }
@@ -2535,6 +2733,102 @@ var DashboardServer = class {
2535
2733
  res.status(500).json({ error: "Failed to load agent statistics" });
2536
2734
  }
2537
2735
  });
2736
+ this.app.get("/api/soma/report", (_req, res) => {
2737
+ const somaVault = this.config.somaVault;
2738
+ if (!somaVault) {
2739
+ return res.json({ available: false, teaser: true });
2740
+ }
2741
+ try {
2742
+ const reportPath = path3.join(somaVault, "..", "soma-report.json");
2743
+ if (!fs3.existsSync(reportPath)) {
2744
+ return res.json({ available: false, teaser: false, message: "No report file yet. Run soma watch." });
2745
+ }
2746
+ const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
2747
+ res.json(report);
2748
+ } catch (error) {
2749
+ console.error("Soma report error:", error);
2750
+ res.json({ available: false, teaser: false, message: "Failed to read report" });
2751
+ }
2752
+ });
2753
+ this.app.get("/api/soma/governance", (_req, res) => {
2754
+ const somaVault = this.config.somaVault;
2755
+ if (!somaVault) {
2756
+ return res.json({ available: false });
2757
+ }
2758
+ try {
2759
+ const reportPath = path3.join(somaVault, "..", "soma-report.json");
2760
+ if (!fs3.existsSync(reportPath)) {
2761
+ return res.json({ available: false, message: "No report file. Run soma report." });
2762
+ }
2763
+ const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
2764
+ res.json({
2765
+ available: true,
2766
+ layers: report.layers ?? { archive: 0, working: 0, emerging: 0, canon: 0 },
2767
+ governance: report.governance ?? { pending: 0, promoted: 0, rejected: 0 },
2768
+ insights: (report.insights ?? []).filter((i) => i.layer === "emerging" && i.proposal_status === "pending"),
2769
+ canon: (report.insights ?? []).filter((i) => i.layer === "canon"),
2770
+ generatedAt: report.generatedAt
2771
+ });
2772
+ } catch (error) {
2773
+ console.error("Soma governance error:", error);
2774
+ res.status(500).json({ available: false, message: "Failed to read governance data" });
2775
+ }
2776
+ });
2777
+ const sanitizeArg = (s) => s.replace(/[^a-zA-Z0-9_\-.:]/g, "");
2778
+ const sanitizeReason = (s) => s.replace(/["`$\\]/g, "").slice(0, 500);
2779
+ this.app.post("/api/soma/governance/promote", (req, res) => {
2780
+ var _a;
2781
+ const somaVault = this.config.somaVault;
2782
+ if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
2783
+ const { entryId } = req.body ?? {};
2784
+ if (!entryId) return res.status(400).json({ error: "entryId required" });
2785
+ try {
2786
+ const { execSync: execSync2 } = require("child_process");
2787
+ const safeId = sanitizeArg(String(entryId));
2788
+ const result = execSync2(`npx soma governance promote ${safeId} --vault "${somaVault}"`, {
2789
+ encoding: "utf-8",
2790
+ timeout: 1e4
2791
+ });
2792
+ res.json({ success: true, message: result.trim() });
2793
+ } catch (error) {
2794
+ res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
2795
+ }
2796
+ });
2797
+ this.app.post("/api/soma/governance/reject", (req, res) => {
2798
+ var _a;
2799
+ const somaVault = this.config.somaVault;
2800
+ if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
2801
+ const { entryId, reason } = req.body ?? {};
2802
+ if (!entryId || !reason) return res.status(400).json({ error: "entryId and reason required" });
2803
+ try {
2804
+ const { execSync: execSync2 } = require("child_process");
2805
+ const safeId = sanitizeArg(String(entryId));
2806
+ const safeReason = sanitizeReason(String(reason));
2807
+ const result = execSync2(`npx soma governance reject ${safeId} "${safeReason}" --vault "${somaVault}"`, {
2808
+ encoding: "utf-8",
2809
+ timeout: 1e4
2810
+ });
2811
+ res.json({ success: true, message: result.trim() });
2812
+ } catch (error) {
2813
+ res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
2814
+ }
2815
+ });
2816
+ this.app.get("/api/soma/governance/evidence/:id", (req, res) => {
2817
+ var _a;
2818
+ const somaVault = this.config.somaVault;
2819
+ if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
2820
+ try {
2821
+ const { execSync: execSync2 } = require("child_process");
2822
+ const safeId = sanitizeArg(String(req.params.id));
2823
+ const result = execSync2(`npx soma governance show ${safeId} --vault "${somaVault}"`, {
2824
+ encoding: "utf-8",
2825
+ timeout: 1e4
2826
+ });
2827
+ res.json({ available: true, output: result.trim() });
2828
+ } catch (error) {
2829
+ res.status(404).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
2830
+ }
2831
+ });
2538
2832
  this.app.get("/api/process-health", (_req, res) => {
2539
2833
  var _a, _b;
2540
2834
  try {
@@ -2547,7 +2841,14 @@ var DashboardServer = class {
2547
2841
  path3.dirname(this.config.tracesDir),
2548
2842
  ...this.config.dataDirs || []
2549
2843
  ];
2550
- const configs = (0, import_agentflow_core3.discoverAllProcessConfigs)(discoveryDirs);
2844
+ let configs = (0, import_agentflow_core3.discoverAllProcessConfigs)(discoveryDirs);
2845
+ const pref = getProcessPreference(this.userConfig);
2846
+ if (pref) {
2847
+ const hasPreferred = configs.some((c) => c.processName === pref.prefer);
2848
+ if (hasPreferred) {
2849
+ configs = configs.filter((c) => c.processName !== pref.over);
2850
+ }
2851
+ }
2551
2852
  if (configs.length === 0) {
2552
2853
  return res.json(null);
2553
2854
  }
@@ -2637,29 +2938,26 @@ var DashboardServer = class {
2637
2938
  ...extraDirs
2638
2939
  ];
2639
2940
  const discovered = [];
2640
- try {
2641
- const { execSync } = require("child_process");
2642
- const raw = execSync(
2643
- "systemctl --user show --property=ExecStart --no-pager alfred.service openclaw-gateway.service 2>/dev/null",
2644
- { encoding: "utf8", timeout: 5e3 }
2645
- );
2646
- for (const line of raw.split("\n")) {
2647
- const match = line.match(/path=([^\s;]+)/);
2648
- if (match == null ? void 0 : match[1]) {
2649
- const dir = path3.dirname(match[1]);
2650
- if (fs3.existsSync(dir)) discovered.push(dir);
2941
+ const svcNames = getSystemdServices(this.userConfig);
2942
+ if (svcNames.length > 0) {
2943
+ try {
2944
+ const { execSync: execSync2 } = require("child_process");
2945
+ const raw = execSync2(
2946
+ `systemctl --user show --property=ExecStart --no-pager ${svcNames.join(" ")} 2>/dev/null`,
2947
+ { encoding: "utf8", timeout: 5e3 }
2948
+ );
2949
+ for (const line of raw.split("\n")) {
2950
+ const match = line.match(/path=([^\s;]+)/);
2951
+ if (match == null ? void 0 : match[1]) {
2952
+ const dir = path3.dirname(match[1]);
2953
+ if (fs3.existsSync(dir)) discovered.push(dir);
2954
+ }
2651
2955
  }
2956
+ } catch {
2652
2957
  }
2653
- } catch {
2654
2958
  }
2655
2959
  const commonPaths = [
2656
- path3.join(home, ".alfred/traces"),
2657
- path3.join(home, ".alfred/data"),
2658
- path3.join(home, ".openclaw/workspace/traces"),
2659
- path3.join(home, ".openclaw/subagents"),
2660
- path3.join(home, ".openclaw/cron/runs"),
2661
- path3.join(home, ".openclaw/cron"),
2662
- path3.join(home, ".openclaw/agents/main/sessions"),
2960
+ ...getDiscoveryPaths(this.userConfig),
2663
2961
  path3.join(home, ".agentflow/traces")
2664
2962
  ];
2665
2963
  for (const p of commonPaths) {
@@ -2704,46 +3002,54 @@ var DashboardServer = class {
2704
3002
  res.status(500).json({ error: "Failed to update directory config" });
2705
3003
  }
2706
3004
  });
2707
- this.app.post("/v1/traces", import_express.default.json({ limit: "10mb" }), (req, res) => {
2708
- try {
2709
- const traces = parseOtlpPayload(req.body);
2710
- let ingested = 0;
2711
- for (const trace of traces) {
2712
- const nodes = /* @__PURE__ */ new Map();
2713
- for (const [id, node] of Object.entries(trace.nodes)) {
2714
- nodes.set(id, { ...node, state: {} });
3005
+ if (this.config.enableCollector !== false) {
3006
+ this.app.post("/v1/traces", import_express.default.json({ limit: "10mb" }), (req, res) => {
3007
+ try {
3008
+ if (this.config.collectorAuthToken) {
3009
+ const auth = req.headers.authorization;
3010
+ if (!auth || auth !== `Bearer ${this.config.collectorAuthToken}`) {
3011
+ return res.status(401).json({ error: "Unauthorized \u2014 provide Authorization: Bearer <token>" });
3012
+ }
2715
3013
  }
2716
- const watched = {
2717
- id: trace.id,
2718
- rootNodeId: Object.keys(trace.nodes)[0] ?? "",
2719
- agentId: trace.agentId,
2720
- name: trace.name,
2721
- trigger: trace.trigger,
2722
- startTime: trace.startTime,
2723
- endTime: trace.endTime,
2724
- status: trace.status,
2725
- nodes,
2726
- edges: [],
2727
- events: [],
2728
- metadata: { ...trace.metadata, adapterSource: "otel" },
2729
- sessionEvents: [],
2730
- sourceType: "session",
2731
- filename: `otel-${trace.id}`,
2732
- lastModified: Date.now(),
2733
- sourceDir: "http-collector"
2734
- };
2735
- this.watcher.traces.set(`otel:${trace.id}`, watched);
2736
- ingested++;
2737
- }
2738
- if (ingested > 0) {
2739
- this.broadcast({ type: "traces-updated", count: ingested });
3014
+ const traces = parseOtlpPayload(req.body);
3015
+ let ingested = 0;
3016
+ for (const trace of traces) {
3017
+ const nodes = /* @__PURE__ */ new Map();
3018
+ for (const [id, node] of Object.entries(trace.nodes)) {
3019
+ nodes.set(id, { ...node, state: {} });
3020
+ }
3021
+ const watched = {
3022
+ id: trace.id,
3023
+ rootNodeId: Object.keys(trace.nodes)[0] ?? "",
3024
+ agentId: trace.agentId,
3025
+ name: trace.name,
3026
+ trigger: trace.trigger,
3027
+ startTime: trace.startTime,
3028
+ endTime: trace.endTime,
3029
+ status: trace.status,
3030
+ nodes,
3031
+ edges: [],
3032
+ events: [],
3033
+ metadata: { ...trace.metadata, adapterSource: "otel" },
3034
+ sessionEvents: [],
3035
+ sourceType: "session",
3036
+ filename: `otel-${trace.id}`,
3037
+ lastModified: Date.now(),
3038
+ sourceDir: "http-collector"
3039
+ };
3040
+ this.watcher.traces.set(`otel:${trace.id}`, watched);
3041
+ ingested++;
3042
+ }
3043
+ if (ingested > 0) {
3044
+ this.broadcast({ type: "traces-updated", count: ingested });
3045
+ }
3046
+ res.json({ ok: true, tracesIngested: ingested });
3047
+ } catch (error) {
3048
+ console.error("OTLP collector error:", error);
3049
+ res.status(400).json({ error: "Failed to parse OTLP payload" });
2740
3050
  }
2741
- res.json({ ok: true, tracesIngested: ingested });
2742
- } catch (error) {
2743
- console.error("OTLP collector error:", error);
2744
- res.status(400).json({ error: "Failed to parse OTLP payload" });
2745
- }
2746
- });
3051
+ });
3052
+ }
2747
3053
  this.app.get("/health", (_req, res) => {
2748
3054
  res.json({
2749
3055
  status: "ok",
@@ -2755,18 +3061,10 @@ var DashboardServer = class {
2755
3061
  this.app.get("/ready", (_req, res) => {
2756
3062
  res.json({ status: "ready" });
2757
3063
  });
2758
- this.app.get("/v1/*", (_req, res) => {
2759
- const legacyIndex = path3.join(__dirname, "../public/index.html");
2760
- if (fs3.existsSync(legacyIndex)) {
2761
- res.sendFile(legacyIndex);
2762
- } else {
2763
- res.status(404).send("Legacy dashboard not found");
2764
- }
2765
- });
2766
3064
  this.app.get("*", (_req, res) => {
2767
- const clientIndex = path3.join(__dirname, "../dist/client/index.html");
2768
- if (fs3.existsSync(clientIndex)) {
2769
- res.sendFile(clientIndex);
3065
+ const clientIndex2 = path3.join(__dirname, "../dist/client/index.html");
3066
+ if (fs3.existsSync(clientIndex2)) {
3067
+ res.sendFile(clientIndex2);
2770
3068
  } else {
2771
3069
  res.status(404).send("Dashboard not found - public files may not be built");
2772
3070
  }
@@ -3013,24 +3311,49 @@ var DashboardServer = class {
3013
3311
  });
3014
3312
  }
3015
3313
  async start() {
3016
- return new Promise((resolve4) => {
3314
+ return new Promise((resolve5) => {
3017
3315
  const host = this.config.host || "localhost";
3018
3316
  this.server.listen(this.config.port, host, () => {
3019
3317
  console.log(`AgentFlow Dashboard running at http://${host}:${this.config.port}`);
3020
3318
  console.log(`Watching traces in: ${this.config.tracesDir}`);
3021
- resolve4();
3319
+ resolve5();
3022
3320
  });
3023
3321
  });
3024
3322
  }
3323
+ /** Check if any src/client file is newer than the built bundle. */
3324
+ isClientStale(srcDir, distDir) {
3325
+ try {
3326
+ const distIndex = path3.join(distDir, "index.html");
3327
+ if (!fs3.existsSync(distIndex)) return true;
3328
+ const distMtime = fs3.statSync(distIndex).mtimeMs;
3329
+ const check = (dir) => {
3330
+ for (const entry of fs3.readdirSync(dir, { withFileTypes: true })) {
3331
+ const full = path3.join(dir, entry.name);
3332
+ if (entry.isDirectory()) {
3333
+ if (check(full)) return true;
3334
+ } else if (fs3.statSync(full).mtimeMs > distMtime) {
3335
+ return true;
3336
+ }
3337
+ }
3338
+ return false;
3339
+ };
3340
+ return check(srcDir);
3341
+ } catch {
3342
+ return false;
3343
+ }
3344
+ }
3025
3345
  async stop() {
3026
- return new Promise((resolve4) => {
3346
+ return new Promise((resolve5) => {
3027
3347
  this.watcher.stop();
3028
3348
  this.server.close(() => {
3029
3349
  console.log("Dashboard server stopped");
3030
- resolve4();
3350
+ resolve5();
3031
3351
  });
3032
3352
  });
3033
3353
  }
3354
+ getConfigPath() {
3355
+ return this.configPath;
3356
+ }
3034
3357
  getStats() {
3035
3358
  return this.stats.getGlobalStats();
3036
3359
  }