@solongate/proxy 0.70.0 → 0.72.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.
@@ -84,17 +84,27 @@ async function request(method, path, opts = {}) {
84
84
  headers["Content-Type"] = "application/json";
85
85
  bodyInit = JSON.stringify(opts.body);
86
86
  }
87
+ const attempt = () => fetch(url, {
88
+ method,
89
+ headers,
90
+ body: bodyInit,
91
+ signal: AbortSignal.timeout(opts.timeoutMs ?? 15e3)
92
+ });
87
93
  let res;
88
94
  try {
89
- res = await fetch(url, {
90
- method,
91
- headers,
92
- body: bodyInit,
93
- signal: AbortSignal.timeout(opts.timeoutMs ?? 15e3)
94
- });
95
- } catch (err2) {
96
- const msg = err2 instanceof Error ? err2.message : String(err2);
97
- throw new ApiError(0, "NETWORK_ERROR", `Cannot reach SolonGate API: ${msg}`);
95
+ res = await attempt();
96
+ } catch (firstErr) {
97
+ if (method !== "GET") {
98
+ const msg = firstErr instanceof Error ? firstErr.message : String(firstErr);
99
+ throw new ApiError(0, "NETWORK_ERROR", `Cannot reach SolonGate API: ${msg}`);
100
+ }
101
+ await new Promise((r) => setTimeout(r, 800));
102
+ try {
103
+ res = await attempt();
104
+ } catch (err2) {
105
+ const msg = err2 instanceof Error ? err2.message : String(err2);
106
+ throw new ApiError(0, "NETWORK_ERROR", `Cannot reach SolonGate API: ${msg}`);
107
+ }
98
108
  }
99
109
  const text = await res.text().catch(() => "");
100
110
  let json = void 0;
package/dist/index.js CHANGED
@@ -6633,17 +6633,27 @@ async function request(method, path, opts = {}) {
6633
6633
  headers["Content-Type"] = "application/json";
6634
6634
  bodyInit = JSON.stringify(opts.body);
6635
6635
  }
6636
+ const attempt = () => fetch(url, {
6637
+ method,
6638
+ headers,
6639
+ body: bodyInit,
6640
+ signal: AbortSignal.timeout(opts.timeoutMs ?? 15e3)
6641
+ });
6636
6642
  let res;
6637
6643
  try {
6638
- res = await fetch(url, {
6639
- method,
6640
- headers,
6641
- body: bodyInit,
6642
- signal: AbortSignal.timeout(opts.timeoutMs ?? 15e3)
6643
- });
6644
- } catch (err2) {
6645
- const msg = err2 instanceof Error ? err2.message : String(err2);
6646
- throw new ApiError(0, "NETWORK_ERROR", `Cannot reach SolonGate API: ${msg}`);
6644
+ res = await attempt();
6645
+ } catch (firstErr) {
6646
+ if (method !== "GET") {
6647
+ const msg = firstErr instanceof Error ? firstErr.message : String(firstErr);
6648
+ throw new ApiError(0, "NETWORK_ERROR", `Cannot reach SolonGate API: ${msg}`);
6649
+ }
6650
+ await new Promise((r) => setTimeout(r, 800));
6651
+ try {
6652
+ res = await attempt();
6653
+ } catch (err2) {
6654
+ const msg = err2 instanceof Error ? err2.message : String(err2);
6655
+ throw new ApiError(0, "NETWORK_ERROR", `Cannot reach SolonGate API: ${msg}`);
6656
+ }
6647
6657
  }
6648
6658
  const text = await res.text().catch(() => "");
6649
6659
  let json = void 0;
@@ -7055,7 +7065,7 @@ function copyToClipboard(text) {
7055
7065
  }
7056
7066
  }
7057
7067
  process.stdout.write(`\x1B]52;c;${Buffer.from(text, "utf-8").toString("base64")}\x07`);
7058
- return "osc52";
7068
+ return null;
7059
7069
  }
7060
7070
  function cloudItem(e) {
7061
7071
  return {
@@ -7121,14 +7131,14 @@ function PaneTitle({ label, extra, width: width2 }) {
7121
7131
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: tail + "\u2500".repeat(Math.max(0, width2 - used)) })
7122
7132
  ] });
7123
7133
  }
7124
- function StreamLine({ e }) {
7134
+ function StreamLine({ e, loc }) {
7125
7135
  return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7126
7136
  /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
7127
7137
  "[",
7128
7138
  hhmmss(e.at),
7129
7139
  " "
7130
7140
  ] }),
