@brutalist/mcp 0.6.0 → 0.6.2

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 (71) hide show
  1. package/README.md +3 -1
  2. package/dist/brutalist-server.d.ts +5 -0
  3. package/dist/brutalist-server.d.ts.map +1 -1
  4. package/dist/brutalist-server.js +295 -92
  5. package/dist/brutalist-server.js.map +1 -1
  6. package/dist/cli-agents.d.ts +7 -3
  7. package/dist/cli-agents.d.ts.map +1 -1
  8. package/dist/cli-agents.js +316 -56
  9. package/dist/cli-agents.js.map +1 -1
  10. package/dist/streaming/circuit-breaker.d.ts +186 -0
  11. package/dist/streaming/circuit-breaker.d.ts.map +1 -0
  12. package/dist/streaming/circuit-breaker.js +463 -0
  13. package/dist/streaming/circuit-breaker.js.map +1 -0
  14. package/dist/streaming/intelligent-buffer.d.ts +141 -0
  15. package/dist/streaming/intelligent-buffer.d.ts.map +1 -0
  16. package/dist/streaming/intelligent-buffer.js +555 -0
  17. package/dist/streaming/intelligent-buffer.js.map +1 -0
  18. package/dist/streaming/output-parser.d.ts +89 -0
  19. package/dist/streaming/output-parser.d.ts.map +1 -0
  20. package/dist/streaming/output-parser.js +349 -0
  21. package/dist/streaming/output-parser.js.map +1 -0
  22. package/dist/streaming/progress-tracker.d.ts +149 -0
  23. package/dist/streaming/progress-tracker.d.ts.map +1 -0
  24. package/dist/streaming/progress-tracker.js +519 -0
  25. package/dist/streaming/progress-tracker.js.map +1 -0
  26. package/dist/streaming/session-manager.d.ts +238 -0
  27. package/dist/streaming/session-manager.d.ts.map +1 -0
  28. package/dist/streaming/session-manager.js +546 -0
  29. package/dist/streaming/session-manager.js.map +1 -0
  30. package/dist/streaming/sse-transport.d.ts +95 -0
  31. package/dist/streaming/sse-transport.d.ts.map +1 -0
  32. package/dist/streaming/sse-transport.js +319 -0
  33. package/dist/streaming/sse-transport.js.map +1 -0
  34. package/dist/streaming/streaming-orchestrator.d.ts +153 -0
  35. package/dist/streaming/streaming-orchestrator.d.ts.map +1 -0
  36. package/dist/streaming/streaming-orchestrator.js +436 -0
  37. package/dist/streaming/streaming-orchestrator.js.map +1 -0
  38. package/dist/test-utils/process-manager.d.ts +61 -0
  39. package/dist/test-utils/process-manager.d.ts.map +1 -0
  40. package/dist/test-utils/process-manager.js +262 -0
  41. package/dist/test-utils/process-manager.js.map +1 -0
  42. package/dist/test-utils/server-harness.d.ts +73 -0
  43. package/dist/test-utils/server-harness.d.ts.map +1 -0
  44. package/dist/test-utils/server-harness.js +297 -0
  45. package/dist/test-utils/server-harness.js.map +1 -0
  46. package/dist/test-utils/streaming-fuzz.d.ts +57 -0
  47. package/dist/test-utils/streaming-fuzz.d.ts.map +1 -0
  48. package/dist/test-utils/streaming-fuzz.js +287 -0
  49. package/dist/test-utils/streaming-fuzz.js.map +1 -0
  50. package/dist/test-utils/test-isolation.d.ts +70 -0
  51. package/dist/test-utils/test-isolation.d.ts.map +1 -0
  52. package/dist/test-utils/test-isolation.js +193 -0
  53. package/dist/test-utils/test-isolation.js.map +1 -0
  54. package/dist/tool-definitions.d.ts.map +1 -1
  55. package/dist/tool-definitions.js +12 -6
  56. package/dist/tool-definitions.js.map +1 -1
  57. package/dist/types/brutalist.d.ts +3 -3
  58. package/dist/types/brutalist.d.ts.map +1 -1
  59. package/dist/types/tool-config.d.ts +0 -1
  60. package/dist/types/tool-config.d.ts.map +1 -1
  61. package/dist/types/tool-config.js +0 -1
  62. package/dist/types/tool-config.js.map +1 -1
  63. package/dist/utils/pagination.d.ts +3 -3
  64. package/dist/utils/pagination.d.ts.map +1 -1
  65. package/dist/utils/pagination.js +24 -6
  66. package/dist/utils/pagination.js.map +1 -1
  67. package/dist/utils/response-cache.d.ts +23 -7
  68. package/dist/utils/response-cache.d.ts.map +1 -1
  69. package/dist/utils/response-cache.js +202 -62
  70. package/dist/utils/response-cache.js.map +1 -1
  71. package/package.json +13 -3
