@solongate/proxy 0.66.1 → 0.68.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -7019,10 +7019,40 @@ var init_hooks = __esm({
7019
7019
  });
7020
7020
 
7021
7021
  // src/tui/panels/Live.tsx
7022
- import { Box as Box2, Text as Text2 } from "ink";
7022
+ import { Box as Box2, Text as Text2, useInput } from "ink";
7023
+ 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,10 +7104,23 @@ 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);
7115
+ const [frozen, setFrozen] = useState2(false);
7116
+ const frozenRef = useRef2(false);
7117
+ frozenRef.current = frozen;
7118
+ useInput(
7119
+ (input) => {
7120
+ if (input === "p") setFrozen((f) => !f);
7121
+ },
7122
+ { isActive: active2 }
7123
+ );
7083
7124
  const pushLog = useCallback2((msg, level = "ok") => {
7084
7125
  setLog((prev) => [...prev.slice(-59), { ts: Date.now(), msg, level }]);
7085
7126
  }, []);
@@ -7095,7 +7136,69 @@ function LivePanel({ active: active2 }) {
7095
7136
  },
7096
7137
  [pushLog]
7097
7138
  );
7098
- const paused = () => Date.now() < pausedUntil.current;
7139
+ const paused = () => frozenRef.current || Date.now() < pausedUntil.current;
7140
+ const pollLocal = useCallback2(() => {
7141
+ if (frozenRef.current) return;
7142
+ const lines = tailLines(LOCAL_LOG);
7143
+ if (!lines.length) {
7144
+ setLocalOn((prev) => prev === null ? false : prev);
7145
+ return;
7146
+ }
7147
+ setLocalOn(true);
7148
+ const fresh = [];
7149
+ for (const line of lines) {
7150
+ try {
7151
+ const j = JSON.parse(line);
7152
+ const at = Date.parse(j.ts ?? "");
7153
+ if (!Number.isFinite(at) || at <= lastLocalTs.current) continue;
7154
+ fresh.push({
7155
+ id: "l:" + at + ":" + (j.tool ?? ""),
7156
+ at,
7157
+ tool: j.tool ?? "?",
7158
+ decision: j.decision ?? "ALLOW",
7159
+ permission: (j.permission ?? "").slice(0, 4),
7160
+ detail: (j.arguments ? JSON.stringify(j.arguments) : j.reason ?? "").replace(/\s+/g, " "),
7161
+ dlp: !!j.dlp,
7162
+ source: "local",
7163
+ session: j.session_id,
7164
+ agent: j.agent_name
7165
+ });
7166
+ } catch {
7167
+ }
7168
+ }
7169
+ if (fresh.length) {
7170
+ lastLocalTs.current = fresh[fresh.length - 1].at;
7171
+ setLocalBuf((prev) => [...prev, ...fresh].slice(-400));
7172
+ const denies = fresh.filter((f) => f.decision !== "ALLOW").length;
7173
+ if (lastLocalTs.current !== fresh[0].at || fresh.length < 10) {
7174
+ pushLog(`local +${fresh.length} calls${denies ? ` \xB7 ${denies} DENIED` : ""}`, denies ? "bad" : "warn");
7175
+ }
7176
+ }
7177
+ const ringLines = tailLines(RING, 8192).slice(-30);
7178
+ if (ringLines.length) {
7179
+ let sum = 0;
7180
+ let n = 0;
7181
+ let session = "";
7182
+ for (const rl2 of ringLines) {
7183
+ try {
7184
+ const j = JSON.parse(rl2);
7185
+ if (typeof j.ms === "number") {
7186
+ sum += j.ms;
7187
+ n++;
7188
+ }
7189
+ if (j.session) session = j.session;
7190
+ } catch {
7191
+ }
7192
+ }
7193
+ if (n) setRing({ avgMs: Math.round(sum / n), session, count: n });
7194
+ }
7195
+ }, [pushLog]);
7196
+ useEffect2(() => {
7197
+ if (!active2) return;
7198
+ pollLocal();
7199
+ const t = setInterval(pollLocal, 2e3);
7200
+ return () => clearInterval(t);
7201
+ }, [active2, pollLocal]);
7099
7202
  const pollFeed = useCallback2(async () => {
7100
7203
  if (paused()) return;
7101
7204
  const t0 = Date.now();
@@ -7107,16 +7210,11 @@ function LivePanel({ active: active2 }) {
7107
7210
  const firstLoad = seenRef.current.size === 0 && fresh.length > 1;
7108
7211
  for (const e of fresh) seenRef.current.add(e.id);
7109
7212
  if (fresh.length) {
7110
- setBuffer((prev) => [...prev, ...fresh.sort((a, b) => Date.parse(a.created_at) - Date.parse(b.created_at))].slice(-400));
7213
+ setCloudBuf((prev) => [...prev, ...fresh.map(cloudItem).sort((a, b) => a.at - b.at)].slice(-400));
7111
7214
  }
7112
7215
  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
- );
7216
+ if (firstLoad) pushLog(`cloud link up \xB7 api ${ms}ms \xB7 ${fresh.length} calls`);
7217
+ else pushLog(`api ${ms}ms \xB7 +${fresh.length} cloud${denies ? ` \xB7 ${denies} DENIED` : ""}`, denies ? "bad" : fresh.length ? "warn" : "ok");
7120
7218
  } catch (e) {
7121
7219
  onApiError(e);
7122
7220
  }
@@ -7159,62 +7257,76 @@ function LivePanel({ active: active2 }) {
7159
7257
  usePoll(() => {
7160
7258
  if (!paused()) insights.reload();
7161
7259
  }, 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
7260
  const [tick, setTick] = useState2(0);
7175
7261
  useEffect2(() => {
7176
- if (!active2) return;
7262
+ if (!active2 || frozen) return;
7177
7263
  const t = setInterval(() => setTick((n) => n + 1), 200);
7178
7264
  return () => clearInterval(t);
7179
- }, [active2]);
7265
+ }, [active2, frozen]);
7180
7266
  const startRef = useRef2(Date.now());
7181
7267
  const cols = process.stdout.columns ?? 100;
7182
7268
  const rows = process.stdout.rows ?? 30;
7183
7269
  const ins = insights.data ?? {};
7184
7270
  const spin = SPIN[tick % SPIN.length];
7185
7271
  const points = tsData?.timeseries ?? [];
7186
- const traffic = points.map((p) => p.total);
7187
- const trafficHot = points.map((p) => p.denied > 0);
7272
+ const mergedAll = [...cloudBuf, ...localBuf].sort((a, b) => a.at - b.at);
7273
+ const nowMs = Date.now();
7274
+ const minuteCounts = new Array(60).fill(0);
7275
+ const minuteHot = new Array(60).fill(false);
7276
+ for (const e of mergedAll) {
7277
+ const idx = 59 - Math.floor((nowMs - e.at) / 6e4);
7278
+ if (idx >= 0 && idx < 60) {
7279
+ minuteCounts[idx]++;
7280
+ if (e.decision !== "ALLOW") minuteHot[idx] = true;
7281
+ }
7282
+ }
7283
+ const localTraffic = localOn === true && minuteCounts.some((c2) => c2 > 0);
7284
+ const traffic = localTraffic ? minuteCounts : points.map((p) => p.total);
7285
+ const trafficHot = localTraffic ? minuteHot : points.map((p) => p.denied > 0);
7286
+ const trafficLabel = localTraffic ? "calls/min \xB7 60m \xB7 local+cloud" : "24h \xB7 cloud";
7188
7287
  const rl = ins.layers?.rateLimit;
7189
7288
  const dl = ins.layers?.dlp;
7190
7289
  const minuteNow = (ins.activity?.minute ?? []).slice(-1)[0]?.count ?? 0;
7191
7290
  const dlpBars = (ins.dlpByPattern ?? []).slice(0, 2);
7192
7291
  const maxDlpBar = dlpBars[0]?.count ?? 1;
7292
+ const merged = mergedAll;
7293
+ const denials = merged.filter((e) => e.decision !== "ALLOW");
7294
+ const lastDeny = denials[denials.length - 1];
7193
7295
  const toolCounts = /* @__PURE__ */ new Map();
7194
- for (const e of buffer) toolCounts.set(e.tool_name, (toolCounts.get(e.tool_name) ?? 0) + 1);
7296
+ for (const e of merged) toolCounts.set(e.tool, (toolCounts.get(e.tool) ?? 0) + 1);
7195
7297
  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
7298
  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);
