@solongate/proxy 0.74.0 → 0.75.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -7030,6 +7030,7 @@ var init_hooks = __esm({
7030
7030
 
7031
7031
  // src/tui/panels/Live.tsx
7032
7032
  import { Box as Box2, Text as Text2, useInput } from "ink";
7033
+ import TextInput from "ink-text-input";
7033
7034
  import { closeSync, openSync, readSync, statSync } from "fs";
7034
7035
  import { homedir as homedir3 } from "os";
7035
7036
  import { join as join5 } from "path";
@@ -7135,7 +7136,6 @@ function StreamLine({ e, loc }) {
7135
7136
  }
7136
7137
  function LivePanel({ active: active2 }) {
7137
7138
  const [s, setS] = useState2(null);
7138
- const [tsData, setTsData] = useState2(null);
7139
7139
  const [lat, setLat] = useState2([]);
7140
7140
  const [cloudBuf, setCloudBuf] = useState2([]);
7141
7141
  const [localBuf, setLocalBuf] = useState2([]);
@@ -7143,6 +7143,8 @@ function LivePanel({ active: active2 }) {
7143
7143
  const [ring, setRing] = useState2(null);
7144
7144
  const [log6, setLog] = useState2([]);
7145
7145
  const [filter, setFilter] = useState2("all");
7146
+ const [search, setSearch] = useState2("");
7147
+ const [editingSearch, setEditingSearch] = useState2(false);
7146
7148
  const [scroll, setScroll] = useState2(0);
7147
7149
  const [mode, setMode] = useState2("stream");
7148
7150
  const [pickIdx, setPickIdx] = useState2(0);
@@ -7262,38 +7264,26 @@ function LivePanel({ active: active2 }) {
7262
7264
  onApiError(e);
7263
7265
  }
7264
7266
  }, [onApiError]);
7265
- const pollSlow = useCallback2(async () => {
7266
- if (paused()) return;
7267
- try {
7268
- const td = await api.stats.timeseries({ period: "24h" });
7269
- if (!frozenRef.current) setTsData(td);
7270
- } catch (e) {
7271
- onApiError(e);
7272
- }
7273
- }, [onApiError]);
7274
7267
  useEffect2(() => {
7275
7268
  if (!active2) return;
7276
7269
  void pollFeed();
7277
7270
  void pollMedium();
7278
- void pollSlow();
7279
- const t1 = setInterval(() => void pollFeed(), 4e3);
7280
- const t2 = setInterval(() => void pollMedium(), 1e4);
7281
- const t3 = setInterval(() => void pollSlow(), 3e4);
7271
+ const t1 = setInterval(() => void pollFeed(), 3e3);
7272
+ const t2 = setInterval(() => void pollMedium(), 8e3);
7282
7273
  return () => {
7283
7274
  clearInterval(t1);
7284
7275
  clearInterval(t2);
7285
- clearInterval(t3);
7286
7276
  };
7287
- }, [active2, pollFeed, pollMedium, pollSlow]);
7277
+ }, [active2, pollFeed, pollMedium]);
7288
7278
  const sessions = useLoader(() => api.agents.live({ limit: 30, includeDeactivated: true }));
7289
7279
  const insights = useLoader(() => api.stats.securityInsights(7));
7290
7280
  const guard = useLoader(() => api.settings.getGuardStatus());
7291
7281
  usePoll(() => {
7292
7282
  if (!paused()) sessions.reload();
7293
- }, 1e4, active2);
7283
+ }, 8e3, active2);
7294
7284
  usePoll(() => {
7295
7285
  if (!paused()) insights.reload();
7296
- }, 3e4, active2);
7286
+ }, 2e4, active2);
7297
7287
  usePoll(() => {
7298
7288
  if (!paused()) guard.reload();
7299
7289
  }, 6e4, active2);
@@ -7329,21 +7319,17 @@ function LivePanel({ active: active2 }) {
7329
7319
  }
7330
7320
  const cloudDeduped = cloudBuf.filter((e) => !localKeys.has(`${e.tool}|${e.decision}|${Math.round(e.at / 6e4)}`));
7331
7321
  const mergedAll = [...cloudDeduped, ...localBuf].sort((a, b) => a.at - b.at);
7332
- const points = tsData?.timeseries ?? [];
7333
7322
  const nowSlot = Math.floor(nowMs / 1e4);
