@solongate/proxy 0.61.0 → 0.63.0

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/index.js CHANGED
@@ -7018,47 +7018,234 @@ var init_hooks = __esm({
7018
7018
  }
7019
7019
  });
7020
7020
 
7021
- // src/tui/panels/Policies.tsx
7022
- import { Box as Box2, Text as Text2, useInput } from "ink";
7023
- import { useEffect as useEffect2, useState as useState2 } from "react";
7021
+ // src/tui/panels/Live.tsx
7022
+ import { Box as Box2, Text as Text2 } from "ink";
7023
+ import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
7024
7024
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
7025
+ function LivePanel({ active: active2 }) {
7026
+ const stats = useLoader(() => api.stats.get());
7027
+ const ts = useLoader(() => api.stats.timeseries({ period: "24h" }));
7028
+ const feed = useLoader(() => api.audit.list({ limit: 40 }));
7029
+ usePoll(() => {
7030
+ stats.reload();
7031
+ ts.reload();
7032
+ feed.reload();
7033
+ }, 2e3, active2);
7034
+ const [tick, setTick] = useState2(0);
7035
+ useEffect2(() => {
7036
+ if (!active2) return;
7037
+ const t = setInterval(() => setTick((n) => n + 1), 200);
7038
+ return () => clearInterval(t);
7039
+ }, [active2]);
7040
+ const startRef = useRef2(Date.now());
7041
+ const [buffer, setBuffer] = useState2([]);
7042
+ useEffect2(() => {
7043
+ const incoming = feed.data?.entries;
7044
+ if (!incoming?.length) return;
7045
+ setBuffer((prev) => {
7046
+ const seen = new Set(prev.map((e) => e.id));
7047
+ const fresh = incoming.filter((e) => !seen.has(e.id)).sort((a, b) => Date.parse(a.created_at) - Date.parse(b.created_at));
7048
+ return fresh.length ? [...prev, ...fresh].slice(-300) : prev;
7049
+ });
7050
+ }, [feed.data]);
7051
+ const s = stats.data;
7052
+ const points = ts.data?.timeseries ?? [];
7053
+ const cols = process.stdout.columns ?? 100;
7054
+ const rows = process.stdout.rows ?? 32;
7055
+ const feedRows = Math.max(8, Math.min(20, rows - 18));
7056
+ const wide = cols >= 112;
7057
+ const tail = buffer.slice(-feedRows);
7058
+ const denies = buffer.filter((e) => e.decision !== "ALLOW");
7059
+ const dlpHits = buffer.filter((e) => e.dlp_matches?.length);
7060
+ const lastDeny = denies[denies.length - 1];
7061
+ const spin = SPIN[tick % SPIN.length];
7062
+ const cursor = tick % 4 < 2 ? "\u258C" : " ";
7063
+ if (stats.error) return /* @__PURE__ */ jsxs2(Text2, { color: theme.bad, children: [
7064
+ "\u2717 ",
7065
+ stats.error
7066
+ ] });
7067
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
7068
+ /* @__PURE__ */ jsxs2(Box2, { children: [
7069
+ /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: "\u25CF " }),
7070
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: theme.accentBright, children: "LIVE" }),
7071
+ /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
7072
+ " ",
7073
+ spin,
7074
+ " scanning"
7075
+ ] }),
7076
+ /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
7077
+ " up ",
7078
+ fmtUp(Date.now() - startRef.current)
7079
+ ] }),
7080
+ /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
7081
+ " ",
7082
+ hhmmss(Date.now())
7083
+ ] }),
7084
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " poll 2s" })
7085
+ ] }),
7086
+ /* @__PURE__ */ jsxs2(Box2, { children: [
7087
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "calls " }),
7088
+ /* @__PURE__ */ jsx2(Text2, { bold: true, children: s ? s.total_calls : "\xB7\xB7\xB7\xB7" }),
7089
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " allow " }),
7090
+ /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: s ? s.allowed : "\xB7\xB7\xB7" }),
7091
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " deny " }),
7092
+ /* @__PURE__ */ jsx2(Text2, { color: theme.bad, bold: true, children: s ? s.denied : "\xB7\xB7" }),
7093
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " dlp(buf) " }),
7094
+ /* @__PURE__ */ jsx2(Text2, { color: dlpHits.length ? theme.bad : theme.dim, children: dlpHits.length }),
7095
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " policies " }),
7096
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: s ? s.active_policies : "\xB7" })
7097
+ ] }),
7098
+ /* @__PURE__ */ jsxs2(Box2, { children: [
7099
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "act " }),
7100
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: sparkline(points.map((p) => p.total), Math.min(64, cols - 30)) })
7101
+ ] }),
7102
+ /* @__PURE__ */ jsxs2(Box2, { children: [
7103
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "dny " }),
7104
+ /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: sparkline(points.map((p) => p.denied), Math.min(64, cols - 30)) })
7105
+ ] }),
7106
+ /* @__PURE__ */ jsxs2(Box2, { children: [
7107
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ">> " }),
7108
+ lastDeny ? /* @__PURE__ */ jsxs2(Text2, { color: theme.bad, children: [
7109
+ "last deny ",
7110
+ hhmmss(lastDeny.created_at),
7111
+ " ",
7112
+ lastDeny.tool_name,
7113
+ " \xB7 ",
7114
+ truncate2(lastDeny.reason ?? "policy", Math.max(20, cols - 50))
7115
+ ] }) : /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "no denials in buffer" })
7116
+ ] }),
7117
+ /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, children: [
7118
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", flexGrow: 3, borderStyle: "single", borderColor: "gray", paddingX: 1, children: [
7119
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, bold: true, children: "\u2500\u2500 tool stream \u2500\u2500" }),
7120
+ tail.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "awaiting traffic\u2026" }) : null,
7121
+ tail.map((e) => /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7122
+ /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
7123
+ "[",
7124
+ hhmmss(e.created_at),
7125
+ "] "
7126
+ ] }),
7127
+ /* @__PURE__ */ jsx2(Text2, { color: decisionColor(e.decision), bold: e.decision !== "ALLOW", children: e.decision.padEnd(6) }),
7128
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: truncate2(e.tool_name, 14).padEnd(15) }),
7129
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: (e.permission ?? "").slice(0, 4).padEnd(5) }),
7130
+ e.dlp_matches?.length ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: "DLP! " }) : null,
7131
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: (e.arguments_summary ? JSON.stringify(e.arguments_summary) : e.reason ?? "").replace(/\s+/g, " ") })
7132
+ ] }, e.id))
7133
+ ] }),
7134
+ wide ? /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: 34, borderStyle: "single", borderColor: "gray", paddingX: 1, children: [
7135
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, bold: true, children: "\u2500\u2500 raw \u2500\u2500" }),
7136
+ tail.slice(-Math.floor(feedRows / 2)).flatMap((e) => {
7137
+ const raw = e.arguments_summary ? JSON.stringify(e.arguments_summary) : `hash:${e.id.slice(0, 12)}`;
7138
+ return [
7139
+ /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", color: e.decision === "ALLOW" ? theme.dim : theme.bad, children: [
7140
+ e.id.slice(0, 8),
7141
+ " ",
7142
+ e.tool_name.slice(0, 12)
7143
+ ] }, e.id + ":h"),
7144
+ /* @__PURE__ */ jsx2(Text2, { wrap: "truncate", color: theme.dim, children: " " + raw.replace(/\s+/g, " ") }, e.id + ":b")
7145
+ ];
7146
+ })
7147
+ ] }) : null
7148
+ ] }),
7149
+ /* @__PURE__ */ jsxs2(Box2, { children: [
7150
+ /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: "root@solongate" }),
7151
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ":" }),
7152
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, children: "~/live" }),
7153
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "$ " }),
7154
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, children: cursor })
7155
+ ] })
7156
+ ] });
7157
+ }
7158
+ var SPIN, hhmmss, fmtUp;
7159
+ var init_Live = __esm({
7160
+ "src/tui/panels/Live.tsx"() {
7161
+ "use strict";
7162
+ init_api_client();
7163
+ init_hooks();
7164
+ init_theme();
7165
+ SPIN = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
7166
+ hhmmss = (ts) => {
7167
+ const d = new Date(ts);
7168
+ return Number.isNaN(d.getTime()) ? "--:--:--" : d.toTimeString().slice(0, 8);
7169
+ };
7170
+ fmtUp = (ms) => {
7171
+ const s = Math.floor(ms / 1e3);
7172
+ const p = (n) => String(n).padStart(2, "0");
7173
+ return `${p(Math.floor(s / 3600))}:${p(Math.floor(s % 3600 / 60))}:${p(s % 60)}`;
7174
+ };
7175
+ }
7176
+ });
7177
+
7178
+ // src/tui/panels/Policies.tsx
7179
+ import { Box as Box3, Text as Text3, useInput } from "ink";
7180
+ import TextInput from "ink-text-input";
7181
+ import { useEffect as useEffect3, useState as useState3 } from "react";
7182
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
7183
+ function ruleSummary(r) {
7184
+ const bits = [];
7185
+ for (const [g, tag] of [["commandConstraints", "cmd"], ["pathConstraints", "path"], ["filenameConstraints", "file"], ["urlConstraints", "url"]]) {
7186
+ const c2 = r[g];
7187
+ const n = (c2?.allowed?.length ?? 0) + (c2?.denied?.length ?? 0);
7188
+ if (n) bits.push(`${tag}:${n}`);
7189
+ }
7190
+ return bits.join(" ");
7191
+ }
7025
7192
  function PoliciesPanel({ focused }) {
7026
7193
  const list4 = useLoader(() => api.policies.list());
7027
7194
  const policies = list4.data?.policies ?? [];
7028
- const [view, setView] = useState2("list");
7029
- const [pi, setPi] = useState2(0);
7030
- const [ri, setRi] = useState2(0);
7031
- const [rules, setRules] = useState2(null);
7032
- const [mode, setMode] = useState2("denylist");
7033
- const [status, setStatus] = useState2(null);
7195
+ const [view, setView] = useState3("list");
7196
+ const [pi, setPi] = useState3(0);
7197
+ const [ri, setRi] = useState3(0);
7198
+ const [fi, setFi] = useState3(0);
7199
+ const [rules, setRules] = useState3([]);
7200
+ const [mode, setMode] = useState3("denylist");
7201
+ const [dirty, setDirty] = useState3(false);
7202
+ const [editing, setEditing] = useState3(false);
7203
+ const [editVal, setEditVal] = useState3("");
7204
+ const [status, setStatus] = useState3(null);
7034
7205
  const selected = policies[Math.min(pi, Math.max(0, policies.length - 1))];
7035
7206
  const detail = useLoader(() => selected ? api.policies.get(selected.id) : Promise.resolve(null), [selected?.id]);
7036
- useEffect2(() => {
7207
+ useEffect3(() => {
7037
7208
  if (detail.data) {
7038
7209
  setRules(detail.data.rules);
7039
7210
  setMode(detail.data.mode ?? "denylist");
7211
+ setDirty(false);
7040
7212
  setRi(0);
7041
7213
  }
7042
7214
  }, [detail.data]);
7043
- const persist = async (nextRules, nextMode) => {
7044
- if (!detail.data || !selected) return;
7215
+ const mutate = (nextRules, nextMode = mode) => {
7216
+ setRules(nextRules);
7217
+ setMode(nextMode);
7218
+ setDirty(true);
7219
+ setStatus(null);
7220
+ };
7221
+ const save = async () => {
7222
+ if (!selected || !detail.data) return;
7045
7223
  setStatus("Saving\u2026");
7046
7224
  try {
7047
7225
  const res = await api.policies.update(selected.id, {
7048
7226
  id: selected.id,
7049
7227
  name: detail.data.name,
7050
7228
  description: detail.data.description,
7051
- mode: nextMode,
7229
+ mode,
7052
7230
  agents: detail.data.agents,
7053
- rules: nextRules
7231
+ rules
7054
7232
  });
7055
7233
  setRules(res.rules);
7234
+ setDirty(false);
7056
7235
  setStatus("\u2713 Saved v" + res._version);
7057
7236
  list4.reload();
7058
7237
  } catch (e) {
7059
7238
  setStatus("\u2717 " + (e instanceof Error ? e.message : String(e)));
7060
7239
  }
7061
7240
  };
7241
+ const discard = () => {
7242
+ if (detail.data) {
7243
+ setRules(detail.data.rules);
7244
+ setMode(detail.data.mode ?? "denylist");
7245
+ }
7246
+ setDirty(false);
7247
+ setStatus("discarded");
7248
+ };
7062
7249
  useInput(
7063
7250
  (input, key) => {
7064
7251
  if (view === "list") {
@@ -7066,44 +7253,61 @@ function PoliciesPanel({ focused }) {
7066
7253
  else if (key.downArrow) setPi((n) => Math.min(policies.length - 1, n + 1));
7067
7254
  else if (key.return || key.rightArrow) {
7068
7255
  setStatus(null);
7069
- setView("detail");
7256
+ setView("rules");
7070
7257
  }
7071
7258
  return;
7072
7259
  }
7073
- const rs2 = rules ?? [];
7074
- if (key.leftArrow) {
7075
- setView("list");
7076
- setStatus(null);
7077
- } else if (key.upArrow) setRi((n) => Math.max(0, n - 1));
7078
- else if (key.downArrow) setRi((n) => Math.min(rs2.length - 1, n + 1));
7079
- else if (input === " " || input === "e") {
7080
- if (!rs2[ri]) return;
7081
- const next = rs2.map((r, i) => i === ri ? { ...r, enabled: !r.enabled } : r);
7082
- setRules(next);
7083
- void persist(next, mode);
7084
- } else if (input === "d") {
7085
- const target = rs2[ri];
7086
- if (!target || !selected) return;
7087
- setStatus("Deleting\u2026");
7088
- void api.policies.revokeRule(selected.id, target.id).then(() => {
7089
- setStatus("\u2713 Deleted");
7090
- detail.reload();
7091
- list4.reload();
7092
- }).catch((e) => setStatus("\u2717 " + (e instanceof Error ? e.message : String(e))));
7093
- } else if (input === "m") {
7094
- const nm = mode === "denylist" ? "whitelist" : "denylist";
7095
- setMode(nm);
7096
- void persist(rs2, nm);
7260
+ if (view === "rules") {
7261
+ if (key.leftArrow) return setView("list"), void setStatus(null);
7262
+ if (key.upArrow) setRi((n) => Math.max(0, n - 1));
7263
+ else if (key.downArrow) setRi((n) => Math.min(rules.length - 1, n + 1));
7264
+ else if (key.return || key.rightArrow) {
7265
+ if (rules[ri]) {
7266
+ setFi(0);
7267
+ setView("rule");
7268
+ }
7269
+ } else if (input === " ") {
7270
+ if (rules[ri]) mutate(rules.map((r, i) => i === ri ? { ...r, enabled: !r.enabled } : r));
7271
+ } else if (input === "e") {
7272
+ if (rules[ri]) mutate(rules.map((r, i) => i === ri ? { ...r, effect: r.effect === "ALLOW" ? "DENY" : "ALLOW" } : r));
7273
+ } else if (input === "d") {
7274
+ if (rules[ri]) {
7275
+ const next = rules.filter((_, i) => i !== ri);
7276
+ setRi((n) => Math.max(0, Math.min(n, next.length - 1)));
7277
+ mutate(next);
7278
+ }
7279
+ } else if (input === "m") {
7280
+ mutate(rules, mode === "denylist" ? "whitelist" : "denylist");
7281
+ } else if (input === "s") void save();
7282
+ else if (input === "x") discard();
7283
+ return;
7097
7284
  }
7285
+ const rule2 = rules[ri];
7286
+ if (!rule2) return setView("rules");
7287
+ const field = FIELDS[fi];
7288
+ if (key.leftArrow) setView("rules");
7289
+ else if (key.upArrow) setFi((n) => Math.max(0, n - 1));
7290
+ else if (key.downArrow) setFi((n) => Math.min(FIELDS.length - 1, n + 1));
7291
+ else if (field.kind === "effect" && (input === " " || key.rightArrow)) {
7292
+ mutate(rules.map((r, i) => i === ri ? { ...r, effect: r.effect === "ALLOW" ? "DENY" : "ALLOW" } : r));
7293
+ } else if (field.kind === "enabled" && (input === " " || key.rightArrow)) {
7294
+ mutate(rules.map((r, i) => i === ri ? { ...r, enabled: !r.enabled } : r));
7295
+ } else if (field.kind === "priority" && (key.rightArrow || key.leftArrow)) {
7296
+ const d = key.rightArrow ? 1 : -1;
7297
+ mutate(rules.map((r, i) => i === ri ? { ...r, priority: Math.max(0, r.priority + d) } : r));
7298
+ } else if (field.kind === "text" && key.return) {
7299
+ setEditVal(field.get(rule2));
7300
+ setEditing(true);
7301
+ } else if (input === "s") void save();
7098
7302
  },
7099
- { isActive: focused }
7303
+ { isActive: focused && !editing }
7100
7304
  );
7101
7305
  if (view === "list") {
7102
- return /* @__PURE__ */ jsx2(DataView, { loading: list4.loading && !list4.data, error: list4.error, empty: !!list4.data && policies.length === 0, emptyText: "No policies.", children: /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
7103
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: focused ? "\u2191\u2193 select \xB7 enter open" : "press \u2192 to browse" }),
7104
- /* @__PURE__ */ jsx2(Box2, { marginTop: 1, flexDirection: "column", children: policies.map((p, i) => /* @__PURE__ */ jsxs2(Text2, { color: i === pi ? theme.accentBright : void 0, bold: i === pi, children: [
7306
+ return /* @__PURE__ */ jsx3(DataView, { loading: list4.loading && !list4.data, error: list4.error, empty: !!list4.data && policies.length === 0, emptyText: "No policies.", children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
7307
+ /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: focused ? "\u2191\u2193 select \xB7 enter open" : "press \u2192 to browse" }),
7308
+ /* @__PURE__ */ jsx3(Box3, { marginTop: 1, flexDirection: "column", children: policies.map((p, i) => /* @__PURE__ */ jsxs3(Text3, { color: i === pi ? theme.accentBright : void 0, bold: i === pi, children: [
7105
7309
  (i === pi ? "\u25B8 " : " ") + truncate2(p.name, 26).padEnd(27),
7106
- /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
7310
+ /* @__PURE__ */ jsxs3(Text3, { color: theme.dim, children: [
7107
7311
  p.mode.padEnd(10),
7108
7312
  " ",
7109
7313
  p.rules.length,
@@ -7113,37 +7317,78 @@ function PoliciesPanel({ focused }) {
7113
7317
  ] }, p.id)) })
7114
7318
  ] }) });
7115
7319
  }
7116
- const rs = rules ?? [];
7117
- return /* @__PURE__ */ jsx2(DataView, { loading: detail.loading && !rules, error: detail.error, children: /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
7118
- /* @__PURE__ */ jsxs2(Box2, { children: [
7119
- /* @__PURE__ */ jsx2(Text2, { bold: true, color: theme.accentBright, children: truncate2(selected?.name ?? "", 30) }),
7120
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " mode: " }),
7121
- /* @__PURE__ */ jsx2(Text2, { color: mode === "whitelist" ? theme.ok : void 0, children: mode })
7320
+ const dirtyTag = dirty ? /* @__PURE__ */ jsx3(Text3, { color: theme.warn, children: " \u25CF unsaved (s save \xB7 x discard)" }) : null;
7321
+ if (view === "rules") {
7322
+ return /* @__PURE__ */ jsx3(DataView, { loading: detail.loading && !detail.data, error: detail.error, children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
7323
+ /* @__PURE__ */ jsxs3(Box3, { children: [
7324
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: theme.accentBright, children: truncate2(selected?.name ?? "", 28) }),
7325
+ /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: " mode: " }),
7326
+ /* @__PURE__ */ jsx3(Text3, { color: mode === "whitelist" ? theme.warn : void 0, children: mode }),
7327
+ dirtyTag
7328
+ ] }),
7329
+ /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: "\u2191\u2193 rule \xB7 enter edit \xB7 space on/off \xB7 e effect \xB7 d delete \xB7 m mode \xB7 s save \xB7 \u2190 back" }),
7330
+ /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(
7331
+ Table,
7332
+ {
7333
+ columns: [
7334
+ { header: "", width: 2 },
7335
+ { header: "ON", width: 2 },
7336
+ { header: "EFFECT", width: 7 },
7337
+ { header: "PRIO", width: 4 },
7338
+ { header: "TOOL", width: 20 },
7339
+ { header: "CONSTRAINTS", width: 22 }
7340
+ ],
7341
+ rows: rules.map((r, i) => [
7342
+ { value: i === ri ? "\u25B8" : "", color: theme.accentBright },
7343
+ { value: r.enabled ? "\u25CF" : "\u25CB", color: r.enabled ? theme.ok : theme.dim },
7344
+ { value: r.effect, color: r.effect === "ALLOW" ? theme.ok : theme.bad, dim: !r.enabled },
7345
+ { value: String(r.priority), dim: true },
7346
+ { value: truncate2(r.toolPattern, 20), color: theme.accent, dim: !r.enabled },
7347
+ { value: ruleSummary(r) || truncate2(r.description || "\u2014", 22), dim: true }
7348
+ ])
7349
+ }
7350
+ ) }),
7351
+ rules.length === 0 ? /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: "(no rules)" }) : null,
7352
+ status ? /* @__PURE__ */ jsx3(Text3, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) : null
7353
+ ] }) });
7354
+ }
7355
+ const rule = rules[ri];
7356
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
7357
+ /* @__PURE__ */ jsxs3(Box3, { children: [
7358
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: theme.accentBright, children: "Rule " }),
7359
+ /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: truncate2(rule?.id ?? "", 26) }),
7360
+ dirtyTag
7122
7361
  ] }),
