@brutalist/mcp 0.8.1 → 1.0.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.
Files changed (61) hide show
  1. package/README.md +34 -7
  2. package/dist/brutalist-server.d.ts +55 -16
  3. package/dist/brutalist-server.d.ts.map +1 -1
  4. package/dist/brutalist-server.js +550 -732
  5. package/dist/brutalist-server.js.map +1 -1
  6. package/dist/cli-agents.d.ts +9 -7
  7. package/dist/cli-agents.d.ts.map +1 -1
  8. package/dist/cli-agents.js +290 -202
  9. package/dist/cli-agents.js.map +1 -1
  10. package/dist/domains/argument-space.d.ts +12 -3
  11. package/dist/domains/argument-space.d.ts.map +1 -1
  12. package/dist/domains/argument-space.js +30 -23
  13. package/dist/domains/argument-space.js.map +1 -1
  14. package/dist/domains/critique-domain.d.ts +12 -0
  15. package/dist/domains/critique-domain.d.ts.map +1 -1
  16. package/dist/domains/critique-domain.js +12 -1
  17. package/dist/domains/critique-domain.js.map +1 -1
  18. package/dist/formatting/response-formatter.d.ts +43 -0
  19. package/dist/formatting/response-formatter.d.ts.map +1 -0
  20. package/dist/formatting/response-formatter.js +277 -0
  21. package/dist/formatting/response-formatter.js.map +1 -0
  22. package/dist/generators/tool-generator.d.ts.map +1 -1
  23. package/dist/generators/tool-generator.js +8 -6
  24. package/dist/generators/tool-generator.js.map +1 -1
  25. package/dist/handlers/tool-handler.d.ts +33 -0
  26. package/dist/handlers/tool-handler.d.ts.map +1 -0
  27. package/dist/handlers/tool-handler.js +307 -0
  28. package/dist/handlers/tool-handler.js.map +1 -0
  29. package/dist/registry/argument-spaces.js +17 -17
  30. package/dist/registry/argument-spaces.js.map +1 -1
  31. package/dist/registry/domains.d.ts +10 -0
  32. package/dist/registry/domains.d.ts.map +1 -1
  33. package/dist/registry/domains.js +153 -11
  34. package/dist/registry/domains.js.map +1 -1
  35. package/dist/system-prompts.d.ts +8 -0
  36. package/dist/system-prompts.d.ts.map +1 -0
  37. package/dist/system-prompts.js +596 -0
  38. package/dist/system-prompts.js.map +1 -0
  39. package/dist/tool-definitions.d.ts +20 -1
  40. package/dist/tool-definitions.d.ts.map +1 -1
  41. package/dist/tool-definitions.js +42 -213
  42. package/dist/tool-definitions.js.map +1 -1
  43. package/dist/tool-router.d.ts +12 -0
  44. package/dist/tool-router.d.ts.map +1 -0
  45. package/dist/tool-router.js +59 -0
  46. package/dist/tool-router.js.map +1 -0
  47. package/dist/transport/http-transport.d.ts +40 -0
  48. package/dist/transport/http-transport.d.ts.map +1 -0
  49. package/dist/transport/http-transport.js +182 -0
  50. package/dist/transport/http-transport.js.map +1 -0
  51. package/dist/types/brutalist.d.ts +1 -0
  52. package/dist/types/brutalist.d.ts.map +1 -1
  53. package/dist/types/tool-config.d.ts +4 -3
  54. package/dist/types/tool-config.d.ts.map +1 -1
  55. package/dist/types/tool-config.js +7 -6
  56. package/dist/types/tool-config.js.map +1 -1
  57. package/dist/utils.d.ts +1 -1
  58. package/dist/utils.d.ts.map +1 -1
  59. package/dist/utils.js +13 -6
  60. package/dist/utils.js.map +1 -1
  61. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  import { spawn, exec } from 'child_process';
2
- import { realpathSync } from 'fs';
2
+ import { promises as fs, realpathSync } from 'fs';
3
3
  import { promisify } from 'util';
4
4
  import { logger } from './logger.js';
5
5
  // Configurable timeouts and limits
