@solongate/proxy 0.66.1 → 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,9 +7104,13 @@ 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);
7082
7114
  const pausedUntil = useRef2(0);
7083
7115
  const pushLog = useCallback2((msg, level = "ok") => {
7084
7116
  setLog((prev) => [...prev.slice(-59), { ts: Date.now(), msg, level }]);
@@ -7096,6 +7128,65 @@ function LivePanel({ active: active2 }) {
7096
7128
  [pushLog]
7097
7129
  );
7098
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]);
7099
7190
  const pollFeed = useCallback2(async () => {
7100
7191
  if (paused()) return;
7101
7192
  const t0 = Date.now();
@@ -7107,16 +7198,11 @@ function LivePanel({ active: active2 }) {
7107
7198
  const firstLoad = seenRef.current.size === 0 && fresh.length > 1;
7108
7199
  for (const e of fresh) seenRef.current.add(e.id);
7109
7200
  if (fresh.length) {
7110
- 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));
7111
7202
  }
7112
7203
  const denies = fresh.filter((e) => e.decision !== "ALLOW").length;
7113
- const dlp = fresh.filter((e) => e.dlp_matches?.length).length;
7114
- if (firstLoad) pushLog(`link up \xB7 api ${ms}ms \xB7 loaded ${fresh.length} calls`);
7115
- else
7116
- pushLog(
7117
- `api ${ms}ms \xB7 +${fresh.length} calls${denies ? ` \xB7 ${denies} DENIED` : ""}${dlp ? ` \xB7 ${dlp} DLP` : ""}`,
7118
- denies || dlp ? "bad" : fresh.length ? "warn" : "ok"
7119
- );
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");
7120
7206
  } catch (e) {
7121
7207
  onApiError(e);
7122
7208
  }
@@ -7159,18 +7245,6 @@ function LivePanel({ active: active2 }) {
7159
7245
  usePoll(() => {
7160
7246
  if (!paused()) insights.reload();
7161
7247
  }, 3e4, active2);
7162
- useEffect2(() => {
7163
- const c2 = sessions.data?.counts;
7164
- if (c2) pushLog(`sessions ${c2.active}\u25B2 ${c2.idle}\u25CC ${c2.deactivated}\u25CB`);
7165
- }, [sessions.data, pushLog]);
7166
- useEffect2(() => {
7167
- const ib = insights.data;
7168
- if (ib?.layers) {
7169
- const rl2 = ib.layers.rateLimit;
7170
- const dl2 = ib.layers.dlp;
7171
- pushLog(`layers \xB7 ratelimit ${rl2?.mode ?? "?"} ${rl2?.perMinute || "\u2014"}/m \xB7 dlp ${dl2?.mode ?? "?"} ${(dl2?.patterns ?? []).length} armed`);
7172
- }
7173
- }, [insights.data, pushLog]);
7174
7248
  const [tick, setTick] = useState2(0);
7175
7249
  useEffect2(() => {
7176
7250
  if (!active2) return;
@@ -7190,31 +7264,33 @@ function LivePanel({ active: active2 }) {
7190
7264
  const minuteNow = (ins.activity?.minute ?? []).slice(-1)[0]?.count ?? 0;
7191
7265
  const dlpBars = (ins.dlpByPattern ?? []).slice(0, 2);
7192
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];
7193
7270
  const toolCounts = /* @__PURE__ */ new Map();
7194
- 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);
7195
7272
  const topTools = [...toolCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3);
7196
- const denials = buffer.filter((e) => e.decision !== "ALLOW");
7197
- const lastDeny = denials[denials.length - 1];
7198
7273
  const latNow = lat[lat.length - 1] ?? 0;
7199
- const latAvg = lat.length ? Math.round(lat.reduce((a, b) => a + b, 0) / lat.length) : 0;
7200
- const latHotAt = Math.max(1500, latAvg * 1.8);
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);
7201
7277
  const backingOff = Date.now() < pausedUntil.current;
7202
7278
  const sess = (sessions.data?.agents ?? []).slice().sort((a, b) => Date.parse(b.last_seen_at) - Date.parse(a.last_seen_at));
7203
7279
  const sessCounts = sessions.data?.counts;
7204
7280
  const chartH = rows >= 36 ? 6 : 4;
7205
7281
  const colH = rows >= 32 ? 6 : 5;
7206
- const streamRows = Math.max(4, rows - 6 - chartH - colH);
7207
- 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);
7208
7284
  const innerW = cols - 2;
