@solongate/proxy 0.75.1 → 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);
@@ -7507,10 +7544,13 @@ function LivePanel({ active: active2 }) {
7507
7544
  const tl = d.slice(dScroll, dScroll + tlRows);
7508
7545
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
7509
7546
  titleBar,
7510
- searchRow,
7511
7547
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7512
7548
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#AAC8FA", bold: true, children: ` SESSION ${detail.id.slice(0, 8)} ` }),
7513
- /* @__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() }),
7514
7554
  /* @__PURE__ */ jsx2(Text2, { bold: true, children: " " + truncate2(detail.isMe ? detail.agent + " (this machine)" : detail.agent, 34) }),
7515
7555
  detail.trust != null ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ` trust ${detail.trust}/100` }) : null,
7516
7556
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2190 back" })
@@ -7541,6 +7581,7 @@ function LivePanel({ active: active2 }) {
7541
7581
  ] }),
7542
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 }),
7543
7583
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height: tlRows, overflow: "hidden", children: [
7584
+ searchRow,
7544
7585
  tl.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: detailCloud.length === 0 ? spin + " loading history\u2026" : "no entries" }) : null,
7545
7586
  tl.map((e) => /* @__PURE__ */ jsx2(StreamLine, { e, loc: isLoc(e) }, e.id))
7546
7587
  ] }),
@@ -7574,7 +7615,7 @@ function LivePanel({ active: active2 }) {
7574
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" }),
7575
7616
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502" })
7576
7617
  ] }),
7577
- searchRow,
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)),
7578
7619
  /* @__PURE__ */ jsxs2(Box2, { height: 1 + chartH, children: [
7579
7620
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: leftW, marginRight: 2, children: [
7580
7621
  /* @__PURE__ */ jsx2(PaneTitle, { label: "TRAFFIC", extra: `${trafficLabel} \xB7 peak ${Math.max(0, ...traffic)} \xB7 red = denials`, width: leftW }),
@@ -7612,14 +7653,14 @@ function LivePanel({ active: active2 }) {
7612
7653
  /* @__PURE__ */ jsx2(PaneTitle, { label: "SESSIONS", extra: mode === "pick" ? "\u2191\u2193 pick \xB7 enter open" : `${combinedSess.length} \xB7 s = inspect`, width: colW }),
7613
7654
  pickable.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "scanning\u2026" }) : null,
7614
7655
  pickable.map((r, i) => {
7615
- const freshDot = nowMs - r.lastAt < 9e4;
7616
7656
  const sel = mode === "pick" && i === pickIdx;
7657
+ const st = STATUS_STYLE[r.status];
7617
7658
  return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", backgroundColor: sel ? "#1c2f63" : void 0, bold: sel || r.isMe, children: [
7618
- /* @__PURE__ */ jsxs2(Text2, { color: sel ? theme.accentBright : freshDot ? theme.ok : theme.dim, children: [
7619
- sel ? "\u25B8" : freshDot ? "\u25CF" : "\u25CB",
7659
+ /* @__PURE__ */ jsxs2(Text2, { color: sel ? theme.accentBright : st.color, children: [
7660
+ sel ? "\u25B8" : st.dot,
7620
7661
  " "
7621
7662
  ] }),
7622
- /* @__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) }),
7623
7664
  /* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, children: truncate2(r.isMe ? "this machine" : r.agent, 12).padEnd(13) }),
7624
7665
  /* @__PURE__ */ jsxs2(Text2, { children: [
7625
7666
  String(r.calls).padStart(4),
@@ -7654,6 +7695,7 @@ function LivePanel({ active: active2 }) {
7654
7695
  }
7655
7696
  ),
7656
7697
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height: searchRow ? streamRows - 1 : streamRows, overflow: "hidden", children: [
7698
+ searchRow,
7657
7699
  localOn === false ? /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7658
7700
  /* @__PURE__ */ jsx2(Text2, { color: theme.warn, children: "local logs off" }),
7659
7701
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2014 enable: dashboard \u2192 Settings \u2192 Local logs \xB7 hooks write ~/.solongate/local-logs \xB7 view: `solongate logs-server`" })
@@ -7671,7 +7713,7 @@ function LivePanel({ active: active2 }) {
7671
7713
  ] })
7672
7714
  ] });
7673
7715
  }
7674
- 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;
7675
7717
  var init_Live = __esm({
7676
7718
  "src/tui/panels/Live.tsx"() {
7677
7719
  "use strict";
@@ -7694,6 +7736,12 @@ var init_Live = __esm({
7694
7736
  return `${p(Math.floor(s / 3600))}:${p(Math.floor(s % 3600 / 60))}:${p(s % 60)}`;
7695
7737
  };
7696
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
+ };
7697
7745
  }
7698
7746
  });
7699
7747
 
@@ -9635,7 +9683,7 @@ var init_global_install = __esm({
9635
9683
 
9636
9684
  // src/login.ts
9637
9685
  var login_exports = {};
9638
- import { spawn } from "child_process";
9686
+ import { spawn as spawn2 } from "child_process";
9639
9687
  function startSpinner(text) {
9640
9688
  if (!process.stderr.isTTY) {
9641
9689
  process.stderr.write(` ${text}
@@ -9671,7 +9719,7 @@ function openBrowser(url) {
9671
9719
  try {
9672
9720
  const cmd = process.platform === "win32" ? "cmd" : process.platform === "darwin" ? "open" : "xdg-open";
9673
9721
  const args = process.platform === "win32" ? ["/c", "start", '""', url] : [url];
9674
- const child = spawn(cmd, args, { stdio: "ignore", detached: true });
9722
+ const child = spawn2(cmd, args, { stdio: "ignore", detached: true });
9675
9723
  child.on("error", () => {
9676
9724
  });
9677
9725
  child.unref();
@@ -9790,7 +9838,7 @@ __export(shield_exports, {
9790
9838
  });
9791
9839
  import { createServer, request as httpRequest } from "http";
9792
9840
  import { request as httpsRequest } from "https";
9793
- import { spawn as spawn2 } from "child_process";
9841
+ import { spawn as spawn3 } from "child_process";
9794
9842
  import { URL as URL2 } from "url";
9795
9843
  import { readFileSync as readFileSync7, existsSync as existsSync5, readdirSync, statSync as statSync2 } from "fs";
9796
9844
  import { resolve as resolve5 } from "path";
@@ -10038,7 +10086,7 @@ async function runShield() {
10038
10086
  const upstream = pickUpstream();
10039
10087
  const { port, close } = await startProxy(upstream);
10040
10088
  log4(`redacting secrets on the LLM path \u2192 masking before ${upstream.host} (127.0.0.1:${port})`);
10041
- const child = spawn2(cmd[0], cmd.slice(1), {
10089
+ const child = spawn3(cmd[0], cmd.slice(1), {
10042
10090
  stdio: "inherit",
10043
10091
  env: { ...process.env, ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}` },
10044
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);
@@ -932,10 +975,13 @@ function LivePanel({ active: active2 }) {
932
975
  const tl = d.slice(dScroll, dScroll + tlRows);
933
976
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
934
977
  titleBar,
935
- searchRow,
936
978
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
937
979
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#AAC8FA", bold: true, children: ` SESSION ${detail.id.slice(0, 8)} ` }),
938
- /* @__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() }),
939
985
  /* @__PURE__ */ jsx2(Text2, { bold: true, children: " " + truncate(detail.isMe ? detail.agent + " (this machine)" : detail.agent, 34) }),
940
986
  detail.trust != null ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ` trust ${detail.trust}/100` }) : null,
941
987
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2190 back" })
@@ -966,6 +1012,7 @@ function LivePanel({ active: active2 }) {
966
1012
  ] }),
967
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 }),
968
1014
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height: tlRows, overflow: "hidden", children: [
1015
+ searchRow,
969
1016
  tl.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: detailCloud.length === 0 ? spin + " loading history\u2026" : "no entries" }) : null,
970
1017
  tl.map((e) => /* @__PURE__ */ jsx2(StreamLine, { e, loc: isLoc(e) }, e.id))
971
1018
  ] }),
