@solongate/proxy 0.67.0 → 0.68.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/index.js CHANGED
@@ -7019,7 +7019,7 @@ var init_hooks = __esm({
7019
7019
  });
7020
7020
 
7021
7021
  // src/tui/panels/Live.tsx
7022
- import { Box as Box2, Text as Text2 } from "ink";
7022
+ import { Box as Box2, Text as Text2, useInput } from "ink";
7023
7023
  import { closeSync, openSync, readSync, statSync } from "fs";
7024
7024
  import { homedir as homedir3 } from "os";
7025
7025
  import { join as join5 } from "path";
@@ -7112,6 +7112,15 @@ function LivePanel({ active: active2 }) {
7112
7112
  const seenRef = useRef2(/* @__PURE__ */ new Set());
7113
7113
  const lastLocalTs = useRef2(0);
7114
7114
  const pausedUntil = useRef2(0);
7115
+ const [frozen, setFrozen] = useState2(false);
7116
+ const frozenRef = useRef2(false);
7117
+ frozenRef.current = frozen;
7118
+ useInput(
7119
+ (input) => {
7120
+ if (input === "p") setFrozen((f) => !f);
7121
+ },
7122
+ { isActive: active2 }
7123
+ );
7115
7124
  const pushLog = useCallback2((msg, level = "ok") => {
7116
7125
  setLog((prev) => [...prev.slice(-59), { ts: Date.now(), msg, level }]);
7117
7126
  }, []);
@@ -7127,8 +7136,9 @@ function LivePanel({ active: active2 }) {
7127
7136
  },
7128
7137
  [pushLog]
7129
7138
  );
7130
- const paused = () => Date.now() < pausedUntil.current;
7139
+ const paused = () => frozenRef.current || Date.now() < pausedUntil.current;
7131
7140
  const pollLocal = useCallback2(() => {
7141
+ if (frozenRef.current) return;
7132
7142
  const lines = tailLines(LOCAL_LOG);
7133
7143
  if (!lines.length) {
7134
7144
  setLocalOn((prev) => prev === null ? false : prev);
@@ -7146,10 +7156,12 @@ function LivePanel({ active: active2 }) {
7146
7156
  at,
7147
7157
  tool: j.tool ?? "?",
7148
7158
  decision: j.decision ?? "ALLOW",
7149
- permission: "",
7159
+ permission: (j.permission ?? "").slice(0, 4),
7150
7160
  detail: (j.arguments ? JSON.stringify(j.arguments) : j.reason ?? "").replace(/\s+/g, " "),
7151
7161
  dlp: !!j.dlp,
7152
- source: "local"
7162
+ source: "local",
7163
+ session: j.session_id,
7164
+ agent: j.agent_name
7153
7165
  });
7154
7166
  } catch {
7155
7167
  }
@@ -7247,24 +7259,37 @@ function LivePanel({ active: active2 }) {
7247
7259
  }, 3e4, active2);
7248
7260
  const [tick, setTick] = useState2(0);
7249
7261
  useEffect2(() => {
7250
- if (!active2) return;
7262
+ if (!active2 || frozen) return;
7251
7263
  const t = setInterval(() => setTick((n) => n + 1), 200);
7252
7264
  return () => clearInterval(t);
7253
- }, [active2]);
7265
+ }, [active2, frozen]);
7254
7266
  const startRef = useRef2(Date.now());
7255
7267
  const cols = process.stdout.columns ?? 100;
7256
7268
  const rows = process.stdout.rows ?? 30;
7257
7269
  const ins = insights.data ?? {};
7258
7270
  const spin = SPIN[tick % SPIN.length];
7259
7271
  const points = tsData?.timeseries ?? [];