7209
7285
  const leftW = Math.floor(innerW * 0.55);
7210
7286
  const rightW = innerW - leftW - 2;
7211
7287
  const colW = Math.max(20, Math.floor((innerW - 4) / 3));
7212
- 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 };
7213
7289
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
7214
7290
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7215
- /* @__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 " }),
7216
7292
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: ` ${spin} up ${fmtUp(Date.now() - startRef.current)} \xB7 ${hhmmss(Date.now())} \xB7 api ${latNow}ms ` }),
7217
- backingOff ? /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#3d2a12", color: "#ffb454", bold: true, children: " \u23F8 RATE LIMITED \xB7 backing off " }) : 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 " })
7218
7294
  ] }),
7219
7295
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7220
7296
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "\u2502 calls " }),
@@ -7223,14 +7299,14 @@ function LivePanel({ active: active2 }) {
7223
7299
  /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: s ? s.allowed : "\xB7\xB7\xB7" }),
7224
7300
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 deny " }),
7225
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" }),
7226
7304
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 ratelimit " }),
7227
7305
  /* @__PURE__ */ jsx2(Text2, { color: rl?.mode === "block" ? theme.ok : rl?.mode === "detect" ? theme.warn : theme.dim, children: rl?.mode ?? "?" }),
7228
7306
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 dlp " }),
7229
7307
  /* @__PURE__ */ jsx2(Text2, { color: dl?.mode === "block" ? theme.ok : dl?.mode === "detect" ? theme.warn : theme.dim, children: dl?.mode ?? "?" }),
7230
7308
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 sessions " }),
7231
7309
  /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: sessCounts ? `${sessCounts.active}\u25B2 ${sessCounts.idle}\u25CC ${sessCounts.deactivated}\u25CB` : "\xB7" }),
7232
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 policies " }),
7233
- /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: s ? s.active_policies : "\xB7" }),
7234
7310
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502" })
7235
7311
  ] }),
7236
7312
  /* @__PURE__ */ jsxs2(Box2, { children: [
@@ -7239,8 +7315,8 @@ function LivePanel({ active: active2 }) {
7239
7315
  /* @__PURE__ */ jsx2(ColumnChart, { series: traffic, hot: trafficHot, height: chartH, width: leftW, color: theme.accent })
7240
7316
  ] }),
7241
7317
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: rightW, children: [
7242
- /* @__PURE__ */ jsx2(PaneTitle, { label: "API LATENCY", extra: `live \xB7 now ${latNow}ms \xB7 avg ${latAvg}ms \xB7 red >${Math.round(latHotAt)}ms`, width: rightW }),
7243
- /* @__PURE__ */ jsx2(ColumnChart, { series: lat, hot: lat.map((v) => v > latHotAt), 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" })
7244
7320
  ] })
7245
7321
  ] }),
7246
7322
  /* @__PURE__ */ jsxs2(Box2, { children: [
@@ -7260,12 +7336,22 @@ function LivePanel({ active: active2 }) {
7260
7336
  " patterns armed"
7261
7337
  ] })
7262
7338
  ] }),
7263
- 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
7264
7345
  ] }),