7123
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "\u2191\u2193 rule \xB7 space toggle \xB7 d delete \xB7 m mode \xB7 \u2190 back" }),
7124
- /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(
7125
- Table,
7126
- {
7127
- columns: [
7128
- { header: "", width: 2 },
7129
- { header: "EFFECT", width: 7 },
7130
- { header: "PRIO", width: 4 },
7131
- { header: "TOOL", width: 20 },
7132
- { header: "DESCRIPTION", width: 34 }
7133
- ],
7134
- rows: rs.map((r, i) => [
7135
- { value: i === ri ? "\u25B8" : "", color: theme.accentBright },
7136
- { value: r.effect, color: r.effect === "ALLOW" ? theme.ok : theme.bad, dim: !r.enabled },
7137
- { value: String(r.priority), dim: true },
7138
- { value: (r.enabled ? "" : "\u2298 ") + truncate2(r.toolPattern, 18), color: theme.accent, dim: !r.enabled },
7139
- { value: truncate2(r.description || "\u2014", 34), dim: !r.enabled }
7140
- ])
7141
- }
7142
- ) }),
7143
- rs.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "(no rules)" }) : null,
7144
- status ? /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) }) : null
7145
- ] }) });
7362
+ /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: "\u2191\u2193 field \xB7 enter edit text \xB7 space/\u2192 toggle \xB7 s save \xB7 \u2190 back" }),
7363
+ /* @__PURE__ */ jsx3(Box3, { marginTop: 1, flexDirection: "column", children: FIELDS.map((f, i) => {
7364
+ const val = rule ? f.get(rule) : "";
7365
+ const active2 = i === fi;
7366
+ return /* @__PURE__ */ jsxs3(Box3, { children: [
7367
+ /* @__PURE__ */ jsx3(Text3, { color: active2 ? theme.accentBright : void 0, children: (active2 ? "\u25B8 " : " ") + f.label.padEnd(13) }),
7368
+ active2 && editing && f.kind === "text" ? /* @__PURE__ */ jsx3(
7369
+ TextInput,
7370
+ {
7371
+ value: editVal,
7372
+ onChange: setEditVal,
7373
+ onSubmit: (v) => {
7374
+ if (rule && f.set) mutate(rules.map((r, idx) => idx === ri ? f.set(r, v) : r));
7375
+ setEditing(false);
7376
+ }
7377
+ }
7378
+ ) : /* @__PURE__ */ jsx3(
7379
+ Text3,
7380
+ {
7381
+ color: f.kind === "effect" ? val === "ALLOW" ? theme.ok : theme.bad : void 0,
7382
+ dimColor: !val,
7383
+ children: val || "\u2014"
7384
+ }
7385
+ )
7386
+ ] }, f.label);
7387
+ }) }),
7388
+ status ? /* @__PURE__ */ jsx3(Text3, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) : null
7389
+ ] });
7146
7390
  }
