@solongate/proxy 0.62.0 → 0.63.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,165 @@ 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 })
7028
- ] });
7029
- }
7030
7025
  function LivePanel({ active: active2 }) {
7031
7026
  const stats = useLoader(() => api.stats.get());
7032
7027
  const ts = useLoader(() => api.stats.timeseries({ period: "24h" }));
7033
- const feed = useLoader(() => api.audit.list({ limit: 12 }));
7028
+ const feed = useLoader(() => api.audit.list({ limit: 40 }));
7034
7029
  usePoll(() => {
7035
7030
  stats.reload();
7036
7031
  ts.reload();
7037
7032
  feed.reload();
7038
7033
  }, 2e3, active2);
7034
+ const [tick, setTick] = useState2(0);
7035
+ useEffect2(() => {
7036
+ if (!active2) return;
7037
+ const t = setInterval(() => setTick((n) => n + 1), 200);
7038
+ return () => clearInterval(t);
7039
+ }, [active2]);
7040
+ const startRef = useRef2(Date.now());
7041
+ const [buffer, setBuffer] = useState2([]);
7042
+ useEffect2(() => {
7043
+ const incoming = feed.data?.entries;
7044
+ if (!incoming?.length) return;
7045
+ setBuffer((prev) => {
7046
+ const seen = new Set(prev.map((e) => e.id));
7047
+ const fresh = incoming.filter((e) => !seen.has(e.id)).sort((a, b) => Date.parse(a.created_at) - Date.parse(b.created_at));
7048
+ return fresh.length ? [...prev, ...fresh].slice(-300) : prev;
7049
+ });
7050
+ }, [feed.data]);
7039
7051
  const s = stats.data;
7040
7052
  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: [
7053
+ const cols = process.stdout.columns ?? 100;
7054
+ const rows = process.stdout.rows ?? 32;
7055
+ const feedRows = Math.max(8, Math.min(20, rows - 18));
7056
+ const wide = cols >= 112;
7057
+ const tail = buffer.slice(-feedRows);
7058
+ const denies = buffer.filter((e) => e.decision !== "ALLOW");
7059
+ const dlpHits = buffer.filter((e) => e.dlp_matches?.length);
7060
+ const lastDeny = denies[denies.length - 1];
7061
+ const spin = SPIN[tick % SPIN.length];
7062
+ const cursor = tick % 4 < 2 ? "\u258C" : " ";
7063
+ if (stats.error) return /* @__PURE__ */ jsxs2(Text2, { color: theme.bad, children: [
7064
+ "\u2717 ",
7065
+ stats.error
7066
+ ] });
7067
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
7044
7068
  /* @__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" })
7069
+ /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: "\u25CF " }),
7070
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: theme.accentBright, children: "LIVE" }),
7071
+ /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
7072
+ " ",
7073
+ spin,
7074
+ " scanning"
7075
+ ] }),
7076
+ /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
7077
+ " up ",
7078
+ fmtUp(Date.now() - startRef.current)
7079
+ ] }),
7080
+ /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
7081
+ " ",
7082
+ hhmmss(Date.now())
7083
+ ] }),
7084
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " poll 2s" })
7047
7085
  ] }),
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) })
7086
+ /* @__PURE__ */ jsxs2(Box2, { children: [
7087
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "calls " }),
7088
+ /* @__PURE__ */ jsx2(Text2, { bold: true, children: s ? s.total_calls : "\xB7\xB7\xB7\xB7" }),
7089
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " allow " }),
7090
+ /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: s ? s.allowed : "\xB7\xB7\xB7" }),
7091
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " deny " }),
7092
+ /* @__PURE__ */ jsx2(Text2, { color: theme.bad, bold: true, children: s ? s.denied : "\xB7\xB7" }),
7093
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " dlp(buf) " }),
7094
+ /* @__PURE__ */ jsx2(Text2, { color: dlpHits.length ? theme.bad : theme.dim, children: dlpHits.length }),
7095
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " policies " }),
7096
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: s ? s.active_policies : "\xB7" })
7097
+ ] }),
7098
+ /* @__PURE__ */ jsxs2(Box2, { children: [
7099
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "act " }),
7100
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: sparkline(points.map((p) => p.total), Math.min(64, cols - 30)) })
7101
+ ] }),
7102
+ /* @__PURE__ */ jsxs2(Box2, { children: [
7103
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "dny " }),
7104
+ /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: sparkline(points.map((p) => p.denied), Math.min(64, cols - 30)) })
7105
+ ] }),
7106
+ /* @__PURE__ */ jsxs2(Box2, { children: [
7107
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ">> " }),
7108
+ lastDeny ? /* @__PURE__ */ jsxs2(Text2, { color: theme.bad, children: [
7109
+ "last deny ",
7110
+ hhmmss(lastDeny.created_at),
7111
+ " ",
7112
+ lastDeny.tool_name,
7113
+ " \xB7 ",
7114
+ truncate2(lastDeny.reason ?? "policy", Math.max(20, cols - 50))
7115
+ ] }) : /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "no denials in buffer" })
7116
+ ] }),
7117
+ /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, children: [
7118
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", flexGrow: 3, borderStyle: "single", borderColor: "gray", paddingX: 1, children: [
7119
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, bold: true, children: "\u2500\u2500 tool stream \u2500\u2500" }),
7120
+ tail.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "awaiting traffic\u2026" }) : null,
7121
+ tail.map((e) => /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7122
+ /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
7123
+ "[",
7124
+ hhmmss(e.created_at),
7125
+ "] "
7126
+ ] }),
7127
+ /* @__PURE__ */ jsx2(Text2, { color: decisionColor(e.decision), bold: e.decision !== "ALLOW", children: e.decision.padEnd(6) }),
7128
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: truncate2(e.tool_name, 14).padEnd(15) }),
7129
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: (e.permission ?? "").slice(0, 4).padEnd(5) }),
7130
+ e.dlp_matches?.length ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: "DLP! " }) : null,
7131
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: (e.arguments_summary ? JSON.stringify(e.arguments_summary) : e.reason ?? "").replace(/\s+/g, " ") })
7132
+ ] }, e.id))
7060
7133
  ] }),
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) })
7064
- ] })
7134
+ wide ? /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: 34, borderStyle: "single", borderColor: "gray", paddingX: 1, children: [
7135
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, bold: true, children: "\u2500\u2500 raw \u2500\u2500" }),
7136
+ tail.slice(-Math.floor(feedRows / 2)).flatMap((e) => {
7137
+ const raw = e.arguments_summary ? JSON.stringify(e.arguments_summary) : `hash:${e.id.slice(0, 12)}`;
7138
+ return [
7139
+ /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", color: e.decision === "ALLOW" ? theme.dim : theme.bad, children: [
7140
+ e.id.slice(0, 8),
7141
+ " ",
7142
+ e.tool_name.slice(0, 12)
7143
+ ] }, e.id + ":h"),
7144
+ /* @__PURE__ */ jsx2(Text2, { wrap: "truncate", color: theme.dim, children: " " + raw.replace(/\s+/g, " ") }, e.id + ":b")
7145
+ ];
7146
+ })
7147
+ ] }) : null
7065
7148
  ] }),
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))
7149
+ /* @__PURE__ */ jsxs2(Box2, { children: [
7150
+ /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: "root@solongate" }),
7151
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ":" }),
7152
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, children: "~/live" }),
7153
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "$ " }),
7154
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, children: cursor })
7078
7155
  ] })
