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.
@@ -6,10 +6,84 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
6
6
  });
7
7
 
8
8
  // src/server.ts
9
+ import { execSync } from "child_process";
9
10
  import * as fs3 from "fs";
10
11
  import { createServer } from "http";
11
12
  import * as path3 from "path";
12
13
  import { fileURLToPath } from "url";
14
+
15
+ // src/config.ts
16
+ import { existsSync, readFileSync } from "fs";
17
+ import { homedir } from "os";
18
+ import { join, resolve } from "path";
19
+ var EMPTY_CONFIG = {};
20
+ function expandTilde(p) {
21
+ if (p.startsWith("~/") || p === "~") {
22
+ return join(homedir(), p.slice(1));
23
+ }
24
+ return p;
25
+ }
26
+ function loadConfig(explicitPath) {
27
+ const candidates = [];
28
+ if (explicitPath) {
29
+ candidates.push(resolve(explicitPath));
30
+ }
31
+ if (process.env.AGENTFLOW_CONFIG) {
32
+ candidates.push(resolve(process.env.AGENTFLOW_CONFIG));
33
+ }
34
+ candidates.push(resolve("agentflow.config.json"));
35
+ candidates.push(join(homedir(), ".config", "agentflow", "config.json"));
36
+ for (const candidate of candidates) {
37
+ if (!existsSync(candidate)) continue;
38
+ try {
39
+ const raw = readFileSync(candidate, "utf-8");
40
+ const parsed = JSON.parse(raw);
41
+ const cleaned = stripCommentKeys(parsed);
42
+ console.log(`Loaded config: ${candidate}`);
43
+ return { config: cleaned, configPath: candidate };
44
+ } catch (err) {
45
+ console.warn(`Warning: Failed to load config from ${candidate}: ${err.message}`);
46
+ console.warn("Continuing with empty defaults.");
47
+ return { config: EMPTY_CONFIG, configPath: null };
48
+ }
49
+ }
50
+ return { config: EMPTY_CONFIG, configPath: null };
51
+ }
52
+ function stripCommentKeys(obj) {
53
+ if (Array.isArray(obj)) return obj.map(stripCommentKeys);
54
+ if (obj && typeof obj === "object") {
55
+ const result = {};
56
+ for (const [key, value] of Object.entries(obj)) {
57
+ if (key.startsWith("//")) continue;
58
+ result[key] = stripCommentKeys(value);
59
+ }
60
+ return result;
61
+ }
62
+ return obj;
63
+ }
64
+ function getAliases(config) {
65
+ return config.aliases ?? {};
66
+ }
67
+ function getSkipFiles(config) {
68
+ return config.skipFiles ?? [];
69
+ }
70
+ function getSkipDirectories(config) {
71
+ return config.skipDirectories ?? [];
72
+ }
73
+ function getDiscoveryPaths(config) {
74
+ return (config.discoveryPaths ?? []).map(expandTilde);
75
+ }
76
+ function getSystemdServices(config) {
77
+ return config.systemdServices ?? [];
78
+ }
79
+ function getAgentDetection(config) {
80
+ return config.agentDetection ?? {};
81
+ }
82
+ function getProcessPreference(config) {
83
+ return config.processPreference ?? null;
84
+ }
85
+
86
+ // src/server.ts
13
87
  import {
14
88
  auditProcesses,
15
89
  createExecutionEvent,
@@ -20,6 +94,7 @@ import {
20
94
  getBottlenecks,
21
95
  loadGraph as loadGraph2
22
96
  } from "agentflow-core";
97
+ import chokidar2 from "chokidar";
23
98
  import express from "express";
24
99
  import { WebSocketServer } from "ws";
25
100
 
@@ -52,17 +127,17 @@ var AgentFlowAdapter = class {
52
127
  };
53
128
 
54
129
  // src/adapters/openclaw.ts
55
- import { existsSync, readFileSync } from "fs";
56
- import { basename, dirname, join } from "path";
130
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
131
+ import { basename, dirname, join as join2 } from "path";
57
132
  var jobCache = /* @__PURE__ */ new Map();
58
133
  function loadJobs(openclawDir) {
59
134
  const cached = jobCache.get(openclawDir);
60
135
  if (cached) return cached;
61
- const jobsPath = join(openclawDir, "cron", "jobs.json");
136
+ const jobsPath = join2(openclawDir, "cron", "jobs.json");
62
137
  const map = /* @__PURE__ */ new Map();
63
138
  try {
64
- if (existsSync(jobsPath)) {
65
- const data = JSON.parse(readFileSync(jobsPath, "utf-8"));
139
+ if (existsSync2(jobsPath)) {
140
+ const data = JSON.parse(readFileSync2(jobsPath, "utf-8"));
66
141
  const jobs = Array.isArray(data) ? data : data.jobs ?? [];
67
142
  for (const job of jobs) {
68
143
  if (job.id) map.set(job.id, job);
@@ -76,7 +151,7 @@ function loadJobs(openclawDir) {
76
151
  function findOpenClawRoot(filePath) {
77
152
  let dir = dirname(filePath);
78
153
  for (let i = 0; i < 5; i++) {
79
- if (existsSync(join(dir, "cron", "jobs.json")) || basename(dir) === ".openclaw") {
154
+ if (existsSync2(join2(dir, "cron", "jobs.json")) || basename(dir) === ".openclaw") {
80
155
  return dir;
81
156
  }
82
157
  dir = dirname(dir);
@@ -86,7 +161,7 @@ function findOpenClawRoot(filePath) {
86
161
  var OpenClawAdapter = class {
87
162
  name = "openclaw";
88
163
  detect(dirPath) {
89
- return existsSync(join(dirPath, "cron", "jobs.json")) || dirPath.includes(".openclaw") || existsSync(join(dirPath, "cron", "runs"));
164
+ return existsSync2(join2(dirPath, "cron", "jobs.json")) || dirPath.includes(".openclaw") || existsSync2(join2(dirPath, "cron", "runs"));
90
165
  }
91
166
  canHandle(filePath) {
92
167
  if (!filePath.endsWith(".jsonl")) return false;
@@ -95,7 +170,7 @@ var OpenClawAdapter = class {
95
170
  parse(filePath) {
96
171
  const traces = [];
97
172
  try {
98
- const content = readFileSync(filePath, "utf-8");
173
+ const content = readFileSync2(filePath, "utf-8");
99
174
  const root = findOpenClawRoot(filePath);
100
175
  const jobs = root ? loadJobs(root) : /* @__PURE__ */ new Map();
101
176
  for (const line of content.split("\n")) {
@@ -159,8 +234,8 @@ var OpenClawAdapter = class {
159
234
  };
160
235
 
161
236
  // src/adapters/otel.ts
162
- import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync } from "fs";
163
- import { join as join2 } from "path";
237
+ import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync } from "fs";
238
+ import { join as join3 } from "path";
164
239
  var SPAN_TYPE_MAP = {
165
240
  "gen_ai.chat": "llm",
166
241
  "gen_ai.completion": "llm",
@@ -267,7 +342,7 @@ var OTelAdapter = class {
267
342
  name = "otel";
268
343
  detect(dirPath) {
269
344
  try {
270
- if (existsSync2(join2(dirPath, "otel-traces"))) return true;
345
+ if (existsSync3(join3(dirPath, "otel-traces"))) return true;
271
346
  const files = readdirSync(dirPath);
272
347
  return files.some((f) => f.endsWith(".otlp.json"));
273
348
  } catch {
@@ -279,7 +354,7 @@ var OTelAdapter = class {
279
354
  }
280
355
  parse(filePath) {
281
356
  try {
282
- const content = readFileSync2(filePath, "utf-8");
357
+ const content = readFileSync3(filePath, "utf-8");
283
358
  const payload = JSON.parse(content);
284
359
  const traces = parseOtlpPayload(payload);
285
360
  for (const t of traces) t.filePath = filePath;
@@ -321,9 +396,7 @@ function extractSource(agentId) {
321
396
  const colonIdx = agentId.indexOf(":");
322
397
  if (colonIdx > 0 && colonIdx < 20) {
323
398
  const prefix = agentId.slice(0, colonIdx);
324
- if (["openclaw", "otel", "langchain", "crewai", "mastra"].includes(prefix)) {
325
- return { source: prefix, localId: agentId.slice(colonIdx + 1) };
326
- }
399
+ return { source: prefix, localId: agentId.slice(colonIdx + 1) };
327
400
  }
328
401
  return { source: "agentflow", localId: agentId };
329
402
  }
@@ -354,16 +427,20 @@ function deduplicateAgents(agents) {
354
427
  for (const a of tagged) {
355
428
  const suffix = extractSuffix(a.localId);
356
429
  if (!suffix) continue;
357
- const group = suffixGroups.get(suffix) ?? [];
430
+ const key = `${a.source}:${suffix}`;
431
+ const group = suffixGroups.get(key) ?? [];
358
432
  group.push(a);
359
- suffixGroups.set(suffix, group);
433
+ suffixGroups.set(key, group);
360
434
  }
361
435
  const mergedIds = /* @__PURE__ */ new Set();
362
436
  const mergedAgents = [];
363
- for (const [suffix, group] of suffixGroups) {
437
+ for (const [_key, group] of suffixGroups) {
438
+ const suffix = extractSuffix(group[0].localId);
364
439
  if (group.length < 2) continue;
365
440
  const prefixes = new Set(group.map((a) => a.localId.split("-")[0]));
366
441
  if (prefixes.size < 2) continue;
442
+ const longPrefixes = [...prefixes].filter((p) => p !== suffix && p.length > 2);
443
+ if (longPrefixes.length >= 2) continue;
367
444
  const merged = {
368
445
  agentId: group[0].source === "agentflow" ? suffix : `${group[0].source}:${suffix}`,
369
446
  displayName: suffix,
@@ -411,10 +488,7 @@ function groupAgents(agents) {
411
488
  }
412
489
  const SOURCE_DISPLAY = {
413
490
  agentflow: "AgentFlow",
414
- openclaw: "OpenClaw",
415
- otel: "OpenTelemetry",
416
- langchain: "LangChain",
417
- crewai: "CrewAI"
491
+ otel: "OpenTelemetry"
418
492
  };
419
493
  const groups = [];
420
494
  for (const [source, sourceAgents] of sourceMap) {
@@ -760,10 +834,6 @@ function getUniversalNodeStatus(activity) {
760
834
  return "completed";
761
835
  }
762
836
  function openClawSessionIdToAgent(sessionId) {
763
- if (sessionId.startsWith("janitor-")) return "vault-janitor";
764
- if (sessionId.startsWith("curator-")) return "vault-curator";
765
- if (sessionId.startsWith("distiller-")) return "vault-distiller";
766
- if (sessionId.startsWith("main-")) return "alfred-main";
767
837
  const firstSegment = sessionId.split("-")[0];
768
838
  if (firstSegment) return firstSegment;
769
839
  return "openclaw";
@@ -776,19 +846,81 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
776
846
  tracesDir;
777
847
  dataDirs;
778
848
  allWatchDirs;
849
+ maxAgeMs;
850
+ userConfig;
779
851
  constructor(tracesDirOrOptions) {
780
852
  super();
853
+ const defaultMaxAgeMs = 48 * 60 * 60 * 1e3;
854
+ const envHours = process.env.AGENTFLOW_TRACE_WINDOW_HOURS;
855
+ const envMaxAgeMs = envHours ? parseFloat(envHours) * 60 * 60 * 1e3 : void 0;
781
856
  if (typeof tracesDirOrOptions === "string") {
782
857
  this.tracesDir = path.resolve(tracesDirOrOptions);
783
858
  this.dataDirs = [];
859
+ this.maxAgeMs = envMaxAgeMs ?? defaultMaxAgeMs;
860
+ this.userConfig = {};
784
861
  } else {
785
862
  this.tracesDir = path.resolve(tracesDirOrOptions.tracesDir);
786
863
  this.dataDirs = (tracesDirOrOptions.dataDirs || []).map((d) => path.resolve(d));
787
- }
788
- this.allWatchDirs = [this.tracesDir, ...this.dataDirs];
864
+ this.maxAgeMs = envMaxAgeMs ?? tracesDirOrOptions.maxAgeMs ?? defaultMaxAgeMs;
865
+ this.userConfig = tracesDirOrOptions.userConfig ?? {};
866
+ }
867
+ this.skipFiles = /* @__PURE__ */ new Set([
868
+ ..._TraceWatcher.STRUCTURAL_SKIP_FILES,
869
+ ...getSkipFiles(this.userConfig)
870
+ ]);
871
+ this.userSkipDirs = new Set(getSkipDirectories(this.userConfig));
872
+ this.allWatchDirs = [...new Set([this.tracesDir, ...this.dataDirs].map((d) => path.resolve(d)))];
789
873
  this.ensureTracesDir();
790
874
  this.loadExistingFiles();
875
+ this.archiveOldTraces();
791
876
  this.startWatching();
877
+ setInterval(() => this.archiveOldTraces(), 6 * 60 * 60 * 1e3);
878
+ }
879
+ /** Move trace files older than maxAgeMs into archive/YYYY-MM/ subdirectories. */
880
+ archiveOldTraces() {
881
+ const cutoff = Date.now() - this.maxAgeMs;
882
+ let archived = 0;
883
+ for (const dir of this.allWatchDirs) {
884
+ if (!fs.existsSync(dir)) continue;
885
+ try {
886
+ this.archiveDirectory(dir, cutoff, 0);
887
+ } catch (error) {
888
+ console.warn(`Archival error in ${dir}:`, error.message);
889
+ }
890
+ }
891
+ }
892
+ archiveDirectory(dir, cutoff, depth) {
893
+ if (depth > 10) return 0;
894
+ if (path.basename(dir) === "archive") return 0;
895
+ let archived = 0;
896
+ try {
897
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
898
+ for (const entry of entries) {
899
+ if (entry.name.startsWith(".") || entry.name === "archive" || this.userSkipDirs.has(entry.name)) continue;
900
+ const fullPath = path.join(dir, entry.name);
901
+ if (entry.isDirectory()) {
902
+ archived += this.archiveDirectory(fullPath, cutoff, depth + 1);
903
+ continue;
904
+ }
905
+ if (!entry.isFile() || !this.isSupportedFile(entry.name)) continue;
906
+ try {
907
+ const stats = fs.statSync(fullPath);
908
+ if (stats.mtimeMs >= cutoff) continue;
909
+ const mtime = new Date(stats.mtimeMs);
910
+ const yearMonth = `${mtime.getFullYear()}-${String(mtime.getMonth() + 1).padStart(2, "0")}`;
911
+ const archiveDir = path.join(this.tracesDir, "archive", yearMonth);
912
+ fs.mkdirSync(archiveDir, { recursive: true });
913
+ const dest = path.join(archiveDir, entry.name);
914
+ fs.renameSync(fullPath, dest);
915
+ const key = this.traceKey(fullPath);
916
+ this.traces.delete(key);
917
+ archived++;
918
+ } catch {
919
+ }
920
+ }
921
+ } catch {
922
+ }
923
+ return archived;
792
924
  }
793
925
  ensureTracesDir() {
794
926
  if (!fs.existsSync(this.tracesDir)) {
@@ -821,9 +953,17 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
821
953
  const entries = fs.readdirSync(dir, { withFileTypes: true });
822
954
  for (const entry of entries) {
823
955
  if (entry.name.startsWith(".")) continue;
956
+ if (entry.name === "archive") continue;
957
+ if (this.userSkipDirs.has(entry.name)) continue;
824
958
  const fullPath = path.join(dir, entry.name);
825
959
  if (entry.isFile()) {
826
960
  if (this.isSupportedFile(entry.name)) {
961
+ try {
962
+ const mtime = fs.statSync(fullPath).mtimeMs;
963
+ if (Date.now() - mtime > this.maxAgeMs) continue;
964
+ } catch {
965
+ continue;
966
+ }
827
967
  if (this.loadFile(fullPath)) {
828
968
  fileCount++;
829
969
  }
@@ -841,8 +981,8 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
841
981
  isSupportedFile(filename) {
842
982
  return filename.endsWith(".json") || filename.endsWith(".jsonl") || filename.endsWith(".log") || filename.endsWith(".trace");
843
983
  }
844
- /** File names that are config/state, not tracesskip them. */
845
- static SKIP_FILES = /* @__PURE__ */ new Set([
984
+ /** Structural file names that are never trace dataalways skipped. */
985
+ static STRUCTURAL_SKIP_FILES = /* @__PURE__ */ new Set([
846
986
  "workers.json",
847
987
  "package.json",
848
988
  "package-lock.json",
@@ -857,6 +997,10 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
857
997
  "update-check.json",
858
998
  "exec-approvals.json"
859
999
  ]);
1000
+ /** Skip files = structural + user config */
1001
+ skipFiles;
1002
+ /** Skip directories from user config */
1003
+ userSkipDirs;
860
1004
  static SKIP_SUFFIXES = [
861
1005
  "-state.json",
862
1006
  "-config.json",
@@ -868,7 +1012,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
868
1012
  /** Load a file using the adapter registry, falling back to built-in parsing. */
869
1013
  loadFile(filePath) {
870
1014
  const filename = path.basename(filePath);
871
- if (_TraceWatcher.SKIP_FILES.has(filename)) return false;
1015
+ if (this.skipFiles.has(filename)) return false;
872
1016
  if (_TraceWatcher.SKIP_SUFFIXES.some((s) => filename.endsWith(s))) return false;
873
1017
  const adapter = findAdapter(filePath);
874
1018
  if (adapter && adapter.name !== "agentflow") {
@@ -1045,43 +1189,26 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1045
1189
  }
1046
1190
  return traces;
1047
1191
  }
1048
- /**
1049
- * Normalise agent identifiers so that the same worker is never shown
1050
- * under two different names (e.g. "vault-curator" vs "openclaw-vault-curator").
1051
- *
1052
- * Canonical names: alfred-main, vault-curator, vault-janitor,
1053
- * vault-distiller, vault-surveyor
1054
- */
1055
- static AGENT_ALIASES = {
1056
- "openclaw-main": "alfred-main",
1057
- "openclaw-vault-curator": "vault-curator",
1058
- "openclaw-vault-janitor": "vault-janitor",
1059
- "openclaw-vault-distiller": "vault-distiller",
1060
- "openclaw-vault-surveyor": "vault-surveyor",
1061
- "alfred-curator": "vault-curator",
1062
- "alfred-janitor": "vault-janitor",
1063
- "alfred-distiller": "vault-distiller",
1064
- "alfred-surveyor": "vault-surveyor",
1065
- curator: "vault-curator",
1066
- janitor: "vault-janitor",
1067
- distiller: "vault-distiller",
1068
- surveyor: "vault-surveyor"
1069
- };
1192
+ /** Normalise agent identifiers using config-driven alias map. */
1070
1193
  normaliseAgentId(raw) {
1071
- return _TraceWatcher.AGENT_ALIASES[raw] ?? raw;
1194
+ const aliases = getAliases(this.userConfig);
1195
+ return aliases[raw] ?? raw;
1072
1196
  }
1073
1197
  detectAgentIdentifier(activity, _filename, filePath) {
1074
1198
  if (activity.agent_id) {
1075
- const agentId = activity.agent_id;
1076
- if (agentId === "main" && filePath.includes(".alfred/")) return this.normaliseAgentId("alfred-main");
1077
- return this.normaliseAgentId(agentId);
1199
+ return this.normaliseAgentId(activity.agent_id);
1078
1200
  }
1079
1201
  const pathAgent = this.extractAgentFromPath(filePath);
1080
- if (filePath.includes(".alfred/") && !pathAgent.startsWith("alfred-")) {
1202
+ const detection = getAgentDetection(this.userConfig);
1203
+ if (detection.filePatterns) {
1081
1204
  const basename3 = path.basename(filePath, path.extname(filePath));
1082
- if (basename3.match(/^(janitor|curator|distiller|surveyor|alfred)$/)) {
1083
- const raw = basename3 === "alfred" ? "alfred" : `alfred-${basename3}`;
1084
- return this.normaliseAgentId(raw);
1205
+ for (const [pattern, template] of Object.entries(detection.filePatterns)) {
1206
+ const re = new RegExp(`^(${pattern})$`);
1207
+ const match = basename3.match(re);
1208
+ if (match) {
1209
+ const resolved = template.replace("${match}", match[1]);
1210
+ return this.normaliseAgentId(resolved);
1211
+ }
1085
1212
  }
1086
1213
  }
1087
1214
  return this.normaliseAgentId(pathAgent);
@@ -1089,20 +1216,23 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1089
1216
  extractAgentFromPath(filePath) {
1090
1217
  const filename = path.basename(filePath, path.extname(filePath));
1091
1218
  const pathParts = filePath.split(path.sep);
1092
- if (filePath.includes(".openclaw/")) {
1093
- const agentsIndex = pathParts.lastIndexOf("agents");
1094
- if (agentsIndex !== -1 && agentsIndex + 1 < pathParts.length) {
1095
- return `openclaw-${pathParts[agentsIndex + 1]}`;
1096
- }
1097
- if (filename.startsWith("openclaw-")) {
1098
- return "openclaw-gateway";
1219
+ const detection = getAgentDetection(this.userConfig);
1220
+ let pathPrefix = "";
1221
+ if (detection.pathPatterns) {
1222
+ for (const [pathSubstring, agentId] of Object.entries(detection.pathPatterns)) {
1223
+ if (filePath.includes(pathSubstring)) {
1224
+ pathPrefix = agentId;
1225
+ break;
1226
+ }
1099
1227
  }
1100
- return "openclaw";
1101
1228
  }
1102
- if (filePath.includes(".alfred/") || filename.includes("alfred")) {
1103
- return "alfred";
1229
+ const agentsIndex = pathParts.lastIndexOf("agents");
1230
+ if (agentsIndex !== -1 && agentsIndex + 1 < pathParts.length) {
1231
+ const agentName = pathParts[agentsIndex + 1];
1232
+ return pathPrefix ? `${pathPrefix}-${agentName}` : agentName;
1104
1233
  }
1105
- for (const part of pathParts.reverse()) {
1234
+ if (pathPrefix) return pathPrefix;
1235
+ for (const part of [...pathParts].reverse()) {
1106
1236
  if (part.match(/agent|worker|service|daemon|bot|ai|llm/i)) {
1107
1237
  return part;
1108
1238
  }
@@ -1437,19 +1567,22 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1437
1567
  const parentDir = path.basename(path.dirname(filePath));
1438
1568
  const grandParentDir = path.basename(path.dirname(path.dirname(filePath)));
1439
1569
  const greatGrandParentDir = path.basename(path.dirname(path.dirname(path.dirname(filePath))));
1440
- let agentId;
1570
+ let agentName;
1441
1571
  if (parentDir === "sessions" && greatGrandParentDir === "agents") {
1442
- agentId = grandParentDir;
1572
+ agentName = grandParentDir;
1443
1573
  } else if (grandParentDir === "agents") {
1444
- agentId = parentDir;
1445
- } else if (parentDir === "runs" && grandParentDir === "cron") {
1446
- agentId = "openclaw-cron";
1574
+ agentName = parentDir;
1447
1575
  } else {
1448
- agentId = parentDir;
1449
- }
1450
- if (filePath.includes(".alfred/") || filePath.includes("alfred")) {
1451
- if (!agentId.startsWith("alfred-")) {
1452
- agentId = `alfred-${agentId}`;
1576
+ agentName = parentDir;
1577
+ }
1578
+ let agentId = agentName;
1579
+ const detection = getAgentDetection(this.userConfig);
1580
+ if (detection.pathPatterns) {
1581
+ for (const [pathSubstring, prefix] of Object.entries(detection.pathPatterns)) {
1582
+ if (filePath.includes(pathSubstring)) {
1583
+ agentId = `${prefix}-${agentName}`;
1584
+ break;
1585
+ }
1453
1586
  }
1454
1587
  }
1455
1588
  const modelEvent = rawEvents.find((e) => e.type === "model_change");
@@ -1738,6 +1871,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1738
1871
  edges: [],
1739
1872
  events: [],
1740
1873
  startTime,
1874
+ status,
1741
1875
  agentId,
1742
1876
  trigger,
1743
1877
  name: rootName,
@@ -1858,8 +1992,12 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1858
1992
  // Ignore git directories
1859
1993
  /\.vscode/,
1860
1994
  // Ignore vscode
1861
- /\.idea/
1995
+ /\.idea/,
1862
1996
  // Ignore idea
1997
+ /\/archive\//,
1998
+ // Ignore archived trace files
1999
+ // Ignore user-configured skip directories
2000
+ ...getSkipDirectories(this.userConfig).map((d) => new RegExp(`/${d}/`))
1863
2001
  ],
1864
2002
  persistent: true,
1865
2003
  ignoreInitial: true,
@@ -1913,29 +2051,42 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1913
2051
  });
1914
2052
  }
1915
2053
  getTrace(filename) {
2054
+ const candidates = [];
1916
2055
  const exact = this.traces.get(filename);
1917
- if (exact) return exact;
2056
+ if (exact) candidates.push(exact);
1918
2057
  if (filename.includes("::")) {
1919
2058
  const [fname, startTimeStr] = filename.split("::");
1920
2059
  const startTime = Number(startTimeStr);
1921
2060
  if (fname && !Number.isNaN(startTime)) {
1922
2061
  for (const trace of this.traces.values()) {
1923
2062
  if (trace.filename === fname && trace.startTime === startTime) {
1924
- return trace;
2063
+ candidates.push(trace);
1925
2064
  }
1926
2065
  }
1927
2066
  }
1928
2067
  }
1929
2068
  for (const prefix of ["openclaw:", "otel:", ""]) {
1930
2069
  const prefixed = this.traces.get(prefix + filename);
1931
- if (prefixed) return prefixed;
2070
+ if (prefixed) candidates.push(prefixed);
1932
2071
  }
1933
2072
  for (const [key, trace] of this.traces) {
1934
2073
  if (trace.filename === filename || trace.id === filename || key.endsWith(filename)) {
1935
- return trace;
2074
+ candidates.push(trace);
2075
+ }
2076
+ }
2077
+ if (candidates.length === 0) return void 0;
2078
+ if (candidates.length === 1) return candidates[0];
2079
+ let best = candidates[0];
2080
+ let bestNodeCount = best.nodes instanceof Map ? best.nodes.size : Object.keys(best.nodes ?? {}).length;
2081
+ for (let i = 1; i < candidates.length; i++) {
2082
+ const c = candidates[i];
2083
+ const nc = c.nodes instanceof Map ? c.nodes.size : Object.keys(c.nodes ?? {}).length;
2084
+ if (nc > bestNodeCount) {
2085
+ best = c;
2086
+ bestNodeCount = nc;
1936
2087
  }
1937
2088
  }
1938
- return void 0;
2089
+ return best;
1939
2090
  }
1940
2091
  getTracesByAgent(agentId) {
1941
2092
  return this.getAllTraces().filter((trace) => trace.agentId === agentId);
@@ -1982,7 +2133,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1982
2133
  import * as fs2 from "fs";
1983
2134
  import * as os from "os";
1984
2135
  import * as path2 from "path";
1985
- var VERSION = "0.4.0";
2136
+ var VERSION = "0.8.0";
1986
2137
  function getLanAddress() {
1987
2138
  const interfaces = os.networkInterfaces();
1988
2139
  for (const name of Object.keys(interfaces)) {
@@ -1994,7 +2145,7 @@ function getLanAddress() {
1994
2145
  }
1995
2146
  return null;
1996
2147
  }
1997
- function printBanner(config, traceCount, stats) {
2148
+ function printBanner(config, traceCount, stats, configPath) {
1998
2149
  var _a;
1999
2150
  const lan = getLanAddress();
2000
2151
  const host = config.host || "localhost";
@@ -2010,26 +2161,25 @@ function printBanner(config, traceCount, stats) {
2010
2161
 
2011
2162
  See your agents think.
2012
2163
 
2013
- \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
2014
- \u2502 \u{1F916} Agents \u2502 TRACE FILES \u2502 \u{1F4CA} AgentFlow \u2502 SHOWS YOU \u2502 \u{1F310} Your browser \u2502
2015
- \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
2016
- \u2502 write JSON \u2502 \u2502 builds graphs, \u2502 \u2502 graph, timeline, \u2502
2017
- \u2502 trace files. \u2502 \u2502 serves dashboard.\u2502 \u2502 metrics, health. \u2502
2018
- \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
2019
-
2020
- Runs locally. Your data never leaves your machine.
2021
-
2022
- Tabs: \u{1F3AF} Graph \xB7 \u23F1\uFE0F Timeline \xB7 \u{1F4CA} Metrics \xB7 \u{1F6E0}\uFE0F Process Health \xB7 \u26A0\uFE0F Errors
2023
-
2024
2164
  Traces: ${config.tracesDir}${((_a = config.dataDirs) == null ? void 0 : _a.length) ? `
2025
2165
  Data dirs: ${config.dataDirs.join("\n ")}` : ""}
2026
2166
  Loaded: ${traceCount} traces \xB7 ${stats.totalAgents} agents \xB7 ${stats.totalExecutions} executions
2027
2167
  Success: ${stats.globalSuccessRate.toFixed(1)}%${stats.activeAgents > 0 ? ` \xB7 ${stats.activeAgents} active now` : ""}
2168
+ Config: ${configPath ?? "none (using defaults)"}
2028
2169
  CORS: ${config.enableCors ? "enabled" : "disabled"}
2029
2170
  WebSocket: live updates enabled
2171
+ Window: ${process.env.AGENTFLOW_TRACE_WINDOW_HOURS ?? "48"}h (set AGENTFLOW_TRACE_WINDOW_HOURS to change)
2030
2172
 
2031
2173
  \u2192 http://localhost:${port}${isPublic && lan ? `
2032
2174
  \u2192 http://${lan}:${port} (LAN)` : ""}
2175
+
2176
+ Pages: Agents \xB7 SOMA
2177
+ Agent: Profile \xB7 Execution Detail
2178
+ SOMA: Intelligence \xB7 Review \xB7 Policies \xB7 Knowledge \xB7 Activity
2179
+ Tabs: Flame Chart \xB7 Agent Flow \xB7 Metrics \xB7 Dependencies
2180
+ State Machine \xB7 Summary \xB7 Transcript
2181
+
2182
+ Runs locally. Your data never leaves your machine.
2033
2183
  `);
2034
2184
  }
2035
2185
  async function startDashboard() {
@@ -2067,6 +2217,12 @@ async function startDashboard() {
2067
2217
  case "--collector-token":
2068
2218
  config.collectorAuthToken = args[++i];
2069
2219
  break;
2220
+ case "--soma-vault":
2221
+ config.somaVault = args[++i];
2222
+ break;
2223
+ case "--config":
2224
+ config.configPath = args[++i];
2225
+ break;
2070
2226
  case "--help":
2071
2227
  printHelp();
2072
2228
  process.exit(0);
@@ -2078,6 +2234,9 @@ async function startDashboard() {
2078
2234
  if (process.env.AGENTFLOW_NO_COLLECTOR === "true") {
2079
2235
  config.enableCollector = false;
2080
2236
  }
2237
+ if (!config.somaVault && process.env.SOMA_VAULT) {
2238
+ config.somaVault = process.env.SOMA_VAULT;
2239
+ }
2081
2240
  const tracesPath = path2.resolve(config.tracesDir);
2082
2241
  if (!fs2.existsSync(tracesPath)) {
2083
2242
  fs2.mkdirSync(tracesPath, { recursive: true });
@@ -2099,7 +2258,7 @@ async function startDashboard() {
2099
2258
  setTimeout(() => {
2100
2259
  const stats = dashboard.getStats();
2101
2260
  const traces = dashboard.getTraces();
2102
- printBanner(config, traces.length, stats);
2261
+ printBanner(config, traces.length, stats, dashboard.getConfigPath());
2103
2262
  }, 1500);
2104
2263
  } catch (error) {
2105
2264
  console.error("\u274C Failed to start dashboard:", error);
@@ -2108,7 +2267,7 @@ async function startDashboard() {
2108
2267
  }
2109
2268
  function printHelp() {
2110
2269
  console.log(`
2111
- \u{1F4CA} AgentFlow Dashboard v${VERSION} \u2014 See your agents think.
2270
+ AgentFlow Dashboard v${VERSION} \u2014 See your agents think.
2112
2271
 
2113
2272
  Usage:
2114
2273
  agentflow-dashboard [options]
@@ -2119,22 +2278,34 @@ Options:
2119
2278
  -t, --traces <path> Traces directory (default: ./traces)
2120
2279
  -h, --host <address> Host address (default: localhost)
2121
2280
  --data-dir <path> Extra data directory for process discovery (repeatable)
2281
+ --config <path> Path to agentflow.config.json (aliases, skip files, etc.)
2282
+ --soma-vault <path> SOMA vault directory for intelligence data
2122
2283
  --cors Enable CORS headers
2123
2284
  --no-collector Disable OTLP trace collector (POST /v1/traces)
2124
2285
  --collector-token <tok> Require auth token for collector (or set AGENTFLOW_COLLECTOR_TOKEN)
2125
2286
  --help Show this help message
2126
2287
 
2127
- Examples:
2128
- agentflow-dashboard --traces ./traces --host 0.0.0.0 --cors
2129
- agentflow-dashboard -p 8080 -t /var/log/agentflow
2130
- agentflow-dashboard --traces ./traces --data-dir ./workers --data-dir ./cron
2288
+ Config file:
2289
+ The dashboard loads agentflow.config.json for agent aliases, skip files,
2290
+ discovery paths, and systemd services. Resolution order:
2291
+ 1. --config flag
2292
+ 2. AGENTFLOW_CONFIG env var
2293
+ 3. ./agentflow.config.json
2294
+ 4. ~/.config/agentflow/config.json
2131
2295
 
2132
- Tabs:
2133
- \u{1F3AF} Graph Interactive Cytoscape.js execution graph
2134
- \u23F1\uFE0F Timeline Waterfall view of node durations
2135
- \u{1F4CA} Metrics Success rates, durations, node breakdown
2136
- \u{1F6E0}\uFE0F Process Health PID files, systemd, workers, orphans
2137
- \u26A0\uFE0F Errors Failed and hung nodes with metadata
2296
+ See agentflow.config.example.json for a complete reference.
2297
+
2298
+ Environment:
2299
+ AGENTFLOW_CONFIG Path to config file
2300
+ AGENTFLOW_TRACE_WINDOW_HOURS Max age of traces to load (default: 48)
2301
+ AGENTFLOW_COLLECTOR_TOKEN Auth token for OTLP collector
2302
+ AGENTFLOW_NO_COLLECTOR=true Disable OTLP collector
2303
+ SOMA_VAULT SOMA vault directory
2304
+
2305
+ Examples:
2306
+ agentflow-dashboard --traces ./traces --host 0.0.0.0
2307
+ agentflow-dashboard --traces ./traces --config ./agentflow.config.json
2308
+ agentflow-dashboard -p 8080 -t /var/log/agentflow --cors
2138
2309
  `);
2139
2310
  }
2140
2311
 
@@ -2156,12 +2327,15 @@ function serializeTrace(trace) {
2156
2327
  var DashboardServer = class {
2157
2328
  constructor(config) {
2158
2329
  this.config = config;
2159
- const home = process.env.HOME ?? "/home/trader";
2160
- const configPath = path3.join(home, ".agentflow/dashboard-config.json");
2330
+ const { config: userCfg, configPath: cfgPath } = loadConfig(config.configPath);
2331
+ this.userConfig = userCfg;
2332
+ this.configPath = cfgPath;
2333
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
2334
+ const dashConfigPath = path3.join(home, ".agentflow/dashboard-config.json");
2161
2335
  if (!config.dataDirs) config.dataDirs = [];
2162
2336
  try {
2163
- if (fs3.existsSync(configPath)) {
2164
- const saved = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
2337
+ if (fs3.existsSync(dashConfigPath)) {
2338
+ const saved = JSON.parse(fs3.readFileSync(dashConfigPath, "utf-8"));
2165
2339
  const extraDirs = saved.extraDirs ?? [];
2166
2340
  for (const d of extraDirs) {
2167
2341
  if (!config.dataDirs.includes(d)) config.dataDirs.push(d);
@@ -2169,21 +2343,15 @@ var DashboardServer = class {
2169
2343
  }
2170
2344
  } catch {
2171
2345
  }
2172
- const autoDiscoverPaths = [
2173
- path3.join(home, ".openclaw/cron/runs"),
2174
- path3.join(home, ".openclaw/workspace/traces"),
2175
- path3.join(home, ".openclaw/subagents"),
2176
- path3.join(home, ".openclaw/agents/main/sessions"),
2177
- path3.join(home, ".agentflow/traces")
2178
- ];
2179
- for (const p of autoDiscoverPaths) {
2346
+ for (const p of getDiscoveryPaths(this.userConfig)) {
2180
2347
  if (fs3.existsSync(p) && !config.dataDirs.includes(p)) {
2181
2348
  config.dataDirs.push(p);
2182
2349
  }
2183
2350
  }
2184
2351
  this.watcher = new TraceWatcher({
2185
2352
  tracesDir: config.tracesDir,
2186
- dataDirs: config.dataDirs
2353
+ dataDirs: config.dataDirs,
2354
+ userConfig: this.userConfig
2187
2355
  });
2188
2356
  this.stats = new AgentStats();
2189
2357
  this.knowledgeStore = createKnowledgeStore({
@@ -2192,6 +2360,7 @@ var DashboardServer = class {
2192
2360
  this.setupExpress();
2193
2361
  this.setupWebSocket();
2194
2362
  this.setupTraceWatcher();
2363
+ this.setupSomaReportWatcher();
2195
2364
  let knowledgeCount = 0;
2196
2365
  for (const trace of this.watcher.getAllTraces()) {
2197
2366
  this.stats.processTrace(trace);
@@ -2218,6 +2387,8 @@ var DashboardServer = class {
2218
2387
  ts: 0
2219
2388
  };
2220
2389
  knowledgeStore;
2390
+ userConfig;
2391
+ configPath;
2221
2392
  setupExpress() {
2222
2393
  if (this.config.enableCors) {
2223
2394
  this.app.use((_req, res, next) => {
@@ -2229,18 +2400,35 @@ var DashboardServer = class {
2229
2400
  next();
2230
2401
  });
2231
2402
  }
2232
- const clientDir = path3.join(__dirname, "../dist/client");
2403
+ const pkgDir = path3.join(__dirname, "..");
2404
+ const clientDir = path3.join(pkgDir, "dist/client");
2405
+ const clientIndex = path3.join(clientDir, "index.html");
2406
+ const srcDir = path3.join(pkgDir, "src/client");
2407
+ const needsBuild = !fs3.existsSync(clientIndex) || fs3.existsSync(srcDir) && this.isClientStale(srcDir, clientDir);
2408
+ if (needsBuild) {
2409
+ try {
2410
+ console.log("Building dashboard client...");
2411
+ execSync("npm run build:client", { cwd: pkgDir, stdio: "inherit", timeout: 3e4 });
2412
+ } catch (err) {
2413
+ console.warn("Client build failed \u2014 dashboard UI may be stale:", err.message);
2414
+ }
2415
+ }
2233
2416
  if (fs3.existsSync(clientDir)) {
2234
2417
  this.app.use(express.static(clientDir));
2235
2418
  }
2236
- const publicDir = path3.join(__dirname, "../public");
2237
- if (fs3.existsSync(publicDir)) {
2238
- this.app.use("/v1", express.static(publicDir));
2239
- }
2240
- this.app.get("/api/traces", (_req, res) => {
2419
+ this.app.get("/api/traces", (req, res) => {
2241
2420
  try {
2242
- const traces = this.watcher.getAllTraces().map(serializeTrace);
2243
- res.json(traces);
2421
+ const limit = Math.min(Math.max(parseInt(req.query.limit, 10) || 50, 1), 200);
2422
+ const cursor = req.query.cursor ? parseFloat(req.query.cursor) : void 0;
2423
+ let allTraces = this.watcher.getAllTraces();
2424
+ if (cursor) {
2425
+ allTraces = allTraces.filter((t) => (t.lastModified || t.startTime) < cursor);
2426
+ }
2427
+ const page = allTraces.slice(0, limit);
2428
+ const serialized = page.map(serializeTrace);
2429
+ const lastTrace = page[page.length - 1];
2430
+ const nextCursor = page.length === limit && lastTrace ? lastTrace.lastModified || lastTrace.startTime : null;
2431
+ res.json({ traces: serialized, nextCursor });
2244
2432
  } catch (_error) {
2245
2433
  res.status(500).json({ error: "Failed to load traces" });
2246
2434
  }
@@ -2531,6 +2719,235 @@ var DashboardServer = class {
2531
2719
  res.status(500).json({ error: "Failed to load agent statistics" });
2532
2720
  }
2533
2721
  });
2722
+ this.app.get("/api/soma/tier", (_req, res) => {
2723
+ const somaVault = this.config.somaVault;
2724
+ if (!somaVault) {
2725
+ return res.json({ tier: "teaser", somaVault: false, governanceAvailable: false });
2726
+ }
2727
+ try {
2728
+ const reportPath = path3.join(somaVault, "..", "soma-report.json");
2729
+ if (!fs3.existsSync(reportPath)) {
2730
+ return res.json({ tier: "free", somaVault: true, governanceAvailable: false });
2731
+ }
2732
+ const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
2733
+ const hasGovernance = report.governance && typeof report.governance.pending === "number";
2734
+ return res.json({
2735
+ tier: hasGovernance ? "pro" : "free",
2736
+ somaVault: true,
2737
+ governanceAvailable: !!hasGovernance
2738
+ });
2739
+ } catch {
2740
+ return res.json({ tier: "free", somaVault: true, governanceAvailable: false });
2741
+ }
2742
+ });
2743
+ this.app.get("/api/soma/report", (_req, res) => {
2744
+ const somaVault = this.config.somaVault;
2745
+ if (!somaVault) {
2746
+ return res.json({ available: false, teaser: true });
2747
+ }
2748
+ try {
2749
+ const reportPath = path3.join(somaVault, "..", "soma-report.json");
2750
+ if (!fs3.existsSync(reportPath)) {
2751
+ return res.json({ available: false, teaser: false, message: "No report file yet. Run soma watch." });
2752
+ }
2753
+ const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
2754
+ res.json(report);
2755
+ } catch (error) {
2756
+ console.error("Soma report error:", error);
2757
+ res.json({ available: false, teaser: false, message: "Failed to read report" });
2758
+ }
2759
+ });
2760
+ this.app.get("/api/soma/governance", (_req, res) => {
2761
+ const somaVault = this.config.somaVault;
2762
+ if (!somaVault) {
2763
+ return res.json({ available: false });
2764
+ }
2765
+ try {
2766
+ const reportPath = path3.join(somaVault, "..", "soma-report.json");
2767
+ if (!fs3.existsSync(reportPath)) {
2768
+ return res.json({ available: false, message: "No report file. Run soma report." });
2769
+ }
2770
+ const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
2771
+ res.json({
2772
+ available: true,
2773
+ layers: report.layers ?? { archive: 0, working: 0, emerging: 0, canon: 0 },
2774
+ governance: report.governance ?? { pending: 0, promoted: 0, rejected: 0 },
2775
+ insights: (report.insights ?? []).filter((i) => i.layer === "emerging" && i.proposal_status === "pending"),
2776
+ canon: (report.insights ?? []).filter((i) => i.layer === "canon"),
2777
+ generatedAt: report.generatedAt
2778
+ });
2779
+ } catch (error) {
2780
+ console.error("Soma governance error:", error);
2781
+ res.status(500).json({ available: false, message: "Failed to read governance data" });
2782
+ }
2783
+ });
2784
+ const sanitizeArg = (s) => s.replace(/[^a-zA-Z0-9_\-.:]/g, "");
2785
+ const sanitizeReason = (s) => s.replace(/["`$\\]/g, "").slice(0, 500);
2786
+ this.app.post("/api/soma/governance/promote", (req, res) => {
2787
+ var _a;
2788
+ const somaVault = this.config.somaVault;
2789
+ if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
2790
+ const { entryId } = req.body ?? {};
2791
+ if (!entryId) return res.status(400).json({ error: "entryId required" });
2792
+ try {
2793
+ const { execSync: execSync2 } = __require("child_process");
2794
+ const safeId = sanitizeArg(String(entryId));
2795
+ const result = execSync2(`npx soma governance promote ${safeId} --vault "${somaVault}"`, {
2796
+ encoding: "utf-8",
2797
+ timeout: 1e4
2798
+ });
2799
+ res.json({ success: true, message: result.trim() });
2800
+ } catch (error) {
2801
+ res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
2802
+ }
2803
+ });
2804
+ this.app.post("/api/soma/governance/reject", (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, reason } = req.body ?? {};
2809
+ if (!entryId || !reason) return res.status(400).json({ error: "entryId and reason required" });
2810
+ try {
2811
+ const { execSync: execSync2 } = __require("child_process");
2812
+ const safeId = sanitizeArg(String(entryId));
2813
+ const safeReason = sanitizeReason(String(reason));
2814
+ const result = execSync2(`npx soma governance reject ${safeId} "${safeReason}" --vault "${somaVault}"`, {
2815
+ encoding: "utf-8",
2816
+ timeout: 1e4
2817
+ });
2818
+ res.json({ success: true, message: result.trim() });
2819
+ } catch (error) {
2820
+ res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
2821
+ }
2822
+ });
2823
+ this.app.get("/api/soma/governance/evidence/:id", (req, res) => {
2824
+ var _a;
2825
+ const somaVault = this.config.somaVault;
2826
+ if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
2827
+ try {
2828
+ const { execSync: execSync2 } = __require("child_process");
2829
+ const safeId = sanitizeArg(String(req.params.id));
2830
+ const result = execSync2(`npx soma governance show ${safeId} --vault "${somaVault}"`, {
2831
+ encoding: "utf-8",
2832
+ timeout: 1e4
2833
+ });
2834
+ res.json({ available: true, output: result.trim() });
2835
+ } catch (error) {
2836
+ res.status(404).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
2837
+ }
2838
+ });
2839
+ this.app.get("/api/soma/policies", (_req, res) => {
2840
+ const somaVault = this.config.somaVault;
2841
+ if (!somaVault) return res.json({ policies: [] });
2842
+ try {
2843
+ const reportPath = path3.join(somaVault, "..", "soma-report.json");
2844
+ if (!fs3.existsSync(reportPath)) return res.json({ policies: [] });
2845
+ const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
2846
+ res.json({ policies: report.policies ?? [] });
2847
+ } catch {
2848
+ res.json({ policies: [] });
2849
+ }
2850
+ });
2851
+ this.app.post("/api/soma/policies", express.json(), (req, res) => {
2852
+ var _a;
2853
+ const somaVault = this.config.somaVault;
2854
+ if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
2855
+ const { name, enforcement, scope, conditions } = req.body ?? {};
2856
+ if (!name) return res.status(400).json({ error: "name required" });
2857
+ try {
2858
+ const safeName = sanitizeArg(String(name));
2859
+ const safeEnf = sanitizeArg(String(enforcement || "warn"));
2860
+ const safeScope = sanitizeReason(String(scope || "all"));
2861
+ const safeCond = sanitizeReason(String(conditions || ""));
2862
+ const result = execSync(
2863
+ `npx soma policy create "${safeName}" --enforcement ${safeEnf} --scope "${safeScope}" --conditions "${safeCond}" --vault "${somaVault}"`,
2864
+ { encoding: "utf-8", timeout: 1e4 }
2865
+ );
2866
+ res.json({ success: true, message: result.trim() });
2867
+ } catch (error) {
2868
+ res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
2869
+ }
2870
+ });
2871
+ this.app.delete("/api/soma/policies/:name", (req, res) => {
2872
+ var _a;
2873
+ const somaVault = this.config.somaVault;
2874
+ if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
2875
+ try {
2876
+ const safeName = sanitizeArg(String(req.params.name));
2877
+ const result = execSync(
2878
+ `npx soma policy delete "${safeName}" --vault "${somaVault}"`,
2879
+ { encoding: "utf-8", timeout: 1e4 }
2880
+ );
2881
+ res.json({ success: true, message: result.trim() });
2882
+ } catch (error) {
2883
+ res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
2884
+ }
2885
+ });
2886
+ this.app.get("/api/soma/vault/entities", (req, res) => {
2887
+ const somaVault = this.config.somaVault;
2888
+ if (!somaVault) return res.json({ entities: [], total: 0 });
2889
+ try {
2890
+ const reportPath = path3.join(somaVault, "..", "soma-report.json");
2891
+ if (!fs3.existsSync(reportPath)) return res.json({ entities: [], total: 0 });
2892
+ const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
2893
+ let entities = [
2894
+ ...(report.agents ?? []).map((a) => ({ ...a, type: "agent", id: a.name })),
2895
+ ...(report.insights ?? []).map((i, idx) => {
2896
+ var _a;
2897
+ return { ...i, type: i.type || "insight", id: ((_a = i.title) == null ? void 0 : _a.replace(/\s+/g, "-").toLowerCase()) || `insight-${idx}` };
2898
+ }),
2899
+ ...(report.policies ?? []).map((p) => ({ ...p, type: "policy", id: p.name }))
2900
+ ];
2901
+ const { type, layer, q, limit: limitStr, offset: offsetStr } = req.query;
2902
+ if (type) entities = entities.filter((e) => e.type === type);
2903
+ if (layer) entities = entities.filter((e) => e.layer === layer);
2904
+ if (q) {
2905
+ const lq = q.toLowerCase();
2906
+ entities = entities.filter((e) => (e.name || e.title || "").toLowerCase().includes(lq) || (e.claim || e.body || "").toLowerCase().includes(lq));
2907
+ }
2908
+ const total = entities.length;
2909
+ const offset = parseInt(offsetStr || "0", 10);
2910
+ const limit = Math.min(parseInt(limitStr || "50", 10), 200);
2911
+ entities = entities.slice(offset, offset + limit);
2912
+ res.json({ entities, total });
2913
+ } catch (error) {
2914
+ console.error("Vault entities error:", error);
2915
+ res.json({ entities: [], total: 0 });
2916
+ }
2917
+ });
2918
+ this.app.get("/api/soma/vault/entities/:type/:id", (req, res) => {
2919
+ const somaVault = this.config.somaVault;
2920
+ if (!somaVault) return res.status(404).json({ error: "Soma vault not configured" });
2921
+ try {
2922
+ const reportPath = path3.join(somaVault, "..", "soma-report.json");
2923
+ const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
2924
+ const { type, id } = req.params;
2925
+ let entity = null;
2926
+ if (type === "agent") {
2927
+ entity = (report.agents ?? []).find((a) => a.name === id);
2928
+ } else if (type === "policy") {
2929
+ entity = (report.policies ?? []).find((p) => p.name === id);
2930
+ } else {
2931
+ entity = (report.insights ?? []).find(
2932
+ (i) => {
2933
+ var _a;
2934
+ return (((_a = i.title) == null ? void 0 : _a.replace(/\s+/g, "-").toLowerCase()) || "") === id || i.title === id;
2935
+ }
2936
+ );
2937
+ }
2938
+ if (!entity) return res.status(404).json({ error: "Entity not found" });
2939
+ res.json({
2940
+ ...entity,
2941
+ type,
2942
+ id,
2943
+ body: entity.claim || entity.conditions || "",
2944
+ tags: entity.tags ?? [],
2945
+ related: entity.related ?? []
2946
+ });
2947
+ } catch {
2948
+ res.status(404).json({ error: "Entity not found" });
2949
+ }
2950
+ });
2534
2951
  this.app.get("/api/process-health", (_req, res) => {
2535
2952
  var _a, _b;
2536
2953
  try {
@@ -2543,7 +2960,14 @@ var DashboardServer = class {
2543
2960
  path3.dirname(this.config.tracesDir),
2544
2961
  ...this.config.dataDirs || []
2545
2962
  ];
2546
- const configs = discoverAllProcessConfigs(discoveryDirs);
2963
+ let configs = discoverAllProcessConfigs(discoveryDirs);
2964
+ const pref = getProcessPreference(this.userConfig);
2965
+ if (pref) {
2966
+ const hasPreferred = configs.some((c) => c.processName === pref.prefer);
2967
+ if (hasPreferred) {
2968
+ configs = configs.filter((c) => c.processName !== pref.over);
2969
+ }
2970
+ }
2547
2971
  if (configs.length === 0) {
2548
2972
  return res.json(null);
2549
2973
  }
@@ -2627,35 +3051,32 @@ var DashboardServer = class {
2627
3051
  }
2628
3052
  } catch {
2629
3053
  }
2630
- const watched = [
3054
+ const watched = [...new Set([
2631
3055
  this.config.tracesDir,
2632
3056
  ...this.config.dataDirs || [],
2633
3057
  ...extraDirs
2634
- ];
3058
+ ].map((w) => path3.resolve(w)))];
2635
3059
  const discovered = [];
2636
- try {
2637
- const { execSync } = __require("child_process");
2638
- const raw = execSync(
2639
- "systemctl --user show --property=ExecStart --no-pager alfred.service openclaw-gateway.service 2>/dev/null",
2640
- { encoding: "utf8", timeout: 5e3 }
2641
- );
2642
- for (const line of raw.split("\n")) {
2643
- const match = line.match(/path=([^\s;]+)/);
2644
- if (match == null ? void 0 : match[1]) {
2645
- const dir = path3.dirname(match[1]);
2646
- if (fs3.existsSync(dir)) discovered.push(dir);
3060
+ const svcNames = getSystemdServices(this.userConfig);
3061
+ if (svcNames.length > 0) {
3062
+ try {
3063
+ const { execSync: execSync2 } = __require("child_process");
3064
+ const raw = execSync2(
3065
+ `systemctl --user show --property=ExecStart --no-pager ${svcNames.join(" ")} 2>/dev/null`,
3066
+ { encoding: "utf8", timeout: 5e3 }
3067
+ );
3068
+ for (const line of raw.split("\n")) {
3069
+ const match = line.match(/path=([^\s;]+)/);
3070
+ if (match == null ? void 0 : match[1]) {
3071
+ const dir = path3.dirname(match[1]);
3072
+ if (fs3.existsSync(dir)) discovered.push(dir);
3073
+ }
2647
3074
  }
3075
+ } catch {
2648
3076
  }
2649
- } catch {
2650
3077
  }
2651
3078
  const commonPaths = [
2652
- path3.join(home, ".alfred/traces"),
2653
- path3.join(home, ".alfred/data"),
2654
- path3.join(home, ".openclaw/workspace/traces"),
2655
- path3.join(home, ".openclaw/subagents"),
2656
- path3.join(home, ".openclaw/cron/runs"),
2657
- path3.join(home, ".openclaw/cron"),
2658
- path3.join(home, ".openclaw/agents/main/sessions"),
3079
+ ...getDiscoveryPaths(this.userConfig),
2659
3080
  path3.join(home, ".agentflow/traces")
2660
3081
  ];
2661
3082
  for (const p of commonPaths) {
@@ -2759,18 +3180,10 @@ var DashboardServer = class {
2759
3180
  this.app.get("/ready", (_req, res) => {
2760
3181
  res.json({ status: "ready" });
2761
3182
  });
2762
- this.app.get("/v1/*", (_req, res) => {
2763
- const legacyIndex = path3.join(__dirname, "../public/index.html");
2764
- if (fs3.existsSync(legacyIndex)) {
2765
- res.sendFile(legacyIndex);
2766
- } else {
2767
- res.status(404).send("Legacy dashboard not found");
2768
- }
2769
- });
2770
3183
  this.app.get("*", (_req, res) => {
2771
- const clientIndex = path3.join(__dirname, "../dist/client/index.html");
2772
- if (fs3.existsSync(clientIndex)) {
2773
- res.sendFile(clientIndex);
3184
+ const clientIndex2 = path3.join(__dirname, "../dist/client/index.html");
3185
+ if (fs3.existsSync(clientIndex2)) {
3186
+ res.sendFile(clientIndex2);
2774
3187
  } else {
2775
3188
  res.status(404).send("Dashboard not found - public files may not be built");
2776
3189
  }
@@ -2796,6 +3209,41 @@ var DashboardServer = class {
2796
3209
  });
2797
3210
  });
2798
3211
  }
3212
+ /** Watch soma-report.json for changes and broadcast updates via WebSocket. */
3213
+ setupSomaReportWatcher() {
3214
+ const somaVault = this.config.somaVault;
3215
+ if (!somaVault) return;
3216
+ const reportPath = path3.join(somaVault, "..", "soma-report.json");
3217
+ const reportDir = path3.dirname(reportPath);
3218
+ if (!fs3.existsSync(reportDir)) return;
3219
+ let debounceTimer = null;
3220
+ const watcher = chokidar2.watch(reportPath, {
3221
+ ignoreInitial: true,
3222
+ persistent: true,
3223
+ awaitWriteFinish: { stabilityThreshold: 500 }
3224
+ });
3225
+ watcher.on("change", () => {
3226
+ if (debounceTimer) clearTimeout(debounceTimer);
3227
+ debounceTimer = setTimeout(() => {
3228
+ var _a, _b;
3229
+ try {
3230
+ const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
3231
+ this.broadcast({ type: "soma-report-updated", data: report });
3232
+ if (report.generatedAt) {
3233
+ this.broadcast({
3234
+ type: "soma-activity",
3235
+ data: {
3236
+ action: "report-updated",
3237
+ description: `Report updated: ${((_a = report.totals) == null ? void 0 : _a.agents) ?? 0} agents, ${((_b = report.totals) == null ? void 0 : _b.insights) ?? 0} insights`,
3238
+ timestamp: report.generatedAt
3239
+ }
3240
+ });
3241
+ }
3242
+ } catch {
3243
+ }
3244
+ }, 500);
3245
+ });
3246
+ }
2799
3247
  /**
2800
3248
  * Filter an agent's traces to valid ExecutionGraphs and convert via loadGraph().
2801
3249
  * Returns only traces with proper nodes (Map or non-empty object), skipping session-only traces.
@@ -3017,24 +3465,49 @@ var DashboardServer = class {
3017
3465
  });
3018
3466
  }
3019
3467
  async start() {
3020
- return new Promise((resolve4) => {
3468
+ return new Promise((resolve5) => {
3021
3469
  const host = this.config.host || "localhost";
3022
3470
  this.server.listen(this.config.port, host, () => {
3023
3471
  console.log(`AgentFlow Dashboard running at http://${host}:${this.config.port}`);
3024
3472
  console.log(`Watching traces in: ${this.config.tracesDir}`);
3025
- resolve4();
3473
+ resolve5();
3026
3474
  });
3027
3475
  });
3028
3476
  }
3477
+ /** Check if any src/client file is newer than the built bundle. */
3478
+ isClientStale(srcDir, distDir) {
3479
+ try {
3480
+ const distIndex = path3.join(distDir, "index.html");
3481
+ if (!fs3.existsSync(distIndex)) return true;
3482
+ const distMtime = fs3.statSync(distIndex).mtimeMs;
3483
+ const check = (dir) => {
3484
+ for (const entry of fs3.readdirSync(dir, { withFileTypes: true })) {
3485
+ const full = path3.join(dir, entry.name);
3486
+ if (entry.isDirectory()) {
3487
+ if (check(full)) return true;
3488
+ } else if (fs3.statSync(full).mtimeMs > distMtime) {
3489
+ return true;
3490
+ }
3491
+ }
3492
+ return false;
3493
+ };
3494
+ return check(srcDir);
3495
+ } catch {
3496
+ return false;
3497
+ }
3498
+ }
3029
3499
  async stop() {
3030
- return new Promise((resolve4) => {
3500
+ return new Promise((resolve5) => {
3031
3501
  this.watcher.stop();
3032
3502
  this.server.close(() => {
3033
3503
  console.log("Dashboard server stopped");
3034
- resolve4();
3504
+ resolve5();
3035
3505
  });
3036
3506
  });
3037
3507
  }
3508
+ getConfigPath() {
3509
+ return this.configPath;
3510
+ }
3038
3511
  getStats() {
3039
3512
  return this.stats.getGlobalStats();
3040
3513
  }