@solongate/proxy 0.72.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 +195 -212
  2. package/dist/tui/index.js +165 -182
  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,12 +579,11 @@ 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);
@@ -637,6 +628,7 @@ function LivePanel({ active: active2 }) {
637
628
  permission: (j.permission ?? "").slice(0, 4),
638
629
  detail: (j.arguments ? JSON.stringify(j.arguments) : j.reason ?? "").replace(/\s+/g, " "),
639
630
  dlp: !!j.dlp,
631
+ burst: !!j.rate_limit_burst,
640
632
  source: "local",
641
633
  session: j.session_id,
642
634
  agent: j.agent_name,
@@ -741,19 +733,6 @@ function LivePanel({ active: active2 }) {
741
733
  usePoll(() => {
742
734
  if (!paused()) guard.reload();
743
735
  }, 6e4, active2);
744
- useEffect2(() => {
745
- if (!detail) return;
746
- setDetailCloud([]);
747
- setDetailScroll(0);
748
- let alive = true;
749
- api.audit.list({ session_id: detail.id, limit: 100 }).then((r) => {
750
- if (alive) setDetailCloud(r.entries.map(cloudItem));
751
- }).catch(() => {
752
- });
753
- return () => {
754
- alive = false;
755
- };
756
- }, [detail]);
757
736
  const [tick, setTick] = useState2(0);
758
737
  useEffect2(() => {
759
738
  if (!active2 || frozen) return;
@@ -773,67 +752,68 @@ function LivePanel({ active: active2 }) {
773
752
  }
774
753
  const cloudDeduped = cloudBuf.filter((e) => !localKeys.has(`${e.tool}|${e.decision}|${Math.round(e.at / 6e4)}`));
775
754
  const mergedAll = [...cloudDeduped, ...localBuf].sort((a, b) => a.at - b.at);
776
- const points = tsData?.timeseries ?? [];
777
- const nowMin = Math.floor(nowMs / 6e4);
778
- const minuteCounts = new Array(60).fill(0);
779
- const minuteHot = new Array(60).fill(false);
780
- for (const e of mergedAll) {
781
- const idx = 59 - (nowMin - Math.floor(e.at / 6e4));
782
- if (idx >= 0 && idx < 60) {
783
- minuteCounts[idx]++;
784
- if (e.decision !== "ALLOW") minuteHot[idx] = true;
785
- }
786
- }
787
- const localTraffic = localOn === true && minuteCounts.some((c2) => c2 > 0);
788
- const traffic = localTraffic ? minuteCounts : points.map((p) => p.total);
789
- const trafficHot = localTraffic ? minuteHot : points.map((p) => p.denied > 0);
790
- const trafficLabel = localTraffic ? "calls/min \xB7 60m \xB7 local+cloud" : "24h \xB7 cloud";
791
- const rl = ins.layers?.rateLimit;
792
- const dl = ins.layers?.dlp;
793
- const gh = ins.layers?.ghost;
794
- const minuteNow = minuteCounts[59] ?? 0;
795
- const dlpBars = (ins.dlpByPattern ?? []).slice(0, 2);
796
- const maxDlpBar = dlpBars[0]?.count ?? 1;
797
- const denialsAll = mergedAll.filter((e) => e.decision !== "ALLOW");
798
- const lastDeny = denialsAll[denialsAll.length - 1];
799
- const toolCounts = /* @__PURE__ */ new Map();
800
- for (const e of mergedAll) toolCounts.set(e.tool, (toolCounts.get(e.tool) ?? 0) + 1);
801
- const topTools = [...toolCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3);
802
- const latNow = lat[lat.length - 1] ?? 0;
803
- const sortedLat = [...lat].sort((a, b) => a - b);
804
- const latMed = sortedLat.length ? sortedLat[Math.floor(sortedLat.length / 2)] : 0;
805
- const latHotAt = Math.max(2e3, latMed * 2.5);
806
- const backingOff = Date.now() < pausedUntil.current;
807
- const sess = (sessions.data?.agents ?? []).slice().sort((a, b) => Date.parse(b.last_seen_at) - Date.parse(a.last_seen_at));
808
- const sessCounts = sessions.data?.counts;
809
755
  const localSess = /* @__PURE__ */ new Map();
810
756
  for (const e of localBuf) {
811
757
  if (!e.session) continue;
812
- 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 };
813
759
  cur.calls++;
814
760
  if (e.decision !== "ALLOW") cur.denies++;
761
+ if (e.dlp) cur.dlp++;
815
762
  cur.lastAt = Math.max(cur.lastAt, e.at);
816
763
  if (e.agent) cur.agent = e.agent;
817
764
  localSess.set(e.session, cur);
818
765
  }
819
- const combinedSess = [
820
- ...[...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 })),
821
- ...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) => ({
822
769
  source: "cloud",
823
770
  id: a.session_id,
824
771
  agent: a.agent_name ?? a.session_id,
825
772
  calls: a.total_calls,
826
773
  denies: a.denied_calls,
774
+ dlp: a.dlp_events,
827
775
  lastAt: Date.parse(a.last_seen_at),
828
776
  trust: a.trust_score,
829
777
  isMe: false
830
778
  }))
