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