@solongate/proxy 0.77.1 → 0.78.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.
@@ -20,3 +20,11 @@ export declare function whitelist(id: string, scope?: 'exact' | 'tool'): Promise
20
20
  policy_version?: number;
21
21
  message?: string;
22
22
  }>;
23
+ export declare function block(id: string, scope?: 'exact' | 'tool'): Promise<{
24
+ ok: true;
25
+ deduped: boolean;
26
+ scope: string;
27
+ policy_id: string;
28
+ policy_version?: number;
29
+ message?: string;
30
+ }>;
@@ -13,6 +13,7 @@ export interface RuleSpec {
13
13
  toolPattern?: string;
14
14
  kind?: 'command' | 'path' | 'url' | 'tool';
15
15
  value?: string;
16
+ effect?: 'ALLOW' | 'DENY';
16
17
  }
17
18
  export declare function addRule(id: string, spec: RuleSpec): Promise<{
18
19
  ok: true;
@@ -233,6 +233,7 @@ function securityInsights(days) {
233
233
  // src/api-client/audit.ts
234
234
  var audit_exports = {};
235
235
  __export(audit_exports, {
236
+ block: () => block,
236
237
  list: () => list2,
237
238
  whitelist: () => whitelist
238
239
  });
@@ -242,6 +243,9 @@ function list2(query = {}) {
242
243
  function whitelist(id, scope = "exact") {
243
244
  return request("POST", `/audit-logs/${encodeURIComponent(id)}/whitelist`, { body: { scope } });
244
245
  }
246
+ function block(id, scope = "exact") {
247
+ return request("POST", `/audit-logs/${encodeURIComponent(id)}/block`, { body: { scope } });
248
+ }
245
249
 
246
250
  // src/api-client/agents.ts
247
251
  var agents_exports = {};
package/dist/index.js CHANGED
@@ -6924,6 +6924,7 @@ var init_stats = __esm({
6924
6924
  // src/api-client/audit.ts
6925
6925
  var audit_exports = {};
6926
6926
  __export(audit_exports, {
6927
+ block: () => block,
6927
6928
  list: () => list2,
6928
6929
  whitelist: () => whitelist
6929
6930
  });
@@ -6933,6 +6934,9 @@ function list2(query = {}) {
6933
6934
  function whitelist(id, scope = "exact") {
6934
6935
  return request("POST", `/audit-logs/${encodeURIComponent(id)}/whitelist`, { body: { scope } });
6935
6936
  }
6937
+ function block(id, scope = "exact") {
6938
+ return request("POST", `/audit-logs/${encodeURIComponent(id)}/block`, { body: { scope } });
6939
+ }
6936
6940
  var init_audit = __esm({
6937
6941
  "src/api-client/audit.ts"() {
6938
6942
  "use strict";
@@ -7052,6 +7056,19 @@ function tailLines(file, maxBytes = 131072) {
7052
7056
  return [];
7053
7057
  }
7054
7058
  }
7059
+ function extractTarget(detail) {
7060
+ try {
7061
+ const j = JSON.parse(detail);
7062
+ const cmd = String(j.command ?? j.cmd ?? "").trim();
7063
+ if (cmd) return { kind: "command", value: cmd };
7064
+ const fp = String(j.file_path ?? j.path ?? "").trim();
7065
+ if (fp) return { kind: "path", value: fp.split(/[\\/]/).pop() || fp };
7066
+ const url = String(j.url ?? "").trim();
7067
+ if (url) return { kind: "url", value: url };
7068
+ } catch {
7069
+ }
7070
+ return null;
7071
+ }
7055
7072
  function cloudItem(e) {
7056
7073
  return {
7057
7074
  id: "c:" + e.id,
@@ -7061,6 +7078,7 @@ function cloudItem(e) {
7061
7078
  permission: (e.permission ?? "").slice(0, 4),
7062
7079
  detail: (e.arguments_summary ? JSON.stringify(e.arguments_summary) : e.reason ?? "").replace(/\s+/g, " "),
7063
7080
  dlp: !!e.dlp_matches?.length,
7081
+ burst: !!e.rate_limit_burst,
7064
7082
  source: "cloud",
7065
7083
  session: e.session_id ?? void 0,
7066
7084
  agent: e.agent_name ?? void 0,
@@ -7116,8 +7134,9 @@ function PaneTitle({ label, extra, width: width2 }) {
7116
7134
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: tail + "\u2500".repeat(Math.max(0, width2 - used)) })
7117
7135
  ] });
7118
7136
  }
7119
- function StreamLine({ e, loc }) {
7120
- return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7137
+ function StreamLine({ e, loc, selected }) {
7138
+ return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", backgroundColor: selected ? "#1c2f63" : void 0, children: [
7139
+ /* @__PURE__ */ jsx2(Text2, { color: selected ? theme.accentBright : theme.dim, children: selected ? "\u25B8" : " " }),
7121
7140
  /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
7122
7141
  "[",
7123
7142
  hhmmss(e.at),
@@ -7144,9 +7163,14 @@ function LivePanel({ active: active2 }) {
7144
7163
  const [ring, setRing] = useState2(null);
7145
7164
  const [log6, setLog] = useState2([]);
7146
7165
  const [filter, setFilter] = useState2("all");
7166
+ const [signal, setSignal] = useState2("none");
7147
7167
  const [search, setSearch] = useState2("");
7148
7168
  const [editingSearch, setEditingSearch] = useState2(false);
7149
- const [scroll, setScroll] = useState2(0);
7169
+ const [sel, setSel] = useState2(0);
7170
+ const [actionMsg, setActionMsg] = useState2(null);
7171
+ const notifiedRef = useRef2(/* @__PURE__ */ new Set());
7172
+ const notifyReady = useRef2(false);
7173
+ const activePolicyRef = useRef2(null);
7150
7174
  const [mode, setMode] = useState2("stream");
7151
7175
  const [pickIdx, setPickIdx] = useState2(0);
7152
7176
  const [detail, setDetail] = useState2(null);
@@ -7156,6 +7180,7 @@ function LivePanel({ active: active2 }) {
7156
7180
  const lastLocalTs = useRef2(0);
7157
7181
  const pausedUntil = useRef2(0);
7158
7182
  const sessRef = useRef2([]);
7183
+ const mergedRef = useRef2([]);
7159
7184
  const prevStatus = useRef2(/* @__PURE__ */ new Map());
7160
7185
  const [alerts, setAlerts] = useState2([]);
7161
7186
  const [frozen, setFrozen] = useState2(false);
@@ -7204,6 +7229,7 @@ function LivePanel({ active: active2 }) {
7204
7229
  permission: (j.permission ?? "").slice(0, 4),
7205
7230
  detail: (j.arguments ? JSON.stringify(j.arguments) : j.reason ?? "").replace(/\s+/g, " "),
7206
7231
  dlp: !!j.dlp,
7232
+ burst: !!j.rate_limit_burst,
7207
7233
  source: "local",
7208
7234
  session: j.session_id,
7209
7235
  agent: j.agent_name,
@@ -7318,30 +7344,52 @@ function LivePanel({ active: active2 }) {
7318
7344
  return () => clearInterval(t);
7319
7345
  }, [active2, frozen]);
7320
7346
  const startRef = useRef2(Date.now());
7347
+ const fireAlert = useCallback2(
7348
+ (id, title, msg, level = "warn") => {
7349
+ pushLog(`${level === "bad" ? "\u26D4" : "\u23F8"} ${msg}`, level);
7350
+ setAlerts((a) => [...a.filter((x) => x.id !== id), { id, msg, level, until: Date.now() + 15e3 }].slice(-4));
7351
+ try {
7352
+ process.stdout.write("\x07");
7353
+ } catch {
7354
+ }
7355
+ try {
7356
+ const child = spawn("notify-send", ["-a", "SolonGate", title, msg], { stdio: "ignore", detached: true });
7357
+ child.on("error", () => {
7358
+ });
7359
+ child.unref();
7360
+ } catch {
7361
+ }
7362
+ },
7363
+ [pushLog]
7364
+ );
7321
7365
  useEffect2(() => {
7322
7366
  if (!active2) return;
7323
7367
  for (const r of sessRef.current) {
7324
7368
  const prev = prevStatus.current.get(r.id);
7325
7369
  if (prev && prev === "active" && r.status === "idle") {
7326
7370
  const name = r.isMe ? `${r.agent} (this machine)` : r.agent;
7327
- const msg = `${name} went IDLE, no tool call for 60s`;
7328
- pushLog(`\u23F8 ${msg}`, "warn");
7329
- setAlerts((a) => [...a.filter((x) => x.id !== r.id), { id: r.id, msg, until: Date.now() + 15e3 }].slice(-4));
7330
- try {
7331
- process.stdout.write("\x07");
7332
- } catch {
7333
- }
7334
- try {
7335
- const child = spawn("notify-send", ["-a", "SolonGate", "Agent idle", msg], { stdio: "ignore", detached: true });
7336
- child.on("error", () => {
7337
- });
7338
- child.unref();
7339
- } catch {
7340
- }
7371
+ fireAlert("idle:" + r.id, "Agent idle", `${name} went IDLE, no tool call for 60s`, "warn");
7341
7372
  }
7342
7373
  prevStatus.current.set(r.id, r.status);
7343
7374
  }
7344
- }, [tick, active2, pushLog]);
7375
+ }, [tick, active2, fireAlert]);
7376
+ useEffect2(() => {
7377
+ if (!active2) return;
7378
+ const notable = mergedRef.current.filter((e) => e.decision !== "ALLOW" || e.dlp || e.burst);
7379
+ if (!notifyReady.current) {
7380
+ for (const e of notable) notifiedRef.current.add(e.id);
7381
+ notifyReady.current = true;
7382
+ return;
7383
+ }
7384
+ for (const e of notable) {
7385
+ if (notifiedRef.current.has(e.id)) continue;
7386
+ notifiedRef.current.add(e.id);
7387
+ const who = e.agent ? ` \xB7 ${truncate2(e.agent, 20)}` : "";
7388
+ if (e.dlp) fireAlert("sec:" + e.id, "DLP hit", `SECRET in ${e.tool}${who}: ${truncate2(e.detail, 60)}`, "bad");
7389
+ else if (e.decision !== "ALLOW") fireAlert("sec:" + e.id, "Call denied", `DENY ${e.tool}${who}: ${truncate2(e.rule ?? e.detail, 60)}`, "bad");
7390
+ else if (e.burst) fireAlert("sec:" + e.id, "Rate-limit burst", `BURST ${e.tool}${who}`, "warn");
7391
+ }
7392
+ }, [tick, active2, fireAlert]);
7345
7393
  const cols = process.stdout.columns ?? 100;
7346
7394
  const rows = process.stdout.rows ?? 30;
7347
7395
  const ins = insights.data ?? {};
@@ -7354,6 +7402,7 @@ function LivePanel({ active: active2 }) {
7354
7402
  }
7355
7403
  const cloudDeduped = cloudBuf.filter((e) => !localKeys.has(`${e.tool}|${e.decision}|${Math.round(e.at / 6e4)}`));
7356
7404
  const mergedAll = [...cloudDeduped, ...localBuf].sort((a, b) => a.at - b.at);
7405
+ mergedRef.current = mergedAll;
7357
7406
  const nowSlot = Math.floor(nowMs / 1e4);
7358
7407
  const traffic = new Array(60).fill(0);
7359
7408
  const trafficHot = new Array(60).fill(false);
@@ -7413,7 +7462,8 @@ function LivePanel({ active: active2 }) {
7413
7462
  const isLoc = (e) => e.source === "local" || !!e.session && localSessIds.has(e.session);
7414
7463
  const q = search.trim().toLowerCase();
7415
7464
  const matches = (e) => !q || `${e.tool} ${e.agent ?? ""} ${e.decision} ${e.permission} ${e.rule ?? ""} ${e.detail}`.toLowerCase().includes(q);
7416
- const filtered = (filter === "all" ? mergedAll : mergedAll.filter((e) => filter === "local" ? isLoc(e) : !isLoc(e))).filter(matches);
7465
+ const signalOk = (e) => signal === "none" ? true : signal === "deny" ? e.decision !== "ALLOW" : signal === "dlp" ? e.dlp : e.burst;
7466
+ const filtered = (filter === "all" ? mergedAll : mergedAll.filter((e) => filter === "local" ? isLoc(e) : !isLoc(e))).filter(matches).filter(signalOk);
7417
7467
  const visibleDesc = filtered.slice().reverse();
7418
7468
  const chartH = rows >= 36 ? 6 : 4;
7419
7469
  const colH = rows >= 32 ? 7 : 5;
@@ -7424,9 +7474,11 @@ function LivePanel({ active: active2 }) {
7424
7474
  const colW = Math.max(20, Math.floor((innerW - 4) / 3));
7425
7475
  const pickable = combinedSess.slice(0, colH - 1);
7426
7476
  const streamBodyRows = Math.max(3, streamRows - 1);
7477
+ const selClamped = Math.min(Math.max(0, sel), Math.max(0, visibleDesc.length - 1));
7427
7478
  const maxScroll = Math.max(0, visibleDesc.length - streamBodyRows);
7428
- const clampedScroll = Math.min(scroll, maxScroll);
7479
+ const clampedScroll = Math.min(Math.max(0, selClamped - Math.floor((streamBodyRows - 1) / 2)), maxScroll);
7429
7480
  const windowed = visibleDesc.slice(clampedScroll, clampedScroll + streamBodyRows);
7481
+ const selEntry = visibleDesc[selClamped];
7430
7482
  const detailEntries = detail ? (() => {
7431
7483
  const seen = /* @__PURE__ */ new Set();
7432
7484
  const all = [];
@@ -7438,6 +7490,42 @@ function LivePanel({ active: active2 }) {
7438
7490
  }
7439
7491
  return all.sort((a, b) => b.at - a.at);
7440
7492
  })() : [];
7493
+ const doAction = async (act) => {
7494
+ const e = selEntry;
7495
+ if (!e) return;
7496
+ const ok = act === "whitelist" ? e.decision !== "ALLOW" : e.decision === "ALLOW";
7497
+ if (!ok) {
7498
+ setActionMsg({ text: act === "whitelist" ? "select a DENY to whitelist (w)" : "select an ALLOW to block (b)", level: "bad", until: Date.now() + 4e3 });
7499
+ return;
7500
+ }
7501
+ setActionMsg({ text: act === "whitelist" ? "whitelisting\u2026" : "blocking\u2026", level: "ok", until: Date.now() + 4e3 });
7502
+ try {
7503
+ let res;
7504
+ if (e.source === "cloud") {
7505
+ const realId = e.id.slice(2);
7506
+ res = act === "whitelist" ? await api.audit.whitelist(realId) : await api.audit.block(realId);
7507
+ } else {
7508
+ let pid = activePolicyRef.current;
7509
+ if (!pid) {
7510
+ const a = await api.policies.active();
7511
+ pid = a.policy?.id ?? null;
7512
+ activePolicyRef.current = pid;
7513
+ }
7514
+ if (!pid) throw new Error("no active policy resolves for this machine");
7515
+ const t = extractTarget(e.detail);
7516
+ res = await api.policies.addRule(pid, { toolPattern: e.tool, kind: t?.kind ?? "tool", value: t?.value, effect: act === "whitelist" ? "ALLOW" : "DENY" });
7517
+ }
7518
+ const verb = act === "whitelist" ? "ALLOW" : "DENY";
7519
+ setActionMsg({
7520
+ 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`,
7521
+ level: "ok",
7522
+ until: Date.now() + 7e3
7523
+ });
7524
+ pushLog(`${act === "whitelist" ? "\u2713 whitelisted" : "\u26D4 blocked"} ${e.tool}`, act === "whitelist" ? "ok" : "warn");
7525
+ } catch (err2) {
7526
+ setActionMsg({ text: "\u2717 " + (err2 instanceof Error ? err2.message : String(err2)), level: "bad", until: Date.now() + 7e3 });
7527
+ }
7528
+ };
7441
7529
  useInput(
7442
7530
  (input, key) => {
7443
7531
  if (input === " ") {
@@ -7472,16 +7560,25 @@ function LivePanel({ active: active2 }) {
7472
7560
  } else if (input === "s" || key.leftArrow) setMode("stream");
7473
7561
  return;
7474
7562
  }
7563
+ const toggleSignal = (s2) => {
7564
+ setSignal((cur) => cur === s2 ? "none" : s2);
7565
+ setSel(0);
7566
+ };
7475
7567
  if (input === "f") {
7476
7568
  setFilter((f) => FILTERS[(FILTERS.indexOf(f) + 1) % FILTERS.length]);
7477
- setScroll(0);
7569
+ setSel(0);
7478
7570
  } else if (input === "s") {
7479
7571
  setPickIdx(0);
7480
7572
  setMode("pick");
7481
- } else if (key.downArrow) setScroll((n) => Math.min(maxScroll, n + 1));
7482
- else if (key.upArrow) setScroll((n) => Math.max(0, n - 1));
7483
- else if (key.pageDown) setScroll((n) => Math.min(maxScroll, n + streamRows));
7484
- else if (key.pageUp) setScroll((n) => Math.max(0, n - streamRows));
7573
+ } else if (input === "w") void doAction("whitelist");
7574
+ else if (input === "b") void doAction("block");
7575
+ else if (input === "d") toggleSignal("deny");
7576
+ else if (input === "x") toggleSignal("dlp");
7577
+ else if (input === "r") toggleSignal("ratelimit");
7578
+ else if (key.downArrow) setSel((n) => Math.min(visibleDesc.length - 1, n + 1));
7579
+ else if (key.upArrow) setSel((n) => Math.max(0, n - 1));
7580
+ else if (key.pageDown) setSel((n) => Math.min(visibleDesc.length - 1, n + streamBodyRows));
7581
+ else if (key.pageUp) setSel((n) => Math.max(0, n - streamBodyRows));
7485
7582
  },
7486
7583
  { isActive: active2 && !editingSearch }
7487
7584
  );
@@ -7498,7 +7595,7 @@ function LivePanel({ active: active2 }) {
7498
7595
  return;
7499
7596
  }
7500
7597
  setSearch(v);
7501
- setScroll(0);
7598
+ setSel(0);
7502
7599
  setDetailScroll(0);
7503
7600
  },
7504
7601
  onSubmit: () => setEditingSearch(false)
@@ -7618,7 +7715,18 @@ function LivePanel({ active: active2 }) {
7618
7715
  /* @__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" }),
7619
7716
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502" })
7620
7717
  ] }),
7621
- 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 ${truncate2(a.msg, innerW - 12)} ` }, a.id)),
7718
+ alerts.filter((a) => nowMs < a.until).slice(-1).map((a) => /* @__PURE__ */ jsx2(
7719
+ Text2,
7720
+ {
7721
+ wrap: "truncate",
7722
+ backgroundColor: a.level === "bad" ? "#3d1220" : "#3d2a12",
7723
+ color: a.level === "bad" ? "#ff6b6b" : "#ffb454",
7724
+ bold: true,
7725
+ children: ` ${a.level === "bad" ? "\u26D4 ALERT" : "\u23F8 IDLE"} \xB7 ${truncate2(a.msg, innerW - 14)} `
7726
+ },
7727
+ a.id
7728
+ )),
7729
+ actionMsg && nowMs < actionMsg.until ? /* @__PURE__ */ jsx2(Text2, { wrap: "truncate", backgroundColor: actionMsg.level === "bad" ? "#3d1220" : "#123d1f", color: actionMsg.level === "bad" ? "#ff6b6b" : "#7bd88f", bold: true, children: ` ${truncate2(actionMsg.text, innerW - 4)} ` }) : null,
7622
7730
  /* @__PURE__ */ jsxs2(Box2, { height: 1 + chartH, children: [
7623
7731
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: leftW, marginRight: 2, children: [
7624
7732
  /* @__PURE__ */ jsx2(PaneTitle, { label: "TRAFFIC", extra: `${trafficLabel} \xB7 peak ${Math.max(0, ...traffic)} \xB7 red = denials`, width: leftW }),
@@ -7656,11 +7764,11 @@ function LivePanel({ active: active2 }) {
7656
7764
  /* @__PURE__ */ jsx2(PaneTitle, { label: "SESSIONS", extra: mode === "pick" ? "\u2191\u2193 pick \xB7 enter open" : `${combinedSess.length} \xB7 s = inspect`, width: colW }),
7657
7765
  pickable.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "scanning\u2026" }) : null,
7658
7766
  pickable.map((r, i) => {
7659
- const sel = mode === "pick" && i === pickIdx;
7767
+ const sel2 = mode === "pick" && i === pickIdx;
7660
7768
  const st = STATUS_STYLE[r.status];
7661
- return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", backgroundColor: sel ? "#1c2f63" : void 0, bold: sel || r.isMe, children: [
7662
- /* @__PURE__ */ jsxs2(Text2, { color: sel ? theme.accentBright : st.color, children: [
7663
- sel ? "\u25B8" : st.dot,
7769
+ return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", backgroundColor: sel2 ? "#1c2f63" : void 0, bold: sel2 || r.isMe, children: [
7770
+ /* @__PURE__ */ jsxs2(Text2, { color: sel2 ? theme.accentBright : st.color, children: [
7771
+ sel2 ? "\u25B8" : st.dot,
7664
7772
  " "
7665
7773
  ] }),
7666
7774
  /* @__PURE__ */ jsx2(Text2, { color: st.color, children: st.label.padEnd(7) }),
@@ -7693,7 +7801,7 @@ function LivePanel({ active: active2 }) {
7693
7801
  PaneTitle,
7694
7802
  {
7695
7803
  label: "TOOL STREAM",
7696
- 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`,
7804
+ 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`,
7697
7805
  width: innerW
7698
7806
  }
7699
7807
  ),
@@ -7707,12 +7815,12 @@ function LivePanel({ active: active2 }) {
7707
7815
  spin,
7708
7816
  " awaiting traffic\u2026"
7709
7817
  ] }) : null,
7710
- windowed.map((e) => /* @__PURE__ */ jsx2(StreamLine, { e, loc: isLoc(e) }, e.id))
7818
+ windowed.map((e, i) => /* @__PURE__ */ jsx2(StreamLine, { e, loc: isLoc(e), selected: clampedScroll + i === selClamped }, e.id))
7711
7819
  ] }),
7712
7820
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7713
7821
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", bold: true, children: " LIVE " }),
7714
7822
  /* @__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"} ` }),
7715
- /* @__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 " })
7823
+ /* @__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 " })
7716
7824
  ] })
7717
7825
  ] });
7718
7826
  }
@@ -9560,13 +9668,13 @@ function shimTargets() {
9560
9668
  }
9561
9669
  return [".bashrc", ".zshrc", ".profile"].map((f) => join6(homedir4(), f)).filter((f) => existsSync4(f));
9562
9670
  }
9563
- function writeShimBlock(file, block) {
9671
+ function writeShimBlock(file, block2) {
9564
9672
  const re = new RegExp(escapeRe(SHIM_BEGIN) + "[\\s\\S]*?" + escapeRe(SHIM_END) + "\\r?\\n?", "g");
9565
9673
  let content = existsSync4(file) ? readFileSync6(file, "utf-8") : "";
9566
9674
  content = content.replace(re, "");
9567
- if (block) {
9675
+ if (block2) {
9568
9676
  if (content.length && !content.endsWith("\n")) content += "\n";
9569
- content += block + "\n";
9677
+ content += block2 + "\n";
9570
9678
  }
9571
9679
  mkdirSync3(dirname(file), { recursive: true });
9572
9680
  writeFileSync3(file, content);
@@ -9580,7 +9688,7 @@ function installClaudeShim(shieldPath) {
9580
9688
  const node = process.execPath.replace(/\\/g, "/");
9581
9689
  const shield = shieldPath.replace(/\\/g, "/");
9582
9690
  const win = process.platform === "win32";
9583
- const block = win ? `${SHIM_BEGIN}
9691
+ const block2 = win ? `${SHIM_BEGIN}
9584
9692
  function claude { & "${node}" "${shield}" -- "${real}" @args }
9585
9693
  ${SHIM_END}` : `${SHIM_BEGIN}
9586
9694
  claude() { "${node}" "${shield}" -- "${real}" "$@"; }
@@ -9592,7 +9700,7 @@ ${SHIM_END}`;
9592
9700
  }
9593
9701
  for (const file of targets) {
9594
9702
  try {
9595
- writeShimBlock(file, block);
9703
+ writeShimBlock(file, block2);
9596
9704
  console.log(` Auto-shield on: \`claude\` now masks secrets (${file})`);
9597
9705
  } catch (e) {
9598
9706
  console.log(` (Could not enable auto-shield in ${file}: ${e.message})`);
package/dist/tui/index.js CHANGED
@@ -370,6 +370,7 @@ function securityInsights(days) {
370
370
  // src/api-client/audit.ts
371
371
  var audit_exports = {};
372
372
  __export(audit_exports, {
373
+ block: () => block,
373
374
  list: () => list2,
374
375
  whitelist: () => whitelist
375
376
  });
@@ -379,6 +380,9 @@ function list2(query = {}) {
379
380
  function whitelist(id, scope = "exact") {
380
381
  return request("POST", `/audit-logs/${encodeURIComponent(id)}/whitelist`, { body: { scope } });
381
382
  }
383
+ function block(id, scope = "exact") {
384
+ return request("POST", `/audit-logs/${encodeURIComponent(id)}/block`, { body: { scope } });
385
+ }
382
386
 
383
387
  // src/api-client/agents.ts
384
388
  var agents_exports = {};
@@ -475,6 +479,19 @@ function tailLines(file, maxBytes = 131072) {
475
479
  return [];
476
480
  }
477
481
  }
482
+ function extractTarget(detail) {
483
+ try {
484
+ const j = JSON.parse(detail);
485
+ const cmd = String(j.command ?? j.cmd ?? "").trim();
486
+ if (cmd) return { kind: "command", value: cmd };
487
+ const fp = String(j.file_path ?? j.path ?? "").trim();
488
+ if (fp) return { kind: "path", value: fp.split(/[\\/]/).pop() || fp };
489
+ const url = String(j.url ?? "").trim();
490
+ if (url) return { kind: "url", value: url };
491
+ } catch {
492
+ }
493
+ return null;
494
+ }
478
495
  function cloudItem(e) {
479
496
  return {
480
497
  id: "c:" + e.id,
@@ -484,6 +501,7 @@ function cloudItem(e) {
484
501
  permission: (e.permission ?? "").slice(0, 4),
485
502
  detail: (e.arguments_summary ? JSON.stringify(e.arguments_summary) : e.reason ?? "").replace(/\s+/g, " "),
486
503
  dlp: !!e.dlp_matches?.length,
504
+ burst: !!e.rate_limit_burst,
487
505
  source: "cloud",
488
506
  session: e.session_id ?? void 0,
489
507
  agent: e.agent_name ?? void 0,
@@ -539,8 +557,9 @@ function PaneTitle({ label, extra, width }) {
539
557
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: tail + "\u2500".repeat(Math.max(0, width - used)) })
540
558
  ] });
541
559
  }
542
- function StreamLine({ e, loc }) {
543
- return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
560
+ function StreamLine({ e, loc, selected }) {
561
+ return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", backgroundColor: selected ? "#1c2f63" : void 0, children: [
562
+ /* @__PURE__ */ jsx2(Text2, { color: selected ? theme.accentBright : theme.dim, children: selected ? "\u25B8" : " " }),
544
563
  /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
545
564
  "[",
546
565
  hhmmss(e.at),
@@ -574,9 +593,14 @@ function LivePanel({ active: active2 }) {
574
593
  const [ring, setRing] = useState2(null);
575
594
  const [log, setLog] = useState2([]);
576
595
  const [filter, setFilter] = useState2("all");
596
+ const [signal, setSignal] = useState2("none");
577
597
  const [search, setSearch] = useState2("");
578
598
  const [editingSearch, setEditingSearch] = useState2(false);
579
- const [scroll, setScroll] = useState2(0);
599
+ const [sel, setSel] = useState2(0);
600
+ const [actionMsg, setActionMsg] = useState2(null);
601
+ const notifiedRef = useRef2(/* @__PURE__ */ new Set());
602
+ const notifyReady = useRef2(false);
603
+ const activePolicyRef = useRef2(null);
580
604
  const [mode, setMode] = useState2("stream");
581
605
  const [pickIdx, setPickIdx] = useState2(0);
582
606
  const [detail, setDetail] = useState2(null);
@@ -586,6 +610,7 @@ function LivePanel({ active: active2 }) {
586
610
  const lastLocalTs = useRef2(0);
587
611
  const pausedUntil = useRef2(0);
588
612
  const sessRef = useRef2([]);
613
+ const mergedRef = useRef2([]);
589
614
  const prevStatus = useRef2(/* @__PURE__ */ new Map());
590
615
  const [alerts, setAlerts] = useState2([]);
591
616
  const [frozen, setFrozen] = useState2(false);
@@ -634,6 +659,7 @@ function LivePanel({ active: active2 }) {
634
659
  permission: (j.permission ?? "").slice(0, 4),
635
660
  detail: (j.arguments ? JSON.stringify(j.arguments) : j.reason ?? "").replace(/\s+/g, " "),
636
661
  dlp: !!j.dlp,
662
+ burst: !!j.rate_limit_burst,
637
663
  source: "local",
638
664
  session: j.session_id,
639
665
  agent: j.agent_name,
@@ -748,30 +774,52 @@ function LivePanel({ active: active2 }) {
748
774
  return () => clearInterval(t);
749
775
  }, [active2, frozen]);
750
776
  const startRef = useRef2(Date.now());
777
+ const fireAlert = useCallback2(
778
+ (id, title, msg, level = "warn") => {
779
+ pushLog(`${level === "bad" ? "\u26D4" : "\u23F8"} ${msg}`, level);
780
+ setAlerts((a) => [...a.filter((x) => x.id !== id), { id, msg, level, until: Date.now() + 15e3 }].slice(-4));
781
+ try {
782
+ process.stdout.write("\x07");
783
+ } catch {
784
+ }
785
+ try {
786
+ const child = spawn("notify-send", ["-a", "SolonGate", title, msg], { stdio: "ignore", detached: true });
787
+ child.on("error", () => {
788
+ });
789
+ child.unref();
790
+ } catch {
791
+ }
792
+ },
793
+ [pushLog]
794
+ );
751
795
  useEffect2(() => {
752
796
  if (!active2) return;
753
797
  for (const r of sessRef.current) {
754
798
  const prev = prevStatus.current.get(r.id);
755
799
  if (prev && prev === "active" && r.status === "idle") {
756
800
  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
- }
801
+ fireAlert("idle:" + r.id, "Agent idle", `${name} went IDLE, no tool call for 60s`, "warn");
771
802
  }
772
803
  prevStatus.current.set(r.id, r.status);
773
804
  }
774
- }, [tick, active2, pushLog]);
805
+ }, [tick, active2, fireAlert]);
806
+ useEffect2(() => {
807
+ if (!active2) return;
808
+ const notable = mergedRef.current.filter((e) => e.decision !== "ALLOW" || e.dlp || e.burst);
809
+ if (!notifyReady.current) {
810
+ for (const e of notable) notifiedRef.current.add(e.id);
811
+ notifyReady.current = true;
812
+ return;
813
+ }
814
+ for (const e of notable) {
815
+ if (notifiedRef.current.has(e.id)) continue;
816
+ notifiedRef.current.add(e.id);
817
+ const who = e.agent ? ` \xB7 ${truncate(e.agent, 20)}` : "";
818
+ if (e.dlp) fireAlert("sec:" + e.id, "DLP hit", `SECRET in ${e.tool}${who}: ${truncate(e.detail, 60)}`, "bad");
819
+ else if (e.decision !== "ALLOW") fireAlert("sec:" + e.id, "Call denied", `DENY ${e.tool}${who}: ${truncate(e.rule ?? e.detail, 60)}`, "bad");
820
+ else if (e.burst) fireAlert("sec:" + e.id, "Rate-limit burst", `BURST ${e.tool}${who}`, "warn");
821
+ }
822
+ }, [tick, active2, fireAlert]);
775
823
  const cols = process.stdout.columns ?? 100;
776
824
  const rows = process.stdout.rows ?? 30;
777
825
  const ins = insights.data ?? {};
@@ -784,6 +832,7 @@ function LivePanel({ active: active2 }) {
784
832
  }
785
833
  const cloudDeduped = cloudBuf.filter((e) => !localKeys.has(`${e.tool}|${e.decision}|${Math.round(e.at / 6e4)}`));
786
834
  const mergedAll = [...cloudDeduped, ...localBuf].sort((a, b) => a.at - b.at);
835
+ mergedRef.current = mergedAll;
787
836
  const nowSlot = Math.floor(nowMs / 1e4);
788
837
  const traffic = new Array(60).fill(0);
789
838
  const trafficHot = new Array(60).fill(false);
@@ -843,7 +892,8 @@ function LivePanel({ active: active2 }) {
843
892
  const isLoc = (e) => e.source === "local" || !!e.session && localSessIds.has(e.session);
844
893
  const q = search.trim().toLowerCase();
845
894
  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);
895
+ const signalOk = (e) => signal === "none" ? true : signal === "deny" ? e.decision !== "ALLOW" : signal === "dlp" ? e.dlp : e.burst;
896
+ const filtered = (filter === "all" ? mergedAll : mergedAll.filter((e) => filter === "local" ? isLoc(e) : !isLoc(e))).filter(matches).filter(signalOk);
847
897
  const visibleDesc = filtered.slice().reverse();
848
898
  const chartH = rows >= 36 ? 6 : 4;
849
899
  const colH = rows >= 32 ? 7 : 5;
@@ -854,9 +904,11 @@ function LivePanel({ active: active2 }) {
854
904
  const colW = Math.max(20, Math.floor((innerW - 4) / 3));
855
905
  const pickable = combinedSess.slice(0, colH - 1);
856
906
  const streamBodyRows = Math.max(3, streamRows - 1);
907
+ const selClamped = Math.min(Math.max(0, sel), Math.max(0, visibleDesc.length - 1));
857
908
  const maxScroll = Math.max(0, visibleDesc.length - streamBodyRows);
858
- const clampedScroll = Math.min(scroll, maxScroll);
909
+ const clampedScroll = Math.min(Math.max(0, selClamped - Math.floor((streamBodyRows - 1) / 2)), maxScroll);
859
910
  const windowed = visibleDesc.slice(clampedScroll, clampedScroll + streamBodyRows);
911
+ const selEntry = visibleDesc[selClamped];
860
912
  const detailEntries = detail ? (() => {
861
913
  const seen = /* @__PURE__ */ new Set();
862
914
  const all = [];
@@ -868,6 +920,42 @@ function LivePanel({ active: active2 }) {
868
920
  }
869
921
  return all.sort((a, b) => b.at - a.at);
870
922
  })() : [];
923
+ const doAction = async (act) => {
924
+ const e = selEntry;
925
+ if (!e) return;
926
+ const ok = act === "whitelist" ? e.decision !== "ALLOW" : e.decision === "ALLOW";
927
+ if (!ok) {
928
+ setActionMsg({ text: act === "whitelist" ? "select a DENY to whitelist (w)" : "select an ALLOW to block (b)", level: "bad", until: Date.now() + 4e3 });
929
+ return;
930
+ }
931
+ setActionMsg({ text: act === "whitelist" ? "whitelisting\u2026" : "blocking\u2026", level: "ok", until: Date.now() + 4e3 });
932
+ try {
933
+ let res;
934
+ if (e.source === "cloud") {
935
+ const realId = e.id.slice(2);
936
+ res = act === "whitelist" ? await api.audit.whitelist(realId) : await api.audit.block(realId);
937
+ } else {
938
+ let pid = activePolicyRef.current;
939
+ if (!pid) {
940
+ const a = await api.policies.active();
941
+ pid = a.policy?.id ?? null;
942
+ activePolicyRef.current = pid;
943
+ }
944
+ if (!pid) throw new Error("no active policy resolves for this machine");
945
+ const t = extractTarget(e.detail);
946
+ res = await api.policies.addRule(pid, { toolPattern: e.tool, kind: t?.kind ?? "tool", value: t?.value, effect: act === "whitelist" ? "ALLOW" : "DENY" });
947
+ }
948
+ const verb = act === "whitelist" ? "ALLOW" : "DENY";
949
+ setActionMsg({
950
+ 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`,
951
+ level: "ok",
952
+ until: Date.now() + 7e3
953
+ });
954
+ pushLog(`${act === "whitelist" ? "\u2713 whitelisted" : "\u26D4 blocked"} ${e.tool}`, act === "whitelist" ? "ok" : "warn");
955
+ } catch (err) {
956
+ setActionMsg({ text: "\u2717 " + (err instanceof Error ? err.message : String(err)), level: "bad", until: Date.now() + 7e3 });
957
+ }
958
+ };
871
959
  useInput(
872
960
  (input, key) => {
873
961
  if (input === " ") {
@@ -902,16 +990,25 @@ function LivePanel({ active: active2 }) {
902
990
  } else if (input === "s" || key.leftArrow) setMode("stream");
903
991
  return;
904
992
  }
993
+ const toggleSignal = (s2) => {
994
+ setSignal((cur) => cur === s2 ? "none" : s2);
995
+ setSel(0);
996
+ };
905
997
  if (input === "f") {
906
998
  setFilter((f) => FILTERS[(FILTERS.indexOf(f) + 1) % FILTERS.length]);
907
- setScroll(0);
999
+ setSel(0);
908
1000
  } else if (input === "s") {
909
1001
  setPickIdx(0);
910
1002
  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));
1003
+ } else if (input === "w") void doAction("whitelist");
1004
+ else if (input === "b") void doAction("block");
1005
+ else if (input === "d") toggleSignal("deny");
1006
+ else if (input === "x") toggleSignal("dlp");
1007
+ else if (input === "r") toggleSignal("ratelimit");
1008
+ else if (key.downArrow) setSel((n) => Math.min(visibleDesc.length - 1, n + 1));
1009
+ else if (key.upArrow) setSel((n) => Math.max(0, n - 1));
1010
+ else if (key.pageDown) setSel((n) => Math.min(visibleDesc.length - 1, n + streamBodyRows));
1011
+ else if (key.pageUp) setSel((n) => Math.max(0, n - streamBodyRows));
915
1012
  },
916
1013
  { isActive: active2 && !editingSearch }
917
1014
  );
@@ -928,7 +1025,7 @@ function LivePanel({ active: active2 }) {
928
1025
  return;
929
1026
  }
930
1027
  setSearch(v);
931
- setScroll(0);
1028
+ setSel(0);
932
1029
  setDetailScroll(0);
933
1030
  },
934
1031
  onSubmit: () => setEditingSearch(false)
@@ -1048,7 +1145,18 @@ function LivePanel({ active: active2 }) {
1048
1145
  /* @__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
1146
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502" })
1050
1147
  ] }),
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)),
1148
+ alerts.filter((a) => nowMs < a.until).slice(-1).map((a) => /* @__PURE__ */ jsx2(
1149
+ Text2,
1150
+ {
1151
+ wrap: "truncate",
1152
+ backgroundColor: a.level === "bad" ? "#3d1220" : "#3d2a12",
1153
+ color: a.level === "bad" ? "#ff6b6b" : "#ffb454",
1154
+ bold: true,
1155
+ children: ` ${a.level === "bad" ? "\u26D4 ALERT" : "\u23F8 IDLE"} \xB7 ${truncate(a.msg, innerW - 14)} `
1156
+ },
1157
+ a.id
1158
+ )),
1159
+ 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
1160
  /* @__PURE__ */ jsxs2(Box2, { height: 1 + chartH, children: [
1053
1161
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: leftW, marginRight: 2, children: [
1054
1162
  /* @__PURE__ */ jsx2(PaneTitle, { label: "TRAFFIC", extra: `${trafficLabel} \xB7 peak ${Math.max(0, ...traffic)} \xB7 red = denials`, width: leftW }),
@@ -1086,11 +1194,11 @@ function LivePanel({ active: active2 }) {
1086
1194
  /* @__PURE__ */ jsx2(PaneTitle, { label: "SESSIONS", extra: mode === "pick" ? "\u2191\u2193 pick \xB7 enter open" : `${combinedSess.length} \xB7 s = inspect`, width: colW }),
1087
1195
  pickable.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "scanning\u2026" }) : null,
1088
1196
  pickable.map((r, i) => {
1089
- const sel = mode === "pick" && i === pickIdx;
1197
+ const sel2 = mode === "pick" && i === pickIdx;
1090
1198
  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,
1199
+ return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", backgroundColor: sel2 ? "#1c2f63" : void 0, bold: sel2 || r.isMe, children: [
1200
+ /* @__PURE__ */ jsxs2(Text2, { color: sel2 ? theme.accentBright : st.color, children: [
1201
+ sel2 ? "\u25B8" : st.dot,
1094
1202
  " "
1095
1203
  ] }),
1096
1204
  /* @__PURE__ */ jsx2(Text2, { color: st.color, children: st.label.padEnd(7) }),
@@ -1123,7 +1231,7 @@ function LivePanel({ active: active2 }) {
1123
1231
  PaneTitle,
1124
1232
  {
1125
1233
  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`,
1234
+ 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
1235
  width: innerW
1128
1236
  }
1129
1237
  ),
@@ -1137,12 +1245,12 @@ function LivePanel({ active: active2 }) {
1137
1245
  spin,
1138
1246
  " awaiting traffic\u2026"
1139
1247
  ] }) : null,
1140
- windowed.map((e) => /* @__PURE__ */ jsx2(StreamLine, { e, loc: isLoc(e) }, e.id))
1248
+ windowed.map((e, i) => /* @__PURE__ */ jsx2(StreamLine, { e, loc: isLoc(e), selected: clampedScroll + i === selClamped }, e.id))
1141
1249
  ] }),
1142
1250
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
1143
1251
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", bold: true, children: " LIVE " }),
1144
1252
  /* @__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 " })
1253
+ /* @__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
1254
  ] })
1147
1255
  ] });
1148
1256
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.77.1",
3
+ "version": "0.78.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": {