agentflow-dashboard 0.8.3 → 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", "*");
@@ -2660,6 +2689,7 @@ var DashboardServer = class {
2660
2689
  }
2661
2690
  });
2662
2691
  this.app.get("/api/process-model/:agentId", (req, res) => {
2692
+ var _a, _b;
2663
2693
  try {
2664
2694
  const agentId = req.params.agentId;
2665
2695
  const allTraces = this.watcher.getTracesByAgent(agentId);
@@ -2677,8 +2707,8 @@ var DashboardServer = class {
2677
2707
  const nodeArr = Object.values(nodes);
2678
2708
  const sorted = nodeArr.filter((n) => n.name && typeof n.startTime === "number" && n.startTime > 0).sort((a, b) => (a.startTime ?? 0) - (b.startTime ?? 0));
2679
2709
  for (let i = 0; i < sorted.length - 1; i++) {
2680
- const from = sorted[i].name;
2681
- 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;
2682
2712
  const key = `${from}|||${to}`;
2683
2713
  transMap.set(key, (transMap.get(key) ?? 0) + 1);
2684
2714
  }
@@ -2777,7 +2807,11 @@ var DashboardServer = class {
2777
2807
  try {
2778
2808
  const reportPath = path3.join(somaVault, "..", "soma-report.json");
2779
2809
  if (!fs3.existsSync(reportPath)) {
2780
- 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
+ });
2781
2815
  }
2782
2816
  const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
2783
2817
  res.json(report);
@@ -2801,7 +2835,9 @@ var DashboardServer = class {
2801
2835
  available: true,
2802
2836
  layers: report.layers ?? { archive: 0, working: 0, emerging: 0, canon: 0 },
2803
2837
  governance: report.governance ?? { pending: 0, promoted: 0, rejected: 0 },
2804
- 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
+ ),
2805
2841
  canon: (report.insights ?? []).filter((i) => i.layer === "canon"),
2806
2842
  generatedAt: report.generatedAt
2807
2843
  });
@@ -2810,21 +2846,23 @@ var DashboardServer = class {
2810
2846
  res.status(500).json({ available: false, message: "Failed to read governance data" });
2811
2847
  }
2812
2848
  });