7131
- /* @__PURE__ */ jsx2(Text2, { color: e.source === "local" ? theme.ok : "#4f6db8", children: e.source === "local" ? "LOC" : "CLD" }),
7141
+ /* @__PURE__ */ jsx2(Text2, { color: loc ? theme.ok : "#4f6db8", children: loc ? "LOC" : "CLD" }),
7132
7142
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "] " }),
7133
7143
  /* @__PURE__ */ jsx2(Text2, { color: decisionColor(e.decision), bold: e.decision !== "ALLOW", children: e.decision.padEnd(6) }),
7134
7144
  /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: truncate2(e.tool, 12).padEnd(13) }),
@@ -7160,6 +7170,9 @@ function LivePanel({ active: active2 }) {
7160
7170
  const seenRef = useRef2(/* @__PURE__ */ new Set());
7161
7171
  const lastLocalTs = useRef2(0);
7162
7172
  const pausedUntil = useRef2(0);
7173
+ const [frozen, setFrozen] = useState2(false);
7174
+ const frozenRef = useRef2(false);
7175
+ frozenRef.current = frozen;
7163
7176
  const pushLog = useCallback2((msg, level = "ok") => {
7164
7177
  setLog((prev) => [...prev.slice(-59), { ts: Date.now(), msg, level }]);
7165
7178
  }, []);
@@ -7175,8 +7188,9 @@ function LivePanel({ active: active2 }) {
7175
7188
  },
7176
7189
  [pushLog]
7177
7190
  );
