@solongate/proxy 0.79.0 → 0.81.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/tui/index.js CHANGED
@@ -8,8 +8,8 @@ var __export = (target, all) => {
8
8
  import { render } from "ink";
9
9
 
10
10
  // src/tui/App.tsx
11
- import { Box as Box8, Text as Text8, useApp, useInput as useInput6 } from "ink";
12
- import { useState as useState7 } from "react";
11
+ import { Box as Box9, Text as Text9, useApp, useInput as useInput7 } from "ink";
12
+ import { useState as useState8 } from "react";
13
13
 
14
14
  // src/cli-utils.ts
15
15
  var c = {
@@ -44,10 +44,33 @@ var BANNER_COLORS = [c.blue1, c.blue2, c.blue3, c.blue4, c.blue5, c.blue6];
44
44
  // src/tui/components.tsx
45
45
  import { Box, Text } from "ink";
46
46
 
47
+ // src/tui/config.ts
48
+ import { readFileSync } from "fs";
49
+ import { homedir } from "os";
50
+ import { join } from "path";
51
+ var cached = null;
52
+ function loadConfig() {
53
+ if (cached) return cached;
54
+ const defaults = { notifications: true };
55
+ try {
56
+ const raw = readFileSync(join(homedir(), ".solongate", "tui-config.json"), "utf-8");
57
+ const j = JSON.parse(raw);
58
+ cached = {
59
+ notifications: j.notifications !== false,
60
+ accent: typeof j.accent === "string" ? j.accent : void 0,
61
+ pollMs: typeof j.pollMs === "number" ? j.pollMs : void 0
62
+ };
63
+ } catch {
64
+ cached = defaults;
65
+ }
66
+ return cached;
67
+ }
68
+
47
69
  // src/tui/theme.ts
70
+ var accent = loadConfig().accent;
48
71
  var theme = {
49
- accent: "cyan",
50
- accentBright: "#5a8ce6",
72
+ accent: accent || "cyan",
73
+ accentBright: accent || "#5a8ce6",
51
74
  ok: "green",
52
75
  warn: "yellow",
53
76
  bad: "red",
@@ -128,15 +151,15 @@ function KeyHints({ hints }) {
128
151
  import { Box as Box2, Text as Text2, useInput } from "ink";
129
152
  import TextInput from "ink-text-input";
130
153
  import { spawn } from "child_process";
131
- import { closeSync, openSync, readSync, statSync } from "fs";
132
- import { homedir as homedir2 } from "os";
133
- import { join as join2 } from "path";
154
+ import { closeSync, mkdirSync, openSync, readSync, statSync, writeFileSync } from "fs";
155
+ import { homedir as homedir3 } from "os";
156
+ import { join as join3 } from "path";
134
157
  import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
135
158
 
136
159
  // src/api-client/client.ts
137
- import { readFileSync, existsSync } from "fs";
138
- import { resolve, join } from "path";
139
- import { homedir } from "os";
160
+ import { readFileSync as readFileSync2, existsSync } from "fs";
161
+ import { resolve, join as join2 } from "path";
162
+ import { homedir as homedir2 } from "os";
140
163
  var DEFAULT_API_URL = "https://api.solongate.com";
141
164
  var ApiError = class extends Error {
142
165
  status;
@@ -156,9 +179,9 @@ var NotAuthenticatedError = class extends Error {
156
179
  };
157
180
  function loginCredentialFile() {
158
181
  try {
159
- const p = join(homedir(), ".solongate", "cloud-guard.json");
182
+ const p = join2(homedir2(), ".solongate", "cloud-guard.json");
160
183
  if (!existsSync(p)) return {};
161
- const c2 = JSON.parse(readFileSync(p, "utf-8"));
184
+ const c2 = JSON.parse(readFileSync2(p, "utf-8"));
162
185
  return c2 && typeof c2 === "object" ? c2 : {};
163
186
  } catch {
164
187
  return {};
@@ -168,7 +191,7 @@ function dotenvApiKey() {
168
191
  try {
169
192
  const envPath = resolve(".env");
170
193
  if (!existsSync(envPath)) return void 0;
171
- for (const line of readFileSync(envPath, "utf-8").split("\n")) {
194
+ for (const line of readFileSync2(envPath, "utf-8").split("\n")) {
172
195
  const trimmed = line.trim();
173
196
  if (!trimmed || trimmed.startsWith("#")) continue;
174
197
  const eq = trimmed.indexOf("=");
@@ -181,7 +204,7 @@ function dotenvApiKey() {
181
204
  }
182
205
  return void 0;
183
206
  }
184
- var cached = null;
207
+ var cached2 = null;
185
208
  function isAuthenticated() {
186
209
  try {
187
210
  resolveCredentials();
@@ -191,13 +214,13 @@ function isAuthenticated() {
191
214
  }
192
215
  }
193
216
  function resolveCredentials(apiUrlOverride) {
194
- if (cached && !apiUrlOverride) return cached;
217
+ if (cached2 && !apiUrlOverride) return cached2;
195
218
  const file = loginCredentialFile();
196
219
  const apiKey = process.env["SOLONGATE_API_KEY"] || file.apiKey || dotenvApiKey();
197
220
  if (!apiKey) throw new NotAuthenticatedError();
198
221
  const apiUrl = apiUrlOverride || process.env["SOLONGATE_API_URL"] || file.apiUrl || DEFAULT_API_URL;
199
222
  const creds = { apiKey, apiUrl: apiUrl.replace(/\/$/, "") };
200
- if (!apiUrlOverride) cached = creds;
223
+ if (!apiUrlOverride) cached2 = creds;
201
224
  return creds;
202
225
  }
203
226
  function buildUrl(base, path, query) {
@@ -333,8 +356,10 @@ __export(settings_exports, {
333
356
  getGuardStatus: () => getGuardStatus,
334
357
  getRateLimitHistory: () => getRateLimitHistory,
335
358
  getSecurityLayers: () => getSecurityLayers,
359
+ getSelfProtection: () => getSelfProtection,
336
360
  getWebhooks: () => getWebhooks,
337
- setSecurityLayers: () => setSecurityLayers
361
+ setSecurityLayers: () => setSecurityLayers,
362
+ setSelfProtection: () => setSelfProtection
338
363
  });
339
364
  function getSecurityLayers() {
340
365
  return request("GET", "/settings/security-layers");
@@ -351,6 +376,12 @@ function clearRateLimitHistory() {
351
376
  function getGuardStatus() {
352
377
  return request("GET", "/settings/guard-status");
353
378
  }
379
+ function getSelfProtection() {
380
+ return request("GET", "/settings/self-protection");
381
+ }
382
+ function setSelfProtection(enabled) {
383
+ return request("PUT", "/settings/self-protection", { body: { enabled } });
384
+ }
354
385
  function getAlerts() {
355
386
  return request("GET", "/settings/denial-alerts");
356
387
  }
@@ -500,11 +531,12 @@ function usePoll(reload, intervalMs, enabled = true) {
500
531
 
501
532
  // src/tui/panels/Live.tsx
502
533
  import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
534
+ var CONFIG = loadConfig();
503
535
  var SPIN = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
504
536
  var BG = "#12234f";
505
537
  var DIM_FLOOR = "#233457";
506
- var LOCAL_LOG = join2(homedir2(), ".solongate", "local-logs", "solongate-audit.jsonl");
507
- var RING = join2(process.cwd(), ".solongate", ".eval-ring.jsonl");
538
+ var LOCAL_LOG = join3(homedir3(), ".solongate", "local-logs", "solongate-audit.jsonl");
539
+ var RING = join3(process.cwd(), ".solongate", ".eval-ring.jsonl");
508
540
  var hhmmss = (ts) => {
509
541
  const d = new Date(ts);
510
542
  return Number.isNaN(d.getTime()) ? "--:--:--" : d.toTimeString().slice(0, 8);
@@ -828,6 +860,7 @@ function LivePanel({ active: active2 }) {
828
860
  (id, title, msg, level = "warn") => {
829
861
  pushLog(`${level === "bad" ? "\u26D4" : "\u23F8"} ${msg}`, level);
830
862
  setAlerts((a) => [...a.filter((x) => x.id !== id), { id, msg, level, until: Date.now() + 15e3 }].slice(-4));
863
+ if (!CONFIG.notifications) return;
831
864
  try {
832
865
  process.stdout.write("\x07");
833
866
  } catch {
@@ -1055,7 +1088,16 @@ function LivePanel({ active: active2 }) {
1055
1088
  else if (input === "d") toggleSignal("deny");
1056
1089
  else if (input === "x") toggleSignal("dlp");
1057
1090
  else if (input === "r") toggleSignal("ratelimit");
1058
- else if (key.downArrow) setSel((n) => Math.min(visibleDesc.length - 1, n + 1));
1091
+ else if (input === "e") {
1092
+ const file = join3(homedir3(), ".solongate", "live-export.jsonl");
1093
+ try {
1094
+ mkdirSync(join3(homedir3(), ".solongate"), { recursive: true });
1095
+ writeFileSync(file, visibleDesc.map((x) => JSON.stringify(x)).join("\n") + "\n");
1096
+ setActionMsg({ text: `\u2713 exported ${visibleDesc.length} lines \u2192 ${file}`, level: "ok", until: Date.now() + 6e3 });
1097
+ } catch (err) {
1098
+ setActionMsg({ text: "\u2717 export failed: " + (err instanceof Error ? err.message : String(err)), level: "bad", until: Date.now() + 6e3 });
1099
+ }
1100
+ } else if (key.downArrow) setSel((n) => Math.min(visibleDesc.length - 1, n + 1));
1059
1101
  else if (key.upArrow) setSel((n) => Math.max(0, n - 1));
1060
1102
  else if (key.pageDown) setSel((n) => Math.min(visibleDesc.length - 1, n + streamBodyRows));
1061
1103
  else if (key.pageUp) setSel((n) => Math.max(0, n - streamBodyRows));
@@ -1309,7 +1351,7 @@ function LivePanel({ active: active2 }) {
1309
1351
  import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
1310
1352
  import TextInput2 from "ink-text-input";
1311
1353
  import { useEffect as useEffect3, useState as useState3 } from "react";
1312
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1354
+ import { Fragment as Fragment3, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1313
1355
  var constr = (r, g, side) => (r[g]?.[side] ?? []).join(", ");
1314
1356
  var setConstr = (r, g, side, val) => {
1315
1357
  const arr = val.split(",").map((s) => s.trim()).filter(Boolean);
@@ -1347,6 +1389,7 @@ function PoliciesPanel({ focused }) {
1347
1389
  const [pi, setPi] = useState3(0);
1348
1390
  const [ri, setRi] = useState3(0);
1349
1391
  const [fi, setFi] = useState3(0);
1392
+ const [vi, setVi] = useState3(0);
1350
1393
  const [rules, setRules] = useState3([]);
1351
1394
  const [mode, setMode] = useState3("denylist");
1352
1395
  const [dirty, setDirty] = useState3(false);
@@ -1355,6 +1398,15 @@ function PoliciesPanel({ focused }) {
1355
1398
  const [status, setStatus] = useState3(null);
1356
1399
  const selected = policies[Math.min(pi, Math.max(0, policies.length - 1))];
1357
1400
  const detail = useLoader(() => selected ? api.policies.get(selected.id) : Promise.resolve(null), [selected?.id]);
1401
+ const versionsQ = useLoader(
1402
+ () => view === "versions" && selected ? api.policies.versions(selected.id, { limit: 50 }) : Promise.resolve(null),
1403
+ [view, selected?.id]
1404
+ );
1405
+ const diffVer = versionsQ.data?.versions[vi]?.version;
1406
+ const diffQ = useLoader(
1407
+ () => view === "versions" && selected && diffVer !== void 0 ? api.policies.get(selected.id, diffVer) : Promise.resolve(null),
1408
+ [view, selected?.id, diffVer]
1409
+ );
1358
1410
  useEffect3(() => {
1359
1411
  if (detail.data) {
1360
1412
  setRules(detail.data.rules);
@@ -1429,10 +1481,41 @@ function PoliciesPanel({ focused }) {
1429
1481
  }
1430
1482
  } else if (input === "m") {
1431
1483
  mutate(rules, mode === "denylist" ? "whitelist" : "denylist");
1484
+ } else if (input === "n") {
1485
+ const nr = { id: `rule-${Date.now()}`, description: "", effect: "DENY", priority: 100, toolPattern: "*", minimumTrustLevel: "UNTRUSTED", enabled: true };
1486
+ mutate([nr, ...rules]);
1487
+ setRi(0);
1488
+ setFi(0);
1489
+ setView("rule");
1490
+ } else if (input === "D") {
1491
+ setStatus("Dry-running\u2026");
1492
+ void api.policies.dryRun({ rules, mode }).then((res) => setStatus(`dry-run ${res.evaluated} calls \xB7 allow ${res.would_allow} / deny ${res.would_deny} \xB7 newly blocked ${res.newly_blocked} \xB7 newly allowed ${res.newly_allowed}`)).catch((e) => setStatus("\u2717 " + (e instanceof Error ? e.message : String(e))));
1493
+ } else if (input === "v") {
1494
+ setVi(0);
1495
+ setView("versions");
1432
1496
  } else if (input === "s") void save();
1433
1497
  else if (input === "x") discard();
1434
1498
  return;
1435
1499
  }
1500
+ if (view === "versions") {
1501
+ const vers = versionsQ.data?.versions ?? [];
1502
+ if (key.leftArrow) return void setView("rules");
1503
+ if (key.upArrow) setVi((n) => Math.max(0, n - 1));
1504
+ else if (key.downArrow) setVi((n) => Math.min(vers.length - 1, n + 1));
1505
+ else if (key.return) {
1506
+ const v = vers[vi];
1507
+ if (v && selected) {
1508
+ setStatus("Rolling back\u2026");
1509
+ void api.policies.rollback(selected.id, v.version).then((r) => {
1510
+ setStatus(`\u2713 rolled back to v${v.version} \u2192 new v${r.version}`);
1511
+ detail.reload();
1512
+ list5.reload();
1513
+ setView("rules");
1514
+ }).catch((e) => setStatus("\u2717 " + (e instanceof Error ? e.message : String(e))));
1515
+ }
1516
+ }
1517
+ return;
1518
+ }
1436
1519
  const rule2 = rules[ri];
1437
1520
  if (!rule2) return setView("rules");
1438
1521
  const field = FIELDS[fi];
@@ -1477,7 +1560,7 @@ function PoliciesPanel({ focused }) {
1477
1560
  /* @__PURE__ */ jsx3(Text3, { color: mode === "whitelist" ? theme.warn : void 0, children: mode }),
1478
1561
  dirtyTag
1479
1562
  ] }),
1480
- /* @__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" }),
1563
+ /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: "\u2191\u2193 \xB7 enter edit \xB7 space on/off \xB7 e effect \xB7 n new \xB7 d del \xB7 m mode \xB7 D dry-run \xB7 v versions \xB7 s save \xB7 \u2190 back" }),
1481
1564
  /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(
1482
1565
  Table,
1483
1566
  {
@@ -1503,6 +1586,50 @@ function PoliciesPanel({ focused }) {
1503
1586
  status ? /* @__PURE__ */ jsx3(Text3, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) : null
1504
1587
  ] }) });
1505
1588
  }
1589
+ if (view === "versions") {
1590
+ const vers = versionsQ.data?.versions ?? [];
1591
+ return /* @__PURE__ */ jsx3(DataView, { loading: versionsQ.loading && !versionsQ.data, error: versionsQ.error, empty: !!versionsQ.data && vers.length === 0, emptyText: "No versions.", children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
1592
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: theme.accentBright, children: truncate(selected?.name ?? "", 28) }),
1593
+ /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: "\u2191\u2193 select \xB7 enter roll back to that version \xB7 \u2190 back" }),
1594
+ /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(
1595
+ Table,
1596
+ {
1597
+ columns: [
1598
+ { header: "", width: 2 },
1599
+ { header: "VER", width: 5 },
1600
+ { header: "RULES", width: 5 },
1601
+ { header: "REASON", width: 34 },
1602
+ { header: "WHEN", width: 16 }
1603
+ ],
1604
+ rows: vers.map((v, i) => [
1605
+ { value: i === vi ? "\u25B8" : "", color: theme.accentBright },
1606
+ { value: `v${v.version}`, bold: i === vi },
1607
+ { value: String(v.rules_count), dim: true },
1608
+ { value: truncate(v.reason || "\u2014", 34) },
1609
+ { value: truncate(v.created_at, 16), dim: true }
1610
+ ])
1611
+ }
1612
+ ) }),
1613
+ /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
1614
+ /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: `Diff v${diffVer ?? "?"} vs current draft` }),
1615
+ (() => {
1616
+ const old = diffQ.data?.rules ?? [];
1617
+ const cur = rules;
1618
+ const oldIds = new Set(old.map((r) => r.id));
1619
+ const curIds = new Set(cur.map((r) => r.id));
1620
+ const added = cur.filter((r) => !oldIds.has(r.id));
1621
+ const removed = old.filter((r) => !curIds.has(r.id));
1622
+ if (diffQ.loading && !diffQ.data) return /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: " loading\u2026" });
1623
+ if (!added.length && !removed.length) return /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: " identical rule set" });
1624
+ return /* @__PURE__ */ jsxs3(Fragment3, { children: [
1625
+ added.slice(0, 4).map((r) => /* @__PURE__ */ jsx3(Text3, { wrap: "truncate", color: theme.ok, children: ` + ${r.effect} ${truncate(r.toolPattern, 18)} ${truncate(r.description || r.id, 30)}` }, "a" + r.id)),
1626
+ removed.slice(0, 4).map((r) => /* @__PURE__ */ jsx3(Text3, { wrap: "truncate", color: theme.bad, children: ` - ${r.effect} ${truncate(r.toolPattern, 18)} ${truncate(r.description || r.id, 30)}` }, "r" + r.id))
1627
+ ] });
1628
+ })()
1629
+ ] }),
1630
+ status ? /* @__PURE__ */ jsx3(Text3, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) : null
1631
+ ] }) });
1632
+ }
1506
1633
  const rule = rules[ri];
