@solongate/proxy 0.43.0 → 0.45.0
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/global-install.js +198 -0
- package/dist/index.js +7423 -2722
- package/dist/init.js +300 -68
- package/dist/lib.js +6759 -2062
- package/dist/login.js +327 -0
- package/dist/pull-push.js +2 -1
- package/hooks/audit.mjs +32 -11
- package/hooks/guard.bundled.mjs +7257 -0
- package/hooks/guard.mjs +703 -1077
- package/package.json +6 -2
package/dist/init.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/init.ts
|
|
4
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
5
|
-
import { resolve, join, dirname } from "path";
|
|
6
|
-
import { fileURLToPath } from "url";
|
|
7
|
-
import { execFileSync } from "child_process";
|
|
8
|
-
import { createInterface } from "readline";
|
|
4
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
5
|
+
import { resolve as resolve2, join as join2, dirname as dirname2 } from "path";
|
|
6
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
8
|
+
import { createInterface as createInterface2 } from "readline";
|
|
9
9
|
|
|
10
10
|
// src/cli-utils.ts
|
|
11
11
|
var c = {
|
|
@@ -37,13 +37,201 @@ var BANNER_FULL = [
|
|
|
37
37
|
];
|
|
38
38
|
var BANNER_COLORS = [c.blue1, c.blue2, c.blue3, c.blue4, c.blue5, c.blue6];
|
|
39
39
|
|
|
40
|
+
// src/global-install.ts
|
|
41
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
42
|
+
import { resolve, join, dirname } from "path";
|
|
43
|
+
import { homedir } from "os";
|
|
44
|
+
import { fileURLToPath } from "url";
|
|
45
|
+
import { createInterface } from "readline";
|
|
46
|
+
import { execFileSync } from "child_process";
|
|
47
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
48
|
+
var HOOKS_DIR = resolve(__dirname, "..", "hooks");
|
|
49
|
+
function lockFile(file) {
|
|
50
|
+
if (!existsSync(file)) return;
|
|
51
|
+
try {
|
|
52
|
+
if (process.platform === "win32") {
|
|
53
|
+
try {
|
|
54
|
+
execFileSync("icacls", [file, "/deny", "*S-1-1-0:(WD,AD,DC,DE)"], { stdio: "ignore" });
|
|
55
|
+
} catch {
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
execFileSync("attrib", ["+R", file], { stdio: "ignore" });
|
|
59
|
+
} catch {
|
|
60
|
+
}
|
|
61
|
+
} else if (process.platform === "darwin") {
|
|
62
|
+
try {
|
|
63
|
+
execFileSync("chflags", ["uchg", file], { stdio: "ignore" });
|
|
64
|
+
} catch {
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
try {
|
|
68
|
+
execFileSync("chattr", ["+i", file], { stdio: "ignore" });
|
|
69
|
+
} catch {
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function unlockFile(file) {
|
|
76
|
+
if (!existsSync(file)) return;
|
|
77
|
+
try {
|
|
78
|
+
if (process.platform === "win32") {
|
|
79
|
+
try {
|
|
80
|
+
execFileSync("icacls", [file, "/remove:d", "*S-1-1-0"], { stdio: "ignore" });
|
|
81
|
+
} catch {
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
execFileSync("icacls", [file, "/reset"], { stdio: "ignore" });
|
|
85
|
+
} catch {
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
execFileSync("attrib", ["-R", file], { stdio: "ignore" });
|
|
89
|
+
} catch {
|
|
90
|
+
}
|
|
91
|
+
} else if (process.platform === "darwin") {
|
|
92
|
+
try {
|
|
93
|
+
execFileSync("chflags", ["nouchg", file], { stdio: "ignore" });
|
|
94
|
+
} catch {
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
try {
|
|
98
|
+
execFileSync("chattr", ["-i", file], { stdio: "ignore" });
|
|
99
|
+
} catch {
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function protectedTargets() {
|
|
106
|
+
const p = globalPaths();
|
|
107
|
+
return [
|
|
108
|
+
join(p.hooksDir, "guard.mjs"),
|
|
109
|
+
join(p.hooksDir, "audit.mjs"),
|
|
110
|
+
join(p.hooksDir, "stop.mjs"),
|
|
111
|
+
p.configPath,
|
|
112
|
+
p.settingsPath
|
|
113
|
+
];
|
|
114
|
+
}
|
|
115
|
+
function lockProtected() {
|
|
116
|
+
for (const f of protectedTargets()) lockFile(f);
|
|
117
|
+
}
|
|
118
|
+
function unlockProtected() {
|
|
119
|
+
for (const f of protectedTargets()) unlockFile(f);
|
|
120
|
+
}
|
|
121
|
+
function globalPaths() {
|
|
122
|
+
const home = homedir();
|
|
123
|
+
const sgDir = join(home, ".solongate");
|
|
124
|
+
const hooksDir = join(sgDir, "hooks");
|
|
125
|
+
const claudeDir = join(home, ".claude");
|
|
126
|
+
return {
|
|
127
|
+
home,
|
|
128
|
+
sgDir,
|
|
129
|
+
hooksDir,
|
|
130
|
+
claudeDir,
|
|
131
|
+
settingsPath: join(claudeDir, "settings.json"),
|
|
132
|
+
backupPath: join(claudeDir, "settings.solongate.bak"),
|
|
133
|
+
configPath: join(sgDir, "cloud-guard.json")
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function readHook(filename) {
|
|
137
|
+
return readFileSync(join(HOOKS_DIR, filename), "utf-8");
|
|
138
|
+
}
|
|
139
|
+
function readGuard() {
|
|
140
|
+
const bundled = join(HOOKS_DIR, "guard.bundled.mjs");
|
|
141
|
+
return existsSync(bundled) ? readFileSync(bundled, "utf-8") : readHook("guard.mjs");
|
|
142
|
+
}
|
|
143
|
+
function ask(question) {
|
|
144
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
145
|
+
return new Promise((res) => rl.question(question, (a) => {
|
|
146
|
+
rl.close();
|
|
147
|
+
res(a.trim());
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
function runGlobalRestore() {
|
|
151
|
+
const p = globalPaths();
|
|
152
|
+
unlockProtected();
|
|
153
|
+
if (existsSync(p.backupPath)) {
|
|
154
|
+
writeFileSync(p.settingsPath, readFileSync(p.backupPath, "utf-8"));
|
|
155
|
+
console.log(` Restored ${p.settingsPath} from backup.`);
|
|
156
|
+
} else if (existsSync(p.settingsPath)) {
|
|
157
|
+
try {
|
|
158
|
+
const s = JSON.parse(readFileSync(p.settingsPath, "utf-8"));
|
|
159
|
+
delete s.hooks;
|
|
160
|
+
writeFileSync(p.settingsPath, JSON.stringify(s, null, 2) + "\n");
|
|
161
|
+
console.log(` Removed SolonGate hooks from ${p.settingsPath}.`);
|
|
162
|
+
} catch {
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
console.log(" Nothing to restore \u2014 no global Claude Code settings found.");
|
|
166
|
+
}
|
|
167
|
+
console.log(" Global SolonGate enforcement uninstalled. Restart Claude Code.");
|
|
168
|
+
}
|
|
169
|
+
async function runGlobalInstall(opts = {}) {
|
|
170
|
+
const p = globalPaths();
|
|
171
|
+
let apiKey = opts.apiKey || process.env["SOLONGATE_API_KEY"] || "";
|
|
172
|
+
if (!apiKey || apiKey === "sg_live_your_key_here") {
|
|
173
|
+
try {
|
|
174
|
+
const cfg = JSON.parse(readFileSync(p.configPath, "utf-8"));
|
|
175
|
+
if (cfg && typeof cfg.apiKey === "string") apiKey = cfg.apiKey;
|
|
176
|
+
} catch {
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (!apiKey || apiKey === "sg_live_your_key_here") {
|
|
180
|
+
apiKey = await ask(" Enter your SolonGate API key (sg_live_\u2026 from https://dashboard.solongate.com): ");
|
|
181
|
+
}
|
|
182
|
+
if (!apiKey.startsWith("sg_live_") && !apiKey.startsWith("sg_test_")) {
|
|
183
|
+
console.log(" Invalid API key. Must start with sg_live_ or sg_test_");
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
const apiUrl = opts.apiUrl || process.env["SOLONGATE_API_URL"] || "https://api.solongate.com";
|
|
187
|
+
mkdirSync(p.hooksDir, { recursive: true });
|
|
188
|
+
mkdirSync(p.claudeDir, { recursive: true });
|
|
189
|
+
unlockProtected();
|
|
190
|
+
writeFileSync(join(p.hooksDir, "guard.mjs"), readGuard());
|
|
191
|
+
writeFileSync(join(p.hooksDir, "audit.mjs"), readHook("audit.mjs"));
|
|
192
|
+
writeFileSync(join(p.hooksDir, "stop.mjs"), readHook("stop.mjs"));
|
|
193
|
+
console.log(` Installed hooks \u2192 ${p.hooksDir}`);
|
|
194
|
+
writeFileSync(p.configPath, JSON.stringify({ apiKey, apiUrl }, null, 2) + "\n");
|
|
195
|
+
console.log(` Wrote ${p.configPath}`);
|
|
196
|
+
let existing = {};
|
|
197
|
+
if (existsSync(p.settingsPath)) {
|
|
198
|
+
const raw = readFileSync(p.settingsPath, "utf-8");
|
|
199
|
+
if (!existsSync(p.backupPath)) {
|
|
200
|
+
writeFileSync(p.backupPath, raw);
|
|
201
|
+
console.log(` Backed up existing settings \u2192 ${p.backupPath}`);
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
existing = JSON.parse(raw);
|
|
205
|
+
} catch {
|
|
206
|
+
existing = {};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const guardAbs = join(p.hooksDir, "guard.mjs").replace(/\\/g, "/");
|
|
210
|
+
const auditAbs = join(p.hooksDir, "audit.mjs").replace(/\\/g, "/");
|
|
211
|
+
const stopAbs = join(p.hooksDir, "stop.mjs").replace(/\\/g, "/");
|
|
212
|
+
const merged = {
|
|
213
|
+
...existing,
|
|
214
|
+
hooks: {
|
|
215
|
+
PreToolUse: [{ matcher: "", hooks: [{ type: "command", command: `node "${guardAbs}" claude-code "Claude Code"` }] }],
|
|
216
|
+
PostToolUse: [{ matcher: "", hooks: [{ type: "command", command: `node "${auditAbs}" claude-code "Claude Code"` }] }],
|
|
217
|
+
Stop: [{ matcher: "", hooks: [{ type: "command", command: `node "${stopAbs}" claude-code "Claude Code"` }] }]
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
writeFileSync(p.settingsPath, JSON.stringify(merged, null, 2) + "\n");
|
|
221
|
+
console.log(` Registered global hooks \u2192 ${p.settingsPath}`);
|
|
222
|
+
if (process.env["SOLONGATE_OS_LOCK"] === "1") {
|
|
223
|
+
lockProtected();
|
|
224
|
+
console.log(" Locked protection files (OS-level read-only/immutable).");
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
40
228
|
// src/init.ts
|
|
41
229
|
var SEARCH_PATHS = [
|
|
42
230
|
".mcp.json",
|
|
43
231
|
"mcp.json",
|
|
44
232
|
".claude/mcp.json"
|
|
45
233
|
];
|
|
46
|
-
var CLAUDE_DESKTOP_PATHS = process.platform === "win32" ? [
|
|
234
|
+
var CLAUDE_DESKTOP_PATHS = process.platform === "win32" ? [join2(process.env["APPDATA"] ?? "", "Claude", "claude_desktop_config.json")] : process.platform === "darwin" ? [join2(process.env["HOME"] ?? "", "Library", "Application Support", "Claude", "claude_desktop_config.json")] : [join2(process.env["HOME"] ?? "", ".config", "claude", "claude_desktop_config.json")];
|
|
47
235
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
48
236
|
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
49
237
|
var spinnerInterval = null;
|
|
@@ -66,14 +254,14 @@ function stopSpinner(result) {
|
|
|
66
254
|
}
|
|
67
255
|
function findConfigFile(explicitPath, createIfMissing = false) {
|
|
68
256
|
if (explicitPath) {
|
|
69
|
-
if (
|
|
70
|
-
return { path:
|
|
257
|
+
if (existsSync2(explicitPath)) {
|
|
258
|
+
return { path: resolve2(explicitPath), type: "mcp" };
|
|
71
259
|
}
|
|
72
260
|
return null;
|
|
73
261
|
}
|
|
74
262
|
for (const searchPath of SEARCH_PATHS) {
|
|
75
|
-
const full =
|
|
76
|
-
if (
|
|
263
|
+
const full = resolve2(searchPath);
|
|
264
|
+
if (existsSync2(full)) return { path: full, type: "mcp" };
|
|
77
265
|
}
|
|
78
266
|
if (createIfMissing) {
|
|
79
267
|
const starterConfig = {
|
|
@@ -88,19 +276,19 @@ function findConfigFile(explicitPath, createIfMissing = false) {
|
|
|
88
276
|
}
|
|
89
277
|
}
|
|
90
278
|
};
|
|
91
|
-
const starterPath =
|
|
92
|
-
|
|
279
|
+
const starterPath = resolve2(".mcp.json");
|
|
280
|
+
writeFileSync2(starterPath, JSON.stringify(starterConfig, null, 2) + "\n");
|
|
93
281
|
console.log(" Created .mcp.json with starter servers (filesystem, playwright).");
|
|
94
282
|
console.log("");
|
|
95
283
|
return { path: starterPath, type: "mcp", created: true };
|
|
96
284
|
}
|
|
97
285
|
for (const desktopPath of CLAUDE_DESKTOP_PATHS) {
|
|
98
|
-
if (
|
|
286
|
+
if (existsSync2(desktopPath)) return { path: desktopPath, type: "claude-desktop" };
|
|
99
287
|
}
|
|
100
288
|
return null;
|
|
101
289
|
}
|
|
102
290
|
function readConfig(filePath) {
|
|
103
|
-
const content =
|
|
291
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
104
292
|
const parsed = JSON.parse(content);
|
|
105
293
|
if (parsed.mcpServers) return parsed;
|
|
106
294
|
throw new Error(`Unrecognized config format in ${filePath}`);
|
|
@@ -135,7 +323,7 @@ function wrapServer(serverName, server, policy, agentName) {
|
|
|
135
323
|
};
|
|
136
324
|
}
|
|
137
325
|
async function prompt(question) {
|
|
138
|
-
const rl =
|
|
326
|
+
const rl = createInterface2({ input: process.stdin, output: process.stderr });
|
|
139
327
|
return new Promise((res) => {
|
|
140
328
|
rl.question(question, (answer) => {
|
|
141
329
|
rl.close();
|
|
@@ -147,7 +335,9 @@ function parseInitArgs(argv) {
|
|
|
147
335
|
const args = argv.slice(2);
|
|
148
336
|
const options = {
|
|
149
337
|
all: false,
|
|
150
|
-
tools: []
|
|
338
|
+
tools: [],
|
|
339
|
+
global: false,
|
|
340
|
+
restore: false
|
|
151
341
|
};
|
|
152
342
|
for (let i = 0; i < args.length; i++) {
|
|
153
343
|
switch (args[i]) {
|
|
@@ -163,6 +353,14 @@ function parseInitArgs(argv) {
|
|
|
163
353
|
case "--all":
|
|
164
354
|
options.all = true;
|
|
165
355
|
break;
|
|
356
|
+
case "--global":
|
|
357
|
+
case "-g":
|
|
358
|
+
options.global = true;
|
|
359
|
+
break;
|
|
360
|
+
case "--restore":
|
|
361
|
+
case "--uninstall":
|
|
362
|
+
options.restore = true;
|
|
363
|
+
break;
|
|
166
364
|
case "--claude-code":
|
|
167
365
|
options.tools.push("claude-code");
|
|
168
366
|
break;
|
|
@@ -183,12 +381,17 @@ SolonGate Init \u2014 Protect your MCP servers in seconds
|
|
|
183
381
|
|
|
184
382
|
USAGE
|
|
185
383
|
npx @solongate/proxy init --all
|
|
384
|
+
npx @solongate/proxy init --global # system-wide (every project)
|
|
186
385
|
|
|
187
386
|
OPTIONS
|
|
188
387
|
--config <path> Path to MCP config file (default: auto-detect)
|
|
189
388
|
--policy <file> Custom policy JSON file (auto-detects policy.json)
|
|
190
389
|
--api-key <key> SolonGate API key (sg_live_... or sg_test_...)
|
|
191
390
|
--all Protect all servers without prompting
|
|
391
|
+
--global, -g Install a SYSTEM-WIDE Claude Code hook (~/.claude) that
|
|
392
|
+
guards EVERY session on the machine with your cloud
|
|
393
|
+
policy (OPA WASM) \u2014 like the air-gapped product.
|
|
394
|
+
--restore With --global: undo the system-wide install.
|
|
192
395
|
-h, --help Show this help message
|
|
193
396
|
|
|
194
397
|
AI TOOL HOOKS (default: all)
|
|
@@ -199,59 +402,66 @@ EXAMPLES
|
|
|
199
402
|
npx @solongate/proxy init --all # Protect everything, all tools
|
|
200
403
|
npx @solongate/proxy init --all --claude-code # Only Claude Code hooks
|
|
201
404
|
npx @solongate/proxy init --all --policy policy.json # With custom policy
|
|
405
|
+
npx @solongate/proxy init --global --api-key sg_live_\u2026 # System-wide enforcement
|
|
406
|
+
npx @solongate/proxy init --global --restore # Undo system-wide install
|
|
202
407
|
`;
|
|
203
408
|
console.log(help);
|
|
204
409
|
}
|
|
205
|
-
var
|
|
206
|
-
var
|
|
410
|
+
var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
|
|
411
|
+
var HOOKS_DIR2 = resolve2(__dirname2, "..", "hooks");
|
|
207
412
|
function readHookScript(filename) {
|
|
208
|
-
return
|
|
413
|
+
return readFileSync2(join2(HOOKS_DIR2, filename), "utf-8");
|
|
414
|
+
}
|
|
415
|
+
function readGuardForGlobal() {
|
|
416
|
+
const bundled = join2(HOOKS_DIR2, "guard.bundled.mjs");
|
|
417
|
+
if (existsSync2(bundled)) return readFileSync2(bundled, "utf-8");
|
|
418
|
+
return readFileSync2(join2(HOOKS_DIR2, "guard.mjs"), "utf-8");
|
|
209
419
|
}
|
|
210
420
|
function unlockProtectedDirs() {
|
|
211
421
|
const dirs = [".solongate", ".claude", ".gemini"];
|
|
212
422
|
for (const dir of dirs) {
|
|
213
|
-
const fullDir =
|
|
214
|
-
if (!
|
|
423
|
+
const fullDir = resolve2(dir);
|
|
424
|
+
if (!existsSync2(fullDir)) continue;
|
|
215
425
|
try {
|
|
216
426
|
if (process.platform === "win32") {
|
|
217
427
|
try {
|
|
218
|
-
|
|
428
|
+
execFileSync2("icacls", [fullDir, "/remove:d", "*S-1-1-0", "/T", "/Q"], { stdio: "ignore" });
|
|
219
429
|
} catch {
|
|
220
430
|
}
|
|
221
431
|
try {
|
|
222
|
-
|
|
432
|
+
execFileSync2("attrib", ["-R", "/S", "/D", fullDir], { stdio: "ignore" });
|
|
223
433
|
} catch {
|
|
224
434
|
}
|
|
225
435
|
} else {
|
|
226
436
|
try {
|
|
227
|
-
|
|
437
|
+
execFileSync2("chattr", ["-i", "-R", fullDir], { stdio: "ignore" });
|
|
228
438
|
} catch {
|
|
229
439
|
}
|
|
230
|
-
|
|
440
|
+
execFileSync2("chmod", ["-R", "u+w", fullDir], { stdio: "ignore" });
|
|
231
441
|
}
|
|
232
442
|
} catch {
|
|
233
443
|
}
|
|
234
444
|
}
|
|
235
445
|
const protectedFiles = [".env", ".gitignore", ".mcp.json", "policy.json"];
|
|
236
446
|
for (const file of protectedFiles) {
|
|
237
|
-
const fullPath =
|
|
238
|
-
if (!
|
|
447
|
+
const fullPath = resolve2(file);
|
|
448
|
+
if (!existsSync2(fullPath)) continue;
|
|
239
449
|
try {
|
|
240
450
|
if (process.platform === "win32") {
|
|
241
451
|
try {
|
|
242
|
-
|
|
452
|
+
execFileSync2("icacls", [fullPath, "/remove:d", "*S-1-1-0", "/Q"], { stdio: "ignore" });
|
|
243
453
|
} catch {
|
|
244
454
|
}
|
|
245
455
|
try {
|
|
246
|
-
|
|
456
|
+
execFileSync2("attrib", ["-R", fullPath], { stdio: "ignore" });
|
|
247
457
|
} catch {
|
|
248
458
|
}
|
|
249
459
|
} else {
|
|
250
460
|
try {
|
|
251
|
-
|
|
461
|
+
execFileSync2("chattr", ["-i", fullPath], { stdio: "ignore" });
|
|
252
462
|
} catch {
|
|
253
463
|
}
|
|
254
|
-
|
|
464
|
+
execFileSync2("chmod", ["u+w", fullPath], { stdio: "ignore" });
|
|
255
465
|
}
|
|
256
466
|
} catch {
|
|
257
467
|
}
|
|
@@ -259,26 +469,26 @@ function unlockProtectedDirs() {
|
|
|
259
469
|
}
|
|
260
470
|
function installHooks(selectedTools = [], wrappedMcpConfig) {
|
|
261
471
|
unlockProtectedDirs();
|
|
262
|
-
const hooksDir =
|
|
263
|
-
|
|
472
|
+
const hooksDir = resolve2(".solongate", "hooks");
|
|
473
|
+
mkdirSync2(hooksDir, { recursive: true });
|
|
264
474
|
const hookFiles = ["guard.mjs", "audit.mjs", "stop.mjs"];
|
|
265
475
|
let hooksUpdated = 0;
|
|
266
476
|
let hooksSkipped = 0;
|
|
267
477
|
for (const filename of hookFiles) {
|
|
268
|
-
const hookPath =
|
|
269
|
-
const latest = readHookScript(filename);
|
|
478
|
+
const hookPath = join2(hooksDir, filename);
|
|
479
|
+
const latest = filename === "guard.mjs" ? readGuardForGlobal() : readHookScript(filename);
|
|
270
480
|
let needsWrite = true;
|
|
271
|
-
if (
|
|
272
|
-
const current =
|
|
481
|
+
if (existsSync2(hookPath)) {
|
|
482
|
+
const current = readFileSync2(hookPath, "utf-8");
|
|
273
483
|
if (current === latest) {
|
|
274
484
|
needsWrite = false;
|
|
275
485
|
hooksSkipped++;
|
|
276
486
|
}
|
|
277
487
|
}
|
|
278
488
|
if (needsWrite) {
|
|
279
|
-
|
|
489
|
+
writeFileSync2(hookPath, latest);
|
|
280
490
|
hooksUpdated++;
|
|
281
|
-
console.log(` ${
|
|
491
|
+
console.log(` ${existsSync2(hookPath) ? "Updated" : "Created"} ${hookPath}`);
|
|
282
492
|
}
|
|
283
493
|
}
|
|
284
494
|
if (hooksSkipped > 0 && hooksUpdated === 0) {
|
|
@@ -292,8 +502,8 @@ function installHooks(selectedTools = [], wrappedMcpConfig) {
|
|
|
292
502
|
const activatedNames = [];
|
|
293
503
|
const skippedNames = [];
|
|
294
504
|
for (const client of clients) {
|
|
295
|
-
const clientDir =
|
|
296
|
-
|
|
505
|
+
const clientDir = resolve2(client.dir);
|
|
506
|
+
mkdirSync2(clientDir, { recursive: true });
|
|
297
507
|
const guardCmd = `node .solongate/hooks/guard.mjs ${client.agentId} "${client.agentName}"`;
|
|
298
508
|
const auditCmd = `node .solongate/hooks/audit.mjs ${client.agentId} "${client.agentName}"`;
|
|
299
509
|
const stopCmd = `node .solongate/hooks/stop.mjs ${client.agentId} "${client.agentName}"`;
|
|
@@ -320,7 +530,7 @@ function installHooks(selectedTools = [], wrappedMcpConfig) {
|
|
|
320
530
|
}
|
|
321
531
|
}
|
|
322
532
|
function installStandardHookConfig(clientDir, clientName, guardCmd, auditCmd, stopCmd) {
|
|
323
|
-
const settingsPath =
|
|
533
|
+
const settingsPath = join2(clientDir, "settings.json");
|
|
324
534
|
const hookSettings = {
|
|
325
535
|
PreToolUse: [
|
|
326
536
|
{ matcher: "", hooks: [{ type: "command", command: guardCmd }] }
|
|
@@ -334,22 +544,22 @@ function installStandardHookConfig(clientDir, clientName, guardCmd, auditCmd, st
|
|
|
334
544
|
};
|
|
335
545
|
let existing = {};
|
|
336
546
|
try {
|
|
337
|
-
existing = JSON.parse(
|
|
547
|
+
existing = JSON.parse(readFileSync2(settingsPath, "utf-8"));
|
|
338
548
|
} catch {
|
|
339
549
|
}
|
|
340
550
|
const merged = { ...existing, hooks: hookSettings };
|
|
341
551
|
const mergedStr = JSON.stringify(merged, null, 2) + "\n";
|
|
342
|
-
const existingStr =
|
|
552
|
+
const existingStr = existsSync2(settingsPath) ? readFileSync2(settingsPath, "utf-8") : "";
|
|
343
553
|
if (mergedStr === existingStr) return "skipped";
|
|
344
|
-
|
|
554
|
+
writeFileSync2(settingsPath, mergedStr);
|
|
345
555
|
console.log(` ${existingStr ? "Updated" : "Created"} ${settingsPath}`);
|
|
346
556
|
return "installed";
|
|
347
557
|
}
|
|
348
558
|
function installGeminiConfig(clientDir, guardCmd, auditCmd, _stopCmd, wrappedMcpConfig) {
|
|
349
|
-
const settingsPath =
|
|
559
|
+
const settingsPath = join2(clientDir, "settings.json");
|
|
350
560
|
let existing = {};
|
|
351
561
|
try {
|
|
352
|
-
existing = JSON.parse(
|
|
562
|
+
existing = JSON.parse(readFileSync2(settingsPath, "utf-8"));
|
|
353
563
|
} catch {
|
|
354
564
|
}
|
|
355
565
|
const merged = { ...existing };
|
|
@@ -377,17 +587,17 @@ function installGeminiConfig(clientDir, guardCmd, auditCmd, _stopCmd, wrappedMcp
|
|
|
377
587
|
]
|
|
378
588
|
};
|
|
379
589
|
const mergedStr = JSON.stringify(merged, null, 2) + "\n";
|
|
380
|
-
const existingStr =
|
|
590
|
+
const existingStr = existsSync2(settingsPath) ? readFileSync2(settingsPath, "utf-8") : "";
|
|
381
591
|
if (mergedStr === existingStr) return "skipped";
|
|
382
|
-
|
|
592
|
+
writeFileSync2(settingsPath, mergedStr);
|
|
383
593
|
console.log(` ${existingStr ? "Updated" : "Created"} ${settingsPath}`);
|
|
384
594
|
return "installed";
|
|
385
595
|
}
|
|
386
596
|
function ensureEnvFile() {
|
|
387
597
|
let envChanged = false;
|
|
388
598
|
let gitignoreChanged = false;
|
|
389
|
-
const envPath =
|
|
390
|
-
if (!
|
|
599
|
+
const envPath = resolve2(".env");
|
|
600
|
+
if (!existsSync2(envPath)) {
|
|
391
601
|
const envContent = `# SolonGate Configuration
|
|
392
602
|
# IMPORTANT: Never commit this file to git!
|
|
393
603
|
|
|
@@ -398,25 +608,25 @@ SOLONGATE_API_KEY=sg_live_your_key_here
|
|
|
398
608
|
# Enable with: npx @solongate/proxy --ai-judge ...
|
|
399
609
|
GROQ_API_KEY=gsk_your_groq_key_here
|
|
400
610
|
`;
|
|
401
|
-
|
|
611
|
+
writeFileSync2(envPath, envContent);
|
|
402
612
|
console.log(` Created .env`);
|
|
403
613
|
console.log(` \u2192 Set your API key in .env (get one at https://dashboard.solongate.com)`);
|
|
404
614
|
envChanged = true;
|
|
405
615
|
} else {
|
|
406
|
-
const existingEnv =
|
|
616
|
+
const existingEnv = readFileSync2(envPath, "utf-8");
|
|
407
617
|
if (!existingEnv.includes("SOLONGATE_API_KEY")) {
|
|
408
618
|
const separator = existingEnv.endsWith("\n") ? "" : "\n";
|
|
409
619
|
const appendContent = `${separator}
|
|
410
620
|
# SolonGate API key \u2014 get one at https://dashboard.solongate.com
|
|
411
621
|
SOLONGATE_API_KEY=sg_live_your_key_here
|
|
412
622
|
`;
|
|
413
|
-
|
|
623
|
+
writeFileSync2(envPath, existingEnv + appendContent);
|
|
414
624
|
console.log(` Updated .env (added SOLONGATE_API_KEY)`);
|
|
415
625
|
console.log(` \u2192 Set your API key in .env (get one at https://dashboard.solongate.com)`);
|
|
416
626
|
envChanged = true;
|
|
417
627
|
}
|
|
418
628
|
}
|
|
419
|
-
const gitignorePath =
|
|
629
|
+
const gitignorePath = resolve2(".gitignore");
|
|
420
630
|
const requiredLines = [
|
|
421
631
|
".env",
|
|
422
632
|
".env.local",
|
|
@@ -425,19 +635,19 @@ SOLONGATE_API_KEY=sg_live_your_key_here
|
|
|
425
635
|
".claude/**",
|
|
426
636
|
".gemini/**"
|
|
427
637
|
];
|
|
428
|
-
if (
|
|
429
|
-
let gitignore =
|
|
638
|
+
if (existsSync2(gitignorePath)) {
|
|
639
|
+
let gitignore = readFileSync2(gitignorePath, "utf-8");
|
|
430
640
|
const existingLines = new Set(gitignore.split("\n").map((l) => l.trim()));
|
|
431
641
|
const missing = requiredLines.filter((line) => !existingLines.has(line));
|
|
432
642
|
if (missing.length > 0) {
|
|
433
643
|
gitignore = gitignore.trimEnd() + "\n\n# SolonGate\n" + missing.join("\n") + "\n";
|
|
434
|
-
|
|
644
|
+
writeFileSync2(gitignorePath, gitignore);
|
|
435
645
|
console.log(` Updated .gitignore (+${missing.length} entries)`);
|
|
436
646
|
gitignoreChanged = true;
|
|
437
647
|
}
|
|
438
648
|
} else {
|
|
439
649
|
const content = "# SolonGate\n" + requiredLines.join("\n") + "\nnode_modules/\n";
|
|
440
|
-
|
|
650
|
+
writeFileSync2(gitignorePath, content);
|
|
441
651
|
console.log(` Created .gitignore`);
|
|
442
652
|
gitignoreChanged = true;
|
|
443
653
|
}
|
|
@@ -448,6 +658,28 @@ SOLONGATE_API_KEY=sg_live_your_key_here
|
|
|
448
658
|
}
|
|
449
659
|
async function main() {
|
|
450
660
|
const options = parseInitArgs(process.argv);
|
|
661
|
+
if (options.global) {
|
|
662
|
+
console.log("");
|
|
663
|
+
console.log(` ${c.bold}SolonGate \u2014 Global Install${c.reset}`);
|
|
664
|
+
console.log("");
|
|
665
|
+
if (options.restore) {
|
|
666
|
+
runGlobalRestore();
|
|
667
|
+
} else {
|
|
668
|
+
await runGlobalInstall({ apiKey: options.apiKey });
|
|
669
|
+
console.log("");
|
|
670
|
+
console.log(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
671
|
+
console.log(" \u2502 Global enforcement active. \u2502");
|
|
672
|
+
console.log(" \u2502 Every Claude Code session on this machine is \u2502");
|
|
673
|
+
console.log(" \u2502 now guarded by your cloud policy (OPA WASM). \u2502");
|
|
674
|
+
console.log(" \u2502 \u2502");
|
|
675
|
+
console.log(" \u2502 Undo: init --global --restore \u2502");
|
|
676
|
+
console.log(" \u2502 Logs: https://dashboard.solongate.com \u2502");
|
|
677
|
+
console.log(" \u2502 Restart Claude Code to apply. \u2502");
|
|
678
|
+
console.log(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
679
|
+
}
|
|
680
|
+
console.log("");
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
451
683
|
const fullBanner = BANNER_FULL;
|
|
452
684
|
const mediumBanner = [
|
|
453
685
|
" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2557",
|
|
@@ -556,9 +788,9 @@ async function main() {
|
|
|
556
788
|
console.log("");
|
|
557
789
|
let apiKey = options.apiKey || process.env.SOLONGATE_API_KEY || "";
|
|
558
790
|
if (!apiKey) {
|
|
559
|
-
const envPath =
|
|
560
|
-
if (
|
|
561
|
-
const envContent =
|
|
791
|
+
const envPath = resolve2(".env");
|
|
792
|
+
if (existsSync2(envPath)) {
|
|
793
|
+
const envContent = readFileSync2(envPath, "utf-8");
|
|
562
794
|
const match = envContent.match(/^SOLONGATE_API_KEY=(sg_(?:live|test)_\w+)/m);
|
|
563
795
|
if (match) apiKey = match[1];
|
|
564
796
|
}
|
|
@@ -583,8 +815,8 @@ async function main() {
|
|
|
583
815
|
}
|
|
584
816
|
let policyValue;
|
|
585
817
|
if (options.policy) {
|
|
586
|
-
const policyPath =
|
|
587
|
-
if (
|
|
818
|
+
const policyPath = resolve2(options.policy);
|
|
819
|
+
if (existsSync2(policyPath)) {
|
|
588
820
|
policyValue = `./${options.policy}`;
|
|
589
821
|
console.log(` Policy: ${policyPath}`);
|
|
590
822
|
} else {
|
|
@@ -592,8 +824,8 @@ async function main() {
|
|
|
592
824
|
process.exit(1);
|
|
593
825
|
}
|
|
594
826
|
} else {
|
|
595
|
-
const defaultPolicy =
|
|
596
|
-
if (
|
|
827
|
+
const defaultPolicy = resolve2("policy.json");
|
|
828
|
+
if (existsSync2(defaultPolicy)) {
|
|
597
829
|
policyValue = "./policy.json";
|
|
598
830
|
console.log(` Policy: ${defaultPolicy} (auto-detected)`);
|
|
599
831
|
} else {
|
|
@@ -612,7 +844,7 @@ async function main() {
|
|
|
612
844
|
newConfig.mcpServers[name] = config.mcpServers[name];
|
|
613
845
|
}
|
|
614
846
|
}
|
|
615
|
-
const currentContent =
|
|
847
|
+
const currentContent = readFileSync2(configInfo.path, "utf-8");
|
|
616
848
|
let newContent;
|
|
617
849
|
if (configInfo.type === "claude-desktop") {
|
|
618
850
|
const original = JSON.parse(currentContent);
|
|
@@ -625,7 +857,7 @@ async function main() {
|
|
|
625
857
|
if (newContent === currentContent) {
|
|
626
858
|
stopSpinner("\u2713 Config already up to date");
|
|
627
859
|
} else {
|
|
628
|
-
|
|
860
|
+
writeFileSync2(configInfo.path, newContent);
|
|
629
861
|
stopSpinner("\u2713 Config updated");
|
|
630
862
|
}
|
|
631
863
|
console.log("");
|