@@ -17,19 +17,37 @@ const activeProcesses = new Map();
17
17
  export const AVAILABLE_MODELS = {
18
18
  claude: {
19
19
  default: undefined, // Uses user's configured model (respects preferences)
20
- aliases: ['opus', 'sonnet', 'haiku'],
21
- full: ['claude-opus-4-1-20250805', 'claude-sonnet-4-20250514'],
22
- recommended: 'opus' // Highest capacity Claude model
20
+ aliases: ['opus', 'sonnet', 'haiku', 'opus-4.5', 'sonnet-4.5'],
21
+ full: [
22
+ 'claude-opus-4-5-20251101',
23
+ 'claude-sonnet-4-5-20250929',
24
+ 'claude-haiku-4-5-20251001',
25
+ 'claude-opus-4-1-20250805'
26
+ ],
27
+ recommended: 'claude-opus-4-5-20251101' // Highest capacity Claude model
23
28
  },
24
29
  codex: {
25
30
  default: undefined, // Uses Codex CLI's default model (stays current automatically)
26
- models: ['gpt-5.1-codex-max', 'gpt-5.1-codex', 'gpt-5.1-codex-mini', 'gpt-5-codex', 'gpt-5', 'o4-mini'],
31
+ models: [
32
+ 'gpt-5.1-codex-max',
33
+ 'gpt-5.1-codex',
34
+ 'gpt-5.1',
35
+ 'gpt-5-thinking-pro',
36
+ 'gpt-5',
37
+ 'gpt-5-mini',
38
+ 'o4-mini'
39
+ ],
27
40
  recommended: 'gpt-5.1-codex-max' // Current frontier model with compaction
28
41
  },
29
42
  gemini: {
30
43
  default: undefined, // Uses Gemini CLI's default model (stays current automatically)
31
- models: ['gemini-3-pro-preview', 'gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-2.5-flash-lite'],
32
- recommended: 'gemini-3-pro-preview' // Current #1 on LMArena
44
+ models: [
45
+ 'gemini-3-pro',
46
+ 'gemini-2.5-pro',
47
+ 'gemini-2.5-flash',
48
+ 'gemini-2.5-flash-lite'
49
+ ],
50
+ recommended: 'gemini-3-pro' // Current #1 on LMArena
33
51
  }
34
52
  };
35
53
  // Security utilities for CLI execution
@@ -72,6 +90,32 @@ function validatePath(path, name) {
72
90
  throw new Error(`Invalid ${name}: ${error instanceof Error ? error.message : String(error)}`);
73
91
  }
74
92
  }
93
+ // Async version of validatePath for use in async contexts
94
+ async function asyncValidatePath(path, name) {
95
+ if (!path) {
96
+ throw new Error(`${name} cannot be empty`);
97
+ }
98
+ // Check for null bytes
99
+ if (path.includes('\0')) {
100
+ throw new Error(`${name} contains null byte`);
101
+ }
102
+ // Check for dangerous path traversal patterns
103
+ if (path.includes('../') || path.includes('..\\') || path.includes('/..') || path.includes('\\..')) {
104
+ throw new Error(`${name} contains path traversal attempt: ${path}`);
105
+ }
106
+ // Check path depth to prevent deeply nested attacks
107
+ const depth = path.split('/').length - 1;
108
+ if (depth > MAX_PATH_DEPTH) {
109
+ throw new Error(`${name} exceeds maximum depth: ${depth} > ${MAX_PATH_DEPTH}`);
110
+ }
111
+ // Canonicalize the path (this also validates it exists and resolves symlinks)
112
+ try {
113
+ return await fs.realpath(path);
114
+ }
115
+ catch (error) {
116
+ throw new Error(`Invalid ${name}: ${error instanceof Error ? error.message : String(error)}`);
117
+ }
118
+ }
75
119
  // Create secure environment for CLI processes
76
120
  function createSecureEnvironment() {
77
121
  // Minimal environment whitelist
@@ -319,6 +363,30 @@ async function spawnAsync(command, args, options = {}) {
319
363
  }
320
364
  });
321
365
  }