1507
1634
  return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
1508
1635
  /* @__PURE__ */ jsxs3(Box3, { children: [
@@ -1616,6 +1743,20 @@ function RateLimitPanel({ focused }) {
1616
1743
  /* @__PURE__ */ jsx4(FieldRow, { label: "Per hour", active: focused && fi === 2, children: /* @__PURE__ */ jsx4(Text4, { bold: true, children: draft.perHour === 0 ? "off" : draft.perHour }) }),
1617
1744
  /* @__PURE__ */ jsx4(FieldRow, { label: "Per day", active: focused && fi === 3, children: /* @__PURE__ */ jsx4(Text4, { bold: true, children: draft.perDay === 0 ? "off" : draft.perDay }) })
1618
1745
  ] }),
1746
+ draft.perMinute > 0 ? (() => {
1747
+ const now = minuteSeries.slice(-1)[0] ?? 0;
1748
+ const pct = Math.min(100, Math.round(now / draft.perMinute * 100));
1749
+ const width = 30;
1750
+ const filled = Math.min(width, Math.round(now / draft.perMinute * width));
1751
+ const col = pct >= 100 ? theme.bad : pct >= 80 ? theme.warn : theme.ok;
1752
+ return /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, children: [
1753
+ /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: "load " }),
1754
+ /* @__PURE__ */ jsx4(Text4, { color: col, children: "\u2588".repeat(filled) }),
1755
+ /* @__PURE__ */ jsx4(Text4, { color: "#233457", children: "\u2591".repeat(Math.max(0, width - filled)) }),
1756
+ /* @__PURE__ */ jsx4(Text4, { color: col, children: ` ${now}/${draft.perMinute} ` }),
1757
+ /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: `(${pct}% this min)` })
1758
+ ] });
1759
+ })() : null,
1619
1760
  dirty || status ? /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, children: [
1620
1761
  dirty ? /* @__PURE__ */ jsx4(Text4, { color: theme.warn, children: "\u25CF unsaved \u2014 press s to apply " }) : null,
1621
1762
  status ? /* @__PURE__ */ jsx4(Text4, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) : null
@@ -1686,12 +1827,19 @@ function DlpPanel({ focused }) {
1686
1827
  const [adding, setAdding] = useState5(null);
1687
1828
  const [newName, setNewName] = useState5("");
1688
1829
  const [newRe, setNewRe] = useState5("");
1830
+ const [ghostOn, setGhostOn] = useState5(false);
1831
+ const spQ = useLoader(() => api.settings.getSelfProtection());
1832
+ const [selfProt, setSelfProt] = useState5(null);
1689
1833
  useEffect5(() => {
1690
1834
  if (q.data && !dlp) {
1691
1835
  setDlp({ ...q.data.layers.dlp, patterns: [...q.data.layers.dlp.patterns], custom: [...q.data.layers.dlp.custom] });
1692
1836
  setAvailable(q.data.availablePatterns);
1837
+ setGhostOn(q.data.layers.ghost.mode === "on");
1693
1838
  }
1694
1839
  }, [q.data, dlp]);
1840
+ useEffect5(() => {
1841
+ if (spQ.data && selfProt === null) setSelfProt(spQ.data.enabled);
1842
+ }, [spQ.data, selfProt]);
1695
1843
  const mutate = (next) => {
1696
1844
  setDlp(next);
1697
1845
  setDirty(true);
@@ -1701,14 +1849,27 @@ function DlpPanel({ focused }) {
1701
1849
  if (!dlp || !q.data) return;
1702
1850
  setStatus("Saving\u2026");
1703
1851
  try {
1704
- const res = await api.settings.setSecurityLayers({ ...q.data.layers, dlp });
1852
+ const ghost = { ...q.data.layers.ghost, mode: ghostOn ? "on" : "off" };
1853
+ const res = await api.settings.setSecurityLayers({ ...q.data.layers, dlp, ghost });
1705
1854
  setDlp({ ...res.layers.dlp, patterns: [...res.layers.dlp.patterns], custom: [...res.layers.dlp.custom] });
1855
+ setGhostOn(res.layers.ghost.mode === "on");
1706
1856
  setDirty(false);
1707
1857
  setStatus("\u2713 Saved");
1708
1858
  } catch (e) {
1709
1859
  setStatus("\u2717 " + (e instanceof Error ? e.message : String(e)));
1710
1860
  }
1711
1861
  };
1862
+ const toggleSelfProt = async () => {
1863
+ const next = !(selfProt ?? false);
1864
+ setSelfProt(next);
1865
+ try {
1866
+ await api.settings.setSelfProtection(next);
1867
+ setStatus(`\u2713 self-protection ${next ? "on" : "off"}`);
1868
+ } catch (e) {
1869
+ setSelfProt(!next);
1870
+ setStatus("\u2717 " + (e instanceof Error ? e.message : String(e)));
1871
+ }
1872
+ };
1712
1873
  const discard = () => {
1713
1874
  if (q.data) setDlp({ ...q.data.layers.dlp, patterns: [...q.data.layers.dlp.patterns], custom: [...q.data.layers.dlp.custom] });
1714
1875
  setDirty(false);
@@ -1738,7 +1899,12 @@ function DlpPanel({ focused }) {
1738
1899
  const ci = sel - available.length;
1739
1900
  mutate({ ...dlp, custom: dlp.custom.filter((_, i) => i !== ci) });
1740
1901
  setSel((n) => Math.max(0, n - 1));
1741
- } else if (input === "s") void save();
1902
+ } else if (input === "g") {
1903
+ setGhostOn((v) => !v);
1904
+ setDirty(true);
1905
+ setStatus(null);
1906
+ } else if (input === "P") void toggleSelfProt();
1907
+ else if (input === "s") void save();
1742
1908
  else if (input === "x") discard();
1743
1909
  },
1744
1910
  { isActive: focused && !adding }
@@ -1750,7 +1916,13 @@ function DlpPanel({ focused }) {
1750
1916
  /* @__PURE__ */ jsx5(Text5, { color: modeColor(dlp.mode), children: dlp.mode }),
1751
1917
  dirty ? /* @__PURE__ */ jsx5(Text5, { color: theme.warn, children: " \u25CF unsaved (s save \xB7 x discard)" }) : null
1752
1918
  ] }),
