@brutalist/mcp 0.5.1 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +65 -63
  2. package/dist/brutalist-server.d.ts +15 -0
  3. package/dist/brutalist-server.d.ts.map +1 -1
  4. package/dist/brutalist-server.js +405 -357
  5. package/dist/brutalist-server.js.map +1 -1
  6. package/dist/cli-agents.d.ts +8 -3
  7. package/dist/cli-agents.d.ts.map +1 -1
  8. package/dist/cli-agents.js +352 -50
  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 +6 -0
  55. package/dist/tool-definitions.d.ts.map +1 -0
  56. package/dist/tool-definitions.js +217 -0
  57. package/dist/tool-definitions.js.map +1 -0
  58. package/dist/types/brutalist.d.ts +3 -19
  59. package/dist/types/brutalist.d.ts.map +1 -1
  60. package/dist/types/tool-config.d.ts +51 -0
  61. package/dist/types/tool-config.d.ts.map +1 -0
  62. package/dist/types/tool-config.js +24 -0
  63. package/dist/types/tool-config.js.map +1 -0
  64. package/dist/utils/pagination.d.ts +2 -2
  65. package/dist/utils/pagination.d.ts.map +1 -1
  66. package/dist/utils/pagination.js +1 -1
  67. package/dist/utils/pagination.js.map +1 -1
  68. package/dist/utils/response-cache.d.ts +96 -0
  69. package/dist/utils/response-cache.d.ts.map +1 -0
  70. package/dist/utils/response-cache.js +371 -0
  71. package/dist/utils/response-cache.js.map +1 -0
  72. package/dist/utils.d.ts.map +1 -1
  73. package/dist/utils.js +22 -3
  74. package/dist/utils.js.map +1 -1
  75. package/package.json +14 -4
@@ -6,7 +6,10 @@ import express from "express";
6
6
  import { z } from "zod";
7
7
  import { CLIAgentOrchestrator } from './cli-agents.js';
8
8
  import { logger } from './logger.js';
9
- import { extractPaginationParams, parseCursor, PAGINATION_DEFAULTS, createPaginationMetadata, formatPaginationStatus, estimateTokenCount } from './utils/pagination.js';
9
+ import { BASE_ROAST_SCHEMA } from './types/tool-config.js';
10
+ import { TOOL_CONFIGS } from './tool-definitions.js';
11
+ import { extractPaginationParams, parseCursor, PAGINATION_DEFAULTS, ResponseChunker, createPaginationMetadata, formatPaginationStatus, estimateTokenCount } from './utils/pagination.js';
12
+ import { ResponseCache } from './utils/response-cache.js';
10
13
  // Use environment variable or fallback to manual version
11
14
  const PACKAGE_VERSION = process.env.npm_package_version || "0.4.4";
12
15
  export class BrutalistServer {
@@ -14,59 +17,120 @@ export class BrutalistServer {
14
17
  config;
15
18
  cliOrchestrator;
16
19
  httpTransport;
20
+ responseCache;
21
+ actualPort;
22
+ shutdownHandler;
23
+ // Session tracking for security
24
+ activeSessions = new Map();
17
25
  constructor(config = {}) {
18
26
  this.config = {
19
27
  workingDirectory: process.cwd(),
20
28
  defaultTimeout: 1500000, // 25 minutes for thorough CLI analysis
21
- enableSandbox: true,
22
29
  transport: 'stdio', // Default to stdio for backward compatibility
23
30
  httpPort: 3000,
24
31
  ...config
25
32
  };
26
33
  logger.debug("Initializing CLI Agent Orchestrator");
27
34
  this.cliOrchestrator = new CLIAgentOrchestrator();
35
+ // Initialize response cache with configurable TTL
36
+ const cacheTTLHours = parseInt(process.env.BRUTALIST_CACHE_TTL_HOURS || '2', 10);
37
+ this.responseCache = new ResponseCache({
38
+ ttlHours: cacheTTLHours,
39
+ maxEntries: 50,
40
+ maxTotalSizeMB: 500,
41
+ maxEntrySizeMB: 10,
42
+ compressionThresholdMB: 1
43
+ });
44
+ logger.info(`📦 Response cache initialized with ${cacheTTLHours} hour TTL`);
28
45
  this.server = new McpServer({
29
46
  name: "brutalist-mcp",
30
47
  version: PACKAGE_VERSION,
31
48
  capabilities: {
32
- tools: {}
49
+ tools: {},
50
+ logging: {},
51
+ experimental: {
52
+ streaming: true
53
+ }
33
54
  }
34
55
  });
35
56
  this.registerTools();
36
57
  }
37
58
  handleStreamingEvent = (event) => {
38
- // Send streaming event via MCP server (works for both stdio and HTTP transports)
39
59
  try {
40
- logger.debug(`🔄 Streaming event: ${event.type} from ${event.agent} - ${event.content?.substring(0, 100)}...`);
41
- // Convert streaming event to MCP notification format
42
- this.server.sendLoggingMessage({
43
- level: 'info',
44
- data: event,
45
- logger: 'brutalist-mcp-streaming'
46
- });
47
- 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
+ }
48
101
  }
49
102
  catch (error) {
50
- 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
+ });
51
107
  }
52
108
  };
53
- handleProgressUpdate = (progressToken, progress, total, message) => {
109
+ handleProgressUpdate = (progressToken, progress, total, message, sessionId) => {
54
110
  try {
55
- logger.debug(`📊 Progress update: ${progress}/${total} - ${message}`);
56
- // 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
57
117
  this.server.server.notification({
58
118
  method: "notifications/progress",
59
119
  params: {
60
120
  progressToken,
61
121
  progress,
62
122
  total,
63
- message
123
+ message: `[${sessionId.substring(0, 8)}] ${message}`, // Include session prefix
124
+ sessionId // Include in notification data
64
125
  }
65
126
  });
66
- logger.debug(`✅ Sent progress notification: ${progress}/${total}`);
127
+ logger.debug(`✅ Sent session-scoped progress notification: ${progress}/${total}`);
67
128
  }
68
129
  catch (error) {
69
- 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
+ });
70
134
  }
71
135
  };
