@solongate/proxy 0.66.0 → 0.67.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
@@ -7020,9 +7020,39 @@ var init_hooks = __esm({
7020
7020
 
7021
7021
  // src/tui/panels/Live.tsx
7022
7022
  import { Box as Box2, Text as Text2 } from "ink";
7023
+ import { closeSync, openSync, readSync, statSync } from "fs";
7024
+ import { homedir as homedir3 } from "os";
7025
+ import { join as join5 } from "path";
7023
7026
  import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
7024
7027
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
7025
- function ColumnChart({ series, hot, height, width: width2, color }) {
7028
+ function tailLines(file, maxBytes = 65536) {
7029
+ try {
7030
+ const size = statSync(file).size;
7031
+ const start = Math.max(0, size - maxBytes);
7032
+ const fd = openSync(file, "r");
7033
+ const buf = Buffer.alloc(size - start);
7034
+ readSync(fd, buf, 0, buf.length, start);
7035
+ closeSync(fd);
7036
+ const lines = buf.toString("utf-8").split("\n").filter(Boolean);
7037
+ if (start > 0) lines.shift();
7038
+ return lines;
7039
+ } catch {
7040
+ return [];
7041
+ }
7042
+ }
7043
+ function cloudItem(e) {
7044
+ return {
7045
+ id: "c:" + e.id,
7046
+ at: Date.parse(e.created_at),
7047
+ tool: e.tool_name,
7048
+ decision: e.decision,
7049
+ permission: (e.permission ?? "").slice(0, 4),
7050
+ detail: (e.arguments_summary ? JSON.stringify(e.arguments_summary) : e.reason ?? "").replace(/\s+/g, " "),
7051
+ dlp: !!e.dlp_matches?.length,
7052
+ source: "cloud"
7053
+ };
7054
+ }
7055
+ function ColumnChart({ series, hot, height, width: width2, color, hotColor = theme.bad }) {
7026
7056
  const pad = Math.max(0, width2 - series.length);
7027
7057
  const data = [...new Array(pad).fill(0), ...series.slice(-width2)];
7028
7058
  const hotPad = hot ? [...new Array(pad).fill(false), ...hot.slice(-width2)] : void 0;
@@ -7034,13 +7064,11 @@ function ColumnChart({ series, hot, height, width: width2, color }) {
7034
7064
  const v = data[i];
7035
7065
  const frac = v / max;
7036
7066
  let ch;
7037
- let c2;
7038
7067
  if (frac >= r / height) ch = "\u2588";
7039
7068
  else if (frac >= (r - 0.5) / height) ch = "\u2584";
7040
7069
  else if (r === 1) ch = "\u2581";
7041
7070
  else ch = " ";
7042
- if (r === 1 && v === 0) c2 = "#233457";
7043
- else c2 = hotPad?.[i] ? theme.bad : color;
7071
+ const c2 = r === 1 && v === 0 ? "#233457" : hotPad?.[i] ? hotColor : color;
7044
7072
  const last = segs[segs.length - 1];
7045
7073
  if (last && last.color === c2) last.text += ch;
7046
7074
  else segs.push({ text: ch, color: c2 });
@@ -7076,60 +7104,147 @@ function LivePanel({ active: active2 }) {
7076
7104
  const [s, setS] = useState2(null);
7077
7105
  const [tsData, setTsData] = useState2(null);
7078
7106
  const [lat, setLat] = useState2([]);
7079
- const [buffer, setBuffer] = useState2([]);
7107
+ const [cloudBuf, setCloudBuf] = useState2([]);
7108
+ const [localBuf, setLocalBuf] = useState2([]);
7109
+ const [localOn, setLocalOn] = useState2(null);
7110
+ const [ring, setRing] = useState2(null);
7080
7111
  const [log6, setLog] = useState2([]);
7081
7112
  const seenRef = useRef2(/* @__PURE__ */ new Set());
7113
+ const lastLocalTs = useRef2(0);
7114
+ const pausedUntil = useRef2(0);
7082
7115
  const pushLog = useCallback2((msg, level = "ok") => {
7083
7116
  setLog((prev) => [...prev.slice(-59), { ts: Date.now(), msg, level }]);
7084
7117
  }, []);
7085
- const doPoll = useCallback2(async () => {
7118
+ const onApiError = useCallback2(
7119
+ (e) => {
7120
+ const is429 = e instanceof ApiError && e.status === 429;
7121
+ if (is429 && Date.now() >= pausedUntil.current) {
7122
+ pausedUntil.current = Date.now() + 3e4;
7123
+ pushLog("rate limited by api \xB7 backing off 30s", "warn");
7124
+ } else if (!is429) {
7125
+ pushLog(`api error \xB7 ${truncate2(e instanceof Error ? e.message : String(e), 40)}`, "bad");
7126
+ }
7127
+ },
7128
+ [pushLog]
7129
+ );
7130
+ const paused = () => Date.now() < pausedUntil.current;
7131
+ const pollLocal = useCallback2(() => {
7132
+ const lines = tailLines(LOCAL_LOG);
7133
+ if (!lines.length) {
7134
+ setLocalOn((prev) => prev === null ? false : prev);
7135
+ return;
7136
+ }
7137
+ setLocalOn(true);
7138
+ const fresh = [];
7139
+ for (const line of lines) {
7140
+ try {
7141
+ const j = JSON.parse(line);
7142
+ const at = Date.parse(j.ts ?? "");
7143
+ if (!Number.isFinite(at) || at <= lastLocalTs.current) continue;
7144
+ fresh.push({
7145
+ id: "l:" + at + ":" + (j.tool ?? ""),
7146
+ at,
7147
+ tool: j.tool ?? "?",
7148
+ decision: j.decision ?? "ALLOW",
7149
+ permission: "",
7150
+ detail: (j.arguments ? JSON.stringify(j.arguments) : j.reason ?? "").replace(/\s+/g, " "),
7151
+ dlp: !!j.dlp,
7152
+ source: "local"
7153
+ });
7154
+ } catch {
7155
+ }
7156
+ }
7157
+ if (fresh.length) {
7158
+ lastLocalTs.current = fresh[fresh.length - 1].at;
7159
+ setLocalBuf((prev) => [...prev, ...fresh].slice(-400));
7160
+ const denies = fresh.filter((f) => f.decision !== "ALLOW").length;
7161
+ if (lastLocalTs.current !== fresh[0].at || fresh.length < 10) {
7162
+ pushLog(`local +${fresh.length} calls${denies ? ` \xB7 ${denies} DENIED` : ""}`, denies ? "bad" : "warn");
7163
+ }
7164
+ }
7165
+ const ringLines = tailLines(RING, 8192).slice(-30);
7166
+ if (ringLines.length) {
7167
+ let sum = 0;
7168
+ let n = 0;
7169
+ let session = "";
7170
+ for (const rl2 of ringLines) {
7171
+ try {
7172
+ const j = JSON.parse(rl2);
7173
+ if (typeof j.ms === "number") {
7174
+ sum += j.ms;
7175
+ n++;
7176
+ }
7177
+ if (j.session) session = j.session;
7178
+ } catch {
7179
+ }
7180
+ }
7181
+ if (n) setRing({ avgMs: Math.round(sum / n), session, count: n });
7182
+ }
7183
+ }, [pushLog]);
7184
+ useEffect2(() => {
7185
+ if (!active2) return;
7186
+ pollLocal();
7187
+ const t = setInterval(pollLocal, 2e3);
7188
+ return () => clearInterval(t);
7189
+ }, [active2, pollLocal]);
7190
+ const pollFeed = useCallback2(async () => {
7191
+ if (paused()) return;
7086
7192
  const t0 = Date.now();
7087
7193
  try {
7088
- const [sd, fd, td] = await Promise.all([api.stats.get(), api.audit.list({ limit: 50 }), api.stats.timeseries({ period: "24h" })]);
7194
+ const fd = await api.audit.list({ limit: 50 });
7089
7195
  const ms = Date.now() - t0;
7090
- setS(sd);
7091
- setTsData(td);
7092
- setLat((l) => [...l.slice(-47), ms]);
7196
+ setLat((l) => [...l, ms].slice(-240));
7093
7197
  const fresh = fd.entries.filter((e) => !seenRef.current.has(e.id));
7198
+ const firstLoad = seenRef.current.size === 0 && fresh.length > 1;
7094
7199
  for (const e of fresh) seenRef.current.add(e.id);
7095
- const firstLoad = seenRef.current.size === fresh.length && fresh.length > 1;
7096
7200
  if (fresh.length) {
7097
- setBuffer((prev) => [...prev, ...fresh.sort((a, b) => Date.parse(a.created_at) - Date.parse(b.created_at))].slice(-400));
7201
+ setCloudBuf((prev) => [...prev, ...fresh.map(cloudItem).sort((a, b) => a.at - b.at)].slice(-400));
7098
7202
  }
7099
7203
  const denies = fresh.filter((e) => e.decision !== "ALLOW").length;
7100
- const dlp = fresh.filter((e) => e.dlp_matches?.length).length;
7101
- if (firstLoad) pushLog(`link up \xB7 api ${ms}ms \xB7 loaded ${fresh.length} calls`);
7102
- else
7103
- pushLog(
7104
- `api ${ms}ms \xB7 +${fresh.length} calls${denies ? ` \xB7 ${denies} DENIED` : ""}${dlp ? ` \xB7 ${dlp} DLP` : ""}`,
7105
- denies || dlp ? "bad" : fresh.length ? "warn" : "ok"
7106
- );
7204
+ if (firstLoad) pushLog(`cloud link up \xB7 api ${ms}ms \xB7 ${fresh.length} calls`);
7205
+ else pushLog(`api ${ms}ms \xB7 +${fresh.length} cloud${denies ? ` \xB7 ${denies} DENIED` : ""}`, denies ? "bad" : fresh.length ? "warn" : "ok");
7107
7206
  } catch (e) {
7108
- pushLog(`api error \xB7 ${truncate2(e instanceof Error ? e.message : String(e), 40)}`, "bad");
7207
+ onApiError(e);
7109
7208
  }
7110
- }, [pushLog]);
7209
+ }, [pushLog, onApiError]);
7210
+ const pollMedium = useCallback2(async () => {
7211
+ if (paused()) return;
7212
+ try {
7213
+ setS(await api.stats.get());
7214
+ } catch (e) {
7215
+ onApiError(e);
7216
+ }
7217
+ }, [onApiError]);
7218
+ const pollSlow = useCallback2(async () => {
7219
+ if (paused()) return;
7220
+ try {
7221
+ setTsData(await api.stats.timeseries({ period: "24h" }));
7222
+ } catch (e) {
7223
+ onApiError(e);
7224
+ }
7225
+ }, [onApiError]);
7111
7226
  useEffect2(() => {
7112
7227
  if (!active2) return;
7113
- void doPoll();
7114
- const t = setInterval(() => void doPoll(), 2e3);
7115
- return () => clearInterval(t);
7116
- }, [active2, doPoll]);
7228
+ void pollFeed();
7229
+ void pollMedium();
7230
+ void pollSlow();
7231
+ const t1 = setInterval(() => void pollFeed(), 4e3);
7232
+ const t2 = setInterval(() => void pollMedium(), 1e4);
7233
+ const t3 = setInterval(() => void pollSlow(), 3e4);
7234
+ return () => {
7235
+ clearInterval(t1);
7236
+ clearInterval(t2);
7237
+ clearInterval(t3);
7238
+ };
7239
+ }, [active2, pollFeed, pollMedium, pollSlow]);
7117
7240
  const sessions = useLoader(() => api.agents.live({ limit: 30, includeDeactivated: true }));
7118
7241
  const insights = useLoader(() => api.stats.securityInsights(7));
7119
- usePoll(sessions.reload, 5e3, active2);
7120
- usePoll(insights.reload, 1e4, active2);
7121
- useEffect2(() => {
7122
- const c2 = sessions.data?.counts;
7123
- if (c2) pushLog(`sessions ${c2.active}\u25B2 ${c2.idle}\u25CC ${c2.deactivated}\u25CB`);
7124
- }, [sessions.data, pushLog]);
7125
- useEffect2(() => {
7126
- const ib = insights.data;
7127
- if (ib?.layers) {
7128
- const rl2 = ib.layers.rateLimit;
7129
- const dl2 = ib.layers.dlp;
7130
- pushLog(`layers \xB7 ratelimit ${rl2?.mode ?? "?"} ${rl2?.perMinute || "\u2014"}/m \xB7 dlp ${dl2?.mode ?? "?"} ${(dl2?.patterns ?? []).length} armed`);
7131
- }
7132
- }, [insights.data, pushLog]);
7242
+ usePoll(() => {
7243
+ if (!paused()) sessions.reload();
7244
+ }, 1e4, active2);
7245
+ usePoll(() => {
7246
+ if (!paused()) insights.reload();
7247
+ }, 3e4, active2);
7133
7248
  const [tick, setTick] = useState2(0);
7134
7249
  useEffect2(() => {
7135
7250
  if (!active2) return;
@@ -7149,29 +7264,33 @@ function LivePanel({ active: active2 }) {
7149
7264
  const minuteNow = (ins.activity?.minute ?? []).slice(-1)[0]?.count ?? 0;
7150
7265
  const dlpBars = (ins.dlpByPattern ?? []).slice(0, 2);
7151
7266
  const maxDlpBar = dlpBars[0]?.count ?? 1;
7267
+ const merged = [...cloudBuf, ...localBuf].sort((a, b) => a.at - b.at);
7268
+ const denials = merged.filter((e) => e.decision !== "ALLOW");
7269
+ const lastDeny = denials[denials.length - 1];
7152
7270
  const toolCounts = /* @__PURE__ */ new Map();
7153
- for (const e of buffer) toolCounts.set(e.tool_name, (toolCounts.get(e.tool_name) ?? 0) + 1);
7271
+ for (const e of merged) toolCounts.set(e.tool, (toolCounts.get(e.tool) ?? 0) + 1);
7154
7272
  const topTools = [...toolCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3);
7155
- const denials = buffer.filter((e) => e.decision !== "ALLOW");
7156
- const lastDeny = denials[denials.length - 1];
7157
7273
  const latNow = lat[lat.length - 1] ?? 0;
7158
- const latAvg = lat.length ? Math.round(lat.reduce((a, b) => a + b, 0) / lat.length) : 0;
7274
+ const sortedLat = [...lat].sort((a, b) => a - b);
7275
+ const latMed = sortedLat.length ? sortedLat[Math.floor(sortedLat.length / 2)] : 0;
7276
+ const latHotAt = Math.max(2e3, latMed * 2.5);
7277
+ const backingOff = Date.now() < pausedUntil.current;
7159
7278
  const sess = (sessions.data?.agents ?? []).slice().sort((a, b) => Date.parse(b.last_seen_at) - Date.parse(a.last_seen_at));
7160
7279
  const sessCounts = sessions.data?.counts;
7161
7280
  const chartH = rows >= 36 ? 6 : 4;
7162
7281
  const colH = rows >= 32 ? 6 : 5;
7163
- const streamRows = Math.max(4, rows - 6 - chartH - colH);
7164
- const tail = buffer.slice(-streamRows);
7282
+ const streamRows = Math.max(4, rows - 6 - chartH - colH - (localOn === false ? 1 : 0));
7283
+ const tail = merged.slice(-streamRows);
7165
7284
  const innerW = cols - 2;
7166
7285
  const leftW = Math.floor(innerW * 0.55);
7167
7286
  const rightW = innerW - leftW - 2;
7168
7287
  const colW = Math.max(20, Math.floor((innerW - 4) / 3));
7169
- const sessionDot = (st) => st === "active" ? { ch: "\u25CF", color: theme.ok } : st === "idle" ? { ch: "\u25D0", color: theme.warn } : { ch: "\u25CB", color: theme.dim };
7288
+ const sessionDot = (st) => st === "active" ? { ch: "\u25CF", color: theme.ok } : st === "idle" ? { ch: "\u25CC", color: theme.warn } : { ch: "\u25CB", color: theme.dim };
7170
7289
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
7171
7290
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7172
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#1432A0", color: "#AAC8FA", bold: true, children: " \u26E8 SOLONGATE LIVE " }),
7291
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#1432A0", color: "#AAC8FA", bold: true, children: " SOLONGATE LIVE " }),
7173
7292
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: ` ${spin} up ${fmtUp(Date.now() - startRef.current)} \xB7 ${hhmmss(Date.now())} \xB7 api ${latNow}ms ` }),
7174
- lastDeny ? /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#3d1220", color: "#ff6b6b", bold: true, children: ` \u26A0 ${hhmmss(lastDeny.created_at)} ${lastDeny.tool_name} DENIED ` }) : /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#4f6db8", children: " \u2713 clean " })
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 " })
7175
7294
  ] }),
7176
7295
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7177
7296
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "\u2502 calls " }),
@@ -7180,14 +7299,14 @@ function LivePanel({ active: active2 }) {
7180
7299
  /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: s ? s.allowed : "\xB7\xB7\xB7" }),
