@solongate/proxy 0.77.2 → 0.79.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
@@ -325,9 +325,15 @@ function dryRun(body) {
325
325
  var settings_exports = {};
326
326
  __export(settings_exports, {
327
327
  clearRateLimitHistory: () => clearRateLimitHistory,
328
+ createAlert: () => createAlert,
329
+ createWebhook: () => createWebhook,
330
+ deleteAlert: () => deleteAlert,
331
+ deleteWebhook: () => deleteWebhook,
332
+ getAlerts: () => getAlerts,
328
333
  getGuardStatus: () => getGuardStatus,
329
334
  getRateLimitHistory: () => getRateLimitHistory,
330
335
  getSecurityLayers: () => getSecurityLayers,
336
+ getWebhooks: () => getWebhooks,
331
337
  setSecurityLayers: () => setSecurityLayers
332
338
  });
333
339
  function getSecurityLayers() {
@@ -345,6 +351,24 @@ function clearRateLimitHistory() {
345
351
  function getGuardStatus() {
346
352
  return request("GET", "/settings/guard-status");
347
353
  }
354
+ function getAlerts() {
355
+ return request("GET", "/settings/denial-alerts");
356
+ }
357
+ function createAlert(body) {
358
+ return request("POST", "/settings/denial-alerts", { body });
359
+ }
360
+ function deleteAlert(id) {
361
+ return request("DELETE", "/settings/denial-alerts", { query: { id } });
362
+ }
363
+ function getWebhooks() {
364
+ return request("GET", "/settings/denial-webhook");
365
+ }
366
+ function createWebhook(body) {
367
+ return request("POST", "/settings/denial-webhook", { body });
368
+ }
369
+ function deleteWebhook(id) {
370
+ return request("DELETE", "/settings/denial-webhook", { query: { id } });
371
+ }
348
372
 
349
373
  // src/api-client/stats.ts
350
374
  var stats_exports = {};
@@ -370,6 +394,7 @@ function securityInsights(days) {
370
394
  // src/api-client/audit.ts
371
395
  var audit_exports = {};
372
396
  __export(audit_exports, {
397
+ block: () => block,
373
398
  list: () => list2,
374
399
  whitelist: () => whitelist
375
400
  });
@@ -379,6 +404,9 @@ function list2(query = {}) {
379
404
  function whitelist(id, scope = "exact") {
380
405
  return request("POST", `/audit-logs/${encodeURIComponent(id)}/whitelist`, { body: { scope } });
381
406
  }
407
+ function block(id, scope = "exact") {
408
+ return request("POST", `/audit-logs/${encodeURIComponent(id)}/block`, { body: { scope } });
409
+ }
382
410
 
383
411
  // src/api-client/agents.ts
384
412
  var agents_exports = {};
@@ -401,8 +429,34 @@ function anomalies(id, limit) {
401
429
  });
402
430
  }
403
431
 
432
+ // src/api-client/keys.ts
433
+ var keys_exports = {};
434
+ __export(keys_exports, {
435
+ create: () => create2,
436
+ list: () => list3,
437
+ revoke: () => revoke
438
+ });
439
+ function list3() {
440
+ return request("GET", "/keys");
441
+ }
442
+ function create2(name, isLive = true) {
443
+ return request("POST", "/keys", { body: { name, is_live: isLive } });
444
+ }
445
+ function revoke(id) {
446
+ return request("DELETE", `/keys/${encodeURIComponent(id)}`);
447
+ }
448
+
449
+ // src/api-client/mcp.ts
450
+ var mcp_exports = {};
451
+ __export(mcp_exports, {
452
+ list: () => list4
453
+ });
454
+ function list4() {
455
+ return request("GET", "/mcp-servers");
456
+ }
457
+
404
458
  // src/api-client/index.ts
405
- var api = { policies: policies_exports, settings: settings_exports, stats: stats_exports, audit: audit_exports, agents: agents_exports };
459
+ var api = { policies: policies_exports, settings: settings_exports, stats: stats_exports, audit: audit_exports, agents: agents_exports, keys: keys_exports, mcp: mcp_exports };
406
460
 
407
461
  // src/tui/hooks.ts
408
462
  import { useCallback, useEffect, useRef, useState } from "react";
@@ -475,6 +529,19 @@ function tailLines(file, maxBytes = 131072) {
475
529
  return [];
476
530
  }
477
531
  }
532
+ function extractTarget(detail) {
533
+ try {
534
+ const j = JSON.parse(detail);
535
+ const cmd = String(j.command ?? j.cmd ?? "").trim();
536
+ if (cmd) return { kind: "command", value: cmd };
537
+ const fp = String(j.file_path ?? j.path ?? "").trim();
538
+ if (fp) return { kind: "path", value: fp.split(/[\\/]/).pop() || fp };
539
+ const url = String(j.url ?? "").trim();
540
+ if (url) return { kind: "url", value: url };
541
+ } catch {
542
+ }
543
+ return null;
544
+ }
478
545
  function cloudItem(e) {
479
546
  return {
480
547
  id: "c:" + e.id,
@@ -484,6 +551,7 @@ function cloudItem(e) {
484
551
  permission: (e.permission ?? "").slice(0, 4),
485
552
  detail: (e.arguments_summary ? JSON.stringify(e.arguments_summary) : e.reason ?? "").replace(/\s+/g, " "),
486
553
  dlp: !!e.dlp_matches?.length,
554
+ burst: !!e.rate_limit_burst,
487
555
  source: "cloud",
488
556
  session: e.session_id ?? void 0,
489
557
  agent: e.agent_name ?? void 0,
@@ -539,8 +607,9 @@ function PaneTitle({ label, extra, width }) {
539
607
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: tail + "\u2500".repeat(Math.max(0, width - used)) })
540
608
  ] });
541
609
  }