1753
- /* @__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" }),
1919
+ /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: focused ? "\u2191\u2193 \xB7 space toggle \xB7 m mode \xB7 a add \xB7 d remove \xB7 g ghost \xB7 P self-protect \xB7 s save" : "press \u2192 to edit" }),
1920
+ /* @__PURE__ */ jsxs5(Box5, { children: [
1921
+ /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: "ghost: " }),
1922
+ /* @__PURE__ */ jsx5(Text5, { color: ghostOn ? theme.ok : theme.dim, children: ghostOn ? "on" : "off" }),
1923
+ /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: " self-protection: " }),
1924
+ /* @__PURE__ */ jsx5(Text5, { color: selfProt ? theme.ok : theme.dim, children: selfProt === null ? "\u2026" : selfProt ? "on" : "off" })
1925
+ ] }),
1754
1926
  /* @__PURE__ */ jsx5(Box5, { marginTop: 1, flexDirection: "column", children: available.map((p, i) => {
1755
1927
  const on = enabled.has(p);
1756
1928
  const active2 = focused && sel === i;
@@ -1797,9 +1969,27 @@ function DlpPanel({ focused }) {
1797
1969
  }
1798
1970
  )
1799
1971
  ] }) : null,
1972
+ adding === "re" ? /* @__PURE__ */ jsx5(RegexTest, { re: newRe }) : null,
1800
1973
  status ? /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) }) : null
