@solongate/proxy 0.75.1 → 0.76.1

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,11 +7031,12 @@ 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";
7037
7038
  import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
7038
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
7039
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
7039
7040
  function tailLines(file, maxBytes = 131072) {
7040
7041
  try {
7041
7042
  const size = statSync(file).size;
@@ -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);
@@ -7386,9 +7423,10 @@ function LivePanel({ active: active2 }) {
7386
7423
  const rightW = innerW - leftW - 2;
7387
7424
  const colW = Math.max(20, Math.floor((innerW - 4) / 3));
7388
7425
  const pickable = combinedSess.slice(0, colH - 1);
7389
- const maxScroll = Math.max(0, visibleDesc.length - streamRows);
7426
+ const streamBodyRows = Math.max(3, streamRows - 1);
7427
+ const maxScroll = Math.max(0, visibleDesc.length - streamBodyRows);
7390
7428
  const clampedScroll = Math.min(scroll, maxScroll);
7391
- const windowed = visibleDesc.slice(clampedScroll, clampedScroll + streamRows);
7429
+ const windowed = visibleDesc.slice(clampedScroll, clampedScroll + streamBodyRows);
7392
7430
  const detailEntries = detail ? (() => {
7393
7431
  const seen = /* @__PURE__ */ new Set();
7394
7432
  const all = [];
@@ -7447,30 +7485,31 @@ function LivePanel({ active: active2 }) {
7447
7485
  },
7448
7486
  { isActive: active2 && !editingSearch }
7449
7487
  );
7450
- const searchRow = editingSearch ? /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7451
- /* @__PURE__ */ jsx2(Text2, { color: theme.warn, children: "search: " }),
7452
- /* @__PURE__ */ jsx2(
7453
- TextInput,
7454
- {
7455
- value: search,
7456
- onChange: (v) => {
7457
- if (v.endsWith("/")) {
7458
- setEditingSearch(false);
7459
- return;
7460
- }
7461
- setSearch(v);
7462
- setScroll(0);
7463
- setDetailScroll(0);
7464
- },
7465
- onSubmit: () => setEditingSearch(false)
7466
- }
7467
- ),
7468
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " (enter/\u200B/ done \xB7 empty = clear)" })
7469
- ] }) : search ? /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7470
- /* @__PURE__ */ jsx2(Text2, { color: theme.warn, children: "search: " }),
7471
- /* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, bold: true, children: search }),
7472
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ` ${filtered.length} match \xB7 / edit` })
7473
- ] }) : null;
7488
+ const searchRow = /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7489
+ /* @__PURE__ */ jsx2(Text2, { color: editingSearch ? theme.warn : theme.dim, bold: editingSearch, children: editingSearch ? "\u2315 search: " : "\u2315 search: " }),
7490
+ editingSearch ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
7491
+ /* @__PURE__ */ jsx2(
7492
+ TextInput,
7493
+ {
7494
+ value: search,
7495
+ onChange: (v) => {
7496
+ if (v.endsWith("/")) {
7497
+ setEditingSearch(false);
7498
+ return;
7499
+ }
7500
+ setSearch(v);
7501
+ setScroll(0);
7502
+ setDetailScroll(0);
7503
+ },
7504
+ onSubmit: () => setEditingSearch(false)
7505
+ }
7506
+ ),
7507
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ` ${filtered.length} match \xB7 enter/\u200B/ done \xB7 empty clears` })
7508
+ ] }) : search ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
7509
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, bold: true, children: search }),
7510
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ` ${filtered.length} match \xB7 press / to edit \xB7 type to clear` })
7511
+ ] }) : /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "press / to filter the stream by tool, agent, command\u2026" })
7512
+ ] });
7474
7513
  const sessionDot = (st) => st === "active" ? { ch: "\u25CF", color: theme.ok } : st === "idle" ? { ch: "\u25CC", color: theme.warn } : { ch: "\u25CB", color: theme.dim };
7475
7514
  void sessionDot;