7265
7346
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, marginRight: 2, children: [
7266
7347
  /* @__PURE__ */ jsx2(PaneTitle, { label: "SESSIONS", extra: sessCounts ? `${sessCounts.active} live` : "", width: colW }),
7267
- sess.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "scanning\u2026" }) : null,
7268
- 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) => {
7269
7355
  const dot = sessionDot(a.status);
7270
7356
  return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7271
7357
  /* @__PURE__ */ jsxs2(Text2, { color: dot.color, children: [
@@ -7297,7 +7383,11 @@ function LivePanel({ active: active2 }) {
7297
7383
  ] }, l.ts + ":" + i))
7298
7384
  ] })
7299
7385
  ] }),
7300
- /* @__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,
7301
7391
  tail.length === 0 ? /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
7302
7392
  spin,
7303
7393
  " awaiting traffic\u2026"
@@ -7305,23 +7395,25 @@ function LivePanel({ active: active2 }) {
7305
7395
  tail.map((e) => /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7306
7396
  /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
7307
7397
  "[",
7308
- hhmmss(e.created_at),
7309
- "] "
7398
+ hhmmss(e.at),
7399
+ " "
7310
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: "] " }),
7311
7403
  /* @__PURE__ */ jsx2(Text2, { color: decisionColor(e.decision), bold: e.decision !== "ALLOW", children: e.decision.padEnd(6) }),
