@solongate/proxy 0.67.0 → 0.69.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tui/index.js CHANGED
@@ -8,7 +8,7 @@ var __export = (target, all) => {
8
8
  import { render } from "ink";
9
9
 
10
10
  // src/tui/App.tsx
11
- import { Box as Box8, Text as Text8, useApp, useInput as useInput5 } from "ink";
11
+ import { Box as Box8, Text as Text8, useApp, useInput as useInput6 } from "ink";
12
12
  import { useState as useState7 } from "react";
13
13
 
14
14
  // src/cli-utils.ts
@@ -125,10 +125,10 @@ function KeyHints({ hints }) {
125
125
  }
126
126
 
127
127
  // src/tui/panels/Live.tsx
128
- import { Box as Box2, Text as Text2 } from "ink";
129
- import { closeSync, openSync, readSync, statSync } from "fs";
128
+ import { Box as Box2, Text as Text2, useInput } from "ink";
129
+ import { closeSync, mkdirSync, openSync, readSync, statSync, writeFileSync } from "fs";
130
130
  import { homedir as homedir2 } from "os";
131
- import { join as join2 } from "path";
131
+ import { dirname, join as join2 } from "path";
132
132
  import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
133
133
 
134
134
  // src/api-client/client.ts
@@ -436,8 +436,11 @@ function usePoll(reload, intervalMs, enabled = true) {
436
436
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
437
437
  var SPIN = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
438
438
  var BG = "#12234f";
439
+ var DIM_FLOOR = "#233457";
439
440
  var LOCAL_LOG = join2(homedir2(), ".solongate", "local-logs", "solongate-audit.jsonl");
440
441
  var RING = join2(process.cwd(), ".solongate", ".eval-ring.jsonl");
442
+ var DUMP = join2(homedir2(), ".solongate", "live-stream.log");
443
+ var STREAM_KEEP = 100;
441
444
  var hhmmss = (ts) => {
442
445
  const d = new Date(ts);
443
446
  return Number.isNaN(d.getTime()) ? "--:--:--" : d.toTimeString().slice(0, 8);
@@ -447,7 +450,7 @@ var fmtUp = (ms) => {
447
450
  const p = (n) => String(n).padStart(2, "0");
448
451
  return `${p(Math.floor(s / 3600))}:${p(Math.floor(s % 3600 / 60))}:${p(s % 60)}`;
449
452
  };
450
- function tailLines(file, maxBytes = 65536) {
453
+ function tailLines(file, maxBytes = 131072) {
451
454
  try {
452
455
  const size = statSync(file).size;
453
456
  const start = Math.max(0, size - maxBytes);
@@ -462,6 +465,9 @@ function tailLines(file, maxBytes = 65536) {
462
465
  return [];
463
466
  }
464
467
  }
468
+ function oscCopy(text) {
469
+ process.stdout.write(`\x1B]52;c;${Buffer.from(text, "utf-8").toString("base64")}\x07`);
470
+ }
465
471
  function cloudItem(e) {
466
472
  return {
467
473
  id: "c:" + e.id,
@@ -471,7 +477,11 @@ function cloudItem(e) {
471
477
  permission: (e.permission ?? "").slice(0, 4),
472
478
  detail: (e.arguments_summary ? JSON.stringify(e.arguments_summary) : e.reason ?? "").replace(/\s+/g, " "),
473
479
  dlp: !!e.dlp_matches?.length,
474
- source: "cloud"
480
+ source: "cloud",
481
+ session: e.session_id ?? void 0,
482
+ agent: e.agent_name ?? void 0,
483
+ evalMs: e.evaluation_time_ms ?? void 0,
484
+ rule: e.matched_rule_id ?? void 0
475
485
  };
476
486
  }
477
487
  function ColumnChart({ series, hot, height, width, color, hotColor = theme.bad }) {
@@ -490,7 +500,7 @@ function ColumnChart({ series, hot, height, width, color, hotColor = theme.bad }
490
500
  else if (frac >= (r - 0.5) / height) ch = "\u2584";
491
501
  else if (r === 1) ch = "\u2581";
492
502
  else ch = " ";
493
- const c2 = r === 1 && v === 0 ? "#233457" : hotPad?.[i] ? hotColor : color;
503
+ const c2 = r === 1 && v === 0 ? DIM_FLOOR : hotPad?.[i] ? hotColor : color;
494
504
  const last = segs[segs.length - 1];
495
505
  if (last && last.color === c2) last.text += ch;
496
506
  else segs.push({ text: ch, color: c2 });
@@ -506,7 +516,7 @@ function HBar({ label, value, max, width, color }) {
506
516
  return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
507
517
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: label.padEnd(10) }),
508
518
  /* @__PURE__ */ jsx2(Text2, { color, children: "\u2588".repeat(Math.min(width, filled)) }),
509
- /* @__PURE__ */ jsx2(Text2, { color: "#233457", children: "\u2591".repeat(Math.max(0, width - filled)) }),
519
+ /* @__PURE__ */ jsx2(Text2, { color: DIM_FLOOR, children: "\u2591".repeat(Math.max(0, width - filled)) }),
510
520
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " " + value })
511
521
  ] });
512
522
  }
@@ -522,6 +532,7 @@ function PaneTitle({ label, extra, width }) {
522
532
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: tail + "\u2500".repeat(Math.max(0, width - used)) })
523
533
  ] });
