@solongate/proxy 0.62.0 → 0.64.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,78 +7020,205 @@ 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 { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
7023
7024
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
7024
- function Tile({ label, value, color }) {
7025
- return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, marginRight: 1, minWidth: 14, children: [
7026
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: label }),
7027
- /* @__PURE__ */ jsx2(Text2, { bold: true, color, children: value })
7025
+ function ColumnChart({ series, height, width: width2, color, limit = 0 }) {
7026
+ const tailVals = series.slice(-width2);
7027
+ const data = [...new Array(Math.max(0, width2 - tailVals.length)).fill(0), ...tailVals];
7028
+ const max = Math.max(1, ...data, limit);
7029
+ const lines = [];
7030
+ for (let r = height; r >= 1; r--) {
7031
+ const segs = [];
7032
+ for (let i = 0; i < data.length; i++) {
7033
+ const frac = data[i] / max;
7034
+ const ch = frac >= r / height ? "\u2588" : frac >= (r - 0.5) / height ? "\u2584" : r === 1 && data[i] > 0 ? "\u2581" : " ";
7035
+ const c2 = limit > 0 && data[i] > limit ? theme.bad : color;
7036
+ const last = segs[segs.length - 1];
7037
+ if (last && last.color === c2) last.text += ch;
7038
+ else segs.push({ text: ch, color: c2 });
7039
+ }
7040
+ lines.push(
7041
+ /* @__PURE__ */ jsx2(Text2, { children: segs.map((s, i) => /* @__PURE__ */ jsx2(Text2, { color: s.color, children: s.text }, i)) }, r)
7042
+ );
7043
+ }
7044
+ return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: lines });
7045
+ }
7046
+ function HBar({ label, value, max, width: width2, color }) {
7047
+ const filled = max > 0 ? Math.round(value / max * width2) : 0;
7048
+ return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7049
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: label.padEnd(9) }),
7050
+ /* @__PURE__ */ jsx2(Text2, { color, children: "\u2588".repeat(Math.min(width2, filled)) }),
7051
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "\u2591".repeat(Math.max(0, width2 - filled)) + " " + value })
7028
7052
  ] });
7029
7053
  }
7054
+ function SectionTitle({ children, width: width2 }) {
7055
+ const label = `\u2500\u2500 ${children} `;
7056
+ return /* @__PURE__ */ jsx2(Text2, { color: theme.dim, wrap: "truncate", children: label + "\u2500".repeat(Math.max(0, width2 - label.length)) });
7057
+ }
7030
7058
  function LivePanel({ active: active2 }) {
7031
7059
  const stats = useLoader(() => api.stats.get());
7032
7060
  const ts = useLoader(() => api.stats.timeseries({ period: "24h" }));
7033
- const feed = useLoader(() => api.audit.list({ limit: 12 }));
7061
+ const feed = useLoader(() => api.audit.list({ limit: 50 }));
7062
+ const insights = useLoader(() => api.stats.securityInsights(7));
7034
7063
  usePoll(() => {
7035
7064
  stats.reload();
7036
7065
  ts.reload();
7037
7066
  feed.reload();
7038
7067
  }, 2e3, active2);
7068
+ usePoll(insights.reload, 1e4, active2);
7069
+ const [tick, setTick] = useState2(0);
7070
+ useEffect2(() => {
7071
+ if (!active2) return;
7072
+ const t = setInterval(() => setTick((n) => n + 1), 200);
7073
+ return () => clearInterval(t);
7074
+ }, [active2]);
7075
+ const startRef = useRef2(Date.now());
7076
+ const [buffer, setBuffer] = useState2([]);
7077
+ useEffect2(() => {
7078
+ const incoming = feed.data?.entries;
7079
+ if (!incoming?.length) return;
7080
+ setBuffer((prev) => {
7081
+ const seen = new Set(prev.map((e) => e.id));
7082
+ const fresh = incoming.filter((e) => !seen.has(e.id)).sort((a, b) => Date.parse(a.created_at) - Date.parse(b.created_at));
7083
+ return fresh.length ? [...prev, ...fresh].slice(-400) : prev;
7084
+ });
7085
+ }, [feed.data]);
7086
+ const cols = process.stdout.columns ?? 100;
7087
+ const rows = process.stdout.rows ?? 30;
7039
7088
  const s = stats.data;
7040
- const points = ts.data?.timeseries ?? [];
7041
- const entries = feed.data?.entries ?? [];
7042
- const denyRate = s && s.total_calls > 0 ? Math.round(s.denied / s.total_calls * 100) : 0;
7043
- return /* @__PURE__ */ jsx2(DataView, { loading: stats.loading && !s, error: stats.error, children: /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
7044
- /* @__PURE__ */ jsxs2(Box2, { children: [
7045
- /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: "\u25CF" }),
7046
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " LIVE \xB7 refreshing every 2s" })
7089
+ const ins = insights.data ?? {};
7090
+ const minuteRaw = (ins.activity?.minute ?? []).map((b) => b.count);
7091
+ const hourRaw = (ins.activity?.hour ?? []).map((b) => b.count);
7092
+ const minuteLive = minuteRaw.some((c2) => c2 > 0);
7093
+ const minuteSeries = minuteLive ? minuteRaw : hourRaw;
7094
+ const activityLabel = minuteLive ? "calls/min \xB7 last 60m" : "calls/hour \xB7 last 24h";
7095
+ const limit = minuteLive ? ins.layers?.rateLimit?.perMinute ?? 0 : 0;
7096
+ const denySeries = (ts.data?.timeseries ?? []).map((p) => p.denied);
7097
+ const spin = SPIN[tick % SPIN.length];
7098
+ const cursor = tick % 4 < 2 ? "\u258C" : " ";
7099
+ const toolCounts = /* @__PURE__ */ new Map();
7100
+ const permCounts = /* @__PURE__ */ new Map();
7101
+ for (const e of buffer) {
7102
+ toolCounts.set(e.tool_name, (toolCounts.get(e.tool_name) ?? 0) + 1);
7103
+ const perm = (e.permission || "?").toUpperCase();
7104
+ permCounts.set(perm, (permCounts.get(perm) ?? 0) + 1);
7105
+ }
7106
+ const topTools = [...toolCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5);
7107
+ const maxTool = topTools[0]?.[1] ?? 1;
7108
+ const perms = ["READ", "WRITE", "EXECUTE", "NETWORK"].map((p) => [p, permCounts.get(p) ?? 0]);
7109
+ const maxPerm = Math.max(1, ...perms.map(([, c2]) => c2));
7110
+ const dlpTop = (ins.dlpByPattern ?? []).slice(0, 2);
7111
+ const denials = buffer.filter((e) => e.decision !== "ALLOW");
7112
+ const lastDeny = denials[denials.length - 1];
7113
+ const bursts = limit > 0 ? minuteSeries.filter((c2) => c2 > limit).length : 0;
7114
+ const bigH = rows >= 34 ? 6 : 4;
7115
+ const colH = rows >= 34 ? 6 : 5;
7116
+ const streamRows = Math.max(4, rows - (2 + 1 + bigH + 1 + colH + 1 + 1));
7117
+ const tail = buffer.slice(-streamRows);
7118
+ const colW = Math.max(18, Math.floor((cols - 6) / 3));
7119
+ if (stats.error) return /* @__PURE__ */ jsxs2(Text2, { color: theme.bad, children: [
7120
+ "\u2717 ",
7121
+ stats.error
7122
+ ] });
7123
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
7124
+ /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7125
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#1432A0", color: "#AAC8FA", bold: true, children: " SOLONGATE::LIVE " }),
7126
+ /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
7127
+ " ",
7128
+ spin,
7129
+ " up ",
7130
+ fmtUp(Date.now() - startRef.current),
7131
+ " \xB7 ",
7132
+ hhmmss(Date.now()),
7133
+ " \xB7 poll 2s"
7134
+ ] }),
7135
+ lastDeny ? /* @__PURE__ */ jsxs2(Text2, { color: theme.bad, children: [
7136
+ " \u26A0 ",
7137
+ hhmmss(lastDeny.created_at),
7138
+ " ",
7139
+ lastDeny.tool_name,
7140
+ " denied"
7141
+ ] }) : null,
7142
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " esc menu \xB7 q quit" })
7047
7143
  ] }),