7476
7515
  if (s === null && localOn === null) {
@@ -7502,15 +7541,19 @@ function LivePanel({ active: active2 }) {
7502
7541
  const first = d[d.length - 1]?.at;
7503
7542
  const last = d[0]?.at;
7504
7543
  const tlRows = Math.max(4, rows - 10);
7505
- const maxD = Math.max(0, d.length - tlRows);
7544
+ const tlBody = Math.max(3, tlRows - 1);
7545
+ const maxD = Math.max(0, d.length - tlBody);
7506
7546
  const dScroll = Math.min(detailScroll, maxD);
7507
- const tl = d.slice(dScroll, dScroll + tlRows);
7547
+ const tl = d.slice(dScroll, dScroll + tlBody);
7508
7548
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
7509
7549
  titleBar,
7510
- searchRow,
7511
7550
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7512
7551
  /* @__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() }),
7552
+ (() => {
7553
+ const st = STATUS_STYLE[sessStatus(detail.lastAt, nowMs)];
7554
+ return /* @__PURE__ */ jsx2(Text2, { color: st.color, bold: true, children: ` ${st.dot} ${st.label}` });
7555
+ })(),
7556
+ /* @__PURE__ */ jsx2(Text2, { color: detail.source === "local" ? theme.ok : "#4f6db8", children: " " + detail.source.toUpperCase() }),
7514
7557
  /* @__PURE__ */ jsx2(Text2, { bold: true, children: " " + truncate2(detail.isMe ? detail.agent + " (this machine)" : detail.agent, 34) }),
7515
7558
  detail.trust != null ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ` trust ${detail.trust}/100` }) : null,
7516
7559
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2190 back" })
@@ -7541,6 +7584,7 @@ function LivePanel({ active: active2 }) {
7541
7584
  ] }),
7542
7585
  /* @__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
7586
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height: tlRows, overflow: "hidden", children: [
7587
+ searchRow,
7544
7588
  tl.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: detailCloud.length === 0 ? spin + " loading history\u2026" : "no entries" }) : null,
7545
7589
  tl.map((e) => /* @__PURE__ */ jsx2(StreamLine, { e, loc: isLoc(e) }, e.id))
7546
7590
  ] }),