7334
- const slotCounts = new Array(60).fill(0);
7335
- const slotHot = new Array(60).fill(false);
7323
+ const traffic = new Array(60).fill(0);
7324
+ const trafficHot = new Array(60).fill(false);
7336
7325
  for (const e of mergedAll) {
7337
7326
  const idx = 59 - (nowSlot - Math.floor(e.at / 1e4));
7338
7327
  if (idx >= 0 && idx < 60) {
7339
- slotCounts[idx]++;
7340
- if (e.decision !== "ALLOW") slotHot[idx] = true;
7328
+ traffic[idx]++;
7329
+ if (e.decision !== "ALLOW") trafficHot[idx] = true;
7341
7330
  }
7342
7331
  }
7343
- const localTraffic = localOn === true && slotCounts.some((c2) => c2 > 0);
7344
- const traffic = localTraffic ? slotCounts : points.map((p) => p.total);
7345
- const trafficHot = localTraffic ? slotHot : points.map((p) => p.denied > 0);
7346
- const trafficLabel = localTraffic ? "calls/10s \xB7 last 10m" : "24h \xB7 cloud";
7332
+ const trafficLabel = "calls/10s \xB7 last 10m \xB7 live";
7347
7333
  const rl = ins.layers?.rateLimit;
7348
7334
  const dl = ins.layers?.dlp;
7349
7335
  const gh = ins.layers?.ghost;
@@ -7388,7 +7374,9 @@ function LivePanel({ active: active2 }) {
7388
7374
  const localSessIds = new Set(localSess.keys());
7389
7375
  if (ring?.session) localSessIds.add(ring.session);
7390
7376
  const isLoc = (e) => e.source === "local" || !!e.session && localSessIds.has(e.session);
7391
- const filtered = filter === "all" ? mergedAll : mergedAll.filter((e) => filter === "local" ? isLoc(e) : !isLoc(e));
7377
+ const q = search.trim().toLowerCase();
7378
+ const matches = (e) => !q || `${e.tool} ${e.agent ?? ""} ${e.decision} ${e.permission} ${e.rule ?? ""} ${e.detail}`.toLowerCase().includes(q);
7379
+ const filtered = (filter === "all" ? mergedAll : mergedAll.filter((e) => filter === "local" ? isLoc(e) : !isLoc(e))).filter(matches);
7392
7380
  const visibleDesc = filtered.slice(-STREAM_KEEP).reverse();
7393
7381
  const chartH = rows >= 36 ? 6 : 4;
7394
7382
  const colH = rows >= 32 ? 7 : 5;
@@ -7408,12 +7396,21 @@ function LivePanel({ active: active2 }) {
7408
7396
  const key = `${e.at}:${e.tool}:${e.decision}`;
7409
7397
  if (seen.has(key)) continue;
7410
7398
  seen.add(key);
7411
- all.push(e);
7399
+ if (matches(e)) all.push(e);
7412
7400
  }
7413
7401
  return all.sort((a, b) => b.at - a.at);
7414
7402
  })() : [];
7415
7403
  useInput(
7416
7404
  (input, key) => {
7405
+ if (input === " ") {
7406
+ setFrozen((f) => !f);
7407
+ return;
7408
+ }
7409
+ if (frozen) return;
7410
+ if (input === "/" && mode !== "pick") {
7411
+ setEditingSearch(true);
7412
+ return;
7413
+ }
7417
7414
  if (mode === "detail") {
7418
7415
  const maxD = Math.max(0, detailEntries.length - (rows - 10));
7419
7416
  if (key.leftArrow) {
@@ -7437,11 +7434,6 @@ function LivePanel({ active: active2 }) {
7437
7434
  } else if (input === "s" || key.leftArrow) setMode("stream");
7438
7435
  return;
7439
7436
  }
7440
- if (input === " ") {
7441
- setFrozen((f) => !f);
7442
- return;
7443
- }
7444
- if (frozen) return;
7445
7437
  if (input === "f") {
7446
7438
  setFilter((f) => FILTERS[(FILTERS.indexOf(f) + 1) % FILTERS.length]);
7447
7439
  setScroll(0);
@@ -7453,8 +7445,32 @@ function LivePanel({ active: active2 }) {
7453
7445
  else if (key.pageDown) setScroll((n) => Math.min(maxScroll, n + streamRows));
7454
7446
  else if (key.pageUp) setScroll((n) => Math.max(0, n - streamRows));
7455
7447
  },
7456
- { isActive: active2 }
7448
+ { isActive: active2 && !editingSearch }
7457
7449
  );
7450
+ const searchRow = editingSearch ? /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7451
+ /* @__PURE__ */ jsx2(Text2, { color: theme.warn, children: "search: " }),
7452
+ /* @__PURE__ */ jsx2(
7453
+ TextInput,
7454
+ {
7455
+ value: search,
7456
+ onChange: (v) => {
7457
+ if (v.endsWith("/")) {
7458
+ setEditingSearch(false);
7459
+ return;
7460
+ }
7461
+ setSearch(v);
7462
+ setScroll(0);
7463
+ setDetailScroll(0);
7464
+ },
7465
+ onSubmit: () => setEditingSearch(false)
7466
+ }
7467
+ ),
7468
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " (enter/\u200B/ done \xB7 empty = clear)" })
7469
+ ] }) : search ? /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7470
+ /* @__PURE__ */ jsx2(Text2, { color: theme.warn, children: "search: " }),
7471
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, bold: true, children: search }),
7472
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ` ${filtered.length} match \xB7 / edit` })
7473
+ ] }) : null;
7458
7474
  const sessionDot = (st) => st === "active" ? { ch: "\u25CF", color: theme.ok } : st === "idle" ? { ch: "\u25CC", color: theme.warn } : { ch: "\u25CB", color: theme.dim };
