@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/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" ? [join(process.env["APPDATA"] ?? "", "Claude", "claude_desktop_config.json")] : process.platform === "darwin" ? [join(process.env["HOME"] ?? "", "Library", "Application Support", "Claude", "claude_desktop_config.json")] : [join(process.env["HOME"] ?? "", ".config", "claude", "claude_desktop_config.json")];
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 (existsSync(explicitPath)) {
70
- return { path: resolve(explicitPath), type: "mcp" };
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 = resolve(searchPath);
76
- if (existsSync(full)) return { path: full, type: "mcp" };
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 = resolve(".mcp.json");
92
- writeFileSync(starterPath, JSON.stringify(starterConfig, null, 2) + "\n");
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 (existsSync(desktopPath)) return { path: desktopPath, type: "claude-desktop" };
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 = readFileSync(filePath, "utf-8");
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 = createInterface({ input: process.stdin, output: process.stderr });
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 __dirname = dirname(fileURLToPath(import.meta.url));
206
- var HOOKS_DIR = resolve(__dirname, "..", "hooks");
410
+ var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
411
+ var HOOKS_DIR2 = resolve2(__dirname2, "..", "hooks");
207
412
  function readHookScript(filename) {
208
- return readFileSync(join(HOOKS_DIR, filename), "utf-8");
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 = resolve(dir);
214
- if (!existsSync(fullDir)) continue;
423
+ const fullDir = resolve2(dir);
424
+ if (!existsSync2(fullDir)) continue;
215
425
  try {
216
426
  if (process.platform === "win32") {
217
427
  try {
218
- execFileSync("icacls", [fullDir, "/remove:d", "*S-1-1-0", "/T", "/Q"], { stdio: "ignore" });
428
+ execFileSync2("icacls", [fullDir, "/remove:d", "*S-1-1-0", "/T", "/Q"], { stdio: "ignore" });
219
429
  } catch {
220
430
  }
221
431
  try {
222
- execFileSync("attrib", ["-R", "/S", "/D", fullDir], { stdio: "ignore" });
432
+ execFileSync2("attrib", ["-R", "/S", "/D", fullDir], { stdio: "ignore" });
223
433
  } catch {
224
434
  }
225
435
  } else {
226
436
  try {
227
- execFileSync("chattr", ["-i", "-R", fullDir], { stdio: "ignore" });
437
+ execFileSync2("chattr", ["-i", "-R", fullDir], { stdio: "ignore" });
228
438
  } catch {
229
439
  }
230
- execFileSync("chmod", ["-R", "u+w", fullDir], { stdio: "ignore" });
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 = resolve(file);
238
- if (!existsSync(fullPath)) continue;
447
+ const fullPath = resolve2(file);
448
+ if (!existsSync2(fullPath)) continue;
239
449
  try {
240
450
  if (process.platform === "win32") {
241
451
  try {
242
- execFileSync("icacls", [fullPath, "/remove:d", "*S-1-1-0", "/Q"], { stdio: "ignore" });
452
+ execFileSync2("icacls", [fullPath, "/remove:d", "*S-1-1-0", "/Q"], { stdio: "ignore" });
243
453
  } catch {
244
454
  }
245
455
  try {
246
- execFileSync("attrib", ["-R", fullPath], { stdio: "ignore" });
456
+ execFileSync2("attrib", ["-R", fullPath], { stdio: "ignore" });
247
457
  } catch {
248
458
  }
249
459
  } else {
250
460
  try {
251
- execFileSync("chattr", ["-i", fullPath], { stdio: "ignore" });
461
+ execFileSync2("chattr", ["-i", fullPath], { stdio: "ignore" });
252
462
  } catch {
253
463
  }
254
- execFileSync("chmod", ["u+w", fullPath], { stdio: "ignore" });
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 = resolve(".solongate", "hooks");
263
- mkdirSync(hooksDir, { recursive: true });
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 = join(hooksDir, filename);
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 (existsSync(hookPath)) {
272
- const current = readFileSync(hookPath, "utf-8");
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
- writeFileSync(hookPath, latest);
489
+ writeFileSync2(hookPath, latest);
280
490
  hooksUpdated++;
281
- console.log(` ${existsSync(hookPath) ? "Updated" : "Created"} ${hookPath}`);
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 = resolve(client.dir);
296
- mkdirSync(clientDir, { recursive: true });
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 = join(clientDir, "settings.json");
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(readFileSync(settingsPath, "utf-8"));
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 = existsSync(settingsPath) ? readFileSync(settingsPath, "utf-8") : "";
552
+ const existingStr = existsSync2(settingsPath) ? readFileSync2(settingsPath, "utf-8") : "";
343
553
  if (mergedStr === existingStr) return "skipped";
344
- writeFileSync(settingsPath, mergedStr);
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 = join(clientDir, "settings.json");
559
+ const settingsPath = join2(clientDir, "settings.json");
350
560
  let existing = {};
351
561
  try {
352
- existing = JSON.parse(readFileSync(settingsPath, "utf-8"));
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 = existsSync(settingsPath) ? readFileSync(settingsPath, "utf-8") : "";
590
+ const existingStr = existsSync2(settingsPath) ? readFileSync2(settingsPath, "utf-8") : "";
381
591
  if (mergedStr === existingStr) return "skipped";
382
- writeFileSync(settingsPath, mergedStr);
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 = resolve(".env");
390
- if (!existsSync(envPath)) {
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
- writeFileSync(envPath, envContent);
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 = readFileSync(envPath, "utf-8");
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
- writeFileSync(envPath, existingEnv + appendContent);
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 = resolve(".gitignore");
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 (existsSync(gitignorePath)) {
429
- let gitignore = readFileSync(gitignorePath, "utf-8");
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
- writeFileSync(gitignorePath, gitignore);
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
- writeFileSync(gitignorePath, content);
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 = resolve(".env");
560
- if (existsSync(envPath)) {
561
- const envContent = readFileSync(envPath, "utf-8");
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 = resolve(options.policy);
587
- if (existsSync(policyPath)) {
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 = resolve("policy.json");
596
- if (existsSync(defaultPolicy)) {
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 = readFileSync(configInfo.path, "utf-8");
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
- writeFileSync(configInfo.path, newContent);
860
+ writeFileSync2(configInfo.path, newContent);
629
861
  stopSpinner("\u2713 Config updated");
630
862
  }
631
863
  console.log("");