contextguard 0.1.8 → 0.2.1

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.
Files changed (60) hide show
  1. package/LICENSE +23 -17
  2. package/README.md +157 -109
  3. package/dist/agent.d.ts +24 -0
  4. package/dist/agent.js +376 -0
  5. package/dist/cli.d.ts +11 -0
  6. package/dist/cli.js +298 -0
  7. package/dist/config.d.ts +23 -0
  8. package/dist/config.js +56 -0
  9. package/dist/database.d.ts +116 -0
  10. package/dist/database.js +291 -0
  11. package/dist/index.d.ts +16 -0
  12. package/dist/index.js +18 -0
  13. package/dist/init.d.ts +7 -0
  14. package/dist/init.js +178 -0
  15. package/dist/lib/supabase-client.d.ts +27 -0
  16. package/dist/lib/supabase-client.js +97 -0
  17. package/dist/logger.d.ts +36 -0
  18. package/dist/logger.js +145 -0
  19. package/dist/mcp-security-wrapper.d.ts +84 -0
  20. package/dist/mcp-security-wrapper.js +389 -147
  21. package/dist/mcp-traceability-integration.d.ts +118 -0
  22. package/dist/mcp-traceability-integration.js +302 -0
  23. package/dist/policy.d.ts +30 -0
  24. package/dist/policy.js +273 -0
  25. package/dist/premium-features.d.ts +364 -0
  26. package/dist/premium-features.js +950 -0
  27. package/dist/security-logger.d.ts +45 -0
  28. package/dist/security-logger.js +125 -0
  29. package/dist/security-policy.d.ts +55 -0
  30. package/dist/security-policy.js +140 -0
  31. package/dist/semantic-detector.d.ts +21 -0
  32. package/dist/semantic-detector.js +49 -0
  33. package/dist/sse-proxy.d.ts +21 -0
  34. package/dist/sse-proxy.js +276 -0
  35. package/dist/supabase-client.d.ts +27 -0
  36. package/dist/supabase-client.js +89 -0
  37. package/dist/types/database.types.d.ts +220 -0
  38. package/dist/types/database.types.js +8 -0
  39. package/dist/types/mcp.d.ts +27 -0
  40. package/dist/types/mcp.js +15 -0
  41. package/dist/types/types.d.ts +65 -0
  42. package/dist/types/types.js +8 -0
  43. package/dist/types.d.ts +84 -0
  44. package/dist/types.js +8 -0
  45. package/dist/wrapper.d.ts +115 -0
  46. package/dist/wrapper.js +417 -0
  47. package/package.json +35 -11
  48. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -57
  49. package/CONTRIBUTING.md +0 -532
  50. package/SECURITY.md +0 -254
  51. package/assets/demo.mp4 +0 -0
  52. package/eslint.config.mts +0 -23
  53. package/examples/config/config.json +0 -19
  54. package/examples/mcp-server/demo.js +0 -228
  55. package/examples/mcp-server/package-lock.json +0 -978
  56. package/examples/mcp-server/package.json +0 -16
  57. package/examples/mcp-server/pnpm-lock.yaml +0 -745
  58. package/src/mcp-security-wrapper.ts +0 -570
  59. package/test/test-server.ts +0 -295
  60. package/tsconfig.json +0 -16