7048
- s ? /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, children: [
7049
- /* @__PURE__ */ jsx2(Tile, { label: "total calls", value: String(s.total_calls) }),
7050
- /* @__PURE__ */ jsx2(Tile, { label: "allowed", value: String(s.allowed), color: theme.ok }),
7051
- /* @__PURE__ */ jsx2(Tile, { label: "denied", value: String(s.denied), color: theme.bad }),
7052
- /* @__PURE__ */ jsx2(Tile, { label: "deny rate", value: denyRate + "%", color: denyRate > 10 ? theme.bad : void 0 }),
7053
- /* @__PURE__ */ jsx2(Tile, { label: "policies", value: String(s.active_policies), color: theme.accent })
7054
- ] }) : null,
7055
- /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
7056
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "Activity \xB7 24h" }),
7057
- /* @__PURE__ */ jsxs2(Box2, { children: [
7058
- /* @__PURE__ */ jsx2(Text2, { children: "calls " }),
7059
- /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: sparkline(points.map((p) => p.total), 60) })
7144
+ /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7145
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "calls " }),
7146
+ /* @__PURE__ */ jsx2(Text2, { bold: true, children: s ? s.total_calls : "\xB7\xB7\xB7\xB7" }),
7147
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " allow " }),
7148
+ /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: s ? s.allowed : "\xB7\xB7\xB7" }),
7149
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " deny " }),
7150
+ /* @__PURE__ */ jsx2(Text2, { color: theme.bad, bold: true, children: s ? s.denied : "\xB7\xB7" }),
7151
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " bursts(60m) " }),
7152
+ /* @__PURE__ */ jsx2(Text2, { color: bursts ? theme.bad : theme.dim, children: bursts }),
7153
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " limit/min " }),
7154
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: limit || "off" }),
7155
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " policies " }),
7156
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: s ? s.active_policies : "\xB7" })
7157
+ ] }),
7158
+ /* @__PURE__ */ jsx2(SectionTitle, { width: cols - 2, children: `ACTIVITY ${activityLabel} \xB7 peak ${Math.max(0, ...minuteSeries)}${limit ? ` \xB7 limit ${limit} (red = over)` : ""}` }),
7159
+ minuteSeries.length ? /* @__PURE__ */ jsx2(ColumnChart, { series: minuteSeries, height: bigH, width: cols - 2, color: theme.accent, limit }) : /* @__PURE__ */ jsx2(Box2, { height: bigH, children: /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: insights.loading ? "loading activity\u2026" : "no activity data" }) }),
7160
+ /* @__PURE__ */ jsxs2(Box2, { children: [
7161
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, marginRight: 2, children: [
7162
+ /* @__PURE__ */ jsx2(SectionTitle, { width: colW, children: "DENIALS 24h" }),
7163
+ /* @__PURE__ */ jsx2(ColumnChart, { series: denySeries, height: colH - 1, width: colW, color: theme.bad })
7164
+ ] }),
7165
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, marginRight: 2, children: [
7166
+ /* @__PURE__ */ jsx2(SectionTitle, { width: colW, children: "TOP TOOLS (buffer)" }),
7167
+ topTools.length ? topTools.slice(0, colH - 1).map(([tool, count]) => /* @__PURE__ */ jsx2(HBar, { label: truncate2(tool, 9), value: count, max: maxTool, width: Math.max(6, colW - 16), color: theme.accent }, tool)) : /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "no traffic yet" })
7060
7168
  ] }),
7061
- /* @__PURE__ */ jsxs2(Box2, { children: [
7062
- /* @__PURE__ */ jsx2(Text2, { children: "denied " }),
7063
- /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: sparkline(points.map((p) => p.denied), 60) })
7169
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, children: [
7170
+ /* @__PURE__ */ jsx2(SectionTitle, { width: colW, children: "PERMISSIONS \xB7 DLP" }),
7171
+ perms.map(([p, c2]) => /* @__PURE__ */ jsx2(HBar, { label: p, value: c2, max: maxPerm, width: Math.max(6, colW - 16), color: p === "EXECUTE" ? theme.warn : theme.accent }, p)),
7172
+ dlpTop.map((d) => /* @__PURE__ */ jsx2(Text2, { wrap: "truncate", color: theme.bad, children: "DLP " + truncate2(d.pattern, colW - 10) + " \xD7" + d.count }, d.pattern))
7064
7173
  ] })
7065
7174
  ] }),
7066
- /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
7067
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "Live feed" }),
7068
- entries.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " waiting for tool calls\u2026" }) : entries.slice(0, 10).map((e) => /* @__PURE__ */ jsxs2(Box2, { children: [
7069
- /* @__PURE__ */ jsx2(Text2, { color: decisionColor(e.decision), children: e.decision.padEnd(8) }),
7070
- /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: truncate2(e.tool_name, 16).padEnd(17) }),
7071
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: truncate2(e.agent_name ?? "\u2014", 14).padEnd(15) }),
7072
- e.dlp_matches?.length ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: "DLP " }) : /* @__PURE__ */ jsx2(Text2, { children: " " }),
7073
- /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
7074
- ago(e.created_at),
7075
- " ago"
7076
- ] })
7077
- ] }, e.id))
7175
+ /* @__PURE__ */ jsx2(SectionTitle, { width: cols - 2, children: `TOOL STREAM \xB7 live tail \xB7 ${buffer.length} buffered` }),
7176
+ tail.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "awaiting traffic\u2026" }) : null,
7177
+ tail.map((e) => /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7178
+ /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
7179
+ "[",
7180
+ hhmmss(e.created_at),
7181
+ "] "
7182
+ ] }),
7183
+ /* @__PURE__ */ jsx2(Text2, { color: decisionColor(e.decision), bold: e.decision !== "ALLOW", children: e.decision.padEnd(6) }),
7184
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: truncate2(e.tool_name, 14).padEnd(15) }),
7185
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: (e.permission ?? "").slice(0, 4).padEnd(5) }),
7186
+ e.dlp_matches?.length ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: "DLP! " }) : null,
7187
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: (e.arguments_summary ? JSON.stringify(e.arguments_summary) : e.reason ?? "").replace(/\s+/g, " ") })
7188
+ ] }, e.id)),
7189
+ /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7190
+ /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: "root@solongate" }),
7191
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ":" }),
7192
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, children: "~/live" }),
7193
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "$ " }),
7194
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, children: cursor })
7078
7195
  ] })