7260
- const traffic = points.map((p) => p.total);
7261
- const trafficHot = points.map((p) => p.denied > 0);
7272
+ const mergedAll = [...cloudBuf, ...localBuf].sort((a, b) => a.at - b.at);
7273
+ const nowMs = Date.now();
7274
+ const minuteCounts = new Array(60).fill(0);
7275
+ const minuteHot = new Array(60).fill(false);
7276
+ for (const e of mergedAll) {
7277
+ const idx = 59 - Math.floor((nowMs - e.at) / 6e4);
7278
+ if (idx >= 0 && idx < 60) {
7279
+ minuteCounts[idx]++;
7280
+ if (e.decision !== "ALLOW") minuteHot[idx] = true;
7281
+ }
7282
+ }
7283
+ const localTraffic = localOn === true && minuteCounts.some((c2) => c2 > 0);
7284
+ const traffic = localTraffic ? minuteCounts : points.map((p) => p.total);
7285
+ const trafficHot = localTraffic ? minuteHot : points.map((p) => p.denied > 0);
7286
+ const trafficLabel = localTraffic ? "calls/min \xB7 60m \xB7 local+cloud" : "24h \xB7 cloud";
7262
7287
  const rl = ins.layers?.rateLimit;
7263
7288
  const dl = ins.layers?.dlp;
7264
7289
  const minuteNow = (ins.activity?.minute ?? []).slice(-1)[0]?.count ?? 0;
7265
7290
  const dlpBars = (ins.dlpByPattern ?? []).slice(0, 2);
7266
7291
  const maxDlpBar = dlpBars[0]?.count ?? 1;
7267
- const merged = [...cloudBuf, ...localBuf].sort((a, b) => a.at - b.at);
7292
+ const merged = mergedAll;
7268
7293
  const denials = merged.filter((e) => e.decision !== "ALLOW");
7269
7294
  const lastDeny = denials[denials.length - 1];
7270
7295
  const toolCounts = /* @__PURE__ */ new Map();
@@ -7277,6 +7302,17 @@ function LivePanel({ active: active2 }) {
7277
7302
  const backingOff = Date.now() < pausedUntil.current;
7278
7303
  const sess = (sessions.data?.agents ?? []).slice().sort((a, b) => Date.parse(b.last_seen_at) - Date.parse(a.last_seen_at));
7279
7304
  const sessCounts = sessions.data?.counts;
7305
+ const localSess = /* @__PURE__ */ new Map();
7306
+ for (const e of localBuf) {
7307
+ if (!e.session) continue;
7308
+ const cur = localSess.get(e.session) ?? { agent: e.agent ?? "local agent", calls: 0, denies: 0, lastAt: 0 };
7309
+ cur.calls++;
7310
+ if (e.decision !== "ALLOW") cur.denies++;
7311
+ cur.lastAt = Math.max(cur.lastAt, e.at);
7312
+ if (e.agent) cur.agent = e.agent;
7313
+ localSess.set(e.session, cur);
7314
+ }
7315
+ const localSessList = [...localSess.entries()].sort((a, b) => b[1].lastAt - a[1].lastAt);
7280
7316
  const chartH = rows >= 36 ? 6 : 4;
7281
7317
  const colH = rows >= 32 ? 6 : 5;
7282
7318
  const streamRows = Math.max(4, rows - 6 - chartH - colH - (localOn === false ? 1 : 0));
@@ -7290,7 +7326,7 @@ function LivePanel({ active: active2 }) {
7290
7326
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7291
7327
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#1432A0", color: "#AAC8FA", bold: true, children: " SOLONGATE LIVE " }),
7292
7328
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: ` ${spin} up ${fmtUp(Date.now() - startRef.current)} \xB7 ${hhmmss(Date.now())} \xB7 api ${latNow}ms ` }),
7293
- 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 " })
7329
+ frozen ? /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#3d2a12", color: "#ffb454", bold: true, children: " PAUSED \xB7 copy freely \xB7 p 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 " })
7294
7330
  ] }),
7295
7331
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7296
7332
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "\u2502 calls " }),
@@ -7311,7 +7347,7 @@ function LivePanel({ active: active2 }) {
7311
7347
  ] }),