7459
7475
  void sessionDot;
7460
7476
  if (s === null && localOn === null) {
@@ -7491,6 +7507,7 @@ function LivePanel({ active: active2 }) {
7491
7507
  const tl = d.slice(dScroll, dScroll + tlRows);
7492
7508
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
7493
7509
  titleBar,
7510
+ searchRow,
7494
7511
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7495
7512
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#AAC8FA", bold: true, children: ` SESSION ${detail.id.slice(0, 8)} ` }),
7496
7513
  /* @__PURE__ */ jsx2(Text2, { color: detail.source === "local" ? theme.ok : "#4f6db8", children: " " + detail.source.toUpperCase() }),
@@ -7522,7 +7539,7 @@ function LivePanel({ active: active2 }) {
7522
7539
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: perms.map(([p, c2]) => `${p}\xD7${c2}`).join(" ") || "\u2014" }),
7523
7540
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502" })
7524
7541
  ] }),
7525
- /* @__PURE__ */ jsx2(PaneTitle, { label: "TIMELINE", extra: `${d.length} calls \xB7 newest first${dScroll ? ` \xB7 \u25BC${dScroll} older` : ""} \xB7 \u2191\u2193 scroll \xB7 \u2190 back`, width: innerW }),
7542
+ /* @__PURE__ */ jsx2(PaneTitle, { label: "TIMELINE", extra: `${d.length} calls \xB7 newest first${dScroll ? ` \xB7 \u25BC${dScroll} older` : ""} \xB7 \u2191\u2193 scroll \xB7 / search \xB7 \u2190 back`, width: innerW }),
7526
7543
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height: tlRows, overflow: "hidden", children: [
7527
7544
  tl.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: detailCloud.length === 0 ? spin + " loading history\u2026" : "no entries" }) : null,
7528
7545
  tl.map((e) => /* @__PURE__ */ jsx2(StreamLine, { e, loc: isLoc(e) }, e.id))