@@ -999,7 +1046,7 @@ function LivePanel({ active: active2 }) {
999
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" }),
1000
1047
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502" })
1001
1048
  ] }),
1002
- searchRow,
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)),
1003
1050
  /* @__PURE__ */ jsxs2(Box2, { height: 1 + chartH, children: [
1004
1051
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: leftW, marginRight: 2, children: [
1005
1052
  /* @__PURE__ */ jsx2(PaneTitle, { label: "TRAFFIC", extra: `${trafficLabel} \xB7 peak ${Math.max(0, ...traffic)} \xB7 red = denials`, width: leftW }),
@@ -1037,14 +1084,14 @@ function LivePanel({ active: active2 }) {
1037
1084
  /* @__PURE__ */ jsx2(PaneTitle, { label: "SESSIONS", extra: mode === "pick" ? "\u2191\u2193 pick \xB7 enter open" : `${combinedSess.length} \xB7 s = inspect`, width: colW }),
1038
1085
  pickable.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "scanning\u2026" }) : null,
1039
1086
  pickable.map((r, i) => {
1040
- const freshDot = nowMs - r.lastAt < 9e4;
1041
1087
  const sel = mode === "pick" && i === pickIdx;
1088
+ const st = STATUS_STYLE[r.status];
1042
1089
  return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", backgroundColor: sel ? "#1c2f63" : void 0, bold: sel || r.isMe, children: [
1043
- /* @__PURE__ */ jsxs2(Text2, { color: sel ? theme.accentBright : freshDot ? theme.ok : theme.dim, children: [
1044
- sel ? "\u25B8" : freshDot ? "\u25CF" : "\u25CB",
1090
+ /* @__PURE__ */ jsxs2(Text2, { color: sel ? theme.accentBright : st.color, children: [
1091
+ sel ? "\u25B8" : st.dot,
1045
1092
  " "
1046
1093
  ] }),
1047
- /* @__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) }),
1048
1095
  /* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, children: truncate(r.isMe ? "this machine" : r.agent, 12).padEnd(13) }),
1049
1096
  /* @__PURE__ */ jsxs2(Text2, { children: [
1050
1097
  String(r.calls).padStart(4),
@@ -1079,6 +1126,7 @@ function LivePanel({ active: active2 }) {
1079
1126
  }
1080
1127
  ),
1081
1128
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height: searchRow ? streamRows - 1 : streamRows, overflow: "hidden", children: [
1129
+ searchRow,
1082
1130
  localOn === false ? /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
1083
1131
  /* @__PURE__ */ jsx2(Text2, { color: theme.warn, children: "local logs off" }),
1084
1132
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2014 enable: dashboard \u2192 Settings \u2192 Local logs \xB7 hooks write ~/.solongate/local-logs \xB7 view: `solongate logs-server`" })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.75.1",
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": {