@solongate/proxy 0.83.56 → 0.83.58
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/index.js +154 -71
- package/dist/global-install.d.ts +28 -0
- package/dist/global-install.js +349 -130
- package/dist/hook-health.d.ts +26 -0
- package/dist/hook-launcher.d.ts +57 -0
- package/dist/index.js +511 -252
- package/dist/tui/index.js +422 -167
- package/hooks/conversation.mjs +85 -30
- package/hooks/opencode-plugin.mjs +94 -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,17 @@ function installGoBinaries(binDir) {
|
|
|
224
410
|
}
|
|
225
411
|
var ANTIGRAVITY_GROUP = "solongate-guard";
|
|
226
412
|
function antigravityHookCommand(guardAbs) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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 (
|
|
235
|
-
const raw =
|
|
236
|
-
if (!
|
|
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 (!
|
|
440
|
+
if (!existsSync2(p.antigravityHooksPath)) return;
|
|
253
441
|
try {
|
|
254
|
-
const s = JSON.parse(
|
|
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
|
-
|
|
265
|
-
|
|
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 (!
|
|
463
|
+
if (!existsSync2(path)) return out;
|
|
274
464
|
let raw;
|
|
275
465
|
try {
|
|
276
|
-
raw = JSON.parse(
|
|
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,
|
|
511
|
+
function installCodexGuard(p, hooksDir2) {
|
|
322
512
|
mkdirSync(p.codexDir, { recursive: true });
|
|
323
|
-
if (
|
|
324
|
-
writeFileSync(p.codexBackupPath,
|
|
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(
|
|
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 (!
|
|
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
|
|
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 =
|
|
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(
|
|
603
|
+
function warmPolicyCache(hooksDir2, agents) {
|
|
414
604
|
for (const agent of agents) {
|
|
415
605
|
try {
|
|
416
|
-
const child = spawn(process.execPath, [
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
648
|
+
const full = join2(dir, name);
|
|
459
649
|
if (full === store) continue;
|
|
460
650
|
let isDir = false;
|
|
461
651
|
try {
|
|
462
|
-
isDir =
|
|
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(
|
|
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 (
|
|
514
|
-
writeFileSync(p.settingsPath,
|
|
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 (
|
|
706
|
+
} else if (existsSync2(p.settingsPath)) {
|
|
517
707
|
try {
|
|
518
|
-
const s = JSON.parse(
|
|
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) =>
|
|
532
|
-
const guardFile =
|
|
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(
|
|
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(
|
|
818
|
+
writeFileSync(join2(p.hooksDir, "guard.mjs"), readGuard());
|
|
623
819
|
installGoBinaries(p.binDir);
|
|
624
|
-
writeFileSync(
|
|
625
|
-
writeFileSync(
|
|
626
|
-
writeFileSync(
|
|
627
|
-
writeFileSync(
|
|
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 (
|
|
631
|
-
const raw =
|
|
632
|
-
if (!
|
|
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
|
|
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,
|
|
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 =
|
|
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
|
|
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 (!
|
|
708
|
-
const s = JSON.parse(
|
|
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 (!
|
|
732
|
-
const s = JSON.parse(
|
|
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 =
|
|
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 =
|
|
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) =>
|
|
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 =
|
|
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(
|
|
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(
|
|
837
|
-
writeFileSync(
|
|
838
|
-
writeFileSync(
|
|
839
|
-
writeFileSync(
|
|
840
|
-
writeFileSync(
|
|
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(
|
|
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 (
|
|
847
|
-
const raw =
|
|
848
|
-
if (!
|
|
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 =
|
|
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"`;
|
|
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(
|
|
869
|
-
PostToolUse: [{ matcher: "", hooks: [{ type: "command", command: hookCmd(
|
|
870
|
-
UserPromptSubmit: [{ matcher: "", hooks: [{ type: "command", command: hookCmd(
|
|
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(
|
|
873
|
-
{ matcher: "", hooks: [{ type: "command", command: hookCmd(
|
|
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
|
};
|