542
- function StreamLine({ e, loc }) {
543
- return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
610
+ function StreamLine({ e, loc, selected }) {
611
+ return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", backgroundColor: selected ? "#1c2f63" : void 0, children: [
612
+ /* @__PURE__ */ jsx2(Text2, { color: selected ? theme.accentBright : theme.dim, children: selected ? "\u25B8" : " " }),
544
613
  /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
545
614
  "[",
546
615
  hhmmss(e.at),
@@ -574,9 +643,14 @@ function LivePanel({ active: active2 }) {
574
643
  const [ring, setRing] = useState2(null);
575
644
  const [log, setLog] = useState2([]);
576
645
  const [filter, setFilter] = useState2("all");
646
+ const [signal, setSignal] = useState2("none");
577
647
  const [search, setSearch] = useState2("");
578
648
  const [editingSearch, setEditingSearch] = useState2(false);
579
- const [scroll, setScroll] = useState2(0);
649
+ const [sel, setSel] = useState2(0);
650
+ const [actionMsg, setActionMsg] = useState2(null);
651
+ const notifiedRef = useRef2(/* @__PURE__ */ new Set());
652
+ const notifyReady = useRef2(false);
653
+ const activePolicyRef = useRef2(null);
580
654
  const [mode, setMode] = useState2("stream");
581
655
  const [pickIdx, setPickIdx] = useState2(0);
582
656
  const [detail, setDetail] = useState2(null);
@@ -586,6 +660,7 @@ function LivePanel({ active: active2 }) {
586
660
  const lastLocalTs = useRef2(0);
587
661
  const pausedUntil = useRef2(0);
588
662
  const sessRef = useRef2([]);
663
+ const mergedRef = useRef2([]);
589
664
  const prevStatus = useRef2(/* @__PURE__ */ new Map());
590
665
  const [alerts, setAlerts] = useState2([]);
591
666
  const [frozen, setFrozen] = useState2(false);
@@ -634,6 +709,7 @@ function LivePanel({ active: active2 }) {
634
709
  permission: (j.permission ?? "").slice(0, 4),
635
710
  detail: (j.arguments ? JSON.stringify(j.arguments) : j.reason ?? "").replace(/\s+/g, " "),
636
711
  dlp: !!j.dlp,
712
+ burst: !!j.rate_limit_burst,
637
713
  source: "local",
638
714
  session: j.session_id,
639
715
  agent: j.agent_name,
@@ -748,30 +824,52 @@ function LivePanel({ active: active2 }) {
748
824
  return () => clearInterval(t);
749
825
  }, [active2, frozen]);
750
826
  const startRef = useRef2(Date.now());
827
+ const fireAlert = useCallback2(
828
+ (id, title, msg, level = "warn") => {
829
+ pushLog(`${level === "bad" ? "\u26D4" : "\u23F8"} ${msg}`, level);
830
+ setAlerts((a) => [...a.filter((x) => x.id !== id), { id, msg, level, until: Date.now() + 15e3 }].slice(-4));
831
+ try {
832
+ process.stdout.write("\x07");
833
+ } catch {
834
+ }
835
+ try {
836
+ const child = spawn("notify-send", ["-a", "SolonGate", title, msg], { stdio: "ignore", detached: true });
837
+ child.on("error", () => {
838
+ });
839
+ child.unref();
840
+ } catch {
841
+ }
842
+ },
843
+ [pushLog]
844
+ );
751
845
  useEffect2(() => {
752
846
  if (!active2) return;
753
847
  for (const r of sessRef.current) {
754
848
  const prev = prevStatus.current.get(r.id);
755
849
  if (prev && prev === "active" && r.status === "idle") {
756
850
  const name = r.isMe ? `${r.agent} (this machine)` : r.agent;
757
- const msg = `${name} went IDLE, no tool call for 60s`;
758
- pushLog(`\u23F8 ${msg}`, "warn");
759
- setAlerts((a) => [...a.filter((x) => x.id !== r.id), { id: r.id, msg, until: Date.now() + 15e3 }].slice(-4));
760
- try {
761
- process.stdout.write("\x07");
762
- } catch {
763
- }
764
- try {
765
- const child = spawn("notify-send", ["-a", "SolonGate", "Agent idle", msg], { stdio: "ignore", detached: true });
766
- child.on("error", () => {
767
- });
768
- child.unref();
769
- } catch {
770
- }
851
+ fireAlert("idle:" + r.id, "Agent idle", `${name} went IDLE, no tool call for 60s`, "warn");
771
852
  }
772
853
  prevStatus.current.set(r.id, r.status);
773
854
  }
774
- }, [tick, active2, pushLog]);
855
+ }, [tick, active2, fireAlert]);
856
+ useEffect2(() => {
857
+ if (!active2) return;
858
+ const notable = mergedRef.current.filter((e) => e.decision !== "ALLOW" || e.dlp || e.burst);
859
+ if (!notifyReady.current) {
860
+ for (const e of notable) notifiedRef.current.add(e.id);
861
+ notifyReady.current = true;
862
+ return;
863
+ }
864
+ for (const e of notable) {
865
+ if (notifiedRef.current.has(e.id)) continue;
866
+ notifiedRef.current.add(e.id);
867
+ const who = e.agent ? ` \xB7 ${truncate(e.agent, 20)}` : "";
868
+ if (e.dlp) fireAlert("sec:" + e.id, "DLP hit", `SECRET in ${e.tool}${who}: ${truncate(e.detail, 60)}`, "bad");
869
+ else if (e.decision !== "ALLOW") fireAlert("sec:" + e.id, "Call denied", `DENY ${e.tool}${who}: ${truncate(e.rule ?? e.detail, 60)}`, "bad");
870
+ else if (e.burst) fireAlert("sec:" + e.id, "Rate-limit burst", `BURST ${e.tool}${who}`, "warn");
871
+ }
872
+ }, [tick, active2, fireAlert]);
775
873
  const cols = process.stdout.columns ?? 100;
776
874
  const rows = process.stdout.rows ?? 30;
777
875
  const ins = insights.data ?? {};
@@ -784,6 +882,7 @@ function LivePanel({ active: active2 }) {
784
882
  }
785
883
  const cloudDeduped = cloudBuf.filter((e) => !localKeys.has(`${e.tool}|${e.decision}|${Math.round(e.at / 6e4)}`));
786
884
  const mergedAll = [...cloudDeduped, ...localBuf].sort((a, b) => a.at - b.at);
885
+ mergedRef.current = mergedAll;
787
886
  const nowSlot = Math.floor(nowMs / 1e4);
788
887
  const traffic = new Array(60).fill(0);
789
888
  const trafficHot = new Array(60).fill(false);
@@ -843,7 +942,8 @@ function LivePanel({ active: active2 }) {
843
942
  const isLoc = (e) => e.source === "local" || !!e.session && localSessIds.has(e.session);
844
943
  const q = search.trim().toLowerCase();
845
944
  const matches = (e) => !q || `${e.tool} ${e.agent ?? ""} ${e.decision} ${e.permission} ${e.rule ?? ""} ${e.detail}`.toLowerCase().includes(q);
846
- const filtered = (filter === "all" ? mergedAll : mergedAll.filter((e) => filter === "local" ? isLoc(e) : !isLoc(e))).filter(matches);
945
+ const signalOk = (e) => signal === "none" ? true : signal === "deny" ? e.decision !== "ALLOW" : signal === "dlp" ? e.dlp : e.burst;
946
+ const filtered = (filter === "all" ? mergedAll : mergedAll.filter((e) => filter === "local" ? isLoc(e) : !isLoc(e))).filter(matches).filter(signalOk);
847
947
  const visibleDesc = filtered.slice().reverse();
848
948
  const chartH = rows >= 36 ? 6 : 4;
849
949
  const colH = rows >= 32 ? 7 : 5;
@@ -854,9 +954,11 @@ function LivePanel({ active: active2 }) {
854
954
  const colW = Math.max(20, Math.floor((innerW - 4) / 3));
855
955
  const pickable = combinedSess.slice(0, colH - 1);
856
956
  const streamBodyRows = Math.max(3, streamRows - 1);
957
+ const selClamped = Math.min(Math.max(0, sel), Math.max(0, visibleDesc.length - 1));
857
958
  const maxScroll = Math.max(0, visibleDesc.length - streamBodyRows);
858
- const clampedScroll = Math.min(scroll, maxScroll);
959
+ const clampedScroll = Math.min(Math.max(0, selClamped - Math.floor((streamBodyRows - 1) / 2)), maxScroll);
859
960
  const windowed = visibleDesc.slice(clampedScroll, clampedScroll + streamBodyRows);
961
+ const selEntry = visibleDesc[selClamped];
860
962
  const detailEntries = detail ? (() => {
861
963
  const seen = /* @__PURE__ */ new Set();
862
964
  const all = [];
@@ -868,6 +970,42 @@ function LivePanel({ active: active2 }) {
868
970
  }
869
971
  return all.sort((a, b) => b.at - a.at);
870
972
  })() : [];
973
+ const doAction = async (act) => {
974
+ const e = selEntry;
975
+ if (!e) return;
976
+ const ok = act === "whitelist" ? e.decision !== "ALLOW" : e.decision === "ALLOW";
977
+ if (!ok) {
978
+ setActionMsg({ text: act === "whitelist" ? "select a DENY to whitelist (w)" : "select an ALLOW to block (b)", level: "bad", until: Date.now() + 4e3 });
979
+ return;
980
+ }
981
+ setActionMsg({ text: act === "whitelist" ? "whitelisting\u2026" : "blocking\u2026", level: "ok", until: Date.now() + 4e3 });
982
+ try {
983
+ let res;
984
+ if (e.source === "cloud") {
985
+ const realId = e.id.slice(2);
986
+ res = act === "whitelist" ? await api.audit.whitelist(realId) : await api.audit.block(realId);
987
+ } else {
988
+ let pid = activePolicyRef.current;
989
+ if (!pid) {
990
+ const a = await api.policies.active();
991
+ pid = a.policy?.id ?? null;
992
+ activePolicyRef.current = pid;
993
+ }
994
+ if (!pid) throw new Error("no active policy resolves for this machine");
995
+ const t = extractTarget(e.detail);
996
+ res = await api.policies.addRule(pid, { toolPattern: e.tool, kind: t?.kind ?? "tool", value: t?.value, effect: act === "whitelist" ? "ALLOW" : "DENY" });
997
+ }
998
+ const verb = act === "whitelist" ? "ALLOW" : "DENY";
999
+ setActionMsg({
1000
+ text: res.deduped ? `${verb} rule already present` : `${verb} rule added \u2192 ${res.policy_id ?? "?"}${res.policy_version ? " v" + res.policy_version : ""} \xB7 reaches agents in ~30s`,
1001
+ level: "ok",
1002
+ until: Date.now() + 7e3
1003
+ });
1004
+ pushLog(`${act === "whitelist" ? "\u2713 whitelisted" : "\u26D4 blocked"} ${e.tool}`, act === "whitelist" ? "ok" : "warn");
1005
+ } catch (err) {
1006
+ setActionMsg({ text: "\u2717 " + (err instanceof Error ? err.message : String(err)), level: "bad", until: Date.now() + 7e3 });
1007
+ }
1008
+ };
871
1009
  useInput(
872
1010
  (input, key) => {
873
1011
  if (input === " ") {
@@ -902,16 +1040,25 @@ function LivePanel({ active: active2 }) {
902
1040
  } else if (input === "s" || key.leftArrow) setMode("stream");
903
1041
  return;
904
1042
  }
1043
+ const toggleSignal = (s2) => {
1044
+ setSignal((cur) => cur === s2 ? "none" : s2);
1045
+ setSel(0);
1046
+ };
905
1047
  if (input === "f") {
906
1048
  setFilter((f) => FILTERS[(FILTERS.indexOf(f) + 1) % FILTERS.length]);
907
- setScroll(0);
1049
+ setSel(0);
908
1050
  } else if (input === "s") {
909
1051
  setPickIdx(0);
910
1052
  setMode("pick");
911
- } else if (key.downArrow) setScroll((n) => Math.min(maxScroll, n + 1));
912
- else if (key.upArrow) setScroll((n) => Math.max(0, n - 1));
913
- else if (key.pageDown) setScroll((n) => Math.min(maxScroll, n + streamRows));
914
- else if (key.pageUp) setScroll((n) => Math.max(0, n - streamRows));
1053
+ } else if (input === "w") void doAction("whitelist");
1054
+ else if (input === "b") void doAction("block");
1055
+ else if (input === "d") toggleSignal("deny");
1056
+ else if (input === "x") toggleSignal("dlp");
1057
+ else if (input === "r") toggleSignal("ratelimit");
1058
+ else if (key.downArrow) setSel((n) => Math.min(visibleDesc.length - 1, n + 1));
1059
+ else if (key.upArrow) setSel((n) => Math.max(0, n - 1));
1060
+ else if (key.pageDown) setSel((n) => Math.min(visibleDesc.length - 1, n + streamBodyRows));
1061
+ else if (key.pageUp) setSel((n) => Math.max(0, n - streamBodyRows));
915
1062
  },
916
1063
  { isActive: active2 && !editingSearch }
917
1064
  );
@@ -928,7 +1075,7 @@ function LivePanel({ active: active2 }) {
928
1075
  return;
929
1076
  }
930
1077
  setSearch(v);
931
- setScroll(0);
1078
+ setSel(0);
932
1079
  setDetailScroll(0);
933
1080
  },
934
1081
  onSubmit: () => setEditingSearch(false)
@@ -1048,7 +1195,18 @@ function LivePanel({ active: active2 }) {
1048
1195
  /* @__PURE__ */ jsx2(Text2, { color: guard.data?.up_to_date ? theme.ok : theme.warn, children: guard.data ? `v${guard.data.installed ?? "?"}${guard.data.up_to_date ? "" : "\u2192v" + guard.data.latest} ${guard.data.device_count}dev` : "\xB7" }),
1049
1196
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502" })
1050
1197
  ] }),
