@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.
- package/README.md +65 -63
- package/dist/brutalist-server.d.ts +15 -0
- package/dist/brutalist-server.d.ts.map +1 -1
- package/dist/brutalist-server.js +405 -357
- package/dist/brutalist-server.js.map +1 -1
- package/dist/cli-agents.d.ts +8 -3
- package/dist/cli-agents.d.ts.map +1 -1
- package/dist/cli-agents.js +352 -50
- package/dist/cli-agents.js.map +1 -1
- package/dist/streaming/circuit-breaker.d.ts +186 -0
- package/dist/streaming/circuit-breaker.d.ts.map +1 -0
- package/dist/streaming/circuit-breaker.js +463 -0
- package/dist/streaming/circuit-breaker.js.map +1 -0
- package/dist/streaming/intelligent-buffer.d.ts +141 -0
- package/dist/streaming/intelligent-buffer.d.ts.map +1 -0
- package/dist/streaming/intelligent-buffer.js +555 -0
- package/dist/streaming/intelligent-buffer.js.map +1 -0
- package/dist/streaming/output-parser.d.ts +89 -0
- package/dist/streaming/output-parser.d.ts.map +1 -0
- package/dist/streaming/output-parser.js +349 -0
- package/dist/streaming/output-parser.js.map +1 -0
- package/dist/streaming/progress-tracker.d.ts +149 -0
- package/dist/streaming/progress-tracker.d.ts.map +1 -0
- package/dist/streaming/progress-tracker.js +519 -0
- package/dist/streaming/progress-tracker.js.map +1 -0
- package/dist/streaming/session-manager.d.ts +238 -0
- package/dist/streaming/session-manager.d.ts.map +1 -0
- package/dist/streaming/session-manager.js +546 -0
- package/dist/streaming/session-manager.js.map +1 -0
- package/dist/streaming/sse-transport.d.ts +95 -0
- package/dist/streaming/sse-transport.d.ts.map +1 -0
- package/dist/streaming/sse-transport.js +319 -0
- package/dist/streaming/sse-transport.js.map +1 -0
- package/dist/streaming/streaming-orchestrator.d.ts +153 -0
- package/dist/streaming/streaming-orchestrator.d.ts.map +1 -0
- package/dist/streaming/streaming-orchestrator.js +436 -0
- package/dist/streaming/streaming-orchestrator.js.map +1 -0
- package/dist/test-utils/process-manager.d.ts +61 -0
- package/dist/test-utils/process-manager.d.ts.map +1 -0
- package/dist/test-utils/process-manager.js +262 -0
- package/dist/test-utils/process-manager.js.map +1 -0
- package/dist/test-utils/server-harness.d.ts +73 -0
- package/dist/test-utils/server-harness.d.ts.map +1 -0
- package/dist/test-utils/server-harness.js +296 -0
- package/dist/test-utils/server-harness.js.map +1 -0
- package/dist/test-utils/streaming-fuzz.d.ts +57 -0
- package/dist/test-utils/streaming-fuzz.d.ts.map +1 -0
- package/dist/test-utils/streaming-fuzz.js +287 -0
- package/dist/test-utils/streaming-fuzz.js.map +1 -0
- package/dist/test-utils/test-isolation.d.ts +70 -0
- package/dist/test-utils/test-isolation.d.ts.map +1 -0
- package/dist/test-utils/test-isolation.js +193 -0
- package/dist/test-utils/test-isolation.js.map +1 -0
- package/dist/tool-definitions.d.ts +6 -0
- package/dist/tool-definitions.d.ts.map +1 -0
- package/dist/tool-definitions.js +217 -0
- package/dist/tool-definitions.js.map +1 -0
- package/dist/types/brutalist.d.ts +3 -19
- package/dist/types/brutalist.d.ts.map +1 -1
- package/dist/types/tool-config.d.ts +51 -0
- package/dist/types/tool-config.d.ts.map +1 -0
- package/dist/types/tool-config.js +24 -0
- package/dist/types/tool-config.js.map +1 -0
- package/dist/utils/pagination.d.ts +2 -2
- package/dist/utils/pagination.d.ts.map +1 -1
- package/dist/utils/pagination.js +1 -1
- package/dist/utils/pagination.js.map +1 -1
- package/dist/utils/response-cache.d.ts +96 -0
- package/dist/utils/response-cache.d.ts.map +1 -0
- package/dist/utils/response-cache.js +371 -0
- package/dist/utils/response-cache.js.map +1 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +22 -3
- package/dist/utils.js.map +1 -1
- package/package.json +14 -4
package/dist/cli-agents.js
CHANGED
|
@@ -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
|
-
//
|
|
28
|
-
|
|
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
|
|
34
|
-
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
520
|
-
|
|
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
|
|
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,
|
|
797
|
+
return await this.executeCodex(userPrompt, systemPromptSpec, options);
|
|
565
798
|
case 'gemini':
|
|
566
|
-
return await this.executeGemini(userPrompt, systemPromptSpec,
|
|
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
|
|
585
|
-
const
|
|
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,
|
|
969
|
+
constructUserPrompt(analysisType, primaryContent, context) {
|
|
668
970
|
// Trust CLI tools to handle their own security
|
|
669
|
-
const
|
|
971
|
+
const sanitizedContent = primaryContent;
|
|
670
972
|
const sanitizedContext = context || 'No additional context provided';
|
|
671
973
|
const prompts = {
|
|
672
|
-
code: `Analyze the codebase at ${
|
|
673
|
-
codebase: `Analyze the codebase directory at ${
|
|
674
|
-
architecture: `Review the architecture: ${
|
|
675
|
-
idea: `Analyze this idea: ${
|
|
676
|
-
research: `Review this research: ${
|
|
677
|
-
data: `Analyze this data/model: ${
|
|
678
|
-
security: `Security audit of: ${
|
|
679
|
-
product: `Product review: ${
|
|
680
|
-
infrastructure: `Infrastructure review: ${
|
|
681
|
-
debate: `Debate topic: ${
|
|
682
|
-
fileStructure: `Analyze the directory structure at ${
|
|
683
|
-
dependencies: `Analyze dependencies at ${
|
|
684
|
-
gitHistory: `Analyze git history at ${
|
|
685
|
-
testCoverage: `Analyze test coverage at ${
|
|
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 ${
|
|
989
|
+
const specificPrompt = prompts[analysisType] || `Analyze ${sanitizedContent} for ${analysisType} issues.`;
|
|
688
990
|
return `${specificPrompt} ${context ? `Context: ${sanitizedContext}` : ''}`;
|
|
689
991
|
}
|
|
690
992
|
}
|