7391
+ var constr, setConstr, FIELDS;
7147
7392
  var init_Policies = __esm({
7148
7393
  "src/tui/panels/Policies.tsx"() {
7149
7394
  "use strict";
@@ -7151,27 +7396,57 @@ var init_Policies = __esm({
7151
7396
  init_components();
7152
7397
  init_hooks();
7153
7398
  init_theme();
7399
+ constr = (r, g, side) => (r[g]?.[side] ?? []).join(", ");
7400
+ setConstr = (r, g, side, val) => {
7401
+ const arr = val.split(",").map((s) => s.trim()).filter(Boolean);
7402
+ const prev = r[g] ?? {};
7403
+ return { ...r, [g]: { ...prev, [side]: arr } };
7404
+ };
7405
+ FIELDS = [
7406
+ { label: "Effect", kind: "effect", get: (r) => r.effect },
7407
+ { label: "Enabled", kind: "enabled", get: (r) => r.enabled ? "yes" : "no" },
7408
+ { label: "Priority", kind: "priority", get: (r) => String(r.priority) },
7409
+ { label: "Tool pattern", kind: "text", get: (r) => r.toolPattern, set: (r, v) => ({ ...r, toolPattern: v || "*" }) },
7410
+ { label: "Description", kind: "text", get: (r) => r.description ?? "", set: (r, v) => ({ ...r, description: v }) },
7411
+ { label: "Cmd allow", kind: "text", get: (r) => constr(r, "commandConstraints", "allowed"), set: (r, v) => setConstr(r, "commandConstraints", "allowed", v) },
7412
+ { label: "Cmd deny", kind: "text", get: (r) => constr(r, "commandConstraints", "denied"), set: (r, v) => setConstr(r, "commandConstraints", "denied", v) },
7413
+ { label: "Path allow", kind: "text", get: (r) => constr(r, "pathConstraints", "allowed"), set: (r, v) => setConstr(r, "pathConstraints", "allowed", v) },
7414
+ { label: "Path deny", kind: "text", get: (r) => constr(r, "pathConstraints", "denied"), set: (r, v) => setConstr(r, "pathConstraints", "denied", v) },
7415
+ { label: "File allow", kind: "text", get: (r) => constr(r, "filenameConstraints", "allowed"), set: (r, v) => setConstr(r, "filenameConstraints", "allowed", v) },
7416
+ { label: "File deny", kind: "text", get: (r) => constr(r, "filenameConstraints", "denied"), set: (r, v) => setConstr(r, "filenameConstraints", "denied", v) },
7417
+ { label: "URL allow", kind: "text", get: (r) => constr(r, "urlConstraints", "allowed"), set: (r, v) => setConstr(r, "urlConstraints", "allowed", v) },
7418
+ { label: "URL deny", kind: "text", get: (r) => constr(r, "urlConstraints", "denied"), set: (r, v) => setConstr(r, "urlConstraints", "denied", v) }
7419
+ ];
7154
7420
  }
7155
7421
  });
7156
7422
 
7157
7423
  // src/tui/panels/RateLimit.tsx
7158
- import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
7159
- import { useEffect as useEffect3, useState as useState3 } from "react";
7160
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
7424
+ import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
7425
+ import { useEffect as useEffect4, useState as useState4 } from "react";
7426
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
7427
+ function ColoredSpark({ values, limit, width: width2 = 60 }) {
7428
+ const v = values.length > width2 ? values.slice(values.length - width2) : values;
7429
+ const max = Math.max(1, ...v);
7430
+ return /* @__PURE__ */ jsx4(Text4, { children: v.map((n, i) => {
7431
+ const ch = BLOCKS2[Math.min(BLOCKS2.length - 1, Math.round(n / max * (BLOCKS2.length - 1)))];
7432
+ const over = limit > 0 && n > limit;
7433
+ return /* @__PURE__ */ jsx4(Text4, { color: over ? theme.bad : theme.accent, children: ch }, i);
7434
+ }) });
7435
+ }
7161
7436
  function RateLimitPanel({ focused }) {
7162
7437
  const layersQ = useLoader(() => api.settings.getSecurityLayers());
7163
7438
  const historyQ = useLoader(() => api.settings.getRateLimitHistory());
7164
7439
  const insightsQ = useLoader(() => api.stats.securityInsights(7));
7165
- const [draft, setDraft] = useState3(null);
7166
- const [dirty, setDirty] = useState3(false);
7167
- const [fi, setFi] = useState3(0);
7168
- const [status, setStatus] = useState3(null);
7169
- useEffect3(() => {
7440
+ const [draft, setDraft] = useState4(null);
7441
+ const [dirty, setDirty] = useState4(false);
7442
+ const [fi, setFi] = useState4(0);
7443
+ const [status, setStatus] = useState4(null);
7444
+ useEffect4(() => {
7170
7445
  if (layersQ.data && !draft) setDraft({ ...layersQ.data.layers.rateLimit });
7171
7446
  }, [layersQ.data, draft]);
7172
7447
  const adjust = (dir, step) => {
7173
7448
  if (!draft) return;
7174
- const field = FIELDS[fi];
7449
+ const field = FIELDS2[fi];
7175
7450
  if (field === "mode") {
7176
7451
  const idx = (MODES.indexOf(draft.mode) + dir + MODES.length) % MODES.length;
7177
7452
  setDraft({ ...draft, mode: MODES[idx] });
@@ -7185,8 +7460,7 @@ function RateLimitPanel({ focused }) {
7185
7460
  if (!draft || !layersQ.data) return;
7186
7461
  setStatus("Saving\u2026");
7187
7462
  try {
7188
- const next = { ...layersQ.data.layers, rateLimit: draft };
7189
- const res = await api.settings.setSecurityLayers(next);
7463
+ const res = await api.settings.setSecurityLayers({ ...layersQ.data.layers, rateLimit: draft });
7190
7464
  setDraft({ ...res.layers.rateLimit });
7191
7465
  setDirty(false);
7192
7466
  setStatus("\u2713 Saved");
@@ -7198,8 +7472,8 @@ function RateLimitPanel({ focused }) {
7198
7472
  useInput2(
7199
7473
  (input, key) => {
7200
7474
  const step = key.shift ? 10 : 1;
7201
- if (key.upArrow) setFi((n) => (n - 1 + FIELDS.length) % FIELDS.length);
7202
- else if (key.downArrow) setFi((n) => (n + 1) % FIELDS.length);
7475
+ if (key.upArrow) setFi((n) => (n - 1 + FIELDS2.length) % FIELDS2.length);
7476
+ else if (key.downArrow) setFi((n) => (n + 1) % FIELDS2.length);
7203
7477
  else if (key.leftArrow) adjust(-1, step);
7204
7478
  else if (key.rightArrow) adjust(1, step);
7205
7479
  else if (input === "s") void save();
@@ -7207,77 +7481,72 @@ function RateLimitPanel({ focused }) {
7207
7481
  { isActive: focused }
7208
7482
  );
7209
7483
  const history = (historyQ.data?.history ?? []).slice().sort((a, b) => b.ts - a.ts);
7210
- const bursts = (insightsQ.data?.["anomalies"] ?? []).slice(0, 6);
7211
- return /* @__PURE__ */ jsx3(DataView, { loading: layersQ.loading && !draft, error: layersQ.error, children: draft ? /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
7212
- /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: focused ? "\u2191\u2193 field \xB7 \u2190\u2192 \xB11 \xB7 shift+\u2190\u2192 \xB110 \xB7 s save" : "press \u2192 to edit" }),
7213
- /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
7214
- /* @__PURE__ */ jsx3(FieldRow, { label: "Mode", active: focused && fi === 0, children: /* @__PURE__ */ jsx3(Text3, { color: modeColor(draft.mode), children: draft.mode }) }),
7215
- /* @__PURE__ */ jsx3(FieldRow, { label: "Per minute", active: focused && fi === 1, children: /* @__PURE__ */ jsx3(Text3, { bold: true, children: draft.perMinute }) }),
7216
- /* @__PURE__ */ jsx3(FieldRow, { label: "Per hour", active: focused && fi === 2, children: /* @__PURE__ */ jsx3(Text3, { bold: true, children: draft.perHour === 0 ? "off" : draft.perHour }) }),
7217
- /* @__PURE__ */ jsx3(FieldRow, { label: "Per day", active: focused && fi === 3, children: /* @__PURE__ */ jsx3(Text3, { bold: true, children: draft.perDay === 0 ? "off" : draft.perDay }) })
7484
+ const activity = insightsQ.data?.["activity"] ?? {};
7485
+ const minuteSeries = (activity.minute ?? []).map((b) => b.count);
7486
+ const hourSeries = (activity.hour ?? []).map((b) => b.count);
7487
+ const bursts = minuteSeries.filter((c2) => draft && draft.perMinute > 0 && c2 > draft.perMinute).length;
7488
+ return /* @__PURE__ */ jsx4(DataView, { loading: layersQ.loading && !draft, error: layersQ.error, children: draft ? /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
7489
+ /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: focused ? "\u2191\u2193 field \xB7 \u2190\u2192 \xB11 \xB7 shift+\u2190\u2192 \xB110 \xB7 s save" : "press \u2192 to edit" }),
7490
+ /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
7491
+ /* @__PURE__ */ jsx4(FieldRow, { label: "Mode", active: focused && fi === 0, children: /* @__PURE__ */ jsx4(Text4, { color: modeColor(draft.mode), children: draft.mode }) }),
7492
+ /* @__PURE__ */ jsx4(FieldRow, { label: "Per minute", active: focused && fi === 1, children: /* @__PURE__ */ jsx4(Text4, { bold: true, children: draft.perMinute }) }),
7493
+ /* @__PURE__ */ jsx4(FieldRow, { label: "Per hour", active: focused && fi === 2, children: /* @__PURE__ */ jsx4(Text4, { bold: true, children: draft.perHour === 0 ? "off" : draft.perHour }) }),
7494
+ /* @__PURE__ */ jsx4(FieldRow, { label: "Per day", active: focused && fi === 3, children: /* @__PURE__ */ jsx4(Text4, { bold: true, children: draft.perDay === 0 ? "off" : draft.perDay }) })
7218
7495
  ] }),
7219
- dirty || status ? /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, children: [
7220
- dirty ? /* @__PURE__ */ jsx3(Text3, { color: theme.warn, children: "\u25CF unsaved \u2014 press s to apply " }) : null,
7221
- status ? /* @__PURE__ */ jsx3(Text3, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) : null
7496
+ dirty || status ? /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, children: [
7497
+ dirty ? /* @__PURE__ */ jsx4(Text4, { color: theme.warn, children: "\u25CF unsaved \u2014 press s to apply " }) : null,
7498
+ status ? /* @__PURE__ */ jsx4(Text4, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) : null
7222
7499
  ] }) : null,
7223
- /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
7224
- /* @__PURE__ */ jsxs3(Text3, { color: theme.dim, children: [
7500
+ /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
7501
+ /* @__PURE__ */ jsxs4(Box4, { children: [
7502
+ /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: "Activity " }),
7503
+ bursts > 0 ? /* @__PURE__ */ jsxs4(Text4, { color: theme.bad, children: [
7504
+ bursts,
7505
+ " burst min"
7506
+ ] }) : /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: "no bursts" }),
7507
+ insightsQ.loading && !insightsQ.data ? /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: " \u2026" }) : null
7508
+ ] }),
7509
+ /* @__PURE__ */ jsxs4(Box4, { children: [
7510
+ /* @__PURE__ */ jsx4(Text4, { children: "/min " }),
7511
+ minuteSeries.length ? /* @__PURE__ */ jsx4(ColoredSpark, { values: minuteSeries, limit: draft.perMinute }) : /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: "no data" })
7512
+ ] }),
7513
+ /* @__PURE__ */ jsxs4(Box4, { children: [
7514
+ /* @__PURE__ */ jsx4(Text4, { children: "/hour " }),
7515
+ hourSeries.length ? /* @__PURE__ */ jsx4(ColoredSpark, { values: hourSeries, limit: draft.perHour, width: 24 }) : /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: "no data" })
7516
+ ] })
7517
+ ] }),
7518
+ /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
7519
+ /* @__PURE__ */ jsxs4(Text4, { color: theme.dim, children: [
7225
7520
  "Change history ",
7226
7521
  history.length ? `(${history.length})` : ""
7227
7522
  ] }),