7312
7348
  /* @__PURE__ */ jsxs2(Box2, { children: [
7313
7349
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: leftW, marginRight: 2, children: [
7314
- /* @__PURE__ */ jsx2(PaneTitle, { label: "TRAFFIC", extra: `24h \xB7 peak ${Math.max(0, ...traffic)} \xB7 red = had denials`, width: leftW }),
7350
+ /* @__PURE__ */ jsx2(PaneTitle, { label: "TRAFFIC", extra: `${trafficLabel} \xB7 peak ${Math.max(0, ...traffic)} \xB7 red = denials`, width: leftW }),
7315
7351
  /* @__PURE__ */ jsx2(ColumnChart, { series: traffic, hot: trafficHot, height: chartH, width: leftW, color: theme.accent })
7316
7352
  ] }),
7317
7353
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: rightW, children: [
@@ -7336,7 +7372,7 @@ function LivePanel({ active: active2 }) {
7336
7372
  " patterns armed"
7337
7373
  ] })
7338
7374
  ] }),
7339
- dlpBars.length ? dlpBars.map((d) => /* @__PURE__ */ jsx2(HBar, { label: truncate2(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" }),
7375
+ dlpBars.length ? dlpBars.map((d) => /* @__PURE__ */ jsx2(HBar, { label: truncate2(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" }),
7340
7376
  ring ? /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7341
7377
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "GUARD".padEnd(10) }),
7342
7378
  /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: "local \u2713" }),
@@ -7344,20 +7380,37 @@ function LivePanel({ active: active2 }) {
7344
7380
  ] }) : null
7345
7381
  ] }),
7346
7382
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, marginRight: 2, children: [
7347
- /* @__PURE__ */ jsx2(PaneTitle, { label: "SESSIONS", extra: sessCounts ? `${sessCounts.active} live` : "", width: colW }),
7348
- ring?.session ? /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7349
- /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: "\u25CF " }),
7350
- /* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, bold: true, children: "this machine" }),
7351
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " " + truncate2(ring.session, 12) + " \xB7 guarded" })
7352
- ] }) : null,
7353
- sess.length === 0 && !ring ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "scanning\u2026" }) : null,
7354
- sess.slice(0, colH - 1 - (ring ? 1 : 0)).map((a) => {
7383
+ /* @__PURE__ */ jsx2(PaneTitle, { label: "SESSIONS", extra: `${localSessList.length} local \xB7 ${sessCounts?.active ?? 0} cloud live`, width: colW }),
7384
+ localSessList.length === 0 && sess.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "scanning\u2026" }) : null,
7385
+ localSessList.slice(0, Math.max(1, colH - 2)).map(([id, v]) => {
7386
+ const fresh = nowMs - v.lastAt < 9e4;
7387
+ const isMe = ring?.session === id;
7388
+ return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7389
+ /* @__PURE__ */ jsxs2(Text2, { color: fresh ? theme.ok : theme.dim, children: [
7390
+ fresh ? "\u25CF" : "\u25CB",
7391
+ " "
7392
+ ] }),
7393
+ /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: "LOC " }),
7394
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, bold: isMe, children: truncate2(isMe ? "this machine" : v.agent, 12).padEnd(13) }),
7395
+ /* @__PURE__ */ jsxs2(Text2, { children: [
7396
+ String(v.calls).padStart(4),
7397
+ "c "
7398
+ ] }),
7399
+ /* @__PURE__ */ jsxs2(Text2, { color: v.denies ? theme.bad : theme.dim, children: [
7400
+ String(v.denies).padStart(3),
7401
+ "d "
7402
+ ] }),
7403
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ago(v.lastAt) })
7404
+ ] }, id);
7405
+ }),
7406
+ sess.slice(0, Math.max(0, colH - 1 - Math.min(localSessList.length, Math.max(1, colH - 2)))).map((a) => {
7355
7407
  const dot = sessionDot(a.status);
7356
7408
  return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7357
7409
  /* @__PURE__ */ jsxs2(Text2, { color: dot.color, children: [
7358
7410
  dot.ch,
7359
7411
  " "
7360
7412
  ] }),
7413
+ /* @__PURE__ */ jsx2(Text2, { color: "#4f6db8", children: "CLD " }),
7361
7414
  /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: truncate2(a.agent_name ?? a.session_id, 12).padEnd(13) }),
