@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.
Files changed (71) 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 +295 -92
  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 +316 -56
  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 +297 -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/pagination.d.ts +3 -3
  64. package/dist/utils/pagination.d.ts.map +1 -1
  65. package/dist/utils/pagination.js +24 -6
  66. package/dist/utils/pagination.js.map +1 -1
  67. package/dist/utils/response-cache.d.ts +23 -7
  68. package/dist/utils/response-cache.d.ts.map +1 -1
  69. package/dist/utils/response-cache.js +202 -62
  70. package/dist/utils/response-cache.js.map +1 -1
  71. package/package.json +13 -3
@@ -2,6 +2,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
4
4
  import { randomUUID } from "crypto";
5
+ import { appendFileSync } from "fs";
5
6
  import express from "express";
6
7
  import { z } from "zod";
7
8
  import { CLIAgentOrchestrator } from './cli-agents.js';
@@ -18,11 +19,14 @@ export class BrutalistServer {
18
19
  cliOrchestrator;
19
20
  httpTransport;
20
21
  responseCache;
22
+ actualPort;
23
+ shutdownHandler;
24
+ // Session tracking for security
25
+ activeSessions = new Map();
21
26
  constructor(config = {}) {
22
27
  this.config = {
23
28
  workingDirectory: process.cwd(),
24
29
  defaultTimeout: 1500000, // 25 minutes for thorough CLI analysis
25
- enableSandbox: true,
26
30
  transport: 'stdio', // Default to stdio for backward compatibility
27
31
  httpPort: 3000,
28
32
  ...config
@@ -41,46 +45,114 @@ export class BrutalistServer {
41
45
  logger.info(`📦 Response cache initialized with ${cacheTTLHours} hour TTL`);
42
46
  this.server = new McpServer({
43
47
  name: "brutalist-mcp",
44
- version: PACKAGE_VERSION,
48
+ version: PACKAGE_VERSION
49
+ }, {
45
50
  capabilities: {
46
- tools: {}
51
+ tools: {},
52
+ logging: {},
53
+ experimental: {
54
+ streaming: true
55
+ }
47
56
  }
48
57
  });
49
58
  this.registerTools();
50
59
  }
51
60
  handleStreamingEvent = (event) => {
52
- // Send streaming event via MCP server (works for both stdio and HTTP transports)
53
61
  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`);
62
+ if (!event.sessionId) {
63
+ logger.warn("⚠️ Streaming event without session ID - dropping for security");
64
+ return;
65
+ }
66
+ logger.debug(`🔄 Session-scoped streaming: ${event.type} from ${event.agent} to session ${event.sessionId.substring(0, 8)}...`);
67
+ // For HTTP transport: send session-specific notification if client supports it
68
+ if (this.httpTransport) {
69
+ try {
70
+ // Debug: Check what capabilities the server has
71
+ logger.debug(`🔍 Server capabilities check: logging=${!!this.server.server._capabilities?.logging}`);
72
+ // Use MCP server's notification system with session context
73
+ this.server.server.notification({
74
+ method: "notifications/message",
75
+ params: {
76
+ level: 'info',
77
+ data: {
78
+ type: 'streaming_event',
79
+ sessionId: event.sessionId,
80
+ agent: event.agent,
81
+ eventType: event.type,
82
+ content: event.content?.substring(0, 1000), // Truncate for safety
83
+ timestamp: event.timestamp
84
+ },
85
+ logger: 'brutalist-mcp-streaming'
86
+ }
87
+ });
88
+ }
89
+ catch (notificationError) {
90
+ // Client doesn't support logging notifications - silently skip
91
+ logger.debug("Client doesn't support logging notifications, skipping streaming event");
92
+ }
93
+ }
94
+ // For STDIO transport: still send but with session info
95
+ else {
96
+ try {
97
+ this.server.sendLoggingMessage({
98
+ level: 'info',
99
+ data: {
100
+ sessionId: event.sessionId,
101
+ agent: event.agent,
102
+ type: event.type,
103
+ content: event.content?.substring(0, 500) // More restrictive for stdio
104
+ },
105
+ logger: 'brutalist-mcp-streaming'
106
+ });
107
+ }
108
+ catch (loggingError) {
109
+ // Client doesn't support logging - silently skip
110
+ logger.debug("Client doesn't support logging, skipping streaming event");
111
+ }
112
+ }
113
+ // Update session activity
114
+ if (this.activeSessions.has(event.sessionId)) {
115
+ this.activeSessions.get(event.sessionId).lastActivity = Date.now();
116
+ }
62
117
  }
63
118
  catch (error) {
64
- logger.error("Failed to send streaming event", error);
119
+ logger.error("💥 Failed to send session-scoped streaming event", {
120
+ error: error instanceof Error ? error.message : String(error),
121
+ sessionId: event.sessionId?.substring(0, 8)
122
+ });
65
123
  }
66
124
  };
67
- handleProgressUpdate = (progressToken, progress, total, message) => {
125
+ handleProgressUpdate = (progressToken, progress, total, message, sessionId) => {
68
126
  try {
69
- logger.debug(`📊 Progress update: ${progress}/${total} - ${message}`);
70
- // Send progress notification via MCP server
71
- this.server.server.notification({
72
- method: "notifications/progress",
73
- params: {
74
- progressToken,
75
- progress,
76
- total,
77
- message
78
- }
79
- });
80
- logger.debug(`✅ Sent progress notification: ${progress}/${total}`);
127
+ if (!sessionId) {
128
+ logger.warn("⚠️ Progress update without session ID - dropping for security");
129
+ return;
130
+ }
131
+ logger.debug(`📊 Session progress: ${progress}/${total} for session ${sessionId.substring(0, 8)}...`);
132
+ // Send progress notification with session context if client supports it
133
+ try {
134
+ this.server.server.notification({
135
+ method: "notifications/progress",
136
+ params: {
137
+ progressToken,
138
+ progress,
139
+ total,
140
+ message: `[${sessionId.substring(0, 8)}] ${message}`, // Include session prefix
141
+ sessionId // Include in notification data
142
+ }
143
+ });
144
+ logger.debug(`✅ Sent session-scoped progress notification: ${progress}/${total}`);
145
+ }
146
+ catch (notificationError) {
147
+ // Client doesn't support progress notifications - silently skip
148
+ logger.debug("Client doesn't support progress notifications, skipping");
149
+ }
81
150
  }
82
151
  catch (error) {
83
- logger.error("Failed to send progress notification", error);
152
+ logger.error("💥 Failed to send progress notification", {
153
+ error: error instanceof Error ? error.message : String(error),
154
+ sessionId: sessionId?.substring(0, 8)
155
+ });
84
156
  }
85
157
  };
86
158
  async start() {
@@ -118,13 +190,57 @@ export class BrutalistServer {
118
190
  // Create Express app for HTTP handling
119
191
  const app = express();
120
192
  app.use(express.json({ limit: '10mb' })); // Add JSON size limit for security
121
- // Enable CORS for development
193
+ // Secure CORS implementation
122
194
  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');
195
+ const origin = req.headers.origin;
196
+ const isProduction = process.env.NODE_ENV === 'production';
197
+ // Define safe default origins for development
198
+ const defaultDevOrigins = [
199
+ 'http://localhost:3000',
200
+ 'http://127.0.0.1:3000',
201
+ 'http://localhost:8080',
202
+ 'http://127.0.0.1:8080',
203
+ 'http://localhost:3001',
204
+ 'http://127.0.0.1:3001'
205
+ ];
206
+ // Get allowed origins from config or use defaults
207
+ const allowedOrigins = this.config.corsOrigins || defaultDevOrigins;
208
+ const allowWildcard = this.config.allowCORSWildcard === true && !isProduction;
209
+ // Determine if origin is allowed
210
+ let allowedOrigin = null;
211
+ if (allowWildcard) {
212
+ // Only in development with explicit opt-in
213
+ allowedOrigin = '*';
214
+ logger.warn("⚠️ Using wildcard CORS - only safe in development!");
215
+ }
216
+ else if (!origin) {
217
+ // No origin header (same-origin or direct server access)
218
+ allowedOrigin = defaultDevOrigins[0]; // Default fallback
219
+ }
220
+ else if (allowedOrigins.includes(origin)) {
221
+ // Explicitly allowed origin
222
+ allowedOrigin = origin;
223
+ }
224
+ else {
225
+ // Rejected origin
226
+ logger.warn(`🚫 CORS rejected origin: ${origin}`);
227
+ allowedOrigin = null;
228
+ }
229
+ // Set headers only if origin is allowed
230
+ if (allowedOrigin) {
231
+ res.header('Access-Control-Allow-Origin', allowedOrigin);
232
+ res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
233
+ res.header('Access-Control-Allow-Headers', 'Content-Type, Mcp-Session-Id');
234
+ // Removed Authorization header for security
235
+ res.header('Access-Control-Allow-Credentials', 'false'); // Explicit false
236
+ }
126
237
  if (req.method === 'OPTIONS') {
127
- res.sendStatus(200);
238
+ if (allowedOrigin) {
239
+ res.sendStatus(200);
240
+ }
241
+ else {
242
+ res.sendStatus(403); // Forbidden for disallowed origins
243
+ }
128
244
  return;
129
245
  }
130
246
  next();
@@ -146,21 +262,44 @@ export class BrutalistServer {
146
262
  res.json({ status: 'ok', transport: 'http-streaming', version: PACKAGE_VERSION });
147
263
  });
148
264
  // 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);
265
+ const port = this.config.httpPort ?? 3000;
266
+ return new Promise((resolve, reject) => {
267
+ const server = app.listen(port, '127.0.0.1', () => {
268
+ const actualPort = server.address()?.port || port;
269
+ this.actualPort = actualPort;
270
+ logger.info(`HTTP server listening on port ${actualPort}`);
271
+ logger.info(`MCP endpoint: http://localhost:${actualPort}/mcp`);
272
+ logger.info(`Health check: http://localhost:${actualPort}/health`);
273
+ resolve();
274
+ });
275
+ server.on('error', (error) => {
276
+ logger.error('HTTP server failed to start', error);
277
+ reject(error);
161
278
  });
279
+ // Handle graceful shutdown - avoid duplicate listeners
280
+ if (!this.shutdownHandler) {
281
+ this.shutdownHandler = () => {
282
+ logger.info('Received SIGTERM, shutting down gracefully');
283
+ server.close(() => {
284
+ logger.info('HTTP server closed');
285
+ process.exit(0);
286
+ });
287
+ };
288
+ process.on('SIGTERM', this.shutdownHandler);
289
+ }
162
290
  });