7228
- history.length ? /* @__PURE__ */ jsx3(
7523
+ history.length ? /* @__PURE__ */ jsx4(
7229
7524
  Table,
7230
7525
  {
7231
7526
  columns: [
7232
- { header: "WHEN", width: 8 },
7527
+ { header: "WHEN", width: 10 },
7233
7528
  { header: "/MIN", width: 6 },
7234
7529
  { header: "/HOUR", width: 7 },
7235
7530
  { header: "/DAY", width: 6 }
7236
7531
  ],
7237
- rows: history.slice(0, 5).map((h, i) => [
7532
+ rows: history.slice(0, 4).map((h, i) => [
7238
7533
  { value: ago(h.ts) + " ago", dim: true, bold: i === 0 },
7239
7534
  { value: String(h.minute), bold: i === 0 },
7240
7535
  { value: h.hour === 0 ? "off" : String(h.hour), dim: h.hour === 0 },
7241
7536
  { value: h.day === 0 ? "off" : String(h.day), dim: h.day === 0 }
7242
7537
  ])
7243
7538
  }
7244
- ) : /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: " no changes" })
7245
- ] }),
7246
- /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
7247
- /* @__PURE__ */ jsxs3(Text3, { color: theme.dim, children: [
7248
- "Recent bursts ",
7249
- insightsQ.loading && !insightsQ.data ? "\u2026" : `(${bursts.length})`
7250
- ] }),
7251
- bursts.length ? /* @__PURE__ */ jsx3(
7252
- Table,
7253
- {
7254
- columns: [
7255
- { header: "RESULT", width: 9 },
7256
- { header: "AGENT", width: 16 },
7257
- { header: "CALLS/LIMIT", width: 12 },
7258
- { header: "WHEN", width: 8 }
7259
- ],
7260
- rows: bursts.map((b) => [
7261
- { value: b.blocked ? "blocked" : "BYPASSED", color: b.blocked ? theme.accent : theme.bad, bold: !b.blocked },
7262
- { value: (b.agent || "\u2014").slice(0, 16), dim: true },
7263
- { value: `${b.count}/${b.limit}` },
7264
- { value: ago(b.minute) + " ago", dim: true }
7265
- ])
7266
- }
7267
- ) : /* @__PURE__ */ jsxs3(Text3, { color: theme.dim, children: [
7268
- " ",
7269
- insightsQ.error ? insightsQ.error : "none in 7d"
7270
- ] })
7539
+ ) : /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: " no changes" })
7271
7540
  ] })