7362
7415
  /* @__PURE__ */ jsxs2(Text2, { children: [
7363
7416
  String(a.total_calls).padStart(4),
@@ -7409,7 +7462,7 @@ function LivePanel({ active: active2 }) {
7409
7462
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7410
7463
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", bold: true, children: " LIVE " }),
7411
7464
  /* @__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} ` }),
7412
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: " esc menu \xB7 q quit " })
7465
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: " p pause/copy \xB7 esc menu \xB7 q quit " })
7413
7466
  ] })
7414
7467
  ] });
7415
7468
  }
@@ -7437,7 +7490,7 @@ var init_Live = __esm({
7437
7490
  });
7438
7491
 
7439
7492
  // src/tui/panels/Policies.tsx
7440
- import { Box as Box3, Text as Text3, useInput } from "ink";
7493
+ import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
7441
7494
  import TextInput from "ink-text-input";
7442
7495
  import { useEffect as useEffect3, useState as useState3 } from "react";
7443
7496
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
@@ -7507,7 +7560,7 @@ function PoliciesPanel({ focused }) {
7507
7560
  setDirty(false);
7508
7561
  setStatus("discarded");
7509
7562
  };
7510
- useInput(
7563
+ useInput2(
7511
7564
  (input, key) => {
7512
7565
  if (view === "list") {
7513
7566
  if (key.upArrow) setPi((n) => Math.max(0, n - 1));
@@ -7682,7 +7735,7 @@ var init_Policies = __esm({
7682
7735
  });
7683
7736
 
7684
7737
  // src/tui/panels/RateLimit.tsx
7685
- import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
7738
+ import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
7686
7739
  import { useEffect as useEffect4, useState as useState4 } from "react";
7687
7740
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
7688
7741
  function ColoredSpark({ values, limit, width: width2 = 60 }) {
@@ -7730,7 +7783,7 @@ function RateLimitPanel({ focused }) {
7730
7783
  setStatus("\u2717 " + (e instanceof Error ? e.message : String(e)));
7731
7784
  }
7732
7785
  };
7733
- useInput2(
7786
+ useInput3(
7734
7787
  (input, key) => {
7735
7788
  const step = key.shift ? 10 : 1;
7736
7789
  if (key.upArrow) setFi((n) => (n - 1 + FIELDS2.length) % FIELDS2.length);
@@ -7822,7 +7875,7 @@ var init_RateLimit = __esm({
7822
7875
  });
7823
7876
 
7824
7877
  // src/tui/panels/Dlp.tsx
7825
- import { Box as Box5, Text as Text5, useInput as useInput3 } from "ink";
7878
+ import { Box as Box5, Text as Text5, useInput as useInput4 } from "ink";
7826
7879
  import TextInput2 from "ink-text-input";
7827
7880
  import { useEffect as useEffect5, useState as useState5 } from "react";
7828
7881
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
@@ -7866,7 +7919,7 @@ function DlpPanel({ focused }) {
7866
7919
  };
7867
7920
  const customCount = dlp?.custom.length ?? 0;
7868
7921
  const total = available.length + customCount;
7869
- useInput3(
7922
+ useInput4(
7870
7923
  (input, key) => {
7871
7924
  if (!dlp) return;
7872
7925
  if (key.upArrow) setSel((n) => Math.max(0, n - 1));
@@ -8048,7 +8101,7 @@ var init_Stats = __esm({
8048
8101
  });
8049
8102
 
8050
8103
  // src/tui/panels/Audit.tsx
8051
- import { Box as Box7, Text as Text7, useInput as useInput4 } from "ink";
8104
+ import { Box as Box7, Text as Text7, useInput as useInput5 } from "ink";
8052
8105
  import TextInput3 from "ink-text-input";
8053
8106
  import { useState as useState6 } from "react";
8054
8107
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
@@ -8077,7 +8130,7 @@ function AuditPanel({ active: active2, focused }) {
8077
8130
  () => view === "detail" && current?.session_id ? api.audit.list({ session_id: current.session_id, limit: 40 }) : Promise.resolve(null),
8078
8131
  [view, current?.session_id]
8079
8132
  );
8080
- useInput4(
8133
+ useInput5(
8081
8134
  (input, key) => {
8082
8135
  if (view === "detail") {
8083
8136
  if (key.leftArrow || key.escape) setView("list");
@@ -8227,7 +8280,7 @@ var init_Audit = __esm({
8227
8280
  });
8228
8281
 
8229
8282
  // src/tui/App.tsx
8230
- import { Box as Box8, Text as Text8, useApp, useInput as useInput5 } from "ink";
8283
+ import { Box as Box8, Text as Text8, useApp, useInput as useInput6 } from "ink";
8231
8284
  import { useState as useState7 } from "react";
8232
8285
  import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
8233
8286
  function LiveHint() {
@@ -8259,7 +8312,7 @@ function App() {
8259
8312
  const { exit } = useApp();
8260
8313
  const [section, setSection] = useState7(0);
8261
8314
  const [focus, setFocus] = useState7("nav");
8262
- useInput5((input, key) => {
8315
+ useInput6((input, key) => {
8263
8316
  if (focus === "nav") {
8264
8317
  if (key.upArrow) setSection((n) => (n - 1 + SECTIONS.length) % SECTIONS.length);
8265
8318
  else if (key.downArrow) setSection((n) => (n + 1) % SECTIONS.length);
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,7 +125,7 @@ function KeyHints({ hints }) {
125
125
  }
126
126
 
127
127
  // src/tui/panels/Live.tsx
128
- import { Box as Box2, Text as Text2 } from "ink";
128
+ import { Box as Box2, Text as Text2, useInput } from "ink";
129
129
  import { closeSync, openSync, readSync, statSync } from "fs";
130
130
  import { homedir as homedir2 } from "os";
131
131
  import { join as join2 } from "path";
@@ -534,6 +534,15 @@ function LivePanel({ active: active2 }) {
534
534
  const seenRef = useRef2(/* @__PURE__ */ new Set());
535
535
  const lastLocalTs = useRef2(0);
536
536
  const pausedUntil = useRef2(0);
537
+ const [frozen, setFrozen] = useState2(false);
538
+ const frozenRef = useRef2(false);
539
+ frozenRef.current = frozen;
540
+ useInput(
541
+ (input) => {
542
+ if (input === "p") setFrozen((f) => !f);
543
+ },
544
+ { isActive: active2 }
545
+ );
537
546
  const pushLog = useCallback2((msg, level = "ok") => {
538
547
  setLog((prev) => [...prev.slice(-59), { ts: Date.now(), msg, level }]);
539
548
  }, []);
@@ -549,8 +558,9 @@ function LivePanel({ active: active2 }) {
549
558
  },
550
559
  [pushLog]
551
560
  );
552
- const paused = () => Date.now() < pausedUntil.current;
561
+ const paused = () => frozenRef.current || Date.now() < pausedUntil.current;
553
562
  const pollLocal = useCallback2(() => {
563
+ if (frozenRef.current) return;
554
564
  const lines = tailLines(LOCAL_LOG);
555
565
  if (!lines.length) {
556
566
  setLocalOn((prev) => prev === null ? false : prev);
@@ -568,10 +578,12 @@ function LivePanel({ active: active2 }) {
568
578
  at,
569
579
  tool: j.tool ?? "?",
570
580
  decision: j.decision ?? "ALLOW",
571
- permission: "",
581
+ permission: (j.permission ?? "").slice(0, 4),
572
582
  detail: (j.arguments ? JSON.stringify(j.arguments) : j.reason ?? "").replace(/\s+/g, " "),
573
583
  dlp: !!j.dlp,
574
- source: "local"
584
+ source: "local",
585
+ session: j.session_id,
586
+ agent: j.agent_name
575
587
  });
576
588
  } catch {
577
589
  }
@@ -669,24 +681,37 @@ function LivePanel({ active: active2 }) {
669
681
  }, 3e4, active2);
670
682
  const [tick, setTick] = useState2(0);
671
683
  useEffect2(() => {
672
- if (!active2) return;
684
+ if (!active2 || frozen) return;
673
685
  const t = setInterval(() => setTick((n) => n + 1), 200);
674
686
  return () => clearInterval(t);
675
- }, [active2]);
687
+ }, [active2, frozen]);
676
688
  const startRef = useRef2(Date.now());