1801
1974
  ] }) : null });
1802
1975
  }
1976
+ var SAMPLES = ["AKIA1234567890ABCD00", "sk-ant-api03-xxxxxxxx", "ghp_16chars0000000000000000000000000000", "password=hunter2", "Bearer eyJhbGciOi"];
1977
+ function RegexTest({ re }) {
1978
+ if (!re.trim()) return /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: " regex: type a pattern\u2026" });
1979
+ let rx = null;
1980
+ let err = "";
1981
+ try {
1982
+ rx = new RegExp(re);
1983
+ } catch (e) {
1984
+ err = e instanceof Error ? e.message : String(e);
1985
+ }
1986
+ if (!rx) return /* @__PURE__ */ jsx5(Text5, { color: theme.bad, children: ` \u2717 invalid: ${err.slice(0, 50)}` });
1987
+ const hits = SAMPLES.filter((s) => rx.test(s));
1988
+ return /* @__PURE__ */ jsxs5(Text5, { wrap: "truncate", children: [
1989
+ /* @__PURE__ */ jsx5(Text5, { color: theme.ok, children: " \u2713 valid" }),
1990
+ /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: hits.length ? ` matches ${hits.length}/${SAMPLES.length} samples: ${hits.map((h) => h.slice(0, 8)).join(", ")}` : " (no sample match \u2014 will only catch your custom secrets)" })
1991
+ ] });
1992
+ }
1803
1993
 
