@solongate/proxy 0.75.0 → 0.76.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.
@@ -86,26 +86,26 @@ async function request(method, path, opts = {}) {
86
86
  }
87
87
  const attempt = () => fetch(url, {
88
88
  method,
89
- headers,
89
+ headers: { ...headers, Connection: "close" },
90
90
  body: bodyInit,
91
91
  signal: AbortSignal.timeout(opts.timeoutMs ?? 15e3)
92
92
  });
93
+ const maxTries = method === "GET" ? 3 : 1;
93
94
  let res;
94
- try {
95
- res = await attempt();
96
- } catch (firstErr) {
97
- if (method !== "GET") {
98
- const msg = firstErr instanceof Error ? firstErr.message : String(firstErr);
99
- throw new ApiError(0, "NETWORK_ERROR", `Cannot reach SolonGate API: ${msg}`);
100
- }
101
- await new Promise((r) => setTimeout(r, 800));
95
+ let lastErr;
96
+ for (let i = 0; i < maxTries; i++) {
102
97
  try {
103
98
  res = await attempt();
99
+ break;
104
100
  } catch (err2) {
105
- const msg = err2 instanceof Error ? err2.message : String(err2);
106
- throw new ApiError(0, "NETWORK_ERROR", `Cannot reach SolonGate API: ${msg}`);
101
+ lastErr = err2;
102
+ if (i < maxTries - 1) await new Promise((r) => setTimeout(r, 600 * (i + 1)));
107
103
  }
108
104
  }
105
+ if (!res) {
106
+ const msg = lastErr instanceof Error ? lastErr.message : String(lastErr);
107
+ throw new ApiError(0, "NETWORK_ERROR", `Cannot reach SolonGate API: ${msg}`);
108
+ }
109
109
  const text = await res.text().catch(() => "");
110
110
  let json = void 0;
111
111
  if (text) {
package/dist/index.js CHANGED
@@ -6635,26 +6635,26 @@ async function request(method, path, opts = {}) {
6635
6635
  }
6636
6636
  const attempt = () => fetch(url, {
6637
6637
  method,
6638
- headers,
6638
+ headers: { ...headers, Connection: "close" },
6639
6639
  body: bodyInit,
6640
6640
  signal: AbortSignal.timeout(opts.timeoutMs ?? 15e3)
6641
6641
  });
6642
+ const maxTries = method === "GET" ? 3 : 1;
6642
6643
  let res;
6643
- try {
6644
- res = await attempt();
6645
- } catch (firstErr) {
6646
- if (method !== "GET") {
6647
- const msg = firstErr instanceof Error ? firstErr.message : String(firstErr);
6648
- throw new ApiError(0, "NETWORK_ERROR", `Cannot reach SolonGate API: ${msg}`);
6649
- }
6650
- await new Promise((r) => setTimeout(r, 800));
6644
+ let lastErr;
6645
+ for (let i = 0; i < maxTries; i++) {
6651
6646
  try {
6652
6647
  res = await attempt();
6648
+ break;
6653
6649
  } catch (err2) {
6654
- const msg = err2 instanceof Error ? err2.message : String(err2);
6655
- throw new ApiError(0, "NETWORK_ERROR", `Cannot reach SolonGate API: ${msg}`);
6650
+ lastErr = err2;
6651
+ if (i < maxTries - 1) await new Promise((r) => setTimeout(r, 600 * (i + 1)));
6656
6652
  }
6657
6653
  }
6654
+ if (!res) {
6655
+ const msg = lastErr instanceof Error ? lastErr.message : String(lastErr);
6656
+ throw new ApiError(0, "NETWORK_ERROR", `Cannot reach SolonGate API: ${msg}`);
6657
+ }
6658
6658
  const text = await res.text().catch(() => "");
6659
6659
  let json = void 0;
6660
6660
  if (text) {
@@ -7031,6 +7031,7 @@ var init_hooks = __esm({
7031
7031
  // src/tui/panels/Live.tsx
7032
7032
  import { Box as Box2, Text as Text2, useInput } from "ink";
7033
7033
  import TextInput from "ink-text-input";
7034
+ import { spawn } from "child_process";
7034
7035
  import { closeSync, openSync, readSync, statSync } from "fs";
7035
7036
  import { homedir as homedir3 } from "os";
7036
7037
  import { join as join5 } from "path";
@@ -7154,18 +7155,26 @@ function LivePanel({ active: active2 }) {
7154
7155
  const seenRef = useRef2(/* @__PURE__ */ new Set());
7155
7156
  const lastLocalTs = useRef2(0);
7156
7157
  const pausedUntil = useRef2(0);
7158
+ const sessRef = useRef2([]);
7159
+ const prevStatus = useRef2(/* @__PURE__ */ new Map());
7160
+ const [alerts, setAlerts] = useState2([]);
7157
7161
  const [frozen, setFrozen] = useState2(false);
7158
7162
  const frozenRef = useRef2(false);
7159
7163
  frozenRef.current = frozen;
7160
7164
  const pushLog = useCallback2((msg, level = "ok") => {
7161
7165
  setLog((prev) => [...prev.slice(-59), { ts: Date.now(), msg, level }]);
7162
7166
  }, []);
7167
+ const netFails = useRef2(0);
7163
7168
  const onApiError = useCallback2(
7164
7169
  (e) => {
7165
7170
  const is429 = e instanceof ApiError && e.status === 429;
7171
+ const isNet = e instanceof ApiError && e.status === 0;
7166
7172
  if (is429 && Date.now() >= pausedUntil.current) {
7167
7173
  pausedUntil.current = Date.now() + 3e4;
7168
7174
  pushLog("rate limited by api \xB7 backing off 30s", "warn");
7175
+ } else if (isNet) {
7176
+ netFails.current += 1;
7177
+ if (netFails.current === 3) pushLog("network unreachable \xB7 retrying\u2026", "warn");
7169
7178
  } else if (!is429) {
7170
7179
  pushLog(`api error \xB7 ${truncate2(e instanceof Error ? e.message : String(e), 40)}`, "bad");
7171
7180
  }
@@ -7241,6 +7250,8 @@ function LivePanel({ active: active2 }) {
7241
7250
  try {
7242
7251
  const fd = await api.audit.list({ limit: 50 });
7243
7252
  if (frozenRef.current) return;
7253
+ if (netFails.current >= 3) pushLog("network recovered", "ok");
7254
+ netFails.current = 0;
7244
7255
  const ms = Date.now() - t0;
7245
7256
  setLat((l) => [...l, ms].slice(-240));
7246
7257
  const fresh = fd.entries.filter((e) => !seenRef.current.has(e.id));
@@ -7307,6 +7318,30 @@ function LivePanel({ active: active2 }) {
7307
7318
  return () => clearInterval(t);
7308
7319
  }, [active2, frozen]);
7309
7320
  const startRef = useRef2(Date.now());
7321
+ useEffect2(() => {
7322
+ if (!active2) return;
7323
+ for (const r of sessRef.current) {
7324
+ const prev = prevStatus.current.get(r.id);
7325
+ if (prev && prev === "active" && r.status === "idle") {
7326
+ const name = r.isMe ? `${r.agent} (this machine)` : r.agent;
7327
+ const msg = `${name} went IDLE \u2014 no tool call for 60s`;
7328
+ pushLog(`\u23F8 ${msg}`, "warn");
7329
+ setAlerts((a) => [...a.filter((x) => x.id !== r.id), { id: r.id, msg, until: Date.now() + 15e3 }].slice(-4));
7330
+ try {
7331
+ process.stdout.write("\x07");
7332
+ } catch {
7333
+ }
7334
+ try {
7335
+ const child = spawn("notify-send", ["-a", "SolonGate", "Agent idle", msg], { stdio: "ignore", detached: true });
7336
+ child.on("error", () => {
7337
+ });
7338
+ child.unref();
7339
+ } catch {
7340
+ }
7341
+ }
7342
+ prevStatus.current.set(r.id, r.status);
7343
+ }
7344
+ }, [tick, active2, pushLog]);
7310
7345
  const cols = process.stdout.columns ?? 100;
7311
7346
  const rows = process.stdout.rows ?? 30;
7312
7347
  const ins = insights.data ?? {};
@@ -7359,7 +7394,7 @@ function LivePanel({ active: active2 }) {
7359
7394
  localSess.set(e.session, cur);
7360
7395
  }
7361
7396
  const combinedSess = [
7362
- ...[...localSess.entries()].sort((a, b) => b[1].lastAt - a[1].lastAt).map(([id, v]) => ({ source: "local", id, agent: v.agent, calls: v.calls, denies: v.denies, lastAt: v.lastAt, isMe: ring?.session === id })),
7397
+ ...[...localSess.entries()].sort((a, b) => b[1].lastAt - a[1].lastAt).map(([id, v]) => ({ source: "local", id, agent: v.agent, calls: v.calls, denies: v.denies, lastAt: v.lastAt, status: sessStatus(v.lastAt, nowMs), isMe: ring?.session === id })),
7363
7398
  ...sess.filter((a) => !localSess.has(a.session_id)).map((a) => ({
7364
7399
  source: "cloud",
7365
7400
  id: a.session_id,
@@ -7367,10 +7402,12 @@ function LivePanel({ active: active2 }) {
7367
7402
  calls: a.total_calls,
7368
7403
  denies: a.denied_calls,
7369
7404
  lastAt: Date.parse(a.last_seen_at),
7405
+ status: sessStatus(Date.parse(a.last_seen_at), nowMs),
7370
7406
  trust: a.trust_score,
7371
7407
  isMe: false
7372
7408
  }))
7373
7409
  ];
7410
+ sessRef.current = combinedSess;
7374
7411
  const localSessIds = new Set(localSess.keys());
7375
7412
  if (ring?.session) localSessIds.add(ring.session);
7376
7413
  const isLoc = (e) => e.source === "local" || !!e.session && localSessIds.has(e.session);
@@ -7454,6 +7491,10 @@ function LivePanel({ active: active2 }) {
7454
7491
  {
7455
7492
  value: search,
7456
7493
  onChange: (v) => {
7494
+ if (v.endsWith("/")) {
7495
+ setEditingSearch(false);
7496
+ return;
7497
+ }
7457
7498
  setSearch(v);
7458
7499
  setScroll(0);
7459
7500
  setDetailScroll(0);
@@ -7461,7 +7502,7 @@ function LivePanel({ active: active2 }) {
7461
7502
  onSubmit: () => setEditingSearch(false)
7462
7503
  }
7463
7504
  ),
7464
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " (enter done \xB7 empty = clear)" })
7505
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " (enter/\u200B/ done \xB7 empty = clear)" })
7465
7506
  ] }) : search ? /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7466
7507
  /* @__PURE__ */ jsx2(Text2, { color: theme.warn, children: "search: " }),
7467
7508
  /* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, bold: true, children: search }),
@@ -7505,7 +7546,11 @@ function LivePanel({ active: active2 }) {
7505
7546
  titleBar,
7506
7547
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7507
7548
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#AAC8FA", bold: true, children: ` SESSION ${detail.id.slice(0, 8)} ` }),
7508
- /* @__PURE__ */ jsx2(Text2, { color: detail.source === "local" ? theme.ok : "#4f6db8", children: " " + detail.source.toUpperCase() }),
7549
+ (() => {
7550
+ const st = STATUS_STYLE[sessStatus(detail.lastAt, nowMs)];
7551
+ return /* @__PURE__ */ jsx2(Text2, { color: st.color, bold: true, children: ` ${st.dot} ${st.label}` });
7552
+ })(),
7553
+ /* @__PURE__ */ jsx2(Text2, { color: detail.source === "local" ? theme.ok : "#4f6db8", children: " " + detail.source.toUpperCase() }),
7509
7554
  /* @__PURE__ */ jsx2(Text2, { bold: true, children: " " + truncate2(detail.isMe ? detail.agent + " (this machine)" : detail.agent, 34) }),
7510
7555
  detail.trust != null ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ` trust ${detail.trust}/100` }) : null,
7511
7556
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2190 back" })
@@ -7535,8 +7580,8 @@ function LivePanel({ active: active2 }) {
7535
7580
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502" })
7536
7581
  ] }),
7537
7582
  /* @__PURE__ */ jsx2(PaneTitle, { label: "TIMELINE", extra: `${d.length} calls \xB7 newest first${dScroll ? ` \xB7 \u25BC${dScroll} older` : ""} \xB7 \u2191\u2193 scroll \xB7 / search \xB7 \u2190 back`, width: innerW }),
7538
- searchRow,
7539
7583
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height: tlRows, overflow: "hidden", children: [
7584
+ searchRow,
7540
7585
  tl.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: detailCloud.length === 0 ? spin + " loading history\u2026" : "no entries" }) : null,
7541
7586
  tl.map((e) => /* @__PURE__ */ jsx2(StreamLine, { e, loc: isLoc(e) }, e.id))
7542
7587
  ] }),