7299
+ const sortedLat = [...lat].sort((a, b) => a - b);
7300
+ const latMed = sortedLat.length ? sortedLat[Math.floor(sortedLat.length / 2)] : 0;
7301
+ const latHotAt = Math.max(2e3, latMed * 2.5);
7201
7302
  const backingOff = Date.now() < pausedUntil.current;
7202
7303
  const sess = (sessions.data?.agents ?? []).slice().sort((a, b) => Date.parse(b.last_seen_at) - Date.parse(a.last_seen_at));
7203
7304
  const sessCounts = sessions.data?.counts;
7305
+ const localSess = /* @__PURE__ */ new Map();
7306
+ for (const e of localBuf) {
7307
+ if (!e.session) continue;
7308
+ const cur = localSess.get(e.session) ?? { agent: e.agent ?? "local agent", calls: 0, denies: 0, lastAt: 0 };
7309
+ cur.calls++;
7310
+ if (e.decision !== "ALLOW") cur.denies++;
7311
+ cur.lastAt = Math.max(cur.lastAt, e.at);
7312
+ if (e.agent) cur.agent = e.agent;
7313
+ localSess.set(e.session, cur);
7314
+ }
7315
+ const localSessList = [...localSess.entries()].sort((a, b) => b[1].lastAt - a[1].lastAt);
7204
7316
  const chartH = rows >= 36 ? 6 : 4;
7205
7317
  const colH = rows >= 32 ? 6 : 5;
7206
- const streamRows = Math.max(4, rows - 6 - chartH - colH);
7207
- const tail = buffer.slice(-streamRows);
7318
+ const streamRows = Math.max(4, rows - 6 - chartH - colH - (localOn === false ? 1 : 0));
7319
+ const tail = merged.slice(-streamRows);
7208
7320
  const innerW = cols - 2;
7209
7321
  const leftW = Math.floor(innerW * 0.55);
7210
7322
  const rightW = innerW - leftW - 2;
7211
7323
  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 };
7324
+ const sessionDot = (st) => st === "active" ? { ch: "\u25CF", color: theme.ok } : st === "idle" ? { ch: "\u25CC", color: theme.warn } : { ch: "\u25CB", color: theme.dim };
7213
7325
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
7214
7326
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7215
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#1432A0", color: "#AAC8FA", bold: true, children: " \u26E8 SOLONGATE LIVE " }),
7327
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#1432A0", color: "#AAC8FA", bold: true, children: " SOLONGATE LIVE " }),
7216
7328
  /* @__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 " })
7329
+ frozen ? /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#3d2a12", color: "#ffb454", bold: true, children: " PAUSED \xB7 copy freely \xB7 p resume " }) : backingOff ? /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#3d2a12", color: "#ffb454", bold: true, children: " RATE LIMITED \xB7 backing off " }) : lastDeny ? /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#3d1220", color: "#ff6b6b", bold: true, children: ` \u26A0 ${hhmmss(lastDeny.at)} ${lastDeny.tool} DENIED ` }) : /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#4f6db8", children: " \u2713 clean " })
7218
7330
  ] }),
7219
7331
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7220
7332
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "\u2502 calls " }),
@@ -7223,24 +7335,24 @@ function LivePanel({ active: active2 }) {
7223
7335
  /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: s ? s.allowed : "\xB7\xB7\xB7" }),
7224
7336
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 deny " }),
7225
7337
  /* @__PURE__ */ jsx2(Text2, { color: theme.bad, bold: true, children: s ? s.denied : "\xB7\xB7" }),
7338
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 local " }),
7339
+ localOn ? /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: "\u2713 streaming" }) : /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "off" }),
7226
7340
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 ratelimit " }),
7227
7341
  /* @__PURE__ */ jsx2(Text2, { color: rl?.mode === "block" ? theme.ok : rl?.mode === "detect" ? theme.warn : theme.dim, children: rl?.mode ?? "?" }),
7228
7342
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 dlp " }),
7229
7343
  /* @__PURE__ */ jsx2(Text2, { color: dl?.mode === "block" ? theme.ok : dl?.mode === "detect" ? theme.warn : theme.dim, children: dl?.mode ?? "?" }),
7230
7344
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 sessions " }),
7231
7345
  /* @__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
7346
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502" })
7235
7347
  ] }),
7236
7348
  /* @__PURE__ */ jsxs2(Box2, { children: [
7237
7349
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: leftW, marginRight: 2, children: [
7238
- /* @__PURE__ */ jsx2(PaneTitle, { label: "TRAFFIC", extra: `24h \xB7 peak ${Math.max(0, ...traffic)} \xB7 red = had denials`, width: leftW }),
7350
+ /* @__PURE__ */ jsx2(PaneTitle, { label: "TRAFFIC", extra: `${trafficLabel} \xB7 peak ${Math.max(0, ...traffic)} \xB7 red = denials`, width: leftW }),
7239
7351
  /* @__PURE__ */ jsx2(ColumnChart, { series: traffic, hot: trafficHot, height: chartH, width: leftW, color: theme.accent })
7240
7352
  ] }),
7241
7353
  /* @__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" })
7354
+ /* @__PURE__ */ jsx2(PaneTitle, { label: "API LATENCY", extra: `now ${latNow}ms \xB7 med ${latMed}ms \xB7 amber >${Math.round(latHotAt)}ms`, width: rightW }),
7355
+ /* @__PURE__ */ jsx2(ColumnChart, { series: lat, hot: lat.map((v) => v > latHotAt), height: chartH, width: rightW, color: "#82AAF0", hotColor: "#ffb454" })
7244
7356
  ] })
7245
7357
  ] }),
7246
7358
  /* @__PURE__ */ jsxs2(Box2, { children: [
@@ -7260,18 +7372,45 @@ function LivePanel({ active: active2 }) {
7260
7372
  " patterns armed"
7261
7373
  ] })
7262
7374
  ] }),
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" })
7375
+ dlpBars.length ? dlpBars.map((d) => /* @__PURE__ */ jsx2(HBar, { label: truncate2(d.pattern, 10), value: d.count, max: maxDlpBar, width: Math.max(6, colW - 16), color: theme.bad }, d.pattern)) : /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " no dlp hits in last 7 days" }),
7376
+ ring ? /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7377
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "GUARD".padEnd(10) }),
7378
+ /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: "local \u2713" }),
7379
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ` eval avg ${ring.avgMs}ms \xB7 ${ring.count} recent` })
7380
+ ] }) : null
7264
7381
  ] }),
