@solongate/proxy 0.71.0 → 0.73.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.
Files changed (3) hide show
  1. package/dist/index.js +208 -213
  2. package/dist/tui/index.js +178 -183
  3. package/package.json +1 -1
package/dist/tui/index.js CHANGED
@@ -126,10 +126,9 @@ 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 { spawnSync } from "child_process";
130
- import { closeSync, mkdirSync, openSync, readSync, statSync, writeFileSync } from "fs";
129
+ import { closeSync, openSync, readSync, statSync } from "fs";
131
130
  import { homedir as homedir2 } from "os";
132
- import { dirname, join as join2 } from "path";
131
+ import { join as join2 } from "path";
133
132
  import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
134
133
 
135
134
  // src/api-client/client.ts
@@ -444,13 +443,12 @@ function usePoll(reload, intervalMs, enabled = true) {
444
443
  }
445
444
 
446
445
  // src/tui/panels/Live.tsx
447
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
446
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
448
447
  var SPIN = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
449
448
  var BG = "#12234f";
450
449
  var DIM_FLOOR = "#233457";
451
450
  var LOCAL_LOG = join2(homedir2(), ".solongate", "local-logs", "solongate-audit.jsonl");
452
451
  var RING = join2(process.cwd(), ".solongate", ".eval-ring.jsonl");
453
- var DUMP = join2(homedir2(), ".solongate", "live-stream.log");
454
452
  var STREAM_KEEP = 100;
455
453
  var hhmmss = (ts) => {
456
454
  const d = new Date(ts);
@@ -476,22 +474,6 @@ function tailLines(file, maxBytes = 131072) {
476
474
  return [];
477
475
  }
478
476
  }
479
- function copyToClipboard(text) {
480
- const tools = [
481
- ["wl-copy", []],
482
- ["xclip", ["-selection", "clipboard"]],
483
- ["xsel", ["--clipboard", "--input"]]
484
- ];
485
- for (const [cmd, args] of tools) {
486
- try {
487
- const r = spawnSync(cmd, args, { input: text, timeout: 2e3 });
488
- if (r.status === 0) return cmd;
489
- } catch {
490
- }
491
- }
492
- process.stdout.write(`\x1B]52;c;${Buffer.from(text, "utf-8").toString("base64")}\x07`);
493
- return null;
494
- }
495
477
  function cloudItem(e) {
496
478
  return {
497
479
  id: "c:" + e.id,
@@ -501,6 +483,7 @@ function cloudItem(e) {
501
483
  permission: (e.permission ?? "").slice(0, 4),
502
484
  detail: (e.arguments_summary ? JSON.stringify(e.arguments_summary) : e.reason ?? "").replace(/\s+/g, " "),
503
485
  dlp: !!e.dlp_matches?.length,
486
+ burst: !!e.rate_limit_burst,
504
487
  source: "cloud",
505
488
  session: e.session_id ?? void 0,
506
489
  agent: e.agent_name ?? void 0,
@@ -556,6 +539,12 @@ function PaneTitle({ label, extra, width }) {
556
539
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: tail + "\u2500".repeat(Math.max(0, width - used)) })
557
540
  ] });
558
541
  }
542
+ function Stat({ label, value, color }) {
543
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginRight: 3, children: [
544
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: label }),
545
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color, children: String(value) })
546
+ ] });
547
+ }
559
548
  function StreamLine({ e, loc }) {
560
549
  return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
561
550
  /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
@@ -571,11 +560,14 @@ function StreamLine({ e, loc }) {
571
560
  /* @__PURE__ */ jsx2(Text2, { color: e.evalMs != null && e.evalMs > 500 ? theme.warn : theme.dim, children: (e.evalMs != null ? `${e.evalMs}ms` : "\u2014").padEnd(7) }),
572
561
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: truncate(e.agent ?? "-", 11).padEnd(12) }),
573
562
  e.dlp ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: "DLP! " }) : null,
563
+ e.burst ? /* @__PURE__ */ jsx2(Text2, { color: theme.warn, children: "BURST " }) : null,
574
564
  e.rule && e.decision !== "ALLOW" ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: truncate(e.rule, 14) + " " }) : null,
575
565
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: e.detail })
576
566
  ] });
577
567
  }
578
568
  var FILTERS = ["all", "local", "cloud"];