@@ -7574,7 +7618,7 @@ function LivePanel({ active: active2 }) {
7574
7618
  /* @__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
7619
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502" })
7576
7620
  ] }),
7577
- searchRow,
7621
+ 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
7622
  /* @__PURE__ */ jsxs2(Box2, { height: 1 + chartH, children: [
7579
7623
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: leftW, marginRight: 2, children: [
7580
7624
  /* @__PURE__ */ jsx2(PaneTitle, { label: "TRAFFIC", extra: `${trafficLabel} \xB7 peak ${Math.max(0, ...traffic)} \xB7 red = denials`, width: leftW }),
@@ -7612,14 +7656,14 @@ function LivePanel({ active: active2 }) {
7612
7656
  /* @__PURE__ */ jsx2(PaneTitle, { label: "SESSIONS", extra: mode === "pick" ? "\u2191\u2193 pick \xB7 enter open" : `${combinedSess.length} \xB7 s = inspect`, width: colW }),
7613
7657
  pickable.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "scanning\u2026" }) : null,
7614
7658
  pickable.map((r, i) => {
7615
- const freshDot = nowMs - r.lastAt < 9e4;
7616
7659
  const sel = mode === "pick" && i === pickIdx;
7660
+ const st = STATUS_STYLE[r.status];
7617
7661
  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",
7662
+ /* @__PURE__ */ jsxs2(Text2, { color: sel ? theme.accentBright : st.color, children: [
7663
+ sel ? "\u25B8" : st.dot,
7620
7664
  " "
7621
7665
  ] }),
7622
- /* @__PURE__ */ jsx2(Text2, { color: r.source === "local" ? theme.ok : "#4f6db8", children: r.source === "local" ? "LOC " : "CLD " }),
7666
+ /* @__PURE__ */ jsx2(Text2, { color: st.color, children: st.label.padEnd(7) }),
7623
7667
  /* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, children: truncate2(r.isMe ? "this machine" : r.agent, 12).padEnd(13) }),
7624
7668
  /* @__PURE__ */ jsxs2(Text2, { children: [
7625
7669
  String(r.calls).padStart(4),
@@ -7653,7 +7697,8 @@ function LivePanel({ active: active2 }) {
7653
7697
  width: innerW
7654
7698
  }
7655
7699
  ),
7656
- /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height: searchRow ? streamRows - 1 : streamRows, overflow: "hidden", children: [
7700
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height: streamRows, overflow: "hidden", children: [
7701
+ searchRow,
7657
7702
  localOn === false ? /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7658
7703
  /* @__PURE__ */ jsx2(Text2, { color: theme.warn, children: "local logs off" }),
7659
7704
  /* @__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 +7716,7 @@ function LivePanel({ active: active2 }) {
7671
7716
  ] })
7672
7717
  ] });
7673
7718
  }
7674
- var SPIN, BG, DIM_FLOOR, LOCAL_LOG, RING, STREAM_KEEP, hhmmss, fmtUp, FILTERS;
7719
+ var SPIN, BG, DIM_FLOOR, LOCAL_LOG, RING, STREAM_KEEP, hhmmss, fmtUp, FILTERS, sessStatus, STATUS_STYLE;
7675
7720
  var init_Live = __esm({
7676
7721
  "src/tui/panels/Live.tsx"() {
7677
7722
  "use strict";
@@ -7694,6 +7739,12 @@ var init_Live = __esm({
7694
7739
  return `${p(Math.floor(s / 3600))}:${p(Math.floor(s % 3600 / 60))}:${p(s % 60)}`;
7695
7740
  };
7696
7741
  FILTERS = ["all", "local", "cloud"];
7742
+ sessStatus = (lastAt, now) => now - lastAt < 6e4 ? "active" : now - lastAt < 3e5 ? "idle" : "ended";
7743
+ STATUS_STYLE = {
7744
+ active: { dot: "\u25CF", label: "ACTIVE", color: theme.ok },
7745
+ idle: { dot: "\u25D0", label: "IDLE", color: theme.warn },
7746
+ ended: { dot: "\u25CB", label: "ENDED", color: theme.dim }
7747
+ };
7697
7748
  }
7698
7749
  });
7699
7750
 
@@ -9635,7 +9686,7 @@ var init_global_install = __esm({
9635
9686
 
9636
9687
  // src/login.ts
9637
9688
  var login_exports = {};
9638
- import { spawn } from "child_process";
9689
+ import { spawn as spawn2 } from "child_process";
9639
9690
  function startSpinner(text) {
9640
9691
  if (!process.stderr.isTTY) {
9641
9692
  process.stderr.write(` ${text}
@@ -9671,7 +9722,7 @@ function openBrowser(url) {
9671
9722
  try {
9672
9723
  const cmd = process.platform === "win32" ? "cmd" : process.platform === "darwin" ? "open" : "xdg-open";
9673
9724
  const args = process.platform === "win32" ? ["/c", "start", '""', url] : [url];
9674
- const child = spawn(cmd, args, { stdio: "ignore", detached: true });
9725
+ const child = spawn2(cmd, args, { stdio: "ignore", detached: true });
9675
9726
  child.on("error", () => {
9676
9727
  });
9677
9728
  child.unref();
@@ -9790,7 +9841,7 @@ __export(shield_exports, {
9790
9841
  });
9791
9842
  import { createServer, request as httpRequest } from "http";
9792
9843
  import { request as httpsRequest } from "https";
9793
- import { spawn as spawn2 } from "child_process";
9844
+ import { spawn as spawn3 } from "child_process";
9794
9845
  import { URL as URL2 } from "url";
9795
9846
  import { readFileSync as readFileSync7, existsSync as existsSync5, readdirSync, statSync as statSync2 } from "fs";
9796
9847
  import { resolve as resolve5 } from "path";
@@ -10038,7 +10089,7 @@ async function runShield() {
10038
10089
  const upstream = pickUpstream();
10039
10090
  const { port, close } = await startProxy(upstream);
10040
10091
  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), {
10092
+ const child = spawn3(cmd[0], cmd.slice(1), {
10042
10093
  stdio: "inherit",
10043
10094
  env: { ...process.env, ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}` },
10044
10095
  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) {
@@ -444,7 +445,7 @@ function usePoll(reload, intervalMs, enabled = true) {
444
445
  }
445
446
 
446
447
  // src/tui/panels/Live.tsx
447
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
448
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
448
449
  var SPIN = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
449
450
  var BG = "#12234f";
450
451
  var DIM_FLOOR = "#233457";
@@ -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);
@@ -811,9 +854,10 @@ function LivePanel({ active: active2 }) {
811
854
  const rightW = innerW - leftW - 2;
812
855
  const colW = Math.max(20, Math.floor((innerW - 4) / 3));
813
856
  const pickable = combinedSess.slice(0, colH - 1);
814
- const maxScroll = Math.max(0, visibleDesc.length - streamRows);
857
+ const streamBodyRows = Math.max(3, streamRows - 1);
858
+ const maxScroll = Math.max(0, visibleDesc.length - streamBodyRows);
815
859
  const clampedScroll = Math.min(scroll, maxScroll);
816
- const windowed = visibleDesc.slice(clampedScroll, clampedScroll + streamRows);
860
+ const windowed = visibleDesc.slice(clampedScroll, clampedScroll + streamBodyRows);
817
861
  const detailEntries = detail ? (() => {
818
862
  const seen = /* @__PURE__ */ new Set();
819
863
  const all = [];
@@ -872,30 +916,31 @@ function LivePanel({ active: active2 }) {
872
916
  },
873
917
  { isActive: active2 && !editingSearch }
874
918
  );
875
- const searchRow = editingSearch ? /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
876
- /* @__PURE__ */ jsx2(Text2, { color: theme.warn, children: "search: " }),
877
- /* @__PURE__ */ jsx2(
878
- TextInput,
879
- {
880
- value: search,
881
- onChange: (v) => {
882
- if (v.endsWith("/")) {
883
- setEditingSearch(false);
884
- return;
885
- }
886
- setSearch(v);
887
- setScroll(0);
888
- setDetailScroll(0);
889
- },
890
- onSubmit: () => setEditingSearch(false)
891
- }
892
- ),
893
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " (enter/\u200B/ done \xB7 empty = clear)" })
894
- ] }) : search ? /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
895
- /* @__PURE__ */ jsx2(Text2, { color: theme.warn, children: "search: " }),
896
- /* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, bold: true, children: search }),
897
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ` ${filtered.length} match \xB7 / edit` })
898
- ] }) : null;
919
+ const searchRow = /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
920
+ /* @__PURE__ */ jsx2(Text2, { color: editingSearch ? theme.warn : theme.dim, bold: editingSearch, children: editingSearch ? "\u2315 search: " : "\u2315 search: " }),
921
+ editingSearch ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
922
+ /* @__PURE__ */ jsx2(
923
+ TextInput,
924
+ {
925
+ value: search,
926
+ onChange: (v) => {
927
+ if (v.endsWith("/")) {
928
+ setEditingSearch(false);
929
+ return;
930
+ }
931
+ setSearch(v);
932
+ setScroll(0);
933
+ setDetailScroll(0);
934
+ },
935
+ onSubmit: () => setEditingSearch(false)
936
+ }
937
+ ),
938
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ` ${filtered.length} match \xB7 enter/\u200B/ done \xB7 empty clears` })
939
+ ] }) : search ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
940
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, bold: true, children: search }),
941
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ` ${filtered.length} match \xB7 press / to edit \xB7 type to clear` })
942
+ ] }) : /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "press / to filter the stream by tool, agent, command\u2026" })
943
+ ] });
899
944
  const sessionDot = (st) => st === "active" ? { ch: "\u25CF", color: theme.ok } : st === "idle" ? { ch: "\u25CC", color: theme.warn } : { ch: "\u25CB", color: theme.dim };