524
534
  }
535
+ var FILTERS = ["all", "local", "cloud"];
525
536
  function LivePanel({ active: active2 }) {
526
537
  const [s, setS] = useState2(null);
527
538
  const [tsData, setTsData] = useState2(null);
@@ -531,6 +542,9 @@ function LivePanel({ active: active2 }) {
531
542
  const [localOn, setLocalOn] = useState2(null);
532
543
  const [ring, setRing] = useState2(null);
533
544
  const [log, setLog] = useState2([]);
545
+ const [filter, setFilter] = useState2("all");
546
+ const [scroll, setScroll] = useState2(0);
547
+ const [toast, setToast] = useState2(null);
534
548
  const seenRef = useRef2(/* @__PURE__ */ new Set());
535
549
  const lastLocalTs = useRef2(0);
536
550
  const pausedUntil = useRef2(0);
@@ -568,10 +582,14 @@ function LivePanel({ active: active2 }) {
568
582
  at,
569
583
  tool: j.tool ?? "?",
570
584
  decision: j.decision ?? "ALLOW",
571
- permission: "",
585
+ permission: (j.permission ?? "").slice(0, 4),
572
586
  detail: (j.arguments ? JSON.stringify(j.arguments) : j.reason ?? "").replace(/\s+/g, " "),
573
587
  dlp: !!j.dlp,
574
- source: "local"
588
+ source: "local",
589
+ session: j.session_id,
590
+ agent: j.agent_name,
591
+ evalMs: j.evaluation_time_ms,
592
+ rule: j.matched_rule_id
575
593
  });
576
594
  } catch {
577
595
  }
@@ -580,9 +598,7 @@ function LivePanel({ active: active2 }) {
580
598
  lastLocalTs.current = fresh[fresh.length - 1].at;
581
599
  setLocalBuf((prev) => [...prev, ...fresh].slice(-400));
582
600
  const denies = fresh.filter((f) => f.decision !== "ALLOW").length;
583
- if (lastLocalTs.current !== fresh[0].at || fresh.length < 10) {
584
- pushLog(`local +${fresh.length} calls${denies ? ` \xB7 ${denies} DENIED` : ""}`, denies ? "bad" : "warn");
585
- }
601
+ if (fresh.length < 10) pushLog(`local +${fresh.length} calls${denies ? ` \xB7 ${denies} DENIED` : ""}`, denies ? "bad" : "warn");
586
602
  }
587
603
  const ringLines = tailLines(RING, 8192).slice(-30);