7312
- /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: truncate2(e.tool_name, 14).padEnd(15) }),
7313
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: (e.permission ?? "").slice(0, 4).padEnd(5) }),
7314
- e.dlp_matches?.length ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: "DLP! " }) : null,
7315
- /* @__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 })
7316
7408
  ] }, e.id)),
7317
7409
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7318
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", bold: true, children: " \u25B2 LIVE " }),
7319
- /* @__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} ` }),
7320
7412
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: " esc menu \xB7 q quit " })
7321
7413
  ] })
7322
7414
  ] });
7323
7415
  }
7324
- var SPIN, BG, hhmmss, fmtUp;
7416
+ var SPIN, BG, LOCAL_LOG, RING, hhmmss, fmtUp;
7325
7417
  var init_Live = __esm({
7326
7418
  "src/tui/panels/Live.tsx"() {
7327
7419
  "use strict";
@@ -7330,6 +7422,8 @@ var init_Live = __esm({
7330
7422
  init_theme();
7331
7423
  SPIN = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
7332
7424
  BG = "#12234f";
7425
+ LOCAL_LOG = join5(homedir3(), ".solongate", "local-logs", "solongate-audit.jsonl");
7426
+ RING = join5(process.cwd(), ".solongate", ".eval-ring.jsonl");
7333
7427
  hhmmss = (ts) => {
7334
7428
  const d = new Date(ts);
7335
7429
  return Number.isNaN(d.getTime()) ? "--:--:--" : d.toTimeString().slice(0, 8);
@@ -9001,8 +9095,8 @@ __export(global_install_exports, {
9001
9095
  unlockProtected: () => unlockProtected
9002
9096
  });
9003
9097
  import { readFileSync as readFileSync6, writeFileSync as writeFileSync3, existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
9004
- import { resolve as resolve4, join as join5, dirname } from "path";
9005
- import { homedir as homedir3 } from "os";
9098
+ import { resolve as resolve4, join as join6, dirname } from "path";
9099
+ import { homedir as homedir4 } from "os";
9006
9100
  import { fileURLToPath } from "url";
9007
9101
  import { createInterface } from "readline";
9008
9102
  import { execFileSync as execFileSync2 } from "child_process";
@@ -9065,10 +9159,10 @@ function unlockFile(file) {
9065
9159
  function protectedTargets() {
9066
9160
  const p = globalPaths();
9067
9161
  return [
9068
- join5(p.hooksDir, "guard.mjs"),
9069
- join5(p.hooksDir, "audit.mjs"),
9070
- join5(p.hooksDir, "stop.mjs"),
9071
- 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"),
9072
9166
  p.configPath,
9073
9167
  p.settingsPath
9074
9168
  ];
@@ -9080,25 +9174,25 @@ function unlockProtected() {
9080
9174
  for (const f of protectedTargets()) unlockFile(f);
9081
9175
  }
9082
9176
  function globalPaths() {
9083
- const home = homedir3();
9084
- const sgDir = join5(home, ".solongate");
9085
- const hooksDir = join5(sgDir, "hooks");
9086
- 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");
9087
9181
  return {
9088
9182
  home,
9089
9183
  sgDir,
9090
9184
  hooksDir,
9091
9185
  claudeDir,
9092
- settingsPath: join5(claudeDir, "settings.json"),
9093
- backupPath: join5(claudeDir, "settings.solongate.bak"),
9094
- 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")
9095
9189
  };
9096
9190
  }
9097
9191
  function readHook(filename) {
9098
- return readFileSync6(join5(HOOKS_DIR, filename), "utf-8");
9192
+ return readFileSync6(join6(HOOKS_DIR, filename), "utf-8");
9099
9193
  }
9100
9194
  function readGuard() {
9101
- const bundled = join5(HOOKS_DIR, "guard.bundled.mjs");
9195
+ const bundled = join6(HOOKS_DIR, "guard.bundled.mjs");
9102
9196
  return existsSync4(bundled) ? readFileSync6(bundled, "utf-8") : readHook("guard.mjs");
9103
9197
  }
9104
9198
  function ask(question) {
@@ -9153,7 +9247,7 @@ function shimTargets() {
9153
9247
  return [];
9154
9248
  }
9155
9249
  }
9156
- 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));
9157
9251
  }
9158
9252
  function writeShimBlock(file, block) {
9159
9253
  const re = new RegExp(escapeRe(SHIM_BEGIN) + "[\\s\\S]*?" + escapeRe(SHIM_END) + "\\r?\\n?", "g");
@@ -9224,10 +9318,10 @@ async function runGlobalInstall(opts = {}) {
9224
9318
  mkdirSync3(p.hooksDir, { recursive: true });
9225
9319
  mkdirSync3(p.claudeDir, { recursive: true });
9226
9320
  unlockProtected();
9227
- writeFileSync3(join5(p.hooksDir, "guard.mjs"), readGuard());
9228
- writeFileSync3(join5(p.hooksDir, "audit.mjs"), readHook("audit.mjs"));
9229
- writeFileSync3(join5(p.hooksDir, "stop.mjs"), readHook("stop.mjs"));
9230
- 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"));
9231
9325
  console.log(` Installed hooks \u2192 ${p.hooksDir}`);
9232
9326
  removeClaudeShim();
9233
9327
  writeFileSync3(p.configPath, JSON.stringify({ apiKey, apiUrl }, null, 2) + "\n");
@@ -9245,9 +9339,9 @@ async function runGlobalInstall(opts = {}) {
9245
9339
  existing = {};
9246
9340
  }
9247
9341
  }
9248
- const guardAbs = join5(p.hooksDir, "guard.mjs").replace(/\\/g, "/");
9249
- const auditAbs = join5(p.hooksDir, "audit.mjs").replace(/\\/g, "/");
9250
- 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, "/");
9251
9345
  const nodeBin = process.execPath.replace(/\\/g, "/");
9252
9346
  const merged = {
9253
9347
  ...existing,
@@ -9437,11 +9531,11 @@ import { createServer, request as httpRequest } from "http";
9437
9531
  import { request as httpsRequest } from "https";
9438
9532
  import { spawn as spawn2 } from "child_process";
9439
9533
  import { URL as URL2 } from "url";
9440
- 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";
9441
9535
  import { resolve as resolve5 } from "path";
9442
- import { homedir as homedir4 } from "os";
9536
+ import { homedir as homedir5 } from "os";
9443
9537
  function findCacheFile() {
9444
- const dir = resolve5(homedir4(), ".solongate");
9538
+ const dir = resolve5(homedir5(), ".solongate");
9445
9539
  const envSel = process.env.SOLONGATE_AGENT_ID;
9446
9540
  if (envSel) {
9447
9541
  const f = resolve5(dir, ".policy-cache-" + envSel.replace(/[^a-zA-Z0-9_-]/g, "_") + ".json");
@@ -9452,7 +9546,7 @@ function findCacheFile() {
9452
9546
  for (const name of readdirSync(dir)) {
9453
9547
  if (name.startsWith(".policy-cache-") && name.endsWith(".json")) {
9454
9548
  const full = resolve5(dir, name);
9455
- const ts = statSync(full).mtimeMs;
9549
+ const ts = statSync2(full).mtimeMs;
9456
9550
  if (ts > bestTs) {
9457
9551
  bestTs = ts;
9458
9552
  best = full;
@@ -9743,9 +9837,9 @@ __export(logs_server_exports, {
9743
9837
  runLogsServer: () => runLogsServer
9744
9838
  });
9745
9839
  import { createServer as createServer2 } from "http";
9746
- import { readFileSync as readFileSync8, statSync as statSync2 } from "fs";
9747
- import { resolve as resolve6, join as join6, isAbsolute } from "path";
9748
- 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";
9749
9843
  import { readdirSync as readdirSync2 } from "fs";
9750
9844
  function allowedOrigins() {
9751
9845
  const base = [
@@ -9762,15 +9856,15 @@ function resolveLocalLogDir(rawPath) {
9762
9856
  const dir = String(rawPath || "").trim().replace(/[\\/]+$/, "");
9763
9857
  if (!dir) return null;
9764
9858
  if (isAbsolute(dir)) return dir;
9765
- return resolve6(homedir5(), ".solongate", "local-logs");
9859
+ return resolve6(homedir6(), ".solongate", "local-logs");
9766
9860
  }
9767
9861
  async function findLogDir() {
9768
- const base = resolve6(homedir5(), ".solongate");
9862
+ const base = resolve6(homedir6(), ".solongate");
9769
9863
  try {
9770
9864
  const files = readdirSync2(base).filter((f) => f.startsWith(".policy-cache-") && f.endsWith(".json"));
9771
9865
  for (const f of files) {
9772
9866
  try {
9773
- const c2 = JSON.parse(readFileSync8(join6(base, f), "utf-8"));
9867
+ const c2 = JSON.parse(readFileSync8(join7(base, f), "utf-8"));
9774
9868
  const p = c2?.security?.localLogs?.path;
9775
9869
  if (typeof p === "string" && p.trim()) return { dir: resolveLocalLogDir(p), configured: p };
9776
9870
  } catch {
@@ -9779,7 +9873,7 @@ async function findLogDir() {
9779
9873
  } catch {
9780
9874
  }
9781
9875
  try {
9782
- const cfgRaw = readFileSync8(join6(base, "cloud-guard.json"), "utf-8");
9876
+ const cfgRaw = readFileSync8(join7(base, "cloud-guard.json"), "utf-8");
9783
9877
  const { apiKey, apiUrl } = JSON.parse(cfgRaw);
9784
9878
  if (apiKey) {
9785
9879
  const url = `${apiUrl || "https://api.solongate.com"}/api/v1/policies/active`;
@@ -9808,9 +9902,9 @@ function setCors(req, res) {
9808
9902
  }
9809
9903
  function fileInfo(dir) {
9810
9904
  if (!dir) return { file: null, exists: false, size: 0, mtimeMs: 0 };
9811
- const file = join6(dir, LOG_FILENAME);
9905
+ const file = join7(dir, LOG_FILENAME);
9812
9906
  try {
9813
- const st = statSync2(file);
9907
+ const st = statSync3(file);
9814
9908
  return { file, exists: true, size: st.size, mtimeMs: st.mtimeMs };
9815
9909
  } catch {
9816
9910
  return { file, exists: false, size: 0, mtimeMs: 0 };
@@ -10259,7 +10353,7 @@ var init_inject = __esm({
10259
10353
  // src/create.ts
10260
10354
  var create_exports = {};
10261
10355
  import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync5, existsSync as existsSync7 } from "fs";
10262
- import { resolve as resolve8, join as join7 } from "path";
10356
+ import { resolve as resolve8, join as join8 } from "path";
10263
10357
  import { execSync as execSync2 } from "child_process";
10264
10358
  function withSpinner(message, fn) {
10265
10359
  const frames = ["\u28FE", "\u28FD", "\u28FB", "\u28BF", "\u287F", "\u28DF", "\u28EF", "\u28F7"];
@@ -10339,7 +10433,7 @@ EXAMPLES
10339
10433
  }
10340
10434
  function createProject(dir, name, _policy) {
10341
10435
  writeFileSync5(
10342
- join7(dir, "package.json"),
10436
+ join8(dir, "package.json"),
10343
10437
  JSON.stringify(
10344
10438
  {
10345
10439
  name,
@@ -10369,7 +10463,7 @@ function createProject(dir, name, _policy) {
10369
10463
  ) + "\n"
10370
10464
  );
10371
10465
  writeFileSync5(
10372
- join7(dir, "tsconfig.json"),
10466
+ join8(dir, "tsconfig.json"),
10373
10467
  JSON.stringify(
10374
10468
  {
10375
10469
  compilerOptions: {
@@ -10389,9 +10483,9 @@ function createProject(dir, name, _policy) {
10389
10483
  2
10390
10484
  ) + "\n"
10391
10485
  );
10392
- mkdirSync4(join7(dir, "src"), { recursive: true });
10486
+ mkdirSync4(join8(dir, "src"), { recursive: true });
10393
10487
  writeFileSync5(
10394
- join7(dir, "src", "index.ts"),
10488
+ join8(dir, "src", "index.ts"),
10395
10489
  `#!/usr/bin/env node
10396
10490
 
10397
10491
  console.log = (...args: unknown[]) => {
@@ -10433,7 +10527,7 @@ console.log('Press Ctrl+C to stop.');
10433
10527
  `
10434
10528
  );
10435
10529
  writeFileSync5(
10436
- join7(dir, ".mcp.json"),
10530
+ join8(dir, ".mcp.json"),
10437
10531
  JSON.stringify(
10438
10532
  {
10439
10533
  mcpServers: {
@@ -10451,12 +10545,12 @@ console.log('Press Ctrl+C to stop.');
10451
10545
  ) + "\n"
10452
10546
  );
10453
10547
  writeFileSync5(
10454
- join7(dir, ".env"),
10548
+ join8(dir, ".env"),
10455
10549
  `SOLONGATE_API_KEY=sg_live_YOUR_KEY_HERE
10456
10550
  `
10457
10551
  );
10458
10552
  writeFileSync5(
10459
- join7(dir, ".gitignore"),
10553
+ join8(dir, ".gitignore"),
10460
10554
  `node_modules/
10461
10555
  dist/
10462
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,9 +526,13 @@ 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);
502
536
  const pausedUntil = useRef2(0);
503
537
  const pushLog = useCallback2((msg, level = "ok") => {
504
538
  setLog((prev) => [...prev.slice(-59), { ts: Date.now(), msg, level }]);
@@ -516,6 +550,65 @@ function LivePanel({ active: active2 }) {
516
550
  [pushLog]
517
551
  );
518
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]);
519
612
  const pollFeed = useCallback2(async () => {
520
613
  if (paused()) return;
521
614
  const t0 = Date.now();
@@ -527,16 +620,11 @@ function LivePanel({ active: active2 }) {
527
620
  const firstLoad = seenRef.current.size === 0 && fresh.length > 1;
528
621
  for (const e of fresh) seenRef.current.add(e.id);
529
622
  if (fresh.length) {
530
- 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));
531
624
  }
532
625
  const denies = fresh.filter((e) => e.decision !== "ALLOW").length;
533
- const dlp = fresh.filter((e) => e.dlp_matches?.length).length;
534
- if (firstLoad) pushLog(`link up \xB7 api ${ms}ms \xB7 loaded ${fresh.length} calls`);
535
- else
536
- pushLog(
537
- `api ${ms}ms \xB7 +${fresh.length} calls${denies ? ` \xB7 ${denies} DENIED` : ""}${dlp ? ` \xB7 ${dlp} DLP` : ""}`,
538
- denies || dlp ? "bad" : fresh.length ? "warn" : "ok"
539
- );
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");
540
628
  } catch (e) {
541
629
  onApiError(e);
542
630
  }
@@ -579,18 +667,6 @@ function LivePanel({ active: active2 }) {
579
667
  usePoll(() => {
580
668
  if (!paused()) insights.reload();
581
669
  }, 3e4, active2);
582
- useEffect2(() => {
583
- const c2 = sessions.data?.counts;
584
- if (c2) pushLog(`sessions ${c2.active}\u25B2 ${c2.idle}\u25CC ${c2.deactivated}\u25CB`);
585
- }, [sessions.data, pushLog]);
586
- useEffect2(() => {
587
- const ib = insights.data;
588
- if (ib?.layers) {
589
- const rl2 = ib.layers.rateLimit;
590
- const dl2 = ib.layers.dlp;
591
- pushLog(`layers \xB7 ratelimit ${rl2?.mode ?? "?"} ${rl2?.perMinute || "\u2014"}/m \xB7 dlp ${dl2?.mode ?? "?"} ${(dl2?.patterns ?? []).length} armed`);
592
- }
593
- }, [insights.data, pushLog]);
594
670
  const [tick, setTick] = useState2(0);
595
671
  useEffect2(() => {
596
672
  if (!active2) return;
@@ -610,31 +686,33 @@ function LivePanel({ active: active2 }) {
610
686
  const minuteNow = (ins.activity?.minute ?? []).slice(-1)[0]?.count ?? 0;
611
687
  const dlpBars = (ins.dlpByPattern ?? []).slice(0, 2);
612
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];
613
692
  const toolCounts = /* @__PURE__ */ new Map();