7181
7300
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 deny " }),
7182
7301
  /* @__PURE__ */ jsx2(Text2, { color: theme.bad, bold: true, children: s ? s.denied : "\xB7\xB7" }),
7302
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 local " }),
7303
+ localOn ? /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: "\u2713 streaming" }) : /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "off" }),
7183
7304
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 ratelimit " }),
7184
7305
  /* @__PURE__ */ jsx2(Text2, { color: rl?.mode === "block" ? theme.ok : rl?.mode === "detect" ? theme.warn : theme.dim, children: rl?.mode ?? "?" }),
7185
7306
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 dlp " }),
7186
7307
  /* @__PURE__ */ jsx2(Text2, { color: dl?.mode === "block" ? theme.ok : dl?.mode === "detect" ? theme.warn : theme.dim, children: dl?.mode ?? "?" }),
7187
7308
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 sessions " }),
7188
7309
  /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: sessCounts ? `${sessCounts.active}\u25B2 ${sessCounts.idle}\u25CC ${sessCounts.deactivated}\u25CB` : "\xB7" }),
7189
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 policies " }),
7190
- /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: s ? s.active_policies : "\xB7" }),
7191
7310
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502" })
7192
7311
  ] }),
7193
7312
  /* @__PURE__ */ jsxs2(Box2, { children: [
@@ -7196,8 +7315,8 @@ function LivePanel({ active: active2 }) {
7196
7315
  /* @__PURE__ */ jsx2(ColumnChart, { series: traffic, hot: trafficHot, height: chartH, width: leftW, color: theme.accent })
7197
7316
  ] }),
7198
7317
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: rightW, children: [
7199
- /* @__PURE__ */ jsx2(PaneTitle, { label: "API LATENCY", extra: `live \xB7 now ${latNow}ms \xB7 avg ${latAvg}ms`, width: rightW }),
7200
- /* @__PURE__ */ jsx2(ColumnChart, { series: lat, hot: lat.map((v) => v > 800), height: chartH, width: rightW, color: "#82AAF0" })
7318
+ /* @__PURE__ */ jsx2(PaneTitle, { label: "API LATENCY", extra: `now ${latNow}ms \xB7 med ${latMed}ms \xB7 amber >${Math.round(latHotAt)}ms`, width: rightW }),
7319
+ /* @__PURE__ */ jsx2(ColumnChart, { series: lat, hot: lat.map((v) => v > latHotAt), height: chartH, width: rightW, color: "#82AAF0", hotColor: "#ffb454" })
7201
7320
  ] })
7202
7321
  ] }),
7203
7322
  /* @__PURE__ */ jsxs2(Box2, { children: [
@@ -7217,12 +7336,22 @@ function LivePanel({ active: active2 }) {
7217
7336
  " patterns armed"
7218
7337
  ] })
7219
7338
  ] }),
7220
- 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" })
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" }),
7340
+ ring ? /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7341
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "GUARD".padEnd(10) }),
7342
+ /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: "local \u2713" }),
7343
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ` eval avg ${ring.avgMs}ms \xB7 ${ring.count} recent` })
7344
+ ] }) : null
7221
7345
  ] }),
7222
7346
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, marginRight: 2, children: [
7223
7347
  /* @__PURE__ */ jsx2(PaneTitle, { label: "SESSIONS", extra: sessCounts ? `${sessCounts.active} live` : "", width: colW }),
7224
- sess.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "scanning\u2026" }) : null,
7225
- sess.slice(0, colH - 1).map((a) => {
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) => {
7226
7355
  const dot = sessionDot(a.status);
7227
7356
  return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7228
7357
  /* @__PURE__ */ jsxs2(Text2, { color: dot.color, children: [
@@ -7254,7 +7383,11 @@ function LivePanel({ active: active2 }) {
7254
7383
  ] }, l.ts + ":" + i))
7255
7384
  ] })
7256
7385
  ] }),
7257
- /* @__PURE__ */ jsx2(PaneTitle, { label: "TOOL STREAM", extra: `tail -f \xB7 ${buffer.length} buffered`, width: innerW }),
7386
+ /* @__PURE__ */ jsx2(PaneTitle, { label: "TOOL STREAM", extra: `local+cloud tail \xB7 ${merged.length} buffered`, width: innerW }),
7387
+ localOn === false ? /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7388
+ /* @__PURE__ */ jsx2(Text2, { color: theme.warn, children: "local logs off" }),
7389
+ /* @__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`)" })
7390
+ ] }) : null,
7258
7391
  tail.length === 0 ? /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
7259
7392
  spin,
7260
7393
  " awaiting traffic\u2026"
@@ -7262,23 +7395,25 @@ function LivePanel({ active: active2 }) {
7262
7395
  tail.map((e) => /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7263
7396
  /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
7264
7397
  "[",
7265
- hhmmss(e.created_at),
7266
- "] "
7398
+ hhmmss(e.at),
7399
+ " "
7267
7400
  ] }),
7401
+ /* @__PURE__ */ jsx2(Text2, { color: e.source === "local" ? theme.ok : "#4f6db8", children: e.source === "local" ? "LOC" : "CLD" }),
7402
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "] " }),
7268
7403
  /* @__PURE__ */ jsx2(Text2, { color: decisionColor(e.decision), bold: e.decision !== "ALLOW", children: e.decision.padEnd(6) }),
7269
- /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: truncate2(e.tool_name, 14).padEnd(15) }),
7270
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: (e.permission ?? "").slice(0, 4).padEnd(5) }),
7271
- e.dlp_matches?.length ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: "DLP! " }) : null,
7272
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: (e.arguments_summary ? JSON.stringify(e.arguments_summary) : e.reason ?? "").replace(/\s+/g, " ") })
7404
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: truncate2(e.tool, 14).padEnd(15) }),
7405
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: e.permission.padEnd(5) }),
7406
+ e.dlp ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: "DLP! " }) : null,
7407
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: e.detail })
7273
7408
  ] }, e.id)),
7274
7409
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7275
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", bold: true, children: " \u25B2 LIVE " }),
7276
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#0b1530", color: "#4f6db8", children: ` top ${topTools.map(([t, c2]) => `${t}\xD7${c2}`).join(" ") || "\u2014"} \xB7 buf ${buffer.length} ` }),
7410
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", bold: true, children: " LIVE " }),
7411
+ /* @__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} ` }),
7277
7412
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: " esc menu \xB7 q quit " })
7278
7413
  ] })
7279
7414
  ] });
7280
7415
  }
7281
- var SPIN, BG, hhmmss, fmtUp;
7416
+ var SPIN, BG, LOCAL_LOG, RING, hhmmss, fmtUp;
7282
7417
  var init_Live = __esm({
7283
7418
  "src/tui/panels/Live.tsx"() {
7284
7419
  "use strict";
@@ -7287,6 +7422,8 @@ var init_Live = __esm({
7287
7422
  init_theme();
7288
7423
  SPIN = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
7289
7424
  BG = "#12234f";
7425
+ LOCAL_LOG = join5(homedir3(), ".solongate", "local-logs", "solongate-audit.jsonl");
7426
+ RING = join5(process.cwd(), ".solongate", ".eval-ring.jsonl");
7290
7427
  hhmmss = (ts) => {
7291
7428
  const d = new Date(ts);
7292
7429
  return Number.isNaN(d.getTime()) ? "--:--:--" : d.toTimeString().slice(0, 8);
@@ -8958,8 +9095,8 @@ __export(global_install_exports, {
8958
9095
  unlockProtected: () => unlockProtected
8959
9096
  });
8960
9097
  import { readFileSync as readFileSync6, writeFileSync as writeFileSync3, existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
8961
- import { resolve as resolve4, join as join5, dirname } from "path";
8962
- import { homedir as homedir3 } from "os";
9098
+ import { resolve as resolve4, join as join6, dirname } from "path";
9099
+ import { homedir as homedir4 } from "os";
8963
9100
  import { fileURLToPath } from "url";
8964
9101
  import { createInterface } from "readline";
8965
9102
  import { execFileSync as execFileSync2 } from "child_process";
@@ -9022,10 +9159,10 @@ function unlockFile(file) {
9022
9159
  function protectedTargets() {
9023
9160
  const p = globalPaths();
9024
9161
  return [
9025
- join5(p.hooksDir, "guard.mjs"),
9026
- join5(p.hooksDir, "audit.mjs"),
9027
- join5(p.hooksDir, "stop.mjs"),
9028
- join5(p.hooksDir, "shield.mjs"),
9162
+ join6(p.hooksDir, "guard.mjs"),
9163
+ join6(p.hooksDir, "audit.mjs"),
9164
+ join6(p.hooksDir, "stop.mjs"),
9165
+ join6(p.hooksDir, "shield.mjs"),
9029
9166
  p.configPath,
9030
9167
  p.settingsPath
9031
9168
  ];
@@ -9037,25 +9174,25 @@ function unlockProtected() {
9037
9174
  for (const f of protectedTargets()) unlockFile(f);
9038
9175
  }
9039
9176
  function globalPaths() {
9040
- const home = homedir3();
9041
- const sgDir = join5(home, ".solongate");
9042
- const hooksDir = join5(sgDir, "hooks");
9043
- const claudeDir = join5(home, ".claude");
9177
+ const home = homedir4();
9178
+ const sgDir = join6(home, ".solongate");
9179
+ const hooksDir = join6(sgDir, "hooks");
9180
+ const claudeDir = join6(home, ".claude");
9044
9181
  return {
9045
9182
  home,
9046
9183
  sgDir,
9047
9184
  hooksDir,
9048
9185
  claudeDir,
9049
- settingsPath: join5(claudeDir, "settings.json"),
9050
- backupPath: join5(claudeDir, "settings.solongate.bak"),
9051
- configPath: join5(sgDir, "cloud-guard.json")
9186
+ settingsPath: join6(claudeDir, "settings.json"),
9187
+ backupPath: join6(claudeDir, "settings.solongate.bak"),
9188
+ configPath: join6(sgDir, "cloud-guard.json")
9052
9189
  };
9053
9190
  }
9054
9191
  function readHook(filename) {
9055
- return readFileSync6(join5(HOOKS_DIR, filename), "utf-8");
9192
+ return readFileSync6(join6(HOOKS_DIR, filename), "utf-8");
9056
9193
  }
9057
9194
  function readGuard() {
9058
- const bundled = join5(HOOKS_DIR, "guard.bundled.mjs");
9195
+ const bundled = join6(HOOKS_DIR, "guard.bundled.mjs");
9059
9196
  return existsSync4(bundled) ? readFileSync6(bundled, "utf-8") : readHook("guard.mjs");
9060
9197
  }
9061
9198
  function ask(question) {
@@ -9110,7 +9247,7 @@ function shimTargets() {
9110
9247
  return [];
9111
9248
  }
9112
9249
  }
9113
- return [".bashrc", ".zshrc", ".profile"].map((f) => join5(homedir3(), f)).filter((f) => existsSync4(f));
9250
+ return [".bashrc", ".zshrc", ".profile"].map((f) => join6(homedir4(), f)).filter((f) => existsSync4(f));
9114
9251
  }
9115
9252
  function writeShimBlock(file, block) {
9116
9253
  const re = new RegExp(escapeRe(SHIM_BEGIN) + "[\\s\\S]*?" + escapeRe(SHIM_END) + "\\r?\\n?", "g");
@@ -9181,10 +9318,10 @@ async function runGlobalInstall(opts = {}) {
9181
9318
  mkdirSync3(p.hooksDir, { recursive: true });
9182
9319
  mkdirSync3(p.claudeDir, { recursive: true });
9183
9320
  unlockProtected();
9184
- writeFileSync3(join5(p.hooksDir, "guard.mjs"), readGuard());
9185
- writeFileSync3(join5(p.hooksDir, "audit.mjs"), readHook("audit.mjs"));
9186
- writeFileSync3(join5(p.hooksDir, "stop.mjs"), readHook("stop.mjs"));
9187
- writeFileSync3(join5(p.hooksDir, "shield.mjs"), readHook("shield.mjs"));
9321
+ writeFileSync3(join6(p.hooksDir, "guard.mjs"), readGuard());
9322
+ writeFileSync3(join6(p.hooksDir, "audit.mjs"), readHook("audit.mjs"));
9323
+ writeFileSync3(join6(p.hooksDir, "stop.mjs"), readHook("stop.mjs"));
9324
+ writeFileSync3(join6(p.hooksDir, "shield.mjs"), readHook("shield.mjs"));
9188
9325
  console.log(` Installed hooks \u2192 ${p.hooksDir}`);
9189
9326
  removeClaudeShim();
9190
9327
  writeFileSync3(p.configPath, JSON.stringify({ apiKey, apiUrl }, null, 2) + "\n");
@@ -9202,9 +9339,9 @@ async function runGlobalInstall(opts = {}) {
9202
9339
  existing = {};
9203
9340
  }
9204
9341
  }
9205
- const guardAbs = join5(p.hooksDir, "guard.mjs").replace(/\\/g, "/");
9206
- const auditAbs = join5(p.hooksDir, "audit.mjs").replace(/\\/g, "/");
9207
- const stopAbs = join5(p.hooksDir, "stop.mjs").replace(/\\/g, "/");
9342
+ const guardAbs = join6(p.hooksDir, "guard.mjs").replace(/\\/g, "/");
9343
+ const auditAbs = join6(p.hooksDir, "audit.mjs").replace(/\\/g, "/");
9344
+ const stopAbs = join6(p.hooksDir, "stop.mjs").replace(/\\/g, "/");
9208
9345
  const nodeBin = process.execPath.replace(/\\/g, "/");
9209
9346
  const merged = {
9210
9347
  ...existing,
@@ -9394,11 +9531,11 @@ import { createServer, request as httpRequest } from "http";
9394
9531
  import { request as httpsRequest } from "https";
9395
9532
  import { spawn as spawn2 } from "child_process";
9396
9533
  import { URL as URL2 } from "url";
9397
- import { readFileSync as readFileSync7, existsSync as existsSync5, readdirSync, statSync } from "fs";
9534
+ import { readFileSync as readFileSync7, existsSync as existsSync5, readdirSync, statSync as statSync2 } from "fs";
9398
9535
  import { resolve as resolve5 } from "path";
9399
- import { homedir as homedir4 } from "os";
9536
+ import { homedir as homedir5 } from "os";
9400
9537
  function findCacheFile() {
9401
- const dir = resolve5(homedir4(), ".solongate");
9538
+ const dir = resolve5(homedir5(), ".solongate");
9402
9539
  const envSel = process.env.SOLONGATE_AGENT_ID;
9403
9540
  if (envSel) {
9404
9541
  const f = resolve5(dir, ".policy-cache-" + envSel.replace(/[^a-zA-Z0-9_-]/g, "_") + ".json");
@@ -9409,7 +9546,7 @@ function findCacheFile() {
9409
9546
  for (const name of readdirSync(dir)) {
9410
9547
  if (name.startsWith(".policy-cache-") && name.endsWith(".json")) {
9411
9548
  const full = resolve5(dir, name);
9412
- const ts = statSync(full).mtimeMs;
9549
+ const ts = statSync2(full).mtimeMs;
9413
9550
  if (ts > bestTs) {
9414
9551
  bestTs = ts;
9415
9552
  best = full;
@@ -9700,9 +9837,9 @@ __export(logs_server_exports, {
9700
9837
  runLogsServer: () => runLogsServer
9701
9838
  });
9702
9839
  import { createServer as createServer2 } from "http";
9703
- import { readFileSync as readFileSync8, statSync as statSync2 } from "fs";
9704
- import { resolve as resolve6, join as join6, isAbsolute } from "path";
9705
- import { homedir as homedir5 } from "os";
9840
+ import { readFileSync as readFileSync8, statSync as statSync3 } from "fs";
9841
+ import { resolve as resolve6, join as join7, isAbsolute } from "path";
9842
+ import { homedir as homedir6 } from "os";
9706
9843
  import { readdirSync as readdirSync2 } from "fs";
9707
9844
  function allowedOrigins() {
9708
9845
  const base = [
@@ -9719,15 +9856,15 @@ function resolveLocalLogDir(rawPath) {
9719
9856
  const dir = String(rawPath || "").trim().replace(/[\\/]+$/, "");
9720
9857
  if (!dir) return null;
9721
9858
  if (isAbsolute(dir)) return dir;
9722
- return resolve6(homedir5(), ".solongate", "local-logs");
9859
+ return resolve6(homedir6(), ".solongate", "local-logs");
9723
9860
  }
9724
9861
  async function findLogDir() {
9725
- const base = resolve6(homedir5(), ".solongate");
9862
+ const base = resolve6(homedir6(), ".solongate");
9726
9863
  try {
9727
9864
  const files = readdirSync2(base).filter((f) => f.startsWith(".policy-cache-") && f.endsWith(".json"));
9728
9865
  for (const f of files) {
9729
9866
  try {
9730
- const c2 = JSON.parse(readFileSync8(join6(base, f), "utf-8"));
9867
+ const c2 = JSON.parse(readFileSync8(join7(base, f), "utf-8"));
9731
9868
  const p = c2?.security?.localLogs?.path;
9732
9869
  if (typeof p === "string" && p.trim()) return { dir: resolveLocalLogDir(p), configured: p };
9733
9870
  } catch {
@@ -9736,7 +9873,7 @@ async function findLogDir() {
9736
9873
  } catch {
9737
9874
  }
9738
9875
  try {
9739
- const cfgRaw = readFileSync8(join6(base, "cloud-guard.json"), "utf-8");
9876
+ const cfgRaw = readFileSync8(join7(base, "cloud-guard.json"), "utf-8");
9740
9877
  const { apiKey, apiUrl } = JSON.parse(cfgRaw);
9741
9878
  if (apiKey) {
9742
9879
  const url = `${apiUrl || "https://api.solongate.com"}/api/v1/policies/active`;
@@ -9765,9 +9902,9 @@ function setCors(req, res) {
9765
9902
  }
9766
9903
  function fileInfo(dir) {
9767
9904
  if (!dir) return { file: null, exists: false, size: 0, mtimeMs: 0 };
9768
- const file = join6(dir, LOG_FILENAME);
9905
+ const file = join7(dir, LOG_FILENAME);
9769
9906
  try {
9770
- const st = statSync2(file);
9907
+ const st = statSync3(file);
9771
9908
  return { file, exists: true, size: st.size, mtimeMs: st.mtimeMs };
9772
9909
  } catch {
9773
9910
  return { file, exists: false, size: 0, mtimeMs: 0 };
@@ -10216,7 +10353,7 @@ var init_inject = __esm({
10216
10353
  // src/create.ts
10217
10354
  var create_exports = {};
10218
10355
  import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync5, existsSync as existsSync7 } from "fs";
10219
- import { resolve as resolve8, join as join7 } from "path";
10356
+ import { resolve as resolve8, join as join8 } from "path";
10220
10357
  import { execSync as execSync2 } from "child_process";
10221
10358
  function withSpinner(message, fn) {
10222
10359
  const frames = ["\u28FE", "\u28FD", "\u28FB", "\u28BF", "\u287F", "\u28DF", "\u28EF", "\u28F7"];
@@ -10296,7 +10433,7 @@ EXAMPLES
10296
10433
  }
10297
10434
  function createProject(dir, name, _policy) {
10298
10435
  writeFileSync5(
10299
- join7(dir, "package.json"),
10436
+ join8(dir, "package.json"),
10300
10437
  JSON.stringify(
10301
10438
  {
10302
10439
  name,
@@ -10326,7 +10463,7 @@ function createProject(dir, name, _policy) {
10326
10463
  ) + "\n"
10327
10464
  );
10328
10465
  writeFileSync5(
10329
- join7(dir, "tsconfig.json"),
10466
+ join8(dir, "tsconfig.json"),
10330
10467
  JSON.stringify(
10331
10468
  {
10332
10469
  compilerOptions: {
@@ -10346,9 +10483,9 @@ function createProject(dir, name, _policy) {
10346
10483
  2
10347
10484
  ) + "\n"
10348
10485
  );
10349
- mkdirSync4(join7(dir, "src"), { recursive: true });
10486
+ mkdirSync4(join8(dir, "src"), { recursive: true });
10350
10487
  writeFileSync5(
10351
- join7(dir, "src", "index.ts"),
10488
+ join8(dir, "src", "index.ts"),
10352
10489
  `#!/usr/bin/env node
10353
10490
 
10354
10491
  console.log = (...args: unknown[]) => {
@@ -10390,7 +10527,7 @@ console.log('Press Ctrl+C to stop.');
10390
10527
  `
10391
10528
  );
10392
10529
  writeFileSync5(
10393
- join7(dir, ".mcp.json"),
10530
+ join8(dir, ".mcp.json"),
10394
10531
  JSON.stringify(
10395
10532
  {
10396
10533
  mcpServers: {
@@ -10408,12 +10545,12 @@ console.log('Press Ctrl+C to stop.');
10408
10545
  ) + "\n"
10409
10546
  );
10410
10547
  writeFileSync5(
10411
- join7(dir, ".env"),
10548
+ join8(dir, ".env"),
10412
10549
  `SOLONGATE_API_KEY=sg_live_YOUR_KEY_HERE
10413
10550
  `
10414
10551
  );
10415
10552
  writeFileSync5(
10416
- join7(dir, ".gitignore"),
10553
+ join8(dir, ".gitignore"),
10417
10554
  `node_modules/
10418
10555
  dist/
10419
10556
  *.solongate-backup
package/dist/tui/index.js CHANGED
@@ -126,6 +126,9 @@ function KeyHints({ hints }) {
126
126
 
127
127
  // src/tui/panels/Live.tsx
128
128
  import { Box as Box2, Text as Text2 } from "ink";
129
+ import { closeSync, openSync, readSync, statSync } from "fs";
130
+ import { homedir as homedir2 } from "os";
131
+ import { join as join2 } from "path";
129
132
  import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
130
133
 
131
134
  // src/api-client/client.ts
@@ -433,6 +436,8 @@ function usePoll(reload, intervalMs, enabled = true) {
433
436
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
434
437
  var SPIN = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
435
438
  var BG = "#12234f";
439
+ var LOCAL_LOG = join2(homedir2(), ".solongate", "local-logs", "solongate-audit.jsonl");
440
+ var RING = join2(process.cwd(), ".solongate", ".eval-ring.jsonl");
436
441
  var hhmmss = (ts) => {
437
442
  const d = new Date(ts);
438
443
  return Number.isNaN(d.getTime()) ? "--:--:--" : d.toTimeString().slice(0, 8);
@@ -442,7 +447,34 @@ var fmtUp = (ms) => {
442
447
  const p = (n) => String(n).padStart(2, "0");
443
448
  return `${p(Math.floor(s / 3600))}:${p(Math.floor(s % 3600 / 60))}:${p(s % 60)}`;
444
449
  };
445
- function ColumnChart({ series, hot, height, width, color }) {
450
+ function tailLines(file, maxBytes = 65536) {
451
+ try {
452
+ const size = statSync(file).size;
453
+ const start = Math.max(0, size - maxBytes);
454
+ const fd = openSync(file, "r");
455
+ const buf = Buffer.alloc(size - start);
456
+ readSync(fd, buf, 0, buf.length, start);
457
+ closeSync(fd);
458
+ const lines = buf.toString("utf-8").split("\n").filter(Boolean);
459
+ if (start > 0) lines.shift();
460
+ return lines;
461
+ } catch {
462
+ return [];
463
+ }
464
+ }
465
+ function cloudItem(e) {
466
+ return {
467
+ id: "c:" + e.id,
468
+ at: Date.parse(e.created_at),
469
+ tool: e.tool_name,
470
+ decision: e.decision,
471
+ permission: (e.permission ?? "").slice(0, 4),
472
+ detail: (e.arguments_summary ? JSON.stringify(e.arguments_summary) : e.reason ?? "").replace(/\s+/g, " "),
473
+ dlp: !!e.dlp_matches?.length,
474
+ source: "cloud"
475
+ };
476
+ }
477
+ function ColumnChart({ series, hot, height, width, color, hotColor = theme.bad }) {
446
478
  const pad = Math.max(0, width - series.length);
447
479
  const data = [...new Array(pad).fill(0), ...series.slice(-width)];
448
480
  const hotPad = hot ? [...new Array(pad).fill(false), ...hot.slice(-width)] : void 0;
@@ -454,13 +486,11 @@ function ColumnChart({ series, hot, height, width, color }) {
454
486
  const v = data[i];
455
487
  const frac = v / max;
456
488
  let ch;
457
- let c2;
458
489
  if (frac >= r / height) ch = "\u2588";
459
490
  else if (frac >= (r - 0.5) / height) ch = "\u2584";
460
491
  else if (r === 1) ch = "\u2581";
461
492
  else ch = " ";
462
- if (r === 1 && v === 0) c2 = "#233457";
463
- else c2 = hotPad?.[i] ? theme.bad : color;
493
+ const c2 = r === 1 && v === 0 ? "#233457" : hotPad?.[i] ? hotColor : color;
464
494
  const last = segs[segs.length - 1];
465
495
  if (last && last.color === c2) last.text += ch;
466
496
  else segs.push({ text: ch, color: c2 });
@@ -496,60 +526,147 @@ function LivePanel({ active: active2 }) {
496
526
  const [s, setS] = useState2(null);
497
527
  const [tsData, setTsData] = useState2(null);
498
528
  const [lat, setLat] = useState2([]);
499
- const [buffer, setBuffer] = useState2([]);
529
+ const [cloudBuf, setCloudBuf] = useState2([]);
530
+ const [localBuf, setLocalBuf] = useState2([]);
531
+ const [localOn, setLocalOn] = useState2(null);
532
+ const [ring, setRing] = useState2(null);
500
533
  const [log, setLog] = useState2([]);
501
534
  const seenRef = useRef2(/* @__PURE__ */ new Set());
535
+ const lastLocalTs = useRef2(0);
536
+ const pausedUntil = useRef2(0);
502
537
  const pushLog = useCallback2((msg, level = "ok") => {
503
538
  setLog((prev) => [...prev.slice(-59), { ts: Date.now(), msg, level }]);
504
539
  }, []);
505
- const doPoll = useCallback2(async () => {
540
+ const onApiError = useCallback2(
541
+ (e) => {
542
+ const is429 = e instanceof ApiError && e.status === 429;
543
+ if (is429 && Date.now() >= pausedUntil.current) {
544
+ pausedUntil.current = Date.now() + 3e4;
545
+ pushLog("rate limited by api \xB7 backing off 30s", "warn");
546
+ } else if (!is429) {
547
+ pushLog(`api error \xB7 ${truncate(e instanceof Error ? e.message : String(e), 40)}`, "bad");
548
+ }
549
+ },
550
+ [pushLog]
551
+ );
552
+ const paused = () => Date.now() < pausedUntil.current;
553
+ const pollLocal = useCallback2(() => {
554
+ const lines = tailLines(LOCAL_LOG);
555
+ if (!lines.length) {
556
+ setLocalOn((prev) => prev === null ? false : prev);
557
+ return;
558
+ }
559
+ setLocalOn(true);
560
+ const fresh = [];
561
+ for (const line of lines) {
562
+ try {
563
+ const j = JSON.parse(line);
564
+ const at = Date.parse(j.ts ?? "");
565
+ if (!Number.isFinite(at) || at <= lastLocalTs.current) continue;
566
+ fresh.push({
567
+ id: "l:" + at + ":" + (j.tool ?? ""),
568
+ at,
569
+ tool: j.tool ?? "?",
570
+ decision: j.decision ?? "ALLOW",
571
+ permission: "",
572
+ detail: (j.arguments ? JSON.stringify(j.arguments) : j.reason ?? "").replace(/\s+/g, " "),
573
+ dlp: !!j.dlp,
574
+ source: "local"
575
+ });
576
+ } catch {
577
+ }
578
+ }
579
+ if (fresh.length) {
580
+ lastLocalTs.current = fresh[fresh.length - 1].at;
581
+ setLocalBuf((prev) => [...prev, ...fresh].slice(-400));
582
+ 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
+ }
586
+ }
587
+ const ringLines = tailLines(RING, 8192).slice(-30);
588
+ if (ringLines.length) {
589
+ let sum = 0;
590
+ let n = 0;
591
+ let session = "";
592
+ for (const rl2 of ringLines) {
593
+ try {
594
+ const j = JSON.parse(rl2);
595
+ if (typeof j.ms === "number") {
596
+ sum += j.ms;
597
+ n++;
598
+ }
599
+ if (j.session) session = j.session;
600
+ } catch {
601
+ }
602
+ }
603
+ if (n) setRing({ avgMs: Math.round(sum / n), session, count: n });
604
+ }
605
+ }, [pushLog]);
606
+ useEffect2(() => {
607
+ if (!active2) return;
608
+ pollLocal();
609
+ const t = setInterval(pollLocal, 2e3);
610
+ return () => clearInterval(t);
611
+ }, [active2, pollLocal]);
612
+ const pollFeed = useCallback2(async () => {
613
+ if (paused()) return;
506
614
  const t0 = Date.now();
507
615
  try {
508
- const [sd, fd, td] = await Promise.all([api.stats.get(), api.audit.list({ limit: 50 }), api.stats.timeseries({ period: "24h" })]);
616
+ const fd = await api.audit.list({ limit: 50 });
509
617
  const ms = Date.now() - t0;
510
- setS(sd);
511
- setTsData(td);
512
- setLat((l) => [...l.slice(-47), ms]);
618
+ setLat((l) => [...l, ms].slice(-240));
513
619
  const fresh = fd.entries.filter((e) => !seenRef.current.has(e.id));
620
+ const firstLoad = seenRef.current.size === 0 && fresh.length > 1;
514
621
  for (const e of fresh) seenRef.current.add(e.id);
515
- const firstLoad = seenRef.current.size === fresh.length && fresh.length > 1;
516
622
  if (fresh.length) {
517
- setBuffer((prev) => [...prev, ...fresh.sort((a, b) => Date.parse(a.created_at) - Date.parse(b.created_at))].slice(-400));
623
+ setCloudBuf((prev) => [...prev, ...fresh.map(cloudItem).sort((a, b) => a.at - b.at)].slice(-400));
518
624
  }
519
625
  const denies = fresh.filter((e) => e.decision !== "ALLOW").length;
520
- const dlp = fresh.filter((e) => e.dlp_matches?.length).length;
521
- if (firstLoad) pushLog(`link up \xB7 api ${ms}ms \xB7 loaded ${fresh.length} calls`);
522
- else
523
- pushLog(
524
- `api ${ms}ms \xB7 +${fresh.length} calls${denies ? ` \xB7 ${denies} DENIED` : ""}${dlp ? ` \xB7 ${dlp} DLP` : ""}`,
525
- denies || dlp ? "bad" : fresh.length ? "warn" : "ok"
526
- );
626
+ if (firstLoad) pushLog(`cloud link up \xB7 api ${ms}ms \xB7 ${fresh.length} calls`);
627
+ else pushLog(`api ${ms}ms \xB7 +${fresh.length} cloud${denies ? ` \xB7 ${denies} DENIED` : ""}`, denies ? "bad" : fresh.length ? "warn" : "ok");
527
628
  } catch (e) {
528
- pushLog(`api error \xB7 ${truncate(e instanceof Error ? e.message : String(e), 40)}`, "bad");
629
+ onApiError(e);
529
630
  }
530
- }, [pushLog]);
631
+ }, [pushLog, onApiError]);
632
+ const pollMedium = useCallback2(async () => {
633
+ if (paused()) return;
634
+ try {
635
+ setS(await api.stats.get());
636
+ } catch (e) {
637
+ onApiError(e);
638
+ }
639
+ }, [onApiError]);
640
+ const pollSlow = useCallback2(async () => {
641
+ if (paused()) return;
642
+ try {
643
+ setTsData(await api.stats.timeseries({ period: "24h" }));
644
+ } catch (e) {
645
+ onApiError(e);
646
+ }
647
+ }, [onApiError]);
531
648
  useEffect2(() => {
532
649
  if (!active2) return;
533
- void doPoll();
534
- const t = setInterval(() => void doPoll(), 2e3);
535
- return () => clearInterval(t);
536
- }, [active2, doPoll]);
650
+ void pollFeed();
651
+ void pollMedium();
652
+ void pollSlow();
653
+ const t1 = setInterval(() => void pollFeed(), 4e3);
654
+ const t2 = setInterval(() => void pollMedium(), 1e4);
655
+ const t3 = setInterval(() => void pollSlow(), 3e4);
656
+ return () => {
657
+ clearInterval(t1);
658
+ clearInterval(t2);
659
+ clearInterval(t3);
660
+ };
661
+ }, [active2, pollFeed, pollMedium, pollSlow]);
537
662
  const sessions = useLoader(() => api.agents.live({ limit: 30, includeDeactivated: true }));
538
663
  const insights = useLoader(() => api.stats.securityInsights(7));
539
- usePoll(sessions.reload, 5e3, active2);
540
- usePoll(insights.reload, 1e4, active2);
541
- useEffect2(() => {
542
- const c2 = sessions.data?.counts;
543
- if (c2) pushLog(`sessions ${c2.active}\u25B2 ${c2.idle}\u25CC ${c2.deactivated}\u25CB`);
544
- }, [sessions.data, pushLog]);
545
- useEffect2(() => {
546
- const ib = insights.data;
547
- if (ib?.layers) {
548
- const rl2 = ib.layers.rateLimit;
549
- const dl2 = ib.layers.dlp;
550
- pushLog(`layers \xB7 ratelimit ${rl2?.mode ?? "?"} ${rl2?.perMinute || "\u2014"}/m \xB7 dlp ${dl2?.mode ?? "?"} ${(dl2?.patterns ?? []).length} armed`);
551
- }
552
- }, [insights.data, pushLog]);
664
+ usePoll(() => {
665
+ if (!paused()) sessions.reload();
666
+ }, 1e4, active2);
667
+ usePoll(() => {
668
+ if (!paused()) insights.reload();
669
+ }, 3e4, active2);
553
670
  const [tick, setTick] = useState2(0);