1804
1994
  // src/tui/panels/Stats.tsx
1805
1995
  import { Box as Box6, Text as Text6 } from "ink";
@@ -1807,12 +1997,15 @@ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1807
1997
  function StatsPanel({ active: active2 }) {
1808
1998
  const stats = useLoader(() => api.stats.get());
1809
1999
  const ts = useLoader(() => api.stats.timeseries({ period: "24h" }));
2000
+ const drift2 = useLoader(() => api.stats.drift(7));
1810
2001
  usePoll(() => {
1811
2002
  stats.reload();
1812
2003
  ts.reload();
1813
2004
  }, 5e3, active2);
2005
+ usePoll(drift2.reload, 3e4, active2);
1814
2006
  const s = stats.data;
1815
2007
  const points = ts.data?.timeseries ?? [];
2008
+ const driftRules = (drift2.data?.rules ?? []).slice(0, 5);
1816
2009
  return /* @__PURE__ */ jsx6(DataView, { loading: stats.loading && !s, error: stats.error, children: s ? /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
1817
2010
  /* @__PURE__ */ jsxs6(Box6, { children: [
1818
2011
  /* @__PURE__ */ jsxs6(Text6, { bold: true, children: [
@@ -1865,7 +2058,7 @@ function StatsPanel({ active: active2 }) {
1865
2058
  { header: "MS", width: 5 },
1866
2059
  { header: "WHEN", width: 6 }
1867
2060
  ],
1868
- rows: (s.recent_activity ?? []).slice(0, 8).map((a) => [
2061
+ rows: (s.recent_activity ?? []).slice(0, 6).map((a) => [
1869
2062
  { value: a.decision, color: decisionColor(a.decision) },
1870
2063
  { value: truncate(a.tool_name, 20), color: theme.accent },
1871
2064
  { value: String(a.evaluation_time_ms ?? "\u2014"), dim: true },
@@ -1873,6 +2066,31 @@ function StatsPanel({ active: active2 }) {
1873
2066
  ])
1874
2067
  }
1875
2068
  )
2069
+ ] }),
2070
+ /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, flexDirection: "column", children: [
2071
+ /* @__PURE__ */ jsxs6(Text6, { color: theme.dim, children: [
2072
+ "Denial drift ",
2073
+ drift2.data ? `(7d: ${drift2.data.total_current} now vs ${drift2.data.total_previous} prev)` : ""
2074
+ ] }),
2075
+ driftRules.length ? /* @__PURE__ */ jsx6(
2076
+ Table,
2077
+ {
2078
+ columns: [
2079
+ { header: "NOW", width: 5 },
2080
+ { header: "PREV", width: 5 },
2081
+ { header: "\u0394", width: 6 },
2082
+ { header: "RULE", width: 22 },
2083
+ { header: "REASON", width: 26 }
2084
+ ],
2085
+ rows: driftRules.map((r) => [
2086
+ { value: String(r.current), bold: true },
2087
+ { value: String(r.previous), dim: true },
2088
+ { value: r.is_new ? "NEW" : r.spike ? `+${r.delta}` : String(r.delta), color: r.is_new ? theme.ok : r.spike ? theme.bad : void 0 },
2089
+ { value: truncate(r.rule_id ?? "\u2014", 22), color: theme.accent },
2090
+ { value: truncate(r.reason ?? r.last_tool ?? "\u2014", 26), dim: true }
2091
+ ])
2092
+ }
2093
+ ) : /* @__PURE__ */ jsx6(Text6, { color: theme.dim, children: " no denials in window" })
1876
2094
  ] })
1877
2095
  ] }) : null });
1878
2096
  }
@@ -2046,22 +2264,109 @@ function Detail({ label, value, color }) {
2046
2264
  ] });
2047
2265
  }
2048
2266
 
