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/cli.cjs CHANGED
@@ -37,11 +37,86 @@ var os = __toESM(require("os"), 1);
37
37
  var path3 = __toESM(require("path"), 1);
38
38
 
39
39
  // src/server.ts
40
+ var import_node_child_process = require("child_process");
40
41
  var fs2 = __toESM(require("fs"), 1);
41
42
  var import_node_http = require("http");
42
43
  var path2 = __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");
119
+ var import_chokidar2 = __toESM(require("chokidar"), 1);
45
120
  var import_express = __toESM(require("express"), 1);
46
121
  var import_ws = require("ws");
47
122
 
@@ -74,17 +149,17 @@ var AgentFlowAdapter = class {
74
149
  };
75
150
 
76
151
  // src/adapters/openclaw.ts
77
- var import_node_fs = require("fs");
78
- var import_node_path = require("path");
152
+ var import_node_fs2 = require("fs");
153
+ var import_node_path2 = require("path");
79
154
  var jobCache = /* @__PURE__ */ new Map();
80
155
  function loadJobs(openclawDir) {
81
156
  const cached = jobCache.get(openclawDir);
82
157
  if (cached) return cached;
83
- const jobsPath = (0, import_node_path.join)(openclawDir, "cron", "jobs.json");
158
+ const jobsPath = (0, import_node_path2.join)(openclawDir, "cron", "jobs.json");
84
159
  const map = /* @__PURE__ */ new Map();
85
160
  try {
86
- if ((0, import_node_fs.existsSync)(jobsPath)) {
87
- const data = JSON.parse((0, import_node_fs.readFileSync)(jobsPath, "utf-8"));
161
+ if ((0, import_node_fs2.existsSync)(jobsPath)) {
162
+ const data = JSON.parse((0, import_node_fs2.readFileSync)(jobsPath, "utf-8"));
88
163
  const jobs = Array.isArray(data) ? data : data.jobs ?? [];
89
164
  for (const job of jobs) {
90
165
  if (job.id) map.set(job.id, job);
@@ -96,19 +171,19 @@ function loadJobs(openclawDir) {
96
171
  return map;
97
172
  }
98
173
  function findOpenClawRoot(filePath) {
99
- let dir = (0, import_node_path.dirname)(filePath);
174
+ let dir = (0, import_node_path2.dirname)(filePath);
100
175
  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") {
176
+ if ((0, import_node_fs2.existsSync)((0, import_node_path2.join)(dir, "cron", "jobs.json")) || (0, import_node_path2.basename)(dir) === ".openclaw") {
102
177
  return dir;
103
178
  }
104
- dir = (0, import_node_path.dirname)(dir);
179
+ dir = (0, import_node_path2.dirname)(dir);
105
180
  }
106
181
  return null;
107
182
  }
108
183
  var OpenClawAdapter = class {
109
184
  name = "openclaw";
110
185
  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"));
186
+ 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
187
  }
113
188
  canHandle(filePath) {
114
189
  if (!filePath.endsWith(".jsonl")) return false;
@@ -117,7 +192,7 @@ var OpenClawAdapter = class {
117
192
  parse(filePath) {
118
193
  const traces = [];
119
194
  try {
120
- const content = (0, import_node_fs.readFileSync)(filePath, "utf-8");
195
+ const content = (0, import_node_fs2.readFileSync)(filePath, "utf-8");
121
196
  const root = findOpenClawRoot(filePath);
122
197
  const jobs = root ? loadJobs(root) : /* @__PURE__ */ new Map();
123
198
  for (const line of content.split("\n")) {
@@ -129,7 +204,7 @@ var OpenClawAdapter = class {
129
204
  continue;
130
205
  }
131
206
  if (entry.action !== "finished") continue;
132
- const jobId = entry.jobId ?? (0, import_node_path.basename)(filePath, ".jsonl");
207
+ const jobId = entry.jobId ?? (0, import_node_path2.basename)(filePath, ".jsonl");
133
208
  const job = jobs.get(jobId);
134
209
  const jobName = (job == null ? void 0 : job.name) ?? jobId;
135
210
  const startTime = entry.runAtMs ?? entry.ts;
@@ -181,8 +256,8 @@ var OpenClawAdapter = class {
181
256
  };
182
257
 
183
258
  // src/adapters/otel.ts
184
- var import_node_fs2 = require("fs");
185
- var import_node_path2 = require("path");
259
+ var import_node_fs3 = require("fs");
260
+ var import_node_path3 = require("path");
186
261
  var SPAN_TYPE_MAP = {
187
262
  "gen_ai.chat": "llm",
188
263
  "gen_ai.completion": "llm",
@@ -289,8 +364,8 @@ var OTelAdapter = class {
289
364
  name = "otel";
290
365
  detect(dirPath) {
291
366
  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);
367
+ if ((0, import_node_fs3.existsSync)((0, import_node_path3.join)(dirPath, "otel-traces"))) return true;
368
+ const files = (0, import_node_fs3.readdirSync)(dirPath);
294
369
  return files.some((f) => f.endsWith(".otlp.json"));
295
370
  } catch {
296
371
  return false;
@@ -301,7 +376,7 @@ var OTelAdapter = class {
301
376
  }
302
377
  parse(filePath) {
303
378
  try {
304
- const content = (0, import_node_fs2.readFileSync)(filePath, "utf-8");
379
+ const content = (0, import_node_fs3.readFileSync)(filePath, "utf-8");
305
380
  const payload = JSON.parse(content);
306
381
  const traces = parseOtlpPayload(payload);
307
382
  for (const t of traces) t.filePath = filePath;
@@ -343,9 +418,7 @@ function extractSource(agentId) {
343
418
  const colonIdx = agentId.indexOf(":");
344
419
  if (colonIdx > 0 && colonIdx < 20) {
345
420
  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
- }
421
+ return { source: prefix, localId: agentId.slice(colonIdx + 1) };
349
422
  }
350
423
  return { source: "agentflow", localId: agentId };
351
424
  }
@@ -376,16 +449,20 @@ function deduplicateAgents(agents) {
376
449
  for (const a of tagged) {
377
450
  const suffix = extractSuffix(a.localId);
378
451
  if (!suffix) continue;
379
- const group = suffixGroups.get(suffix) ?? [];
452
+ const key = `${a.source}:${suffix}`;
453
+ const group = suffixGroups.get(key) ?? [];
380
454
  group.push(a);
381
- suffixGroups.set(suffix, group);
455
+ suffixGroups.set(key, group);
382
456
  }
383
457
  const mergedIds = /* @__PURE__ */ new Set();
384
458
  const mergedAgents = [];
385
- for (const [suffix, group] of suffixGroups) {
459
+ for (const [_key, group] of suffixGroups) {
460
+ const suffix = extractSuffix(group[0].localId);
386
461
  if (group.length < 2) continue;
387
462
  const prefixes = new Set(group.map((a) => a.localId.split("-")[0]));
388
463
  if (prefixes.size < 2) continue;
464
+ const longPrefixes = [...prefixes].filter((p) => p !== suffix && p.length > 2);
465
+ if (longPrefixes.length >= 2) continue;
389
466
  const merged = {
390
467
  agentId: group[0].source === "agentflow" ? suffix : `${group[0].source}:${suffix}`,
391
468
  displayName: suffix,
@@ -433,10 +510,7 @@ function groupAgents(agents) {
433
510
  }
434
511
  const SOURCE_DISPLAY = {
435
512
  agentflow: "AgentFlow",
436
- openclaw: "OpenClaw",
437
- otel: "OpenTelemetry",
438
- langchain: "LangChain",
439
- crewai: "CrewAI"
513
+ otel: "OpenTelemetry"
440
514
  };
441
515
  const groups = [];
442
516
  for (const [source, sourceAgents] of sourceMap) {
@@ -782,10 +856,6 @@ function getUniversalNodeStatus(activity) {
782
856
  return "completed";
783
857
  }
784
858
  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
859
  const firstSegment = sessionId.split("-")[0];
790
860
  if (firstSegment) return firstSegment;
791
861
  return "openclaw";
@@ -798,19 +868,81 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
798
868
  tracesDir;
799
869
  dataDirs;
800
870
  allWatchDirs;
871
+ maxAgeMs;
872
+ userConfig;
801
873
  constructor(tracesDirOrOptions) {
802
874
  super();
875
+ const defaultMaxAgeMs = 48 * 60 * 60 * 1e3;
876
+ const envHours = process.env.AGENTFLOW_TRACE_WINDOW_HOURS;
877
+ const envMaxAgeMs = envHours ? parseFloat(envHours) * 60 * 60 * 1e3 : void 0;
803
878
  if (typeof tracesDirOrOptions === "string") {
804
879
  this.tracesDir = path.resolve(tracesDirOrOptions);
805
880
  this.dataDirs = [];
881
+ this.maxAgeMs = envMaxAgeMs ?? defaultMaxAgeMs;
882
+ this.userConfig = {};
806
883
  } else {
807
884
  this.tracesDir = path.resolve(tracesDirOrOptions.tracesDir);
808
885
  this.dataDirs = (tracesDirOrOptions.dataDirs || []).map((d) => path.resolve(d));
809
- }
810
- this.allWatchDirs = [this.tracesDir, ...this.dataDirs];
886
+ this.maxAgeMs = envMaxAgeMs ?? tracesDirOrOptions.maxAgeMs ?? defaultMaxAgeMs;
887
+ this.userConfig = tracesDirOrOptions.userConfig ?? {};
888
+ }
889
+ this.skipFiles = /* @__PURE__ */ new Set([
890
+ ..._TraceWatcher.STRUCTURAL_SKIP_FILES,
891
+ ...getSkipFiles(this.userConfig)
892
+ ]);
893
+ this.userSkipDirs = new Set(getSkipDirectories(this.userConfig));
894
+ this.allWatchDirs = [...new Set([this.tracesDir, ...this.dataDirs].map((d) => path.resolve(d)))];
811
895
  this.ensureTracesDir();
812
896
  this.loadExistingFiles();
897
+ this.archiveOldTraces();
813
898
  this.startWatching();
899
+ setInterval(() => this.archiveOldTraces(), 6 * 60 * 60 * 1e3);
900
+ }
901
+ /** Move trace files older than maxAgeMs into archive/YYYY-MM/ subdirectories. */
902
+ archiveOldTraces() {
903
+ const cutoff = Date.now() - this.maxAgeMs;
904
+ let archived = 0;
905
+ for (const dir of this.allWatchDirs) {
906
+ if (!fs.existsSync(dir)) continue;
907
+ try {
908
+ this.archiveDirectory(dir, cutoff, 0);
909
+ } catch (error) {
910
+ console.warn(`Archival error in ${dir}:`, error.message);
911
+ }
912
+ }
913
+ }
914
+ archiveDirectory(dir, cutoff, depth) {
915
+ if (depth > 10) return 0;
916
+ if (path.basename(dir) === "archive") return 0;
917
+ let archived = 0;
918
+ try {
919
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
920
+ for (const entry of entries) {
921
+ if (entry.name.startsWith(".") || entry.name === "archive" || this.userSkipDirs.has(entry.name)) continue;
922
+ const fullPath = path.join(dir, entry.name);
923
+ if (entry.isDirectory()) {
924
+ archived += this.archiveDirectory(fullPath, cutoff, depth + 1);
925
+ continue;
926
+ }
927
+ if (!entry.isFile() || !this.isSupportedFile(entry.name)) continue;
928
+ try {
929
+ const stats = fs.statSync(fullPath);
930
+ if (stats.mtimeMs >= cutoff) continue;
931
+ const mtime = new Date(stats.mtimeMs);
932
+ const yearMonth = `${mtime.getFullYear()}-${String(mtime.getMonth() + 1).padStart(2, "0")}`;
933
+ const archiveDir = path.join(this.tracesDir, "archive", yearMonth);
934
+ fs.mkdirSync(archiveDir, { recursive: true });
935
+ const dest = path.join(archiveDir, entry.name);
936
+ fs.renameSync(fullPath, dest);
937
+ const key = this.traceKey(fullPath);
938
+ this.traces.delete(key);
939
+ archived++;
940
+ } catch {
941
+ }
942
+ }
943
+ } catch {
944
+ }
945
+ return archived;
814
946
  }
815
947
  ensureTracesDir() {
816
948
  if (!fs.existsSync(this.tracesDir)) {
@@ -843,9 +975,17 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
843
975
  const entries = fs.readdirSync(dir, { withFileTypes: true });
844
976
  for (const entry of entries) {
845
977
  if (entry.name.startsWith(".")) continue;
978
+ if (entry.name === "archive") continue;
979
+ if (this.userSkipDirs.has(entry.name)) continue;
846
980
  const fullPath = path.join(dir, entry.name);
847
981
  if (entry.isFile()) {
848
982
  if (this.isSupportedFile(entry.name)) {
983
+ try {
984
+ const mtime = fs.statSync(fullPath).mtimeMs;
985
+ if (Date.now() - mtime > this.maxAgeMs) continue;
986
+ } catch {
987
+ continue;
988
+ }
849
989
  if (this.loadFile(fullPath)) {
850
990
  fileCount++;
851
991
  }
@@ -863,8 +1003,8 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
863
1003
  isSupportedFile(filename) {
864
1004
  return filename.endsWith(".json") || filename.endsWith(".jsonl") || filename.endsWith(".log") || filename.endsWith(".trace");
865
1005
  }
866
- /** File names that are config/state, not tracesskip them. */
867
- static SKIP_FILES = /* @__PURE__ */ new Set([
1006
+ /** Structural file names that are never trace dataalways skipped. */
1007
+ static STRUCTURAL_SKIP_FILES = /* @__PURE__ */ new Set([
868
1008
  "workers.json",
869
1009
  "package.json",
870
1010
  "package-lock.json",
@@ -879,6 +1019,10 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
879
1019
  "update-check.json",
880
1020
  "exec-approvals.json"
881
1021
  ]);
1022
+ /** Skip files = structural + user config */
1023
+ skipFiles;
1024
+ /** Skip directories from user config */
1025
+ userSkipDirs;
882
1026
  static SKIP_SUFFIXES = [
883
1027
  "-state.json",
884
1028
  "-config.json",
@@ -890,7 +1034,7 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
890
1034
  /** Load a file using the adapter registry, falling back to built-in parsing. */
891
1035
  loadFile(filePath) {
892
1036
  const filename = path.basename(filePath);
893
- if (_TraceWatcher.SKIP_FILES.has(filename)) return false;
1037
+ if (this.skipFiles.has(filename)) return false;
894
1038
  if (_TraceWatcher.SKIP_SUFFIXES.some((s) => filename.endsWith(s))) return false;
895
1039
  const adapter = findAdapter(filePath);
896
1040
  if (adapter && adapter.name !== "agentflow") {
@@ -1067,43 +1211,26 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
1067
1211
  }
1068
1212
  return traces;
1069
1213
  }
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
- };
1214
+ /** Normalise agent identifiers using config-driven alias map. */
1092
1215
  normaliseAgentId(raw) {
1093
- return _TraceWatcher.AGENT_ALIASES[raw] ?? raw;
1216
+ const aliases = getAliases(this.userConfig);
1217
+ return aliases[raw] ?? raw;
1094
1218
  }
1095
1219
  detectAgentIdentifier(activity, _filename, filePath) {
1096
1220
  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);
1221
+ return this.normaliseAgentId(activity.agent_id);
1100
1222
  }
1101
1223
  const pathAgent = this.extractAgentFromPath(filePath);
1102
- if (filePath.includes(".alfred/") && !pathAgent.startsWith("alfred-")) {
1224
+ const detection = getAgentDetection(this.userConfig);
1225
+ if (detection.filePatterns) {
1103
1226
  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);
1227
+ for (const [pattern, template] of Object.entries(detection.filePatterns)) {
1228
+ const re = new RegExp(`^(${pattern})$`);
1229
+ const match = basename3.match(re);
1230
+ if (match) {
1231
+ const resolved = template.replace("${match}", match[1]);
1232
+ return this.normaliseAgentId(resolved);
1233
+ }
1107
1234
  }
1108
1235
  }
1109
1236
  return this.normaliseAgentId(pathAgent);
@@ -1111,20 +1238,23 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
1111
1238
  extractAgentFromPath(filePath) {
1112
1239
  const filename = path.basename(filePath, path.extname(filePath));
1113
1240
  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";
1241
+ const detection = getAgentDetection(this.userConfig);
1242
+ let pathPrefix = "";
1243
+ if (detection.pathPatterns) {
1244
+ for (const [pathSubstring, agentId] of Object.entries(detection.pathPatterns)) {
1245
+ if (filePath.includes(pathSubstring)) {
1246
+ pathPrefix = agentId;
1247
+ break;
1248
+ }
1121
1249
  }
1122
- return "openclaw";
1123
1250
  }
1124
- if (filePath.includes(".alfred/") || filename.includes("alfred")) {
1125
- return "alfred";
1251
+ const agentsIndex = pathParts.lastIndexOf("agents");
1252
+ if (agentsIndex !== -1 && agentsIndex + 1 < pathParts.length) {
1253
+ const agentName = pathParts[agentsIndex + 1];
1254
+ return pathPrefix ? `${pathPrefix}-${agentName}` : agentName;
1126
1255
  }
1127
- for (const part of pathParts.reverse()) {
1256
+ if (pathPrefix) return pathPrefix;
1257
+ for (const part of [...pathParts].reverse()) {
1128
1258
  if (part.match(/agent|worker|service|daemon|bot|ai|llm/i)) {
1129
1259
  return part;
1130
1260
  }
@@ -1459,19 +1589,22 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
1459
1589
  const parentDir = path.basename(path.dirname(filePath));
1460
1590
  const grandParentDir = path.basename(path.dirname(path.dirname(filePath)));
1461
1591
  const greatGrandParentDir = path.basename(path.dirname(path.dirname(path.dirname(filePath))));
1462
- let agentId;
1592
+ let agentName;
1463
1593
  if (parentDir === "sessions" && greatGrandParentDir === "agents") {
1464
- agentId = grandParentDir;
1594
+ agentName = grandParentDir;
1465
1595
  } else if (grandParentDir === "agents") {
1466
- agentId = parentDir;
1467
- } else if (parentDir === "runs" && grandParentDir === "cron") {
1468
- agentId = "openclaw-cron";
1596
+ agentName = parentDir;
1469
1597
  } else {
1470
- agentId = parentDir;
1471
- }
1472
- if (filePath.includes(".alfred/") || filePath.includes("alfred")) {
1473
- if (!agentId.startsWith("alfred-")) {
1474
- agentId = `alfred-${agentId}`;
1598
+ agentName = parentDir;
1599
+ }
1600
+ let agentId = agentName;
1601
+ const detection = getAgentDetection(this.userConfig);
1602
+ if (detection.pathPatterns) {
1603
+ for (const [pathSubstring, prefix] of Object.entries(detection.pathPatterns)) {
1604
+ if (filePath.includes(pathSubstring)) {
1605
+ agentId = `${prefix}-${agentName}`;
1606
+ break;
1607
+ }
1475
1608
  }
1476
1609
  }
1477
1610
  const modelEvent = rawEvents.find((e) => e.type === "model_change");
@@ -1760,6 +1893,7 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
1760
1893
  edges: [],
1761
1894
  events: [],
1762
1895
  startTime,
1896
+ status,
1763
1897
  agentId,
1764
1898
  trigger,
1765
1899
  name: rootName,
@@ -1880,8 +2014,12 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
1880
2014
  // Ignore git directories
1881
2015
  /\.vscode/,
1882
2016
  // Ignore vscode
1883
- /\.idea/
2017
+ /\.idea/,
1884
2018
  // Ignore idea
2019
+ /\/archive\//,
2020
+ // Ignore archived trace files
2021
+ // Ignore user-configured skip directories
2022
+ ...getSkipDirectories(this.userConfig).map((d) => new RegExp(`/${d}/`))
1885
2023
  ],
1886
2024
  persistent: true,
1887
2025
  ignoreInitial: true,
@@ -1935,29 +2073,42 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
1935
2073
  });
1936
2074
  }
1937
2075
  getTrace(filename) {
2076
+ const candidates = [];
1938
2077
  const exact = this.traces.get(filename);
1939
- if (exact) return exact;
2078
+ if (exact) candidates.push(exact);
1940
2079
  if (filename.includes("::")) {
1941
2080
  const [fname, startTimeStr] = filename.split("::");
1942
2081
  const startTime = Number(startTimeStr);
1943
2082
  if (fname && !Number.isNaN(startTime)) {
1944
2083
  for (const trace of this.traces.values()) {
1945
2084
  if (trace.filename === fname && trace.startTime === startTime) {
1946
- return trace;
2085
+ candidates.push(trace);
1947
2086
  }
1948
2087
  }
1949
2088
  }
1950
2089
  }
1951
2090
  for (const prefix of ["openclaw:", "otel:", ""]) {
1952
2091
  const prefixed = this.traces.get(prefix + filename);
1953
- if (prefixed) return prefixed;
2092
+ if (prefixed) candidates.push(prefixed);
1954
2093
  }
1955
2094
  for (const [key, trace] of this.traces) {
1956
2095
  if (trace.filename === filename || trace.id === filename || key.endsWith(filename)) {
1957
- return trace;
2096
+ candidates.push(trace);
1958
2097
  }
1959
2098
  }
1960
- return void 0;
2099
+ if (candidates.length === 0) return void 0;
2100
+ if (candidates.length === 1) return candidates[0];
2101
+ let best = candidates[0];
2102
+ let bestNodeCount = best.nodes instanceof Map ? best.nodes.size : Object.keys(best.nodes ?? {}).length;
2103
+ for (let i = 1; i < candidates.length; i++) {
2104
+ const c = candidates[i];
2105
+ const nc = c.nodes instanceof Map ? c.nodes.size : Object.keys(c.nodes ?? {}).length;
2106
+ if (nc > bestNodeCount) {
2107
+ best = c;
2108
+ bestNodeCount = nc;
2109
+ }
2110
+ }
2111
+ return best;
1961
2112
  }
1962
2113
  getTracesByAgent(agentId) {
1963
2114
  return this.getAllTraces().filter((trace) => trace.agentId === agentId);
@@ -2019,12 +2170,15 @@ function serializeTrace(trace) {
2019
2170
  var DashboardServer = class {
2020
2171
  constructor(config) {
2021
2172
  this.config = config;
2022
- const home = process.env.HOME ?? "/home/trader";
2023
- const configPath = path2.join(home, ".agentflow/dashboard-config.json");
2173
+ const { config: userCfg, configPath: cfgPath } = loadConfig(config.configPath);
2174
+ this.userConfig = userCfg;
2175
+ this.configPath = cfgPath;
2176
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
2177
+ const dashConfigPath = path2.join(home, ".agentflow/dashboard-config.json");
2024
2178
  if (!config.dataDirs) config.dataDirs = [];
2025
2179
  try {
2026
- if (fs2.existsSync(configPath)) {
2027
- const saved = JSON.parse(fs2.readFileSync(configPath, "utf-8"));
2180
+ if (fs2.existsSync(dashConfigPath)) {
2181
+ const saved = JSON.parse(fs2.readFileSync(dashConfigPath, "utf-8"));
2028
2182
  const extraDirs = saved.extraDirs ?? [];
2029
2183
  for (const d of extraDirs) {
2030
2184
  if (!config.dataDirs.includes(d)) config.dataDirs.push(d);
@@ -2032,21 +2186,15 @@ var DashboardServer = class {
2032
2186
  }
2033
2187
  } catch {
2034
2188
  }
2035
- const autoDiscoverPaths = [
2036
- path2.join(home, ".openclaw/cron/runs"),
2037
- path2.join(home, ".openclaw/workspace/traces"),
2038
- path2.join(home, ".openclaw/subagents"),
2039
- path2.join(home, ".openclaw/agents/main/sessions"),
2040
- path2.join(home, ".agentflow/traces")
2041
- ];
2042
- for (const p of autoDiscoverPaths) {
2189
+ for (const p of getDiscoveryPaths(this.userConfig)) {
2043
2190
  if (fs2.existsSync(p) && !config.dataDirs.includes(p)) {
2044
2191
  config.dataDirs.push(p);
2045
2192
  }
2046
2193
  }
2047
2194
  this.watcher = new TraceWatcher({
2048
2195
  tracesDir: config.tracesDir,
2049
- dataDirs: config.dataDirs
2196
+ dataDirs: config.dataDirs,
2197
+ userConfig: this.userConfig
2050
2198
  });
2051
2199
  this.stats = new AgentStats();
2052
2200
  this.knowledgeStore = (0, import_agentflow_core3.createKnowledgeStore)({
@@ -2055,6 +2203,7 @@ var DashboardServer = class {
2055
2203
  this.setupExpress();
2056
2204
  this.setupWebSocket();
2057
2205
  this.setupTraceWatcher();
2206
+ this.setupSomaReportWatcher();
2058
2207
  let knowledgeCount = 0;
2059
2208
  for (const trace of this.watcher.getAllTraces()) {
2060
2209
  this.stats.processTrace(trace);
@@ -2081,6 +2230,8 @@ var DashboardServer = class {
2081
2230
  ts: 0
2082
2231
  };
2083
2232
  knowledgeStore;
2233
+ userConfig;
2234
+ configPath;
2084
2235
  setupExpress() {
2085
2236
  if (this.config.enableCors) {
2086
2237
  this.app.use((_req, res, next) => {
@@ -2092,18 +2243,35 @@ var DashboardServer = class {
2092
2243
  next();
2093
2244
  });
2094
2245
  }
2095
- const clientDir = path2.join(__dirname, "../dist/client");
2246
+ const pkgDir = path2.join(__dirname, "..");
2247
+ const clientDir = path2.join(pkgDir, "dist/client");
2248
+ const clientIndex = path2.join(clientDir, "index.html");
2249
+ const srcDir = path2.join(pkgDir, "src/client");
2250
+ const needsBuild = !fs2.existsSync(clientIndex) || fs2.existsSync(srcDir) && this.isClientStale(srcDir, clientDir);
2251
+ if (needsBuild) {
2252
+ try {
2253
+ console.log("Building dashboard client...");
2254
+ (0, import_node_child_process.execSync)("npm run build:client", { cwd: pkgDir, stdio: "inherit", timeout: 3e4 });
2255
+ } catch (err) {
2256
+ console.warn("Client build failed \u2014 dashboard UI may be stale:", err.message);
2257
+ }
2258
+ }
2096
2259
  if (fs2.existsSync(clientDir)) {
2097
2260
  this.app.use(import_express.default.static(clientDir));
2098
2261
  }
2099
- const publicDir = path2.join(__dirname, "../public");
2100
- if (fs2.existsSync(publicDir)) {
2101
- this.app.use("/v1", import_express.default.static(publicDir));
2102
- }
2103
- this.app.get("/api/traces", (_req, res) => {
2262
+ this.app.get("/api/traces", (req, res) => {
2104
2263
  try {
2105
- const traces = this.watcher.getAllTraces().map(serializeTrace);
2106
- res.json(traces);
2264
+ const limit = Math.min(Math.max(parseInt(req.query.limit, 10) || 50, 1), 200);
2265
+ const cursor = req.query.cursor ? parseFloat(req.query.cursor) : void 0;
2266
+ let allTraces = this.watcher.getAllTraces();
2267
+ if (cursor) {
2268
+ allTraces = allTraces.filter((t) => (t.lastModified || t.startTime) < cursor);
2269
+ }
2270
+ const page = allTraces.slice(0, limit);
2271
+ const serialized = page.map(serializeTrace);
2272
+ const lastTrace = page[page.length - 1];
2273
+ const nextCursor = page.length === limit && lastTrace ? lastTrace.lastModified || lastTrace.startTime : null;
2274
+ res.json({ traces: serialized, nextCursor });
2107
2275
  } catch (_error) {
2108
2276
  res.status(500).json({ error: "Failed to load traces" });
2109
2277
  }
@@ -2394,6 +2562,235 @@ var DashboardServer = class {
2394
2562
  res.status(500).json({ error: "Failed to load agent statistics" });
2395
2563
  }
2396
2564
  });
2565
+ this.app.get("/api/soma/tier", (_req, res) => {
2566
+ const somaVault = this.config.somaVault;
2567
+ if (!somaVault) {
2568
+ return res.json({ tier: "teaser", somaVault: false, governanceAvailable: false });
2569
+ }
2570
+ try {
2571
+ const reportPath = path2.join(somaVault, "..", "soma-report.json");
2572
+ if (!fs2.existsSync(reportPath)) {
2573
+ return res.json({ tier: "free", somaVault: true, governanceAvailable: false });
2574
+ }
2575
+ const report = JSON.parse(fs2.readFileSync(reportPath, "utf-8"));
2576
+ const hasGovernance = report.governance && typeof report.governance.pending === "number";
2577
+ return res.json({
2578
+ tier: hasGovernance ? "pro" : "free",
2579
+ somaVault: true,
2580
+ governanceAvailable: !!hasGovernance
2581
+ });
2582
+ } catch {
2583
+ return res.json({ tier: "free", somaVault: true, governanceAvailable: false });
2584
+ }
2585
+ });
2586
+ this.app.get("/api/soma/report", (_req, res) => {
2587
+ const somaVault = this.config.somaVault;
2588
+ if (!somaVault) {
2589
+ return res.json({ available: false, teaser: true });
2590
+ }
2591
+ try {
2592
+ const reportPath = path2.join(somaVault, "..", "soma-report.json");
2593
+ if (!fs2.existsSync(reportPath)) {
2594
+ return res.json({ available: false, teaser: false, message: "No report file yet. Run soma watch." });
2595
+ }
2596
+ const report = JSON.parse(fs2.readFileSync(reportPath, "utf-8"));
2597
+ res.json(report);
2598
+ } catch (error) {
2599
+ console.error("Soma report error:", error);
2600
+ res.json({ available: false, teaser: false, message: "Failed to read report" });
2601
+ }
2602
+ });
2603
+ this.app.get("/api/soma/governance", (_req, res) => {
2604
+ const somaVault = this.config.somaVault;
2605
+ if (!somaVault) {
2606
+ return res.json({ available: false });
2607
+ }
2608
+ try {
2609
+ const reportPath = path2.join(somaVault, "..", "soma-report.json");
2610
+ if (!fs2.existsSync(reportPath)) {
2611
+ return res.json({ available: false, message: "No report file. Run soma report." });
2612
+ }
2613
+ const report = JSON.parse(fs2.readFileSync(reportPath, "utf-8"));
2614
+ res.json({
2615
+ available: true,
2616
+ layers: report.layers ?? { archive: 0, working: 0, emerging: 0, canon: 0 },
2617
+ governance: report.governance ?? { pending: 0, promoted: 0, rejected: 0 },
2618
+ insights: (report.insights ?? []).filter((i) => i.layer === "emerging" && i.proposal_status === "pending"),
2619
+ canon: (report.insights ?? []).filter((i) => i.layer === "canon"),
2620
+ generatedAt: report.generatedAt
2621
+ });
2622
+ } catch (error) {
2623
+ console.error("Soma governance error:", error);
2624
+ res.status(500).json({ available: false, message: "Failed to read governance data" });
2625
+ }
2626
+ });
2627
+ const sanitizeArg = (s) => s.replace(/[^a-zA-Z0-9_\-.:]/g, "");
2628
+ const sanitizeReason = (s) => s.replace(/["`$\\]/g, "").slice(0, 500);
2629
+ this.app.post("/api/soma/governance/promote", (req, res) => {
2630
+ var _a;
2631
+ const somaVault = this.config.somaVault;
2632
+ if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
2633
+ const { entryId } = req.body ?? {};
2634
+ if (!entryId) return res.status(400).json({ error: "entryId required" });
2635
+ try {
2636
+ const { execSync: execSync2 } = require("child_process");
2637
+ const safeId = sanitizeArg(String(entryId));
2638
+ const result = execSync2(`npx soma governance promote ${safeId} --vault "${somaVault}"`, {
2639
+ encoding: "utf-8",
2640
+ timeout: 1e4
2641
+ });
2642
+ res.json({ success: true, message: result.trim() });
2643
+ } catch (error) {
2644
+ res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
2645
+ }
2646
+ });
2647
+ this.app.post("/api/soma/governance/reject", (req, res) => {
2648
+ var _a;
2649
+ const somaVault = this.config.somaVault;
2650
+ if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
2651
+ const { entryId, reason } = req.body ?? {};
2652
+ if (!entryId || !reason) return res.status(400).json({ error: "entryId and reason required" });
2653
+ try {
2654
+ const { execSync: execSync2 } = require("child_process");
2655
+ const safeId = sanitizeArg(String(entryId));
2656
+ const safeReason = sanitizeReason(String(reason));
2657
+ const result = execSync2(`npx soma governance reject ${safeId} "${safeReason}" --vault "${somaVault}"`, {
2658
+ encoding: "utf-8",
2659
+ timeout: 1e4
2660
+ });
2661
+ res.json({ success: true, message: result.trim() });
2662
+ } catch (error) {
2663
+ res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
2664
+ }
2665
+ });
2666
+ this.app.get("/api/soma/governance/evidence/:id", (req, res) => {
2667
+ var _a;
2668
+ const somaVault = this.config.somaVault;
2669
+ if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
2670
+ try {
2671
+ const { execSync: execSync2 } = require("child_process");
2672
+ const safeId = sanitizeArg(String(req.params.id));
2673
+ const result = execSync2(`npx soma governance show ${safeId} --vault "${somaVault}"`, {
2674
+ encoding: "utf-8",
2675
+ timeout: 1e4
2676
+ });
2677
+ res.json({ available: true, output: result.trim() });
2678
+ } catch (error) {
2679
+ res.status(404).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
2680
+ }
2681
+ });
2682
+ this.app.get("/api/soma/policies", (_req, res) => {
2683
+ const somaVault = this.config.somaVault;
2684
+ if (!somaVault) return res.json({ policies: [] });
2685
+ try {
2686
+ const reportPath = path2.join(somaVault, "..", "soma-report.json");
2687
+ if (!fs2.existsSync(reportPath)) return res.json({ policies: [] });
2688
+ const report = JSON.parse(fs2.readFileSync(reportPath, "utf-8"));
2689
+ res.json({ policies: report.policies ?? [] });
2690
+ } catch {
2691
+ res.json({ policies: [] });
2692
+ }
2693
+ });
2694
+ this.app.post("/api/soma/policies", import_express.default.json(), (req, res) => {
2695
+ var _a;
2696
+ const somaVault = this.config.somaVault;
2697
+ if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
2698
+ const { name, enforcement, scope, conditions } = req.body ?? {};
2699
+ if (!name) return res.status(400).json({ error: "name required" });
2700
+ try {
2701
+ const safeName = sanitizeArg(String(name));
2702
+ const safeEnf = sanitizeArg(String(enforcement || "warn"));
2703
+ const safeScope = sanitizeReason(String(scope || "all"));
2704
+ const safeCond = sanitizeReason(String(conditions || ""));
2705
+ const result = (0, import_node_child_process.execSync)(
2706
+ `npx soma policy create "${safeName}" --enforcement ${safeEnf} --scope "${safeScope}" --conditions "${safeCond}" --vault "${somaVault}"`,
2707
+ { encoding: "utf-8", timeout: 1e4 }
2708
+ );
2709
+ res.json({ success: true, message: result.trim() });
2710
+ } catch (error) {
2711
+ res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
2712
+ }
2713
+ });
2714
+ this.app.delete("/api/soma/policies/:name", (req, res) => {
2715
+ var _a;
2716
+ const somaVault = this.config.somaVault;
2717
+ if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
2718
+ try {
2719
+ const safeName = sanitizeArg(String(req.params.name));
2720
+ const result = (0, import_node_child_process.execSync)(
2721
+ `npx soma policy delete "${safeName}" --vault "${somaVault}"`,
2722
+ { encoding: "utf-8", timeout: 1e4 }
2723
+ );
2724
+ res.json({ success: true, message: result.trim() });
2725
+ } catch (error) {
2726
+ res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
2727
+ }
2728
+ });
2729
+ this.app.get("/api/soma/vault/entities", (req, res) => {
2730
+ const somaVault = this.config.somaVault;
2731
+ if (!somaVault) return res.json({ entities: [], total: 0 });
2732
+ try {
2733
+ const reportPath = path2.join(somaVault, "..", "soma-report.json");
2734
+ if (!fs2.existsSync(reportPath)) return res.json({ entities: [], total: 0 });
2735
+ const report = JSON.parse(fs2.readFileSync(reportPath, "utf-8"));
2736
+ let entities = [
2737
+ ...(report.agents ?? []).map((a) => ({ ...a, type: "agent", id: a.name })),
2738
+ ...(report.insights ?? []).map((i, idx) => {
2739
+ var _a;
2740
+ return { ...i, type: i.type || "insight", id: ((_a = i.title) == null ? void 0 : _a.replace(/\s+/g, "-").toLowerCase()) || `insight-${idx}` };
2741
+ }),
2742
+ ...(report.policies ?? []).map((p) => ({ ...p, type: "policy", id: p.name }))
2743
+ ];
2744
+ const { type, layer, q, limit: limitStr, offset: offsetStr } = req.query;
2745
+ if (type) entities = entities.filter((e) => e.type === type);
2746
+ if (layer) entities = entities.filter((e) => e.layer === layer);
2747
+ if (q) {
2748
+ const lq = q.toLowerCase();
2749
+ entities = entities.filter((e) => (e.name || e.title || "").toLowerCase().includes(lq) || (e.claim || e.body || "").toLowerCase().includes(lq));
2750
+ }
2751
+ const total = entities.length;
2752
+ const offset = parseInt(offsetStr || "0", 10);
2753
+ const limit = Math.min(parseInt(limitStr || "50", 10), 200);
2754
+ entities = entities.slice(offset, offset + limit);
2755
+ res.json({ entities, total });
2756
+ } catch (error) {
2757
+ console.error("Vault entities error:", error);
2758
+ res.json({ entities: [], total: 0 });
2759
+ }
2760
+ });
2761
+ this.app.get("/api/soma/vault/entities/:type/:id", (req, res) => {
2762
+ const somaVault = this.config.somaVault;
2763
+ if (!somaVault) return res.status(404).json({ error: "Soma vault not configured" });
2764
+ try {
2765
+ const reportPath = path2.join(somaVault, "..", "soma-report.json");
2766
+ const report = JSON.parse(fs2.readFileSync(reportPath, "utf-8"));
2767
+ const { type, id } = req.params;
2768
+ let entity = null;
2769
+ if (type === "agent") {
2770
+ entity = (report.agents ?? []).find((a) => a.name === id);
2771
+ } else if (type === "policy") {
2772
+ entity = (report.policies ?? []).find((p) => p.name === id);
2773
+ } else {
2774
+ entity = (report.insights ?? []).find(
2775
+ (i) => {
2776
+ var _a;
2777
+ return (((_a = i.title) == null ? void 0 : _a.replace(/\s+/g, "-").toLowerCase()) || "") === id || i.title === id;
2778
+ }
2779
+ );
2780
+ }
2781
+ if (!entity) return res.status(404).json({ error: "Entity not found" });
2782
+ res.json({
2783
+ ...entity,
2784
+ type,
2785
+ id,
2786
+ body: entity.claim || entity.conditions || "",
2787
+ tags: entity.tags ?? [],
2788
+ related: entity.related ?? []
2789
+ });
2790
+ } catch {
2791
+ res.status(404).json({ error: "Entity not found" });
2792
+ }
2793
+ });
2397
2794
  this.app.get("/api/process-health", (_req, res) => {
2398
2795
  var _a, _b;
2399
2796
  try {
@@ -2406,7 +2803,14 @@ var DashboardServer = class {
2406
2803
  path2.dirname(this.config.tracesDir),
2407
2804
  ...this.config.dataDirs || []
2408
2805
  ];
2409
- const configs = (0, import_agentflow_core3.discoverAllProcessConfigs)(discoveryDirs);
2806
+ let configs = (0, import_agentflow_core3.discoverAllProcessConfigs)(discoveryDirs);
2807
+ const pref = getProcessPreference(this.userConfig);
2808
+ if (pref) {
2809
+ const hasPreferred = configs.some((c) => c.processName === pref.prefer);
2810
+ if (hasPreferred) {
2811
+ configs = configs.filter((c) => c.processName !== pref.over);
2812
+ }
2813
+ }
2410
2814
  if (configs.length === 0) {
2411
2815
  return res.json(null);
2412
2816
  }
@@ -2490,35 +2894,32 @@ var DashboardServer = class {
2490
2894
  }
2491
2895
  } catch {
2492
2896
  }
2493
- const watched = [
2897
+ const watched = [...new Set([
2494
2898
  this.config.tracesDir,
2495
2899
  ...this.config.dataDirs || [],
2496
2900
  ...extraDirs
2497
- ];
2901
+ ].map((w) => path2.resolve(w)))];
2498
2902
  const discovered = [];
2499
- try {
2500
- const { execSync } = require("child_process");
2501
- const raw = execSync(
2502
- "systemctl --user show --property=ExecStart --no-pager alfred.service openclaw-gateway.service 2>/dev/null",
2503
- { encoding: "utf8", timeout: 5e3 }
2504
- );
2505
- for (const line of raw.split("\n")) {
2506
- const match = line.match(/path=([^\s;]+)/);
2507
- if (match == null ? void 0 : match[1]) {
2508
- const dir = path2.dirname(match[1]);
2509
- if (fs2.existsSync(dir)) discovered.push(dir);
2903
+ const svcNames = getSystemdServices(this.userConfig);
2904
+ if (svcNames.length > 0) {
2905
+ try {
2906
+ const { execSync: execSync2 } = require("child_process");
2907
+ const raw = execSync2(
2908
+ `systemctl --user show --property=ExecStart --no-pager ${svcNames.join(" ")} 2>/dev/null`,
2909
+ { encoding: "utf8", timeout: 5e3 }
2910
+ );
2911
+ for (const line of raw.split("\n")) {
2912
+ const match = line.match(/path=([^\s;]+)/);
2913
+ if (match == null ? void 0 : match[1]) {
2914
+ const dir = path2.dirname(match[1]);
2915
+ if (fs2.existsSync(dir)) discovered.push(dir);
2916
+ }
2510
2917
  }
2918
+ } catch {
2511
2919
  }
2512
- } catch {
2513
2920
  }
2514
2921
  const commonPaths = [
2515
- path2.join(home, ".alfred/traces"),
2516
- path2.join(home, ".alfred/data"),
2517
- path2.join(home, ".openclaw/workspace/traces"),
2518
- path2.join(home, ".openclaw/subagents"),
2519
- path2.join(home, ".openclaw/cron/runs"),
2520
- path2.join(home, ".openclaw/cron"),
2521
- path2.join(home, ".openclaw/agents/main/sessions"),
2922
+ ...getDiscoveryPaths(this.userConfig),
2522
2923
  path2.join(home, ".agentflow/traces")
2523
2924
  ];
2524
2925
  for (const p of commonPaths) {
@@ -2622,18 +3023,10 @@ var DashboardServer = class {
2622
3023
  this.app.get("/ready", (_req, res) => {
2623
3024
  res.json({ status: "ready" });
2624
3025
  });
2625
- this.app.get("/v1/*", (_req, res) => {
2626
- const legacyIndex = path2.join(__dirname, "../public/index.html");
2627
- if (fs2.existsSync(legacyIndex)) {
2628
- res.sendFile(legacyIndex);
2629
- } else {
2630
- res.status(404).send("Legacy dashboard not found");
2631
- }
2632
- });
2633
3026
  this.app.get("*", (_req, res) => {
2634
- const clientIndex = path2.join(__dirname, "../dist/client/index.html");
2635
- if (fs2.existsSync(clientIndex)) {
2636
- res.sendFile(clientIndex);
3027
+ const clientIndex2 = path2.join(__dirname, "../dist/client/index.html");
3028
+ if (fs2.existsSync(clientIndex2)) {
3029
+ res.sendFile(clientIndex2);
2637
3030
  } else {
2638
3031
  res.status(404).send("Dashboard not found - public files may not be built");
2639
3032
  }
@@ -2659,6 +3052,41 @@ var DashboardServer = class {
2659
3052
  });
2660
3053
  });
2661
3054
  }
3055
+ /** Watch soma-report.json for changes and broadcast updates via WebSocket. */
3056
+ setupSomaReportWatcher() {
3057
+ const somaVault = this.config.somaVault;
3058
+ if (!somaVault) return;
3059
+ const reportPath = path2.join(somaVault, "..", "soma-report.json");
3060
+ const reportDir = path2.dirname(reportPath);
3061
+ if (!fs2.existsSync(reportDir)) return;
3062
+ let debounceTimer = null;
3063
+ const watcher = import_chokidar2.default.watch(reportPath, {
3064
+ ignoreInitial: true,
3065
+ persistent: true,
3066
+ awaitWriteFinish: { stabilityThreshold: 500 }
3067
+ });
3068
+ watcher.on("change", () => {
3069
+ if (debounceTimer) clearTimeout(debounceTimer);
3070
+ debounceTimer = setTimeout(() => {
3071
+ var _a, _b;
3072
+ try {
3073
+ const report = JSON.parse(fs2.readFileSync(reportPath, "utf-8"));
3074
+ this.broadcast({ type: "soma-report-updated", data: report });
3075
+ if (report.generatedAt) {
3076
+ this.broadcast({
3077
+ type: "soma-activity",
3078
+ data: {
3079
+ action: "report-updated",
3080
+ description: `Report updated: ${((_a = report.totals) == null ? void 0 : _a.agents) ?? 0} agents, ${((_b = report.totals) == null ? void 0 : _b.insights) ?? 0} insights`,
3081
+ timestamp: report.generatedAt
3082
+ }
3083
+ });
3084
+ }
3085
+ } catch {
3086
+ }
3087
+ }, 500);
3088
+ });
3089
+ }
2662
3090
  /**
2663
3091
  * Filter an agent's traces to valid ExecutionGraphs and convert via loadGraph().
2664
3092
  * Returns only traces with proper nodes (Map or non-empty object), skipping session-only traces.
@@ -2880,24 +3308,49 @@ var DashboardServer = class {
2880
3308
  });
2881
3309
  }
2882
3310
  async start() {
2883
- return new Promise((resolve4) => {
3311
+ return new Promise((resolve5) => {
2884
3312
  const host = this.config.host || "localhost";
2885
3313
  this.server.listen(this.config.port, host, () => {
2886
3314
  console.log(`AgentFlow Dashboard running at http://${host}:${this.config.port}`);
2887
3315
  console.log(`Watching traces in: ${this.config.tracesDir}`);
2888
- resolve4();
3316
+ resolve5();
2889
3317
  });
2890
3318
  });
2891
3319
  }
3320
+ /** Check if any src/client file is newer than the built bundle. */
3321
+ isClientStale(srcDir, distDir) {
3322
+ try {
3323
+ const distIndex = path2.join(distDir, "index.html");
3324
+ if (!fs2.existsSync(distIndex)) return true;
3325
+ const distMtime = fs2.statSync(distIndex).mtimeMs;
3326
+ const check = (dir) => {
3327
+ for (const entry of fs2.readdirSync(dir, { withFileTypes: true })) {
3328
+ const full = path2.join(dir, entry.name);
3329
+ if (entry.isDirectory()) {
3330
+ if (check(full)) return true;
3331
+ } else if (fs2.statSync(full).mtimeMs > distMtime) {
3332
+ return true;
3333
+ }
3334
+ }
3335
+ return false;
3336
+ };
3337
+ return check(srcDir);
3338
+ } catch {
3339
+ return false;
3340
+ }
3341
+ }
2892
3342
  async stop() {
2893
- return new Promise((resolve4) => {
3343
+ return new Promise((resolve5) => {
2894
3344
  this.watcher.stop();
2895
3345
  this.server.close(() => {
2896
3346
  console.log("Dashboard server stopped");
2897
- resolve4();
3347
+ resolve5();
2898
3348
  });
2899
3349
  });
2900
3350
  }
3351
+ getConfigPath() {
3352
+ return this.configPath;
3353
+ }
2901
3354
  getStats() {
2902
3355
  return this.stats.getGlobalStats();
2903
3356
  }
@@ -2910,7 +3363,7 @@ if (import_meta.url === `file://${process.argv[1]}`) {
2910
3363
  }
2911
3364
 
2912
3365
  // src/cli.ts
2913
- var VERSION = "0.4.0";
3366
+ var VERSION = "0.8.0";
2914
3367
  function getLanAddress() {
2915
3368
  const interfaces = os.networkInterfaces();
2916
3369
  for (const name of Object.keys(interfaces)) {
@@ -2922,7 +3375,7 @@ function getLanAddress() {
2922
3375
  }
2923
3376
  return null;
2924
3377
  }
2925
- function printBanner(config, traceCount, stats) {
3378
+ function printBanner(config, traceCount, stats, configPath) {
2926
3379
  var _a;
2927
3380
  const lan = getLanAddress();
2928
3381
  const host = config.host || "localhost";
@@ -2938,26 +3391,25 @@ function printBanner(config, traceCount, stats) {
2938
3391
 
2939
3392
  See your agents think.
2940
3393
 
2941
- \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
2942
- \u2502 \u{1F916} Agents \u2502 TRACE FILES \u2502 \u{1F4CA} AgentFlow \u2502 SHOWS YOU \u2502 \u{1F310} Your browser \u2502
2943
- \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
2944
- \u2502 write JSON \u2502 \u2502 builds graphs, \u2502 \u2502 graph, timeline, \u2502
2945
- \u2502 trace files. \u2502 \u2502 serves dashboard.\u2502 \u2502 metrics, health. \u2502
2946
- \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
2947
-
2948
- Runs locally. Your data never leaves your machine.
2949
-
2950
- Tabs: \u{1F3AF} Graph \xB7 \u23F1\uFE0F Timeline \xB7 \u{1F4CA} Metrics \xB7 \u{1F6E0}\uFE0F Process Health \xB7 \u26A0\uFE0F Errors
2951
-
2952
3394
  Traces: ${config.tracesDir}${((_a = config.dataDirs) == null ? void 0 : _a.length) ? `
2953
3395
  Data dirs: ${config.dataDirs.join("\n ")}` : ""}
2954
3396
  Loaded: ${traceCount} traces \xB7 ${stats.totalAgents} agents \xB7 ${stats.totalExecutions} executions
2955
3397
  Success: ${stats.globalSuccessRate.toFixed(1)}%${stats.activeAgents > 0 ? ` \xB7 ${stats.activeAgents} active now` : ""}
3398
+ Config: ${configPath ?? "none (using defaults)"}
2956
3399
  CORS: ${config.enableCors ? "enabled" : "disabled"}
2957
3400
  WebSocket: live updates enabled
3401
+ Window: ${process.env.AGENTFLOW_TRACE_WINDOW_HOURS ?? "48"}h (set AGENTFLOW_TRACE_WINDOW_HOURS to change)
2958
3402
 
2959
3403
  \u2192 http://localhost:${port}${isPublic && lan ? `
2960
3404
  \u2192 http://${lan}:${port} (LAN)` : ""}
3405
+
3406
+ Pages: Agents \xB7 SOMA
3407
+ Agent: Profile \xB7 Execution Detail
3408
+ SOMA: Intelligence \xB7 Review \xB7 Policies \xB7 Knowledge \xB7 Activity
3409
+ Tabs: Flame Chart \xB7 Agent Flow \xB7 Metrics \xB7 Dependencies
3410
+ State Machine \xB7 Summary \xB7 Transcript
3411
+
3412
+ Runs locally. Your data never leaves your machine.
2961
3413
  `);
2962
3414
  }
2963
3415
  async function startDashboard() {
@@ -2995,6 +3447,12 @@ async function startDashboard() {
2995
3447
  case "--collector-token":
2996
3448
  config.collectorAuthToken = args[++i];
2997
3449
  break;
3450
+ case "--soma-vault":
3451
+ config.somaVault = args[++i];
3452
+ break;
3453
+ case "--config":
3454
+ config.configPath = args[++i];
3455
+ break;
2998
3456
  case "--help":
2999
3457
  printHelp();
3000
3458
  process.exit(0);
@@ -3006,6 +3464,9 @@ async function startDashboard() {
3006
3464
  if (process.env.AGENTFLOW_NO_COLLECTOR === "true") {
3007
3465
  config.enableCollector = false;
3008
3466
  }
3467
+ if (!config.somaVault && process.env.SOMA_VAULT) {
3468
+ config.somaVault = process.env.SOMA_VAULT;
3469
+ }
3009
3470
  const tracesPath = path3.resolve(config.tracesDir);
3010
3471
  if (!fs3.existsSync(tracesPath)) {
3011
3472
  fs3.mkdirSync(tracesPath, { recursive: true });
@@ -3027,7 +3488,7 @@ async function startDashboard() {
3027
3488
  setTimeout(() => {
3028
3489
  const stats = dashboard.getStats();
3029
3490
  const traces = dashboard.getTraces();
3030
- printBanner(config, traces.length, stats);
3491
+ printBanner(config, traces.length, stats, dashboard.getConfigPath());
3031
3492
  }, 1500);
3032
3493
  } catch (error) {
3033
3494
  console.error("\u274C Failed to start dashboard:", error);
@@ -3036,7 +3497,7 @@ async function startDashboard() {
3036
3497
  }
3037
3498
  function printHelp() {
3038
3499
  console.log(`
3039
- \u{1F4CA} AgentFlow Dashboard v${VERSION} \u2014 See your agents think.
3500
+ AgentFlow Dashboard v${VERSION} \u2014 See your agents think.
3040
3501
 
3041
3502
  Usage:
3042
3503
  agentflow-dashboard [options]
@@ -3047,22 +3508,34 @@ Options:
3047
3508
  -t, --traces <path> Traces directory (default: ./traces)
3048
3509
  -h, --host <address> Host address (default: localhost)
3049
3510
  --data-dir <path> Extra data directory for process discovery (repeatable)
3511
+ --config <path> Path to agentflow.config.json (aliases, skip files, etc.)
3512
+ --soma-vault <path> SOMA vault directory for intelligence data
3050
3513
  --cors Enable CORS headers
3051
3514
  --no-collector Disable OTLP trace collector (POST /v1/traces)
3052
3515
  --collector-token <tok> Require auth token for collector (or set AGENTFLOW_COLLECTOR_TOKEN)
3053
3516
  --help Show this help message
3054
3517
 
3055
- Examples:
3056
- agentflow-dashboard --traces ./traces --host 0.0.0.0 --cors
3057
- agentflow-dashboard -p 8080 -t /var/log/agentflow
3058
- agentflow-dashboard --traces ./traces --data-dir ./workers --data-dir ./cron
3518
+ Config file:
3519
+ The dashboard loads agentflow.config.json for agent aliases, skip files,
3520
+ discovery paths, and systemd services. Resolution order:
3521
+ 1. --config flag
3522
+ 2. AGENTFLOW_CONFIG env var
3523
+ 3. ./agentflow.config.json
3524
+ 4. ~/.config/agentflow/config.json
3525
+
3526
+ See agentflow.config.example.json for a complete reference.
3059
3527
 
3060
- Tabs:
3061
- \u{1F3AF} Graph Interactive Cytoscape.js execution graph
3062
- \u23F1\uFE0F Timeline Waterfall view of node durations
3063
- \u{1F4CA} Metrics Success rates, durations, node breakdown
3064
- \u{1F6E0}\uFE0F Process Health PID files, systemd, workers, orphans
3065
- \u26A0\uFE0F Errors Failed and hung nodes with metadata
3528
+ Environment:
3529
+ AGENTFLOW_CONFIG Path to config file
3530
+ AGENTFLOW_TRACE_WINDOW_HOURS Max age of traces to load (default: 48)
3531
+ AGENTFLOW_COLLECTOR_TOKEN Auth token for OTLP collector
3532
+ AGENTFLOW_NO_COLLECTOR=true Disable OTLP collector
3533
+ SOMA_VAULT SOMA vault directory
3534
+
3535
+ Examples:
3536
+ agentflow-dashboard --traces ./traces --host 0.0.0.0
3537
+ agentflow-dashboard --traces ./traces --config ./agentflow.config.json
3538
+ agentflow-dashboard -p 8080 -t /var/log/agentflow --cors
3066
3539
  `);
3067
3540
  }
3068
3541
  // Annotate the CommonJS export names for ESM import in node: