@brutalist/mcp 0.5.1 → 0.6.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 (75) hide show
  1. package/README.md +65 -63
  2. package/dist/brutalist-server.d.ts +15 -0
  3. package/dist/brutalist-server.d.ts.map +1 -1
  4. package/dist/brutalist-server.js +405 -357
  5. package/dist/brutalist-server.js.map +1 -1
  6. package/dist/cli-agents.d.ts +8 -3
  7. package/dist/cli-agents.d.ts.map +1 -1
  8. package/dist/cli-agents.js +352 -50
  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 +296 -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 +6 -0
  55. package/dist/tool-definitions.d.ts.map +1 -0
  56. package/dist/tool-definitions.js +217 -0
  57. package/dist/tool-definitions.js.map +1 -0
  58. package/dist/types/brutalist.d.ts +3 -19
  59. package/dist/types/brutalist.d.ts.map +1 -1
  60. package/dist/types/tool-config.d.ts +51 -0
  61. package/dist/types/tool-config.d.ts.map +1 -0
  62. package/dist/types/tool-config.js +24 -0
  63. package/dist/types/tool-config.js.map +1 -0
  64. package/dist/utils/pagination.d.ts +2 -2
  65. package/dist/utils/pagination.d.ts.map +1 -1
  66. package/dist/utils/pagination.js +1 -1
  67. package/dist/utils/pagination.js.map +1 -1
  68. package/dist/utils/response-cache.d.ts +96 -0
  69. package/dist/utils/response-cache.d.ts.map +1 -0
  70. package/dist/utils/response-cache.js +371 -0
  71. package/dist/utils/response-cache.js.map +1 -0
  72. package/dist/utils.d.ts.map +1 -1
  73. package/dist/utils.js +22 -3
  74. package/dist/utils.js.map +1 -1
  75. package/package.json +14 -4
@@ -1,10 +1,18 @@
1
- import { spawn } from 'child_process';
1
+ import { spawn, exec } from 'child_process';
2
+ import { realpathSync } 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 || '600', 10); // 10 minutes CPU time
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,30 +29,210 @@ 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
+ // TEMPORARILY DISABLED FOR TESTING
46
+ // if (DANGEROUS_CHARS.test(arg)) {
47
+ // throw new Error(`Argument contains dangerous characters: ${arg}`);
48
+ // }
49
+ // Check for null bytes (common injection technique)
50
+ if (arg.includes('\0')) {
51
+ throw new Error('Argument contains null byte');
52
+ }
53
+ }
54
+ }
55
+ // Validate and canonicalize paths to prevent traversal attacks
56
+ function validatePath(path, name) {
57
+ if (!path) {
58
+ throw new Error(`${name} cannot be empty`);
59
+ }
60
+ // Check for null bytes
61
+ if (path.includes('\0')) {
62
+ throw new Error(`${name} contains null byte`);
63
+ }
64
+ // Check for dangerous path traversal patterns
65
+ if (path.includes('../') || path.includes('..\\') || path.includes('/..') || path.includes('\\..')) {
66
+ throw new Error(`${name} contains path traversal attempt: ${path}`);
67
+ }
68
+ // Check path depth to prevent deeply nested attacks
69
+ const depth = path.split('/').length - 1;
70
+ if (depth > MAX_PATH_DEPTH) {
71
+ throw new Error(`${name} exceeds maximum depth: ${depth} > ${MAX_PATH_DEPTH}`);
72
+ }
73
+ // Canonicalize the path (this also validates it exists and resolves symlinks)
74
+ try {
75
+ return realpathSync(path);
76
+ }
77
+ catch (error) {
78
+ throw new Error(`Invalid ${name}: ${error instanceof Error ? error.message : String(error)}`);
79
+ }
80
+ }
81
+ // Create secure environment for CLI processes
82
+ function createSecureEnvironment() {
83
+ // Minimal environment whitelist
84
+ const SAFE_ENV_VARS = [
85
+ 'PATH',
86
+ 'HOME',
87
+ 'USER',
88
+ 'SHELL',
89
+ 'TERM',
90
+ 'LANG',
91
+ 'LC_ALL',
92
+ 'TZ',
93
+ 'NODE_ENV'
94
+ ];
95
+ const secureEnv = {};
96
+ // Copy only safe environment variables
97
+ for (const varName of SAFE_ENV_VARS) {
98
+ if (process.env[varName]) {
99
+ secureEnv[varName] = process.env[varName];
100
+ }
101
+ }
102
+ // Add security-focused environment variables
103
+ secureEnv.TERM = 'dumb'; // Disable terminal features
104
+ secureEnv.NO_COLOR = '1'; // Disable color output
105
+ secureEnv.CI = 'true'; // Indicate non-interactive environment
106
+ return secureEnv;
107
+ }
108
+ // Cross-platform memory usage monitoring
109
+ async function getUnixMemoryUsage(pid) {
110
+ try {
111
+ const execAsync = promisify(exec);
112
+ // Use ps command to get memory usage in KB
113
+ const { stdout } = await execAsync(`ps -o rss= -p ${pid}`);
114
+ const memoryKB = parseInt(stdout.trim(), 10);
115
+ if (isNaN(memoryKB))
116
+ return null;
117
+ return { memoryMB: Math.round(memoryKB / 1024) };
118
+ }
119
+ catch {
120
+ return null;
121
+ }
122
+ }
123
+ async function getWindowsMemoryUsage(pid) {
124
+ try {
125
+ const execAsync = promisify(exec);
126
+ // Use wmic command to get memory usage
127
+ const { stdout } = await execAsync(`wmic process where "ProcessId=${pid}" get WorkingSetSize /value`);
128
+ const match = stdout.match(/WorkingSetSize=(\d+)/);
129
+ if (!match)
130
+ return null;
131
+ const memoryBytes = parseInt(match[1], 10);
132
+ return { memoryMB: Math.round(memoryBytes / (1024 * 1024)) };
133
+ }
134
+ catch {
135
+ return null;
136
+ }
137
+ }
24
138
  // Safe command execution helper using spawn instead of exec to prevent command injection