366
+ const CLI_BUILDER_CONFIGS = {
367
+ claude: {
368
+ command: 'claude',
369
+ defaultArgs: ['--print'],
370
+ modelArgName: '--model',
371
+ mpcEnvCleanup: ['CLAUDE_MCP_CONFIG', 'MCP_ENABLED', 'CLAUDECODE', 'CLAUDE_CODE_ENTRYPOINT'],
372
+ streamingArgs: (opts) => opts.progressToken ? ['--output-format', 'stream-json', '--verbose'] : []
373
+ },
374
+ codex: {
375
+ command: 'codex',
376
+ defaultArgs: ['exec', '--sandbox', 'read-only'],
377
+ modelArgName: '--model',
378
+ jsonFlag: '--json',
379
+ mpcEnvCleanup: ['CODEX_MCP_CONFIG', 'MCP_ENABLED'],
380
+ promptWrapper: (sys, user) => `${sys}\n\n${user}\n\nUse your shell tools to read files (cat, ls, find, grep, head, etc.) and analyze the codebase. You ARE allowed to run read-only commands. Explore the directory structure, read relevant source files, and provide a comprehensive brutal analysis based on what you find.`
381
+ },
382
+ gemini: {
383
+ command: 'gemini',
384
+ defaultArgs: [],
385
+ modelArgName: '--model',
386
+ envExtras: { TERM: 'dumb', NO_COLOR: '1', CI: 'true' },
387
+ mpcEnvCleanup: ['GEMINI_MCP_CONFIG', 'MCP_ENABLED']
388
+ }
389
+ };
322
390
  export class CLIAgentOrchestrator {
323
391
  defaultTimeout = 1800000; // 30 minutes - complex codebases need time
324
392
  defaultWorkingDir = process.cwd();
@@ -366,50 +434,116 @@ export class CLIAgentOrchestrator {
366
434
  return chunk;
367
435
  }
368
436
  }
437
+ // Parse NDJSON with proper JSON boundary detection
438
+ // Handles JSON objects that contain embedded newlines without data loss
439
+ parseNDJSON(input) {
440
+ if (!input || !input.trim()) {
441
+ return [];
442
+ }
443
+ const results = [];
444
+ let depth = 0;
445
+ let inString = false;
446
+ let escape = false;
447
+ let start = 0;
448
+ for (let i = 0; i < input.length; i++) {
449
+ const char = input[i];
450
+ // Handle escape sequences
451
+ if (escape) {
452
+ escape = false;
453
+ continue;
454
+ }
455
+ if (char === '\\') {
456
+ escape = true;
457
+ continue;
458
+ }
459
+ // Track string boundaries
460
+ if (char === '"') {
461
+ inString = !inString;
462
+ continue;
463
+ }
464
+ // Only count braces/brackets outside of strings
465
+ if (inString)
466
+ continue;
467
+ // Track depth
468
+ if (char === '{' || char === '[') {
469
+ depth++;
470
+ }
471
+ else if (char === '}' || char === ']') {
472
+ depth--;
473
+ // When depth returns to 0, we've found a complete JSON object
474
+ if (depth === 0) {
475
+ const jsonStr = input.slice(start, i + 1).trim();
476
+ if (jsonStr) {
477
+ try {
478
+ const parsed = JSON.parse(jsonStr);
479
+ results.push(parsed);
480
+ }
481
+ catch (e) {
482
+ // Log unparseable segments (not silent)
483
+ logger.warn(`Failed to parse JSON segment at position ${start}-${i + 1}:`, {
484
+ preview: jsonStr.substring(0, 100),
485
+ error: e instanceof Error ? e.message : String(e)
486
+ });
487
+ }
488
+ }
489
+ // Move start pointer past this object and any whitespace
490
+ start = i + 1;
491
+ while (start < input.length && /\s/.test(input[start])) {
492
+ start++;
493
+ }
494
+ i = start - 1; // Will be incremented by loop
495
+ }
496
+ }
497
+ }
498
+ // Warn about incomplete JSON at end of input
499
+ if (start < input.length) {
500
+ const remaining = input.slice(start).trim();
501
+ if (remaining) {
502
+ logger.warn(`Incomplete JSON at end of input:`, {
503
+ preview: remaining.substring(0, 100)
504
+ });
505
+ }
506
+ }
507
+ return results;
508
+ }
369
509
  // Decode Claude's stream-json NDJSON output into plain text