7272
7541
  ] }) : null });
7273
7542
  }
7274
7543
  function FieldRow({ label, active: active2, children }) {
7275
- return /* @__PURE__ */ jsxs3(Box3, { children: [
7276
- /* @__PURE__ */ jsx3(Text3, { color: active2 ? theme.accentBright : void 0, children: (active2 ? "\u25B8 " : " ") + label.padEnd(12) }),
7544
+ return /* @__PURE__ */ jsxs4(Box4, { children: [
7545
+ /* @__PURE__ */ jsx4(Text4, { color: active2 ? theme.accentBright : void 0, children: (active2 ? "\u25B8 " : " ") + label.padEnd(12) }),
7277
7546
  children
7278
7547
  ] });
7279
7548
  }
7280
- var MODES, FIELDS;
7549
+ var MODES, FIELDS2, BLOCKS2;
7281
7550
  var init_RateLimit = __esm({
7282
7551
  "src/tui/panels/RateLimit.tsx"() {
7283
7552
  "use strict";
@@ -7286,81 +7555,138 @@ var init_RateLimit = __esm({
7286
7555
  init_hooks();
7287
7556
  init_theme();
7288
7557
  MODES = ["off", "detect", "block"];
7289
- FIELDS = ["mode", "perMinute", "perHour", "perDay"];
7558
+ FIELDS2 = ["mode", "perMinute", "perHour", "perDay"];
7559
+ BLOCKS2 = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
7290
7560
  }
7291
7561
  });
7292
7562
 
7293
7563
  // src/tui/panels/Dlp.tsx
7294
- import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
7295
- import { useEffect as useEffect4, useState as useState4 } from "react";
7296
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
7564
+ import { Box as Box5, Text as Text5, useInput as useInput3 } from "ink";
7565
+ import TextInput2 from "ink-text-input";
7566
+ import { useEffect as useEffect5, useState as useState5 } from "react";
7567
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
7297
7568
  function DlpPanel({ focused }) {
7298
7569
  const q = useLoader(() => api.settings.getSecurityLayers());
7299
- const [dlp, setDlp] = useState4(null);
7300
- const [available, setAvailable] = useState4([]);
7301
- const [sel, setSel] = useState4(0);
7302
- const [status, setStatus] = useState4(null);
7303
- useEffect4(() => {
7570
+ const [dlp, setDlp] = useState5(null);
7571
+ const [available, setAvailable] = useState5([]);
7572
+ const [sel, setSel] = useState5(0);
7573
+ const [dirty, setDirty] = useState5(false);
7574
+ const [status, setStatus] = useState5(null);
7575
+ const [adding, setAdding] = useState5(null);
7576
+ const [newName, setNewName] = useState5("");
7577
+ const [newRe, setNewRe] = useState5("");
7578
+ useEffect5(() => {
7304
7579
  if (q.data && !dlp) {
7305
- setDlp({ ...q.data.layers.dlp, patterns: [...q.data.layers.dlp.patterns] });
7580
+ setDlp({ ...q.data.layers.dlp, patterns: [...q.data.layers.dlp.patterns], custom: [...q.data.layers.dlp.custom] });
7306
7581
  setAvailable(q.data.availablePatterns);
7307
7582
  }
7308
7583
  }, [q.data, dlp]);
7309
- const persist = async (nextDlp) => {
7310
- if (!q.data) return;
7311
- setDlp(nextDlp);
7584
+ const mutate = (next) => {
7585
+ setDlp(next);
7586
+ setDirty(true);
7587
+ setStatus(null);
7588
+ };
7589
+ const save = async () => {
7590
+ if (!dlp || !q.data) return;
7312
7591
  setStatus("Saving\u2026");
7313
7592
  try {
7314
- const res = await api.settings.setSecurityLayers({ ...q.data.layers, dlp: nextDlp });
7315
- setDlp({ ...res.layers.dlp, patterns: [...res.layers.dlp.patterns] });
7593
+ const res = await api.settings.setSecurityLayers({ ...q.data.layers, dlp });
7594
+ setDlp({ ...res.layers.dlp, patterns: [...res.layers.dlp.patterns], custom: [...res.layers.dlp.custom] });
7595
+ setDirty(false);
7316
7596
  setStatus("\u2713 Saved");
7317
7597
  } catch (e) {
7318
7598
  setStatus("\u2717 " + (e instanceof Error ? e.message : String(e)));
7319
7599
  }
7320
7600
  };
7601
+ const discard = () => {
7602
+ if (q.data) setDlp({ ...q.data.layers.dlp, patterns: [...q.data.layers.dlp.patterns], custom: [...q.data.layers.dlp.custom] });
7603
+ setDirty(false);
7604
+ setStatus("discarded");
7605
+ };
7606
+ const customCount = dlp?.custom.length ?? 0;
7607
+ const total = available.length + customCount;
7321
7608
  useInput3(
7322
7609
  (input, key) => {
7323
7610
  if (!dlp) return;
7324
7611
  if (key.upArrow) setSel((n) => Math.max(0, n - 1));
7325
- else if (key.downArrow) setSel((n) => Math.min(available.length - 1, n + 1));
7326
- else if (input === " " || key.return) {
7612
+ else if (key.downArrow) setSel((n) => Math.min(total - 1, n + 1));
7613
+ else if (input === "m") {
7614
+ const idx = (MODES2.indexOf(dlp.mode) + 1) % MODES2.length;
7615
+ mutate({ ...dlp, mode: MODES2[idx] });
7616
+ } else if (input === "a") {
7617
+ setNewName("");
7618
+ setNewRe("");
7619
+ setAdding("name");
7620
+ } else if (input === " " && sel < available.length) {
7327
7621
  const p = available[sel];
7328
- if (!p) return;
7329
7622
  const set = new Set(dlp.patterns);
7330
7623
  if (set.has(p)) set.delete(p);
7331
7624
  else set.add(p);
7332
- void persist({ ...dlp, patterns: [...set] });
7333
- } else if (input === "m") {
7334
- const idx = (MODES2.indexOf(dlp.mode) + 1) % MODES2.length;
7335
- void persist({ ...dlp, mode: MODES2[idx] });
7336
- }
7625
+ mutate({ ...dlp, patterns: [...set] });
7626
+ } else if (input === "d" && sel >= available.length) {
7627
+ const ci = sel - available.length;
7628
+ mutate({ ...dlp, custom: dlp.custom.filter((_, i) => i !== ci) });
7629
+ setSel((n) => Math.max(0, n - 1));
7630
+ } else if (input === "s") void save();
7631
+ else if (input === "x") discard();
7337
7632
  },
7338
- { isActive: focused }
7633
+ { isActive: focused && !adding }
7339
7634
  );
7340
7635
  const enabled = new Set(dlp?.patterns ?? []);
7341
- return /* @__PURE__ */ jsx4(DataView, { loading: q.loading && !dlp, error: q.error, children: dlp ? /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
7342
- /* @__PURE__ */ jsxs4(Box4, { children: [
7343
- /* @__PURE__ */ jsx4(Text4, { children: "mode: " }),
7344
- /* @__PURE__ */ jsx4(Text4, { color: modeColor(dlp.mode), children: dlp.mode }),
7345
- /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: focused ? " \u2191\u2193 move \xB7 space toggle \xB7 m mode" : " press \u2192 to edit" })
7636
+ return /* @__PURE__ */ jsx5(DataView, { loading: q.loading && !dlp, error: q.error, children: dlp ? /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
7637
+ /* @__PURE__ */ jsxs5(Box5, { children: [
7638
+ /* @__PURE__ */ jsx5(Text5, { children: "mode: " }),
7639
+ /* @__PURE__ */ jsx5(Text5, { color: modeColor(dlp.mode), children: dlp.mode }),
7640
+ dirty ? /* @__PURE__ */ jsx5(Text5, { color: theme.warn, children: " \u25CF unsaved (s save \xB7 x discard)" }) : null
7346
7641
  ] }),
7347
- /* @__PURE__ */ jsx4(Box4, { marginTop: 1, flexDirection: "column", children: available.map((p, i) => {
7642
+ /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: focused ? "\u2191\u2193 move \xB7 space toggle \xB7 m mode \xB7 a add \xB7 d remove \xB7 s save" : "press \u2192 to edit" }),
7643
+ /* @__PURE__ */ jsx5(Box5, { marginTop: 1, flexDirection: "column", children: available.map((p, i) => {
7348
7644
  const on = enabled.has(p);
7349
- return /* @__PURE__ */ jsxs4(Text4, { color: focused && i === sel ? theme.accentBright : void 0, bold: focused && i === sel, children: [
7350
- (focused && i === sel ? "\u25B8 " : " ") + (on ? "\u25CF " : "\u25CB "),
7351
- /* @__PURE__ */ jsx4(Text4, { color: on ? theme.ok : theme.dim, children: p })
7645
+ const active2 = focused && sel === i;
7646
+ return /* @__PURE__ */ jsxs5(Text5, { color: active2 ? theme.accentBright : void 0, bold: active2, children: [
7647
+ (active2 ? "\u25B8 " : " ") + (on ? "\u25CF " : "\u25CB "),
7648
+ /* @__PURE__ */ jsx5(Text5, { color: on ? theme.ok : theme.dim, children: p })
7352
7649
  ] }, p);
7353
7650
  }) }),
7354
- dlp.custom.length ? /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
7355
- /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: "Custom:" }),
7356
- dlp.custom.map((c2) => /* @__PURE__ */ jsxs4(Text4, { children: [
7357
- " ",
7358
- /* @__PURE__ */ jsx4(Text4, { color: theme.accent, children: c2.name }),
7359
- " ",
7360
- /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: c2.re })
7361
- ] }, c2.name))
7651
+ /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
7652
+ /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: "Custom patterns" }),
7653
+ dlp.custom.length === 0 ? /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: " (none \u2014 press a to add)" }) : null,
7654
+ dlp.custom.map((c2, i) => {
7655
+ const active2 = focused && sel === available.length + i;
7656
+ return /* @__PURE__ */ jsxs5(Text5, { color: active2 ? theme.accentBright : void 0, bold: active2, children: [
7657
+ active2 ? "\u25B8 " : " ",
7658
+ /* @__PURE__ */ jsx5(Text5, { color: theme.accent, children: c2.name }),
7659
+ " ",
7660
+ /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: c2.re })
7661
+ ] }, c2.name + i);
7662
+ })
7663
+ ] }),
7664
+ adding ? /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, children: [
7665
+ /* @__PURE__ */ jsx5(Text5, { color: theme.warn, children: adding === "name" ? "New pattern name: " : `Regex for "${newName}": ` }),
7666
+ adding === "name" ? /* @__PURE__ */ jsx5(
7667
+ TextInput2,
7668
+ {
7669
+ value: newName,
7670
+ onChange: setNewName,
7671
+ onSubmit: (v) => {
7672
+ if (!v.trim()) return setAdding(null);
7673
+ setNewName(v.trim());
7674
+ setAdding("re");
7675
+ }
7676
+ }
7677
+ ) : /* @__PURE__ */ jsx5(
7678
+ TextInput2,
7679
+ {
7680
+ value: newRe,
7681
+ onChange: setNewRe,
7682
+ onSubmit: (v) => {
7683
+ if (v.trim() && dlp) mutate({ ...dlp, custom: [...dlp.custom, { name: newName, re: v.trim() }] });
7684
+ setAdding(null);
7685
+ }
7686
+ }
7687
+ )
7362
7688
  ] }) : null,
7363
- status ? /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) }) : null
7689
+ status ? /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) }) : null
7364
7690
  ] }) : null });