677
689
  const cols = process.stdout.columns ?? 100;
678
690
  const rows = process.stdout.rows ?? 30;
679
691
  const ins = insights.data ?? {};
680
692
  const spin = SPIN[tick % SPIN.length];
681
693
  const points = tsData?.timeseries ?? [];
682
- const traffic = points.map((p) => p.total);
683
- const trafficHot = points.map((p) => p.denied > 0);
694
+ const mergedAll = [...cloudBuf, ...localBuf].sort((a, b) => a.at - b.at);
695
+ const nowMs = Date.now();
696
+ const minuteCounts = new Array(60).fill(0);
697
+ const minuteHot = new Array(60).fill(false);
698
+ for (const e of mergedAll) {
699
+ const idx = 59 - Math.floor((nowMs - e.at) / 6e4);
700
+ if (idx >= 0 && idx < 60) {
701
+ minuteCounts[idx]++;
702
+ if (e.decision !== "ALLOW") minuteHot[idx] = true;
703
+ }
704
+ }
705
+ const localTraffic = localOn === true && minuteCounts.some((c2) => c2 > 0);
706
+ const traffic = localTraffic ? minuteCounts : points.map((p) => p.total);
707
+ const trafficHot = localTraffic ? minuteHot : points.map((p) => p.denied > 0);
708
+ const trafficLabel = localTraffic ? "calls/min \xB7 60m \xB7 local+cloud" : "24h \xB7 cloud";
684
709
  const rl = ins.layers?.rateLimit;