@@ -7570,6 +7615,7 @@ function LivePanel({ active: active2 }) {
7570
7615
  /* @__PURE__ */ jsx2(Text2, { color: guard.data?.up_to_date ? theme.ok : theme.warn, children: guard.data ? `v${guard.data.installed ?? "?"}${guard.data.up_to_date ? "" : "\u2192v" + guard.data.latest} ${guard.data.device_count}dev` : "\xB7" }),
7571
7616
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502" })
7572
7617
  ] }),
7618
+ alerts.filter((a) => nowMs < a.until).slice(-1).map((a) => /* @__PURE__ */ jsx2(Text2, { wrap: "truncate", backgroundColor: "#3d2a12", color: "#ffb454", bold: true, children: ` \u23F8 IDLE \xB7 ${truncate2(a.msg, innerW - 12)} ` }, a.id)),
7573
7619
  /* @__PURE__ */ jsxs2(Box2, { height: 1 + chartH, children: [
7574
7620
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: leftW, marginRight: 2, children: [
7575
7621
  /* @__PURE__ */ jsx2(PaneTitle, { label: "TRAFFIC", extra: `${trafficLabel} \xB7 peak ${Math.max(0, ...traffic)} \xB7 red = denials`, width: leftW }),
@@ -7607,14 +7653,14 @@ function LivePanel({ active: active2 }) {
7607
7653
  /* @__PURE__ */ jsx2(PaneTitle, { label: "SESSIONS", extra: mode === "pick" ? "\u2191\u2193 pick \xB7 enter open" : `${combinedSess.length} \xB7 s = inspect`, width: colW }),
7608
7654
  pickable.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "scanning\u2026" }) : null,
7609
7655
  pickable.map((r, i) => {
7610
- const freshDot = nowMs - r.lastAt < 9e4;
7611
7656
  const sel = mode === "pick" && i === pickIdx;
7657
+ const st = STATUS_STYLE[r.status];
7612
7658
  return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", backgroundColor: sel ? "#1c2f63" : void 0, bold: sel || r.isMe, children: [
7613
- /* @__PURE__ */ jsxs2(Text2, { color: sel ? theme.accentBright : freshDot ? theme.ok : theme.dim, children: [
7614
- sel ? "\u25B8" : freshDot ? "\u25CF" : "\u25CB",
7659
+ /* @__PURE__ */ jsxs2(Text2, { color: sel ? theme.accentBright : st.color, children: [
7660
+ sel ? "\u25B8" : st.dot,
7615
7661
  " "
7616
7662
  ] }),
7617
- /* @__PURE__ */ jsx2(Text2, { color: r.source === "local" ? theme.ok : "#4f6db8", children: r.source === "local" ? "LOC " : "CLD " }),
7663
+ /* @__PURE__ */ jsx2(Text2, { color: st.color, children: st.label.padEnd(7) }),
7618
7664
  /* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, children: truncate2(r.isMe ? "this machine" : r.agent, 12).padEnd(13) }),
7619
7665
  /* @__PURE__ */ jsxs2(Text2, { children: [
7620
7666
  String(r.calls).padStart(4),
@@ -7648,7 +7694,7 @@ function LivePanel({ active: active2 }) {
7648
7694
  width: innerW
7649
7695
  }
7650
7696
  ),
7651
- /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height: streamRows, overflow: "hidden", children: [
7697
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height: searchRow ? streamRows - 1 : streamRows, overflow: "hidden", children: [
7652
7698
  searchRow,
7653
7699
  localOn === false ? /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7654
7700
  /* @__PURE__ */ jsx2(Text2, { color: theme.warn, children: "local logs off" }),
@@ -7667,7 +7713,7 @@ function LivePanel({ active: active2 }) {
7667
7713
  ] })
7668
7714
  ] });
7669
7715
  }
7670
- var SPIN, BG, DIM_FLOOR, LOCAL_LOG, RING, STREAM_KEEP, hhmmss, fmtUp, FILTERS;
7716
+ var SPIN, BG, DIM_FLOOR, LOCAL_LOG, RING, STREAM_KEEP, hhmmss, fmtUp, FILTERS, sessStatus, STATUS_STYLE;
7671
7717
  var init_Live = __esm({
7672
7718
  "src/tui/panels/Live.tsx"() {
7673
7719
  "use strict";
@@ -7690,6 +7736,12 @@ var init_Live = __esm({
7690
7736
  return `${p(Math.floor(s / 3600))}:${p(Math.floor(s % 3600 / 60))}:${p(s % 60)}`;
7691
7737
  };
7692
7738
  FILTERS = ["all", "local", "cloud"];
7739
+ sessStatus = (lastAt, now) => now - lastAt < 6e4 ? "active" : now - lastAt < 3e5 ? "idle" : "ended";
7740
+ STATUS_STYLE = {
7741
+ active: { dot: "\u25CF", label: "ACTIVE", color: theme.ok },
7742
+ idle: { dot: "\u25D0", label: "IDLE", color: theme.warn },
7743
+ ended: { dot: "\u25CB", label: "ENDED", color: theme.dim }
7744
+ };
7693
7745
  }
7694
7746
  });
7695
7747
 
@@ -9631,7 +9683,7 @@ var init_global_install = __esm({
9631
9683
 
9632
9684
  // src/login.ts
9633
9685
  var login_exports = {};
9634
- import { spawn } from "child_process";
9686
+ import { spawn as spawn2 } from "child_process";
9635
9687
  function startSpinner(text) {
9636
9688
  if (!process.stderr.isTTY) {
9637
9689
  process.stderr.write(` ${text}
@@ -9667,7 +9719,7 @@ function openBrowser(url) {
9667
9719
  try {
9668
9720
  const cmd = process.platform === "win32" ? "cmd" : process.platform === "darwin" ? "open" : "xdg-open";
9669
9721
  const args = process.platform === "win32" ? ["/c", "start", '""', url] : [url];
9670
- const child = spawn(cmd, args, { stdio: "ignore", detached: true });
9722
+ const child = spawn2(cmd, args, { stdio: "ignore", detached: true });
9671
9723
  child.on("error", () => {
9672
9724
  });
9673
9725
  child.unref();
@@ -9786,7 +9838,7 @@ __export(shield_exports, {
9786
9838
  });
9787
9839
  import { createServer, request as httpRequest } from "http";
9788
9840
  import { request as httpsRequest } from "https";
9789
- import { spawn as spawn2 } from "child_process";
9841
+ import { spawn as spawn3 } from "child_process";
9790
9842
  import { URL as URL2 } from "url";
9791
9843
  import { readFileSync as readFileSync7, existsSync as existsSync5, readdirSync, statSync as statSync2 } from "fs";
9792
9844
  import { resolve as resolve5 } from "path";
@@ -10034,7 +10086,7 @@ async function runShield() {
10034
10086
  const upstream = pickUpstream();
10035
10087
  const { port, close } = await startProxy(upstream);
10036
10088
  log4(`redacting secrets on the LLM path \u2192 masking before ${upstream.host} (127.0.0.1:${port})`);
10037
- const child = spawn2(cmd[0], cmd.slice(1), {
10089
+ const child = spawn3(cmd[0], cmd.slice(1), {
10038
10090
  stdio: "inherit",
10039
10091
  env: { ...process.env, ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}` },
10040
10092
  shell: process.platform === "win32"
package/dist/tui/index.js CHANGED
@@ -127,6 +127,7 @@ function KeyHints({ hints }) {
127
127
  // src/tui/panels/Live.tsx
128
128
  import { Box as Box2, Text as Text2, useInput } from "ink";
129
129
  import TextInput from "ink-text-input";
130
+ import { spawn } from "child_process";
130
131
  import { closeSync, openSync, readSync, statSync } from "fs";
131
132
  import { homedir as homedir2 } from "os";
132
133
  import { join as join2 } from "path";
@@ -222,26 +223,26 @@ async function request(method, path, opts = {}) {
222
223
  }
223
224
  const attempt = () => fetch(url, {
224
225
  method,
225
- headers,
226
+ headers: { ...headers, Connection: "close" },
226
227
  body: bodyInit,
227
228
  signal: AbortSignal.timeout(opts.timeoutMs ?? 15e3)
228
229
  });
230
+ const maxTries = method === "GET" ? 3 : 1;
229
231
  let res;
230
- try {
231
- res = await attempt();
232
- } catch (firstErr) {
233
- if (method !== "GET") {
234
- const msg = firstErr instanceof Error ? firstErr.message : String(firstErr);
235
- throw new ApiError(0, "NETWORK_ERROR", `Cannot reach SolonGate API: ${msg}`);
236
- }
237
- await new Promise((r) => setTimeout(r, 800));
232
+ let lastErr;
233
+ for (let i = 0; i < maxTries; i++) {
238
234
  try {
239
235
  res = await attempt();
236
+ break;
240
237
  } catch (err) {
241
- const msg = err instanceof Error ? err.message : String(err);
242
- throw new ApiError(0, "NETWORK_ERROR", `Cannot reach SolonGate API: ${msg}`);
238
+ lastErr = err;
239
+ if (i < maxTries - 1) await new Promise((r) => setTimeout(r, 600 * (i + 1)));
243
240
  }
244
241
  }
242
+ if (!res) {
243
+ const msg = lastErr instanceof Error ? lastErr.message : String(lastErr);
244
+ throw new ApiError(0, "NETWORK_ERROR", `Cannot reach SolonGate API: ${msg}`);
245
+ }
245
246
  const text = await res.text().catch(() => "");
246
247
  let json = void 0;
247
248
  if (text) {
@@ -559,6 +560,12 @@ function StreamLine({ e, loc }) {
559
560
  ] });
560
561
  }
561
562
  var FILTERS = ["all", "local", "cloud"];
563
+ var sessStatus = (lastAt, now) => now - lastAt < 6e4 ? "active" : now - lastAt < 3e5 ? "idle" : "ended";
564
+ var STATUS_STYLE = {
565
+ active: { dot: "\u25CF", label: "ACTIVE", color: theme.ok },
566
+ idle: { dot: "\u25D0", label: "IDLE", color: theme.warn },
567
+ ended: { dot: "\u25CB", label: "ENDED", color: theme.dim }
568
+ };
562
569
  function LivePanel({ active: active2 }) {
563
570
  const [s, setS] = useState2(null);
564
571
  const [lat, setLat] = useState2([]);
@@ -579,18 +586,26 @@ function LivePanel({ active: active2 }) {
579
586
  const seenRef = useRef2(/* @__PURE__ */ new Set());
580
587
  const lastLocalTs = useRef2(0);
581
588
  const pausedUntil = useRef2(0);
589
+ const sessRef = useRef2([]);
590
+ const prevStatus = useRef2(/* @__PURE__ */ new Map());
591
+ const [alerts, setAlerts] = useState2([]);
582
592
  const [frozen, setFrozen] = useState2(false);
583
593
  const frozenRef = useRef2(false);
584
594
  frozenRef.current = frozen;
585
595
  const pushLog = useCallback2((msg, level = "ok") => {
586
596
  setLog((prev) => [...prev.slice(-59), { ts: Date.now(), msg, level }]);
587
597
  }, []);
598
+ const netFails = useRef2(0);
588
599
  const onApiError = useCallback2(
589
600
  (e) => {
590
601
  const is429 = e instanceof ApiError && e.status === 429;
602
+ const isNet = e instanceof ApiError && e.status === 0;
591
603
  if (is429 && Date.now() >= pausedUntil.current) {
592
604
  pausedUntil.current = Date.now() + 3e4;
593
605
  pushLog("rate limited by api \xB7 backing off 30s", "warn");
606
+ } else if (isNet) {
607
+ netFails.current += 1;
608
+ if (netFails.current === 3) pushLog("network unreachable \xB7 retrying\u2026", "warn");
594
609
  } else if (!is429) {
595
610
  pushLog(`api error \xB7 ${truncate(e instanceof Error ? e.message : String(e), 40)}`, "bad");
596
611
  }
@@ -666,6 +681,8 @@ function LivePanel({ active: active2 }) {
666
681
  try {
667
682
  const fd = await api.audit.list({ limit: 50 });
668
683
  if (frozenRef.current) return;
684
+ if (netFails.current >= 3) pushLog("network recovered", "ok");
685
+ netFails.current = 0;
669
686
  const ms = Date.now() - t0;
670
687
  setLat((l) => [...l, ms].slice(-240));
671
688
  const fresh = fd.entries.filter((e) => !seenRef.current.has(e.id));
@@ -732,6 +749,30 @@ function LivePanel({ active: active2 }) {
732
749
  return () => clearInterval(t);
733
750
  }, [active2, frozen]);
734
751
  const startRef = useRef2(Date.now());
752
+ useEffect2(() => {
753
+ if (!active2) return;
754
+ for (const r of sessRef.current) {
755
+ const prev = prevStatus.current.get(r.id);
756
+ if (prev && prev === "active" && r.status === "idle") {
757
+ const name = r.isMe ? `${r.agent} (this machine)` : r.agent;
758
+ const msg = `${name} went IDLE \u2014 no tool call for 60s`;
759
+ pushLog(`\u23F8 ${msg}`, "warn");
760
+ setAlerts((a) => [...a.filter((x) => x.id !== r.id), { id: r.id, msg, until: Date.now() + 15e3 }].slice(-4));
761
+ try {
762
+ process.stdout.write("\x07");
763
+ } catch {
764
+ }
765
+ try {
766
+ const child = spawn("notify-send", ["-a", "SolonGate", "Agent idle", msg], { stdio: "ignore", detached: true });
767
+ child.on("error", () => {
768
+ });
769
+ child.unref();
770
+ } catch {
771
+ }
772
+ }
773
+ prevStatus.current.set(r.id, r.status);
774
+ }
775
+ }, [tick, active2, pushLog]);
735
776
  const cols = process.stdout.columns ?? 100;
736
777
  const rows = process.stdout.rows ?? 30;
737
778
  const ins = insights.data ?? {};
@@ -784,7 +825,7 @@ function LivePanel({ active: active2 }) {
784
825
  localSess.set(e.session, cur);
785
826
  }
786
827
  const combinedSess = [
787
- ...[...localSess.entries()].sort((a, b) => b[1].lastAt - a[1].lastAt).map(([id, v]) => ({ source: "local", id, agent: v.agent, calls: v.calls, denies: v.denies, lastAt: v.lastAt, isMe: ring?.session === id })),
828
+ ...[...localSess.entries()].sort((a, b) => b[1].lastAt - a[1].lastAt).map(([id, v]) => ({ source: "local", id, agent: v.agent, calls: v.calls, denies: v.denies, lastAt: v.lastAt, status: sessStatus(v.lastAt, nowMs), isMe: ring?.session === id })),
788
829
  ...sess.filter((a) => !localSess.has(a.session_id)).map((a) => ({
789
830
  source: "cloud",
790
831
  id: a.session_id,
@@ -792,10 +833,12 @@ function LivePanel({ active: active2 }) {
792
833
  calls: a.total_calls,
793
834
  denies: a.denied_calls,
794
835
  lastAt: Date.parse(a.last_seen_at),
836
+ status: sessStatus(Date.parse(a.last_seen_at), nowMs),
795
837
  trust: a.trust_score,
796
838
  isMe: false
797
839
  }))
798
840
  ];
841
+ sessRef.current = combinedSess;
799
842
  const localSessIds = new Set(localSess.keys());
800
843
  if (ring?.session) localSessIds.add(ring.session);
801
844
  const isLoc = (e) => e.source === "local" || !!e.session && localSessIds.has(e.session);
@@ -879,6 +922,10 @@ function LivePanel({ active: active2 }) {
879
922
  {
880
923
  value: search,
881
924
  onChange: (v) => {
925
+ if (v.endsWith("/")) {
926
+ setEditingSearch(false);
927
+ return;
928
+ }
882
929
  setSearch(v);
883
930
  setScroll(0);
884
931
  setDetailScroll(0);
@@ -886,7 +933,7 @@ function LivePanel({ active: active2 }) {
886
933
  onSubmit: () => setEditingSearch(false)
887
934
  }
888
935
  ),
889
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " (enter done \xB7 empty = clear)" })
936
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " (enter/\u200B/ done \xB7 empty = clear)" })
890
937
  ] }) : search ? /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
891
938
  /* @__PURE__ */ jsx2(Text2, { color: theme.warn, children: "search: " }),
892
939
  /* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, bold: true, children: search }),
@@ -930,7 +977,11 @@ function LivePanel({ active: active2 }) {
930
977
  titleBar,
931
978
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
932
979
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#AAC8FA", bold: true, children: ` SESSION ${detail.id.slice(0, 8)} ` }),
933
- /* @__PURE__ */ jsx2(Text2, { color: detail.source === "local" ? theme.ok : "#4f6db8", children: " " + detail.source.toUpperCase() }),
980
+ (() => {
981
+ const st = STATUS_STYLE[sessStatus(detail.lastAt, nowMs)];
982
+ return /* @__PURE__ */ jsx2(Text2, { color: st.color, bold: true, children: ` ${st.dot} ${st.label}` });
983
+ })(),
984
+ /* @__PURE__ */ jsx2(Text2, { color: detail.source === "local" ? theme.ok : "#4f6db8", children: " " + detail.source.toUpperCase() }),
934
985
  /* @__PURE__ */ jsx2(Text2, { bold: true, children: " " + truncate(detail.isMe ? detail.agent + " (this machine)" : detail.agent, 34) }),
935
986
  detail.trust != null ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ` trust ${detail.trust}/100` }) : null,
936
987
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2190 back" })
@@ -960,8 +1011,8 @@ function LivePanel({ active: active2 }) {
960
1011
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502" })
961
1012
  ] }),
962
1013
  /* @__PURE__ */ jsx2(PaneTitle, { label: "TIMELINE", extra: `${d.length} calls \xB7 newest first${dScroll ? ` \xB7 \u25BC${dScroll} older` : ""} \xB7 \u2191\u2193 scroll \xB7 / search \xB7 \u2190 back`, width: innerW }),
963
- searchRow,
964
1014
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height: tlRows, overflow: "hidden", children: [
1015
+ searchRow,
965
1016
  tl.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: detailCloud.length === 0 ? spin + " loading history\u2026" : "no entries" }) : null,
966
1017
  tl.map((e) => /* @__PURE__ */ jsx2(StreamLine, { e, loc: isLoc(e) }, e.id))
967
1018
  ] }),
@@ -995,6 +1046,7 @@ function LivePanel({ active: active2 }) {
995
1046
  /* @__PURE__ */ jsx2(Text2, { color: guard.data?.up_to_date ? theme.ok : theme.warn, children: guard.data ? `v${guard.data.installed ?? "?"}${guard.data.up_to_date ? "" : "\u2192v" + guard.data.latest} ${guard.data.device_count}dev` : "\xB7" }),
996
1047
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502" })
997
1048
  ] }),
1049
+ alerts.filter((a) => nowMs < a.until).slice(-1).map((a) => /* @__PURE__ */ jsx2(Text2, { wrap: "truncate", backgroundColor: "#3d2a12", color: "#ffb454", bold: true, children: ` \u23F8 IDLE \xB7 ${truncate(a.msg, innerW - 12)} ` }, a.id)),
998
1050
  /* @__PURE__ */ jsxs2(Box2, { height: 1 + chartH, children: [
999
1051
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: leftW, marginRight: 2, children: [
1000
1052
  /* @__PURE__ */ jsx2(PaneTitle, { label: "TRAFFIC", extra: `${trafficLabel} \xB7 peak ${Math.max(0, ...traffic)} \xB7 red = denials`, width: leftW }),
@@ -1032,14 +1084,14 @@ function LivePanel({ active: active2 }) {
1032
1084
  /* @__PURE__ */ jsx2(PaneTitle, { label: "SESSIONS", extra: mode === "pick" ? "\u2191\u2193 pick \xB7 enter open" : `${combinedSess.length} \xB7 s = inspect`, width: colW }),
1033
1085
  pickable.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "scanning\u2026" }) : null,
1034
1086
  pickable.map((r, i) => {
1035
- const freshDot = nowMs - r.lastAt < 9e4;
1036
1087
  const sel = mode === "pick" && i === pickIdx;
1088
+ const st = STATUS_STYLE[r.status];
1037
1089
  return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", backgroundColor: sel ? "#1c2f63" : void 0, bold: sel || r.isMe, children: [
1038
- /* @__PURE__ */ jsxs2(Text2, { color: sel ? theme.accentBright : freshDot ? theme.ok : theme.dim, children: [
1039
- sel ? "\u25B8" : freshDot ? "\u25CF" : "\u25CB",
1090
+ /* @__PURE__ */ jsxs2(Text2, { color: sel ? theme.accentBright : st.color, children: [
1091
+ sel ? "\u25B8" : st.dot,
1040
1092
  " "
1041
1093
  ] }),
1042
- /* @__PURE__ */ jsx2(Text2, { color: r.source === "local" ? theme.ok : "#4f6db8", children: r.source === "local" ? "LOC " : "CLD " }),
1094
+ /* @__PURE__ */ jsx2(Text2, { color: st.color, children: st.label.padEnd(7) }),
1043
1095
  /* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, children: truncate(r.isMe ? "this machine" : r.agent, 12).padEnd(13) }),
1044
1096
  /* @__PURE__ */ jsxs2(Text2, { children: [
1045
1097
  String(r.calls).padStart(4),
@@ -1073,7 +1125,7 @@ function LivePanel({ active: active2 }) {
1073
1125
  width: innerW
1074
1126
  }
1075
1127
  ),
1076
- /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height: streamRows, overflow: "hidden", children: [
1128
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height: searchRow ? streamRows - 1 : streamRows, overflow: "hidden", children: [
1077
1129
  searchRow,
1078
1130
  localOn === false ? /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
1079
1131
  /* @__PURE__ */ jsx2(Text2, { color: theme.warn, children: "local logs off" }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.75.0",
3
+ "version": "0.76.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": {