1051
- alerts.filter((a) => nowMs < a.until).slice(-1).map((a) => /* @__PURE__ */ jsx2(Text2, { wrap: "truncate", backgroundColor: "#3d2a12", color: "#ffb454", bold: true, children: ` \u23F8 IDLE \xB7 ${truncate(a.msg, innerW - 12)} ` }, a.id)),
1198
+ alerts.filter((a) => nowMs < a.until).slice(-1).map((a) => /* @__PURE__ */ jsx2(
1199
+ Text2,
1200
+ {
1201
+ wrap: "truncate",
1202
+ backgroundColor: a.level === "bad" ? "#3d1220" : "#3d2a12",
1203
+ color: a.level === "bad" ? "#ff6b6b" : "#ffb454",
1204
+ bold: true,
1205
+ children: ` ${a.level === "bad" ? "\u26D4 ALERT" : "\u23F8 IDLE"} \xB7 ${truncate(a.msg, innerW - 14)} `
1206
+ },
1207
+ a.id
1208
+ )),
1209
+ actionMsg && nowMs < actionMsg.until ? /* @__PURE__ */ jsx2(Text2, { wrap: "truncate", backgroundColor: actionMsg.level === "bad" ? "#3d1220" : "#123d1f", color: actionMsg.level === "bad" ? "#ff6b6b" : "#7bd88f", bold: true, children: ` ${truncate(actionMsg.text, innerW - 4)} ` }) : null,
1052
1210
  /* @__PURE__ */ jsxs2(Box2, { height: 1 + chartH, children: [
1053
1211
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: leftW, marginRight: 2, children: [
1054
1212
  /* @__PURE__ */ jsx2(PaneTitle, { label: "TRAFFIC", extra: `${trafficLabel} \xB7 peak ${Math.max(0, ...traffic)} \xB7 red = denials`, width: leftW }),
@@ -1086,11 +1244,11 @@ function LivePanel({ active: active2 }) {
1086
1244
  /* @__PURE__ */ jsx2(PaneTitle, { label: "SESSIONS", extra: mode === "pick" ? "\u2191\u2193 pick \xB7 enter open" : `${combinedSess.length} \xB7 s = inspect`, width: colW }),
1087
1245
  pickable.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "scanning\u2026" }) : null,
1088
1246
  pickable.map((r, i) => {
1089
- const sel = mode === "pick" && i === pickIdx;
1247
+ const sel2 = mode === "pick" && i === pickIdx;
1090
1248
  const st = STATUS_STYLE[r.status];
1091
- return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", backgroundColor: sel ? "#1c2f63" : void 0, bold: sel || r.isMe, children: [
1092
- /* @__PURE__ */ jsxs2(Text2, { color: sel ? theme.accentBright : st.color, children: [
1093
- sel ? "\u25B8" : st.dot,
1249
+ return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", backgroundColor: sel2 ? "#1c2f63" : void 0, bold: sel2 || r.isMe, children: [
1250
+ /* @__PURE__ */ jsxs2(Text2, { color: sel2 ? theme.accentBright : st.color, children: [
1251
+ sel2 ? "\u25B8" : st.dot,
1094
1252
  " "
1095
1253
  ] }),
1096
1254
  /* @__PURE__ */ jsx2(Text2, { color: st.color, children: st.label.padEnd(7) }),
@@ -1123,7 +1281,7 @@ function LivePanel({ active: active2 }) {
1123
1281
  PaneTitle,
1124
1282
  {
1125
1283
  label: "TOOL STREAM",
1126
- extra: `${visibleDesc.length} entries${q ? " matched" : ""} \xB7 newest first \xB7 filter ${filter}${clampedScroll > 0 ? ` \xB7 \u25BC${clampedScroll} older` : ""} \xB7 f \xB7 / \xB7 \u2191\u2193 \xB7 s`,
1284
+ extra: `${selClamped + 1}/${visibleDesc.length}${signal !== "none" ? ` \xB7 ${signal}` : ""}${q ? " \xB7 search" : ""} \xB7 \u2191\u2193 select \xB7 w allow \xB7 b block \xB7 d/x/r ${filter} \xB7 s`,
1127
1285
  width: innerW
1128
1286
  }
1129
1287
  ),
@@ -1137,12 +1295,12 @@ function LivePanel({ active: active2 }) {
1137
1295
  spin,
1138
1296
  " awaiting traffic\u2026"
1139
1297
  ] }) : null,
1140
- windowed.map((e) => /* @__PURE__ */ jsx2(StreamLine, { e, loc: isLoc(e) }, e.id))
1298
+ windowed.map((e, i) => /* @__PURE__ */ jsx2(StreamLine, { e, loc: isLoc(e), selected: clampedScroll + i === selClamped }, e.id))
1141
1299
  ] }),
1142
1300
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
1143
1301
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", bold: true, children: " LIVE " }),
1144
1302
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#0b1530", color: "#4f6db8", children: ` local ${localOn ? "on" : "off"} \xB7 ${localBuf.length} loc/${cloudBuf.length} cld \xB7 top ${topTools.map(([t, c2]) => `${t}\xD7${c2}`).join(" ") || "\u2014"} ` }),
1145
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: " space copy-mode \xB7 / search \xB7 f filter \xB7 s sessions \xB7 esc menu \xB7 q quit " })
1303
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: " \u2191\u2193 select \xB7 w allow \xB7 b block \xB7 d/x/r \xB7 / search \xB7 f \xB7 s \xB7 space copy \xB7 esc \xB7 q " })
1146
1304
  ] })