7265
7382
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, marginRight: 2, children: [
7266
- /* @__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) => {
7383
+ /* @__PURE__ */ jsx2(PaneTitle, { label: "SESSIONS", extra: `${localSessList.length} local \xB7 ${sessCounts?.active ?? 0} cloud live`, width: colW }),
7384
+ localSessList.length === 0 && sess.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "scanning\u2026" }) : null,
7385
+ localSessList.slice(0, Math.max(1, colH - 2)).map(([id, v]) => {
7386
+ const fresh = nowMs - v.lastAt < 9e4;
7387
+ const isMe = ring?.session === id;
7388
+ return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7389
+ /* @__PURE__ */ jsxs2(Text2, { color: fresh ? theme.ok : theme.dim, children: [
7390
+ fresh ? "\u25CF" : "\u25CB",
7391
+ " "
7392
+ ] }),
7393
+ /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: "LOC " }),
7394
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, bold: isMe, children: truncate2(isMe ? "this machine" : v.agent, 12).padEnd(13) }),
7395
+ /* @__PURE__ */ jsxs2(Text2, { children: [
7396
+ String(v.calls).padStart(4),
7397
+ "c "
7398
+ ] }),
7399
+ /* @__PURE__ */ jsxs2(Text2, { color: v.denies ? theme.bad : theme.dim, children: [
7400
+ String(v.denies).padStart(3),
7401
+ "d "
7402
+ ] }),
7403
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ago(v.lastAt) })
7404
+ ] }, id);
7405
+ }),
7406
+ sess.slice(0, Math.max(0, colH - 1 - Math.min(localSessList.length, Math.max(1, colH - 2)))).map((a) => {
7269
7407
  const dot = sessionDot(a.status);
7270
7408
  return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7271
7409
  /* @__PURE__ */ jsxs2(Text2, { color: dot.color, children: [
7272
7410
  dot.ch,
7273
7411
  " "
7274
7412
  ] }),
7413
+ /* @__PURE__ */ jsx2(Text2, { color: "#4f6db8", children: "CLD " }),
7275
7414
  /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: truncate2(a.agent_name ?? a.session_id, 12).padEnd(13) }),
7276
7415
  /* @__PURE__ */ jsxs2(Text2, { children: [
7277
7416
  String(a.total_calls).padStart(4),
@@ -7297,7 +7436,11 @@ function LivePanel({ active: active2 }) {
7297
7436
  ] }, l.ts + ":" + i))
7298
7437
  ] })
7299
7438
  ] }),