831
779
  ];
780
+ const combinedSess = sessFilter === "all" ? combinedSessAll : combinedSessAll.filter((r) => sessStatus(r.lastAt, nowMs) === sessFilter);
832
781
  const localSessIds = new Set(localSess.keys());
833
782
  if (ring?.session) localSessIds.add(ring.session);
834
783
  const isLoc = (e) => e.source === "local" || !!e.session && localSessIds.has(e.session);
835
784
  const filtered = filter === "all" ? mergedAll : mergedAll.filter((e) => filter === "local" ? isLoc(e) : !isLoc(e));
836
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;
837
817
  const chartH = rows >= 36 ? 6 : 4;
838
818
  const colH = rows >= 32 ? 7 : 5;
839
819
  const streamRows = Math.max(4, rows - 5 - (1 + chartH) - colH);
@@ -841,14 +821,25 @@ function LivePanel({ active: active2 }) {
841
821
  const leftW = Math.floor(innerW * 0.55);
842
822
  const rightW = innerW - leftW - 2;
843
823
  const colW = Math.max(20, Math.floor((innerW - 4) / 3));
844
- const pickable = combinedSess.slice(0, colH - 1);
845
824
  const maxScroll = Math.max(0, visibleDesc.length - streamRows);
846
825
  const clampedScroll = Math.min(scroll, maxScroll);
847
826
  const windowed = visibleDesc.slice(clampedScroll, clampedScroll + streamRows);
848
- 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 ? (() => {
849
840
  const seen = /* @__PURE__ */ new Set();
850
841
  const all = [];
851
- 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)]) {
852
843
  const key = `${e.at}:${e.tool}:${e.decision}`;
853
844
  if (seen.has(key)) continue;
854
845
  seen.add(key);
@@ -858,63 +849,37 @@ function LivePanel({ active: active2 }) {
858
849
  })() : [];
859
850
  useInput(
860
851
  (input, key) => {
861
- if (mode === "detail") {
862
- const maxD = Math.max(0, detailEntries.length - (rows - 10));
863
- if (key.leftArrow) {
864
- setMode("stream");
865
- setDetail(null);
866
- } else if (key.upArrow) setDetailScroll((n) => Math.max(0, n - 1));
867
- else if (key.downArrow) setDetailScroll((n) => Math.min(maxD, n + 1));
868
- else if (key.pageUp) setDetailScroll((n) => Math.max(0, n - 10));
869
- else if (key.pageDown) setDetailScroll((n) => Math.min(maxD, n + 10));
870
- return;
871
- }
872
- if (mode === "pick") {
873
- if (key.upArrow) setPickIdx((n) => Math.max(0, n - 1));
874
- else if (key.downArrow) setPickIdx((n) => Math.min(pickable.length - 1, n + 1));
875
- else if (key.return) {
876
- const row = pickable[pickIdx];
877
- if (row) {
878
- setDetail(row);
879
- setMode("detail");
880
- }
881
- } else if (input === "s" || key.leftArrow) setMode("stream");
882
- return;
883
- }
884
852
  if (input === " ") {
885
853
  setFrozen((f) => !f);
886
854
  return;
887
855
  }
888
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);
868
+ return;
869
+ }
889
870
  if (input === "f") {
890
871
  setFilter((f) => FILTERS[(FILTERS.indexOf(f) + 1) % FILTERS.length]);
891
872
  setScroll(0);
892
873
  } else if (input === "s") {
893
874
  setPickIdx(0);
894
- setMode("pick");
875
+ setMode("sessions");
895
876
  } else if (key.downArrow) setScroll((n) => Math.min(maxScroll, n + 1));