554
671
  useEffect2(() => {
555
672
  if (!active2) return;
@@ -569,29 +686,33 @@ function LivePanel({ active: active2 }) {
569
686
  const minuteNow = (ins.activity?.minute ?? []).slice(-1)[0]?.count ?? 0;
570
687
  const dlpBars = (ins.dlpByPattern ?? []).slice(0, 2);
571
688
  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");
691
+ const lastDeny = denials[denials.length - 1];
572
692
  const toolCounts = /* @__PURE__ */ new Map();
573
- for (const e of buffer) toolCounts.set(e.tool_name, (toolCounts.get(e.tool_name) ?? 0) + 1);
693
+ for (const e of merged) toolCounts.set(e.tool, (toolCounts.get(e.tool) ?? 0) + 1);
574
694
  const topTools = [...toolCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3);
575
- const denials = buffer.filter((e) => e.decision !== "ALLOW");
576
- const lastDeny = denials[denials.length - 1];
577
695
  const latNow = lat[lat.length - 1] ?? 0;
578
- const latAvg = lat.length ? Math.round(lat.reduce((a, b) => a + b, 0) / lat.length) : 0;
696
+ const sortedLat = [...lat].sort((a, b) => a - b);
697
+ const latMed = sortedLat.length ? sortedLat[Math.floor(sortedLat.length / 2)] : 0;
698
+ const latHotAt = Math.max(2e3, latMed * 2.5);
699
+ const backingOff = Date.now() < pausedUntil.current;
579
700
  const sess = (sessions.data?.agents ?? []).slice().sort((a, b) => Date.parse(b.last_seen_at) - Date.parse(a.last_seen_at));
580
701
  const sessCounts = sessions.data?.counts;
581
702
  const chartH = rows >= 36 ? 6 : 4;
582
703
  const colH = rows >= 32 ? 6 : 5;
583
- const streamRows = Math.max(4, rows - 6 - chartH - colH);
584
- const tail = buffer.slice(-streamRows);
704
+ const streamRows = Math.max(4, rows - 6 - chartH - colH - (localOn === false ? 1 : 0));
705
+ const tail = merged.slice(-streamRows);
585
706
  const innerW = cols - 2;
586
707
  const leftW = Math.floor(innerW * 0.55);
587
708
  const rightW = innerW - leftW - 2;
588
709
  const colW = Math.max(20, Math.floor((innerW - 4) / 3));
589
- const sessionDot = (st) => st === "active" ? { ch: "\u25CF", color: theme.ok } : st === "idle" ? { ch: "\u25D0", color: theme.warn } : { ch: "\u25CB", color: theme.dim };
710
+ const sessionDot = (st) => st === "active" ? { ch: "\u25CF", color: theme.ok } : st === "idle" ? { ch: "\u25CC", color: theme.warn } : { ch: "\u25CB", color: theme.dim };
590
711
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
591
712
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
592
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#1432A0", color: "#AAC8FA", bold: true, children: " \u26E8 SOLONGATE LIVE " }),
713
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#1432A0", color: "#AAC8FA", bold: true, children: " SOLONGATE LIVE " }),
593
714
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: ` ${spin} up ${fmtUp(Date.now() - startRef.current)} \xB7 ${hhmmss(Date.now())} \xB7 api ${latNow}ms ` }),
594
- lastDeny ? /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#3d1220", color: "#ff6b6b", bold: true, children: ` \u26A0 ${hhmmss(lastDeny.created_at)} ${lastDeny.tool_name} DENIED ` }) : /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#4f6db8", children: " \u2713 clean " })
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 " })
595
716
  ] }),