7300
- /* @__PURE__ */ jsx2(PaneTitle, { label: "TOOL STREAM", extra: `tail -f \xB7 ${buffer.length} buffered`, width: innerW }),
7439
+ /* @__PURE__ */ jsx2(PaneTitle, { label: "TOOL STREAM", extra: `local+cloud tail \xB7 ${merged.length} buffered`, width: innerW }),
7440
+ localOn === false ? /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7441
+ /* @__PURE__ */ jsx2(Text2, { color: theme.warn, children: "local logs off" }),
7442
+ /* @__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`)" })
7443
+ ] }) : null,
7301
7444
  tail.length === 0 ? /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
7302
7445
  spin,
7303
7446
  " awaiting traffic\u2026"
@@ -7305,23 +7448,25 @@ function LivePanel({ active: active2 }) {
7305
7448
  tail.map((e) => /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7306
7449
  /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
7307
7450
  "[",
7308
- hhmmss(e.created_at),
7309
- "] "
7451
+ hhmmss(e.at),
7452
+ " "
7310
7453
  ] }),
7454
+ /* @__PURE__ */ jsx2(Text2, { color: e.source === "local" ? theme.ok : "#4f6db8", children: e.source === "local" ? "LOC" : "CLD" }),
7455
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "] " }),
7311
7456
  /* @__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, " ") })
7457
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: truncate2(e.tool, 14).padEnd(15) }),
7458
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: e.permission.padEnd(5) }),
7459
+ e.dlp ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: "DLP! " }) : null,
7460
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: e.detail })
7316
7461
  ] }, e.id)),
7317
7462
  /* @__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} ` }),
7320
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: " esc menu \xB7 q quit " })
7463
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", bold: true, children: " LIVE " }),
7464
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#0b1530", color: "#4f6db8", children: ` local ${localOn ? "on" : "off"} \xB7 top ${topTools.map(([t, c2]) => `${t}\xD7${c2}`).join(" ") || "\u2014"} \xB7 buf ${merged.length} ` }),
7465
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: " p pause/copy \xB7 esc menu \xB7 q quit " })
7321
7466
  ] })
7322
7467
  ] });
7323
7468
  }
7324
- var SPIN, BG, hhmmss, fmtUp;
7469
+ var SPIN, BG, LOCAL_LOG, RING, hhmmss, fmtUp;
7325
7470
  var init_Live = __esm({
7326
7471
  "src/tui/panels/Live.tsx"() {
7327
7472
  "use strict";
@@ -7330,6 +7475,8 @@ var init_Live = __esm({
7330
7475
  init_theme();
7331
7476
  SPIN = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
7332
7477
  BG = "#12234f";
7478
+ LOCAL_LOG = join5(homedir3(), ".solongate", "local-logs", "solongate-audit.jsonl");
7479
+ RING = join5(process.cwd(), ".solongate", ".eval-ring.jsonl");
7333
7480
  hhmmss = (ts) => {
7334
7481
  const d = new Date(ts);
7335
7482
  return Number.isNaN(d.getTime()) ? "--:--:--" : d.toTimeString().slice(0, 8);
@@ -7343,7 +7490,7 @@ var init_Live = __esm({
7343
7490
  });
7344
7491
 
7345
7492
  // src/tui/panels/Policies.tsx
7346
- import { Box as Box3, Text as Text3, useInput } from "ink";
7493
+ import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
7347
7494
  import TextInput from "ink-text-input";
7348
7495
  import { useEffect as useEffect3, useState as useState3 } from "react";
7349
7496
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
@@ -7413,7 +7560,7 @@ function PoliciesPanel({ focused }) {
7413
7560
  setDirty(false);
7414
7561
  setStatus("discarded");
7415
7562
  };
7416
- useInput(
7563
+ useInput2(
7417
7564
  (input, key) => {
7418
7565
  if (view === "list") {
7419
7566
  if (key.upArrow) setPi((n) => Math.max(0, n - 1));
@@ -7588,7 +7735,7 @@ var init_Policies = __esm({
7588
7735
  });
7589
7736
 
7590
7737
  // src/tui/panels/RateLimit.tsx
7591
- import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
7738
+ import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
7592
7739
  import { useEffect as useEffect4, useState as useState4 } from "react";
7593
7740
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
7594
7741
  function ColoredSpark({ values, limit, width: width2 = 60 }) {
@@ -7636,7 +7783,7 @@ function RateLimitPanel({ focused }) {
7636
7783
  setStatus("\u2717 " + (e instanceof Error ? e.message : String(e)));
7637
7784
  }
7638
7785
  };
7639
- useInput2(
7786
+ useInput3(
7640
7787
  (input, key) => {
7641
7788
  const step = key.shift ? 10 : 1;
7642
7789
  if (key.upArrow) setFi((n) => (n - 1 + FIELDS2.length) % FIELDS2.length);
@@ -7728,7 +7875,7 @@ var init_RateLimit = __esm({
7728
7875
  });
7729
7876
 
7730
7877
  // src/tui/panels/Dlp.tsx
7731
- import { Box as Box5, Text as Text5, useInput as useInput3 } from "ink";
7878
+ import { Box as Box5, Text as Text5, useInput as useInput4 } from "ink";
7732
7879
  import TextInput2 from "ink-text-input";
7733
7880
  import { useEffect as useEffect5, useState as useState5 } from "react";
7734
7881
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
@@ -7772,7 +7919,7 @@ function DlpPanel({ focused }) {
7772
7919
  };
7773
7920
  const customCount = dlp?.custom.length ?? 0;
7774
7921
  const total = available.length + customCount;
7775
- useInput3(
7922
+ useInput4(
7776
7923
  (input, key) => {
7777
7924
  if (!dlp) return;
7778
7925
  if (key.upArrow) setSel((n) => Math.max(0, n - 1));
@@ -7954,7 +8101,7 @@ var init_Stats = __esm({
7954
8101
  });
7955
8102
 
7956
8103
  // src/tui/panels/Audit.tsx
7957
- import { Box as Box7, Text as Text7, useInput as useInput4 } from "ink";
8104
+ import { Box as Box7, Text as Text7, useInput as useInput5 } from "ink";
7958
8105
  import TextInput3 from "ink-text-input";
7959
8106
  import { useState as useState6 } from "react";
7960
8107
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
@@ -7983,7 +8130,7 @@ function AuditPanel({ active: active2, focused }) {
7983
8130
  () => view === "detail" && current?.session_id ? api.audit.list({ session_id: current.session_id, limit: 40 }) : Promise.resolve(null),
7984
8131
  [view, current?.session_id]
7985
8132
  );
7986
- useInput4(
8133
+ useInput5(
7987
8134
  (input, key) => {
7988
8135
  if (view === "detail") {
7989
8136
  if (key.leftArrow || key.escape) setView("list");
@@ -8133,7 +8280,7 @@ var init_Audit = __esm({
8133
8280
  });
8134
8281
 
8135
8282
  // src/tui/App.tsx
8136
- import { Box as Box8, Text as Text8, useApp, useInput as useInput5 } from "ink";
8283
+ import { Box as Box8, Text as Text8, useApp, useInput as useInput6 } from "ink";
8137
8284
  import { useState as useState7 } from "react";
8138
8285
  import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
8139
8286
  function LiveHint() {
@@ -8165,7 +8312,7 @@ function App() {
8165
8312
  const { exit } = useApp();
8166
8313
  const [section, setSection] = useState7(0);
8167
8314
  const [focus, setFocus] = useState7("nav");
8168
- useInput5((input, key) => {
8315
+ useInput6((input, key) => {
8169
8316
  if (focus === "nav") {
8170
8317
  if (key.upArrow) setSection((n) => (n - 1 + SECTIONS.length) % SECTIONS.length);
8171
8318
  else if (key.downArrow) setSection((n) => (n + 1) % SECTIONS.length);
@@ -9001,8 +9148,8 @@ __export(global_install_exports, {
9001
9148
  unlockProtected: () => unlockProtected
9002
9149
  });
9003
9150
  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";
9151
+ import { resolve as resolve4, join as join6, dirname } from "path";
9152
+ import { homedir as homedir4 } from "os";
9006
9153
  import { fileURLToPath } from "url";
9007
9154
  import { createInterface } from "readline";
9008
9155
  import { execFileSync as execFileSync2 } from "child_process";
@@ -9065,10 +9212,10 @@ function unlockFile(file) {
9065
9212
  function protectedTargets() {
9066
9213
  const p = globalPaths();
9067
9214
  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"),
9215
+ join6(p.hooksDir, "guard.mjs"),
9216
+ join6(p.hooksDir, "audit.mjs"),
9217
+ join6(p.hooksDir, "stop.mjs"),
9218
+ join6(p.hooksDir, "shield.mjs"),
9072
9219
  p.configPath,
9073
9220
  p.settingsPath
9074
9221
  ];
@@ -9080,25 +9227,25 @@ function unlockProtected() {
9080
9227
  for (const f of protectedTargets()) unlockFile(f);
9081
9228
  }
9082
9229
  function globalPaths() {
9083
- const home = homedir3();
9084
- const sgDir = join5(home, ".solongate");
9085
- const hooksDir = join5(sgDir, "hooks");
9086
- const claudeDir = join5(home, ".claude");
9230
+ const home = homedir4();
9231
+ const sgDir = join6(home, ".solongate");
9232
+ const hooksDir = join6(sgDir, "hooks");
9233
+ const claudeDir = join6(home, ".claude");
9087
9234
  return {
9088
9235
  home,
9089
9236
  sgDir,
9090
9237
  hooksDir,
9091
9238
  claudeDir,
9092
- settingsPath: join5(claudeDir, "settings.json"),
9093
- backupPath: join5(claudeDir, "settings.solongate.bak"),
9094
- configPath: join5(sgDir, "cloud-guard.json")
9239
+ settingsPath: join6(claudeDir, "settings.json"),
9240
+ backupPath: join6(claudeDir, "settings.solongate.bak"),
9241
+ configPath: join6(sgDir, "cloud-guard.json")
9095
9242
  };
9096
9243
  }
9097
9244
  function readHook(filename) {
9098
- return readFileSync6(join5(HOOKS_DIR, filename), "utf-8");
9245
+ return readFileSync6(join6(HOOKS_DIR, filename), "utf-8");
9099
9246
  }
9100
9247
  function readGuard() {
9101
- const bundled = join5(HOOKS_DIR, "guard.bundled.mjs");
9248
+ const bundled = join6(HOOKS_DIR, "guard.bundled.mjs");
9102
9249
  return existsSync4(bundled) ? readFileSync6(bundled, "utf-8") : readHook("guard.mjs");
9103
9250
  }
9104
9251
  function ask(question) {
@@ -9153,7 +9300,7 @@ function shimTargets() {
9153
9300
  return [];
9154
9301
  }
9155
9302
  }
9156
- return [".bashrc", ".zshrc", ".profile"].map((f) => join5(homedir3(), f)).filter((f) => existsSync4(f));
9303
+ return [".bashrc", ".zshrc", ".profile"].map((f) => join6(homedir4(), f)).filter((f) => existsSync4(f));
9157
9304
  }
9158
9305
  function writeShimBlock(file, block) {
9159
9306
  const re = new RegExp(escapeRe(SHIM_BEGIN) + "[\\s\\S]*?" + escapeRe(SHIM_END) + "\\r?\\n?", "g");
@@ -9224,10 +9371,10 @@ async function runGlobalInstall(opts = {}) {
9224
9371
  mkdirSync3(p.hooksDir, { recursive: true });
9225
9372
  mkdirSync3(p.claudeDir, { recursive: true });
9226
9373
  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"));
9374
+ writeFileSync3(join6(p.hooksDir, "guard.mjs"), readGuard());
9375
+ writeFileSync3(join6(p.hooksDir, "audit.mjs"), readHook("audit.mjs"));
9376
+ writeFileSync3(join6(p.hooksDir, "stop.mjs"), readHook("stop.mjs"));
9377
+ writeFileSync3(join6(p.hooksDir, "shield.mjs"), readHook("shield.mjs"));
9231
9378
  console.log(` Installed hooks \u2192 ${p.hooksDir}`);
9232
9379
  removeClaudeShim();
9233
9380
  writeFileSync3(p.configPath, JSON.stringify({ apiKey, apiUrl }, null, 2) + "\n");
@@ -9245,9 +9392,9 @@ async function runGlobalInstall(opts = {}) {
9245
9392
  existing = {};
9246
9393
  }
9247
9394
  }
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, "/");
9395
+ const guardAbs = join6(p.hooksDir, "guard.mjs").replace(/\\/g, "/");
9396
+ const auditAbs = join6(p.hooksDir, "audit.mjs").replace(/\\/g, "/");
9397
+ const stopAbs = join6(p.hooksDir, "stop.mjs").replace(/\\/g, "/");
9251
9398
  const nodeBin = process.execPath.replace(/\\/g, "/");
9252
9399
  const merged = {
9253
9400
  ...existing,
@@ -9437,11 +9584,11 @@ import { createServer, request as httpRequest } from "http";
9437
9584
  import { request as httpsRequest } from "https";
9438
9585
  import { spawn as spawn2 } from "child_process";
9439
9586
  import { URL as URL2 } from "url";
9440
- import { readFileSync as readFileSync7, existsSync as existsSync5, readdirSync, statSync } from "fs";
9587
+ import { readFileSync as readFileSync7, existsSync as existsSync5, readdirSync, statSync as statSync2 } from "fs";
9441
9588
  import { resolve as resolve5 } from "path";
9442
- import { homedir as homedir4 } from "os";
9589
+ import { homedir as homedir5 } from "os";
9443
9590
  function findCacheFile() {
9444
- const dir = resolve5(homedir4(), ".solongate");
9591
+ const dir = resolve5(homedir5(), ".solongate");
9445
9592
  const envSel = process.env.SOLONGATE_AGENT_ID;
9446
9593
  if (envSel) {
9447
9594
  const f = resolve5(dir, ".policy-cache-" + envSel.replace(/[^a-zA-Z0-9_-]/g, "_") + ".json");
@@ -9452,7 +9599,7 @@ function findCacheFile() {
9452
9599
  for (const name of readdirSync(dir)) {
9453
9600
  if (name.startsWith(".policy-cache-") && name.endsWith(".json")) {
9454
9601
  const full = resolve5(dir, name);
9455
- const ts = statSync(full).mtimeMs;
9602
+ const ts = statSync2(full).mtimeMs;
9456
9603
  if (ts > bestTs) {
9457
9604
  bestTs = ts;
9458
9605
  best = full;
@@ -9743,9 +9890,9 @@ __export(logs_server_exports, {
9743
9890
  runLogsServer: () => runLogsServer
9744
9891
  });
9745
9892
  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";
9893
+ import { readFileSync as readFileSync8, statSync as statSync3 } from "fs";
9894
+ import { resolve as resolve6, join as join7, isAbsolute } from "path";
9895
+ import { homedir as homedir6 } from "os";
9749
9896
  import { readdirSync as readdirSync2 } from "fs";
9750
9897
  function allowedOrigins() {
9751
9898
  const base = [
@@ -9762,15 +9909,15 @@ function resolveLocalLogDir(rawPath) {
9762
9909
  const dir = String(rawPath || "").trim().replace(/[\\/]+$/, "");
9763
9910
  if (!dir) return null;
9764
9911
  if (isAbsolute(dir)) return dir;
9765
- return resolve6(homedir5(), ".solongate", "local-logs");
9912
+ return resolve6(homedir6(), ".solongate", "local-logs");
9766
9913
  }
9767
9914
  async function findLogDir() {
9768
- const base = resolve6(homedir5(), ".solongate");
9915
+ const base = resolve6(homedir6(), ".solongate");
9769
9916
  try {
9770
9917
  const files = readdirSync2(base).filter((f) => f.startsWith(".policy-cache-") && f.endsWith(".json"));
9771
9918
  for (const f of files) {
9772
9919
  try {
9773
- const c2 = JSON.parse(readFileSync8(join6(base, f), "utf-8"));
9920
+ const c2 = JSON.parse(readFileSync8(join7(base, f), "utf-8"));
9774
9921
  const p = c2?.security?.localLogs?.path;
9775
9922
  if (typeof p === "string" && p.trim()) return { dir: resolveLocalLogDir(p), configured: p };
9776
9923
  } catch {
@@ -9779,7 +9926,7 @@ async function findLogDir() {
9779
9926
  } catch {
9780
9927
  }
9781
9928
  try {
9782
- const cfgRaw = readFileSync8(join6(base, "cloud-guard.json"), "utf-8");
9929
+ const cfgRaw = readFileSync8(join7(base, "cloud-guard.json"), "utf-8");
9783
9930
  const { apiKey, apiUrl } = JSON.parse(cfgRaw);
9784
9931
  if (apiKey) {
9785
9932
  const url = `${apiUrl || "https://api.solongate.com"}/api/v1/policies/active`;
@@ -9808,9 +9955,9 @@ function setCors(req, res) {
9808
9955
  }
9809
9956
  function fileInfo(dir) {
9810
9957
  if (!dir) return { file: null, exists: false, size: 0, mtimeMs: 0 };
9811
- const file = join6(dir, LOG_FILENAME);
9958
+ const file = join7(dir, LOG_FILENAME);
9812
9959
  try {
9813
- const st = statSync2(file);
9960
+ const st = statSync3(file);
9814
9961
  return { file, exists: true, size: st.size, mtimeMs: st.mtimeMs };
9815
9962
  } catch {
9816
9963
  return { file, exists: false, size: 0, mtimeMs: 0 };
@@ -10259,7 +10406,7 @@ var init_inject = __esm({
10259
10406
  // src/create.ts
10260
10407
  var create_exports = {};
10261
10408
  import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync5, existsSync as existsSync7 } from "fs";
10262
- import { resolve as resolve8, join as join7 } from "path";
10409
+ import { resolve as resolve8, join as join8 } from "path";
10263
10410
  import { execSync as execSync2 } from "child_process";
10264
10411
  function withSpinner(message, fn) {
10265
10412
  const frames = ["\u28FE", "\u28FD", "\u28FB", "\u28BF", "\u287F", "\u28DF", "\u28EF", "\u28F7"];
@@ -10339,7 +10486,7 @@ EXAMPLES
10339
10486
  }
10340
10487
  function createProject(dir, name, _policy) {
10341
10488
  writeFileSync5(
10342
- join7(dir, "package.json"),
10489
+ join8(dir, "package.json"),
10343
10490
  JSON.stringify(
10344
10491
  {
10345
10492
  name,
@@ -10369,7 +10516,7 @@ function createProject(dir, name, _policy) {
10369
10516
  ) + "\n"
10370
10517
  );
10371
10518
  writeFileSync5(
10372
- join7(dir, "tsconfig.json"),
10519
+ join8(dir, "tsconfig.json"),
10373
10520
  JSON.stringify(
10374
10521
  {
10375
10522
  compilerOptions: {
@@ -10389,9 +10536,9 @@ function createProject(dir, name, _policy) {
10389
10536
  2
10390
10537
  ) + "\n"
10391
10538
  );
10392
- mkdirSync4(join7(dir, "src"), { recursive: true });
10539
+ mkdirSync4(join8(dir, "src"), { recursive: true });
10393
10540
  writeFileSync5(
10394
- join7(dir, "src", "index.ts"),
10541
+ join8(dir, "src", "index.ts"),
10395
10542
  `#!/usr/bin/env node
10396
10543
 
10397
10544
  console.log = (...args: unknown[]) => {
@@ -10433,7 +10580,7 @@ console.log('Press Ctrl+C to stop.');
10433
10580
  `
10434
10581
  );
10435
10582
  writeFileSync5(
10436
- join7(dir, ".mcp.json"),
10583
+ join8(dir, ".mcp.json"),
10437
10584
  JSON.stringify(
10438
10585
  {
10439
10586
  mcpServers: {
@@ -10451,12 +10598,12 @@ console.log('Press Ctrl+C to stop.');
10451
10598
  ) + "\n"
10452
10599
  );
10453
10600
  writeFileSync5(
10454
- join7(dir, ".env"),
10601
+ join8(dir, ".env"),
10455
10602
  `SOLONGATE_API_KEY=sg_live_YOUR_KEY_HERE
10456
10603
  `
10457
10604
  );
10458
10605
  writeFileSync5(
10459
- join7(dir, ".gitignore"),
10606
+ join8(dir, ".gitignore"),
10460
10607
  `node_modules/
10461
10608
  dist/
10462
10609
  *.solongate-backup
package/dist/tui/index.js CHANGED
@@ -8,7 +8,7 @@ var __export = (target, all) => {
8
8
  import { render } from "ink";
9
9
 
10
10
  // src/tui/App.tsx
11
- import { Box as Box8, Text as Text8, useApp, useInput as useInput5 } from "ink";
11
+ import { Box as Box8, Text as Text8, useApp, useInput as useInput6 } from "ink";
12
12
  import { useState as useState7 } from "react";
13
13
 
14
14
  // src/cli-utils.ts
@@ -125,7 +125,10 @@ function KeyHints({ hints }) {
125
125
  }
126
126
 
127
127
  // src/tui/panels/Live.tsx
128
- import { Box as Box2, Text as Text2 } from "ink";
128
+ import { Box as Box2, Text as Text2, useInput } 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,10 +526,23 @@ 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);
537
+ const [frozen, setFrozen] = useState2(false);
538
+ const frozenRef = useRef2(false);
539
+ frozenRef.current = frozen;
540
+ useInput(
541
+ (input) => {
542
+ if (input === "p") setFrozen((f) => !f);
543
+ },
544
+ { isActive: active2 }
545
+ );
503
546
  const pushLog = useCallback2((msg, level = "ok") => {
504
547
  setLog((prev) => [...prev.slice(-59), { ts: Date.now(), msg, level }]);
505
548
  }, []);
@@ -515,7 +558,69 @@ function LivePanel({ active: active2 }) {
515
558
  },
516
559
  [pushLog]
517
560
  );
518
- const paused = () => Date.now() < pausedUntil.current;
561
+ const paused = () => frozenRef.current || Date.now() < pausedUntil.current;
562
+ const pollLocal = useCallback2(() => {
563
+ if (frozenRef.current) return;
564
+ const lines = tailLines(LOCAL_LOG);
565
+ if (!lines.length) {
566
+ setLocalOn((prev) => prev === null ? false : prev);
567
+ return;
568
+ }
569
+ setLocalOn(true);
570
+ const fresh = [];
571
+ for (const line of lines) {
572
+ try {
573
+ const j = JSON.parse(line);
574
+ const at = Date.parse(j.ts ?? "");
575
+ if (!Number.isFinite(at) || at <= lastLocalTs.current) continue;
576
+ fresh.push({
577
+ id: "l:" + at + ":" + (j.tool ?? ""),
578
+ at,
579
+ tool: j.tool ?? "?",
580
+ decision: j.decision ?? "ALLOW",
581
+ permission: (j.permission ?? "").slice(0, 4),
582
+ detail: (j.arguments ? JSON.stringify(j.arguments) : j.reason ?? "").replace(/\s+/g, " "),
583
+ dlp: !!j.dlp,
584
+ source: "local",
585
+ session: j.session_id,
586
+ agent: j.agent_name
587
+ });
588
+ } catch {
589
+ }
590
+ }
591
+ if (fresh.length) {
592
+ lastLocalTs.current = fresh[fresh.length - 1].at;
593
+ setLocalBuf((prev) => [...prev, ...fresh].slice(-400));
594
+ const denies = fresh.filter((f) => f.decision !== "ALLOW").length;
595
+ if (lastLocalTs.current !== fresh[0].at || fresh.length < 10) {
596
+ pushLog(`local +${fresh.length} calls${denies ? ` \xB7 ${denies} DENIED` : ""}`, denies ? "bad" : "warn");
597
+ }
598
+ }
599
+ const ringLines = tailLines(RING, 8192).slice(-30);
600
+ if (ringLines.length) {
601
+ let sum = 0;
602
+ let n = 0;
603
+ let session = "";
604
+ for (const rl2 of ringLines) {
605
+ try {
606
+ const j = JSON.parse(rl2);
607
+ if (typeof j.ms === "number") {
608
+ sum += j.ms;
609
+ n++;
610
+ }
611
+ if (j.session) session = j.session;
612
+ } catch {
613
+ }
614
+ }
615
+ if (n) setRing({ avgMs: Math.round(sum / n), session, count: n });
616
+ }
617
+ }, [pushLog]);
618
+ useEffect2(() => {
619
+ if (!active2) return;
620
+ pollLocal();
621
+ const t = setInterval(pollLocal, 2e3);
622
+ return () => clearInterval(t);
623
+ }, [active2, pollLocal]);
519
624
  const pollFeed = useCallback2(async () => {
520
625
  if (paused()) return;
521
626
  const t0 = Date.now();
@@ -527,16 +632,11 @@ function LivePanel({ active: active2 }) {
527
632
  const firstLoad = seenRef.current.size === 0 && fresh.length > 1;
528
633
  for (const e of fresh) seenRef.current.add(e.id);
529
634
  if (fresh.length) {
530
- setBuffer((prev) => [...prev, ...fresh.sort((a, b) => Date.parse(a.created_at) - Date.parse(b.created_at))].slice(-400));
635
+ setCloudBuf((prev) => [...prev, ...fresh.map(cloudItem).sort((a, b) => a.at - b.at)].slice(-400));
531
636
  }
532
637
  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
- );
638
+ if (firstLoad) pushLog(`cloud link up \xB7 api ${ms}ms \xB7 ${fresh.length} calls`);
639
+ else pushLog(`api ${ms}ms \xB7 +${fresh.length} cloud${denies ? ` \xB7 ${denies} DENIED` : ""}`, denies ? "bad" : fresh.length ? "warn" : "ok");
540
640
  } catch (e) {
541
641
  onApiError(e);
542
642
  }
@@ -579,62 +679,76 @@ function LivePanel({ active: active2 }) {
579
679
  usePoll(() => {
580
680
  if (!paused()) insights.reload();
581
681
  }, 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
682
  const [tick, setTick] = useState2(0);
595
683
  useEffect2(() => {
596
- if (!active2) return;
684
+ if (!active2 || frozen) return;
597
685
  const t = setInterval(() => setTick((n) => n + 1), 200);
598
686
  return () => clearInterval(t);
599
- }, [active2]);
687
+ }, [active2, frozen]);
600
688
  const startRef = useRef2(Date.now());
601
689
  const cols = process.stdout.columns ?? 100;
602
690
  const rows = process.stdout.rows ?? 30;
603
691
  const ins = insights.data ?? {};
604
692
  const spin = SPIN[tick % SPIN.length];
605
693
  const points = tsData?.timeseries ?? [];
606
- const traffic = points.map((p) => p.total);
607
- const trafficHot = points.map((p) => p.denied > 0);
694
+ const mergedAll = [...cloudBuf, ...localBuf].sort((a, b) => a.at - b.at);
695
+ const nowMs = Date.now();
696
+ const minuteCounts = new Array(60).fill(0);
697
+ const minuteHot = new Array(60).fill(false);
698
+ for (const e of mergedAll) {
699
+ const idx = 59 - Math.floor((nowMs - e.at) / 6e4);
700
+ if (idx >= 0 && idx < 60) {
701
+ minuteCounts[idx]++;
702
+ if (e.decision !== "ALLOW") minuteHot[idx] = true;
703
+ }
704
+ }
705
+ const localTraffic = localOn === true && minuteCounts.some((c2) => c2 > 0);
706
+ const traffic = localTraffic ? minuteCounts : points.map((p) => p.total);
707
+ const trafficHot = localTraffic ? minuteHot : points.map((p) => p.denied > 0);
708
+ const trafficLabel = localTraffic ? "calls/min \xB7 60m \xB7 local+cloud" : "24h \xB7 cloud";
608
709
  const rl = ins.layers?.rateLimit;
609
710
  const dl = ins.layers?.dlp;
610
711
  const minuteNow = (ins.activity?.minute ?? []).slice(-1)[0]?.count ?? 0;
611
712
  const dlpBars = (ins.dlpByPattern ?? []).slice(0, 2);
612
713
  const maxDlpBar = dlpBars[0]?.count ?? 1;
714
+ const merged = mergedAll;
715
+ const denials = merged.filter((e) => e.decision !== "ALLOW");
716
+ const lastDeny = denials[denials.length - 1];
613
717
  const toolCounts = /* @__PURE__ */ new Map();
