@solongate/proxy 0.83.55 → 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;
@@ -1024,31 +1250,6 @@ function tailLines(file, maxBytes = 131072) {
1024
1250
  return [];
1025
1251
  }
1026
1252
  }
1027
- function deleteLocalEntry(at, tool, session) {
1028
- try {
1029
- const file = localLogFile();
1030
- const lines = readFileSync2(file, "utf-8").split("\n");
1031
- let removed = 0;
1032
- const kept = lines.filter((line) => {
1033
- if (!line.trim()) return false;
1034
- if (removed) return true;
1035
- try {
1036
- const j = JSON.parse(line);
1037
- const hit = Date.parse(j.ts ?? "") === at && (j.tool ?? "?") === tool && (!session || j.session_id === session);
1038
- if (hit) {
1039
- removed++;
1040
- return false;
1041
- }
1042
- } catch {
1043
- }
1044
- return true;
1045
- });
1046
- if (removed) writeFileSync(file, kept.length ? kept.join("\n") + "\n" : "");
1047
- return removed;
1048
- } catch {
1049
- return 0;
1050
- }
1051
- }
1052
1253
  function clearLocalLog() {
1053
1254
  try {
1054
1255
  const file = localLogFile();
@@ -1099,15 +1300,15 @@ import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2
1099
1300
 
1100
1301
  // src/api-client/client.ts
1101
1302
  init_global_install();
1102
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "fs";
1103
- import { resolve as resolve2, join as join4 } from "path";
1104
- 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";
1105
1306
  var DEFAULT_API_URL = "https://api.solongate.com";
1106
- var accountsFile = () => join4(homedir4(), ".solongate", "accounts.json");
1307
+ var accountsFile = () => join5(homedir5(), ".solongate", "accounts.json");
1107
1308
  function listAccounts() {
1108
1309
  let list5 = [];
1109
1310
  try {
1110
- const raw = JSON.parse(readFileSync4(accountsFile(), "utf-8"));
1311
+ const raw = JSON.parse(readFileSync5(accountsFile(), "utf-8"));
1111
1312
  if (Array.isArray(raw)) list5 = raw.filter((a) => a && typeof a.apiKey === "string");
1112
1313
  } catch {
1113
1314
  }
@@ -1121,7 +1322,7 @@ function saveAccount(acc) {
1121
1322
  try {
1122
1323
  const list5 = (() => {
1123
1324
  try {
1124
- const raw = JSON.parse(readFileSync4(accountsFile(), "utf-8"));
1325
+ const raw = JSON.parse(readFileSync5(accountsFile(), "utf-8"));
1125
1326
  return Array.isArray(raw) ? raw.filter((a) => a && a.apiKey) : [];
1126
1327
  } catch {
1127
1328
  return [];
@@ -1129,7 +1330,7 @@ function saveAccount(acc) {
1129
1330
  })();
1130
1331
  const next = list5.filter((a) => a.apiKey !== acc.apiKey);
1131
1332
  next.unshift({ ...acc, addedAt: acc.addedAt ?? Date.now() });
1132
- mkdirSync2(join4(homedir4(), ".solongate"), { recursive: true });
1333
+ mkdirSync2(join5(homedir5(), ".solongate"), { recursive: true });
1133
1334
  writeFileSync3(accountsFile(), JSON.stringify(next, null, 2));
1134
1335
  } catch {
1135
1336
  }
@@ -1138,7 +1339,7 @@ function removeAccount(apiKey) {
1138
1339
  try {
1139
1340
  const list5 = (() => {
1140
1341
  try {
1141
- const raw = JSON.parse(readFileSync4(accountsFile(), "utf-8"));
1342
+ const raw = JSON.parse(readFileSync5(accountsFile(), "utf-8"));
1142
1343
  return Array.isArray(raw) ? raw.filter((a) => a && a.apiKey) : [];
1143
1344
  } catch {
1144
1345
  return [];
@@ -1161,12 +1362,12 @@ function isActiveAccount(apiKey) {
1161
1362
  }
1162
1363
  function setActiveAccount(creds) {
1163
1364
  try {
1164
- const dir = join4(homedir4(), ".solongate");
1365
+ const dir = join5(homedir5(), ".solongate");
1165
1366
  mkdirSync2(dir, { recursive: true });
1166
- const p = join4(dir, ["cloud", "guard.json"].join("-"));
1367
+ const p = join5(dir, ["cloud", "guard.json"].join("-"));
1167
1368
  let existing = {};
1168
1369
  try {
1169
- existing = JSON.parse(readFileSync4(p, "utf-8"));
1370
+ existing = JSON.parse(readFileSync5(p, "utf-8"));
1170
1371
  } catch {
1171
1372
  }
1172
1373
  if (!writeProtectedFile(p, JSON.stringify({ ...existing, apiKey: creds.apiKey, apiUrl: creds.apiUrl }, null, 2))) return false;
@@ -1178,14 +1379,14 @@ function setActiveAccount(creds) {
1178
1379
  }
1179
1380
  function clearActiveCredential() {
1180
1381
  try {
1181
- const p = join4(homedir4(), ".solongate", ["cloud", "guard.json"].join("-"));
1182
- if (!existsSync3(p)) {
1382
+ const p = join5(homedir5(), ".solongate", ["cloud", "guard.json"].join("-"));
1383
+ if (!existsSync4(p)) {
1183
1384
  cached2 = null;
1184
1385
  return true;
1185
1386
  }
1186
1387
  let existing = {};
1187
1388
  try {
1188
- existing = JSON.parse(readFileSync4(p, "utf-8"));
1389
+ existing = JSON.parse(readFileSync5(p, "utf-8"));
1189
1390
  } catch {
1190
1391
  }
1191
1392
  delete existing.apiKey;
@@ -1216,9 +1417,9 @@ var NotAuthenticatedError = class extends Error {
1216
1417
  };
1217
1418
  function loginCredentialFile() {
1218
1419
  try {
1219
- const p = join4(homedir4(), ".solongate", "cloud-guard.json");
1220
- if (!existsSync3(p)) return {};
1221
- 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"));
1222
1423
  return c2 && typeof c2 === "object" ? c2 : {};
1223
1424
  } catch {
1224
1425
  return {};
@@ -1227,8 +1428,8 @@ function loginCredentialFile() {
1227
1428
  function dotenvApiKey() {
1228
1429
  try {
1229
1430
  const envPath = resolve2(".env");
1230
- if (!existsSync3(envPath)) return void 0;
1231
- 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")) {
1232
1433
  const trimmed = line.trim();
1233
1434
  if (!trimmed || trimmed.startsWith("#")) continue;
1234
1435
  const eq = trimmed.indexOf("=");
@@ -1545,19 +1746,11 @@ var audit_exports = {};
1545
1746
  __export(audit_exports, {
1546
1747
  block: () => block,
1547
1748
  list: () => list2,
1548
- remove: () => remove2,
1549
- removeAll: () => removeAll,
1550
1749
  whitelist: () => whitelist
1551
1750
  });
1552
1751
  function list2(query = {}) {
1553
1752
  return request("GET", "/audit-logs", { query });
1554
1753
  }
1555
- function remove2(ids) {
1556
- return request("DELETE", "/audit-logs", { body: { ids } });
1557
- }
1558
- function removeAll() {
1559
- return request("DELETE", "/audit-logs", { body: { scope: "logs" } });
1560
- }
1561
1754
  function whitelist(id, scope = "exact") {
1562
1755
  return request("POST", `/audit-logs/${encodeURIComponent(id)}/whitelist`, { body: { scope } });
1563
1756
  }
@@ -1720,7 +1913,7 @@ var projectKey = (dir) => {
1720
1913
  }
1721
1914
  return h.toString(16);
1722
1915
  };
1723
- 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");
1724
1917
  var fmtUp = (ms) => {
1725
1918
  const s = Math.floor(ms / 1e3);
1726
1919
  const p = (n) => String(n).padStart(2, "0");
@@ -2327,9 +2520,9 @@ function LivePanel({ active: active2 }) {
2327
2520
  else if (input === "x") toggleSignal("dlp");
2328
2521
  else if (input === "r") toggleSignal("ratelimit");
2329
2522
  else if (input === "e") {
2330
- const file = join5(homedir5(), ".solongate", "live-export.jsonl");
2523
+ const file = join6(homedir6(), ".solongate", "live-export.jsonl");
2331
2524
  try {
2332
- mkdirSync3(join5(homedir5(), ".solongate"), { recursive: true });
2525
+ mkdirSync3(join6(homedir6(), ".solongate"), { recursive: true });
2333
2526
  writeFileSync4(file, visibleDesc.map((x) => JSON.stringify(x)).join("\n") + "\n");
2334
2527
  setActionMsg({ text: `\u2713 exported ${visibleDesc.length} lines \u2192 ${file}`, level: "ok", until: Date.now() + 6e3 });
2335
2528
  } catch (err2) {
@@ -3929,8 +4122,8 @@ function DlpPanel({ focused }) {
3929
4122
  import { Box as Box7, Text as Text7, useInput as useInput6 } from "ink";
3930
4123
  import TextInput5 from "ink-text-input";
3931
4124
  import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "fs";
3932
- import { homedir as homedir6 } from "os";
3933
- import { join as join6 } from "path";
4125
+ import { homedir as homedir7 } from "os";
4126
+ import { join as join7 } from "path";
3934
4127
  import { useState as useState7 } from "react";
3935
4128
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
3936
4129
  var DECISIONS = [void 0, "DENY", "ALLOW"];
@@ -4003,8 +4196,6 @@ var AUDIT_HELP = [
4003
4196
  ["g", "signal filter: all \u2192 dlp \u2192 ratelimit"],
4004
4197
  ["t / n", "tool / agent filter (type, enter done)"],
4005
4198
  ["/", "free-text search"],
4006
- ["x", "delete ONLY the selected entry (press x twice)"],
4007
- ["X", "delete ALL matched logs of the source (press X twice)"],
4008
4199
  ["e", "export this page \u2192 ~/.solongate/audit-export-<src>.jsonl"],
4009
4200
  ["E", "export ALL matched rows (cloud: up to 10k)"],
4010
4201
  ["c", "clear every filter (incl. session)"]
@@ -4055,7 +4246,6 @@ function AuditPanel({ active: active2, focused }) {
4055
4246
  const [si, setSi] = useState7(0);
4056
4247
  const [sessSearch, setSessSearch] = useState7("");
4057
4248
  const [sessSel, setSessSel] = useState7(0);
4058
- const [confirm, setConfirm] = useState7(null);
4059
4249
  const [msg, setMsg] = useState7(null);
4060
4250
  const [showHelp, setShowHelp] = useState7(false);
4061
4251
  const [frozen, setFrozen] = useState7(false);
@@ -4137,34 +4327,11 @@ function AuditPanel({ active: active2, focused }) {
4137
4327
  const currentSess = sessionsFiltered[Math.min(sessSel, Math.max(0, sessionsFiltered.length - 1))];
4138
4328
  const logsLoading = (source === "cloud" ? cloudQ.loading : localQ.loading) && !editing;
4139
4329
  const sessLoading = source === "cloud" ? agentsQ.loading : localQ.loading;
4140
- const doDelete = (kind) => {
4141
- setMsg({ text: "deleting\u2026", level: "ok" });
4142
- const run = async () => {
4143
- if (source === "cloud") {
4144
- if (kind === "one") {
4145
- if (!current) throw new Error("nothing selected");
4146
- await api.audit.remove([current.id]);
4147
- } else {
4148
- await api.audit.removeAll();
4149
- }
4150
- cloudQ.reload();
4151
- statsQ.reloadQuiet();
4152
- } else {
4153
- const n = kind === "one" && current ? deleteLocalEntry(current.at, current.tool, current.session) : kind === "all" ? clearLocalLog() : 0;
4154
- if (!n) throw new Error("entry not found in the local file");
4155
- localQ.reload();
4156
- }
4157
- };
4158
- run().then(() => {
4159
- setMsg({ text: kind === "one" ? "\u2713 entry deleted" : `\u2713 ALL ${source} logs deleted`, level: "ok" });
4160
- toTop();
4161
- }).catch((e) => setMsg({ text: "\u2717 " + (e instanceof Error ? e.message : String(e)), level: "bad" }));
4162
- };
4163
4330
  const doExport = (kind) => {
4164
4331
  setMsg({ text: "exporting\u2026", level: "ok" });
4165
4332
  const run = async () => {
4166
- const dir = join6(homedir6(), ".solongate");
4167
- const file = join6(dir, `audit-export-${source}.jsonl`);
4333
+ const dir = join7(homedir7(), ".solongate");
4334
+ const file = join7(dir, `audit-export-${source}.jsonl`);
4168
4335
  let rows2;
4169
4336
  if (kind === "page") rows2 = pageRows;
4170
4337
  else if (source === "cloud") {
@@ -4210,8 +4377,7 @@ function AuditPanel({ active: active2, focused }) {
4210
4377
  return;
4211
4378
  }
4212
4379
  if (view === "logs" ? logsLoading : sessLoading) return;
4213
- if (confirm && input !== "x" && input !== "X") {
4214
- setConfirm(null);
4380
+ if (msg) {
4215
4381
  setMsg(null);
4216
4382
  }
4217
4383
  if (input === "s") {
@@ -4275,23 +4441,6 @@ function AuditPanel({ active: active2, focused }) {
4275
4441
  setGi((n) => (n + 1) % SIGNALS.length);
4276
4442
  setPage(0);
4277
4443
  toTop();
4278
- } else if (input === "x") {
4279
- if (!current) return;
4280
- if (confirm?.kind !== "one" || confirm.key !== current.id) {
4281
- setConfirm({ kind: "one", key: current.id });
4282
- setMsg({ text: `x = delete ONLY the selected entry: ${current.decision} ${current.tool} (${ago(current.at)} ago) \u2014 press x again`, level: "bad" });
4283
- return;
4284
- }
4285
- setConfirm(null);
4286
- doDelete("one");
4287
- } else if (input === "X") {
4288
- if (confirm?.kind !== "all") {
4289
- setConfirm({ kind: "all", key: "all" });
4290
- setMsg({ text: `\u26A0 X = delete ALL ${source} logs \u2014 every one of the ${total} matched entries! press X again`, level: "bad" });
4291
- return;
4292
- }
4293
- setConfirm(null);
4294
- doDelete("all");
4295
4444
  } else if (input === "e") doExport("page");
4296
4445
  else if (input === "E") doExport("all");
4297
4446
  else if (input === "t") setEditing("tool");
@@ -4556,10 +4705,11 @@ import { useEffect as useEffect7, useRef as useRef3, useState as useState8 } fro
4556
4705
  init_global_install();
4557
4706
 
4558
4707
  // src/commands/doctor.ts
4559
- import { existsSync as existsSync4, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
4560
- import { homedir as homedir7 } from "os";
4561
- 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";
4562
4711
  init_global_install();
4712
+ init_hook_health();
4563
4713
  async function collectChecks() {
4564
4714
  const checks = [];
4565
4715
  if (!isAuthenticated()) {
@@ -4607,8 +4757,32 @@ async function collectChecks() {
4607
4757
  ok: claudeReg,
4608
4758
  detail: claudeReg ? "guard registered" : "guard NOT registered - run `solongate repair`"
4609
4759
  });
4610
- if (existsSync4(globalPaths().antigravityDir)) {
4611
- 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);
4612
4786
  checks.push({
4613
4787
  name: "Antigravity hooks",
4614
4788
  ok: reg,
@@ -4636,7 +4810,7 @@ async function collectChecks() {
4636
4810
  });
4637
4811
  }
4638
4812
  try {
4639
- const raw = readFileSync5(join7(homedir7(), ".solongate", ".key-rejected.json"), "utf-8");
4813
+ const raw = readFileSync6(join8(homedir8(), ".solongate", ".key-rejected.json"), "utf-8");
4640
4814
  const m = JSON.parse(raw);
4641
4815
  const ageMin = m.ts ? Math.round((Date.now() - m.ts) / 6e4) : null;
4642
4816
  checks.push({
@@ -4647,8 +4821,8 @@ async function collectChecks() {
4647
4821
  } catch {
4648
4822
  }
4649
4823
  const LOCAL_LOG2 = localLogFile();
4650
- if (existsSync4(LOCAL_LOG2)) {
4651
- const st = statSync3(LOCAL_LOG2);
4824
+ if (existsSync5(LOCAL_LOG2)) {
4825
+ const st = statSync4(LOCAL_LOG2);
4652
4826
  const ageMin = (Date.now() - st.mtimeMs) / 6e4;
4653
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"}` });
4654
4828
  } else {
@@ -4659,17 +4833,17 @@ async function collectChecks() {
4659
4833
 
4660
4834
  // src/logs-server-daemon.ts
4661
4835
  import { spawn as spawn4 } from "child_process";
4662
- import { mkdirSync as mkdirSync5, openSync as openSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
4663
- import { homedir as homedir8 } from "os";
4664
- 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";
4665
4839
  import { fileURLToPath as fileURLToPath2 } from "url";
4666
- var DIR = join8(homedir8(), ".solongate");
4667
- var STATE_FILE = join8(DIR, ".logs-server.json");
4668
- 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");
4669
4843
  var LOGS_SERVER_PORT = 8788;
4670
4844
  function readState() {
4671
4845
  try {
4672
- const s = JSON.parse(readFileSync6(STATE_FILE, "utf-8"));
4846
+ const s = JSON.parse(readFileSync7(STATE_FILE, "utf-8"));
4673
4847
  return s && typeof s === "object" ? s : {};
4674
4848
  } catch {
4675
4849
  return {};
@@ -4705,7 +4879,7 @@ function startLogsServerDaemon() {
4705
4879
  try {
4706
4880
  mkdirSync5(DIR, { recursive: true });
4707
4881
  const log = openSync2(LOG_FILE, "a");
4708
- const cli = join8(dirname2(fileURLToPath2(import.meta.url)), "index.js");
4882
+ const cli = join9(dirname2(fileURLToPath2(import.meta.url)), "index.js");
4709
4883
  const p = spawn4(process.execPath, [cli, "logs-server"], {
4710
4884
  detached: true,
4711
4885
  stdio: ["ignore", log, log],
@@ -4736,22 +4910,22 @@ function stopLogsServerDaemon() {
4736
4910
  }
4737
4911
 
4738
4912
  // src/self-update.ts
4739
- import { execFile, execFileSync as execFileSync2, spawn as spawn5 } from "child_process";
4740
- import { access, mkdirSync as mkdirSync6, openSync as openSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync7, constants as FS } from "fs";
4741
- import { homedir as homedir9 } from "os";
4742
- 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";
4743
4917
  import { fileURLToPath as fileURLToPath3 } from "url";
4744
4918
  var PKG = "@solongate/proxy";
4745
4919
  var CHECK_EVERY_MS = 30 * 60 * 1e3;
4746
4920
  var ATTEMPT_EVERY_MS = 6 * 60 * 60 * 1e3;
4747
- var STATE_FILE2 = join9(homedir9(), ".solongate", ".self-update.json");
4748
- var LOG_FILE2 = join9(homedir9(), ".solongate", "self-update.log");
4749
- 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");
4750
4924
  var LOCK_STALE_MS = 3 * 6e4;
4751
4925
  var NEEDS_ADMIN_RE = /\bEACCES\b|\bEPERM\b|permission denied|operation not permitted/i;
4752
4926
  function readState2() {
4753
4927
  try {
4754
- const s = JSON.parse(readFileSync7(STATE_FILE2, "utf-8"));
4928
+ const s = JSON.parse(readFileSync8(STATE_FILE2, "utf-8"));
4755
4929
  return s && typeof s === "object" ? s : {};
4756
4930
  } catch {
4757
4931
  return {};
@@ -4759,7 +4933,7 @@ function readState2() {
4759
4933
  }
4760
4934
  function writeState2(s) {
4761
4935
  try {
4762
- mkdirSync6(join9(homedir9(), ".solongate"), { recursive: true });
4936
+ mkdirSync6(join10(homedir10(), ".solongate"), { recursive: true });
4763
4937
  writeFileSync7(STATE_FILE2, JSON.stringify(s));
4764
4938
  } catch {
4765
4939
  }
@@ -4780,7 +4954,7 @@ function setAutoUpdate(on) {
4780
4954
  }
4781
4955
  function currentVersion() {
4782
4956
  try {
4783
- 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"));
4784
4958
  return pkg.version ?? "0.0.0";
4785
4959
  } catch {
4786
4960
  return "0.0.0";
@@ -4817,7 +4991,7 @@ async function fetchLatest() {
4817
4991
  function runGlobalInstall(version) {
4818
4992
  return new Promise((resolve4) => {
4819
4993
  try {
4820
- mkdirSync6(join9(homedir9(), ".solongate"), { recursive: true });
4994
+ mkdirSync6(join10(homedir10(), ".solongate"), { recursive: true });
4821
4995
  execFile(
4822
4996
  "npm",
4823
4997
  ["install", "-g", `${PKG}@${version}`],
@@ -5981,11 +6155,11 @@ async function launchTui() {
5981
6155
  return;
5982
6156
  }
5983
6157
  process.stdout.write("\x1B[?1049h\x1B[H");
5984
- const debugLog = join10(homedir10(), ".solongate", "dataroom-debug.log");
6158
+ const debugLog = join11(homedir11(), ".solongate", "dataroom-debug.log");
5985
6159
  const saved = { log: console.log, warn: console.warn, error: console.error, info: console.info, debug: console.debug };
5986
6160
  const toFile = (level) => (...args) => {
5987
6161
  try {
5988
- mkdirSync7(join10(homedir10(), ".solongate"), { recursive: true });
6162
+ mkdirSync7(join11(homedir11(), ".solongate"), { recursive: true });
5989
6163
  appendFileSync(debugLog, `${(/* @__PURE__ */ new Date()).toISOString()} [${level}] ${args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ")}
5990
6164
  `);
5991
6165
  } catch {