370
510
  decodeClaudeStreamJson(ndjsonOutput) {
371
511
  if (!ndjsonOutput || !ndjsonOutput.trim()) {
372
512
  return '';
373
513
  }
374
514
  const textParts = [];
375
- const lines = ndjsonOutput.split('\n');
376
- for (const line of lines) {
377
- if (!line.trim())
515
+ const events = this.parseNDJSON(ndjsonOutput);
516
+ for (const event of events) {
517
+ if (typeof event !== 'object' || event === null)
378
518
  continue;
379
- try {
380
- const event = JSON.parse(line);
381
- // Handle different event types from Claude's stream-json format
382
- if (event.type === 'message' && event.message?.content) {
383
- // Full message event
384
- const content = event.message.content;
385
- if (Array.isArray(content)) {
386
- for (const item of content) {
387
- if (item.type === 'text' && item.text) {
388
- textParts.push(item.text);
389
- }
519
+ const typedEvent = event;
520
+ // Handle different event types from Claude's stream-json format
521
+ if (typedEvent.type === 'message' && typedEvent.message?.content) {
522
+ // Full message event
523
+ const content = typedEvent.message.content;
524
+ if (Array.isArray(content)) {
525
+ for (const item of content) {
526
+ if (item.type === 'text' && item.text) {
527
+ textParts.push(item.text);
390
528
  }
391
529
  }
392
530
  }
393
- else if (event.type === 'content_block_delta' && event.delta?.text) {
394
- // Incremental text delta
395
- textParts.push(event.delta.text);
396
- }
397
- else if (event.type === 'assistant' && event.message?.content) {
398
- // Assistant message format (same as parseClaudeStreamOutput)
399
- const content = event.message.content;
400
- if (Array.isArray(content)) {
401
- for (const item of content) {
402
- if (item.type === 'text' && item.text) {
403
- textParts.push(item.text);
404
- }
531
+ }
532
+ else if (typedEvent.type === 'content_block_delta' && typedEvent.delta?.text) {
533
+ // Incremental text delta
534
+ textParts.push(typedEvent.delta.text);
535
+ }
536
+ else if (typedEvent.type === 'assistant' && typedEvent.message?.content) {
537
+ // Assistant message format (same as parseClaudeStreamOutput)
538
+ const content = typedEvent.message.content;
539
+ if (Array.isArray(content)) {
540
+ for (const item of content) {
541
+ if (item.type === 'text' && item.text) {
542
+ textParts.push(item.text);
405
543
  }
406
544
  }
407
545
  }
408
546
  }
409
- catch {
410
- // Skip invalid JSON lines
411
- continue;
412
- }
413
547
  }
414
548
  return textParts.join('');
415
549
  }
@@ -420,32 +554,25 @@ export class CLIAgentOrchestrator {
420
554
  return '';
421
555
  }
422
556
  const agentMessages = [];
423
- const lines = jsonOutput.split('\n');
424
- logger.debug(`extractCodexAgentMessage: processing ${lines.length} lines`);
425
- for (const line of lines) {
426
- if (!line.trim())
557
+ const events = this.parseNDJSON(jsonOutput);
558
+ logger.debug(`extractCodexAgentMessage: processing ${events.length} JSON events`);
559
+ for (const event of events) {
560
+ if (typeof event !== 'object' || event === null)
427
561
  continue;
428
- try {
429
- const event = JSON.parse(line);
430
- logger.debug(`extractCodexAgentMessage: parsed event type=${event.type}, item.type=${event.item?.type}`);
431
- // Codex --json outputs events with structure: {"type":"item.completed","item":{...}}
432
- // Only extract agent_message type - this is the actual response
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
- logger.info(`✅ extractCodexAgentMessage: found agent_message with ${event.item.text.length} chars`);
437
- agentMessages.push(event.item.text);
438
- }
439
- // Skip all other types:
440
- // - reasoning: internal thinking steps
441
- // - command_execution: file reads, bash commands
442
- // - error: will be in stderr
562
+ const typedEvent = event;
563
+ logger.debug(`extractCodexAgentMessage: parsed event type=${typedEvent.type}, item.type=${typedEvent.item?.type}`);
564
+ // Codex --json outputs events with structure: {"type":"item.completed","item":{...}}
565
+ // Only extract agent_message type - this is the actual response
566
+ if (typedEvent.type === 'item.completed' && typedEvent.item) {
567
+ if (typedEvent.item.type === 'agent_message' && typedEvent.item.text) {
568
+ // Agent's actual response text
569
+ logger.info(`✅ extractCodexAgentMessage: found agent_message with ${typedEvent.item.text.length} chars`);
570
+ agentMessages.push(typedEvent.item.text);
443
571
  }
444
- }
445
- catch (e) {
446
- // Skip non-JSON lines (config output, prompts, etc.)
447
- logger.debug(`extractCodexAgentMessage: failed to parse line: ${line.substring(0, 50)}`);
448
- continue;
572
+ // Skip all other types:
573
+ // - reasoning: internal thinking steps
574
+ // - command_execution: file reads, bash commands
575
+ // - error: will be in stderr
449
576
  }
450
577
  }
451
578
  const result = agentMessages.join('\n\n').trim();
@@ -499,6 +626,49 @@ export class CLIAgentOrchestrator {
499
626
  buffer.lastFlush = now;
500
627
  }
501
628
  }
629
+ buildCLICommand(cli, userPrompt, systemPrompt, options) {
630
+ const config = CLI_BUILDER_CONFIGS[cli];
631
+ // Build args
632
+ const args = [...config.defaultArgs];
633
+ const model = options.models?.[cli] || AVAILABLE_MODELS[cli].default;
634
+ if (model) {
635
+ args.push(config.modelArgName, model);
636
+ }
637
+ if (config.jsonFlag && process.env.CODEX_USE_JSON !== 'false') {
638
+ args.push(config.jsonFlag);
639
+ }
640
+ if (config.streamingArgs) {
641
+ args.push(...config.streamingArgs(options));
642
+ }
643
+ // Build prompt
644
+ const combinedPrompt = config.promptWrapper
645
+ ? config.promptWrapper(systemPrompt, userPrompt)
646
+ : `${systemPrompt}\n\n${userPrompt}`;
647
+ // Build secure env
648
+ const secureEnv = createSecureEnvironment();
649
+ // Add CLI-specific env extras
650
+ if (config.envExtras) {
651
+ Object.assign(secureEnv, config.envExtras);
652
+ }
653
+ // Add required API key
654
+ const apiKeyMap = {
655
+ claude: ['ANTHROPIC_API_KEY'],
656
+ codex: ['OPENAI_API_KEY'],
657
+ gemini: ['GOOGLE_API_KEY', 'GEMINI_API_KEY']
658
+ };
659
+ for (const key of apiKeyMap[cli]) {
660
+ if (process.env[key])
661
+ secureEnv[key] = process.env[key];
662
+ }
663
+ // Clean up MPC env vars that could cause deadlock
664
+ if (config.mpcEnvCleanup) {
665
+ for (const envVar of config.mpcEnvCleanup) {
666
+ delete secureEnv[envVar];
667
+ }
668
+ }
669
+ secureEnv.BRUTALIST_SUBPROCESS = '1';
670
+ return { command: config.command, args, input: combinedPrompt, env: secureEnv };
671
+ }
502
672
  async detectCLIContext() {
503
673
  // Return cached context if still valid
504
674
  if (this.cliContextCached && Date.now() - this.cliContextCacheTime < this.CLI_CACHE_TTL) {
@@ -694,103 +864,13 @@ export class CLIAgentOrchestrator {
694
864
  }
695
865
  }
696
866
  async executeClaudeCode(userPrompt, systemPromptSpec, options = {}) {
697
- return this._executeCLI('claude', userPrompt, systemPromptSpec, options, (userPrompt, systemPromptSpec, options) => {
698
- const combinedPrompt = `${systemPromptSpec}\n\n${userPrompt}`;
699
- const args = ['--print'];
700
- // Enable streaming for real-time progress if progress notifications are enabled
701
- if (options.progressToken) {
702
- args.push('--output-format', 'stream-json', '--verbose');
703
- }
704
- // Use provided model or let Claude use its default
705
- const model = options.models?.claude || AVAILABLE_MODELS.claude.default;
706
- if (model) {
707
- args.push('--model', model);
708
- }
709
- // Use stdin to avoid MAX_ARG_LENGTH limit (4096 chars)
710
- // Claude --print can read from stdin when no positional argument is provided
711
- // DEFENSIVE: Disable MCP and Claude Code integration to prevent stdio deadlock
712
- // When Claude CLI runs with MCP enabled or detects Claude Code context,
713
- // it tries to communicate over stdio which conflicts with our stdin/stdout usage
714
- const cleanEnv = { ...process.env };
715
- delete cleanEnv.CLAUDE_MCP_CONFIG;
716
- delete cleanEnv.MCP_ENABLED;
717
- delete cleanEnv.CLAUDECODE;
718
- delete cleanEnv.CLAUDE_CODE_ENTRYPOINT;
719
- return {
720
- command: 'claude',
721
- args,
722
- input: combinedPrompt,
723
- env: {
724
- ...cleanEnv,
725
- BRUTALIST_SUBPROCESS: '1' // Mark this as a brutalist-spawned subprocess
726
- }
727
- };
728
- });
867
+ return this._executeCLI('claude', userPrompt, systemPromptSpec, options, (user, sys, opts) => this.buildCLICommand('claude', user, sys, opts));
729
868
  }
730
869
  async executeCodex(userPrompt, systemPromptSpec, options = {}) {
731
- return this._executeCLI('codex', userPrompt, systemPromptSpec, { ...options }, (userPrompt, systemPromptSpec, options) => {
732
- // Instruct Codex to analyze immediately in one shot without waiting for approval
733
- const combinedPrompt = `${systemPromptSpec}\n\n${userPrompt}\n\nExecute the complete analysis now in a single response without creating a plan first or waiting for input. Provide your full findings immediately.`;
734
- const args = ['exec'];
735
- // Use provided model or let Codex use its default
736
- const model = options.models?.codex || AVAILABLE_MODELS.codex.default;
737
- if (model) {
738
- args.push('--model', model);
739
- }
740
- // OPTIONAL: Use --json flag to get structured output (can be disabled for compatibility)
741
- if (process.env.CODEX_USE_JSON !== 'false') {
742
- args.push('--json');
743
- }
744
- // DEFENSIVE: Disable MCP if Codex supports it (currently no known MCP support)
745
- // This prevents potential stdio deadlock if Codex adds MCP in the future
746
- // Note: Codex CLI doesn't currently have documented MCP config flags
747
- // Use stdin for the prompt instead of argv to avoid ARG_MAX limits
748
- // Create clean environment without MCP-related variables
749
- const cleanEnv = { ...process.env };
750
- delete cleanEnv.CODEX_MCP_CONFIG;
751
- delete cleanEnv.MCP_ENABLED;
752
- return {
753
- command: 'codex',
754
- args,
755
- input: combinedPrompt,
756
- env: {
757
- ...cleanEnv,
758
- BRUTALIST_SUBPROCESS: '1' // Mark this as a brutalist-spawned subprocess
759
- }
760
- };
761
- });
870
+ return this._executeCLI('codex', userPrompt, systemPromptSpec, options, (user, sys, opts) => this.buildCLICommand('codex', user, sys, opts));
762
871
  }
763
872
  async executeGemini(userPrompt, systemPromptSpec, options = {}) {
764
- return this._executeCLI('gemini', userPrompt, systemPromptSpec, { ...options }, (userPrompt, systemPromptSpec, options) => {
765
- const args = [];
766
- // Use provided model or let Gemini use its default
767
- const modelName = options.models?.gemini || AVAILABLE_MODELS.gemini.default;
768
- if (modelName) {
769
- args.push('--model', modelName);
770
- }
771
- // DEFENSIVE: Disable MCP if Gemini supports it (currently no known MCP support)
772
- // This prevents potential stdio deadlock if Gemini adds MCP in the future
773
- // Note: Gemini CLI doesn't currently have documented MCP config flags
774
- const combinedPrompt = `${systemPromptSpec}\n\n${userPrompt}`;
775
- // Use stdin to avoid MAX_ARG_LENGTH limit (4096 chars)
776
- // Gemini CLI can read from stdin instead of positional argument
777
- // Create clean environment without MCP-related variables
778
- const cleanEnv = { ...process.env };
779
- delete cleanEnv.GEMINI_MCP_CONFIG;
780
- delete cleanEnv.MCP_ENABLED;
781
- return {
782
- command: 'gemini',
783
- args: args,
784
- input: combinedPrompt, // Pass prompt via stdin instead of args
785
- env: {
786
- ...cleanEnv,
787
- TERM: 'dumb',
788
- NO_COLOR: '1',
789
- CI: 'true',
790
- BRUTALIST_SUBPROCESS: '1' // Mark this as a brutalist-spawned subprocess
791
- }
792
- };
793
- });
873
+ return this._executeCLI('gemini', userPrompt, systemPromptSpec, options, (user, sys, opts) => this.buildCLICommand('gemini', user, sys, opts));
794
874
  }
795
875
  async executeSingleCLI(cli, userPrompt, systemPromptSpec, options = {}) {
796
876
  // Wait for available slot to prevent resource exhaustion
@@ -823,28 +903,33 @@ export class CLIAgentOrchestrator {
823
903
  }
824
904
  }
825
905
  async executeCLIAgents(cliAgents, systemPrompt, userPrompt, options = {}) {
826
- const responses = [];
827
- for (const agent of cliAgents) {
828
- if (['claude', 'codex', 'gemini'].includes(agent)) {
829
- try {
830
- const response = await this.executeCLIAgent(agent, systemPrompt, userPrompt, options);
831
- responses.push(response);
832
- }
833
- catch (error) {
834
- responses.push({
835
- agent: agent,
836
- success: false,
837
- output: '',
838
- error: error instanceof Error ? error.message : String(error),
839
- executionTime: 0,
840
- command: `${agent} execution failed`,
841
- workingDirectory: options.workingDirectory || process.cwd(),
842
- exitCode: -1
843
- });
844
- }
845
- }
906
+ // Filter to valid CLI agents
907
+ const validAgents = cliAgents.filter(agent => ['claude', 'codex', 'gemini'].includes(agent));
908
+ if (validAgents.length === 0) {
909
+ return [];
846
910
  }
847
- return responses;
911
+ // Execute all CLIs in parallel with Promise.allSettled
912
+ const promises = validAgents.map(async (agent) => {
913
+ try {
914
+ return await this.executeCLIAgent(agent, systemPrompt, userPrompt, options);
915
+ }
916
+ catch (error) {
917
+ return {
918
+ agent,
919
+ success: false,
920
+ output: '',
921
+ error: error instanceof Error ? error.message : String(error),
922
+ executionTime: 0,
923
+ command: `${agent} execution failed`,
924
+ workingDirectory: options.workingDirectory || process.cwd(),
925
+ exitCode: -1
926
+ };
927
+ }
928
+ });
929
+ const results = await Promise.allSettled(promises);
930
+ return results
931
+ .filter((result) => result.status === 'fulfilled')
932
+ .map(result => result.value);
848
933
  }
849
934
  async executeCLIAgent(agent, systemPrompt, userPrompt, options = {}) {
850
935
  if (!['claude', 'codex', 'gemini'].includes(agent)) {
@@ -854,12 +939,13 @@ export class CLIAgentOrchestrator {
854
939
  }
855
940
  async executeBrutalistAnalysis(analysisType, primaryContent, systemPromptSpec, context, options = {}) {
856
941
  // Only validate filesystem paths for tools that actually operate on files/directories
857
- const filesystemTools = ['codebase', 'file_structure', 'dependencies', 'git_history', 'test_coverage'];
942
+ // NOTE: Must match BrutalistPromptType values (camelCase)
943
+ const filesystemTools = ['codebase', 'fileStructure', 'dependencies', 'gitHistory', 'testCoverage'];
858
944
  logger.debug(`Validation check: analysisType="${analysisType}", isFilesystemTool=${filesystemTools.includes(analysisType)}`);
859
945
  try {
860
946
  if (filesystemTools.includes(analysisType) && primaryContent && primaryContent.trim() !== '') {
861
947
  logger.debug(`Validating path: "${primaryContent}"`);
862
- validatePath(primaryContent, 'targetPath');
948
+ await asyncValidatePath(primaryContent, 'targetPath');
863
949
  }
864
950
  }
865
951
  catch (error) {
@@ -869,41 +955,43 @@ export class CLIAgentOrchestrator {
869
955
  // Validate workingDirectory if provided
870
956
  try {
871
957
  if (options.workingDirectory) {
872
- validatePath(options.workingDirectory, 'workingDirectory');
958
+ await asyncValidatePath(options.workingDirectory, 'workingDirectory');
873
959
  }
874
960
  }
875
961
  catch (error) {
876
962
  throw new Error(`Security validation failed: ${error instanceof Error ? error.message : String(error)}`);
877
963
  }
878
964
  const userPrompt = this.constructUserPrompt(analysisType, primaryContent, context);
879
- // If preferred CLI is specified, use single CLI mode
880
- if (options.preferredCLI) {
881
- const selectedCLI = this.selectSingleCLI(options.preferredCLI, analysisType // Use the direct parameter, not options.analysisType
882
- );
883
- logger.info(`✅ Using preferred CLI: ${selectedCLI}`);
884
- const response = await this.executeSingleCLI(selectedCLI, userPrompt, systemPromptSpec, options);
885
- return [{
886
- ...response,
887
- selectionMethod: 'user-specified',
888
- analysisType
889
- }];
890
- }
891
- // Multi-CLI execution (default behavior)
892
- logger.info(`🚀 Executing multi-CLI analysis`);
893
- // Use all available CLIs - spawning separate processes is fine
894
- let availableCLIs = [...this.cliContext.availableCLIs];
895
- logger.info(`📋 Using all available CLIs: ${availableCLIs.join(', ')}`);
896
- if (availableCLIs.length === 0) {
965
+ // Determine which CLIs to use
966
+ let clisToUse;
967
+ if (options.clis && options.clis.length > 0) {
968
+ // User specified which CLIs to use - validate they're available
969
+ const unavailable = options.clis.filter(cli => !this.cliContext.availableCLIs.includes(cli));
970
+ if (unavailable.length > 0) {
971
+ throw new Error(`Requested CLIs not available: ${unavailable.join(', ')}. ` +
972
+ `Available: ${this.cliContext.availableCLIs.join(', ')}`);
973
+ }
974
+ // Deduplicate
975
+ clisToUse = [...new Set(options.clis)];
976
+ logger.info(`🎯 Using user-specified CLIs: ${clisToUse.join(', ')}`);
977
+ }
978
+ else {
979
+ // Default: use all available CLIs
980
+ clisToUse = [...this.cliContext.availableCLIs];
981
+ logger.info(`📋 Using all available CLIs: ${clisToUse.join(', ')}`);
982
+ }
983
+ if (clisToUse.length === 0) {
897
984
  throw new Error('No CLI agents available for analysis');
898
985
  }
899
- logger.info(`📊 Available CLIs: ${availableCLIs.join(', ')}`);
900
- // Execute all available CLIs in parallel with allSettled for better error handling
901
- const promises = availableCLIs.map(async (cli) => {
986
+ const selectionMethod = options.clis ? 'user-specified' : 'all-available';
987
+ logger.info(`📊 Executing ${clisToUse.length} CLI(s): ${clisToUse.join(', ')} (${selectionMethod})`);
988
+ // Execute selected CLIs in parallel with allSettled for better error handling
989
+ const promises = clisToUse.map(async (cli) => {
902
990
  try {
903
991
  const response = await this.executeSingleCLI(cli, userPrompt, systemPromptSpec, options);
904
992
  return {
905
993
  ...response,
906
- selectionMethod: 'multi-cli',
994
+ selectionMethod,
907
995
  analysisType
908
996
  };
909
997
  }
@@ -915,7 +1003,7 @@ export class CLIAgentOrchestrator {
915
1003
  output: '',
916
1004
  error: error instanceof Error ? error.message : String(error),
917
1005
  executionTime: 0,
918
- selectionMethod: 'multi-cli',
1006
+ selectionMethod,
919
1007
  analysisType
920
1008
  };
921
1009
  }
@@ -925,7 +1013,7 @@ export class CLIAgentOrchestrator {
925
1013
  const responses = results
926
1014
  .filter(result => result.status === 'fulfilled')
927
1015
  .map(result => result.value);
928
- logger.info(`✅ Multi-CLI analysis complete: ${responses.filter(r => r.success).length}/${responses.length} successful`);
1016
+ logger.info(`✅ CLI analysis complete: ${responses.filter(r => r.success).length}/${responses.length} successful`);
929
1017
  return responses;
930
1018
  }
931
1019
  synthesizeBrutalistFeedback(responses, analysisType) {