7079
- ] }) });
7196
+ ] });
7080
7197
  }
7198
+ var SPIN, hhmmss, fmtUp;
7081
7199
  var init_Live = __esm({
7082
7200
  "src/tui/panels/Live.tsx"() {
7083
7201
  "use strict";
7084
7202
  init_api_client();
7085
- init_components();
7086
7203
  init_hooks();
7087
7204
  init_theme();
7205
+ SPIN = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
7206
+ hhmmss = (ts) => {
7207
+ const d = new Date(ts);
7208
+ return Number.isNaN(d.getTime()) ? "--:--:--" : d.toTimeString().slice(0, 8);
7209
+ };
7210
+ fmtUp = (ms) => {
7211
+ const s = Math.floor(ms / 1e3);
7212
+ const p = (n) => String(n).padStart(2, "0");
7213
+ return `${p(Math.floor(s / 3600))}:${p(Math.floor(s % 3600 / 60))}:${p(s % 60)}`;
7214
+ };
7088
7215
  }
7089
7216
  });
7090
7217
 
7091
7218
  // src/tui/panels/Policies.tsx
7092
7219
  import { Box as Box3, Text as Text3, useInput } from "ink";
7093
7220
  import TextInput from "ink-text-input";
7094
- import { useEffect as useEffect2, useState as useState2 } from "react";
7221
+ import { useEffect as useEffect3, useState as useState3 } from "react";
7095
7222
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
7096
7223
  function ruleSummary(r) {
7097
7224
  const bits = [];
@@ -7105,19 +7232,19 @@ function ruleSummary(r) {
7105
7232
  function PoliciesPanel({ focused }) {
7106
7233
  const list4 = useLoader(() => api.policies.list());
7107
7234
  const policies = list4.data?.policies ?? [];
7108
- const [view, setView] = useState2("list");
7109
- const [pi, setPi] = useState2(0);
7110
- const [ri, setRi] = useState2(0);
7111
- const [fi, setFi] = useState2(0);
7112
- const [rules, setRules] = useState2([]);
7113
- const [mode, setMode] = useState2("denylist");
7114
- const [dirty, setDirty] = useState2(false);
7115
- const [editing, setEditing] = useState2(false);
7116
- const [editVal, setEditVal] = useState2("");
7117
- const [status, setStatus] = useState2(null);
7235
+ const [view, setView] = useState3("list");
7236
+ const [pi, setPi] = useState3(0);
7237
+ const [ri, setRi] = useState3(0);
7238
+ const [fi, setFi] = useState3(0);
7239
+ const [rules, setRules] = useState3([]);
7240
+ const [mode, setMode] = useState3("denylist");
7241
+ const [dirty, setDirty] = useState3(false);
7242
+ const [editing, setEditing] = useState3(false);
7243
+ const [editVal, setEditVal] = useState3("");
7244
+ const [status, setStatus] = useState3(null);
7118
7245
  const selected = policies[Math.min(pi, Math.max(0, policies.length - 1))];
7119
7246
  const detail = useLoader(() => selected ? api.policies.get(selected.id) : Promise.resolve(null), [selected?.id]);
7120
- useEffect2(() => {
7247
+ useEffect3(() => {
7121
7248
  if (detail.data) {
7122
7249
  setRules(detail.data.rules);
7123
7250
  setMode(detail.data.mode ?? "denylist");
@@ -7335,7 +7462,7 @@ var init_Policies = __esm({
7335
7462
 
7336
7463
  // src/tui/panels/RateLimit.tsx
7337
7464
  import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
7338
- import { useEffect as useEffect3, useState as useState3 } from "react";
7465
+ import { useEffect as useEffect4, useState as useState4 } from "react";
7339
7466
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
7340
7467
  function ColoredSpark({ values, limit, width: width2 = 60 }) {
7341
7468
  const v = values.length > width2 ? values.slice(values.length - width2) : values;
@@ -7350,11 +7477,11 @@ function RateLimitPanel({ focused }) {
7350
7477
  const layersQ = useLoader(() => api.settings.getSecurityLayers());
7351
7478
  const historyQ = useLoader(() => api.settings.getRateLimitHistory());
7352
7479
  const insightsQ = useLoader(() => api.stats.securityInsights(7));
7353
- const [draft, setDraft] = useState3(null);
7354
- const [dirty, setDirty] = useState3(false);
7355
- const [fi, setFi] = useState3(0);
7356
- const [status, setStatus] = useState3(null);
7357
- useEffect3(() => {
7480
+ const [draft, setDraft] = useState4(null);
7481
+ const [dirty, setDirty] = useState4(false);
7482
+ const [fi, setFi] = useState4(0);
7483
+ const [status, setStatus] = useState4(null);
7484
+ useEffect4(() => {
7358
7485
  if (layersQ.data && !draft) setDraft({ ...layersQ.data.layers.rateLimit });
7359
7486
  }, [layersQ.data, draft]);
7360
7487
  const adjust = (dir, step) => {
@@ -7476,19 +7603,19 @@ var init_RateLimit = __esm({
7476
7603
  // src/tui/panels/Dlp.tsx
7477
7604
  import { Box as Box5, Text as Text5, useInput as useInput3 } from "ink";
7478
7605
  import TextInput2 from "ink-text-input";
7479
- import { useEffect as useEffect4, useState as useState4 } from "react";
7606
+ import { useEffect as useEffect5, useState as useState5 } from "react";
7480
7607
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
7481
7608
  function DlpPanel({ focused }) {
7482
7609
  const q = useLoader(() => api.settings.getSecurityLayers());
7483
- const [dlp, setDlp] = useState4(null);
7484
- const [available, setAvailable] = useState4([]);
7485
- const [sel, setSel] = useState4(0);
7486
- const [dirty, setDirty] = useState4(false);
7487
- const [status, setStatus] = useState4(null);
7488
- const [adding, setAdding] = useState4(null);
7489
- const [newName, setNewName] = useState4("");
7490
- const [newRe, setNewRe] = useState4("");
7491
- useEffect4(() => {
7610
+ const [dlp, setDlp] = useState5(null);
7611
+ const [available, setAvailable] = useState5([]);
7612
+ const [sel, setSel] = useState5(0);
7613
+ const [dirty, setDirty] = useState5(false);
7614
+ const [status, setStatus] = useState5(null);
7615
+ const [adding, setAdding] = useState5(null);
7616
+ const [newName, setNewName] = useState5("");
7617
+ const [newRe, setNewRe] = useState5("");
7618
+ useEffect5(() => {
7492
7619
  if (q.data && !dlp) {
7493
7620
  setDlp({ ...q.data.layers.dlp, patterns: [...q.data.layers.dlp.patterns], custom: [...q.data.layers.dlp.custom] });
7494
7621
  setAvailable(q.data.availablePatterns);
@@ -7702,17 +7829,17 @@ var init_Stats = __esm({
7702
7829
  // src/tui/panels/Audit.tsx
7703
7830
  import { Box as Box7, Text as Text7, useInput as useInput4 } from "ink";
7704
7831
  import TextInput3 from "ink-text-input";
7705
- import { useState as useState5 } from "react";
7832
+ import { useState as useState6 } from "react";
7706
7833
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
7707
7834
  function AuditPanel({ active: active2, focused }) {
7708
- const [di, setDi] = useState5(0);
7709
- const [gi, setGi] = useState5(0);
7710
- const [tool, setTool] = useState5("");
7711
- const [agent, setAgent] = useState5("");
7712
- const [search, setSearch] = useState5("");
7713
- const [sel, setSel] = useState5(0);
7714
- const [view, setView] = useState5("list");
7715
- const [editing, setEditing] = useState5(null);
7835
+ const [di, setDi] = useState6(0);
7836
+ const [gi, setGi] = useState6(0);
7837
+ const [tool, setTool] = useState6("");
7838
+ const [agent, setAgent] = useState6("");
7839
+ const [search, setSearch] = useState6("");
7840
+ const [sel, setSel] = useState6(0);
7841
+ const [view, setView] = useState6("list");
7842
+ const [editing, setEditing] = useState6(null);
7716
7843
  const query = {
7717
7844
  filter: DECISIONS[di],
7718
7845
  signal: SIGNALS[gi],
@@ -7880,8 +8007,20 @@ var init_Audit = __esm({
7880
8007
 
7881
8008
  // src/tui/App.tsx
7882
8009
  import { Box as Box8, Text as Text8, useApp, useInput as useInput5 } from "ink";
7883
- import { useState as useState6 } from "react";
8010
+ import { useState as useState7 } from "react";
7884
8011
  import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
8012
+ function LiveHint() {
8013
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
8014
+ /* @__PURE__ */ jsx8(Text8, { color: theme.accentBright, bold: true, children: "\u25B6 REALTIME CONSOLE" }),
8015
+ /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "Fullscreen live monitor \u2014 streaming tool calls, active charts," }),
8016
+ /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "counters, DLP hits and denial alerts. Updates every 2s." }),
8017
+ /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsxs8(Text8, { children: [
8018
+ "press ",
8019
+ /* @__PURE__ */ jsx8(Text8, { color: theme.accent, children: "enter" }),
8020
+ " to go live"
8021
+ ] }) })
8022
+ ] });
8023
+ }
7885
8024
  function Banner() {
7886
8025
  const wide = (process.stdout.columns ?? 80) >= 82;
7887
8026
  if (!wide) {
@@ -7897,8 +8036,8 @@ function Banner() {
7897
8036
  }
7898
8037
  function App() {
7899
8038
  const { exit } = useApp();
7900
- const [section, setSection] = useState6(0);
7901
- const [focus, setFocus] = useState6("nav");
8039
+ const [section, setSection] = useState7(0);
8040
+ const [focus, setFocus] = useState7("nav");
7902
8041
  useInput5((input, key) => {
7903
8042
  if (focus === "nav") {
7904
8043
  if (key.upArrow) setSection((n) => (n - 1 + SECTIONS.length) % SECTIONS.length);
@@ -7907,17 +8046,23 @@ function App() {
7907
8046
  else if (input === "q") exit();
7908
8047
  } else {
7909
8048
  if (key.escape || key.tab) setFocus("nav");
8049
+ else if (input === "q" && SECTIONS[section].label === "Live") exit();
7910
8050
  }
7911
8051
  });
8052
+ const cols = process.stdout.columns ?? 100;
8053
+ const rows = process.stdout.rows ?? 30;
7912
8054
  const current = SECTIONS[section];
8055
+ if (current.label === "Live" && focus === "panel") {
8056
+ return /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", width: cols, height: rows, children: /* @__PURE__ */ jsx8(LivePanel, { active: true, focused: true }) });
8057
+ }
7913
8058
  const Panel = current.Panel;
7914
- return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", paddingX: 1, paddingTop: 1, children: [
8059
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", width: cols, height: rows, paddingX: 1, paddingTop: 1, children: [
7915
8060
  /* @__PURE__ */ jsx8(Banner, {}),
7916
- /* @__PURE__ */ jsxs8(Box8, { marginTop: 1, children: [
8061
+ /* @__PURE__ */ jsxs8(Box8, { marginTop: 1, flexGrow: 1, children: [
7917
8062
  /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", width: 16, borderStyle: "round", borderColor: focus === "nav" ? theme.accent : "gray", paddingX: 1, children: SECTIONS.map((s, i) => /* @__PURE__ */ jsx8(Text8, { color: i === section ? theme.accentBright : void 0, bold: i === section, children: (i === section ? "\u25B8 " : " ") + s.label }, s.label)) }),
7918
- /* @__PURE__ */ jsx8(Box8, { flexGrow: 1, minHeight: 16, borderStyle: "round", borderColor: focus === "panel" ? theme.accent : "gray", paddingX: 1, paddingY: 0, children: /* @__PURE__ */ jsx8(Panel, { active: true, focused: focus === "panel" }) })
8063
+ /* @__PURE__ */ jsx8(Box8, { flexGrow: 1, borderStyle: "round", borderColor: focus === "panel" ? theme.accent : "gray", paddingX: 1, paddingY: 0, children: /* @__PURE__ */ jsx8(Panel, { active: focus === "panel" || current.label !== "Live", focused: focus === "panel" }) })
7919
8064
  ] }),
7920
- /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: focus === "nav" ? /* @__PURE__ */ jsx8(KeyHints, { hints: [["\u2191\u2193", "section"], ["\u2192/enter", "open"], ["q", "quit"]] }) : /* @__PURE__ */ jsx8(KeyHints, { hints: [["\u2191\u2193", "move"], ["\u2190\u2192/space/s", "edit"], ["esc", "back to menu"]] }) })
8065
+ /* @__PURE__ */ jsx8(Box8, { children: focus === "nav" ? /* @__PURE__ */ jsx8(KeyHints, { hints: [["\u2191\u2193", "section"], ["\u2192/enter", "open"], ["q", "quit"]] }) : /* @__PURE__ */ jsx8(KeyHints, { hints: [["\u2190/esc", "back"], ["\u2191\u2193", "in-panel"], ["space/s", "act"]] }) })
7921
8066
  ] });
7922
8067
  }
7923
8068
  var SECTIONS, BANNER_HEX;
@@ -7934,7 +8079,7 @@ var init_App = __esm({
7934
8079
  init_Stats();
7935
8080
  init_Audit();
7936
8081
  SECTIONS = [
7937
- { label: "Live", Panel: LivePanel },
8082
+ { label: "Live", Panel: LiveHint },
7938
8083
  { label: "Policies", Panel: PoliciesPanel },
7939
8084
  { label: "Rate Limit", Panel: RateLimitPanel },
7940
8085
  { label: "DLP", Panel: DlpPanel },
@@ -7963,8 +8108,13 @@ async function launchTui() {
7963
8108
  process.stderr.write("Not logged in. Run `solongate login` first.\n");
7964
8109
  return;
7965
8110
  }
7966
- const { waitUntilExit } = render(/* @__PURE__ */ jsx9(App, {}));
7967
- await waitUntilExit();
8111
+ process.stdout.write("\x1B[?1049h\x1B[H");
8112
+ try {
8113
+ const { waitUntilExit } = render(/* @__PURE__ */ jsx9(App, {}));
8114
+ await waitUntilExit();
8115
+ } finally {
8116
+ process.stdout.write("\x1B[?1049l");
8117
+ }
7968
8118
  }
7969
8119
  var init_tui = __esm({
7970
8120
  "src/tui/index.tsx"() {
package/dist/tui/index.js CHANGED
@@ -9,7 +9,7 @@ import { render } from "ink";
9
9
 
10
10
  // src/tui/App.tsx
11
11
  import { Box as Box8, Text as Text8, useApp, useInput as useInput5 } from "ink";
12
- import { useState as useState6 } from "react";
12
+ import { useState as useState7 } from "react";
13
13
 
14
14
  // src/cli-utils.ts
15
15
  var c = {
@@ -126,6 +126,7 @@ 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 { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
129
130
 
130
131
  // src/api-client/client.ts
131
132
  import { readFileSync, existsSync } from "fs";
@@ -430,68 +431,194 @@ function usePoll(reload, intervalMs, enabled = true) {
430
431
 
431
432
  // src/tui/panels/Live.tsx
432
433
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
433
- function Tile({ label, value, color }) {
434
- return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, marginRight: 1, minWidth: 14, children: [
435
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: label }),
436
- /* @__PURE__ */ jsx2(Text2, { bold: true, color, children: value })
434
+ var SPIN = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
435
+ var hhmmss = (ts) => {
436
+ const d = new Date(ts);
437
+ return Number.isNaN(d.getTime()) ? "--:--:--" : d.toTimeString().slice(0, 8);
438
+ };
439
+ var fmtUp = (ms) => {
440
+ const s = Math.floor(ms / 1e3);
441
+ const p = (n) => String(n).padStart(2, "0");
442
+ return `${p(Math.floor(s / 3600))}:${p(Math.floor(s % 3600 / 60))}:${p(s % 60)}`;
443
+ };
444
+ function ColumnChart({ series, height, width, color, limit = 0 }) {
445
+ const tailVals = series.slice(-width);
446
+ const data = [...new Array(Math.max(0, width - tailVals.length)).fill(0), ...tailVals];
447
+ const max = Math.max(1, ...data, limit);
448
+ const lines = [];
449
+ for (let r = height; r >= 1; r--) {
450
+ const segs = [];
451
+ for (let i = 0; i < data.length; i++) {
452
+ const frac = data[i] / max;
453
+ const ch = frac >= r / height ? "\u2588" : frac >= (r - 0.5) / height ? "\u2584" : r === 1 && data[i] > 0 ? "\u2581" : " ";
454
+ const c2 = limit > 0 && data[i] > limit ? theme.bad : color;
455
+ const last = segs[segs.length - 1];
456
+ if (last && last.color === c2) last.text += ch;
457
+ else segs.push({ text: ch, color: c2 });
458
+ }
459
+ lines.push(
460
+ /* @__PURE__ */ jsx2(Text2, { children: segs.map((s, i) => /* @__PURE__ */ jsx2(Text2, { color: s.color, children: s.text }, i)) }, r)
461
+ );
462
+ }
463
+ return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: lines });
464
+ }
465
+ function HBar({ label, value, max, width, color }) {
466
+ const filled = max > 0 ? Math.round(value / max * width) : 0;
467
+ return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
468
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: label.padEnd(9) }),
469
+ /* @__PURE__ */ jsx2(Text2, { color, children: "\u2588".repeat(Math.min(width, filled)) }),
470
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "\u2591".repeat(Math.max(0, width - filled)) + " " + value })
437
471
  ] });
438
472
  }
473
+ function SectionTitle({ children, width }) {
474
+ const label = `\u2500\u2500 ${children} `;
475
+ return /* @__PURE__ */ jsx2(Text2, { color: theme.dim, wrap: "truncate", children: label + "\u2500".repeat(Math.max(0, width - label.length)) });
476
+ }
439
477
  function LivePanel({ active: active2 }) {
440
478
  const stats = useLoader(() => api.stats.get());
441
479
  const ts = useLoader(() => api.stats.timeseries({ period: "24h" }));
442
- const feed = useLoader(() => api.audit.list({ limit: 12 }));
480
+ const feed = useLoader(() => api.audit.list({ limit: 50 }));
481
+ const insights = useLoader(() => api.stats.securityInsights(7));
443
482
  usePoll(() => {
444
483
  stats.reload();
445
484
  ts.reload();
446
485
  feed.reload();
447
486
  }, 2e3, active2);
487
+ usePoll(insights.reload, 1e4, active2);
488
+ const [tick, setTick] = useState2(0);
489
+ useEffect2(() => {
490
+ if (!active2) return;
491
+ const t = setInterval(() => setTick((n) => n + 1), 200);
492
+ return () => clearInterval(t);
493
+ }, [active2]);
494
+ const startRef = useRef2(Date.now());
495
+ const [buffer, setBuffer] = useState2([]);
496
+ useEffect2(() => {
497
+ const incoming = feed.data?.entries;
498
+ if (!incoming?.length) return;
499
+ setBuffer((prev) => {
500
+ const seen = new Set(prev.map((e) => e.id));
501
+ const fresh = incoming.filter((e) => !seen.has(e.id)).sort((a, b) => Date.parse(a.created_at) - Date.parse(b.created_at));
502
+ return fresh.length ? [...prev, ...fresh].slice(-400) : prev;
503
+ });
504
+ }, [feed.data]);
505
+ const cols = process.stdout.columns ?? 100;
506
+ const rows = process.stdout.rows ?? 30;
448
507
  const s = stats.data;
449
- const points = ts.data?.timeseries ?? [];
450
- const entries = feed.data?.entries ?? [];
451
- const denyRate = s && s.total_calls > 0 ? Math.round(s.denied / s.total_calls * 100) : 0;
452
- return /* @__PURE__ */ jsx2(DataView, { loading: stats.loading && !s, error: stats.error, children: /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
453
- /* @__PURE__ */ jsxs2(Box2, { children: [
454
- /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: "\u25CF" }),
455
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " LIVE \xB7 refreshing every 2s" })
508
+ const ins = insights.data ?? {};
509
+ const minuteRaw = (ins.activity?.minute ?? []).map((b) => b.count);
510
+ const hourRaw = (ins.activity?.hour ?? []).map((b) => b.count);
511
+ const minuteLive = minuteRaw.some((c2) => c2 > 0);
512
+ const minuteSeries = minuteLive ? minuteRaw : hourRaw;
513
+ const activityLabel = minuteLive ? "calls/min \xB7 last 60m" : "calls/hour \xB7 last 24h";
514
+ const limit = minuteLive ? ins.layers?.rateLimit?.perMinute ?? 0 : 0;
515
+ const denySeries = (ts.data?.timeseries ?? []).map((p) => p.denied);
516
+ const spin = SPIN[tick % SPIN.length];
517
+ const cursor = tick % 4 < 2 ? "\u258C" : " ";
518
+ const toolCounts = /* @__PURE__ */ new Map();
519
+ const permCounts = /* @__PURE__ */ new Map();
520
+ for (const e of buffer) {
521
+ toolCounts.set(e.tool_name, (toolCounts.get(e.tool_name) ?? 0) + 1);
522
+ const perm = (e.permission || "?").toUpperCase();
523
+ permCounts.set(perm, (permCounts.get(perm) ?? 0) + 1);
524
+ }
525
+ const topTools = [...toolCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5);
526
+ const maxTool = topTools[0]?.[1] ?? 1;
527
+ const perms = ["READ", "WRITE", "EXECUTE", "NETWORK"].map((p) => [p, permCounts.get(p) ?? 0]);
528
+ const maxPerm = Math.max(1, ...perms.map(([, c2]) => c2));
529
+ const dlpTop = (ins.dlpByPattern ?? []).slice(0, 2);
530
+ const denials = buffer.filter((e) => e.decision !== "ALLOW");
531
+ const lastDeny = denials[denials.length - 1];
532
+ const bursts = limit > 0 ? minuteSeries.filter((c2) => c2 > limit).length : 0;
533
+ const bigH = rows >= 34 ? 6 : 4;
534
+ const colH = rows >= 34 ? 6 : 5;
535
+ const streamRows = Math.max(4, rows - (2 + 1 + bigH + 1 + colH + 1 + 1));
536
+ const tail = buffer.slice(-streamRows);
537
+ const colW = Math.max(18, Math.floor((cols - 6) / 3));
538
+ if (stats.error) return /* @__PURE__ */ jsxs2(Text2, { color: theme.bad, children: [
539
+ "\u2717 ",
540
+ stats.error
541
+ ] });
542
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
543
+ /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
544
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#1432A0", color: "#AAC8FA", bold: true, children: " SOLONGATE::LIVE " }),
545
+ /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
546
+ " ",
547
+ spin,
548
+ " up ",
549
+ fmtUp(Date.now() - startRef.current),
550
+ " \xB7 ",
551
+ hhmmss(Date.now()),
552
+ " \xB7 poll 2s"
553
+ ] }),
554
+ lastDeny ? /* @__PURE__ */ jsxs2(Text2, { color: theme.bad, children: [
555
+ " \u26A0 ",
556
+ hhmmss(lastDeny.created_at),
557
+ " ",
558
+ lastDeny.tool_name,
559
+ " denied"
560
+ ] }) : null,
561
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " esc menu \xB7 q quit" })
456
562
  ] }),
457
- s ? /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, children: [
458
- /* @__PURE__ */ jsx2(Tile, { label: "total calls", value: String(s.total_calls) }),
459
- /* @__PURE__ */ jsx2(Tile, { label: "allowed", value: String(s.allowed), color: theme.ok }),
460
- /* @__PURE__ */ jsx2(Tile, { label: "denied", value: String(s.denied), color: theme.bad }),
461
- /* @__PURE__ */ jsx2(Tile, { label: "deny rate", value: denyRate + "%", color: denyRate > 10 ? theme.bad : void 0 }),
462
- /* @__PURE__ */ jsx2(Tile, { label: "policies", value: String(s.active_policies), color: theme.accent })
463
- ] }) : null,
464
- /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
465
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "Activity \xB7 24h" }),
466
- /* @__PURE__ */ jsxs2(Box2, { children: [
467
- /* @__PURE__ */ jsx2(Text2, { children: "calls " }),
468
- /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: sparkline(points.map((p) => p.total), 60) })
563
+ /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
564
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "calls " }),
565
+ /* @__PURE__ */ jsx2(Text2, { bold: true, children: s ? s.total_calls : "\xB7\xB7\xB7\xB7" }),
566
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " allow " }),
567
+ /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: s ? s.allowed : "\xB7\xB7\xB7" }),
568
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " deny " }),
569
+ /* @__PURE__ */ jsx2(Text2, { color: theme.bad, bold: true, children: s ? s.denied : "\xB7\xB7" }),
570
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " bursts(60m) " }),
571
+ /* @__PURE__ */ jsx2(Text2, { color: bursts ? theme.bad : theme.dim, children: bursts }),
572
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " limit/min " }),
573
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: limit || "off" }),
574
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " policies " }),
575
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: s ? s.active_policies : "\xB7" })
576
+ ] }),
577
+ /* @__PURE__ */ jsx2(SectionTitle, { width: cols - 2, children: `ACTIVITY ${activityLabel} \xB7 peak ${Math.max(0, ...minuteSeries)}${limit ? ` \xB7 limit ${limit} (red = over)` : ""}` }),
578
+ minuteSeries.length ? /* @__PURE__ */ jsx2(ColumnChart, { series: minuteSeries, height: bigH, width: cols - 2, color: theme.accent, limit }) : /* @__PURE__ */ jsx2(Box2, { height: bigH, children: /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: insights.loading ? "loading activity\u2026" : "no activity data" }) }),
579
+ /* @__PURE__ */ jsxs2(Box2, { children: [
580
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, marginRight: 2, children: [
581
+ /* @__PURE__ */ jsx2(SectionTitle, { width: colW, children: "DENIALS 24h" }),
582
+ /* @__PURE__ */ jsx2(ColumnChart, { series: denySeries, height: colH - 1, width: colW, color: theme.bad })
583
+ ] }),
584
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, marginRight: 2, children: [
585
+ /* @__PURE__ */ jsx2(SectionTitle, { width: colW, children: "TOP TOOLS (buffer)" }),
586
+ topTools.length ? topTools.slice(0, colH - 1).map(([tool, count]) => /* @__PURE__ */ jsx2(HBar, { label: truncate(tool, 9), value: count, max: maxTool, width: Math.max(6, colW - 16), color: theme.accent }, tool)) : /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "no traffic yet" })
469
587
  ] }),