685
710
  const dl = ins.layers?.dlp;
686
711
  const minuteNow = (ins.activity?.minute ?? []).slice(-1)[0]?.count ?? 0;
687
712
  const dlpBars = (ins.dlpByPattern ?? []).slice(0, 2);
688
713
  const maxDlpBar = dlpBars[0]?.count ?? 1;
689
- const merged = [...cloudBuf, ...localBuf].sort((a, b) => a.at - b.at);
714
+ const merged = mergedAll;
690
715
  const denials = merged.filter((e) => e.decision !== "ALLOW");
691
716
  const lastDeny = denials[denials.length - 1];
692
717
  const toolCounts = /* @__PURE__ */ new Map();
@@ -699,6 +724,17 @@ function LivePanel({ active: active2 }) {
699
724
  const backingOff = Date.now() < pausedUntil.current;
700
725
  const sess = (sessions.data?.agents ?? []).slice().sort((a, b) => Date.parse(b.last_seen_at) - Date.parse(a.last_seen_at));
701
726
  const sessCounts = sessions.data?.counts;
727
+ const localSess = /* @__PURE__ */ new Map();
728
+ for (const e of localBuf) {
729
+ if (!e.session) continue;
730
+ const cur = localSess.get(e.session) ?? { agent: e.agent ?? "local agent", calls: 0, denies: 0, lastAt: 0 };
731
+ cur.calls++;
732
+ if (e.decision !== "ALLOW") cur.denies++;
733
+ cur.lastAt = Math.max(cur.lastAt, e.at);
734
+ if (e.agent) cur.agent = e.agent;
735
+ localSess.set(e.session, cur);
736
+ }
737
+ const localSessList = [...localSess.entries()].sort((a, b) => b[1].lastAt - a[1].lastAt);
702
738
  const chartH = rows >= 36 ? 6 : 4;
703
739
  const colH = rows >= 32 ? 6 : 5;
704
740
  const streamRows = Math.max(4, rows - 6 - chartH - colH - (localOn === false ? 1 : 0));
@@ -712,7 +748,7 @@ function LivePanel({ active: active2 }) {
712
748
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
713
749
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#1432A0", color: "#AAC8FA", bold: true, children: " SOLONGATE LIVE " }),
714
750
  /* @__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 " })
751
+ frozen ? /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#3d2a12", color: "#ffb454", bold: true, children: " PAUSED \xB7 copy freely \xB7 p 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 " })
716
752
  ] }),
717
753
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
718
754
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "\u2502 calls " }),
@@ -733,7 +769,7 @@ function LivePanel({ active: active2 }) {
733
769
  ] }),
734
770
  /* @__PURE__ */ jsxs2(Box2, { children: [
735
771
  /* @__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 }),
772
+ /* @__PURE__ */ jsx2(PaneTitle, { label: "TRAFFIC", extra: `${trafficLabel} \xB7 peak ${Math.max(0, ...traffic)} \xB7 red = denials`, width: leftW }),
737
773
  /* @__PURE__ */ jsx2(ColumnChart, { series: traffic, hot: trafficHot, height: chartH, width: leftW, color: theme.accent })
738
774
  ] }),
739
775
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: rightW, children: [
@@ -758,7 +794,7 @@ function LivePanel({ active: active2 }) {
758
794
  " patterns armed"
759
795
  ] })
760
796
  ] }),
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" }),
797
+ 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" }),
762
798
  ring ? /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