614
- 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);
615
694
  const topTools = [...toolCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3);
616
- const denials = buffer.filter((e) => e.decision !== "ALLOW");
617
- const lastDeny = denials[denials.length - 1];
618
695
  const latNow = lat[lat.length - 1] ?? 0;
619
- const latAvg = lat.length ? Math.round(lat.reduce((a, b) => a + b, 0) / lat.length) : 0;
620
- const latHotAt = Math.max(1500, latAvg * 1.8);
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);
621
699
  const backingOff = Date.now() < pausedUntil.current;
622
700
  const sess = (sessions.data?.agents ?? []).slice().sort((a, b) => Date.parse(b.last_seen_at) - Date.parse(a.last_seen_at));
623
701
  const sessCounts = sessions.data?.counts;
624
702
  const chartH = rows >= 36 ? 6 : 4;
625
703
  const colH = rows >= 32 ? 6 : 5;
626
- const streamRows = Math.max(4, rows - 6 - chartH - colH);
627
- 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);
628
706
  const innerW = cols - 2;
629
707
  const leftW = Math.floor(innerW * 0.55);
630
708
  const rightW = innerW - leftW - 2;
631
709
  const colW = Math.max(20, Math.floor((innerW - 4) / 3));
632
- 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 };
633
711
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
634
712
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
635
- /* @__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 " }),
636
714
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: ` ${spin} up ${fmtUp(Date.now() - startRef.current)} \xB7 ${hhmmss(Date.now())} \xB7 api ${latNow}ms ` }),
637
- backingOff ? /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#3d2a12", color: "#ffb454", bold: true, children: " \u23F8 RATE LIMITED \xB7 backing off " }) : 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 " })
638
716
  ] }),
639
717
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
640
718
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "\u2502 calls " }),
@@ -643,14 +721,14 @@ function LivePanel({ active: active2 }) {
643
721
  /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: s ? s.allowed : "\xB7\xB7\xB7" }),
644
722
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 deny " }),
645
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" }),
646
726
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 ratelimit " }),
647
727
  /* @__PURE__ */ jsx2(Text2, { color: rl?.mode === "block" ? theme.ok : rl?.mode === "detect" ? theme.warn : theme.dim, children: rl?.mode ?? "?" }),