614
- for (const e of buffer) toolCounts.set(e.tool_name, (toolCounts.get(e.tool_name) ?? 0) + 1);
718
+ for (const e of merged) toolCounts.set(e.tool, (toolCounts.get(e.tool) ?? 0) + 1);
615
719
  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
720
  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);
721
+ const sortedLat = [...lat].sort((a, b) => a - b);
722
+ const latMed = sortedLat.length ? sortedLat[Math.floor(sortedLat.length / 2)] : 0;
723
+ const latHotAt = Math.max(2e3, latMed * 2.5);
621
724
  const backingOff = Date.now() < pausedUntil.current;
622
725
  const sess = (sessions.data?.agents ?? []).slice().sort((a, b) => Date.parse(b.last_seen_at) - Date.parse(a.last_seen_at));
623
726
  const sessCounts = sessions.data?.counts;
727
+ const localSess = /* @__PURE__ */ new Map();
728
+ for (const e of localBuf) {
729
+ if (!e.session) continue;
730
+ const cur = localSess.get(e.session) ?? { agent: e.agent ?? "local agent", calls: 0, denies: 0, lastAt: 0 };
731
+ cur.calls++;
732
+ if (e.decision !== "ALLOW") cur.denies++;
733
+ cur.lastAt = Math.max(cur.lastAt, e.at);
734
+ if (e.agent) cur.agent = e.agent;
735
+ localSess.set(e.session, cur);
736
+ }
737
+ const localSessList = [...localSess.entries()].sort((a, b) => b[1].lastAt - a[1].lastAt);
624
738
  const chartH = rows >= 36 ? 6 : 4;