2813
- const sanitizeArg = (s) => s.replace(/[^a-zA-Z0-9_\-.:]/g, "");
2814
- const sanitizeReason = (s) => s.replace(/["`$\\]/g, "").slice(0, 500);
2849
+ const isValidId = (s) => /^[a-zA-Z0-9_\-.:]+$/.test(s);
2815
2850
  this.app.post("/api/soma/governance/promote", (req, res) => {
2816
2851
  var _a;
2817
2852
  const somaVault = this.config.somaVault;
2818
2853
  if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
2819
2854
  const { entryId } = req.body ?? {};
2820
- 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" });
2821
2857
  try {
2822
- const { execSync: execSync2 } = require("child_process");
2823
- const safeId = sanitizeArg(String(entryId));
2824
- const result = execSync2(`npx soma governance promote ${safeId} --vault "${somaVault}"`, {
2825
- encoding: "utf-8",
2826
- timeout: 1e4
2827
- });
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
+ );
2828
2866
  res.json({ success: true, message: result.trim() });
2829
2867
  } catch (error) {
2830
2868
  res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
@@ -2835,15 +2873,27 @@ var DashboardServer = class {
2835
2873
  const somaVault = this.config.somaVault;
2836
2874
  if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
2837
2875
  const { entryId, reason } = req.body ?? {};
2838
- 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" });
2839
2880
  try {
2840
- const { execSync: execSync2 } = require("child_process");
2841
- const safeId = sanitizeArg(String(entryId));
2842
- const safeReason = sanitizeReason(String(reason));
2843
- const result = execSync2(`npx soma governance reject ${safeId} "${safeReason}" --vault "${somaVault}"`, {
2844
- encoding: "utf-8",
2845
- timeout: 1e4
2846
- });
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
+ );
2847
2897
  res.json({ success: true, message: result.trim() });
2848
2898
  } catch (error) {
2849
2899
  res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
@@ -2853,13 +2903,16 @@ var DashboardServer = class {
2853
2903
  var _a;
2854
2904
  const somaVault = this.config.somaVault;
2855
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" });
2856
2907
  try {
2857
- const { execSync: execSync2 } = require("child_process");
2858
- const safeId = sanitizeArg(String(req.params.id));
2859
- const result = execSync2(`npx soma governance show ${safeId} --vault "${somaVault}"`, {
2860
- encoding: "utf-8",
2861
- timeout: 1e4
2862
- });
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
+ );
2863
2916
  res.json({ available: true, output: result.trim() });
2864
2917
  } catch (error) {
2865
2918
  res.status(404).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
@@ -2882,16 +2935,24 @@ var DashboardServer = class {
2882
2935
  const somaVault = this.config.somaVault;
2883
2936
  if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
2884
2937
  const { name, enforcement, scope, conditions } = req.body ?? {};
2885
- 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" });
2886
2942
  try {
2887
- const safeName = sanitizeArg(String(name));
2888
- const safeEnf = sanitizeArg(String(enforcement || "warn"));
2889
- const safeScope = sanitizeReason(String(scope || "all"));
2890
- const safeCond = sanitizeReason(String(conditions || ""));
2891
- const result = (0, import_node_child_process.execSync)(
2892
- `npx soma policy create "${safeName}" --enforcement ${safeEnf} --scope "${safeScope}" --conditions "${safeCond}" --vault "${somaVault}"`,
2893
- { encoding: "utf-8", timeout: 1e4 }
2894
- );
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 });
2895
2956
  res.json({ success: true, message: result.trim() });
2896
2957
  } catch (error) {
2897
2958
  res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
@@ -2901,11 +2962,16 @@ var DashboardServer = class {
2901
2962
  var _a;
2902
2963
  const somaVault = this.config.somaVault;
2903
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" });
2904
2967
  try {
2905
- const safeName = sanitizeArg(String(req.params.name));
2906
- const result = (0, import_node_child_process.execSync)(
2907
- `npx soma policy delete "${safeName}" --vault "${somaVault}"`,
2908
- { 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
+ }
2909
2975
  );
2910
2976
  res.json({ success: true, message: result.trim() });
2911
2977
  } catch (error) {
@@ -2923,16 +2989,28 @@ var DashboardServer = class {
2923
2989
  ...(report.agents ?? []).map((a) => ({ ...a, type: "agent", id: a.name })),
2924
2990
  ...(report.insights ?? []).map((i, idx) => {
2925
2991
  var _a;
2926
- 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
+ };
2927
2997
  }),
2928
2998
  ...(report.policies ?? []).map((p) => ({ ...p, type: "policy", id: p.name }))
2929
2999
  ];
2930
- 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;
2931
3007
  if (type) entities = entities.filter((e) => e.type === type);
2932
3008
  if (layer) entities = entities.filter((e) => e.layer === layer);
2933
3009
  if (q) {
2934
3010
  const lq = q.toLowerCase();
2935
- 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
+ );
2936
3014
  }
2937
3015
  const total = entities.length;
2938
3016
  const offset = parseInt(offsetStr || "0", 10);
@@ -3023,9 +3101,7 @@ var DashboardServer = class {
3023
3101
  const orphans = uniqueProcesses.filter(
3024
3102
  (p) => !allKnownPids.has(p.pid) && p.pid !== process.pid && p.pid !== process.ppid
3025
3103
  );
3026
- const problems = services.flatMap(
3027
- (s) => s.audit.problems.map((p) => `[${s.name}] ${p}`)
3028
- );
3104
+ const problems = services.flatMap((s) => s.audit.problems.map((p) => `[${s.name}] ${p}`));
3029
3105
  const result = {
3030
3106
  // Backward-compatible fields from primary service
3031
3107
  pidFile: (primary == null ? void 0 : primary.audit.pidFile) ?? null,
@@ -3080,19 +3156,24 @@ var DashboardServer = class {
3080
3156
  }
3081
3157
  } catch {
3082
3158
  }
3083
- const watched = [...new Set([
3084
- this.config.tracesDir,
3085
- ...this.config.dataDirs || [],
3086
- ...extraDirs
3087
- ].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
+ ];
3088
3166
  const discovered = [];
3089
3167
  const svcNames = getSystemdServices(this.userConfig);
3090
3168
  if (svcNames.length > 0) {
3091
3169
  try {
3092
- const { execSync: execSync2 } = require("child_process");
3093
- const raw = execSync2(
3094
- `systemctl --user show --property=ExecStart --no-pager ${svcNames.join(" ")} 2>/dev/null`,
3095
- { 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
+ }
3096
3177
  );
3097
3178
  for (const line of raw.split("\n")) {
3098
3179
  const match = line.match(/path=([^\s;]+)/);
@@ -3124,10 +3205,19 @@ var DashboardServer = class {
3124
3205
  this.app.post("/api/directories", import_express.default.json(), (req, res) => {
3125
3206
  try {
3126
3207
  const { add, remove } = req.body;
3127
- if (add && !fs3.existsSync(add)) {
3128
- 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
+ }
3129
3216
  }
3130
- 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
+ );
3131
3221
  let config = {};
3132
3222
  try {
3133
3223
  if (fs3.existsSync(configPath)) {
@@ -3337,13 +3427,31 @@ var DashboardServer = class {
3337
3427
  isVirtual: false
3338
3428
  });
3339
3429
  }
3340
- const rootSteps = new Set(model.steps);
3341
- const childSteps = new Set(model.transitions.map((t) => t.to));
3342
- const leafSteps = new Set(model.steps);
3343
- for (const t of model.transitions) {
3344
- }
3345
- nodes.push({ id: "[START]", label: "[START]", count: model.totalGraphs, frequency: 1, avgDuration: 0, failRate: 0, p95Duration: 0, isVirtual: true });
3346
- 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
+ });
3347
3455
  const edges = model.transitions.map((t) => ({
3348
3456
  source: t.from,
3349
3457
  target: t.to,
@@ -3363,7 +3471,10 @@ var DashboardServer = class {
3363
3471
  }
3364
3472
  }
3365
3473
  const maxEdgeCount = Math.max(...edges.map((e) => e.count), 1);
3366
- 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
+ );
3367
3478
  return { agentId, totalTraces: model.totalGraphs, nodes, edges, maxEdgeCount, maxNodeCount };
3368
3479
  }
3369
3480
  /**
package/dist/server.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  DashboardServer
3
- } from "./chunk-E5RJCBK2.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.3",
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}