@@ -1,10 +1,18 @@
1
- import { spawn } from 'child_process';
1
+ import { spawn, exec } from 'child_process';
2
+ import { realpathSync, appendFileSync } from 'fs';
3
+ import { promisify } from 'util';
2
4
  import { logger } from './logger.js';
3
5
  // Configurable timeouts and limits
4
6
  const DEFAULT_TIMEOUT = parseInt(process.env.BRUTALIST_TIMEOUT || '300000', 10); // 5 minutes default
5
7
  const CLI_CHECK_TIMEOUT = parseInt(process.env.BRUTALIST_CLI_CHECK_TIMEOUT || '5000', 10); // 5 seconds for CLI checks
6
8
  const MAX_BUFFER_SIZE = parseInt(process.env.BRUTALIST_MAX_BUFFER || String(10 * 1024 * 1024), 10); // 10MB default
7
9
  const MAX_CONCURRENT_CLIS = parseInt(process.env.BRUTALIST_MAX_CONCURRENT || '3', 10); // 3 concurrent CLIs
10
+ // Resource limits for security
11
+ const MAX_MEMORY_MB = parseInt(process.env.BRUTALIST_MAX_MEMORY || '2048', 10); // 2GB memory limit per process
12
+ const MAX_CPU_TIME_SEC = parseInt(process.env.BRUTALIST_MAX_CPU_TIME || '1800', 10); // 30 minutes CPU time (should exceed default timeout)
13
+ const MEMORY_CHECK_INTERVAL = 5000; // Check memory usage every 5 seconds
14
+ // Process tracking for resource management
15
+ const activeProcesses = new Map();
8
16
  // Available models for each CLI