2267
+ // src/tui/panels/Agents.tsx
2268
+ import { Box as Box8, Text as Text8, useInput as useInput6 } from "ink";
2269
+ import { useState as useState7 } from "react";
2270
+ import { Fragment as Fragment4, jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
2271
+ var statusColor = (s) => s === "active" ? theme.ok : s === "idle" ? theme.warn : theme.dim;
2272
+ var sevColor = (s) => s === "high" ? theme.bad : s === "medium" ? theme.warn : theme.dim;
2273
+ function Bar({ label, value, max = 100, width = 16, color = theme.accent }) {
2274
+ const filled = max > 0 ? Math.round(Math.min(value, max) / max * width) : 0;
2275
+ return /* @__PURE__ */ jsxs8(Text8, { wrap: "truncate", children: [
2276
+ /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: label.padEnd(9) }),
2277
+ /* @__PURE__ */ jsx8(Text8, { color, children: "\u2588".repeat(filled) }),
2278
+ /* @__PURE__ */ jsx8(Text8, { color: "#233457", children: "\u2591".repeat(Math.max(0, width - filled)) }),
2279
+ /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: " " + value })
2280
+ ] });
2281
+ }
2282
+ function AgentsPanel({ focused }) {
2283
+ const live2 = useLoader(() => api.agents.live({ limit: 30, includeDeactivated: true }));
2284
+ usePoll(live2.reload, 6e3, focused ? false : true);
2285
+ const [sel, setSel] = useState7(0);
2286
+ const agents = (live2.data?.agents ?? []).filter((a) => a.agent_id);
2287
+ const selected = agents[Math.min(sel, Math.max(0, agents.length - 1))];
2288
+ const detail = useLoader(() => selected?.agent_id ? api.agents.get(selected.agent_id) : Promise.resolve(null), [selected?.agent_id]);
2289
+ useInput6(
2290
+ (_input, key) => {
2291
+ if (key.upArrow) setSel((n) => Math.max(0, n - 1));
2292
+ else if (key.downArrow) setSel((n) => Math.min(agents.length - 1, n + 1));
2293
+ },
2294
+ { isActive: focused }
2295
+ );
2296
+ const d = detail.data;
2297
+ const base = d?.baseline;
2298
+ const radar = base?.radar;
2299
+ const anomalies2 = d?.anomalies ?? [];
2300
+ const tmap = d?.trust_map;
2301
+ const tools = tmap?.tools ?? [];
2302
+ const resources = tmap?.resources ?? [];
2303
+ const listW = 30;
2304
+ return /* @__PURE__ */ jsx8(DataView, { loading: live2.loading && !live2.data, error: live2.error, empty: !!live2.data && agents.length === 0, emptyText: "No identified agents yet.", children: /* @__PURE__ */ jsxs8(Box8, { children: [
2305
+ /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", width: listW, marginRight: 2, overflow: "hidden", children: [
2306
+ /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: focused ? "\u2191\u2193 select agent" : "press \u2192 to browse" }),
2307
+ agents.slice(0, 16).map((a, i) => /* @__PURE__ */ jsxs8(Text8, { wrap: "truncate", backgroundColor: focused && i === sel ? "#1c2f63" : void 0, bold: i === sel, children: [
2308
+ /* @__PURE__ */ jsxs8(Text8, { color: statusColor(a.status), children: [
2309
+ a.status === "active" ? "\u25CF" : a.status === "idle" ? "\u25D0" : "\u25CB",
2310
+ " "
2311
+ ] }),
2312
+ /* @__PURE__ */ jsx8(Text8, { color: theme.accent, children: truncate(a.agent_name ?? a.agent_id ?? "?", 16).padEnd(17) }),
2313
+ /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: `t${a.trust_score}` })
2314
+ ] }, a.session_id))
2315
+ ] }),
2316
+ /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: !selected ? /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "select an agent" }) : detail.loading && !d ? /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "loading profile\u2026" }) : !d ? /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "no profile (agent has no calls yet)" }) : /* @__PURE__ */ jsxs8(Fragment4, { children: [
2317
+ /* @__PURE__ */ jsxs8(Box8, { children: [
2318
+ /* @__PURE__ */ jsx8(Text8, { bold: true, color: theme.accentBright, children: truncate(selected.agent_name ?? selected.agent_id ?? "?", 22) }),
2319
+ /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: ` trust ${base?.trustScore ?? "\u2014"}/100 \xB7 ${base?.character ?? ""}` })
2320
+ ] }),
2321
+ base?.characterBlurb ? /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: truncate(String(base.characterBlurb), 70) }) : null,
2322
+ radar ? /* @__PURE__ */ jsxs8(Box8, { marginTop: 1, flexDirection: "column", children: [
2323
+ /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "Behaviour radar (0\u2013100)" }),
2324
+ /* @__PURE__ */ jsx8(Bar, { label: "read", value: Math.round((radar.read ?? 0) * 100) }),
2325
+ /* @__PURE__ */ jsx8(Bar, { label: "write", value: Math.round((radar.write ?? 0) * 100), color: theme.warn }),
2326
+ /* @__PURE__ */ jsx8(Bar, { label: "execute", value: Math.round((radar.execute ?? 0) * 100), color: theme.warn }),
2327
+ /* @__PURE__ */ jsx8(Bar, { label: "network", value: Math.round((radar.network ?? 0) * 100) }),
2328
+ /* @__PURE__ */ jsx8(Bar, { label: "complian.", value: Math.round((radar.compliance ?? 0) * 100), color: theme.ok })
2329
+ ] }) : null,
2330
+ /* @__PURE__ */ jsxs8(Box8, { marginTop: 1, flexDirection: "column", children: [
2331
+ /* @__PURE__ */ jsxs8(Text8, { color: theme.dim, children: [
2332
+ "Anomalies ",
2333
+ anomalies2.length ? `(${anomalies2.length})` : ""
2334
+ ] }),
2335
+ anomalies2.length === 0 ? /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: " none" }) : anomalies2.slice(0, 4).map((an, i) => /* @__PURE__ */ jsxs8(Text8, { wrap: "truncate", children: [
2336
+ /* @__PURE__ */ jsx8(Text8, { color: sevColor(String(an.severity)), children: ` ${String(an.severity).toUpperCase().slice(0, 4).padEnd(5)}` }),
2337
+ /* @__PURE__ */ jsx8(Text8, { color: theme.accent, children: String(an.kind).padEnd(12) }),
2338
+ /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: truncate(String(an.description ?? ""), 44) })
2339
+ ] }, i))
2340
+ ] }),
2341
+ /* @__PURE__ */ jsxs8(Box8, { marginTop: 1, flexDirection: "column", children: [
2342
+ /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "Trust map \xB7 top tools" }),
2343
+ tools.slice(0, 4).map((t, i) => /* @__PURE__ */ jsxs8(Text8, { wrap: "truncate", children: [
2344
+ /* @__PURE__ */ jsx8(Text8, { color: t.anomalous ? theme.bad : theme.accent, children: ` ${truncate(String(t.name), 16).padEnd(17)}` }),
2345
+ /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: `${t.count}\xD7 ${t.permission ?? ""}` })
2346
+ ] }, i)),
2347
+ resources.slice(0, 3).map((r, i) => /* @__PURE__ */ jsx8(Text8, { wrap: "truncate", color: r.anomalous ? theme.bad : theme.dim, children: ` ${r.type === "domain" ? "\u{1F310}" : "\u{1F4C1}"} ${truncate(String(r.value), 40)} ${r.count}\xD7` }, "r" + i))
2348
+ ] })
2349
+ ] }) })
2350
+ ] }) });
2351
+ }
2352
+
2049
2353
  // src/tui/App.tsx