625
739
  const colH = rows >= 32 ? 6 : 5;
626
- const streamRows = Math.max(4, rows - 6 - chartH - colH);
627
- const tail = buffer.slice(-streamRows);
740
+ const streamRows = Math.max(4, rows - 6 - chartH - colH - (localOn === false ? 1 : 0));
741
+ const tail = merged.slice(-streamRows);
628
742
  const innerW = cols - 2;
629
743
  const leftW = Math.floor(innerW * 0.55);
630
744
  const rightW = innerW - leftW - 2;
631
745
  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 };
746
+ const sessionDot = (st) => st === "active" ? { ch: "\u25CF", color: theme.ok } : st === "idle" ? { ch: "\u25CC", color: theme.warn } : { ch: "\u25CB", color: theme.dim };
633
747
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
634
748
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
635
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#1432A0", color: "#AAC8FA", bold: true, children: " \u26E8 SOLONGATE LIVE " }),
749
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#1432A0", color: "#AAC8FA", bold: true, children: " SOLONGATE LIVE " }),
636
750
  /* @__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 " })
751
+ frozen ? /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#3d2a12", color: "#ffb454", bold: true, children: " PAUSED \xB7 copy freely \xB7 p resume " }) : backingOff ? /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#3d2a12", color: "#ffb454", bold: true, children: " RATE LIMITED \xB7 backing off " }) : lastDeny ? /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#3d1220", color: "#ff6b6b", bold: true, children: ` \u26A0 ${hhmmss(lastDeny.at)} ${lastDeny.tool} DENIED ` }) : /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#4f6db8", children: " \u2713 clean " })
638
752
  ] }),
639
753
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
640
754
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "\u2502 calls " }),
@@ -643,24 +757,24 @@ function LivePanel({ active: active2 }) {
643
757
  /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: s ? s.allowed : "\xB7\xB7\xB7" }),
644
758
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 deny " }),
645
759
  /* @__PURE__ */ jsx2(Text2, { color: theme.bad, bold: true, children: s ? s.denied : "\xB7\xB7" }),
