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/index.cjs CHANGED
@@ -37,10 +37,84 @@ __export(index_exports, {
37
37
  module.exports = __toCommonJS(index_exports);
38
38
 
39
39
  // src/server.ts
40
+ var import_node_child_process = require("child_process");
40
41
  var fs3 = __toESM(require("fs"), 1);
41
42
  var import_node_http = require("http");
42
43
  var path3 = __toESM(require("path"), 1);
43
44
  var import_node_url = require("url");
45
+
46
+ // src/config.ts
47
+ var import_node_fs = require("fs");
48
+ var import_node_os = require("os");
49
+ var import_node_path = require("path");
50
+ var EMPTY_CONFIG = {};
51
+ function expandTilde(p) {
52
+ if (p.startsWith("~/") || p === "~") {
53
+ return (0, import_node_path.join)((0, import_node_os.homedir)(), p.slice(1));
54
+ }
55
+ return p;
56
+ }
57
+ function loadConfig(explicitPath) {
58
+ const candidates = [];
59
+ if (explicitPath) {
60
+ candidates.push((0, import_node_path.resolve)(explicitPath));
61
+ }
62
+ if (process.env.AGENTFLOW_CONFIG) {
63
+ candidates.push((0, import_node_path.resolve)(process.env.AGENTFLOW_CONFIG));
64
+ }
65
+ candidates.push((0, import_node_path.resolve)("agentflow.config.json"));
66
+ candidates.push((0, import_node_path.join)((0, import_node_os.homedir)(), ".config", "agentflow", "config.json"));
67
+ for (const candidate of candidates) {
68
+ if (!(0, import_node_fs.existsSync)(candidate)) continue;
69
+ try {
70
+ const raw = (0, import_node_fs.readFileSync)(candidate, "utf-8");
71
+ const parsed = JSON.parse(raw);
72
+ const cleaned = stripCommentKeys(parsed);
73
+ console.log(`Loaded config: ${candidate}`);
74
+ return { config: cleaned, configPath: candidate };
75
+ } catch (err) {
76
+ console.warn(`Warning: Failed to load config from ${candidate}: ${err.message}`);
77
+ console.warn("Continuing with empty defaults.");
78
+ return { config: EMPTY_CONFIG, configPath: null };
79
+ }
80
+ }
81
+ return { config: EMPTY_CONFIG, configPath: null };
82
+ }
83
+ function stripCommentKeys(obj) {
84
+ if (Array.isArray(obj)) return obj.map(stripCommentKeys);
85
+ if (obj && typeof obj === "object") {
86
+ const result = {};
87
+ for (const [key, value] of Object.entries(obj)) {
88
+ if (key.startsWith("//")) continue;
89
+ result[key] = stripCommentKeys(value);
90
+ }
91
+ return result;
92
+ }
93
+ return obj;
94
+ }
95
+ function getAliases(config) {
96
+ return config.aliases ?? {};
97
+ }
98
+ function getSkipFiles(config) {
99
+ return config.skipFiles ?? [];
100
+ }
101
+ function getSkipDirectories(config) {
102
+ return config.skipDirectories ?? [];
103
+ }
104
+ function getDiscoveryPaths(config) {
105
+ return (config.discoveryPaths ?? []).map(expandTilde);
106
+ }
107
+ function getSystemdServices(config) {
108
+ return config.systemdServices ?? [];
109
+ }
110
+ function getAgentDetection(config) {
111
+ return config.agentDetection ?? {};
112
+ }
113
+ function getProcessPreference(config) {
114
+ return config.processPreference ?? null;
115
+ }
116
+
117
+ // src/server.ts
44
118
  var import_agentflow_core3 = require("agentflow-core");
45
119
  var import_express = __toESM(require("express"), 1);
46
120
  var import_ws = require("ws");
@@ -74,17 +148,17 @@ var AgentFlowAdapter = class {
74
148
  };
75
149
 
76
150
  // src/adapters/openclaw.ts
77
- var import_node_fs = require("fs");
78
- var import_node_path = require("path");
151
+ var import_node_fs2 = require("fs");
152
+ var import_node_path2 = require("path");
79
153
  var jobCache = /* @__PURE__ */ new Map();
80
154
  function loadJobs(openclawDir) {
81
155
  const cached = jobCache.get(openclawDir);
82
156
  if (cached) return cached;
83
- const jobsPath = (0, import_node_path.join)(openclawDir, "cron", "jobs.json");
157
+ const jobsPath = (0, import_node_path2.join)(openclawDir, "cron", "jobs.json");
84
158
  const map = /* @__PURE__ */ new Map();
85
159
  try {
86
- if ((0, import_node_fs.existsSync)(jobsPath)) {
87
- const data = JSON.parse((0, import_node_fs.readFileSync)(jobsPath, "utf-8"));
160
+ if ((0, import_node_fs2.existsSync)(jobsPath)) {
161
+ const data = JSON.parse((0, import_node_fs2.readFileSync)(jobsPath, "utf-8"));
88
162
  const jobs = Array.isArray(data) ? data : data.jobs ?? [];
89
163
  for (const job of jobs) {
90
164
  if (job.id) map.set(job.id, job);
@@ -96,19 +170,19 @@ function loadJobs(openclawDir) {
96
170
  return map;
97
171
  }
98
172
  function findOpenClawRoot(filePath) {
99
- let dir = (0, import_node_path.dirname)(filePath);
173
+ let dir = (0, import_node_path2.dirname)(filePath);
100
174
  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") {
175
+ if ((0, import_node_fs2.existsSync)((0, import_node_path2.join)(dir, "cron", "jobs.json")) || (0, import_node_path2.basename)(dir) === ".openclaw") {
102
176
  return dir;
103
177
  }
104
- dir = (0, import_node_path.dirname)(dir);
178
+ dir = (0, import_node_path2.dirname)(dir);
105
179
  }
106
180
  return null;
107
181
  }
108
182
  var OpenClawAdapter = class {
109
183
  name = "openclaw";
110
184
  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"));
185
+ 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"));
112
186
  }
113
187
  canHandle(filePath) {
114
188
  if (!filePath.endsWith(".jsonl")) return false;
@@ -117,7 +191,7 @@ var OpenClawAdapter = class {
117
191
  parse(filePath) {
118
192
  const traces = [];
119
193
  try {
120
- const content = (0, import_node_fs.readFileSync)(filePath, "utf-8");
194
+ const content = (0, import_node_fs2.readFileSync)(filePath, "utf-8");
121
195
  const root = findOpenClawRoot(filePath);
122
196
  const jobs = root ? loadJobs(root) : /* @__PURE__ */ new Map();
123
197
  for (const line of content.split("\n")) {
@@ -129,7 +203,7 @@ var OpenClawAdapter = class {
129
203
  continue;
130
204
  }
131
205
  if (entry.action !== "finished") continue;
132
- const jobId = entry.jobId ?? (0, import_node_path.basename)(filePath, ".jsonl");
206
+ const jobId = entry.jobId ?? (0, import_node_path2.basename)(filePath, ".jsonl");
133
207
  const job = jobs.get(jobId);
134
208
  const jobName = (job == null ? void 0 : job.name) ?? jobId;
135
209
  const startTime = entry.runAtMs ?? entry.ts;
@@ -181,8 +255,8 @@ var OpenClawAdapter = class {
181
255
  };
182
256
 
183
257
  // src/adapters/otel.ts
184
- var import_node_fs2 = require("fs");
185
- var import_node_path2 = require("path");
258
+ var import_node_fs3 = require("fs");
259
+ var import_node_path3 = require("path");
186
260
  var SPAN_TYPE_MAP = {
187
261
  "gen_ai.chat": "llm",
188
262
  "gen_ai.completion": "llm",
@@ -289,8 +363,8 @@ var OTelAdapter = class {
289
363
  name = "otel";
290
364
  detect(dirPath) {
291
365
  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);
366
+ if ((0, import_node_fs3.existsSync)((0, import_node_path3.join)(dirPath, "otel-traces"))) return true;
367
+ const files = (0, import_node_fs3.readdirSync)(dirPath);
294
368
  return files.some((f) => f.endsWith(".otlp.json"));
295
369
  } catch {
296
370
  return false;
@@ -301,7 +375,7 @@ var OTelAdapter = class {
301
375
  }
302
376
  parse(filePath) {
303
377
  try {
304
- const content = (0, import_node_fs2.readFileSync)(filePath, "utf-8");
378
+ const content = (0, import_node_fs3.readFileSync)(filePath, "utf-8");
305
379
  const payload = JSON.parse(content);
306
380
  const traces = parseOtlpPayload(payload);
307
381
  for (const t of traces) t.filePath = filePath;
@@ -343,9 +417,7 @@ function extractSource(agentId) {
343
417
  const colonIdx = agentId.indexOf(":");
344
418
  if (colonIdx > 0 && colonIdx < 20) {
345
419
  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
- }
420
+ return { source: prefix, localId: agentId.slice(colonIdx + 1) };
349
421
  }
350
422
  return { source: "agentflow", localId: agentId };
351
423
  }
@@ -376,16 +448,20 @@ function deduplicateAgents(agents) {
376
448
  for (const a of tagged) {
377
449
  const suffix = extractSuffix(a.localId);
378
450
  if (!suffix) continue;
379
- const group = suffixGroups.get(suffix) ?? [];
451
+ const key = `${a.source}:${suffix}`;
452
+ const group = suffixGroups.get(key) ?? [];
380
453
  group.push(a);
381
- suffixGroups.set(suffix, group);
454
+ suffixGroups.set(key, group);
382
455
  }
383
456
  const mergedIds = /* @__PURE__ */ new Set();
384
457
  const mergedAgents = [];
385
- for (const [suffix, group] of suffixGroups) {
458
+ for (const [_key, group] of suffixGroups) {
459
+ const suffix = extractSuffix(group[0].localId);
386
460
  if (group.length < 2) continue;
387
461
  const prefixes = new Set(group.map((a) => a.localId.split("-")[0]));
388
462
  if (prefixes.size < 2) continue;
463
+ const longPrefixes = [...prefixes].filter((p) => p !== suffix && p.length > 2);
464
+ if (longPrefixes.length >= 2) continue;
389
465
  const merged = {
390
466
  agentId: group[0].source === "agentflow" ? suffix : `${group[0].source}:${suffix}`,
391
467
  displayName: suffix,
@@ -433,10 +509,7 @@ function groupAgents(agents) {
433
509
  }
434
510
  const SOURCE_DISPLAY = {
435
511
  agentflow: "AgentFlow",
436
- openclaw: "OpenClaw",
437
- otel: "OpenTelemetry",
438
- langchain: "LangChain",
439
- crewai: "CrewAI"
512
+ otel: "OpenTelemetry"
440
513
  };
441
514
  const groups = [];
442
515
  for (const [source, sourceAgents] of sourceMap) {
@@ -782,10 +855,6 @@ function getUniversalNodeStatus(activity) {
782
855
  return "completed";
783
856
  }
784
857
  function openClawSessionIdToAgent(sessionId) {
785
- if (sessionId.startsWith("janitor-")) return "vault-janitor";
786
- if (sessionId.startsWith("curator-")) return "vault-curator";
787
- if (sessionId.startsWith("distiller-")) return "vault-distiller";
788
- if (sessionId.startsWith("main-")) return "alfred-main";
789
858
  const firstSegment = sessionId.split("-")[0];
790
859
  if (firstSegment) return firstSegment;
791
860
  return "openclaw";
@@ -798,19 +867,81 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
798
867
  tracesDir;
799
868
  dataDirs;
800
869
  allWatchDirs;
870
+ maxAgeMs;
871
+ userConfig;
801
872
  constructor(tracesDirOrOptions) {
802
873
  super();
874
+ const defaultMaxAgeMs = 48 * 60 * 60 * 1e3;
875
+ const envHours = process.env.AGENTFLOW_TRACE_WINDOW_HOURS;
876
+ const envMaxAgeMs = envHours ? parseFloat(envHours) * 60 * 60 * 1e3 : void 0;
803
877
  if (typeof tracesDirOrOptions === "string") {
804
878
  this.tracesDir = path.resolve(tracesDirOrOptions);
805
879
  this.dataDirs = [];
880
+ this.maxAgeMs = envMaxAgeMs ?? defaultMaxAgeMs;
881
+ this.userConfig = {};
806
882
  } else {
807
883
  this.tracesDir = path.resolve(tracesDirOrOptions.tracesDir);
808
884
  this.dataDirs = (tracesDirOrOptions.dataDirs || []).map((d) => path.resolve(d));
809
- }
885
+ this.maxAgeMs = envMaxAgeMs ?? tracesDirOrOptions.maxAgeMs ?? defaultMaxAgeMs;
886
+ this.userConfig = tracesDirOrOptions.userConfig ?? {};
887
+ }
888
+ this.skipFiles = /* @__PURE__ */ new Set([
889
+ ..._TraceWatcher.STRUCTURAL_SKIP_FILES,
890
+ ...getSkipFiles(this.userConfig)
891
+ ]);
892
+ this.userSkipDirs = new Set(getSkipDirectories(this.userConfig));
810
893
  this.allWatchDirs = [this.tracesDir, ...this.dataDirs];
811
894
  this.ensureTracesDir();
812
895
  this.loadExistingFiles();
896
+ this.archiveOldTraces();
813
897
  this.startWatching();
898
+ setInterval(() => this.archiveOldTraces(), 6 * 60 * 60 * 1e3);
899
+ }
900
+ /** Move trace files older than maxAgeMs into archive/YYYY-MM/ subdirectories. */
901
+ archiveOldTraces() {
902
+ const cutoff = Date.now() - this.maxAgeMs;
903
+ let archived = 0;
904
+ for (const dir of this.allWatchDirs) {
905
+ if (!fs.existsSync(dir)) continue;
906
+ try {
907
+ this.archiveDirectory(dir, cutoff, 0);
908
+ } catch (error) {
909
+ console.warn(`Archival error in ${dir}:`, error.message);
910
+ }
911
+ }
912
+ }
913
+ archiveDirectory(dir, cutoff, depth) {
914
+ if (depth > 10) return 0;
915
+ if (path.basename(dir) === "archive") return 0;
916
+ let archived = 0;
917
+ try {
918
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
919
+ for (const entry of entries) {
920
+ if (entry.name.startsWith(".") || entry.name === "archive" || this.userSkipDirs.has(entry.name)) continue;
921
+ const fullPath = path.join(dir, entry.name);
922
+ if (entry.isDirectory()) {
923
+ archived += this.archiveDirectory(fullPath, cutoff, depth + 1);
924
+ continue;
925
+ }
926
+ if (!entry.isFile() || !this.isSupportedFile(entry.name)) continue;
927
+ try {
928
+ const stats = fs.statSync(fullPath);
929
+ if (stats.mtimeMs >= cutoff) continue;
930
+ const mtime = new Date(stats.mtimeMs);
931
+ const yearMonth = `${mtime.getFullYear()}-${String(mtime.getMonth() + 1).padStart(2, "0")}`;
932
+ const archiveDir = path.join(this.tracesDir, "archive", yearMonth);
933
+ fs.mkdirSync(archiveDir, { recursive: true });
934
+ const dest = path.join(archiveDir, entry.name);
935
+ fs.renameSync(fullPath, dest);
936
+ const key = this.traceKey(fullPath);
937
+ this.traces.delete(key);
938
+ archived++;
939
+ } catch {
940
+ }
941
+ }
942
+ } catch {
943
+ }
944
+ return archived;
814
945
  }
815
946
  ensureTracesDir() {
816
947
  if (!fs.existsSync(this.tracesDir)) {
@@ -843,9 +974,17 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
843
974
  const entries = fs.readdirSync(dir, { withFileTypes: true });
844
975
  for (const entry of entries) {
845
976
  if (entry.name.startsWith(".")) continue;
977
+ if (entry.name === "archive") continue;
978
+ if (this.userSkipDirs.has(entry.name)) continue;
846
979
  const fullPath = path.join(dir, entry.name);
847
980
  if (entry.isFile()) {
848
981
  if (this.isSupportedFile(entry.name)) {
982
+ try {
983
+ const mtime = fs.statSync(fullPath).mtimeMs;
984
+ if (Date.now() - mtime > this.maxAgeMs) continue;
985
+ } catch {
986
+ continue;
987
+ }
849
988
  if (this.loadFile(fullPath)) {
850
989
  fileCount++;
851
990
  }
@@ -863,8 +1002,8 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
863
1002
  isSupportedFile(filename) {
864
1003
  return filename.endsWith(".json") || filename.endsWith(".jsonl") || filename.endsWith(".log") || filename.endsWith(".trace");
865
1004
  }
866
- /** File names that are config/state, not tracesskip them. */
867
- static SKIP_FILES = /* @__PURE__ */ new Set([
1005
+ /** Structural file names that are never trace dataalways skipped. */
1006
+ static STRUCTURAL_SKIP_FILES = /* @__PURE__ */ new Set([
868
1007
  "workers.json",
869
1008
  "package.json",
870
1009
  "package-lock.json",
@@ -879,6 +1018,10 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
879
1018
  "update-check.json",
880
1019
  "exec-approvals.json"
881
1020
  ]);
1021
+ /** Skip files = structural + user config */
1022
+ skipFiles;
1023
+ /** Skip directories from user config */
1024
+ userSkipDirs;
882
1025
  static SKIP_SUFFIXES = [
883
1026
  "-state.json",
884
1027
  "-config.json",
@@ -890,7 +1033,7 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
890
1033
  /** Load a file using the adapter registry, falling back to built-in parsing. */
891
1034
  loadFile(filePath) {
892
1035
  const filename = path.basename(filePath);
893
- if (_TraceWatcher.SKIP_FILES.has(filename)) return false;
1036
+ if (this.skipFiles.has(filename)) return false;
894
1037
  if (_TraceWatcher.SKIP_SUFFIXES.some((s) => filename.endsWith(s))) return false;
895
1038
  const adapter = findAdapter(filePath);
896
1039
  if (adapter && adapter.name !== "agentflow") {
@@ -1067,43 +1210,26 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
1067
1210
  }
1068
1211
  return traces;
1069
1212
  }
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
- };
1213
+ /** Normalise agent identifiers using config-driven alias map. */
1092
1214
  normaliseAgentId(raw) {
1093
- return _TraceWatcher.AGENT_ALIASES[raw] ?? raw;
1215
+ const aliases = getAliases(this.userConfig);
1216
+ return aliases[raw] ?? raw;
1094
1217
  }
1095
1218
  detectAgentIdentifier(activity, _filename, filePath) {
1096
1219
  if (activity.agent_id) {
1097
- const agentId = activity.agent_id;
1098
- if (agentId === "main" && filePath.includes(".alfred/")) return this.normaliseAgentId("alfred-main");
1099
- return this.normaliseAgentId(agentId);
1220
+ return this.normaliseAgentId(activity.agent_id);
1100
1221
  }
1101
1222
  const pathAgent = this.extractAgentFromPath(filePath);
1102
- if (filePath.includes(".alfred/") && !pathAgent.startsWith("alfred-")) {
1223
+ const detection = getAgentDetection(this.userConfig);
1224
+ if (detection.filePatterns) {
1103
1225
  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);
1226
+ for (const [pattern, template] of Object.entries(detection.filePatterns)) {
1227
+ const re = new RegExp(`^(${pattern})$`);
1228
+ const match = basename3.match(re);
1229
+ if (match) {
1230
+ const resolved = template.replace("${match}", match[1]);
1231
+ return this.normaliseAgentId(resolved);
1232
+ }
1107
1233
  }
1108
1234
  }
1109
1235
  return this.normaliseAgentId(pathAgent);
@@ -1111,20 +1237,23 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
1111
1237
  extractAgentFromPath(filePath) {
1112
1238
  const filename = path.basename(filePath, path.extname(filePath));
1113
1239
  const pathParts = filePath.split(path.sep);
1114
- if (filePath.includes(".openclaw/")) {
1115
- const agentsIndex = pathParts.lastIndexOf("agents");
1116
- if (agentsIndex !== -1 && agentsIndex + 1 < pathParts.length) {
1117
- return `openclaw-${pathParts[agentsIndex + 1]}`;
1118
- }
1119
- if (filename.startsWith("openclaw-")) {
1120
- return "openclaw-gateway";
1240
+ const detection = getAgentDetection(this.userConfig);
1241
+ let pathPrefix = "";
1242
+ if (detection.pathPatterns) {
1243
+ for (const [pathSubstring, agentId] of Object.entries(detection.pathPatterns)) {
1244
+ if (filePath.includes(pathSubstring)) {
1245
+ pathPrefix = agentId;
1246
+ break;
1247
+ }
1121
1248
  }
1122
- return "openclaw";
1123
1249
  }
1124
- if (filePath.includes(".alfred/") || filename.includes("alfred")) {
1125
- return "alfred";
1250
+ const agentsIndex = pathParts.lastIndexOf("agents");
1251
+ if (agentsIndex !== -1 && agentsIndex + 1 < pathParts.length) {
1252
+ const agentName = pathParts[agentsIndex + 1];
1253
+ return pathPrefix ? `${pathPrefix}-${agentName}` : agentName;
1126
1254
  }
1127
- for (const part of pathParts.reverse()) {
1255
+ if (pathPrefix) return pathPrefix;
1256
+ for (const part of [...pathParts].reverse()) {
1128
1257
  if (part.match(/agent|worker|service|daemon|bot|ai|llm/i)) {
1129
1258
  return part;
1130
1259
  }
@@ -1459,19 +1588,22 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
1459
1588
  const parentDir = path.basename(path.dirname(filePath));
1460
1589
  const grandParentDir = path.basename(path.dirname(path.dirname(filePath)));
1461
1590
  const greatGrandParentDir = path.basename(path.dirname(path.dirname(path.dirname(filePath))));
1462
- let agentId;
1591
+ let agentName;
1463
1592
  if (parentDir === "sessions" && greatGrandParentDir === "agents") {
1464
- agentId = grandParentDir;
1593
+ agentName = grandParentDir;
1465
1594
  } else if (grandParentDir === "agents") {
1466
- agentId = parentDir;
1467
- } else if (parentDir === "runs" && grandParentDir === "cron") {
1468
- agentId = "openclaw-cron";
1595
+ agentName = parentDir;
1469
1596
  } else {
1470
- agentId = parentDir;
1471
- }
1472
- if (filePath.includes(".alfred/") || filePath.includes("alfred")) {
1473
- if (!agentId.startsWith("alfred-")) {
1474
- agentId = `alfred-${agentId}`;
1597
+ agentName = parentDir;
1598
+ }
1599
+ let agentId = agentName;
1600
+ const detection = getAgentDetection(this.userConfig);
1601
+ if (detection.pathPatterns) {
1602
+ for (const [pathSubstring, prefix] of Object.entries(detection.pathPatterns)) {
1603
+ if (filePath.includes(pathSubstring)) {
1604
+ agentId = `${prefix}-${agentName}`;
1605
+ break;
1606
+ }
1475
1607
  }
1476
1608
  }
1477
1609
  const modelEvent = rawEvents.find((e) => e.type === "model_change");
@@ -1760,6 +1892,7 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
1760
1892
  edges: [],
1761
1893
  events: [],
1762
1894
  startTime,
1895
+ status,
1763
1896
  agentId,
1764
1897
  trigger,
1765
1898
  name: rootName,
@@ -1880,8 +2013,12 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
1880
2013
  // Ignore git directories
1881
2014
  /\.vscode/,
1882
2015
  // Ignore vscode
1883
- /\.idea/
2016
+ /\.idea/,
1884
2017
  // Ignore idea
2018
+ /\/archive\//,
2019
+ // Ignore archived trace files
2020
+ // Ignore user-configured skip directories
2021
+ ...getSkipDirectories(this.userConfig).map((d) => new RegExp(`/${d}/`))
1885
2022
  ],
1886
2023
  persistent: true,
1887
2024
  ignoreInitial: true,
@@ -1935,29 +2072,42 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
1935
2072
  });
1936
2073
  }
1937
2074
  getTrace(filename) {
2075
+ const candidates = [];
1938
2076
  const exact = this.traces.get(filename);
1939
- if (exact) return exact;
2077
+ if (exact) candidates.push(exact);
1940
2078
  if (filename.includes("::")) {
1941
2079
  const [fname, startTimeStr] = filename.split("::");
1942
2080
  const startTime = Number(startTimeStr);
1943
2081
  if (fname && !Number.isNaN(startTime)) {
1944
2082
  for (const trace of this.traces.values()) {
1945
2083
  if (trace.filename === fname && trace.startTime === startTime) {
1946
- return trace;
2084
+ candidates.push(trace);
1947
2085
  }
1948
2086
  }
1949
2087
  }
1950
2088
  }
1951
2089
  for (const prefix of ["openclaw:", "otel:", ""]) {
1952
2090
  const prefixed = this.traces.get(prefix + filename);
1953
- if (prefixed) return prefixed;
2091
+ if (prefixed) candidates.push(prefixed);
1954
2092
  }
1955
2093
  for (const [key, trace] of this.traces) {
1956
2094
  if (trace.filename === filename || trace.id === filename || key.endsWith(filename)) {
1957
- return trace;
2095
+ candidates.push(trace);
2096
+ }
2097
+ }
2098
+ if (candidates.length === 0) return void 0;
2099
+ if (candidates.length === 1) return candidates[0];
2100
+ let best = candidates[0];
2101
+ let bestNodeCount = best.nodes instanceof Map ? best.nodes.size : Object.keys(best.nodes ?? {}).length;
2102
+ for (let i = 1; i < candidates.length; i++) {
2103
+ const c = candidates[i];
2104
+ const nc = c.nodes instanceof Map ? c.nodes.size : Object.keys(c.nodes ?? {}).length;
2105
+ if (nc > bestNodeCount) {
2106
+ best = c;
2107
+ bestNodeCount = nc;
1958
2108
  }
1959
2109
  }
1960
- return void 0;
2110
+ return best;
1961
2111
  }
1962
2112
  getTracesByAgent(agentId) {
1963
2113
  return this.getAllTraces().filter((trace) => trace.agentId === agentId);
@@ -2004,7 +2154,7 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
2004
2154
  var fs2 = __toESM(require("fs"), 1);
2005
2155
  var os = __toESM(require("os"), 1);
2006
2156
  var path2 = __toESM(require("path"), 1);
2007
- var VERSION = "0.4.0";
2157
+ var VERSION = "0.8.0";
2008
2158
  function getLanAddress() {
2009
2159
  const interfaces = os.networkInterfaces();
2010
2160
  for (const name of Object.keys(interfaces)) {
@@ -2016,7 +2166,7 @@ function getLanAddress() {
2016
2166
  }
2017
2167
  return null;
2018
2168
  }
2019
- function printBanner(config, traceCount, stats) {
2169
+ function printBanner(config, traceCount, stats, configPath) {
2020
2170
  var _a;
2021
2171
  const lan = getLanAddress();
2022
2172
  const host = config.host || "localhost";
@@ -2032,26 +2182,23 @@ function printBanner(config, traceCount, stats) {
2032
2182
 
2033
2183
  See your agents think.
2034
2184
 
2035
- \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
2036
- \u2502 \u{1F916} Agents \u2502 TRACE FILES \u2502 \u{1F4CA} AgentFlow \u2502 SHOWS YOU \u2502 \u{1F310} Your browser \u2502
2037
- \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
2038
- \u2502 write JSON \u2502 \u2502 builds graphs, \u2502 \u2502 graph, timeline, \u2502
2039
- \u2502 trace files. \u2502 \u2502 serves dashboard.\u2502 \u2502 metrics, health. \u2502
2040
- \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
2041
-
2042
- Runs locally. Your data never leaves your machine.
2043
-
2044
- Tabs: \u{1F3AF} Graph \xB7 \u23F1\uFE0F Timeline \xB7 \u{1F4CA} Metrics \xB7 \u{1F6E0}\uFE0F Process Health \xB7 \u26A0\uFE0F Errors
2045
-
2046
2185
  Traces: ${config.tracesDir}${((_a = config.dataDirs) == null ? void 0 : _a.length) ? `
2047
2186
  Data dirs: ${config.dataDirs.join("\n ")}` : ""}
2048
2187
  Loaded: ${traceCount} traces \xB7 ${stats.totalAgents} agents \xB7 ${stats.totalExecutions} executions
2049
2188
  Success: ${stats.globalSuccessRate.toFixed(1)}%${stats.activeAgents > 0 ? ` \xB7 ${stats.activeAgents} active now` : ""}
2189
+ Config: ${configPath ?? "none (using defaults)"}
2050
2190
  CORS: ${config.enableCors ? "enabled" : "disabled"}
2051
2191
  WebSocket: live updates enabled
2192
+ Window: ${process.env.AGENTFLOW_TRACE_WINDOW_HOURS ?? "48"}h (set AGENTFLOW_TRACE_WINDOW_HOURS to change)
2052
2193
 
2053
2194
  \u2192 http://localhost:${port}${isPublic && lan ? `
2054
2195
  \u2192 http://${lan}:${port} (LAN)` : ""}
2196
+
2197
+ Views: Agent Profile \xB7 Execution Detail \xB7 Governance
2198
+ Tabs: Flame Chart \xB7 Agent Flow \xB7 Metrics \xB7 Dependencies
2199
+ State Machine \xB7 Summary \xB7 Transcript
2200
+
2201
+ Runs locally. Your data never leaves your machine.
2055
2202
  `);
2056
2203
  }
2057
2204
  async function startDashboard() {
@@ -2083,11 +2230,32 @@ async function startDashboard() {
2083
2230
  case "--cors":
2084
2231
  config.enableCors = true;
2085
2232
  break;
2233
+ case "--no-collector":
2234
+ config.enableCollector = false;
2235
+ break;
2236
+ case "--collector-token":
2237
+ config.collectorAuthToken = args[++i];
2238
+ break;
2239
+ case "--soma-vault":
2240
+ config.somaVault = args[++i];
2241
+ break;
2242
+ case "--config":
2243
+ config.configPath = args[++i];
2244
+ break;
2086
2245
  case "--help":
2087
2246
  printHelp();
2088
2247
  process.exit(0);
2089
2248
  }
2090
2249
  }
2250
+ if (!config.collectorAuthToken && process.env.AGENTFLOW_COLLECTOR_TOKEN) {
2251
+ config.collectorAuthToken = process.env.AGENTFLOW_COLLECTOR_TOKEN;
2252
+ }
2253
+ if (process.env.AGENTFLOW_NO_COLLECTOR === "true") {
2254
+ config.enableCollector = false;
2255
+ }
2256
+ if (!config.somaVault && process.env.SOMA_VAULT) {
2257
+ config.somaVault = process.env.SOMA_VAULT;
2258
+ }
2091
2259
  const tracesPath = path2.resolve(config.tracesDir);
2092
2260
  if (!fs2.existsSync(tracesPath)) {
2093
2261
  fs2.mkdirSync(tracesPath, { recursive: true });
@@ -2109,7 +2277,7 @@ async function startDashboard() {
2109
2277
  setTimeout(() => {
2110
2278
  const stats = dashboard.getStats();
2111
2279
  const traces = dashboard.getTraces();
2112
- printBanner(config, traces.length, stats);
2280
+ printBanner(config, traces.length, stats, dashboard.getConfigPath());
2113
2281
  }, 1500);
2114
2282
  } catch (error) {
2115
2283
  console.error("\u274C Failed to start dashboard:", error);
@@ -2118,7 +2286,7 @@ async function startDashboard() {
2118
2286
  }
2119
2287
  function printHelp() {
2120
2288
  console.log(`
2121
- \u{1F4CA} AgentFlow Dashboard v${VERSION} \u2014 See your agents think.
2289
+ AgentFlow Dashboard v${VERSION} \u2014 See your agents think.
2122
2290
 
2123
2291
  Usage:
2124
2292
  agentflow-dashboard [options]
@@ -2129,20 +2297,34 @@ Options:
2129
2297
  -t, --traces <path> Traces directory (default: ./traces)
2130
2298
  -h, --host <address> Host address (default: localhost)
2131
2299
  --data-dir <path> Extra data directory for process discovery (repeatable)
2300
+ --config <path> Path to agentflow.config.json (aliases, skip files, etc.)
2301
+ --soma-vault <path> SOMA vault directory for intelligence data
2132
2302
  --cors Enable CORS headers
2303
+ --no-collector Disable OTLP trace collector (POST /v1/traces)
2304
+ --collector-token <tok> Require auth token for collector (or set AGENTFLOW_COLLECTOR_TOKEN)
2133
2305
  --help Show this help message
2134
2306
 
2135
- Examples:
2136
- agentflow-dashboard --traces ./traces --host 0.0.0.0 --cors
2137
- agentflow-dashboard -p 8080 -t /var/log/agentflow
2138
- agentflow-dashboard --traces ./traces --data-dir ./workers --data-dir ./cron
2307
+ Config file:
2308
+ The dashboard loads agentflow.config.json for agent aliases, skip files,
2309
+ discovery paths, and systemd services. Resolution order:
2310
+ 1. --config flag
2311
+ 2. AGENTFLOW_CONFIG env var
2312
+ 3. ./agentflow.config.json
2313
+ 4. ~/.config/agentflow/config.json
2314
+
2315
+ See agentflow.config.example.json for a complete reference.
2316
+
2317
+ Environment:
2318
+ AGENTFLOW_CONFIG Path to config file
2319
+ AGENTFLOW_TRACE_WINDOW_HOURS Max age of traces to load (default: 48)
2320
+ AGENTFLOW_COLLECTOR_TOKEN Auth token for OTLP collector
2321
+ AGENTFLOW_NO_COLLECTOR=true Disable OTLP collector
2322
+ SOMA_VAULT SOMA vault directory
2139
2323
 
2140
- Tabs:
2141
- \u{1F3AF} Graph Interactive Cytoscape.js execution graph
2142
- \u23F1\uFE0F Timeline Waterfall view of node durations
2143
- \u{1F4CA} Metrics Success rates, durations, node breakdown
2144
- \u{1F6E0}\uFE0F Process Health PID files, systemd, workers, orphans
2145
- \u26A0\uFE0F Errors Failed and hung nodes with metadata
2324
+ Examples:
2325
+ agentflow-dashboard --traces ./traces --host 0.0.0.0
2326
+ agentflow-dashboard --traces ./traces --config ./agentflow.config.json
2327
+ agentflow-dashboard -p 8080 -t /var/log/agentflow --cors
2146
2328
  `);
2147
2329
  }
2148
2330
 
@@ -2165,12 +2347,15 @@ function serializeTrace(trace) {
2165
2347
  var DashboardServer = class {
2166
2348
  constructor(config) {
2167
2349
  this.config = config;
2168
- const home = process.env.HOME ?? "/home/trader";
2169
- const configPath = path3.join(home, ".agentflow/dashboard-config.json");
2350
+ const { config: userCfg, configPath: cfgPath } = loadConfig(config.configPath);
2351
+ this.userConfig = userCfg;
2352
+ this.configPath = cfgPath;
2353
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
2354
+ const dashConfigPath = path3.join(home, ".agentflow/dashboard-config.json");
2170
2355
  if (!config.dataDirs) config.dataDirs = [];
2171
2356
  try {
2172
- if (fs3.existsSync(configPath)) {
2173
- const saved = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
2357
+ if (fs3.existsSync(dashConfigPath)) {
2358
+ const saved = JSON.parse(fs3.readFileSync(dashConfigPath, "utf-8"));
2174
2359
  const extraDirs = saved.extraDirs ?? [];
2175
2360
  for (const d of extraDirs) {
2176
2361
  if (!config.dataDirs.includes(d)) config.dataDirs.push(d);
@@ -2178,21 +2363,15 @@ var DashboardServer = class {
2178
2363
  }
2179
2364
  } catch {
2180
2365
  }
2181
- const autoDiscoverPaths = [
2182
- path3.join(home, ".openclaw/cron/runs"),
2183
- path3.join(home, ".openclaw/workspace/traces"),
2184
- path3.join(home, ".openclaw/subagents"),
2185
- path3.join(home, ".openclaw/agents/main/sessions"),
2186
- path3.join(home, ".agentflow/traces")
2187
- ];
2188
- for (const p of autoDiscoverPaths) {
2366
+ for (const p of getDiscoveryPaths(this.userConfig)) {
2189
2367
  if (fs3.existsSync(p) && !config.dataDirs.includes(p)) {
2190
2368
  config.dataDirs.push(p);
2191
2369
  }
2192
2370
  }
2193
2371
  this.watcher = new TraceWatcher({
2194
2372
  tracesDir: config.tracesDir,
2195
- dataDirs: config.dataDirs
2373
+ dataDirs: config.dataDirs,
2374
+ userConfig: this.userConfig
2196
2375
  });
2197
2376
  this.stats = new AgentStats();
2198
2377
  this.knowledgeStore = (0, import_agentflow_core3.createKnowledgeStore)({
@@ -2227,6 +2406,8 @@ var DashboardServer = class {
2227
2406
  ts: 0
2228
2407
  };
2229
2408
  knowledgeStore;
2409
+ userConfig;
2410
+ configPath;
2230
2411
  setupExpress() {
2231
2412
  if (this.config.enableCors) {
2232
2413
  this.app.use((_req, res, next) => {
@@ -2238,18 +2419,35 @@ var DashboardServer = class {
2238
2419
  next();
2239
2420
  });
2240
2421
  }
2241
- const clientDir = path3.join(__dirname, "../dist/client");
2422
+ const pkgDir = path3.join(__dirname, "..");
2423
+ const clientDir = path3.join(pkgDir, "dist/client");
2424
+ const clientIndex = path3.join(clientDir, "index.html");
2425
+ const srcDir = path3.join(pkgDir, "src/client");
2426
+ const needsBuild = !fs3.existsSync(clientIndex) || fs3.existsSync(srcDir) && this.isClientStale(srcDir, clientDir);
2427
+ if (needsBuild) {
2428
+ try {
2429
+ console.log("Building dashboard client...");
2430
+ (0, import_node_child_process.execSync)("npm run build:client", { cwd: pkgDir, stdio: "inherit", timeout: 3e4 });
2431
+ } catch (err) {
2432
+ console.warn("Client build failed \u2014 dashboard UI may be stale:", err.message);
2433
+ }
2434
+ }
2242
2435
  if (fs3.existsSync(clientDir)) {
2243
2436
  this.app.use(import_express.default.static(clientDir));
2244
2437
  }
2245
- const publicDir = path3.join(__dirname, "../public");
2246
- if (fs3.existsSync(publicDir)) {
2247
- this.app.use("/v1", import_express.default.static(publicDir));
2248
- }
2249
- this.app.get("/api/traces", (_req, res) => {
2438
+ this.app.get("/api/traces", (req, res) => {
2250
2439
  try {
2251
- const traces = this.watcher.getAllTraces().map(serializeTrace);
2252
- res.json(traces);
2440
+ const limit = Math.min(Math.max(parseInt(req.query.limit, 10) || 50, 1), 200);
2441
+ const cursor = req.query.cursor ? parseFloat(req.query.cursor) : void 0;
2442
+ let allTraces = this.watcher.getAllTraces();
2443
+ if (cursor) {
2444
+ allTraces = allTraces.filter((t) => (t.lastModified || t.startTime) < cursor);
2445
+ }
2446
+ const page = allTraces.slice(0, limit);
2447
+ const serialized = page.map(serializeTrace);
2448
+ const lastTrace = page[page.length - 1];
2449
+ const nextCursor = page.length === limit && lastTrace ? lastTrace.lastModified || lastTrace.startTime : null;
2450
+ res.json({ traces: serialized, nextCursor });
2253
2451
  } catch (_error) {
2254
2452
  res.status(500).json({ error: "Failed to load traces" });
2255
2453
  }
@@ -2540,6 +2738,102 @@ var DashboardServer = class {
2540
2738
  res.status(500).json({ error: "Failed to load agent statistics" });
2541
2739
  }
2542
2740
  });
2741
+ this.app.get("/api/soma/report", (_req, res) => {
2742
+ const somaVault = this.config.somaVault;
2743
+ if (!somaVault) {
2744
+ return res.json({ available: false, teaser: true });
2745
+ }
2746
+ try {
2747
+ const reportPath = path3.join(somaVault, "..", "soma-report.json");
2748
+ if (!fs3.existsSync(reportPath)) {
2749
+ return res.json({ available: false, teaser: false, message: "No report file yet. Run soma watch." });
2750
+ }
2751
+ const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
2752
+ res.json(report);
2753
+ } catch (error) {
2754
+ console.error("Soma report error:", error);
2755
+ res.json({ available: false, teaser: false, message: "Failed to read report" });
2756
+ }
2757
+ });
2758
+ this.app.get("/api/soma/governance", (_req, res) => {
2759
+ const somaVault = this.config.somaVault;
2760
+ if (!somaVault) {
2761
+ return res.json({ available: false });
2762
+ }
2763
+ try {
2764
+ const reportPath = path3.join(somaVault, "..", "soma-report.json");
2765
+ if (!fs3.existsSync(reportPath)) {
2766
+ return res.json({ available: false, message: "No report file. Run soma report." });
2767
+ }
2768
+ const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
2769
+ res.json({
2770
+ available: true,
2771
+ layers: report.layers ?? { archive: 0, working: 0, emerging: 0, canon: 0 },
2772
+ governance: report.governance ?? { pending: 0, promoted: 0, rejected: 0 },
2773
+ insights: (report.insights ?? []).filter((i) => i.layer === "emerging" && i.proposal_status === "pending"),
2774
+ canon: (report.insights ?? []).filter((i) => i.layer === "canon"),
2775
+ generatedAt: report.generatedAt
2776
+ });
2777
+ } catch (error) {
2778
+ console.error("Soma governance error:", error);
2779
+ res.status(500).json({ available: false, message: "Failed to read governance data" });
2780
+ }
2781
+ });
2782
+ const sanitizeArg = (s) => s.replace(/[^a-zA-Z0-9_\-.:]/g, "");
2783
+ const sanitizeReason = (s) => s.replace(/["`$\\]/g, "").slice(0, 500);
2784
+ this.app.post("/api/soma/governance/promote", (req, res) => {
2785
+ var _a;
2786
+ const somaVault = this.config.somaVault;
2787
+ if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
2788
+ const { entryId } = req.body ?? {};
2789
+ if (!entryId) return res.status(400).json({ error: "entryId required" });
2790
+ try {
2791
+ const { execSync: execSync2 } = require("child_process");
2792
+ const safeId = sanitizeArg(String(entryId));
2793
+ const result = execSync2(`npx soma governance promote ${safeId} --vault "${somaVault}"`, {
2794
+ encoding: "utf-8",
2795
+ timeout: 1e4
2796
+ });
2797
+ res.json({ success: true, message: result.trim() });
2798
+ } catch (error) {
2799
+ res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
2800
+ }
2801
+ });
2802
+ this.app.post("/api/soma/governance/reject", (req, res) => {
2803
+ var _a;
2804
+ const somaVault = this.config.somaVault;
2805
+ if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
2806
+ const { entryId, reason } = req.body ?? {};
2807
+ if (!entryId || !reason) return res.status(400).json({ error: "entryId and reason required" });
2808
+ try {
2809
+ const { execSync: execSync2 } = require("child_process");
2810
+ const safeId = sanitizeArg(String(entryId));
2811
+ const safeReason = sanitizeReason(String(reason));
2812
+ const result = execSync2(`npx soma governance reject ${safeId} "${safeReason}" --vault "${somaVault}"`, {
2813
+ encoding: "utf-8",
2814
+ timeout: 1e4
2815
+ });
2816
+ res.json({ success: true, message: result.trim() });
2817
+ } catch (error) {
2818
+ res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
2819
+ }
2820
+ });
2821
+ this.app.get("/api/soma/governance/evidence/:id", (req, res) => {
2822
+ var _a;
2823
+ const somaVault = this.config.somaVault;
2824
+ if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
2825
+ try {
2826
+ const { execSync: execSync2 } = require("child_process");
2827
+ const safeId = sanitizeArg(String(req.params.id));
2828
+ const result = execSync2(`npx soma governance show ${safeId} --vault "${somaVault}"`, {
2829
+ encoding: "utf-8",
2830
+ timeout: 1e4
2831
+ });
2832
+ res.json({ available: true, output: result.trim() });
2833
+ } catch (error) {
2834
+ res.status(404).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
2835
+ }
2836
+ });
2543
2837
  this.app.get("/api/process-health", (_req, res) => {
2544
2838
  var _a, _b;
2545
2839
  try {
@@ -2552,7 +2846,14 @@ var DashboardServer = class {
2552
2846
  path3.dirname(this.config.tracesDir),
2553
2847
  ...this.config.dataDirs || []
2554
2848
  ];
2555
- const configs = (0, import_agentflow_core3.discoverAllProcessConfigs)(discoveryDirs);
2849
+ let configs = (0, import_agentflow_core3.discoverAllProcessConfigs)(discoveryDirs);
2850
+ const pref = getProcessPreference(this.userConfig);
2851
+ if (pref) {
2852
+ const hasPreferred = configs.some((c) => c.processName === pref.prefer);
2853
+ if (hasPreferred) {
2854
+ configs = configs.filter((c) => c.processName !== pref.over);
2855
+ }
2856
+ }
2556
2857
  if (configs.length === 0) {
2557
2858
  return res.json(null);
2558
2859
  }
@@ -2642,29 +2943,26 @@ var DashboardServer = class {
2642
2943
  ...extraDirs
2643
2944
  ];
2644
2945
  const discovered = [];
2645
- try {
2646
- const { execSync } = require("child_process");
2647
- const raw = execSync(
2648
- "systemctl --user show --property=ExecStart --no-pager alfred.service openclaw-gateway.service 2>/dev/null",
2649
- { encoding: "utf8", timeout: 5e3 }
2650
- );
2651
- for (const line of raw.split("\n")) {
2652
- const match = line.match(/path=([^\s;]+)/);
2653
- if (match == null ? void 0 : match[1]) {
2654
- const dir = path3.dirname(match[1]);
2655
- if (fs3.existsSync(dir)) discovered.push(dir);
2946
+ const svcNames = getSystemdServices(this.userConfig);
2947
+ if (svcNames.length > 0) {
2948
+ try {
2949
+ const { execSync: execSync2 } = require("child_process");
2950
+ const raw = execSync2(
2951
+ `systemctl --user show --property=ExecStart --no-pager ${svcNames.join(" ")} 2>/dev/null`,
2952
+ { encoding: "utf8", timeout: 5e3 }
2953
+ );
2954
+ for (const line of raw.split("\n")) {
2955
+ const match = line.match(/path=([^\s;]+)/);
2956
+ if (match == null ? void 0 : match[1]) {
2957
+ const dir = path3.dirname(match[1]);
2958
+ if (fs3.existsSync(dir)) discovered.push(dir);
2959
+ }
2656
2960
  }
2961
+ } catch {
2657
2962
  }
2658
- } catch {
2659
2963
  }
2660
2964
  const commonPaths = [
2661
- path3.join(home, ".alfred/traces"),
2662
- path3.join(home, ".alfred/data"),
2663
- path3.join(home, ".openclaw/workspace/traces"),
2664
- path3.join(home, ".openclaw/subagents"),
2665
- path3.join(home, ".openclaw/cron/runs"),
2666
- path3.join(home, ".openclaw/cron"),
2667
- path3.join(home, ".openclaw/agents/main/sessions"),
2965
+ ...getDiscoveryPaths(this.userConfig),
2668
2966
  path3.join(home, ".agentflow/traces")
2669
2967
  ];
2670
2968
  for (const p of commonPaths) {
@@ -2709,46 +3007,54 @@ var DashboardServer = class {
2709
3007
  res.status(500).json({ error: "Failed to update directory config" });
2710
3008
  }
2711
3009
  });
2712
- this.app.post("/v1/traces", import_express.default.json({ limit: "10mb" }), (req, res) => {
2713
- try {
2714
- const traces = parseOtlpPayload(req.body);
2715
- let ingested = 0;
2716
- for (const trace of traces) {
2717
- const nodes = /* @__PURE__ */ new Map();
2718
- for (const [id, node] of Object.entries(trace.nodes)) {
2719
- nodes.set(id, { ...node, state: {} });
3010
+ if (this.config.enableCollector !== false) {
3011
+ this.app.post("/v1/traces", import_express.default.json({ limit: "10mb" }), (req, res) => {
3012
+ try {
3013
+ if (this.config.collectorAuthToken) {
3014
+ const auth = req.headers.authorization;
3015
+ if (!auth || auth !== `Bearer ${this.config.collectorAuthToken}`) {
3016
+ return res.status(401).json({ error: "Unauthorized \u2014 provide Authorization: Bearer <token>" });
3017
+ }
2720
3018
  }
2721
- const watched = {
2722
- id: trace.id,
2723
- rootNodeId: Object.keys(trace.nodes)[0] ?? "",
2724
- agentId: trace.agentId,
2725
- name: trace.name,
2726
- trigger: trace.trigger,
2727
- startTime: trace.startTime,
2728
- endTime: trace.endTime,
2729
- status: trace.status,
2730
- nodes,
2731
- edges: [],
2732
- events: [],
2733
- metadata: { ...trace.metadata, adapterSource: "otel" },
2734
- sessionEvents: [],
2735
- sourceType: "session",
2736
- filename: `otel-${trace.id}`,
2737
- lastModified: Date.now(),
2738
- sourceDir: "http-collector"
2739
- };
2740
- this.watcher.traces.set(`otel:${trace.id}`, watched);
2741
- ingested++;
2742
- }
2743
- if (ingested > 0) {
2744
- this.broadcast({ type: "traces-updated", count: ingested });
3019
+ const traces = parseOtlpPayload(req.body);
3020
+ let ingested = 0;
3021
+ for (const trace of traces) {
3022
+ const nodes = /* @__PURE__ */ new Map();
3023
+ for (const [id, node] of Object.entries(trace.nodes)) {
3024
+ nodes.set(id, { ...node, state: {} });
3025
+ }
3026
+ const watched = {
3027
+ id: trace.id,
3028
+ rootNodeId: Object.keys(trace.nodes)[0] ?? "",
3029
+ agentId: trace.agentId,
3030
+ name: trace.name,
3031
+ trigger: trace.trigger,
3032
+ startTime: trace.startTime,
3033
+ endTime: trace.endTime,
3034
+ status: trace.status,
3035
+ nodes,
3036
+ edges: [],
3037
+ events: [],
3038
+ metadata: { ...trace.metadata, adapterSource: "otel" },
3039
+ sessionEvents: [],
3040
+ sourceType: "session",
3041
+ filename: `otel-${trace.id}`,
3042
+ lastModified: Date.now(),
3043
+ sourceDir: "http-collector"
3044
+ };
3045
+ this.watcher.traces.set(`otel:${trace.id}`, watched);
3046
+ ingested++;
3047
+ }
3048
+ if (ingested > 0) {
3049
+ this.broadcast({ type: "traces-updated", count: ingested });
3050
+ }
3051
+ res.json({ ok: true, tracesIngested: ingested });
3052
+ } catch (error) {
3053
+ console.error("OTLP collector error:", error);
3054
+ res.status(400).json({ error: "Failed to parse OTLP payload" });
2745
3055
  }
2746
- res.json({ ok: true, tracesIngested: ingested });
2747
- } catch (error) {
2748
- console.error("OTLP collector error:", error);
2749
- res.status(400).json({ error: "Failed to parse OTLP payload" });
2750
- }
2751
- });
3056
+ });
3057
+ }
2752
3058
  this.app.get("/health", (_req, res) => {
2753
3059
  res.json({
2754
3060
  status: "ok",
@@ -2760,18 +3066,10 @@ var DashboardServer = class {
2760
3066
  this.app.get("/ready", (_req, res) => {
2761
3067
  res.json({ status: "ready" });
2762
3068
  });
2763
- this.app.get("/v1/*", (_req, res) => {
2764
- const legacyIndex = path3.join(__dirname, "../public/index.html");
2765
- if (fs3.existsSync(legacyIndex)) {
2766
- res.sendFile(legacyIndex);
2767
- } else {
2768
- res.status(404).send("Legacy dashboard not found");
2769
- }
2770
- });
2771
3069
  this.app.get("*", (_req, res) => {
2772
- const clientIndex = path3.join(__dirname, "../dist/client/index.html");
2773
- if (fs3.existsSync(clientIndex)) {
2774
- res.sendFile(clientIndex);
3070
+ const clientIndex2 = path3.join(__dirname, "../dist/client/index.html");
3071
+ if (fs3.existsSync(clientIndex2)) {
3072
+ res.sendFile(clientIndex2);
2775
3073
  } else {
2776
3074
  res.status(404).send("Dashboard not found - public files may not be built");
2777
3075
  }
@@ -3018,24 +3316,49 @@ var DashboardServer = class {
3018
3316
  });
3019
3317
  }
3020
3318
  async start() {
3021
- return new Promise((resolve4) => {
3319
+ return new Promise((resolve5) => {
3022
3320
  const host = this.config.host || "localhost";
3023
3321
  this.server.listen(this.config.port, host, () => {
3024
3322
  console.log(`AgentFlow Dashboard running at http://${host}:${this.config.port}`);
3025
3323
  console.log(`Watching traces in: ${this.config.tracesDir}`);
3026
- resolve4();
3324
+ resolve5();
3027
3325
  });
3028
3326
  });
3029
3327
  }
3328
+ /** Check if any src/client file is newer than the built bundle. */
3329
+ isClientStale(srcDir, distDir) {
3330
+ try {
3331
+ const distIndex = path3.join(distDir, "index.html");
3332
+ if (!fs3.existsSync(distIndex)) return true;
3333
+ const distMtime = fs3.statSync(distIndex).mtimeMs;
3334
+ const check = (dir) => {
3335
+ for (const entry of fs3.readdirSync(dir, { withFileTypes: true })) {
3336
+ const full = path3.join(dir, entry.name);
3337
+ if (entry.isDirectory()) {
3338
+ if (check(full)) return true;
3339
+ } else if (fs3.statSync(full).mtimeMs > distMtime) {
3340
+ return true;
3341
+ }
3342
+ }
3343
+ return false;
3344
+ };
3345
+ return check(srcDir);
3346
+ } catch {
3347
+ return false;
3348
+ }
3349
+ }
3030
3350
  async stop() {
3031
- return new Promise((resolve4) => {
3351
+ return new Promise((resolve5) => {
3032
3352
  this.watcher.stop();
3033
3353
  this.server.close(() => {
3034
3354
  console.log("Dashboard server stopped");
3035
- resolve4();
3355
+ resolve5();
3036
3356
  });
3037
3357
  });
3038
3358
  }
3359
+ getConfigPath() {
3360
+ return this.configPath;
3361
+ }
3039
3362
  getStats() {
3040
3363
  return this.stats.getGlobalStats();
3041
3364
  }