@solongate/proxy 0.82.82 → 0.82.84

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tui/index.js CHANGED
@@ -338,6 +338,36 @@ function codexHooksStatus() {
338
338
  }
339
339
  return { registered, trusted, disabled };
340
340
  }
341
+ function repairQuiet() {
342
+ const p = globalPaths();
343
+ const has = (f) => existsSync2(f);
344
+ const guardFile = join3(p.hooksDir, "guard.mjs");
345
+ const line = (label, ok, yes, no) => ({ label, ok, detail: ok ? yes : no });
346
+ const before = [
347
+ line("guard hook file", has(guardFile), "present", "MISSING"),
348
+ line("cloud credential", has(p.configPath), "present", "MISSING"),
349
+ line("Claude Code settings", isGuardInstalled(), "guard registered", "guard NOT registered"),
350
+ line("Antigravity hooks", has(p.antigravityHooksPath), "present", "MISSING"),
351
+ line("Codex hooks", isCodexGuardInstalled(), "guard registered", "guard NOT registered")
352
+ ];
353
+ const r = installGlobalQuiet();
354
+ if (!r.ok) return { ok: false, message: r.message, before, after: [], notes: [] };
355
+ const after = [
356
+ { label: "guard hook file", ok: true, detail: `present (v${installedGuardVersion() ?? "?"})` },
357
+ line("Claude Code settings", isGuardInstalled(), "guard registered", "NOT registered"),
358
+ line("Antigravity hooks", has(p.antigravityHooksPath), "present", "MISSING"),
359
+ line("Codex hooks", isCodexGuardInstalled(), "guard registered", "NOT registered")
360
+ ];
361
+ const notes = [];
362
+ const cx = codexHooksStatus();
363
+ if (cx.registered && !cx.trusted) {
364
+ notes.push("Codex only: run `/hooks` inside Codex once and trust the SolonGate hooks (Codex skips any hook it has not been told to trust).");
365
+ }
366
+ if (cx.disabled) {
367
+ notes.push("Codex only: hooks are turned OFF in ~/.codex/config.toml ([features] hooks = false) - the guard cannot run there until that line is removed.");
368
+ }
369
+ return { ok: true, message: "guard repaired. Open a new AI session for it to take effect.", before, after, notes };
370
+ }
341
371
  function installGlobalQuiet() {
342
372
  try {
343
373
  const p = globalPaths();
@@ -500,8 +530,8 @@ var init_global_install = __esm({
500
530
 
501
531
  // src/tui/index.tsx
502
532
  import { appendFileSync, mkdirSync as mkdirSync7 } from "fs";
503
- import { homedir as homedir9 } from "os";
504
- import { join as join9 } from "path";
533
+ import { homedir as homedir10 } from "os";
534
+ import { join as join10 } from "path";
505
535
  import { render } from "ink";
506
536
 
507
537
  // src/tui/App.tsx
@@ -960,6 +990,14 @@ function dotenvApiKey() {
960
990
  return void 0;
961
991
  }
962
992
  var cached2 = null;
993
+ function isAuthenticated() {
994
+ try {
995
+ resolveCredentials();
996
+ return true;
997
+ } catch {
998
+ return false;
999
+ }
1000
+ }
963
1001
  function resolveCredentials(apiUrlOverride) {
964
1002
  if (viewOverride && !apiUrlOverride) return viewOverride;
965
1003
  if (cached2 && !apiUrlOverride) return cached2;
@@ -1005,8 +1043,8 @@ async function request(method, path, opts = {}) {
1005
1043
  try {
1006
1044
  res = await attempt();
1007
1045
  break;
1008
- } catch (err) {
1009
- lastErr = err;
1046
+ } catch (err2) {
1047
+ lastErr = err2;
1010
1048
  if (i < maxTries - 1) await new Promise((r) => setTimeout(r, 600 * (i + 1)));
1011
1049
  }
1012
1050
  }
@@ -1931,8 +1969,8 @@ function LivePanel({ active: active2 }) {
1931
1969
  until: Date.now() + 7e3
1932
1970
  });
1933
1971
  pushLog(`${act === "whitelist" ? "\u2713 whitelisted" : "\u26D4 blocked"} ${e.tool}`, act === "whitelist" ? "ok" : "warn");
1934
- } catch (err) {
1935
- setActionMsg({ text: "\u2717 " + (err instanceof Error ? err.message : String(err)), level: "bad", until: Date.now() + 7e3 });
1972
+ } catch (err2) {
1973
+ setActionMsg({ text: "\u2717 " + (err2 instanceof Error ? err2.message : String(err2)), level: "bad", until: Date.now() + 7e3 });
1936
1974
  }
1937
1975
  };
1938
1976
  useInput(
@@ -2035,8 +2073,8 @@ function LivePanel({ active: active2 }) {
2035
2073
  mkdirSync3(join5(homedir5(), ".solongate"), { recursive: true });
2036
2074
  writeFileSync4(file, visibleDesc.map((x) => JSON.stringify(x)).join("\n") + "\n");
2037
2075
  setActionMsg({ text: `\u2713 exported ${visibleDesc.length} lines \u2192 ${file}`, level: "ok", until: Date.now() + 6e3 });
2038
- } catch (err) {
2039
- setActionMsg({ text: "\u2717 export failed: " + (err instanceof Error ? err.message : String(err)), level: "bad", until: Date.now() + 6e3 });
2076
+ } catch (err2) {
2077
+ setActionMsg({ text: "\u2717 export failed: " + (err2 instanceof Error ? err2.message : String(err2)), level: "bad", until: Date.now() + 6e3 });
2040
2078
  }
2041
2079
  } else if (key.downArrow) setSel((n) => Math.min(visibleDesc.length - 1, n + 1));
2042
2080
  else if (key.upArrow) setSel((n) => Math.max(0, n - 1));
@@ -2461,7 +2499,7 @@ function DryRunPanel({ focused }) {
2461
2499
  const totalLogs = statsQ.data?.total_calls ?? null;
2462
2500
  const [res, setRes] = useState3(null);
2463
2501
  const [running, setRunning] = useState3(false);
2464
- const [err, setErr] = useState3(null);
2502
+ const [err2, setErr] = useState3(null);
2465
2503
  const [off, setOff] = useState3(0);
2466
2504
  const { cols, rows } = usePanelSize();
2467
2505
  const selected = policies[Math.min(pi, Math.max(0, policies.length - 1))];
@@ -2475,7 +2513,7 @@ function DryRunPanel({ focused }) {
2475
2513
  api.policies.backtest({ rules: p.rules ?? [], mode: p.mode, limit: lim }).then((r) => setRes(r)).catch((e) => setErr(e instanceof Error ? e.message : String(e))).finally(() => setRunning(false));
2476
2514
  }
2477
2515
  useEffect3(() => {
2478
- if (policies.length === 0 || res || running || err) return;
2516
+ if (policies.length === 0 || res || running || err2) return;
2479
2517
  let idx = 0;
2480
2518
  if (pendingPolicyId) {
2481
2519
  const found = policies.findIndex((p) => p.id === pendingPolicyId);
@@ -2508,7 +2546,7 @@ function DryRunPanel({ focused }) {
2508
2546
  const w = Math.max(40, cols);
2509
2547
  const lines = [];
2510
2548
  const head = (t) => lines.push(/* @__PURE__ */ jsx3(Text3, { bold: true, color: theme.accentBright, children: t }, `h${lines.length}`));
2511
- const dim = (t) => lines.push(/* @__PURE__ */ jsx3(Text3, { color: theme.dim, wrap: "truncate", children: t }, `d${lines.length}`));
2549
+ const dim2 = (t) => lines.push(/* @__PURE__ */ jsx3(Text3, { color: theme.dim, wrap: "truncate", children: t }, `d${lines.length}`));
2512
2550
  const row = (t) => lines.push(/* @__PURE__ */ jsx3(Text3, { wrap: "truncate", children: t }, `r${lines.length}`));
2513
2551
  const blank = () => lines.push(/* @__PURE__ */ jsx3(Text3, { children: " " }, `b${lines.length}`));
2514
2552
  if (res) {
@@ -2534,7 +2572,7 @@ function DryRunPanel({ focused }) {
2534
2572
  blank();
2535
2573
  if (res.per_rule.length) {
2536
2574
  head("Per-rule impact");
2537
- dim(` ${"RULE".padEnd(38)}${"MATCHED".padEnd(9)}${"%".padEnd(8)}${"NEW-BLK".padEnd(9)}NEW-ALW`);
2575
+ dim2(` ${"RULE".padEnd(38)}${"MATCHED".padEnd(9)}${"%".padEnd(8)}${"NEW-BLK".padEnd(9)}NEW-ALW`);
2538
2576
  for (const r of res.per_rule.slice(0, 30)) {
2539
2577
  row(` ${truncate(r.rule_id, 36).padEnd(38)}${String(r.matched).padEnd(9)}${pct(r.matched, s.evaluated).padEnd(8)}${String(r.newly_blocked).padEnd(9)}${r.newly_allowed}`);
2540
2578
  }
@@ -2542,7 +2580,7 @@ function DryRunPanel({ focused }) {
2542
2580
  }
2543
2581
  if (res.per_agent.length) {
2544
2582
  head("Per-agent impact");
2545
- dim(` ${"AGENT".padEnd(24)}${"EVALUATED".padEnd(11)}${"CHANGED".padEnd(9)}${"NEW-BLK".padEnd(9)}NEW-ALW`);
2583
+ dim2(` ${"AGENT".padEnd(24)}${"EVALUATED".padEnd(11)}${"CHANGED".padEnd(9)}${"NEW-BLK".padEnd(9)}NEW-ALW`);
2546
2584
  for (const a of res.per_agent) {
2547
2585
  row(` ${truncate(a.agent || "-", 22).padEnd(24)}${String(a.evaluated).padEnd(11)}${String(a.changed).padEnd(9)}${String(a.newly_blocked).padEnd(9)}${a.newly_allowed}`);
2548
2586
  }
@@ -2550,7 +2588,7 @@ function DryRunPanel({ focused }) {
2550
2588
  }
2551
2589
  if (res.per_tool.length) {
2552
2590
  head("Per-tool breakdown");
2553
- dim(` ${"TOOL".padEnd(24)}${"EVALUATED".padEnd(11)}${"CHANGED".padEnd(9)}${"NEW-BLK".padEnd(9)}NEW-ALW`);
2591
+ dim2(` ${"TOOL".padEnd(24)}${"EVALUATED".padEnd(11)}${"CHANGED".padEnd(9)}${"NEW-BLK".padEnd(9)}NEW-ALW`);
2554
2592
  for (const t of res.per_tool) {
2555
2593
  row(` ${truncate(t.tool || "-", 22).padEnd(24)}${String(t.evaluated).padEnd(11)}${String(t.changed).padEnd(9)}${String(t.newly_blocked).padEnd(9)}${t.newly_allowed}`);
2556
2594
  }
@@ -2599,8 +2637,8 @@ function DryRunPanel({ focused }) {
2599
2637
  ] }),
2600
2638
  /* @__PURE__ */ jsx3(Text3, { color: theme.dim, wrap: "truncate", children: focused ? `\u2191\u2193 scroll \xB7 p policy \xB7 n logs \xB7 r re-run${above ? ` \xB7 \u25B2${above}` : ""}${below ? ` \xB7 \u25BC${below}` : ""}` : "press \u2192 to open" }),
2601
2639
  running ? /* @__PURE__ */ jsx3(Text3, { color: theme.warn, children: "replaying recent traffic\u2026" }) : null,
2602
- err ? /* @__PURE__ */ jsx3(Text3, { color: theme.bad, wrap: "truncate", children: "\u2717 " + err }) : null,
2603
- !running && !err && !res ? /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: "no result yet, press r to run" }) : null,
2640
+ err2 ? /* @__PURE__ */ jsx3(Text3, { color: theme.bad, wrap: "truncate", children: "\u2717 " + err2 }) : null,
2641
+ !running && !err2 && !res ? /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: "no result yet, press r to run" }) : null,
2604
2642
  /* @__PURE__ */ jsx3(Box3, { marginTop: 1, flexDirection: "column", children: win })
2605
2643
  ] }) });
2606
2644
  }
