@solongate/proxy 0.83.56 → 0.83.57

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
@@ -8,38 +8,230 @@ var __export = (target, all) => {
8
8
  __defProp(target, name, { get: all[name], enumerable: true });
9
9
  };
10
10
 
11
- // src/global-install.ts
12
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync, rmSync, rmdirSync, readdirSync as readdirSync2, statSync as statSync2, chmodSync, copyFileSync, renameSync } from "fs";
13
- import { resolve, join as join3, dirname } from "path";
11
+ // src/hook-launcher.ts
12
+ function launcherScript(pinnedNode) {
13
+ const pinned = pinnedNode.replace(/'/g, `'\\''`);
14
+ return `#!/bin/sh
15
+ # SolonGate hook launcher \u2014 generated by \`solongate init --global\` / \`repair\`.
16
+ #
17
+ # Finds a working node and execs the hook with it. Do not edit: a reinstall
18
+ # overwrites this file, and on macOS it is chflags-locked besides.
19
+ #
20
+ # The reason it exists rather than the hook naming a node directly: an absolute
21
+ # node path recorded at install time is a Homebrew Cellar path or an nvm version
22
+ # directory, and both are deleted by a routine upgrade. The hook then could not
23
+ # start, nothing was enforced, nothing was logged, and every check still said
24
+ # the guard was registered \u2014 because it was.
25
+
26
+ set -u
27
+
28
+ script="\${1:-}"
29
+ [ -n "$script" ] || { echo "solongate: launcher called with no hook script" >&2; exit 1; }
30
+ shift
31
+
32
+ hook=\${script##*/}
33
+ home=\${HOME:-~}
34
+ beatdir="$home/.solongate/${BEAT_DIR}"
35
+
36
+ # The beat. Written BEFORE node is resolved, because its whole job is to record
37
+ # that the client invoked us \u2014 which is true even when everything after this
38
+ # line fails. The file's modification time is the timestamp; nothing is forked
39
+ # to produce one, and the directory test is a builtin.
40
+ #
41
+ # The braces matter. A redirect into a missing directory is reported by the
42
+ # SHELL, before the command runs, so a \`2>/dev/null\` on the printf alone does
43
+ # not suppress it \u2014 it lands on the hook's stderr, and Claude Code shows a
44
+ # hook's stderr to the person using it. Redirecting the group catches both.
45
+ beat() {
46
+ [ -d "$beatdir" ] || mkdir -p "$beatdir" 2>/dev/null || return 0
47
+ { printf '%s\\n' "$1" > "$beatdir/$hook"; } 2>/dev/null || true
48
+ }
49
+
50
+ try() {
51
+ [ -n "\${1:-}" ] && [ -x "$1" ]
52
+ }
53
+
54
+ resolve_node() {
55
+ # Told explicitly.
56
+ if try "\${SOLONGATE_NODE:-}"; then echo "$SOLONGATE_NODE"; return 0; fi
57
+ # The node this was installed with.
58
+ if try '${pinned}'; then echo '${pinned}'; return 0; fi
59
+ # PATH, when the client gave us one worth having.
60
+ p=$(command -v node 2>/dev/null) || p=
61
+ if try "$p"; then echo "$p"; return 0; fi
62
+ # The fixed locations.
63
+ for c in ${NODE_CANDIDATES.map((c2) => `"${c2}"`).join(" ")}; do
64
+ for g in $c; do
65
+ if try "$g"; then echo "$g"; return 0; fi
66
+ done
67
+ done
68
+ # The version managers.
69
+ for c in ${NODE_GLOBS.join(" ")}; do
70
+ for g in $c; do
71
+ if try "$g"; then echo "$g"; return 0; fi
72
+ done
73
+ done
74
+ return 1
75
+ }
76
+
77
+ node_bin=$(resolve_node) || node_bin=
78
+
79
+ # --sg-doctor: report what would be used and leave. This is what \`solongate
80
+ # doctor\` runs, so the health check exercises the REAL resolution rather than a
81
+ # copy of it that can drift.
82
+ if [ "$script" = "--sg-doctor" ] || [ "\${1:-}" = "--sg-doctor" ]; then
83
+ [ -n "$node_bin" ] && { echo "$node_bin"; exit 0; }
84
+ echo "no node found" >&2
85
+ exit 1
86
+ fi
87
+
88
+ if [ -z "$node_bin" ]; then
89
+ beat "no-node"
90
+ # Nothing can be enforced without node, and which way to fail is not a
91
+ # judgement call: the guard is fail-closed, so it refuses the call and says
92
+ # why. Everything else here only records what already happened, and refusing
93
+ # a tool call because the log could not be written would be a worse product
94
+ # than a gap in the log.
95
+ echo "solongate: no node runtime found, so $hook did not run." >&2
96
+ echo "solongate: run \\\`solongate repair\\\` in a terminal, or set SOLONGATE_NODE to your node binary." >&2
97
+ case "$hook" in
98
+ guard.mjs) echo "solongate: this tool call is REFUSED \u2014 the guard is fail-closed." >&2; exit 2 ;;
99
+ *) exit 0 ;;
100
+ esac
101
+ fi
102
+
103
+ beat "$node_bin"
104
+ exec "$node_bin" "$script" "$@"
105
+ `;
106
+ }
107
+ var NODE_CANDIDATES, NODE_GLOBS, LAUNCHER_NAME, BEAT_DIR;
108
+ var init_hook_launcher = __esm({
109
+ "src/hook-launcher.ts"() {
110
+ "use strict";
111
+ NODE_CANDIDATES = [
112
+ // Homebrew, by the STABLE symlink rather than the Cellar path behind it —
113
+ // the exact distinction this file exists for. Apple Silicon then Intel.
114
+ "/opt/homebrew/bin/node",
115
+ "/usr/local/bin/node",
116
+ // Homebrew's keg-only layout, both prefixes.
117
+ "/opt/homebrew/opt/node/bin/node",
118
+ "/usr/local/opt/node/bin/node",
119
+ // Distro and hand-built.
120
+ "/usr/bin/node",
121
+ "/usr/local/n/versions/node/*/bin/node",
122
+ "/snap/bin/node"
123
+ ];
124
+ NODE_GLOBS = [
125
+ // nvm. NVM_DIR is exported by its own shell hook, which a non-interactive
126
+ // hook environment does not run, so the default location is tried too.
127
+ '"${NVM_DIR:-}"/versions/node/*/bin/node',
128
+ '"$home"/.nvm/versions/node/*/bin/node',
129
+ // fnm, both the XDG location and the macOS Application Support one.
130
+ '"$home"/.local/share/fnm/node-versions/*/installation/bin/node',
131
+ '"$home"/Library/Application Support/fnm/node-versions/*/installation/bin/node',
132
+ // Volta.
133
+ '"$home"/.volta/tools/image/node/*/bin/node',
134
+ // asdf, old layout and the current plugin one.
135
+ '"$home"/.asdf/installs/nodejs/*/bin/node',
136
+ '"$home"/.asdf/installs/node/*/bin/node'
137
+ ];
138
+ LAUNCHER_NAME = "sg-run.sh";
139
+ BEAT_DIR = ".beat";
140
+ }
141
+ });
142
+
143
+ // src/hook-health.ts
144
+ import { execFileSync } from "child_process";
145
+ import { existsSync as existsSync2, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
146
+ import { join as join3 } from "path";
14
147
  import { homedir as homedir3 } from "os";
148
+ function hookCanStart() {
149
+ const launcher = join3(hooksDir(), LAUNCHER_NAME);
150
+ if (process.platform === "win32") {
151
+ const ok = existsSync2(process.execPath);
152
+ return { ok, node: process.execPath, detail: ok ? process.execPath : `${process.execPath} is gone` };
153
+ }
154
+ if (!existsSync2(launcher)) {
155
+ return { ok: false, detail: "hook launcher missing - run `solongate repair`" };
156
+ }
157
+ try {
158
+ const out = execFileSync("/bin/sh", [launcher, "--sg-doctor"], {
159
+ encoding: "utf-8",
160
+ timeout: 5e3,
161
+ stdio: ["ignore", "pipe", "pipe"]
162
+ }).trim();
163
+ if (!out) return { ok: false, detail: "launcher found no node runtime - set SOLONGATE_NODE or install node" };
164
+ return { ok: true, node: out, detail: out };
165
+ } catch (e) {
166
+ const msg = e instanceof Error ? e.message : String(e);
167
+ return { ok: false, detail: `launcher will not run: ${msg.split("\n")[0]}` };
168
+ }
169
+ }
170
+ function hookBeats() {
171
+ const dir = join3(sgDir(), BEAT_DIR);
172
+ const out = [];
173
+ for (const hook of ["guard.mjs", "audit.mjs", "conversation.mjs", "stop.mjs"]) {
174
+ const f = join3(dir, hook);
175
+ try {
176
+ const st = statSync2(f);
177
+ out.push({ hook, at: st.mtime, node: readFileSync3(f, "utf-8").trim() });
178
+ } catch {
179
+ }
180
+ }
181
+ return out.sort((a, b) => b.at.getTime() - a.at.getTime());
182
+ }
183
+ function guardBeat() {
184
+ return hookBeats().find((b) => b.hook === "guard.mjs") ?? null;
185
+ }
186
+ function agoLabel(d) {
187
+ const s = Math.max(0, Math.round((Date.now() - d.getTime()) / 1e3));
188
+ if (s < 60) return s <= 3 ? "just now" : `${s}s ago`;
189
+ if (s < 3600) return `${Math.round(s / 60)}m ago`;
190
+ if (s < 86400) return `${Math.round(s / 3600)}h ago`;
191
+ return `${Math.round(s / 86400)}d ago`;
192
+ }
193
+ var sgDir, hooksDir;
194
+ var init_hook_health = __esm({
195
+ "src/hook-health.ts"() {
196
+ "use strict";
197
+ init_hook_launcher();
198
+ sgDir = () => join3(homedir3(), ".solongate");
199
+ hooksDir = () => join3(sgDir(), "hooks");
200
+ }
201
+ });
202
+
203
+ // src/global-install.ts
204
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, existsSync as existsSync3, mkdirSync, rmSync, rmdirSync, readdirSync as readdirSync2, statSync as statSync3, chmodSync, copyFileSync, renameSync } from "fs";
205
+ import { resolve, join as join4, dirname, basename } from "path";
206
+ import { homedir as homedir4 } from "os";
15
207
  import { createRequire } from "module";
16
208
  import { fileURLToPath } from "url";
17
209
  import { createInterface } from "readline";
18
- import { execFileSync, spawn } from "child_process";
210
+ import { execFileSync as execFileSync2, spawn } from "child_process";
19
211
  function lockFile(file) {
20
- if (!existsSync2(file)) return;
212
+ if (!existsSync3(file)) return;
21
213
  try {
22
214
  if (process.platform === "win32") {
23
215
  try {
24
- execFileSync("icacls", [file, "/deny", "*S-1-1-0:(WD,AD,DC,DE,WDAC,WO)"], { stdio: "ignore" });
216
+ execFileSync2("icacls", [file, "/deny", "*S-1-1-0:(WD,AD,DC,DE,WDAC,WO)"], { stdio: "ignore" });
25
217
  } catch {
26
218
  }
27
219
  try {
28
- execFileSync("icacls", [file, "/grant", "*S-1-3-4:(RX)"], { stdio: "ignore" });
220
+ execFileSync2("icacls", [file, "/grant", "*S-1-3-4:(RX)"], { stdio: "ignore" });
29
221
  } catch {
30
222
  }
31
223
  try {
32
- execFileSync("attrib", ["+R", file], { stdio: "ignore" });
224
+ execFileSync2("attrib", ["+R", file], { stdio: "ignore" });
33
225
  } catch {
34
226
  }
35
227
  } else if (process.platform === "darwin") {
36
228
  try {
37
- execFileSync("chflags", ["uchg", file], { stdio: "ignore" });
229
+ execFileSync2("chflags", ["uchg", file], { stdio: "ignore" });
38
230
  } catch {
39
231
  }
40
232
  } else {
41
233
  try {
42
- execFileSync("chattr", ["+i", file], { stdio: "ignore" });
234
+ execFileSync2("chattr", ["+i", file], { stdio: "ignore" });
43
235
  } catch {
44
236
  }
45
237
  try {
@@ -51,33 +243,33 @@ function lockFile(file) {
51
243
  }
52
244
  }
53
245
  function unlockFile(file) {
54
- if (!existsSync2(file)) return;
246
+ if (!existsSync3(file)) return;
55
247
  try {
56
248
  if (process.platform === "win32") {
57
249
  try {
58
- execFileSync("icacls", [file, "/remove:g", "*S-1-3-4"], { stdio: "ignore" });
250
+ execFileSync2("icacls", [file, "/remove:g", "*S-1-3-4"], { stdio: "ignore" });
59
251
  } catch {
60
252
  }
61
253
  try {
62
- execFileSync("icacls", [file, "/remove:d", "*S-1-1-0"], { stdio: "ignore" });
254
+ execFileSync2("icacls", [file, "/remove:d", "*S-1-1-0"], { stdio: "ignore" });
63
255
  } catch {
64
256
  }
65
257
  try {
66
- execFileSync("icacls", [file, "/reset"], { stdio: "ignore" });
258
+ execFileSync2("icacls", [file, "/reset"], { stdio: "ignore" });
67
259
  } catch {
68
260
  }
69
261
  try {
70
- execFileSync("attrib", ["-R", file], { stdio: "ignore" });
262
+ execFileSync2("attrib", ["-R", file], { stdio: "ignore" });
71
263
  } catch {
72
264
  }
73
265
  } else if (process.platform === "darwin") {
74
266
  try {
75
- execFileSync("chflags", ["nouchg", file], { stdio: "ignore" });
267
+ execFileSync2("chflags", ["nouchg", file], { stdio: "ignore" });
76
268
  } catch {
77
269
  }
78
270
  } else {
79
271
  try {
80
- execFileSync("chattr", ["-i", file], { stdio: "ignore" });
272
+ execFileSync2("chattr", ["-i", file], { stdio: "ignore" });
81
273
  } catch {
82
274
  }
83
275
  try {
@@ -91,14 +283,19 @@ function unlockFile(file) {
91
283
  function protectedTargets() {
92
284
  const p = globalPaths();
93
285
  return [
94
- join3(p.hooksDir, "guard.mjs"),
95
- join3(p.hooksDir, "audit.mjs"),
96
- join3(p.hooksDir, "stop.mjs"),
97
- join3(p.hooksDir, "shield.mjs"),
286
+ join4(p.hooksDir, "guard.mjs"),
287
+ join4(p.hooksDir, "audit.mjs"),
288
+ join4(p.hooksDir, "stop.mjs"),
289
+ join4(p.hooksDir, "shield.mjs"),
98
290
  // The conversation record is locked with the rest. A guest who could edit
99
291
  // it could decide what their host sees them say, which is the same class of
100
292
  // problem as editing the guard.
101
- join3(p.hooksDir, "conversation.mjs"),
293
+ join4(p.hooksDir, "conversation.mjs"),
294
+ // The launcher is the enforcement path now: every hook command in every
295
+ // client config runs THROUGH it. A program that could rewrite it could
296
+ // point every hook at /bin/true and disarm the guard without touching a
297
+ // single file that used to be locked.
298
+ join4(p.hooksDir, LAUNCHER_NAME),
102
299
  p.configPath,
103
300
  p.settingsPath,
104
301
  p.antigravityHooksPath,
@@ -132,52 +329,52 @@ function writeProtectedFile(file, contents) {
132
329
  }
133
330
  }
134
331
  function globalPaths() {
135
- const home = homedir3();
136
- const sgDir = join3(home, ".solongate");
137
- const hooksDir = join3(sgDir, "hooks");
138
- const claudeDir = join3(home, ".claude");
139
- const antigravityDir = join3(home, ".gemini", "config");
140
- const codexDir = process.env["CODEX_HOME"] ? resolve(process.env["CODEX_HOME"]) : join3(home, ".codex");
141
- const opencodeDir = join3(process.env["XDG_CONFIG_HOME"] ? resolve(process.env["XDG_CONFIG_HOME"]) : join3(home, ".config"), "opencode");
142
- const binDir = join3(sgDir, "bin");
332
+ const home = homedir4();
333
+ const sgDir2 = join4(home, ".solongate");
334
+ const hooksDir2 = join4(sgDir2, "hooks");
335
+ const claudeDir = join4(home, ".claude");
336
+ const antigravityDir = join4(home, ".gemini", "config");
337
+ const codexDir = process.env["CODEX_HOME"] ? resolve(process.env["CODEX_HOME"]) : join4(home, ".codex");
338
+ const opencodeDir = join4(process.env["XDG_CONFIG_HOME"] ? resolve(process.env["XDG_CONFIG_HOME"]) : join4(home, ".config"), "opencode");
339
+ const binDir = join4(sgDir2, "bin");
143
340
  return {
144
341
  home,
145
- sgDir,
146
- hooksDir,
342
+ sgDir: sgDir2,
343
+ hooksDir: hooksDir2,
147
344
  binDir,
148
345
  claudeDir,
149
346
  antigravityDir,
150
347
  codexDir,
151
348
  opencodeDir,
152
- settingsPath: join3(claudeDir, "settings.json"),
153
- backupPath: join3(claudeDir, "settings.solongate.bak"),
154
- configPath: join3(sgDir, "cloud-guard.json"),
349
+ settingsPath: join4(claudeDir, "settings.json"),
350
+ backupPath: join4(claudeDir, "settings.solongate.bak"),
351
+ configPath: join4(sgDir2, "cloud-guard.json"),
155
352
  // Antigravity reads global hooks from ~/.gemini/config/hooks.json. Only the
156
353
  // guard is registered there (PreToolUse); Antigravity's ALLOW-path audit is
157
354
  // covered by the passive session-log collector, not a hook.
158
- antigravityHooksPath: join3(antigravityDir, "hooks.json"),
159
- antigravityBackupPath: join3(antigravityDir, "hooks.solongate.bak"),
355
+ antigravityHooksPath: join4(antigravityDir, "hooks.json"),
356
+ antigravityBackupPath: join4(antigravityDir, "hooks.solongate.bak"),
160
357
  // Codex reads user-level hooks from ~/.codex/hooks.json (or a [hooks] table
161
358
  // in ~/.codex/config.toml — we use the JSON file so we never have to rewrite
162
359
  // the user's TOML, which also holds the hook TRUST state Codex manages).
163
- codexHooksPath: join3(codexDir, "hooks.json"),
164
- codexBackupPath: join3(codexDir, "hooks.solongate.bak"),
165
- codexConfigPath: join3(codexDir, "config.toml"),
360
+ codexHooksPath: join4(codexDir, "hooks.json"),
361
+ codexBackupPath: join4(codexDir, "hooks.solongate.bak"),
362
+ codexConfigPath: join4(codexDir, "config.toml"),
166
363
  // OpenCode scans its plugin folder at startup and loads every module in it.
167
364
  // Measured on 1.18.10: BOTH `plugin/` and `plugins/` are scanned, so the
168
365
  // docs and the field reports are each half right. We write the documented
169
366
  // one. There is nothing to register anywhere — dropping the file IS the
170
367
  // installation, which also means deleting the file IS the uninstall.
171
- opencodePluginDir: join3(opencodeDir, "plugins"),
172
- opencodePluginPath: join3(opencodeDir, "plugins", "solongate.js")
368
+ opencodePluginDir: join4(opencodeDir, "plugins"),
369
+ opencodePluginPath: join4(opencodeDir, "plugins", "solongate.js")
173
370
  };
174
371
  }
175
372
  function readHook(filename) {
176
- return readFileSync3(join3(HOOKS_DIR, filename), "utf-8");
373
+ return readFileSync4(join4(HOOKS_DIR, filename), "utf-8");
177
374
  }
178
375
  function firstAccountCredential() {
179
376
  try {
180
- const raw = JSON.parse(readFileSync3(join3(homedir3(), ".solongate", "accounts.json"), "utf-8"));
377
+ const raw = JSON.parse(readFileSync4(join4(homedir4(), ".solongate", "accounts.json"), "utf-8"));
181
378
  if (Array.isArray(raw)) {
182
379
  const acc = raw.find((a) => a && typeof a.apiKey === "string" && a.apiKey);
183
380
  if (acc) return { apiKey: acc.apiKey, apiUrl: typeof acc.apiUrl === "string" ? acc.apiUrl : void 0 };
@@ -187,8 +384,8 @@ function firstAccountCredential() {
187
384
  return {};
188
385
  }
189
386
  function readGuard() {
190
- const bundled = join3(HOOKS_DIR, "guard.bundled.mjs");
191
- return existsSync2(bundled) ? readFileSync3(bundled, "utf-8") : readHook("guard.mjs");
387
+ const bundled = join4(HOOKS_DIR, "guard.bundled.mjs");
388
+ return existsSync3(bundled) ? readFileSync4(bundled, "utf-8") : readHook("guard.mjs");
192
389
  }
193
390
  function installGoBinaries(binDir) {
194
391
  const os_ = process.platform === "win32" ? "win32" : process.platform;
@@ -208,10 +405,10 @@ function installGoBinaries(binDir) {
208
405
  return placed;
209
406
  }
210
407
  for (const name of ["solongate-guard", "solongate"]) {
211
- const from = join3(pkgDir, name + suffix);
212
- const to = join3(binDir, name + suffix);
408
+ const from = join4(pkgDir, name + suffix);
409
+ const to = join4(binDir, name + suffix);
213
410
  try {
214
- if (!existsSync2(from)) continue;
411
+ if (!existsSync3(from)) continue;
215
412
  const tmp = to + ".new";
216
413
  copyFileSync(from, tmp);
217
414
  chmodSync(tmp, 493);
@@ -223,16 +420,14 @@ function installGoBinaries(binDir) {
223
420
  return placed;
224
421
  }
225
422
  function antigravityHookCommand(guardAbs) {
226
- const nodeBin = process.execPath.replace(/\\/g, "/");
227
- const call = process.platform === "win32" ? "& " : "";
228
- return `${call}"${nodeBin}" "${guardAbs.replace(/\\/g, "/")}" antigravity "Antigravity"`;
423
+ return hookCommandFor(dirname(guardAbs), basename(guardAbs), "antigravity", "Antigravity");
229
424
  }
230
425
  function installAntigravityGuard(p, guardAbs) {
231
426
  mkdirSync(p.antigravityDir, { recursive: true });
232
427
  let existing = {};
233
- if (existsSync2(p.antigravityHooksPath)) {
234
- const raw = readFileSync3(p.antigravityHooksPath, "utf-8");
235
- if (!existsSync2(p.antigravityBackupPath)) writeFileSync2(p.antigravityBackupPath, raw);
428
+ if (existsSync3(p.antigravityHooksPath)) {
429
+ const raw = readFileSync4(p.antigravityHooksPath, "utf-8");
430
+ if (!existsSync3(p.antigravityBackupPath)) writeFileSync2(p.antigravityBackupPath, raw);
236
431
  try {
237
432
  existing = JSON.parse(raw);
238
433
  } catch {
@@ -248,9 +443,9 @@ function installAntigravityGuard(p, guardAbs) {
248
443
  writeFileSync2(p.antigravityHooksPath, JSON.stringify(merged, null, 2) + "\n");
249
444
  }
250
445
  function removeAntigravityGuard(p) {
251
- if (!existsSync2(p.antigravityHooksPath)) return;
446
+ if (!existsSync3(p.antigravityHooksPath)) return;
252
447
  try {
253
- const s = JSON.parse(readFileSync3(p.antigravityHooksPath, "utf-8"));
448
+ const s = JSON.parse(readFileSync4(p.antigravityHooksPath, "utf-8"));
254
449
  if (!(ANTIGRAVITY_GROUP in s)) return;
255
450
  delete s[ANTIGRAVITY_GROUP];
256
451
  writeFileSync2(p.antigravityHooksPath, JSON.stringify(s, null, 2) + "\n");
@@ -258,8 +453,10 @@ function removeAntigravityGuard(p) {
258
453
  }
259
454
  }
260
455
  function codexHookCommand(scriptAbs) {
261
- const nodeBin = process.execPath.replace(/\\/g, "/");
262
- return `"${nodeBin}" "${scriptAbs.replace(/\\/g, "/")}" codex "Codex"`;
456
+ if (process.platform === "win32") {
457
+ return `"${process.execPath.replace(/\\/g, "/")}" "${scriptAbs.replace(/\\/g, "/")}" codex "Codex"`;
458
+ }
459
+ return hookCommandFor(dirname(scriptAbs), basename(scriptAbs), "codex", "Codex");
263
460
  }
264
461
  function isOurCodexGroup(group) {
265
462
  const hooks = Array.isArray(group?.hooks) ? group.hooks : [];
@@ -267,10 +464,10 @@ function isOurCodexGroup(group) {
267
464
  }
268
465
  function readCodexHooksFile(path) {
269
466
  const out = { hooks: {}, rest: {} };
270
- if (!existsSync2(path)) return out;
467
+ if (!existsSync3(path)) return out;
271
468
  let raw;
272
469
  try {
273
- raw = JSON.parse(readFileSync3(path, "utf-8"));
470
+ raw = JSON.parse(readFileSync4(path, "utf-8"));
274
471
  } catch {
275
472
  return out;
276
473
  }
@@ -315,10 +512,10 @@ function writeCodexHooksFile(path, file) {
315
512
  body["hooks"] = hooks;
316
513
  writeFileSync2(path, JSON.stringify(body, null, 2) + "\n");
317
514
  }
318
- function installCodexGuard(p, hooksDir) {
515
+ function installCodexGuard(p, hooksDir2) {
319
516
  mkdirSync(p.codexDir, { recursive: true });
320
- if (existsSync2(p.codexHooksPath) && !existsSync2(p.codexBackupPath)) {
321
- writeFileSync2(p.codexBackupPath, readFileSync3(p.codexHooksPath, "utf-8"));
517
+ if (existsSync3(p.codexHooksPath) && !existsSync3(p.codexBackupPath)) {
518
+ writeFileSync2(p.codexBackupPath, readFileSync4(p.codexHooksPath, "utf-8"));
322
519
  }
323
520
  const file = readCodexHooksFile(p.codexHooksPath);
324
521
  const script = {
@@ -342,7 +539,7 @@ function installCodexGuard(p, hooksDir) {
342
539
  ...ev === "Stop" ? {} : { matcher: "*" },
343
540
  hooks: [{
344
541
  type: "command",
345
- command: codexHookCommand(join3(hooksDir, script[ev]).replace(/\\/g, "/")),
542
+ command: codexHookCommand(join4(hooksDir2, script[ev]).replace(/\\/g, "/")),
346
543
  timeout: CODEX_TIMEOUT_SEC,
347
544
  statusMessage: status[ev]
348
545
  }]
@@ -352,7 +549,7 @@ function installCodexGuard(p, hooksDir) {
352
549
  writeCodexHooksFile(p.codexHooksPath, file);
353
550
  }
354
551
  function removeCodexGuard(p) {
355
- if (!existsSync2(p.codexHooksPath)) return;
552
+ if (!existsSync3(p.codexHooksPath)) return;
356
553
  try {
357
554
  const file = readCodexHooksFile(p.codexHooksPath);
358
555
  let changed = false;
@@ -378,7 +575,7 @@ function isCodexGuardInstalled() {
378
575
  }
379
576
  function codexDetected() {
380
577
  try {
381
- return existsSync2(globalPaths().codexDir);
578
+ return existsSync3(globalPaths().codexDir);
382
579
  } catch {
383
580
  return false;
384
581
  }
@@ -388,7 +585,7 @@ function codexHooksStatus() {
388
585
  const registered = isCodexGuardInstalled();
389
586
  let trusted = false, disabled = false;
390
587
  try {
391
- const toml = readFileSync3(p.codexConfigPath, "utf-8");
588
+ const toml = readFileSync4(p.codexConfigPath, "utf-8");
392
589
  trusted = /trusted_hash\s*=/.test(toml);
393
590
  disabled = /^\s*hooks\s*=\s*false\s*$/m.test(toml);
394
591
  } catch {
@@ -407,10 +604,10 @@ function removeOpencodeGuard(p) {
407
604
  } catch {
408
605
  }
409
606
  }
410
- function warmPolicyCache(hooksDir, agents) {
607
+ function warmPolicyCache(hooksDir2, agents) {
411
608
  for (const agent of agents) {
412
609
  try {
413
- const child = spawn(process.execPath, [join3(hooksDir, "guard.mjs"), agent, "--sg-refresh-policy"], {
610
+ const child = spawn(process.execPath, [join4(hooksDir2, "guard.mjs"), agent, "--sg-refresh-policy"], {
414
611
  detached: true,
415
612
  stdio: "ignore",
416
613
  windowsHide: true
@@ -424,23 +621,23 @@ function warmPolicyCache(hooksDir, agents) {
424
621
  }
425
622
  function isOpencodeGuardInstalled() {
426
623
  try {
427
- return readFileSync3(globalPaths().opencodePluginPath, "utf-8").includes("tool.execute.before");
624
+ return readFileSync4(globalPaths().opencodePluginPath, "utf-8").includes("tool.execute.before");
428
625
  } catch {
429
626
  return false;
430
627
  }
431
628
  }
432
629
  function opencodeDetected() {
433
630
  try {
434
- return existsSync2(globalPaths().opencodeDir);
631
+ return existsSync3(globalPaths().opencodeDir);
435
632
  } catch {
436
633
  return false;
437
634
  }
438
635
  }
439
- function sweepStrayScratchDirs(root = homedir3(), maxDepth = 6, budget = 4e4) {
636
+ function sweepStrayScratchDirs(root = homedir4(), maxDepth = 6, budget = 4e4) {
440
637
  let removed = 0;
441
638
  let visited = 0;
442
639
  const skip = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".next", "build"]);
443
- const store = join3(homedir3(), ".solongate");
640
+ const store = join4(homedir4(), ".solongate");
444
641
  const walk = (dir, depth) => {
445
642
  if (depth > maxDepth || visited++ > budget) return;
446
643
  let entries;
@@ -451,11 +648,11 @@ function sweepStrayScratchDirs(root = homedir3(), maxDepth = 6, budget = 4e4) {
451
648
  }
452
649
  for (const name of entries) {
453
650
  if (skip.has(name)) continue;
454
- const full = join3(dir, name);
651
+ const full = join4(dir, name);
455
652
  if (full === store) continue;
456
653
  let isDir = false;
457
654
  try {
458
- isDir = statSync2(full).isDirectory();
655
+ isDir = statSync3(full).isDirectory();
459
656
  } catch {
460
657
  continue;
461
658
  }
@@ -466,7 +663,7 @@ function sweepStrayScratchDirs(root = homedir3(), maxDepth = 6, budget = 4e4) {
466
663
  for (const f of readdirSync2(full)) {
467
664
  if (SCRATCH_FILES.has(f)) {
468
665
  try {
469
- rmSync(join3(full, f), { force: true });
666
+ rmSync(join4(full, f), { force: true });
470
667
  } catch {
471
668
  left.push(f);
472
669
  }
@@ -488,12 +685,17 @@ function sweepStrayScratchDirs(root = homedir3(), maxDepth = 6, budget = 4e4) {
488
685
  }
489
686
  function repairQuiet() {
490
687
  const p = globalPaths();
491
- const has = (f) => existsSync2(f);
492
- const guardFile = join3(p.hooksDir, "guard.mjs");
688
+ const has = (f) => existsSync3(f);
689
+ const guardFile = join4(p.hooksDir, "guard.mjs");
493
690
  const line = (label, ok, yes, no) => ({ label, ok, detail: ok ? yes : no });
691
+ const runtime = () => {
692
+ const r2 = hookCanStart();
693
+ return { label: "hook runtime", ok: r2.ok, detail: r2.ok ? `node ${r2.detail}` : r2.detail };
694
+ };
494
695
  const before = [
495
696
  line("guard hook file", has(guardFile), "present", "MISSING"),
496
697
  line("cloud credential", has(p.configPath), "present", "MISSING"),
698
+ runtime(),
497
699
  line("Claude hooks", isGuardInstalled(), "guard registered", "guard NOT registered"),
498
700
  line("Antigravity hooks", has(p.antigravityHooksPath), "guard registered", "guard NOT registered"),
499
701
  line("Codex hooks", isCodexGuardInstalled(), "guard registered", "guard NOT registered"),
@@ -503,6 +705,7 @@ function repairQuiet() {
503
705
  if (!r.ok) return { ok: false, message: r.message, before, after: [], notes: [] };
504
706
  const after = [
505
707
  { label: "guard hook file", ok: true, detail: `present (v${installedGuardVersion() ?? "?"})` },
708
+ runtime(),
506
709
  line("Claude hooks", isGuardInstalled(), "guard registered", "NOT registered"),
507
710
  line("Antigravity hooks", has(p.antigravityHooksPath), "guard registered", "NOT registered"),
508
711
  line("Codex hooks", isCodexGuardInstalled(), "guard registered", "NOT registered"),
@@ -529,7 +732,7 @@ function installGlobalQuiet() {
529
732
  let apiKey = process.env["SOLONGATE_API_KEY"] || "";
530
733
  let apiUrl = process.env["SOLONGATE_API_URL"] || "https://api.solongate.com";
531
734
  try {
532
- const cfg = JSON.parse(readFileSync3(p.configPath, "utf-8"));
735
+ const cfg = JSON.parse(readFileSync4(p.configPath, "utf-8"));
533
736
  if (cfg && typeof cfg.apiKey === "string") apiKey = apiKey || cfg.apiKey;
534
737
  if (cfg && typeof cfg.apiUrl === "string") apiUrl = cfg.apiUrl;
535
738
  } catch {
@@ -545,26 +748,26 @@ function installGlobalQuiet() {
545
748
  mkdirSync(p.hooksDir, { recursive: true });
546
749
  mkdirSync(p.claudeDir, { recursive: true });
547
750
  unlockProtected();
548
- writeFileSync2(join3(p.hooksDir, "guard.mjs"), readGuard());
751
+ writeFileSync2(join4(p.hooksDir, "guard.mjs"), readGuard());
549
752
  installGoBinaries(p.binDir);
550
- writeFileSync2(join3(p.hooksDir, "audit.mjs"), readHook("audit.mjs"));
551
- writeFileSync2(join3(p.hooksDir, "stop.mjs"), readHook("stop.mjs"));
552
- writeFileSync2(join3(p.hooksDir, "shield.mjs"), readHook("shield.mjs"));
553
- writeFileSync2(join3(p.hooksDir, "conversation.mjs"), readHook("conversation.mjs"));
753
+ writeFileSync2(join4(p.hooksDir, "audit.mjs"), readHook("audit.mjs"));
754
+ writeFileSync2(join4(p.hooksDir, "stop.mjs"), readHook("stop.mjs"));
755
+ writeFileSync2(join4(p.hooksDir, "shield.mjs"), readHook("shield.mjs"));
756
+ writeFileSync2(join4(p.hooksDir, "conversation.mjs"), readHook("conversation.mjs"));
757
+ writeLauncher(p.hooksDir);
758
+ writeLauncher(p.hooksDir);
554
759
  writeFileSync2(p.configPath, JSON.stringify({ apiKey, apiUrl }, null, 2) + "\n");
555
760
  let existing = {};
556
- if (existsSync2(p.settingsPath)) {
557
- const raw = readFileSync3(p.settingsPath, "utf-8");
558
- if (!existsSync2(p.backupPath)) writeFileSync2(p.backupPath, raw);
761
+ if (existsSync3(p.settingsPath)) {
762
+ const raw = readFileSync4(p.settingsPath, "utf-8");
763
+ if (!existsSync3(p.backupPath)) writeFileSync2(p.backupPath, raw);
559
764
  try {
560
765
  existing = JSON.parse(raw);
561
766
  } catch {
562
767
  existing = {};
563
768
  }
564
769
  }
565
- const nodeBin = process.execPath.replace(/\\/g, "/");
566
- const call = process.platform === "win32" ? "& " : "";
567
- const hookCmd = (script) => `${call}"${nodeBin}" "${join3(p.hooksDir, script).replace(/\\/g, "/")}" claude-code "Claude Code"`;
770
+ const hookCmd = (script) => hookCommandFor(p.hooksDir, script);
568
771
  const merged = {
569
772
  ...existing,
570
773
  hooks: {
@@ -591,7 +794,7 @@ function installGlobalQuiet() {
591
794
  };
592
795
  writeFileSync2(p.settingsPath, JSON.stringify(merged, null, 2) + "\n");
593
796
  try {
594
- installAntigravityGuard(p, join3(p.hooksDir, "guard.mjs").replace(/\\/g, "/"));
797
+ installAntigravityGuard(p, join4(p.hooksDir, "guard.mjs").replace(/\\/g, "/"));
595
798
  } catch {
596
799
  }
597
800
  try {
@@ -612,7 +815,7 @@ function installGlobalQuiet() {
612
815
  function installedGuardVersion() {
613
816
  try {
614
817
  const p = globalPaths();
615
- const s = readFileSync3(join3(p.hooksDir, "guard.mjs"), "utf-8");
818
+ const s = readFileSync4(join4(p.hooksDir, "guard.mjs"), "utf-8");
616
819
  const m = s.match(/HOOK_VERSION\s*=\s*(\d+)/);
617
820
  return m ? parseInt(m[1], 10) : null;
618
821
  } catch {
@@ -622,16 +825,35 @@ function installedGuardVersion() {
622
825
  function guardHookOutdated() {
623
826
  try {
624
827
  const p = globalPaths();
625
- return readFileSync3(join3(p.hooksDir, "guard.mjs"), "utf-8") !== readGuard();
828
+ return readFileSync4(join4(p.hooksDir, "guard.mjs"), "utf-8") !== readGuard();
626
829
  } catch {
627
830
  return false;
628
831
  }
629
832
  }
833
+ function hookCommandFor(hooksDir2, script, client = "claude-code", label = "Claude Code") {
834
+ const target = join4(hooksDir2, script).replace(/\\/g, "/");
835
+ if (process.platform === "win32") {
836
+ return `& "${process.execPath.replace(/\\/g, "/")}" "${target}" ${client} "${label}"`;
837
+ }
838
+ const launcher = join4(hooksDir2, LAUNCHER_NAME).replace(/\\/g, "/");
839
+ return `/bin/sh "${launcher}" "${target}" ${client} "${label}"`;
840
+ }
841
+ function writeLauncher(hooksDir2) {
842
+ writeFileSync2(join4(hooksDir2, LAUNCHER_NAME), launcherScript(process.execPath));
843
+ try {
844
+ chmodSync(join4(hooksDir2, LAUNCHER_NAME), 493);
845
+ } catch {
846
+ }
847
+ try {
848
+ mkdirSync(join4(hooksDir2, "..", BEAT_DIR), { recursive: true });
849
+ } catch {
850
+ }
851
+ }
630
852
  function isGuardInstalled() {
631
853
  try {
632
854
  const p = globalPaths();
633
- if (!existsSync2(p.settingsPath)) return false;
634
- const s = JSON.parse(readFileSync3(p.settingsPath, "utf-8"));
855
+ if (!existsSync3(p.settingsPath)) return false;
856
+ const s = JSON.parse(readFileSync4(p.settingsPath, "utf-8"));
635
857
  return !!s.hooks && JSON.stringify(s.hooks).includes(".solongate");
636
858
  } catch {
637
859
  return false;
@@ -654,8 +876,8 @@ function uninstallGlobalQuiet() {
654
876
  removeOpencodeGuard(p);
655
877
  } catch {
656
878
  }
657
- if (!existsSync2(p.settingsPath)) return { ok: true, message: "guard removed (open a new session)" };
658
- const s = JSON.parse(readFileSync3(p.settingsPath, "utf-8"));
879
+ if (!existsSync3(p.settingsPath)) return { ok: true, message: "guard removed (open a new session)" };
880
+ const s = JSON.parse(readFileSync4(p.settingsPath, "utf-8"));
659
881
  delete s.hooks;
660
882
  writeFileSync2(p.settingsPath, JSON.stringify(s, null, 2) + "\n");
661
883
  return { ok: true, message: "guard removed (open a new session)" };
@@ -669,17 +891,17 @@ function escapeRe(s) {
669
891
  function shimTargets() {
670
892
  if (process.platform === "win32") {
671
893
  try {
672
- const prof = execFileSync("powershell", ["-NoProfile", "-Command", "$PROFILE.CurrentUserAllHosts"], { encoding: "utf-8" }).trim();
894
+ const prof = execFileSync2("powershell", ["-NoProfile", "-Command", "$PROFILE.CurrentUserAllHosts"], { encoding: "utf-8" }).trim();
673
895
  return prof ? [prof] : [];
674
896
  } catch {
675
897
  return [];
676
898
  }
677
899
  }
678
- return [".bashrc", ".zshrc", ".profile"].map((f) => join3(homedir3(), f)).filter((f) => existsSync2(f));
900
+ return [".bashrc", ".zshrc", ".profile"].map((f) => join4(homedir4(), f)).filter((f) => existsSync3(f));
679
901
  }
680
902
  function writeShimBlock(file, block2) {
681
903
  const re = new RegExp(escapeRe(SHIM_BEGIN) + "[\\s\\S]*?" + escapeRe(SHIM_END) + "\\r?\\n?", "g");
682
- let content = existsSync2(file) ? readFileSync3(file, "utf-8") : "";
904
+ let content = existsSync3(file) ? readFileSync4(file, "utf-8") : "";
683
905
  content = content.replace(re, "");
684
906
  if (block2) {
685
907
  if (content.length && !content.endsWith("\n")) content += "\n";
@@ -700,6 +922,10 @@ var __dirname, HOOKS_DIR, ANTIGRAVITY_GROUP, CODEX_EVENTS, CODEX_TIMEOUT_SEC, SC
700
922
  var init_global_install = __esm({
701
923
  "src/global-install.ts"() {
702
924
  "use strict";
925
+ init_hook_launcher();
926
+ init_hook_launcher();
927
+ init_hook_health();
928
+ init_hook_health();
703
929
  __dirname = dirname(fileURLToPath(import.meta.url));
704
930
  HOOKS_DIR = resolve(__dirname, "..", "hooks");
705
931
  ANTIGRAVITY_GROUP = "solongate-guard";
@@ -713,8 +939,8 @@ var init_global_install = __esm({
713
939
 
714
940
  // src/tui/index.tsx
715
941
  import { appendFileSync, mkdirSync as mkdirSync7 } from "fs";
716
- import { homedir as homedir10 } from "os";
717
- import { join as join10 } from "path";
942
+ import { homedir as homedir11 } from "os";
943
+ import { join as join11 } from "path";
718
944
  import { render } from "ink";
719
945
 
720
946
  // src/tui/App.tsx
@@ -915,8 +1141,8 @@ function KeyHints({ hints }) {
915
1141
  import { Box as Box2, Text as Text2, useInput } from "ink";
916
1142
  import TextInput from "ink-text-input";
917
1143
  import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
918
- import { homedir as homedir5 } from "os";
919
- import { join as join5, resolve as resolve3 } from "path";
1144
+ import { homedir as homedir6 } from "os";
1145
+ import { join as join6, resolve as resolve3 } from "path";
920
1146
 
921
1147
  // src/tui/local-log.ts
922
1148
  import { closeSync, existsSync, openSync, readdirSync, readFileSync as readFileSync2, readSync, statSync, writeFileSync } from "fs";
@@ -948,9 +1174,9 @@ function localLogsSetting() {
948
1174
  return off;
949
1175
  }
950
1176
  function policyCachesNewestFirst() {
951
- const sgDir = join2(homedir2(), ".solongate");
952
- return readdirSync(sgDir).filter((f) => f.startsWith(".policy-cache-") && f.endsWith(".json")).map((f) => {
953
- const p = join2(sgDir, f);
1177
+ const sgDir2 = join2(homedir2(), ".solongate");
1178
+ return readdirSync(sgDir2).filter((f) => f.startsWith(".policy-cache-") && f.endsWith(".json")).map((f) => {
1179
+ const p = join2(sgDir2, f);
954
1180
  let mtime = 0;
955
1181
  try {
956
1182
  mtime = statSync(p).mtimeMs;
@@ -961,9 +1187,9 @@ function policyCachesNewestFirst() {
961
1187
  }
962
1188
  function localLogFile() {
963
1189
  try {
964
- const sgDir = join2(homedir2(), ".solongate");
965
- const caches = readdirSync(sgDir).filter((f) => f.startsWith(".policy-cache-") && f.endsWith(".json")).map((f) => {
966
- const p = join2(sgDir, f);
1190
+ const sgDir2 = join2(homedir2(), ".solongate");
1191
+ const caches = readdirSync(sgDir2).filter((f) => f.startsWith(".policy-cache-") && f.endsWith(".json")).map((f) => {
1192
+ const p = join2(sgDir2, f);
967
1193
  let mtime = 0;
968
1194
  try {
969
1195
  mtime = statSync(p).mtimeMs;
@@ -1074,15 +1300,15 @@ import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2
1074
1300
 
1075
1301
  // src/api-client/client.ts
1076
1302
  init_global_install();
1077
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "fs";
1078
- import { resolve as resolve2, join as join4 } from "path";
1079
- import { homedir as homedir4 } from "os";
1303
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync4 } from "fs";
1304
+ import { resolve as resolve2, join as join5 } from "path";
1305
+ import { homedir as homedir5 } from "os";
1080
1306
  var DEFAULT_API_URL = "https://api.solongate.com";
1081
- var accountsFile = () => join4(homedir4(), ".solongate", "accounts.json");
1307
+ var accountsFile = () => join5(homedir5(), ".solongate", "accounts.json");
1082
1308
  function listAccounts() {
1083
1309
  let list5 = [];
1084
1310
  try {
1085
- const raw = JSON.parse(readFileSync4(accountsFile(), "utf-8"));
1311
+ const raw = JSON.parse(readFileSync5(accountsFile(), "utf-8"));
1086
1312
  if (Array.isArray(raw)) list5 = raw.filter((a) => a && typeof a.apiKey === "string");
1087
1313
  } catch {
1088
1314
  }
@@ -1096,7 +1322,7 @@ function saveAccount(acc) {
1096
1322
  try {
1097
1323
  const list5 = (() => {
1098
1324
  try {
1099
- const raw = JSON.parse(readFileSync4(accountsFile(), "utf-8"));
1325
+ const raw = JSON.parse(readFileSync5(accountsFile(), "utf-8"));
1100
1326
  return Array.isArray(raw) ? raw.filter((a) => a && a.apiKey) : [];
1101
1327
  } catch {
1102
1328
  return [];
@@ -1104,7 +1330,7 @@ function saveAccount(acc) {
1104
1330
  })();
1105
1331
  const next = list5.filter((a) => a.apiKey !== acc.apiKey);
1106
1332
  next.unshift({ ...acc, addedAt: acc.addedAt ?? Date.now() });
1107
- mkdirSync2(join4(homedir4(), ".solongate"), { recursive: true });
1333
+ mkdirSync2(join5(homedir5(), ".solongate"), { recursive: true });
1108
1334
  writeFileSync3(accountsFile(), JSON.stringify(next, null, 2));
1109
1335
  } catch {
1110
1336
  }
@@ -1113,7 +1339,7 @@ function removeAccount(apiKey) {
1113
1339
  try {
1114
1340
  const list5 = (() => {
1115
1341
  try {
1116
- const raw = JSON.parse(readFileSync4(accountsFile(), "utf-8"));
1342
+ const raw = JSON.parse(readFileSync5(accountsFile(), "utf-8"));
1117
1343
  return Array.isArray(raw) ? raw.filter((a) => a && a.apiKey) : [];
1118
1344
  } catch {
1119
1345
  return [];
@@ -1136,12 +1362,12 @@ function isActiveAccount(apiKey) {
1136
1362
  }
1137
1363
  function setActiveAccount(creds) {
1138
1364
  try {
1139
- const dir = join4(homedir4(), ".solongate");
1365
+ const dir = join5(homedir5(), ".solongate");
1140
1366
  mkdirSync2(dir, { recursive: true });
1141
- const p = join4(dir, ["cloud", "guard.json"].join("-"));
1367
+ const p = join5(dir, ["cloud", "guard.json"].join("-"));
1142
1368
  let existing = {};
1143
1369
  try {
1144
- existing = JSON.parse(readFileSync4(p, "utf-8"));
1370
+ existing = JSON.parse(readFileSync5(p, "utf-8"));
1145
1371
  } catch {
1146
1372
  }
1147
1373
  if (!writeProtectedFile(p, JSON.stringify({ ...existing, apiKey: creds.apiKey, apiUrl: creds.apiUrl }, null, 2))) return false;
@@ -1153,14 +1379,14 @@ function setActiveAccount(creds) {
1153
1379
  }
1154
1380
  function clearActiveCredential() {
1155
1381
  try {
1156
- const p = join4(homedir4(), ".solongate", ["cloud", "guard.json"].join("-"));
1157
- if (!existsSync3(p)) {
1382
+ const p = join5(homedir5(), ".solongate", ["cloud", "guard.json"].join("-"));
1383
+ if (!existsSync4(p)) {
1158
1384
  cached2 = null;
1159
1385
  return true;
1160
1386
  }
1161
1387
  let existing = {};
1162
1388
  try {
1163
- existing = JSON.parse(readFileSync4(p, "utf-8"));
1389
+ existing = JSON.parse(readFileSync5(p, "utf-8"));
1164
1390
  } catch {
1165
1391
  }
1166
1392
  delete existing.apiKey;
@@ -1191,9 +1417,9 @@ var NotAuthenticatedError = class extends Error {
1191
1417
  };
1192
1418
  function loginCredentialFile() {
1193
1419
  try {
1194
- const p = join4(homedir4(), ".solongate", "cloud-guard.json");
1195
- if (!existsSync3(p)) return {};
1196
- const c2 = JSON.parse(readFileSync4(p, "utf-8"));
1420
+ const p = join5(homedir5(), ".solongate", "cloud-guard.json");
1421
+ if (!existsSync4(p)) return {};
1422
+ const c2 = JSON.parse(readFileSync5(p, "utf-8"));
1197
1423
  return c2 && typeof c2 === "object" ? c2 : {};
1198
1424
  } catch {
1199
1425
  return {};
@@ -1202,8 +1428,8 @@ function loginCredentialFile() {
1202
1428
  function dotenvApiKey() {
1203
1429
  try {
1204
1430
  const envPath = resolve2(".env");
1205
- if (!existsSync3(envPath)) return void 0;
1206
- for (const line of readFileSync4(envPath, "utf-8").split("\n")) {
1431
+ if (!existsSync4(envPath)) return void 0;
1432
+ for (const line of readFileSync5(envPath, "utf-8").split("\n")) {
1207
1433
  const trimmed = line.trim();
1208
1434
  if (!trimmed || trimmed.startsWith("#")) continue;
1209
1435
  const eq = trimmed.indexOf("=");
@@ -1687,7 +1913,7 @@ var projectKey = (dir) => {
1687
1913
  }
1688
1914
  return h.toString(16);
1689
1915
  };
1690
- var RING = join5(homedir5(), ".solongate", "projects", projectKey(resolve3(process.cwd())), ".eval-ring.jsonl");
1916
+ var RING = join6(homedir6(), ".solongate", "projects", projectKey(resolve3(process.cwd())), ".eval-ring.jsonl");
1691
1917
  var fmtUp = (ms) => {
1692
1918
  const s = Math.floor(ms / 1e3);
1693
1919
  const p = (n) => String(n).padStart(2, "0");
@@ -2294,9 +2520,9 @@ function LivePanel({ active: active2 }) {
2294
2520
  else if (input === "x") toggleSignal("dlp");
2295
2521
  else if (input === "r") toggleSignal("ratelimit");
2296
2522
  else if (input === "e") {
2297
- const file = join5(homedir5(), ".solongate", "live-export.jsonl");
2523
+ const file = join6(homedir6(), ".solongate", "live-export.jsonl");
2298
2524
  try {
2299
- mkdirSync3(join5(homedir5(), ".solongate"), { recursive: true });
2525
+ mkdirSync3(join6(homedir6(), ".solongate"), { recursive: true });
2300
2526
  writeFileSync4(file, visibleDesc.map((x) => JSON.stringify(x)).join("\n") + "\n");
2301
2527
  setActionMsg({ text: `\u2713 exported ${visibleDesc.length} lines \u2192 ${file}`, level: "ok", until: Date.now() + 6e3 });
2302
2528
  } catch (err2) {
@@ -3896,8 +4122,8 @@ function DlpPanel({ focused }) {
3896
4122
  import { Box as Box7, Text as Text7, useInput as useInput6 } from "ink";
3897
4123
  import TextInput5 from "ink-text-input";
3898
4124
  import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "fs";
3899
- import { homedir as homedir6 } from "os";
3900
- import { join as join6 } from "path";
4125
+ import { homedir as homedir7 } from "os";
4126
+ import { join as join7 } from "path";
3901
4127
  import { useState as useState7 } from "react";
3902
4128
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
3903
4129
  var DECISIONS = [void 0, "DENY", "ALLOW"];
@@ -4104,8 +4330,8 @@ function AuditPanel({ active: active2, focused }) {
4104
4330
  const doExport = (kind) => {
4105
4331
  setMsg({ text: "exporting\u2026", level: "ok" });
4106
4332
  const run = async () => {
4107
- const dir = join6(homedir6(), ".solongate");
4108
- const file = join6(dir, `audit-export-${source}.jsonl`);
4333
+ const dir = join7(homedir7(), ".solongate");
4334
+ const file = join7(dir, `audit-export-${source}.jsonl`);
4109
4335
  let rows2;
4110
4336
  if (kind === "page") rows2 = pageRows;
4111
4337
  else if (source === "cloud") {
@@ -4479,10 +4705,11 @@ import { useEffect as useEffect7, useRef as useRef3, useState as useState8 } fro
4479
4705
  init_global_install();
4480
4706
 
4481
4707
  // src/commands/doctor.ts
4482
- import { existsSync as existsSync4, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
4483
- import { homedir as homedir7 } from "os";
4484
- import { join as join7 } from "path";
4708
+ import { existsSync as existsSync5, readFileSync as readFileSync6, statSync as statSync4 } from "fs";
4709
+ import { homedir as homedir8 } from "os";
4710
+ import { join as join8 } from "path";
4485
4711
  init_global_install();
4712
+ init_hook_health();
4486
4713
  async function collectChecks() {
4487
4714
  const checks = [];
4488
4715
  if (!isAuthenticated()) {
@@ -4530,8 +4757,32 @@ async function collectChecks() {
4530
4757
  ok: claudeReg,
4531
4758
  detail: claudeReg ? "guard registered" : "guard NOT registered - run `solongate repair`"
4532
4759
  });
4533
- if (existsSync4(globalPaths().antigravityDir)) {
4534
- const reg = existsSync4(globalPaths().antigravityHooksPath);
4760
+ if (claudeReg) {
4761
+ const start = hookCanStart();
4762
+ checks.push({
4763
+ name: "hook runtime",
4764
+ ok: start.ok,
4765
+ detail: start.ok ? `node ${start.detail}` : `${start.detail} - nothing is being enforced or logged`
4766
+ });
4767
+ const beat = guardBeat();
4768
+ if (!beat) {
4769
+ checks.push({
4770
+ name: "guard fired",
4771
+ ok: "warn",
4772
+ detail: "never - open your agent and run one tool call, then check again"
4773
+ });
4774
+ } else if (beat.node === "no-node") {
4775
+ checks.push({
4776
+ name: "guard fired",
4777
+ ok: false,
4778
+ detail: `${agoLabel(beat.at)}, but found no node to run with - run \`solongate repair\``
4779
+ });
4780
+ } else {
4781
+ checks.push({ name: "guard fired", ok: true, detail: `${agoLabel(beat.at)} \xB7 ${beat.node}` });
4782
+ }
4783
+ }
4784
+ if (existsSync5(globalPaths().antigravityDir)) {
4785
+ const reg = existsSync5(globalPaths().antigravityHooksPath);
4535
4786
  checks.push({
4536
4787
  name: "Antigravity hooks",
4537
4788
  ok: reg,
@@ -4559,7 +4810,7 @@ async function collectChecks() {
4559
4810
  });
4560
4811
  }
4561
4812
  try {
4562
- const raw = readFileSync5(join7(homedir7(), ".solongate", ".key-rejected.json"), "utf-8");
4813
+ const raw = readFileSync6(join8(homedir8(), ".solongate", ".key-rejected.json"), "utf-8");
4563
4814
  const m = JSON.parse(raw);
4564
4815
  const ageMin = m.ts ? Math.round((Date.now() - m.ts) / 6e4) : null;
4565
4816
  checks.push({
@@ -4570,8 +4821,8 @@ async function collectChecks() {
4570
4821
  } catch {
4571
4822
  }
4572
4823
  const LOCAL_LOG2 = localLogFile();
4573
- if (existsSync4(LOCAL_LOG2)) {
4574
- const st = statSync3(LOCAL_LOG2);
4824
+ if (existsSync5(LOCAL_LOG2)) {
4825
+ const st = statSync4(LOCAL_LOG2);
4575
4826
  const ageMin = (Date.now() - st.mtimeMs) / 6e4;
4576
4827
  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"}` });
4577
4828
  } else {
@@ -4582,17 +4833,17 @@ async function collectChecks() {
4582
4833
 
4583
4834
  // src/logs-server-daemon.ts
4584
4835
  import { spawn as spawn4 } from "child_process";
4585
- import { mkdirSync as mkdirSync5, openSync as openSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
4586
- import { homedir as homedir8 } from "os";
4587
- import { dirname as dirname2, join as join8 } from "path";
4836
+ import { mkdirSync as mkdirSync5, openSync as openSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
4837
+ import { homedir as homedir9 } from "os";
4838
+ import { dirname as dirname2, join as join9 } from "path";
4588
4839
  import { fileURLToPath as fileURLToPath2 } from "url";
4589
- var DIR = join8(homedir8(), ".solongate");
4590
- var STATE_FILE = join8(DIR, ".logs-server.json");
4591
- var LOG_FILE = join8(DIR, "logs-server.log");
4840
+ var DIR = join9(homedir9(), ".solongate");
4841
+ var STATE_FILE = join9(DIR, ".logs-server.json");
4842
+ var LOG_FILE = join9(DIR, "logs-server.log");
4592
4843
  var LOGS_SERVER_PORT = 8788;
4593
4844
  function readState() {
4594
4845
  try {
4595
- const s = JSON.parse(readFileSync6(STATE_FILE, "utf-8"));
4846
+ const s = JSON.parse(readFileSync7(STATE_FILE, "utf-8"));
4596
4847
  return s && typeof s === "object" ? s : {};
4597
4848
  } catch {
4598
4849
  return {};
@@ -4628,7 +4879,7 @@ function startLogsServerDaemon() {
4628
4879
  try {
4629
4880
  mkdirSync5(DIR, { recursive: true });
4630
4881
  const log = openSync2(LOG_FILE, "a");
4631
- const cli = join8(dirname2(fileURLToPath2(import.meta.url)), "index.js");
4882
+ const cli = join9(dirname2(fileURLToPath2(import.meta.url)), "index.js");
4632
4883
  const p = spawn4(process.execPath, [cli, "logs-server"], {
4633
4884
  detached: true,
4634
4885
  stdio: ["ignore", log, log],
@@ -4659,22 +4910,22 @@ function stopLogsServerDaemon() {
4659
4910
  }
4660
4911
 
4661
4912
  // src/self-update.ts
4662
- import { execFile, execFileSync as execFileSync2, spawn as spawn5 } from "child_process";
4663
- import { access, mkdirSync as mkdirSync6, openSync as openSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync7, constants as FS } from "fs";
4664
- import { homedir as homedir9 } from "os";
4665
- import { dirname as dirname3, join as join9, sep } from "path";
4913
+ import { execFile, execFileSync as execFileSync3, spawn as spawn5 } from "child_process";
4914
+ import { access, mkdirSync as mkdirSync6, openSync as openSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync7, constants as FS } from "fs";
4915
+ import { homedir as homedir10 } from "os";
4916
+ import { dirname as dirname3, join as join10, sep } from "path";
4666
4917
  import { fileURLToPath as fileURLToPath3 } from "url";
4667
4918
  var PKG = "@solongate/proxy";
4668
4919
  var CHECK_EVERY_MS = 30 * 60 * 1e3;
4669
4920
  var ATTEMPT_EVERY_MS = 6 * 60 * 60 * 1e3;
4670
- var STATE_FILE2 = join9(homedir9(), ".solongate", ".self-update.json");
4671
- var LOG_FILE2 = join9(homedir9(), ".solongate", "self-update.log");
4672
- var LOCK_FILE = join9(homedir9(), ".solongate", ".update-install.lock");
4921
+ var STATE_FILE2 = join10(homedir10(), ".solongate", ".self-update.json");
4922
+ var LOG_FILE2 = join10(homedir10(), ".solongate", "self-update.log");
4923
+ var LOCK_FILE = join10(homedir10(), ".solongate", ".update-install.lock");
4673
4924
  var LOCK_STALE_MS = 3 * 6e4;
4674
4925
  var NEEDS_ADMIN_RE = /\bEACCES\b|\bEPERM\b|permission denied|operation not permitted/i;
4675
4926
  function readState2() {
4676
4927
  try {
4677
- const s = JSON.parse(readFileSync7(STATE_FILE2, "utf-8"));
4928
+ const s = JSON.parse(readFileSync8(STATE_FILE2, "utf-8"));
4678
4929
  return s && typeof s === "object" ? s : {};
4679
4930
  } catch {
4680
4931
  return {};
@@ -4682,7 +4933,7 @@ function readState2() {
4682
4933
  }
4683
4934
  function writeState2(s) {
4684
4935
  try {
4685
- mkdirSync6(join9(homedir9(), ".solongate"), { recursive: true });
4936
+ mkdirSync6(join10(homedir10(), ".solongate"), { recursive: true });
4686
4937
  writeFileSync7(STATE_FILE2, JSON.stringify(s));
4687
4938
  } catch {
4688
4939
  }
@@ -4703,7 +4954,7 @@ function setAutoUpdate(on) {
4703
4954
  }
4704
4955
  function currentVersion() {
4705
4956
  try {
4706
- const pkg = JSON.parse(readFileSync7(join9(dirname3(fileURLToPath3(import.meta.url)), "..", "package.json"), "utf-8"));
4957
+ const pkg = JSON.parse(readFileSync8(join10(dirname3(fileURLToPath3(import.meta.url)), "..", "package.json"), "utf-8"));
4707
4958
  return pkg.version ?? "0.0.0";
4708
4959
  } catch {
4709
4960
  return "0.0.0";
@@ -4740,7 +4991,7 @@ async function fetchLatest() {
4740
4991
  function runGlobalInstall(version) {
4741
4992
  return new Promise((resolve4) => {
4742
4993
  try {
4743
- mkdirSync6(join9(homedir9(), ".solongate"), { recursive: true });
4994
+ mkdirSync6(join10(homedir10(), ".solongate"), { recursive: true });
4744
4995
  execFile(
4745
4996
  "npm",
4746
4997
  ["install", "-g", `${PKG}@${version}`],
@@ -5904,11 +6155,11 @@ async function launchTui() {
5904
6155
  return;
5905
6156
  }
5906
6157
  process.stdout.write("\x1B[?1049h\x1B[H");
5907
- const debugLog = join10(homedir10(), ".solongate", "dataroom-debug.log");
6158
+ const debugLog = join11(homedir11(), ".solongate", "dataroom-debug.log");
5908
6159
  const saved = { log: console.log, warn: console.warn, error: console.error, info: console.info, debug: console.debug };
5909
6160
  const toFile = (level) => (...args) => {
5910
6161
  try {
5911
- mkdirSync7(join10(homedir10(), ".solongate"), { recursive: true });
6162
+ mkdirSync7(join11(homedir11(), ".solongate"), { recursive: true });
5912
6163
  appendFileSync(debugLog, `${(/* @__PURE__ */ new Date()).toISOString()} [${level}] ${args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ")}
5913
6164
  `);
5914
6165
  } catch {