470
- /* @__PURE__ */ jsxs2(Box2, { children: [
471
- /* @__PURE__ */ jsx2(Text2, { children: "denied " }),
472
- /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: sparkline(points.map((p) => p.denied), 60) })
588
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, children: [
589
+ /* @__PURE__ */ jsx2(SectionTitle, { width: colW, children: "PERMISSIONS \xB7 DLP" }),
590
+ perms.map(([p, c2]) => /* @__PURE__ */ jsx2(HBar, { label: p, value: c2, max: maxPerm, width: Math.max(6, colW - 16), color: p === "EXECUTE" ? theme.warn : theme.accent }, p)),
591
+ dlpTop.map((d) => /* @__PURE__ */ jsx2(Text2, { wrap: "truncate", color: theme.bad, children: "DLP " + truncate(d.pattern, colW - 10) + " \xD7" + d.count }, d.pattern))
473
592
  ] })
474
593
  ] }),
475
- /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
476
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "Live feed" }),
477
- entries.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " waiting for tool calls\u2026" }) : entries.slice(0, 10).map((e) => /* @__PURE__ */ jsxs2(Box2, { children: [
478
- /* @__PURE__ */ jsx2(Text2, { color: decisionColor(e.decision), children: e.decision.padEnd(8) }),
479
- /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: truncate(e.tool_name, 16).padEnd(17) }),
480
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: truncate(e.agent_name ?? "\u2014", 14).padEnd(15) }),
481
- e.dlp_matches?.length ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: "DLP " }) : /* @__PURE__ */ jsx2(Text2, { children: " " }),
482
- /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
483
- ago(e.created_at),
484
- " ago"
485
- ] })
486
- ] }, e.id))
594
+ /* @__PURE__ */ jsx2(SectionTitle, { width: cols - 2, children: `TOOL STREAM \xB7 live tail \xB7 ${buffer.length} buffered` }),
595
+ tail.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "awaiting traffic\u2026" }) : null,
596
+ tail.map((e) => /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
597
+ /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
598
+ "[",
599
+ hhmmss(e.created_at),
600
+ "] "
601
+ ] }),
602
+ /* @__PURE__ */ jsx2(Text2, { color: decisionColor(e.decision), bold: e.decision !== "ALLOW", children: e.decision.padEnd(6) }),
603
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: truncate(e.tool_name, 14).padEnd(15) }),
604
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: (e.permission ?? "").slice(0, 4).padEnd(5) }),
605
+ e.dlp_matches?.length ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: "DLP! " }) : null,
606
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: (e.arguments_summary ? JSON.stringify(e.arguments_summary) : e.reason ?? "").replace(/\s+/g, " ") })
607
+ ] }, e.id)),
608
+ /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
609
+ /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: "root@solongate" }),
610
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ":" }),
611
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, children: "~/live" }),
612
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "$ " }),
613
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, children: cursor })
487
614
  ] })