900
945
  void sessionDot;
901
946
  if (s === null && localOn === null) {
@@ -927,15 +972,19 @@ function LivePanel({ active: active2 }) {
927
972
  const first = d[d.length - 1]?.at;
928
973
  const last = d[0]?.at;
929
974
  const tlRows = Math.max(4, rows - 10);
930
- const maxD = Math.max(0, d.length - tlRows);
975
+ const tlBody = Math.max(3, tlRows - 1);
976
+ const maxD = Math.max(0, d.length - tlBody);
931
977
  const dScroll = Math.min(detailScroll, maxD);
932
- const tl = d.slice(dScroll, dScroll + tlRows);
978
+ const tl = d.slice(dScroll, dScroll + tlBody);
933
979
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
934
980
  titleBar,
935
- searchRow,
936
981
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
937
982
  /* @__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() }),
983
+ (() => {
984
+ const st = STATUS_STYLE[sessStatus(detail.lastAt, nowMs)];
985
+ return /* @__PURE__ */ jsx2(Text2, { color: st.color, bold: true, children: ` ${st.dot} ${st.label}` });
986
+ })(),
987
+ /* @__PURE__ */ jsx2(Text2, { color: detail.source === "local" ? theme.ok : "#4f6db8", children: " " + detail.source.toUpperCase() }),
939
988
  /* @__PURE__ */ jsx2(Text2, { bold: true, children: " " + truncate(detail.isMe ? detail.agent + " (this machine)" : detail.agent, 34) }),
