@brutalist/mcp 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +3 -1
  2. package/dist/brutalist-server.d.ts +5 -0
  3. package/dist/brutalist-server.d.ts.map +1 -1
  4. package/dist/brutalist-server.js +244 -80
  5. package/dist/brutalist-server.js.map +1 -1
  6. package/dist/cli-agents.d.ts +7 -3
  7. package/dist/cli-agents.d.ts.map +1 -1
  8. package/dist/cli-agents.js +307 -48
  9. package/dist/cli-agents.js.map +1 -1
  10. package/dist/streaming/circuit-breaker.d.ts +186 -0
  11. package/dist/streaming/circuit-breaker.d.ts.map +1 -0
  12. package/dist/streaming/circuit-breaker.js +463 -0
  13. package/dist/streaming/circuit-breaker.js.map +1 -0
  14. package/dist/streaming/intelligent-buffer.d.ts +141 -0
  15. package/dist/streaming/intelligent-buffer.d.ts.map +1 -0
  16. package/dist/streaming/intelligent-buffer.js +555 -0
  17. package/dist/streaming/intelligent-buffer.js.map +1 -0
  18. package/dist/streaming/output-parser.d.ts +89 -0
  19. package/dist/streaming/output-parser.d.ts.map +1 -0
  20. package/dist/streaming/output-parser.js +349 -0
  21. package/dist/streaming/output-parser.js.map +1 -0
  22. package/dist/streaming/progress-tracker.d.ts +149 -0
  23. package/dist/streaming/progress-tracker.d.ts.map +1 -0
  24. package/dist/streaming/progress-tracker.js +519 -0
  25. package/dist/streaming/progress-tracker.js.map +1 -0
  26. package/dist/streaming/session-manager.d.ts +238 -0
  27. package/dist/streaming/session-manager.d.ts.map +1 -0
  28. package/dist/streaming/session-manager.js +546 -0
  29. package/dist/streaming/session-manager.js.map +1 -0
  30. package/dist/streaming/sse-transport.d.ts +95 -0
  31. package/dist/streaming/sse-transport.d.ts.map +1 -0
  32. package/dist/streaming/sse-transport.js +319 -0
  33. package/dist/streaming/sse-transport.js.map +1 -0
  34. package/dist/streaming/streaming-orchestrator.d.ts +153 -0
  35. package/dist/streaming/streaming-orchestrator.d.ts.map +1 -0
  36. package/dist/streaming/streaming-orchestrator.js +436 -0
  37. package/dist/streaming/streaming-orchestrator.js.map +1 -0
  38. package/dist/test-utils/process-manager.d.ts +61 -0
  39. package/dist/test-utils/process-manager.d.ts.map +1 -0
  40. package/dist/test-utils/process-manager.js +262 -0
  41. package/dist/test-utils/process-manager.js.map +1 -0
  42. package/dist/test-utils/server-harness.d.ts +73 -0
  43. package/dist/test-utils/server-harness.d.ts.map +1 -0
  44. package/dist/test-utils/server-harness.js +296 -0
  45. package/dist/test-utils/server-harness.js.map +1 -0
  46. package/dist/test-utils/streaming-fuzz.d.ts +57 -0
  47. package/dist/test-utils/streaming-fuzz.d.ts.map +1 -0
  48. package/dist/test-utils/streaming-fuzz.js +287 -0
  49. package/dist/test-utils/streaming-fuzz.js.map +1 -0
  50. package/dist/test-utils/test-isolation.d.ts +70 -0
  51. package/dist/test-utils/test-isolation.d.ts.map +1 -0
  52. package/dist/test-utils/test-isolation.js +193 -0
  53. package/dist/test-utils/test-isolation.js.map +1 -0
  54. package/dist/tool-definitions.d.ts.map +1 -1
  55. package/dist/tool-definitions.js +12 -6
  56. package/dist/tool-definitions.js.map +1 -1
  57. package/dist/types/brutalist.d.ts +3 -3
  58. package/dist/types/brutalist.d.ts.map +1 -1
  59. package/dist/types/tool-config.d.ts +0 -1
  60. package/dist/types/tool-config.d.ts.map +1 -1
  61. package/dist/types/tool-config.js +0 -1
  62. package/dist/types/tool-config.js.map +1 -1
  63. package/dist/utils/response-cache.d.ts +14 -7
  64. package/dist/utils/response-cache.d.ts.map +1 -1
  65. package/dist/utils/response-cache.js +173 -62
  66. package/dist/utils/response-cache.js.map +1 -1
  67. package/package.json +13 -3
package/README.md CHANGED
@@ -14,6 +14,8 @@ Three brutal CLI agents that can analyze anything. Each agent brings different p
14
14
 
15
15
  Real file-system analysis. Actual brutal prompts. Intelligent pagination for enterprise codebases. No participation trophies.
16
16
 
17
+ ## Setup
18
+
17
19
  ### Prerequisites
