@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.
@@ -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,14 @@ 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");
230
414
  }
231
415
  function installAntigravityGuard(p, guardAbs) {
232
416
  mkdirSync(p.antigravityDir, { recursive: true });
233
417
  let existing = {};
234
- if (existsSync(p.antigravityHooksPath)) {
235
- const raw = readFileSync(p.antigravityHooksPath, "utf-8");
236
- if (!existsSync(p.antigravityBackupPath)) writeFileSync(p.antigravityBackupPath, raw);
418
+ if (existsSync2(p.antigravityHooksPath)) {
419
+ const raw = readFileSync2(p.antigravityHooksPath, "utf-8");
420
+ if (!existsSync2(p.antigravityBackupPath)) writeFileSync(p.antigravityBackupPath, raw);
237
421
  try {
238
422
  existing = JSON.parse(raw);
239
423
  } catch {
@@ -249,9 +433,9 @@ function installAntigravityGuard(p, guardAbs) {
249
433
  writeFileSync(p.antigravityHooksPath, JSON.stringify(merged, null, 2) + "\n");
250
434
  }
251
435
  function removeAntigravityGuard(p) {
252
- if (!existsSync(p.antigravityHooksPath)) return;
436
+ if (!existsSync2(p.antigravityHooksPath)) return;
253
437
  try {
254
- const s = JSON.parse(readFileSync(p.antigravityHooksPath, "utf-8"));
438
+ const s = JSON.parse(readFileSync2(p.antigravityHooksPath, "utf-8"));
255
439
  if (!(ANTIGRAVITY_GROUP in s)) return;
256
440
  delete s[ANTIGRAVITY_GROUP];
257
441
  writeFileSync(p.antigravityHooksPath, JSON.stringify(s, null, 2) + "\n");
@@ -261,8 +445,10 @@ function removeAntigravityGuard(p) {
261
445
  var CODEX_EVENTS = ["PreToolUse", "PostToolUse", "UserPromptSubmit", "Stop"];
262
446
  var CODEX_TIMEOUT_SEC = 30;
263
447
  function codexHookCommand(scriptAbs) {
264
- const nodeBin = process.execPath.replace(/\\/g, "/");
265
- return `"${nodeBin}" "${scriptAbs.replace(/\\/g, "/")}" codex "Codex"`;
448
+ if (process.platform === "win32") {
449
+ return `"${process.execPath.replace(/\\/g, "/")}" "${scriptAbs.replace(/\\/g, "/")}" codex "Codex"`;
450
+ }
451
+ return hookCommandFor(dirname(scriptAbs), basename(scriptAbs), "codex", "Codex");
266
452
  }
267
453
  function isOurCodexGroup(group) {
268
454
  const hooks = Array.isArray(group?.hooks) ? group.hooks : [];
@@ -270,10 +456,10 @@ function isOurCodexGroup(group) {
270
456
  }
271
457
  function readCodexHooksFile(path) {
272
458
  const out = { hooks: {}, rest: {} };
273
- if (!existsSync(path)) return out;
459
+ if (!existsSync2(path)) return out;
274
460
  let raw;
275
461
  try {
276
- raw = JSON.parse(readFileSync(path, "utf-8"));
462
+ raw = JSON.parse(readFileSync2(path, "utf-8"));
277
463
  } catch {
278
464
  return out;
279
465
  }
@@ -318,10 +504,10 @@ function writeCodexHooksFile(path, file) {
318
504
  body["hooks"] = hooks;
319
505
  writeFileSync(path, JSON.stringify(body, null, 2) + "\n");
320
506
  }
321
- function installCodexGuard(p, hooksDir) {
507
+ function installCodexGuard(p, hooksDir2) {
322
508
  mkdirSync(p.codexDir, { recursive: true });
323
- if (existsSync(p.codexHooksPath) && !existsSync(p.codexBackupPath)) {
324
- writeFileSync(p.codexBackupPath, readFileSync(p.codexHooksPath, "utf-8"));
509
+ if (existsSync2(p.codexHooksPath) && !existsSync2(p.codexBackupPath)) {
510
+ writeFileSync(p.codexBackupPath, readFileSync2(p.codexHooksPath, "utf-8"));
325
511
  }
326
512
  const file = readCodexHooksFile(p.codexHooksPath);
327
513
  const script = {
@@ -345,7 +531,7 @@ function installCodexGuard(p, hooksDir) {
345
531
  ...ev === "Stop" ? {} : { matcher: "*" },
346
532
  hooks: [{
347
533
  type: "command",
348
- command: codexHookCommand(join(hooksDir, script[ev]).replace(/\\/g, "/")),
534
+ command: codexHookCommand(join2(hooksDir2, script[ev]).replace(/\\/g, "/")),
349
535
  timeout: CODEX_TIMEOUT_SEC,
350
536
  statusMessage: status[ev]
351
537
  }]
@@ -355,7 +541,7 @@ function installCodexGuard(p, hooksDir) {
355
541
  writeCodexHooksFile(p.codexHooksPath, file);
356
542
  }
357
543
  function removeCodexGuard(p) {
358
- if (!existsSync(p.codexHooksPath)) return;
544
+ if (!existsSync2(p.codexHooksPath)) return;
359
545
  try {
360
546
  const file = readCodexHooksFile(p.codexHooksPath);
361
547
  let changed = false;
@@ -381,7 +567,7 @@ function isCodexGuardInstalled() {
381
567
  }
382
568
  function codexDetected() {
383
569
  try {
384
- return existsSync(globalPaths().codexDir);
570
+ return existsSync2(globalPaths().codexDir);
385
571
  } catch {
386
572
  return false;
387
573
  }
@@ -391,7 +577,7 @@ function codexHooksStatus() {
391
577
  const registered = isCodexGuardInstalled();
392
578
  let trusted = false, disabled = false;
393
579
  try {
394
- const toml = readFileSync(p.codexConfigPath, "utf-8");
580
+ const toml = readFileSync2(p.codexConfigPath, "utf-8");
395
581
  trusted = /trusted_hash\s*=/.test(toml);
396
582
  disabled = /^\s*hooks\s*=\s*false\s*$/m.test(toml);
397
583
  } catch {
@@ -410,10 +596,10 @@ function removeOpencodeGuard(p) {
410
596
  } catch {
411
597
  }
412
598
  }
413
- function warmPolicyCache(hooksDir, agents) {
599
+ function warmPolicyCache(hooksDir2, agents) {
414
600
  for (const agent of agents) {
415
601
  try {
416
- const child = spawn(process.execPath, [join(hooksDir, "guard.mjs"), agent, "--sg-refresh-policy"], {
602
+ const child = spawn(process.execPath, [join2(hooksDir2, "guard.mjs"), agent, "--sg-refresh-policy"], {
417
603
  detached: true,
418
604
  stdio: "ignore",
419
605
  windowsHide: true
@@ -427,24 +613,24 @@ function warmPolicyCache(hooksDir, agents) {
427
613
  }
428
614
  function isOpencodeGuardInstalled() {
429
615
  try {
430
- return readFileSync(globalPaths().opencodePluginPath, "utf-8").includes("tool.execute.before");
616
+ return readFileSync2(globalPaths().opencodePluginPath, "utf-8").includes("tool.execute.before");
431
617
  } catch {
432
618
  return false;
433
619
  }
434
620
  }
435
621
  function opencodeDetected() {
436
622
  try {
437
- return existsSync(globalPaths().opencodeDir);
623
+ return existsSync2(globalPaths().opencodeDir);
438
624
  } catch {
439
625
  return false;
440
626
  }
441
627
  }
442
628
  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) {
629
+ function sweepStrayScratchDirs(root = homedir2(), maxDepth = 6, budget = 4e4) {
444
630
  let removed = 0;
445
631
  let visited = 0;
446
632
  const skip = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".next", "build"]);
447
- const store = join(homedir(), ".solongate");
633
+ const store = join2(homedir2(), ".solongate");
448
634
  const walk = (dir, depth) => {
449
635
  if (depth > maxDepth || visited++ > budget) return;
450
636
  let entries;
@@ -455,11 +641,11 @@ function sweepStrayScratchDirs(root = homedir(), maxDepth = 6, budget = 4e4) {
455
641
  }
456
642
  for (const name of entries) {
457
643
  if (skip.has(name)) continue;
458
- const full = join(dir, name);
644
+ const full = join2(dir, name);
459
645
  if (full === store) continue;
460
646
  let isDir = false;
461
647
  try {
462
- isDir = statSync(full).isDirectory();
648
+ isDir = statSync2(full).isDirectory();
463
649
  } catch {
464
650
  continue;
465
651
  }
@@ -470,7 +656,7 @@ function sweepStrayScratchDirs(root = homedir(), maxDepth = 6, budget = 4e4) {
470
656
  for (const f of readdirSync(full)) {
471
657
  if (SCRATCH_FILES.has(f)) {
472
658
  try {
473
- rmSync(join(full, f), { force: true });
659
+ rmSync(join2(full, f), { force: true });
474
660
  } catch {
475
661
  left.push(f);
476
662
  }
@@ -510,12 +696,12 @@ function runGlobalRestore() {
510
696
  removeOpencodeGuard(p);
511
697
  } catch {
512
698
  }
513
- if (existsSync(p.backupPath)) {
514
- writeFileSync(p.settingsPath, readFileSync(p.backupPath, "utf-8"));
699
+ if (existsSync2(p.backupPath)) {
700
+ writeFileSync(p.settingsPath, readFileSync2(p.backupPath, "utf-8"));
515
701
  console.log(` Restored ${p.settingsPath} from backup.`);
516
- } else if (existsSync(p.settingsPath)) {
702
+ } else if (existsSync2(p.settingsPath)) {
517
703
  try {
518
- const s = JSON.parse(readFileSync(p.settingsPath, "utf-8"));
704
+ const s = JSON.parse(readFileSync2(p.settingsPath, "utf-8"));
519
705
  delete s.hooks;
520
706
  writeFileSync(p.settingsPath, JSON.stringify(s, null, 2) + "\n");
521
707
  console.log(` Removed SolonGate hooks from ${p.settingsPath}.`);
@@ -528,12 +714,17 @@ function runGlobalRestore() {
528
714
  }
529
715
  function repairQuiet() {
530
716
  const p = globalPaths();
531
- const has = (f) => existsSync(f);
532
- const guardFile = join(p.hooksDir, "guard.mjs");
717
+ const has = (f) => existsSync2(f);
718
+ const guardFile = join2(p.hooksDir, "guard.mjs");
533
719
  const line = (label, ok, yes, no) => ({ label, ok, detail: ok ? yes : no });
720
+ const runtime = () => {
721
+ const r2 = hookCanStart();
722
+ return { label: "hook runtime", ok: r2.ok, detail: r2.ok ? `node ${r2.detail}` : r2.detail };
723
+ };
534
724
  const before = [
535
725
  line("guard hook file", has(guardFile), "present", "MISSING"),
536
726
  line("cloud credential", has(p.configPath), "present", "MISSING"),
727
+ runtime(),
537
728
  line("Claude hooks", isGuardInstalled(), "guard registered", "guard NOT registered"),
538
729
  line("Antigravity hooks", has(p.antigravityHooksPath), "guard registered", "guard NOT registered"),
539
730
  line("Codex hooks", isCodexGuardInstalled(), "guard registered", "guard NOT registered"),
@@ -543,6 +734,7 @@ function repairQuiet() {
543
734
  if (!r.ok) return { ok: false, message: r.message, before, after: [], notes: [] };
544
735
  const after = [
545
736
  { label: "guard hook file", ok: true, detail: `present (v${installedGuardVersion() ?? "?"})` },
737
+ runtime(),
546
738
  line("Claude hooks", isGuardInstalled(), "guard registered", "NOT registered"),
547
739
  line("Antigravity hooks", has(p.antigravityHooksPath), "guard registered", "NOT registered"),
548
740
  line("Codex hooks", isCodexGuardInstalled(), "guard registered", "NOT registered"),
@@ -603,7 +795,7 @@ function installGlobalQuiet() {
603
795
  let apiKey = process.env["SOLONGATE_API_KEY"] || "";
604
796
  let apiUrl = process.env["SOLONGATE_API_URL"] || "https://api.solongate.com";
605
797
  try {
606
- const cfg = JSON.parse(readFileSync(p.configPath, "utf-8"));
798
+ const cfg = JSON.parse(readFileSync2(p.configPath, "utf-8"));
607
799
  if (cfg && typeof cfg.apiKey === "string") apiKey = apiKey || cfg.apiKey;
608
800
  if (cfg && typeof cfg.apiUrl === "string") apiUrl = cfg.apiUrl;
609
801
  } catch {
@@ -619,26 +811,26 @@ function installGlobalQuiet() {
619
811
  mkdirSync(p.hooksDir, { recursive: true });
620
812
  mkdirSync(p.claudeDir, { recursive: true });
621
813
  unlockProtected();
622
- writeFileSync(join(p.hooksDir, "guard.mjs"), readGuard());
814
+ writeFileSync(join2(p.hooksDir, "guard.mjs"), readGuard());
623
815
  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"));
816
+ writeFileSync(join2(p.hooksDir, "audit.mjs"), readHook("audit.mjs"));
817
+ writeFileSync(join2(p.hooksDir, "stop.mjs"), readHook("stop.mjs"));
818
+ writeFileSync(join2(p.hooksDir, "shield.mjs"), readHook("shield.mjs"));
819
+ writeFileSync(join2(p.hooksDir, "conversation.mjs"), readHook("conversation.mjs"));
820
+ writeLauncher(p.hooksDir);
821
+ writeLauncher(p.hooksDir);
628
822
  writeFileSync(p.configPath, JSON.stringify({ apiKey, apiUrl }, null, 2) + "\n");
629
823
  let existing = {};
630
- if (existsSync(p.settingsPath)) {
631
- const raw = readFileSync(p.settingsPath, "utf-8");
632
- if (!existsSync(p.backupPath)) writeFileSync(p.backupPath, raw);
824
+ if (existsSync2(p.settingsPath)) {
825
+ const raw = readFileSync2(p.settingsPath, "utf-8");
826
+ if (!existsSync2(p.backupPath)) writeFileSync(p.backupPath, raw);
633
827
  try {
634
828
  existing = JSON.parse(raw);
635
829
  } catch {
636
830
  existing = {};
637
831
  }
638
832
  }
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"`;
833
+ const hookCmd = (script) => hookCommandFor(p.hooksDir, script);
642
834
  const merged = {
643
835
  ...existing,
644
836
  hooks: {
@@ -665,7 +857,7 @@ function installGlobalQuiet() {
665
857
  };
666
858
  writeFileSync(p.settingsPath, JSON.stringify(merged, null, 2) + "\n");
667
859
  try {
668
- installAntigravityGuard(p, join(p.hooksDir, "guard.mjs").replace(/\\/g, "/"));
860
+ installAntigravityGuard(p, join2(p.hooksDir, "guard.mjs").replace(/\\/g, "/"));
669
861
  } catch {
670
862
  }
671
863
  try {
@@ -686,7 +878,7 @@ function installGlobalQuiet() {
686
878
  function installedGuardVersion() {
687
879
  try {
688
880
  const p = globalPaths();
689
- const s = readFileSync(join(p.hooksDir, "guard.mjs"), "utf-8");
881
+ const s = readFileSync2(join2(p.hooksDir, "guard.mjs"), "utf-8");
690
882
  const m = s.match(/HOOK_VERSION\s*=\s*(\d+)/);
691
883
  return m ? parseInt(m[1], 10) : null;
692
884
  } catch {
@@ -696,16 +888,35 @@ function installedGuardVersion() {
696
888
  function guardHookOutdated() {
697
889
  try {
698
890
  const p = globalPaths();
699
- return readFileSync(join(p.hooksDir, "guard.mjs"), "utf-8") !== readGuard();
891
+ return readFileSync2(join2(p.hooksDir, "guard.mjs"), "utf-8") !== readGuard();
700
892
  } catch {
701
893
  return false;
702
894
  }
703
895
  }
896
+ function hookCommandFor(hooksDir2, script, client = "claude-code", label = "Claude Code") {
897
+ const target = join2(hooksDir2, script).replace(/\\/g, "/");
898
+ if (process.platform === "win32") {
899
+ return `& "${process.execPath.replace(/\\/g, "/")}" "${target}" ${client} "${label}"`;
900
+ }
901
+ const launcher = join2(hooksDir2, LAUNCHER_NAME).replace(/\\/g, "/");
902
+ return `/bin/sh "${launcher}" "${target}" ${client} "${label}"`;
903
+ }
904
+ function writeLauncher(hooksDir2) {
905
+ writeFileSync(join2(hooksDir2, LAUNCHER_NAME), launcherScript(process.execPath));
906
+ try {
907
+ chmodSync(join2(hooksDir2, LAUNCHER_NAME), 493);
908
+ } catch {
909
+ }
910
+ try {
911
+ mkdirSync(join2(hooksDir2, "..", BEAT_DIR), { recursive: true });
912
+ } catch {
913
+ }
914
+ }
704
915
  function isGuardInstalled() {
705
916
  try {
706
917
  const p = globalPaths();
707
- if (!existsSync(p.settingsPath)) return false;
708
- const s = JSON.parse(readFileSync(p.settingsPath, "utf-8"));
918
+ if (!existsSync2(p.settingsPath)) return false;
919
+ const s = JSON.parse(readFileSync2(p.settingsPath, "utf-8"));
709
920
  return !!s.hooks && JSON.stringify(s.hooks).includes(".solongate");
710
921
  } catch {
711
922
  return false;
@@ -728,8 +939,8 @@ function uninstallGlobalQuiet() {
728
939
  removeOpencodeGuard(p);
729
940
  } catch {
730
941
  }
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"));
942
+ if (!existsSync2(p.settingsPath)) return { ok: true, message: "guard removed (open a new session)" };
943
+ const s = JSON.parse(readFileSync2(p.settingsPath, "utf-8"));
733
944
  delete s.hooks;
734
945
  writeFileSync(p.settingsPath, JSON.stringify(s, null, 2) + "\n");
735
946
  return { ok: true, message: "guard removed (open a new session)" };
@@ -745,7 +956,7 @@ function escapeRe(s) {
745
956
  function resolveRealClaude() {
746
957
  try {
747
958
  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);
959
+ const out = execFileSync2(finder, ["claude"], { encoding: "utf-8" }).split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
749
960
  if (process.platform === "win32") {
750
961
  const low = (s) => s.toLowerCase();
751
962
  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 +969,17 @@ function resolveRealClaude() {
758
969
  function shimTargets() {
759
970
  if (process.platform === "win32") {
760
971
  try {
761
- const prof = execFileSync("powershell", ["-NoProfile", "-Command", "$PROFILE.CurrentUserAllHosts"], { encoding: "utf-8" }).trim();
972
+ const prof = execFileSync2("powershell", ["-NoProfile", "-Command", "$PROFILE.CurrentUserAllHosts"], { encoding: "utf-8" }).trim();
762
973
  return prof ? [prof] : [];
763
974
  } catch {
764
975
  return [];
765
976
  }
766
977
  }
767
- return [".bashrc", ".zshrc", ".profile"].map((f) => join(homedir(), f)).filter((f) => existsSync(f));
978
+ return [".bashrc", ".zshrc", ".profile"].map((f) => join2(homedir2(), f)).filter((f) => existsSync2(f));
768
979
  }
769
980
  function writeShimBlock(file, block) {
770
981
  const re = new RegExp(escapeRe(SHIM_BEGIN) + "[\\s\\S]*?" + escapeRe(SHIM_END) + "\\r?\\n?", "g");
771
- let content = existsSync(file) ? readFileSync(file, "utf-8") : "";
982
+ let content = existsSync2(file) ? readFileSync2(file, "utf-8") : "";
772
983
  content = content.replace(re, "");
773
984
  if (block) {
774
985
  if (content.length && !content.endsWith("\n")) content += "\n";
@@ -813,7 +1024,7 @@ async function runGlobalInstall(opts = {}) {
813
1024
  let apiKey = opts.apiKey || process.env["SOLONGATE_API_KEY"] || "";
814
1025
  if (!apiKey || apiKey === "sg_live_your_key_here") {
815
1026
  try {
816
- const cfg = JSON.parse(readFileSync(p.configPath, "utf-8"));
1027
+ const cfg = JSON.parse(readFileSync2(p.configPath, "utf-8"));
817
1028
  if (cfg && typeof cfg.apiKey === "string") apiKey = cfg.apiKey;
818
1029
  } catch {
819
1030
  }
@@ -833,19 +1044,19 @@ async function runGlobalInstall(opts = {}) {
833
1044
  mkdirSync(p.hooksDir, { recursive: true });
834
1045
  mkdirSync(p.claudeDir, { recursive: true });
835
1046
  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"));
1047
+ writeFileSync(join2(p.hooksDir, "guard.mjs"), readGuard());
1048
+ writeFileSync(join2(p.hooksDir, "audit.mjs"), readHook("audit.mjs"));
1049
+ writeFileSync(join2(p.hooksDir, "stop.mjs"), readHook("stop.mjs"));
1050
+ writeFileSync(join2(p.hooksDir, "shield.mjs"), readHook("shield.mjs"));
1051
+ writeFileSync(join2(p.hooksDir, "conversation.mjs"), readHook("conversation.mjs"));
841
1052
  console.log(` Installed hooks \u2192 ${p.hooksDir}`);
842
- installClaudeShim(join(p.hooksDir, "shield.mjs"));
1053
+ installClaudeShim(join2(p.hooksDir, "shield.mjs"));
843
1054
  writeFileSync(p.configPath, JSON.stringify({ apiKey, apiUrl }, null, 2) + "\n");
844
1055
  console.log(` Wrote ${p.configPath}`);
845
1056
  let existing = {};
846
- if (existsSync(p.settingsPath)) {
847
- const raw = readFileSync(p.settingsPath, "utf-8");
848
- if (!existsSync(p.backupPath)) {
1057
+ if (existsSync2(p.settingsPath)) {
1058
+ const raw = readFileSync2(p.settingsPath, "utf-8");
1059
+ if (!existsSync2(p.backupPath)) {
849
1060
  writeFileSync(p.backupPath, raw);
850
1061
  console.log(` Backed up existing settings \u2192 ${p.backupPath}`);
851
1062
  }
@@ -855,22 +1066,17 @@ async function runGlobalInstall(opts = {}) {
855
1066
  existing = {};
856
1067
  }
857
1068
  }
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"`;
1069
+ const guardAbs = join2(p.hooksDir, "guard.mjs").replace(/\\/g, "/");
1070
+ const hookCmd = (script) => hookCommandFor(p.hooksDir, script);
865
1071
  const merged = {
866
1072
  ...existing,
867
1073
  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) }] }],
1074
+ PreToolUse: [{ matcher: "", hooks: [{ type: "command", command: hookCmd("guard.mjs") }] }],
1075
+ PostToolUse: [{ matcher: "", hooks: [{ type: "command", command: hookCmd("audit.mjs") }] }],
1076
+ UserPromptSubmit: [{ matcher: "", hooks: [{ type: "command", command: hookCmd("conversation.mjs") }] }],
871
1077
  Stop: [
872
- { matcher: "", hooks: [{ type: "command", command: hookCmd(stopAbs) }] },
873
- { matcher: "", hooks: [{ type: "command", command: hookCmd(convAbs) }] }
1078
+ { matcher: "", hooks: [{ type: "command", command: hookCmd("stop.mjs") }] },
1079
+ { matcher: "", hooks: [{ type: "command", command: hookCmd("conversation.mjs") }] }
874
1080
  ]
875
1081
  }
876
1082
  };
@@ -912,11 +1118,18 @@ async function installGlobalWithKey(apiKey, apiUrl) {
912
1118
  await runGlobalInstall({ apiKey, apiUrl });
913
1119
  }
914
1120
  export {
1121
+ BEAT_DIR,
1122
+ LAUNCHER_NAME,
1123
+ agoLabel,
915
1124
  clearGuardUpdateCheck,
916
1125
  codexDetected,
917
1126
  codexHooksStatus,
918
1127
  globalPaths,
1128
+ guardBeat,
919
1129
  guardHookOutdated,
1130
+ hookBeats,
1131
+ hookCanStart,
1132
+ hookCommandFor,
920
1133
  installClaudeShim,
921
1134
  installGlobalQuiet,
922
1135
  installGlobalWithKey,
@@ -924,6 +1137,7 @@ export {
924
1137
  isCodexGuardInstalled,
925
1138
  isGuardInstalled,
926
1139
  isOpencodeGuardInstalled,
1140
+ launcherScript,
927
1141
  lockProtected,
928
1142
  opencodeDetected,
929
1143
  removeClaudeShim,
@@ -934,5 +1148,6 @@ export {
934
1148
  sweepStrayScratchDirs,
935
1149
  uninstallGlobalQuiet,
936
1150
  unlockProtected,
1151
+ writeLauncher,
937
1152
  writeProtectedFile
938
1153
  };