@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.
- package/README.md +34 -7
- package/dist/brutalist-server.d.ts +55 -16
- package/dist/brutalist-server.d.ts.map +1 -1
- package/dist/brutalist-server.js +550 -732
- package/dist/brutalist-server.js.map +1 -1
- package/dist/cli-agents.d.ts +9 -7
- package/dist/cli-agents.d.ts.map +1 -1
- package/dist/cli-agents.js +290 -202
- package/dist/cli-agents.js.map +1 -1
- package/dist/domains/argument-space.d.ts +12 -3
- package/dist/domains/argument-space.d.ts.map +1 -1
- package/dist/domains/argument-space.js +30 -23
- package/dist/domains/argument-space.js.map +1 -1
- package/dist/domains/critique-domain.d.ts +12 -0
- package/dist/domains/critique-domain.d.ts.map +1 -1
- package/dist/domains/critique-domain.js +12 -1
- package/dist/domains/critique-domain.js.map +1 -1
- package/dist/formatting/response-formatter.d.ts +43 -0
- package/dist/formatting/response-formatter.d.ts.map +1 -0
- package/dist/formatting/response-formatter.js +277 -0
- package/dist/formatting/response-formatter.js.map +1 -0
- package/dist/generators/tool-generator.d.ts.map +1 -1
- package/dist/generators/tool-generator.js +8 -6
- package/dist/generators/tool-generator.js.map +1 -1
- package/dist/handlers/tool-handler.d.ts +33 -0
- package/dist/handlers/tool-handler.d.ts.map +1 -0
- package/dist/handlers/tool-handler.js +307 -0
- package/dist/handlers/tool-handler.js.map +1 -0
- package/dist/registry/argument-spaces.js +17 -17
- package/dist/registry/argument-spaces.js.map +1 -1
- package/dist/registry/domains.d.ts +10 -0
- package/dist/registry/domains.d.ts.map +1 -1
- package/dist/registry/domains.js +153 -11
- package/dist/registry/domains.js.map +1 -1
- package/dist/system-prompts.d.ts +8 -0
- package/dist/system-prompts.d.ts.map +1 -0
- package/dist/system-prompts.js +596 -0
- package/dist/system-prompts.js.map +1 -0
- package/dist/tool-definitions.d.ts +20 -1
- package/dist/tool-definitions.d.ts.map +1 -1
- package/dist/tool-definitions.js +42 -213
- package/dist/tool-definitions.js.map +1 -1
- package/dist/tool-router.d.ts +12 -0
- package/dist/tool-router.d.ts.map +1 -0
- package/dist/tool-router.js +59 -0
- package/dist/tool-router.js.map +1 -0
- package/dist/transport/http-transport.d.ts +40 -0
- package/dist/transport/http-transport.d.ts.map +1 -0
- package/dist/transport/http-transport.js +182 -0
- package/dist/transport/http-transport.js.map +1 -0
- package/dist/types/brutalist.d.ts +1 -0
- package/dist/types/brutalist.d.ts.map +1 -1
- package/dist/types/tool-config.d.ts +4 -3
- package/dist/types/tool-config.d.ts.map +1 -1
- package/dist/types/tool-config.js +7 -6
- package/dist/types/tool-config.js.map +1 -1
- package/dist/utils.d.ts +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +13 -6
- package/dist/utils.js.map +1 -1
- package/package.json +1 -1
package/dist/cli-agents.js
CHANGED
|
@@ -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: [
|
|
22
|
-
|
|
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: [
|
|
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: [
|
|
32
|
-
|
|
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
|
|
376
|
-
for (const
|
|
377
|
-
if (
|
|
515
|
+
const events = this.parseNDJSON(ndjsonOutput);
|
|
516
|
+
for (const event of events) {
|
|
517
|
+
if (typeof event !== 'object' || event === null)
|
|
378
518
|
continue;
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
|
424
|
-
logger.debug(`extractCodexAgentMessage: processing ${
|
|
425
|
-
for (const
|
|
426
|
-
if (
|
|
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
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
if (
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
-
|
|
446
|
-
//
|
|
447
|
-
|
|
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, (
|
|
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,
|
|
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,
|
|
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
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
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
|
-
|
|
900
|
-
|
|
901
|
-
|
|
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
|
|
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
|
|
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(`✅
|
|
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) {
|