@solongate/proxy 0.49.1 → 0.51.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 DELETED
@@ -1,910 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // src/init.ts
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
-
10
- // src/cli-utils.ts
11
- var c = {
12
- reset: "\x1B[0m",
13
- bold: "\x1B[1m",
14
- dim: "\x1B[2m",
15
- italic: "\x1B[3m",
16
- white: "\x1B[97m",
17
- gray: "\x1B[90m",
18
- blue1: "\x1B[38;2;20;50;160m",
19
- blue2: "\x1B[38;2;40;80;190m",
20
- blue3: "\x1B[38;2;60;110;215m",
21
- blue4: "\x1B[38;2;90;140;230m",
22
- blue5: "\x1B[38;2;130;170;240m",
23
- blue6: "\x1B[38;2;170;200;250m",
24
- green: "\x1B[38;2;80;200;120m",
25
- red: "\x1B[38;2;220;80;80m",
26
- cyan: "\x1B[38;2;100;200;220m",
27
- yellow: "\x1B[38;2;220;200;80m",
28
- bgBlue: "\x1B[48;2;20;50;160m"
29
- };
30
- var BANNER_FULL = [
31
- " \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 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557",
32
- " \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D",
33
- " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2557 ",
34
- " \u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u255D ",
35
- " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557",
36
- " \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D"
37
- ];
38
- var BANNER_COLORS = [c.blue1, c.blue2, c.blue3, c.blue4, c.blue5, c.blue6];
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 nodeBin = process.execPath.replace(/\\/g, "/");
213
- const merged = {
214
- ...existing,
215
- hooks: {
216
- PreToolUse: [{ matcher: "", hooks: [{ type: "command", command: `"${nodeBin}" "${guardAbs}" claude-code "Claude Code"` }] }],
217
- PostToolUse: [{ matcher: "", hooks: [{ type: "command", command: `"${nodeBin}" "${auditAbs}" claude-code "Claude Code"` }] }],
218
- Stop: [{ matcher: "", hooks: [{ type: "command", command: `"${nodeBin}" "${stopAbs}" claude-code "Claude Code"` }] }]
219
- }
220
- };
221
- writeFileSync(p.settingsPath, JSON.stringify(merged, null, 2) + "\n");
222
- console.log(` Registered global hooks \u2192 ${p.settingsPath}`);
223
- if (process.env["SOLONGATE_OS_LOCK"] === "1") {
224
- lockProtected();
225
- console.log(" Locked protection files (OS-level read-only/immutable).");
226
- }
227
- }
228
-
229
- // src/init.ts
230
- var SEARCH_PATHS = [
231
- ".mcp.json",
232
- "mcp.json",
233
- ".claude/mcp.json"
234
- ];
235
- 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")];
236
- var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
237
- var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
238
- var spinnerInterval = null;
239
- var spinnerFrame = 0;
240
- function startSpinner(message) {
241
- spinnerFrame = 0;
242
- process.stderr.write(` ${SPINNER_FRAMES[0]} ${message}`);
243
- spinnerInterval = setInterval(() => {
244
- spinnerFrame = (spinnerFrame + 1) % SPINNER_FRAMES.length;
245
- process.stderr.write(`\r ${SPINNER_FRAMES[spinnerFrame]} ${message}`);
246
- }, 80);
247
- }
248
- function stopSpinner(result) {
249
- if (spinnerInterval) {
250
- clearInterval(spinnerInterval);
251
- spinnerInterval = null;
252
- }
253
- process.stderr.write(`\r ${result}
254
- `);
255
- }
256
- function findConfigFile(explicitPath, createIfMissing = false) {
257
- if (explicitPath) {
258
- if (existsSync2(explicitPath)) {
259
- return { path: resolve2(explicitPath), type: "mcp" };
260
- }
261
- return null;
262
- }
263
- for (const searchPath of SEARCH_PATHS) {
264
- const full = resolve2(searchPath);
265
- if (existsSync2(full)) return { path: full, type: "mcp" };
266
- }
267
- if (createIfMissing) {
268
- const starterConfig = {
269
- mcpServers: {
270
- "filesystem": {
271
- command: "npx",
272
- args: ["-y", "@modelcontextprotocol/server-filesystem@latest", "."]
273
- },
274
- "playwright": {
275
- command: "npx",
276
- args: ["-y", "@playwright/mcp@latest"]
277
- }
278
- }
279
- };
280
- const starterPath = resolve2(".mcp.json");
281
- writeFileSync2(starterPath, JSON.stringify(starterConfig, null, 2) + "\n");
282
- console.log(" Created .mcp.json with starter servers (filesystem, playwright).");
283
- console.log("");
284
- return { path: starterPath, type: "mcp", created: true };
285
- }
286
- for (const desktopPath of CLAUDE_DESKTOP_PATHS) {
287
- if (existsSync2(desktopPath)) return { path: desktopPath, type: "claude-desktop" };
288
- }
289
- return null;
290
- }
291
- function readConfig(filePath) {
292
- const content = readFileSync2(filePath, "utf-8");
293
- const parsed = JSON.parse(content);
294
- if (parsed.mcpServers) return parsed;
295
- throw new Error(`Unrecognized config format in ${filePath}`);
296
- }
297
- function isAlreadyProtected(server) {
298
- const args = server.args ?? [];
299
- if (server.command === "solongate-proxy") return true;
300
- if (server.command === "@solongate/proxy") return true;
301
- for (const arg of args) {
302
- if (arg === "-y" || arg === "--yes") continue;
303
- if (arg === "@solongate/proxy" || arg === "@solongate/proxy@latest" || arg === "solongate-proxy") return true;
304
- if (/[/\\]packages[/\\]proxy[/\\]dist[/\\]index\.js$/.test(arg)) return true;
305
- break;
306
- }
307
- return false;
308
- }
309
- function wrapServer(serverName, server, policy, agentName) {
310
- const env = { ...server.env ?? {} };
311
- env.SOLONGATE_API_KEY = "${SOLONGATE_API_KEY}";
312
- const proxyArgs = ["-y", "@solongate/proxy@latest"];
313
- if (policy) {
314
- proxyArgs.push("--policy", policy);
315
- }
316
- if (agentName) {
317
- proxyArgs.push("--agent-name", agentName);
318
- }
319
- proxyArgs.push("--verbose", "--", server.command, ...server.args ?? []);
320
- return {
321
- command: "npx",
322
- args: proxyArgs,
323
- env
324
- };
325
- }
326
- async function prompt(question) {
327
- const rl = createInterface2({ input: process.stdin, output: process.stderr });
328
- return new Promise((res) => {
329
- rl.question(question, (answer) => {
330
- rl.close();
331
- res(answer.trim());
332
- });
333
- });
334
- }
335
- function parseInitArgs(argv) {
336
- const args = argv.slice(2);
337
- const options = {
338
- all: false,
339
- tools: [],
340
- global: false,
341
- restore: false
342
- };
343
- for (let i = 0; i < args.length; i++) {
344
- switch (args[i]) {
345
- case "--config":
346
- options.configPath = args[++i];
347
- break;
348
- case "--policy":
349
- options.policy = args[++i];
350
- break;
351
- case "--api-key":
352
- options.apiKey = args[++i];
353
- break;
354
- case "--all":
355
- options.all = true;
356
- break;
357
- case "--global":
358
- case "-g":
359
- options.global = true;
360
- break;
361
- case "--restore":
362
- case "--uninstall":
363
- options.restore = true;
364
- break;
365
- case "--claude-code":
366
- options.tools.push("claude-code");
367
- break;
368
- case "--gemini":
369
- options.tools.push("gemini");
370
- break;
371
- case "--help":
372
- case "-h":
373
- printHelp();
374
- process.exit(0);
375
- }
376
- }
377
- return options;
378
- }
379
- function printHelp() {
380
- const help = `
381
- SolonGate Init \u2014 Protect your MCP servers in seconds
382
-
383
- USAGE
384
- npx @solongate/proxy init --all
385
- npx @solongate/proxy init --global # system-wide (every project)
386
-
387
- OPTIONS
388
- --config <path> Path to MCP config file (default: auto-detect)
389
- --policy <file> Custom policy JSON file (auto-detects policy.json)
390
- --api-key <key> SolonGate API key (sg_live_... or sg_test_...)
391
- --all Protect all servers without prompting
392
- --global, -g Install a SYSTEM-WIDE Claude Code hook (~/.claude) that
393
- guards EVERY session on the machine with your cloud
394
- policy (OPA WASM) \u2014 like the air-gapped product.
395
- --restore With --global: undo the system-wide install.
396
- -h, --help Show this help message
397
-
398
- AI TOOL HOOKS (default: all)
399
- --claude-code Install hooks for Claude Code
400
- --gemini Install hooks for Gemini CLI
401
-
402
- EXAMPLES
403
- npx @solongate/proxy init --all # Protect everything, all tools
404
- npx @solongate/proxy init --all --claude-code # Only Claude Code hooks
405
- npx @solongate/proxy init --all --policy policy.json # With custom policy
406
- npx @solongate/proxy init --global --api-key sg_live_\u2026 # System-wide enforcement
407
- npx @solongate/proxy init --global --restore # Undo system-wide install
408
- `;
409
- console.log(help);
410
- }
411
- var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
412
- var HOOKS_DIR2 = resolve2(__dirname2, "..", "hooks");
413
- function readHookScript(filename) {
414
- return readFileSync2(join2(HOOKS_DIR2, filename), "utf-8");
415
- }
416
- function readGuardForGlobal() {
417
- const bundled = join2(HOOKS_DIR2, "guard.bundled.mjs");
418
- if (existsSync2(bundled)) return readFileSync2(bundled, "utf-8");
419
- return readFileSync2(join2(HOOKS_DIR2, "guard.mjs"), "utf-8");
420
- }
421
- function unlockProtectedDirs() {
422
- const dirs = [".solongate", ".claude", ".gemini"];
423
- for (const dir of dirs) {
424
- const fullDir = resolve2(dir);
425
- if (!existsSync2(fullDir)) continue;
426
- try {
427
- if (process.platform === "win32") {
428
- try {
429
- execFileSync2("icacls", [fullDir, "/remove:d", "*S-1-1-0", "/T", "/Q"], { stdio: "ignore" });
430
- } catch {
431
- }
432
- try {
433
- execFileSync2("attrib", ["-R", "/S", "/D", fullDir], { stdio: "ignore" });
434
- } catch {
435
- }
436
- } else {
437
- try {
438
- execFileSync2("chattr", ["-i", "-R", fullDir], { stdio: "ignore" });
439
- } catch {
440
- }
441
- execFileSync2("chmod", ["-R", "u+w", fullDir], { stdio: "ignore" });
442
- }
443
- } catch {
444
- }
445
- }
446
- const protectedFiles = [".env", ".gitignore", ".mcp.json", "policy.json"];
447
- for (const file of protectedFiles) {
448
- const fullPath = resolve2(file);
449
- if (!existsSync2(fullPath)) continue;
450
- try {
451
- if (process.platform === "win32") {
452
- try {
453
- execFileSync2("icacls", [fullPath, "/remove:d", "*S-1-1-0", "/Q"], { stdio: "ignore" });
454
- } catch {
455
- }
456
- try {
457
- execFileSync2("attrib", ["-R", fullPath], { stdio: "ignore" });
458
- } catch {
459
- }
460
- } else {
461
- try {
462
- execFileSync2("chattr", ["-i", fullPath], { stdio: "ignore" });
463
- } catch {
464
- }
465
- execFileSync2("chmod", ["u+w", fullPath], { stdio: "ignore" });
466
- }
467
- } catch {
468
- }
469
- }
470
- }
471
- function installHooks(selectedTools = [], wrappedMcpConfig) {
472
- unlockProtectedDirs();
473
- const hooksDir = resolve2(".solongate", "hooks");
474
- mkdirSync2(hooksDir, { recursive: true });
475
- const hookFiles = ["guard.mjs", "audit.mjs", "stop.mjs"];
476
- let hooksUpdated = 0;
477
- let hooksSkipped = 0;
478
- for (const filename of hookFiles) {
479
- const hookPath = join2(hooksDir, filename);
480
- const latest = filename === "guard.mjs" ? readGuardForGlobal() : readHookScript(filename);
481
- let needsWrite = true;
482
- if (existsSync2(hookPath)) {
483
- const current = readFileSync2(hookPath, "utf-8");
484
- if (current === latest) {
485
- needsWrite = false;
486
- hooksSkipped++;
487
- }
488
- }
489
- if (needsWrite) {
490
- writeFileSync2(hookPath, latest);
491
- hooksUpdated++;
492
- console.log(` ${existsSync2(hookPath) ? "Updated" : "Created"} ${hookPath}`);
493
- }
494
- }
495
- if (hooksSkipped > 0 && hooksUpdated === 0) {
496
- console.log(` Hook files already up to date`);
497
- }
498
- const allClients = [
499
- { name: "Claude Code", dir: ".claude", key: "claude-code", agentId: "claude-code", agentName: "Claude Code" },
500
- { name: "Gemini CLI", dir: ".gemini", key: "gemini", agentId: "gemini-cli", agentName: "Gemini CLI" }
501
- ];
502
- const clients = selectedTools.length > 0 ? allClients.filter((c2) => selectedTools.includes(c2.key)) : allClients;
503
- const activatedNames = [];
504
- const skippedNames = [];
505
- for (const client of clients) {
506
- const clientDir = resolve2(client.dir);
507
- mkdirSync2(clientDir, { recursive: true });
508
- const nodeBin = process.execPath.replace(/\\/g, "/");
509
- const guardCmd = `"${nodeBin}" .solongate/hooks/guard.mjs ${client.agentId} "${client.agentName}"`;
510
- const auditCmd = `"${nodeBin}" .solongate/hooks/audit.mjs ${client.agentId} "${client.agentName}"`;
511
- const stopCmd = `"${nodeBin}" .solongate/hooks/stop.mjs ${client.agentId} "${client.agentName}"`;
512
- if (client.key === "gemini") {
513
- const result = installGeminiConfig(clientDir, guardCmd, auditCmd, stopCmd, wrappedMcpConfig);
514
- if (result === "skipped") skippedNames.push(client.name);
515
- else activatedNames.push(client.name);
516
- } else {
517
- const result = installStandardHookConfig(clientDir, client.name, guardCmd, auditCmd, stopCmd);
518
- if (result === "skipped") skippedNames.push(client.name);
519
- else activatedNames.push(client.name);
520
- }
521
- }
522
- console.log("");
523
- if (activatedNames.length > 0) {
524
- console.log(" Hooks installed:");
525
- console.log(" guard.mjs \u2192 blocks policy-violating calls (pre-execution)");
526
- console.log(" audit.mjs \u2192 logs all calls to dashboard (post-execution)");
527
- console.log(" stop.mjs \u2192 tracks text-only responses (no tool calls)");
528
- console.log(` Activated for: ${activatedNames.join(", ")}`);
529
- }
530
- if (skippedNames.length > 0) {
531
- console.log(` Already configured: ${skippedNames.join(", ")}`);
532
- }
533
- }
534
- function installStandardHookConfig(clientDir, clientName, guardCmd, auditCmd, stopCmd) {
535
- const settingsPath = join2(clientDir, "settings.json");
536
- const hookSettings = {
537
- PreToolUse: [
538
- { matcher: "", hooks: [{ type: "command", command: guardCmd }] }
539
- ],
540
- PostToolUse: [
541
- { matcher: "", hooks: [{ type: "command", command: auditCmd }] }
542
- ],
543
- Stop: [
544
- { matcher: "", hooks: [{ type: "command", command: stopCmd }] }
545
- ]
546
- };
547
- let existing = {};
548
- try {
549
- existing = JSON.parse(readFileSync2(settingsPath, "utf-8"));
550
- } catch {
551
- }
552
- const merged = { ...existing, hooks: hookSettings };
553
- const mergedStr = JSON.stringify(merged, null, 2) + "\n";
554
- const existingStr = existsSync2(settingsPath) ? readFileSync2(settingsPath, "utf-8") : "";
555
- if (mergedStr === existingStr) return "skipped";
556
- writeFileSync2(settingsPath, mergedStr);
557
- console.log(` ${existingStr ? "Updated" : "Created"} ${settingsPath}`);
558
- return "installed";
559
- }
560
- function installGeminiConfig(clientDir, guardCmd, auditCmd, _stopCmd, wrappedMcpConfig) {
561
- const settingsPath = join2(clientDir, "settings.json");
562
- let existing = {};
563
- try {
564
- existing = JSON.parse(readFileSync2(settingsPath, "utf-8"));
565
- } catch {
566
- }
567
- const merged = { ...existing };
568
- if (wrappedMcpConfig) {
569
- const geminiMcp = {};
570
- for (const [name, server] of Object.entries(wrappedMcpConfig.mcpServers)) {
571
- const args = [...server.args ?? []];
572
- const agentIdx = args.indexOf("--agent-name");
573
- if (agentIdx >= 0 && agentIdx + 1 < args.length) {
574
- args[agentIdx + 1] = "gemini-cli";
575
- } else {
576
- const sepIdx = args.indexOf("--");
577
- if (sepIdx >= 0) args.splice(sepIdx, 0, "--agent-name", "gemini-cli");
578
- }
579
- geminiMcp[name] = { ...server, args };
580
- }
581
- merged.mcpServers = geminiMcp;
582
- }
583
- merged.hooks = {
584
- BeforeTool: [
585
- { matcher: ".*", hooks: [{ type: "command", command: guardCmd }] }
586
- ],
587
- AfterTool: [
588
- { matcher: ".*", hooks: [{ type: "command", command: auditCmd }] }
589
- ]
590
- };
591
- const mergedStr = JSON.stringify(merged, null, 2) + "\n";
592
- const existingStr = existsSync2(settingsPath) ? readFileSync2(settingsPath, "utf-8") : "";
593
- if (mergedStr === existingStr) return "skipped";
594
- writeFileSync2(settingsPath, mergedStr);
595
- console.log(` ${existingStr ? "Updated" : "Created"} ${settingsPath}`);
596
- return "installed";
597
- }
598
- function ensureEnvFile() {
599
- let envChanged = false;
600
- let gitignoreChanged = false;
601
- const envPath = resolve2(".env");
602
- if (!existsSync2(envPath)) {
603
- const envContent = `# SolonGate Configuration
604
- # IMPORTANT: Never commit this file to git!
605
-
606
- # Your SolonGate API key \u2014 get one at https://dashboard.solongate.com
607
- SOLONGATE_API_KEY=sg_live_your_key_here
608
-
609
- # AI Judge (semantic security layer) \u2014 get a free key at https://console.groq.com/keys
610
- # Enable with: npx @solongate/proxy --ai-judge ...
611
- GROQ_API_KEY=gsk_your_groq_key_here
612
- `;
613
- writeFileSync2(envPath, envContent);
614
- console.log(` Created .env`);
615
- console.log(` \u2192 Set your API key in .env (get one at https://dashboard.solongate.com)`);
616
- envChanged = true;
617
- } else {
618
- const existingEnv = readFileSync2(envPath, "utf-8");
619
- if (!existingEnv.includes("SOLONGATE_API_KEY")) {
620
- const separator = existingEnv.endsWith("\n") ? "" : "\n";
621
- const appendContent = `${separator}
622
- # SolonGate API key \u2014 get one at https://dashboard.solongate.com
623
- SOLONGATE_API_KEY=sg_live_your_key_here
624
- `;
625
- writeFileSync2(envPath, existingEnv + appendContent);
626
- console.log(` Updated .env (added SOLONGATE_API_KEY)`);
627
- console.log(` \u2192 Set your API key in .env (get one at https://dashboard.solongate.com)`);
628
- envChanged = true;
629
- }
630
- }
631
- const gitignorePath = resolve2(".gitignore");
632
- const requiredLines = [
633
- ".env",
634
- ".env.local",
635
- ".mcp.json",
636
- ".solongate/**",
637
- ".claude/**",
638
- ".gemini/**"
639
- ];
640
- if (existsSync2(gitignorePath)) {
641
- let gitignore = readFileSync2(gitignorePath, "utf-8");
642
- const existingLines = new Set(gitignore.split("\n").map((l) => l.trim()));
643
- const missing = requiredLines.filter((line) => !existingLines.has(line));
644
- if (missing.length > 0) {
645
- gitignore = gitignore.trimEnd() + "\n\n# SolonGate\n" + missing.join("\n") + "\n";
646
- writeFileSync2(gitignorePath, gitignore);
647
- console.log(` Updated .gitignore (+${missing.length} entries)`);
648
- gitignoreChanged = true;
649
- }
650
- } else {
651
- const content = "# SolonGate\n" + requiredLines.join("\n") + "\nnode_modules/\n";
652
- writeFileSync2(gitignorePath, content);
653
- console.log(` Created .gitignore`);
654
- gitignoreChanged = true;
655
- }
656
- if (!envChanged && !gitignoreChanged) {
657
- console.log(` .env and .gitignore already up to date`);
658
- }
659
- return { envChanged, gitignoreChanged };
660
- }
661
- async function main() {
662
- const options = parseInitArgs(process.argv);
663
- if (options.global) {
664
- console.log("");
665
- console.log(` ${c.bold}SolonGate \u2014 Global Install${c.reset}`);
666
- console.log("");
667
- if (options.restore) {
668
- runGlobalRestore();
669
- } else {
670
- await runGlobalInstall({ apiKey: options.apiKey });
671
- console.log("");
672
- 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");
673
- console.log(" \u2502 Global enforcement active. \u2502");
674
- console.log(" \u2502 Every Claude Code session on this machine is \u2502");
675
- console.log(" \u2502 now guarded by your cloud policy (OPA WASM). \u2502");
676
- console.log(" \u2502 \u2502");
677
- console.log(" \u2502 Undo: init --global --restore \u2502");
678
- console.log(" \u2502 Logs: https://dashboard.solongate.com \u2502");
679
- console.log(" \u2502 Restart Claude Code to apply. \u2502");
680
- 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");
681
- }
682
- console.log("");
683
- return;
684
- }
685
- const fullBanner = BANNER_FULL;
686
- const mediumBanner = [
687
- " \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",
688
- " \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551",
689
- " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551",
690
- " \u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551",
691
- " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551",
692
- " \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D",
693
- " \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557",
694
- " \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D",
695
- " \u2588\u2588\u2551 \u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2557 ",
696
- " \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u255D ",
697
- " \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557",
698
- " \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D"
699
- ];
700
- const smallBanner = [
701
- " \u250F\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513",
702
- " \u2503 \u2554\u2550\u2557\u2554\u2550\u2557\u2566 \u2554\u2550\u2557\u2554\u2557\u2554\u2554\u2550\u2557 \u2503",
703
- " \u2503 \u255A\u2550\u2557\u2551 \u2551\u2551 \u2551 \u2551\u2551\u2551\u2551\u2551 \u2566 \u2503",
704
- " \u2503 \u255A\u2550\u255D\u255A\u2550\u255D\u2569\u2550\u255D\u255A\u2550\u255D\u255D\u255A\u255D\u255A\u2550\u255D \u2503",
705
- " \u2503 \u2554\u2550\u2557\u2554\u2550\u2557\u2554\u2566\u2557\u2554\u2550\u2557 \u2503",
706
- " \u2503 \u2551 \u2566\u2560\u2550\u2563 \u2551 \u2551\u2563 \u2503",
707
- " \u2503 \u255A\u2550\u255D\u2569 \u2569 \u2569 \u255A\u2550\u255D \u2503",
708
- " \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251B"
709
- ];
710
- const cols = process.stdout.columns || 80;
711
- const bannerLines = cols >= 82 ? fullBanner : cols >= 50 ? mediumBanner : smallBanner;
712
- const bannerColors = BANNER_COLORS;
713
- console.log("");
714
- for (let i = 0; i < bannerLines.length; i++) {
715
- console.log(`${c.bold}${bannerColors[i % bannerColors.length]}${bannerLines[i]}${c.reset}`);
716
- }
717
- console.log("");
718
- console.log(` ${c.dim}${c.italic}Init Setup${c.reset}`);
719
- console.log("");
720
- startSpinner("Scanning for MCP config...");
721
- await sleep(400);
722
- const configInfo = findConfigFile(options.configPath, true);
723
- if (!configInfo) {
724
- stopSpinner("\u2717 No MCP config found and could not create one.");
725
- process.exit(1);
726
- }
727
- stopSpinner(`\u2713 Found config: ${configInfo.path}`);
728
- console.log(` Type: ${configInfo.type === "claude-desktop" ? "Claude Desktop" : "MCP JSON"}`);
729
- console.log("");
730
- const config = readConfig(configInfo.path);
731
- const serverNames = Object.keys(config.mcpServers);
732
- if (serverNames.length === 0) {
733
- console.log(" No MCP servers found in config.");
734
- process.exit(1);
735
- }
736
- startSpinner("Analyzing servers...");
737
- await sleep(300);
738
- const toProtect = [];
739
- const alreadyProtected = [];
740
- const skipped = [];
741
- for (const name of serverNames) {
742
- const server = config.mcpServers[name];
743
- if (isAlreadyProtected(server)) {
744
- alreadyProtected.push(name);
745
- } else {
746
- if (options.all) {
747
- toProtect.push(name);
748
- }
749
- }
750
- }
751
- stopSpinner(`\u2713 Found ${serverNames.length} MCP server(s)`);
752
- console.log("");
753
- for (const name of serverNames) {
754
- const server = config.mcpServers[name];
755
- if (alreadyProtected.includes(name)) {
756
- console.log(` [protected] ${name}`);
757
- } else {
758
- console.log(` [exposed] ${name} \u2192 ${server.command} ${(server.args ?? []).join(" ")}`);
759
- }
760
- }
761
- console.log("");
762
- if (alreadyProtected.length === serverNames.length) {
763
- console.log(" All servers are already protected by SolonGate!");
764
- ensureEnvFile();
765
- console.log("");
766
- installHooks(options.tools, config);
767
- process.exit(0);
768
- }
769
- if (!options.all) {
770
- const exposed = serverNames.filter((n) => !alreadyProtected.includes(n));
771
- for (const name of exposed) {
772
- const answer = await prompt(` Protect "${name}"? [Y/n] `);
773
- if (answer === "" || answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
774
- toProtect.push(name);
775
- } else {
776
- skipped.push(name);
777
- }
778
- }
779
- }
780
- if (toProtect.length === 0) {
781
- console.log(" No servers selected for protection.");
782
- ensureEnvFile();
783
- process.exit(0);
784
- }
785
- console.log("");
786
- startSpinner("Setting up environment...");
787
- await sleep(300);
788
- stopSpinner("\u2713 Environment");
789
- ensureEnvFile();
790
- console.log("");
791
- let apiKey = options.apiKey || process.env.SOLONGATE_API_KEY || "";
792
- if (!apiKey) {
793
- const envPath = resolve2(".env");
794
- if (existsSync2(envPath)) {
795
- const envContent = readFileSync2(envPath, "utf-8");
796
- const match = envContent.match(/^SOLONGATE_API_KEY=(sg_(?:live|test)_\w+)/m);
797
- if (match) apiKey = match[1];
798
- }
799
- }
800
- if (!apiKey || apiKey === "sg_live_your_key_here") {
801
- if (options.all) {
802
- apiKey = "sg_test_demo_init_key";
803
- console.log(" No API key found \u2014 using demo test key.");
804
- console.log(" Set your real key in .env for cloud features.");
805
- console.log("");
806
- } else {
807
- apiKey = await prompt(" Enter your SolonGate API key (from https://dashboard.solongate.com): ");
808
- }
809
- if (!apiKey) {
810
- console.log(" API key is required. Get one at https://dashboard.solongate.com");
811
- process.exit(1);
812
- }
813
- }
814
- if (!apiKey.startsWith("sg_live_") && !apiKey.startsWith("sg_test_")) {
815
- console.log(" Invalid API key format. Must start with sg_live_ or sg_test_");
816
- process.exit(1);
817
- }
818
- let policyValue;
819
- if (options.policy) {
820
- const policyPath = resolve2(options.policy);
821
- if (existsSync2(policyPath)) {
822
- policyValue = `./${options.policy}`;
823
- console.log(` Policy: ${policyPath}`);
824
- } else {
825
- console.log(` Policy file not found: ${options.policy}`);
826
- process.exit(1);
827
- }
828
- } else {
829
- const defaultPolicy = resolve2("policy.json");
830
- if (existsSync2(defaultPolicy)) {
831
- policyValue = "./policy.json";
832
- console.log(` Policy: ${defaultPolicy} (auto-detected)`);
833
- } else {
834
- console.log(` Policy: cloud-managed (fetched via API key)`);
835
- }
836
- }
837
- console.log(` API Key: ${apiKey.slice(0, 12)}...${apiKey.slice(-4)}`);
838
- console.log(` Protecting: ${toProtect.join(", ")}`);
839
- console.log("");
840
- startSpinner("Wrapping MCP servers with proxy...");
841
- const newConfig = { mcpServers: {} };
842
- for (const name of serverNames) {
843
- if (toProtect.includes(name)) {
844
- newConfig.mcpServers[name] = wrapServer(name, config.mcpServers[name], policyValue);
845
- } else {
846
- newConfig.mcpServers[name] = config.mcpServers[name];
847
- }
848
- }
849
- const currentContent = readFileSync2(configInfo.path, "utf-8");
850
- let newContent;
851
- if (configInfo.type === "claude-desktop") {
852
- const original = JSON.parse(currentContent);
853
- original.mcpServers = newConfig.mcpServers;
854
- newContent = JSON.stringify(original, null, 2) + "\n";
855
- } else {
856
- newContent = JSON.stringify(newConfig, null, 2) + "\n";
857
- }
858
- await sleep(400);
859
- if (newContent === currentContent) {
860
- stopSpinner("\u2713 Config already up to date");
861
- } else {
862
- writeFileSync2(configInfo.path, newContent);
863
- stopSpinner("\u2713 Config updated");
864
- }
865
- console.log("");
866
- startSpinner("Installing hooks...");
867
- await sleep(300);
868
- stopSpinner("\u2713 Hooks");
869
- installHooks(options.tools, newConfig);
870
- console.log("");
871
- const allUpToDate = toProtect.length === 0 && alreadyProtected.length === serverNames.length;
872
- console.log(" \u2500\u2500 Summary \u2500\u2500");
873
- console.log("");
874
- for (const name of toProtect) {
875
- console.log(` \u2713 ${name} \u2014 protected`);
876
- }
877
- for (const name of alreadyProtected) {
878
- console.log(` \u25CF ${name} \u2014 already protected`);
879
- }
880
- for (const name of skipped) {
881
- console.log(` \u25CB ${name} \u2014 skipped`);
882
- }
883
- console.log("");
884
- if (allUpToDate) {
885
- 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");
886
- console.log(" \u2502 Everything is already up to date! \u2502");
887
- console.log(" \u2502 \u2502");
888
- console.log(" \u2502 All MCP servers protected, hooks configured. \u2502");
889
- console.log(" \u2502 View logs: https://dashboard.solongate.com \u2502");
890
- 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");
891
- } else {
892
- 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");
893
- console.log(" \u2502 Setup complete! \u2502");
894
- console.log(" \u2502 \u2502");
895
- console.log(" \u2502 MCP servers \u2192 Protected via proxy \u2502");
896
- console.log(" \u2502 AI tools \u2192 Guarded via hooks \u2502");
897
- console.log(" \u2502 Claude Code, Gemini CLI \u2502");
898
- console.log(" \u2502 API key \u2192 Set in .env \u2502");
899
- console.log(" \u2502 \u2502");
900
- console.log(" \u2502 View logs: https://dashboard.solongate.com \u2502");
901
- console.log(" \u2502 \u2502");
902
- console.log(" \u2502 Restart your MCP client to apply changes. \u2502");
903
- 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");
904
- }
905
- console.log("");
906
- }
907
- main().catch((err) => {
908
- console.log(`Fatal: ${err instanceof Error ? err.message : String(err)}`);
909
- process.exit(1);
910
- });