@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/api-client/audit.d.ts +0 -13
- package/dist/commands/index.js +154 -79
- package/dist/global-install.d.ts +28 -0
- package/dist/global-install.js +344 -129
- package/dist/hook-health.d.ts +26 -0
- package/dist/hook-launcher.d.ts +57 -0
- package/dist/index.js +507 -329
- package/dist/tui/index.js +418 -244
- package/dist/tui/local-log.d.ts +0 -2
- package/hooks/opencode-plugin.mjs +21 -1
- package/package.json +7 -7
package/dist/global-install.js
CHANGED
|
@@ -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 (!
|
|
193
|
+
if (!existsSync2(file)) return;
|
|
13
194
|
try {
|
|
14
195
|
if (process.platform === "win32") {
|
|
15
196
|
try {
|
|
16
|
-
|
|
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
|
-
|
|
201
|
+
execFileSync2("icacls", [file, "/grant", "*S-1-3-4:(RX)"], { stdio: "ignore" });
|
|
21
202
|
} catch {
|
|
22
203
|
}
|
|
23
204
|
try {
|
|
24
|
-
|
|
205
|
+
execFileSync2("attrib", ["+R", file], { stdio: "ignore" });
|
|
25
206
|
} catch {
|
|
26
207
|
}
|
|
27
208
|
} else if (process.platform === "darwin") {
|
|
28
209
|
try {
|
|
29
|
-
|
|
210
|
+
execFileSync2("chflags", ["uchg", file], { stdio: "ignore" });
|
|
30
211
|
} catch {
|
|
31
212
|
}
|
|
32
213
|
} else {
|
|
33
214
|
try {
|
|
34
|
-
|
|
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 (!
|
|
227
|
+
if (!existsSync2(file)) return;
|
|
47
228
|
try {
|
|
48
229
|
if (process.platform === "win32") {
|
|
49
230
|
try {
|
|
50
|
-
|
|
231
|
+
execFileSync2("icacls", [file, "/remove:g", "*S-1-3-4"], { stdio: "ignore" });
|
|
51
232
|
} catch {
|
|
52
233
|
}
|
|
53
234
|
try {
|
|
54
|
-
|
|
235
|
+
execFileSync2("icacls", [file, "/remove:d", "*S-1-1-0"], { stdio: "ignore" });
|
|
55
236
|
} catch {
|
|
56
237
|
}
|
|
57
238
|
try {
|
|
58
|
-
|
|
239
|
+
execFileSync2("icacls", [file, "/reset"], { stdio: "ignore" });
|
|
59
240
|
} catch {
|
|
60
241
|
}
|
|
61
242
|
try {
|
|
62
|
-
|
|
243
|
+
execFileSync2("attrib", ["-R", file], { stdio: "ignore" });
|
|
63
244
|
} catch {
|
|
64
245
|
}
|
|
65
246
|
} else if (process.platform === "darwin") {
|
|
66
247
|
try {
|
|
67
|
-
|
|
248
|
+
execFileSync2("chflags", ["nouchg", file], { stdio: "ignore" });
|
|
68
249
|
} catch {
|
|
69
250
|
}
|
|
70
251
|
} else {
|
|
71
252
|
try {
|
|
72
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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 =
|
|
128
|
-
const
|
|
129
|
-
const
|
|
130
|
-
const claudeDir =
|
|
131
|
-
const antigravityDir =
|
|
132
|
-
const codexDir = process.env["CODEX_HOME"] ? resolve(process.env["CODEX_HOME"]) :
|
|
133
|
-
const opencodeDir =
|
|
134
|
-
const binDir =
|
|
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:
|
|
145
|
-
backupPath:
|
|
146
|
-
configPath:
|
|
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:
|
|
151
|
-
antigravityBackupPath:
|
|
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:
|
|
156
|
-
codexBackupPath:
|
|
157
|
-
codexConfigPath:
|
|
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:
|
|
164
|
-
opencodePluginPath:
|
|
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(
|
|
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
|
|
362
|
+
return readFileSync2(join2(HOOKS_DIR, filename), "utf-8");
|
|
177
363
|
}
|
|
178
364
|
function firstAccountCredential() {
|
|
179
365
|
try {
|
|
180
|
-
const raw = JSON.parse(
|
|
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 =
|
|
191
|
-
return
|
|
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 =
|
|
212
|
-
const to =
|
|
397
|
+
const from = join2(pkgDir, name + suffix);
|
|
398
|
+
const to = join2(binDir, name + suffix);
|
|
213
399
|
try {
|
|
214
|
-
if (!
|
|
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
|
-
|
|
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 (
|
|
235
|
-
const raw =
|
|
236
|
-
if (!
|
|
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 (!
|
|
436
|
+
if (!existsSync2(p.antigravityHooksPath)) return;
|
|
253
437
|
try {
|
|
254
|
-
const s = JSON.parse(
|
|
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
|
-
|
|
265
|
-
|
|
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 (!
|
|
459
|
+
if (!existsSync2(path)) return out;
|
|
274
460
|
let raw;
|
|
275
461
|
try {
|
|
276
|
-
raw = JSON.parse(
|
|
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,
|
|
507
|
+
function installCodexGuard(p, hooksDir2) {
|
|
322
508
|
mkdirSync(p.codexDir, { recursive: true });
|
|
323
|
-
if (
|
|
324
|
-
writeFileSync(p.codexBackupPath,
|
|
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(
|
|
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 (!
|
|
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
|
|
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 =
|
|
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(
|
|
599
|
+
function warmPolicyCache(hooksDir2, agents) {
|
|
414
600
|
for (const agent of agents) {
|
|
415
601
|
try {
|
|
416
|
-
const child = spawn(process.execPath, [
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
644
|
+
const full = join2(dir, name);
|
|
459
645
|
if (full === store) continue;
|
|
460
646
|
let isDir = false;
|
|
461
647
|
try {
|
|
462
|
-
isDir =
|
|
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(
|
|
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 (
|
|
514
|
-
writeFileSync(p.settingsPath,
|
|
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 (
|
|
702
|
+
} else if (existsSync2(p.settingsPath)) {
|
|
517
703
|
try {
|
|
518
|
-
const s = JSON.parse(
|
|
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) =>
|
|
532
|
-
const guardFile =
|
|
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(
|
|
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(
|
|
814
|
+
writeFileSync(join2(p.hooksDir, "guard.mjs"), readGuard());
|
|
623
815
|
installGoBinaries(p.binDir);
|
|
624
|
-
writeFileSync(
|
|
625
|
-
writeFileSync(
|
|
626
|
-
writeFileSync(
|
|
627
|
-
writeFileSync(
|
|
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 (
|
|
631
|
-
const raw =
|
|
632
|
-
if (!
|
|
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
|
|
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,
|
|
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 =
|
|
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
|
|
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 (!
|
|
708
|
-
const s = JSON.parse(
|
|
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 (!
|
|
732
|
-
const s = JSON.parse(
|
|
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 =
|
|
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 =
|
|
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) =>
|
|
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 =
|
|
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(
|
|
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(
|
|
837
|
-
writeFileSync(
|
|
838
|
-
writeFileSync(
|
|
839
|
-
writeFileSync(
|
|
840
|
-
writeFileSync(
|
|
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(
|
|
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 (
|
|
847
|
-
const raw =
|
|
848
|
-
if (!
|
|
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 =
|
|
859
|
-
const
|
|
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(
|
|
869
|
-
PostToolUse: [{ matcher: "", hooks: [{ type: "command", command: hookCmd(
|
|
870
|
-
UserPromptSubmit: [{ matcher: "", hooks: [{ type: "command", command: hookCmd(
|
|
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(
|
|
873
|
-
{ matcher: "", hooks: [{ type: "command", command: hookCmd(
|
|
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
|
};
|