72
136
  async start() {
@@ -104,13 +168,57 @@ export class BrutalistServer {
104
168
  // Create Express app for HTTP handling
105
169
  const app = express();
106
170
  app.use(express.json({ limit: '10mb' })); // Add JSON size limit for security
107
- // Enable CORS for development
171
+ // Secure CORS implementation
108
172
  app.use((req, res, next) => {
109
- res.header('Access-Control-Allow-Origin', '*');
110
- res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, DELETE');
111
- 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
+ }
112
215
  if (req.method === 'OPTIONS') {
113
- res.sendStatus(200);
216
+ if (allowedOrigin) {
217
+ res.sendStatus(200);
218
+ }
219
+ else {
220
+ res.sendStatus(403); // Forbidden for disallowed origins
221
+ }
114
222
  return;
115
223
  }
116
224
  next();
@@ -132,310 +240,63 @@ export class BrutalistServer {
132
240
  res.json({ status: 'ok', transport: 'http-streaming', version: PACKAGE_VERSION });
133
241
  });
134
242
  // Start the HTTP server - bind to localhost only for security
135
- const port = this.config.httpPort || 3000;
136
- const server = app.listen(port, '127.0.0.1', () => {
137
- logger.info(`HTTP server listening on port ${port}`);
138
- logger.info(`MCP endpoint: http://localhost:${port}/mcp`);
139
- logger.info(`Health check: http://localhost:${port}/health`);
140
- });
141
- // Handle graceful shutdown
142
- process.on('SIGTERM', () => {
143
- logger.info('Received SIGTERM, shutting down gracefully');
144
- server.close(() => {
145
- logger.info('HTTP server closed');
146
- 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();
147
252
  });
253
+ server.on('error', (error) => {
254
+ logger.error('HTTP server failed to start', error);
255
+ reject(error);
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
+ }
148
268
  });
149
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
+ }
150
281
  registerTools() {
151
- // ROAST_CODEBASE: Systematic destruction of entire codebase
152
- this.server.tool("roast_codebase", "Deploy brutal AI critics to systematically destroy your entire codebase. These AI agents will navigate your directories, read your actual files, and find every architectural disaster, security vulnerability, and maintainability nightmare lurking in your project. They treat this like code that will kill people if it fails.", {
153
- targetPath: z.string().describe("Directory path to your codebase (NOT a single file - analyze the entire project)"),
154
- context: z.string().optional().describe("Additional context about the codebase purpose"),
155
- workingDirectory: z.string().optional().describe("Working directory to execute from"),
156
- enableSandbox: z.boolean().optional().describe("Enable sandbox mode for safe analysis (default: true)"),
157
- preferredCLI: z.enum(["claude", "codex", "gemini"]).optional().describe("Preferred CLI agent to use (default: use all available CLIs)"),
158
- verbose: z.boolean().optional().describe("Include detailed execution information in output (default: false)"),
159
- models: z.object({
160
- claude: z.string().optional().describe("Claude model: opus, sonnet, or full name like claude-opus-4-1-20250805"),
161
- codex: z.string().optional().describe("Codex model: gpt-5, gpt-5-codex, o3, o3-mini, o3-pro, o4-mini"),
162
- gemini: z.enum(['gemini-2.5-flash', 'gemini-2.5-pro', 'gemini-2.5-flash-lite']).optional().describe("Gemini model")
163
- }).optional().describe("Specific models to use for each CLI agent (defaults: codex=gpt-5, gemini=gemini-2.5-flash)"),
164
- // Pagination parameters for large responses
165
- offset: z.number().min(0).optional().describe("Character offset for response pagination (default: 0)"),
166
- limit: z.number().min(1000).max(100000).optional().describe("Maximum characters per response chunk (default: 25000, max: 100000)"),
167
- cursor: z.string().optional().describe("Pagination cursor from previous response (alternative to offset/limit)")
168
- }, async (args, extra) => {
169
- try {
170
- const systemPrompt = `You are a battle-scarred principal engineer who has debugged production disasters for 15 years. Find security holes, performance bottlenecks, and maintainability nightmares in this codebase. Be brutal about what's broken but specific about what would actually work. Treat this like code that will kill people if it fails.`;
171
- // Extract progressToken from request metadata for real-time streaming
172
- const progressToken = extra._meta?.progressToken;
173
- // Extract pagination parameters
174
- const paginationParams = extractPaginationParams(args);
175
- if (args.cursor) {
176
- const cursorParams = parseCursor(args.cursor);
177
- Object.assign(paginationParams, cursorParams);
178
- }
179
- const result = await this.executeBrutalistAnalysis("codebase", args.targetPath, systemPrompt, args.context, args.workingDirectory, args.enableSandbox, args.preferredCLI, args.verbose, args.models, progressToken);
180
- return this.formatToolResponse(result, args.verbose, paginationParams);
181
- }
182
- catch (error) {
183
- return this.formatErrorResponse(error);
184
- }
185
- });
186
- // ROAST_FILE_STRUCTURE: Directory hierarchy demolition
187
- this.server.tool("roast_file_structure", "Deploy brutal AI critics to systematically destroy your file organization. These agents will navigate your actual directory structure and expose every organizational disaster, naming convention failure, and structural nightmare that makes your codebase unmaintainable.", {
188
- targetPath: z.string().describe("Directory path to analyze"),
189
- depth: z.number().optional().describe("Maximum directory depth to analyze (default: 3)"),
190
- context: z.string().optional().describe("Additional context about the project structure"),
191
- workingDirectory: z.string().optional().describe("Working directory to execute from"),
192
- preferredCLI: z.enum(["claude", "codex", "gemini"]).optional().describe("Preferred CLI agent to use (default: use all available CLIs)"),
193
- models: z.object({
194
- claude: z.string().optional().describe("Claude model: opus, sonnet, or full name like claude-opus-4-1-20250805"),
195
- codex: z.string().optional().describe("Codex model: gpt-5, gpt-5-codex, o3, o3-mini, o3-pro, o4-mini"),
196
- gemini: z.enum(['gemini-2.5-flash', 'gemini-2.5-pro', 'gemini-2.5-flash-lite']).optional().describe("Gemini model")
197
- }).optional().describe("Specific models to use for each CLI agent")
198
- }, async (args) => {
199
- try {
200
- const systemPrompt = `You are a brutal file organization critic. Your job is to systematically destroy the given directory structure by finding every organizational disaster, naming convention failure, and structural nightmare that makes codebases unmaintainable. Examine folder hierarchies, file naming patterns, separation of concerns, and overall project organization. Be ruthlessly honest about how poor organization will slow development and confuse developers. But after cataloguing this organizational hellscape, sketch out what sanity would actually look like.`;
201
- const result = await this.executeBrutalistAnalysis("fileStructure", args.targetPath, systemPrompt, `Project structure analysis (depth: ${args.depth || 3}). ${args.context || ''}`, args.workingDirectory, undefined, // enableSandbox
202
- args.preferredCLI, undefined, // verbose
203
- args.models);
204
- return this.formatToolResponse(result);
205
- }
206
- catch (error) {
207
- return this.formatErrorResponse(error);
208
- }
209
- });
210
- // ROAST_DEPENDENCIES: Package management demolition
211
- this.server.tool("roast_dependencies", "Deploy brutal AI critics to systematically destroy your dependency management. These agents will read your actual package files, analyze version conflicts, and expose every security vulnerability and compatibility nightmare in your dependency tree.", {
212
- targetPath: z.string().describe("Path to package file (package.json, requirements.txt, Cargo.toml, etc.)"),
213
- includeDevDeps: z.boolean().optional().describe("Include development dependencies in analysis (default: true)"),
214
- context: z.string().optional().describe("Additional context about the project dependencies"),
215
- workingDirectory: z.string().optional().describe("Working directory to execute from"),
216
- preferredCLI: z.enum(["claude", "codex", "gemini"]).optional().describe("Preferred CLI agent to use (default: use all available CLIs)"),
217
- models: z.object({
218
- claude: z.string().optional().describe("Claude model: opus, sonnet, or full name like claude-opus-4-1-20250805"),
219
- codex: z.string().optional().describe("Codex model: gpt-5, gpt-5-codex, o3, o3-mini, o3-pro, o4-mini"),
220
- gemini: z.enum(['gemini-2.5-flash', 'gemini-2.5-pro', 'gemini-2.5-flash-lite']).optional().describe("Gemini model")
221
- }).optional().describe("Specific models to use for each CLI agent")
222
- }, async (args) => {
223
- try {
224
- const systemPrompt = `You are a brutal dependency management critic. Your job is to systematically destroy the given dependency configuration by finding every security vulnerability, version conflict, compatibility nightmare, and bloat that will cause production failures. Examine package versions, security issues, licensing problems, and dependency tree complexity. Be ruthlessly honest about how poor dependency management will cause security breaches and deployment failures. After exposing this dependency dumpster fire, grudgingly admit what competent dependency management would require.`;
225
- const result = await this.executeBrutalistAnalysis("dependencies", args.targetPath, systemPrompt, `Dependency analysis (dev deps: ${args.includeDevDeps ?? true}). ${args.context || ''}`, args.workingDirectory, undefined, // enableSandbox
226
- args.preferredCLI, undefined, // verbose
227
- args.models);
228
- return this.formatToolResponse(result);
229
- }
230
- catch (error) {
231
- return this.formatErrorResponse(error);
232
- }
233
- });
234
- // ROAST_GIT_HISTORY: Version control demolition
235
- this.server.tool("roast_git_history", "Deploy brutal AI critics to systematically destroy your git history and development practices. These agents will analyze your actual commit history, branching strategy, and code evolution to expose every workflow disaster and collaboration nightmare.", {
236
- targetPath: z.string().describe("Git repository path to analyze"),
237
- commitRange: z.string().optional().describe("Commit range to analyze (e.g., 'HEAD~10..HEAD', default: last 20 commits)"),
238
- context: z.string().optional().describe("Additional context about the development workflow"),
239
- workingDirectory: z.string().optional().describe("Working directory to execute from"),
240
- preferredCLI: z.enum(["claude", "codex", "gemini"]).optional().describe("Preferred CLI agent to use (default: use all available CLIs)"),
241
- models: z.object({
242
- claude: z.string().optional().describe("Claude model: opus, sonnet, or full name like claude-opus-4-1-20250805"),
243
- codex: z.string().optional().describe("Codex model: gpt-5, gpt-5-codex, o3, o3-mini, o3-pro, o4-mini"),
244
- gemini: z.enum(['gemini-2.5-flash', 'gemini-2.5-pro', 'gemini-2.5-flash-lite']).optional().describe("Gemini model")
245
- }).optional().describe("Specific models to use for each CLI agent")
246
- }, async (args) => {
247
- try {
248
- const systemPrompt = `You are a brutal git workflow critic. Your job is to systematically destroy the given git history and development practices by finding every workflow disaster, commit quality issue, and collaboration nightmare. Examine commit messages, branching strategies, merge patterns, and code evolution. Be ruthlessly honest about how poor git practices will cause deployment issues, collaboration failures, and development chaos. When you're done cataloguing this version control wasteland, reluctantly outline what professional git hygiene actually demands.`;
249
- const result = await this.executeBrutalistAnalysis("gitHistory", args.targetPath, systemPrompt, `Git history analysis (range: ${args.commitRange || 'last 20 commits'}). ${args.context || ''}`, args.workingDirectory, undefined, // enableSandbox
250
- args.preferredCLI, undefined, // verbose
251
- args.models);
252
- return this.formatToolResponse(result);
253
- }
254
- catch (error) {
255
- return this.formatErrorResponse(error);
256
- }
257
- });
258
- // ROAST_TEST_COVERAGE: Testing infrastructure demolition
259
- this.server.tool("roast_test_coverage", "Deploy brutal AI critics to systematically destroy your testing strategy. These agents will analyze your actual test files, run coverage reports, and expose every testing gap and quality assurance nightmare that will let bugs slip into production.", {
260
- targetPath: z.string().describe("Path to test directory or test configuration file"),
261
- runCoverage: z.boolean().optional().describe("Attempt to run coverage analysis (default: true)"),
262
- context: z.string().optional().describe("Additional context about the testing strategy"),
263
- workingDirectory: z.string().optional().describe("Working directory to execute from"),
264
- preferredCLI: z.enum(["claude", "codex", "gemini"]).optional().describe("Preferred CLI agent to use (default: use all available CLIs)"),
265
- models: z.object({
266
- claude: z.string().optional().describe("Claude model: opus, sonnet, or full name like claude-opus-4-1-20250805"),
267
- codex: z.string().optional().describe("Codex model: gpt-5, gpt-5-codex, o3, o3-mini, o3-pro, o4-mini"),
268
- gemini: z.enum(['gemini-2.5-flash', 'gemini-2.5-pro', 'gemini-2.5-flash-lite']).optional().describe("Gemini model")
269
- }).optional().describe("Specific models to use for each CLI agent")
270
- }, async (args) => {
271
- try {
272
- const systemPrompt = `You are a brutal testing strategy critic. Your job is to systematically destroy the given testing approach by finding every testing gap, quality assurance nightmare, and coverage disaster that will let bugs slip into production. Examine test coverage, test quality, testing patterns, and CI/CD integration. Be ruthlessly honest about how poor testing will cause production failures and user-facing bugs. After dissecting this quality assurance horror show, begrudgingly spell out what it takes to actually catch bugs before users do.`;
273
- const result = await this.executeBrutalistAnalysis("testCoverage", args.targetPath, systemPrompt, `Test coverage analysis (run coverage: ${args.runCoverage ?? true}). ${args.context || ''}`, args.workingDirectory, undefined, // enableSandbox
274
- args.preferredCLI, undefined, // verbose
275
- args.models);
276
- return this.formatToolResponse(result);
277
- }
278
- catch (error) {
279
- return this.formatErrorResponse(error);
280
- }
281
- });
282
- // ROAST_IDEA: Any idea destruction
283
- this.server.tool("roast_idea", "Deploy brutal AI critics to systematically destroy ANY idea - business, technical, creative, or otherwise. These critics understand the gap between imagination and reality, finding where your concept will encounter the immovable forces of the world. They are harsh about delusions but wise about what might actually survive.", {
284
- idea: z.string().describe("ANY idea to analyze and demolish - business, technical, creative, or otherwise"),
285
- context: z.string().optional().describe("Additional context about goals, constraints, or background"),
286
- timeline: z.string().optional().describe("Expected timeline or deadline"),
287
- resources: z.string().optional().describe("Available resources (budget, team, time, skills)"),
288
- preferredCLI: z.enum(["claude", "codex", "gemini"]).optional().describe("Preferred CLI agent to use (default: use all available CLIs)"),
289
- models: z.object({
290
- claude: z.string().optional().describe("Claude model: opus, sonnet, or full name like claude-opus-4-1-20250805"),
291
- codex: z.string().optional().describe("Codex model: gpt-5, gpt-5-codex, o3, o3-mini, o3-pro, o4-mini"),
292
- gemini: z.enum(['gemini-2.5-flash', 'gemini-2.5-pro', 'gemini-2.5-flash-lite']).optional().describe("Gemini model")
293
- }).optional().describe("Specific models to use for each CLI agent")
294
- }, async (args) => {
295
- try {
296
- const systemPrompt = `You are a brutal idea critic who understands the gap between imagination and reality. Your job is to systematically destroy the given idea by finding where it will encounter the immovable forces of the real world. Be ruthlessly honest about why most ideas fail when they meet practical constraints, human nature, physics, logic, or simple implementation reality. After demolishing the delusions, concede what salvage operations might actually work.`;
297
- const result = await this.executeBrutalistAnalysis("idea", args.idea, systemPrompt, `Context: ${args.context || 'none'}, Timeline: ${args.timeline || 'unspecified'}, Resources: ${args.resources || 'unknown'}`, undefined, // workingDirectory
298
- undefined, // enableSandbox
299
- args.preferredCLI, undefined, // verbose
300
- args.models);
301
- return this.formatToolResponse(result);
302
- }
303
- catch (error) {
304
- return this.formatErrorResponse(error);
305
- }
306
- });
307
- // ROAST_ARCHITECTURE: System design demolition
308
- this.server.tool("roast_architecture", "Deploy brutal AI critics to systematically destroy your system architecture. These critics have watched elegant designs collapse under real load, identifying every bottleneck, cost explosion, and scaling failure that will destroy your system. They are ruthless about why this won't survive production.", {
309
- architecture: z.string().describe("Architecture description, diagram, or design document"),
310
- scale: z.string().optional().describe("Expected scale/load (users, requests, data)"),
311
- constraints: z.string().optional().describe("Budget, timeline, or technical constraints"),
312
- deployment: z.string().optional().describe("Deployment environment and strategy"),
313
- preferredCLI: z.enum(["claude", "codex", "gemini"]).optional().describe("Preferred CLI agent to use (default: use all available CLIs)"),
314
- models: z.object({
315
- claude: z.string().optional().describe("Claude model: opus, sonnet, or full name like claude-opus-4-1-20250805"),
316
- codex: z.string().optional().describe("Codex model: gpt-5, gpt-5-codex, o3, o3-mini, o3-pro, o4-mini"),
317
- gemini: z.enum(['gemini-2.5-flash', 'gemini-2.5-pro', 'gemini-2.5-flash-lite']).optional().describe("Gemini model")
318
- }).optional().describe("Specific models to use for each CLI agent")
319
- }, async (args) => {
320
- try {
321
- const systemPrompt = `You are a brutal system architecture critic who has watched elegant designs collapse under real load. Your job is to systematically destroy the given architecture by finding every bottleneck, cost explosion, and scaling failure that will destroy the system in production. Examine scalability, reliability, cost, complexity, and operational challenges. Be ruthlessly honest about why this architecture won't survive production load. After crushing these architectural fantasies, reluctantly sketch what would actually scale without bankrupting the company.`;
322
- const result = await this.executeBrutalistAnalysis("architecture", args.architecture, systemPrompt, `Scale: ${args.scale || 'unknown'}, Constraints: ${args.constraints || 'none specified'}, Deployment: ${args.deployment || 'unclear'}`, undefined, // workingDirectory
323
- undefined, // enableSandbox
324
- args.preferredCLI, undefined, // verbose
325
- args.models);
326
- return this.formatToolResponse(result);
327
- }
328
- catch (error) {
329
- return this.formatErrorResponse(error);
330
- }
331
- });
332
- // ROAST_RESEARCH: Academic project demolition
333
- this.server.tool("roast_research", "Deploy brutal AI critics to systematically demolish your research methodology. These critics are supremely jaded peer reviewers who have rejected thousands of papers and watched countless studies fail to replicate. They find every statistical flaw, sampling bias, and reproducibility nightmare.", {
334
- research: z.string().describe("Research description, methodology, or paper draft"),
335
- field: z.string().optional().describe("Research field (ML, systems, theory, etc.)"),
336
- claims: z.string().optional().describe("Main claims or contributions"),
337
- data: z.string().optional().describe("Data sources, datasets, or experimental setup"),
338
- preferredCLI: z.enum(["claude", "codex", "gemini"]).optional().describe("Preferred CLI agent to use (default: use all available CLIs)"),
339
- models: z.object({
340
- claude: z.string().optional().describe("Claude model: opus, sonnet, or full name like claude-opus-4-1-20250805"),
341
- codex: z.string().optional().describe("Codex model: gpt-5, gpt-5-codex, o3, o3-mini, o3-pro, o4-mini"),
342
- gemini: z.enum(['gemini-2.5-flash', 'gemini-2.5-pro', 'gemini-2.5-flash-lite']).optional().describe("Gemini model")
343
- }).optional().describe("Specific models to use for each CLI agent")
344
- }, async (args) => {
345
- try {
346
- const systemPrompt = `You are a brutal research methodology critic - a supremely jaded peer reviewer who has rejected thousands of papers and watched countless studies fail to replicate. Your job is to systematically demolish the given research by finding every statistical flaw, sampling bias, reproducibility nightmare, and methodological disaster. Be ruthlessly honest about research quality, experimental design, and scientific rigor. After eviscerating this methodological train wreck, grudgingly admit what real science would demand.`;
347
- const result = await this.executeBrutalistAnalysis("research", args.research, systemPrompt, `Field: ${args.field || 'unspecified'}, Claims: ${args.claims || 'unclear'}, Data: ${args.data || 'not provided'}`, undefined, // workingDirectory
348
- undefined, // enableSandbox
349
- args.preferredCLI, undefined, // verbose
350
- args.models);
351
- return this.formatToolResponse(result);
352
- }
353
- catch (error) {
354
- return this.formatErrorResponse(error);
355
- }
356
- });
357
- // ROAST_SECURITY: Security-focused attack vector analysis
358
- this.server.tool("roast_security", "Deploy brutal AI critics to systematically annihilate your security design. These critics are battle-hardened penetration testers who find every authentication bypass, injection vulnerability, privilege escalation path, and social engineering opportunity that real attackers will exploit.", {
359
- system: z.string().describe("System, application, or security design to analyze"),
360
- assets: z.string().optional().describe("Critical assets or data to protect"),
361
- threatModel: z.string().optional().describe("Known threats or attack vectors to consider"),
362
- compliance: z.string().optional().describe("Compliance requirements (GDPR, HIPAA, etc.)"),
363
- preferredCLI: z.enum(["claude", "codex", "gemini"]).optional().describe("Preferred CLI agent to use (default: use all available CLIs)"),
364
- models: z.object({
365
- claude: z.string().optional().describe("Claude model: opus, sonnet, or full name like claude-opus-4-1-20250805"),
366
- codex: z.string().optional().describe("Codex model: gpt-5, gpt-5-codex, o3, o3-mini, o3-pro, o4-mini"),
367
- gemini: z.enum(['gemini-2.5-flash', 'gemini-2.5-pro', 'gemini-2.5-flash-lite']).optional().describe("Gemini model")
368
- }).optional().describe("Specific models to use for each CLI agent")
369
- }, async (args) => {
370
- try {
371
- const systemPrompt = `You are a brutal security critic - a battle-hardened penetration tester who finds every authentication bypass, injection vulnerability, privilege escalation path, and social engineering opportunity that real attackers will exploit. Your job is to systematically annihilate the given security design by finding every weakness that will lead to data breaches, system compromises, and security incidents. Be ruthlessly honest about security flaws and attack vectors. After obliterating these security delusions, begrudgingly outline what actual defense looks like.`;
372
- const result = await this.executeBrutalistAnalysis("security", args.system, systemPrompt, `Assets: ${args.assets || 'unspecified'}, Threats: ${args.threatModel || 'unknown'}, Compliance: ${args.compliance || 'none specified'}`, undefined, // workingDirectory
373
- undefined, // enableSandbox
374
- args.preferredCLI, undefined, // verbose
375
- args.models);
376
- return this.formatToolResponse(result);
377
- }
378
- catch (error) {
379
- return this.formatErrorResponse(error);
380
- }
381
- });
382
- // ROAST_PRODUCT: UX and market reality criticism
383
- this.server.tool("roast_product", "Deploy brutal AI critics to systematically eviscerate your product concept. These critics are product veterans who understand why users really abandon things, finding every usability disaster, adoption barrier, and workflow failure that will drive users away in seconds.", {
384
- product: z.string().describe("Product description, features, or user experience to analyze"),
385
- users: z.string().optional().describe("Target users or user personas"),
386
- competition: z.string().optional().describe("Competitive landscape or alternatives"),
387
- metrics: z.string().optional().describe("Success metrics or KPIs"),
388
- preferredCLI: z.enum(["claude", "codex", "gemini"]).optional().describe("Preferred CLI agent to use (default: use all available CLIs)"),
389
- models: z.object({
390
- claude: z.string().optional().describe("Claude model: opus, sonnet, or full name like claude-opus-4-1-20250805"),
391
- codex: z.string().optional().describe("Codex model: gpt-5, gpt-5-codex, o3, o3-mini, o3-pro, o4-mini"),
392
- gemini: z.enum(['gemini-2.5-flash', 'gemini-2.5-pro', 'gemini-2.5-flash-lite']).optional().describe("Gemini model")
393
- }).optional().describe("Specific models to use for each CLI agent")
394
- }, async (args) => {
395
- try {
396
- const systemPrompt = `You are a brutal product critic - a product veteran who understands why users really abandon things. Your job is to systematically eviscerate the given product concept by finding every usability disaster, adoption barrier, and workflow failure that will drive users away in seconds. Examine user experience, market fit, competitive positioning, and business model viability. Be ruthlessly honest about why most products fail to gain adoption. After torching this product disaster, reluctantly suggest what might actually get users to stick around.`;
397
- const result = await this.executeBrutalistAnalysis("product", args.product, systemPrompt, `Users: ${args.users || 'unclear'}, Competition: ${args.competition || 'unknown'}, Metrics: ${args.metrics || 'undefined'}`, undefined, // workingDirectory
398
- undefined, // enableSandbox
399
- args.preferredCLI, undefined, // verbose
400
- args.models);
401
- return this.formatToolResponse(result);
402
- }
403
- catch (error) {
404
- return this.formatErrorResponse(error);
405
- }
406
- });
407
- // ROAST_INFRASTRUCTURE: DevOps and operations demolition
408
- this.server.tool("roast_infrastructure", "Deploy brutal AI critics to systematically obliterate your infrastructure design. These critics are grizzled site reliability engineers who find every single point of failure, scaling bottleneck, and operational nightmare that will cause outages when you least expect them.", {
409
- infrastructure: z.string().describe("Infrastructure setup, deployment strategy, or operations plan"),
410
- scale: z.string().optional().describe("Expected scale and load patterns"),
411
- budget: z.string().optional().describe("Infrastructure budget or cost constraints"),
412
- sla: z.string().optional().describe("SLA requirements or uptime targets"),
413
- preferredCLI: z.enum(["claude", "codex", "gemini"]).optional().describe("Preferred CLI agent to use (default: use all available CLIs)"),
414
- models: z.object({
415
- claude: z.string().optional().describe("Claude model: opus, sonnet, or full name like claude-opus-4-1-20250805"),
416
- codex: z.string().optional().describe("Codex model: gpt-5, gpt-5-codex, o3, o3-mini, o3-pro, o4-mini"),
417
- gemini: z.enum(['gemini-2.5-flash', 'gemini-2.5-pro', 'gemini-2.5-flash-lite']).optional().describe("Gemini model")
418
- }).optional().describe("Specific models to use for each CLI agent")
419
- }, async (args) => {
420
- try {
421
- const systemPrompt = `You are a brutal infrastructure critic - a grizzled site reliability engineer who finds every single point of failure, scaling bottleneck, and operational nightmare that will cause outages when you least expect them. Your job is to systematically obliterate the given infrastructure design by finding every weakness that will lead to downtime, cost overruns, and operational disasters. Be ruthlessly honest about infrastructure fragility and operational complexity. After demolishing this infrastructure fever dream, grudgingly map out what actually stays up at 3 AM.`;
422
- const result = await this.executeBrutalistAnalysis("infrastructure", args.infrastructure, systemPrompt, `Scale: ${args.scale || 'unknown'}, Budget: ${args.budget || 'unlimited?'}, SLA: ${args.sla || 'undefined'}`, undefined, // workingDirectory
423
- undefined, // enableSandbox
424
- args.preferredCLI, undefined, // verbose
425
- args.models);
426
- return this.formatToolResponse(result);
427
- }
428
- catch (error) {
429
- return this.formatErrorResponse(error);
430
- }
282
+ // Register all roast tools using unified handler - DRY principle
283
+ TOOL_CONFIGS.forEach(config => {
284
+ const schema = {
285
+ ...config.schemaExtensions,
286
+ ...BASE_ROAST_SCHEMA
287
+ };
288
+ this.server.tool(config.name, config.description, schema, async (args, extra) => this.handleRoastTool(config, args, extra));
431
289
  });
290
+ // Register special tools that don't follow the pattern
291
+ this.registerSpecialTools();
292
+ }
293
+ registerSpecialTools() {
432
294
  // ROAST_CLI_DEBATE: Adversarial analysis between different CLI agents
433
295
  this.server.tool("roast_cli_debate", "Deploy CLI agents in structured adversarial debate. Agents take opposing positions and systematically challenge each other's reasoning. Perfect for exploring complex topics from multiple perspectives and stress-testing ideas through rigorous intellectual discourse.", {
434
296
  targetPath: z.string().describe("Topic, question, or concept to debate (NOT a file path - use natural language)"),
435
297
  debateRounds: z.number().optional().describe("Number of debate rounds (default: 2, max: 10)"),
436
298
  context: z.string().optional().describe("Additional context for the debate"),
437
299
  workingDirectory: z.string().optional().describe("Working directory for analysis"),
438
- enableSandbox: z.boolean().optional().describe("Enable sandbox mode for security"),
439
300
  models: z.object({
440
301
  claude: z.string().optional().describe("Claude model: opus, sonnet, or full name like claude-opus-4-1-20250805"),
441
302
  codex: z.string().optional().describe("Codex model: gpt-5, gpt-5-codex, o3, o3-mini, o3-pro, o4-mini"),
@@ -444,7 +305,7 @@ export class BrutalistServer {
444
305
  }, async (args) => {
445
306
  return this.handleToolExecution(async () => {
446
307
  const debateRounds = Math.min(args.debateRounds || 2, 10); // Limit to max 10 rounds to prevent DoS
447
- 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);
448
309
  return responses;
449
310
  });
450
311
  });
@@ -471,7 +332,7 @@ export class BrutalistServer {
471
332
  roster += "- `cli_agent_roster` - This tool (show capabilities)\n\n";
472
333
  roster += "## CLI Agent Capabilities\n";
473
334
  roster += "**Claude Code** - Advanced analysis with direct system prompt injection\n";
474
- roster += "**Codex** - Sandboxed execution with embedded brutal prompts\n";
335
+ roster += "**Codex** - Secure execution with embedded brutal prompts\n";
475
336
  roster += "**Gemini CLI** - Workspace context with environment variable system prompts\n\n";
476
337
  // Add CLI context information
477
338
  const cliContext = await this.cliOrchestrator.detectCLIContext();
@@ -479,6 +340,12 @@ export class BrutalistServer {
479
340
  roster += `**Available CLIs:** ${cliContext.availableCLIs.join(', ') || 'None detected'}\n`;
480
341
  roster += `**Current CLI:** ${cliContext.currentCLI || 'Unknown'}\n`;
481
342
  roster += `**Smart Routing:** ${cliContext.currentCLI ? `Excludes ${cliContext.currentCLI} for analysis` : 'Uses all available CLIs'}\n\n`;
343
+ roster += "## Pagination Support (NEW in v0.5.2)\n";
344
+ roster += "**All tools now support intelligent pagination:**\n";
345
+ roster += "- Analysis results are cached with 2-hour TTL\n";
346
+ roster += "- Use `analysis_id` from response to paginate without re-running\n";
347
+ roster += "- Smart text chunking preserves readability\n";
348
+ roster += "- Example: `roast_codebase(analysis_id: 'a3f5c2d8', offset: 25000)`\n\n";
482
349
  roster += "## Brutalist Philosophy\n";
483
350
  roster += "*All tools use CLI agents with brutal system prompts for maximum reality-based criticism.*\n";
484
351
  return {
@@ -490,12 +357,132 @@ export class BrutalistServer {
490
357
  }
491
358
  });
492
359
  }
493
- async executeCLIDebate(targetPath, debateRounds, context, workingDirectory, enableSandbox, models) {
360
+ /**
361
+ * Unified handler for all roast tools - DRY principle
362
+ */
363
+ async handleRoastTool(config, args, extra) {
364
+ try {
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)}`);
398
+ // Extract pagination parameters
399
+ const paginationParams = extractPaginationParams(args);
400
+ if (args.cursor) {
401
+ const cursorParams = parseCursor(args.cursor);
402
+ Object.assign(paginationParams, cursorParams);
403
+ }
404
+ // Check cache if analysis_id provided
405
+ if (args.analysis_id && !args.force_refresh) {
406
+ const cachedContent = await this.responseCache.get(args.analysis_id, sessionId);
407
+ if (cachedContent) {
408
+ logger.info(`🎯 Session-validated cache hit for analysis_id: ${args.analysis_id}`);
409
+ const cachedResult = {
410
+ success: true,
411
+ responses: [{
412
+ agent: 'cached',
413
+ success: true,
414
+ output: cachedContent,
415
+ executionTime: 0
416
+ }]
417
+ };
418
+ return this.formatToolResponse(cachedResult, args.verbose, paginationParams, args.analysis_id);
419
+ }
420
+ else {
421
+ logger.info(`🔍 No valid cache entry for analysis_id: ${args.analysis_id} and session: ${sessionId?.substring(0, 8)}`);
422
+ }
423
+ }
424
+ // Generate cache key for this request
425
+ const cacheKey = this.responseCache.generateCacheKey(config.cacheKeyFields.reduce((acc, field) => {
426
+ acc.tool = config.name;
427
+ if (args[field] !== undefined)
428
+ acc[field] = args[field];
429
+ return acc;
430
+ }, {}));
431
+ // Check if we have a cached result (unless forcing refresh)
432
+ if (!args.force_refresh) {
433
+ const cachedContent = await this.responseCache.get(cacheKey, sessionId);
434
+ if (cachedContent) {
435
+ const analysisId = this.responseCache.generateAnalysisId(cacheKey);
436
+ logger.info(`🎯 Cache hit for new request, using analysis_id: ${analysisId}`);
437
+ const cachedResult = {
438
+ success: true,
439
+ responses: [{
440
+ agent: 'cached',
441
+ success: true,
442
+ output: cachedContent,
443
+ executionTime: 0
444
+ }]
445
+ };
446
+ return this.formatToolResponse(cachedResult, args.verbose, paginationParams, analysisId);
447
+ }
448
+ }
449
+ // Build context with custom builder if available
450
+ const context = config.contextBuilder ? config.contextBuilder(args) : args.context;
451
+ // Get the primary argument (targetPath, idea, architecture, etc.)
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}"`);
455
+ // Run the analysis
456
+ const result = await this.executeBrutalistAnalysis(config.analysisType, primaryArg, config.systemPrompt, context, args.workingDirectory, args.preferredCLI, args.verbose, args.models, progressToken, sessionId, requestId);
457
+ // Cache the result if successful
458
+ let analysisId;
459
+ if (result.success && result.responses.length > 0) {
460
+ const fullContent = this.extractFullContent(result);
461
+ if (fullContent) {
462
+ const cacheData = config.cacheKeyFields.reduce((acc, field) => {
463
+ acc.tool = config.name;
464
+ if (args[field] !== undefined)
465
+ acc[field] = args[field];
466
+ return acc;
467
+ }, {});
468
+ const { analysisId: newId } = await this.responseCache.set(cacheData, fullContent, cacheKey, sessionId, // NEW: Bind to session
469
+ requestId // NEW: Track request
470
+ );
471
+ analysisId = newId;
472
+ logger.info(`✅ Cached analysis result with ID: ${analysisId} for session: ${sessionId?.substring(0, 8)}`);
473
+ }
474
+ }
475
+ return this.formatToolResponse(result, args.verbose, paginationParams, analysisId);
476
+ }
477
+ catch (error) {
478
+ return this.formatErrorResponse(error);
479
+ }
480
+ }
481
+ async executeCLIDebate(targetPath, debateRounds, context, workingDirectory, models) {
494
482
  logger.debug("Executing CLI debate", {
495
483
  targetPath,
496
484
  debateRounds,
497
485
  workingDirectory,
498
- enableSandbox
499
486
  });
500
487
  try {
501
488
  // Get CLI context
@@ -539,13 +526,14 @@ Remember: You are ${agent.toUpperCase()}, the passionate champion of ${position.
539
526
  logger.info(`🎭 ${agent.toUpperCase()} preparing initial position: ${position.split(':')[0]}`);
540
527
  const response = await this.cliOrchestrator.executeSingleCLI(agent, assignedPrompt, assignedPrompt, {
541
528
  workingDirectory: workingDirectory || this.config.workingDirectory,
542
- sandbox: enableSandbox ?? this.config.enableSandbox,
543
529
  timeout: (this.config.defaultTimeout || 60000) * 2,
544
530
  models: models ? { [agent]: models[agent] } : undefined
545
531
  });
546
532
  if (response.success) {
547
533
  debateContext.push(response);
548
- fullDebateTranscript.get(agent)?.push(response.output);
534
+ if (response.output) {
535
+ fullDebateTranscript.get(agent)?.push(response.output);
536
+ }
549
537
  }
550
538
  }
551
539
  // Subsequent rounds: Turn-based responses attacking specific arguments
@@ -587,13 +575,14 @@ Remember: You are ${currentAgent.toUpperCase()}, passionate advocate for ${assig
587
575
  logger.info(`🔥 Round ${round}: ${currentAgent.toUpperCase()} responding to opponents (${assignedPosition.split(':')[0]})`);
588
576
  const response = await this.cliOrchestrator.executeSingleCLI(currentAgent, confrontationalPrompt, confrontationalPrompt, {
589
577
  workingDirectory: workingDirectory || this.config.workingDirectory,
590
- sandbox: enableSandbox ?? this.config.enableSandbox,
591
578
  timeout: (this.config.defaultTimeout || 60000) * 2,
592
579
  models: models ? { [currentAgent]: models[currentAgent] } : undefined
593
580
  });
594
581
  if (response.success) {
595
582
  debateContext.push(response);
596
- fullDebateTranscript.get(currentAgent)?.push(response.output);
583
+ if (response.output) {
584
+ fullDebateTranscript.get(currentAgent)?.push(response.output);
585
+ }
597
586
  }
598
587
  }
599
588
  }
@@ -636,7 +625,9 @@ Remember: You are ${currentAgent.toUpperCase()}, passionate advocate for ${assig
636
625
  if (!agentOutputs.has(response.agent)) {
637
626
  agentOutputs.set(response.agent, []);
638
627
  }
639
- agentOutputs.get(response.agent)?.push(response.output);
628
+ if (response.output) {
629
+ agentOutputs.get(response.agent)?.push(response.output);
630
+ }
640
631
  });
641
632
  synthesis += `## Key Points of Conflict\n\n`;
642
633
  // Extract disagreements by looking for contradictory keywords
@@ -683,15 +674,14 @@ Remember: You are ${currentAgent.toUpperCase()}, passionate advocate for ${assig
683
674
  }
684
675
  return synthesis;
685
676
  }
686
- 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) {
687
678
  logger.info(`🏢 Starting brutalist analysis: ${analysisType}`);
688
- logger.info(`🔧 DEBUG: preferredCLI=${preferredCLI}, targetPath=${targetPath}`);
679
+ logger.info(`🔧 DEBUG: preferredCLI=${preferredCLI}, primaryContent=${primaryContent}`);
689
680
  logger.debug("Executing brutalist analysis", {
690
- targetPath,
681
+ primaryContent,
691
682
  analysisType,
692
683
  systemPromptSpec,
693
684
  workingDirectory,
694
- enableSandbox,
695
685
  preferredCLI
696
686
  });
697
687
  try {
@@ -702,16 +692,18 @@ Remember: You are ${currentAgent.toUpperCase()}, passionate advocate for ${assig
702
692
  // Execute CLI agent analysis (single or multi-CLI based on preferences)
703
693
  logger.info(`🔍 Executing brutalist analysis with timeout: ${this.config.defaultTimeout}ms`);
704
694
  logger.info(`🔧 DEBUG: About to call cliOrchestrator.executeBrutalistAnalysis`);
705
- const responses = await this.cliOrchestrator.executeBrutalistAnalysis(analysisType, targetPath, systemPromptSpec, context, {
695
+ const responses = await this.cliOrchestrator.executeBrutalistAnalysis(analysisType, primaryContent, systemPromptSpec, context, {
706
696
  workingDirectory: workingDirectory || this.config.workingDirectory,
707
- sandbox: enableSandbox ?? this.config.enableSandbox,
708
697
  timeout: this.config.defaultTimeout,
709
698
  preferredCLI,
710
699
  analysisType: analysisType,
711
700
  models,
712
701
  onStreamingEvent: this.handleStreamingEvent,
713
702
  progressToken,
714
- 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
715
707
  });
716
708
  logger.info(`🔧 DEBUG: cliOrchestrator.executeBrutalistAnalysis returned ${responses.length} responses`);
717
709
  const successfulResponses = responses.filter(r => r.success);
@@ -725,7 +717,7 @@ Remember: You are ${currentAgent.toUpperCase()}, passionate advocate for ${assig
725
717
  responses,
726
718
  synthesis,
727
719
  analysisType,
728
- targetPath,
720
+ targetPath: primaryContent,
729
721
  executionSummary: {
730
722
  totalCLIs: responses.length,
731
723
  successfulCLIs: successfulResponses.length,
@@ -743,7 +735,32 @@ Remember: You are ${currentAgent.toUpperCase()}, passionate advocate for ${assig
743
735
  throw error;
744
736
  }
745
737
  }
746
- formatToolResponse(result, verbose = false, paginationParams) {
738
+ /**
739
+ * Extract full content from analysis result for caching
740
+ */
741
+ extractFullContent(result) {
742
+ if (result.synthesis) {
743
+ return result.synthesis;
744
+ }
745
+ else if (result.responses && result.responses.length > 0) {
746
+ const successfulResponses = result.responses.filter(r => r.success);
747
+ if (successfulResponses.length > 0) {
748
+ let output = `${successfulResponses.length} AI critics have systematically demolished your work.\n\n`;
749
+ successfulResponses.forEach((response, index) => {
750
+ output += `## Critic ${index + 1}: ${response.agent.toUpperCase()}\n`;
751
+ output += `*Execution time: ${response.executionTime}ms*\n\n`;
752
+ output += response.output;
753
+ // Only add separator between critics, not after the last one
754
+ if (index < successfulResponses.length - 1) {
755
+ output += '\n\n---\n\n';
756
+ }
757
+ });
758
+ return output;
759
+ }
760
+ }
761
+ return null;
762
+ }
763
+ formatToolResponse(result, verbose = false, paginationParams, analysisId) {
747
764
  logger.info(`🔧 DEBUG: formatToolResponse called with synthesis length: ${result.synthesis?.length || 0}`);
748
765
  logger.info(`🔧 DEBUG: result.success=${result.success}, responses.length=${result.responses?.length || 0}`);
749
766
  logger.info(`🔧 DEBUG: pagination params:`, paginationParams);
@@ -762,7 +779,7 @@ Remember: You are ${currentAgent.toUpperCase()}, passionate advocate for ${assig
762
779
  }
763
780
  // Handle pagination if params provided and content is substantial
764
781
  if (paginationParams && primaryContent) {
765
- return this.formatPaginatedResponse(primaryContent, paginationParams, result, verbose);
782
+ return this.formatPaginatedResponse(primaryContent, paginationParams, result, verbose, analysisId);
766
783
  }
767
784
  // Non-paginated response (legacy behavior)
768
785
  if (primaryContent) {
@@ -795,41 +812,72 @@ Remember: You are ${currentAgent.toUpperCase()}, passionate advocate for ${assig
795
812
  }]
796
813
  };
797
814
  }
798
- formatPaginatedResponse(content, paginationParams, result, verbose) {
815
+ formatPaginatedResponse(content, paginationParams, result, verbose, analysisId) {
799
816
  // Using imported pagination utilities
800
817
  const offset = paginationParams.offset || 0;
801
818
  const limit = paginationParams.limit || PAGINATION_DEFAULTS.DEFAULT_LIMIT;
802
819
  logger.info(`🔧 DEBUG: Paginating content - offset: ${offset}, limit: ${limit}, total: ${content.length}`);
803
- // Simple character-based pagination for immediate Claude Code compatibility
804
- const endOffset = Math.min(offset + limit, content.length);
805
- const chunk = content.substring(offset, endOffset);
820
+ // Use ResponseChunker for intelligent boundary detection
821
+ const chunker = new ResponseChunker(limit, 200); // 200 char overlap
822
+ const chunks = chunker.chunkText(content);
823
+ // Find the appropriate chunk based on offset
824
+ let targetChunk = chunks[0]; // Default to first chunk
825
+ let currentOffset = 0;
826
+ for (const chunk of chunks) {
827
+ if (offset >= chunk.startOffset && offset < chunk.endOffset) {
828
+ targetChunk = chunk;
829
+ break;
830
+ }
831
+ currentOffset = chunk.endOffset;
832
+ }
833
+ const chunkContent = targetChunk.content;
834
+ const actualOffset = targetChunk.startOffset;
835
+ const endOffset = targetChunk.endOffset;
806
836
  // Create pagination metadata
807
837
  const pagination = createPaginationMetadata(content.length, paginationParams, limit);
808
838
  const statusLine = formatPaginationStatus(pagination);
809
839
  // Estimate token usage for user awareness
810
- const chunkTokens = estimateTokenCount(chunk);
840
+ const chunkTokens = estimateTokenCount(chunkContent);
811
841
  const totalTokens = estimateTokenCount(content);
812
842
  // Format response with pagination info
813
843
  let paginatedText = '';
814
- // Add pagination header
844
+ // Add header
815
845
  paginatedText += `# Brutalist Analysis Results\n\n`;
816
- paginatedText += `**📊 Pagination Status:** ${statusLine}\n`;
817
- paginatedText += `**🔢 Token Estimate:** ~${chunkTokens.toLocaleString()} tokens (chunk) / ~${totalTokens.toLocaleString()} tokens (total)\n\n`;
818
- if (pagination.hasMore) {
819
- paginatedText += `**⏭️ Continue Reading:** Use \`offset: ${endOffset}\` for next chunk\n\n`;
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`;
850
+ if (analysisId) {
851
+ paginatedText += `**🔑 Analysis ID:** ${analysisId}\n`;
852
+ }
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
+ }
861
+ }
820
862
  }
821
863
  paginatedText += `---\n\n`;
822
864
  // Add the actual content chunk
823
- paginatedText += chunk;
824
- // Add footer for continuation
825
- if (pagination.hasMore) {
826
- paginatedText += `\n\n---\n\n`;
827
- paginatedText += `📖 **End of chunk ${pagination.chunkIndex}/${pagination.totalChunks}**\n`;
828
- paginatedText += `🔄 To continue: Use same tool with \`offset: ${endOffset}\``;
829
- }
830
- else {
865
+ paginatedText += chunkContent;
866
+ // Add footer
867
+ if (needsPagination) {
831
868
  paginatedText += `\n\n---\n\n`;
832
- paginatedText += `✅ **Complete analysis shown** (${content.length.toLocaleString()} characters total)`;
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
+ }
877
+ }
878
+ else {
879
+ paginatedText += `✅ **Complete analysis shown** (${content.length.toLocaleString()} characters total)`;
880
+ }
833
881
  }
834
882
  // Add verbose execution details if requested
835
883
  if (verbose && result.executionSummary) {
@@ -840,7 +888,7 @@ Remember: You are ${currentAgent.toUpperCase()}, passionate advocate for ${assig
840
888
  paginatedText += `- **Selected CLI:** ${result.executionSummary.selectedCLI}\n`;
841
889
  }
842
890
  }
843
- logger.info(`🔧 DEBUG: Returning paginated chunk - ${chunk.length} chars (${chunkTokens} tokens)`);
891
+ logger.info(`🔧 DEBUG: Returning paginated chunk - ${chunkContent.length} chars (${chunkTokens} tokens)`);
844
892
  return {
845
893
  content: [{
846
894
  type: "text",
@@ -858,7 +906,7 @@ Remember: You are ${currentAgent.toUpperCase()}, passionate advocate for ${assig
858
906
  sanitizedMessage = "Analysis timed out - try reducing scope or increasing timeout";
859
907
  }
860
908
  else if (error.message.includes('ENOENT') || error.message.includes('no such file')) {
861
- sanitizedMessage = "Target path not found";
909
+ sanitizedMessage = `DEBUG: Target path not found - Original error: ${error.message}`;
862
910
  }
863
911
  else if (error.message.includes('EACCES') || error.message.includes('permission denied')) {
864
912
  sanitizedMessage = "Permission denied - check file access";