7365
7691
  }
7366
7692
  var MODES2;
@@ -7376,8 +7702,8 @@ var init_Dlp = __esm({
7376
7702
  });
7377
7703
 
7378
7704
  // src/tui/panels/Stats.tsx
7379
- import { Box as Box5, Text as Text5 } from "ink";
7380
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
7705
+ import { Box as Box6, Text as Text6 } from "ink";
7706
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
7381
7707
  function StatsPanel({ active: active2 }) {
7382
7708
  const stats = useLoader(() => api.stats.get());
7383
7709
  const ts = useLoader(() => api.stats.timeseries({ period: "24h" }));
@@ -7387,50 +7713,50 @@ function StatsPanel({ active: active2 }) {
7387
7713
  }, 5e3, active2);
7388
7714
  const s = stats.data;
7389
7715
  const points = ts.data?.timeseries ?? [];
7390
- return /* @__PURE__ */ jsx5(DataView, { loading: stats.loading && !s, error: stats.error, children: s ? /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
7391
- /* @__PURE__ */ jsxs5(Box5, { children: [
7392
- /* @__PURE__ */ jsxs5(Text5, { bold: true, children: [
7716
+ return /* @__PURE__ */ jsx6(DataView, { loading: stats.loading && !s, error: stats.error, children: s ? /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
7717
+ /* @__PURE__ */ jsxs6(Box6, { children: [
7718
+ /* @__PURE__ */ jsxs6(Text6, { bold: true, children: [
7393
7719
  s.total_calls,
7394
7720
  " "
7395
7721
  ] }),
7396
- /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: "calls " }),
7397
- /* @__PURE__ */ jsxs5(Text5, { color: theme.ok, children: [
7722
+ /* @__PURE__ */ jsx6(Text6, { color: theme.dim, children: "calls " }),
7723
+ /* @__PURE__ */ jsxs6(Text6, { color: theme.ok, children: [
7398
7724
  s.allowed,
7399
7725
  " allowed"
7400
7726
  ] }),
7401
- /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: " " }),
7402
- /* @__PURE__ */ jsxs5(Text5, { color: theme.bad, children: [
7727
+ /* @__PURE__ */ jsx6(Text6, { color: theme.dim, children: " " }),
7728
+ /* @__PURE__ */ jsxs6(Text6, { color: theme.bad, children: [
7403
7729
  s.denied,
7404
7730
  " denied"
7405
7731
  ] })
7406
7732
  ] }),
7407
- /* @__PURE__ */ jsxs5(Text5, { color: theme.dim, children: [
7733
+ /* @__PURE__ */ jsxs6(Text6, { color: theme.dim, children: [
7408
7734
  s.active_policies,
7409
7735
  " policies \xB7 ",
7410
7736
  s.registered_tools,
7411
7737
  " tools"
7412
7738
  ] }),
7413
- /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
7414
- /* @__PURE__ */ jsxs5(Text5, { color: theme.dim, children: [
7739
+ /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, flexDirection: "column", children: [
7740
+ /* @__PURE__ */ jsxs6(Text6, { color: theme.dim, children: [
7415
7741
  "Last 24h ",
7416
7742
  ts.data ? `(per ${ts.data.granularity})` : ""
7417
7743
  ] }),
7418
- /* @__PURE__ */ jsxs5(Box5, { children: [
7419
- /* @__PURE__ */ jsx5(Text5, { children: "total " }),
7420
- /* @__PURE__ */ jsx5(Text5, { color: theme.accent, children: sparkline(points.map((p) => p.total), 64) })
7744
+ /* @__PURE__ */ jsxs6(Box6, { children: [
7745
+ /* @__PURE__ */ jsx6(Text6, { children: "total " }),
7746
+ /* @__PURE__ */ jsx6(Text6, { color: theme.accent, children: sparkline(points.map((p) => p.total), 64) })
7421
7747
  ] }),
7422
- /* @__PURE__ */ jsxs5(Box5, { children: [
7423
- /* @__PURE__ */ jsx5(Text5, { children: "allowed " }),
7424
- /* @__PURE__ */ jsx5(Text5, { color: theme.ok, children: sparkline(points.map((p) => p.allowed), 64) })
7748
+ /* @__PURE__ */ jsxs6(Box6, { children: [
7749
+ /* @__PURE__ */ jsx6(Text6, { children: "allowed " }),
7750
+ /* @__PURE__ */ jsx6(Text6, { color: theme.ok, children: sparkline(points.map((p) => p.allowed), 64) })
7425
7751
  ] }),
7426
- /* @__PURE__ */ jsxs5(Box5, { children: [
7427
- /* @__PURE__ */ jsx5(Text5, { children: "denied " }),
7428
- /* @__PURE__ */ jsx5(Text5, { color: theme.bad, children: sparkline(points.map((p) => p.denied), 64) })
7752
+ /* @__PURE__ */ jsxs6(Box6, { children: [
7753
+ /* @__PURE__ */ jsx6(Text6, { children: "denied " }),
7754
+ /* @__PURE__ */ jsx6(Text6, { color: theme.bad, children: sparkline(points.map((p) => p.denied), 64) })
7429
7755
  ] })
7430
7756
  ] }),
7431
- /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
7432
- /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: "Recent" }),
7433
- /* @__PURE__ */ jsx5(
7757
+ /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, flexDirection: "column", children: [
7758
+ /* @__PURE__ */ jsx6(Text6, { color: theme.dim, children: "Recent" }),
7759
+ /* @__PURE__ */ jsx6(
7434
7760
  Table,
7435
7761
  {
7436
7762
  columns: [
@@ -7461,52 +7787,172 @@ var init_Stats = __esm({
7461
7787
  });
7462
7788
 
7463
7789
  // src/tui/panels/Audit.tsx
7464
- import { Box as Box6, Text as Text6, useInput as useInput4 } from "ink";
7465
- import { useState as useState5 } from "react";
7466
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
7790
+ import { Box as Box7, Text as Text7, useInput as useInput4 } from "ink";
7791
+ import TextInput3 from "ink-text-input";
7792
+ import { useState as useState6 } from "react";
7793
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
7467
7794
  function AuditPanel({ active: active2, focused }) {
7468
- const [fi, setFi] = useState5(0);
7469
- const filter = FILTERS[fi];
7470
- const audit = useLoader(() => api.audit.list({ filter, limit: 14 }), [fi]);
7471
- usePoll(audit.reload, 5e3, active2);
7795
+ const [di, setDi] = useState6(0);
7796
+ const [gi, setGi] = useState6(0);
7797
+ const [tool, setTool] = useState6("");
7798
+ const [agent, setAgent] = useState6("");
7799
+ const [search, setSearch] = useState6("");
7800
+ const [sel, setSel] = useState6(0);
7801
+ const [view, setView] = useState6("list");
7802
+ const [editing, setEditing] = useState6(null);
7803
+ const query = {
7804
+ filter: DECISIONS[di],
7805
+ signal: SIGNALS[gi],
7806
+ tool: tool || void 0,
7807
+ agent_name: agent || void 0,
7808
+ search: search || void 0,
7809
+ limit: 100
7810
+ };
7811
+ const auditQ = useLoader(() => api.audit.list(query), [di, gi, tool, agent, search]);
7812
+ usePoll(auditQ.reload, 6e3, active2 && view === "list" && !editing);
7813
+ const entries = auditQ.data?.entries ?? [];
7814
+ const current = entries[Math.min(sel, Math.max(0, entries.length - 1))];
7815
+ const sessionQ = useLoader(
7816
+ () => view === "detail" && current?.session_id ? api.audit.list({ session_id: current.session_id, limit: 40 }) : Promise.resolve(null),
7817
+ [view, current?.session_id]
7818
+ );
7472
7819
  useInput4(
7473
- (input) => {
7474
- if (input === "f") setFi((n) => (n + 1) % FILTERS.length);
7820
+ (input, key) => {
7821
+ if (view === "detail") {
7822
+ if (key.leftArrow || key.escape) setView("list");
7823
+ return;
7824
+ }
7825
+ if (key.upArrow) setSel((n) => Math.max(0, n - 1));
7826
+ else if (key.downArrow) setSel((n) => Math.min(entries.length - 1, n + 1));
7827
+ else if (key.return || key.rightArrow) {
7828
+ if (current) setView("detail");
7829
+ } else if (input === "f") setDi((n) => (n + 1) % DECISIONS.length);
7830
+ else if (input === "g") setGi((n) => (n + 1) % SIGNALS.length);
7831
+ else if (input === "t") setEditing("tool");
7832
+ else if (input === "n") setEditing("agent");
7833
+ else if (input === "/") setEditing("search");
7834
+ else if (input === "c") {
7835
+ setDi(0);
7836
+ setGi(0);
7837
+ setTool("");
7838
+ setAgent("");
7839
+ setSearch("");
7840
+ }
7475
7841
  },
7476
- { isActive: focused }
7842
+ { isActive: focused && !editing }
7477
7843
  );
7478
- const entries = audit.data?.entries ?? [];
7479
- return /* @__PURE__ */ jsx6(DataView, { loading: audit.loading && !audit.data, error: audit.error, empty: !!audit.data && entries.length === 0, emptyText: "No audit entries.", children: /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
7480
- /* @__PURE__ */ jsxs6(Text6, { color: theme.dim, children: [
7481
- audit.data?.total ?? 0,
7482
- " entries \xB7 filter: ",
7483
- /* @__PURE__ */ jsx6(Text6, { color: theme.accent, children: filter ?? "all" }),
7484
- " (press f)"
7844
+ if (view === "detail" && current) {
7845
+ const sessionCalls = sessionQ.data?.entries ?? [];
7846
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
7847
+ /* @__PURE__ */ jsxs7(Box7, { children: [
7848
+ /* @__PURE__ */ jsx7(Text7, { color: decisionColor(current.decision), bold: true, children: current.decision }),
7849
+ /* @__PURE__ */ jsx7(Text7, { color: theme.accent, children: " " + current.tool_name }),
7850
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " " + current.permission + " \xB7 " + current.trust_level })
7851
+ ] }),
7852
+ /* @__PURE__ */ jsx7(Detail, { label: "reason", value: current.reason ?? "\u2014" }),
7853
+ /* @__PURE__ */ jsx7(Detail, { label: "rule", value: current.matched_rule_id ?? "\u2014" }),
7854
+ /* @__PURE__ */ jsx7(Detail, { label: "agent", value: current.agent_name ?? "\u2014" }),
7855
+ /* @__PURE__ */ jsx7(Detail, { label: "session", value: current.session_id ?? "\u2014" }),
7856
+ /* @__PURE__ */ jsx7(Detail, { label: "dlp", value: current.dlp_matches?.length ? current.dlp_matches.join(", ") : "none", color: current.dlp_matches?.length ? theme.bad : void 0 }),
7857
+ /* @__PURE__ */ jsx7(Detail, { label: "when", value: new Date(current.created_at).toLocaleString() }),
7858
+ current.arguments_summary ? /* @__PURE__ */ jsx7(Detail, { label: "args", value: truncate2(JSON.stringify(current.arguments_summary), 60) }) : null,
7859
+ /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexDirection: "column", children: [
7860
+ /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
7861
+ "Session ",
7862
+ current.session_id ? `\xB7 ${sessionCalls.length} calls` : "(no session id)"
7863
+ ] }),
7864
+ current.session_id ? /* @__PURE__ */ jsx7(
7865
+ Table,
7866
+ {
7867
+ columns: [
7868
+ { header: "DECISION", width: 9 },
7869
+ { header: "TOOL", width: 20 },
7870
+ { header: "REASON", width: 26 },
7871
+ { header: "WHEN", width: 6 }
7872
+ ],
7873
+ rows: sessionCalls.slice(0, 12).map((e) => [
7874
+ { value: e.decision, color: decisionColor(e.decision) },
7875
+ { value: truncate2(e.tool_name, 20), color: theme.accent },
7876
+ { value: truncate2(e.reason ?? "\u2014", 26) },
7877
+ { value: ago(e.created_at), dim: true }
7878
+ ])
7879
+ }
7880
+ ) : null
7881
+ ] }),
7882
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "\u2190 back to list" })
7883
+ ] });
7884
+ }
7885
+ const chip = (label, val, on) => /* @__PURE__ */ jsxs7(Text7, { children: [
7886
+ /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
7887
+ label,
7888
+ ":"
7889
+ ] }),
7890
+ /* @__PURE__ */ jsx7(Text7, { color: on ? theme.accentBright : theme.dim, children: val }),
7891
+ /* @__PURE__ */ jsx7(Text7, { children: " " })
7892
+ ] });
7893
+ return /* @__PURE__ */ jsx7(DataView, { loading: auditQ.loading && !auditQ.data, error: auditQ.error, children: /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
7894
+ /* @__PURE__ */ jsxs7(Box7, { children: [
7895
+ chip("dec", DECISIONS[di] ?? "all", di !== 0),
7896
+ chip("sig", SIGNALS[gi] ?? "all", gi !== 0),
7897
+ chip("tool", tool || "\xB7", !!tool),
7898
+ chip("agent", agent || "\xB7", !!agent),
7899
+ chip("search", search || "\xB7", !!search)
7485
7900
  ] }),
7486
- /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(
7901
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: focused ? "\u2191\u2193 \xB7 enter detail \xB7 f dec \xB7 g sig \xB7 t tool \xB7 n agent \xB7 / search \xB7 c clear" : "press \u2192 to browse" }),
7902
+ editing ? /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, children: [
7903
+ /* @__PURE__ */ jsxs7(Text7, { color: theme.warn, children: [
7904
+ editing,
7905
+ ": "
7906
+ ] }),
7907
+ /* @__PURE__ */ jsx7(
7908
+ TextInput3,
7909
+ {
7910
+ value: editing === "tool" ? tool : editing === "agent" ? agent : search,
7911
+ onChange: editing === "tool" ? setTool : editing === "agent" ? setAgent : setSearch,
7912
+ onSubmit: () => setEditing(null)
7913
+ }
7914
+ )
7915
+ ] }) : null,
7916
+ /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
7917
+ auditQ.data?.total ?? 0,
7918
+ " matched \xB7 showing ",
7919
+ Math.min(entries.length, 12)
7920
+ ] }) }),
7921
+ /* @__PURE__ */ jsx7(
7487
7922
  Table,
7488
7923
  {
7489
7924
  columns: [
7925
+ { header: "", width: 2 },
7490
7926
  { header: "DECISION", width: 9 },
7491
7927
  { header: "TOOL", width: 18 },
7492
7928
  { header: "AGENT", width: 14 },
7493
- { header: "REASON", width: 26 },
7929
+ { header: "REASON", width: 24 },
7494
7930
  { header: "DLP", width: 4 },
7495
7931
  { header: "WHEN", width: 6 }
7496
7932
  ],
7497
- rows: entries.map((e) => [
7498
- { value: e.decision, color: decisionColor(e.decision) },
7499
- { value: truncate2(e.tool_name, 18), color: theme.accent },
7500
- { value: truncate2(e.agent_name ?? "\u2014", 14), dim: true },
7501
- { value: truncate2(e.reason ?? "\u2014", 26) },
7502
- { value: e.dlp_matches?.length ? String(e.dlp_matches.length) : "0", color: e.dlp_matches?.length ? theme.bad : void 0, dim: !e.dlp_matches?.length },
7503
- { value: ago(e.created_at), dim: true }
7504
- ])
7933
+ rows: entries.slice(0, 12).map((e, i) => rowFor(e, i === sel && focused))
7505
7934
  }
7506
- ) })
7935
+ )
7507
7936
  ] }) });
7508
7937
  }
7509
- var FILTERS;
7938
+ function rowFor(e, active2) {
7939
+ return [
7940
+ { value: active2 ? "\u25B8" : "", color: theme.accentBright },
7941
+ { value: e.decision, color: decisionColor(e.decision) },
7942
+ { value: truncate2(e.tool_name, 18), color: theme.accent },
7943
+ { value: truncate2(e.agent_name ?? "\u2014", 14), dim: true },
7944
+ { value: truncate2(e.reason ?? "\u2014", 24) },
7945
+ { value: e.dlp_matches?.length ? String(e.dlp_matches.length) : "0", color: e.dlp_matches?.length ? theme.bad : void 0, dim: !e.dlp_matches?.length },
7946
+ { value: ago(e.created_at), dim: true }
7947
+ ];
7948
+ }
7949
+ function Detail({ label, value, color }) {
7950
+ return /* @__PURE__ */ jsxs7(Box7, { children: [
7951
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: label.padEnd(9) }),
7952
+ /* @__PURE__ */ jsx7(Text7, { color, children: value })
7953
+ ] });
7954
+ }
7955
+ var DECISIONS, SIGNALS;
7510
7956
  var init_Audit = __esm({
7511
7957
  "src/tui/panels/Audit.tsx"() {
7512
7958
  "use strict";
@@ -7514,71 +7960,14 @@ var init_Audit = __esm({
7514
7960
  init_components();
7515
7961
  init_hooks();
7516
7962
  init_theme();
7517
- FILTERS = [void 0, "DENY", "ALLOW"];
7518
- }
7519
- });
7520
-
7521
- // src/tui/panels/Agents.tsx
7522
- import { Box as Box7, Text as Text7 } from "ink";
7523
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
7524
- function AgentsPanel({ active: active2 }) {
7525
- const agents = useLoader(() => api.agents.live({ limit: 14 }));
7526
- usePoll(agents.reload, 4e3, active2);
7527
- const data = agents.data;
7528
- const rows = data?.agents ?? [];
7529
- return /* @__PURE__ */ jsx7(DataView, { loading: agents.loading && !data, error: agents.error, empty: !!data && rows.length === 0, emptyText: "No agent sessions.", children: /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
7530
- data ? /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
7531
- /* @__PURE__ */ jsxs7(Text7, { color: theme.ok, children: [
7532
- data.counts.active,
7533
- " active"
7534
- ] }),
7535
- " \xB7 ",
7536
- /* @__PURE__ */ jsxs7(Text7, { color: theme.warn, children: [
7537
- data.counts.idle,
7538
- " idle"
7539
- ] }),
7540
- " \xB7 ",
7541
- data.counts.deactivated,
7542
- " off"
7543
- ] }) : null,
7544
- /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(
7545
- Table,
7546
- {
7547
- columns: [
7548
- { header: "STATUS", width: 7 },
7549
- { header: "AGENT", width: 20 },
7550
- { header: "CALLS", width: 6 },
7551
- { header: "DENY", width: 5 },
7552
- { header: "DLP", width: 4 },
7553
- { header: "TRUST", width: 6 },
7554
- { header: "CHARACTER", width: 20 }
7555
- ],
7556
- rows: rows.map((a) => [
7557
- { value: a.status, color: modeColor(a.status) },
7558
- { value: truncate2(a.agent_name ?? a.agent_id ?? a.session_id, 20), color: theme.accent },
7559
- { value: String(a.total_calls) },
7560
- { value: String(a.denied_calls), color: a.denied_calls ? theme.bad : void 0, dim: !a.denied_calls },
7561
- { value: String(a.dlp_events), color: a.dlp_events ? theme.bad : void 0, dim: !a.dlp_events },
7562
- { value: `${a.trust_score}` },
7563
- { value: truncate2(a.character || "\u2014", 20), dim: true }
7564
- ])
7565
- }
7566
- ) })
7567
- ] }) });
7568
- }
7569
- var init_Agents = __esm({
7570
- "src/tui/panels/Agents.tsx"() {
7571
- "use strict";
7572
- init_api_client();
7573
- init_components();
7574
- init_hooks();
7575
- init_theme();
7963
+ DECISIONS = [void 0, "DENY", "ALLOW"];
7964
+ SIGNALS = [void 0, "dlp", "ratelimit"];
7576
7965
  }
7577
7966
  });