1147
1305
  ] });
1148
1306
  }
@@ -1183,8 +1341,8 @@ function ruleSummary(r) {
1183
1341
  return bits.join(" ");
1184
1342
  }
1185
1343
  function PoliciesPanel({ focused }) {
1186
- const list3 = useLoader(() => api.policies.list());
1187
- const policies = list3.data?.policies ?? [];
1344
+ const list5 = useLoader(() => api.policies.list());
1345
+ const policies = list5.data?.policies ?? [];
1188
1346
  const [view, setView] = useState3("list");
1189
1347
  const [pi, setPi] = useState3(0);
1190
1348
  const [ri, setRi] = useState3(0);
@@ -1226,7 +1384,7 @@ function PoliciesPanel({ focused }) {
1226
1384
  setRules(res.rules);
1227
1385
  setDirty(false);
1228
1386
  setStatus("\u2713 Saved v" + res._version);
1229
- list3.reload();
1387
+ list5.reload();
1230
1388
  } catch (e) {
1231
1389
  setStatus("\u2717 " + (e instanceof Error ? e.message : String(e)));
1232
1390
  }
@@ -1296,7 +1454,7 @@ function PoliciesPanel({ focused }) {
1296
1454
  { isActive: focused && !editing }
1297
1455
  );
1298
1456
  if (view === "list") {
1299
- return /* @__PURE__ */ jsx3(DataView, { loading: list3.loading && !list3.data, error: list3.error, empty: !!list3.data && policies.length === 0, emptyText: "No policies.", children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
1457
+ return /* @__PURE__ */ jsx3(DataView, { loading: list5.loading && !list5.data, error: list5.error, empty: !!list5.data && policies.length === 0, emptyText: "No policies.", children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
1300
1458
  /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: focused ? "\u2191\u2193 select \xB7 enter open" : "press \u2192 to browse" }),
1301
1459
  /* @__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: [
1302
1460
  (i === pi ? "\u25B8 " : " ") + truncate(p.name, 26).padEnd(27),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.77.2",
3
+ "version": "0.79.0",
4
4
  "description": "AI tool security proxy: protect any AI tool server with customizable policies, path/command constraints, rate limiting, and audit logging. No code changes required.",
5
5
  "type": "module",
6
6
  "bin": {