940
989
  detail.trust != null ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ` trust ${detail.trust}/100` }) : null,
941
990
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2190 back" })
@@ -966,6 +1015,7 @@ function LivePanel({ active: active2 }) {
966
1015
  ] }),
967
1016
  /* @__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
1017
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height: tlRows, overflow: "hidden", children: [
1018
+ searchRow,
969
1019
  tl.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: detailCloud.length === 0 ? spin + " loading history\u2026" : "no entries" }) : null,
970
1020
  tl.map((e) => /* @__PURE__ */ jsx2(StreamLine, { e, loc: isLoc(e) }, e.id))
971
1021
  ] }),
@@ -999,7 +1049,7 @@ function LivePanel({ active: active2 }) {
999
1049
  /* @__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
1050
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502" })
1001
1051
  ] }),
1002
- searchRow,
1052
+ 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
1053
  /* @__PURE__ */ jsxs2(Box2, { height: 1 + chartH, children: [
1004
1054
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: leftW, marginRight: 2, children: [
1005
1055
  /* @__PURE__ */ jsx2(PaneTitle, { label: "TRAFFIC", extra: `${trafficLabel} \xB7 peak ${Math.max(0, ...traffic)} \xB7 red = denials`, width: leftW }),
@@ -1037,14 +1087,14 @@ function LivePanel({ active: active2 }) {
1037
1087
  /* @__PURE__ */ jsx2(PaneTitle, { label: "SESSIONS", extra: mode === "pick" ? "\u2191\u2193 pick \xB7 enter open" : `${combinedSess.length} \xB7 s = inspect`, width: colW }),
1038
1088
  pickable.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "scanning\u2026" }) : null,
1039
1089
  pickable.map((r, i) => {
1040
- const freshDot = nowMs - r.lastAt < 9e4;
1041
1090
  const sel = mode === "pick" && i === pickIdx;
1091
+ const st = STATUS_STYLE[r.status];
1042
1092
  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",
1093
+ /* @__PURE__ */ jsxs2(Text2, { color: sel ? theme.accentBright : st.color, children: [
1094
+ sel ? "\u25B8" : st.dot,
1045
1095
  " "
1046
1096
  ] }),
1047
- /* @__PURE__ */ jsx2(Text2, { color: r.source === "local" ? theme.ok : "#4f6db8", children: r.source === "local" ? "LOC " : "CLD " }),
1097
+ /* @__PURE__ */ jsx2(Text2, { color: st.color, children: st.label.padEnd(7) }),
1048
1098
  /* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, children: truncate(r.isMe ? "this machine" : r.agent, 12).padEnd(13) }),
1049
1099
  /* @__PURE__ */ jsxs2(Text2, { children: [
1050
1100
  String(r.calls).padStart(4),
@@ -1078,7 +1128,8 @@ function LivePanel({ active: active2 }) {
1078
1128
  width: innerW
1079
1129
  }
1080
1130
  ),
1081
- /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height: searchRow ? streamRows - 1 : streamRows, overflow: "hidden", children: [
1131
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height: streamRows, overflow: "hidden", children: [
1132
+ searchRow,
1082
1133
  localOn === false ? /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
1083
1134
  /* @__PURE__ */ jsx2(Text2, { color: theme.warn, children: "local logs off" }),
1084
1135
  /* @__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.1",
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": {