569
+ var SESS_FILTERS = ["all", "active", "idle", "ended"];
570
+ var sessStatus = (lastAt, now) => now - lastAt < 9e4 ? "active" : now - lastAt < 3e5 ? "idle" : "ended";
579
571
  function LivePanel({ active: active2 }) {
580
572
  const [s, setS] = useState2(null);
581
573
  const [tsData, setTsData] = useState2(null);
@@ -587,15 +579,17 @@ function LivePanel({ active: active2 }) {
587
579
  const [log, setLog] = useState2([]);
588
580
  const [filter, setFilter] = useState2("all");
589
581
  const [scroll, setScroll] = useState2(0);
590
- const [toast, setToast] = useState2(null);
591
582
  const [mode, setMode] = useState2("stream");
583
+ const [sessFilter, setSessFilter] = useState2("all");
592
584
  const [pickIdx, setPickIdx] = useState2(0);
593
- const [detail, setDetail] = useState2(null);
594
585
  const [detailCloud, setDetailCloud] = useState2([]);
595
- const [detailScroll, setDetailScroll] = useState2(0);
586
+ const [tlScroll, setTlScroll] = useState2(0);
596
587
  const seenRef = useRef2(/* @__PURE__ */ new Set());
597
588
  const lastLocalTs = useRef2(0);
598
589
  const pausedUntil = useRef2(0);
590
+ const [frozen, setFrozen] = useState2(false);
591
+ const frozenRef = useRef2(false);
592
+ frozenRef.current = frozen;
599
593
  const pushLog = useCallback2((msg, level = "ok") => {
600
594
  setLog((prev) => [...prev.slice(-59), { ts: Date.now(), msg, level }]);
601
595
  }, []);
@@ -611,8 +605,9 @@ function LivePanel({ active: active2 }) {
611
605
  },
612
606
  [pushLog]
613
607
  );
614
- const paused = () => Date.now() < pausedUntil.current;
608
+ const paused = () => frozenRef.current || Date.now() < pausedUntil.current;
615
609
  const pollLocal = useCallback2(() => {
610
+ if (frozenRef.current) return;
616
611
  const lines = tailLines(LOCAL_LOG);
617
612
  if (!lines.length) {
618
613
  setLocalOn((prev) => prev === null ? false : prev);
@@ -633,6 +628,7 @@ function LivePanel({ active: active2 }) {
633
628
  permission: (j.permission ?? "").slice(0, 4),
634
629
  detail: (j.arguments ? JSON.stringify(j.arguments) : j.reason ?? "").replace(/\s+/g, " "),
635
630
  dlp: !!j.dlp,
631
+ burst: !!j.rate_limit_burst,
636
632
  source: "local",
637
633
  session: j.session_id,
638
634
  agent: j.agent_name,
@@ -678,6 +674,7 @@ function LivePanel({ active: active2 }) {
678
674
  const t0 = Date.now();
679
675
  try {
680
676
  const fd = await api.audit.list({ limit: 50 });
677
+ if (frozenRef.current) return;
681
678
  const ms = Date.now() - t0;
682
679
  setLat((l) => [...l, ms].slice(-240));
683
680
  const fresh = fd.entries.filter((e) => !seenRef.current.has(e.id));
@@ -695,7 +692,8 @@ function LivePanel({ active: active2 }) {
695
692
  const pollMedium = useCallback2(async () => {
696
693
  if (paused()) return;
697
694
  try {
698
- setS(await api.stats.get());
695
+ const sd = await api.stats.get();
696
+ if (!frozenRef.current) setS(sd);
699
697
  } catch (e) {
700
698
  onApiError(e);
701
699
  }
@@ -703,7 +701,8 @@ function LivePanel({ active: active2 }) {
703
701
  const pollSlow = useCallback2(async () => {
704
702
  if (paused()) return;
705
703
  try {
706
- setTsData(await api.stats.timeseries({ period: "24h" }));
704
+ const td = await api.stats.timeseries({ period: "24h" });
705
+ if (!frozenRef.current) setTsData(td);
707
706
  } catch (e) {
708
707
  onApiError(e);
709
708
  }
@@ -734,25 +733,12 @@ function LivePanel({ active: active2 }) {
734
733
  usePoll(() => {
735
734
  if (!paused()) guard.reload();
736
735
  }, 6e4, active2);
737
- useEffect2(() => {
738
- if (!detail) return;
739
- setDetailCloud([]);
740
- setDetailScroll(0);
741
- let alive = true;
742
- api.audit.list({ session_id: detail.id, limit: 100 }).then((r) => {
743
- if (alive) setDetailCloud(r.entries.map(cloudItem));
744
- }).catch(() => {
745
- });
746
- return () => {
747
- alive = false;
748
- };
749
- }, [detail]);
750
736
  const [tick, setTick] = useState2(0);
751
737
  useEffect2(() => {
752
- if (!active2) return;
738
+ if (!active2 || frozen) return;
753
739
  const t = setInterval(() => setTick((n) => n + 1), 500);
754
740
  return () => clearInterval(t);
755
- }, [active2]);
741
+ }, [active2, frozen]);
756
742
  const startRef = useRef2(Date.now());
757
743
  const cols = process.stdout.columns ?? 100;
758
744
  const rows = process.stdout.rows ?? 30;
@@ -766,67 +752,68 @@ function LivePanel({ active: active2 }) {
766
752
  }
767
753
  const cloudDeduped = cloudBuf.filter((e) => !localKeys.has(`${e.tool}|${e.decision}|${Math.round(e.at / 6e4)}`));
768
754
  const mergedAll = [...cloudDeduped, ...localBuf].sort((a, b) => a.at - b.at);
769
- const points = tsData?.timeseries ?? [];
770
- const nowMin = Math.floor(nowMs / 6e4);
771
- const minuteCounts = new Array(60).fill(0);
772
- const minuteHot = new Array(60).fill(false);
773
- for (const e of mergedAll) {
774
- const idx = 59 - (nowMin - Math.floor(e.at / 6e4));
775
- if (idx >= 0 && idx < 60) {
776
- minuteCounts[idx]++;
777
- if (e.decision !== "ALLOW") minuteHot[idx] = true;
778
- }
779
- }
780
- const localTraffic = localOn === true && minuteCounts.some((c2) => c2 > 0);
781
- const traffic = localTraffic ? minuteCounts : points.map((p) => p.total);
782
- const trafficHot = localTraffic ? minuteHot : points.map((p) => p.denied > 0);
783
- const trafficLabel = localTraffic ? "calls/min \xB7 60m \xB7 local+cloud" : "24h \xB7 cloud";
784
- const rl = ins.layers?.rateLimit;
785
- const dl = ins.layers?.dlp;
786
- const gh = ins.layers?.ghost;
787
- const minuteNow = minuteCounts[59] ?? 0;
788
- const dlpBars = (ins.dlpByPattern ?? []).slice(0, 2);
789
- const maxDlpBar = dlpBars[0]?.count ?? 1;
790
- const denialsAll = mergedAll.filter((e) => e.decision !== "ALLOW");
791
- const lastDeny = denialsAll[denialsAll.length - 1];
792
- const toolCounts = /* @__PURE__ */ new Map();
793
- for (const e of mergedAll) toolCounts.set(e.tool, (toolCounts.get(e.tool) ?? 0) + 1);
794
- const topTools = [...toolCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3);
795
- const latNow = lat[lat.length - 1] ?? 0;
796
- const sortedLat = [...lat].sort((a, b) => a - b);
797
- const latMed = sortedLat.length ? sortedLat[Math.floor(sortedLat.length / 2)] : 0;
798
- const latHotAt = Math.max(2e3, latMed * 2.5);
799
- const backingOff = Date.now() < pausedUntil.current;
800
- const sess = (sessions.data?.agents ?? []).slice().sort((a, b) => Date.parse(b.last_seen_at) - Date.parse(a.last_seen_at));
801
- const sessCounts = sessions.data?.counts;
802
755
  const localSess = /* @__PURE__ */ new Map();
803
756
  for (const e of localBuf) {
804
757
  if (!e.session) continue;
805
- const cur = localSess.get(e.session) ?? { agent: e.agent ?? "local agent", calls: 0, denies: 0, lastAt: 0 };
758
+ const cur = localSess.get(e.session) ?? { agent: e.agent ?? "local agent", calls: 0, denies: 0, dlp: 0, lastAt: 0 };
806
759
  cur.calls++;
807
760
  if (e.decision !== "ALLOW") cur.denies++;
761
+ if (e.dlp) cur.dlp++;
808
762
  cur.lastAt = Math.max(cur.lastAt, e.at);
809
763
  if (e.agent) cur.agent = e.agent;
810
764
  localSess.set(e.session, cur);
811
765
  }
812
- const combinedSess = [
813
- ...[...localSess.entries()].sort((a, b) => b[1].lastAt - a[1].lastAt).map(([id, v]) => ({ source: "local", id, agent: v.agent, calls: v.calls, denies: v.denies, lastAt: v.lastAt, isMe: ring?.session === id })),
814
- ...sess.filter((a) => !localSess.has(a.session_id)).map((a) => ({
766
+ const combinedSessAll = [
767
+ ...[...localSess.entries()].sort((a, b) => b[1].lastAt - a[1].lastAt).map(([id, v]) => ({ source: "local", id, agent: v.agent, calls: v.calls, denies: v.denies, dlp: v.dlp, lastAt: v.lastAt, isMe: ring?.session === id })),
768
+ ...(sessions.data?.agents ?? []).filter((a) => !localSess.has(a.session_id)).sort((a, b) => Date.parse(b.last_seen_at) - Date.parse(a.last_seen_at)).map((a) => ({
815
769
  source: "cloud",
816
770
  id: a.session_id,
817
771
  agent: a.agent_name ?? a.session_id,
818
772
  calls: a.total_calls,
819
773
  denies: a.denied_calls,
774
+ dlp: a.dlp_events,
820
775
  lastAt: Date.parse(a.last_seen_at),
821
776
  trust: a.trust_score,
822
777
  isMe: false
823
778
  }))
824
779
  ];
780
+ const combinedSess = sessFilter === "all" ? combinedSessAll : combinedSessAll.filter((r) => sessStatus(r.lastAt, nowMs) === sessFilter);
825
781
  const localSessIds = new Set(localSess.keys());
826
782
  if (ring?.session) localSessIds.add(ring.session);
827
783
  const isLoc = (e) => e.source === "local" || !!e.session && localSessIds.has(e.session);
828
784
  const filtered = filter === "all" ? mergedAll : mergedAll.filter((e) => filter === "local" ? isLoc(e) : !isLoc(e));
829
785
  const visibleDesc = filtered.slice(-STREAM_KEEP).reverse();
786
+ const points = tsData?.timeseries ?? [];
787
+ const nowSlot = Math.floor(nowMs / 1e4);
788
+ const slotCounts = new Array(60).fill(0);
789
+ const slotHot = new Array(60).fill(false);
790
+ for (const e of mergedAll) {
791
+ const idx = 59 - (nowSlot - Math.floor(e.at / 1e4));
792
+ if (idx >= 0 && idx < 60) {
793
+ slotCounts[idx]++;
794
+ if (e.decision !== "ALLOW") slotHot[idx] = true;
795
+ }
796
+ }
797
+ const localTraffic = localOn === true && slotCounts.some((c2) => c2 > 0);
798
+ const traffic = localTraffic ? slotCounts : points.map((p) => p.total);
799
+ const trafficHot = localTraffic ? slotHot : points.map((p) => p.denied > 0);
800
+ const trafficLabel = localTraffic ? "calls/10s \xB7 last 10m" : "24h \xB7 cloud";
801
+ const rl = ins.layers?.rateLimit;
802
+ const dl = ins.layers?.dlp;
803
+ const gh = ins.layers?.ghost;
804
+ const minuteNow = mergedAll.filter((e) => nowMs - e.at < 6e4).length;
805
+ const dlpBars = (ins.dlpByPattern ?? []).slice(0, 2);
806
+ const maxDlpBar = dlpBars[0]?.count ?? 1;
807
+ const denialsAll = mergedAll.filter((e) => e.decision !== "ALLOW");
808
+ const lastDeny = denialsAll[denialsAll.length - 1];
809
+ const toolCounts = /* @__PURE__ */ new Map();
810
+ for (const e of mergedAll) toolCounts.set(e.tool, (toolCounts.get(e.tool) ?? 0) + 1);
811
+ const topTools = [...toolCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3);
812
+ const latNow = lat[lat.length - 1] ?? 0;
813
+ const sortedLat = [...lat].sort((a, b) => a - b);
814
+ const latMed = sortedLat.length ? sortedLat[Math.floor(sortedLat.length / 2)] : 0;
815
+ const latHotAt = Math.max(2e3, latMed * 2.5);
816
+ const backingOff = Date.now() < pausedUntil.current;
830
817
  const chartH = rows >= 36 ? 6 : 4;
831
818
  const colH = rows >= 32 ? 7 : 5;
832
819
  const streamRows = Math.max(4, rows - 5 - (1 + chartH) - colH);
@@ -834,14 +821,25 @@ function LivePanel({ active: active2 }) {
834
821
  const leftW = Math.floor(innerW * 0.55);
835
822
  const rightW = innerW - leftW - 2;
836
823
  const colW = Math.max(20, Math.floor((innerW - 4) / 3));
837
- const pickable = combinedSess.slice(0, colH - 1);
838
824
  const maxScroll = Math.max(0, visibleDesc.length - streamRows);
839
825
  const clampedScroll = Math.min(scroll, maxScroll);
840
826
  const windowed = visibleDesc.slice(clampedScroll, clampedScroll + streamRows);
841
- const detailEntries = detail ? (() => {
827
+ const selIdx = Math.min(pickIdx, Math.max(0, combinedSess.length - 1));
828
+ const selected = combinedSess[selIdx];
829
+ useEffect2(() => {
830
+ if (mode !== "sessions" || !selected?.id) return;
831
+ setTlScroll(0);
832
+ const t = setTimeout(() => {
833
+ api.audit.list({ session_id: selected.id, limit: 100 }).then((r) => {
834
+ if (!frozenRef.current) setDetailCloud(r.entries.map(cloudItem));
835
+ }).catch(() => setDetailCloud([]));
836
+ }, 400);
837
+ return () => clearTimeout(t);
838
+ }, [mode, selected?.id]);
839
+ const detailEntries = selected ? (() => {
842
840
  const seen = /* @__PURE__ */ new Set();
843
841
  const all = [];
844
- for (const e of [...detailCloud, ...mergedAll.filter((x) => x.session === detail.id)]) {
842
+ for (const e of [...detailCloud, ...mergedAll.filter((x) => x.session === selected.id)]) {
845
843
  const key = `${e.at}:${e.tool}:${e.decision}`;
846
844
  if (seen.has(key)) continue;
847
845
  seen.add(key);
@@ -851,27 +849,22 @@ function LivePanel({ active: active2 }) {
851
849
  })() : [];
852
850
  useInput(
853
851
  (input, key) => {
854
- if (mode === "detail") {
855
- const maxD = Math.max(0, detailEntries.length - (rows - 10));
856
- if (key.leftArrow) {
857
- setMode("stream");
858
- setDetail(null);
859
- } else if (key.upArrow) setDetailScroll((n) => Math.max(0, n - 1));
860
- else if (key.downArrow) setDetailScroll((n) => Math.min(maxD, n + 1));
861
- else if (key.pageUp) setDetailScroll((n) => Math.max(0, n - 10));
862
- else if (key.pageDown) setDetailScroll((n) => Math.min(maxD, n + 10));
852
+ if (input === " ") {
853
+ setFrozen((f) => !f);
863
854
  return;
864
855
  }
865
- if (mode === "pick") {
866
- if (key.upArrow) setPickIdx((n) => Math.max(0, n - 1));
867
- else if (key.downArrow) setPickIdx((n) => Math.min(pickable.length - 1, n + 1));
868
- else if (key.return) {
869
- const row = pickable[pickIdx];
870
- if (row) {
871
- setDetail(row);
872
- setMode("detail");
873
- }
874
- } else if (input === "s" || key.leftArrow) setMode("stream");
856
+ if (frozen) return;
857
+ if (mode === "sessions") {
858
+ if (key.leftArrow) {
859
+ setMode("stream");
860
+ setDetailCloud([]);
861
+ } else if (key.upArrow) setPickIdx((n) => Math.max(0, n - 1));
862
+ else if (key.downArrow) setPickIdx((n) => Math.min(combinedSess.length - 1, n + 1));
863
+ else if (input === "f") {
864
+ setSessFilter((f) => SESS_FILTERS[(SESS_FILTERS.indexOf(f) + 1) % SESS_FILTERS.length]);
865
+ setPickIdx(0);
866
+ } else if (key.pageUp) setTlScroll((n) => Math.max(0, n - 10));
867
+ else if (key.pageDown) setTlScroll((n) => n + 10);
875
868
  return;
876
869
  }
877
870
  if (input === "f") {
@@ -879,30 +872,14 @@ function LivePanel({ active: active2 }) {
879
872
  setScroll(0);
880
873
  } else if (input === "s") {
881
874
  setPickIdx(0);
882
- setMode("pick");
875
+ setMode("sessions");
883
876
  } else if (key.downArrow) setScroll((n) => Math.min(maxScroll, n + 1));
884
877
  else if (key.upArrow) setScroll((n) => Math.max(0, n - 1));
885
878
  else if (key.pageDown) setScroll((n) => Math.min(maxScroll, n + streamRows));
886
879
  else if (key.pageUp) setScroll((n) => Math.max(0, n - streamRows));
887
- else if (input === "c") {
888
- const dump = visibleDesc.map(
889
- (e) => `[${new Date(e.at).toISOString()}] ${e.source.toUpperCase().padEnd(5)} ${e.decision.padEnd(6)} ${e.tool.padEnd(16)} ${e.permission.padEnd(5)} ${e.evalMs != null ? String(e.evalMs).padStart(5) + "ms " : " "}${(e.agent ?? "-").padEnd(14)} ${e.session ? e.session.slice(0, 8) : "- "} ${e.detail}`
890
- ).join("\n");
891
- try {
892
- mkdirSync(dirname(DUMP), { recursive: true });
893
- writeFileSync(DUMP, dump + "\n");
894
- } catch {
895
- }
896
- const via = copyToClipboard(dump);
897
- setToast(
898
- via ? { msg: `\u2713 ${visibleDesc.length} lines in clipboard (${via})`, until: Date.now() + 4e3 } : { msg: "\u2717 no clipboard tool \u2014 install wl-clipboard or xclip", until: Date.now() + 6e3 }
899
- );
900
- }
901
880
  },
902
881
  { isActive: active2 }
903
882
  );
904
- const sessionDot = (st) => st === "active" ? { ch: "\u25CF", color: theme.ok } : st === "idle" ? { ch: "\u25CC", color: theme.warn } : { ch: "\u25CB", color: theme.dim };
905
- void sessionDot;
906
883
  if (s === null && localOn === null) {
907
884
  return /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
908
885
  spin,
@@ -912,72 +889,91 @@ function LivePanel({ active: active2 }) {
912
889
  const titleBar = /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
913
890
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#1432A0", color: "#AAC8FA", bold: true, children: " SOLONGATE LIVE " }),
914
891
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: ` ${spin} up ${fmtUp(nowMs - startRef.current)} \xB7 ${hhmmss(nowMs)} \xB7 api ${latNow}ms ` }),
915
- backingOff ? /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#3d2a12", color: "#ffb454", bold: true, children: " RATE LIMITED \xB7 backing off " }) : lastDeny ? /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#3d1220", color: "#ff6b6b", bold: true, children: ` \u26A0 ${hhmmss(lastDeny.at)} ${lastDeny.tool} DENIED ` }) : /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#4f6db8", children: " \u2713 clean " }),
916
- toast && nowMs < toast.until ? /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#123d1f", color: "#7bd88f", bold: true, children: ` ${toast.msg} ` }) : null
892
+ frozen ? /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#123d1f", color: "#7bd88f", bold: true, children: " \u23F5 COPY MODE \u2014 screen frozen, select & copy freely \xB7 space resume " }) : backingOff ? /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#3d2a12", color: "#ffb454", bold: true, children: " RATE LIMITED \xB7 backing off " }) : lastDeny ? /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#3d1220", color: "#ff6b6b", bold: true, children: ` \u26A0 ${hhmmss(lastDeny.at)} ${lastDeny.tool} DENIED ` }) : /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#4f6db8", children: " \u2713 clean " })
917
893
  ] });
918
- if (mode === "detail" && detail) {
894
+ if (mode === "sessions") {
895
+ const listW = Math.min(44, Math.floor(innerW * 0.38));
896
+ const detailW = innerW - listW - 2;
897
+ const bodyH = rows - 4;
898
+ const listRows = bodyH - 1;
919
899
  const d = detailEntries;
920
900
  const allow = d.filter((e) => e.decision === "ALLOW").length;
921
901
  const deny = d.length - allow;
922
902
  const dlpN = d.filter((e) => e.dlp).length;
903
+ const burstN = d.filter((e) => e.burst).length;
923
904
  const evals = d.filter((e) => e.evalMs != null).map((e) => e.evalMs);
924
905
  const evalAvg = evals.length ? Math.round(evals.reduce((a, b) => a + b, 0) / evals.length) : null;
925
906
  const tCounts = /* @__PURE__ */ new Map();
926
- const pCounts = /* @__PURE__ */ new Map();
927
- for (const e of d) {
928
- tCounts.set(e.tool, (tCounts.get(e.tool) ?? 0) + 1);
929
- if (e.permission) pCounts.set(e.permission, (pCounts.get(e.permission) ?? 0) + 1);
930
- }
931
- const top = [...tCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 6);
932
- const perms = [...pCounts.entries()].sort((a, b) => b[1] - a[1]);
907
+ for (const e of d) tCounts.set(e.tool, (tCounts.get(e.tool) ?? 0) + 1);
908
+ const top = [...tCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5);
933
909
  const first = d[d.length - 1]?.at;
934
910
  const last = d[0]?.at;
935
- const tlRows = Math.max(4, rows - 10);
936
- const maxD = Math.max(0, d.length - tlRows);
937
- const dScroll = Math.min(detailScroll, maxD);
938
- const tl = d.slice(dScroll, dScroll + tlRows);
911
+ const tlRows = Math.max(3, bodyH - 8);
912
+ const maxTl = Math.max(0, d.length - tlRows);
913
+ const tScroll = Math.min(tlScroll, maxTl);
914
+ const tl = d.slice(tScroll, tScroll + tlRows);
939
915
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
940
916
  titleBar,
941
- /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
942
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#AAC8FA", bold: true, children: ` SESSION ${detail.id.slice(0, 8)} ` }),
943
- /* @__PURE__ */ jsx2(Text2, { color: detail.source === "local" ? theme.ok : "#4f6db8", children: " " + detail.source.toUpperCase() }),
944
- /* @__PURE__ */ jsx2(Text2, { bold: true, children: " " + truncate(detail.isMe ? detail.agent + " (this machine)" : detail.agent, 34) }),
945
- detail.trust != null ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ` trust ${detail.trust}/100` }) : null,
946
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2190 back" })
947
- ] }),
948
- /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
949
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "\u2502 calls " }),
950
- /* @__PURE__ */ jsx2(Text2, { bold: true, children: d.length }),
951
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 allow " }),
952
- /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: allow }),
953
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 deny " }),
954
- /* @__PURE__ */ jsx2(Text2, { color: deny ? theme.bad : theme.dim, bold: deny > 0, children: deny }),
955
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 dlp " }),
956
- /* @__PURE__ */ jsx2(Text2, { color: dlpN ? theme.bad : theme.dim, children: dlpN }),
957
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 avg eval " }),
958
- /* @__PURE__ */ jsx2(Text2, { children: evalAvg != null ? `${evalAvg}ms` : "\u2014" }),
959
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 first " }),
960
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: first ? `${hhmmss(first)} (${ago(first)} ago)` : "\u2014" }),
961
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 last " }),
962
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: last ? `${hhmmss(last)} (${ago(last)} ago)` : "\u2014" }),
963
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502" })
964
- ] }),
965
- /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
966
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "\u2502 tools " }),
967
- /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: top.map(([t, c2]) => `${t}\xD7${c2}`).join(" ") || "\u2014" }),
968
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 perms " }),
969
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: perms.map(([p, c2]) => `${p}\xD7${c2}`).join(" ") || "\u2014" }),
970
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502" })
971
- ] }),
972
- /* @__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 }),
973
- /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height: tlRows, overflow: "hidden", children: [
974
- tl.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: detailCloud.length === 0 ? spin + " loading history\u2026" : "no entries" }) : null,
975
- tl.map((e) => /* @__PURE__ */ jsx2(StreamLine, { e, loc: isLoc(e) }, e.id))
917
+ /* @__PURE__ */ jsx2(PaneTitle, { label: "SESSIONS", extra: `${combinedSess.length} shown \xB7 filter ${sessFilter} (f) \xB7 \u2191\u2193 select \xB7 PgUp/Dn timeline \xB7 \u2190 back`, width: innerW }),
918
+ /* @__PURE__ */ jsxs2(Box2, { height: bodyH, children: [
919
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: listW, marginRight: 2, overflow: "hidden", children: [
920
+ combinedSess.length === 0 ? /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
921
+ "no sessions",
922
+ sessFilter !== "all" ? ` (${sessFilter})` : ""
923
+ ] }) : null,
924
+ combinedSess.slice(0, listRows).map((r, i) => {
925
+ const st = sessStatus(r.lastAt, nowMs);
926
+ const dot = st === "active" ? { ch: "\u25CF", color: theme.ok } : st === "idle" ? { ch: "\u25CC", color: theme.warn } : { ch: "\u25CB", color: theme.dim };
927
+ const sel = i === selIdx;
928
+ return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", backgroundColor: sel ? "#1c2f63" : void 0, bold: sel, children: [
929
+ /* @__PURE__ */ jsxs2(Text2, { color: sel ? theme.accentBright : dot.color, children: [
930
+ sel ? "\u25B8" : dot.ch,
931
+ " "
932
+ ] }),
933
+ /* @__PURE__ */ jsx2(Text2, { color: r.source === "local" ? theme.ok : "#4f6db8", children: r.source === "local" ? "LOC " : "CLD " }),
934
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, children: truncate(r.isMe ? "this machine" : r.agent, 13).padEnd(14) }),
935
+ /* @__PURE__ */ jsxs2(Text2, { children: [
936
+ String(r.calls).padStart(4),
937
+ "c "
938
+ ] }),
939
+ /* @__PURE__ */ jsxs2(Text2, { color: r.denies ? theme.bad : theme.dim, children: [
940
+ String(r.denies).padStart(3),
941
+ "d "
942
+ ] }),
943
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ago(r.lastAt) })
944
+ ] }, r.id);
945
+ })
946
+ ] }),
947
+ /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", width: detailW, overflow: "hidden", children: !selected ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "select a session to see its run timeline" }) : /* @__PURE__ */ jsxs2(Fragment2, { children: [
948
+ /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
949
+ /* @__PURE__ */ jsx2(Text2, { color: sessStatus(selected.lastAt, nowMs) === "active" ? theme.ok : theme.dim, children: sessStatus(selected.lastAt, nowMs) === "active" ? "\u25CF " : "\u25CB " }),
950
+ /* @__PURE__ */ jsx2(Text2, { bold: true, children: truncate(selected.isMe ? selected.agent + " (this machine)" : selected.agent, 30) }),
951
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " " + selected.id })
952
+ ] }),
953
+ /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, children: [
954
+ /* @__PURE__ */ jsx2(Stat, { label: "Total", value: d.length }),
955
+ /* @__PURE__ */ jsx2(Stat, { label: "Allowed", value: allow, color: theme.ok }),
956
+ /* @__PURE__ */ jsx2(Stat, { label: "Denied", value: deny, color: deny ? theme.bad : void 0 }),
957
+ /* @__PURE__ */ jsx2(Stat, { label: "DLP", value: dlpN, color: dlpN ? theme.bad : void 0 }),
958
+ /* @__PURE__ */ jsx2(Stat, { label: "Rate limit", value: burstN, color: burstN ? theme.warn : void 0 }),
959
+ /* @__PURE__ */ jsx2(Stat, { label: "Avg eval", value: evalAvg != null ? `${evalAvg}ms` : "\u2014" }),
960
+ selected.trust != null ? /* @__PURE__ */ jsx2(Stat, { label: "Trust", value: `${selected.trust}/100`, color: theme.accent }) : null
961
+ ] }),
962
+ /* @__PURE__ */ jsx2(Text2, { wrap: "truncate", color: theme.dim, children: `first ${first ? `${hhmmss(first)} (${ago(first)} ago)` : "\u2014"} \xB7 last ${last ? `${hhmmss(last)} (${ago(last)} ago)` : "\u2014"} \xB7 tools ${top.map(([t, c2]) => `${t}\xD7${c2}`).join(" ") || "\u2014"}` }),
963
+ /* @__PURE__ */ jsx2(PaneTitle, { label: "TIMELINE", extra: `${d.length} events \xB7 newest first${tScroll ? ` \xB7 \u25BC${tScroll}` : ""}`, width: detailW }),
964
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", overflow: "hidden", flexGrow: 1, children: [
965
+ tl.length === 0 ? /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
966
+ spin,
967
+ " loading history\u2026"
968
+ ] }) : null,
969
+ tl.map((e) => /* @__PURE__ */ jsx2(StreamLine, { e, loc: isLoc(e) }, e.id))
970
+ ] })
971
+ ] }) })
976
972
  ] }),
