agentflow-dashboard 0.7.1 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.cjs CHANGED
@@ -32,11 +32,86 @@ __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");
114
+ var import_chokidar2 = __toESM(require("chokidar"), 1);
40
115
  var import_express = __toESM(require("express"), 1);
41
116
  var import_ws = require("ws");
42
117
 
@@ -69,17 +144,17 @@ var AgentFlowAdapter = class {
69
144
  };
70
145
 
71
146
  // src/adapters/openclaw.ts
72
- var import_node_fs = require("fs");
73
- var import_node_path = require("path");
147
+ var import_node_fs2 = require("fs");
148
+ var import_node_path2 = require("path");
74
149
  var jobCache = /* @__PURE__ */ new Map();
75
150
  function loadJobs(openclawDir) {
76
151
  const cached = jobCache.get(openclawDir);
77
152
  if (cached) return cached;
78
- const jobsPath = (0, import_node_path.join)(openclawDir, "cron", "jobs.json");
153
+ const jobsPath = (0, import_node_path2.join)(openclawDir, "cron", "jobs.json");
79
154
  const map = /* @__PURE__ */ new Map();
80
155
  try {
81
- if ((0, import_node_fs.existsSync)(jobsPath)) {
82
- const data = JSON.parse((0, import_node_fs.readFileSync)(jobsPath, "utf-8"));
156
+ if ((0, import_node_fs2.existsSync)(jobsPath)) {
157
+ const data = JSON.parse((0, import_node_fs2.readFileSync)(jobsPath, "utf-8"));
83
158
  const jobs = Array.isArray(data) ? data : data.jobs ?? [];
84
159
  for (const job of jobs) {
85
160
  if (job.id) map.set(job.id, job);
@@ -91,19 +166,19 @@ function loadJobs(openclawDir) {
91
166
  return map;
92
167
  }
93
168
  function findOpenClawRoot(filePath) {
94
- let dir = (0, import_node_path.dirname)(filePath);
169
+ let dir = (0, import_node_path2.dirname)(filePath);
95
170
  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") {
171
+ if ((0, import_node_fs2.existsSync)((0, import_node_path2.join)(dir, "cron", "jobs.json")) || (0, import_node_path2.basename)(dir) === ".openclaw") {
97
172
  return dir;
98
173
  }
99
- dir = (0, import_node_path.dirname)(dir);
174
+ dir = (0, import_node_path2.dirname)(dir);
100
175
  }
101
176
  return null;
102
177
  }
103
178
  var OpenClawAdapter = class {
104
179
  name = "openclaw";
105
180
  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"));
181
+ 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
182
  }
108
183
  canHandle(filePath) {
109
184
  if (!filePath.endsWith(".jsonl")) return false;
@@ -112,7 +187,7 @@ var OpenClawAdapter = class {
112
187
  parse(filePath) {
113
188
  const traces = [];
114
189
  try {
115
- const content = (0, import_node_fs.readFileSync)(filePath, "utf-8");
190
+ const content = (0, import_node_fs2.readFileSync)(filePath, "utf-8");
116
191
  const root = findOpenClawRoot(filePath);
117
192
  const jobs = root ? loadJobs(root) : /* @__PURE__ */ new Map();
118
193
  for (const line of content.split("\n")) {
@@ -124,7 +199,7 @@ var OpenClawAdapter = class {
124
199
  continue;
125
200
  }
126
201
  if (entry.action !== "finished") continue;
127
- const jobId = entry.jobId ?? (0, import_node_path.basename)(filePath, ".jsonl");
202
+ const jobId = entry.jobId ?? (0, import_node_path2.basename)(filePath, ".jsonl");
128
203
  const job = jobs.get(jobId);
129
204
  const jobName = (job == null ? void 0 : job.name) ?? jobId;
130
205
  const startTime = entry.runAtMs ?? entry.ts;
@@ -176,8 +251,8 @@ var OpenClawAdapter = class {
176
251
  };
177
252
 
178
253
  // src/adapters/otel.ts
179
- var import_node_fs2 = require("fs");
180
- var import_node_path2 = require("path");
254
+ var import_node_fs3 = require("fs");
255
+ var import_node_path3 = require("path");
181
256
  var SPAN_TYPE_MAP = {
182
257
  "gen_ai.chat": "llm",
183
258
  "gen_ai.completion": "llm",
@@ -284,8 +359,8 @@ var OTelAdapter = class {
284
359
  name = "otel";
285
360
  detect(dirPath) {
286
361
  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);
362
+ if ((0, import_node_fs3.existsSync)((0, import_node_path3.join)(dirPath, "otel-traces"))) return true;
363
+ const files = (0, import_node_fs3.readdirSync)(dirPath);
289
364
  return files.some((f) => f.endsWith(".otlp.json"));
290
365
  } catch {
291
366
  return false;
@@ -296,7 +371,7 @@ var OTelAdapter = class {
296
371
  }
297
372
  parse(filePath) {
298
373
  try {
299
- const content = (0, import_node_fs2.readFileSync)(filePath, "utf-8");
374
+ const content = (0, import_node_fs3.readFileSync)(filePath, "utf-8");
300
375
  const payload = JSON.parse(content);
301
376
  const traces = parseOtlpPayload(payload);
302
377
  for (const t of traces) t.filePath = filePath;
@@ -338,9 +413,7 @@ function extractSource(agentId) {
338
413
  const colonIdx = agentId.indexOf(":");
339
414
  if (colonIdx > 0 && colonIdx < 20) {
340
415
  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
- }
416
+ return { source: prefix, localId: agentId.slice(colonIdx + 1) };
344
417
  }
345
418
  return { source: "agentflow", localId: agentId };
346
419
  }
@@ -371,16 +444,20 @@ function deduplicateAgents(agents) {
371
444
  for (const a of tagged) {
372
445
  const suffix = extractSuffix(a.localId);
373
446
  if (!suffix) continue;
374
- const group = suffixGroups.get(suffix) ?? [];
447
+ const key = `${a.source}:${suffix}`;
448
+ const group = suffixGroups.get(key) ?? [];
375
449
  group.push(a);
376
- suffixGroups.set(suffix, group);
450
+ suffixGroups.set(key, group);
377
451
  }
378
452
  const mergedIds = /* @__PURE__ */ new Set();
379
453
  const mergedAgents = [];
380
- for (const [suffix, group] of suffixGroups) {
454
+ for (const [_key, group] of suffixGroups) {
455
+ const suffix = extractSuffix(group[0].localId);
381
456
  if (group.length < 2) continue;
382
457
  const prefixes = new Set(group.map((a) => a.localId.split("-")[0]));
383
458
  if (prefixes.size < 2) continue;
459
+ const longPrefixes = [...prefixes].filter((p) => p !== suffix && p.length > 2);
460
+ if (longPrefixes.length >= 2) continue;
384
461
  const merged = {
385
462
  agentId: group[0].source === "agentflow" ? suffix : `${group[0].source}:${suffix}`,
386
463
  displayName: suffix,
@@ -428,10 +505,7 @@ function groupAgents(agents) {
428
505
  }
429
506
  const SOURCE_DISPLAY = {
430
507
  agentflow: "AgentFlow",
431
- openclaw: "OpenClaw",
432
- otel: "OpenTelemetry",
433
- langchain: "LangChain",
434
- crewai: "CrewAI"
508
+ otel: "OpenTelemetry"
435
509
  };
436
510
  const groups = [];
437
511
  for (const [source, sourceAgents] of sourceMap) {
@@ -777,10 +851,6 @@ function getUniversalNodeStatus(activity) {
777
851
  return "completed";
778
852
  }
779
853
  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
854
  const firstSegment = sessionId.split("-")[0];
785
855
  if (firstSegment) return firstSegment;
786
856
  return "openclaw";
@@ -793,19 +863,81 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
793
863
  tracesDir;
794
864
  dataDirs;
795
865
  allWatchDirs;
866
+ maxAgeMs;
867
+ userConfig;
796
868
  constructor(tracesDirOrOptions) {
797
869
  super();
870
+ const defaultMaxAgeMs = 48 * 60 * 60 * 1e3;
871
+ const envHours = process.env.AGENTFLOW_TRACE_WINDOW_HOURS;
872
+ const envMaxAgeMs = envHours ? parseFloat(envHours) * 60 * 60 * 1e3 : void 0;
798
873
  if (typeof tracesDirOrOptions === "string") {
799
874
  this.tracesDir = path.resolve(tracesDirOrOptions);
800
875
  this.dataDirs = [];
876
+ this.maxAgeMs = envMaxAgeMs ?? defaultMaxAgeMs;
877
+ this.userConfig = {};
801
878
  } else {
802
879
  this.tracesDir = path.resolve(tracesDirOrOptions.tracesDir);
803
880
  this.dataDirs = (tracesDirOrOptions.dataDirs || []).map((d) => path.resolve(d));
804
- }
805
- this.allWatchDirs = [this.tracesDir, ...this.dataDirs];
881
+ this.maxAgeMs = envMaxAgeMs ?? tracesDirOrOptions.maxAgeMs ?? defaultMaxAgeMs;
882
+ this.userConfig = tracesDirOrOptions.userConfig ?? {};
883
+ }
884
+ this.skipFiles = /* @__PURE__ */ new Set([
885
+ ..._TraceWatcher.STRUCTURAL_SKIP_FILES,
886
+ ...getSkipFiles(this.userConfig)
887
+ ]);
888
+ this.userSkipDirs = new Set(getSkipDirectories(this.userConfig));
889
+ this.allWatchDirs = [...new Set([this.tracesDir, ...this.dataDirs].map((d) => path.resolve(d)))];
806
890
  this.ensureTracesDir();
807
891
  this.loadExistingFiles();
892
+ this.archiveOldTraces();
808
893
  this.startWatching();
894
+ setInterval(() => this.archiveOldTraces(), 6 * 60 * 60 * 1e3);
895
+ }
896
+ /** Move trace files older than maxAgeMs into archive/YYYY-MM/ subdirectories. */
897
+ archiveOldTraces() {
898
+ const cutoff = Date.now() - this.maxAgeMs;
899
+ let archived = 0;
900
+ for (const dir of this.allWatchDirs) {
901
+ if (!fs.existsSync(dir)) continue;
902
+ try {
903
+ this.archiveDirectory(dir, cutoff, 0);
904
+ } catch (error) {
905
+ console.warn(`Archival error in ${dir}:`, error.message);
906
+ }
907
+ }
908
+ }
909
+ archiveDirectory(dir, cutoff, depth) {
910
+ if (depth > 10) return 0;
911
+ if (path.basename(dir) === "archive") return 0;
912
+ let archived = 0;
913
+ try {
914
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
915
+ for (const entry of entries) {
916
+ if (entry.name.startsWith(".") || entry.name === "archive" || this.userSkipDirs.has(entry.name)) continue;
917
+ const fullPath = path.join(dir, entry.name);
918
+ if (entry.isDirectory()) {
919
+ archived += this.archiveDirectory(fullPath, cutoff, depth + 1);
920
+ continue;
921
+ }
922
+ if (!entry.isFile() || !this.isSupportedFile(entry.name)) continue;
923
+ try {
924
+ const stats = fs.statSync(fullPath);
925
+ if (stats.mtimeMs >= cutoff) continue;
926
+ const mtime = new Date(stats.mtimeMs);
927
+ const yearMonth = `${mtime.getFullYear()}-${String(mtime.getMonth() + 1).padStart(2, "0")}`;
928
+ const archiveDir = path.join(this.tracesDir, "archive", yearMonth);
929
+ fs.mkdirSync(archiveDir, { recursive: true });
930
+ const dest = path.join(archiveDir, entry.name);
931
+ fs.renameSync(fullPath, dest);
932
+ const key = this.traceKey(fullPath);
933
+ this.traces.delete(key);
934
+ archived++;
935
+ } catch {
936
+ }
937
+ }
938
+ } catch {
939
+ }
940
+ return archived;
809
941
  }
810
942
  ensureTracesDir() {
811
943
  if (!fs.existsSync(this.tracesDir)) {
@@ -838,9 +970,17 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
838
970
  const entries = fs.readdirSync(dir, { withFileTypes: true });
839
971
  for (const entry of entries) {
840
972
  if (entry.name.startsWith(".")) continue;
973
+ if (entry.name === "archive") continue;
974
+ if (this.userSkipDirs.has(entry.name)) continue;
841
975
  const fullPath = path.join(dir, entry.name);
842
976
  if (entry.isFile()) {
843
977
  if (this.isSupportedFile(entry.name)) {
978
+ try {
979
+ const mtime = fs.statSync(fullPath).mtimeMs;
980
+ if (Date.now() - mtime > this.maxAgeMs) continue;
981
+ } catch {
982
+ continue;
983
+ }
844
984
  if (this.loadFile(fullPath)) {
845
985
  fileCount++;
846
986
  }
@@ -858,8 +998,8 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
858
998
  isSupportedFile(filename) {
859
999
  return filename.endsWith(".json") || filename.endsWith(".jsonl") || filename.endsWith(".log") || filename.endsWith(".trace");
860
1000
  }
861
- /** File names that are config/state, not tracesskip them. */
862
- static SKIP_FILES = /* @__PURE__ */ new Set([
1001
+ /** Structural file names that are never trace dataalways skipped. */
1002
+ static STRUCTURAL_SKIP_FILES = /* @__PURE__ */ new Set([
863
1003
  "workers.json",
864
1004
  "package.json",
865
1005
  "package-lock.json",
@@ -874,6 +1014,10 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
874
1014
  "update-check.json",
875
1015
  "exec-approvals.json"
876
1016
  ]);
1017
+ /** Skip files = structural + user config */
1018
+ skipFiles;
1019
+ /** Skip directories from user config */
1020
+ userSkipDirs;
877
1021
  static SKIP_SUFFIXES = [
878
1022
  "-state.json",
879
1023
  "-config.json",
@@ -885,7 +1029,7 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
885
1029
  /** Load a file using the adapter registry, falling back to built-in parsing. */
886
1030
  loadFile(filePath) {
887
1031
  const filename = path.basename(filePath);
888
- if (_TraceWatcher.SKIP_FILES.has(filename)) return false;
1032
+ if (this.skipFiles.has(filename)) return false;
889
1033
  if (_TraceWatcher.SKIP_SUFFIXES.some((s) => filename.endsWith(s))) return false;
890
1034
  const adapter = findAdapter(filePath);
891
1035
  if (adapter && adapter.name !== "agentflow") {
@@ -1062,43 +1206,26 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
1062
1206
  }
1063
1207
  return traces;
1064
1208
  }
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
- };
1209
+ /** Normalise agent identifiers using config-driven alias map. */
1087
1210
  normaliseAgentId(raw) {
1088
- return _TraceWatcher.AGENT_ALIASES[raw] ?? raw;
1211
+ const aliases = getAliases(this.userConfig);
1212
+ return aliases[raw] ?? raw;
1089
1213
  }
1090
1214
  detectAgentIdentifier(activity, _filename, filePath) {
1091
1215
  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);
1216
+ return this.normaliseAgentId(activity.agent_id);
1095
1217
  }
1096
1218
  const pathAgent = this.extractAgentFromPath(filePath);
1097
- if (filePath.includes(".alfred/") && !pathAgent.startsWith("alfred-")) {
1219
+ const detection = getAgentDetection(this.userConfig);
1220
+ if (detection.filePatterns) {
1098
1221
  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);
1222
+ for (const [pattern, template] of Object.entries(detection.filePatterns)) {
1223
+ const re = new RegExp(`^(${pattern})$`);
1224
+ const match = basename3.match(re);
1225
+ if (match) {
1226
+ const resolved = template.replace("${match}", match[1]);
1227
+ return this.normaliseAgentId(resolved);
1228
+ }
1102
1229
  }
1103
1230
  }
1104
1231
  return this.normaliseAgentId(pathAgent);
@@ -1106,20 +1233,23 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
1106
1233
  extractAgentFromPath(filePath) {
1107
1234
  const filename = path.basename(filePath, path.extname(filePath));
1108
1235
  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";
1236
+ const detection = getAgentDetection(this.userConfig);
1237
+ let pathPrefix = "";
1238
+ if (detection.pathPatterns) {
1239
+ for (const [pathSubstring, agentId] of Object.entries(detection.pathPatterns)) {
1240
+ if (filePath.includes(pathSubstring)) {
1241
+ pathPrefix = agentId;
1242
+ break;
1243
+ }
1116
1244
  }
1117
- return "openclaw";
1118
1245
  }
1119
- if (filePath.includes(".alfred/") || filename.includes("alfred")) {
1120
- return "alfred";
1246
+ const agentsIndex = pathParts.lastIndexOf("agents");
1247
+ if (agentsIndex !== -1 && agentsIndex + 1 < pathParts.length) {
1248
+ const agentName = pathParts[agentsIndex + 1];
1249
+ return pathPrefix ? `${pathPrefix}-${agentName}` : agentName;
1121
1250
  }
1122
- for (const part of pathParts.reverse()) {
1251
+ if (pathPrefix) return pathPrefix;
1252
+ for (const part of [...pathParts].reverse()) {
1123
1253
  if (part.match(/agent|worker|service|daemon|bot|ai|llm/i)) {
1124
1254
  return part;
1125
1255
  }
@@ -1454,19 +1584,22 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
1454
1584
  const parentDir = path.basename(path.dirname(filePath));
1455
1585
  const grandParentDir = path.basename(path.dirname(path.dirname(filePath)));
1456
1586
  const greatGrandParentDir = path.basename(path.dirname(path.dirname(path.dirname(filePath))));
1457
- let agentId;
1587
+ let agentName;
1458
1588
  if (parentDir === "sessions" && greatGrandParentDir === "agents") {
1459
- agentId = grandParentDir;
1589
+ agentName = grandParentDir;
1460
1590
  } else if (grandParentDir === "agents") {
1461
- agentId = parentDir;
1462
- } else if (parentDir === "runs" && grandParentDir === "cron") {
1463
- agentId = "openclaw-cron";
1591
+ agentName = parentDir;
1464
1592
  } else {
1465
- agentId = parentDir;
1466
- }
1467
- if (filePath.includes(".alfred/") || filePath.includes("alfred")) {
1468
- if (!agentId.startsWith("alfred-")) {
1469
- agentId = `alfred-${agentId}`;
1593
+ agentName = parentDir;
1594
+ }
1595
+ let agentId = agentName;
1596
+ const detection = getAgentDetection(this.userConfig);
1597
+ if (detection.pathPatterns) {
1598
+ for (const [pathSubstring, prefix] of Object.entries(detection.pathPatterns)) {
1599
+ if (filePath.includes(pathSubstring)) {
1600
+ agentId = `${prefix}-${agentName}`;
1601
+ break;
1602
+ }
1470
1603
  }
1471
1604
  }
1472
1605
  const modelEvent = rawEvents.find((e) => e.type === "model_change");
@@ -1755,6 +1888,7 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
1755
1888
  edges: [],
1756
1889
  events: [],
1757
1890
  startTime,
1891
+ status,
1758
1892
  agentId,
1759
1893
  trigger,
1760
1894
  name: rootName,
@@ -1875,8 +2009,12 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
1875
2009
  // Ignore git directories
1876
2010
  /\.vscode/,
1877
2011
  // Ignore vscode
1878
- /\.idea/
2012
+ /\.idea/,
1879
2013
  // Ignore idea
2014
+ /\/archive\//,
2015
+ // Ignore archived trace files
2016
+ // Ignore user-configured skip directories
2017
+ ...getSkipDirectories(this.userConfig).map((d) => new RegExp(`/${d}/`))
1880
2018
  ],
1881
2019
  persistent: true,
1882
2020
  ignoreInitial: true,
@@ -1930,29 +2068,42 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
1930
2068
  });
1931
2069
  }
1932
2070
  getTrace(filename) {
2071
+ const candidates = [];
1933
2072
  const exact = this.traces.get(filename);
1934
- if (exact) return exact;
2073
+ if (exact) candidates.push(exact);
1935
2074
  if (filename.includes("::")) {
1936
2075
  const [fname, startTimeStr] = filename.split("::");
1937
2076
  const startTime = Number(startTimeStr);
1938
2077
  if (fname && !Number.isNaN(startTime)) {
1939
2078
  for (const trace of this.traces.values()) {
1940
2079
  if (trace.filename === fname && trace.startTime === startTime) {
1941
- return trace;
2080
+ candidates.push(trace);
1942
2081
  }
1943
2082
  }
1944
2083
  }
1945
2084
  }
1946
2085
  for (const prefix of ["openclaw:", "otel:", ""]) {
1947
2086
  const prefixed = this.traces.get(prefix + filename);
1948
- if (prefixed) return prefixed;
2087
+ if (prefixed) candidates.push(prefixed);
1949
2088
  }
1950
2089
  for (const [key, trace] of this.traces) {
1951
2090
  if (trace.filename === filename || trace.id === filename || key.endsWith(filename)) {
1952
- return trace;
2091
+ candidates.push(trace);
1953
2092
  }
1954
2093
  }
1955
- return void 0;
2094
+ if (candidates.length === 0) return void 0;
2095
+ if (candidates.length === 1) return candidates[0];
2096
+ let best = candidates[0];
2097
+ let bestNodeCount = best.nodes instanceof Map ? best.nodes.size : Object.keys(best.nodes ?? {}).length;
2098
+ for (let i = 1; i < candidates.length; i++) {
2099
+ const c = candidates[i];
2100
+ const nc = c.nodes instanceof Map ? c.nodes.size : Object.keys(c.nodes ?? {}).length;
2101
+ if (nc > bestNodeCount) {
2102
+ best = c;
2103
+ bestNodeCount = nc;
2104
+ }
2105
+ }
2106
+ return best;
1956
2107
  }
1957
2108
  getTracesByAgent(agentId) {
1958
2109
  return this.getAllTraces().filter((trace) => trace.agentId === agentId);
@@ -1999,7 +2150,7 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
1999
2150
  var fs2 = __toESM(require("fs"), 1);
2000
2151
  var os = __toESM(require("os"), 1);
2001
2152
  var path2 = __toESM(require("path"), 1);
2002
- var VERSION = "0.4.0";
2153
+ var VERSION = "0.8.0";
2003
2154
  function getLanAddress() {
2004
2155
  const interfaces = os.networkInterfaces();
2005
2156
  for (const name of Object.keys(interfaces)) {
@@ -2011,7 +2162,7 @@ function getLanAddress() {
2011
2162
  }
2012
2163
  return null;
2013
2164
  }
2014
- function printBanner(config, traceCount, stats) {
2165
+ function printBanner(config, traceCount, stats, configPath) {
2015
2166
  var _a;
2016
2167
  const lan = getLanAddress();
2017
2168
  const host = config.host || "localhost";
@@ -2027,26 +2178,25 @@ function printBanner(config, traceCount, stats) {
2027
2178
 
2028
2179
  See your agents think.
2029
2180
 
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
2181
  Traces: ${config.tracesDir}${((_a = config.dataDirs) == null ? void 0 : _a.length) ? `
2042
2182
  Data dirs: ${config.dataDirs.join("\n ")}` : ""}
2043
2183
  Loaded: ${traceCount} traces \xB7 ${stats.totalAgents} agents \xB7 ${stats.totalExecutions} executions
2044
2184
  Success: ${stats.globalSuccessRate.toFixed(1)}%${stats.activeAgents > 0 ? ` \xB7 ${stats.activeAgents} active now` : ""}
2185
+ Config: ${configPath ?? "none (using defaults)"}
2045
2186
  CORS: ${config.enableCors ? "enabled" : "disabled"}
2046
2187
  WebSocket: live updates enabled
2188
+ Window: ${process.env.AGENTFLOW_TRACE_WINDOW_HOURS ?? "48"}h (set AGENTFLOW_TRACE_WINDOW_HOURS to change)
2047
2189
 
2048
2190
  \u2192 http://localhost:${port}${isPublic && lan ? `
2049
2191
  \u2192 http://${lan}:${port} (LAN)` : ""}
2192
+
2193
+ Pages: Agents \xB7 SOMA
2194
+ Agent: Profile \xB7 Execution Detail
2195
+ SOMA: Intelligence \xB7 Review \xB7 Policies \xB7 Knowledge \xB7 Activity
2196
+ Tabs: Flame Chart \xB7 Agent Flow \xB7 Metrics \xB7 Dependencies
2197
+ State Machine \xB7 Summary \xB7 Transcript
2198
+
2199
+ Runs locally. Your data never leaves your machine.
2050
2200
  `);
2051
2201
  }
2052
2202
  async function startDashboard() {
@@ -2084,6 +2234,12 @@ async function startDashboard() {
2084
2234
  case "--collector-token":
2085
2235
  config.collectorAuthToken = args[++i];
2086
2236
  break;
2237
+ case "--soma-vault":
2238
+ config.somaVault = args[++i];
2239
+ break;
2240
+ case "--config":
2241
+ config.configPath = args[++i];
2242
+ break;
2087
2243
  case "--help":
2088
2244
  printHelp();
2089
2245
  process.exit(0);
@@ -2095,6 +2251,9 @@ async function startDashboard() {
2095
2251
  if (process.env.AGENTFLOW_NO_COLLECTOR === "true") {
2096
2252
  config.enableCollector = false;
2097
2253
  }
2254
+ if (!config.somaVault && process.env.SOMA_VAULT) {
2255
+ config.somaVault = process.env.SOMA_VAULT;
2256
+ }
2098
2257
  const tracesPath = path2.resolve(config.tracesDir);
2099
2258
  if (!fs2.existsSync(tracesPath)) {
2100
2259
  fs2.mkdirSync(tracesPath, { recursive: true });
@@ -2116,7 +2275,7 @@ async function startDashboard() {
2116
2275
  setTimeout(() => {
2117
2276
  const stats = dashboard.getStats();
2118
2277
  const traces = dashboard.getTraces();
2119
- printBanner(config, traces.length, stats);
2278
+ printBanner(config, traces.length, stats, dashboard.getConfigPath());
2120
2279
  }, 1500);
2121
2280
  } catch (error) {
2122
2281
  console.error("\u274C Failed to start dashboard:", error);
@@ -2125,7 +2284,7 @@ async function startDashboard() {
2125
2284
  }
2126
2285
  function printHelp() {
2127
2286
  console.log(`
2128
- \u{1F4CA} AgentFlow Dashboard v${VERSION} \u2014 See your agents think.
2287
+ AgentFlow Dashboard v${VERSION} \u2014 See your agents think.
2129
2288
 
2130
2289
  Usage:
2131
2290
  agentflow-dashboard [options]
@@ -2136,22 +2295,34 @@ Options:
2136
2295
  -t, --traces <path> Traces directory (default: ./traces)
2137
2296
  -h, --host <address> Host address (default: localhost)
2138
2297
  --data-dir <path> Extra data directory for process discovery (repeatable)
2298
+ --config <path> Path to agentflow.config.json (aliases, skip files, etc.)
2299
+ --soma-vault <path> SOMA vault directory for intelligence data
2139
2300
  --cors Enable CORS headers
2140
2301
  --no-collector Disable OTLP trace collector (POST /v1/traces)
2141
2302
  --collector-token <tok> Require auth token for collector (or set AGENTFLOW_COLLECTOR_TOKEN)
2142
2303
  --help Show this help message
2143
2304
 
2144
- Examples:
2145
- agentflow-dashboard --traces ./traces --host 0.0.0.0 --cors
2146
- agentflow-dashboard -p 8080 -t /var/log/agentflow
2147
- agentflow-dashboard --traces ./traces --data-dir ./workers --data-dir ./cron
2305
+ Config file:
2306
+ The dashboard loads agentflow.config.json for agent aliases, skip files,
2307
+ discovery paths, and systemd services. Resolution order:
2308
+ 1. --config flag
2309
+ 2. AGENTFLOW_CONFIG env var
2310
+ 3. ./agentflow.config.json
2311
+ 4. ~/.config/agentflow/config.json
2312
+
2313
+ See agentflow.config.example.json for a complete reference.
2314
+
2315
+ Environment:
2316
+ AGENTFLOW_CONFIG Path to config file
2317
+ AGENTFLOW_TRACE_WINDOW_HOURS Max age of traces to load (default: 48)
2318
+ AGENTFLOW_COLLECTOR_TOKEN Auth token for OTLP collector
2319
+ AGENTFLOW_NO_COLLECTOR=true Disable OTLP collector
2320
+ SOMA_VAULT SOMA vault directory
2148
2321
 
2149
- Tabs:
2150
- \u{1F3AF} Graph Interactive Cytoscape.js execution graph
2151
- \u23F1\uFE0F Timeline Waterfall view of node durations
2152
- \u{1F4CA} Metrics Success rates, durations, node breakdown
2153
- \u{1F6E0}\uFE0F Process Health PID files, systemd, workers, orphans
2154
- \u26A0\uFE0F Errors Failed and hung nodes with metadata
2322
+ Examples:
2323
+ agentflow-dashboard --traces ./traces --host 0.0.0.0
2324
+ agentflow-dashboard --traces ./traces --config ./agentflow.config.json
2325
+ agentflow-dashboard -p 8080 -t /var/log/agentflow --cors
2155
2326
  `);
2156
2327
  }
2157
2328
 
@@ -2174,12 +2345,15 @@ function serializeTrace(trace) {
2174
2345
  var DashboardServer = class {
2175
2346
  constructor(config) {
2176
2347
  this.config = config;
2177
- const home = process.env.HOME ?? "/home/trader";
2178
- const configPath = path3.join(home, ".agentflow/dashboard-config.json");
2348
+ const { config: userCfg, configPath: cfgPath } = loadConfig(config.configPath);
2349
+ this.userConfig = userCfg;
2350
+ this.configPath = cfgPath;
2351
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
2352
+ const dashConfigPath = path3.join(home, ".agentflow/dashboard-config.json");
2179
2353
  if (!config.dataDirs) config.dataDirs = [];
2180
2354
  try {
2181
- if (fs3.existsSync(configPath)) {
2182
- const saved = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
2355
+ if (fs3.existsSync(dashConfigPath)) {
2356
+ const saved = JSON.parse(fs3.readFileSync(dashConfigPath, "utf-8"));
2183
2357
  const extraDirs = saved.extraDirs ?? [];
2184
2358
  for (const d of extraDirs) {
2185
2359
  if (!config.dataDirs.includes(d)) config.dataDirs.push(d);
@@ -2187,21 +2361,15 @@ var DashboardServer = class {
2187
2361
  }
2188
2362
  } catch {
2189
2363
  }
2190
- const autoDiscoverPaths = [
2191
- path3.join(home, ".openclaw/cron/runs"),
2192
- path3.join(home, ".openclaw/workspace/traces"),
2193
- path3.join(home, ".openclaw/subagents"),
2194
- path3.join(home, ".openclaw/agents/main/sessions"),
2195
- path3.join(home, ".agentflow/traces")
2196
- ];
2197
- for (const p of autoDiscoverPaths) {
2364
+ for (const p of getDiscoveryPaths(this.userConfig)) {
2198
2365
  if (fs3.existsSync(p) && !config.dataDirs.includes(p)) {
2199
2366
  config.dataDirs.push(p);
2200
2367
  }
2201
2368
  }
2202
2369
  this.watcher = new TraceWatcher({
2203
2370
  tracesDir: config.tracesDir,
2204
- dataDirs: config.dataDirs
2371
+ dataDirs: config.dataDirs,
2372
+ userConfig: this.userConfig
2205
2373
  });
2206
2374
  this.stats = new AgentStats();
2207
2375
  this.knowledgeStore = (0, import_agentflow_core3.createKnowledgeStore)({
@@ -2210,6 +2378,7 @@ var DashboardServer = class {
2210
2378
  this.setupExpress();
2211
2379
  this.setupWebSocket();
2212
2380
  this.setupTraceWatcher();
2381
+ this.setupSomaReportWatcher();
2213
2382
  let knowledgeCount = 0;
2214
2383
  for (const trace of this.watcher.getAllTraces()) {
2215
2384
  this.stats.processTrace(trace);
@@ -2236,6 +2405,8 @@ var DashboardServer = class {
2236
2405
  ts: 0
2237
2406
  };
2238
2407
  knowledgeStore;
2408
+ userConfig;
2409
+ configPath;
2239
2410
  setupExpress() {
2240
2411
  if (this.config.enableCors) {
2241
2412
  this.app.use((_req, res, next) => {
@@ -2247,18 +2418,35 @@ var DashboardServer = class {
2247
2418
  next();
2248
2419
  });
2249
2420
  }
2250
- const clientDir = path3.join(__dirname, "../dist/client");
2421
+ const pkgDir = path3.join(__dirname, "..");
2422
+ const clientDir = path3.join(pkgDir, "dist/client");
2423
+ const clientIndex = path3.join(clientDir, "index.html");
2424
+ const srcDir = path3.join(pkgDir, "src/client");
2425
+ const needsBuild = !fs3.existsSync(clientIndex) || fs3.existsSync(srcDir) && this.isClientStale(srcDir, clientDir);
2426
+ if (needsBuild) {
2427
+ try {
2428
+ console.log("Building dashboard client...");
2429
+ (0, import_node_child_process.execSync)("npm run build:client", { cwd: pkgDir, stdio: "inherit", timeout: 3e4 });
2430
+ } catch (err) {
2431
+ console.warn("Client build failed \u2014 dashboard UI may be stale:", err.message);
2432
+ }
2433
+ }
2251
2434
  if (fs3.existsSync(clientDir)) {
2252
2435
  this.app.use(import_express.default.static(clientDir));
2253
2436
  }
2254
- const publicDir = path3.join(__dirname, "../public");
2255
- if (fs3.existsSync(publicDir)) {
2256
- this.app.use("/v1", import_express.default.static(publicDir));
2257
- }
2258
- this.app.get("/api/traces", (_req, res) => {
2437
+ this.app.get("/api/traces", (req, res) => {
2259
2438
  try {
2260
- const traces = this.watcher.getAllTraces().map(serializeTrace);
2261
- res.json(traces);
2439
+ const limit = Math.min(Math.max(parseInt(req.query.limit, 10) || 50, 1), 200);
2440
+ const cursor = req.query.cursor ? parseFloat(req.query.cursor) : void 0;
2441
+ let allTraces = this.watcher.getAllTraces();
2442
+ if (cursor) {
2443
+ allTraces = allTraces.filter((t) => (t.lastModified || t.startTime) < cursor);
2444
+ }
2445
+ const page = allTraces.slice(0, limit);
2446
+ const serialized = page.map(serializeTrace);
2447
+ const lastTrace = page[page.length - 1];
2448
+ const nextCursor = page.length === limit && lastTrace ? lastTrace.lastModified || lastTrace.startTime : null;
2449
+ res.json({ traces: serialized, nextCursor });
2262
2450
  } catch (_error) {
2263
2451
  res.status(500).json({ error: "Failed to load traces" });
2264
2452
  }
@@ -2549,6 +2737,235 @@ var DashboardServer = class {
2549
2737
  res.status(500).json({ error: "Failed to load agent statistics" });
2550
2738
  }
2551
2739
  });
2740
+ this.app.get("/api/soma/tier", (_req, res) => {
2741
+ const somaVault = this.config.somaVault;
2742
+ if (!somaVault) {
2743
+ return res.json({ tier: "teaser", somaVault: false, governanceAvailable: false });
2744
+ }
2745
+ try {
2746
+ const reportPath = path3.join(somaVault, "..", "soma-report.json");
2747
+ if (!fs3.existsSync(reportPath)) {
2748
+ return res.json({ tier: "free", somaVault: true, governanceAvailable: false });
2749
+ }
2750
+ const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
2751
+ const hasGovernance = report.governance && typeof report.governance.pending === "number";
2752
+ return res.json({
2753
+ tier: hasGovernance ? "pro" : "free",
2754
+ somaVault: true,
2755
+ governanceAvailable: !!hasGovernance
2756
+ });
2757
+ } catch {
2758
+ return res.json({ tier: "free", somaVault: true, governanceAvailable: false });
2759
+ }
2760
+ });
2761
+ this.app.get("/api/soma/report", (_req, res) => {
2762
+ const somaVault = this.config.somaVault;
2763
+ if (!somaVault) {
2764
+ return res.json({ available: false, teaser: true });
2765
+ }
2766
+ try {
2767
+ const reportPath = path3.join(somaVault, "..", "soma-report.json");
2768
+ if (!fs3.existsSync(reportPath)) {
2769
+ return res.json({ available: false, teaser: false, message: "No report file yet. Run soma watch." });
2770
+ }
2771
+ const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
2772
+ res.json(report);
2773
+ } catch (error) {
2774
+ console.error("Soma report error:", error);
2775
+ res.json({ available: false, teaser: false, message: "Failed to read report" });
2776
+ }
2777
+ });
2778
+ this.app.get("/api/soma/governance", (_req, res) => {
2779
+ const somaVault = this.config.somaVault;
2780
+ if (!somaVault) {
2781
+ return res.json({ available: false });
2782
+ }
2783
+ try {
2784
+ const reportPath = path3.join(somaVault, "..", "soma-report.json");
2785
+ if (!fs3.existsSync(reportPath)) {
2786
+ return res.json({ available: false, message: "No report file. Run soma report." });
2787
+ }
2788
+ const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
2789
+ res.json({
2790
+ available: true,
2791
+ layers: report.layers ?? { archive: 0, working: 0, emerging: 0, canon: 0 },
2792
+ governance: report.governance ?? { pending: 0, promoted: 0, rejected: 0 },
2793
+ insights: (report.insights ?? []).filter((i) => i.layer === "emerging" && i.proposal_status === "pending"),
2794
+ canon: (report.insights ?? []).filter((i) => i.layer === "canon"),
2795
+ generatedAt: report.generatedAt
2796
+ });
2797
+ } catch (error) {
2798
+ console.error("Soma governance error:", error);
2799
+ res.status(500).json({ available: false, message: "Failed to read governance data" });
2800
+ }
2801
+ });
2802
+ const sanitizeArg = (s) => s.replace(/[^a-zA-Z0-9_\-.:]/g, "");
2803
+ const sanitizeReason = (s) => s.replace(/["`$\\]/g, "").slice(0, 500);
2804
+ this.app.post("/api/soma/governance/promote", (req, res) => {
2805
+ var _a;
2806
+ const somaVault = this.config.somaVault;
2807
+ if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
2808
+ const { entryId } = req.body ?? {};
2809
+ if (!entryId) return res.status(400).json({ error: "entryId required" });
2810
+ try {
2811
+ const { execSync: execSync2 } = require("child_process");
2812
+ const safeId = sanitizeArg(String(entryId));
2813
+ const result = execSync2(`npx soma governance promote ${safeId} --vault "${somaVault}"`, {
2814
+ encoding: "utf-8",
2815
+ timeout: 1e4
2816
+ });
2817
+ res.json({ success: true, message: result.trim() });
2818
+ } catch (error) {
2819
+ res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
2820
+ }
2821
+ });
2822
+ this.app.post("/api/soma/governance/reject", (req, res) => {
2823
+ var _a;
2824
+ const somaVault = this.config.somaVault;
2825
+ if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
2826
+ const { entryId, reason } = req.body ?? {};
2827
+ if (!entryId || !reason) return res.status(400).json({ error: "entryId and reason required" });
2828
+ try {
2829
+ const { execSync: execSync2 } = require("child_process");
2830
+ const safeId = sanitizeArg(String(entryId));
2831
+ const safeReason = sanitizeReason(String(reason));
2832
+ const result = execSync2(`npx soma governance reject ${safeId} "${safeReason}" --vault "${somaVault}"`, {
2833
+ encoding: "utf-8",
2834
+ timeout: 1e4
2835
+ });
2836
+ res.json({ success: true, message: result.trim() });
2837
+ } catch (error) {
2838
+ res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
2839
+ }
2840
+ });
2841
+ this.app.get("/api/soma/governance/evidence/:id", (req, res) => {
2842
+ var _a;
2843
+ const somaVault = this.config.somaVault;
2844
+ if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
2845
+ try {
2846
+ const { execSync: execSync2 } = require("child_process");
2847
+ const safeId = sanitizeArg(String(req.params.id));
2848
+ const result = execSync2(`npx soma governance show ${safeId} --vault "${somaVault}"`, {
2849
+ encoding: "utf-8",
2850
+ timeout: 1e4
2851
+ });
2852
+ res.json({ available: true, output: result.trim() });
2853
+ } catch (error) {
2854
+ res.status(404).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
2855
+ }
2856
+ });
2857
+ this.app.get("/api/soma/policies", (_req, res) => {
2858
+ const somaVault = this.config.somaVault;
2859
+ if (!somaVault) return res.json({ policies: [] });
2860
+ try {
2861
+ const reportPath = path3.join(somaVault, "..", "soma-report.json");
2862
+ if (!fs3.existsSync(reportPath)) return res.json({ policies: [] });
2863
+ const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
2864
+ res.json({ policies: report.policies ?? [] });
2865
+ } catch {
2866
+ res.json({ policies: [] });
2867
+ }
2868
+ });
2869
+ this.app.post("/api/soma/policies", import_express.default.json(), (req, res) => {
2870
+ var _a;
2871
+ const somaVault = this.config.somaVault;
2872
+ if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
2873
+ const { name, enforcement, scope, conditions } = req.body ?? {};
2874
+ if (!name) return res.status(400).json({ error: "name required" });
2875
+ try {
2876
+ const safeName = sanitizeArg(String(name));
2877
+ const safeEnf = sanitizeArg(String(enforcement || "warn"));
2878
+ const safeScope = sanitizeReason(String(scope || "all"));
2879
+ const safeCond = sanitizeReason(String(conditions || ""));
2880
+ const result = (0, import_node_child_process.execSync)(
2881
+ `npx soma policy create "${safeName}" --enforcement ${safeEnf} --scope "${safeScope}" --conditions "${safeCond}" --vault "${somaVault}"`,
2882
+ { encoding: "utf-8", timeout: 1e4 }
2883
+ );
2884
+ res.json({ success: true, message: result.trim() });
2885
+ } catch (error) {
2886
+ res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
2887
+ }
2888
+ });
2889
+ this.app.delete("/api/soma/policies/:name", (req, res) => {
2890
+ var _a;
2891
+ const somaVault = this.config.somaVault;
2892
+ if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
2893
+ try {
2894
+ const safeName = sanitizeArg(String(req.params.name));
2895
+ const result = (0, import_node_child_process.execSync)(
2896
+ `npx soma policy delete "${safeName}" --vault "${somaVault}"`,
2897
+ { encoding: "utf-8", timeout: 1e4 }
2898
+ );
2899
+ res.json({ success: true, message: result.trim() });
2900
+ } catch (error) {
2901
+ res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
2902
+ }
2903
+ });
2904
+ this.app.get("/api/soma/vault/entities", (req, res) => {
2905
+ const somaVault = this.config.somaVault;
2906
+ if (!somaVault) return res.json({ entities: [], total: 0 });
2907
+ try {
2908
+ const reportPath = path3.join(somaVault, "..", "soma-report.json");
2909
+ if (!fs3.existsSync(reportPath)) return res.json({ entities: [], total: 0 });
2910
+ const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
2911
+ let entities = [
2912
+ ...(report.agents ?? []).map((a) => ({ ...a, type: "agent", id: a.name })),
2913
+ ...(report.insights ?? []).map((i, idx) => {
2914
+ var _a;
2915
+ return { ...i, type: i.type || "insight", id: ((_a = i.title) == null ? void 0 : _a.replace(/\s+/g, "-").toLowerCase()) || `insight-${idx}` };
2916
+ }),
2917
+ ...(report.policies ?? []).map((p) => ({ ...p, type: "policy", id: p.name }))
2918
+ ];
2919
+ const { type, layer, q, limit: limitStr, offset: offsetStr } = req.query;
2920
+ if (type) entities = entities.filter((e) => e.type === type);
2921
+ if (layer) entities = entities.filter((e) => e.layer === layer);
2922
+ if (q) {
2923
+ const lq = q.toLowerCase();
2924
+ entities = entities.filter((e) => (e.name || e.title || "").toLowerCase().includes(lq) || (e.claim || e.body || "").toLowerCase().includes(lq));
2925
+ }
2926
+ const total = entities.length;
2927
+ const offset = parseInt(offsetStr || "0", 10);
2928
+ const limit = Math.min(parseInt(limitStr || "50", 10), 200);
2929
+ entities = entities.slice(offset, offset + limit);
2930
+ res.json({ entities, total });
2931
+ } catch (error) {
2932
+ console.error("Vault entities error:", error);
2933
+ res.json({ entities: [], total: 0 });
2934
+ }
2935
+ });
2936
+ this.app.get("/api/soma/vault/entities/:type/:id", (req, res) => {
2937
+ const somaVault = this.config.somaVault;
2938
+ if (!somaVault) return res.status(404).json({ error: "Soma vault not configured" });
2939
+ try {
2940
+ const reportPath = path3.join(somaVault, "..", "soma-report.json");
2941
+ const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
2942
+ const { type, id } = req.params;
2943
+ let entity = null;
2944
+ if (type === "agent") {
2945
+ entity = (report.agents ?? []).find((a) => a.name === id);
2946
+ } else if (type === "policy") {
2947
+ entity = (report.policies ?? []).find((p) => p.name === id);
2948
+ } else {
2949
+ entity = (report.insights ?? []).find(
2950
+ (i) => {
2951
+ var _a;
2952
+ return (((_a = i.title) == null ? void 0 : _a.replace(/\s+/g, "-").toLowerCase()) || "") === id || i.title === id;
2953
+ }
2954
+ );
2955
+ }
2956
+ if (!entity) return res.status(404).json({ error: "Entity not found" });
2957
+ res.json({
2958
+ ...entity,
2959
+ type,
2960
+ id,
2961
+ body: entity.claim || entity.conditions || "",
2962
+ tags: entity.tags ?? [],
2963
+ related: entity.related ?? []
2964
+ });
2965
+ } catch {
2966
+ res.status(404).json({ error: "Entity not found" });
2967
+ }
2968
+ });
2552
2969
  this.app.get("/api/process-health", (_req, res) => {
2553
2970
  var _a, _b;
2554
2971
  try {
@@ -2561,7 +2978,14 @@ var DashboardServer = class {
2561
2978
  path3.dirname(this.config.tracesDir),
2562
2979
  ...this.config.dataDirs || []
2563
2980
  ];
2564
- const configs = (0, import_agentflow_core3.discoverAllProcessConfigs)(discoveryDirs);
2981
+ let configs = (0, import_agentflow_core3.discoverAllProcessConfigs)(discoveryDirs);
2982
+ const pref = getProcessPreference(this.userConfig);
2983
+ if (pref) {
2984
+ const hasPreferred = configs.some((c) => c.processName === pref.prefer);
2985
+ if (hasPreferred) {
2986
+ configs = configs.filter((c) => c.processName !== pref.over);
2987
+ }
2988
+ }
2565
2989
  if (configs.length === 0) {
2566
2990
  return res.json(null);
2567
2991
  }
@@ -2645,35 +3069,32 @@ var DashboardServer = class {
2645
3069
  }
2646
3070
  } catch {
2647
3071
  }
2648
- const watched = [
3072
+ const watched = [...new Set([
2649
3073
  this.config.tracesDir,
2650
3074
  ...this.config.dataDirs || [],
2651
3075
  ...extraDirs
2652
- ];
3076
+ ].map((w) => path3.resolve(w)))];
2653
3077
  const discovered = [];
2654
- try {
2655
- const { execSync } = require("child_process");
2656
- const raw = execSync(
2657
- "systemctl --user show --property=ExecStart --no-pager alfred.service openclaw-gateway.service 2>/dev/null",
2658
- { encoding: "utf8", timeout: 5e3 }
2659
- );
2660
- for (const line of raw.split("\n")) {
2661
- const match = line.match(/path=([^\s;]+)/);
2662
- if (match == null ? void 0 : match[1]) {
2663
- const dir = path3.dirname(match[1]);
2664
- if (fs3.existsSync(dir)) discovered.push(dir);
3078
+ const svcNames = getSystemdServices(this.userConfig);
3079
+ if (svcNames.length > 0) {
3080
+ try {
3081
+ const { execSync: execSync2 } = require("child_process");
3082
+ const raw = execSync2(
3083
+ `systemctl --user show --property=ExecStart --no-pager ${svcNames.join(" ")} 2>/dev/null`,
3084
+ { encoding: "utf8", timeout: 5e3 }
3085
+ );
3086
+ for (const line of raw.split("\n")) {
3087
+ const match = line.match(/path=([^\s;]+)/);
3088
+ if (match == null ? void 0 : match[1]) {
3089
+ const dir = path3.dirname(match[1]);
3090
+ if (fs3.existsSync(dir)) discovered.push(dir);
3091
+ }
2665
3092
  }
3093
+ } catch {
2666
3094
  }
2667
- } catch {
2668
3095
  }
2669
3096
  const commonPaths = [
2670
- path3.join(home, ".alfred/traces"),
2671
- path3.join(home, ".alfred/data"),
2672
- path3.join(home, ".openclaw/workspace/traces"),
2673
- path3.join(home, ".openclaw/subagents"),
2674
- path3.join(home, ".openclaw/cron/runs"),
2675
- path3.join(home, ".openclaw/cron"),
2676
- path3.join(home, ".openclaw/agents/main/sessions"),
3097
+ ...getDiscoveryPaths(this.userConfig),
2677
3098
  path3.join(home, ".agentflow/traces")
2678
3099
  ];
2679
3100
  for (const p of commonPaths) {
@@ -2777,18 +3198,10 @@ var DashboardServer = class {
2777
3198
  this.app.get("/ready", (_req, res) => {
2778
3199
  res.json({ status: "ready" });
2779
3200
  });
2780
- this.app.get("/v1/*", (_req, res) => {
2781
- const legacyIndex = path3.join(__dirname, "../public/index.html");
2782
- if (fs3.existsSync(legacyIndex)) {
2783
- res.sendFile(legacyIndex);
2784
- } else {
2785
- res.status(404).send("Legacy dashboard not found");
2786
- }
2787
- });
2788
3201
  this.app.get("*", (_req, res) => {
2789
- const clientIndex = path3.join(__dirname, "../dist/client/index.html");
2790
- if (fs3.existsSync(clientIndex)) {
2791
- res.sendFile(clientIndex);
3202
+ const clientIndex2 = path3.join(__dirname, "../dist/client/index.html");
3203
+ if (fs3.existsSync(clientIndex2)) {
3204
+ res.sendFile(clientIndex2);
2792
3205
  } else {
2793
3206
  res.status(404).send("Dashboard not found - public files may not be built");
2794
3207
  }
@@ -2814,6 +3227,41 @@ var DashboardServer = class {
2814
3227
  });
2815
3228
  });
2816
3229
  }
3230
+ /** Watch soma-report.json for changes and broadcast updates via WebSocket. */
3231
+ setupSomaReportWatcher() {
3232
+ const somaVault = this.config.somaVault;
3233
+ if (!somaVault) return;
3234
+ const reportPath = path3.join(somaVault, "..", "soma-report.json");
3235
+ const reportDir = path3.dirname(reportPath);
3236
+ if (!fs3.existsSync(reportDir)) return;
3237
+ let debounceTimer = null;
3238
+ const watcher = import_chokidar2.default.watch(reportPath, {
3239
+ ignoreInitial: true,
3240
+ persistent: true,
3241
+ awaitWriteFinish: { stabilityThreshold: 500 }
3242
+ });
3243
+ watcher.on("change", () => {
3244
+ if (debounceTimer) clearTimeout(debounceTimer);
3245
+ debounceTimer = setTimeout(() => {
3246
+ var _a, _b;
3247
+ try {
3248
+ const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
3249
+ this.broadcast({ type: "soma-report-updated", data: report });
3250
+ if (report.generatedAt) {
3251
+ this.broadcast({
3252
+ type: "soma-activity",
3253
+ data: {
3254
+ action: "report-updated",
3255
+ description: `Report updated: ${((_a = report.totals) == null ? void 0 : _a.agents) ?? 0} agents, ${((_b = report.totals) == null ? void 0 : _b.insights) ?? 0} insights`,
3256
+ timestamp: report.generatedAt
3257
+ }
3258
+ });
3259
+ }
3260
+ } catch {
3261
+ }
3262
+ }, 500);
3263
+ });
3264
+ }
2817
3265
  /**
2818
3266
  * Filter an agent's traces to valid ExecutionGraphs and convert via loadGraph().
2819
3267
  * Returns only traces with proper nodes (Map or non-empty object), skipping session-only traces.
@@ -3035,24 +3483,49 @@ var DashboardServer = class {
3035
3483
  });
3036
3484
  }
3037
3485
  async start() {
3038
- return new Promise((resolve4) => {
3486
+ return new Promise((resolve5) => {
3039
3487
  const host = this.config.host || "localhost";
3040
3488
  this.server.listen(this.config.port, host, () => {
3041
3489
  console.log(`AgentFlow Dashboard running at http://${host}:${this.config.port}`);
3042
3490
  console.log(`Watching traces in: ${this.config.tracesDir}`);
3043
- resolve4();
3491
+ resolve5();
3044
3492
  });
3045
3493
  });
3046
3494
  }
3495
+ /** Check if any src/client file is newer than the built bundle. */
3496
+ isClientStale(srcDir, distDir) {
3497
+ try {
3498
+ const distIndex = path3.join(distDir, "index.html");
3499
+ if (!fs3.existsSync(distIndex)) return true;
3500
+ const distMtime = fs3.statSync(distIndex).mtimeMs;
3501
+ const check = (dir) => {
3502
+ for (const entry of fs3.readdirSync(dir, { withFileTypes: true })) {
3503
+ const full = path3.join(dir, entry.name);
3504
+ if (entry.isDirectory()) {
3505
+ if (check(full)) return true;
3506
+ } else if (fs3.statSync(full).mtimeMs > distMtime) {
3507
+ return true;
3508
+ }
3509
+ }
3510
+ return false;
3511
+ };
3512
+ return check(srcDir);
3513
+ } catch {
3514
+ return false;
3515
+ }
3516
+ }
3047
3517
  async stop() {
3048
- return new Promise((resolve4) => {
3518
+ return new Promise((resolve5) => {
3049
3519
  this.watcher.stop();
3050
3520
  this.server.close(() => {
3051
3521
  console.log("Dashboard server stopped");
3052
- resolve4();
3522
+ resolve5();
3053
3523
  });
3054
3524
  });
3055
3525
  }
3526
+ getConfigPath() {
3527
+ return this.configPath;
3528
+ }
3056
3529
  getStats() {
3057
3530
  return this.stats.getGlobalStats();
3058
3531
  }