763
799
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "GUARD".padEnd(10) }),
764
800
  /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: "local \u2713" }),
@@ -766,20 +802,37 @@ function LivePanel({ active: active2 }) {
766
802
  ] }) : null
767
803
  ] }),
768
804
  /* @__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) => {
805
+ /* @__PURE__ */ jsx2(PaneTitle, { label: "SESSIONS", extra: `${localSessList.length} local \xB7 ${sessCounts?.active ?? 0} cloud live`, width: colW }),
806
+ localSessList.length === 0 && sess.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "scanning\u2026" }) : null,
807
+ localSessList.slice(0, Math.max(1, colH - 2)).map(([id, v]) => {
808
+ const fresh = nowMs - v.lastAt < 9e4;
809
+ const isMe = ring?.session === id;
810
+ return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
811
+ /* @__PURE__ */ jsxs2(Text2, { color: fresh ? theme.ok : theme.dim, children: [
812
+ fresh ? "\u25CF" : "\u25CB",
813
+ " "
814
+ ] }),
815
+ /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: "LOC " }),
816
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, bold: isMe, children: truncate(isMe ? "this machine" : v.agent, 12).padEnd(13) }),
817
+ /* @__PURE__ */ jsxs2(Text2, { children: [
818
+ String(v.calls).padStart(4),
819
+ "c "
820
+ ] }),
821
+ /* @__PURE__ */ jsxs2(Text2, { color: v.denies ? theme.bad : theme.dim, children: [
822
+ String(v.denies).padStart(3),
823
+ "d "
824
+ ] }),
825
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ago(v.lastAt) })
826
+ ] }, id);
827
+ }),
828
+ sess.slice(0, Math.max(0, colH - 1 - Math.min(localSessList.length, Math.max(1, colH - 2)))).map((a) => {
777
829
  const dot = sessionDot(a.status);
778
830
  return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
779
831
  /* @__PURE__ */ jsxs2(Text2, { color: dot.color, children: [
780
832
  dot.ch,
781
833
  " "
782
834
  ] }),
835
+ /* @__PURE__ */ jsx2(Text2, { color: "#4f6db8", children: "CLD " }),
783
836
  /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: truncate(a.agent_name ?? a.session_id, 12).padEnd(13) }),