977
973
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
978
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", bold: true, children: " SESSION " }),
979
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#0b1530", color: "#4f6db8", children: ` ${detail.id} ` }),
980
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: " \u2191\u2193 scroll \xB7 \u2190 back \xB7 esc menu " })
974
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", bold: true, children: " SESSIONS " }),
975
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#0b1530", color: "#4f6db8", children: ` ${combinedSess.length} sessions \xB7 space copy-mode ` }),
976
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: " \u2191\u2193 select \xB7 f status \xB7 \u2190 back \xB7 esc menu " })
981
977
  ] })
982
978
  ] });
983
979
  }
@@ -999,7 +995,7 @@ function LivePanel({ active: active2 }) {
999
995
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 ghost " }),
1000
996
  /* @__PURE__ */ jsx2(Text2, { color: gh?.mode === "on" ? theme.ok : theme.dim, children: gh?.mode ?? "?" }),
1001
997
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 sess " }),
1002
- /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: `${combinedSess.length} (${sessCounts?.active ?? 0} live)` }),
998
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: String(combinedSessAll.length) }),
1003
999
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 hooks " }),
1004
1000
  /* @__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" }),
1005
1001
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502" })
@@ -1038,14 +1034,13 @@ function LivePanel({ active: active2 }) {
1038
1034
  ] })
1039
1035
  ] }),
1040
1036
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, marginRight: 2, overflow: "hidden", children: [
1041
- /* @__PURE__ */ jsx2(PaneTitle, { label: "SESSIONS", extra: mode === "pick" ? "\u2191\u2193 pick \xB7 enter open" : `${combinedSess.length} \xB7 s = inspect`, width: colW }),
1042
- pickable.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "scanning\u2026" }) : null,
1043
- pickable.map((r, i) => {
1044
- const freshDot = nowMs - r.lastAt < 9e4;
1045
- const sel = mode === "pick" && i === pickIdx;
1046
- return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", backgroundColor: sel ? "#1c2f63" : void 0, bold: sel || r.isMe, children: [
1047
- /* @__PURE__ */ jsxs2(Text2, { color: sel ? theme.accentBright : freshDot ? theme.ok : theme.dim, children: [
1048
- sel ? "\u25B8" : freshDot ? "\u25CF" : "\u25CB",
1037
+ /* @__PURE__ */ jsx2(PaneTitle, { label: "SESSIONS", extra: "press s to inspect", width: colW }),
1038
+ combinedSessAll.slice(0, colH - 1).map((r) => {
1039
+ const st = sessStatus(r.lastAt, nowMs);
1040
+ const dot = st === "active" ? { ch: "\u25CF", color: theme.ok } : st === "idle" ? { ch: "\u25CC", color: theme.warn } : { ch: "\u25CB", color: theme.dim };
1041
+ return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", bold: r.isMe, children: [
1042
+ /* @__PURE__ */ jsxs2(Text2, { color: dot.color, children: [
1043
+ dot.ch,
1049
1044
  " "
1050
1045
  ] }),
1051
1046
  /* @__PURE__ */ jsx2(Text2, { color: r.source === "local" ? theme.ok : "#4f6db8", children: r.source === "local" ? "LOC " : "CLD " }),
@@ -1058,7 +1053,7 @@ function LivePanel({ active: active2 }) {
1058
1053
  String(r.denies).padStart(3),
1059
1054
  "d "
1060
1055
  ] }),
1061
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: (r.trust != null ? `t${r.trust} ` : "") + ago(r.lastAt) })
1056
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ago(r.lastAt) })
1062
1057
  ] }, r.id);