488
- ] }) });
615
+ ] });
489
616
  }
490
617
 
491
618
  // src/tui/panels/Policies.tsx
492
619
  import { Box as Box3, Text as Text3, useInput } from "ink";
493
620
  import TextInput from "ink-text-input";
494
- import { useEffect as useEffect2, useState as useState2 } from "react";
621
+ import { useEffect as useEffect3, useState as useState3 } from "react";
495
622
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
496
623
  var constr = (r, g, side) => (r[g]?.[side] ?? []).join(", ");
497
624
  var setConstr = (r, g, side, val) => {
@@ -526,19 +653,19 @@ function ruleSummary(r) {
526
653
  function PoliciesPanel({ focused }) {
527
654
  const list3 = useLoader(() => api.policies.list());
528
655
  const policies = list3.data?.policies ?? [];
529
- const [view, setView] = useState2("list");
530
- const [pi, setPi] = useState2(0);
531
- const [ri, setRi] = useState2(0);
532
- const [fi, setFi] = useState2(0);
533
- const [rules, setRules] = useState2([]);
534
- const [mode, setMode] = useState2("denylist");
535
- const [dirty, setDirty] = useState2(false);
536
- const [editing, setEditing] = useState2(false);
537
- const [editVal, setEditVal] = useState2("");
538
- const [status, setStatus] = useState2(null);
656
+ const [view, setView] = useState3("list");
657
+ const [pi, setPi] = useState3(0);
658
+ const [ri, setRi] = useState3(0);
659
+ const [fi, setFi] = useState3(0);
660
+ const [rules, setRules] = useState3([]);
661
+ const [mode, setMode] = useState3("denylist");
662
+ const [dirty, setDirty] = useState3(false);
663
+ const [editing, setEditing] = useState3(false);
664
+ const [editVal, setEditVal] = useState3("");
665
+ const [status, setStatus] = useState3(null);
539
666
  const selected = policies[Math.min(pi, Math.max(0, policies.length - 1))];
540
667
  const detail = useLoader(() => selected ? api.policies.get(selected.id) : Promise.resolve(null), [selected?.id]);
541
- useEffect2(() => {
668
+ useEffect3(() => {
542
669
  if (detail.data) {
543
670
  setRules(detail.data.rules);
544
671
  setMode(detail.data.mode ?? "denylist");
@@ -725,7 +852,7 @@ function PoliciesPanel({ focused }) {
725
852
 
726
853
  // src/tui/panels/RateLimit.tsx
727
854
  import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
728
- import { useEffect as useEffect3, useState as useState3 } from "react";
855
+ import { useEffect as useEffect4, useState as useState4 } from "react";
729
856
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
730
857
  var MODES = ["off", "detect", "block"];
731
858
  var FIELDS2 = ["mode", "perMinute", "perHour", "perDay"];
@@ -743,11 +870,11 @@ function RateLimitPanel({ focused }) {
743
870
  const layersQ = useLoader(() => api.settings.getSecurityLayers());
744
871
  const historyQ = useLoader(() => api.settings.getRateLimitHistory());
745
872
  const insightsQ = useLoader(() => api.stats.securityInsights(7));
746
- const [draft, setDraft] = useState3(null);
747
- const [dirty, setDirty] = useState3(false);
748
- const [fi, setFi] = useState3(0);
749
- const [status, setStatus] = useState3(null);
750
- useEffect3(() => {
873
+ const [draft, setDraft] = useState4(null);
874
+ const [dirty, setDirty] = useState4(false);
875
+ const [fi, setFi] = useState4(0);
876
+ const [status, setStatus] = useState4(null);
877
+ useEffect4(() => {
751
878
  if (layersQ.data && !draft) setDraft({ ...layersQ.data.layers.rateLimit });
752
879
  }, [layersQ.data, draft]);
753
880
  const adjust = (dir, step) => {
@@ -856,20 +983,20 @@ function FieldRow({ label, active: active2, children }) {
856
983
  // src/tui/panels/Dlp.tsx
857
984
  import { Box as Box5, Text as Text5, useInput as useInput3 } from "ink";
858
985
  import TextInput2 from "ink-text-input";
859
- import { useEffect as useEffect4, useState as useState4 } from "react";
986
+ import { useEffect as useEffect5, useState as useState5 } from "react";
860
987
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
861
988
  var MODES2 = ["off", "detect", "block"];
862
989
  function DlpPanel({ focused }) {
863
990
  const q = useLoader(() => api.settings.getSecurityLayers());
864
- const [dlp, setDlp] = useState4(null);
865
- const [available, setAvailable] = useState4([]);
866
- const [sel, setSel] = useState4(0);
867
- const [dirty, setDirty] = useState4(false);
868
- const [status, setStatus] = useState4(null);
869
- const [adding, setAdding] = useState4(null);
870
- const [newName, setNewName] = useState4("");
871
- const [newRe, setNewRe] = useState4("");
872
- useEffect4(() => {
991
+ const [dlp, setDlp] = useState5(null);
992
+ const [available, setAvailable] = useState5([]);
993
+ const [sel, setSel] = useState5(0);
994
+ const [dirty, setDirty] = useState5(false);
995
+ const [status, setStatus] = useState5(null);
996
+ const [adding, setAdding] = useState5(null);
997
+ const [newName, setNewName] = useState5("");
998
+ const [newRe, setNewRe] = useState5("");
999
+ useEffect5(() => {
873
1000
  if (q.data && !dlp) {
874
1001
  setDlp({ ...q.data.layers.dlp, patterns: [...q.data.layers.dlp.patterns], custom: [...q.data.layers.dlp.custom] });
875
1002
  setAvailable(q.data.availablePatterns);
@@ -1063,19 +1190,19 @@ function StatsPanel({ active: active2 }) {
1063
1190
  // src/tui/panels/Audit.tsx
1064
1191
  import { Box as Box7, Text as Text7, useInput as useInput4 } from "ink";
1065
1192
  import TextInput3 from "ink-text-input";
1066
- import { useState as useState5 } from "react";
1193
+ import { useState as useState6 } from "react";
1067
1194
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1068
1195
  var DECISIONS = [void 0, "DENY", "ALLOW"];
1069
1196
  var SIGNALS = [void 0, "dlp", "ratelimit"];
1070
1197
  function AuditPanel({ active: active2, focused }) {
1071
- const [di, setDi] = useState5(0);
1072
- const [gi, setGi] = useState5(0);
1073
- const [tool, setTool] = useState5("");
1074
- const [agent, setAgent] = useState5("");
1075
- const [search, setSearch] = useState5("");
1076
- const [sel, setSel] = useState5(0);
1077
- const [view, setView] = useState5("list");
1078
- const [editing, setEditing] = useState5(null);
1198
+ const [di, setDi] = useState6(0);
1199
+ const [gi, setGi] = useState6(0);
1200
+ const [tool, setTool] = useState6("");
1201
+ const [agent, setAgent] = useState6("");
1202
+ const [search, setSearch] = useState6("");
1203
+ const [sel, setSel] = useState6(0);
1204
+ const [view, setView] = useState6("list");
1205
+ const [editing, setEditing] = useState6(null);
1079
1206
  const query = {
1080
1207
  filter: DECISIONS[di],
1081
1208
  signal: SIGNALS[gi],
@@ -1231,8 +1358,20 @@ function Detail({ label, value, color }) {
1231
1358
 
1232
1359
  // src/tui/App.tsx
1233
1360
  import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1361
+ function LiveHint() {
1362
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
1363
+ /* @__PURE__ */ jsx8(Text8, { color: theme.accentBright, bold: true, children: "\u25B6 REALTIME CONSOLE" }),
1364
+ /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "Fullscreen live monitor \u2014 streaming tool calls, active charts," }),
1365
+ /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "counters, DLP hits and denial alerts. Updates every 2s." }),
1366
+ /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsxs8(Text8, { children: [
1367
+ "press ",
1368
+ /* @__PURE__ */ jsx8(Text8, { color: theme.accent, children: "enter" }),
1369
+ " to go live"
1370
+ ] }) })
1371
+ ] });
1372
+ }
1234
1373
  var SECTIONS = [
1235
- { label: "Live", Panel: LivePanel },
1374
+ { label: "Live", Panel: LiveHint },
1236
1375
  { label: "Policies", Panel: PoliciesPanel },
1237
1376
  { label: "Rate Limit", Panel: RateLimitPanel },
1238
1377
  { label: "DLP", Panel: DlpPanel },
@@ -1255,8 +1394,8 @@ function Banner() {
1255
1394
  }
1256
1395
  function App() {
1257
1396
  const { exit } = useApp();
1258
- const [section, setSection] = useState6(0);
1259
- const [focus, setFocus] = useState6("nav");
1397
+ const [section, setSection] = useState7(0);
1398
+ const [focus, setFocus] = useState7("nav");
1260
1399
  useInput5((input, key) => {
1261
1400
  if (focus === "nav") {
1262
1401
  if (key.upArrow) setSection((n) => (n - 1 + SECTIONS.length) % SECTIONS.length);
@@ -1265,17 +1404,23 @@ function App() {
1265
1404
  else if (input === "q") exit();
1266
1405
  } else {
1267
1406
  if (key.escape || key.tab) setFocus("nav");
1407
+ else if (input === "q" && SECTIONS[section].label === "Live") exit();
1268
1408
  }
1269
1409
  });
1410
+ const cols = process.stdout.columns ?? 100;
1411
+ const rows = process.stdout.rows ?? 30;
1270
1412
  const current = SECTIONS[section];
1413
+ if (current.label === "Live" && focus === "panel") {
1414
+ return /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", width: cols, height: rows, children: /* @__PURE__ */ jsx8(LivePanel, { active: true, focused: true }) });
1415
+ }
1271
1416
  const Panel = current.Panel;
1272
- return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", paddingX: 1, paddingTop: 1, children: [
1417
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", width: cols, height: rows, paddingX: 1, paddingTop: 1, children: [
1273
1418
  /* @__PURE__ */ jsx8(Banner, {}),
1274
- /* @__PURE__ */ jsxs8(Box8, { marginTop: 1, children: [
1419
+ /* @__PURE__ */ jsxs8(Box8, { marginTop: 1, flexGrow: 1, children: [
1275
1420
  /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", width: 16, borderStyle: "round", borderColor: focus === "nav" ? theme.accent : "gray", paddingX: 1, children: SECTIONS.map((s, i) => /* @__PURE__ */ jsx8(Text8, { color: i === section ? theme.accentBright : void 0, bold: i === section, children: (i === section ? "\u25B8 " : " ") + s.label }, s.label)) }),
1276
- /* @__PURE__ */ jsx8(Box8, { flexGrow: 1, minHeight: 16, borderStyle: "round", borderColor: focus === "panel" ? theme.accent : "gray", paddingX: 1, paddingY: 0, children: /* @__PURE__ */ jsx8(Panel, { active: true, focused: focus === "panel" }) })
1421
+ /* @__PURE__ */ jsx8(Box8, { flexGrow: 1, borderStyle: "round", borderColor: focus === "panel" ? theme.accent : "gray", paddingX: 1, paddingY: 0, children: /* @__PURE__ */ jsx8(Panel, { active: focus === "panel" || current.label !== "Live", focused: focus === "panel" }) })
1277
1422
  ] }),
1278
- /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: focus === "nav" ? /* @__PURE__ */ jsx8(KeyHints, { hints: [["\u2191\u2193", "section"], ["\u2192/enter", "open"], ["q", "quit"]] }) : /* @__PURE__ */ jsx8(KeyHints, { hints: [["\u2191\u2193", "move"], ["\u2190\u2192/space/s", "edit"], ["esc", "back to menu"]] }) })
1423
+ /* @__PURE__ */ jsx8(Box8, { children: focus === "nav" ? /* @__PURE__ */ jsx8(KeyHints, { hints: [["\u2191\u2193", "section"], ["\u2192/enter", "open"], ["q", "quit"]] }) : /* @__PURE__ */ jsx8(KeyHints, { hints: [["\u2190/esc", "back"], ["\u2191\u2193", "in-panel"], ["space/s", "act"]] }) })
1279
1424
  ] });
1280
1425
  }
1281
1426
 
@@ -1292,8 +1437,13 @@ async function launchTui() {
1292
1437
  process.stderr.write("Not logged in. Run `solongate login` first.\n");
1293
1438
  return;
1294
1439
  }
1295
- const { waitUntilExit } = render(/* @__PURE__ */ jsx9(App, {}));
1296
- await waitUntilExit();
1440
+ process.stdout.write("\x1B[?1049h\x1B[H");
1441
+ try {
1442
+ const { waitUntilExit } = render(/* @__PURE__ */ jsx9(App, {}));
1443
+ await waitUntilExit();
1444
+ } finally {
1445
+ process.stdout.write("\x1B[?1049l");
1446
+ }
1297
1447
  }
1298
1448
  export {
1299
1449
  launchTui
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.62.0",
3
+ "version": "0.64.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": {