7079
- ] }) });
7156
+ ] });
7080
7157
  }
7158
+ var SPIN, hhmmss, fmtUp;
7081
7159
  var init_Live = __esm({
7082
7160
  "src/tui/panels/Live.tsx"() {
7083
7161
  "use strict";
7084
7162
  init_api_client();
7085
- init_components();
7086
7163
  init_hooks();
7087
7164
  init_theme();
7165
+ SPIN = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
7166
+ hhmmss = (ts) => {
7167
+ const d = new Date(ts);
7168
+ return Number.isNaN(d.getTime()) ? "--:--:--" : d.toTimeString().slice(0, 8);
7169
+ };
7170
+ fmtUp = (ms) => {
7171
+ const s = Math.floor(ms / 1e3);
7172
+ const p = (n) => String(n).padStart(2, "0");
7173
+ return `${p(Math.floor(s / 3600))}:${p(Math.floor(s % 3600 / 60))}:${p(s % 60)}`;
7174
+ };
7088
7175
  }
7089
7176
  });
7090
7177
 
7091
7178
  // src/tui/panels/Policies.tsx
7092
7179
  import { Box as Box3, Text as Text3, useInput } from "ink";
7093
7180
  import TextInput from "ink-text-input";
7094
- import { useEffect as useEffect2, useState as useState2 } from "react";
7181
+ import { useEffect as useEffect3, useState as useState3 } from "react";
7095
7182
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
7096
7183
  function ruleSummary(r) {
7097
7184
  const bits = [];
@@ -7105,19 +7192,19 @@ function ruleSummary(r) {
7105
7192
  function PoliciesPanel({ focused }) {
7106
7193
  const list4 = useLoader(() => api.policies.list());
7107
7194
  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);
7195
+ const [view, setView] = useState3("list");
7196
+ const [pi, setPi] = useState3(0);
7197
+ const [ri, setRi] = useState3(0);
7198
+ const [fi, setFi] = useState3(0);
7199
+ const [rules, setRules] = useState3([]);
7200
+ const [mode, setMode] = useState3("denylist");
7201
+ const [dirty, setDirty] = useState3(false);
7202
+ const [editing, setEditing] = useState3(false);
7203
+ const [editVal, setEditVal] = useState3("");
7204
+ const [status, setStatus] = useState3(null);
7118
7205
  const selected = policies[Math.min(pi, Math.max(0, policies.length - 1))];
7119
7206
  const detail = useLoader(() => selected ? api.policies.get(selected.id) : Promise.resolve(null), [selected?.id]);
7120
- useEffect2(() => {
7207
+ useEffect3(() => {
7121
7208
  if (detail.data) {
7122
7209
  setRules(detail.data.rules);
7123
7210
  setMode(detail.data.mode ?? "denylist");
@@ -7335,7 +7422,7 @@ var init_Policies = __esm({
7335
7422
 
7336
7423
  // src/tui/panels/RateLimit.tsx
7337
7424
  import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
7338
- import { useEffect as useEffect3, useState as useState3 } from "react";
7425
+ import { useEffect as useEffect4, useState as useState4 } from "react";
7339
7426
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
7340
7427
  function ColoredSpark({ values, limit, width: width2 = 60 }) {
7341
7428
  const v = values.length > width2 ? values.slice(values.length - width2) : values;
@@ -7350,11 +7437,11 @@ function RateLimitPanel({ focused }) {
7350
7437
  const layersQ = useLoader(() => api.settings.getSecurityLayers());
7351
7438
  const historyQ = useLoader(() => api.settings.getRateLimitHistory());
7352
7439
  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(() => {
7440
+ const [draft, setDraft] = useState4(null);
7441
+ const [dirty, setDirty] = useState4(false);
7442
+ const [fi, setFi] = useState4(0);
7443
+ const [status, setStatus] = useState4(null);
7444
+ useEffect4(() => {
7358
7445
  if (layersQ.data && !draft) setDraft({ ...layersQ.data.layers.rateLimit });
7359
7446
  }, [layersQ.data, draft]);
7360
7447
  const adjust = (dir, step) => {
@@ -7476,19 +7563,19 @@ var init_RateLimit = __esm({
7476
7563
  // src/tui/panels/Dlp.tsx
7477
7564
  import { Box as Box5, Text as Text5, useInput as useInput3 } from "ink";
7478
7565
  import TextInput2 from "ink-text-input";
7479
- import { useEffect as useEffect4, useState as useState4 } from "react";
7566
+ import { useEffect as useEffect5, useState as useState5 } from "react";
7480
7567
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
7481
7568
  function DlpPanel({ focused }) {
7482
7569
  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(() => {
7570
+ const [dlp, setDlp] = useState5(null);
7571
+ const [available, setAvailable] = useState5([]);
7572
+ const [sel, setSel] = useState5(0);
7573
+ const [dirty, setDirty] = useState5(false);
7574
+ const [status, setStatus] = useState5(null);
7575
+ const [adding, setAdding] = useState5(null);
7576
+ const [newName, setNewName] = useState5("");
7577
+ const [newRe, setNewRe] = useState5("");
7578
+ useEffect5(() => {
7492
7579
  if (q.data && !dlp) {
7493
7580
  setDlp({ ...q.data.layers.dlp, patterns: [...q.data.layers.dlp.patterns], custom: [...q.data.layers.dlp.custom] });
7494
7581
  setAvailable(q.data.availablePatterns);
@@ -7702,17 +7789,17 @@ var init_Stats = __esm({
7702
7789
  // src/tui/panels/Audit.tsx
7703
7790
  import { Box as Box7, Text as Text7, useInput as useInput4 } from "ink";
7704
7791
  import TextInput3 from "ink-text-input";
7705
- import { useState as useState5 } from "react";
7792
+ import { useState as useState6 } from "react";
7706
7793
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
7707
7794
  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);
7795
+ const [di, setDi] = useState6(0);
7796
+ const [gi, setGi] = useState6(0);
7797
+ const [tool, setTool] = useState6("");
7798
+ const [agent, setAgent] = useState6("");
7799
+ const [search, setSearch] = useState6("");
7800
+ const [sel, setSel] = useState6(0);
7801
+ const [view, setView] = useState6("list");
7802
+ const [editing, setEditing] = useState6(null);
7716
7803
  const query = {
7717
7804
  filter: DECISIONS[di],
7718
7805
  signal: SIGNALS[gi],
@@ -7880,7 +7967,7 @@ var init_Audit = __esm({
7880
7967
 
7881
7968
  // src/tui/App.tsx
7882
7969
  import { Box as Box8, Text as Text8, useApp, useInput as useInput5 } from "ink";
7883
- import { useState as useState6 } from "react";
7970
+ import { useState as useState7 } from "react";
7884
7971
  import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
7885
7972
  function Banner() {
7886
7973
  const wide = (process.stdout.columns ?? 80) >= 82;
@@ -7897,8 +7984,8 @@ function Banner() {
7897
7984
  }
7898
7985
  function App() {
7899
7986
  const { exit } = useApp();
7900
- const [section, setSection] = useState6(0);
7901
- const [focus, setFocus] = useState6("nav");
7987
+ const [section, setSection] = useState7(0);
7988
+ const [focus, setFocus] = useState7("nav");
7902
7989
  useInput5((input, key) => {
7903
7990
  if (focus === "nav") {
7904
7991
  if (key.upArrow) setSection((n) => (n - 1 + SECTIONS.length) % SECTIONS.length);
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,154 @@ 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 })
437
- ] });
438
- }
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
+ };
439
444
  function LivePanel({ active: active2 }) {
440
445
  const stats = useLoader(() => api.stats.get());
441
446
  const ts = useLoader(() => api.stats.timeseries({ period: "24h" }));
442
- const feed = useLoader(() => api.audit.list({ limit: 12 }));
447
+ const feed = useLoader(() => api.audit.list({ limit: 40 }));
443
448
  usePoll(() => {
444
449
  stats.reload();
445
450
  ts.reload();
446
451
  feed.reload();
447
452
  }, 2e3, active2);
453
+ const [tick, setTick] = useState2(0);
454
+ useEffect2(() => {
455
+ if (!active2) return;
456
+ const t = setInterval(() => setTick((n) => n + 1), 200);
457
+ return () => clearInterval(t);
458
+ }, [active2]);
459
+ const startRef = useRef2(Date.now());
460
+ const [buffer, setBuffer] = useState2([]);
461
+ useEffect2(() => {
462
+ const incoming = feed.data?.entries;
463
+ if (!incoming?.length) return;
464
+ setBuffer((prev) => {
465
+ const seen = new Set(prev.map((e) => e.id));
466
+ const fresh = incoming.filter((e) => !seen.has(e.id)).sort((a, b) => Date.parse(a.created_at) - Date.parse(b.created_at));
467
+ return fresh.length ? [...prev, ...fresh].slice(-300) : prev;
468
+ });
469
+ }, [feed.data]);
448
470
  const s = stats.data;
449
471
  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: [
472
+ const cols = process.stdout.columns ?? 100;
473
+ const rows = process.stdout.rows ?? 32;
474
+ const feedRows = Math.max(8, Math.min(20, rows - 18));
475
+ const wide = cols >= 112;
476
+ const tail = buffer.slice(-feedRows);
477
+ const denies = buffer.filter((e) => e.decision !== "ALLOW");
478
+ const dlpHits = buffer.filter((e) => e.dlp_matches?.length);
479
+ const lastDeny = denies[denies.length - 1];
480
+ const spin = SPIN[tick % SPIN.length];
481
+ const cursor = tick % 4 < 2 ? "\u258C" : " ";
482
+ if (stats.error) return /* @__PURE__ */ jsxs2(Text2, { color: theme.bad, children: [
483
+ "\u2717 ",
484
+ stats.error
485
+ ] });
486
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
453
487
  /* @__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" })
488
+ /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: "\u25CF " }),
489
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: theme.accentBright, children: "LIVE" }),
490
+ /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
491
+ " ",
492
+ spin,
493
+ " scanning"
494
+ ] }),
495
+ /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
496
+ " up ",
497
+ fmtUp(Date.now() - startRef.current)
498
+ ] }),
499
+ /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
500
+ " ",
501
+ hhmmss(Date.now())
502
+ ] }),
503
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " poll 2s" })
456
504
  ] }),
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) })
505
+ /* @__PURE__ */ jsxs2(Box2, { children: [
506
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "calls " }),
507
+ /* @__PURE__ */ jsx2(Text2, { bold: true, children: s ? s.total_calls : "\xB7\xB7\xB7\xB7" }),
508
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " allow " }),
509
+ /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: s ? s.allowed : "\xB7\xB7\xB7" }),
510
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " deny " }),
511
+ /* @__PURE__ */ jsx2(Text2, { color: theme.bad, bold: true, children: s ? s.denied : "\xB7\xB7" }),
512
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " dlp(buf) " }),
513
+ /* @__PURE__ */ jsx2(Text2, { color: dlpHits.length ? theme.bad : theme.dim, children: dlpHits.length }),
514
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " policies " }),
515
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: s ? s.active_policies : "\xB7" })
516
+ ] }),
517
+ /* @__PURE__ */ jsxs2(Box2, { children: [
518
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "act " }),
519
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: sparkline(points.map((p) => p.total), Math.min(64, cols - 30)) })
520
+ ] }),
521
+ /* @__PURE__ */ jsxs2(Box2, { children: [
522
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "dny " }),
523
+ /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: sparkline(points.map((p) => p.denied), Math.min(64, cols - 30)) })
524
+ ] }),
525
+ /* @__PURE__ */ jsxs2(Box2, { children: [
526
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ">> " }),
527
+ lastDeny ? /* @__PURE__ */ jsxs2(Text2, { color: theme.bad, children: [
528
+ "last deny ",
529
+ hhmmss(lastDeny.created_at),
530
+ " ",
531
+ lastDeny.tool_name,
532
+ " \xB7 ",
533
+ truncate(lastDeny.reason ?? "policy", Math.max(20, cols - 50))
534
+ ] }) : /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "no denials in buffer" })
535
+ ] }),
536
+ /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, children: [
537
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", flexGrow: 3, borderStyle: "single", borderColor: "gray", paddingX: 1, children: [
538
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, bold: true, children: "\u2500\u2500 tool stream \u2500\u2500" }),
539
+ tail.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "awaiting traffic\u2026" }) : null,
540
+ tail.map((e) => /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
541
+ /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
542
+ "[",
543
+ hhmmss(e.created_at),
544
+ "] "
545
+ ] }),
546
+ /* @__PURE__ */ jsx2(Text2, { color: decisionColor(e.decision), bold: e.decision !== "ALLOW", children: e.decision.padEnd(6) }),
547
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: truncate(e.tool_name, 14).padEnd(15) }),
548
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: (e.permission ?? "").slice(0, 4).padEnd(5) }),
549
+ e.dlp_matches?.length ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: "DLP! " }) : null,
550
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: (e.arguments_summary ? JSON.stringify(e.arguments_summary) : e.reason ?? "").replace(/\s+/g, " ") })
551
+ ] }, e.id))
469
552
  ] }),
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) })
473
- ] })
553
+ wide ? /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: 34, borderStyle: "single", borderColor: "gray", paddingX: 1, children: [
554
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, bold: true, children: "\u2500\u2500 raw \u2500\u2500" }),
555
+ tail.slice(-Math.floor(feedRows / 2)).flatMap((e) => {
556
+ const raw = e.arguments_summary ? JSON.stringify(e.arguments_summary) : `hash:${e.id.slice(0, 12)}`;
557
+ return [
558
+ /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", color: e.decision === "ALLOW" ? theme.dim : theme.bad, children: [
559
+ e.id.slice(0, 8),
560
+ " ",
561
+ e.tool_name.slice(0, 12)
562
+ ] }, e.id + ":h"),
563
+ /* @__PURE__ */ jsx2(Text2, { wrap: "truncate", color: theme.dim, children: " " + raw.replace(/\s+/g, " ") }, e.id + ":b")
564
+ ];
565
+ })
566
+ ] }) : null
474
567
  ] }),
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))
568
+ /* @__PURE__ */ jsxs2(Box2, { children: [
569
+ /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: "root@solongate" }),
570
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ":" }),
571
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, children: "~/live" }),
572
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "$ " }),
573
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, children: cursor })
487
574
  ] })