18
20
 
19
21
  Install at least one CLI agent:
@@ -21,7 +23,7 @@ Install at least one CLI agent:
21
23
  - **Codex**: Install from [OpenAI Codex](https://github.com/openai/codex-cli)
22
24
  - **Gemini**: `npm install -g @google/gemini-cli` or authenticate via `gemini auth`
23
25
 
24
- ### Setup
26
+ ### Installation
25
27
 
26
28
  <details>
27
29
  <summary><strong>Claude Code</strong> — One-liner</summary>
@@ -6,12 +6,17 @@ export declare class BrutalistServer {
6
6
  private cliOrchestrator;
7
7
  private httpTransport?;
8
8
  private responseCache;
9
+ private actualPort?;
10
+ private shutdownHandler?;
11
+ private activeSessions;
9
12
  constructor(config?: BrutalistServerConfig);
10
13
  private handleStreamingEvent;
11
14
  private handleProgressUpdate;
12
15
  start(): Promise<void>;
13
16
  private startStdioServer;
14
17
  private startHttpServer;
18
+ getActualPort(): number | undefined;
19
+ cleanup(): void;
15
20
  private registerTools;
16
21
  private registerSpecialTools;
17
22
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"brutalist-server.d.ts","sourceRoot":"","sources":["../src/brutalist-server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAUpE,OAAO,EACL,qBAAqB,EAKtB,MAAM,sBAAsB,CAAC;AAc9B,qBAAa,eAAe;IACnB,MAAM,EAAE,SAAS,CAAC;IAClB,MAAM,EAAE,qBAAqB,CAAC;IACrC,OAAO,CAAC,eAAe,CAAuB;IAC9C,OAAO,CAAC,aAAa,CAAC,CAAgC;IACtD,OAAO,CAAC,aAAa,CAAgB;gBAEzB,MAAM,GAAE,qBAA0B;IAmC9C,OAAO,CAAC,oBAAoB,CAgB1B;IAEF,OAAO,CAAC,oBAAoB,CAmB1B;IAEI,KAAK;YAeG,gBAAgB;YAMhB,eAAe;IAqE7B,OAAO,CAAC,aAAa;IAoBrB,OAAO,CAAC,oBAAoB;IA6F5B;;OAEG;YACW,eAAe;YA4Gf,gBAAgB;IAkK9B,OAAO,CAAC,gBAAgB;YAwFV,wBAAwB;IAuFtC;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAwB1B,OAAO,CAAC,kBAAkB;IAwD1B,OAAO,CAAC,uBAAuB;IAoG/B,OAAO,CAAC,mBAAmB;YA8Bb,mBAAmB;CAUlC"}
1
+ {"version":3,"file":"brutalist-server.d.ts","sourceRoot":"","sources":["../src/brutalist-server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAUpE,OAAO,EACL,qBAAqB,EAKtB,MAAM,sBAAsB,CAAC;AAc9B,qBAAa,eAAe;IACnB,MAAM,EAAE,SAAS,CAAC;IAClB,MAAM,EAAE,qBAAqB,CAAC;IACrC,OAAO,CAAC,eAAe,CAAuB;IAC9C,OAAO,CAAC,aAAa,CAAC,CAAgC;IACtD,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,UAAU,CAAC,CAAS;IAC5B,OAAO,CAAC,eAAe,CAAC,CAAa;IAErC,OAAO,CAAC,cAAc,CAIjB;gBAEO,MAAM,GAAE,qBAA0B;IAsC9C,OAAO,CAAC,oBAAoB,CAqD1B;IAEF,OAAO,CAAC,oBAAoB,CAkC1B;IAEI,KAAK;YAeG,gBAAgB;YAMhB,eAAe;IAmItB,aAAa,IAAI,MAAM,GAAG,SAAS;IAKnC,OAAO,IAAI,IAAI;IAOtB,OAAO,CAAC,aAAa;IAoBrB,OAAO,CAAC,oBAAoB;IA2F5B;;OAEG;YACW,eAAe;YAyJf,gBAAgB;IAkK9B,OAAO,CAAC,gBAAgB;YA0FV,wBAAwB;IA0FtC;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAwB1B,OAAO,CAAC,kBAAkB;IAwD1B,OAAO,CAAC,uBAAuB;IA2G/B,OAAO,CAAC,mBAAmB;YA8Bb,mBAAmB;CAUlC"}
@@ -18,11 +18,14 @@ export class BrutalistServer {
18
18
  cliOrchestrator;
19
19
  httpTransport;
20
20
  responseCache;
21
+ actualPort;
22
+ shutdownHandler;
23
+ // Session tracking for security
24
+ activeSessions = new Map();
21
25
  constructor(config = {}) {
22
26
  this.config = {
23
27
  workingDirectory: process.cwd(),
24
28
  defaultTimeout: 1500000, // 25 minutes for thorough CLI analysis
25
- enableSandbox: true,
26
29
  transport: 'stdio', // Default to stdio for backward compatibility
27
30
  httpPort: 3000,
28
31
  ...config
@@ -43,44 +46,91 @@ export class BrutalistServer {
43
46
  name: "brutalist-mcp",
44
47
  version: PACKAGE_VERSION,
45
48
  capabilities: {
46
- tools: {}
49
+ tools: {},
50
+ logging: {},
51
+ experimental: {
52
+ streaming: true
53
+ }
47
54
  }
48
55
  });
49
56
  this.registerTools();
50
57
  }
51
58
  handleStreamingEvent = (event) => {
52
- // Send streaming event via MCP server (works for both stdio and HTTP transports)
53
59
  try {
54
- logger.debug(`🔄 Streaming event: ${event.type} from ${event.agent} - ${event.content?.substring(0, 100)}...`);
55
- // Convert streaming event to MCP notification format
56
- this.server.sendLoggingMessage({
57
- level: 'info',
58
- data: event,
59
- logger: 'brutalist-mcp-streaming'
60
- });
61
- logger.debug(`✅ Sent logging message for ${event.type} event`);
60
+ if (!event.sessionId) {
61
+ logger.warn("⚠️ Streaming event without session ID - dropping for security");
62
+ return;
63
+ }
64
+ logger.debug(`🔄 Session-scoped streaming: ${event.type} from ${event.agent} to session ${event.sessionId.substring(0, 8)}...`);
65
+ // For HTTP transport: send session-specific notification
66
+ if (this.httpTransport) {
67
+ // Use MCP server's notification system with session context
68
+ this.server.server.notification({
69
+ method: "notifications/message",
70
+ params: {
71
+ level: 'info',
72
+ data: {
73
+ type: 'streaming_event',
74
+ sessionId: event.sessionId,
75
+ agent: event.agent,
76
+ eventType: event.type,
77
+ content: event.content?.substring(0, 1000), // Truncate for safety
78
+ timestamp: event.timestamp
79
+ },
80
+ logger: 'brutalist-mcp-streaming'
81
+ }
82
+ });
83
+ }
84
+ // For STDIO transport: still send but with session info
85
+ else {
86
+ this.server.sendLoggingMessage({
87
+ level: 'info',
88
+ data: {
89
+ sessionId: event.sessionId,
90
+ agent: event.agent,
91
+ type: event.type,
92
+ content: event.content?.substring(0, 500) // More restrictive for stdio
93
+ },
94
+ logger: 'brutalist-mcp-streaming'
95
+ });
96
+ }
97
+ // Update session activity
98
+ if (this.activeSessions.has(event.sessionId)) {
99
+ this.activeSessions.get(event.sessionId).lastActivity = Date.now();
100
+ }
62
101
  }
63
102
  catch (error) {
64
- logger.error("Failed to send streaming event", error);
103
+ logger.error("💥 Failed to send session-scoped streaming event", {
104
+ error: error instanceof Error ? error.message : String(error),
105
+ sessionId: event.sessionId?.substring(0, 8)
106
+ });
65
107
  }
66
108
  };
67
- handleProgressUpdate = (progressToken, progress, total, message) => {
109
+ handleProgressUpdate = (progressToken, progress, total, message, sessionId) => {
68
110
  try {
69
- logger.debug(`📊 Progress update: ${progress}/${total} - ${message}`);
70
- // Send progress notification via MCP server
111
+ if (!sessionId) {
112
+ logger.warn("⚠️ Progress update without session ID - dropping for security");
113
+ return;
114
+ }
115
+ logger.debug(`📊 Session progress: ${progress}/${total} for session ${sessionId.substring(0, 8)}...`);
116
+ // Send progress notification with session context
71
117
  this.server.server.notification({
72
118
  method: "notifications/progress",
73
119
  params: {
74
120
  progressToken,
75
121
  progress,
76
122
  total,
77
- message
123
+ message: `[${sessionId.substring(0, 8)}] ${message}`, // Include session prefix
124
+ sessionId // Include in notification data
78
125
  }
79
126
  });
80
- logger.debug(`✅ Sent progress notification: ${progress}/${total}`);
127
+ logger.debug(`✅ Sent session-scoped progress notification: ${progress}/${total}`);
81
128
  }
82
129
  catch (error) {
83
- logger.error("Failed to send progress notification", error);
130
+ logger.error("💥 Failed to send progress notification", {
131
+ error: error instanceof Error ? error.message : String(error),
132
+ sessionId: sessionId?.substring(0, 8)
133
+ });
84
134
  }
85
135
  };
86
136
  async start() {
@@ -118,13 +168,57 @@ export class BrutalistServer {
118
168
  // Create Express app for HTTP handling
119
169
  const app = express();
120
170
  app.use(express.json({ limit: '10mb' })); // Add JSON size limit for security
121
- // Enable CORS for development
171
+ // Secure CORS implementation
122
172
  app.use((req, res, next) => {
123
- res.header('Access-Control-Allow-Origin', '*');
124
- res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, DELETE');
125
- res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, Mcp-Session-Id');
173
+ const origin = req.headers.origin;
174
+ const isProduction = process.env.NODE_ENV === 'production';
175
+ // Define safe default origins for development
176
+ const defaultDevOrigins = [
177
+ 'http://localhost:3000',
178
+ 'http://127.0.0.1:3000',
179
+ 'http://localhost:8080',
180
+ 'http://127.0.0.1:8080',
181
+ 'http://localhost:3001',
182
+ 'http://127.0.0.1:3001'
183
+ ];
184
+ // Get allowed origins from config or use defaults
185
+ const allowedOrigins = this.config.corsOrigins || defaultDevOrigins;
186
+ const allowWildcard = this.config.allowCORSWildcard === true && !isProduction;
187
+ // Determine if origin is allowed
188
+ let allowedOrigin = null;
189
+ if (allowWildcard) {
190
+ // Only in development with explicit opt-in
191
+ allowedOrigin = '*';
192
+ logger.warn("⚠️ Using wildcard CORS - only safe in development!");
193
+ }
194
+ else if (!origin) {
195
+ // No origin header (same-origin or direct server access)
196
+ allowedOrigin = defaultDevOrigins[0]; // Default fallback
197
+ }
198
+ else if (allowedOrigins.includes(origin)) {
199
+ // Explicitly allowed origin
200
+ allowedOrigin = origin;
201
+ }
202
+ else {
203
+ // Rejected origin
204
+ logger.warn(`🚫 CORS rejected origin: ${origin}`);
205
+ allowedOrigin = null;
206
+ }
207
+ // Set headers only if origin is allowed
208
+ if (allowedOrigin) {
209
+ res.header('Access-Control-Allow-Origin', allowedOrigin);
210
+ res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
211
+ res.header('Access-Control-Allow-Headers', 'Content-Type, Mcp-Session-Id');
212
+ // Removed Authorization header for security
213
+ res.header('Access-Control-Allow-Credentials', 'false'); // Explicit false
214
+ }
126
215
  if (req.method === 'OPTIONS') {
127
- res.sendStatus(200);
216
+ if (allowedOrigin) {
217
+ res.sendStatus(200);
218
+ }
219
+ else {
220
+ res.sendStatus(403); // Forbidden for disallowed origins
221
+ }
128
222
  return;
129
223
  }
130
224
  next();
@@ -146,21 +240,44 @@ export class BrutalistServer {
146
240
  res.json({ status: 'ok', transport: 'http-streaming', version: PACKAGE_VERSION });
147
241
  });
148
242
  // Start the HTTP server - bind to localhost only for security
149
- const port = this.config.httpPort || 3000;
150
- const server = app.listen(port, '127.0.0.1', () => {
151
- logger.info(`HTTP server listening on port ${port}`);
152
- logger.info(`MCP endpoint: http://localhost:${port}/mcp`);
153
- logger.info(`Health check: http://localhost:${port}/health`);
154
- });
155
- // Handle graceful shutdown
156
- process.on('SIGTERM', () => {
157
- logger.info('Received SIGTERM, shutting down gracefully');
158
- server.close(() => {
159
- logger.info('HTTP server closed');
160
- process.exit(0);
243
+ const port = this.config.httpPort ?? 3000;
244
+ return new Promise((resolve, reject) => {
245
+ const server = app.listen(port, '127.0.0.1', () => {
246
+ const actualPort = server.address()?.port || port;
247
+ this.actualPort = actualPort;
248
+ logger.info(`HTTP server listening on port ${actualPort}`);
249
+ logger.info(`MCP endpoint: http://localhost:${actualPort}/mcp`);
250
+ logger.info(`Health check: http://localhost:${actualPort}/health`);
251
+ resolve();
252
+ });
253
+ server.on('error', (error) => {
254
+ logger.error('HTTP server failed to start', error);
255
+ reject(error);
161
256
  });
257
+ // Handle graceful shutdown - avoid duplicate listeners
258
+ if (!this.shutdownHandler) {
259
+ this.shutdownHandler = () => {
260
+ logger.info('Received SIGTERM, shutting down gracefully');
261
+ server.close(() => {
262
+ logger.info('HTTP server closed');
263
+ process.exit(0);
264
+ });
265
+ };
266
+ process.on('SIGTERM', this.shutdownHandler);
267
+ }
162
268
  });
163
269
  }
270
+ // Getter for actual listening port (useful for tests)
271
+ getActualPort() {
272
+ return this.actualPort;
273
+ }
274
+ // Cleanup method for tests - remove event listeners
275
+ cleanup() {
276
+ if (this.shutdownHandler) {
277
+ process.removeListener('SIGTERM', this.shutdownHandler);
278
+ this.shutdownHandler = undefined;
279
+ }
280
+ }
164
281
  registerTools() {
165
282
  // Register all roast tools using unified handler - DRY principle
166
283
  TOOL_CONFIGS.forEach(config => {
@@ -180,7 +297,6 @@ export class BrutalistServer {
180
297
  debateRounds: z.number().optional().describe("Number of debate rounds (default: 2, max: 10)"),
181
298
  context: z.string().optional().describe("Additional context for the debate"),
182
299
  workingDirectory: z.string().optional().describe("Working directory for analysis"),
183
- enableSandbox: z.boolean().optional().describe("Enable sandbox mode for security"),
184
300
  models: z.object({
185
301
  claude: z.string().optional().describe("Claude model: opus, sonnet, or full name like claude-opus-4-1-20250805"),
186
302
  codex: z.string().optional().describe("Codex model: gpt-5, gpt-5-codex, o3, o3-mini, o3-pro, o4-mini"),
@@ -189,7 +305,7 @@ export class BrutalistServer {
189
305
  }, async (args) => {
190
306
  return this.handleToolExecution(async () => {
191
307
  const debateRounds = Math.min(args.debateRounds || 2, 10); // Limit to max 10 rounds to prevent DoS
192
- const responses = await this.executeCLIDebate(args.targetPath, debateRounds, args.context, args.workingDirectory, args.enableSandbox, args.models);
308
+ const responses = await this.executeCLIDebate(args.targetPath, debateRounds, args.context, args.workingDirectory, args.models);
193
309
  return responses;
194
310
  });
195
311
  });
@@ -216,7 +332,7 @@ export class BrutalistServer {
216
332
  roster += "- `cli_agent_roster` - This tool (show capabilities)\n\n";
217
333
  roster += "## CLI Agent Capabilities\n";
218
334
  roster += "**Claude Code** - Advanced analysis with direct system prompt injection\n";
219
- roster += "**Codex** - Sandboxed execution with embedded brutal prompts\n";
335
+ roster += "**Codex** - Secure execution with embedded brutal prompts\n";
220
336
  roster += "**Gemini CLI** - Workspace context with environment variable system prompts\n\n";
221
337
  // Add CLI context information
222
338
  const cliContext = await this.cliOrchestrator.detectCLIContext();
@@ -247,6 +363,38 @@ export class BrutalistServer {
247
363
  async handleRoastTool(config, args, extra) {
248
364
  try {
249
365
  const progressToken = extra._meta?.progressToken;
366
+ // Extract session context for security
367
+ const sessionId = extra?.sessionId ||
368
+ extra?._meta?.sessionId ||
369
+ extra?.headers?.['mcp-session-id'] ||
370
+ `anonymous-${Date.now()}-${Math.random().toString(36).substring(7)}`;
371
+ const requestId = `${sessionId}-${Date.now()}-${Math.random().toString(36).substring(7)}`;
372
+ logger.debug(`🔐 Processing request with session: ${sessionId.substring(0, 8)}..., request: ${requestId.substring(0, 12)}...`);
373
+ // Track session activity
374
+ if (!this.activeSessions.has(sessionId)) {
375
+ this.activeSessions.set(sessionId, {
376
+ startTime: Date.now(),
377
+ requestCount: 0,
378
+ lastActivity: Date.now()
379
+ });
380
+ }
381
+ const sessionInfo = this.activeSessions.get(sessionId);
382
+ sessionInfo.requestCount++;
383
+ sessionInfo.lastActivity = Date.now();
384
+ // Debug logging: Log the received arguments to file
385
+ const fs = require('fs');
386
+ const debugLog = `/tmp/brutalist-tool-debug-${Date.now()}.log`;
387
+ const logMessage = (msg) => {
388
+ try {
389
+ fs.appendFileSync(debugLog, `${new Date().toISOString()}: ${msg}\n`);
390
+ }
391
+ catch (e) {
392
+ // Ignore filesystem errors
393
+ }
394
+ };
395
+ logMessage(`🔧 ROAST TOOL DEBUG: Tool=${config.name}, primaryArgField=${config.primaryArgField}`);
396
+ logMessage(`🔧 ROAST TOOL DEBUG: args=${JSON.stringify(args, null, 2)}`);
397
+ logMessage(`🔧 ROAST TOOL DEBUG: extra=${JSON.stringify(extra, null, 2)}`);
250
398
  // Extract pagination parameters
251
399
  const paginationParams = extractPaginationParams(args);
252
400
  if (args.cursor) {
@@ -255,9 +403,9 @@ export class BrutalistServer {
255
403
  }
256
404
  // Check cache if analysis_id provided
257
405
  if (args.analysis_id && !args.force_refresh) {
258
- const cachedContent = await this.responseCache.get(args.analysis_id);
406
+ const cachedContent = await this.responseCache.get(args.analysis_id, sessionId);
259
407
  if (cachedContent) {
260
- logger.info(`🎯 Using cached result for analysis_id: ${args.analysis_id}`);
408
+ logger.info(`🎯 Session-validated cache hit for analysis_id: ${args.analysis_id}`);
261
409
  const cachedResult = {
262
410
  success: true,
263
411
  responses: [{
@@ -269,6 +417,9 @@ export class BrutalistServer {
269
417
  };
270
418
  return this.formatToolResponse(cachedResult, args.verbose, paginationParams, args.analysis_id);
271
419
  }
420
+ else {
421
+ logger.info(`🔍 No valid cache entry for analysis_id: ${args.analysis_id} and session: ${sessionId?.substring(0, 8)}`);
422
+ }
272
423
  }
273
424
  // Generate cache key for this request
274
425
  const cacheKey = this.responseCache.generateCacheKey(config.cacheKeyFields.reduce((acc, field) => {
@@ -279,7 +430,7 @@ export class BrutalistServer {
279
430
  }, {}));
280
431
  // Check if we have a cached result (unless forcing refresh)
281
432
  if (!args.force_refresh) {
282
- const cachedContent = await this.responseCache.get(cacheKey);
433
+ const cachedContent = await this.responseCache.get(cacheKey, sessionId);
283
434
  if (cachedContent) {
284
435
  const analysisId = this.responseCache.generateAnalysisId(cacheKey);
285
436
  logger.info(`🎯 Cache hit for new request, using analysis_id: ${analysisId}`);
@@ -299,8 +450,10 @@ export class BrutalistServer {
299
450
  const context = config.contextBuilder ? config.contextBuilder(args) : args.context;
300
451
  // Get the primary argument (targetPath, idea, architecture, etc.)
301
452
  const primaryArg = args[config.primaryArgField];
453
+ logMessage(`🔧 PRIMARY ARG DEBUG: primaryArgField=${config.primaryArgField}, primaryArg="${primaryArg}"`);
454
+ logMessage(`🔧 PRIMARY ARG DEBUG: config.analysisType="${config.analysisType}"`);
302
455
  // Run the analysis
303
- const result = await this.executeBrutalistAnalysis(config.analysisType, primaryArg, config.systemPrompt, context, args.workingDirectory, args.enableSandbox, args.preferredCLI, args.verbose, args.models, progressToken);
456
+ const result = await this.executeBrutalistAnalysis(config.analysisType, primaryArg, config.systemPrompt, context, args.workingDirectory, args.preferredCLI, args.verbose, args.models, progressToken, sessionId, requestId);
304
457
  // Cache the result if successful
305
458
  let analysisId;
306
459
  if (result.success && result.responses.length > 0) {
@@ -312,9 +465,11 @@ export class BrutalistServer {
312
465
  acc[field] = args[field];
313
466
  return acc;
314
467
  }, {});
315
- const { analysisId: newId } = await this.responseCache.set(cacheData, fullContent, cacheKey);
468
+ const { analysisId: newId } = await this.responseCache.set(cacheData, fullContent, cacheKey, sessionId, // NEW: Bind to session
469
+ requestId // NEW: Track request
470
+ );
316
471
  analysisId = newId;
317
- logger.info(`✅ Cached analysis result with ID: ${analysisId}`);
472
+ logger.info(`✅ Cached analysis result with ID: ${analysisId} for session: ${sessionId?.substring(0, 8)}`);
318
473
  }
319
474
  }
320
475
  return this.formatToolResponse(result, args.verbose, paginationParams, analysisId);
@@ -323,12 +478,11 @@ export class BrutalistServer {
323
478
  return this.formatErrorResponse(error);
324
479
  }
325
480
  }
326
- async executeCLIDebate(targetPath, debateRounds, context, workingDirectory, enableSandbox, models) {
481
+ async executeCLIDebate(targetPath, debateRounds, context, workingDirectory, models) {
327
482
  logger.debug("Executing CLI debate", {
328
483
  targetPath,
329
484
  debateRounds,
330
485
  workingDirectory,
331
- enableSandbox
332
486
  });
333
487
  try {
334
488
  // Get CLI context
@@ -372,13 +526,14 @@ Remember: You are ${agent.toUpperCase()}, the passionate champion of ${position.
372
526
  logger.info(`🎭 ${agent.toUpperCase()} preparing initial position: ${position.split(':')[0]}`);
373
527
  const response = await this.cliOrchestrator.executeSingleCLI(agent, assignedPrompt, assignedPrompt, {
374
528
  workingDirectory: workingDirectory || this.config.workingDirectory,
375
- sandbox: enableSandbox ?? this.config.enableSandbox,
376
529
  timeout: (this.config.defaultTimeout || 60000) * 2,
377
530
  models: models ? { [agent]: models[agent] } : undefined
378
531
  });
379
532
  if (response.success) {
380
533
  debateContext.push(response);
381
- fullDebateTranscript.get(agent)?.push(response.output);
534
+ if (response.output) {
535
+ fullDebateTranscript.get(agent)?.push(response.output);
536
+ }
382
537
  }
383
538
  }
384
539
  // Subsequent rounds: Turn-based responses attacking specific arguments
@@ -420,13 +575,14 @@ Remember: You are ${currentAgent.toUpperCase()}, passionate advocate for ${assig
420
575
  logger.info(`🔥 Round ${round}: ${currentAgent.toUpperCase()} responding to opponents (${assignedPosition.split(':')[0]})`);
421
576
  const response = await this.cliOrchestrator.executeSingleCLI(currentAgent, confrontationalPrompt, confrontationalPrompt, {
422
577
  workingDirectory: workingDirectory || this.config.workingDirectory,
423
- sandbox: enableSandbox ?? this.config.enableSandbox,
424
578
  timeout: (this.config.defaultTimeout || 60000) * 2,
425
579
  models: models ? { [currentAgent]: models[currentAgent] } : undefined
426
580
  });
427
581
  if (response.success) {
428
582
  debateContext.push(response);
429
- fullDebateTranscript.get(currentAgent)?.push(response.output);
583
+ if (response.output) {
584
+ fullDebateTranscript.get(currentAgent)?.push(response.output);
585
+ }
430
586
  }
431
587
  }
432
588
  }
@@ -469,7 +625,9 @@ Remember: You are ${currentAgent.toUpperCase()}, passionate advocate for ${assig
469
625
  if (!agentOutputs.has(response.agent)) {
470
626
  agentOutputs.set(response.agent, []);
471
627
  }
472
- agentOutputs.get(response.agent)?.push(response.output);
628
+ if (response.output) {
629
+ agentOutputs.get(response.agent)?.push(response.output);
630
+ }
473
631
  });
474
632
  synthesis += `## Key Points of Conflict\n\n`;
475
633
  // Extract disagreements by looking for contradictory keywords
@@ -516,15 +674,14 @@ Remember: You are ${currentAgent.toUpperCase()}, passionate advocate for ${assig
516
674
  }
517
675
  return synthesis;
518
676
  }
519
- async executeBrutalistAnalysis(analysisType, targetPath, systemPromptSpec, context, workingDirectory, enableSandbox, preferredCLI, verbose, models, progressToken) {
677
+ async executeBrutalistAnalysis(analysisType, primaryContent, systemPromptSpec, context, workingDirectory, preferredCLI, verbose, models, progressToken, sessionId, requestId) {
520
678
  logger.info(`🏢 Starting brutalist analysis: ${analysisType}`);
521
- logger.info(`🔧 DEBUG: preferredCLI=${preferredCLI}, targetPath=${targetPath}`);
679
+ logger.info(`🔧 DEBUG: preferredCLI=${preferredCLI}, primaryContent=${primaryContent}`);
522
680
  logger.debug("Executing brutalist analysis", {
523
- targetPath,
681
+ primaryContent,
524
682
  analysisType,
525
683
  systemPromptSpec,
526
684
  workingDirectory,
527
- enableSandbox,
528
685
  preferredCLI
529
686
  });
530
687
  try {
@@ -535,16 +692,18 @@ Remember: You are ${currentAgent.toUpperCase()}, passionate advocate for ${assig
535
692
  // Execute CLI agent analysis (single or multi-CLI based on preferences)
536
693
  logger.info(`🔍 Executing brutalist analysis with timeout: ${this.config.defaultTimeout}ms`);
537
694
  logger.info(`🔧 DEBUG: About to call cliOrchestrator.executeBrutalistAnalysis`);
538
- const responses = await this.cliOrchestrator.executeBrutalistAnalysis(analysisType, targetPath, systemPromptSpec, context, {
695
+ const responses = await this.cliOrchestrator.executeBrutalistAnalysis(analysisType, primaryContent, systemPromptSpec, context, {
539
696
  workingDirectory: workingDirectory || this.config.workingDirectory,
540
- sandbox: enableSandbox ?? this.config.enableSandbox,
541
697
  timeout: this.config.defaultTimeout,
542
698
  preferredCLI,
543
699
  analysisType: analysisType,
544
700
  models,
545
701
  onStreamingEvent: this.handleStreamingEvent,
546
702
  progressToken,
547
- onProgress: progressToken ? this.handleProgressUpdate.bind(this, progressToken) : undefined
703
+ onProgress: progressToken && sessionId ?
704
+ (progress, total, message) => this.handleProgressUpdate(progressToken, progress, total, message, sessionId) : undefined,
705
+ sessionId,
706
+ requestId
548
707
  });
549
708
  logger.info(`🔧 DEBUG: cliOrchestrator.executeBrutalistAnalysis returned ${responses.length} responses`);
550
709
  const successfulResponses = responses.filter(r => r.success);
@@ -558,7 +717,7 @@ Remember: You are ${currentAgent.toUpperCase()}, passionate advocate for ${assig
558
717
  responses,
559
718
  synthesis,
560
719
  analysisType,
561
- targetPath,
720
+ targetPath: primaryContent,
562
721
  executionSummary: {
563
722
  totalCLIs: responses.length,
564
723
  successfulCLIs: successfulResponses.length,
@@ -682,39 +841,44 @@ Remember: You are ${currentAgent.toUpperCase()}, passionate advocate for ${assig
682
841
  const totalTokens = estimateTokenCount(content);
683
842
  // Format response with pagination info
684
843
  let paginatedText = '';
685
- // Add pagination header with analysis ID
844
+ // Add header
686
845
  paginatedText += `# Brutalist Analysis Results\n\n`;
687
- paginatedText += `**📊 Pagination Status:** ${statusLine}\n`;
688
- if (analysisId) {
689
- paginatedText += `**🔑 Analysis ID:** ${analysisId}\n`;
690
- }
691
- paginatedText += `**🔢 Token Estimate:** ~${chunkTokens.toLocaleString()} tokens (chunk) / ~${totalTokens.toLocaleString()} tokens (total)\n\n`;
692
- if (pagination.hasMore) {
846
+ // Only show pagination metadata if pagination is actually needed
847
+ const needsPagination = pagination.totalChunks > 1 || pagination.hasMore;
848
+ if (needsPagination) {
849
+ paginatedText += `**📊 Pagination Status:** ${statusLine}\n`;
693
850
  if (analysisId) {
694
- paginatedText += `**⏭️ Continue Reading:** Use \`analysis_id: "${analysisId}", offset: ${endOffset}\`\n\n`;
851
+ paginatedText += `**🔑 Analysis ID:** ${analysisId}\n`;
695
852
  }
696
- else {
697
- paginatedText += `**⏭️ Continue Reading:** Use \`offset: ${endOffset}\` for next chunk\n\n`;
853
+ paginatedText += `**🔢 Token Estimate:** ~${chunkTokens.toLocaleString()} tokens (chunk) / ~${totalTokens.toLocaleString()} tokens (total)\n\n`;
854
+ if (pagination.hasMore) {
855
+ if (analysisId) {
856
+ paginatedText += `**⏭️ Continue Reading:** Use \`analysis_id: "${analysisId}", offset: ${endOffset}\`\n\n`;
857
+ }
858
+ else {
859
+ paginatedText += `**⏭️ Continue Reading:** Use \`offset: ${endOffset}\` for next chunk\n\n`;
860
+ }
698
861
  }
699
862
  }
700
863
  paginatedText += `---\n\n`;
701
864
  // Add the actual content chunk
702
865
  paginatedText += chunkContent;
703
- // Add footer for continuation
704
- if (pagination.hasMore) {
866
+ // Add footer
867
+ if (needsPagination) {
705
868
  paginatedText += `\n\n---\n\n`;
706
- paginatedText += `📖 **End of chunk ${pagination.chunkIndex}/${pagination.totalChunks}**\n`;
707
- if (analysisId) {
708
- paginatedText += `🔄 To continue: Include \`analysis_id: "${analysisId}"\` with \`offset: ${endOffset}\` in next request`;
869
+ if (pagination.hasMore) {
870
+ paginatedText += `📖 **End of chunk ${pagination.chunkIndex}/${pagination.totalChunks}**\n`;
871
+ if (analysisId) {
872
+ paginatedText += `🔄 To continue: Include \`analysis_id: "${analysisId}"\` with \`offset: ${endOffset}\` in next request`;
873
+ }
874
+ else {
875
+ paginatedText += `🔄 To continue: Use same tool with \`offset: ${endOffset}\``;
876
+ }
709
877
  }
710
878
  else {
711
- paginatedText += `🔄 To continue: Use same tool with \`offset: ${endOffset}\``;
879
+ paginatedText += `✅ **Complete analysis shown** (${content.length.toLocaleString()} characters total)`;
712
880
  }
713
881
  }
714
- else {
715
- paginatedText += `\n\n---\n\n`;
716
- paginatedText += `✅ **Complete analysis shown** (${content.length.toLocaleString()} characters total)`;
717
- }
718
882
  // Add verbose execution details if requested
719
883
  if (verbose && result.executionSummary) {
720
884
  paginatedText += `\n\n### Execution Summary\n`;
@@ -742,7 +906,7 @@ Remember: You are ${currentAgent.toUpperCase()}, passionate advocate for ${assig
742
906
  sanitizedMessage = "Analysis timed out - try reducing scope or increasing timeout";
743
907
  }
744
908
  else if (error.message.includes('ENOENT') || error.message.includes('no such file')) {
745
- sanitizedMessage = "Target path not found";
909
+ sanitizedMessage = `DEBUG: Target path not found - Original error: ${error.message}`;
746
910
  }
747
911
  else if (error.message.includes('EACCES') || error.message.includes('permission denied')) {
748
912
  sanitizedMessage = "Permission denied - check file access";