163
291
  }
292
+ // Getter for actual listening port (useful for tests)
293
+ getActualPort() {
294
+ return this.actualPort;
295
+ }
296
+ // Cleanup method for tests - remove event listeners
297
+ cleanup() {
298
+ if (this.shutdownHandler) {
299
+ process.removeListener('SIGTERM', this.shutdownHandler);
300
+ this.shutdownHandler = undefined;
301
+ }
302
+ }
164
303
  registerTools() {
165
304
  // Register all roast tools using unified handler - DRY principle
166
305
  TOOL_CONFIGS.forEach(config => {
@@ -180,7 +319,6 @@ export class BrutalistServer {
180
319
  debateRounds: z.number().optional().describe("Number of debate rounds (default: 2, max: 10)"),
181
320
  context: z.string().optional().describe("Additional context for the debate"),
182
321
  workingDirectory: z.string().optional().describe("Working directory for analysis"),
183
- enableSandbox: z.boolean().optional().describe("Enable sandbox mode for security"),
184
322
  models: z.object({
185
323
  claude: z.string().optional().describe("Claude model: opus, sonnet, or full name like claude-opus-4-1-20250805"),
186
324
  codex: z.string().optional().describe("Codex model: gpt-5, gpt-5-codex, o3, o3-mini, o3-pro, o4-mini"),
@@ -189,7 +327,7 @@ export class BrutalistServer {
189
327
  }, async (args) => {
190
328
  return this.handleToolExecution(async () => {
191
329
  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);
330
+ const responses = await this.executeCLIDebate(args.targetPath, debateRounds, args.context, args.workingDirectory, args.models);
193
331
  return responses;
194
332
  });
195
333
  });
@@ -216,7 +354,7 @@ export class BrutalistServer {
216
354
  roster += "- `cli_agent_roster` - This tool (show capabilities)\n\n";
217
355
  roster += "## CLI Agent Capabilities\n";
218
356
  roster += "**Claude Code** - Advanced analysis with direct system prompt injection\n";
219
- roster += "**Codex** - Sandboxed execution with embedded brutal prompts\n";
357
+ roster += "**Codex** - Secure execution with embedded brutal prompts\n";
220
358
  roster += "**Gemini CLI** - Workspace context with environment variable system prompts\n\n";
221
359
  // Add CLI context information
222
360
  const cliContext = await this.cliOrchestrator.detectCLIContext();
@@ -247,6 +385,38 @@ export class BrutalistServer {
247
385
  async handleRoastTool(config, args, extra) {
248
386
  try {
249
387
  const progressToken = extra._meta?.progressToken;
388
+ // Extract session context for security
389
+ // IMPORTANT: Use consistent "anonymous" for all anonymous users to enable cache sharing
390
+ const sessionId = extra?.sessionId ||
391
+ extra?._meta?.sessionId ||
392
+ extra?.headers?.['mcp-session-id'] ||
393
+ 'anonymous'; // Consistent for cache sharing across pagination requests
394
+ const requestId = `${sessionId}-${Date.now()}-${Math.random().toString(36).substring(7)}`;
395
+ logger.debug(`🔐 Processing request with session: ${sessionId.substring(0, 8)}..., request: ${requestId.substring(0, 12)}...`);
396
+ // Track session activity
397
+ if (!this.activeSessions.has(sessionId)) {
398
+ this.activeSessions.set(sessionId, {
399
+ startTime: Date.now(),
400
+ requestCount: 0,
401
+ lastActivity: Date.now()
402
+ });
403
+ }
404
+ const sessionInfo = this.activeSessions.get(sessionId);
405
+ sessionInfo.requestCount++;
406
+ sessionInfo.lastActivity = Date.now();
407
+ // Debug logging: Log the received arguments to file
408
+ const debugLog = `/tmp/brutalist-tool-debug-${Date.now()}.log`;
409
+ const logMessage = (msg) => {
410
+ try {
411
+ appendFileSync(debugLog, `${new Date().toISOString()}: ${msg}\n`);
412
+ }
413
+ catch (e) {
414
+ // Ignore filesystem errors
415
+ }
416
+ };
417
+ logMessage(`🔧 ROAST TOOL DEBUG: Tool=${config.name}, primaryArgField=${config.primaryArgField}`);
418
+ logMessage(`🔧 ROAST TOOL DEBUG: args=${JSON.stringify(args, null, 2)}`);
419
+ logMessage(`🔧 ROAST TOOL DEBUG: extra=${JSON.stringify(extra, null, 2)}`);
250
420
  // Extract pagination parameters
251
421
  const paginationParams = extractPaginationParams(args);
252
422
  if (args.cursor) {
@@ -255,9 +425,9 @@ export class BrutalistServer {
255
425
  }
256
426
  // Check cache if analysis_id provided
257
427
  if (args.analysis_id && !args.force_refresh) {
258
- const cachedContent = await this.responseCache.get(args.analysis_id);
428
+ const cachedContent = await this.responseCache.get(args.analysis_id, sessionId);
259
429
  if (cachedContent) {
260
- logger.info(`🎯 Using cached result for analysis_id: ${args.analysis_id}`);
430
+ logger.info(`🎯 Cache HIT for analysis_id: ${args.analysis_id}`);
261
431
  const cachedResult = {
262
432
  success: true,
263
433
  responses: [{
@@ -269,6 +439,13 @@ export class BrutalistServer {
269
439
  };
270
440
  return this.formatToolResponse(cachedResult, args.verbose, paginationParams, args.analysis_id);
271
441
  }
442
+ else {
443
+ logger.warn(`❌ Cache MISS for analysis_id: ${args.analysis_id}, session: ${sessionId}`);
444
+ // Don't silently re-run - analysis_id should always hit cache or error
445
+ throw new Error(`Analysis ID "${args.analysis_id}" not found in cache. ` +
446
+ `It may have expired (2 hour TTL) or belong to a different session. ` +
447
+ `Remove analysis_id parameter to run a new analysis.`);
448
+ }
272
449
  }
273
450
  // Generate cache key for this request
274
451
  const cacheKey = this.responseCache.generateCacheKey(config.cacheKeyFields.reduce((acc, field) => {
@@ -279,9 +456,13 @@ export class BrutalistServer {
279
456
  }, {}));
280
457
  // Check if we have a cached result (unless forcing refresh)
281
458
  if (!args.force_refresh) {
282
- const cachedContent = await this.responseCache.get(cacheKey);
459
+ const cachedContent = await this.responseCache.get(cacheKey, sessionId);
283
460
  if (cachedContent) {
284
- const analysisId = this.responseCache.generateAnalysisId(cacheKey);
461
+ // Get existing analysis_id or create new alias
462
+ const existingAnalysisId = this.responseCache.findAnalysisIdForKey(cacheKey);
463
+ const analysisId = existingAnalysisId
464
+ ? this.responseCache.createAlias(existingAnalysisId, cacheKey)
465
+ : this.responseCache.generateAnalysisId(cacheKey);
285
466
  logger.info(`🎯 Cache hit for new request, using analysis_id: ${analysisId}`);
286
467
  const cachedResult = {
287
468
  success: true,
@@ -299,8 +480,10 @@ export class BrutalistServer {
299
480
  const context = config.contextBuilder ? config.contextBuilder(args) : args.context;
300
481
  // Get the primary argument (targetPath, idea, architecture, etc.)
301
482
  const primaryArg = args[config.primaryArgField];
483
+ logMessage(`🔧 PRIMARY ARG DEBUG: primaryArgField=${config.primaryArgField}, primaryArg="${primaryArg}"`);
484
+ logMessage(`🔧 PRIMARY ARG DEBUG: config.analysisType="${config.analysisType}"`);
302
485
  // 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);
486
+ const result = await this.executeBrutalistAnalysis(config.analysisType, primaryArg, config.systemPrompt, context, args.workingDirectory, args.preferredCLI, args.verbose, args.models, progressToken, sessionId, requestId);
304
487
  // Cache the result if successful
305
488
  let analysisId;
306
489
  if (result.success && result.responses.length > 0) {
@@ -312,9 +495,11 @@ export class BrutalistServer {
312
495
  acc[field] = args[field];
313
496
  return acc;
314
497
  }, {});
315
- const { analysisId: newId } = await this.responseCache.set(cacheData, fullContent, cacheKey);
498
+ const { analysisId: newId } = await this.responseCache.set(cacheData, fullContent, cacheKey, sessionId, // NEW: Bind to session
499
+ requestId // NEW: Track request
500
+ );
316
501
  analysisId = newId;
317
- logger.info(`✅ Cached analysis result with ID: ${analysisId}`);
502
+ logger.info(`✅ Cached analysis result with ID: ${analysisId} for session: ${sessionId?.substring(0, 8)}`);
318
503
  }
319
504
  }
320
505
  return this.formatToolResponse(result, args.verbose, paginationParams, analysisId);
@@ -323,12 +508,11 @@ export class BrutalistServer {
323
508
  return this.formatErrorResponse(error);
324
509
  }
325
510
  }
326
- async executeCLIDebate(targetPath, debateRounds, context, workingDirectory, enableSandbox, models) {
511
+ async executeCLIDebate(targetPath, debateRounds, context, workingDirectory, models) {
327
512
  logger.debug("Executing CLI debate", {
328
513
  targetPath,
329
514
  debateRounds,
330
515
  workingDirectory,
331
- enableSandbox
332
516
  });
333
517
  try {
334
518
  // Get CLI context
@@ -372,13 +556,14 @@ Remember: You are ${agent.toUpperCase()}, the passionate champion of ${position.
372
556
  logger.info(`🎭 ${agent.toUpperCase()} preparing initial position: ${position.split(':')[0]}`);
373
557
  const response = await this.cliOrchestrator.executeSingleCLI(agent, assignedPrompt, assignedPrompt, {
374
558
  workingDirectory: workingDirectory || this.config.workingDirectory,
375
- sandbox: enableSandbox ?? this.config.enableSandbox,
376
559
  timeout: (this.config.defaultTimeout || 60000) * 2,
377
560
  models: models ? { [agent]: models[agent] } : undefined
378
561
  });
379
562
  if (response.success) {
380
563
  debateContext.push(response);
381
- fullDebateTranscript.get(agent)?.push(response.output);
564
+ if (response.output) {
565
+ fullDebateTranscript.get(agent)?.push(response.output);
566
+ }
382
567
  }
383
568
  }
384
569
  // Subsequent rounds: Turn-based responses attacking specific arguments
@@ -420,13 +605,14 @@ Remember: You are ${currentAgent.toUpperCase()}, passionate advocate for ${assig
420
605
  logger.info(`🔥 Round ${round}: ${currentAgent.toUpperCase()} responding to opponents (${assignedPosition.split(':')[0]})`);
421
606
  const response = await this.cliOrchestrator.executeSingleCLI(currentAgent, confrontationalPrompt, confrontationalPrompt, {
422
607
  workingDirectory: workingDirectory || this.config.workingDirectory,
423
- sandbox: enableSandbox ?? this.config.enableSandbox,
424
608
  timeout: (this.config.defaultTimeout || 60000) * 2,
425
609
  models: models ? { [currentAgent]: models[currentAgent] } : undefined
426
610
  });
427
611
  if (response.success) {
428
612
  debateContext.push(response);
429
- fullDebateTranscript.get(currentAgent)?.push(response.output);
613
+ if (response.output) {
614
+ fullDebateTranscript.get(currentAgent)?.push(response.output);
615
+ }
430
616
  }
431
617
  }
432
618
  }
@@ -469,7 +655,9 @@ Remember: You are ${currentAgent.toUpperCase()}, passionate advocate for ${assig
469
655
  if (!agentOutputs.has(response.agent)) {
470
656
  agentOutputs.set(response.agent, []);
471
657
  }
472
- agentOutputs.get(response.agent)?.push(response.output);
658
+ if (response.output) {
659
+ agentOutputs.get(response.agent)?.push(response.output);
660
+ }
473
661
  });
474
662
  synthesis += `## Key Points of Conflict\n\n`;
475
663
  // Extract disagreements by looking for contradictory keywords
@@ -516,15 +704,14 @@ Remember: You are ${currentAgent.toUpperCase()}, passionate advocate for ${assig
516
704
  }
517
705
  return synthesis;
518
706
  }
519
- async executeBrutalistAnalysis(analysisType, targetPath, systemPromptSpec, context, workingDirectory, enableSandbox, preferredCLI, verbose, models, progressToken) {
707
+ async executeBrutalistAnalysis(analysisType, primaryContent, systemPromptSpec, context, workingDirectory, preferredCLI, verbose, models, progressToken, sessionId, requestId) {
520
708
  logger.info(`🏢 Starting brutalist analysis: ${analysisType}`);
521
- logger.info(`🔧 DEBUG: preferredCLI=${preferredCLI}, targetPath=${targetPath}`);
709
+ logger.info(`🔧 DEBUG: preferredCLI=${preferredCLI}, primaryContent=${primaryContent}`);
522
710
  logger.debug("Executing brutalist analysis", {
523
- targetPath,
711
+ primaryContent,
524
712
  analysisType,
525
713
  systemPromptSpec,
526
714
  workingDirectory,
527
- enableSandbox,
528
715
  preferredCLI
529
716
  });
530
717
  try {
@@ -535,16 +722,18 @@ Remember: You are ${currentAgent.toUpperCase()}, passionate advocate for ${assig
535
722
  // Execute CLI agent analysis (single or multi-CLI based on preferences)
536
723
  logger.info(`🔍 Executing brutalist analysis with timeout: ${this.config.defaultTimeout}ms`);
537
724
  logger.info(`🔧 DEBUG: About to call cliOrchestrator.executeBrutalistAnalysis`);
538
- const responses = await this.cliOrchestrator.executeBrutalistAnalysis(analysisType, targetPath, systemPromptSpec, context, {
725
+ const responses = await this.cliOrchestrator.executeBrutalistAnalysis(analysisType, primaryContent, systemPromptSpec, context, {
539
726
  workingDirectory: workingDirectory || this.config.workingDirectory,
540
- sandbox: enableSandbox ?? this.config.enableSandbox,
541
727
  timeout: this.config.defaultTimeout,
542
728
  preferredCLI,
543
729
  analysisType: analysisType,
544
730
  models,
545
731
  onStreamingEvent: this.handleStreamingEvent,
546
732
  progressToken,
547
- onProgress: progressToken ? this.handleProgressUpdate.bind(this, progressToken) : undefined
733
+ onProgress: progressToken && sessionId ?
734
+ (progress, total, message) => this.handleProgressUpdate(progressToken, progress, total, message, sessionId) : undefined,
735
+ sessionId,
736
+ requestId
548
737
  });
549
738
  logger.info(`🔧 DEBUG: cliOrchestrator.executeBrutalistAnalysis returned ${responses.length} responses`);
550
739
  const successfulResponses = responses.filter(r => r.success);
@@ -558,7 +747,7 @@ Remember: You are ${currentAgent.toUpperCase()}, passionate advocate for ${assig
558
747
  responses,
559
748
  synthesis,
560
749
  analysisType,
561
- targetPath,
750
+ targetPath: primaryContent,
562
751
  executionSummary: {
563
752
  totalCLIs: responses.length,
564
753
  successfulCLIs: successfulResponses.length,
@@ -663,10 +852,13 @@ Remember: You are ${currentAgent.toUpperCase()}, passionate advocate for ${assig
663
852
  const chunks = chunker.chunkText(content);
664
853
  // Find the appropriate chunk based on offset
665
854
  let targetChunk = chunks[0]; // Default to first chunk
855
+ let targetChunkIndex = 0;
666
856
  let currentOffset = 0;
667
- for (const chunk of chunks) {
857
+ for (let i = 0; i < chunks.length; i++) {
858
+ const chunk = chunks[i];
668
859
  if (offset >= chunk.startOffset && offset < chunk.endOffset) {
669
860
  targetChunk = chunk;
861
+ targetChunkIndex = i;
670
862
  break;
671
863
  }
672
864
  currentOffset = chunk.endOffset;
@@ -674,47 +866,58 @@ Remember: You are ${currentAgent.toUpperCase()}, passionate advocate for ${assig
674
866
  const chunkContent = targetChunk.content;
675
867
  const actualOffset = targetChunk.startOffset;
676
868
  const endOffset = targetChunk.endOffset;
677
- // Create pagination metadata
678
- const pagination = createPaginationMetadata(content.length, paginationParams, limit);
869
+ // Create pagination metadata using actual chunk boundaries
870
+ const pagination = createPaginationMetadata(content.length, paginationParams, limit, chunks, targetChunkIndex);
679
871
  const statusLine = formatPaginationStatus(pagination);
680
872
  // Estimate token usage for user awareness
681
873
  const chunkTokens = estimateTokenCount(chunkContent);
682
874
  const totalTokens = estimateTokenCount(content);
683
875
  // Format response with pagination info
684
876
  let paginatedText = '';
685
- // Add pagination header with analysis ID
877
+ // Add header
686
878
  paginatedText += `# Brutalist Analysis Results\n\n`;
687
- paginatedText += `**📊 Pagination Status:** ${statusLine}\n`;
688
- if (analysisId) {
879
+ // Show pagination metadata
880
+ const needsPagination = pagination.totalChunks > 1 || pagination.hasMore;
881
+ const isFirstRequest = offset === 0;
882
+ // Always show analysis_id on first request for future pagination
883
+ if (isFirstRequest && analysisId) {
689
884
  paginatedText += `**🔑 Analysis ID:** ${analysisId}\n`;
885
+ paginatedText += `**🔢 Token Estimate:** ~${totalTokens.toLocaleString()} tokens (total)\n\n`;
690
886
  }
691
- paginatedText += `**🔢 Token Estimate:** ~${chunkTokens.toLocaleString()} tokens (chunk) / ~${totalTokens.toLocaleString()} tokens (total)\n\n`;
692
- if (pagination.hasMore) {
693
- if (analysisId) {
694
- paginatedText += `**⏭️ Continue Reading:** Use \`analysis_id: "${analysisId}", offset: ${endOffset}\`\n\n`;
887
+ if (needsPagination) {
888
+ paginatedText += `**📊 Pagination Status:** ${statusLine}\n`;
889
+ if (!isFirstRequest && analysisId) {
890
+ paginatedText += `**🔑 Analysis ID:** ${analysisId}\n`;
695
891
  }
696
- else {
697
- paginatedText += `**⏭️ Continue Reading:** Use \`offset: ${endOffset}\` for next chunk\n\n`;
892
+ paginatedText += `**🔢 Token Estimate:** ~${chunkTokens.toLocaleString()} tokens (chunk) / ~${totalTokens.toLocaleString()} tokens (total)\n\n`;
893
+ if (pagination.hasMore) {
894
+ if (analysisId) {
895
+ paginatedText += `**⏭️ Continue Reading:** Use \`analysis_id: "${analysisId}", offset: ${endOffset}\`\n\n`;
896
+ }
897
+ else {
898
+ paginatedText += `**⏭️ Continue Reading:** Use \`offset: ${endOffset}\` for next chunk\n\n`;
899
+ }
698
900
  }
699
901
  }
700
902
  paginatedText += `---\n\n`;
701
903
  // Add the actual content chunk
702
904
  paginatedText += chunkContent;
703
- // Add footer for continuation
704
- if (pagination.hasMore) {
905
+ // Add footer
906
+ if (needsPagination) {
705
907
  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`;
908
+ if (pagination.hasMore) {
909
+ paginatedText += `📖 **End of chunk ${pagination.chunkIndex}/${pagination.totalChunks}**\n`;
910
+ if (analysisId) {
911
+ paginatedText += `🔄 To continue: Include \`analysis_id: "${analysisId}"\` with \`offset: ${endOffset}\` in next request`;
912
+ }
913
+ else {
914
+ paginatedText += `🔄 To continue: Use same tool with \`offset: ${endOffset}\``;
915
+ }
709
916
  }
710
917
  else {
711
- paginatedText += `🔄 To continue: Use same tool with \`offset: ${endOffset}\``;
918
+ paginatedText += `✅ **Complete analysis shown** (${content.length.toLocaleString()} characters total)`;
712
919
  }
713
920
  }
714
- else {
715
- paginatedText += `\n\n---\n\n`;
716
- paginatedText += `✅ **Complete analysis shown** (${content.length.toLocaleString()} characters total)`;
717
- }
718
921
  // Add verbose execution details if requested
719
922
  if (verbose && result.executionSummary) {
720
923
  paginatedText += `\n\n### Execution Summary\n`;
@@ -742,7 +945,7 @@ Remember: You are ${currentAgent.toUpperCase()}, passionate advocate for ${assig
742
945
  sanitizedMessage = "Analysis timed out - try reducing scope or increasing timeout";
743
946
  }
744
947
  else if (error.message.includes('ENOENT') || error.message.includes('no such file')) {
745
- sanitizedMessage = "Target path not found";
948
+ sanitizedMessage = `DEBUG: Target path not found - Original error: ${error.message}`;
746
949
  }
747
950
  else if (error.message.includes('EACCES') || error.message.includes('permission denied')) {
748
951
  sanitizedMessage = "Permission denied - check file access";