596
717
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
597
718
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "\u2502 calls " }),
@@ -600,14 +721,14 @@ function LivePanel({ active: active2 }) {
600
721
  /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: s ? s.allowed : "\xB7\xB7\xB7" }),
601
722
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 deny " }),
602
723
  /* @__PURE__ */ jsx2(Text2, { color: theme.bad, bold: true, children: s ? s.denied : "\xB7\xB7" }),
724
+ /* @__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" }),
603
726
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 ratelimit " }),
604
727
  /* @__PURE__ */ jsx2(Text2, { color: rl?.mode === "block" ? theme.ok : rl?.mode === "detect" ? theme.warn : theme.dim, children: rl?.mode ?? "?" }),
605
728
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 dlp " }),
606
729
  /* @__PURE__ */ jsx2(Text2, { color: dl?.mode === "block" ? theme.ok : dl?.mode === "detect" ? theme.warn : theme.dim, children: dl?.mode ?? "?" }),
607
730
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 sessions " }),
608
731
  /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: sessCounts ? `${sessCounts.active}\u25B2 ${sessCounts.idle}\u25CC ${sessCounts.deactivated}\u25CB` : "\xB7" }),
609
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 policies " }),
610
- /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: s ? s.active_policies : "\xB7" }),
611
732
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502" })
612
733
  ] }),
613
734
  /* @__PURE__ */ jsxs2(Box2, { children: [
@@ -616,8 +737,8 @@ function LivePanel({ active: active2 }) {
616
737
  /* @__PURE__ */ jsx2(ColumnChart, { series: traffic, hot: trafficHot, height: chartH, width: leftW, color: theme.accent })
617
738
  ] }),
618
739
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: rightW, children: [
619
- /* @__PURE__ */ jsx2(PaneTitle, { label: "API LATENCY", extra: `live \xB7 now ${latNow}ms \xB7 avg ${latAvg}ms`, width: rightW }),
620
- /* @__PURE__ */ jsx2(ColumnChart, { series: lat, hot: lat.map((v) => v > 800), height: chartH, width: rightW, color: "#82AAF0" })
740
+ /* @__PURE__ */ jsx2(PaneTitle, { label: "API LATENCY", extra: `now ${latNow}ms \xB7 med ${latMed}ms \xB7 amber >${Math.round(latHotAt)}ms`, width: rightW }),
741
+ /* @__PURE__ */ jsx2(ColumnChart, { series: lat, hot: lat.map((v) => v > latHotAt), height: chartH, width: rightW, color: "#82AAF0", hotColor: "#ffb454" })
621
742
  ] })
622
743
  ] }),
623
744
  /* @__PURE__ */ jsxs2(Box2, { children: [
@@ -637,12 +758,22 @@ function LivePanel({ active: active2 }) {
637
758
  " patterns armed"
638
759
  ] })
639
760
  ] }),
640
- 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" })
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: [
763
+ /* @__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
641
767
  ] }),
642
768
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, marginRight: 2, children: [
643
769
  /* @__PURE__ */ jsx2(PaneTitle, { label: "SESSIONS", extra: sessCounts ? `${sessCounts.active} live` : "", width: colW }),
644
- sess.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "scanning\u2026" }) : null,
645
- sess.slice(0, colH - 1).map((a) => {
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) => {
646
777
  const dot = sessionDot(a.status);
647
778
  return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
648
779
  /* @__PURE__ */ jsxs2(Text2, { color: dot.color, children: [
@@ -674,7 +805,11 @@ function LivePanel({ active: active2 }) {
674
805
  ] }, l.ts + ":" + i))
675
806
  ] })
676
807
  ] }),
677
- /* @__PURE__ */ jsx2(PaneTitle, { label: "TOOL STREAM", extra: `tail -f \xB7 ${buffer.length} buffered`, width: innerW }),
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,
678
813
  tail.length === 0 ? /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
679
814
  spin,
680
815
  " awaiting traffic\u2026"
@@ -682,18 +817,20 @@ function LivePanel({ active: active2 }) {
682
817
  tail.map((e) => /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
683
818
  /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
684
819
  "[",
685
- hhmmss(e.created_at),
686
- "] "
820
+ hhmmss(e.at),
821
+ " "
687
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: "] " }),
688
825
  /* @__PURE__ */ jsx2(Text2, { color: decisionColor(e.decision), bold: e.decision !== "ALLOW", children: e.decision.padEnd(6) }),
689
- /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: truncate(e.tool_name, 14).padEnd(15) }),
690
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: (e.permission ?? "").slice(0, 4).padEnd(5) }),
691
- e.dlp_matches?.length ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: "DLP! " }) : null,
692
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: (e.arguments_summary ? JSON.stringify(e.arguments_summary) : e.reason ?? "").replace(/\s+/g, " ") })
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 })
693
830
  ] }, e.id)),
694
831
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
695
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", bold: true, children: " \u25B2 LIVE " }),
696
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#0b1530", color: "#4f6db8", children: ` top ${topTools.map(([t, c2]) => `${t}\xD7${c2}`).join(" ") || "\u2014"} \xB7 buf ${buffer.length} ` }),
832
+ /* @__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} ` }),
697
834
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: " esc menu \xB7 q quit " })
698
835
  ] })
699
836
  ] });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.66.0",
3
+ "version": "0.67.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": {