1063
1058
  })
1064
1059
  ] }),
@@ -1078,7 +1073,7 @@ function LivePanel({ active: active2 }) {
1078
1073
  PaneTitle,
1079
1074
  {
1080
1075
  label: "TOOL STREAM",
1081
- extra: `newest first \xB7 last ${STREAM_KEEP} \xB7 filter ${filter}${clampedScroll > 0 ? ` \xB7 \u25BC${clampedScroll} older` : ""} \xB7 f filter \xB7 \u2191\u2193 scroll \xB7 c copy \xB7 s sessions`,
1076
+ 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`,
1082
1077
  width: innerW
1083
1078
  }
1084
1079
  ),
@@ -1095,8 +1090,8 @@ function LivePanel({ active: active2 }) {
1095
1090
  ] }),
1096
1091
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
1097
1092
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", bold: true, children: " LIVE " }),
1098
- /* @__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"} ` }),
1099
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: " f filter \xB7 s sessions \xB7 c copy \xB7 esc menu \xB7 q quit " })
1093
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#0b1530", color: "#4f6db8", children: ` local ${localOn ? "on" : "off"} \xB7 ${localBuf.length} loc/${cloudDeduped.length} cld \xB7 top ${topTools.map(([t, c2]) => `${t}\xD7${c2}`).join(" ") || "\u2014"} ` }),
1094
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: " space copy-mode \xB7 f filter \xB7 s sessions \xB7 esc menu \xB7 q quit " })
1100
1095
  ] })
1101
1096
  ] });
1102
1097
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.71.0",
3
+ "version": "0.73.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": {