7578
7967
 
7579
7968
  // src/tui/App.tsx
7580
7969
  import { Box as Box8, Text as Text8, useApp, useInput as useInput5 } from "ink";
7581
- import { useState as useState6 } from "react";
7970
+ import { useState as useState7 } from "react";
7582
7971
  import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
7583
7972
  function Banner() {
7584
7973
  const wide = (process.stdout.columns ?? 80) >= 82;
@@ -7595,8 +7984,8 @@ function Banner() {
7595
7984
  }
7596
7985
  function App() {
7597
7986
  const { exit } = useApp();
7598
- const [section, setSection] = useState6(0);
7599
- const [focus, setFocus] = useState6("nav");
7987
+ const [section, setSection] = useState7(0);
7988
+ const [focus, setFocus] = useState7("nav");
7600
7989
  useInput5((input, key) => {
7601
7990
  if (focus === "nav") {
7602
7991
  if (key.upArrow) setSection((n) => (n - 1 + SECTIONS.length) % SECTIONS.length);
@@ -7625,19 +8014,19 @@ var init_App = __esm({
7625
8014
  init_cli_utils();
7626
8015
  init_components();
7627
8016
  init_theme();
8017
+ init_Live();
7628
8018
  init_Policies();
7629
8019
  init_RateLimit();
7630
8020
  init_Dlp();
7631
8021
  init_Stats();
7632
8022
  init_Audit();
7633
- init_Agents();
7634
8023
  SECTIONS = [
8024
+ { label: "Live", Panel: LivePanel },
7635
8025
  { label: "Policies", Panel: PoliciesPanel },
7636
8026
  { label: "Rate Limit", Panel: RateLimitPanel },
7637
8027
  { label: "DLP", Panel: DlpPanel },
7638
8028
  { label: "Stats", Panel: StatsPanel },
7639
- { label: "Audit", Panel: AuditPanel },
7640
- { label: "Agents", Panel: AgentsPanel }
8029
+ { label: "Audit", Panel: AuditPanel }
7641
8030
  ];
7642
8031
  BANNER_HEX = ["#1432A0", "#2850BE", "#3C6ED7", "#5A8CE6", "#82AAF0", "#AAC8FA"];
7643
8032
  }
@@ -7702,10 +8091,10 @@ function truncate3(s, n) {
7702
8091
  function sparkline2(values) {
7703
8092
  if (values.length === 0) return "";
7704
8093
  const max = Math.max(...values, 0);
7705
- if (max === 0) return BLOCKS2[0].repeat(values.length);
7706
- return values.map((v) => BLOCKS2[Math.min(BLOCKS2.length - 1, Math.round(v / max * (BLOCKS2.length - 1)))]).join("");
8094
+ if (max === 0) return BLOCKS3[0].repeat(values.length);
8095
+ return values.map((v) => BLOCKS3[Math.min(BLOCKS3.length - 1, Math.round(v / max * (BLOCKS3.length - 1)))]).join("");
7707
8096
  }
7708
- var out, err, dim, bold, green, red, yellow, cyan, ANSI, width, BLOCKS2;
8097
+ var out, err, dim, bold, green, red, yellow, cyan, ANSI, width, BLOCKS3;
7709
8098
  var init_format = __esm({
7710
8099
  "src/commands/format.ts"() {
7711
8100
  "use strict";
@@ -7720,7 +8109,7 @@ var init_format = __esm({
7720
8109
  cyan = (s) => `${c.cyan}${s}${c.reset}`;
7721
8110
  ANSI = /\x1b\[[0-9;]*m/g;
7722
8111
  width = (s) => s.replace(ANSI, "").length;
7723
- BLOCKS2 = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
8112
+ BLOCKS3 = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
7724
8113
  }
7725
8114
  });
7726
8115