648
728
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 dlp " }),
649
729
  /* @__PURE__ */ jsx2(Text2, { color: dl?.mode === "block" ? theme.ok : dl?.mode === "detect" ? theme.warn : theme.dim, children: dl?.mode ?? "?" }),
650
730
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 sessions " }),
651
731
  /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: sessCounts ? `${sessCounts.active}\u25B2 ${sessCounts.idle}\u25CC ${sessCounts.deactivated}\u25CB` : "\xB7" }),
652
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 policies " }),
653
- /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: s ? s.active_policies : "\xB7" }),
654
732
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502" })
655
733
  ] }),
656
734
  /* @__PURE__ */ jsxs2(Box2, { children: [
@@ -659,8 +737,8 @@ function LivePanel({ active: active2 }) {
659
737
  /* @__PURE__ */ jsx2(ColumnChart, { series: traffic, hot: trafficHot, height: chartH, width: leftW, color: theme.accent })
660
738
  ] }),
661
739
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: rightW, children: [
662
- /* @__PURE__ */ jsx2(PaneTitle, { label: "API LATENCY", extra: `live \xB7 now ${latNow}ms \xB7 avg ${latAvg}ms \xB7 red >${Math.round(latHotAt)}ms`, width: rightW }),
663
- /* @__PURE__ */ jsx2(ColumnChart, { series: lat, hot: lat.map((v) => v > latHotAt), 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" })
664
742
  ] })
665
743
  ] }),
