@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.
@@ -1,37 +1,218 @@
1
1
  // src/global-install.ts
2
- import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, rmdirSync, readdirSync, statSync, chmodSync, copyFileSync, renameSync } from "fs";
3
- import { resolve, join, dirname } from "path";
4
- import { homedir } from "os";
2
+ import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync2, mkdirSync, rmSync, rmdirSync, readdirSync, statSync as statSync2, chmodSync, copyFileSync, renameSync } from "fs";
3
+ import { resolve, join as join2, dirname, basename } from "path";
4
+ import { homedir as homedir2 } from "os";
5
5
  import { createRequire } from "module";
6
6
  import { fileURLToPath } from "url";
7
7
  import { createInterface } from "readline";
8
- import { execFileSync, spawn } from "child_process";
8
+ import { execFileSync as execFileSync2, spawn } from "child_process";
9
+
10
+ // src/hook-launcher.ts
11
+ var NODE_CANDIDATES = [
12
+ // Homebrew, by the STABLE symlink rather than the Cellar path behind it —
13
+ // the exact distinction this file exists for. Apple Silicon then Intel.
14
+ "/opt/homebrew/bin/node",
15
+ "/usr/local/bin/node",
16
+ // Homebrew's keg-only layout, both prefixes.
17
+ "/opt/homebrew/opt/node/bin/node",
18
+ "/usr/local/opt/node/bin/node",
19
+ // Distro and hand-built.
20
+ "/usr/bin/node",
21
+ "/usr/local/n/versions/node/*/bin/node",
22
+ "/snap/bin/node"
23
+ ];
24
+ var NODE_GLOBS = [
25
+ // nvm. NVM_DIR is exported by its own shell hook, which a non-interactive
26
+ // hook environment does not run, so the default location is tried too.
27
+ '"${NVM_DIR:-}"/versions/node/*/bin/node',
28
+ '"$home"/.nvm/versions/node/*/bin/node',
29
+ // fnm, both the XDG location and the macOS Application Support one.
30
+ '"$home"/.local/share/fnm/node-versions/*/installation/bin/node',
31
+ '"$home"/Library/Application Support/fnm/node-versions/*/installation/bin/node',
32
+ // Volta.
33
+ '"$home"/.volta/tools/image/node/*/bin/node',
34
+ // asdf, old layout and the current plugin one.
35
+ '"$home"/.asdf/installs/nodejs/*/bin/node',
36
+ '"$home"/.asdf/installs/node/*/bin/node'
37
+ ];
38
+ var LAUNCHER_NAME = "sg-run.sh";
39
+ var BEAT_DIR = ".beat";
40
+ function launcherScript(pinnedNode) {
41
+ const pinned = pinnedNode.replace(/'/g, `'\\''`);
42
+ return `#!/bin/sh
43
+ # SolonGate hook launcher \u2014 generated by \`solongate init --global\` / \`repair\`.
44
+ #
45
+ # Finds a working node and execs the hook with it. Do not edit: a reinstall
46
+ # overwrites this file, and on macOS it is chflags-locked besides.
47
+ #
48
+ # The reason it exists rather than the hook naming a node directly: an absolute
49
+ # node path recorded at install time is a Homebrew Cellar path or an nvm version
50
+ # directory, and both are deleted by a routine upgrade. The hook then could not
51
+ # start, nothing was enforced, nothing was logged, and every check still said
52
+ # the guard was registered \u2014 because it was.
53
+
54
+ set -u
55
+
56
+ script="\${1:-}"
57
+ [ -n "$script" ] || { echo "solongate: launcher called with no hook script" >&2; exit 1; }
58
+ shift
59
+
60
+ hook=\${script##*/}
61
+ home=\${HOME:-~}
62
+ beatdir="$home/.solongate/${BEAT_DIR}"
63
+
64
+ # The beat. Written BEFORE node is resolved, because its whole job is to record
65
+ # that the client invoked us \u2014 which is true even when everything after this
66
+ # line fails. The file's modification time is the timestamp; nothing is forked
67
+ # to produce one, and the directory test is a builtin.
68
+ #
69
+ # The braces matter. A redirect into a missing directory is reported by the
70
+ # SHELL, before the command runs, so a \`2>/dev/null\` on the printf alone does
71
+ # not suppress it \u2014 it lands on the hook's stderr, and Claude Code shows a
72
+ # hook's stderr to the person using it. Redirecting the group catches both.
73
+ beat() {
74
+ [ -d "$beatdir" ] || mkdir -p "$beatdir" 2>/dev/null || return 0
75
+ { printf '%s\\n' "$1" > "$beatdir/$hook"; } 2>/dev/null || true
76
+ }
77
+
78
+ try() {
79
+ [ -n "\${1:-}" ] && [ -x "$1" ]
80
+ }
81
+
82
+ resolve_node() {
83
+ # Told explicitly.
84
+ if try "\${SOLONGATE_NODE:-}"; then echo "$SOLONGATE_NODE"; return 0; fi
85
+ # The node this was installed with.
86
+ if try '${pinned}'; then echo '${pinned}'; return 0; fi
87
+ # PATH, when the client gave us one worth having.
88
+ p=$(command -v node 2>/dev/null) || p=
89
+ if try "$p"; then echo "$p"; return 0; fi
90
+ # The fixed locations.
91
+ for c in ${NODE_CANDIDATES.map((c) => `"${c}"`).join(" ")}; do
92
+ for g in $c; do
93
+ if try "$g"; then echo "$g"; return 0; fi
94
+ done
95
+ done
96
+ # The version managers.
97
+ for c in ${NODE_GLOBS.join(" ")}; do
98
+ for g in $c; do
99
+ if try "$g"; then echo "$g"; return 0; fi
100
+ done
101
+ done
102
+ return 1
103
+ }
104
+
105
+ node_bin=$(resolve_node) || node_bin=
106
+
107
+ # --sg-doctor: report what would be used and leave. This is what \`solongate
108
+ # doctor\` runs, so the health check exercises the REAL resolution rather than a
109
+ # copy of it that can drift.
110
+ if [ "$script" = "--sg-doctor" ] || [ "\${1:-}" = "--sg-doctor" ]; then
111
+ [ -n "$node_bin" ] && { echo "$node_bin"; exit 0; }
112
+ echo "no node found" >&2
113
+ exit 1
114
+ fi
115
+
116
+ if [ -z "$node_bin" ]; then
117
+ beat "no-node"
118
+ # Nothing can be enforced without node, and which way to fail is not a
119
+ # judgement call: the guard is fail-closed, so it refuses the call and says
120
+ # why. Everything else here only records what already happened, and refusing
121
+ # a tool call because the log could not be written would be a worse product
122
+ # than a gap in the log.
123
+ echo "solongate: no node runtime found, so $hook did not run." >&2
124
+ echo "solongate: run \\\`solongate repair\\\` in a terminal, or set SOLONGATE_NODE to your node binary." >&2
125
+ case "$hook" in
126
+ guard.mjs) echo "solongate: this tool call is REFUSED \u2014 the guard is fail-closed." >&2; exit 2 ;;
127
+ *) exit 0 ;;
128
+ esac
129
+ fi
130
+
131
+ beat "$node_bin"
132
+ exec "$node_bin" "$script" "$@"
133
+ `;
134
+ }
135
+
136
+ // src/hook-health.ts
137
+ import { execFileSync } from "child_process";
138
+ import { existsSync, readFileSync, statSync } from "fs";
139
+ import { join } from "path";
140
+ import { homedir } from "os";
141
+ var sgDir = () => join(homedir(), ".solongate");
142
+ var hooksDir = () => join(sgDir(), "hooks");
143
+ function hookCanStart() {
144
+ const launcher = join(hooksDir(), LAUNCHER_NAME);
145
+ if (process.platform === "win32") {
146
+ const ok = existsSync(process.execPath);
147
+ return { ok, node: process.execPath, detail: ok ? process.execPath : `${process.execPath} is gone` };
148
+ }
149
+ if (!existsSync(launcher)) {
150
+ return { ok: false, detail: "hook launcher missing - run `solongate repair`" };
151
+ }
152
+ try {
153
+ const out = execFileSync("/bin/sh", [launcher, "--sg-doctor"], {
154
+ encoding: "utf-8",
155
+ timeout: 5e3,
156
+ stdio: ["ignore", "pipe", "pipe"]
157
+ }).trim();
158
+ if (!out) return { ok: false, detail: "launcher found no node runtime - set SOLONGATE_NODE or install node" };
159
+ return { ok: true, node: out, detail: out };
160
+ } catch (e) {
161
+ const msg = e instanceof Error ? e.message : String(e);
162
+ return { ok: false, detail: `launcher will not run: ${msg.split("\n")[0]}` };
163
+ }
164
+ }
165
+ function hookBeats() {
166
+ const dir = join(sgDir(), BEAT_DIR);
167
+ const out = [];
168
+ for (const hook of ["guard.mjs", "audit.mjs", "conversation.mjs", "stop.mjs"]) {
169
+ const f = join(dir, hook);
170
+ try {
171
+ const st = statSync(f);
172
+ out.push({ hook, at: st.mtime, node: readFileSync(f, "utf-8").trim() });
173
+ } catch {
174
+ }
175
+ }
176
+ return out.sort((a, b) => b.at.getTime() - a.at.getTime());
177
+ }
178
+ function guardBeat() {
179
+ return hookBeats().find((b) => b.hook === "guard.mjs") ?? null;
180
+ }
181
+ function agoLabel(d) {
182
+ const s = Math.max(0, Math.round((Date.now() - d.getTime()) / 1e3));
183
+ if (s < 60) return s <= 3 ? "just now" : `${s}s ago`;
184
+ if (s < 3600) return `${Math.round(s / 60)}m ago`;
185
+ if (s < 86400) return `${Math.round(s / 3600)}h ago`;
186
+ return `${Math.round(s / 86400)}d ago`;
187
+ }
188
+
189
+ // src/global-install.ts
9
190
  var __dirname = dirname(fileURLToPath(import.meta.url));
10
191
  var HOOKS_DIR = resolve(__dirname, "..", "hooks");
11
192
  function lockFile(file) {
12
- if (!existsSync(file)) return;
193
+ if (!existsSync2(file)) return;
13
194
  try {
14
195
  if (process.platform === "win32") {
15
196
  try {
16
- execFileSync("icacls", [file, "/deny", "*S-1-1-0:(WD,AD,DC,DE,WDAC,WO)"], { stdio: "ignore" });
197
+ execFileSync2("icacls", [file, "/deny", "*S-1-1-0:(WD,AD,DC,DE,WDAC,WO)"], { stdio: "ignore" });
17
198
  } catch {
18
199
  }
19
200
  try {
20
- execFileSync("icacls", [file, "/grant", "*S-1-3-4:(RX)"], { stdio: "ignore" });
201
+ execFileSync2("icacls", [file, "/grant", "*S-1-3-4:(RX)"], { stdio: "ignore" });
21
202
  } catch {
22
203
  }
23
204
  try {
24
- execFileSync("attrib", ["+R", file], { stdio: "ignore" });
205
+ execFileSync2("attrib", ["+R", file], { stdio: "ignore" });
25
206
  } catch {
26
207
  }
27
208
  } else if (process.platform === "darwin") {
28
209
  try {
29
- execFileSync("chflags", ["uchg", file], { stdio: "ignore" });
210
+ execFileSync2("chflags", ["uchg", file], { stdio: "ignore" });
30
211
  } catch {
31
212
  }
32
213
  } else {
33
214
  try {
34
- execFileSync("chattr", ["+i", file], { stdio: "ignore" });
215
+ execFileSync2("chattr", ["+i", file], { stdio: "ignore" });
35
216
  } catch {
36
217
  }
37
218
  try {
@@ -43,33 +224,33 @@ function lockFile(file) {
43
224
  }
44
225
  }
45
226
  function unlockFile(file) {
46
- if (!existsSync(file)) return;
227
+ if (!existsSync2(file)) return;
47
228
  try {
48
229
  if (process.platform === "win32") {
49
230
  try {
50
- execFileSync("icacls", [file, "/remove:g", "*S-1-3-4"], { stdio: "ignore" });
231
+ execFileSync2("icacls", [file, "/remove:g", "*S-1-3-4"], { stdio: "ignore" });
51
232
  } catch {
52
233
  }
53
234
  try {
54
- execFileSync("icacls", [file, "/remove:d", "*S-1-1-0"], { stdio: "ignore" });
235
+ execFileSync2("icacls", [file, "/remove:d", "*S-1-1-0"], { stdio: "ignore" });
55
236
  } catch {
56
237
  }
57
238
  try {
58
- execFileSync("icacls", [file, "/reset"], { stdio: "ignore" });
239
+ execFileSync2("icacls", [file, "/reset"], { stdio: "ignore" });
59
240
  } catch {
60
241
  }
61
242
  try {
62
- execFileSync("attrib", ["-R", file], { stdio: "ignore" });
243
+ execFileSync2("attrib", ["-R", file], { stdio: "ignore" });
63
244
  } catch {
64
245
  }
65
246
  } else if (process.platform === "darwin") {
66
247
  try {
67
- execFileSync("chflags", ["nouchg", file], { stdio: "ignore" });
248
+ execFileSync2("chflags", ["nouchg", file], { stdio: "ignore" });
68
249
  } catch {
69
250
  }
70
251
  } else {
71
252
  try {
72
- execFileSync("chattr", ["-i", file], { stdio: "ignore" });
253
+ execFileSync2("chattr", ["-i", file], { stdio: "ignore" });
73
254
  } catch {
74
255
  }
75
256
  try {
@@ -83,14 +264,19 @@ function unlockFile(file) {
83
264
  function protectedTargets() {
84
265
  const p = globalPaths();
85
266
  return [
86
- join(p.hooksDir, "guard.mjs"),
87
- join(p.hooksDir, "audit.mjs"),
88
- join(p.hooksDir, "stop.mjs"),
89
- join(p.hooksDir, "shield.mjs"),
267
+ join2(p.hooksDir, "guard.mjs"),
268
+ join2(p.hooksDir, "audit.mjs"),
269
+ join2(p.hooksDir, "stop.mjs"),
270
+ join2(p.hooksDir, "shield.mjs"),
90
271
  // The conversation record is locked with the rest. A guest who could edit
91
272
  // it could decide what their host sees them say, which is the same class of
92
273
  // problem as editing the guard.
93
- join(p.hooksDir, "conversation.mjs"),
274
+ join2(p.hooksDir, "conversation.mjs"),
275
+ // The launcher is the enforcement path now: every hook command in every
276
+ // client config runs THROUGH it. A program that could rewrite it could
277
+ // point every hook at /bin/true and disarm the guard without touching a
278
+ // single file that used to be locked.
279
+ join2(p.hooksDir, LAUNCHER_NAME),
94
280
  p.configPath,
95
281
  p.settingsPath,
96
282
  p.antigravityHooksPath,
@@ -124,60 +310,60 @@ function writeProtectedFile(file, contents) {
124
310
  }
125
311
  }
126
312
  function globalPaths() {
127
- const home = homedir();
128
- const sgDir = join(home, ".solongate");
129
- const hooksDir = join(sgDir, "hooks");
130
- const claudeDir = join(home, ".claude");
131
- const antigravityDir = join(home, ".gemini", "config");
132
- const codexDir = process.env["CODEX_HOME"] ? resolve(process.env["CODEX_HOME"]) : join(home, ".codex");
133
- const opencodeDir = join(process.env["XDG_CONFIG_HOME"] ? resolve(process.env["XDG_CONFIG_HOME"]) : join(home, ".config"), "opencode");
134
- const binDir = join(sgDir, "bin");
313
+ const home = homedir2();
314
+ const sgDir2 = join2(home, ".solongate");
315
+ const hooksDir2 = join2(sgDir2, "hooks");
316
+ const claudeDir = join2(home, ".claude");
317
+ const antigravityDir = join2(home, ".gemini", "config");
318
+ const codexDir = process.env["CODEX_HOME"] ? resolve(process.env["CODEX_HOME"]) : join2(home, ".codex");
319
+ const opencodeDir = join2(process.env["XDG_CONFIG_HOME"] ? resolve(process.env["XDG_CONFIG_HOME"]) : join2(home, ".config"), "opencode");
320
+ const binDir = join2(sgDir2, "bin");
135
321
  return {
136
322
  home,
137
- sgDir,
138
- hooksDir,
323
+ sgDir: sgDir2,
324
+ hooksDir: hooksDir2,
139
325
  binDir,
140
326
  claudeDir,
141
327
  antigravityDir,
142
328
  codexDir,
143
329
  opencodeDir,
144
- settingsPath: join(claudeDir, "settings.json"),
145
- backupPath: join(claudeDir, "settings.solongate.bak"),
146
- configPath: join(sgDir, "cloud-guard.json"),
330
+ settingsPath: join2(claudeDir, "settings.json"),
331
+ backupPath: join2(claudeDir, "settings.solongate.bak"),
332
+ configPath: join2(sgDir2, "cloud-guard.json"),
147
333
  // Antigravity reads global hooks from ~/.gemini/config/hooks.json. Only the
148
334
  // guard is registered there (PreToolUse); Antigravity's ALLOW-path audit is
149
335
  // covered by the passive session-log collector, not a hook.
150
- antigravityHooksPath: join(antigravityDir, "hooks.json"),
151
- antigravityBackupPath: join(antigravityDir, "hooks.solongate.bak"),
336
+ antigravityHooksPath: join2(antigravityDir, "hooks.json"),
337
+ antigravityBackupPath: join2(antigravityDir, "hooks.solongate.bak"),
152
338
  // Codex reads user-level hooks from ~/.codex/hooks.json (or a [hooks] table
153
339
  // in ~/.codex/config.toml — we use the JSON file so we never have to rewrite
154
340
  // the user's TOML, which also holds the hook TRUST state Codex manages).
155
- codexHooksPath: join(codexDir, "hooks.json"),
156
- codexBackupPath: join(codexDir, "hooks.solongate.bak"),
157
- codexConfigPath: join(codexDir, "config.toml"),
341
+ codexHooksPath: join2(codexDir, "hooks.json"),
342
+ codexBackupPath: join2(codexDir, "hooks.solongate.bak"),
343
+ codexConfigPath: join2(codexDir, "config.toml"),
158
344
  // OpenCode scans its plugin folder at startup and loads every module in it.
159
345
  // Measured on 1.18.10: BOTH `plugin/` and `plugins/` are scanned, so the
160
346
  // docs and the field reports are each half right. We write the documented
161
347
  // one. There is nothing to register anywhere — dropping the file IS the
162
348
  // installation, which also means deleting the file IS the uninstall.
163
- opencodePluginDir: join(opencodeDir, "plugins"),
164
- opencodePluginPath: join(opencodeDir, "plugins", "solongate.js")
349
+ opencodePluginDir: join2(opencodeDir, "plugins"),
350
+ opencodePluginPath: join2(opencodeDir, "plugins", "solongate.js")
165
351
  };
166
352
  }
167
353
  function clearGuardUpdateCheck() {
168
354
  try {
169
- rmSync(join(globalPaths().sgDir, ".hook-update-check"), { force: true });
355
+ rmSync(join2(globalPaths().sgDir, ".hook-update-check"), { force: true });
170
356
  return true;
171
357
  } catch {
172
358
  return false;
173
359
  }
174
360
  }
175
361
  function readHook(filename) {
176
- return readFileSync(join(HOOKS_DIR, filename), "utf-8");
362
+ return readFileSync2(join2(HOOKS_DIR, filename), "utf-8");
177
363
  }
178
364
  function firstAccountCredential() {
179
365
  try {
180
- const raw = JSON.parse(readFileSync(join(homedir(), ".solongate", "accounts.json"), "utf-8"));
366
+ const raw = JSON.parse(readFileSync2(join2(homedir2(), ".solongate", "accounts.json"), "utf-8"));
181
367
  if (Array.isArray(raw)) {
182
368
  const acc = raw.find((a) => a && typeof a.apiKey === "string" && a.apiKey);
183
369
  if (acc) return { apiKey: acc.apiKey, apiUrl: typeof acc.apiUrl === "string" ? acc.apiUrl : void 0 };
@@ -187,8 +373,8 @@ function firstAccountCredential() {
187
373
  return {};
188
374
  }
189
375
  function readGuard() {
190
- const bundled = join(HOOKS_DIR, "guard.bundled.mjs");
191
- return existsSync(bundled) ? readFileSync(bundled, "utf-8") : readHook("guard.mjs");
376
+ const bundled = join2(HOOKS_DIR, "guard.bundled.mjs");
377
+ return existsSync2(bundled) ? readFileSync2(bundled, "utf-8") : readHook("guard.mjs");
192
378
  }
193
379
  function installGoBinaries(binDir) {
194
380
  const os_ = process.platform === "win32" ? "win32" : process.platform;
@@ -208,10 +394,10 @@ function installGoBinaries(binDir) {
208
394
  return placed;
209
395
  }
210
396
  for (const name of ["solongate-guard", "solongate"]) {
211
- const from = join(pkgDir, name + suffix);
212
- const to = join(binDir, name + suffix);
397
+ const from = join2(pkgDir, name + suffix);
398
+ const to = join2(binDir, name + suffix);
213
399
  try {
214
- if (!existsSync(from)) continue;
400
+ if (!existsSync2(from)) continue;
215
401
  const tmp = to + ".new";
216
402
  copyFileSync(from, tmp);
217
403
  chmodSync(tmp, 493);
@@ -224,16 +410,17 @@ function installGoBinaries(binDir) {
224
410
  }
225
411
  var ANTIGRAVITY_GROUP = "solongate-guard";
226
412
  function antigravityHookCommand(guardAbs) {
227
- const nodeBin = process.execPath.replace(/\\/g, "/");
228
- const call = process.platform === "win32" ? "& " : "";
229
- return `${call}"${nodeBin}" "${guardAbs.replace(/\\/g, "/")}" antigravity "Antigravity"`;
413
+ return hookCommandFor(dirname(guardAbs), basename(guardAbs), "antigravity", "Antigravity");
414
+ }
415
+ function antigravityConversationCommand(guardAbs) {
416
+ return hookCommandFor(dirname(guardAbs), "conversation.mjs", "antigravity", "Antigravity");
230
417
  }
231
418
  function installAntigravityGuard(p, guardAbs) {
232
419
  mkdirSync(p.antigravityDir, { recursive: true });
233
420
  let existing = {};
234
- if (existsSync(p.antigravityHooksPath)) {
235
- const raw = readFileSync(p.antigravityHooksPath, "utf-8");
236
- if (!existsSync(p.antigravityBackupPath)) writeFileSync(p.antigravityBackupPath, raw);
421
+ if (existsSync2(p.antigravityHooksPath)) {
422
+ const raw = readFileSync2(p.antigravityHooksPath, "utf-8");
423
+ if (!existsSync2(p.antigravityBackupPath)) writeFileSync(p.antigravityBackupPath, raw);
237
424
  try {
238
425
  existing = JSON.parse(raw);
239
426
  } catch {
@@ -243,15 +430,16 @@ function installAntigravityGuard(p, guardAbs) {
243
430
  const merged = {
244
431
  ...existing,
245
432
  [ANTIGRAVITY_GROUP]: {
246
- PreToolUse: [{ matcher: "", hooks: [{ type: "command", command: antigravityHookCommand(guardAbs) }] }]
433
+ PreToolUse: [{ matcher: "", hooks: [{ type: "command", command: antigravityHookCommand(guardAbs) }] }],
434
+ Stop: [{ matcher: "", hooks: [{ type: "command", command: antigravityConversationCommand(guardAbs) }] }]
247
435
  }
248
436
  };
249
437
  writeFileSync(p.antigravityHooksPath, JSON.stringify(merged, null, 2) + "\n");
250
438
  }
251
439
  function removeAntigravityGuard(p) {
252
- if (!existsSync(p.antigravityHooksPath)) return;
440
+ if (!existsSync2(p.antigravityHooksPath)) return;
253
441
  try {
254
- const s = JSON.parse(readFileSync(p.antigravityHooksPath, "utf-8"));
442
+ const s = JSON.parse(readFileSync2(p.antigravityHooksPath, "utf-8"));
255
443
  if (!(ANTIGRAVITY_GROUP in s)) return;
256
444
  delete s[ANTIGRAVITY_GROUP];
257
445
  writeFileSync(p.antigravityHooksPath, JSON.stringify(s, null, 2) + "\n");
@@ -261,8 +449,10 @@ function removeAntigravityGuard(p) {
261
449
  var CODEX_EVENTS = ["PreToolUse", "PostToolUse", "UserPromptSubmit", "Stop"];
262
450
  var CODEX_TIMEOUT_SEC = 30;
263
451
  function codexHookCommand(scriptAbs) {
264
- const nodeBin = process.execPath.replace(/\\/g, "/");
265
- return `"${nodeBin}" "${scriptAbs.replace(/\\/g, "/")}" codex "Codex"`;
452
+ if (process.platform === "win32") {
453
+ return `"${process.execPath.replace(/\\/g, "/")}" "${scriptAbs.replace(/\\/g, "/")}" codex "Codex"`;
454
+ }
455
+ return hookCommandFor(dirname(scriptAbs), basename(scriptAbs), "codex", "Codex");
266
456
  }
267
457
  function isOurCodexGroup(group) {
268
458
  const hooks = Array.isArray(group?.hooks) ? group.hooks : [];
@@ -270,10 +460,10 @@ function isOurCodexGroup(group) {
270
460
  }
271
461
  function readCodexHooksFile(path) {
272
462
  const out = { hooks: {}, rest: {} };
273
- if (!existsSync(path)) return out;
463
+ if (!existsSync2(path)) return out;
274
464
  let raw;
275
465
  try {
276
- raw = JSON.parse(readFileSync(path, "utf-8"));
466
+ raw = JSON.parse(readFileSync2(path, "utf-8"));
277
467
  } catch {
278
468
  return out;
279
469
  }
@@ -318,10 +508,10 @@ function writeCodexHooksFile(path, file) {
318
508
  body["hooks"] = hooks;
319
509
  writeFileSync(path, JSON.stringify(body, null, 2) + "\n");
320
510
  }
321
- function installCodexGuard(p, hooksDir) {
511
+ function installCodexGuard(p, hooksDir2) {
322
512
  mkdirSync(p.codexDir, { recursive: true });
323
- if (existsSync(p.codexHooksPath) && !existsSync(p.codexBackupPath)) {
324
- writeFileSync(p.codexBackupPath, readFileSync(p.codexHooksPath, "utf-8"));
513
+ if (existsSync2(p.codexHooksPath) && !existsSync2(p.codexBackupPath)) {
514
+ writeFileSync(p.codexBackupPath, readFileSync2(p.codexHooksPath, "utf-8"));
325
515
  }
326
516
  const file = readCodexHooksFile(p.codexHooksPath);
327
517
  const script = {
@@ -345,7 +535,7 @@ function installCodexGuard(p, hooksDir) {
345
535
  ...ev === "Stop" ? {} : { matcher: "*" },
346
536
  hooks: [{
347
537
  type: "command",
348
- command: codexHookCommand(join(hooksDir, script[ev]).replace(/\\/g, "/")),
538
+ command: codexHookCommand(join2(hooksDir2, script[ev]).replace(/\\/g, "/")),
349
539
  timeout: CODEX_TIMEOUT_SEC,
350
540
  statusMessage: status[ev]
351
541
  }]
@@ -355,7 +545,7 @@ function installCodexGuard(p, hooksDir) {
355
545
  writeCodexHooksFile(p.codexHooksPath, file);
356
546
  }
357
547
  function removeCodexGuard(p) {
358
- if (!existsSync(p.codexHooksPath)) return;
548
+ if (!existsSync2(p.codexHooksPath)) return;
359
549
  try {
360
550
  const file = readCodexHooksFile(p.codexHooksPath);
361
551
  let changed = false;
@@ -381,7 +571,7 @@ function isCodexGuardInstalled() {
381
571
  }
382
572
  function codexDetected() {
383
573
  try {
384
- return existsSync(globalPaths().codexDir);
574
+ return existsSync2(globalPaths().codexDir);
385
575
  } catch {
386
576
  return false;
387
577
  }
@@ -391,7 +581,7 @@ function codexHooksStatus() {
391
581
  const registered = isCodexGuardInstalled();
392
582
  let trusted = false, disabled = false;
393
583
  try {
394
- const toml = readFileSync(p.codexConfigPath, "utf-8");
584
+ const toml = readFileSync2(p.codexConfigPath, "utf-8");
395
585
  trusted = /trusted_hash\s*=/.test(toml);
396
586
  disabled = /^\s*hooks\s*=\s*false\s*$/m.test(toml);
397
587
  } catch {
@@ -410,10 +600,10 @@ function removeOpencodeGuard(p) {
410
600
  } catch {
411
601
  }
412
602
  }
413
- function warmPolicyCache(hooksDir, agents) {
603
+ function warmPolicyCache(hooksDir2, agents) {
414
604
  for (const agent of agents) {
415
605
  try {
416
- const child = spawn(process.execPath, [join(hooksDir, "guard.mjs"), agent, "--sg-refresh-policy"], {
606
+ const child = spawn(process.execPath, [join2(hooksDir2, "guard.mjs"), agent, "--sg-refresh-policy"], {
417
607
  detached: true,
418
608
  stdio: "ignore",
419
609
  windowsHide: true
@@ -427,24 +617,24 @@ function warmPolicyCache(hooksDir, agents) {
427
617
  }
428
618
  function isOpencodeGuardInstalled() {
429
619
  try {
430
- return readFileSync(globalPaths().opencodePluginPath, "utf-8").includes("tool.execute.before");
620
+ return readFileSync2(globalPaths().opencodePluginPath, "utf-8").includes("tool.execute.before");
431
621
  } catch {
432
622
  return false;
433
623
  }
434
624
  }
435
625
  function opencodeDetected() {
436
626
  try {
437
- return existsSync(globalPaths().opencodeDir);
627
+ return existsSync2(globalPaths().opencodeDir);
438
628
  } catch {
439
629
  return false;
440
630
  }
441
631
  }
442
632
  var SCRATCH_FILES = /* @__PURE__ */ new Set([".eval-ring.jsonl", ".last-eval", ".last-deny", ".last-tool-call", ".debug-guard-log"]);
443
- function sweepStrayScratchDirs(root = homedir(), maxDepth = 6, budget = 4e4) {
633
+ function sweepStrayScratchDirs(root = homedir2(), maxDepth = 6, budget = 4e4) {
444
634
  let removed = 0;
445
635
  let visited = 0;
446
636
  const skip = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".next", "build"]);
447
- const store = join(homedir(), ".solongate");
637
+ const store = join2(homedir2(), ".solongate");
448
638
  const walk = (dir, depth) => {
449
639
  if (depth > maxDepth || visited++ > budget) return;
450
640
  let entries;
@@ -455,11 +645,11 @@ function sweepStrayScratchDirs(root = homedir(), maxDepth = 6, budget = 4e4) {
455
645
  }
456
646
  for (const name of entries) {
457
647
  if (skip.has(name)) continue;
458
- const full = join(dir, name);
648
+ const full = join2(dir, name);
459
649
  if (full === store) continue;
460
650
  let isDir = false;
461
651
  try {
462
- isDir = statSync(full).isDirectory();
652
+ isDir = statSync2(full).isDirectory();
463
653
  } catch {
464
654
  continue;
465
655
  }
@@ -470,7 +660,7 @@ function sweepStrayScratchDirs(root = homedir(), maxDepth = 6, budget = 4e4) {
470
660
  for (const f of readdirSync(full)) {
471
661
  if (SCRATCH_FILES.has(f)) {
472
662
  try {
473
- rmSync(join(full, f), { force: true });
663
+ rmSync(join2(full, f), { force: true });
474
664
  } catch {
475
665
  left.push(f);
476
666
  }
@@ -510,12 +700,12 @@ function runGlobalRestore() {
510
700
  removeOpencodeGuard(p);
511
701
  } catch {
512
702
  }
513
- if (existsSync(p.backupPath)) {
514
- writeFileSync(p.settingsPath, readFileSync(p.backupPath, "utf-8"));
703
+ if (existsSync2(p.backupPath)) {
704
+ writeFileSync(p.settingsPath, readFileSync2(p.backupPath, "utf-8"));
515
705
  console.log(` Restored ${p.settingsPath} from backup.`);
516
- } else if (existsSync(p.settingsPath)) {
706
+ } else if (existsSync2(p.settingsPath)) {
517
707
  try {
518
- const s = JSON.parse(readFileSync(p.settingsPath, "utf-8"));
708
+ const s = JSON.parse(readFileSync2(p.settingsPath, "utf-8"));
519
709
  delete s.hooks;
520
710
  writeFileSync(p.settingsPath, JSON.stringify(s, null, 2) + "\n");
521
711
  console.log(` Removed SolonGate hooks from ${p.settingsPath}.`);
@@ -528,12 +718,17 @@ function runGlobalRestore() {
528
718
  }
529
719
  function repairQuiet() {
530
720
  const p = globalPaths();
531
- const has = (f) => existsSync(f);
532
- const guardFile = join(p.hooksDir, "guard.mjs");
721
+ const has = (f) => existsSync2(f);
722
+ const guardFile = join2(p.hooksDir, "guard.mjs");
533
723
  const line = (label, ok, yes, no) => ({ label, ok, detail: ok ? yes : no });
724
+ const runtime = () => {
725
+ const r2 = hookCanStart();
726
+ return { label: "hook runtime", ok: r2.ok, detail: r2.ok ? `node ${r2.detail}` : r2.detail };
727
+ };
534
728
  const before = [
535
729
  line("guard hook file", has(guardFile), "present", "MISSING"),
536
730
  line("cloud credential", has(p.configPath), "present", "MISSING"),
731
+ runtime(),
537
732
  line("Claude hooks", isGuardInstalled(), "guard registered", "guard NOT registered"),
538
733
  line("Antigravity hooks", has(p.antigravityHooksPath), "guard registered", "guard NOT registered"),
539
734
  line("Codex hooks", isCodexGuardInstalled(), "guard registered", "guard NOT registered"),
@@ -543,6 +738,7 @@ function repairQuiet() {
543
738
  if (!r.ok) return { ok: false, message: r.message, before, after: [], notes: [] };
544
739
  const after = [
545
740
  { label: "guard hook file", ok: true, detail: `present (v${installedGuardVersion() ?? "?"})` },
741
+ runtime(),
546
742
  line("Claude hooks", isGuardInstalled(), "guard registered", "NOT registered"),
547
743
  line("Antigravity hooks", has(p.antigravityHooksPath), "guard registered", "NOT registered"),
548
744
  line("Codex hooks", isCodexGuardInstalled(), "guard registered", "NOT registered"),
@@ -603,7 +799,7 @@ function installGlobalQuiet() {
603
799
  let apiKey = process.env["SOLONGATE_API_KEY"] || "";
604
800
  let apiUrl = process.env["SOLONGATE_API_URL"] || "https://api.solongate.com";
605
801
  try {
606
- const cfg = JSON.parse(readFileSync(p.configPath, "utf-8"));
802
+ const cfg = JSON.parse(readFileSync2(p.configPath, "utf-8"));
607
803
  if (cfg && typeof cfg.apiKey === "string") apiKey = apiKey || cfg.apiKey;
608
804
  if (cfg && typeof cfg.apiUrl === "string") apiUrl = cfg.apiUrl;
609
805
  } catch {
@@ -619,26 +815,26 @@ function installGlobalQuiet() {
619
815
  mkdirSync(p.hooksDir, { recursive: true });
620
816
  mkdirSync(p.claudeDir, { recursive: true });
621
817
  unlockProtected();
622
- writeFileSync(join(p.hooksDir, "guard.mjs"), readGuard());
818
+ writeFileSync(join2(p.hooksDir, "guard.mjs"), readGuard());
623
819
  installGoBinaries(p.binDir);
624
- writeFileSync(join(p.hooksDir, "audit.mjs"), readHook("audit.mjs"));
625
- writeFileSync(join(p.hooksDir, "stop.mjs"), readHook("stop.mjs"));
626
- writeFileSync(join(p.hooksDir, "shield.mjs"), readHook("shield.mjs"));
627
- writeFileSync(join(p.hooksDir, "conversation.mjs"), readHook("conversation.mjs"));
820
+ writeFileSync(join2(p.hooksDir, "audit.mjs"), readHook("audit.mjs"));
821
+ writeFileSync(join2(p.hooksDir, "stop.mjs"), readHook("stop.mjs"));
822
+ writeFileSync(join2(p.hooksDir, "shield.mjs"), readHook("shield.mjs"));
823
+ writeFileSync(join2(p.hooksDir, "conversation.mjs"), readHook("conversation.mjs"));
824
+ writeLauncher(p.hooksDir);
825
+ writeLauncher(p.hooksDir);
628
826
  writeFileSync(p.configPath, JSON.stringify({ apiKey, apiUrl }, null, 2) + "\n");
629
827
  let existing = {};
630
- if (existsSync(p.settingsPath)) {
631
- const raw = readFileSync(p.settingsPath, "utf-8");
632
- if (!existsSync(p.backupPath)) writeFileSync(p.backupPath, raw);
828
+ if (existsSync2(p.settingsPath)) {
829
+ const raw = readFileSync2(p.settingsPath, "utf-8");
830
+ if (!existsSync2(p.backupPath)) writeFileSync(p.backupPath, raw);
633
831
  try {
634
832
  existing = JSON.parse(raw);
635
833
  } catch {
636
834
  existing = {};
637
835
  }
638
836
  }
639
- const nodeBin = process.execPath.replace(/\\/g, "/");
640
- const call = process.platform === "win32" ? "& " : "";
641
- const hookCmd = (script) => `${call}"${nodeBin}" "${join(p.hooksDir, script).replace(/\\/g, "/")}" claude-code "Claude Code"`;
837
+ const hookCmd = (script) => hookCommandFor(p.hooksDir, script);
642
838
  const merged = {
643
839
  ...existing,
644
840
  hooks: {
@@ -665,7 +861,7 @@ function installGlobalQuiet() {
665
861
  };
666
862
  writeFileSync(p.settingsPath, JSON.stringify(merged, null, 2) + "\n");
667
863
  try {
668
- installAntigravityGuard(p, join(p.hooksDir, "guard.mjs").replace(/\\/g, "/"));
864
+ installAntigravityGuard(p, join2(p.hooksDir, "guard.mjs").replace(/\\/g, "/"));
669
865
  } catch {
670
866
  }
671
867
  try {
@@ -686,7 +882,7 @@ function installGlobalQuiet() {
686
882
  function installedGuardVersion() {
687
883
  try {
688
884
  const p = globalPaths();
689
- const s = readFileSync(join(p.hooksDir, "guard.mjs"), "utf-8");
885
+ const s = readFileSync2(join2(p.hooksDir, "guard.mjs"), "utf-8");
690
886
  const m = s.match(/HOOK_VERSION\s*=\s*(\d+)/);
691
887
  return m ? parseInt(m[1], 10) : null;
692
888
  } catch {
@@ -696,16 +892,35 @@ function installedGuardVersion() {
696
892
  function guardHookOutdated() {
697
893
  try {
698
894
  const p = globalPaths();
699
- return readFileSync(join(p.hooksDir, "guard.mjs"), "utf-8") !== readGuard();
895
+ return readFileSync2(join2(p.hooksDir, "guard.mjs"), "utf-8") !== readGuard();
700
896
  } catch {
701
897
  return false;
702
898
  }
703
899
  }
900
+ function hookCommandFor(hooksDir2, script, client = "claude-code", label = "Claude Code") {
901
+ const target = join2(hooksDir2, script).replace(/\\/g, "/");
902
+ if (process.platform === "win32") {
903
+ return `& "${process.execPath.replace(/\\/g, "/")}" "${target}" ${client} "${label}"`;
904
+ }
905
+ const launcher = join2(hooksDir2, LAUNCHER_NAME).replace(/\\/g, "/");
906
+ return `/bin/sh "${launcher}" "${target}" ${client} "${label}"`;
907
+ }
908
+ function writeLauncher(hooksDir2) {
909
+ writeFileSync(join2(hooksDir2, LAUNCHER_NAME), launcherScript(process.execPath));
910
+ try {
911
+ chmodSync(join2(hooksDir2, LAUNCHER_NAME), 493);
912
+ } catch {
913
+ }
914
+ try {
915
+ mkdirSync(join2(hooksDir2, "..", BEAT_DIR), { recursive: true });
916
+ } catch {
917
+ }
918
+ }
704
919
  function isGuardInstalled() {
705
920
  try {
706
921
  const p = globalPaths();
707
- if (!existsSync(p.settingsPath)) return false;
708
- const s = JSON.parse(readFileSync(p.settingsPath, "utf-8"));
922
+ if (!existsSync2(p.settingsPath)) return false;
923
+ const s = JSON.parse(readFileSync2(p.settingsPath, "utf-8"));
709
924
  return !!s.hooks && JSON.stringify(s.hooks).includes(".solongate");
710
925
  } catch {
711
926
  return false;
@@ -728,8 +943,8 @@ function uninstallGlobalQuiet() {
728
943
  removeOpencodeGuard(p);
729
944
  } catch {
730
945
  }
731
- if (!existsSync(p.settingsPath)) return { ok: true, message: "guard removed (open a new session)" };
732
- const s = JSON.parse(readFileSync(p.settingsPath, "utf-8"));
946
+ if (!existsSync2(p.settingsPath)) return { ok: true, message: "guard removed (open a new session)" };
947
+ const s = JSON.parse(readFileSync2(p.settingsPath, "utf-8"));
733
948
  delete s.hooks;
734
949
  writeFileSync(p.settingsPath, JSON.stringify(s, null, 2) + "\n");
735
950
  return { ok: true, message: "guard removed (open a new session)" };
@@ -745,7 +960,7 @@ function escapeRe(s) {
745
960
  function resolveRealClaude() {
746
961
  try {
747
962
  const finder = process.platform === "win32" ? "where" : "which";
748
- const out = execFileSync(finder, ["claude"], { encoding: "utf-8" }).split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
963
+ const out = execFileSync2(finder, ["claude"], { encoding: "utf-8" }).split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
749
964
  if (process.platform === "win32") {
750
965
  const low = (s) => s.toLowerCase();
751
966
  return out.find((l) => low(l).endsWith(".cmd")) || out.find((l) => low(l).endsWith(".exe")) || out.find((l) => low(l).endsWith(".bat")) || out[0] || null;
@@ -758,17 +973,17 @@ function resolveRealClaude() {
758
973
  function shimTargets() {
759
974
  if (process.platform === "win32") {
760
975
  try {
761
- const prof = execFileSync("powershell", ["-NoProfile", "-Command", "$PROFILE.CurrentUserAllHosts"], { encoding: "utf-8" }).trim();
976
+ const prof = execFileSync2("powershell", ["-NoProfile", "-Command", "$PROFILE.CurrentUserAllHosts"], { encoding: "utf-8" }).trim();
762
977
  return prof ? [prof] : [];
763
978
  } catch {
764
979
  return [];
765
980
  }
766
981
  }
767
- return [".bashrc", ".zshrc", ".profile"].map((f) => join(homedir(), f)).filter((f) => existsSync(f));
982
+ return [".bashrc", ".zshrc", ".profile"].map((f) => join2(homedir2(), f)).filter((f) => existsSync2(f));
768
983
  }
769
984
  function writeShimBlock(file, block) {
770
985
  const re = new RegExp(escapeRe(SHIM_BEGIN) + "[\\s\\S]*?" + escapeRe(SHIM_END) + "\\r?\\n?", "g");
771
- let content = existsSync(file) ? readFileSync(file, "utf-8") : "";
986
+ let content = existsSync2(file) ? readFileSync2(file, "utf-8") : "";
772
987
  content = content.replace(re, "");
773
988
  if (block) {
774
989
  if (content.length && !content.endsWith("\n")) content += "\n";
@@ -813,7 +1028,7 @@ async function runGlobalInstall(opts = {}) {
813
1028
  let apiKey = opts.apiKey || process.env["SOLONGATE_API_KEY"] || "";
814
1029
  if (!apiKey || apiKey === "sg_live_your_key_here") {
815
1030
  try {
816
- const cfg = JSON.parse(readFileSync(p.configPath, "utf-8"));
1031
+ const cfg = JSON.parse(readFileSync2(p.configPath, "utf-8"));
817
1032
  if (cfg && typeof cfg.apiKey === "string") apiKey = cfg.apiKey;
818
1033
  } catch {
819
1034
  }
@@ -833,19 +1048,19 @@ async function runGlobalInstall(opts = {}) {
833
1048
  mkdirSync(p.hooksDir, { recursive: true });
834
1049
  mkdirSync(p.claudeDir, { recursive: true });
835
1050
  unlockProtected();
836
- writeFileSync(join(p.hooksDir, "guard.mjs"), readGuard());
837
- writeFileSync(join(p.hooksDir, "audit.mjs"), readHook("audit.mjs"));
838
- writeFileSync(join(p.hooksDir, "stop.mjs"), readHook("stop.mjs"));
839
- writeFileSync(join(p.hooksDir, "shield.mjs"), readHook("shield.mjs"));
840
- writeFileSync(join(p.hooksDir, "conversation.mjs"), readHook("conversation.mjs"));
1051
+ writeFileSync(join2(p.hooksDir, "guard.mjs"), readGuard());
1052
+ writeFileSync(join2(p.hooksDir, "audit.mjs"), readHook("audit.mjs"));
1053
+ writeFileSync(join2(p.hooksDir, "stop.mjs"), readHook("stop.mjs"));
1054
+ writeFileSync(join2(p.hooksDir, "shield.mjs"), readHook("shield.mjs"));
1055
+ writeFileSync(join2(p.hooksDir, "conversation.mjs"), readHook("conversation.mjs"));
841
1056
  console.log(` Installed hooks \u2192 ${p.hooksDir}`);
842
- installClaudeShim(join(p.hooksDir, "shield.mjs"));
1057
+ installClaudeShim(join2(p.hooksDir, "shield.mjs"));
843
1058
  writeFileSync(p.configPath, JSON.stringify({ apiKey, apiUrl }, null, 2) + "\n");
844
1059
  console.log(` Wrote ${p.configPath}`);
845
1060
  let existing = {};
846
- if (existsSync(p.settingsPath)) {
847
- const raw = readFileSync(p.settingsPath, "utf-8");
848
- if (!existsSync(p.backupPath)) {
1061
+ if (existsSync2(p.settingsPath)) {
1062
+ const raw = readFileSync2(p.settingsPath, "utf-8");
1063
+ if (!existsSync2(p.backupPath)) {
849
1064
  writeFileSync(p.backupPath, raw);
850
1065
  console.log(` Backed up existing settings \u2192 ${p.backupPath}`);
851
1066
  }
@@ -855,22 +1070,17 @@ async function runGlobalInstall(opts = {}) {
855
1070
  existing = {};
856
1071
  }
857
1072
  }
858
- const guardAbs = join(p.hooksDir, "guard.mjs").replace(/\\/g, "/");
859
- const auditAbs = join(p.hooksDir, "audit.mjs").replace(/\\/g, "/");
860
- const stopAbs = join(p.hooksDir, "stop.mjs").replace(/\\/g, "/");
861
- const convAbs = join(p.hooksDir, "conversation.mjs").replace(/\\/g, "/");
862
- const nodeBin = process.execPath.replace(/\\/g, "/");
863
- const call = process.platform === "win32" ? "& " : "";
864
- const hookCmd = (script) => `${call}"${nodeBin}" "${script}" claude-code "Claude Code"`;
1073
+ const guardAbs = join2(p.hooksDir, "guard.mjs").replace(/\\/g, "/");
1074
+ const hookCmd = (script) => hookCommandFor(p.hooksDir, script);
865
1075
  const merged = {
866
1076
  ...existing,
867
1077
  hooks: {
868
- PreToolUse: [{ matcher: "", hooks: [{ type: "command", command: hookCmd(guardAbs) }] }],
869
- PostToolUse: [{ matcher: "", hooks: [{ type: "command", command: hookCmd(auditAbs) }] }],
870
- UserPromptSubmit: [{ matcher: "", hooks: [{ type: "command", command: hookCmd(convAbs) }] }],
1078
+ PreToolUse: [{ matcher: "", hooks: [{ type: "command", command: hookCmd("guard.mjs") }] }],
1079
+ PostToolUse: [{ matcher: "", hooks: [{ type: "command", command: hookCmd("audit.mjs") }] }],
1080
+ UserPromptSubmit: [{ matcher: "", hooks: [{ type: "command", command: hookCmd("conversation.mjs") }] }],
871
1081
  Stop: [
872
- { matcher: "", hooks: [{ type: "command", command: hookCmd(stopAbs) }] },
873
- { matcher: "", hooks: [{ type: "command", command: hookCmd(convAbs) }] }
1082
+ { matcher: "", hooks: [{ type: "command", command: hookCmd("stop.mjs") }] },
1083
+ { matcher: "", hooks: [{ type: "command", command: hookCmd("conversation.mjs") }] }
874
1084
  ]
875
1085
  }
876
1086
  };
@@ -912,11 +1122,18 @@ async function installGlobalWithKey(apiKey, apiUrl) {
912
1122
  await runGlobalInstall({ apiKey, apiUrl });
913
1123
  }
914
1124
  export {
1125
+ BEAT_DIR,
1126
+ LAUNCHER_NAME,
1127
+ agoLabel,
915
1128
  clearGuardUpdateCheck,
916
1129
  codexDetected,
917
1130
  codexHooksStatus,
918
1131
  globalPaths,
1132
+ guardBeat,
919
1133
  guardHookOutdated,
1134
+ hookBeats,
1135
+ hookCanStart,
1136
+ hookCommandFor,
920
1137
  installClaudeShim,
921
1138
  installGlobalQuiet,
922
1139
  installGlobalWithKey,
@@ -924,6 +1141,7 @@ export {
924
1141
  isCodexGuardInstalled,
925
1142
  isGuardInstalled,
926
1143
  isOpencodeGuardInstalled,
1144
+ launcherScript,
927
1145
  lockProtected,
928
1146
  opencodeDetected,
929
1147
  removeClaudeShim,
@@ -934,5 +1152,6 @@ export {
934
1152
  sweepStrayScratchDirs,
935
1153
  uninstallGlobalQuiet,
936
1154
  unlockProtected,
1155
+ writeLauncher,
937
1156
  writeProtectedFile
938
1157
  };