@@ -7557,6 +7574,7 @@ function LivePanel({ active: active2 }) {
7557
7574
  /* @__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" }),
7558
7575
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502" })
7559
7576
  ] }),
7577
+ searchRow,
7560
7578
  /* @__PURE__ */ jsxs2(Box2, { height: 1 + chartH, children: [
7561
7579
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: leftW, marginRight: 2, children: [
7562
7580
  /* @__PURE__ */ jsx2(PaneTitle, { label: "TRAFFIC", extra: `${trafficLabel} \xB7 peak ${Math.max(0, ...traffic)} \xB7 red = denials`, width: leftW }),
@@ -7631,11 +7649,11 @@ function LivePanel({ active: active2 }) {
7631
7649
  PaneTitle,
7632
7650
  {
7633
7651
  label: "TOOL STREAM",
7634
- extra: `newest first \xB7 last ${STREAM_KEEP} \xB7 filter ${filter}${clampedScroll > 0 ? ` \xB7 \u25BC${clampedScroll} older` : ""} \xB7 f filter \xB7 \u2191\u2193 scroll \xB7 s sessions`,
7652
+ extra: `newest first \xB7 last ${STREAM_KEEP} \xB7 filter ${filter}${clampedScroll > 0 ? ` \xB7 \u25BC${clampedScroll} older` : ""} \xB7 f filter \xB7 / search \xB7 \u2191\u2193 scroll \xB7 s sessions`,
7635
7653
  width: innerW
7636
7654
  }
7637
7655
  ),
7638
- /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height: streamRows, overflow: "hidden", children: [
7656
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height: searchRow ? streamRows - 1 : streamRows, overflow: "hidden", children: [
7639
7657
  localOn === false ? /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7640
7658
  /* @__PURE__ */ jsx2(Text2, { color: theme.warn, children: "local logs off" }),
7641
7659
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2014 enable: dashboard \u2192 Settings \u2192 Local logs \xB7 hooks write ~/.solongate/local-logs \xB7 view: `solongate logs-server`" })
@@ -7649,7 +7667,7 @@ function LivePanel({ active: active2 }) {
7649
7667
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7650
7668
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", bold: true, children: " LIVE " }),
7651
7669
  /* @__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"} ` }),
7652
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: " space copy-mode \xB7 f filter \xB7 s sessions \xB7 esc menu \xB7 q quit " })
7670
+ /* @__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 " })
7653
7671
  ] })
7654
7672
  ] });
7655
7673
  }
@@ -7681,7 +7699,7 @@ var init_Live = __esm({
7681
7699
 
7682
7700
  // src/tui/panels/Policies.tsx
7683
7701
  import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
7684
- import TextInput from "ink-text-input";
7702
+ import TextInput2 from "ink-text-input";
7685
7703
  import { useEffect as useEffect3, useState as useState3 } from "react";
7686
7704
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
7687
7705
  function ruleSummary(r) {
@@ -7870,7 +7888,7 @@ function PoliciesPanel({ focused }) {
7870
7888
  return /* @__PURE__ */ jsxs3(Box3, { children: [
7871
7889
  /* @__PURE__ */ jsx3(Text3, { color: active2 ? theme.accentBright : void 0, children: (active2 ? "\u25B8 " : " ") + f.label.padEnd(13) }),
7872
7890
  active2 && editing && f.kind === "text" ? /* @__PURE__ */ jsx3(
7873
- TextInput,
7891
+ TextInput2,
7874
7892
  {
7875
7893
  value: editVal,
7876
7894
  onChange: setEditVal,
@@ -8066,7 +8084,7 @@ var init_RateLimit = __esm({
8066
8084
 
8067
8085
  // src/tui/panels/Dlp.tsx
8068
8086
  import { Box as Box5, Text as Text5, useInput as useInput4 } from "ink";
8069
- import TextInput2 from "ink-text-input";
8087
+ import TextInput3 from "ink-text-input";
8070
8088
  import { useEffect as useEffect5, useState as useState5 } from "react";
8071
8089
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
8072
8090
  function DlpPanel({ focused }) {
@@ -8168,7 +8186,7 @@ function DlpPanel({ focused }) {
8168
8186
  adding ? /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, children: [
8169
8187
  /* @__PURE__ */ jsx5(Text5, { color: theme.warn, children: adding === "name" ? "New pattern name: " : `Regex for "${newName}": ` }),
8170
8188
  adding === "name" ? /* @__PURE__ */ jsx5(
8171
- TextInput2,
8189
+ TextInput3,
8172
8190
  {
8173
8191
  value: newName,
8174
8192
  onChange: setNewName,
@@ -8179,7 +8197,7 @@ function DlpPanel({ focused }) {
8179
8197
  }
8180
8198
  }
8181
8199
  ) : /* @__PURE__ */ jsx5(
8182
- TextInput2,
8200
+ TextInput3,
8183
8201
  {
8184
8202
  value: newRe,
8185
8203
  onChange: setNewRe,
@@ -8292,7 +8310,7 @@ var init_Stats = __esm({
8292
8310
 
8293
8311
  // src/tui/panels/Audit.tsx
8294
8312
  import { Box as Box7, Text as Text7, useInput as useInput5 } from "ink";
8295
- import TextInput3 from "ink-text-input";
8313
+ import TextInput4 from "ink-text-input";
8296
8314
  import { useState as useState6 } from "react";
8297
8315
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
8298
8316
  function AuditPanel({ active: active2, focused }) {
@@ -8409,7 +8427,7 @@ function AuditPanel({ active: active2, focused }) {
8409
8427
  ": "
8410
8428
  ] }),
8411
8429
  /* @__PURE__ */ jsx7(
8412
- TextInput3,
8430
+ TextInput4,
8413
8431
  {
8414
8432
  value: editing === "tool" ? tool : editing === "agent" ? agent : search,
8415
8433
  onChange: editing === "tool" ? setTool : editing === "agent" ? setAgent : setSearch,
package/dist/tui/index.js CHANGED
@@ -126,6 +126,7 @@ function KeyHints({ hints }) {
126
126
 
127
127
  // src/tui/panels/Live.tsx
128
128
  import { Box as Box2, Text as Text2, useInput } from "ink";
129
+ import TextInput from "ink-text-input";
129
130
  import { closeSync, openSync, readSync, statSync } from "fs";
130
131
  import { homedir as homedir2 } from "os";
131
132
  import { join as join2 } from "path";
@@ -560,7 +561,6 @@ function StreamLine({ e, loc }) {
560
561
  var FILTERS = ["all", "local", "cloud"];
561
562
  function LivePanel({ active: active2 }) {
562
563
  const [s, setS] = useState2(null);
563
- const [tsData, setTsData] = useState2(null);
564
564
  const [lat, setLat] = useState2([]);
565
565
  const [cloudBuf, setCloudBuf] = useState2([]);
566
566
  const [localBuf, setLocalBuf] = useState2([]);
@@ -568,6 +568,8 @@ function LivePanel({ active: active2 }) {
568
568
  const [ring, setRing] = useState2(null);
569
569
  const [log, setLog] = useState2([]);
570
570
  const [filter, setFilter] = useState2("all");
571
+ const [search, setSearch] = useState2("");
572
+ const [editingSearch, setEditingSearch] = useState2(false);
571
573
  const [scroll, setScroll] = useState2(0);
572
574
  const [mode, setMode] = useState2("stream");
573
575
  const [pickIdx, setPickIdx] = useState2(0);
@@ -687,38 +689,26 @@ function LivePanel({ active: active2 }) {
687
689
  onApiError(e);
688
690
  }
689
691
  }, [onApiError]);
690
- const pollSlow = useCallback2(async () => {
691
- if (paused()) return;
692
- try {
693
- const td = await api.stats.timeseries({ period: "24h" });
694
- if (!frozenRef.current) setTsData(td);
695
- } catch (e) {
696
- onApiError(e);
697
- }
698
- }, [onApiError]);
699
692
  useEffect2(() => {
700
693
  if (!active2) return;
701
694
  void pollFeed();
702
695
  void pollMedium();
703
- void pollSlow();
704
- const t1 = setInterval(() => void pollFeed(), 4e3);
705
- const t2 = setInterval(() => void pollMedium(), 1e4);
706
- const t3 = setInterval(() => void pollSlow(), 3e4);
696
+ const t1 = setInterval(() => void pollFeed(), 3e3);
697
+ const t2 = setInterval(() => void pollMedium(), 8e3);
707
698
  return () => {
708
699
  clearInterval(t1);
709
700
  clearInterval(t2);
710
- clearInterval(t3);
711
701
  };
712
- }, [active2, pollFeed, pollMedium, pollSlow]);
702
+ }, [active2, pollFeed, pollMedium]);
713
703
  const sessions = useLoader(() => api.agents.live({ limit: 30, includeDeactivated: true }));
714
704
  const insights = useLoader(() => api.stats.securityInsights(7));
715
705
  const guard = useLoader(() => api.settings.getGuardStatus());
716
706
  usePoll(() => {
717
707
  if (!paused()) sessions.reload();
718
- }, 1e4, active2);
708
+ }, 8e3, active2);
719
709
  usePoll(() => {
720
710
  if (!paused()) insights.reload();
721
- }, 3e4, active2);
711
+ }, 2e4, active2);
722
712
  usePoll(() => {
723
713
  if (!paused()) guard.reload();
724
714
  }, 6e4, active2);
@@ -754,21 +744,17 @@ function LivePanel({ active: active2 }) {
754
744
  }
755
745
  const cloudDeduped = cloudBuf.filter((e) => !localKeys.has(`${e.tool}|${e.decision}|${Math.round(e.at / 6e4)}`));
756
746
  const mergedAll = [...cloudDeduped, ...localBuf].sort((a, b) => a.at - b.at);
757
- const points = tsData?.timeseries ?? [];
758
747
  const nowSlot = Math.floor(nowMs / 1e4);
759
- const slotCounts = new Array(60).fill(0);
760
- const slotHot = new Array(60).fill(false);
748
+ const traffic = new Array(60).fill(0);
749
+ const trafficHot = new Array(60).fill(false);
761
750
  for (const e of mergedAll) {
762
751
  const idx = 59 - (nowSlot - Math.floor(e.at / 1e4));
763
752
  if (idx >= 0 && idx < 60) {
764
- slotCounts[idx]++;
765
- if (e.decision !== "ALLOW") slotHot[idx] = true;
753
+ traffic[idx]++;
754
+ if (e.decision !== "ALLOW") trafficHot[idx] = true;
766
755
  }
767
756
  }
768
- const localTraffic = localOn === true && slotCounts.some((c2) => c2 > 0);
769
- const traffic = localTraffic ? slotCounts : points.map((p) => p.total);
770
- const trafficHot = localTraffic ? slotHot : points.map((p) => p.denied > 0);
771
- const trafficLabel = localTraffic ? "calls/10s \xB7 last 10m" : "24h \xB7 cloud";
757
+ const trafficLabel = "calls/10s \xB7 last 10m \xB7 live";
772
758
  const rl = ins.layers?.rateLimit;
773
759
  const dl = ins.layers?.dlp;
774
760
  const gh = ins.layers?.ghost;
@@ -813,7 +799,9 @@ function LivePanel({ active: active2 }) {
813
799
  const localSessIds = new Set(localSess.keys());
814
800
  if (ring?.session) localSessIds.add(ring.session);
815
801
  const isLoc = (e) => e.source === "local" || !!e.session && localSessIds.has(e.session);
816
- const filtered = filter === "all" ? mergedAll : mergedAll.filter((e) => filter === "local" ? isLoc(e) : !isLoc(e));
802
+ const q = search.trim().toLowerCase();
803
+ const matches = (e) => !q || `${e.tool} ${e.agent ?? ""} ${e.decision} ${e.permission} ${e.rule ?? ""} ${e.detail}`.toLowerCase().includes(q);
804
+ const filtered = (filter === "all" ? mergedAll : mergedAll.filter((e) => filter === "local" ? isLoc(e) : !isLoc(e))).filter(matches);
817
805
  const visibleDesc = filtered.slice(-STREAM_KEEP).reverse();
818
806
  const chartH = rows >= 36 ? 6 : 4;
819
807
  const colH = rows >= 32 ? 7 : 5;
@@ -833,12 +821,21 @@ function LivePanel({ active: active2 }) {
833
821
  const key = `${e.at}:${e.tool}:${e.decision}`;
834
822
  if (seen.has(key)) continue;
835
823
  seen.add(key);
836
- all.push(e);
824
+ if (matches(e)) all.push(e);
837
825
  }
838
826
  return all.sort((a, b) => b.at - a.at);
839
827
  })() : [];
840
828
  useInput(
841
829
  (input, key) => {
830
+ if (input === " ") {
831
+ setFrozen((f) => !f);
832
+ return;
833
+ }
834
+ if (frozen) return;
835
+ if (input === "/" && mode !== "pick") {
836
+ setEditingSearch(true);
837
+ return;
838
+ }
842
839
  if (mode === "detail") {
843
840
  const maxD = Math.max(0, detailEntries.length - (rows - 10));
844
841
  if (key.leftArrow) {
@@ -862,11 +859,6 @@ function LivePanel({ active: active2 }) {
862
859
  } else if (input === "s" || key.leftArrow) setMode("stream");
863
860
  return;
864
861
  }
865
- if (input === " ") {
866
- setFrozen((f) => !f);
867
- return;
868
- }
869
- if (frozen) return;
870
862
  if (input === "f") {
871
863
  setFilter((f) => FILTERS[(FILTERS.indexOf(f) + 1) % FILTERS.length]);
872
864
  setScroll(0);
@@ -878,8 +870,32 @@ function LivePanel({ active: active2 }) {
878
870
  else if (key.pageDown) setScroll((n) => Math.min(maxScroll, n + streamRows));
879
871
  else if (key.pageUp) setScroll((n) => Math.max(0, n - streamRows));
880
872
  },
881
- { isActive: active2 }
873
+ { isActive: active2 && !editingSearch }
882
874
  );
875
+ const searchRow = editingSearch ? /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
876
+ /* @__PURE__ */ jsx2(Text2, { color: theme.warn, children: "search: " }),
877
+ /* @__PURE__ */ jsx2(
878
+ TextInput,
879
+ {
880
+ value: search,
881
+ onChange: (v) => {
882
+ if (v.endsWith("/")) {
883
+ setEditingSearch(false);
884
+ return;
885
+ }
886
+ setSearch(v);
887
+ setScroll(0);
888
+ setDetailScroll(0);
889
+ },
890
+ onSubmit: () => setEditingSearch(false)
891
+ }
892
+ ),
893
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " (enter/\u200B/ done \xB7 empty = clear)" })
894
+ ] }) : search ? /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
895
+ /* @__PURE__ */ jsx2(Text2, { color: theme.warn, children: "search: " }),
896
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, bold: true, children: search }),
897
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ` ${filtered.length} match \xB7 / edit` })
898
+ ] }) : null;
883
899
  const sessionDot = (st) => st === "active" ? { ch: "\u25CF", color: theme.ok } : st === "idle" ? { ch: "\u25CC", color: theme.warn } : { ch: "\u25CB", color: theme.dim };
884
900
  void sessionDot;
885
901
  if (s === null && localOn === null) {
@@ -916,6 +932,7 @@ function LivePanel({ active: active2 }) {
916
932
  const tl = d.slice(dScroll, dScroll + tlRows);
917
933
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
918
934
  titleBar,
935
+ searchRow,
919
936
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
920
937
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#AAC8FA", bold: true, children: ` SESSION ${detail.id.slice(0, 8)} ` }),
921
938
  /* @__PURE__ */ jsx2(Text2, { color: detail.source === "local" ? theme.ok : "#4f6db8", children: " " + detail.source.toUpperCase() }),
@@ -947,7 +964,7 @@ function LivePanel({ active: active2 }) {
947
964
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: perms.map(([p, c2]) => `${p}\xD7${c2}`).join(" ") || "\u2014" }),
948
965
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502" })
949
966
  ] }),
950
- /* @__PURE__ */ jsx2(PaneTitle, { label: "TIMELINE", extra: `${d.length} calls \xB7 newest first${dScroll ? ` \xB7 \u25BC${dScroll} older` : ""} \xB7 \u2191\u2193 scroll \xB7 \u2190 back`, width: innerW }),
967
+ /* @__PURE__ */ jsx2(PaneTitle, { label: "TIMELINE", extra: `${d.length} calls \xB7 newest first${dScroll ? ` \xB7 \u25BC${dScroll} older` : ""} \xB7 \u2191\u2193 scroll \xB7 / search \xB7 \u2190 back`, width: innerW }),
951
968
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height: tlRows, overflow: "hidden", children: [
952
969
  tl.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: detailCloud.length === 0 ? spin + " loading history\u2026" : "no entries" }) : null,
953
970
  tl.map((e) => /* @__PURE__ */ jsx2(StreamLine, { e, loc: isLoc(e) }, e.id))
@@ -982,6 +999,7 @@ function LivePanel({ active: active2 }) {
982
999
  /* @__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" }),
983
1000
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502" })
984
1001
  ] }),
1002
+ searchRow,
985
1003
  /* @__PURE__ */ jsxs2(Box2, { height: 1 + chartH, children: [
986
1004
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: leftW, marginRight: 2, children: [
987
1005
  /* @__PURE__ */ jsx2(PaneTitle, { label: "TRAFFIC", extra: `${trafficLabel} \xB7 peak ${Math.max(0, ...traffic)} \xB7 red = denials`, width: leftW }),
@@ -1056,11 +1074,11 @@ function LivePanel({ active: active2 }) {
1056
1074
  PaneTitle,
1057
1075
  {
1058
1076
  label: "TOOL STREAM",
1059
- extra: `newest first \xB7 last ${STREAM_KEEP} \xB7 filter ${filter}${clampedScroll > 0 ? ` \xB7 \u25BC${clampedScroll} older` : ""} \xB7 f filter \xB7 \u2191\u2193 scroll \xB7 s sessions`,
1077
+ extra: `newest first \xB7 last ${STREAM_KEEP} \xB7 filter ${filter}${clampedScroll > 0 ? ` \xB7 \u25BC${clampedScroll} older` : ""} \xB7 f filter \xB7 / search \xB7 \u2191\u2193 scroll \xB7 s sessions`,
1060
1078
  width: innerW
1061
1079
  }
1062
1080
  ),
1063
- /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height: streamRows, overflow: "hidden", children: [
1081
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height: searchRow ? streamRows - 1 : streamRows, overflow: "hidden", children: [
1064
1082
  localOn === false ? /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
1065
1083
  /* @__PURE__ */ jsx2(Text2, { color: theme.warn, children: "local logs off" }),
1066
1084
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2014 enable: dashboard \u2192 Settings \u2192 Local logs \xB7 hooks write ~/.solongate/local-logs \xB7 view: `solongate logs-server`" })
@@ -1074,14 +1092,14 @@ function LivePanel({ active: active2 }) {
1074
1092
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
1075
1093
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", bold: true, children: " LIVE " }),
1076
1094
  /* @__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"} ` }),
1077
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: " space copy-mode \xB7 f filter \xB7 s sessions \xB7 esc menu \xB7 q quit " })
1095
+ /* @__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 " })
1078
1096
  ] })
1079
1097
  ] });
1080
1098
  }
1081
1099
 
1082
1100
  // src/tui/panels/Policies.tsx
1083
1101
  import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
1084
- import TextInput from "ink-text-input";
1102
+ import TextInput2 from "ink-text-input";
1085
1103
  import { useEffect as useEffect3, useState as useState3 } from "react";
1086
1104
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1087
1105
  var constr = (r, g, side) => (r[g]?.[side] ?? []).join(", ");
@@ -1291,7 +1309,7 @@ function PoliciesPanel({ focused }) {
1291
1309
  return /* @__PURE__ */ jsxs3(Box3, { children: [
1292
1310
  /* @__PURE__ */ jsx3(Text3, { color: active2 ? theme.accentBright : void 0, children: (active2 ? "\u25B8 " : " ") + f.label.padEnd(13) }),
1293
1311
  active2 && editing && f.kind === "text" ? /* @__PURE__ */ jsx3(
1294
- TextInput,
1312
+ TextInput2,
1295
1313
  {
1296
1314
  value: editVal,
1297
1315
  onChange: setEditVal,
@@ -1446,7 +1464,7 @@ function FieldRow({ label, active: active2, children }) {
1446
1464
 
1447
1465
  // src/tui/panels/Dlp.tsx
1448
1466
  import { Box as Box5, Text as Text5, useInput as useInput4 } from "ink";
1449
- import TextInput2 from "ink-text-input";
1467
+ import TextInput3 from "ink-text-input";
1450
1468
  import { useEffect as useEffect5, useState as useState5 } from "react";
1451
1469
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1452
1470
  var MODES2 = ["off", "detect", "block"];
@@ -1549,7 +1567,7 @@ function DlpPanel({ focused }) {
1549
1567
  adding ? /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, children: [
1550
1568
  /* @__PURE__ */ jsx5(Text5, { color: theme.warn, children: adding === "name" ? "New pattern name: " : `Regex for "${newName}": ` }),
1551
1569
  adding === "name" ? /* @__PURE__ */ jsx5(
1552
- TextInput2,
1570
+ TextInput3,
1553
1571
  {
1554
1572
  value: newName,
1555
1573
  onChange: setNewName,
@@ -1560,7 +1578,7 @@ function DlpPanel({ focused }) {
1560
1578
  }
1561
1579
  }
1562
1580
  ) : /* @__PURE__ */ jsx5(
1563
- TextInput2,
1581
+ TextInput3,
1564
1582
  {
1565
1583
  value: newRe,
1566
1584
  onChange: setNewRe,
@@ -1653,7 +1671,7 @@ function StatsPanel({ active: active2 }) {
1653
1671
 
1654
1672
  // src/tui/panels/Audit.tsx
1655
1673
  import { Box as Box7, Text as Text7, useInput as useInput5 } from "ink";
1656
- import TextInput3 from "ink-text-input";
1674
+ import TextInput4 from "ink-text-input";
1657
1675
  import { useState as useState6 } from "react";
1658
1676
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1659
1677
  var DECISIONS = [void 0, "DENY", "ALLOW"];
@@ -1772,7 +1790,7 @@ function AuditPanel({ active: active2, focused }) {
1772
1790
  ": "
1773
1791
  ] }),
1774
1792
  /* @__PURE__ */ jsx7(
1775
- TextInput3,
1793
+ TextInput4,
1776
1794
  {
1777
1795
  value: editing === "tool" ? tool : editing === "agent" ? agent : search,
1778
1796
  onChange: editing === "tool" ? setTool : editing === "agent" ? setAgent : setSearch,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.74.0",
3
+ "version": "0.75.1",
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": {