896
877
  else if (key.upArrow) setScroll((n) => Math.max(0, n - 1));
897
878
  else if (key.pageDown) setScroll((n) => Math.min(maxScroll, n + streamRows));
898
879
  else if (key.pageUp) setScroll((n) => Math.max(0, n - streamRows));
899
- else if (input === "c") {
900
- const dump = visibleDesc.map(
901
- (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}`
902
- ).join("\n");
903
- try {
904
- mkdirSync(dirname(DUMP), { recursive: true });
905
- writeFileSync(DUMP, dump + "\n");
906
- } catch {
907
- }
908
- const via = copyToClipboard(dump);
909
- setToast(
910
- 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 }
911
- );
912
- }
913
880
  },
914
881
  { isActive: active2 }
915
882
  );
916
- const sessionDot = (st) => st === "active" ? { ch: "\u25CF", color: theme.ok } : st === "idle" ? { ch: "\u25CC", color: theme.warn } : { ch: "\u25CB", color: theme.dim };
917
- void sessionDot;
918
883
  if (s === null && localOn === null) {
919
884
  return /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
920
885
  spin,
@@ -924,72 +889,91 @@ function LivePanel({ active: active2 }) {
924
889
  const titleBar = /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
925
890
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#1432A0", color: "#AAC8FA", bold: true, children: " SOLONGATE LIVE " }),
926
891
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: ` ${spin} up ${fmtUp(nowMs - startRef.current)} \xB7 ${hhmmss(nowMs)} \xB7 api ${latNow}ms ` }),
927
- 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 " }),
928
- 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 " })
929
893
  ] });
930
- 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;
931
899
  const d = detailEntries;
932
900
  const allow = d.filter((e) => e.decision === "ALLOW").length;
933
901
  const deny = d.length - allow;
934
902
  const dlpN = d.filter((e) => e.dlp).length;
903
+ const burstN = d.filter((e) => e.burst).length;
935
904
  const evals = d.filter((e) => e.evalMs != null).map((e) => e.evalMs);
936
905
  const evalAvg = evals.length ? Math.round(evals.reduce((a, b) => a + b, 0) / evals.length) : null;
937
906
  const tCounts = /* @__PURE__ */ new Map();
938
- const pCounts = /* @__PURE__ */ new Map();
939
- for (const e of d) {
940
- tCounts.set(e.tool, (tCounts.get(e.tool) ?? 0) + 1);
941
- if (e.permission) pCounts.set(e.permission, (pCounts.get(e.permission) ?? 0) + 1);
942
- }
943
- const top = [...tCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 6);
944
- 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);
945
909
  const first = d[d.length - 1]?.at;
946
910
  const last = d[0]?.at;
947
- const tlRows = Math.max(4, rows - 10);
948
- const maxD = Math.max(0, d.length - tlRows);
949
- const dScroll = Math.min(detailScroll, maxD);
950
- 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);
951
915
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
952
916
  titleBar,
953
- /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
954
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#AAC8FA", bold: true, children: ` SESSION ${detail.id.slice(0, 8)} ` }),
955
- /* @__PURE__ */ jsx2(Text2, { color: detail.source === "local" ? theme.ok : "#4f6db8", children: " " + detail.source.toUpperCase() }),
956
- /* @__PURE__ */ jsx2(Text2, { bold: true, children: " " + truncate(detail.isMe ? detail.agent + " (this machine)" : detail.agent, 34) }),
957
- detail.trust != null ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ` trust ${detail.trust}/100` }) : null,
958
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2190 back" })
959
- ] }),
960
- /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
961
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "\u2502 calls " }),
962
- /* @__PURE__ */ jsx2(Text2, { bold: true, children: d.length }),
963
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 allow " }),
964
- /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: allow }),
965
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 deny " }),
966
- /* @__PURE__ */ jsx2(Text2, { color: deny ? theme.bad : theme.dim, bold: deny > 0, children: deny }),
967
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 dlp " }),
968
- /* @__PURE__ */ jsx2(Text2, { color: dlpN ? theme.bad : theme.dim, children: dlpN }),
969
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 avg eval " }),
970
- /* @__PURE__ */ jsx2(Text2, { children: evalAvg != null ? `${evalAvg}ms` : "\u2014" }),
971
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 first " }),
972
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: first ? `${hhmmss(first)} (${ago(first)} ago)` : "\u2014" }),
973
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 last " }),
974
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: last ? `${hhmmss(last)} (${ago(last)} ago)` : "\u2014" }),
975
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502" })
976
- ] }),
977
- /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
978
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "\u2502 tools " }),
979
- /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: top.map(([t, c2]) => `${t}\xD7${c2}`).join(" ") || "\u2014" }),
980
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 perms " }),
981
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: perms.map(([p, c2]) => `${p}\xD7${c2}`).join(" ") || "\u2014" }),
982
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502" })
983
- ] }),
984
- /* @__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 }),
985
- /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height: tlRows, overflow: "hidden", children: [
986
- tl.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: detailCloud.length === 0 ? spin + " loading history\u2026" : "no entries" }) : null,
987
- 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
+ ] }) })
988
972
  ] }),
989
973
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
990
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", bold: true, children: " SESSION " }),
991
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#0b1530", color: "#4f6db8", children: ` ${detail.id} ` }),
992
- /* @__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 " })
993
977
  ] })
994
978
  ] });
995
979
  }
@@ -1011,7 +995,7 @@ function LivePanel({ active: active2 }) {
1011
995
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 ghost " }),
1012
996
  /* @__PURE__ */ jsx2(Text2, { color: gh?.mode === "on" ? theme.ok : theme.dim, children: gh?.mode ?? "?" }),
1013
997
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 sess " }),
1014
- /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: `${combinedSess.length} (${sessCounts?.active ?? 0} live)` }),
998
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: String(combinedSessAll.length) }),
1015
999
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 hooks " }),
1016
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" }),
1017
1001
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502" })
@@ -1050,14 +1034,13 @@ function LivePanel({ active: active2 }) {
1050
1034
  ] })
1051
1035
  ] }),
1052
1036
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, marginRight: 2, overflow: "hidden", children: [
1053
- /* @__PURE__ */ jsx2(PaneTitle, { label: "SESSIONS", extra: mode === "pick" ? "\u2191\u2193 pick \xB7 enter open" : `${combinedSess.length} \xB7 s = inspect`, width: colW }),
1054
- pickable.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "scanning\u2026" }) : null,
1055
- pickable.map((r, i) => {
1056
- const freshDot = nowMs - r.lastAt < 9e4;
1057
- const sel = mode === "pick" && i === pickIdx;
1058
- return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", backgroundColor: sel ? "#1c2f63" : void 0, bold: sel || r.isMe, children: [
1059
- /* @__PURE__ */ jsxs2(Text2, { color: sel ? theme.accentBright : freshDot ? theme.ok : theme.dim, children: [
1060
- 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,
1061
1044
  " "
1062
1045
  ] }),
1063
1046
  /* @__PURE__ */ jsx2(Text2, { color: r.source === "local" ? theme.ok : "#4f6db8", children: r.source === "local" ? "LOC " : "CLD " }),
@@ -1070,7 +1053,7 @@ function LivePanel({ active: active2 }) {
1070
1053
  String(r.denies).padStart(3),
1071
1054
  "d "
1072
1055
  ] }),
1073
- /* @__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) })
1074
1057
  ] }, r.id);
1075
1058
  })
1076
1059
  ] }),
@@ -1090,7 +1073,7 @@ function LivePanel({ active: active2 }) {
1090
1073
  PaneTitle,
1091
1074
  {
1092
1075
  label: "TOOL STREAM",
1093
- 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`,
1094
1077
  width: innerW
1095
1078
  }
1096
1079
  ),
@@ -1107,8 +1090,8 @@ function LivePanel({ active: active2 }) {
1107
1090
  ] }),
1108
1091
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
1109
1092
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", bold: true, children: " LIVE " }),
1110
- /* @__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"} ` }),
1111
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: " space copy-mode \xB7 f filter \xB7 s sessions \xB7 c quick-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 " })
1112
1095
  ] })
1113
1096
  ] });
1114
1097
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.72.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": {