760
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 local " }),
761
+ localOn ? /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: "\u2713 streaming" }) : /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "off" }),
646
762
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 ratelimit " }),
647
763
  /* @__PURE__ */ jsx2(Text2, { color: rl?.mode === "block" ? theme.ok : rl?.mode === "detect" ? theme.warn : theme.dim, children: rl?.mode ?? "?" }),
648
764
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 dlp " }),
649
765
  /* @__PURE__ */ jsx2(Text2, { color: dl?.mode === "block" ? theme.ok : dl?.mode === "detect" ? theme.warn : theme.dim, children: dl?.mode ?? "?" }),
650
766
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 sessions " }),
651
767
  /* @__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
768
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502" })
655
769
  ] }),
656
770
  /* @__PURE__ */ jsxs2(Box2, { children: [
657
771
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: leftW, marginRight: 2, children: [
658
- /* @__PURE__ */ jsx2(PaneTitle, { label: "TRAFFIC", extra: `24h \xB7 peak ${Math.max(0, ...traffic)} \xB7 red = had denials`, width: leftW }),
772
+ /* @__PURE__ */ jsx2(PaneTitle, { label: "TRAFFIC", extra: `${trafficLabel} \xB7 peak ${Math.max(0, ...traffic)} \xB7 red = denials`, width: leftW }),
659
773
  /* @__PURE__ */ jsx2(ColumnChart, { series: traffic, hot: trafficHot, height: chartH, width: leftW, color: theme.accent })
660
774
  ] }),
661
775
  /* @__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" })
776
+ /* @__PURE__ */ jsx2(PaneTitle, { label: "API LATENCY", extra: `now ${latNow}ms \xB7 med ${latMed}ms \xB7 amber >${Math.round(latHotAt)}ms`, width: rightW }),
777
+ /* @__PURE__ */ jsx2(ColumnChart, { series: lat, hot: lat.map((v) => v > latHotAt), height: chartH, width: rightW, color: "#82AAF0", hotColor: "#ffb454" })
664
778
  ] })
665
779
  ] }),
666
780
  /* @__PURE__ */ jsxs2(Box2, { children: [
@@ -680,18 +794,45 @@ function LivePanel({ active: active2 }) {
680
794
  " patterns armed"
681
795
  ] })
682
796
  ] }),
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" })
797
+ dlpBars.length ? dlpBars.map((d) => /* @__PURE__ */ jsx2(HBar, { label: truncate(d.pattern, 10), value: d.count, max: maxDlpBar, width: Math.max(6, colW - 16), color: theme.bad }, d.pattern)) : /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " no dlp hits in last 7 days" }),
798
+ ring ? /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
799
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "GUARD".padEnd(10) }),
800
+ /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: "local \u2713" }),
801
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ` eval avg ${ring.avgMs}ms \xB7 ${ring.count} recent` })
802
+ ] }) : null
684
803
  ] }),
685
804
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, marginRight: 2, children: [
686
- /* @__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) => {
805
+ /* @__PURE__ */ jsx2(PaneTitle, { label: "SESSIONS", extra: `${localSessList.length} local \xB7 ${sessCounts?.active ?? 0} cloud live`, width: colW }),
806
+ localSessList.length === 0 && sess.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "scanning\u2026" }) : null,
807
+ localSessList.slice(0, Math.max(1, colH - 2)).map(([id, v]) => {
808
+ const fresh = nowMs - v.lastAt < 9e4;
809
+ const isMe = ring?.session === id;
810
+ return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
811
+ /* @__PURE__ */ jsxs2(Text2, { color: fresh ? theme.ok : theme.dim, children: [
812
+ fresh ? "\u25CF" : "\u25CB",
813
+ " "
814
+ ] }),
815
+ /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: "LOC " }),
816
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, bold: isMe, children: truncate(isMe ? "this machine" : v.agent, 12).padEnd(13) }),
817
+ /* @__PURE__ */ jsxs2(Text2, { children: [
818
+ String(v.calls).padStart(4),
819
+ "c "
820
+ ] }),
821
+ /* @__PURE__ */ jsxs2(Text2, { color: v.denies ? theme.bad : theme.dim, children: [
822
+ String(v.denies).padStart(3),
823
+ "d "
824
+ ] }),
825
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ago(v.lastAt) })
826
+ ] }, id);
827
+ }),
828
+ sess.slice(0, Math.max(0, colH - 1 - Math.min(localSessList.length, Math.max(1, colH - 2)))).map((a) => {
689
829
  const dot = sessionDot(a.status);
690
830
  return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
691
831
  /* @__PURE__ */ jsxs2(Text2, { color: dot.color, children: [
692
832
  dot.ch,
693
833
  " "
694
834
  ] }),
835
+ /* @__PURE__ */ jsx2(Text2, { color: "#4f6db8", children: "CLD " }),
695
836
  /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: truncate(a.agent_name ?? a.session_id, 12).padEnd(13) }),
