@solongate/proxy 0.83.56 → 0.83.58

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,17 @@ 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");
424
+ }
425
+ function antigravityConversationCommand(guardAbs) {
426
+ return hookCommandFor(dirname(guardAbs), "conversation.mjs", "antigravity", "Antigravity");
229
427
  }
230
428
  function installAntigravityGuard(p, guardAbs) {
231
429
  mkdirSync(p.antigravityDir, { recursive: true });
232
430
  let existing = {};
233
- if (existsSync2(p.antigravityHooksPath)) {
234
- const raw = readFileSync3(p.antigravityHooksPath, "utf-8");
235
- if (!existsSync2(p.antigravityBackupPath)) writeFileSync2(p.antigravityBackupPath, raw);
431
+ if (existsSync3(p.antigravityHooksPath)) {
432
+ const raw = readFileSync4(p.antigravityHooksPath, "utf-8");
433
+ if (!existsSync3(p.antigravityBackupPath)) writeFileSync2(p.antigravityBackupPath, raw);
236
434
  try {
237
435
  existing = JSON.parse(raw);
238
436
  } catch {
@@ -242,15 +440,16 @@ function installAntigravityGuard(p, guardAbs) {
242
440
  const merged = {
243
441
  ...existing,
244
442
  [ANTIGRAVITY_GROUP]: {
245
- PreToolUse: [{ matcher: "", hooks: [{ type: "command", command: antigravityHookCommand(guardAbs) }] }]
443
+ PreToolUse: [{ matcher: "", hooks: [{ type: "command", command: antigravityHookCommand(guardAbs) }] }],
444
+ Stop: [{ matcher: "", hooks: [{ type: "command", command: antigravityConversationCommand(guardAbs) }] }]
246
445
  }
247
446
  };
248
447
  writeFileSync2(p.antigravityHooksPath, JSON.stringify(merged, null, 2) + "\n");
249
448
  }
250
449
  function removeAntigravityGuard(p) {
251
- if (!existsSync2(p.antigravityHooksPath)) return;
450
+ if (!existsSync3(p.antigravityHooksPath)) return;
252
451
  try {
253
- const s = JSON.parse(readFileSync3(p.antigravityHooksPath, "utf-8"));
452
+ const s = JSON.parse(readFileSync4(p.antigravityHooksPath, "utf-8"));
254
453
  if (!(ANTIGRAVITY_GROUP in s)) return;
255
454
  delete s[ANTIGRAVITY_GROUP];
256
455
  writeFileSync2(p.antigravityHooksPath, JSON.stringify(s, null, 2) + "\n");
@@ -258,8 +457,10 @@ function removeAntigravityGuard(p) {
258
457
  }
259
458
  }
260
459
  function codexHookCommand(scriptAbs) {
261
- const nodeBin = process.execPath.replace(/\\/g, "/");
262
- return `"${nodeBin}" "${scriptAbs.replace(/\\/g, "/")}" codex "Codex"`;
460
+ if (process.platform === "win32") {
461
+ return `"${process.execPath.replace(/\\/g, "/")}" "${scriptAbs.replace(/\\/g, "/")}" codex "Codex"`;
462
+ }
463
+ return hookCommandFor(dirname(scriptAbs), basename(scriptAbs), "codex", "Codex");
263
464
  }
264
465
  function isOurCodexGroup(group) {
265
466
  const hooks = Array.isArray(group?.hooks) ? group.hooks : [];
@@ -267,10 +468,10 @@ function isOurCodexGroup(group) {
267
468
  }
268
469
  function readCodexHooksFile(path) {
269
470
  const out = { hooks: {}, rest: {} };
270
- if (!existsSync2(path)) return out;
471
+ if (!existsSync3(path)) return out;
271
472
  let raw;
272
473
  try {
273
- raw = JSON.parse(readFileSync3(path, "utf-8"));
474
+ raw = JSON.parse(readFileSync4(path, "utf-8"));
274
475
  } catch {
275
476
  return out;
276
477
  }
@@ -315,10 +516,10 @@ function writeCodexHooksFile(path, file) {
315
516
  body["hooks"] = hooks;
316
517
  writeFileSync2(path, JSON.stringify(body, null, 2) + "\n");
317
518
  }
318
- function installCodexGuard(p, hooksDir) {
519
+ function installCodexGuard(p, hooksDir2) {
319
520
  mkdirSync(p.codexDir, { recursive: true });
320
- if (existsSync2(p.codexHooksPath) && !existsSync2(p.codexBackupPath)) {
321
- writeFileSync2(p.codexBackupPath, readFileSync3(p.codexHooksPath, "utf-8"));
521
+ if (existsSync3(p.codexHooksPath) && !existsSync3(p.codexBackupPath)) {
522
+ writeFileSync2(p.codexBackupPath, readFileSync4(p.codexHooksPath, "utf-8"));
322
523
  }
323
524
  const file = readCodexHooksFile(p.codexHooksPath);
324
525
  const script = {
@@ -342,7 +543,7 @@ function installCodexGuard(p, hooksDir) {
342
543
  ...ev === "Stop" ? {} : { matcher: "*" },
343
544
  hooks: [{
344
545
  type: "command",
345
- command: codexHookCommand(join3(hooksDir, script[ev]).replace(/\\/g, "/")),
546
+ command: codexHookCommand(join4(hooksDir2, script[ev]).replace(/\\/g, "/")),
346
547
  timeout: CODEX_TIMEOUT_SEC,
347
548
  statusMessage: status[ev]
348
549
  }]
@@ -352,7 +553,7 @@ function installCodexGuard(p, hooksDir) {
352
553
  writeCodexHooksFile(p.codexHooksPath, file);
353
554
  }
354
555
  function removeCodexGuard(p) {
355
- if (!existsSync2(p.codexHooksPath)) return;
556
+ if (!existsSync3(p.codexHooksPath)) return;
356
557
  try {
357
558
  const file = readCodexHooksFile(p.codexHooksPath);
358
559
  let changed = false;
@@ -378,7 +579,7 @@ function isCodexGuardInstalled() {
378
579
  }
379
580
  function codexDetected() {
380
581
  try {
381
- return existsSync2(globalPaths().codexDir);
582
+ return existsSync3(globalPaths().codexDir);
382
583
  } catch {
383
584
  return false;
384
585
  }
@@ -388,7 +589,7 @@ function codexHooksStatus() {
388
589
  const registered = isCodexGuardInstalled();
389
590
  let trusted = false, disabled = false;
390
591
  try {
391
- const toml = readFileSync3(p.codexConfigPath, "utf-8");
592
+ const toml = readFileSync4(p.codexConfigPath, "utf-8");
392
593
  trusted = /trusted_hash\s*=/.test(toml);
393
594
  disabled = /^\s*hooks\s*=\s*false\s*$/m.test(toml);
394
595
  } catch {
@@ -407,10 +608,10 @@ function removeOpencodeGuard(p) {
407
608
  } catch {
408
609
  }
409
610
  }
410
- function warmPolicyCache(hooksDir, agents) {
611
+ function warmPolicyCache(hooksDir2, agents) {
411
612
  for (const agent of agents) {
412
613
  try {
413
- const child = spawn(process.execPath, [join3(hooksDir, "guard.mjs"), agent, "--sg-refresh-policy"], {
614
+ const child = spawn(process.execPath, [join4(hooksDir2, "guard.mjs"), agent, "--sg-refresh-policy"], {
414
615
  detached: true,
415
616
  stdio: "ignore",
416
617
  windowsHide: true
@@ -424,23 +625,23 @@ function warmPolicyCache(hooksDir, agents) {
424
625
  }
425
626
  function isOpencodeGuardInstalled() {
426
627
  try {
427
- return readFileSync3(globalPaths().opencodePluginPath, "utf-8").includes("tool.execute.before");
628
+ return readFileSync4(globalPaths().opencodePluginPath, "utf-8").includes("tool.execute.before");
428
629
  } catch {
429
630
  return false;
430
631
  }
431
632
  }
432
633
  function opencodeDetected() {
433
634
  try {
434
- return existsSync2(globalPaths().opencodeDir);
635
+ return existsSync3(globalPaths().opencodeDir);
435
636
  } catch {
436
637
  return false;
437
638
  }
438
639
  }
439
- function sweepStrayScratchDirs(root = homedir3(), maxDepth = 6, budget = 4e4) {
640
+ function sweepStrayScratchDirs(root = homedir4(), maxDepth = 6, budget = 4e4) {
440
641
  let removed = 0;
441
642
  let visited = 0;
442
643
  const skip = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".next", "build"]);
443
- const store = join3(homedir3(), ".solongate");
644
+ const store = join4(homedir4(), ".solongate");
444
645
  const walk = (dir, depth) => {
445
646
  if (depth > maxDepth || visited++ > budget) return;
446
647
  let entries;
@@ -451,11 +652,11 @@ function sweepStrayScratchDirs(root = homedir3(), maxDepth = 6, budget = 4e4) {
451
652
  }
452
653
  for (const name of entries) {
453
654
  if (skip.has(name)) continue;
454
- const full = join3(dir, name);
655
+ const full = join4(dir, name);
455
656
  if (full === store) continue;
456
657
  let isDir = false;
457
658
  try {
458
- isDir = statSync2(full).isDirectory();
659
+ isDir = statSync3(full).isDirectory();
459
660
  } catch {
460
661
  continue;
461
662
  }
@@ -466,7 +667,7 @@ function sweepStrayScratchDirs(root = homedir3(), maxDepth = 6, budget = 4e4) {
466
667
  for (const f of readdirSync2(full)) {
467
668
  if (SCRATCH_FILES.has(f)) {
468
669
  try {
469
- rmSync(join3(full, f), { force: true });
670
+ rmSync(join4(full, f), { force: true });
470
671
  } catch {
471
672
  left.push(f);
472
673
  }
@@ -488,12 +689,17 @@ function sweepStrayScratchDirs(root = homedir3(), maxDepth = 6, budget = 4e4) {
488
689
  }
489
690
  function repairQuiet() {
490
691
  const p = globalPaths();
491
- const has = (f) => existsSync2(f);
492
- const guardFile = join3(p.hooksDir, "guard.mjs");
692
+ const has = (f) => existsSync3(f);
693
+ const guardFile = join4(p.hooksDir, "guard.mjs");
493
694
  const line = (label, ok, yes, no) => ({ label, ok, detail: ok ? yes : no });
695
+ const runtime = () => {
696
+ const r2 = hookCanStart();
697
+ return { label: "hook runtime", ok: r2.ok, detail: r2.ok ? `node ${r2.detail}` : r2.detail };
698
+ };
494
699
  const before = [
495
700
  line("guard hook file", has(guardFile), "present", "MISSING"),
496
701
  line("cloud credential", has(p.configPath), "present", "MISSING"),
702
+ runtime(),
497
703
  line("Claude hooks", isGuardInstalled(), "guard registered", "guard NOT registered"),
498
704
  line("Antigravity hooks", has(p.antigravityHooksPath), "guard registered", "guard NOT registered"),
499
705
  line("Codex hooks", isCodexGuardInstalled(), "guard registered", "guard NOT registered"),
@@ -503,6 +709,7 @@ function repairQuiet() {
503
709
  if (!r.ok) return { ok: false, message: r.message, before, after: [], notes: [] };
504
710
  const after = [
505
711
  { label: "guard hook file", ok: true, detail: `present (v${installedGuardVersion() ?? "?"})` },
712
+ runtime(),
506
713
  line("Claude hooks", isGuardInstalled(), "guard registered", "NOT registered"),
507
714
  line("Antigravity hooks", has(p.antigravityHooksPath), "guard registered", "NOT registered"),
508
715
  line("Codex hooks", isCodexGuardInstalled(), "guard registered", "NOT registered"),
@@ -529,7 +736,7 @@ function installGlobalQuiet() {
529
736
  let apiKey = process.env["SOLONGATE_API_KEY"] || "";
530
737
  let apiUrl = process.env["SOLONGATE_API_URL"] || "https://api.solongate.com";
531
738
  try {
532
- const cfg = JSON.parse(readFileSync3(p.configPath, "utf-8"));
739
+ const cfg = JSON.parse(readFileSync4(p.configPath, "utf-8"));
533
740
  if (cfg && typeof cfg.apiKey === "string") apiKey = apiKey || cfg.apiKey;
534
741
  if (cfg && typeof cfg.apiUrl === "string") apiUrl = cfg.apiUrl;
535
742
  } catch {
@@ -545,26 +752,26 @@ function installGlobalQuiet() {
545
752
  mkdirSync(p.hooksDir, { recursive: true });
546
753
  mkdirSync(p.claudeDir, { recursive: true });
547
754
  unlockProtected();
548
- writeFileSync2(join3(p.hooksDir, "guard.mjs"), readGuard());
755
+ writeFileSync2(join4(p.hooksDir, "guard.mjs"), readGuard());
549
756
  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"));
757
+ writeFileSync2(join4(p.hooksDir, "audit.mjs"), readHook("audit.mjs"));
758
+ writeFileSync2(join4(p.hooksDir, "stop.mjs"), readHook("stop.mjs"));
759
+ writeFileSync2(join4(p.hooksDir, "shield.mjs"), readHook("shield.mjs"));
760
+ writeFileSync2(join4(p.hooksDir, "conversation.mjs"), readHook("conversation.mjs"));
761
+ writeLauncher(p.hooksDir);
762
+ writeLauncher(p.hooksDir);
554
763
  writeFileSync2(p.configPath, JSON.stringify({ apiKey, apiUrl }, null, 2) + "\n");
555
764
  let existing = {};
556
- if (existsSync2(p.settingsPath)) {
557
- const raw = readFileSync3(p.settingsPath, "utf-8");
558
- if (!existsSync2(p.backupPath)) writeFileSync2(p.backupPath, raw);
765
+ if (existsSync3(p.settingsPath)) {
766
+ const raw = readFileSync4(p.settingsPath, "utf-8");
767
+ if (!existsSync3(p.backupPath)) writeFileSync2(p.backupPath, raw);
559
768
  try {
560
769
  existing = JSON.parse(raw);
561
770
  } catch {
562
771
  existing = {};
563
772
  }
564
773
  }
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"`;
774
+ const hookCmd = (script) => hookCommandFor(p.hooksDir, script);
568
775
  const merged = {
569
776
  ...existing,
570
777
  hooks: {
@@ -591,7 +798,7 @@ function installGlobalQuiet() {
591
798
  };
592
799
  writeFileSync2(p.settingsPath, JSON.stringify(merged, null, 2) + "\n");
593
800
  try {
594
- installAntigravityGuard(p, join3(p.hooksDir, "guard.mjs").replace(/\\/g, "/"));
801
+ installAntigravityGuard(p, join4(p.hooksDir, "guard.mjs").replace(/\\/g, "/"));
595
802
  } catch {
596
803
  }
597
804
  try {
@@ -612,7 +819,7 @@ function installGlobalQuiet() {
612
819
  function installedGuardVersion() {
613
820
  try {
614
821
  const p = globalPaths();
615
- const s = readFileSync3(join3(p.hooksDir, "guard.mjs"), "utf-8");
822
+ const s = readFileSync4(join4(p.hooksDir, "guard.mjs"), "utf-8");
616
823
  const m = s.match(/HOOK_VERSION\s*=\s*(\d+)/);
617
824
  return m ? parseInt(m[1], 10) : null;
618
825
  } catch {
@@ -622,16 +829,35 @@ function installedGuardVersion() {
622
829
  function guardHookOutdated() {
623
830
  try {
624
831
  const p = globalPaths();
625
- return readFileSync3(join3(p.hooksDir, "guard.mjs"), "utf-8") !== readGuard();
832
+ return readFileSync4(join4(p.hooksDir, "guard.mjs"), "utf-8") !== readGuard();
626
833
  } catch {
627
834
  return false;
628
835
  }
629
836
  }
837
+ function hookCommandFor(hooksDir2, script, client = "claude-code", label = "Claude Code") {
838
+ const target = join4(hooksDir2, script).replace(/\\/g, "/");
839
+ if (process.platform === "win32") {
840
+ return `& "${process.execPath.replace(/\\/g, "/")}" "${target}" ${client} "${label}"`;
841
+ }
842
+ const launcher = join4(hooksDir2, LAUNCHER_NAME).replace(/\\/g, "/");
843
+ return `/bin/sh "${launcher}" "${target}" ${client} "${label}"`;
844
+ }
845
+ function writeLauncher(hooksDir2) {
846
+ writeFileSync2(join4(hooksDir2, LAUNCHER_NAME), launcherScript(process.execPath));
847
+ try {
848
+ chmodSync(join4(hooksDir2, LAUNCHER_NAME), 493);
849
+ } catch {
850
+ }
851
+ try {
852
+ mkdirSync(join4(hooksDir2, "..", BEAT_DIR), { recursive: true });
853
+ } catch {
854
+ }
855
+ }
630
856
  function isGuardInstalled() {
631
857
  try {
632
858
  const p = globalPaths();
633
- if (!existsSync2(p.settingsPath)) return false;
634
- const s = JSON.parse(readFileSync3(p.settingsPath, "utf-8"));
859
+ if (!existsSync3(p.settingsPath)) return false;
860
+ const s = JSON.parse(readFileSync4(p.settingsPath, "utf-8"));
635
861
  return !!s.hooks && JSON.stringify(s.hooks).includes(".solongate");
636
862
  } catch {
637
863
  return false;
@@ -654,8 +880,8 @@ function uninstallGlobalQuiet() {
654
880
  removeOpencodeGuard(p);
655
881
  } catch {
656
882
  }
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"));
883
+ if (!existsSync3(p.settingsPath)) return { ok: true, message: "guard removed (open a new session)" };
884
+ const s = JSON.parse(readFileSync4(p.settingsPath, "utf-8"));
659
885
  delete s.hooks;
660
886
  writeFileSync2(p.settingsPath, JSON.stringify(s, null, 2) + "\n");
661
887
  return { ok: true, message: "guard removed (open a new session)" };
@@ -669,17 +895,17 @@ function escapeRe(s) {
669
895
  function shimTargets() {
670
896
  if (process.platform === "win32") {
671
897
  try {
672
- const prof = execFileSync("powershell", ["-NoProfile", "-Command", "$PROFILE.CurrentUserAllHosts"], { encoding: "utf-8" }).trim();
898
+ const prof = execFileSync2("powershell", ["-NoProfile", "-Command", "$PROFILE.CurrentUserAllHosts"], { encoding: "utf-8" }).trim();
673
899
  return prof ? [prof] : [];
674
900
  } catch {
675
901
  return [];
676
902
  }
677
903
  }
678
- return [".bashrc", ".zshrc", ".profile"].map((f) => join3(homedir3(), f)).filter((f) => existsSync2(f));
904
+ return [".bashrc", ".zshrc", ".profile"].map((f) => join4(homedir4(), f)).filter((f) => existsSync3(f));
679
905
  }
680
906
  function writeShimBlock(file, block2) {
681
907
  const re = new RegExp(escapeRe(SHIM_BEGIN) + "[\\s\\S]*?" + escapeRe(SHIM_END) + "\\r?\\n?", "g");
682
- let content = existsSync2(file) ? readFileSync3(file, "utf-8") : "";
908
+ let content = existsSync3(file) ? readFileSync4(file, "utf-8") : "";
683
909
  content = content.replace(re, "");
684
910
  if (block2) {
685
911
  if (content.length && !content.endsWith("\n")) content += "\n";
@@ -700,6 +926,10 @@ var __dirname, HOOKS_DIR, ANTIGRAVITY_GROUP, CODEX_EVENTS, CODEX_TIMEOUT_SEC, SC
700
926
  var init_global_install = __esm({
701
927
  "src/global-install.ts"() {
702
928
  "use strict";
929
+ init_hook_launcher();
930
+ init_hook_launcher();
931
+ init_hook_health();
932
+ init_hook_health();
703
933
  __dirname = dirname(fileURLToPath(import.meta.url));
704
934
  HOOKS_DIR = resolve(__dirname, "..", "hooks");
705
935
  ANTIGRAVITY_GROUP = "solongate-guard";
@@ -713,8 +943,8 @@ var init_global_install = __esm({
713
943
 
714
944
  // src/tui/index.tsx
715
945
  import { appendFileSync, mkdirSync as mkdirSync7 } from "fs";
716
- import { homedir as homedir10 } from "os";
717
- import { join as join10 } from "path";
946
+ import { homedir as homedir11 } from "os";
947
+ import { join as join11 } from "path";
718
948
  import { render } from "ink";
719
949
 
720
950
  // src/tui/App.tsx
@@ -915,8 +1145,8 @@ function KeyHints({ hints }) {
915
1145
  import { Box as Box2, Text as Text2, useInput } from "ink";
916
1146
  import TextInput from "ink-text-input";
917
1147
  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";
1148
+ import { homedir as homedir6 } from "os";
1149
+ import { join as join6, resolve as resolve3 } from "path";
920
1150
 
921
1151
  // src/tui/local-log.ts
922
1152
  import { closeSync, existsSync, openSync, readdirSync, readFileSync as readFileSync2, readSync, statSync, writeFileSync } from "fs";
@@ -948,9 +1178,9 @@ function localLogsSetting() {
948
1178
  return off;
949
1179
  }
950
1180
  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);
1181
+ const sgDir2 = join2(homedir2(), ".solongate");
1182
+ return readdirSync(sgDir2).filter((f) => f.startsWith(".policy-cache-") && f.endsWith(".json")).map((f) => {
1183
+ const p = join2(sgDir2, f);
954
1184
  let mtime = 0;
955
1185
  try {
956
1186
  mtime = statSync(p).mtimeMs;
@@ -961,9 +1191,9 @@ function policyCachesNewestFirst() {
961
1191
  }
962
1192
  function localLogFile() {
963
1193
  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);
1194
+ const sgDir2 = join2(homedir2(), ".solongate");
1195
+ const caches = readdirSync(sgDir2).filter((f) => f.startsWith(".policy-cache-") && f.endsWith(".json")).map((f) => {
1196
+ const p = join2(sgDir2, f);
967
1197
  let mtime = 0;
968
1198
  try {
969
1199
  mtime = statSync(p).mtimeMs;
@@ -1074,15 +1304,15 @@ import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2
1074
1304
 
1075
1305
  // src/api-client/client.ts
1076
1306
  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";
1307
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync4 } from "fs";
1308
+ import { resolve as resolve2, join as join5 } from "path";
1309
+ import { homedir as homedir5 } from "os";
1080
1310
  var DEFAULT_API_URL = "https://api.solongate.com";
1081
- var accountsFile = () => join4(homedir4(), ".solongate", "accounts.json");
1311
+ var accountsFile = () => join5(homedir5(), ".solongate", "accounts.json");
1082
1312
  function listAccounts() {
1083
1313
  let list5 = [];
1084
1314
  try {
1085
- const raw = JSON.parse(readFileSync4(accountsFile(), "utf-8"));
1315
+ const raw = JSON.parse(readFileSync5(accountsFile(), "utf-8"));
1086
1316
  if (Array.isArray(raw)) list5 = raw.filter((a) => a && typeof a.apiKey === "string");
1087
1317
  } catch {
1088
1318
  }
@@ -1096,7 +1326,7 @@ function saveAccount(acc) {
1096
1326
  try {
1097
1327
  const list5 = (() => {
1098
1328
  try {
1099
- const raw = JSON.parse(readFileSync4(accountsFile(), "utf-8"));
1329
+ const raw = JSON.parse(readFileSync5(accountsFile(), "utf-8"));
1100
1330
  return Array.isArray(raw) ? raw.filter((a) => a && a.apiKey) : [];
1101
1331
  } catch {
1102
1332
  return [];
@@ -1104,7 +1334,7 @@ function saveAccount(acc) {
1104
1334
  })();
1105
1335
  const next = list5.filter((a) => a.apiKey !== acc.apiKey);
1106
1336
  next.unshift({ ...acc, addedAt: acc.addedAt ?? Date.now() });
1107
- mkdirSync2(join4(homedir4(), ".solongate"), { recursive: true });
1337
+ mkdirSync2(join5(homedir5(), ".solongate"), { recursive: true });
1108
1338
  writeFileSync3(accountsFile(), JSON.stringify(next, null, 2));
1109
1339
  } catch {
1110
1340
  }
@@ -1113,7 +1343,7 @@ function removeAccount(apiKey) {
1113
1343
  try {
1114
1344
  const list5 = (() => {
1115
1345
  try {
1116
- const raw = JSON.parse(readFileSync4(accountsFile(), "utf-8"));
1346
+ const raw = JSON.parse(readFileSync5(accountsFile(), "utf-8"));
1117
1347
  return Array.isArray(raw) ? raw.filter((a) => a && a.apiKey) : [];
1118
1348
  } catch {
1119
1349
  return [];
@@ -1136,12 +1366,12 @@ function isActiveAccount(apiKey) {
1136
1366
  }
1137
1367
  function setActiveAccount(creds) {
1138
1368
  try {
1139
- const dir = join4(homedir4(), ".solongate");
1369
+ const dir = join5(homedir5(), ".solongate");
1140
1370
  mkdirSync2(dir, { recursive: true });
1141
- const p = join4(dir, ["cloud", "guard.json"].join("-"));
1371
+ const p = join5(dir, ["cloud", "guard.json"].join("-"));
1142
1372
  let existing = {};
1143
1373
  try {
1144
- existing = JSON.parse(readFileSync4(p, "utf-8"));
1374
+ existing = JSON.parse(readFileSync5(p, "utf-8"));
1145
1375
  } catch {
1146
1376
  }
1147
1377
  if (!writeProtectedFile(p, JSON.stringify({ ...existing, apiKey: creds.apiKey, apiUrl: creds.apiUrl }, null, 2))) return false;
@@ -1153,14 +1383,14 @@ function setActiveAccount(creds) {
1153
1383
  }
1154
1384
  function clearActiveCredential() {
1155
1385
  try {
1156
- const p = join4(homedir4(), ".solongate", ["cloud", "guard.json"].join("-"));
1157
- if (!existsSync3(p)) {
1386
+ const p = join5(homedir5(), ".solongate", ["cloud", "guard.json"].join("-"));
1387
+ if (!existsSync4(p)) {
1158
1388
  cached2 = null;
1159
1389
  return true;
1160
1390
  }
1161
1391
  let existing = {};
1162
1392
  try {
1163
- existing = JSON.parse(readFileSync4(p, "utf-8"));
1393
+ existing = JSON.parse(readFileSync5(p, "utf-8"));
1164
1394
  } catch {
1165
1395
  }
1166
1396
  delete existing.apiKey;
@@ -1191,9 +1421,9 @@ var NotAuthenticatedError = class extends Error {
1191
1421
  };
1192
1422
  function loginCredentialFile() {
1193
1423
  try {
1194
- const p = join4(homedir4(), ".solongate", "cloud-guard.json");
1195
- if (!existsSync3(p)) return {};
1196
- const c2 = JSON.parse(readFileSync4(p, "utf-8"));
1424
+ const p = join5(homedir5(), ".solongate", "cloud-guard.json");
1425
+ if (!existsSync4(p)) return {};
1426
+ const c2 = JSON.parse(readFileSync5(p, "utf-8"));
1197
1427
  return c2 && typeof c2 === "object" ? c2 : {};
1198
1428
  } catch {
1199
1429
  return {};
@@ -1202,8 +1432,8 @@ function loginCredentialFile() {
1202
1432
  function dotenvApiKey() {
1203
1433
  try {
1204
1434
  const envPath = resolve2(".env");
1205
- if (!existsSync3(envPath)) return void 0;
1206
- for (const line of readFileSync4(envPath, "utf-8").split("\n")) {
1435
+ if (!existsSync4(envPath)) return void 0;
1436
+ for (const line of readFileSync5(envPath, "utf-8").split("\n")) {
1207
1437
  const trimmed = line.trim();
1208
1438
  if (!trimmed || trimmed.startsWith("#")) continue;
1209
1439
  const eq = trimmed.indexOf("=");
@@ -1687,7 +1917,7 @@ var projectKey = (dir) => {
1687
1917
  }
1688
1918
  return h.toString(16);
1689
1919
  };
1690
- var RING = join5(homedir5(), ".solongate", "projects", projectKey(resolve3(process.cwd())), ".eval-ring.jsonl");
1920
+ var RING = join6(homedir6(), ".solongate", "projects", projectKey(resolve3(process.cwd())), ".eval-ring.jsonl");
1691
1921
  var fmtUp = (ms) => {
1692
1922
  const s = Math.floor(ms / 1e3);
1693
1923
  const p = (n) => String(n).padStart(2, "0");
@@ -2294,9 +2524,9 @@ function LivePanel({ active: active2 }) {
2294
2524
  else if (input === "x") toggleSignal("dlp");
2295
2525
  else if (input === "r") toggleSignal("ratelimit");
2296
2526
  else if (input === "e") {
2297
- const file = join5(homedir5(), ".solongate", "live-export.jsonl");
2527
+ const file = join6(homedir6(), ".solongate", "live-export.jsonl");
2298
2528
  try {
2299
- mkdirSync3(join5(homedir5(), ".solongate"), { recursive: true });
2529
+ mkdirSync3(join6(homedir6(), ".solongate"), { recursive: true });
2300
2530
  writeFileSync4(file, visibleDesc.map((x) => JSON.stringify(x)).join("\n") + "\n");
2301
2531
  setActionMsg({ text: `\u2713 exported ${visibleDesc.length} lines \u2192 ${file}`, level: "ok", until: Date.now() + 6e3 });
2302
2532
  } catch (err2) {
@@ -3896,8 +4126,8 @@ function DlpPanel({ focused }) {
3896
4126
  import { Box as Box7, Text as Text7, useInput as useInput6 } from "ink";
3897
4127
  import TextInput5 from "ink-text-input";
3898
4128
  import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "fs";
3899
- import { homedir as homedir6 } from "os";
3900
- import { join as join6 } from "path";
4129
+ import { homedir as homedir7 } from "os";
4130
+ import { join as join7 } from "path";
3901
4131
  import { useState as useState7 } from "react";
3902
4132
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
3903
4133
  var DECISIONS = [void 0, "DENY", "ALLOW"];
@@ -4104,8 +4334,8 @@ function AuditPanel({ active: active2, focused }) {
4104
4334
  const doExport = (kind) => {
4105
4335
  setMsg({ text: "exporting\u2026", level: "ok" });
4106
4336
  const run = async () => {
4107
- const dir = join6(homedir6(), ".solongate");
4108
- const file = join6(dir, `audit-export-${source}.jsonl`);
4337
+ const dir = join7(homedir7(), ".solongate");
4338
+ const file = join7(dir, `audit-export-${source}.jsonl`);
4109
4339
  let rows2;
4110
4340
  if (kind === "page") rows2 = pageRows;
4111
4341
  else if (source === "cloud") {
@@ -4479,10 +4709,11 @@ import { useEffect as useEffect7, useRef as useRef3, useState as useState8 } fro
4479
4709
  init_global_install();
4480
4710
 
4481
4711
  // 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";
4712
+ import { existsSync as existsSync5, readFileSync as readFileSync6, statSync as statSync4 } from "fs";
4713
+ import { homedir as homedir8 } from "os";
4714
+ import { join as join8 } from "path";
4485
4715
  init_global_install();
4716
+ init_hook_health();
4486
4717
  async function collectChecks() {
4487
4718
  const checks = [];
4488
4719
  if (!isAuthenticated()) {
@@ -4530,8 +4761,32 @@ async function collectChecks() {
4530
4761
  ok: claudeReg,
4531
4762
  detail: claudeReg ? "guard registered" : "guard NOT registered - run `solongate repair`"
4532
4763
  });
4533
- if (existsSync4(globalPaths().antigravityDir)) {
4534
- const reg = existsSync4(globalPaths().antigravityHooksPath);
4764
+ if (claudeReg) {
4765
+ const start = hookCanStart();
4766
+ checks.push({
4767
+ name: "hook runtime",
4768
+ ok: start.ok,
4769
+ detail: start.ok ? `node ${start.detail}` : `${start.detail} - nothing is being enforced or logged`
4770
+ });
4771
+ const beat = guardBeat();
4772
+ if (!beat) {
4773
+ checks.push({
4774
+ name: "guard fired",
4775
+ ok: "warn",
4776
+ detail: "never - open your agent and run one tool call, then check again"
4777
+ });
4778
+ } else if (beat.node === "no-node") {
4779
+ checks.push({
4780
+ name: "guard fired",
4781
+ ok: false,
4782
+ detail: `${agoLabel(beat.at)}, but found no node to run with - run \`solongate repair\``
4783
+ });
4784
+ } else {
4785
+ checks.push({ name: "guard fired", ok: true, detail: `${agoLabel(beat.at)} \xB7 ${beat.node}` });
4786
+ }
4787
+ }
4788
+ if (existsSync5(globalPaths().antigravityDir)) {
4789
+ const reg = existsSync5(globalPaths().antigravityHooksPath);
4535
4790
  checks.push({
4536
4791
  name: "Antigravity hooks",
4537
4792
  ok: reg,
@@ -4559,7 +4814,7 @@ async function collectChecks() {
4559
4814
  });
4560
4815
  }
4561
4816
  try {
4562
- const raw = readFileSync5(join7(homedir7(), ".solongate", ".key-rejected.json"), "utf-8");
4817
+ const raw = readFileSync6(join8(homedir8(), ".solongate", ".key-rejected.json"), "utf-8");
4563
4818
  const m = JSON.parse(raw);
4564
4819
  const ageMin = m.ts ? Math.round((Date.now() - m.ts) / 6e4) : null;
4565
4820
  checks.push({
@@ -4570,8 +4825,8 @@ async function collectChecks() {
4570
4825
  } catch {
4571
4826
  }
4572
4827
  const LOCAL_LOG2 = localLogFile();
4573
- if (existsSync4(LOCAL_LOG2)) {
4574
- const st = statSync3(LOCAL_LOG2);
4828
+ if (existsSync5(LOCAL_LOG2)) {
4829
+ const st = statSync4(LOCAL_LOG2);
4575
4830
  const ageMin = (Date.now() - st.mtimeMs) / 6e4;
4576
4831
  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
4832
  } else {
@@ -4582,17 +4837,17 @@ async function collectChecks() {
4582
4837
 
4583
4838
  // src/logs-server-daemon.ts
4584
4839
  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";
4840
+ import { mkdirSync as mkdirSync5, openSync as openSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
4841
+ import { homedir as homedir9 } from "os";
4842
+ import { dirname as dirname2, join as join9 } from "path";
4588
4843
  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");
4844
+ var DIR = join9(homedir9(), ".solongate");
4845
+ var STATE_FILE = join9(DIR, ".logs-server.json");
4846
+ var LOG_FILE = join9(DIR, "logs-server.log");
4592
4847
  var LOGS_SERVER_PORT = 8788;
4593
4848
  function readState() {
4594
4849
  try {
4595
- const s = JSON.parse(readFileSync6(STATE_FILE, "utf-8"));
4850
+ const s = JSON.parse(readFileSync7(STATE_FILE, "utf-8"));
4596
4851
  return s && typeof s === "object" ? s : {};
4597
4852
  } catch {
4598
4853
  return {};
@@ -4628,7 +4883,7 @@ function startLogsServerDaemon() {
4628
4883
  try {
4629
4884
  mkdirSync5(DIR, { recursive: true });
4630
4885
  const log = openSync2(LOG_FILE, "a");
4631
- const cli = join8(dirname2(fileURLToPath2(import.meta.url)), "index.js");
4886
+ const cli = join9(dirname2(fileURLToPath2(import.meta.url)), "index.js");
4632
4887
  const p = spawn4(process.execPath, [cli, "logs-server"], {
4633
4888
  detached: true,
4634
4889
  stdio: ["ignore", log, log],
@@ -4659,22 +4914,22 @@ function stopLogsServerDaemon() {
4659
4914
  }
4660
4915
 
4661
4916
  // 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";
4917
+ import { execFile, execFileSync as execFileSync3, spawn as spawn5 } from "child_process";
4918
+ import { access, mkdirSync as mkdirSync6, openSync as openSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync7, constants as FS } from "fs";
4919
+ import { homedir as homedir10 } from "os";
4920
+ import { dirname as dirname3, join as join10, sep } from "path";
4666
4921
  import { fileURLToPath as fileURLToPath3 } from "url";
4667
4922
  var PKG = "@solongate/proxy";
4668
4923
  var CHECK_EVERY_MS = 30 * 60 * 1e3;
4669
4924
  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");
4925
+ var STATE_FILE2 = join10(homedir10(), ".solongate", ".self-update.json");
4926
+ var LOG_FILE2 = join10(homedir10(), ".solongate", "self-update.log");
4927
+ var LOCK_FILE = join10(homedir10(), ".solongate", ".update-install.lock");
4673
4928
  var LOCK_STALE_MS = 3 * 6e4;
4674
4929
  var NEEDS_ADMIN_RE = /\bEACCES\b|\bEPERM\b|permission denied|operation not permitted/i;
4675
4930
  function readState2() {
4676
4931
  try {
4677
- const s = JSON.parse(readFileSync7(STATE_FILE2, "utf-8"));
4932
+ const s = JSON.parse(readFileSync8(STATE_FILE2, "utf-8"));
4678
4933
  return s && typeof s === "object" ? s : {};
4679
4934
  } catch {
4680
4935
  return {};
@@ -4682,7 +4937,7 @@ function readState2() {
4682
4937
  }
4683
4938
  function writeState2(s) {
4684
4939
  try {
4685
- mkdirSync6(join9(homedir9(), ".solongate"), { recursive: true });
4940
+ mkdirSync6(join10(homedir10(), ".solongate"), { recursive: true });
4686
4941
  writeFileSync7(STATE_FILE2, JSON.stringify(s));
4687
4942
  } catch {
4688
4943
  }
@@ -4703,7 +4958,7 @@ function setAutoUpdate(on) {
4703
4958
  }
4704
4959
  function currentVersion() {
4705
4960
  try {
4706
- const pkg = JSON.parse(readFileSync7(join9(dirname3(fileURLToPath3(import.meta.url)), "..", "package.json"), "utf-8"));
4961
+ const pkg = JSON.parse(readFileSync8(join10(dirname3(fileURLToPath3(import.meta.url)), "..", "package.json"), "utf-8"));
4707
4962
  return pkg.version ?? "0.0.0";
4708
4963
  } catch {
4709
4964
  return "0.0.0";
@@ -4740,7 +4995,7 @@ async function fetchLatest() {
4740
4995
  function runGlobalInstall(version) {
4741
4996
  return new Promise((resolve4) => {
4742
4997
  try {
4743
- mkdirSync6(join9(homedir9(), ".solongate"), { recursive: true });
4998
+ mkdirSync6(join10(homedir10(), ".solongate"), { recursive: true });
4744
4999
  execFile(
4745
5000
  "npm",
4746
5001
  ["install", "-g", `${PKG}@${version}`],
@@ -5904,11 +6159,11 @@ async function launchTui() {
5904
6159
  return;
5905
6160
  }
5906
6161
  process.stdout.write("\x1B[?1049h\x1B[H");
5907
- const debugLog = join10(homedir10(), ".solongate", "dataroom-debug.log");
6162
+ const debugLog = join11(homedir11(), ".solongate", "dataroom-debug.log");
5908
6163
  const saved = { log: console.log, warn: console.warn, error: console.error, info: console.info, debug: console.debug };
5909
6164
  const toFile = (level) => (...args) => {
5910
6165
  try {
5911
- mkdirSync7(join10(homedir10(), ".solongate"), { recursive: true });
6166
+ mkdirSync7(join11(homedir11(), ".solongate"), { recursive: true });
5912
6167
  appendFileSync(debugLog, `${(/* @__PURE__ */ new Date()).toISOString()} [${level}] ${args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ")}
5913
6168
  `);
5914
6169
  } catch {