agentflow-dashboard 0.8.2 → 0.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.cjs CHANGED
@@ -37,6 +37,11 @@ var fs3 = __toESM(require("fs"), 1);
37
37
  var import_node_http = require("http");
38
38
  var path3 = __toESM(require("path"), 1);
39
39
  var import_node_url2 = require("url");
40
+ var import_agentflow_core3 = require("agentflow-core");
41
+ var import_chokidar2 = __toESM(require("chokidar"), 1);
42
+ var import_express = __toESM(require("express"), 1);
43
+ var import_express_rate_limit = __toESM(require("express-rate-limit"), 1);
44
+ var import_ws = require("ws");
40
45
 
41
46
  // src/config.ts
42
47
  var import_node_fs = require("fs");
@@ -109,12 +114,6 @@ function getProcessPreference(config) {
109
114
  return config.processPreference ?? null;
110
115
  }
111
116
 
112
- // src/server.ts
113
- var import_agentflow_core3 = require("agentflow-core");
114
- var import_chokidar2 = __toESM(require("chokidar"), 1);
115
- var import_express = __toESM(require("express"), 1);
116
- var import_ws = require("ws");
117
-
118
117
  // src/adapters/agentflow.ts
119
118
  var SKIP_FILES = /* @__PURE__ */ new Set([
120
119
  "workers.json",
@@ -126,7 +125,14 @@ var SKIP_FILES = /* @__PURE__ */ new Set([
126
125
  "models.json",
127
126
  "config.json"
128
127
  ]);
129
- var SKIP_SUFFIXES = ["-state.json", "-config.json", "-watch-state.json", ".tmp", ".bak", ".backup"];
128
+ var SKIP_SUFFIXES = [
129
+ "-state.json",
130
+ "-config.json",
131
+ "-watch-state.json",
132
+ ".tmp",
133
+ ".bak",
134
+ ".backup"
135
+ ];
130
136
  var AgentFlowAdapter = class {
131
137
  name = "agentflow";
132
138
  detect(_dirPath) {
@@ -403,8 +409,14 @@ registerAdapter(new AgentFlowAdapter());
403
409
  var PURPOSE_KEYWORDS = [
404
410
  { keywords: ["email", "mail", "inbox", "smtp"], group: "Email Processors" },
405
411
  { keywords: ["monitor", "watch", "alert", "surveillance"], group: "Monitors" },
406
- { keywords: ["digest", "newsletter", "summary", "report", "briefing"], group: "Digests & Reports" },
407
- { keywords: ["curator", "janitor", "distiller", "surveyor", "worker", "indexer"], group: "Workers" },
412
+ {
413
+ keywords: ["digest", "newsletter", "summary", "report", "briefing"],
414
+ group: "Digests & Reports"
415
+ },
416
+ {
417
+ keywords: ["curator", "janitor", "distiller", "surveyor", "worker", "indexer"],
418
+ group: "Workers"
419
+ },
408
420
  { keywords: ["cron", "schedule", "timer", "periodic"], group: "Scheduled Jobs" },
409
421
  { keywords: ["search", "scrape", "crawl", "fetch"], group: "Data Collection" },
410
422
  { keywords: ["embed", "vector", "index"], group: "Embeddings" }
@@ -436,6 +448,7 @@ function capitalize(s) {
436
448
  return s.charAt(0).toUpperCase() + s.slice(1);
437
449
  }
438
450
  function deduplicateAgents(agents) {
451
+ var _a, _b, _c, _d;
439
452
  const tagged = agents.map((a) => ({
440
453
  ...a,
441
454
  ...extractSource(a.agentId)
@@ -452,14 +465,14 @@ function deduplicateAgents(agents) {
452
465
  const mergedIds = /* @__PURE__ */ new Set();
453
466
  const mergedAgents = [];
454
467
  for (const [_key, group] of suffixGroups) {
455
- const suffix = extractSuffix(group[0].localId);
468
+ const suffix = extractSuffix((_a = group[0]) == null ? void 0 : _a.localId);
456
469
  if (group.length < 2) continue;
457
470
  const prefixes = new Set(group.map((a) => a.localId.split("-")[0]));
458
471
  if (prefixes.size < 2) continue;
459
472
  const longPrefixes = [...prefixes].filter((p) => p !== suffix && p.length > 2);
460
473
  if (longPrefixes.length >= 2) continue;
461
474
  const merged = {
462
- agentId: group[0].source === "agentflow" ? suffix : `${group[0].source}:${suffix}`,
475
+ agentId: ((_b = group[0]) == null ? void 0 : _b.source) === "agentflow" ? suffix : `${(_c = group[0]) == null ? void 0 : _c.source}:${suffix}`,
463
476
  displayName: suffix,
464
477
  totalExecutions: group.reduce((s, a) => s + a.totalExecutions, 0),
465
478
  successfulExecutions: group.reduce((s, a) => s + a.successfulExecutions, 0),
@@ -470,7 +483,7 @@ function deduplicateAgents(agents) {
470
483
  triggers: {},
471
484
  recentActivity: group.flatMap((a) => a.recentActivity).sort((a, b) => b.timestamp - a.timestamp).slice(0, 50),
472
485
  sources: group.map((a) => a.agentId),
473
- adapterSource: group[0].source
486
+ adapterSource: (_d = group[0]) == null ? void 0 : _d.source
474
487
  };
475
488
  merged.successRate = merged.totalExecutions > 0 ? merged.successfulExecutions / merged.totalExecutions * 100 : 0;
476
489
  const totalExecTime = group.reduce((s, a) => s + a.avgExecutionTime * a.totalExecutions, 0);
@@ -886,7 +899,9 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
886
899
  ...getSkipFiles(this.userConfig)
887
900
  ]);
888
901
  this.userSkipDirs = new Set(getSkipDirectories(this.userConfig));
889
- this.allWatchDirs = [...new Set([this.tracesDir, ...this.dataDirs].map((d) => path.resolve(d)))];
902
+ this.allWatchDirs = [
903
+ ...new Set([this.tracesDir, ...this.dataDirs].map((d) => path.resolve(d)))
904
+ ];
890
905
  this.ensureTracesDir();
891
906
  this.loadExistingFiles();
892
907
  this.archiveOldTraces();
@@ -896,7 +911,7 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
896
911
  /** Move trace files older than maxAgeMs into archive/YYYY-MM/ subdirectories. */
897
912
  archiveOldTraces() {
898
913
  const cutoff = Date.now() - this.maxAgeMs;
899
- let archived = 0;
914
+ const _archived = 0;
900
915
  for (const dir of this.allWatchDirs) {
901
916
  if (!fs.existsSync(dir)) continue;
902
917
  try {
@@ -913,7 +928,8 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
913
928
  try {
914
929
  const entries = fs.readdirSync(dir, { withFileTypes: true });
915
930
  for (const entry of entries) {
916
- if (entry.name.startsWith(".") || entry.name === "archive" || this.userSkipDirs.has(entry.name)) continue;
931
+ if (entry.name.startsWith(".") || entry.name === "archive" || this.userSkipDirs.has(entry.name))
932
+ continue;
917
933
  const fullPath = path.join(dir, entry.name);
918
934
  if (entry.isDirectory()) {
919
935
  archived += this.archiveDirectory(fullPath, cutoff, depth + 1);
@@ -2160,7 +2176,9 @@ var path2 = __toESM(require("path"), 1);
2160
2176
  var import_node_url = require("url");
2161
2177
  var import_meta = {};
2162
2178
  var __cliDirname = path2.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
2163
- var VERSION = JSON.parse(fs2.readFileSync(path2.resolve(__cliDirname, "../package.json"), "utf-8")).version;
2179
+ var VERSION = JSON.parse(
2180
+ fs2.readFileSync(path2.resolve(__cliDirname, "../package.json"), "utf-8")
2181
+ ).version;
2164
2182
  function getLanAddress() {
2165
2183
  const interfaces = os.networkInterfaces();
2166
2184
  for (const name of Object.keys(interfaces)) {
@@ -2418,6 +2436,17 @@ var DashboardServer = class {
2418
2436
  userConfig;
2419
2437
  configPath;
2420
2438
  setupExpress() {
2439
+ this.app.use(
2440
+ "/api/",
2441
+ (0, import_express_rate_limit.default)({
2442
+ windowMs: 60 * 1e3,
2443
+ // 1 minute
2444
+ max: 300,
2445
+ // 300 requests per minute per IP
2446
+ standardHeaders: true,
2447
+ legacyHeaders: false
2448
+ })
2449
+ );
2421
2450
  if (this.config.enableCors) {
2422
2451
  this.app.use((_req, res, next) => {
2423
2452
  res.header("Access-Control-Allow-Origin", "*");
@@ -2444,10 +2473,6 @@ var DashboardServer = class {
2444
2473
  if (fs3.existsSync(clientDir)) {
2445
2474
  this.app.use(import_express.default.static(clientDir));
2446
2475
  }
2447
- const pkgVersion = JSON.parse(fs3.readFileSync(path3.resolve(__dirname, "../package.json"), "utf-8")).version;
2448
- this.app.get("/api/version", (_req, res) => {
2449
- res.json({ version: pkgVersion });
2450
- });
2451
2476
  this.app.get("/api/traces", (req, res) => {
2452
2477
  try {
2453
2478
  const limit = Math.min(Math.max(parseInt(req.query.limit, 10) || 50, 1), 200);
@@ -2664,6 +2689,7 @@ var DashboardServer = class {
2664
2689
  }
2665
2690
  });
2666
2691
  this.app.get("/api/process-model/:agentId", (req, res) => {
2692
+ var _a, _b;
2667
2693
  try {
2668
2694
  const agentId = req.params.agentId;
2669
2695
  const allTraces = this.watcher.getTracesByAgent(agentId);
@@ -2681,8 +2707,8 @@ var DashboardServer = class {
2681
2707
  const nodeArr = Object.values(nodes);
2682
2708
  const sorted = nodeArr.filter((n) => n.name && typeof n.startTime === "number" && n.startTime > 0).sort((a, b) => (a.startTime ?? 0) - (b.startTime ?? 0));
2683
2709
  for (let i = 0; i < sorted.length - 1; i++) {
2684
- const from = sorted[i].name;
2685
- const to = sorted[i + 1].name;
2710
+ const from = (_a = sorted[i]) == null ? void 0 : _a.name;
2711
+ const to = (_b = sorted[i + 1]) == null ? void 0 : _b.name;
2686
2712
  const key = `${from}|||${to}`;
2687
2713
  transMap.set(key, (transMap.get(key) ?? 0) + 1);
2688
2714
  }
@@ -2781,7 +2807,11 @@ var DashboardServer = class {
2781
2807
  try {
2782
2808
  const reportPath = path3.join(somaVault, "..", "soma-report.json");
2783
2809
  if (!fs3.existsSync(reportPath)) {
2784
- return res.json({ available: false, teaser: false, message: "No report file yet. Run soma watch." });
2810
+ return res.json({
2811
+ available: false,
2812
+ teaser: false,
2813
+ message: "No report file yet. Run soma watch."
2814
+ });
2785
2815
  }
2786
2816
  const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
2787
2817
  res.json(report);
@@ -2805,7 +2835,9 @@ var DashboardServer = class {
2805
2835
  available: true,
2806
2836
  layers: report.layers ?? { archive: 0, working: 0, emerging: 0, canon: 0 },
2807
2837
  governance: report.governance ?? { pending: 0, promoted: 0, rejected: 0 },
2808
- insights: (report.insights ?? []).filter((i) => i.layer === "emerging" && i.proposal_status === "pending"),
2838
+ insights: (report.insights ?? []).filter(
2839
+ (i) => i.layer === "emerging" && i.proposal_status === "pending"
2840
+ ),
2809
2841
  canon: (report.insights ?? []).filter((i) => i.layer === "canon"),
2810
2842
  generatedAt: report.generatedAt
2811
2843
  });
@@ -2814,21 +2846,23 @@ var DashboardServer = class {
2814
2846
  res.status(500).json({ available: false, message: "Failed to read governance data" });
2815
2847
  }
2816
2848
  });
2817
- const sanitizeArg = (s) => s.replace(/[^a-zA-Z0-9_\-.:]/g, "");
2818
- const sanitizeReason = (s) => s.replace(/["`$\\]/g, "").slice(0, 500);
2849
+ const isValidId = (s) => /^[a-zA-Z0-9_\-.:]+$/.test(s);
2819
2850
  this.app.post("/api/soma/governance/promote", (req, res) => {
2820
2851
  var _a;
2821
2852
  const somaVault = this.config.somaVault;
2822
2853
  if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
2823
2854
  const { entryId } = req.body ?? {};
2824
- if (!entryId) return res.status(400).json({ error: "entryId required" });
2855
+ if (!entryId || !isValidId(String(entryId)))
2856
+ return res.status(400).json({ error: "Invalid entryId" });
2825
2857
  try {
2826
- const { execSync: execSync2 } = require("child_process");
2827
- const safeId = sanitizeArg(String(entryId));
2828
- const result = execSync2(`npx soma governance promote ${safeId} --vault "${somaVault}"`, {
2829
- encoding: "utf-8",
2830
- timeout: 1e4
2831
- });
2858
+ const result = (0, import_node_child_process.execFileSync)(
2859
+ "npx",
2860
+ ["soma", "governance", "promote", String(entryId), "--vault", somaVault],
2861
+ {
2862
+ encoding: "utf-8",
2863
+ timeout: 1e4
2864
+ }
2865
+ );
2832
2866
  res.json({ success: true, message: result.trim() });
2833
2867
  } catch (error) {
2834
2868
  res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
@@ -2839,15 +2873,27 @@ var DashboardServer = class {
2839
2873
  const somaVault = this.config.somaVault;
2840
2874
  if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
2841
2875
  const { entryId, reason } = req.body ?? {};
2842
- if (!entryId || !reason) return res.status(400).json({ error: "entryId and reason required" });
2876
+ if (!entryId || !isValidId(String(entryId)))
2877
+ return res.status(400).json({ error: "Invalid entryId" });
2878
+ if (!reason || typeof reason !== "string")
2879
+ return res.status(400).json({ error: "reason required" });
2843
2880
  try {
2844
- const { execSync: execSync2 } = require("child_process");
2845
- const safeId = sanitizeArg(String(entryId));
2846
- const safeReason = sanitizeReason(String(reason));
2847
- const result = execSync2(`npx soma governance reject ${safeId} "${safeReason}" --vault "${somaVault}"`, {
2848
- encoding: "utf-8",
2849
- timeout: 1e4
2850
- });
2881
+ const result = (0, import_node_child_process.execFileSync)(
2882
+ "npx",
2883
+ [
2884
+ "soma",
2885
+ "governance",
2886
+ "reject",
2887
+ String(entryId),
2888
+ String(reason).slice(0, 500),
2889
+ "--vault",
2890
+ somaVault
2891
+ ],
2892
+ {
2893
+ encoding: "utf-8",
2894
+ timeout: 1e4
2895
+ }
2896
+ );
2851
2897
  res.json({ success: true, message: result.trim() });
2852
2898
  } catch (error) {
2853
2899
  res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
@@ -2857,13 +2903,16 @@ var DashboardServer = class {
2857
2903
  var _a;
2858
2904
  const somaVault = this.config.somaVault;
2859
2905
  if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
2906
+ if (!isValidId(String(req.params.id))) return res.status(400).json({ error: "Invalid id" });
2860
2907
  try {
2861
- const { execSync: execSync2 } = require("child_process");
2862
- const safeId = sanitizeArg(String(req.params.id));
2863
- const result = execSync2(`npx soma governance show ${safeId} --vault "${somaVault}"`, {
2864
- encoding: "utf-8",
2865
- timeout: 1e4
2866
- });
2908
+ const result = (0, import_node_child_process.execFileSync)(
2909
+ "npx",
2910
+ ["soma", "governance", "show", String(req.params.id), "--vault", somaVault],
2911
+ {
2912
+ encoding: "utf-8",
2913
+ timeout: 1e4
2914
+ }
2915
+ );
2867
2916
  res.json({ available: true, output: result.trim() });
2868
2917
  } catch (error) {
2869
2918
  res.status(404).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
@@ -2886,16 +2935,24 @@ var DashboardServer = class {
2886
2935
  const somaVault = this.config.somaVault;
2887
2936
  if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
2888
2937
  const { name, enforcement, scope, conditions } = req.body ?? {};
2889
- if (!name) return res.status(400).json({ error: "name required" });
2938
+ if (!name || !isValidId(String(name)))
2939
+ return res.status(400).json({ error: "Invalid policy name" });
2940
+ const enf = String(enforcement || "warn");
2941
+ if (!isValidId(enf)) return res.status(400).json({ error: "Invalid enforcement value" });
2890
2942
  try {
2891
- const safeName = sanitizeArg(String(name));
2892
- const safeEnf = sanitizeArg(String(enforcement || "warn"));
2893
- const safeScope = sanitizeReason(String(scope || "all"));
2894
- const safeCond = sanitizeReason(String(conditions || ""));
2895
- const result = (0, import_node_child_process.execSync)(
2896
- `npx soma policy create "${safeName}" --enforcement ${safeEnf} --scope "${safeScope}" --conditions "${safeCond}" --vault "${somaVault}"`,
2897
- { encoding: "utf-8", timeout: 1e4 }
2898
- );
2943
+ const args = [
2944
+ "soma",
2945
+ "policy",
2946
+ "create",
2947
+ String(name),
2948
+ "--enforcement",
2949
+ enf,
2950
+ "--vault",
2951
+ somaVault
2952
+ ];
2953
+ if (scope) args.push("--scope", String(scope).slice(0, 500));
2954
+ if (conditions) args.push("--conditions", String(conditions).slice(0, 500));
2955
+ const result = (0, import_node_child_process.execFileSync)("npx", args, { encoding: "utf-8", timeout: 1e4 });
2899
2956
  res.json({ success: true, message: result.trim() });
2900
2957
  } catch (error) {
2901
2958
  res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
@@ -2905,11 +2962,16 @@ var DashboardServer = class {
2905
2962
  var _a;
2906
2963
  const somaVault = this.config.somaVault;
2907
2964
  if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
2965
+ if (!isValidId(String(req.params.name)))
2966
+ return res.status(400).json({ error: "Invalid policy name" });
2908
2967
  try {
2909
- const safeName = sanitizeArg(String(req.params.name));
2910
- const result = (0, import_node_child_process.execSync)(
2911
- `npx soma policy delete "${safeName}" --vault "${somaVault}"`,
2912
- { encoding: "utf-8", timeout: 1e4 }
2968
+ const result = (0, import_node_child_process.execFileSync)(
2969
+ "npx",
2970
+ ["soma", "policy", "delete", String(req.params.name), "--vault", somaVault],
2971
+ {
2972
+ encoding: "utf-8",
2973
+ timeout: 1e4
2974
+ }
2913
2975
  );
2914
2976
  res.json({ success: true, message: result.trim() });
2915
2977
  } catch (error) {
@@ -2927,16 +2989,28 @@ var DashboardServer = class {
2927
2989
  ...(report.agents ?? []).map((a) => ({ ...a, type: "agent", id: a.name })),
2928
2990
  ...(report.insights ?? []).map((i, idx) => {
2929
2991
  var _a;
2930
- return { ...i, type: i.type || "insight", id: ((_a = i.title) == null ? void 0 : _a.replace(/\s+/g, "-").toLowerCase()) || `insight-${idx}` };
2992
+ return {
2993
+ ...i,
2994
+ type: i.type || "insight",
2995
+ id: ((_a = i.title) == null ? void 0 : _a.replace(/\s+/g, "-").toLowerCase()) || `insight-${idx}`
2996
+ };
2931
2997
  }),
2932
2998
  ...(report.policies ?? []).map((p) => ({ ...p, type: "policy", id: p.name }))
2933
2999
  ];
2934
- const { type, layer, q, limit: limitStr, offset: offsetStr } = req.query;
3000
+ const {
3001
+ type,
3002
+ layer,
3003
+ q,
3004
+ limit: limitStr,
3005
+ offset: offsetStr
3006
+ } = req.query;
2935
3007
  if (type) entities = entities.filter((e) => e.type === type);
2936
3008
  if (layer) entities = entities.filter((e) => e.layer === layer);
2937
3009
  if (q) {
2938
3010
  const lq = q.toLowerCase();
2939
- entities = entities.filter((e) => (e.name || e.title || "").toLowerCase().includes(lq) || (e.claim || e.body || "").toLowerCase().includes(lq));
3011
+ entities = entities.filter(
3012
+ (e) => (e.name || e.title || "").toLowerCase().includes(lq) || (e.claim || e.body || "").toLowerCase().includes(lq)
3013
+ );
2940
3014
  }
2941
3015
  const total = entities.length;
2942
3016
  const offset = parseInt(offsetStr || "0", 10);
@@ -3027,9 +3101,7 @@ var DashboardServer = class {
3027
3101
  const orphans = uniqueProcesses.filter(
3028
3102
  (p) => !allKnownPids.has(p.pid) && p.pid !== process.pid && p.pid !== process.ppid
3029
3103
  );
3030
- const problems = services.flatMap(
3031
- (s) => s.audit.problems.map((p) => `[${s.name}] ${p}`)
3032
- );
3104
+ const problems = services.flatMap((s) => s.audit.problems.map((p) => `[${s.name}] ${p}`));
3033
3105
  const result = {
3034
3106
  // Backward-compatible fields from primary service
3035
3107
  pidFile: (primary == null ? void 0 : primary.audit.pidFile) ?? null,
@@ -3084,19 +3156,24 @@ var DashboardServer = class {
3084
3156
  }
3085
3157
  } catch {
3086
3158
  }
3087
- const watched = [...new Set([
3088
- this.config.tracesDir,
3089
- ...this.config.dataDirs || [],
3090
- ...extraDirs
3091
- ].map((w) => path3.resolve(w)))];
3159
+ const watched = [
3160
+ ...new Set(
3161
+ [this.config.tracesDir, ...this.config.dataDirs || [], ...extraDirs].map(
3162
+ (w) => path3.resolve(w)
3163
+ )
3164
+ )
3165
+ ];
3092
3166
  const discovered = [];
3093
3167
  const svcNames = getSystemdServices(this.userConfig);
3094
3168
  if (svcNames.length > 0) {
3095
3169
  try {
3096
- const { execSync: execSync2 } = require("child_process");
3097
- const raw = execSync2(
3098
- `systemctl --user show --property=ExecStart --no-pager ${svcNames.join(" ")} 2>/dev/null`,
3099
- { encoding: "utf8", timeout: 5e3 }
3170
+ const raw = (0, import_node_child_process.execFileSync)(
3171
+ "systemctl",
3172
+ ["--user", "show", "--property=ExecStart", "--no-pager", ...svcNames],
3173
+ {
3174
+ encoding: "utf8",
3175
+ timeout: 5e3
3176
+ }
3100
3177
  );
3101
3178
  for (const line of raw.split("\n")) {
3102
3179
  const match = line.match(/path=([^\s;]+)/);
@@ -3128,10 +3205,19 @@ var DashboardServer = class {
3128
3205
  this.app.post("/api/directories", import_express.default.json(), (req, res) => {
3129
3206
  try {
3130
3207
  const { add, remove } = req.body;
3131
- if (add && !fs3.existsSync(add)) {
3132
- return res.status(400).json({ error: `Directory does not exist: ${add}` });
3208
+ if (add) {
3209
+ const resolved = path3.resolve(add);
3210
+ if (resolved !== add || add.includes("..")) {
3211
+ return res.status(400).json({ error: "Invalid directory path" });
3212
+ }
3213
+ if (!fs3.existsSync(resolved)) {
3214
+ return res.status(400).json({ error: `Directory does not exist: ${add}` });
3215
+ }
3133
3216
  }
3134
- const configPath = path3.join(process.env.HOME ?? "/home/trader", ".agentflow/dashboard-config.json");
3217
+ const configPath = path3.join(
3218
+ process.env.HOME ?? "/home/trader",
3219
+ ".agentflow/dashboard-config.json"
3220
+ );
3135
3221
  let config = {};
3136
3222
  try {
3137
3223
  if (fs3.existsSync(configPath)) {
@@ -3341,13 +3427,31 @@ var DashboardServer = class {
3341
3427
  isVirtual: false
3342
3428
  });
3343
3429
  }
3344
- const rootSteps = new Set(model.steps);
3345
- const childSteps = new Set(model.transitions.map((t) => t.to));
3346
- const leafSteps = new Set(model.steps);
3347
- for (const t of model.transitions) {
3348
- }
3349
- nodes.push({ id: "[START]", label: "[START]", count: model.totalGraphs, frequency: 1, avgDuration: 0, failRate: 0, p95Duration: 0, isVirtual: true });
3350
- nodes.push({ id: "[END]", label: "[END]", count: model.totalGraphs, frequency: 1, avgDuration: 0, failRate: 0, p95Duration: 0, isVirtual: true });
3430
+ const _rootSteps = new Set(model.steps);
3431
+ const _childSteps = new Set(model.transitions.map((t) => t.to));
3432
+ const _leafSteps = new Set(model.steps);
3433
+ for (const _t of model.transitions) {
3434
+ }
3435
+ nodes.push({
3436
+ id: "[START]",
3437
+ label: "[START]",
3438
+ count: model.totalGraphs,
3439
+ frequency: 1,
3440
+ avgDuration: 0,
3441
+ failRate: 0,
3442
+ p95Duration: 0,
3443
+ isVirtual: true
3444
+ });
3445
+ nodes.push({
3446
+ id: "[END]",
3447
+ label: "[END]",
3448
+ count: model.totalGraphs,
3449
+ frequency: 1,
3450
+ avgDuration: 0,
3451
+ failRate: 0,
3452
+ p95Duration: 0,
3453
+ isVirtual: true
3454
+ });
3351
3455
  const edges = model.transitions.map((t) => ({
3352
3456
  source: t.from,
3353
3457
  target: t.to,
@@ -3367,7 +3471,10 @@ var DashboardServer = class {
3367
3471
  }
3368
3472
  }
3369
3473
  const maxEdgeCount = Math.max(...edges.map((e) => e.count), 1);
3370
- const maxNodeCount = Math.max(...nodes.filter((n) => !n.isVirtual).map((n) => n.count), 1);
3474
+ const maxNodeCount = Math.max(
3475
+ ...nodes.filter((n) => !n.isVirtual).map((n) => n.count),
3476
+ 1
3477
+ );
3371
3478
  return { agentId, totalTraces: model.totalGraphs, nodes, edges, maxEdgeCount, maxNodeCount };
3372
3479
  }
3373
3480
  /**
package/dist/server.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  DashboardServer
3
- } from "./chunk-EG254FLY.js";
3
+ } from "./chunk-YLQ5MVCW.js";
4
4
  export {
5
5
  DashboardServer
6
6
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentflow-dashboard",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
4
4
  "description": "Real-time monitoring dashboard for AgentFlow - Visualize agent execution graphs and performance",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -34,12 +34,14 @@
34
34
  "test:e2e": "playwright test",
35
35
  "test:e2e:server": "tsx tests/e2e/test-server.ts",
36
36
  "test:all": "npm run test:unit && npm run test:integration && npm run test:performance && npm run test:e2e",
37
- "test:ci": "npm run test:unit && npm run test:integration"
37
+ "test:ci": "npm run test:unit && npm run test:integration",
38
+ "prepublishOnly": "npm run build"
38
39
  },
39
40
  "dependencies": {
40
41
  "agentflow-core": "^0.8.0",
41
42
  "chokidar": "^3.5.3",
42
43
  "express": "^4.18.2",
44
+ "express-rate-limit": "^8.3.1",
43
45
  "react": "^19.1.0",
44
46
  "react-dom": "^19.1.0",
45
47
  "ws": "^8.16.0"
@@ -50,14 +52,14 @@
50
52
  "@types/react": "^19.1.0",
51
53
  "@types/react-dom": "^19.1.0",
52
54
  "@types/ws": "^8.5.10",
53
- "@vitejs/plugin-react": "^4.4.1",
54
- "@vitest/coverage-v8": "^3.0.0",
55
+ "@vitejs/plugin-react": "^6.0.1",
56
+ "@vitest/coverage-v8": "^4.1.0",
55
57
  "get-port": "^7.0.0",
56
58
  "supertest": "^7.0.0",
57
59
  "tsup": "^8.4.0",
58
60
  "tsx": "^4.19.0",
59
- "vite": "^6.3.5",
60
- "vitest": "^3.0.0",
61
+ "vite": "^8.0.1",
62
+ "vitest": "^4.1.0",
61
63
  "ws": "^8.16.0"
62
64
  },
63
65
  "keywords": [
@@ -70,5 +72,17 @@
70
72
  "observability",
71
73
  "real-time"
72
74
  ],
73
- "license": "MIT"
75
+ "license": "Apache-2.0 WITH Commons-Clause-1.0",
76
+ "repository": {
77
+ "type": "git",
78
+ "url": "https://github.com/ClemenceChee/AgentFlow.git"
79
+ },
80
+ "homepage": "https://github.com/ClemenceChee/AgentFlow#readme",
81
+ "bugs": {
82
+ "url": "https://github.com/ClemenceChee/AgentFlow/issues"
83
+ },
84
+ "author": "Clemence Chee",
85
+ "engines": {
86
+ "node": ">=20"
87
+ }
74
88
  }
@@ -1 +0,0 @@
1
- :root{--bg:#0d1117;--bg2:#161b22;--bg3:#1c2129;--bgh:#21262d;--bd:#30363d;--bdm:#21262d;--t1:#e6edf3;--t2:#8b949e;--t3:#6e7681;--ok:#3fb950;--warn:#d29922;--fail:#f85149;--info:#58a6ff;--f:sans-serif;--fm:"SF Mono",Menlo,monospace;--xs:.85rem;--sm:.9rem;--base:1rem;--lg:1.15rem;--xl:1.35rem;--s1:4px;--s2:8px;--s3:12px;--s4:16px;--s5:24px;--r:5px}*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}body{font-family:var(--f);font-size:var(--base);color:var(--t1);background:var(--bg);line-height:1.4;-webkit-font-smoothing:antialiased}.dashboard{display:flex;flex-direction:column;height:100vh;overflow:hidden}.workspace{display:flex;flex:1;min-height:0;overflow:hidden;border-top:1px solid var(--bd)}.workspace__main{flex:1;overflow-y:auto;overflow-x:hidden}.workspace__empty{display:flex;align-items:center;justify-content:center;height:100%;color:var(--t3);font-size:var(--sm)}.health-banner{display:flex;align-items:center;gap:var(--s4);padding:0 var(--s4);height:42px;background:var(--bg2);border-bottom:1px solid var(--bd);flex-shrink:0}.health-banner__title{font-family:var(--fm);font-size:var(--base);font-weight:700;margin-right:var(--s1)}.hb-version{font-size:11px;color:var(--t3);font-family:var(--fm);margin-right:var(--s3)}.hb-live{display:inline-flex;align-items:center;gap:4px;font-size:11px;font-weight:700;letter-spacing:.06em;padding:1px 6px;border-radius:3px;margin-right:var(--s3)}.hb-live--on{color:var(--ok);background:#3fb9501a}.hb-live--off{color:var(--fail);background:#f851491a}.hb-live__dot{width:6px;height:6px;border-radius:50%;background:currentColor}.hb-live__dot--pulse{animation:livePulse 2s ease-in-out infinite}@keyframes livePulse{0%,to{opacity:1}50%{opacity:.3}}.health-banner__stats{display:flex;gap:var(--s4);align-items:center}.stat-cell{display:flex;flex-direction:column;align-items:center}.stat-cell__value{font-family:var(--fm);font-size:var(--sm);font-weight:700;line-height:1}.stat-cell__label{font-size:11px;color:var(--t3);text-transform:uppercase;letter-spacing:.06em}.stat-cell__sparkline{display:flex;gap:1px;margin-top:2px}.spark{width:2px;height:6px;border-radius:1px}.spark--ok{background:var(--ok);opacity:.4}.spark--fail{background:var(--fail)}.dot{display:inline-block;width:7px;height:7px;border-radius:50%;flex-shrink:0}.dot--ok{background:var(--ok)}.dot--fail{background:var(--fail)}.dot--warn{background:var(--warn)}.top-section{flex-shrink:0;max-height:33vh;overflow-y:auto;background:var(--bg2);border-bottom:2px solid var(--bd)}.chip-row{display:flex;gap:var(--s1);flex-wrap:wrap;padding:var(--s1) var(--s4);border-bottom:1px solid var(--bd);background:#161b2299;align-items:center}.chip-row__label{font-size:11px;color:var(--t3);text-transform:uppercase;letter-spacing:.05em;font-weight:600;margin-right:var(--s2);white-space:nowrap}.schip{display:inline-flex;align-items:center;gap:3px;font-size:11px;font-family:var(--fm);padding:2px 6px;border-radius:10px;border:1px solid var(--bd);color:var(--t2)}.schip--ok{border-color:#3fb95033}.schip--fail{border-color:#f851494d;color:var(--fail)}.schip--off{opacity:.5}.schip--infra{border-color:#58a6ff33}.schip--worker{padding:1px 6px;border-radius:var(--r)}.schip__name{font-weight:600;color:var(--t1)}.schip__detail{color:var(--t3)}.schip__state{color:var(--t3);font-style:italic}.chip{font-size:12px;font-family:var(--fm);padding:1px 8px;border-radius:10px;border:1px solid var(--bd);color:var(--t2)}.chip--ok{border-color:#3fb9504d}.chip--fail{border-color:#f851494d;color:var(--fail)}.chip--off{opacity:.5}.chip--infra{border-color:#58a6ff4d}.agroup{border-bottom:1px solid var(--bd)}.agroup__head{display:flex;width:100%;align-items:center;gap:var(--s2);padding:var(--s1) var(--s4);background:transparent;border:none;color:var(--t1);cursor:pointer;font-size:var(--xs);text-align:left}.agroup__head:hover{background:var(--bgh)}.agroup__expand{font-size:11px;color:var(--t3);width:12px}.agroup__name{font-weight:600}.agroup__stats{color:var(--t2);font-family:var(--fm)}.agroup__fail{color:var(--fail);font-weight:600}.agroup__svc{font-size:11px;color:var(--t3);font-family:var(--fm)}.agroup__svc--ok{color:var(--ok)}.agroup__count{margin-left:auto;color:var(--t3)}.agroup__body{padding:var(--s1) var(--s4);display:flex;flex-wrap:wrap;gap:var(--s3);align-items:flex-start}.asubgroup{flex:1;min-width:200px}.asubgroup__label{font-size:11px;color:var(--t3);text-transform:uppercase;letter-spacing:.06em;padding:var(--s1) 0;font-weight:600}.asubgroup__cards{display:flex;gap:var(--s1);flex-wrap:wrap}.acard__merged{font-size:12px;color:var(--info);font-style:italic}.agent-row{display:flex;gap:var(--s2);flex-wrap:wrap;padding:var(--s2) var(--s4)}.acard{background:var(--bg3);border:1px solid var(--bd);border-radius:var(--r);padding:var(--s1) var(--s2);cursor:pointer;text-align:left;min-width:120px;flex:1 1 140px;max-width:220px;transition:border-color .1s;font-size:var(--xs)}.acard:hover{border-color:var(--info)}.acard--sel{border-color:var(--info);background:#58a6ff0f}.acard--fail{border-left:3px solid var(--fail)}.acard__r1{display:flex;align-items:center;gap:3px;margin-bottom:1px}.acard__name{font-family:var(--fm);font-size:var(--xs);font-weight:600;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.acard__source{font-size:12px;padding:0 4px;border-radius:2px;background:#58a6ff26;color:var(--info);text-transform:uppercase;letter-spacing:.04em;flex-shrink:0}.acard__pct{font-family:var(--fm);font-size:var(--xs);color:var(--ok)}.acard__pct--warn{color:var(--warn)}.acard__r2{display:flex;gap:var(--s2);font-size:12px;color:var(--t3)}.acard__failn{color:var(--fail);font-weight:700}.acard__spark{display:flex;gap:1px;align-items:flex-end;height:8px;margin-top:2px;overflow:hidden}.sk{width:2px;min-height:2px;border-radius:1px;flex-shrink:0}.sk--ok{background:var(--ok);opacity:.4}.sk--fail{background:var(--fail)}.exec-sidebar{width:270px;overflow-y:auto;border-right:2px solid var(--bd);flex-shrink:0;background:var(--bg2)}.exec-sidebar__head{display:flex;justify-content:space-between;padding:var(--s2) var(--s3);font-size:var(--xs);font-weight:600;color:var(--t3);text-transform:uppercase;letter-spacing:.06em;border-bottom:1px solid var(--bdm);position:sticky;top:0;background:var(--bg2);z-index:1}.exec-sidebar__agent,.exec-sidebar__count{font-family:var(--fm)}.exec-sidebar__fails{color:var(--fail);font-weight:700}.erow{display:flex;width:100%;align-items:center;gap:4px;padding:3px var(--s2);background:transparent;border:none;border-bottom:1px solid var(--bdm);color:var(--t2);cursor:pointer;font-size:12px;text-align:left}.erow:hover{background:var(--bgh)}.erow--sel{background:var(--bg3);border-left:2px solid var(--info)}.erow--fail{color:var(--fail)}.erow__icon{width:12px;text-align:center;font-size:12px}.erow__time{width:72px;font-size:11px;color:var(--t3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.erow__n{font-family:var(--fm);width:22px;text-align:right}.erow__dur{font-family:var(--fm);width:36px;text-align:right}.erow__bar{flex:1;height:3px;background:var(--bg);border-radius:2px;overflow:hidden}.erow__fill{display:block;height:100%;border-radius:2px}.erow__fill--ok{background:var(--ok);opacity:.4}.erow__fill--fail{background:var(--fail);opacity:.6}.agent-profile{padding:var(--s3);height:100%;display:flex;flex-direction:column}.ap-stats{display:flex;gap:var(--s5);padding:var(--s2) 0;border-bottom:1px solid var(--bdm);margin-bottom:var(--s3);flex-wrap:wrap}.ap-stat{text-align:center}.ap-stat__v{display:block;font-family:var(--fm);font-size:var(--lg);font-weight:700}.ap-stat__l{font-size:11px;color:var(--t3);text-transform:uppercase;letter-spacing:.05em}.ap-tabs{display:flex;gap:0;border-bottom:1px solid var(--bdm);margin-bottom:var(--s3)}.ap-tab{background:transparent;border:none;border-bottom:2px solid transparent;padding:var(--s2) var(--s3);font-size:var(--xs);color:var(--t3);cursor:pointer}.ap-tab:hover{color:var(--t1)}.ap-tab--active{color:var(--t1);border-bottom-color:var(--info)}.ap-content{flex:1;overflow-y:auto}.pmap__controls{display:flex;align-items:center;gap:var(--s4);margin-bottom:var(--s2);font-size:var(--xs);color:var(--t2)}.pmap__slider-label{display:flex;align-items:center;gap:var(--s2)}.pmap__slider{width:120px}.pmap__info{color:var(--t3)}.pmap__svg{display:block}.var-row{padding:var(--s2) 0;border-bottom:1px solid var(--bdm)}.var-row--happy{border-left:3px solid var(--ok);padding-left:var(--s2)}.var-row__header{display:flex;align-items:center;gap:var(--s2);font-size:var(--xs);margin-bottom:var(--s1)}.var-row__rank{font-family:var(--fm);color:var(--t3);width:24px}.var-row__badge{background:#3fb95026;color:var(--ok);font-size:11px;padding:1px 6px;border-radius:3px;font-weight:600}.var-row__count{font-family:var(--fm);color:var(--t2)}.var-row__pct-bar{flex:1;height:4px;background:var(--bg3);border-radius:2px;overflow:hidden}.var-row__pct-fill{height:100%;background:var(--info);border-radius:2px;opacity:.5}.var-row__steps{display:flex;align-items:center;gap:2px;flex-wrap:wrap}.var-row__arrow{color:var(--t3);font-size:12px;margin:0 2px}.var-row__step{font-family:var(--fm);font-size:11px;padding:1px 6px;border:1px solid;border-radius:3px;white-space:nowrap}.bn-layout{display:flex;flex-direction:column;height:100%}.bn-chart-area{flex:1;min-height:200px;max-height:50%;overflow:auto;border:1px solid var(--bdm);border-radius:var(--r);margin-bottom:var(--s2);position:relative}.bn-chart-controls{position:sticky;top:0;right:0;display:flex;justify-content:flex-end;gap:2px;padding:var(--s1);z-index:1;background:#0d1117cc}.bn-ranking{flex:1;overflow-y:auto;min-height:100px}.bn-row{display:flex;align-items:center;gap:var(--s2);padding:var(--s1) 0;font-size:var(--xs)}.bn-row__name{font-family:var(--fm);width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.bn-row__type{color:var(--t3);width:60px}.bn-row__p95{font-family:var(--fm);width:60px;text-align:right;color:var(--fail)}.bn-row__bar{flex:1;height:6px;background:var(--bg3);border-radius:3px;overflow:hidden}.bn-row__fill{height:100%;background:var(--fail);opacity:.6;border-radius:3px}.exec-detail{display:flex;flex-direction:column;height:100%}.ed-header{display:flex;align-items:center;gap:var(--s2);padding:var(--s2) var(--s3);border-bottom:1px solid var(--bdm);flex-shrink:0;flex-wrap:wrap}.ed-header__agent{font-family:var(--fm);font-weight:700;font-size:var(--sm)}.ed-header__meta{font-size:var(--xs);color:var(--t2)}.ed-header__ts{font-size:var(--xs);color:var(--t3);font-family:var(--fm);margin-left:auto}.ed-tag{font-size:11px;padding:1px 6px;background:var(--bg2);border:1px solid var(--bd);border-radius:3px;color:var(--t3);font-family:var(--fm)}.ed-tabs{display:flex;gap:0;border-bottom:1px solid var(--bdm);flex-shrink:0;overflow-x:auto;flex-wrap:wrap}.ed-tab{background:transparent;border:none;border-bottom:2px solid transparent;padding:var(--s1) var(--s2);font-size:var(--xs);color:var(--t3);cursor:pointer;white-space:nowrap}.ed-tab:hover{color:var(--t1)}.ed-tab--active{color:var(--t1);border-bottom-color:var(--info)}.ed-content{flex:1;overflow-y:auto;padding:var(--s3)}.flame__fail-callout{background:#f8514914;border:1px solid rgba(248,81,73,.25);border-radius:var(--r);padding:var(--s2) var(--s3);margin-bottom:var(--s3);font-size:var(--xs)}.flame__fail-title{color:var(--fail);font-weight:700;font-size:var(--sm);margin-bottom:var(--s1)}.flame__fail-item{display:flex;align-items:baseline;gap:var(--s2);padding:1px 0}.flame__fail-ts{font-family:var(--fm);color:var(--t3)}.flame__fail-err{color:var(--fail);font-family:var(--fm);font-size:11px}.flame__range{font-size:var(--xs);color:var(--t2);font-family:var(--fm);margin-bottom:var(--s2)}.flame__axis{display:flex;justify-content:space-between;padding:0 0 var(--s1) 28px;font-size:11px;color:var(--t3);font-family:var(--fm);border-bottom:1px solid var(--bdm)}.flame__row{display:flex;align-items:center;height:24px;position:relative}.flame__depth{width:28px;font-size:11px;color:var(--t3);text-align:right;padding-right:var(--s1);flex-shrink:0;font-family:var(--fm)}.flame__track{flex:1;height:18px;position:relative;background:var(--bg2);border-radius:2px;overflow:visible}.flame__bar{position:absolute;top:1px;height:16px;border-radius:2px;cursor:pointer;display:flex;align-items:center;overflow:hidden;transition:opacity .1s}.flame__bar--hov{z-index:1;box-shadow:0 0 4px #fff3}.flame__bar-label{font-size:12px;color:#fff;padding:0 3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-shadow:0 1px 2px rgba(0,0,0,.5)}.flame__side-label{position:absolute;left:calc(100% + 4px);top:1px;font-size:12px;color:var(--t1);white-space:nowrap;pointer-events:none;font-family:var(--fm)}.flame__tooltip{position:fixed;background:var(--bg3);border:1px solid var(--bd);border-radius:var(--r);padding:var(--s2);font-size:var(--xs);white-space:nowrap;z-index:9999;pointer-events:none;box-shadow:0 4px 16px #0009;min-width:220px;max-width:400px}.flame__tt-title{font-weight:700;font-size:var(--sm);margin-bottom:2px}.flame__tt-type{font-size:var(--xs);margin-bottom:2px}.flame__tt-dur{font-family:var(--fm);color:var(--t2);margin-bottom:4px}.flame__tt-meta{font-size:11px;color:var(--t3)}.flame__tt-err{font-size:var(--xs);color:var(--fail);margin-top:4px}.aflow{position:relative}.af-step{position:relative;padding:1px 0}.af-step--fail{background:#f851490a}.af-step__line{position:absolute;top:-3px;left:11px;width:1px;height:6px;background:var(--bdm)}.af-step__row{display:flex;align-items:center;gap:var(--s2);font-size:var(--xs);padding:2px var(--s2)}.af-step__icon{width:16px;text-align:center;font-size:var(--sm);flex-shrink:0}.af-step__cat{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;width:48px;flex-shrink:0}.af-step__name{font-family:var(--fm);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.af-step__dur{font-family:var(--fm);color:var(--t3);width:48px;text-align:right;flex-shrink:0}.af-step__ts{font-family:var(--fm);color:var(--t3);font-size:11px;width:64px;text-align:right;flex-shrink:0}.af-step__ops{display:flex;gap:var(--s1);align-items:center;padding:1px var(--s2) 1px 32px;flex-wrap:wrap}.af-step__op-tag{font-size:12px;padding:0 4px;border-radius:2px;background:var(--bg);border:1px solid var(--bdm);color:var(--t2);font-family:var(--fm)}.af-step__op-tag--model{border-color:#bc8cff4d;color:#bc8cff}.af-step__op-detail{font-size:12px;color:var(--t3);font-family:var(--fm)}.af-step__err{font-size:var(--xs);color:var(--fail);padding:1px var(--s2) 1px 32px;font-family:var(--fm)}.mv-row{display:flex;gap:var(--s2);flex-wrap:wrap;margin-bottom:var(--s3)}.mv-c{background:var(--bg2);border:1px solid var(--bd);border-radius:var(--r);padding:var(--s2);text-align:center;min-width:70px;flex:1}.mv-v{display:block;font-family:var(--fm);font-size:var(--lg);font-weight:700}.mv-l{font-size:11px;color:var(--t3);text-transform:uppercase;letter-spacing:.05em}.c-ok{color:var(--ok)}.c-fail{color:var(--fail)}.mview__section{font-size:var(--xs);font-weight:600;color:var(--t3);text-transform:uppercase;letter-spacing:.05em;margin:var(--s3) 0 var(--s2)}.mt-row{display:flex;align-items:center;gap:var(--s2);padding:2px 0;font-size:var(--xs)}.mt-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}.mt-name{font-family:var(--fm);width:70px}.mt-cnt{font-family:var(--fm);width:20px;text-align:right}.mt-fail{color:var(--fail);font-weight:700}.mt-dur{color:var(--t3);font-family:var(--fm);width:48px;text-align:right}.mt-bar{flex:1;height:5px;background:var(--bg3);border-radius:3px;overflow:hidden}.mt-fill{height:100%;border-radius:3px;opacity:.5}.dtree{font-size:var(--xs)}.dt-node{display:flex;align-items:center;gap:4px;padding:2px 0}.dt-node--fail{background:#f851490a}.dt-node__name{font-family:var(--fm);font-weight:600}.dt-node__type{font-size:11px;color:var(--t3)}.dt-node__dur{font-family:var(--fm);color:var(--t3);margin-left:auto}.dt-node__err{color:var(--fail);font-family:var(--fm);font-size:11px;margin-left:var(--s2)}.summary-content{font-size:var(--sm)}.sc-grid{display:grid;grid-template-columns:1fr 1fr;gap:var(--s2);margin-bottom:var(--s3)}.sc-label{font-size:11px;color:var(--t3);text-transform:uppercase;display:block}.sc-failures{margin:var(--s3) 0;padding:var(--s2) var(--s3);background:#f851490f;border:1px solid rgba(248,81,73,.2);border-radius:var(--r)}.sc-failures__title{color:var(--fail);font-weight:700;margin-bottom:var(--s1)}.sc-failure{display:flex;gap:var(--s2);font-size:var(--xs);padding:1px 0}.sc-failure__type{color:var(--t3)}.sc-failure__err{color:var(--fail);font-family:var(--fm)}.sc-types{margin-top:var(--s3)}.sc-types__title{font-size:var(--xs);color:var(--t3);text-transform:uppercase;margin-bottom:var(--s1)}.sc-types__list{display:flex;gap:var(--s2);flex-wrap:wrap}.sc-type-badge{font-size:11px;font-family:var(--fm);padding:1px 6px;border:1px solid var(--bd);border-radius:3px;color:var(--t2)}.alert-card{display:flex;align-items:flex-start;gap:var(--s2);padding:var(--s2) var(--s4);font-size:var(--xs);border-bottom:1px solid var(--bdm)}.alert-card--critical{background:#f851490f}.alert-card--warn{background:#d299220f}.alert-icon{flex-shrink:0}.alert-content{flex:1;min-width:0}.alert-title{font-weight:600}.alert-description{color:var(--t3)}.alert-actions{display:flex;gap:var(--s2);margin-top:3px}.alert-action{font-size:11px;font-family:var(--fm);padding:1px 6px;background:var(--bg3);border:1px solid var(--bd);border-radius:3px;color:var(--t2);cursor:pointer}.alert-action:hover{color:var(--t1)}.alert-dismiss{padding:2px;background:transparent;border:none;color:var(--t3);cursor:pointer;font-size:var(--base);flex-shrink:0}.summary-bar{display:flex;gap:var(--s5);padding:var(--s2) var(--s4);background:var(--bg2);border-top:1px solid var(--bd);font-size:var(--xs);flex-shrink:0;height:32px;align-items:center;color:var(--t2)}.tv-chat{display:flex;flex-direction:column;gap:var(--s2)}.tv-bubble{background:var(--bg2);border:1px solid var(--bd);border-radius:12px;padding:var(--s2) var(--s3);max-width:85%;font-size:var(--xs)}.tv-bubble--right{align-self:flex-end;background:#58a6ff14;border-color:#58a6ff33}.tv-bubble--user{border-color:#58a6ff40}.tv-bubble--assistant{border-color:#3fb95033}.tv-bubble--tool{border-color:#d2992233;background:#d299220a}.tv-bubble--thinking{border-color:var(--bdm);opacity:.7;border-style:dashed}.tv-bubble--system{border-color:var(--bdm);background:var(--bg3)}.tv-bubble--event{border-color:var(--bdm)}.tv-bubble--error{border-color:#f851494d;background:#f851490a}.tv-bubble__header{display:flex;align-items:center;gap:var(--s2);margin-bottom:var(--s1);flex-wrap:wrap}.tv-bubble__icon{font-size:var(--sm)}.tv-bubble__role{font-weight:600;font-size:var(--xs)}.tv-bubble__model{color:var(--t3);font-family:var(--fm);font-size:11px}.tv-bubble__tokens{color:var(--t3);font-size:11px;font-family:var(--fm)}.tv-bubble__time{color:var(--t3);margin-left:auto;font-family:var(--fm);font-size:11px}.tv-bubble__tool{margin:var(--s1) 0}.tv-bubble__tool-name{font-family:var(--fm);font-weight:600;color:var(--warn);font-size:var(--xs)}.tv-bubble__content{white-space:pre-wrap;word-break:break-word;line-height:1.5;color:var(--t1)}.tv-bubble__error{color:var(--fail);font-weight:600;margin-top:var(--s1)}.tv-code{background:var(--bg);border:1px solid var(--bdm);border-radius:6px;padding:var(--s2);font-family:var(--fm);font-size:12px;overflow-x:auto;max-height:150px;overflow-y:auto;white-space:pre;margin:var(--s1) 0;color:var(--t2)}.tv-code--error{border-color:#f8514933;color:var(--fail)}.tv-thinking-btn{background:transparent;border:none;color:var(--t3);cursor:pointer;font-size:var(--xs);padding:var(--s1) 0}.tv-bubble--left{align-self:flex-start}.tv-origin{font-size:7px;padding:1px 4px;border-radius:3px;text-transform:uppercase;letter-spacing:.04em;font-weight:700}.tv-origin--user{background:#58a6ff26;color:var(--info)}.tv-origin--agent{background:#3fb9501f;color:var(--ok)}.tv-origin--system{background:#6e768126;color:var(--t3)}.settings-overlay{position:fixed;top:0;right:0;bottom:0;left:0;background:#00000080;z-index:100;display:flex;justify-content:flex-end}.settings-panel{width:420px;max-width:90vw;background:var(--bg2);border-left:1px solid var(--bd);overflow-y:auto;padding:var(--s4)}.sp-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--s4)}.sp-head h3{font-size:var(--base);font-weight:600}.sp-close{background:transparent;border:none;color:var(--t3);cursor:pointer;font-size:var(--xl)}.sp-error{padding:var(--s2);background:#f851491a;border:1px solid rgba(248,81,73,.3);border-radius:var(--r);font-size:var(--xs);color:var(--fail);margin-bottom:var(--s3)}.sp-section{font-size:var(--xs);color:var(--t3);text-transform:uppercase;letter-spacing:.06em;margin:var(--s3) 0 var(--s2);font-weight:600}.sp-dir{display:flex;align-items:center;gap:var(--s2);padding:var(--s1) var(--s2);font-size:var(--sm);border-radius:var(--r)}.sp-dir:hover{background:var(--bgh)}.sp-dir--sug{background:#d299220a}.sp-dir__path{font-family:var(--fm);font-size:var(--xs);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sp-btn{font-size:var(--xs);padding:2px 8px;background:var(--bg3);border:1px solid var(--bd);border-radius:var(--r);cursor:pointer;color:var(--t2)}.sp-btn--add{color:var(--info)}.sp-btn--add:hover{background:var(--bgh);border-color:var(--info)}.sp-btn--rm{color:var(--fail);font-size:var(--sm);padding:0 6px}.sp-btn--rm:hover{background:#f851491a}.sp-btn--rescan{width:100%;padding:var(--s2);color:var(--info);font-size:var(--sm)}.sp-btn--rescan:hover{background:var(--bgh)}.sp-btn:disabled{opacity:.5;cursor:default}.sp-manual{display:flex;gap:var(--s2);margin-bottom:var(--s2)}.sp-input{flex:1;background:var(--bg3);border:1px solid var(--bd);border-radius:var(--r);padding:var(--s1) var(--s2);color:var(--t1);font-family:var(--fm);font-size:var(--xs)}.sp-input:focus{outline:none;border-color:var(--info)}.zb{width:24px;height:24px;background:var(--bg3);border:1px solid var(--bd);border-radius:var(--r);color:var(--t1);cursor:pointer;font-size:var(--sm);display:inline-flex;align-items:center;justify-content:center}.zb:hover{background:var(--bgh);border-color:var(--t3)}.pmap__zoom{display:flex;gap:2px;margin-left:auto}.status-dot{display:inline-block;width:7px;height:7px;border-radius:50%;flex-shrink:0}.status-dot--ok{background:var(--ok)}.status-dot--critical{background:var(--fail)}.status-dot--warn{background:var(--warn)}.status-dot--inactive{background:var(--t3)}.soma-intel{padding:16px;font-size:13px;line-height:1.5}.soma-intel__header{display:flex;align-items:center;gap:8px;margin-bottom:16px}.soma-intel__title{font-size:16px;font-weight:700}.soma-intel__badge{font-size:10px;background:var(--info);color:#000;padding:2px 6px;border-radius:4px;font-weight:600}.soma-intel__ts{margin-left:auto;font-size:11px;color:var(--t3)}.soma-intel__ts--stale{color:var(--warn)}.soma-intel__agent-card{background:var(--bg2);border:1px solid var(--bd);border-radius:6px;padding:12px;margin-bottom:16px}.soma-intel__agent-name{font-weight:700;font-size:14px;margin-bottom:4px}.soma-intel__agent-stats{display:flex;gap:16px;font-size:12px;color:var(--t2)}.soma-intel__guard-block{margin-top:8px;padding:8px;background:#f851491a;border:1px solid var(--fail);border-radius:4px;color:var(--fail);font-size:12px;font-weight:600}.soma-intel__empty{color:var(--t3);padding:12px}.soma-intel__section{margin-bottom:16px}.soma-intel__section-header{display:flex;align-items:center;gap:8px;margin-bottom:8px;border-bottom:1px solid var(--bd);padding-bottom:4px;flex-wrap:wrap}.soma-intel__section-title{font-size:13px;font-weight:700;color:var(--t1);margin:0}.soma-intel__filters{display:flex;gap:4px;margin-left:auto}.soma-intel__filter{font-size:11px;padding:2px 6px;background:var(--bg2);border:1px solid var(--bd);border-radius:4px;color:var(--t1);cursor:pointer}.soma-intel__filter:focus{outline:1px solid var(--info)}.soma-intel__show-more{display:block;width:100%;padding:6px;margin-top:4px;background:none;border:1px dashed var(--bd);border-radius:4px;color:var(--t2);font-size:11px;cursor:pointer;text-align:center}.soma-intel__show-more:hover{background:var(--bg2);color:var(--t1)}.soma-intel__table{font-size:12px;font-family:var(--mono)}.soma-intel__row{display:grid;grid-template-columns:1fr 60px 50px 55px 60px;padding:4px 0;border-bottom:1px solid var(--bd)}.soma-intel__row--header{font-weight:700;color:var(--t2);border-bottom:2px solid var(--bd)}.soma-intel__row--active{background:#58a6ff14}.soma-intel__col-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.soma-intel__col-num,.soma-intel__col-status{text-align:right}.soma-intel__insight{padding:8px;background:var(--bg2);border-radius:4px;margin-bottom:6px}.soma-intel__insight-type{font-size:10px;text-transform:uppercase;color:var(--info);margin-right:6px;font-weight:600}.soma-intel__insight-conf{font-size:10px;color:var(--t3);margin-left:6px}.soma-intel__insight-claim{font-size:12px;color:var(--t2);margin-top:4px}.soma-intel__policy{padding:8px;background:var(--bg2);border-radius:4px;margin-bottom:6px;display:flex;align-items:center;gap:8px;flex-wrap:wrap}.soma-intel__enforcement{font-size:10px;padding:2px 6px;border-radius:3px;font-weight:600}.soma-intel__enforcement--warn{background:#d2992233;color:var(--warn)}.soma-intel__enforcement--error{background:#f8514933;color:var(--fail)}.soma-intel__enforcement--abort{background:#f851494d;color:var(--fail)}.soma-intel__policy-cond{width:100%;font-size:11px;color:var(--t3);margin-top:2px}.soma-intel--teaser{text-align:center;padding:40px 20px}.soma-intel__teaser-icon{font-size:48px;margin-bottom:12px}.soma-intel__teaser-title{font-size:20px;font-weight:700;margin-bottom:4px}.soma-intel__teaser-subtitle{font-size:14px;color:var(--t2);margin-bottom:24px}.soma-intel__teaser-features{text-align:left;max-width:360px;margin:0 auto 24px;font-size:13px}.soma-intel__teaser-feature{padding:6px 0;color:var(--t1)}.soma-intel__teaser-cta{display:inline-block;padding:10px 24px;background:var(--info);color:#000;border-radius:6px;text-decoration:none;font-weight:700;font-size:14px}.soma-intel__teaser-cta:hover{opacity:.9}.page-tabs{display:flex;gap:0;background:var(--bg2);border-bottom:1px solid var(--bd);padding:0 var(--s3);flex-shrink:0}.page-tabs__tab{padding:8px 16px;font-size:13px;font-weight:600;color:var(--t2);background:transparent;border:none;border-bottom:2px solid transparent;cursor:pointer;transition:all .15s}.page-tabs__tab:hover{color:var(--t1);background:var(--bg3)}.page-tabs__tab--active{color:var(--info);border-bottom-color:var(--info)}.soma-page{flex:1;overflow-y:auto;padding:var(--s4)}.soma-page__tabs{display:flex;gap:4px;padding:0 0 var(--s3) 0;border-bottom:1px solid var(--bd);margin-bottom:var(--s3)}.soma-page__tab{padding:6px 14px;font-size:12px;font-weight:600;color:var(--t2);background:transparent;border:1px solid transparent;border-radius:var(--r);cursor:pointer;transition:all .15s}.soma-page__tab:hover{color:var(--t1);background:var(--bg3)}.soma-page__tab--active{color:var(--t1);background:var(--bg3);border-color:var(--bd)}.soma-page__tab--locked{opacity:.5;cursor:not-allowed}.soma-page__content{min-height:300px}.soma-page__loading,.soma-page__locked{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:300px;color:var(--t3);gap:var(--s3)}.soma-page__locked-icon{font-size:32px;opacity:.5}.soma-page__teaser{text-align:center;padding:48px var(--s4);max-width:600px;margin:0 auto}.soma-page__teaser-icon{font-size:48px;margin-bottom:var(--s3)}.soma-page__teaser h2{font-size:var(--xl);margin-bottom:var(--s2)}.soma-page__teaser p{color:var(--t2);margin-bottom:var(--s4)}.soma-page__teaser-features{display:grid;gap:var(--s3);text-align:left;margin-bottom:var(--s5)}.soma-page__teaser-card{padding:var(--s3);background:var(--bg2);border:1px solid var(--bd);border-radius:var(--r)}.soma-page__teaser-card strong{display:block;color:var(--t1);margin-bottom:4px;font-size:13px}.soma-page__teaser-card p{font-size:12px;color:var(--t2);margin:0}.soma-page__teaser-cta{font-size:12px;color:var(--t3)}.soma-page__teaser-cta code{background:var(--bg3);padding:2px 6px;border-radius:3px;font-family:var(--fm);font-size:11px}.soma-policies{padding:var(--s3)}.soma-policies__header{display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--s3)}.soma-policies__header h3{font-size:var(--base);font-weight:700}.soma-policies__add{font-size:12px;padding:4px 12px;background:var(--info);color:#000;border:none;border-radius:var(--r);cursor:pointer;font-weight:600}.soma-policies__form{display:flex;gap:var(--s2);margin-bottom:var(--s3);flex-wrap:wrap}.soma-policies__form input,.soma-policies__form select{padding:6px 10px;background:var(--bg);color:var(--t1);border:1px solid var(--bd);border-radius:var(--r);font-size:12px;flex:1;min-width:120px}.soma-policies__submit{padding:6px 16px;background:var(--ok);color:#000;border:none;border-radius:var(--r);cursor:pointer;font-weight:600;font-size:12px}.soma-policies__list{display:flex;flex-direction:column;gap:4px}.soma-policies__row{display:flex;align-items:center;gap:var(--s2);padding:8px var(--s3);background:var(--bg2);border:1px solid var(--bd);border-radius:var(--r);font-size:12px}.soma-policies__name{font-weight:600;color:var(--t1);min-width:120px}.soma-policies__badge{font-size:10px;font-weight:700;border:1px solid;border-radius:3px;padding:1px 6px}.soma-policies__scope{color:var(--t2);flex:1}.soma-policies__cond{color:var(--t3);flex:2}.soma-policies__del{background:none;border:none;color:var(--fail);cursor:pointer;font-size:12px;padding:2px 6px}.soma-policies__empty{color:var(--t3);font-size:12px;padding:var(--s4);text-align:center}.soma-knowledge{padding:var(--s3)}.soma-knowledge__filters{display:flex;gap:var(--s2);margin-bottom:var(--s3);align-items:center;flex-wrap:wrap}.soma-knowledge__search{padding:6px 10px;background:var(--bg);color:var(--t1);border:1px solid var(--bd);border-radius:var(--r);font-size:12px;flex:1;min-width:150px}.soma-knowledge__filters select{padding:6px 8px;background:var(--bg);color:var(--t1);border:1px solid var(--bd);border-radius:var(--r);font-size:12px}.soma-knowledge__count{font-size:11px;color:var(--t3)}.soma-knowledge__body{display:flex;gap:var(--s3);min-height:400px}.soma-knowledge__list{flex:1;display:flex;flex-direction:column;gap:2px;overflow-y:auto;max-height:600px}.soma-knowledge__row{display:flex;align-items:center;gap:var(--s2);padding:6px var(--s3);background:var(--bg2);border:1px solid var(--bd);border-radius:var(--r);font-size:12px;cursor:pointer;text-align:left;width:100%;color:var(--t1)}.soma-knowledge__row:hover{background:var(--bg3)}.soma-knowledge__row--sel{border-color:var(--info);background:var(--bg3)}.soma-knowledge__type{font-size:10px;font-weight:600;color:var(--t3);min-width:60px;text-transform:uppercase}.soma-knowledge__name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.soma-knowledge__layer{font-size:10px;font-weight:600;border:1px solid;border-radius:3px;padding:1px 4px}.soma-knowledge__empty{color:var(--t3);font-size:12px;padding:var(--s4);text-align:center}.soma-knowledge__more{padding:6px;background:var(--bg3);border:1px solid var(--bd);border-radius:var(--r);color:var(--info);cursor:pointer;font-size:12px;width:100%}.soma-knowledge__detail{flex:1;background:var(--bg2);border:1px solid var(--bd);border-radius:var(--r);padding:var(--s3);overflow-y:auto;max-height:600px}.soma-knowledge__detail-header{display:flex;align-items:center;gap:var(--s2);margin-bottom:var(--s3);flex-wrap:wrap}.soma-knowledge__close{margin-left:auto;background:none;border:none;color:var(--t3);cursor:pointer;font-size:14px}.soma-knowledge__detail-body{margin-bottom:var(--s3)}.soma-knowledge__detail-body pre{font-family:var(--fm);font-size:12px;color:var(--t2);white-space:pre-wrap;word-break:break-word}.soma-knowledge__tags{display:flex;gap:4px;flex-wrap:wrap;margin-bottom:var(--s2)}.soma-knowledge__tag{font-size:10px;padding:2px 6px;background:var(--bg3);border-radius:3px;color:var(--t2)}.soma-knowledge__related{font-size:12px;color:var(--t2)}.soma-knowledge__link{background:none;border:none;color:var(--info);cursor:pointer;font-size:12px;text-decoration:underline;margin-left:4px}.soma-activity{padding:var(--s3)}.soma-activity__header{display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--s3)}.soma-activity__header h3{font-size:var(--base);font-weight:700}.soma-activity__status{font-size:11px;color:var(--t3)}.soma-activity__list{display:flex;flex-direction:column;gap:2px;max-height:600px;overflow-y:auto}.soma-activity__empty{color:var(--t3);font-size:12px;padding:var(--s5);text-align:center}.soma-activity__event{display:flex;align-items:center;gap:var(--s2);padding:6px var(--s3);background:var(--bg2);border:1px solid var(--bd);border-radius:var(--r);font-size:12px}.soma-activity__icon{font-size:14px;width:20px;text-align:center}.soma-activity__time{font-family:var(--fm);font-size:11px;color:var(--t3);min-width:70px}.soma-activity__action{font-weight:600;color:var(--info);min-width:80px}.soma-activity__desc{color:var(--t2);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}