@@ -4238,19 +4276,85 @@ import TextInput6 from "ink-text-input";
4238
4276
  import { useEffect as useEffect7, useRef as useRef3, useState as useState8 } from "react";
4239
4277
  init_global_install();
4240
4278
 
4279
+ // src/commands/doctor.ts
4280
+ import { existsSync as existsSync4, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
4281
+ import { homedir as homedir7 } from "os";
4282
+ import { join as join7 } from "path";
4283
+ init_global_install();
4284
+ async function collectChecks() {
4285
+ const checks = [];
4286
+ if (!isAuthenticated()) {
4287
+ checks.push({ name: "login", ok: false, detail: "not logged in - run `solongate`, add your account in the Accounts panel" });
4288
+ } else {
4289
+ const { apiUrl } = resolveCredentials();
4290
+ checks.push({ name: "login", ok: true, detail: `paired \xB7 ${apiUrl}` });
4291
+ try {
4292
+ const active2 = await api.policies.active();
4293
+ if (active2.policy) {
4294
+ checks.push({ name: "active policy", ok: true, detail: `${active2.policy.name} v${active2.version} \xB7 ${active2.policy.mode ?? "denylist"} \xB7 matched by ${active2.matched_by}` });
4295
+ } else {
4296
+ checks.push({ name: "active policy", ok: "warn", detail: "no policy resolves - every call falls back to default" });
4297
+ }
4298
+ const sec = active2.security;
4299
+ checks.push({ name: "rate limit", ok: sec?.rateLimit ? true : "warn", detail: sec?.rateLimit ? `${sec.rateLimit.perMinute}/min` : "off" });
4300
+ checks.push({ name: "dlp", ok: sec?.dlpBlock ? true : "warn", detail: sec?.dlpBlock ? `block \xB7 ${sec.dlpBlock.patterns.length} patterns` : sec?.dlpRedact ? "redact" : "off" });
4301
+ checks.push({ name: "self-protection", ok: active2.self_protection_enabled ? true : "warn", detail: active2.self_protection_enabled ? "on" : "off" });
4302
+ } catch (e) {
4303
+ checks.push({ name: "api", ok: false, detail: "unreachable: " + (e instanceof Error ? e.message : String(e)) });
4304
+ }
4305
+ try {
4306
+ const g = await api.settings.getGuardStatus();
4307
+ checks.push({ name: "guard hook", ok: g.up_to_date ? true : "warn", detail: g.up_to_date ? `v${g.installed} (latest) \xB7 ${g.device_count} device(s)` : `v${g.installed} \u2192 v${g.latest} available \xB7 update from \`solongate\` \u2192 Settings \u2192 guard` });
4308
+ } catch {
4309
+ }
4310
+ }
4311
+ if (codexDetected()) {
4312
+ const cx = codexHooksStatus();
4313
+ if (!cx.registered) {
4314
+ checks.push({ name: "codex hooks", ok: false, detail: "guard not registered - run `solongate repair`" });
4315
+ } else if (cx.disabled) {
4316
+ checks.push({ name: "codex hooks", ok: false, detail: "hooks disabled in ~/.codex/config.toml ([features] hooks = false)" });
4317
+ } else if (!cx.trusted) {
4318
+ checks.push({ name: "codex hooks", ok: "warn", detail: "registered - run `/hooks` in Codex once and trust them (Codex skips untrusted hooks)" });
4319
+ } else {
4320
+ checks.push({ name: "codex hooks", ok: true, detail: "registered + trusted" });
4321
+ }
4322
+ }
4323
+ try {
4324
+ const raw = readFileSync5(join7(homedir7(), ".solongate", ".key-rejected.json"), "utf-8");
4325
+ const m = JSON.parse(raw);
4326
+ const ageMin = m.ts ? Math.round((Date.now() - m.ts) / 6e4) : null;
4327
+ checks.push({
4328
+ name: "hook credential",
4329
+ ok: false,
4330
+ detail: `rejected by ${m.apiUrl ?? "the API"}${ageMin != null ? ` ${ageMin}m ago` : ""} \xB7 key from ${m.keySource ?? "?"}${m.cwd ? ` (agent cwd ${m.cwd})` : ""} - nothing is being logged from there`
4331
+ });
4332
+ } catch {
4333
+ }
4334
+ const LOCAL_LOG2 = localLogFile();
4335
+ if (existsSync4(LOCAL_LOG2)) {
4336
+ const st = statSync2(LOCAL_LOG2);
4337
+ const ageMin = (Date.now() - st.mtimeMs) / 6e4;
4338
+ checks.push({ name: "local logs", ok: true, detail: `on \xB7 ${(st.size / 1024).toFixed(0)}KB \xB7 last write ${ageMin < 1 ? "just now" : Math.round(ageMin) + "m ago"}` });
4339
+ } else {
4340
+ checks.push({ name: "local logs", ok: "warn", detail: "off (logs go to cloud) - enable in dashboard \u2192 Settings" });
4341
+ }
4342
+ return checks;
4343
+ }
4344
+
4241
4345
  // src/logs-server-daemon.ts
4242
4346
  import { spawn as spawn3 } from "child_process";
4243
- import { mkdirSync as mkdirSync5, openSync as openSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "fs";
4244
- import { homedir as homedir7 } from "os";
4245
- import { dirname as dirname2, join as join7 } from "path";
4347
+ import { mkdirSync as mkdirSync5, openSync as openSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
4348
+ import { homedir as homedir8 } from "os";
4349
+ import { dirname as dirname2, join as join8 } from "path";
4246
4350
  import { fileURLToPath as fileURLToPath2 } from "url";
4247
- var DIR = join7(homedir7(), ".solongate");
4248
- var STATE_FILE = join7(DIR, ".logs-server.json");
4249
- var LOG_FILE = join7(DIR, "logs-server.log");
4351
+ var DIR = join8(homedir8(), ".solongate");
4352
+ var STATE_FILE = join8(DIR, ".logs-server.json");
4353
+ var LOG_FILE = join8(DIR, "logs-server.log");
4250
4354
  var LOGS_SERVER_PORT = 8788;
4251
4355
  function readState() {
4252
4356
  try {
4253
- const s = JSON.parse(readFileSync5(STATE_FILE, "utf-8"));
4357
+ const s = JSON.parse(readFileSync6(STATE_FILE, "utf-8"));
4254
4358
  return s && typeof s === "object" ? s : {};
4255
4359
  } catch {
4256
4360
  return {};
@@ -4286,7 +4390,7 @@ function startLogsServerDaemon() {
4286
4390
  try {
4287
4391
  mkdirSync5(DIR, { recursive: true });
4288
4392
  const log = openSync2(LOG_FILE, "a");
4289
- const cli = join7(dirname2(fileURLToPath2(import.meta.url)), "index.js");
4393
+ const cli = join8(dirname2(fileURLToPath2(import.meta.url)), "index.js");
4290
4394
  const p = spawn3(process.execPath, [cli, "logs-server"], {
4291
4395
  detached: true,
4292
4396
  stdio: ["ignore", log, log],
@@ -4318,18 +4422,18 @@ function stopLogsServerDaemon() {
4318
4422
 
4319
4423
  // src/self-update.ts
4320
4424
  import { execFile, execFileSync as execFileSync2, spawn as spawn4 } from "child_process";
4321
- import { mkdirSync as mkdirSync6, openSync as openSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync7 } from "fs";
4322
- import { homedir as homedir8 } from "os";
4323
- import { dirname as dirname3, join as join8 } from "path";
4425
+ import { mkdirSync as mkdirSync6, openSync as openSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "fs";
4426
+ import { homedir as homedir9 } from "os";
4427
+ import { dirname as dirname3, join as join9 } from "path";
4324
4428
  import { fileURLToPath as fileURLToPath3 } from "url";
4325
4429
  var PKG = "@solongate/proxy";
4326
4430
  var CHECK_EVERY_MS = 30 * 60 * 1e3;
4327
4431
  var ATTEMPT_EVERY_MS = 6 * 60 * 60 * 1e3;
4328
- var STATE_FILE2 = join8(homedir8(), ".solongate", ".self-update.json");
4329
- var LOG_FILE2 = join8(homedir8(), ".solongate", "self-update.log");
4432
+ var STATE_FILE2 = join9(homedir9(), ".solongate", ".self-update.json");
4433
+ var LOG_FILE2 = join9(homedir9(), ".solongate", "self-update.log");
4330
4434
  function readState2() {
4331
4435
  try {
4332
- const s = JSON.parse(readFileSync6(STATE_FILE2, "utf-8"));
4436
+ const s = JSON.parse(readFileSync7(STATE_FILE2, "utf-8"));
4333
4437
  return s && typeof s === "object" ? s : {};
4334
4438
  } catch {
4335
4439
  return {};
@@ -4337,14 +4441,14 @@ function readState2() {
4337
4441
  }
4338
4442
  function writeState2(s) {
4339
4443
  try {
4340
- mkdirSync6(join8(homedir8(), ".solongate"), { recursive: true });
4444
+ mkdirSync6(join9(homedir9(), ".solongate"), { recursive: true });
4341
4445
  writeFileSync7(STATE_FILE2, JSON.stringify(s));
4342
4446
  } catch {
4343
4447
  }
4344
4448
  }
4345
4449
  function currentVersion() {
4346
4450
  try {
4347
- const pkg = JSON.parse(readFileSync6(join8(dirname3(fileURLToPath3(import.meta.url)), "..", "package.json"), "utf-8"));
4451
+ const pkg = JSON.parse(readFileSync7(join9(dirname3(fileURLToPath3(import.meta.url)), "..", "package.json"), "utf-8"));
4348
4452
  return pkg.version ?? "0.0.0";
4349
4453
  } catch {
4350
4454
  return "0.0.0";
@@ -4381,20 +4485,20 @@ async function fetchLatest() {
4381
4485
  function runGlobalInstall(version) {
4382
4486
  return new Promise((resolve3) => {
4383
4487
  try {
4384
- mkdirSync6(join8(homedir8(), ".solongate"), { recursive: true });
4488
+ mkdirSync6(join9(homedir9(), ".solongate"), { recursive: true });
4385
4489
  execFile(
4386
4490
  "npm",
4387
4491
  ["install", "-g", `${PKG}@${version}`],
4388
4492
  { timeout: 3e5, windowsHide: true, shell: process.platform === "win32" },
4389
- (err, stdout, stderr) => {
4493
+ (err2, stdout, stderr) => {
4390
4494
  try {
4391
- writeFileSync7(LOG_FILE2, `${(/* @__PURE__ */ new Date()).toISOString()} install ${version}: ${err ? "FAILED" : "ok"}
4495
+ writeFileSync7(LOG_FILE2, `${(/* @__PURE__ */ new Date()).toISOString()} install ${version}: ${err2 ? "FAILED" : "ok"}
4392
4496
  ${stdout}
4393
4497
  ${stderr}
4394
4498
  `, { flag: "a" });
4395
4499
  } catch {
4396
4500
  }
4397
- resolve3(!err);
4501
+ resolve3(!err2);
4398
4502
  }
4399
4503
  );
4400
4504
  } catch {
@@ -4457,6 +4561,24 @@ function SettingsPanel({
4457
4561
  const [busy, setBusy] = useState8(false);
4458
4562
  const [editor, setEditor] = useState8(null);
4459
4563
  const [latest, setLatest] = useState8(null);
4564
+ const [diagBusy, setDiagBusy] = useState8(null);
4565
+ const [diag, setDiag] = useState8(null);
4566
+ const [diagStep, setDiagStep] = useState8(0);
4567
+ const DIAG_STEPS = {
4568
+ doctor: ["login", "active policy", "guard hook", "agent hooks", "local logs"],
4569
+ repair: ["inspecting protection files", "rewriting hook files", "registering with agents", "re-locking"]
4570
+ };
4571
+ const settle = async (started, of) => {
4572
+ const min = DIAG_STEPS[of].length * 220 + 160;
4573
+ const left = min - (Date.now() - started);
4574
+ if (left > 0) await new Promise((res) => setTimeout(res, left));
4575
+ };
4576
+ useEffect7(() => {
4577
+ if (!diagBusy) return;
4578
+ const total = DIAG_STEPS[diagBusy].length;
4579
+ const step = setInterval(() => setDiagStep((s) => Math.min(s + 1, total - 1)), 220);
4580
+ return () => clearInterval(step);
4581
+ }, [diagBusy]);
4460
4582
  const ver = currentVersion();
4461
4583
  useEffect7(() => {
4462
4584
  let live2 = true;
@@ -4495,10 +4617,11 @@ function SettingsPanel({
4495
4617
  return () => clearInterval(t);
4496
4618
  }, []);
4497
4619
  useEffect7(() => {
4498
- if (!login || login.phase === "done" || login.phase === "error") return;
4620
+ const loggingIn = login && login.phase !== "done" && login.phase !== "error";
4621
+ if (!loggingIn && !diagBusy) return;
4499
4622
  const t = setInterval(() => setTick((n) => n + 1), 120);
4500
4623
  return () => clearInterval(t);
4501
- }, [login]);
4624
+ }, [login, diagBusy]);
4502
4625
  const localQ = useLoader(() => listAccounts().length ? api.settings.getLocalLogs() : Promise.resolve(null));
4503
4626
  const whQ = useLoader(() => listAccounts().length ? api.settings.getWebhooks() : Promise.resolve(null));
4504
4627
  const alertQ = useLoader(() => listAccounts().length ? api.settings.getAlerts() : Promise.resolve(null));
@@ -4530,6 +4653,8 @@ function SettingsPanel({
4530
4653
  ...locked ? [] : [
4531
4654
  { kind: "guard" },
4532
4655
  { kind: "self" },
4656
+ { kind: "doctor" },
4657
+ { kind: "repair" },
4533
4658
  { kind: "ll-enabled" },
4534
4659
  { kind: "ll-path" },
4535
4660
  { kind: "ll-server" },
@@ -4648,6 +4773,64 @@ function SettingsPanel({
4648
4773
  setMsg({ text: (res.ok ? "\u2713 " : "\u2717 ") + res.message, level: res.ok ? "ok" : "bad" });
4649
4774
  refreshGuard();
4650
4775
  guardQ.reload();
4776
+ } else if (r.kind === "doctor") {
4777
+ if (diagBusy) return;
4778
+ setDiagBusy("doctor");
4779
+ setDiagStep(0);
4780
+ setDiag(null);
4781
+ void (async () => {
4782
+ const started = Date.now();
4783
+ try {
4784
+ const checks = await collectChecks();
4785
+ await settle(started, "doctor");
4786
+ const bad = checks.filter((c2) => c2.ok === false).length;
4787
+ const warn = checks.filter((c2) => c2.ok === "warn").length;
4788
+ setDiag({
4789
+ of: "doctor",
4790
+ lines: checks.map((c2) => ({
4791
+ text: `${c2.ok === true ? "\u2713" : c2.ok === "warn" ? "!" : "\u2717"} ${c2.name.padEnd(16)} ${c2.detail}`,
4792
+ level: c2.ok === true ? "ok" : c2.ok === "warn" ? "warn" : "bad"
4793
+ }))
4794
+ });
4795
+ setMsg(
4796
+ bad ? { text: `\u2717 ${bad} problem(s)${warn ? ` \xB7 ${warn} warning(s)` : ""}`, level: "bad" } : { text: warn ? `\u2713 guard is working \xB7 ${warn} warning(s)` : "\u2713 all good", level: "ok" }
4797
+ );
4798
+ } catch (e) {
4799
+ setMsg({ text: "\u2717 doctor failed: " + (e instanceof Error ? e.message : String(e)), level: "bad" });
4800
+ } finally {
4801
+ setDiagBusy(null);
4802
+ }
4803
+ })();
4804
+ } else if (r.kind === "repair") {
4805
+ if (diagBusy) return;
4806
+ setDiagBusy("repair");
4807
+ setDiagStep(0);
4808
+ setDiag(null);
4809
+ void (async () => {
4810
+ try {
4811
+ const started = Date.now();
4812
+ await new Promise((res) => setTimeout(res, 60));
4813
+ const rep = repairQuiet();
4814
+ await settle(started, "repair");
4815
+ setDiag({
4816
+ of: "repair",
4817
+ lines: [
4818
+ ...(rep.ok ? rep.after : rep.before).map((l) => ({
4819
+ text: `${l.ok ? "\u2713" : "\u2717"} ${l.label.padEnd(20)} ${l.detail}`,
4820
+ level: l.ok ? "ok" : "bad"
4821
+ })),
4822
+ ...rep.notes.map((n) => ({ text: n, level: "warn" }))
4823
+ ]
4824
+ });
4825
+ setMsg({ text: (rep.ok ? "\u2713 " : "\u2717 ") + rep.message, level: rep.ok ? "ok" : "bad" });
4826
+ refreshGuard();
4827
+ guardQ.reload();
4828
+ } catch (e) {
4829
+ setMsg({ text: "\u2717 repair failed: " + (e instanceof Error ? e.message : String(e)), level: "bad" });
4830
+ } finally {
4831
+ setDiagBusy(null);
4832
+ }
4833
+ })();
4651
4834
  } else if (r.kind === "self") {
4652
4835
  if (!selfProt) return;
4653
4836
  run(selfProt.enabled ? "self-protection disabled" : "self-protection enabled", () => api.settings.setSelfProtection(!selfProt.enabled), selfQ.reload);
@@ -4937,6 +5120,18 @@ function SettingsPanel({
4937
5120
  selfProt ? onOff(selfProt.enabled) : /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "\u2026" }),
4938
5121
  /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: " blocks agents editing SolonGate\u2019s own hooks/config \xB7 enter toggles" })
4939
5122
  ] });
5123
+ case "doctor":
5124
+ return /* @__PURE__ */ jsxs8(Text8, { wrap: "truncate", children: [
5125
+ cursor(r),
5126
+ /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "doctor".padEnd(11) }),
5127
+ diagBusy === "doctor" ? /* @__PURE__ */ jsx8(Text8, { color: theme.accentBright, children: `${spin} running health check\u2026` }) : /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "health check: login, policy, guard, hooks, local logs \xB7 enter runs it" })
5128
+ ] });
5129
+ case "repair":
5130
+ return /* @__PURE__ */ jsxs8(Text8, { wrap: "truncate", children: [
5131
+ cursor(r),
5132
+ /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "repair".padEnd(11) }),
5133
+ diagBusy === "repair" ? /* @__PURE__ */ jsx8(Text8, { color: theme.accentBright, children: `${spin} restoring protection\u2026` }) : /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "restore every protection file after tampering or deletion \xB7 enter runs it" })
5134
+ ] });
4940
5135
  case "ll-enabled":
4941
5136
  return /* @__PURE__ */ jsxs8(Text8, { wrap: "truncate", children: [
4942
5137
  cursor(r),
@@ -4989,10 +5184,10 @@ function SettingsPanel({
4989
5184
  ] });
4990
5185
  }
4991
5186
  };
4992
- const sectionOf = (r) => r.kind === "acct" || r.kind === "acct-add" ? "ACCOUNTS" : r.kind === "guard" || r.kind === "self" ? "PROTECTION" : r.kind === "ll-enabled" || r.kind === "ll-path" || r.kind === "ll-server" ? "LOCAL LOGS" : r.kind === "wh" || r.kind === "wh-add" ? "WEBHOOKS" : "ALERTS";
5187
+ const sectionOf = (r) => r.kind === "acct" || r.kind === "acct-add" ? "ACCOUNTS" : r.kind === "guard" || r.kind === "self" || r.kind === "doctor" || r.kind === "repair" ? "PROTECTION" : r.kind === "ll-enabled" || r.kind === "ll-path" || r.kind === "ll-server" ? "LOCAL LOGS" : r.kind === "wh" || r.kind === "wh-add" ? "WEBHOOKS" : "ALERTS";
4993
5188
  const SECTION_DESC = {
4994
5189
  ACCOUNTS: `on this device (${accounts.length}) \xB7 \u25CF viewing \xB7 ACTIVE = guard key \xB7 x removes`,
4995
- PROTECTION: "guard hook: enter install/update (writes the files) \xB7 d remove \xB7 + self-protection",
5190
+ PROTECTION: "guard hook: enter install/update \xB7 d remove \xB7 self-protection \xB7 doctor + repair",
4996
5191
  "LOCAL LOGS": "mirror every decision to a file + dashboard link",
4997
5192
  WEBHOOKS: `POST events to a URL (${webhooks.length}) \xB7 t tests`,
4998
5193
  ALERTS: `one email + one telegram (${alerts.length}) \xB7 enter edits \xB7 m on/off`
@@ -5024,6 +5219,31 @@ function SettingsPanel({
5024
5219
  }
5025
5220
  lineEls.push(/* @__PURE__ */ jsx8(Box8, { children: rowLine(r) }, keyOf(r)));
5026
5221
  lineKey.push(keyOf(r));
5222
+ if ((r.kind === "doctor" || r.kind === "repair") && diagBusy === r.kind) {
5223
+ DIAG_STEPS[r.kind].forEach((label, i) => {
5224
+ const done = i < diagStep;
5225
+ lineEls.push(
5226
+ /* @__PURE__ */ jsx8(Text8, { wrap: "truncate", color: done ? theme.ok : i === diagStep ? theme.accentBright : theme.dim, children: ` ${done ? "\u2713" : i === diagStep ? spin : " "} ${label}` }, `step:${r.kind}:${i}`)
5227
+ );
5228
+ lineKey.push("");
5229
+ });
5230
+ }
5231
+ if (diag && (r.kind === "doctor" && diag.of === "doctor" || r.kind === "repair" && diag.of === "repair")) {
5232
+ diag.lines.forEach((l, i) => {
5233
+ lineEls.push(
5234
+ /* @__PURE__ */ jsx8(
5235
+ Text8,
5236
+ {
5237
+ wrap: "truncate",
5238
+ color: l.level === "ok" ? theme.ok : l.level === "warn" ? theme.warn : l.level === "bad" ? theme.bad : theme.dim,
5239
+ children: " " + l.text
5240
+ },
5241
+ `diag:${diag.of}:${i}`
5242
+ )
5243
+ );
5244
+ lineKey.push("");
5245
+ });
5246
+ }
5027
5247
  });
5028
5248
  if (hasEmailAlert && hasTgAlert) {
5029
5249
  lineEls.push(
@@ -5258,11 +5478,11 @@ async function launchTui() {
5258
5478
  return;
5259
5479
  }
5260
5480
  process.stdout.write("\x1B[?1049h\x1B[H");
5261
- const debugLog = join9(homedir9(), ".solongate", "dataroom-debug.log");
5481
+ const debugLog = join10(homedir10(), ".solongate", "dataroom-debug.log");
5262
5482
  const saved = { log: console.log, warn: console.warn, error: console.error, info: console.info, debug: console.debug };
5263
5483
  const toFile = (level) => (...args) => {
5264
5484
  try {
5265
- mkdirSync7(join9(homedir9(), ".solongate"), { recursive: true });
5485
+ mkdirSync7(join10(homedir10(), ".solongate"), { recursive: true });
5266
5486
  appendFileSync(debugLog, `${(/* @__PURE__ */ new Date()).toISOString()} [${level}] ${args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ")}
5267
5487
  `);
5268
5488
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.82.82",
3
+ "version": "0.82.84",
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": {