9
17
  export const AVAILABLE_MODELS = {
10
18
  claude: {
@@ -21,22 +29,200 @@ export const AVAILABLE_MODELS = {
21
29
  models: ['gemini-2.5-flash', 'gemini-2.5-pro', 'gemini-2.5-flash-lite']
22
30
  }
23
31
  };
32
+ // Security utilities for CLI execution
33
+ // Only block actual injection vectors, not natural language punctuation
34
+ const DANGEROUS_CHARS = /[;&|`\$\x00-\x1F\x7F]/;
35
+ const MAX_ARG_LENGTH = 4096; // Maximum argument length
36
+ const MAX_PATH_DEPTH = 10; // Maximum directory depth for paths
37
+ // Validate and sanitize CLI arguments
38
+ function validateArguments(args) {
39
+ for (const arg of args) {
40
+ // Check argument length
41
+ if (arg.length > MAX_ARG_LENGTH) {
42
+ throw new Error(`Argument too long: ${arg.length} > ${MAX_ARG_LENGTH} characters`);
43
+ }
44
+ // Check for dangerous characters that could enable injection
45
+ if (DANGEROUS_CHARS.test(arg)) {
46
+ throw new Error(`Argument contains dangerous characters: ${arg}`);
47
+ }
48
+ // Check for null bytes (common injection technique)
49
+ if (arg.includes('\0')) {
50
+ throw new Error('Argument contains null byte');
51
+ }
52
+ }
53
+ }
54
+ // Validate and canonicalize paths to prevent traversal attacks
55
+ function validatePath(path, name) {
56
+ if (!path) {
57
+ throw new Error(`${name} cannot be empty`);
58
+ }
59
+ // Check for null bytes
60
+ if (path.includes('\0')) {
61
+ throw new Error(`${name} contains null byte`);
62
+ }
63
+ // Check for dangerous path traversal patterns
64
+ if (path.includes('../') || path.includes('..\\') || path.includes('/..') || path.includes('\\..')) {
65
+ throw new Error(`${name} contains path traversal attempt: ${path}`);
66
+ }
67
+ // Check path depth to prevent deeply nested attacks
68
+ const depth = path.split('/').length - 1;
69
+ if (depth > MAX_PATH_DEPTH) {
70
+ throw new Error(`${name} exceeds maximum depth: ${depth} > ${MAX_PATH_DEPTH}`);
71
+ }
72
+ // Canonicalize the path (this also validates it exists and resolves symlinks)
73
+ try {
74
+ return realpathSync(path);
75
+ }
76
+ catch (error) {
77
+ throw new Error(`Invalid ${name}: ${error instanceof Error ? error.message : String(error)}`);
78
+ }
79
+ }
80
+ // Create secure environment for CLI processes
81
+ function createSecureEnvironment() {
82
+ // Minimal environment whitelist
83
+ const SAFE_ENV_VARS = [
84
+ 'PATH',
85
+ 'HOME',
86
+ 'USER',
87
+ 'SHELL',
88
+ 'TERM',
89
+ 'LANG',
90
+ 'LC_ALL',
91
+ 'TZ',
92
+ 'NODE_ENV'
93
+ ];
94
+ const secureEnv = {};
95
+ // Copy only safe environment variables
96
+ for (const varName of SAFE_ENV_VARS) {
97
+ if (process.env[varName]) {
98
+ secureEnv[varName] = process.env[varName];
99
+ }
100
+ }
101
+ // Add security-focused environment variables
102
+ secureEnv.TERM = 'dumb'; // Disable terminal features
103
+ secureEnv.NO_COLOR = '1'; // Disable color output
104
+ secureEnv.CI = 'true'; // Indicate non-interactive environment
105
+ return secureEnv;
106
+ }
107
+ // Cross-platform memory usage monitoring
108
+ async function getUnixMemoryUsage(pid) {
109
+ try {
110
+ const execAsync = promisify(exec);
111
+ // Use ps command to get memory usage in KB
112
+ const { stdout } = await execAsync(`ps -o rss= -p ${pid}`);
113
+ const memoryKB = parseInt(stdout.trim(), 10);
114
+ if (isNaN(memoryKB))
115
+ return null;
116
+ return { memoryMB: Math.round(memoryKB / 1024) };
117
+ }
118
+ catch {
119
+ return null;
120
+ }
121
+ }
122
+ async function getWindowsMemoryUsage(pid) {
123
+ try {
124
+ const execAsync = promisify(exec);
125
+ // Use wmic command to get memory usage
126
+ const { stdout } = await execAsync(`wmic process where "ProcessId=${pid}" get WorkingSetSize /value`);
127
+ const match = stdout.match(/WorkingSetSize=(\d+)/);
128
+ if (!match)
129
+ return null;
130
+ const memoryBytes = parseInt(match[1], 10);
131
+ return { memoryMB: Math.round(memoryBytes / (1024 * 1024)) };
132
+ }
133
+ catch {
134
+ return null;
135
+ }
136
+ }
24
137
  // Safe command execution helper using spawn instead of exec to prevent command injection
25
138
  async function spawnAsync(command, args, options = {}) {
26
139
  return new Promise((resolve, reject) => {
27
- // Use working directory as-is - let CLI tools handle their own sandboxing
28
- const cwd = options.cwd || process.cwd();
140
+ // Validate command name (basic validation)
141
+ if (!command || command.length === 0) {
142
+ reject(new Error('Command cannot be empty'));
143
+ return;
144
+ }
145
+ // Validate arguments for injection attacks
146
+ try {
147
+ validateArguments(args);
148
+ }
149
+ catch (error) {
150
+ reject(error);
151
+ return;
152
+ }
153
+ // Validate and canonicalize working directory
154
+ let cwd;
155
+ try {
156
+ if (options.cwd) {
157
+ cwd = validatePath(options.cwd, 'working directory');
158
+ }
159
+ else {
160
+ cwd = process.cwd();
161
+ }
162
+ }
163
+ catch (error) {
164
+ reject(error);
165
+ return;
166
+ }
167
+ // Use secure environment
168
+ const secureEnv = options.env || createSecureEnvironment();
29
169
  const child = spawn(command, args, {
30
170
  cwd: cwd,
31
171
  stdio: ['pipe', 'pipe', 'pipe'],
32
172
  shell: false, // CRITICAL: disable shell to prevent injection
33
- detached: command !== 'gemini', // Disable detached for Gemini CLI to fix macOS sandbox issue
34
- env: options.env || process.env
173
+ detached: command !== 'gemini', // Disable detached for Gemini CLI to fix macOS issue
174
+ env: secureEnv,
175
+ // Additional security options
176
+ uid: process.getuid ? process.getuid() : undefined, // Maintain current user ID
177
+ gid: process.getgid ? process.getgid() : undefined // Maintain current group ID
35
178
  });
36
179
  let stdout = '';
37
180
  let stderr = '';
38
181
  let timedOut = false;
39
182
  let killed = false;
183
+ // Track process for resource monitoring
184
+ if (child.pid) {
185
+ activeProcesses.set(child.pid, {
186
+ startTime: Date.now(),
187
+ memoryChecks: 0
188
+ });
189
+ }
190
+ // Memory monitoring timer
191
+ let memoryTimer;
192
+ if (child.pid) {
193
+ memoryTimer = setInterval(async () => {
194
+ try {
195
+ const pid = child.pid;
196
+ const processInfo = activeProcesses.get(pid);
197
+ if (!processInfo || killed) {
198
+ if (memoryTimer)
199
+ clearInterval(memoryTimer);
200
+ return;
201
+ }
202
+ processInfo.memoryChecks++;
203
+ // Check memory usage (cross-platform)
204
+ const usage = process.platform === 'win32'
205
+ ? await getWindowsMemoryUsage(pid)
206
+ : await getUnixMemoryUsage(pid);
207
+ if (usage && usage.memoryMB > MAX_MEMORY_MB) {
208
+ child.kill('SIGTERM');
209
+ reject(new Error(`Process exceeded memory limit: ${usage.memoryMB}MB > ${MAX_MEMORY_MB}MB`));
210
+ return;
211
+ }
212
+ // Check CPU time limit
213
+ const runtimeMs = Date.now() - processInfo.startTime;
214
+ if (runtimeMs > MAX_CPU_TIME_SEC * 1000) {
215
+ child.kill('SIGTERM');
216
+ reject(new Error(`Process exceeded CPU time limit: ${runtimeMs}ms > ${MAX_CPU_TIME_SEC * 1000}ms`));
217
+ return;
218
+ }
219
+ }
220
+ catch (error) {
221
+ // Memory check failed, but don't kill process for this
222
+ logger.warn('Memory check failed:', error);
223
+ }
224
+ }, MEMORY_CHECK_INTERVAL);
225
+ }
40
226
  // Set up timeout with SIGKILL escalation
41
227
  const timeoutMs = options.timeout || DEFAULT_TIMEOUT;
42
228
  let killTimer;
@@ -98,6 +284,12 @@ async function spawnAsync(command, args, options = {}) {
98
284
  clearTimeout(timer);
99
285
  if (killTimer)
100
286
  clearTimeout(killTimer);
287
+ if (memoryTimer)
288
+ clearInterval(memoryTimer);
289
+ // Clean up process tracking
290
+ if (child.pid) {
291
+ activeProcesses.delete(child.pid);
292
+ }
101
293
  if (!timedOut) {
102
294
  if (code === 0) {
103
295
  resolve({ stdout, stderr });
@@ -115,6 +307,12 @@ async function spawnAsync(command, args, options = {}) {
115
307
  clearTimeout(timer);
116
308
  if (killTimer)
117
309
  clearTimeout(killTimer);
310
+ if (memoryTimer)
311
+ clearInterval(memoryTimer);
312
+ // Clean up process tracking
313
+ if (child.pid) {
314
+ activeProcesses.delete(child.pid);
315
+ }
118
316
  reject(error);
119
317
  });
120
318
  // Send input if provided
@@ -230,22 +428,25 @@ export class CLIAgentOrchestrator {
230
428
  continue;
231
429
  try {
232
430
  const event = JSON.parse(line);
431
+ // Codex --json outputs events with structure: {"type":"item.completed","item":{...}}
233
432
  // Only extract agent_message type - this is the actual response
234
- if (event.msg?.type === 'agent_message' && event.msg?.message) {
235
- agentMessages.push(event.msg.message);
236
- }
237
- else if (event.msg?.type === 'error' && event.msg?.message) {
238
- // Include error messages
239
- agentMessages.push(`Error: ${event.msg.message}`);
433
+ if (event.type === 'item.completed' && event.item) {
434
+ if (event.item.type === 'agent_message' && event.item.text) {
435
+ // Agent's actual response text
436
+ agentMessages.push(event.item.text);
437
+ }
438
+ // Skip all other types:
439
+ // - reasoning: internal thinking steps
440
+ // - command_execution: file reads, bash commands
441
+ // - error: will be in stderr
240
442
  }
241
- // Skip all other types: agent_reasoning, exec, token_count, task_started, etc.
242
443
  }
243
444
  catch {
244
445
  // Skip non-JSON lines (config output, prompts, etc.)
245
446
  continue;
246
447
  }
247
448
  }
248
- return agentMessages.join('\n').trim();
449
+ return agentMessages.join('\n\n').trim();
249
450
  }
250
451
  emitThrottledStreamingEvent(agent, type, content, onStreamingEvent, options) {
251
452
  if (!onStreamingEvent)
@@ -258,7 +459,9 @@ export class CLIAgentOrchestrator {
258
459
  return; // Skip non-content events
259
460
  processedContent = filtered;
260
461
  }
261
- const key = `${agent}-${type}`;
462
+ // Use requestId to prevent buffer sharing between overlapping requests
463
+ const requestId = options?.requestId || 'default';
464
+ const key = `${agent}-${type}-${requestId}`;
262
465
  const now = Date.now();
263
466
  // Truncate content to prevent huge events
264
467
  const truncatedContent = processedContent.length > this.MAX_CHUNK_SIZE
@@ -284,7 +487,8 @@ export class CLIAgentOrchestrator {
284
487
  type,
285
488
  agent,
286
489
  content: combinedContent,
287
- timestamp: now
490
+ timestamp: now,
491
+ sessionId: options?.sessionId
288
492
  });
289
493
  // Reset buffer
290
494
  buffer.chunks = [];
@@ -406,16 +610,10 @@ export class CLIAgentOrchestrator {
406
610
  type: 'agent_start',
407
611
  agent: cliName,
408
612
  content: `Starting ${cliName.toUpperCase()} analysis...`,
409
- timestamp: Date.now()
613
+ timestamp: Date.now(),
614
+ sessionId: options.sessionId
410
615
  });
411
616
  }
412
- // WARNING: Claude CLI does not have a native --sandbox flag.
413
- // If options.sandbox is true, it is assumed that the environment
414
- // running this Brutalist MCP server provides the sandboxing (e.g., Docker, VM).
415
- // Running Claude without external sandboxing can be a security risk.
416
- if (cliName === 'claude' && options.sandbox) {
417
- logger.warn("⚠️ Claude CLI requested with sandbox: true, but Claude CLI does not support native sandboxing. Ensure external sandboxing is in place.");
418
- }
419
617
  const { command, args, env, input } = commandBuilder(userPrompt, systemPromptSpec, options);
420
618
  logger.info(`📋 Command: ${command} ${args.join(' ')}`);
421
619
  logger.info(`📁 Working directory: ${workingDir}`);
@@ -450,7 +648,8 @@ export class CLIAgentOrchestrator {
450
648
  type: 'agent_complete',
451
649
  agent: cliName,
452
650
  content: `${cliName.toUpperCase()} analysis completed (${Date.now() - startTime}ms)`,
453
- timestamp: Date.now()
651
+ timestamp: Date.now(),
652
+ sessionId: options.sessionId
454
653
  });
455
654
  }
456
655
  // Post-process CLI output if needed
@@ -509,7 +708,8 @@ export class CLIAgentOrchestrator {
509
708
  type: 'agent_error',
510
709
  agent: cliName,
511
710
  content: `${cliName.toUpperCase()} failed: ${error instanceof Error ? error.message : String(error)}`,
512
- timestamp: Date.now()
711
+ timestamp: Date.now(),
712
+ sessionId: options.sessionId
513
713
  });
514
714
  }
515
715
  return {
@@ -550,16 +750,12 @@ export class CLIAgentOrchestrator {
550
750
  });
551
751
  }
552
752
  async executeCodex(userPrompt, systemPromptSpec, options = {}) {
553
- return this._executeCLI('codex', userPrompt, systemPromptSpec, { ...options, sandbox: true }, // Ensure sandbox is always true for Codex
554
- (userPrompt, systemPromptSpec, options) => {
753
+ return this._executeCLI('codex', userPrompt, systemPromptSpec, { ...options }, (userPrompt, systemPromptSpec, options) => {
555
754
  const combinedPrompt = `CONTEXT AND INSTRUCTIONS:\n${systemPromptSpec}\n\nANALYZE:\n${userPrompt}`;
556
755
  const args = ['exec'];
557
756
  // Use provided model or default to gpt-5
558
757
  const model = options.models?.codex || AVAILABLE_MODELS.codex.default;
559
758
  args.push('--model', model);
560
- if (options.sandbox) {
561
- args.push('--sandbox', 'read-only');
562
- }
563
759
  // Add JSON flag to get structured output without verbose details
564
760
  args.push('--json');
565
761
  // Use stdin for the prompt instead of argv to avoid ARG_MAX limits
@@ -571,15 +767,11 @@ export class CLIAgentOrchestrator {
571
767
  });
572
768
  }
573
769
  async executeGemini(userPrompt, systemPromptSpec, options = {}) {
574
- return this._executeCLI('gemini', userPrompt, systemPromptSpec, { ...options, sandbox: true }, // Ensure sandbox is always true for Gemini
575
- (userPrompt, systemPromptSpec, options) => {
770
+ return this._executeCLI('gemini', userPrompt, systemPromptSpec, { ...options }, (userPrompt, systemPromptSpec, options) => {
576
771
  const args = [];
577
772
  // Use provided model or default to gemini-2.5-flash
578
773
  const modelName = options.models?.gemini || AVAILABLE_MODELS.gemini.default;
579
774
  args.push('--model', modelName);
580
- if (options.sandbox) {
581
- args.push('--sandbox');
582
- }
583
775
  const combinedPrompt = `${systemPromptSpec}\n\n${userPrompt}`;
584
776
  args.push(combinedPrompt);
585
777
  return {
@@ -604,9 +796,9 @@ export class CLIAgentOrchestrator {
604
796
  case 'claude':
605
797
  return await this.executeClaudeCode(userPrompt, systemPromptSpec, options);
606
798
  case 'codex':
607
- return await this.executeCodex(userPrompt, systemPromptSpec, { ...options, sandbox: true });
799
+ return await this.executeCodex(userPrompt, systemPromptSpec, options);
608
800
  case 'gemini':
609
- return await this.executeGemini(userPrompt, systemPromptSpec, { ...options, sandbox: true });
801
+ return await this.executeGemini(userPrompt, systemPromptSpec, options);
610
802
  default:
611
803
  throw new Error(`Unknown CLI: ${cli}`);
612
804
  }
@@ -624,8 +816,76 @@ export class CLIAgentOrchestrator {
624
816
  waitTime = Math.min(waitTime * 2, 5000); // Exponential backoff, max 5 seconds
625
817
  }
626
818
  }
627
- async executeBrutalistAnalysis(analysisType, targetPath, systemPromptSpec, context, options = {}) {
628
- const userPrompt = this.constructUserPrompt(analysisType, targetPath, context);
819
+ async executeCLIAgents(cliAgents, systemPrompt, userPrompt, options = {}) {
820
+ const responses = [];
821
+ for (const agent of cliAgents) {
822
+ if (['claude', 'codex', 'gemini'].includes(agent)) {
823
+ try {
824
+ const response = await this.executeCLIAgent(agent, systemPrompt, userPrompt, options);
825
+ responses.push(response);
826
+ }
827
+ catch (error) {
828
+ responses.push({
829
+ agent: agent,
830
+ success: false,
831
+ output: '',
832
+ error: error instanceof Error ? error.message : String(error),
833
+ executionTime: 0,
834
+ command: `${agent} execution failed`,
835
+ workingDirectory: options.workingDirectory || process.cwd(),
836
+ exitCode: -1
837
+ });
838
+ }
839
+ }
840
+ }
841
+ return responses;
842
+ }
843
+ async executeCLIAgent(agent, systemPrompt, userPrompt, options = {}) {
844
+ if (!['claude', 'codex', 'gemini'].includes(agent)) {
845
+ throw new Error(`Unsupported CLI agent: ${agent}`);
846
+ }
847
+ return await this.executeSingleCLI(agent, userPrompt, systemPrompt, options);
848
+ }
849
+ async executeBrutalistAnalysis(analysisType, primaryContent, systemPromptSpec, context, options = {}) {
850
+ // Debug logging for path validation logic - write to file to avoid MCP stdio interference
851
+ const debugLog = `/tmp/brutalist-debug-${Date.now()}.log`;
852
+ const logMessage = (msg) => {
853
+ try {
854
+ appendFileSync(debugLog, `${new Date().toISOString()}: ${msg}\n`);
855
+ }
856
+ catch (e) {
857
+ // Ignore filesystem errors
858
+ }
859
+ };
860
+ logMessage(`🔧 VALIDATION DEBUG: analysisType="${analysisType}", primaryContent="${primaryContent}"`);
861
+ // Only validate filesystem paths for tools that actually operate on files/directories
862
+ const filesystemTools = ['codebase', 'file_structure', 'dependencies', 'git_history', 'test_coverage'];
863
+ logMessage(`🔧 VALIDATION DEBUG: filesystemTools.includes(analysisType)=${filesystemTools.includes(analysisType)}`);
864
+ logMessage(`🔧 VALIDATION DEBUG: primaryContent exists=${!!primaryContent}`);
865
+ logMessage(`🔧 VALIDATION DEBUG: primaryContent.trim() !== ''=${primaryContent ? primaryContent.trim() !== '' : false}`);
866
+ try {
867
+ if (filesystemTools.includes(analysisType) && primaryContent && primaryContent.trim() !== '') {
868
+ logMessage(`🔧 VALIDATION DEBUG: Calling validatePath for "${primaryContent}"`);
869
+ validatePath(primaryContent, 'targetPath');
870
+ }
871
+ else {
872
+ logMessage(`🔧 VALIDATION DEBUG: Skipping validatePath - not a filesystem tool`);
873
+ }
874
+ }
875
+ catch (error) {
876
+ logMessage(`🔧 VALIDATION DEBUG: validatePath failed with error: ${error}`);
877
+ throw new Error(`Security validation failed: ${error instanceof Error ? error.message : String(error)}`);
878
+ }
879
+ // Validate workingDirectory if provided
880
+ try {
881
+ if (options.workingDirectory) {
882
+ validatePath(options.workingDirectory, 'workingDirectory');
883
+ }
884
+ }
885
+ catch (error) {
886
+ throw new Error(`Security validation failed: ${error instanceof Error ? error.message : String(error)}`);
887
+ }
888
+ const userPrompt = this.constructUserPrompt(analysisType, primaryContent, context);
629
889
  // If preferred CLI is specified, use single CLI mode
630
890
  if (options.preferredCLI) {
631
891
  const selectedCLI = this.selectSingleCLI(options.preferredCLI, options.analysisType);
@@ -707,27 +967,27 @@ export class CLIAgentOrchestrator {
707
967
  }
708
968
  return synthesis.trim();
709
969
  }
710
- constructUserPrompt(analysisType, targetPath, context) {
970
+ constructUserPrompt(analysisType, primaryContent, context) {
711
971
  // Trust CLI tools to handle their own security
712
- const sanitizedTargetPath = targetPath;
972
+ const sanitizedContent = primaryContent;
713
973
  const sanitizedContext = context || 'No additional context provided';
714
974
  const prompts = {
715
- code: `Analyze the codebase at ${sanitizedTargetPath} for issues. Context: ${sanitizedContext}`,
716
- codebase: `Analyze the codebase directory at ${sanitizedTargetPath} for security vulnerabilities, performance issues, and architectural problems. Context: ${sanitizedContext}`,
717
- architecture: `Review the architecture: ${sanitizedTargetPath}. Find every scaling failure and cost explosion.`,
718
- idea: `Analyze this idea: ${sanitizedTargetPath}. Find where imagination fails to become reality.`,
719
- research: `Review this research: ${sanitizedTargetPath}. Find every methodological flaw and reproducibility issue.`,
720
- data: `Analyze this data/model: ${sanitizedTargetPath}. Find every overfitting issue, bias, and correlation fallacy.`,
721
- security: `Security audit of: ${sanitizedTargetPath}. Find every attack vector and vulnerability.`,
722
- product: `Product review: ${sanitizedTargetPath}. Find every UX disaster and adoption barrier.`,
723
- infrastructure: `Infrastructure review: ${sanitizedTargetPath}. Find every single point of failure.`,
724
- debate: `Debate topic: ${sanitizedTargetPath}. Take opposing positions and argue until truth emerges.`,
725
- fileStructure: `Analyze the directory structure at ${sanitizedTargetPath}. Find organizational disasters and naming failures.`,
726
- dependencies: `Analyze dependencies at ${sanitizedTargetPath}. Find version conflicts and security vulnerabilities.`,
727
- gitHistory: `Analyze git history at ${sanitizedTargetPath}. Find commit disasters and workflow failures.`,
728
- testCoverage: `Analyze test coverage at ${sanitizedTargetPath}. Find testing gaps and quality issues.`
975
+ code: `Analyze the codebase at ${sanitizedContent} for issues. Context: ${sanitizedContext}`,
976
+ codebase: `Analyze the codebase directory at ${sanitizedContent} for security vulnerabilities, performance issues, and architectural problems. Context: ${sanitizedContext}`,
977
+ architecture: `Review the architecture: ${sanitizedContent}. Find every scaling failure and cost explosion.`,
978
+ idea: `Analyze this idea: ${sanitizedContent}. Find where imagination fails to become reality.`,
979
+ research: `Review this research: ${sanitizedContent}. Find every methodological flaw and reproducibility issue.`,
980
+ data: `Analyze this data/model: ${sanitizedContent}. Find every overfitting issue, bias, and correlation fallacy.`,
981
+ security: `Security audit of: ${sanitizedContent}. Find every attack vector and vulnerability.`,
982
+ product: `Product review: ${sanitizedContent}. Find every UX disaster and adoption barrier.`,
983
+ infrastructure: `Infrastructure review: ${sanitizedContent}. Find every single point of failure.`,
984
+ debate: `Debate topic: ${sanitizedContent}. Take opposing positions and argue until truth emerges.`,
985
+ fileStructure: `Analyze the directory structure at ${sanitizedContent}. Find organizational disasters and naming failures.`,
986
+ dependencies: `Analyze dependencies at ${sanitizedContent}. Find version conflicts and security vulnerabilities.`,
987
+ gitHistory: `Analyze git history at ${sanitizedContent}. Find commit disasters and workflow failures.`,
988
+ testCoverage: `Analyze test coverage at ${sanitizedContent}. Find testing gaps and quality issues.`
729
989
  };
730
- const specificPrompt = prompts[analysisType] || `Analyze ${sanitizedTargetPath} for ${analysisType} issues.`;
990
+ const specificPrompt = prompts[analysisType] || `Analyze ${sanitizedContent} for ${analysisType} issues.`;
731
991
  return `${specificPrompt} ${context ? `Context: ${sanitizedContext}` : ''}`;
732
992
  }
733
993
  }