7178
- const paused = () => Date.now() < pausedUntil.current;
7191
+ const paused = () => frozenRef.current || Date.now() < pausedUntil.current;
7179
7192
  const pollLocal = useCallback2(() => {
7193
+ if (frozenRef.current) return;
7180
7194
  const lines = tailLines(LOCAL_LOG);
7181
7195
  if (!lines.length) {
7182
7196
  setLocalOn((prev) => prev === null ? false : prev);
@@ -7242,6 +7256,7 @@ function LivePanel({ active: active2 }) {
7242
7256
  const t0 = Date.now();
7243
7257
  try {
7244
7258
  const fd = await api.audit.list({ limit: 50 });
7259
+ if (frozenRef.current) return;
7245
7260
  const ms = Date.now() - t0;
7246
7261
  setLat((l) => [...l, ms].slice(-240));
7247
7262
  const fresh = fd.entries.filter((e) => !seenRef.current.has(e.id));
@@ -7259,7 +7274,8 @@ function LivePanel({ active: active2 }) {
7259
7274
  const pollMedium = useCallback2(async () => {
7260
7275
  if (paused()) return;
7261
7276
  try {
7262
- setS(await api.stats.get());
7277
+ const sd = await api.stats.get();
7278
+ if (!frozenRef.current) setS(sd);
7263
7279
  } catch (e) {
7264
7280
  onApiError(e);
7265
7281
  }
@@ -7267,7 +7283,8 @@ function LivePanel({ active: active2 }) {
7267
7283
  const pollSlow = useCallback2(async () => {
7268
7284
  if (paused()) return;
7269
7285
  try {
7270
- setTsData(await api.stats.timeseries({ period: "24h" }));
7286
+ const td = await api.stats.timeseries({ period: "24h" });
7287
+ if (!frozenRef.current) setTsData(td);
7271
7288
  } catch (e) {
7272
7289
  onApiError(e);
7273
7290
  }
@@ -7313,19 +7330,23 @@ function LivePanel({ active: active2 }) {
7313
7330
  }, [detail]);
7314
7331
  const [tick, setTick] = useState2(0);
7315
7332
  useEffect2(() => {
7316
- if (!active2) return;
7333
+ if (!active2 || frozen) return;
7317
7334
  const t = setInterval(() => setTick((n) => n + 1), 500);
7318
7335
  return () => clearInterval(t);
7319
- }, [active2]);
7336
+ }, [active2, frozen]);
7320
7337
  const startRef = useRef2(Date.now());
7321
7338
  const cols = process.stdout.columns ?? 100;
7322
7339
  const rows = process.stdout.rows ?? 30;
7323
7340
  const ins = insights.data ?? {};
7324
7341
  const spin = SPIN[tick % SPIN.length];
7325
7342
  const nowMs = Date.now();
7326
- const mergedAll = [...cloudBuf, ...localBuf].sort((a, b) => a.at - b.at);
7327
- const filtered = filter === "all" ? mergedAll : mergedAll.filter((e) => e.source === filter);
7328
- const visibleDesc = filtered.slice(-STREAM_KEEP).reverse();
7343
+ const localKeys = /* @__PURE__ */ new Set();
7344
+ for (const e of localBuf) {
7345
+ const m = Math.round(e.at / 6e4);
7346
+ for (const d of [-1, 0, 1]) localKeys.add(`${e.tool}|${e.decision}|${m + d}`);
7347
+ }
7348
+ const cloudDeduped = cloudBuf.filter((e) => !localKeys.has(`${e.tool}|${e.decision}|${Math.round(e.at / 6e4)}`));
7349
+ const mergedAll = [...cloudDeduped, ...localBuf].sort((a, b) => a.at - b.at);
7329
7350
  const points = tsData?.timeseries ?? [];
7330
7351
  const nowMin = Math.floor(nowMs / 6e4);
7331
7352
  const minuteCounts = new Array(60).fill(0);
@@ -7382,6 +7403,11 @@ function LivePanel({ active: active2 }) {
7382
7403
  isMe: false
7383
7404
  }))
7384
7405
  ];
7406
+ const localSessIds = new Set(localSess.keys());
7407
+ if (ring?.session) localSessIds.add(ring.session);
7408
+ const isLoc = (e) => e.source === "local" || !!e.session && localSessIds.has(e.session);
7409
+ const filtered = filter === "all" ? mergedAll : mergedAll.filter((e) => filter === "local" ? isLoc(e) : !isLoc(e));
7410
+ const visibleDesc = filtered.slice(-STREAM_KEEP).reverse();
7385
7411
  const chartH = rows >= 36 ? 6 : 4;
7386
7412
  const colH = rows >= 32 ? 7 : 5;
7387
7413
  const streamRows = Math.max(4, rows - 5 - (1 + chartH) - colH);
@@ -7429,6 +7455,11 @@ function LivePanel({ active: active2 }) {
7429
7455
  } else if (input === "s" || key.leftArrow) setMode("stream");
7430
7456
  return;
7431
7457
  }
7458
+ if (input === " ") {
7459
+ setFrozen((f) => !f);
7460
+ return;
7461
+ }
7462
+ if (frozen) return;
7432
7463
  if (input === "f") {
7433
7464
  setFilter((f) => FILTERS[(FILTERS.indexOf(f) + 1) % FILTERS.length]);
7434
7465
  setScroll(0);
@@ -7443,14 +7474,15 @@ function LivePanel({ active: active2 }) {
7443
7474
  const dump = visibleDesc.map(
7444
7475
  (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}`
7445
7476
  ).join("\n");
7446
- let via = "file";
7447
7477
  try {
7448
7478
  mkdirSync3(dirname(DUMP), { recursive: true });
7449
7479
  writeFileSync3(DUMP, dump + "\n");
7450
7480
  } catch {
7451
7481
  }
7452
- via = copyToClipboard(dump);
7453
- setToast({ msg: `copied ${visibleDesc.length} lines via ${via} + ${DUMP}`, until: Date.now() + 5e3 });
7482
+ const via = copyToClipboard(dump);
7483
+ setToast(
7484
+ 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 }
7485
+ );
7454
7486
  }
7455
7487
  },
7456
7488
  { isActive: active2 }
@@ -7466,7 +7498,7 @@ function LivePanel({ active: active2 }) {
7466
7498
  const titleBar = /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7467
7499
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#1432A0", color: "#AAC8FA", bold: true, children: " SOLONGATE LIVE " }),
7468
7500
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: ` ${spin} up ${fmtUp(nowMs - startRef.current)} \xB7 ${hhmmss(nowMs)} \xB7 api ${latNow}ms ` }),
7469
- 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 " }),
7501
+ 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 " }),
7470
7502
  toast && nowMs < toast.until ? /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#123d1f", color: "#7bd88f", bold: true, children: ` ${toast.msg} ` }) : null
7471
7503
  ] });
7472
7504
  if (mode === "detail" && detail) {
@@ -7526,7 +7558,7 @@ function LivePanel({ active: active2 }) {
7526
7558
  /* @__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 }),
7527
7559
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height: tlRows, overflow: "hidden", children: [
7528
7560
  tl.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: detailCloud.length === 0 ? spin + " loading history\u2026" : "no entries" }) : null,
7529
- tl.map((e) => /* @__PURE__ */ jsx2(StreamLine, { e }, e.id))
7561
+ tl.map((e) => /* @__PURE__ */ jsx2(StreamLine, { e, loc: isLoc(e) }, e.id))
7530
7562
  ] }),
7531
7563
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7532
7564
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", bold: true, children: " SESSION " }),
@@ -7645,12 +7677,12 @@ function LivePanel({ active: active2 }) {
7645
7677
  spin,
7646
7678
  " awaiting traffic\u2026"
7647
7679
  ] }) : null,
7648
- windowed.map((e) => /* @__PURE__ */ jsx2(StreamLine, { e }, e.id))
7680
+ windowed.map((e) => /* @__PURE__ */ jsx2(StreamLine, { e, loc: isLoc(e) }, e.id))
7649
7681
  ] }),
7650
7682
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7651
7683
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", bold: true, children: " LIVE " }),
7652
7684
  /* @__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"} ` }),
7653
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: " f filter \xB7 s sessions \xB7 c copy \xB7 esc menu \xB7 q quit " })
7685
+ /* @__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 " })
7654
7686
  ] })
7655
7687
  ] });
7656
7688
  }
package/dist/tui/index.js CHANGED
@@ -220,17 +220,27 @@ async function request(method, path, opts = {}) {
220
220
  headers["Content-Type"] = "application/json";
221
221
  bodyInit = JSON.stringify(opts.body);
222
222
  }
223
+ const attempt = () => fetch(url, {
224
+ method,
225
+ headers,
226
+ body: bodyInit,
227
+ signal: AbortSignal.timeout(opts.timeoutMs ?? 15e3)
228
+ });
223
229
  let res;
224
230
  try {
225
- res = await fetch(url, {
226
- method,
227
- headers,
228
- body: bodyInit,
229
- signal: AbortSignal.timeout(opts.timeoutMs ?? 15e3)
230
- });
231
- } catch (err) {
232
- const msg = err instanceof Error ? err.message : String(err);
233
- throw new ApiError(0, "NETWORK_ERROR", `Cannot reach SolonGate API: ${msg}`);
231
+ res = await attempt();
232
+ } catch (firstErr) {
233
+ if (method !== "GET") {
234
+ const msg = firstErr instanceof Error ? firstErr.message : String(firstErr);
235
+ throw new ApiError(0, "NETWORK_ERROR", `Cannot reach SolonGate API: ${msg}`);
236
+ }
237
+ await new Promise((r) => setTimeout(r, 800));
238
+ try {
239
+ res = await attempt();
240
+ } catch (err) {
241
+ const msg = err instanceof Error ? err.message : String(err);
242
+ throw new ApiError(0, "NETWORK_ERROR", `Cannot reach SolonGate API: ${msg}`);
243
+ }
234
244
  }
235
245
  const text = await res.text().catch(() => "");
236
246
  let json = void 0;
@@ -480,7 +490,7 @@ function copyToClipboard(text) {
480
490
  }
481
491
  }
482
492
  process.stdout.write(`\x1B]52;c;${Buffer.from(text, "utf-8").toString("base64")}\x07`);
483
- return "osc52";
493
+ return null;
484
494
  }
485
495
  function cloudItem(e) {
486
496
  return {
@@ -546,14 +556,14 @@ function PaneTitle({ label, extra, width }) {
546
556
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: tail + "\u2500".repeat(Math.max(0, width - used)) })
547
557
  ] });
548
558
  }
549
- function StreamLine({ e }) {
559
+ function StreamLine({ e, loc }) {
550
560
  return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
551
561
  /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
552
562
  "[",
553
563
  hhmmss(e.at),
554
564
  " "
555
565
  ] }),
556
- /* @__PURE__ */ jsx2(Text2, { color: e.source === "local" ? theme.ok : "#4f6db8", children: e.source === "local" ? "LOC" : "CLD" }),
566
+ /* @__PURE__ */ jsx2(Text2, { color: loc ? theme.ok : "#4f6db8", children: loc ? "LOC" : "CLD" }),
557
567
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "] " }),
558
568
  /* @__PURE__ */ jsx2(Text2, { color: decisionColor(e.decision), bold: e.decision !== "ALLOW", children: e.decision.padEnd(6) }),
559
569
  /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: truncate(e.tool, 12).padEnd(13) }),