588
604
  if (ringLines.length) {
@@ -619,9 +635,7 @@ function LivePanel({ active: active2 }) {
619
635
  const fresh = fd.entries.filter((e) => !seenRef.current.has(e.id));
620
636
  const firstLoad = seenRef.current.size === 0 && fresh.length > 1;
621
637
  for (const e of fresh) seenRef.current.add(e.id);
622
- if (fresh.length) {
623
- setCloudBuf((prev) => [...prev, ...fresh.map(cloudItem).sort((a, b) => a.at - b.at)].slice(-400));
624
- }
638
+ if (fresh.length) setCloudBuf((prev) => [...prev, ...fresh.map(cloudItem).sort((a, b) => a.at - b.at)].slice(-400));
625
639
  const denies = fresh.filter((e) => e.decision !== "ALLOW").length;
626
640
  if (firstLoad) pushLog(`cloud link up \xB7 api ${ms}ms \xB7 ${fresh.length} calls`);
627
641
  else pushLog(`api ${ms}ms \xB7 +${fresh.length} cloud${denies ? ` \xB7 ${denies} DENIED` : ""}`, denies ? "bad" : fresh.length ? "warn" : "ok");
@@ -661,16 +675,20 @@ function LivePanel({ active: active2 }) {
661
675
  }, [active2, pollFeed, pollMedium, pollSlow]);
662
676
  const sessions = useLoader(() => api.agents.live({ limit: 30, includeDeactivated: true }));
663
677
  const insights = useLoader(() => api.stats.securityInsights(7));
678
+ const guard = useLoader(() => api.settings.getGuardStatus());
664
679
  usePoll(() => {
665
680
  if (!paused()) sessions.reload();
666
681
  }, 1e4, active2);
667
682
  usePoll(() => {
668
683
  if (!paused()) insights.reload();
669
684
  }, 3e4, active2);
685
+ usePoll(() => {
686
+ if (!paused()) guard.reload();
687
+ }, 6e4, active2);
670
688
  const [tick, setTick] = useState2(0);
671
689
  useEffect2(() => {
672
690
  if (!active2) return;
673
- const t = setInterval(() => setTick((n) => n + 1), 200);
691
+ const t = setInterval(() => setTick((n) => n + 1), 500);
674
692
  return () => clearInterval(t);
675
693
  }, [active2]);
676
694
  const startRef = useRef2(Date.now());
@@ -678,19 +696,34 @@ function LivePanel({ active: active2 }) {
678
696
  const rows = process.stdout.rows ?? 30;
679
697
  const ins = insights.data ?? {};
680
698
  const spin = SPIN[tick % SPIN.length];
699
+ const nowMs = Date.now();
700
+ const mergedAll = [...cloudBuf, ...localBuf].sort((a, b) => a.at - b.at);
701
+ const filtered = filter === "all" ? mergedAll : mergedAll.filter((e) => e.source === filter);
702
+ const visible = filtered.slice(-STREAM_KEEP);
681
703
  const points = tsData?.timeseries ?? [];
682
- const traffic = points.map((p) => p.total);
683
- const trafficHot = points.map((p) => p.denied > 0);
704
+ const minuteCounts = new Array(60).fill(0);
705
+ const minuteHot = new Array(60).fill(false);
706
+ for (const e of mergedAll) {
707
+ const idx = 59 - Math.floor((nowMs - e.at) / 6e4);
708
+ if (idx >= 0 && idx < 60) {
709
+ minuteCounts[idx]++;
710
+ if (e.decision !== "ALLOW") minuteHot[idx] = true;
711
+ }
712
+ }
713
+ const localTraffic = localOn === true && minuteCounts.some((c2) => c2 > 0);
714
+ const traffic = localTraffic ? minuteCounts : points.map((p) => p.total);
715
+ const trafficHot = localTraffic ? minuteHot : points.map((p) => p.denied > 0);
716
+ const trafficLabel = localTraffic ? "calls/min \xB7 60m \xB7 local+cloud" : "24h \xB7 cloud";
684
717
  const rl = ins.layers?.rateLimit;
685
718
  const dl = ins.layers?.dlp;
686
- const minuteNow = (ins.activity?.minute ?? []).slice(-1)[0]?.count ?? 0;
719
+ const gh = ins.layers?.ghost;
720
+ const minuteNow = minuteCounts[59] ?? 0;
687
721
  const dlpBars = (ins.dlpByPattern ?? []).slice(0, 2);
688
722
  const maxDlpBar = dlpBars[0]?.count ?? 1;
689
- const merged = [...cloudBuf, ...localBuf].sort((a, b) => a.at - b.at);
690
- const denials = merged.filter((e) => e.decision !== "ALLOW");
723
+ const denials = mergedAll.filter((e) => e.decision !== "ALLOW");
691
724
  const lastDeny = denials[denials.length - 1];
692
725
  const toolCounts = /* @__PURE__ */ new Map();
693
- for (const e of merged) toolCounts.set(e.tool, (toolCounts.get(e.tool) ?? 0) + 1);
726
+ for (const e of mergedAll) toolCounts.set(e.tool, (toolCounts.get(e.tool) ?? 0) + 1);
694
727
  const topTools = [...toolCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3);
695
728
  const latNow = lat[lat.length - 1] ?? 0;
696
729
  const sortedLat = [...lat].sort((a, b) => a - b);
@@ -699,20 +732,65 @@ function LivePanel({ active: active2 }) {
699
732
  const backingOff = Date.now() < pausedUntil.current;
700
733
  const sess = (sessions.data?.agents ?? []).slice().sort((a, b) => Date.parse(b.last_seen_at) - Date.parse(a.last_seen_at));
701
734
  const sessCounts = sessions.data?.counts;
735
+ const localSess = /* @__PURE__ */ new Map();
736
+ for (const e of localBuf) {
737
+ if (!e.session) continue;
738
+ const cur = localSess.get(e.session) ?? { agent: e.agent ?? "local agent", calls: 0, denies: 0, lastAt: 0 };
739
+ cur.calls++;
740
+ if (e.decision !== "ALLOW") cur.denies++;
741
+ cur.lastAt = Math.max(cur.lastAt, e.at);
742
+ if (e.agent) cur.agent = e.agent;
743
+ localSess.set(e.session, cur);
744
+ }
745
+ const localSessList = [...localSess.entries()].sort((a, b) => b[1].lastAt - a[1].lastAt);
702
746
  const chartH = rows >= 36 ? 6 : 4;
703
- const colH = rows >= 32 ? 6 : 5;
704
- const streamRows = Math.max(4, rows - 6 - chartH - colH - (localOn === false ? 1 : 0));
705
- const tail = merged.slice(-streamRows);
747
+ const colH = rows >= 32 ? 7 : 5;
748
+ const streamRows = Math.max(4, rows - 5 - (1 + chartH) - colH);
706
749
  const innerW = cols - 2;
707
750
  const leftW = Math.floor(innerW * 0.55);
708
751
  const rightW = innerW - leftW - 2;
709
752
  const colW = Math.max(20, Math.floor((innerW - 4) / 3));
753
+ const maxScroll = Math.max(0, visible.length - streamRows);
754
+ const clampedScroll = Math.min(scroll, maxScroll);
755
+ const start = Math.max(0, visible.length - streamRows - clampedScroll);
756
+ const windowed = visible.slice(start, start + streamRows);
757
+ useInput(
758
+ (input, key) => {
759
+ if (input === "f") {
760
+ setFilter((f) => FILTERS[(FILTERS.indexOf(f) + 1) % FILTERS.length]);
761
+ setScroll(0);
762
+ } else if (key.upArrow) setScroll((n) => Math.min(maxScroll, n + 1));
763
+ else if (key.downArrow) setScroll((n) => Math.max(0, n - 1));
764
+ else if (key.pageUp) setScroll((n) => Math.min(maxScroll, n + streamRows));
765
+ else if (key.pageDown) setScroll((n) => Math.max(0, n - streamRows));
766
+ else if (input === "c") {
767
+ const dump = visible.map(
768
+ (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}`
769
+ ).join("\n");
770
+ try {
771
+ mkdirSync(dirname(DUMP), { recursive: true });
772
+ writeFileSync(DUMP, dump + "\n");
773
+ } catch {
774
+ }
775
+ oscCopy(dump);
776
+ setToast({ msg: `copied ${visible.length} lines \u2192 clipboard + ${DUMP}`, until: Date.now() + 4e3 });
777
+ }
778
+ },
779
+ { isActive: active2 }
780
+ );
710
781
  const sessionDot = (st) => st === "active" ? { ch: "\u25CF", color: theme.ok } : st === "idle" ? { ch: "\u25CC", color: theme.warn } : { ch: "\u25CB", color: theme.dim };
782
+ if (s === null && localOn === null) {
783
+ return /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
784
+ spin,
785
+ " connecting\u2026"
786
+ ] });
787
+ }
711
788
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
712
789
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
713
790
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#1432A0", color: "#AAC8FA", bold: true, children: " SOLONGATE LIVE " }),
714
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: ` ${spin} up ${fmtUp(Date.now() - startRef.current)} \xB7 ${hhmmss(Date.now())} \xB7 api ${latNow}ms ` }),
715
- 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 " })
791
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: ` ${spin} up ${fmtUp(nowMs - startRef.current)} \xB7 ${hhmmss(nowMs)} \xB7 api ${latNow}ms ` }),
792
+ 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 " }),
793
+ toast && nowMs < toast.until ? /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#123d1f", color: "#7bd88f", bold: true, children: ` ${toast.msg} ` }) : null
716
794
  ] }),
717
795
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
718
796
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "\u2502 calls " }),
@@ -722,18 +800,22 @@ function LivePanel({ active: active2 }) {
722
800
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 deny " }),
723
801
  /* @__PURE__ */ jsx2(Text2, { color: theme.bad, bold: true, children: s ? s.denied : "\xB7\xB7" }),
724
802
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 local " }),
725
- localOn ? /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: "\u2713 streaming" }) : /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "off" }),
726
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 ratelimit " }),
803
+ localOn ? /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: "\u2713" }) : /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "off" }),
804
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 rl " }),
727
805
  /* @__PURE__ */ jsx2(Text2, { color: rl?.mode === "block" ? theme.ok : rl?.mode === "detect" ? theme.warn : theme.dim, children: rl?.mode ?? "?" }),
728
806
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 dlp " }),
729
807
  /* @__PURE__ */ jsx2(Text2, { color: dl?.mode === "block" ? theme.ok : dl?.mode === "detect" ? theme.warn : theme.dim, children: dl?.mode ?? "?" }),
730
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 sessions " }),
731
- /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: sessCounts ? `${sessCounts.active}\u25B2 ${sessCounts.idle}\u25CC ${sessCounts.deactivated}\u25CB` : "\xB7" }),
808
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 ghost " }),
809
+ /* @__PURE__ */ jsx2(Text2, { color: gh?.mode === "on" ? theme.ok : theme.dim, children: gh?.mode ?? "?" }),
810
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 sess " }),
811
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: `${localSessList.length}loc ${sessCounts?.active ?? 0}\u25B2${sessCounts?.idle ?? 0}\u25CC` }),
812
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 hooks " }),
813
+ /* @__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" }),
732
814
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502" })
733
815
  ] }),
734
- /* @__PURE__ */ jsxs2(Box2, { children: [
816
+ /* @__PURE__ */ jsxs2(Box2, { height: 1 + chartH, children: [
735
817
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: leftW, marginRight: 2, children: [
736
- /* @__PURE__ */ jsx2(PaneTitle, { label: "TRAFFIC", extra: `24h \xB7 peak ${Math.max(0, ...traffic)} \xB7 red = had denials`, width: leftW }),
818
+ /* @__PURE__ */ jsx2(PaneTitle, { label: "TRAFFIC", extra: `${trafficLabel} \xB7 peak ${Math.max(0, ...traffic)} \xB7 red = denials`, width: leftW }),
737
819
  /* @__PURE__ */ jsx2(ColumnChart, { series: traffic, hot: trafficHot, height: chartH, width: leftW, color: theme.accent })
738
820
  ] }),
739
821
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: rightW, children: [
@@ -741,45 +823,61 @@ function LivePanel({ active: active2 }) {
741
823
  /* @__PURE__ */ jsx2(ColumnChart, { series: lat, hot: lat.map((v) => v > latHotAt), height: chartH, width: rightW, color: "#82AAF0", hotColor: "#ffb454" })
742
824
  ] })
743
825
  ] }),
744
- /* @__PURE__ */ jsxs2(Box2, { children: [
745
- /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, marginRight: 2, children: [
826
+ /* @__PURE__ */ jsxs2(Box2, { height: colH, children: [
827
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, marginRight: 2, overflow: "hidden", children: [
746
828
  /* @__PURE__ */ jsx2(PaneTitle, { label: "LAYERS", width: colW }),
747
829
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
748
830
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "RATELIMIT ".padEnd(10) }),
749
831
  /* @__PURE__ */ jsx2(Text2, { color: rl?.mode === "block" ? theme.ok : rl?.mode === "detect" ? theme.warn : theme.dim, children: (rl?.mode ?? "?").padEnd(7) }),
750
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: rl?.perMinute ? `${minuteNow}/${rl.perMinute} per min` : "no limit set" })
832
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: rl?.perMinute ? `${minuteNow}/${rl.perMinute}m ${rl.perHour || "\u2014"}h ${rl.perDay || "\u2014"}d` : "no limits set" })
751
833
  ] }),
752
- rl?.perMinute ? /* @__PURE__ */ jsx2(HBar, { label: "load", value: minuteNow, max: rl.perMinute, width: Math.max(6, colW - 16), color: minuteNow > rl.perMinute ? theme.bad : theme.accent }) : null,
834
+ rl?.perMinute ? /* @__PURE__ */ jsx2(HBar, { label: "load/min", value: minuteNow, max: rl.perMinute, width: Math.max(6, colW - 16), color: minuteNow > rl.perMinute ? theme.bad : theme.accent }) : null,
753
835
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
754
836
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "DLP".padEnd(10) }),
755
837
  /* @__PURE__ */ jsx2(Text2, { color: dl?.mode === "block" ? theme.ok : dl?.mode === "detect" ? theme.warn : theme.dim, children: (dl?.mode ?? "?").padEnd(7) }),
756
- /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
757
- (dl?.patterns ?? []).length,
758
- " patterns armed"
759
- ] })
838
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: `${(dl?.patterns ?? []).length} builtin \xB7 ${(dl?.custom ?? []).length} custom` })
760
839
  ] }),
761
- dlpBars.length ? dlpBars.map((d) => /* @__PURE__ */ jsx2(HBar, { label: truncate(d.pattern, 10), value: d.count, max: maxDlpBar, width: Math.max(6, colW - 16), color: theme.bad }, d.pattern)) : /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "hits 7d none \u2014 clean" }),
762
- ring ? /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
840
+ dlpBars.length ? dlpBars.map((d) => /* @__PURE__ */ jsx2(HBar, { label: truncate(d.pattern, 10), value: d.count, max: maxDlpBar, width: Math.max(6, colW - 16), color: theme.bad }, d.pattern)) : /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " no dlp hits in last 7 days" }),
841
+ /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
763
842
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "GUARD".padEnd(10) }),
764
- /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: "local \u2713" }),
765
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ` eval avg ${ring.avgMs}ms \xB7 ${ring.count} recent` })
766
- ] }) : null
843
+ ring ? /* @__PURE__ */ jsxs2(Text2, { color: theme.ok, children: [
844
+ "local \u2713 ",
845
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: `eval avg ${ring.avgMs}ms` })
846
+ ] }) : /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "no local ring in cwd" })
847
+ ] })
767
848
  ] }),
768
- /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, marginRight: 2, children: [
769
- /* @__PURE__ */ jsx2(PaneTitle, { label: "SESSIONS", extra: sessCounts ? `${sessCounts.active} live` : "", width: colW }),
770
- ring?.session ? /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
771
- /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: "\u25CF " }),
772
- /* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, bold: true, children: "this machine" }),
773
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " " + truncate(ring.session, 12) + " \xB7 guarded" })
774
- ] }) : null,
775
- sess.length === 0 && !ring ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "scanning\u2026" }) : null,
776
- sess.slice(0, colH - 1 - (ring ? 1 : 0)).map((a) => {
849
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, marginRight: 2, overflow: "hidden", children: [
850
+ /* @__PURE__ */ jsx2(PaneTitle, { label: "SESSIONS", extra: `${localSessList.length} local \xB7 ${sessCounts?.active ?? 0} cloud`, width: colW }),
851
+ localSessList.length === 0 && sess.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "scanning\u2026" }) : null,
852
+ localSessList.slice(0, Math.max(1, colH - 2)).map(([id, v]) => {
853
+ const freshDot = nowMs - v.lastAt < 9e4;
854
+ const isMe = ring?.session === id;
855
+ return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
856
+ /* @__PURE__ */ jsxs2(Text2, { color: freshDot ? theme.ok : theme.dim, children: [
857
+ freshDot ? "\u25CF" : "\u25CB",
858
+ " "
859
+ ] }),
860
+ /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: "LOC " }),
861
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, bold: isMe, children: truncate(isMe ? "this machine" : v.agent, 12).padEnd(13) }),
862
+ /* @__PURE__ */ jsxs2(Text2, { children: [
863
+ String(v.calls).padStart(4),
864
+ "c "
865
+ ] }),
866
+ /* @__PURE__ */ jsxs2(Text2, { color: v.denies ? theme.bad : theme.dim, children: [
867
+ String(v.denies).padStart(3),
868
+ "d "
869
+ ] }),
870
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ago(v.lastAt) })
871
+ ] }, id);
872
+ }),
873
+ sess.slice(0, Math.max(0, colH - 1 - Math.min(localSessList.length, Math.max(1, colH - 2)))).map((a) => {
777
874
  const dot = sessionDot(a.status);
778
875
  return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
779
876
  /* @__PURE__ */ jsxs2(Text2, { color: dot.color, children: [
780
877
  dot.ch,
781
878
  " "
782
879
  ] }),
880
+ /* @__PURE__ */ jsx2(Text2, { color: "#4f6db8", children: "CLD " }),
783
881
  /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: truncate(a.agent_name ?? a.session_id, 12).padEnd(13) }),
784
882
  /* @__PURE__ */ jsxs2(Text2, { children: [
785
883
  String(a.total_calls).padStart(4),
@@ -789,12 +887,12 @@ function LivePanel({ active: active2 }) {
789
887
  String(a.denied_calls).padStart(3),
790
888
  "d "
791
889
  ] }),
792
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ago(a.last_seen_at) })
890
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: `t${a.trust_score} ` + ago(a.last_seen_at) })
793
891
  ] }, a.session_id);
794
892
  })
795
893
  ] }),
796
- /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, children: [
797
- /* @__PURE__ */ jsx2(PaneTitle, { label: "EVENT LOG", extra: "streaming", width: colW }),
894
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, overflow: "hidden", children: [
895
+ /* @__PURE__ */ jsx2(PaneTitle, { label: "EVENT LOG", extra: "system heartbeat", width: colW }),
798
896
  log.slice(-(colH - 1)).map((l, i) => /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
799
897
  /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
800
898
  hhmmss(l.ts),
@@ -805,39 +903,51 @@ function LivePanel({ active: active2 }) {
805
903
  ] }, l.ts + ":" + i))
806
904
  ] })
807
905
  ] }),
808
- /* @__PURE__ */ jsx2(PaneTitle, { label: "TOOL STREAM", extra: `local+cloud tail \xB7 ${merged.length} buffered`, width: innerW }),
809
- localOn === false ? /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
810
- /* @__PURE__ */ jsx2(Text2, { color: theme.warn, children: "local logs off" }),
811
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2014 enable in dashboard \u2192 Settings \u2192 Local logs; hooks then write ~/.solongate/local-logs (view them there with `solongate logs-server`)" })
812
- ] }) : null,
813
- tail.length === 0 ? /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
814
- spin,
815
- " awaiting traffic\u2026"
816
- ] }) : null,
817
- tail.map((e) => /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
818
- /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
819
- "[",
820
- hhmmss(e.at),
821
- " "
822
- ] }),
823
- /* @__PURE__ */ jsx2(Text2, { color: e.source === "local" ? theme.ok : "#4f6db8", children: e.source === "local" ? "LOC" : "CLD" }),
824
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "] " }),
825
- /* @__PURE__ */ jsx2(Text2, { color: decisionColor(e.decision), bold: e.decision !== "ALLOW", children: e.decision.padEnd(6) }),
826
- /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: truncate(e.tool, 14).padEnd(15) }),
827
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: e.permission.padEnd(5) }),
828
- e.dlp ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: "DLP! " }) : null,
829
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: e.detail })
830
- ] }, e.id)),
906
+ /* @__PURE__ */ jsx2(
907
+ PaneTitle,
908
+ {
909
+ label: "TOOL STREAM",
910
+ extra: `last ${STREAM_KEEP} \xB7 filter ${filter}${clampedScroll > 0 ? ` \xB7 \u25B2${clampedScroll} scrolled` : " \xB7 following"} \xB7 f filter \xB7 \u2191\u2193 scroll \xB7 c copy`,
911
+ width: innerW
912
+ }
913
+ ),
914
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height: streamRows, overflow: "hidden", children: [
915
+ localOn === false ? /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
916
+ /* @__PURE__ */ jsx2(Text2, { color: theme.warn, children: "local logs off" }),
917
+ /* @__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`" })
918
+ ] }) : null,
919
+ windowed.length === 0 ? /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
920
+ spin,
921
+ " awaiting traffic\u2026"
922
+ ] }) : null,
923
+ windowed.map((e) => /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
924
+ /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
925
+ "[",
926
+ hhmmss(e.at),
927
+ " "
928
+ ] }),
929
+ /* @__PURE__ */ jsx2(Text2, { color: e.source === "local" ? theme.ok : "#4f6db8", children: e.source === "local" ? "LOC" : "CLD" }),
930
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "] " }),
931
+ /* @__PURE__ */ jsx2(Text2, { color: decisionColor(e.decision), bold: e.decision !== "ALLOW", children: e.decision.padEnd(6) }),
932
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: truncate(e.tool, 12).padEnd(13) }),
933
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: e.permission.padEnd(5) }),
934
+ /* @__PURE__ */ jsx2(Text2, { color: e.evalMs != null && e.evalMs > 500 ? theme.warn : theme.dim, children: (e.evalMs != null ? `${e.evalMs}ms` : "\u2014").padEnd(7) }),
935
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: truncate(e.agent ?? "-", 11).padEnd(12) }),
936
+ e.dlp ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: "DLP! " }) : null,
937
+ e.rule && e.decision !== "ALLOW" ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: truncate(e.rule, 14) + " " }) : null,
938
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: e.detail })
939
+ ] }, e.id))
940
+ ] }),
831
941
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
832
942
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", bold: true, children: " LIVE " }),
833
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#0b1530", color: "#4f6db8", children: ` local ${localOn ? "on" : "off"} \xB7 top ${topTools.map(([t, c2]) => `${t}\xD7${c2}`).join(" ") || "\u2014"} \xB7 buf ${merged.length} ` }),
834
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: " esc menu \xB7 q quit " })
943
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#0b1530", color: "#4f6db8", children: ` local ${localOn ? "on" : "off"} \xB7 ${localBuf.length} loc/${cloudBuf.length} cld buffered \xB7 top ${topTools.map(([t, c2]) => `${t}\xD7${c2}`).join(" ") || "\u2014"} ` }),
944
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: " f filter \xB7 \u2191\u2193 scroll \xB7 c copy \xB7 esc menu \xB7 q quit " })
835
945
  ] })