696
837
  /* @__PURE__ */ jsxs2(Text2, { children: [
697
838
  String(a.total_calls).padStart(4),
@@ -717,7 +858,11 @@ function LivePanel({ active: active2 }) {
717
858
  ] }, l.ts + ":" + i))
718
859
  ] })
719
860
  ] }),
720
- /* @__PURE__ */ jsx2(PaneTitle, { label: "TOOL STREAM", extra: `tail -f \xB7 ${buffer.length} buffered`, width: innerW }),
861
+ /* @__PURE__ */ jsx2(PaneTitle, { label: "TOOL STREAM", extra: `local+cloud tail \xB7 ${merged.length} buffered`, width: innerW }),
862
+ localOn === false ? /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
863
+ /* @__PURE__ */ jsx2(Text2, { color: theme.warn, children: "local logs off" }),
864
+ /* @__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`)" })
865
+ ] }) : null,
721
866
  tail.length === 0 ? /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
722
867
  spin,
723
868
  " awaiting traffic\u2026"
@@ -725,25 +870,27 @@ function LivePanel({ active: active2 }) {
725
870
  tail.map((e) => /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
726
871
  /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
727
872
  "[",
728
- hhmmss(e.created_at),
729
- "] "
873
+ hhmmss(e.at),
874
+ " "
730
875
  ] }),
876
+ /* @__PURE__ */ jsx2(Text2, { color: e.source === "local" ? theme.ok : "#4f6db8", children: e.source === "local" ? "LOC" : "CLD" }),
877
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "] " }),
731
878
  /* @__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, " ") })
879
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: truncate(e.tool, 14).padEnd(15) }),
880
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: e.permission.padEnd(5) }),
881
+ e.dlp ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: "DLP! " }) : null,
882
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: e.detail })
736
883
  ] }, e.id)),
737
884
  /* @__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} ` }),
740
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: " esc menu \xB7 q quit " })
885
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", bold: true, children: " LIVE " }),
886
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#0b1530", color: "#4f6db8", children: ` local ${localOn ? "on" : "off"} \xB7 top ${topTools.map(([t, c2]) => `${t}\xD7${c2}`).join(" ") || "\u2014"} \xB7 buf ${merged.length} ` }),
887
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: " p pause/copy \xB7 esc menu \xB7 q quit " })
741
888
  ] })
742
889
  ] });
743
890
  }
744
891
 
745
892
  // src/tui/panels/Policies.tsx
746
- import { Box as Box3, Text as Text3, useInput } from "ink";
893
+ import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
747
894
  import TextInput from "ink-text-input";
748
895
  import { useEffect as useEffect3, useState as useState3 } from "react";
749
896
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
@@ -834,7 +981,7 @@ function PoliciesPanel({ focused }) {
834
981
  setDirty(false);
835
982
  setStatus("discarded");
836
983
  };
837
- useInput(
984
+ useInput2(
838
985
  (input, key) => {
839
986
  if (view === "list") {
840
987
  if (key.upArrow) setPi((n) => Math.max(0, n - 1));
@@ -978,7 +1125,7 @@ function PoliciesPanel({ focused }) {
978
1125
  }
979
1126
 
980
1127
  // src/tui/panels/RateLimit.tsx
981
- import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
1128
+ import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
982
1129
  import { useEffect as useEffect4, useState as useState4 } from "react";
983
1130
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
984
1131
  var MODES = ["off", "detect", "block"];
@@ -1029,7 +1176,7 @@ function RateLimitPanel({ focused }) {
1029
1176
  setStatus("\u2717 " + (e instanceof Error ? e.message : String(e)));
1030
1177
  }
1031
1178
  };
1032
- useInput2(
1179
+ useInput3(
1033
1180
  (input, key) => {
1034
1181
  const step = key.shift ? 10 : 1;
1035
1182
  if (key.upArrow) setFi((n) => (n - 1 + FIELDS2.length) % FIELDS2.length);
@@ -1108,7 +1255,7 @@ function FieldRow({ label, active: active2, children }) {
1108
1255
  }
1109
1256
 
1110
1257
  // src/tui/panels/Dlp.tsx
1111
- import { Box as Box5, Text as Text5, useInput as useInput3 } from "ink";
1258
+ import { Box as Box5, Text as Text5, useInput as useInput4 } from "ink";
1112
1259
  import TextInput2 from "ink-text-input";
1113
1260
  import { useEffect as useEffect5, useState as useState5 } from "react";
1114
1261
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
@@ -1153,7 +1300,7 @@ function DlpPanel({ focused }) {
1153
1300
  };
1154
1301
  const customCount = dlp?.custom.length ?? 0;
1155
1302
  const total = available.length + customCount;
1156
- useInput3(
1303
+ useInput4(
1157
1304
  (input, key) => {
1158
1305
  if (!dlp) return;
1159
1306
  if (key.upArrow) setSel((n) => Math.max(0, n - 1));
@@ -1315,7 +1462,7 @@ function StatsPanel({ active: active2 }) {
1315
1462
  }
1316
1463
 
1317
1464
  // src/tui/panels/Audit.tsx
1318
- import { Box as Box7, Text as Text7, useInput as useInput4 } from "ink";
1465
+ import { Box as Box7, Text as Text7, useInput as useInput5 } from "ink";
1319
1466
  import TextInput3 from "ink-text-input";
1320
1467
  import { useState as useState6 } from "react";
1321
1468
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
@@ -1346,7 +1493,7 @@ function AuditPanel({ active: active2, focused }) {
1346
1493
  () => view === "detail" && current?.session_id ? api.audit.list({ session_id: current.session_id, limit: 40 }) : Promise.resolve(null),
1347
1494
  [view, current?.session_id]
1348
1495
  );
1349
- useInput4(
1496
+ useInput5(
1350
1497
  (input, key) => {
1351
1498
  if (view === "detail") {
1352
1499
  if (key.leftArrow || key.escape) setView("list");
@@ -1523,7 +1670,7 @@ function App() {
1523
1670
  const { exit } = useApp();
1524
1671
  const [section, setSection] = useState7(0);
1525
1672
  const [focus, setFocus] = useState7("nav");
1526
- useInput5((input, key) => {
1673
+ useInput6((input, key) => {
1527
1674
  if (focus === "nav") {
1528
1675
  if (key.upArrow) setSection((n) => (n - 1 + SECTIONS.length) % SECTIONS.length);
1529
1676
  else if (key.downArrow) setSection((n) => (n + 1) % SECTIONS.length);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.66.1",
3
+ "version": "0.68.0",
4
4
  "description": "AI tool security proxy: protect any AI tool server with customizable policies, path/command constraints, rate limiting, and audit logging. No code changes required.",
5
5
  "type": "module",
6
6
  "bin": {