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