488
- ] }) });
575
+ ] });
489
576
  }
490
577
 
491
578
  // src/tui/panels/Policies.tsx
492
579
  import { Box as Box3, Text as Text3, useInput } from "ink";
493
580
  import TextInput from "ink-text-input";
494
- import { useEffect as useEffect2, useState as useState2 } from "react";
581
+ import { useEffect as useEffect3, useState as useState3 } from "react";
495
582
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
496
583
  var constr = (r, g, side) => (r[g]?.[side] ?? []).join(", ");
497
584
  var setConstr = (r, g, side, val) => {
@@ -526,19 +613,19 @@ function ruleSummary(r) {
526
613
  function PoliciesPanel({ focused }) {
527
614
  const list3 = useLoader(() => api.policies.list());
528
615
  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);
616
+ const [view, setView] = useState3("list");
617
+ const [pi, setPi] = useState3(0);
618
+ const [ri, setRi] = useState3(0);
619
+ const [fi, setFi] = useState3(0);
620
+ const [rules, setRules] = useState3([]);
621
+ const [mode, setMode] = useState3("denylist");
622
+ const [dirty, setDirty] = useState3(false);
623
+ const [editing, setEditing] = useState3(false);
624
+ const [editVal, setEditVal] = useState3("");
625
+ const [status, setStatus] = useState3(null);
539
626
  const selected = policies[Math.min(pi, Math.max(0, policies.length - 1))];
540
627
  const detail = useLoader(() => selected ? api.policies.get(selected.id) : Promise.resolve(null), [selected?.id]);
541
- useEffect2(() => {
628
+ useEffect3(() => {
542
629
  if (detail.data) {
543
630
  setRules(detail.data.rules);
544
631
  setMode(detail.data.mode ?? "denylist");
@@ -725,7 +812,7 @@ function PoliciesPanel({ focused }) {
725
812
 
726
813
  // src/tui/panels/RateLimit.tsx
727
814
  import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
728
- import { useEffect as useEffect3, useState as useState3 } from "react";
815
+ import { useEffect as useEffect4, useState as useState4 } from "react";
729
816
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
730
817
  var MODES = ["off", "detect", "block"];
731
818
  var FIELDS2 = ["mode", "perMinute", "perHour", "perDay"];
@@ -743,11 +830,11 @@ function RateLimitPanel({ focused }) {
743
830
  const layersQ = useLoader(() => api.settings.getSecurityLayers());
744
831
  const historyQ = useLoader(() => api.settings.getRateLimitHistory());
745
832
  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(() => {
833
+ const [draft, setDraft] = useState4(null);
834
+ const [dirty, setDirty] = useState4(false);
835
+ const [fi, setFi] = useState4(0);
836
+ const [status, setStatus] = useState4(null);
837
+ useEffect4(() => {
751
838
  if (layersQ.data && !draft) setDraft({ ...layersQ.data.layers.rateLimit });
752
839
  }, [layersQ.data, draft]);
753
840
  const adjust = (dir, step) => {
@@ -856,20 +943,20 @@ function FieldRow({ label, active: active2, children }) {
856
943
  // src/tui/panels/Dlp.tsx
857
944
  import { Box as Box5, Text as Text5, useInput as useInput3 } from "ink";
858
945
  import TextInput2 from "ink-text-input";
859
- import { useEffect as useEffect4, useState as useState4 } from "react";
946
+ import { useEffect as useEffect5, useState as useState5 } from "react";
860
947
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
861
948
  var MODES2 = ["off", "detect", "block"];
862
949
  function DlpPanel({ focused }) {
863
950
  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(() => {
951
+ const [dlp, setDlp] = useState5(null);
952
+ const [available, setAvailable] = useState5([]);
953
+ const [sel, setSel] = useState5(0);
954
+ const [dirty, setDirty] = useState5(false);
955
+ const [status, setStatus] = useState5(null);
956
+ const [adding, setAdding] = useState5(null);
957
+ const [newName, setNewName] = useState5("");
958
+ const [newRe, setNewRe] = useState5("");
959
+ useEffect5(() => {
873
960
  if (q.data && !dlp) {
874
961
  setDlp({ ...q.data.layers.dlp, patterns: [...q.data.layers.dlp.patterns], custom: [...q.data.layers.dlp.custom] });
875
962
  setAvailable(q.data.availablePatterns);
@@ -1063,19 +1150,19 @@ function StatsPanel({ active: active2 }) {
1063
1150
  // src/tui/panels/Audit.tsx
1064
1151
  import { Box as Box7, Text as Text7, useInput as useInput4 } from "ink";
1065
1152
  import TextInput3 from "ink-text-input";
1066
- import { useState as useState5 } from "react";
1153
+ import { useState as useState6 } from "react";
1067
1154
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1068
1155
  var DECISIONS = [void 0, "DENY", "ALLOW"];
1069
1156
  var SIGNALS = [void 0, "dlp", "ratelimit"];
1070
1157
  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);
1158
+ const [di, setDi] = useState6(0);
1159
+ const [gi, setGi] = useState6(0);
1160
+ const [tool, setTool] = useState6("");
1161
+ const [agent, setAgent] = useState6("");
1162
+ const [search, setSearch] = useState6("");
1163
+ const [sel, setSel] = useState6(0);
1164
+ const [view, setView] = useState6("list");
1165
+ const [editing, setEditing] = useState6(null);
1079
1166
  const query = {
1080
1167
  filter: DECISIONS[di],
1081
1168
  signal: SIGNALS[gi],
@@ -1255,8 +1342,8 @@ function Banner() {
1255
1342
  }
1256
1343
  function App() {
1257
1344
  const { exit } = useApp();
1258
- const [section, setSection] = useState6(0);
1259
- const [focus, setFocus] = useState6("nav");
1345
+ const [section, setSection] = useState7(0);
1346
+ const [focus, setFocus] = useState7("nav");
1260
1347
  useInput5((input, key) => {
1261
1348
  if (focus === "nav") {
1262
1349
  if (key.upArrow) setSection((n) => (n - 1 + SECTIONS.length) % SECTIONS.length);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.62.0",
3
+ "version": "0.63.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": {