package/dist/agent.js ADDED
@@ -0,0 +1,376 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 Amir Mironi
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.createAgent = void 0;
10
+ const child_process_1 = require("child_process");
11
+ const policy_1 = require("./policy");
12
+ const logger_1 = require("./logger");
13
+ const supabase_client_1 = require("./lib/supabase-client");
14
+ const mcp_1 = require("./types/mcp");
15
+ /**
16
+ * Merge policy with defaults
17
+ */
18
+ const mergePolicyWithDefaults = (policy) => ({
19
+ ...policy_1.DEFAULT_POLICY,
20
+ ...policy,
21
+ });
22
+ /**
23
+ * Create an MCP security agent
24
+ * @param serverCommand - Command to start MCP server
25
+ * @param policyConfig - Policy configuration
26
+ * @param supabaseConfig - Optional Supabase configuration
27
+ * @returns Agent functions
28
+ */
29
+ const createAgent = (serverCommand, policyConfig = {}, supabaseConfig) => {
30
+ const config = mergePolicyWithDefaults(policyConfig);
31
+ const policy = (0, policy_1.createPolicyChecker)(config);
32
+ // Create Supabase client if config provided
33
+ const supabaseClient = supabaseConfig
34
+ ? (0, supabase_client_1.createSupabaseClient)(supabaseConfig)
35
+ : undefined;
36
+ const logger = (0, logger_1.createLogger)(config.logPath, supabaseClient, supabaseConfig?.agentId, config.alertWebhook, config.alertOnSeverity);
37
+ const sessionId = (0, mcp_1.generateSessionId)();
38
+ const state = {
39
+ process: null,
40
+ toolCallTimestamps: [],
41
+ clientMessageBuffer: "",
42
+ serverMessageBuffer: "",
43
+ };
44
+ /**
45
+ * Extract file paths from message parameters
46
+ */
47
+ const extractFilePaths = (message) => [
48
+ message.params?.arguments?.path,
49
+ message.params?.arguments?.filePath,
50
+ message.params?.arguments?.file,
51
+ message.params?.arguments?.directory,
52
+ message.params?.path,
53
+ message.params?.filePath,
54
+ ].filter((path) => typeof path === "string");
55
+ /**
56
+ * Handle tool call with security checks
57
+ */
58
+ const handleToolCall = (message) => {
59
+ const violations = [];
60
+ let shouldBlock = false;
61
+ // Rate limiting
62
+ const now = Date.now();
63
+ state.toolCallTimestamps.push(now);
64
+ // Clean up old timestamps
65
+ const oneMinuteAgo = now - 60000;
66
+ state.toolCallTimestamps = state.toolCallTimestamps.filter((t) => t > oneMinuteAgo);
67
+ if (!policy.checkRateLimit(state.toolCallTimestamps)) {
68
+ violations.push("Rate limit exceeded for tool calls");
69
+ shouldBlock = policy.isBlockingMode();
70
+ logger.logEvent("RATE_LIMIT_EXCEEDED", "HIGH", {
71
+ method: message.method,
72
+ toolName: message.params?.name,
73
+ }, sessionId);
74
+ }
75
+ // Check tool allowlist / blocklist
76
+ if (message.params?.name) {
77
+ violations.push(...policy.checkToolAccess(message.params.name));
78
+ }
79
+ // Check parameters for security issues
80
+ const paramsStr = JSON.stringify(message.params);
81
+ violations.push(...policy.checkPromptInjection(paramsStr));
82
+ violations.push(...policy.checkSensitiveData(paramsStr));
83
+ violations.push(...policy.checkBlockedPatterns(paramsStr));
84
+ violations.push(...policy.checkSQLInjection(paramsStr));
85
+ violations.push(...policy.checkXSS(paramsStr));
86
+ violations.push(...policy.checkCustomRules(paramsStr));
87
+ // Check file paths
88
+ const filePathParams = extractFilePaths(message);
89
+ for (const filePath of filePathParams) {
90
+ violations.push(...policy.checkFileAccess(filePath));
91
+ }
92
+ // Log tool call
93
+ logger.logEvent("TOOL_CALL", violations.length > 0 ? "HIGH" : "LOW", {
94
+ toolName: message.params?.name,
95
+ hasViolations: violations.length > 0,
96
+ violations,
97
+ }, sessionId);
98
+ // In blocking mode, any violation blocks
99
+ if (violations.length > 0 && policy.isBlockingMode()) {
100
+ shouldBlock = true;
101
+ }
102
+ return { violations, shouldBlock };
103
+ };
104
+ /**
105
+ * Handle security violations
106
+ */
107
+ const handleViolations = (message, violations, shouldBlock) => {
108
+ logger.logEvent("SECURITY_VIOLATION", "CRITICAL", {
109
+ violations,
110
+ message,
111
+ blocked: shouldBlock,
112
+ }, sessionId);
113
+ console.error(`\n⚠️ SECURITY VIOLATIONS DETECTED:\n${violations.join("\n")}\n`);
114
+ if (shouldBlock) {
115
+ console.error("🚫 REQUEST BLOCKED\n");
116
+ // Send error response
117
+ if (message.id !== undefined) {
118
+ const errorResponse = {
119
+ jsonrpc: message.jsonrpc,
120
+ id: message.id,
121
+ error: {
122
+ code: -32000,
123
+ message: "Security violation: Request blocked",
124
+ data: { violations },
125
+ },
126
+ };
127
+ process.stdout.write(JSON.stringify(errorResponse) + "\n");
128
+ }
129
+ }
130
+ else {
131
+ // Monitor mode: log and warn but still forward
132
+ console.error('⚠️ MONITOR MODE — request forwarded (set mode: "block" to block)\n');
133
+ }
134
+ };
135
+ /**
136
+ * Handle parse errors
137
+ */
138
+ const handleParseError = (err, line) => {
139
+ logger.logEvent("PARSE_ERROR", "MEDIUM", {
140
+ error: err instanceof Error ? err.message : String(err),
141
+ line: line.substring(0, 100),
142
+ }, sessionId);
143
+ console.error(`Failed to parse client message: ${err}`);
144
+ };
145
+ /**
146
+ * Handle sensitive data leak in server response
147
+ */
148
+ const handleSensitiveDataLeak = (message, violations) => {
149
+ logger.logEvent("SENSITIVE_DATA_LEAK", "CRITICAL", {
150
+ violations,
151
+ responseId: message.id,
152
+ blocked: policy.isBlockingMode(),
153
+ }, sessionId);
154
+ console.error(`\n🚨 SENSITIVE DATA DETECTED IN RESPONSE:\n${violations.join("\n")}\n`);
155
+ if (policy.isBlockingMode()) {
156
+ console.error("🚫 RESPONSE BLOCKED\n");
157
+ // Send sanitized error response
158
+ if (message.id !== undefined) {
159
+ const errorResponse = {
160
+ jsonrpc: message.jsonrpc,
161
+ id: message.id,
162
+ error: {
163
+ code: -32001,
164
+ message: "Security violation: Response contains sensitive data",
165
+ data: { violations },
166
+ },
167
+ };
168
+ process.stdout.write(JSON.stringify(errorResponse) + "\n");
169
+ }
170
+ return false; // Don't forward
171
+ }
172
+ else {
173
+ console.error("⚠️ MONITOR MODE — response forwarded\n");
174
+ return true; // Forward anyway
175
+ }
176
+ };
177
+ /**
178
+ * Process a single client message
179
+ */
180
+ const processClientMessage = (line) => {
181
+ try {
182
+ const message = JSON.parse(line);
183
+ logger.logEvent("CLIENT_REQUEST", "LOW", {
184
+ method: message.method,
185
+ id: message.id,
186
+ }, sessionId);
187
+ const violations = [];
188
+ let shouldBlock = false;
189
+ // Handle tool calls with security checks
190
+ if (message.method === "tools/call") {
191
+ const result = handleToolCall(message);
192
+ violations.push(...result.violations);
193
+ shouldBlock = result.shouldBlock;
194
+ }
195
+ // Handle violations
196
+ if (violations.length > 0) {
197
+ handleViolations(message, violations, shouldBlock);
198
+ if (shouldBlock) {
199
+ return; // Don't forward blocked requests
200
+ }
201
+ }
202
+ // Forward to server
203
+ if (state.process?.stdin) {
204
+ state.process.stdin.write(line + "\n");
205
+ }
206
+ }
207
+ catch (err) {
208
+ handleParseError(err, line);
209
+ // Forward unparseable messages
210
+ if (state.process?.stdin) {
211
+ state.process.stdin.write(line + "\n");
212
+ }
213
+ }
214
+ };
215
+ /**
216
+ * Handle client input (buffered line-by-line)
217
+ */
218
+ const handleClientInput = (input) => {
219
+ state.clientMessageBuffer += input;
220
+ const lines = state.clientMessageBuffer.split("\n");
221
+ state.clientMessageBuffer = lines.pop() || "";
222
+ for (const line of lines) {
223
+ if (line.trim()) {
224
+ processClientMessage(line);
225
+ }
226
+ }
227
+ };
228
+ /**
229
+ * Process a single server message
230
+ */
231
+ const processServerMessage = (line) => {
232
+ let shouldForward = true;
233
+ try {
234
+ const message = JSON.parse(line);
235
+ // Check for sensitive data in response
236
+ const violations = [];
237
+ const responseStr = JSON.stringify(message.result || message);
238
+ violations.push(...policy.checkSensitiveData(responseStr));
239
+ if (violations.length > 0) {
240
+ shouldForward = handleSensitiveDataLeak(message, violations);
241
+ }
242
+ else {
243
+ logger.logEvent("SERVER_RESPONSE", "LOW", {
244
+ id: message.id,
245
+ hasError: !!message.error,
246
+ }, sessionId);
247
+ }
248
+ }
249
+ catch (err) {
250
+ // Log parse errors for server output
251
+ logger.logEvent("SERVER_PARSE_ERROR", "LOW", {
252
+ error: err instanceof Error ? err.message : String(err),
253
+ line: line.substring(0, 100),
254
+ }, sessionId);
255
+ }
256
+ // Forward the line if not blocked
257
+ if (shouldForward) {
258
+ process.stdout.write(line + "\n");
259
+ }
260
+ };
261
+ /**
262
+ * Handle server output (buffered line-by-line)
263
+ */
264
+ const handleServerOutput = (output) => {
265
+ state.serverMessageBuffer += output;
266
+ const lines = state.serverMessageBuffer.split("\n");
267
+ state.serverMessageBuffer = lines.pop() || "";
268
+ for (const line of lines) {
269
+ if (line.trim()) {
270
+ processServerMessage(line);
271
+ }
272
+ }
273
+ };
274
+ /**
275
+ * Handle process exit
276
+ */
277
+ const handleProcessExit = (code) => {
278
+ logger.logEvent("SERVER_EXIT", "MEDIUM", { exitCode: code }, sessionId);
279
+ console.error("\n=== MCP Security Statistics ===");
280
+ console.error(JSON.stringify(logger.getStatistics(), null, 2));
281
+ const finish = () => setImmediate(() => process.exit(code || 0));
282
+ if (supabaseClient && supabaseConfig?.agentId) {
283
+ supabaseClient
284
+ .updateAgentStatus(supabaseConfig.agentId, "offline")
285
+ .catch(() => { })
286
+ .finally(finish);
287
+ }
288
+ else {
289
+ finish();
290
+ }
291
+ };
292
+ return {
293
+ /**
294
+ * Start the MCP server wrapper
295
+ */
296
+ start: async () => {
297
+ // Fetch policy from Supabase if configured
298
+ if (supabaseClient && supabaseConfig?.agentId) {
299
+ try {
300
+ const remotePolicy = await supabaseClient.fetchPolicy(supabaseConfig.agentId);
301
+ if (remotePolicy) {
302
+ console.log("✓ Loaded policy from Supabase");
303
+ // Merge remote policy with local config
304
+ Object.assign(config, remotePolicy);
305
+ }
306
+ }
307
+ catch (error) {
308
+ console.warn("⚠ Failed to fetch policy from Supabase:", error);
309
+ }
310
+ // Update agent status to online
311
+ await supabaseClient.updateAgentStatus(supabaseConfig.agentId, "online");
312
+ // Send periodic heartbeats every 30 seconds
313
+ const heartbeatInterval = setInterval(() => {
314
+ supabaseClient
315
+ .updateAgentStatus(supabaseConfig.agentId, "online")
316
+ .catch(() => { });
317
+ }, 30000);
318
+ heartbeatInterval.unref();
319
+ }
320
+ state.process = (0, child_process_1.spawn)(serverCommand[0], serverCommand.slice(1), {
321
+ stdio: ["pipe", "pipe", "pipe"],
322
+ });
323
+ if (!state.process.stdout ||
324
+ !state.process.stdin ||
325
+ !state.process.stderr) {
326
+ throw new Error("Failed to create child process streams");
327
+ }
328
+ logger.logEvent("SERVER_START", "LOW", {
329
+ command: serverCommand.join(" "),
330
+ pid: state.process.pid,
331
+ }, sessionId);
332
+ // Pipe stderr to parent process
333
+ state.process.stderr.pipe(process.stderr);
334
+ // Handle server output
335
+ state.process.stdout.on("data", (data) => {
336
+ handleServerOutput(data.toString());
337
+ });
338
+ // Handle client input
339
+ process.stdin.on("data", (data) => {
340
+ handleClientInput(data.toString());
341
+ });
342
+ // Handle process exit
343
+ state.process.on("exit", (code) => {
344
+ handleProcessExit(code);
345
+ });
346
+ // Handle process errors
347
+ state.process.on("error", (err) => {
348
+ logger.logEvent("SERVER_ERROR", "HIGH", { error: err.message }, sessionId);
349
+ console.error("Server process error:", err);
350
+ });
351
+ // Graceful shutdown on SIGTERM/SIGINT
352
+ const shutdown = (signal) => {
353
+ logger.logEvent("AGENT_SHUTDOWN", "MEDIUM", { signal }, sessionId);
354
+ if (state.process)
355
+ state.process.kill();
356
+ const finish = () => process.exit(0);
357
+ if (supabaseClient && supabaseConfig?.agentId) {
358
+ supabaseClient
359
+ .updateAgentStatus(supabaseConfig.agentId, "offline")
360
+ .catch(() => { })
361
+ .finally(finish);
362
+ }
363
+ else {
364
+ finish();
365
+ }
366
+ };
367
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
368
+ process.on("SIGINT", () => shutdown("SIGINT"));
369
+ },
370
+ /**
371
+ * Get the security logger instance
372
+ */
373
+ getLogger: () => logger,
374
+ };
375
+ };
376
+ exports.createAgent = createAgent;
package/dist/cli.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Copyright (c) 2026 Amir Mironi
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ /**
9
+ * Main CLI entry point
10
+ */
11
+ export declare function main(): Promise<void>;
package/dist/cli.js ADDED
@@ -0,0 +1,298 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * Copyright (c) 2026 Amir Mironi
5
+ *
6
+ * This source code is licensed under the MIT license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ */
9
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ var desc = Object.getOwnPropertyDescriptor(m, k);
12
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
13
+ desc = { enumerable: true, get: function() { return m[k]; } };
14
+ }
15
+ Object.defineProperty(o, k2, desc);
16
+ }) : (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ o[k2] = m[k];
19
+ }));
20
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
21
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
22
+ }) : function(o, v) {
23
+ o["default"] = v;
24
+ });
25
+ var __importStar = (this && this.__importStar) || (function () {
26
+ var ownKeys = function(o) {
27
+ ownKeys = Object.getOwnPropertyNames || function (o) {
28
+ var ar = [];
29
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
30
+ return ar;
31
+ };
32
+ return ownKeys(o);
33
+ };
34
+ return function (mod) {
35
+ if (mod && mod.__esModule) return mod;
36
+ var result = {};
37
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
38
+ __setModuleDefault(result, mod);
39
+ return result;
40
+ };
41
+ })();
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.main = main;
44
+ const fs = __importStar(require("fs"));
45
+ const os = __importStar(require("os"));
46
+ const agent_1 = require("./agent");
47
+ const sse_proxy_1 = require("./sse-proxy");
48
+ const policy_1 = require("./policy");
49
+ const init_1 = require("./init");
50
+ /**
51
+ * Display help message
52
+ */
53
+ function showHelp() {
54
+ console.log(`
55
+ MCP Security Wrapper - ContextGuard
56
+
57
+ Usage:
58
+ # Auto-patch Claude Desktop config (recommended)
59
+ contextguard init --api-key <key>
60
+
61
+ # stdio transport (Claude Desktop)
62
+ contextguard --server "node server.js" --config config.json
63
+
64
+ # SSE/HTTP transport proxy
65
+ contextguard --transport sse --port 3100 --target http://localhost:3000 --config config.json
66
+
67
+ Commands:
68
+ init Auto-patch claude_desktop_config.json
69
+
70
+ Init options:
71
+ --api-key <key> Your ContextGuard API key (required)
72
+ --config <file> Path to claude_desktop_config.json (auto-detected if omitted)
73
+ --dry-run Preview changes without writing
74
+
75
+ Options:
76
+ --server <cmd> Command to start MCP server (stdio mode)
77
+ --transport <type> stdio | sse | http (default: stdio)
78
+ --port <n> Proxy listen port (SSE/HTTP mode, default: 3100)
79
+ --target <url> Upstream server URL (SSE/HTTP mode)
80
+ --config <file> Path to security config JSON (optional)
81
+ --help Show this help
82
+
83
+ Configuration File Format (JSON):
84
+ {
85
+ "mode": "monitor", // "monitor" (log only) | "block" (block + log)
86
+ "maxToolCallsPerMinute": 30,
87
+ "enablePromptInjectionDetection": true,
88
+ "enableSensitiveDataDetection": true,
89
+ "enablePathTraversalPrevention": true,
90
+ "enableSQLInjectionDetection": true,
91
+ "enableXSSDetection": true,
92
+ "enableSemanticDetection": false,
93
+ "blockedPatterns": ["ignore previous", "system prompt"],
94
+ "allowedFilePaths": ["/safe/path"],
95
+ "allowedTools": [],
96
+ "blockedTools": [],
97
+ "customRules": [{ "name": "my-rule", "pattern": "\\bforbidden\\b" }],
98
+ "alertWebhook": "https://hooks.example.com/alert",
99
+ "alertOnSeverity": ["HIGH", "CRITICAL"],
100
+ "logPath": "mcp_security.log"
101
+ }
102
+
103
+ For more information, visit: https://contextguard.dev
104
+ `);
105
+ }
106
+ /**
107
+ * Parse command line arguments
108
+ * @returns Parsed arguments
109
+ */
110
+ function parseArgs() {
111
+ const args = process.argv.slice(2);
112
+ let serverCommand = "";
113
+ let configFile = "";
114
+ let transport = "stdio";
115
+ let port = 3100;
116
+ let targetUrl = "";
117
+ for (let i = 0; i < args.length; i++) {
118
+ if (args[i] === "--server" && args[i + 1]) {
119
+ serverCommand = args[i + 1];
120
+ i++;
121
+ }
122
+ else if (args[i] === "--config" && args[i + 1]) {
123
+ configFile = args[i + 1];
124
+ i++;
125
+ }
126
+ else if (args[i] === "--transport" && args[i + 1]) {
127
+ transport = args[i + 1];
128
+ i++;
129
+ }
130
+ else if (args[i] === "--port" && args[i + 1]) {
131
+ port = parseInt(args[i + 1], 10);
132
+ i++;
133
+ }
134
+ else if (args[i] === "--target" && args[i + 1]) {
135
+ targetUrl = args[i + 1];
136
+ i++;
137
+ }
138
+ }
139
+ return { serverCommand, configFile, transport, port, targetUrl };
140
+ }
141
+ /**
142
+ * Parse a shell command string into an array of arguments,
143
+ * respecting single and double quoted strings.
144
+ */
145
+ function parseCommand(cmd) {
146
+ const tokens = [];
147
+ let current = "";
148
+ let inQuote = "";
149
+ for (const char of cmd) {
150
+ if (inQuote) {
151
+ if (char === inQuote)
152
+ inQuote = "";
153
+ else
154
+ current += char;
155
+ }
156
+ else if (char === '"' || char === "'") {
157
+ inQuote = char;
158
+ }
159
+ else if (char === " ") {
160
+ if (current) {
161
+ tokens.push(current);
162
+ current = "";
163
+ }
164
+ }
165
+ else {
166
+ current += char;
167
+ }
168
+ }
169
+ if (current)
170
+ tokens.push(current);
171
+ return tokens;
172
+ }
173
+ /**
174
+ * Load configuration from file
175
+ * @param configFile - Path to config file
176
+ * @returns Security configuration
177
+ */
178
+ function loadConfig(configFile) {
179
+ if (!fs.existsSync(configFile)) {
180
+ console.error(`Error: Config file not found: ${configFile}`);
181
+ process.exit(1);
182
+ }
183
+ try {
184
+ const config = JSON.parse(fs.readFileSync(configFile, "utf-8"));
185
+ // validateConfig(config);
186
+ return config;
187
+ }
188
+ catch (error) {
189
+ console.error(`Error: Failed to load config file: ${error}`);
190
+ process.exit(1);
191
+ }
192
+ // TypeScript doesn't know process.exit() never returns
193
+ return {};
194
+ }
195
+ /**
196
+ * Main CLI entry point
197
+ */
198
+ async function main() {
199
+ const args = process.argv.slice(2);
200
+ // Show help if requested or no args
201
+ if (args.length === 0 || args.includes("--help")) {
202
+ showHelp();
203
+ process.exit(0);
204
+ }
205
+ // init subcommand — auto-patch claude_desktop_config.json
206
+ if (args[0] === "init") {
207
+ await (0, init_1.runInit)(args.slice(1));
208
+ return;
209
+ }
210
+ // Parse arguments
211
+ const { serverCommand, configFile, transport, port, targetUrl } = parseArgs();
212
+ // Load configuration
213
+ let config = {};
214
+ if (configFile) {
215
+ config = loadConfig(configFile);
216
+ }
217
+ // Merge with defaults
218
+ const fullConfig = { ...policy_1.DEFAULT_POLICY, ...config };
219
+ // CLI flags override config file
220
+ if (transport !== "stdio") {
221
+ fullConfig.transport = transport;
222
+ }
223
+ if (port !== 3100) {
224
+ fullConfig.port = port;
225
+ }
226
+ if (targetUrl) {
227
+ fullConfig.targetUrl = targetUrl;
228
+ }
229
+ const effectiveTransport = fullConfig.transport || "stdio";
230
+ // SSE/HTTP transport mode
231
+ if (effectiveTransport === "sse" || effectiveTransport === "http") {
232
+ if (!fullConfig.targetUrl && !targetUrl) {
233
+ console.error("Error: --target <url> is required for SSE/HTTP transport");
234
+ process.exit(1);
235
+ }
236
+ const proxy = (0, sse_proxy_1.createSSEProxy)(fullConfig);
237
+ proxy.start(fullConfig.port || 3100, fullConfig.targetUrl || targetUrl);
238
+ return;
239
+ }
240
+ // stdio transport mode
241
+ if (!serverCommand) {
242
+ console.error("Error: --server argument is required for stdio transport");
243
+ console.error("Run 'contextguard --help' for usage information");
244
+ process.exit(1);
245
+ }
246
+ // Load Supabase configuration from environment
247
+ const supabaseConfig = process.env.SUPABASE_URL && process.env.SUPABASE_SERVICE_KEY
248
+ ? {
249
+ url: process.env.SUPABASE_URL,
250
+ serviceKey: process.env.SUPABASE_SERVICE_KEY,
251
+ agentId: process.env.AGENT_ID || "default-agent",
252
+ }
253
+ : undefined;
254
+ if (supabaseConfig) {
255
+ console.log(`✓ Supabase integration enabled (Agent ID: ${supabaseConfig.agentId})`);
256
+ }
257
+ // Start REST-based heartbeat if CONTEXTGUARD_API_KEY is set
258
+ const cgApiKey = process.env.CONTEXTGUARD_API_KEY;
259
+ if (cgApiKey) {
260
+ const baseUrl = process.env.CONTEXTGUARD_BASE_URL || "https://contextguard.dev";
261
+ const agentId = process.env.AGENT_ID || `${os.hostname()}-${process.pid}`;
262
+ const sendHeartbeat = async () => {
263
+ try {
264
+ await fetch(`${baseUrl}/api/agents/heartbeat`, {
265
+ method: "POST",
266
+ headers: {
267
+ "Content-Type": "application/json",
268
+ Authorization: `Bearer ${cgApiKey}`,
269
+ },
270
+ body: JSON.stringify({
271
+ agent_id: agentId,
272
+ name: agentId,
273
+ hostname: os.hostname(),
274
+ version: process.env.npm_package_version || "unknown",
275
+ mcp_server_command: serverCommand,
276
+ }),
277
+ });
278
+ }
279
+ catch {
280
+ // silently ignore — don't break the agent if dashboard is unreachable
281
+ }
282
+ };
283
+ sendHeartbeat();
284
+ const interval = setInterval(sendHeartbeat, 30000);
285
+ interval.unref();
286
+ console.log(`✓ ContextGuard monitoring enabled (Agent ID: ${agentId})`);
287
+ }
288
+ // Create and start agent
289
+ const agent = (0, agent_1.createAgent)(parseCommand(serverCommand), config, supabaseConfig);
290
+ await agent.start();
291
+ }
292
+ // Run CLI if this is the main module
293
+ if (require.main === module) {
294
+ main().catch((err) => {
295
+ console.error("Fatal error:", err);
296
+ process.exit(1);
297
+ });
298
+ }