2050
- import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
2354
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
2051
2355
  function LiveHint() {
2052
- return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
2053
- /* @__PURE__ */ jsx8(Text8, { color: theme.accentBright, bold: true, children: "\u25B6 REALTIME CONSOLE" }),
2054
- /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "Fullscreen live monitor \u2014 streaming tool calls, active charts," }),
2055
- /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "counters, DLP hits and denial alerts. Updates every 2s." }),
2056
- /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsxs8(Text8, { children: [
2356
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
2357
+ /* @__PURE__ */ jsx9(Text9, { color: theme.accentBright, bold: true, children: "\u25B6 REALTIME CONSOLE" }),
2358
+ /* @__PURE__ */ jsx9(Text9, { color: theme.dim, children: "Fullscreen live monitor \u2014 streaming tool calls, active charts," }),
2359
+ /* @__PURE__ */ jsx9(Text9, { color: theme.dim, children: "counters, DLP hits and denial alerts. Updates every 2s." }),
2360
+ /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsxs9(Text9, { children: [
2057
2361
  "press ",
2058
- /* @__PURE__ */ jsx8(Text8, { color: theme.accent, children: "enter" }),
2362
+ /* @__PURE__ */ jsx9(Text9, { color: theme.accent, children: "enter" }),
2059
2363
  " to go live"
2060
2364
  ] }) })
2061
2365
  ] });
2062
2366
  }
2063
2367
  var SECTIONS = [
2064
2368
  { label: "Live", Panel: LiveHint },
2369
+ { label: "Agents", Panel: AgentsPanel },
2065
2370
  { label: "Policies", Panel: PoliciesPanel },
2066
2371
  { label: "Rate Limit", Panel: RateLimitPanel },
2067
2372
  { label: "DLP", Panel: DlpPanel },
@@ -2072,21 +2377,30 @@ var BANNER_HEX = ["#1432A0", "#2850BE", "#3C6ED7", "#5A8CE6", "#82AAF0", "#AAC8F
2072
2377
  function Banner() {
2073
2378
  const wide = (process.stdout.columns ?? 80) >= 82;
2074
2379
  if (!wide) {
2075
- return /* @__PURE__ */ jsxs8(Box8, { children: [
2076
- /* @__PURE__ */ jsx8(Text8, { bold: true, color: theme.accentBright, children: "SolonGate" }),
2077
- /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: " \u2014 security control center" })
2380
+ return /* @__PURE__ */ jsxs9(Box9, { children: [
2381
+ /* @__PURE__ */ jsx9(Text9, { bold: true, color: theme.accentBright, children: "SolonGate" }),
2382
+ /* @__PURE__ */ jsx9(Text9, { color: theme.dim, children: " \u2014 security control center" })
2078
2383
  ] });
2079
2384
  }
2080
- return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
2081
- BANNER_FULL.map((line, i) => /* @__PURE__ */ jsx8(Text8, { bold: true, color: BANNER_HEX[i], children: line }, i)),
2082
- /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: " security control center \xB7 manage policies, rate limits, DLP & more" })
2385
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
2386
+ BANNER_FULL.map((line, i) => /* @__PURE__ */ jsx9(Text9, { bold: true, color: BANNER_HEX[i], children: line }, i)),
2387
+ /* @__PURE__ */ jsx9(Text9, { color: theme.dim, children: " security control center \xB7 manage policies, rate limits, DLP & more" })
2083
2388
  ] });
2084
2389
  }