@@ -586,6 +596,9 @@ function LivePanel({ active: active2 }) {
586
596
  const seenRef = useRef2(/* @__PURE__ */ new Set());
587
597
  const lastLocalTs = useRef2(0);
588
598
  const pausedUntil = useRef2(0);
599
+ const [frozen, setFrozen] = useState2(false);
600
+ const frozenRef = useRef2(false);
601
+ frozenRef.current = frozen;
589
602
  const pushLog = useCallback2((msg, level = "ok") => {
590
603
  setLog((prev) => [...prev.slice(-59), { ts: Date.now(), msg, level }]);
591
604
  }, []);
@@ -601,8 +614,9 @@ function LivePanel({ active: active2 }) {
601
614
  },
602
615
  [pushLog]
603
616
  );
604
- const paused = () => Date.now() < pausedUntil.current;
617
+ const paused = () => frozenRef.current || Date.now() < pausedUntil.current;
605
618
  const pollLocal = useCallback2(() => {
619
+ if (frozenRef.current) return;
606
620
  const lines = tailLines(LOCAL_LOG);
607
621
  if (!lines.length) {
608
622
  setLocalOn((prev) => prev === null ? false : prev);
@@ -668,6 +682,7 @@ function LivePanel({ active: active2 }) {
668
682
  const t0 = Date.now();
669
683
  try {
670
684
  const fd = await api.audit.list({ limit: 50 });
685
+ if (frozenRef.current) return;
671
686
  const ms = Date.now() - t0;
672
687
  setLat((l) => [...l, ms].slice(-240));
673
688
  const fresh = fd.entries.filter((e) => !seenRef.current.has(e.id));
@@ -685,7 +700,8 @@ function LivePanel({ active: active2 }) {
685
700
  const pollMedium = useCallback2(async () => {
686
701
  if (paused()) return;
687
702
  try {
688
- setS(await api.stats.get());
703
+ const sd = await api.stats.get();
704
+ if (!frozenRef.current) setS(sd);
689
705
  } catch (e) {
690
706
  onApiError(e);
691
707
  }
@@ -693,7 +709,8 @@ function LivePanel({ active: active2 }) {
693
709
  const pollSlow = useCallback2(async () => {
694
710
  if (paused()) return;
695
711
  try {
696
- setTsData(await api.stats.timeseries({ period: "24h" }));
712
+ const td = await api.stats.timeseries({ period: "24h" });
713
+ if (!frozenRef.current) setTsData(td);
697
714
  } catch (e) {
698
715
  onApiError(e);
699
716
  }
@@ -739,19 +756,23 @@ function LivePanel({ active: active2 }) {
739
756
  }, [detail]);
740
757
  const [tick, setTick] = useState2(0);
741
758
  useEffect2(() => {
742
- if (!active2) return;
759
+ if (!active2 || frozen) return;
743
760
  const t = setInterval(() => setTick((n) => n + 1), 500);
744
761
  return () => clearInterval(t);
745
- }, [active2]);
762
+ }, [active2, frozen]);
746
763
  const startRef = useRef2(Date.now());
747
764
  const cols = process.stdout.columns ?? 100;
748
765
  const rows = process.stdout.rows ?? 30;
749
766
  const ins = insights.data ?? {};
750
767
  const spin = SPIN[tick % SPIN.length];
751
768
  const nowMs = Date.now();
752
- const mergedAll = [...cloudBuf, ...localBuf].sort((a, b) => a.at - b.at);
753
- const filtered = filter === "all" ? mergedAll : mergedAll.filter((e) => e.source === filter);
754
- const visibleDesc = filtered.slice(-STREAM_KEEP).reverse();
769
+ const localKeys = /* @__PURE__ */ new Set();
770
+ for (const e of localBuf) {
771
+ const m = Math.round(e.at / 6e4);
772
+ for (const d of [-1, 0, 1]) localKeys.add(`${e.tool}|${e.decision}|${m + d}`);
773
+ }
774
+ const cloudDeduped = cloudBuf.filter((e) => !localKeys.has(`${e.tool}|${e.decision}|${Math.round(e.at / 6e4)}`));
775
+ const mergedAll = [...cloudDeduped, ...localBuf].sort((a, b) => a.at - b.at);
755
776
  const points = tsData?.timeseries ?? [];
756
777
  const nowMin = Math.floor(nowMs / 6e4);
757
778
  const minuteCounts = new Array(60).fill(0);
@@ -808,6 +829,11 @@ function LivePanel({ active: active2 }) {
808
829
  isMe: false
809
830
  }))
810
831
  ];
832
+ const localSessIds = new Set(localSess.keys());
833
+ if (ring?.session) localSessIds.add(ring.session);
834
+ const isLoc = (e) => e.source === "local" || !!e.session && localSessIds.has(e.session);
835
+ const filtered = filter === "all" ? mergedAll : mergedAll.filter((e) => filter === "local" ? isLoc(e) : !isLoc(e));
836
+ const visibleDesc = filtered.slice(-STREAM_KEEP).reverse();
811
837
  const chartH = rows >= 36 ? 6 : 4;
812
838
  const colH = rows >= 32 ? 7 : 5;
813
839
  const streamRows = Math.max(4, rows - 5 - (1 + chartH) - colH);
@@ -855,6 +881,11 @@ function LivePanel({ active: active2 }) {
855
881
  } else if (input === "s" || key.leftArrow) setMode("stream");
856
882
  return;
857
883
  }
884
+ if (input === " ") {
885
+ setFrozen((f) => !f);
886
+ return;
887
+ }
888
+ if (frozen) return;
858
889
  if (input === "f") {
859
890
  setFilter((f) => FILTERS[(FILTERS.indexOf(f) + 1) % FILTERS.length]);
860
891
  setScroll(0);
@@ -869,14 +900,15 @@ function LivePanel({ active: active2 }) {
869
900
  const dump = visibleDesc.map(
870
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}`
871
902
  ).join("\n");
872
- let via = "file";
873
903
  try {
874
904
  mkdirSync(dirname(DUMP), { recursive: true });
875
905
  writeFileSync(DUMP, dump + "\n");
876
906
  } catch {
877
907
  }
878
- via = copyToClipboard(dump);
879
- setToast({ msg: `copied ${visibleDesc.length} lines via ${via} + ${DUMP}`, until: Date.now() + 5e3 });
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
+ );
880
912
  }
881
913
  },
882
914
  { isActive: active2 }
@@ -892,7 +924,7 @@ function LivePanel({ active: active2 }) {
892
924
  const titleBar = /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
893
925
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#1432A0", color: "#AAC8FA", bold: true, children: " SOLONGATE LIVE " }),
894
926
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: ` ${spin} up ${fmtUp(nowMs - startRef.current)} \xB7 ${hhmmss(nowMs)} \xB7 api ${latNow}ms ` }),
895
- 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 " }),
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 " }),
896
928
  toast && nowMs < toast.until ? /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#123d1f", color: "#7bd88f", bold: true, children: ` ${toast.msg} ` }) : null
897
929
  ] });
898
930
  if (mode === "detail" && detail) {
@@ -952,7 +984,7 @@ function LivePanel({ active: active2 }) {
952
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 }),
953
985
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height: tlRows, overflow: "hidden", children: [
954
986
  tl.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: detailCloud.length === 0 ? spin + " loading history\u2026" : "no entries" }) : null,
955
- tl.map((e) => /* @__PURE__ */ jsx2(StreamLine, { e }, e.id))
987
+ tl.map((e) => /* @__PURE__ */ jsx2(StreamLine, { e, loc: isLoc(e) }, e.id))
956
988
  ] }),
957
989
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
958
990
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", bold: true, children: " SESSION " }),
@@ -1071,12 +1103,12 @@ function LivePanel({ active: active2 }) {
1071
1103
  spin,
1072
1104
  " awaiting traffic\u2026"
1073
1105
  ] }) : null,
1074
- windowed.map((e) => /* @__PURE__ */ jsx2(StreamLine, { e }, e.id))
1106
+ windowed.map((e) => /* @__PURE__ */ jsx2(StreamLine, { e, loc: isLoc(e) }, e.id))
1075
1107
  ] }),
1076
1108
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
1077
1109
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", bold: true, children: " LIVE " }),
1078
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"} ` }),
1079
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: " f filter \xB7 s sessions \xB7 c copy \xB7 esc menu \xB7 q quit " })
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 " })
1080
1112
  ] })
1081
1113
  ] });
1082
1114
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.70.0",
3
+ "version": "0.72.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": {