666
744
  /* @__PURE__ */ jsxs2(Box2, { children: [
@@ -680,12 +758,22 @@ function LivePanel({ active: active2 }) {
680
758
  " patterns armed"
681
759
  ] })
682
760
  ] }),
683
- 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
684
767
  ] }),
685
768
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, marginRight: 2, children: [
686
769
  /* @__PURE__ */ jsx2(PaneTitle, { label: "SESSIONS", extra: sessCounts ? `${sessCounts.active} live` : "", width: colW }),
687
- sess.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "scanning\u2026" }) : null,
688
- 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) => {
689
777
  const dot = sessionDot(a.status);
690
778
  return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
691
779
  /* @__PURE__ */ jsxs2(Text2, { color: dot.color, children: [
@@ -717,7 +805,11 @@ function LivePanel({ active: active2 }) {
717
805
  ] }, l.ts + ":" + i))
718
806
  ] })
719
807
  ] }),
720
- /* @__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,
721
813
  tail.length === 0 ? /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
722
814
  spin,
723
815
  " awaiting traffic\u2026"
@@ -725,18 +817,20 @@ function LivePanel({ active: active2 }) {
725
817
  tail.map((e) => /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
726
818
  /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
727
819
  "[",
728
- hhmmss(e.created_at),
729
- "] "
820
+ hhmmss(e.at),
821
+ " "
730
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: "] " }),
731
825
  /* @__PURE__ */ jsx2(Text2, { color: decisionColor(e.decision), bold: e.decision !== "ALLOW", children: e.decision.padEnd(6) }),
732
- /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: truncate(e.tool_name, 14).padEnd(15) }),
733
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: (e.permission ?? "").slice(0, 4).padEnd(5) }),
734
- e.dlp_matches?.length ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: "DLP! " }) : null,
735
- /* @__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 })
736
830
  ] }, e.id)),
737
831
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
738
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", bold: true, children: " \u25B2 LIVE " }),
739
- /* @__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} ` }),
740
834
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: " esc menu \xB7 q quit " })
741
835
  ] })
742
836
  ] });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.66.1",
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": {