25
139
  async function spawnAsync(command, args, options = {}) {
26
140
  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();
141
+ // Validate command name (basic validation)
142
+ if (!command || command.length === 0) {
143
+ reject(new Error('Command cannot be empty'));
144
+ return;
145
+ }
146
+ // Validate arguments for injection attacks
147
+ // TEMPORARILY DISABLED FOR TESTING
148
+ // try {
149
+ // validateArguments(args);
150
+ // } catch (error) {
151
+ // reject(error);
152
+ // return;
153
+ // }
154
+ // Validate and canonicalize working directory
155
+ let cwd;
156
+ try {
157
+ if (options.cwd) {
158
+ cwd = validatePath(options.cwd, 'working directory');
159
+ }
160
+ else {
161
+ cwd = process.cwd();
162
+ }
163
+ }
164
+ catch (error) {
165
+ reject(error);
166
+ return;
167
+ }
168
+ // Use secure environment
169
+ const secureEnv = options.env || createSecureEnvironment();
29
170
  const child = spawn(command, args, {
30
171
  cwd: cwd,
31
172
  stdio: ['pipe', 'pipe', 'pipe'],
32
173
  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
174
+ detached: command !== 'gemini', // Disable detached for Gemini CLI to fix macOS issue
175
+ env: secureEnv,
176
+ // Additional security options
177
+ uid: process.getuid ? process.getuid() : undefined, // Maintain current user ID
178
+ gid: process.getgid ? process.getgid() : undefined // Maintain current group ID
35
179
  });
36
180
  let stdout = '';
37
181
  let stderr = '';
38
182
  let timedOut = false;
39
183
  let killed = false;
184
+ // Track process for resource monitoring
185
+ if (child.pid) {
186
+ activeProcesses.set(child.pid, {
187
+ startTime: Date.now(),
188
+ memoryChecks: 0
189
+ });
190
+ }
191
+ // Memory monitoring timer
192
+ let memoryTimer;
193
+ if (child.pid) {
194
+ memoryTimer = setInterval(async () => {
195
+ try {
196
+ const pid = child.pid;
197
+ const processInfo = activeProcesses.get(pid);
198
+ if (!processInfo || killed) {
199
+ if (memoryTimer)
200
+ clearInterval(memoryTimer);
201
+ return;
202
+ }
203
+ processInfo.memoryChecks++;
204
+ // Check memory usage (cross-platform)
205
+ const usage = process.platform === 'win32'
206
+ ? await getWindowsMemoryUsage(pid)
207
+ : await getUnixMemoryUsage(pid);
208
+ if (usage && usage.memoryMB > MAX_MEMORY_MB) {
209
+ child.kill('SIGTERM');
210
+ reject(new Error(`Process exceeded memory limit: ${usage.memoryMB}MB > ${MAX_MEMORY_MB}MB`));
211
+ return;
212
+ }
213
+ // Check CPU time limit
214
+ const runtimeMs = Date.now() - processInfo.startTime;
215
+ if (runtimeMs > MAX_CPU_TIME_SEC * 1000) {
216
+ child.kill('SIGTERM');
217
+ reject(new Error(`Process exceeded CPU time limit: ${runtimeMs}ms > ${MAX_CPU_TIME_SEC * 1000}ms`));
218
+ return;
219
+ }
220
+ }
221
+ catch (error) {
222
+ // Memory check failed, but don't kill process for this
223
+ logger.warn('Memory check failed:', error);
224
+ }
225
+ }, MEMORY_CHECK_INTERVAL);
226
+ }
40
227
  // Set up timeout with SIGKILL escalation
41
228
  const timeoutMs = options.timeout || DEFAULT_TIMEOUT;
229
+ let killTimer;
42
230
  const timer = setTimeout(() => {
43
231
  timedOut = true;
44
232
  // First try SIGTERM
45
233
  child.kill('SIGTERM');
46
234
  // If still running after 5 seconds, escalate to SIGKILL
47
- setTimeout(() => {
235
+ killTimer = setTimeout(() => {
48
236
  if (!killed) {
49
237
  try {
50
238
  if (command === 'gemini' || process.platform === 'win32') {
@@ -95,6 +283,14 @@ async function spawnAsync(command, args, options = {}) {
95
283
  child.on('close', (code) => {
96
284
  killed = true;
97
285
  clearTimeout(timer);
286
+ if (killTimer)
287
+ clearTimeout(killTimer);
288
+ if (memoryTimer)
289
+ clearInterval(memoryTimer);
290
+ // Clean up process tracking
291
+ if (child.pid) {
292
+ activeProcesses.delete(child.pid);
293
+ }
98
294
  if (!timedOut) {
99
295
  if (code === 0) {
100
296
  resolve({ stdout, stderr });
@@ -110,6 +306,14 @@ async function spawnAsync(command, args, options = {}) {
110
306
  });
111
307
  child.on('error', (error) => {
112
308
  clearTimeout(timer);
309
+ if (killTimer)
310
+ clearTimeout(killTimer);
311
+ if (memoryTimer)
312
+ clearInterval(memoryTimer);
313
+ // Clean up process tracking
314
+ if (child.pid) {
315
+ activeProcesses.delete(child.pid);
316
+ }
113
317
  reject(error);
114
318
  });
115
319
  // Send input if provided
@@ -213,6 +417,35 @@ export class CLIAgentOrchestrator {
213
417
  }
214
418
  return textParts.join('');
215
419
  }
420
+ // Extract only the agent messages from Codex JSON output (no thinking, no file reads, no commands)
421
+ extractCodexAgentMessage(jsonOutput) {
422
+ if (!jsonOutput || !jsonOutput.trim()) {
423
+ return '';
424
+ }
425
+ const agentMessages = [];
426
+ const lines = jsonOutput.split('\n');
427
+ for (const line of lines) {
428
+ if (!line.trim())
429
+ continue;
430
+ try {
431
+ const event = JSON.parse(line);
432
+ // Only extract agent_message type - this is the actual response
433
+ if (event.msg?.type === 'agent_message' && event.msg?.message) {
434
+ agentMessages.push(event.msg.message);
435
+ }
436
+ else if (event.msg?.type === 'error' && event.msg?.message) {
437
+ // Include error messages
438
+ agentMessages.push(`Error: ${event.msg.message}`);
439
+ }
440
+ // Skip all other types: agent_reasoning, exec, token_count, task_started, etc.
441
+ }
442
+ catch {
443
+ // Skip non-JSON lines (config output, prompts, etc.)
444
+ continue;
445
+ }
446
+ }
447
+ return agentMessages.join('\n').trim();
448
+ }
216
449
  emitThrottledStreamingEvent(agent, type, content, onStreamingEvent, options) {
217
450
  if (!onStreamingEvent)
218
451
  return;
@@ -224,7 +457,9 @@ export class CLIAgentOrchestrator {
224
457
  return; // Skip non-content events
225
458
  processedContent = filtered;
226
459
  }
227
- const key = `${agent}-${type}`;
460
+ // Use requestId to prevent buffer sharing between overlapping requests
461
+ const requestId = options?.requestId || 'default';
462
+ const key = `${agent}-${type}-${requestId}`;
228
463
  const now = Date.now();
229
464
  // Truncate content to prevent huge events
230
465
  const truncatedContent = processedContent.length > this.MAX_CHUNK_SIZE
@@ -250,7 +485,8 @@ export class CLIAgentOrchestrator {
250
485
  type,
251
486
  agent,
252
487
  content: combinedContent,
253
- timestamp: now
488
+ timestamp: now,
489
+ sessionId: options?.sessionId
254
490
  });
255
491
  // Reset buffer
256
492
  buffer.chunks = [];
@@ -372,16 +608,10 @@ export class CLIAgentOrchestrator {
372
608
  type: 'agent_start',
373
609
  agent: cliName,
374
610
  content: `Starting ${cliName.toUpperCase()} analysis...`,
375
- timestamp: Date.now()
611
+ timestamp: Date.now(),
612
+ sessionId: options.sessionId
376
613
  });
377
614
  }
378
- // WARNING: Claude CLI does not have a native --sandbox flag.
379
- // If options.sandbox is true, it is assumed that the environment
380
- // running this Brutalist MCP server provides the sandboxing (e.g., Docker, VM).
381
- // Running Claude without external sandboxing can be a security risk.
382
- if (cliName === 'claude' && options.sandbox) {
383
- logger.warn("⚠️ Claude CLI requested with sandbox: true, but Claude CLI does not support native sandboxing. Ensure external sandboxing is in place.");
384
- }
385
615
  const { command, args, env, input } = commandBuilder(userPrompt, systemPromptSpec, options);
386
616
  logger.info(`📋 Command: ${command} ${args.join(' ')}`);
387
617
  logger.info(`📁 Working directory: ${workingDir}`);
@@ -416,10 +646,11 @@ export class CLIAgentOrchestrator {
416
646
  type: 'agent_complete',
417
647
  agent: cliName,
418
648
  content: `${cliName.toUpperCase()} analysis completed (${Date.now() - startTime}ms)`,
419
- timestamp: Date.now()
649
+ timestamp: Date.now(),
650
+ sessionId: options.sessionId
420
651
  });
421
652
  }
422
- // Post-process Claude stream-json output if needed
653
+ // Post-process CLI output if needed
423
654
  let finalOutput = stdout;
424
655
  // If Claude was run with stream-json format, decode the NDJSON to extract text
425
656
  if (cliName === 'claude' && args.includes('--output-format') && args.includes('stream-json')) {
@@ -428,6 +659,13 @@ export class CLIAgentOrchestrator {
428
659
  finalOutput = decodedText;
429
660
  }
430
661
  }
662
+ // If Codex was run with --json flag, extract only the agent messages
663
+ if (cliName === 'codex' && args.includes('--json')) {
664
+ const decodedText = this.extractCodexAgentMessage(stdout);
665
+ if (decodedText) {
666
+ finalOutput = decodedText;
667
+ }
668
+ }
431
669
  // Fallback: If stdout is empty but stderr has content and exit was successful,
432
670
  // Claude might have written to stderr (common in non-TTY environments)
433
671
  if (!finalOutput.trim() && stderr && stderr.trim()) {
@@ -468,7 +706,8 @@ export class CLIAgentOrchestrator {
468
706
  type: 'agent_error',
469
707
  agent: cliName,
470
708
  content: `${cliName.toUpperCase()} failed: ${error instanceof Error ? error.message : String(error)}`,
471
- timestamp: Date.now()
709
+ timestamp: Date.now(),
710
+ sessionId: options.sessionId
472
711
  });
473
712
  }
474
713
  return {
@@ -509,16 +748,14 @@ export class CLIAgentOrchestrator {
509
748
  });
510
749
  }
511
750
  async executeCodex(userPrompt, systemPromptSpec, options = {}) {
512
- return this._executeCLI('codex', userPrompt, systemPromptSpec, { ...options, sandbox: true }, // Ensure sandbox is always true for Codex
513
- (userPrompt, systemPromptSpec, options) => {
751
+ return this._executeCLI('codex', userPrompt, systemPromptSpec, { ...options }, (userPrompt, systemPromptSpec, options) => {
514
752
  const combinedPrompt = `CONTEXT AND INSTRUCTIONS:\n${systemPromptSpec}\n\nANALYZE:\n${userPrompt}`;
515
753
  const args = ['exec'];
516
754
  // Use provided model or default to gpt-5
517
755
  const model = options.models?.codex || AVAILABLE_MODELS.codex.default;
518
756
  args.push('--model', model);
519
- if (options.sandbox) {
520
- args.push('--sandbox', 'read-only');
521
- }
757
+ // Add JSON flag to get structured output without verbose details
758
+ args.push('--json');
522
759
  // Use stdin for the prompt instead of argv to avoid ARG_MAX limits
523
760
  return {
524
761
  command: 'codex',
@@ -528,15 +765,11 @@ export class CLIAgentOrchestrator {
528
765
  });
529
766
  }
530
767
  async executeGemini(userPrompt, systemPromptSpec, options = {}) {
531
- return this._executeCLI('gemini', userPrompt, systemPromptSpec, { ...options, sandbox: true }, // Ensure sandbox is always true for Gemini
532
- (userPrompt, systemPromptSpec, options) => {
768
+ return this._executeCLI('gemini', userPrompt, systemPromptSpec, { ...options }, (userPrompt, systemPromptSpec, options) => {
533
769
  const args = [];
534
770
  // Use provided model or default to gemini-2.5-flash
535
771
  const modelName = options.models?.gemini || AVAILABLE_MODELS.gemini.default;
536
772
  args.push('--model', modelName);
537
- if (options.sandbox) {
538
- args.push('--sandbox');
539
- }
540
773
  const combinedPrompt = `${systemPromptSpec}\n\n${userPrompt}`;
541
774
  args.push(combinedPrompt);
542
775
  return {
@@ -561,9 +794,9 @@ export class CLIAgentOrchestrator {
561
794
  case 'claude':
562
795
  return await this.executeClaudeCode(userPrompt, systemPromptSpec, options);
563
796
  case 'codex':
564
- return await this.executeCodex(userPrompt, systemPromptSpec, { ...options, sandbox: true });
797
+ return await this.executeCodex(userPrompt, systemPromptSpec, options);
565
798
  case 'gemini':
566
- return await this.executeGemini(userPrompt, systemPromptSpec, { ...options, sandbox: true });
799
+ return await this.executeGemini(userPrompt, systemPromptSpec, options);
567
800
  default:
568
801
  throw new Error(`Unknown CLI: ${cli}`);
569
802
  }
@@ -581,8 +814,77 @@ export class CLIAgentOrchestrator {
581
814
  waitTime = Math.min(waitTime * 2, 5000); // Exponential backoff, max 5 seconds
582
815
  }
583
816
  }
584
- async executeBrutalistAnalysis(analysisType, targetPath, systemPromptSpec, context, options = {}) {
585
- const userPrompt = this.constructUserPrompt(analysisType, targetPath, context);
817
+ async executeCLIAgents(cliAgents, systemPrompt, userPrompt, options = {}) {
818
+ const responses = [];
819
+ for (const agent of cliAgents) {
820
+ if (['claude', 'codex', 'gemini'].includes(agent)) {
821
+ try {
822
+ const response = await this.executeCLIAgent(agent, systemPrompt, userPrompt, options);
823
+ responses.push(response);
824
+ }
825
+ catch (error) {
826
+ responses.push({
827
+ agent: agent,
828
+ success: false,
829
+ output: '',
830
+ error: error instanceof Error ? error.message : String(error),
831
+ executionTime: 0,
832
+ command: `${agent} execution failed`,
833
+ workingDirectory: options.workingDirectory || process.cwd(),
834
+ exitCode: -1
835
+ });
836
+ }
837
+ }
838
+ }
839
+ return responses;
840
+ }
841
+ async executeCLIAgent(agent, systemPrompt, userPrompt, options = {}) {
842
+ if (!['claude', 'codex', 'gemini'].includes(agent)) {
843
+ throw new Error(`Unsupported CLI agent: ${agent}`);
844
+ }
845
+ return await this.executeSingleCLI(agent, userPrompt, systemPrompt, options);
846
+ }
847
+ async executeBrutalistAnalysis(analysisType, primaryContent, systemPromptSpec, context, options = {}) {
848
+ // Debug logging for path validation logic - write to file to avoid MCP stdio interference
849
+ const fs = require('fs');
850
+ const debugLog = `/tmp/brutalist-debug-${Date.now()}.log`;
851
+ const logMessage = (msg) => {
852
+ try {
853
+ fs.appendFileSync(debugLog, `${new Date().toISOString()}: ${msg}\n`);
854
+ }
855
+ catch (e) {
856
+ // Ignore filesystem errors
857
+ }
858
+ };
859
+ logMessage(`🔧 VALIDATION DEBUG: analysisType="${analysisType}", primaryContent="${primaryContent}"`);
860
+ // Only validate filesystem paths for tools that actually operate on files/directories
861
+ const filesystemTools = ['codebase', 'file_structure', 'dependencies', 'git_history', 'test_coverage'];
862
+ logMessage(`🔧 VALIDATION DEBUG: filesystemTools.includes(analysisType)=${filesystemTools.includes(analysisType)}`);
863
+ logMessage(`🔧 VALIDATION DEBUG: primaryContent exists=${!!primaryContent}`);
864
+ logMessage(`🔧 VALIDATION DEBUG: primaryContent.trim() !== ''=${primaryContent ? primaryContent.trim() !== '' : false}`);
865
+ try {
866
+ if (filesystemTools.includes(analysisType) && primaryContent && primaryContent.trim() !== '') {
867
+ logMessage(`🔧 VALIDATION DEBUG: Calling validatePath for "${primaryContent}"`);
868
+ validatePath(primaryContent, 'targetPath');
869
+ }
870
+ else {
871
+ logMessage(`🔧 VALIDATION DEBUG: Skipping validatePath - not a filesystem tool`);
872
+ }
873
+ }
874
+ catch (error) {
875
+ logMessage(`🔧 VALIDATION DEBUG: validatePath failed with error: ${error}`);
876
+ throw new Error(`Security validation failed: ${error instanceof Error ? error.message : String(error)}`);
877
+ }
878
+ // Validate workingDirectory if provided
879
+ try {
880
+ if (options.workingDirectory) {
881
+ validatePath(options.workingDirectory, 'workingDirectory');
882
+ }
883
+ }
884
+ catch (error) {
885
+ throw new Error(`Security validation failed: ${error instanceof Error ? error.message : String(error)}`);
886
+ }
887
+ const userPrompt = this.constructUserPrompt(analysisType, primaryContent, context);
586
888
  // If preferred CLI is specified, use single CLI mode
587
889
  if (options.preferredCLI) {
588
890
  const selectedCLI = this.selectSingleCLI(options.preferredCLI, options.analysisType);
@@ -664,27 +966,27 @@ export class CLIAgentOrchestrator {
664
966
  }
665
967
  return synthesis.trim();
666
968
  }
667
- constructUserPrompt(analysisType, targetPath, context) {
969
+ constructUserPrompt(analysisType, primaryContent, context) {
668
970
  // Trust CLI tools to handle their own security
669
- const sanitizedTargetPath = targetPath;
971
+ const sanitizedContent = primaryContent;
670
972
  const sanitizedContext = context || 'No additional context provided';
671
973
  const prompts = {
672
- code: `Analyze the codebase at ${sanitizedTargetPath} for issues. Context: ${sanitizedContext}`,
673
- codebase: `Analyze the codebase directory at ${sanitizedTargetPath} for security vulnerabilities, performance issues, and architectural problems. Context: ${sanitizedContext}`,
674
- architecture: `Review the architecture: ${sanitizedTargetPath}. Find every scaling failure and cost explosion.`,
675
- idea: `Analyze this idea: ${sanitizedTargetPath}. Find where imagination fails to become reality.`,
676
- research: `Review this research: ${sanitizedTargetPath}. Find every methodological flaw and reproducibility issue.`,
677
- data: `Analyze this data/model: ${sanitizedTargetPath}. Find every overfitting issue, bias, and correlation fallacy.`,
678
- security: `Security audit of: ${sanitizedTargetPath}. Find every attack vector and vulnerability.`,
679
- product: `Product review: ${sanitizedTargetPath}. Find every UX disaster and adoption barrier.`,
680
- infrastructure: `Infrastructure review: ${sanitizedTargetPath}. Find every single point of failure.`,
681
- debate: `Debate topic: ${sanitizedTargetPath}. Take opposing positions and argue until truth emerges.`,
682
- fileStructure: `Analyze the directory structure at ${sanitizedTargetPath}. Find organizational disasters and naming failures.`,
683
- dependencies: `Analyze dependencies at ${sanitizedTargetPath}. Find version conflicts and security vulnerabilities.`,
684
- gitHistory: `Analyze git history at ${sanitizedTargetPath}. Find commit disasters and workflow failures.`,
685
- testCoverage: `Analyze test coverage at ${sanitizedTargetPath}. Find testing gaps and quality issues.`
974
+ code: `Analyze the codebase at ${sanitizedContent} for issues. Context: ${sanitizedContext}`,
975
+ codebase: `Analyze the codebase directory at ${sanitizedContent} for security vulnerabilities, performance issues, and architectural problems. Context: ${sanitizedContext}`,
976
+ architecture: `Review the architecture: ${sanitizedContent}. Find every scaling failure and cost explosion.`,
977
+ idea: `Analyze this idea: ${sanitizedContent}. Find where imagination fails to become reality.`,
978
+ research: `Review this research: ${sanitizedContent}. Find every methodological flaw and reproducibility issue.`,
979
+ data: `Analyze this data/model: ${sanitizedContent}. Find every overfitting issue, bias, and correlation fallacy.`,
980
+ security: `Security audit of: ${sanitizedContent}. Find every attack vector and vulnerability.`,
981
+ product: `Product review: ${sanitizedContent}. Find every UX disaster and adoption barrier.`,
982
+ infrastructure: `Infrastructure review: ${sanitizedContent}. Find every single point of failure.`,
983
+ debate: `Debate topic: ${sanitizedContent}. Take opposing positions and argue until truth emerges.`,
984
+ fileStructure: `Analyze the directory structure at ${sanitizedContent}. Find organizational disasters and naming failures.`,
985
+ dependencies: `Analyze dependencies at ${sanitizedContent}. Find version conflicts and security vulnerabilities.`,
986
+ gitHistory: `Analyze git history at ${sanitizedContent}. Find commit disasters and workflow failures.`,
987
+ testCoverage: `Analyze test coverage at ${sanitizedContent}. Find testing gaps and quality issues.`
686
988
  };
687
- const specificPrompt = prompts[analysisType] || `Analyze ${sanitizedTargetPath} for ${analysisType} issues.`;
989
+ const specificPrompt = prompts[analysisType] || `Analyze ${sanitizedContent} for ${analysisType} issues.`;
688
990
  return `${specificPrompt} ${context ? `Context: ${sanitizedContext}` : ''}`;
689
991
  }
690
992
  }