2085
2390
  function App() {
2086
2391
  const { exit } = useApp();
2087
- const [section, setSection] = useState7(0);
2088
- const [focus, setFocus] = useState7("nav");
2089
- useInput6((input, key) => {
2392
+ const [section, setSection] = useState8(0);
2393
+ const [focus, setFocus] = useState8("nav");
2394
+ const [help, setHelp] = useState8(false);
2395
+ useInput7((input, key) => {
2396
+ if (help) {
2397
+ setHelp(false);
2398
+ return;
2399
+ }
2400
+ if (input === "?" && focus === "nav") {
2401
+ setHelp(true);
2402
+ return;
2403
+ }
2090
2404
  if (focus === "nav") {
2091
2405
  if (key.upArrow) setSection((n) => (n - 1 + SECTIONS.length) % SECTIONS.length);
2092
2406
  else if (key.downArrow) setSection((n) => (n + 1) % SECTIONS.length);
@@ -2100,22 +2414,42 @@ function App() {
2100
2414
  const cols = process.stdout.columns ?? 100;
2101
2415
  const rows = process.stdout.rows ?? 30;
2102
2416
  const current = SECTIONS[section];
2417
+ if (help) return /* @__PURE__ */ jsx9(HelpOverlay, { cols, rows });
2103
2418
  if (current.label === "Live" && focus === "panel") {
2104
- return /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", width: cols, height: rows, children: /* @__PURE__ */ jsx8(LivePanel, { active: true, focused: true }) });
2419
+ return /* @__PURE__ */ jsx9(Box9, { flexDirection: "column", width: cols, height: rows, children: /* @__PURE__ */ jsx9(LivePanel, { active: true, focused: true }) });
2105
2420
  }
2106
2421
  const Panel = current.Panel;
2107
- return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", width: cols, height: rows, paddingX: 1, paddingTop: 1, children: [
2108
- /* @__PURE__ */ jsx8(Banner, {}),
2109
- /* @__PURE__ */ jsxs8(Box8, { marginTop: 1, flexGrow: 1, children: [
2110
- /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", width: 16, borderStyle: "round", borderColor: focus === "nav" ? theme.accent : "gray", paddingX: 1, children: SECTIONS.map((s, i) => /* @__PURE__ */ jsx8(Text8, { color: i === section ? theme.accentBright : void 0, bold: i === section, children: (i === section ? "\u25B8 " : " ") + s.label }, s.label)) }),
2111
- /* @__PURE__ */ jsx8(Box8, { flexGrow: 1, borderStyle: "round", borderColor: focus === "panel" ? theme.accent : "gray", paddingX: 1, paddingY: 0, children: /* @__PURE__ */ jsx8(Panel, { active: focus === "panel" || current.label !== "Live", focused: focus === "panel" }) })
2422
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", width: cols, height: rows, paddingX: 1, paddingTop: 1, children: [
2423
+ /* @__PURE__ */ jsx9(Banner, {}),
2424
+ /* @__PURE__ */ jsxs9(Box9, { marginTop: 1, flexGrow: 1, children: [
2425
+ /* @__PURE__ */ jsx9(Box9, { flexDirection: "column", width: 16, borderStyle: "round", borderColor: focus === "nav" ? theme.accent : "gray", paddingX: 1, children: SECTIONS.map((s, i) => /* @__PURE__ */ jsx9(Text9, { color: i === section ? theme.accentBright : void 0, bold: i === section, children: (i === section ? "\u25B8 " : " ") + s.label }, s.label)) }),
2426
+ /* @__PURE__ */ jsx9(Box9, { flexGrow: 1, borderStyle: "round", borderColor: focus === "panel" ? theme.accent : "gray", paddingX: 1, paddingY: 0, children: /* @__PURE__ */ jsx9(Panel, { active: focus === "panel" || current.label !== "Live", focused: focus === "panel" }) })
2112
2427
  ] }),
2113
- /* @__PURE__ */ jsx8(Box8, { children: focus === "nav" ? /* @__PURE__ */ jsx8(KeyHints, { hints: [["\u2191\u2193", "section"], ["\u2192/enter", "open"], ["q", "quit"]] }) : /* @__PURE__ */ jsx8(KeyHints, { hints: [["\u2190/esc", "back"], ["\u2191\u2193", "in-panel"], ["space/s", "act"]] }) })
2428
+ /* @__PURE__ */ jsx9(Box9, { children: focus === "nav" ? /* @__PURE__ */ jsx9(KeyHints, { hints: [["\u2191\u2193", "section"], ["\u2192/enter", "open"], ["?", "help"], ["q", "quit"]] }) : /* @__PURE__ */ jsx9(KeyHints, { hints: [["\u2190/esc", "back"], ["\u2191\u2193", "in-panel"], ["space/s", "act"]] }) })
2429
+ ] });
2430
+ }
2431
+ var HELP = [
2432
+ ["Global", [["\u2191\u2193", "move between sections"], ["\u2192 / enter", "open a section"], ["\u2190 / esc", "back to the menu"], ["?", "this help"], ["q", "quit"]]],
2433
+ ["Live", [["\u2191\u2193", "select a stream row"], ["w", "whitelist the selected DENY"], ["b", "block the selected ALLOW"], ["d / x / r", "filter denies / dlp / rate-limit"], ["f", "local / cloud filter"], ["/", "search"], ["s", "sessions (\u2191\u2193 pick, enter open)"], ["space", "copy mode (freeze)"]]],
2434
+ ["Policies", [["\u2191\u2193", "browse / select"], ["enter", "open rules \u2192 open a rule"], ["space", "toggle a rule on/off"], ["e", "flip effect"], ["n", "new rule"], ["d", "delete rule"], ["m", "flip mode"], ["D", "dry-run the draft"], ["v", "versions / rollback"], ["s", "save"], ["x", "discard"]]],
2435
+ ["Rate limit / DLP", [["\u2191\u2193", "field / pattern"], ["\u2190\u2192", "adjust (shift = \xB110)"], ["space", "toggle pattern"], ["m", "mode"], ["a / d", "add / remove custom"], ["g", "ghost mode"], ["P", "self-protection"], ["s", "save"]]]
2436
+ ];
2437
+ function HelpOverlay({ cols, rows }) {
2438
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", width: cols, height: rows, paddingX: 2, paddingTop: 1, children: [
2439
+ /* @__PURE__ */ jsx9(Text9, { bold: true, color: theme.accentBright, children: "SolonGate \u2014 keyboard shortcuts" }),
2440
+ /* @__PURE__ */ jsx9(Box9, { marginTop: 1, flexDirection: "column", children: HELP.map(([group, keys]) => /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginBottom: 1, children: [
2441
+ /* @__PURE__ */ jsx9(Text9, { bold: true, color: theme.accent, children: group }),
2442
+ keys.map(([k, desc]) => /* @__PURE__ */ jsxs9(Text9, { children: [
2443
+ /* @__PURE__ */ jsx9(Text9, { color: theme.accentBright, children: (" " + k).padEnd(16) }),
2444
+ /* @__PURE__ */ jsx9(Text9, { color: theme.dim, children: desc })
2445
+ ] }, k))
2446
+ ] }, group)) }),
2447
+ /* @__PURE__ */ jsx9(Text9, { color: theme.dim, children: "press any key to close" })
2114
2448
  ] });
2115
2449
  }
2116
2450
 
2117
2451
  // src/tui/index.tsx
2118
- import { jsx as jsx9 } from "react/jsx-runtime";
2452
+ import { jsx as jsx10 } from "react/jsx-runtime";
2119
2453
  async function launchTui() {
2120
2454
  if (!process.stdout.isTTY || !process.stdin.isTTY) {
2121
2455
  process.stderr.write(
@@ -2129,7 +2463,7 @@ async function launchTui() {
2129
2463
  }
2130
2464
  process.stdout.write("\x1B[?1049h\x1B[H");
2131
2465
  try {
2132
- const { waitUntilExit } = render(/* @__PURE__ */ jsx9(App, {}));
2466
+ const { waitUntilExit } = render(/* @__PURE__ */ jsx10(App, {}));
2133
2467
  await waitUntilExit();
2134
2468
  } finally {
2135
2469
  process.stdout.write("\x1B[?1049l");