836
946
  ] });
837
947
  }
838
948
 
839
949
  // src/tui/panels/Policies.tsx
840
- import { Box as Box3, Text as Text3, useInput } from "ink";
950
+ import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
841
951
  import TextInput from "ink-text-input";
842
952
  import { useEffect as useEffect3, useState as useState3 } from "react";
843
953
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
@@ -928,7 +1038,7 @@ function PoliciesPanel({ focused }) {
928
1038
  setDirty(false);
929
1039
  setStatus("discarded");
930
1040
  };
931
- useInput(
1041
+ useInput2(
932
1042
  (input, key) => {
933
1043
  if (view === "list") {
934
1044
  if (key.upArrow) setPi((n) => Math.max(0, n - 1));
@@ -1072,7 +1182,7 @@ function PoliciesPanel({ focused }) {
1072
1182
  }
1073
1183
 
1074
1184
  // src/tui/panels/RateLimit.tsx
1075
- import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
1185
+ import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
1076
1186
  import { useEffect as useEffect4, useState as useState4 } from "react";
1077
1187
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1078
1188
  var MODES = ["off", "detect", "block"];
@@ -1123,7 +1233,7 @@ function RateLimitPanel({ focused }) {
1123
1233
  setStatus("\u2717 " + (e instanceof Error ? e.message : String(e)));
1124
1234
  }
1125
1235
  };
1126
- useInput2(
1236
+ useInput3(
1127
1237
  (input, key) => {
1128
1238
  const step = key.shift ? 10 : 1;
1129
1239
  if (key.upArrow) setFi((n) => (n - 1 + FIELDS2.length) % FIELDS2.length);
@@ -1202,7 +1312,7 @@ function FieldRow({ label, active: active2, children }) {
1202
1312
  }
1203
1313
 
1204
1314
  // src/tui/panels/Dlp.tsx
1205
- import { Box as Box5, Text as Text5, useInput as useInput3 } from "ink";
1315
+ import { Box as Box5, Text as Text5, useInput as useInput4 } from "ink";
1206
1316
  import TextInput2 from "ink-text-input";
1207
1317
  import { useEffect as useEffect5, useState as useState5 } from "react";
1208
1318
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
@@ -1247,7 +1357,7 @@ function DlpPanel({ focused }) {
1247
1357
  };
1248
1358
  const customCount = dlp?.custom.length ?? 0;
1249
1359
  const total = available.length + customCount;
1250
- useInput3(
1360
+ useInput4(
1251
1361
  (input, key) => {
1252
1362
  if (!dlp) return;
1253
1363
  if (key.upArrow) setSel((n) => Math.max(0, n - 1));
@@ -1409,7 +1519,7 @@ function StatsPanel({ active: active2 }) {
1409
1519
  }
1410
1520
 
1411
1521
  // src/tui/panels/Audit.tsx
1412
- import { Box as Box7, Text as Text7, useInput as useInput4 } from "ink";
1522
+ import { Box as Box7, Text as Text7, useInput as useInput5 } from "ink";
1413
1523
  import TextInput3 from "ink-text-input";
1414
1524
  import { useState as useState6 } from "react";
1415
1525
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
@@ -1440,7 +1550,7 @@ function AuditPanel({ active: active2, focused }) {
1440
1550
  () => view === "detail" && current?.session_id ? api.audit.list({ session_id: current.session_id, limit: 40 }) : Promise.resolve(null),
1441
1551
  [view, current?.session_id]
1442
1552
  );
1443
- useInput4(
1553
+ useInput5(
1444
1554
  (input, key) => {
1445
1555
  if (view === "detail") {
1446
1556
  if (key.leftArrow || key.escape) setView("list");
@@ -1617,7 +1727,7 @@ function App() {
1617
1727
  const { exit } = useApp();
1618
1728
  const [section, setSection] = useState7(0);
1619
1729
  const [focus, setFocus] = useState7("nav");
1620
- useInput5((input, key) => {
1730
+ useInput6((input, key) => {
1621
1731
  if (focus === "nav") {
1622
1732
  if (key.upArrow) setSection((n) => (n - 1 + SECTIONS.length) % SECTIONS.length);
1623
1733
  else if (key.downArrow) setSection((n) => (n + 1) % SECTIONS.length);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.67.0",
3
+ "version": "0.69.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": {