784
837
  /* @__PURE__ */ jsxs2(Text2, { children: [
785
838
  String(a.total_calls).padStart(4),
@@ -831,13 +884,13 @@ function LivePanel({ active: active2 }) {
831
884
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
832
885
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", bold: true, children: " LIVE " }),
833
886
  /* @__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 " })
887
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: " p pause/copy \xB7 esc menu \xB7 q quit " })
835
888
  ] })
836
889
  ] });
837
890
  }
838
891
 
839
892
  // src/tui/panels/Policies.tsx
840
- import { Box as Box3, Text as Text3, useInput } from "ink";
893
+ import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
841
894
  import TextInput from "ink-text-input";
842
895
  import { useEffect as useEffect3, useState as useState3 } from "react";
843
896
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
@@ -928,7 +981,7 @@ function PoliciesPanel({ focused }) {
928
981
  setDirty(false);
929
982
  setStatus("discarded");
930
983
  };
931
- useInput(
984
+ useInput2(
932
985
  (input, key) => {
933
986
  if (view === "list") {
934
987
  if (key.upArrow) setPi((n) => Math.max(0, n - 1));
@@ -1072,7 +1125,7 @@ function PoliciesPanel({ focused }) {
1072
1125
  }
1073
1126
 
1074
1127
  // src/tui/panels/RateLimit.tsx
1075
- import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
1128
+ import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
1076
1129
  import { useEffect as useEffect4, useState as useState4 } from "react";
1077
1130
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1078
1131
  var MODES = ["off", "detect", "block"];
@@ -1123,7 +1176,7 @@ function RateLimitPanel({ focused }) {
1123
1176
  setStatus("\u2717 " + (e instanceof Error ? e.message : String(e)));
1124
1177
  }
1125
1178
  };
1126
- useInput2(
1179
+ useInput3(
1127
1180
  (input, key) => {
1128
1181
  const step = key.shift ? 10 : 1;
1129
1182
  if (key.upArrow) setFi((n) => (n - 1 + FIELDS2.length) % FIELDS2.length);
@@ -1202,7 +1255,7 @@ function FieldRow({ label, active: active2, children }) {
1202
1255
  }
1203
1256
 
1204
1257
  // src/tui/panels/Dlp.tsx
1205
- import { Box as Box5, Text as Text5, useInput as useInput3 } from "ink";
1258
+ import { Box as Box5, Text as Text5, useInput as useInput4 } from "ink";
1206
1259
  import TextInput2 from "ink-text-input";
1207
1260
  import { useEffect as useEffect5, useState as useState5 } from "react";
1208
1261
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
@@ -1247,7 +1300,7 @@ function DlpPanel({ focused }) {
1247
1300
  };
1248
1301
  const customCount = dlp?.custom.length ?? 0;
1249
1302
  const total = available.length + customCount;
1250
- useInput3(
1303
+ useInput4(
1251
1304
  (input, key) => {
1252
1305
  if (!dlp) return;
1253
1306
  if (key.upArrow) setSel((n) => Math.max(0, n - 1));
@@ -1409,7 +1462,7 @@ function StatsPanel({ active: active2 }) {
1409
1462
  }
1410
1463
 
1411
1464
  // src/tui/panels/Audit.tsx
1412
- import { Box as Box7, Text as Text7, useInput as useInput4 } from "ink";
1465
+ import { Box as Box7, Text as Text7, useInput as useInput5 } from "ink";
1413
1466
  import TextInput3 from "ink-text-input";
1414
1467
  import { useState as useState6 } from "react";
1415
1468
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
@@ -1440,7 +1493,7 @@ function AuditPanel({ active: active2, focused }) {
1440
1493
  () => view === "detail" && current?.session_id ? api.audit.list({ session_id: current.session_id, limit: 40 }) : Promise.resolve(null),
1441
1494
  [view, current?.session_id]
1442
1495
  );
1443
- useInput4(
1496
+ useInput5(
1444
1497
  (input, key) => {
1445
1498
  if (view === "detail") {
1446
1499
  if (key.leftArrow || key.escape) setView("list");
@@ -1617,7 +1670,7 @@ function App() {
1617
1670
  const { exit } = useApp();
1618
1671
  const [section, setSection] = useState7(0);
1619
1672
  const [focus, setFocus] = useState7("nav");
1620
- useInput5((input, key) => {
1673
+ useInput6((input, key) => {
1621
1674
  if (focus === "nav") {
1622
1675
  if (key.upArrow) setSection((n) => (n - 1 + SECTIONS.length) % SECTIONS.length);
1623
1676
  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.68.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": {