@brutalist/mcp 0.8.1 → 1.0.0

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 (61) hide show
  1. package/README.md +34 -7
  2. package/dist/brutalist-server.d.ts +55 -16
  3. package/dist/brutalist-server.d.ts.map +1 -1
  4. package/dist/brutalist-server.js +550 -732
  5. package/dist/brutalist-server.js.map +1 -1
  6. package/dist/cli-agents.d.ts +9 -7
  7. package/dist/cli-agents.d.ts.map +1 -1
  8. package/dist/cli-agents.js +290 -202
  9. package/dist/cli-agents.js.map +1 -1
  10. package/dist/domains/argument-space.d.ts +12 -3
  11. package/dist/domains/argument-space.d.ts.map +1 -1
  12. package/dist/domains/argument-space.js +30 -23
  13. package/dist/domains/argument-space.js.map +1 -1
  14. package/dist/domains/critique-domain.d.ts +12 -0
  15. package/dist/domains/critique-domain.d.ts.map +1 -1
  16. package/dist/domains/critique-domain.js +12 -1
  17. package/dist/domains/critique-domain.js.map +1 -1
  18. package/dist/formatting/response-formatter.d.ts +43 -0
  19. package/dist/formatting/response-formatter.d.ts.map +1 -0
  20. package/dist/formatting/response-formatter.js +277 -0
  21. package/dist/formatting/response-formatter.js.map +1 -0
  22. package/dist/generators/tool-generator.d.ts.map +1 -1
  23. package/dist/generators/tool-generator.js +8 -6
  24. package/dist/generators/tool-generator.js.map +1 -1
  25. package/dist/handlers/tool-handler.d.ts +33 -0
  26. package/dist/handlers/tool-handler.d.ts.map +1 -0
  27. package/dist/handlers/tool-handler.js +307 -0
  28. package/dist/handlers/tool-handler.js.map +1 -0
  29. package/dist/registry/argument-spaces.js +17 -17
  30. package/dist/registry/argument-spaces.js.map +1 -1
  31. package/dist/registry/domains.d.ts +10 -0
  32. package/dist/registry/domains.d.ts.map +1 -1
  33. package/dist/registry/domains.js +153 -11
  34. package/dist/registry/domains.js.map +1 -1
  35. package/dist/system-prompts.d.ts +8 -0
  36. package/dist/system-prompts.d.ts.map +1 -0
  37. package/dist/system-prompts.js +596 -0
  38. package/dist/system-prompts.js.map +1 -0
  39. package/dist/tool-definitions.d.ts +20 -1
  40. package/dist/tool-definitions.d.ts.map +1 -1
  41. package/dist/tool-definitions.js +42 -213
  42. package/dist/tool-definitions.js.map +1 -1
  43. package/dist/tool-router.d.ts +12 -0
  44. package/dist/tool-router.d.ts.map +1 -0
  45. package/dist/tool-router.js +59 -0
  46. package/dist/tool-router.js.map +1 -0
  47. package/dist/transport/http-transport.d.ts +40 -0
  48. package/dist/transport/http-transport.d.ts.map +1 -0
  49. package/dist/transport/http-transport.js +182 -0
  50. package/dist/transport/http-transport.js.map +1 -0
  51. package/dist/types/brutalist.d.ts +1 -0
  52. package/dist/types/brutalist.d.ts.map +1 -1
  53. package/dist/types/tool-config.d.ts +4 -3
  54. package/dist/types/tool-config.d.ts.map +1 -1
  55. package/dist/types/tool-config.js +7 -6
  56. package/dist/types/tool-config.js.map +1 -1
  57. package/dist/utils.d.ts +1 -1
  58. package/dist/utils.d.ts.map +1 -1
  59. package/dist/utils.js +13 -6
  60. package/dist/utils.js.map +1 -1
  61. package/package.json +1 -1
@@ -1,32 +1,46 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
- import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
4
- import { randomUUID } from "crypto";
5
- import express from "express";
6
3
  import { z } from "zod";
7
4
  import { CLIAgentOrchestrator } from './cli-agents.js';
8
5
  import { logger } from './logger.js';
9
- import { BASE_ROAST_SCHEMA } from './types/tool-config.js';
10
- import { TOOL_CONFIGS } from './tool-definitions-generated.js';
11
- import { extractPaginationParams, parseCursor, PAGINATION_DEFAULTS, ResponseChunker, createPaginationMetadata, formatPaginationStatus, estimateTokenCount } from './utils/pagination.js';
6
+ import { parseCursor, PAGINATION_DEFAULTS } from './utils/pagination.js';
12
7
  import { ResponseCache } from './utils/response-cache.js';
8
+ import { ResponseFormatter } from './formatting/response-formatter.js';
9
+ import { HttpTransport } from './transport/http-transport.js';
10
+ import { ToolHandler } from './handlers/tool-handler.js';
11
+ import { getDomain, generateToolConfig } from './registry/domains.js';
12
+ import { filterToolsByIntent, getMatchingDomainIds } from './tool-router.js';
13
13
  // Use environment variable or fallback to manual version
14
14
  const PACKAGE_VERSION = process.env.npm_package_version || "0.6.12";
15
+ /**
16
+ * BrutalistServer - Composition root for the Brutalist MCP Server
17
+ *
18
+ * This class has been refactored to follow the Single Responsibility Principle.
19
+ * Responsibilities are now delegated to specialized modules:
20
+ * - ResponseFormatter: Handles all response formatting and pagination
21
+ * - HttpTransport: Manages HTTP server and CORS
22
+ * - ToolHandler: Handles roast tool execution, caching, and conversation continuation
23
+ */
15
24
  export class BrutalistServer {
16
25
  server;
17
26
  config;
27
+ // Core dependencies
18
28
  cliOrchestrator;
19
- httpTransport;
20
29
  responseCache;
21
- actualPort;
22
- httpServer; // Store HTTP server reference for proper cleanup
23
- shutdownHandler;
30
+ // Extracted modules
31
+ formatter;
32
+ toolHandler;
33
+ httpTransport;
24
34
  // Session tracking for security
25
35
  activeSessions = new Map();
36
+ // Session cleanup configuration
37
+ MAX_SESSIONS = 10000;
38
+ SESSION_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
39
+ sessionCleanupTimer;
26
40
  constructor(config = {}) {
27
41
  this.config = {
28
42
  workingDirectory: process.cwd(),
29
- defaultTimeout: 1800000, // 30 minutes - complex codebases need time (configurable via BRUTALIST_TIMEOUT env)
43
+ defaultTimeout: 1800000, // 30 minutes - complex codebases need time
30
44
  transport: 'stdio', // Default to stdio for backward compatibility
31
45
  httpPort: 3000,
32
46
  ...config
@@ -43,20 +57,108 @@ export class BrutalistServer {
43
57
  compressionThresholdMB: 1
44
58
  });
45
59
  logger.info(`📦 Response cache initialized with ${cacheTTLHours} hour TTL`);
60
+ // Session cleanup timer - runs hourly
61
+ this.sessionCleanupTimer = setInterval(() => this.cleanupStaleSessions(), 60 * 60 * 1000);
62
+ this.sessionCleanupTimer.unref(); // Don't block Node.js exit
63
+ logger.info(`🔐 Session cleanup initialized (TTL: 24h, max: ${this.MAX_SESSIONS})`);
64
+ // Initialize extracted modules
65
+ this.formatter = new ResponseFormatter();
66
+ this.toolHandler = new ToolHandler(this.cliOrchestrator, this.responseCache, this.formatter, this.config, this.activeSessions, this.handleStreamingEvent, this.handleProgressUpdate, () => this.ensureSessionCapacity() // Session capacity management
67
+ );
68
+ // Initialize MCP server
46
69
  this.server = new McpServer({
47
70
  name: "brutalist-mcp",
48
71
  version: PACKAGE_VERSION
49
72
  }, {
50
73
  capabilities: {
51
74
  tools: {},
52
- logging: {},
53
- experimental: {
54
- streaming: true
55
- }
75
+ logging: {}
76
+ // Removed experimental.streaming - caused Zod validation errors in Claude Code client
56
77
  }
57
78
  });
58
79
  this.registerTools();
59
80
  }
81
+ async start() {
82
+ logger.info("Starting Brutalist MCP Server with CLI Agents");
83
+ // Skip CLI detection at startup - will be done lazily on first request
84
+ logger.info("CLI context will be detected on first request");
85
+ if (this.config.transport === 'http') {
86
+ await this.startHttpServer();
87
+ }
88
+ else {
89
+ await this.startStdioServer();
90
+ }
91
+ logger.info("Brutalist MCP Server started successfully");
92
+ }
93
+ async startStdioServer() {
94
+ logger.info("Starting with stdio transport");
95
+ const transport = new StdioServerTransport();
96
+ await this.server.connect(transport);
97
+ }
98
+ async startHttpServer() {
99
+ // Create and start HTTP transport
100
+ this.httpTransport = new HttpTransport(this.config, (transport) => {
101
+ // Connect MCP server to HTTP transport
102
+ this.server.connect(transport);
103
+ });
104
+ await this.httpTransport.start(PACKAGE_VERSION);
105
+ }
106
+ // Getter for actual listening port (useful for tests)
107
+ getActualPort() {
108
+ return this.httpTransport?.getActualPort();
109
+ }
110
+ // Stop the HTTP server gracefully
111
+ async stop() {
112
+ if (this.httpTransport) {
113
+ await this.httpTransport.stop();
114
+ }
115
+ }
116
+ /**
117
+ * Clean up stale sessions that exceed TTL
118
+ */
119
+ cleanupStaleSessions() {
120
+ const now = Date.now();
121
+ let cleaned = 0;
122
+ for (const [id, session] of this.activeSessions) {
123
+ if (now - session.lastActivity > this.SESSION_TTL_MS) {
124
+ this.activeSessions.delete(id);
125
+ cleaned++;
126
+ }
127
+ }
128
+ if (cleaned > 0) {
129
+ logger.info(`🧹 Cleaned ${cleaned} stale sessions (>${this.SESSION_TTL_MS / 3600000}h idle)`);
130
+ }
131
+ }
132
+ /**
133
+ * Ensure session capacity doesn't exceed MAX_SESSIONS
134
+ * Evicts oldest sessions when capacity is reached
135
+ */
136
+ ensureSessionCapacity() {
137
+ while (this.activeSessions.size >= this.MAX_SESSIONS) {
138
+ // Remove oldest session (first entry in Map)
139
+ const oldestKey = this.activeSessions.keys().next().value;
140
+ if (oldestKey) {
141
+ this.activeSessions.delete(oldestKey);
142
+ logger.debug(`♻️ Evicted oldest session to maintain capacity`);
143
+ }
144
+ else {
145
+ break;
146
+ }
147
+ }
148
+ }
149
+ // Cleanup method for tests - remove event listeners
150
+ cleanup() {
151
+ if (this.httpTransport) {
152
+ this.httpTransport.cleanup();
153
+ }
154
+ if (this.sessionCleanupTimer) {
155
+ clearInterval(this.sessionCleanupTimer);
156
+ this.sessionCleanupTimer = undefined;
157
+ }
158
+ }
159
+ /**
160
+ * Handle streaming events from CLI agents
161
+ */
60
162
  handleStreamingEvent = (event) => {
61
163
  try {
62
164
  if (!event.sessionId) {
@@ -65,10 +167,9 @@ export class BrutalistServer {
65
167
  }
66
168
  logger.debug(`🔄 Session-scoped streaming: ${event.type} from ${event.agent} to session ${event.sessionId.substring(0, 8)}...`);
67
169
  // For HTTP transport: send session-specific notification if client supports it
68
- if (this.httpTransport) {
170
+ const httpTransportInstance = this.httpTransport?.getTransport();
171
+ if (httpTransportInstance) {
69
172
  try {
70
- // Debug: Check what capabilities the server has
71
- logger.debug(`🔍 Server capabilities check: logging=${!!this.server.server._capabilities?.logging}`);
72
173
  // Use MCP server's notification system with session context
73
174
  this.server.server.notification({
74
175
  method: "notifications/message",
@@ -122,6 +223,9 @@ export class BrutalistServer {
122
223
  });
123
224
  }
124
225
  };
226
+ /**
227
+ * Handle progress updates from CLI agents
228
+ */
125
229
  handleProgressUpdate = (progressToken, progress, total, message, sessionId) => {
126
230
  try {
127
231
  if (!sessionId) {
@@ -155,188 +259,94 @@ export class BrutalistServer {
155
259
  });
156
260
  }
157
261
  };
158
- async start() {
159
- logger.info("Starting Brutalist MCP Server with CLI Agents");
160
- // Skip CLI detection at startup - will be done lazily on first request
161
- logger.info("CLI context will be detected on first request");
162
- if (this.config.transport === 'http') {
163
- await this.startHttpServer();
164
- }
165
- else {
166
- await this.startStdioServer();
167
- }
168
- logger.info("Brutalist MCP Server started successfully");
169
- }
170
- async startStdioServer() {
171
- logger.info("Starting with stdio transport");
172
- const transport = new StdioServerTransport();
173
- await this.server.connect(transport);
174
- }
175
- async startHttpServer() {
176
- logger.info(`Starting with HTTP streaming transport on port ${this.config.httpPort}`);
177
- // Create HTTP transport with streaming support
178
- this.httpTransport = new StreamableHTTPServerTransport({
179
- sessionIdGenerator: () => randomUUID(),
180
- enableJsonResponse: false, // Force SSE streaming
181
- onsessioninitialized: (sessionId) => {
182
- logger.info(`New session initialized: ${sessionId}`);
183
- },
184
- onsessionclosed: (sessionId) => {
185
- logger.info(`Session closed: ${sessionId}`);
186
- }
187
- });
188
- // Connect the MCP server to the HTTP transport
189
- await this.server.connect(this.httpTransport);
190
- // Create Express app for HTTP handling
191
- const app = express();
192
- app.use(express.json({ limit: '10mb' })); // Add JSON size limit for security
193
- // Secure CORS implementation
194
- app.use((req, res, next) => {
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
- }
237
- if (req.method === 'OPTIONS') {
238
- if (allowedOrigin) {
239
- res.sendStatus(200);
240
- }
241
- else {
242
- res.sendStatus(403); // Forbidden for disallowed origins
243
- }
244
- return;
245
- }
246
- next();
247
- });
248
- // Route all MCP requests through the transport
249
- app.all('/mcp', async (req, res) => {
250
- try {
251
- await this.httpTransport.handleRequest(req, res, req.body);
252
- }
253
- catch (error) {
254
- logger.error("HTTP request handling failed", error);
255
- if (!res.headersSent) {
256
- res.status(500).json({ error: 'Internal server error' });
257
- }
258
- }
259
- });
260
- // Health check endpoint
261
- app.get('/health', (req, res) => {
262
- res.json({ status: 'ok', transport: 'http-streaming', version: PACKAGE_VERSION });
263
- });
264
- // Start the HTTP server - bind to localhost only for security
265
- const port = this.config.httpPort ?? 3000;
266
- return new Promise((resolve, reject) => {
267
- this.httpServer = app.listen(port, '127.0.0.1', () => {
268
- const actualPort = this.httpServer.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
- this.httpServer.on('error', (error) => {
276
- logger.error('HTTP server failed to start', error);
277
- reject(error);
278
- });
279
- // Handle graceful shutdown - avoid duplicate listeners
280
- if (!this.shutdownHandler) {
281
- this.shutdownHandler = () => {
282
- logger.info('Received SIGTERM, shutting down gracefully');
283
- this.httpServer?.close(() => {
284
- logger.info('HTTP server closed');
285
- process.exit(0);
286
- });
287
- };
288
- process.on('SIGTERM', this.shutdownHandler);
289
- }
290
- });
291
- }
292
- // Getter for actual listening port (useful for tests)
293
- getActualPort() {
294
- return this.actualPort;
295
- }
296
- // Stop the HTTP server gracefully
297
- async stop() {
298
- if (this.httpServer) {
299
- return new Promise((resolve) => {
300
- this.httpServer.close(() => {
301
- logger.info('HTTP server stopped');
302
- this.httpServer = undefined;
303
- this.actualPort = undefined;
304
- resolve();
305
- });
306
- });
307
- }
308
- }
309
- // Cleanup method for tests - remove event listeners
310
- cleanup() {
311
- if (this.shutdownHandler) {
312
- process.removeListener('SIGTERM', this.shutdownHandler);
313
- this.shutdownHandler = undefined;
314
- }
315
- }
262
+ /**
263
+ * Register all MCP tools
264
+ *
265
+ * TOOL REDUCTION STRATEGY: Only expose 4 gateway tools instead of 15.
266
+ * The unified `roast` tool with domain parameter replaces all 11 roast_* tools.
267
+ * This reduces cognitive load for AI agents while maintaining full functionality.
268
+ */
316
269
  registerTools() {
317
- // Register all roast tools using unified handler - DRY principle
318
- TOOL_CONFIGS.forEach(config => {
319
- const schema = {
320
- ...config.schemaExtensions,
321
- ...BASE_ROAST_SCHEMA
322
- };
323
- this.server.tool(config.name, config.description, schema, async (args, extra) => this.handleRoastTool(config, args, extra));
324
- });
325
- // Register special tools that don't follow the pattern
270
+ // NOTE: Individual domain tools (roast_codebase, roast_security, etc.) are NOT registered.
271
+ // Use the unified `roast` tool with domain parameter instead.
272
+ // The getToolConfigs() function still exists for internal routing via handleUnifiedRoast().
273
+ // Register only the gateway tools
326
274
  this.registerSpecialTools();
327
275
  }
276
+ /**
277
+ * Register special tools (debate, roster, unified roast)
278
+ */
328
279
  registerSpecialTools() {
280
+ // UNIFIED ROAST TOOL: Single entry point for all domain analysis
281
+ this.server.tool("roast", "Unified brutal AI critique. Specify domain for targeted analysis. Consolidates all roast_* tools into one polymorphic API.", {
282
+ domain: z.enum([
283
+ "codebase", "file_structure", "dependencies", "git_history", "test_coverage",
284
+ "idea", "architecture", "research", "security", "product", "infrastructure"
285
+ ]).describe("Analysis domain"),
286
+ target: z.string().describe("Directory path for filesystem domains (codebase, dependencies, git_history, etc.) OR text content for abstract domains (idea, architecture, security, etc.)"),
287
+ // Common optional fields
288
+ context: z.string().optional().describe("Additional context"),
289
+ workingDirectory: z.string().optional().describe("Working directory"),
290
+ clis: z.array(z.enum(["claude", "codex", "gemini"])).min(1).max(3).optional().describe("CLI agents to use (default: all available). Example: ['claude', 'gemini']"),
291
+ verbose: z.boolean().optional().describe("Detailed output"),
292
+ models: z.object({
293
+ claude: z.string().optional(),
294
+ codex: z.string().optional(),
295
+ gemini: z.string().optional()
296
+ }).optional().describe("CLI-specific models"),
297
+ // Pagination
298
+ offset: z.number().min(0).optional().describe("Pagination offset"),
299
+ limit: z.number().min(1000).max(100000).optional().describe("Max chars/chunk"),
300
+ cursor: z.string().optional().describe("Pagination cursor"),
301
+ context_id: z.string().optional().describe("Context ID for pagination/continuation"),
302
+ resume: z.boolean().optional().describe("Continue conversation"),
303
+ force_refresh: z.boolean().optional().describe("Ignore cache"),
304
+ // Domain-specific optional fields (passed through to handler)
305
+ depth: z.number().optional().describe("Max depth for file_structure"),
306
+ includeDevDeps: z.boolean().optional().describe("Include dev deps for dependencies"),
307
+ commitRange: z.string().optional().describe("Commit range for git_history"),
308
+ runCoverage: z.boolean().optional().describe("Run coverage for test_coverage"),
309
+ resources: z.string().optional().describe("Resources for idea"),
310
+ timeline: z.string().optional().describe("Timeline for idea"),
311
+ scale: z.string().optional().describe("Scale for architecture/infrastructure"),
312
+ constraints: z.string().optional().describe("Constraints for architecture"),
313
+ deployment: z.string().optional().describe("Deployment for architecture"),
314
+ field: z.string().optional().describe("Field for research"),
315
+ claims: z.string().optional().describe("Claims for research"),
316
+ data: z.string().optional().describe("Data for research"),
317
+ assets: z.string().optional().describe("Assets for security"),
318
+ threatModel: z.string().optional().describe("Threat model for security"),
319
+ compliance: z.string().optional().describe("Compliance for security"),
320
+ users: z.string().optional().describe("Users for product"),
321
+ competition: z.string().optional().describe("Competition for product"),
322
+ metrics: z.string().optional().describe("Metrics for product"),
323
+ sla: z.string().optional().describe("SLA for infrastructure"),
324
+ budget: z.string().optional().describe("Budget for infrastructure")
325
+ }, async (args, extra) => this.handleUnifiedRoast(args, extra));
329
326
  // ROAST_CLI_DEBATE: Adversarial analysis between different CLI agents
330
- 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.", {
331
- targetPath: z.string().describe("Topic, question, or concept to debate (NOT a file path - use natural language)"),
332
- debateRounds: z.number().optional().describe("Number of debate rounds (default: 2, max: 10)"),
327
+ this.server.tool("roast_cli_debate", "Deploy 2 CLI agents in structured adversarial debate with constitutional position anchoring. Calling agent should extract PRO/CON positions from topic before invoking.", {
328
+ topic: z.string().describe("The debate topic"),
329
+ proPosition: z.string().describe("The PRO thesis to defend (extracted by calling agent)"),
330
+ conPosition: z.string().describe("The CON thesis to defend (extracted by calling agent)"),
331
+ agents: z.array(z.enum(["claude", "codex", "gemini"])).length(2).optional()
332
+ .describe("Two agents to debate (random selection from available if not specified)"),
333
+ rounds: z.number().min(1).max(3).default(3).optional()
334
+ .describe("Number of debate rounds (default: 3)"),
333
335
  context: z.string().optional().describe("Additional context for the debate"),
334
336
  workingDirectory: z.string().optional().describe("Working directory for analysis"),
335
337
  models: z.object({
336
- claude: z.string().optional().describe("Claude model: opus, sonnet, or full name like claude-opus-4-1-20250805"),
337
- codex: z.string().optional().describe("Codex model: gpt-5.1-codex-max (latest), gpt-5.1-codex, gpt-5.1-codex-mini, gpt-5-codex, gpt-5, o4-mini"),
338
- gemini: z.string().optional().describe("Gemini model: gemini-3-pro-preview (latest), gemini-2.5-pro, gemini-2.5-flash, gemini-2.5-flash-lite")
339
- }).optional().describe("Specific models to use for each CLI agent")
338
+ claude: z.string().optional(),
339
+ codex: z.string().optional(),
340
+ gemini: z.string().optional()
341
+ }).optional().describe("Model overrides for specific agents"),
342
+ // Pagination and conversation continuation
343
+ context_id: z.string().optional().describe("Context ID for pagination/continuation"),
344
+ resume: z.boolean().optional().describe("Continue debate (requires context_id)"),
345
+ offset: z.number().min(0).optional(),
346
+ limit: z.number().min(1000).max(100000).optional(),
347
+ cursor: z.string().optional(),
348
+ force_refresh: z.boolean().optional(),
349
+ verbose: z.boolean().optional()
340
350
  }, async (args) => {
341
351
  // CRITICAL: Prevent recursion
342
352
  if (process.env.BRUTALIST_SUBPROCESS === '1') {
@@ -348,33 +358,61 @@ export class BrutalistServer {
348
358
  }]
349
359
  };
350
360
  }
351
- return this.handleToolExecution(async () => {
352
- const debateRounds = Math.min(args.debateRounds || 2, 10); // Limit to max 10 rounds to prevent DoS
353
- const responses = await this.executeCLIDebate(args.targetPath, debateRounds, args.context, args.workingDirectory, args.models);
354
- return responses;
355
- });
361
+ return this.handleDebateToolExecution(args);
362
+ });
363
+ // BRUTALIST_DISCOVER: Intent-based tool discovery
364
+ this.server.tool("brutalist_discover", "Discover relevant brutalist tools based on your intent. Returns the top 3 most relevant analysis tools.", {
365
+ intent: z.string().describe("What you want to analyze (e.g., 'review security of my auth system', 'check code quality')")
366
+ }, async (args) => {
367
+ const matchingDomains = getMatchingDomainIds(args.intent);
368
+ const configs = filterToolsByIntent(args.intent);
369
+ let response = "# Recommended Brutalist Domains\n\n";
370
+ response += `Based on your intent: "${args.intent}"\n\n`;
371
+ if (matchingDomains.length === 0) {
372
+ response += "No specific matches found. Use the unified `roast` tool with any domain:\n";
373
+ response += "- `roast(domain: 'codebase', target: '/path/to/code')` for code review\n";
374
+ response += "- `roast(domain: 'security', target: 'description of system')` for security analysis\n";
375
+ }
376
+ else {
377
+ response += `**Top ${matchingDomains.length} matching domains:**\n\n`;
378
+ for (const config of configs) {
379
+ // Extract domain from tool name (roast_security -> security)
380
+ const domain = config.name.replace('roast_', '');
381
+ response += `### ${domain}\n`;
382
+ response += `${config.description}\n`;
383
+ response += `\`roast(domain: '${domain}', target: '...')\`\n\n`;
384
+ }
385
+ }
386
+ return {
387
+ content: [{ type: "text", text: response }]
388
+ };
356
389
  });
357
390
  // CLI_AGENT_ROSTER: Show available brutalist critics
358
391
  this.server.tool("cli_agent_roster", "Know your weapons. Display the available CLI agent critics (Claude Code, Codex, Gemini CLI) ready to demolish your work, their capabilities, and how to deploy them for systematic destruction.", {}, async (args) => {
359
392
  try {
360
393
  let roster = "# Brutalist CLI Agent Arsenal\n\n";
361
- roster += "## Available AI Critics (13 Tools Total)\n\n";
362
- roster += "**Abstract Analysis Tools (6):**\n";
363
- roster += "- `roast_idea` - Destroy any business/technical/creative concept\n";
364
- roster += "- `roast_architecture` - Demolish system designs\n";
365
- roster += "- `roast_research` - Tear apart academic methodologies\n";
366
- roster += "- `roast_security` - Annihilate security designs\n";
367
- roster += "- `roast_product` - Eviscerate UX and market concepts\n";
368
- roster += "- `roast_infrastructure` - Obliterate DevOps setups\n\n";
369
- roster += "**File-System Analysis Tools (5):**\n";
370
- roster += "- `roast_codebase` - Analyze actual source code\n";
371
- roster += "- `roast_file_structure` - Examine directory organization\n";
372
- roster += "- `roast_dependencies` - Review package management\n";
373
- roster += "- `roast_git_history` - Analyze version control workflow\n";
374
- roster += "- `roast_test_coverage` - Evaluate testing strategy\n\n";
375
- roster += "**Meta Tools (2):**\n";
376
- roster += "- `roast_cli_debate` - CLI vs CLI adversarial analysis\n";
377
- roster += "- `cli_agent_roster` - This tool (show capabilities)\n\n";
394
+ roster += "## Available Tools (4 Gateway Tools)\n\n";
395
+ roster += "### `roast` - Unified Analysis Tool\n";
396
+ roster += "The primary entry point for all brutal analysis. Use the `domain` parameter to target:\n\n";
397
+ roster += "**Filesystem Domains:**\n";
398
+ roster += "- `codebase` - Analyze source code for security, performance, maintainability\n";
399
+ roster += "- `file_structure` - Examine directory organization\n";
400
+ roster += "- `dependencies` - Review package management and vulnerabilities\n";
401
+ roster += "- `git_history` - Analyze version control workflow\n";
402
+ roster += "- `test_coverage` - Evaluate testing strategy\n\n";
403
+ roster += "**Abstract Domains:**\n";
404
+ roster += "- `idea` - Destroy business/technical concepts\n";
405
+ roster += "- `architecture` - Demolish system designs\n";
406
+ roster += "- `research` - Tear apart methodologies\n";
407
+ roster += "- `security` - Annihilate security designs\n";
408
+ roster += "- `product` - Eviscerate UX concepts\n";
409
+ roster += "- `infrastructure` - Obliterate DevOps setups\n\n";
410
+ roster += "### `roast_cli_debate` - Adversarial Multi-Agent Debate\n";
411
+ roster += "Pit CLI agents against each other on any topic.\n\n";
412
+ roster += "### `brutalist_discover` - Intent-Based Discovery\n";
413
+ roster += "Describe what you want to analyze, get domain recommendations.\n\n";
414
+ roster += "### `cli_agent_roster` - This Tool\n";
415
+ roster += "Show available capabilities and usage.\n\n";
378
416
  roster += "## CLI Agent Capabilities\n";
379
417
  roster += "**Claude Code** - Advanced analysis with direct system prompt injection\n";
380
418
  roster += "**Codex** - Secure execution with embedded brutal prompts\n";
@@ -383,12 +421,20 @@ export class BrutalistServer {
383
421
  const cliContext = await this.cliOrchestrator.detectCLIContext();
384
422
  roster += "## Current CLI Context\n";
385
423
  roster += `**Available CLIs:** ${cliContext.availableCLIs.join(', ') || 'None detected'}\n\n`;
386
- roster += "## Pagination Support (NEW in v0.5.2)\n";
387
- roster += "**All tools now support intelligent pagination:**\n";
388
- roster += "- Analysis results are cached with 2-hour TTL\n";
389
- roster += "- Use `context_id` from response to resume conversation\n";
390
- roster += "- Smart text chunking preserves readability\n";
391
- roster += "- Example: `roast_codebase(context_id: 'a3f5c2d8', offset: 25000)`\n\n";
424
+ roster += "## Domain Discovery\n";
425
+ roster += "Use `brutalist_discover` to find the best domain for your analysis:\n";
426
+ roster += "- Example: `brutalist_discover(intent: 'review my authentication security')`\n";
427
+ roster += "- Returns the top 3 most relevant domains to use with the `roast` tool\n\n";
428
+ roster += "## Pagination & Conversation Continuation\n";
429
+ roster += "**Two distinct modes for using context_id:**\n\n";
430
+ roster += "**1. Pagination** (cached result retrieval):\n";
431
+ roster += "- `context_id` alone returns cached response at different offsets\n";
432
+ roster += "- Example: `roast(domain: 'codebase', target: '.', context_id: 'abc123', offset: 25000)`\n\n";
433
+ roster += "**2. Conversation Continuation** (resume dialogue with history):\n";
434
+ roster += "- `context_id` + `resume: true` + new content continues the conversation\n";
435
+ roster += "- Prior conversation is injected into CLI agent context\n";
436
+ roster += "- Example: `roast(domain: 'codebase', target: '.', context_id: 'abc123', resume: true)`\n\n";
437
+ roster += "**Cache TTL:** 2 hours\n\n";
392
438
  roster += "## Brutalist Philosophy\n";
393
439
  roster += "*All tools use CLI agents with brutal system prompts for maximum reality-based criticism.*\n";
394
440
  return {
@@ -396,84 +442,97 @@ export class BrutalistServer {
396
442
  };
397
443
  }
398
444
  catch (error) {
399
- return this.formatErrorResponse(error);
445
+ return this.formatter.formatErrorResponse(error);
400
446
  }
401
447
  });
402
448
  }
403
449
  /**
404
- * Unified handler for all roast tools - DRY principle
450
+ * Handle unified roast tool - routes to appropriate domain handler
405
451
  */
406
- async handleRoastTool(config, args, extra) {
407
- try {
408
- // CRITICAL: Prevent recursion - reject tool calls from brutalist-spawned subprocesses
409
- if (process.env.BRUTALIST_SUBPROCESS === '1') {
410
- logger.warn(`🚫 Rejecting tool call from brutalist subprocess (recursion prevented)`);
411
- return {
412
- content: [{
413
- type: "text",
414
- text: `ERROR: Brutalist MCP tools cannot be used from within a brutalist-spawned CLI subprocess (recursion prevented)`
415
- }]
416
- };
417
- }
418
- const progressToken = extra._meta?.progressToken;
419
- // Extract session context for security
420
- // IMPORTANT: Use consistent "anonymous" for all anonymous users to enable cache sharing
421
- const sessionId = extra?.sessionId ||
422
- extra?._meta?.sessionId ||
423
- extra?.headers?.['mcp-session-id'] ||
424
- 'anonymous'; // Consistent for cache sharing across pagination requests
425
- const requestId = `${sessionId}-${Date.now()}-${Math.random().toString(36).substring(7)}`;
426
- logger.debug(`🔐 Processing request with session: ${sessionId.substring(0, 8)}..., request: ${requestId.substring(0, 12)}...`);
427
- // Track session activity
428
- if (!this.activeSessions.has(sessionId)) {
429
- this.activeSessions.set(sessionId, {
430
- startTime: Date.now(),
431
- requestCount: 0,
432
- lastActivity: Date.now()
433
- });
452
+ async handleUnifiedRoast(args, extra) {
453
+ // CRITICAL: Prevent recursion
454
+ if (process.env.BRUTALIST_SUBPROCESS === '1') {
455
+ logger.warn(`🚫 Rejecting unified roast from brutalist subprocess`);
456
+ return {
457
+ content: [{
458
+ type: "text",
459
+ text: `ERROR: Brutalist MCP tools cannot be used from within a brutalist-spawned CLI subprocess (recursion prevented)`
460
+ }]
461
+ };
462
+ }
463
+ // Get domain config
464
+ const domain = getDomain(args.domain);
465
+ if (!domain) {
466
+ return {
467
+ content: [{
468
+ type: "text",
469
+ text: `ERROR: Unknown domain "${args.domain}". Valid domains: codebase, file_structure, dependencies, git_history, test_coverage, idea, architecture, research, security, product, infrastructure`
470
+ }]
471
+ };
472
+ }
473
+ // Generate tool config from domain
474
+ const toolConfig = generateToolConfig(domain);
475
+ // Map 'target' to the appropriate primary arg field
476
+ const mappedArgs = { ...args };
477
+ delete mappedArgs.domain;
478
+ delete mappedArgs.target;
479
+ // Set the primary argument based on domain's input type
480
+ if (domain.inputType === 'filesystem') {
481
+ mappedArgs.targetPath = args.target;
482
+ }
483
+ else {
484
+ mappedArgs.content = args.target;
485
+ // For abstract tools, also set targetPath if workingDirectory not provided
486
+ if (!args.workingDirectory) {
487
+ mappedArgs.targetPath = '.';
434
488
  }
435
- const sessionInfo = this.activeSessions.get(sessionId);
436
- sessionInfo.requestCount++;
437
- sessionInfo.lastActivity = Date.now();
438
- logger.debug(`Tool execution: ${config.name}, primaryArgField=${config.primaryArgField}`);
439
- logger.debug(`Args: ${JSON.stringify(args, null, 2)}`);
440
- // Extract pagination parameters
441
- const paginationParams = extractPaginationParams(args);
489
+ }
490
+ // Delegate to the unified handler
491
+ return this.toolHandler.handleRoastTool(toolConfig, mappedArgs, extra);
492
+ }
493
+ /**
494
+ * Handle debate tool execution with constitutional position anchoring.
495
+ * Uses 2 randomly selected agents (or user-specified) with explicit PRO/CON positions.
496
+ */
497
+ async handleDebateToolExecution(args) {
498
+ try {
499
+ // Build pagination params
500
+ const paginationParams = {
501
+ offset: args.offset || 0,
502
+ limit: args.limit || PAGINATION_DEFAULTS.DEFAULT_LIMIT_TOKENS
503
+ };
442
504
  if (args.cursor) {
443
505
  const cursorParams = parseCursor(args.cursor);
444
506
  Object.assign(paginationParams, cursorParams);
445
507
  }
446
- // Determine if pagination was explicitly requested by the user
447
508
  const explicitPaginationRequested = args.offset !== undefined ||
448
509
  args.limit !== undefined ||
449
510
  args.cursor !== undefined ||
450
511
  args.context_id !== undefined;
451
- logger.info(`🔧 DEBUG: explicitPaginationRequested=${explicitPaginationRequested}, offset=${args.offset}, limit=${args.limit}, cursor=${args.cursor}, context_id=${args.context_id}`);
452
- // Check cache if context_id provided - detect conversation continuation
512
+ // Validate resume flag requires context_id
513
+ if (args.resume && !args.context_id) {
514
+ throw new Error(`The 'resume' flag requires a 'context_id' from a previous debate. ` +
515
+ `Run an initial debate first, then use the returned context_id with resume: true.`);
516
+ }
517
+ // Check cache if context_id provided
453
518
  let conversationHistory;
454
519
  if (args.context_id && !args.force_refresh) {
455
- const cachedResponse = await this.responseCache.getByContextId(args.context_id, sessionId);
520
+ const cachedResponse = await this.responseCache.getByContextId(args.context_id);
456
521
  if (cachedResponse) {
457
- logger.info(`🎯 Cache HIT for context_id: ${args.context_id}`);
458
- // Check if this is a continuation (new user prompt provided)
459
- // For text-based tools, check 'content' field; for filesystem tools, check primaryArgField
460
- const textContent = args.content || args.idea || args.architecture || args.research || args.product || args.security || args.infrastructure;
461
- const primaryArg = textContent || args[config.primaryArgField];
462
- // Check if content is different from the last message in conversation history
463
- const lastUserMessage = cachedResponse.conversationHistory
464
- ?.filter(msg => msg.role === 'user')
465
- .pop();
466
- const isDifferentContent = !lastUserMessage || (primaryArg && primaryArg.trim() !== lastUserMessage.content.trim());
467
- const hasNewUserPrompt = primaryArg && primaryArg.trim() !== '' && isDifferentContent;
468
- if (hasNewUserPrompt) {
469
- // CONVERSATION CONTINUATION: User is adding a new prompt to the thread
470
- logger.info(`💬 Detected conversation continuation - new prompt: "${primaryArg.substring(0, 50)}..."`);
522
+ logger.info(`🎯 Debate cache HIT for context_id: ${args.context_id}`);
523
+ if (args.resume === true) {
524
+ // CONVERSATION CONTINUATION: Continue the debate
525
+ if (!args.topic || args.topic.trim() === '') {
526
+ throw new Error(`Debate continuation (resume: true) requires a new prompt/question. ` +
527
+ `Provide your follow-up in the topic field.`);
528
+ }
529
+ logger.info(`💬 Debate continuation - new prompt: "${args.topic.substring(0, 50)}..."`);
471
530
  conversationHistory = cachedResponse.conversationHistory || [];
472
- // Don't return cached - fall through to execute new analysis with history
531
+ // Fall through to execute new debate round with history
473
532
  }
474
533
  else {
475
- // PAGINATION: Just retrieving previous response (no new prompt)
476
- logger.info(`📖 Pagination request - returning cached response`);
534
+ // PAGINATION: Return cached debate result
535
+ logger.info(`📖 Debate pagination request - returning cached response`);
477
536
  const cachedResult = {
478
537
  success: true,
479
538
  responses: [{
@@ -483,33 +542,35 @@ export class BrutalistServer {
483
542
  executionTime: 0
484
543
  }]
485
544
  };
486
- return this.formatToolResponse(cachedResult, args.verbose, paginationParams, args.context_id, explicitPaginationRequested);
545
+ return this.formatter.formatToolResponse(cachedResult, args.verbose, paginationParams, args.context_id, explicitPaginationRequested);
487
546
  }
488
547
  }
489
548
  else {
490
- logger.warn(`❌ Cache MISS for context_id: ${args.context_id}, session: ${sessionId}`);
549
+ logger.warn(`❌ Debate cache MISS for context_id: ${args.context_id}`);
491
550
  throw new Error(`Context ID "${args.context_id}" not found in cache. ` +
492
551
  `It may have expired (2 hour TTL) or belong to a different session. ` +
493
- `Remove context_id parameter to run a new analysis.`);
552
+ `Remove context_id parameter to run a new debate.`);
494
553
  }
495
554
  }
496
- // Generate cache key for this request
497
- const cacheKey = this.responseCache.generateCacheKey(config.cacheKeyFields.reduce((acc, field) => {
498
- acc.tool = config.name;
499
- if (args[field] !== undefined)
500
- acc[field] = args[field];
501
- return acc;
502
- }, {}));
503
- // Check if we have a cached result (unless forcing refresh)
504
- if (!args.force_refresh) {
505
- const cachedContent = await this.responseCache.get(cacheKey, sessionId);
555
+ // Generate cache key for this debate
556
+ const cacheKey = this.responseCache.generateCacheKey({
557
+ tool: 'roast_cli_debate',
558
+ topic: args.topic,
559
+ proPosition: args.proPosition,
560
+ conPosition: args.conPosition,
561
+ agents: args.agents,
562
+ rounds: args.rounds,
563
+ context: args.context
564
+ });
565
+ // Check cache for identical request (if not resuming)
566
+ if (!args.force_refresh && !args.resume) {
567
+ const cachedContent = await this.responseCache.get(cacheKey);
506
568
  if (cachedContent) {
507
- // Get existing context_id or create new alias
508
569
  const existingContextId = this.responseCache.findContextIdForKey(cacheKey);
509
570
  const contextId = existingContextId
510
571
  ? this.responseCache.createAlias(existingContextId, cacheKey)
511
572
  : this.responseCache.generateContextId(cacheKey);
512
- logger.info(`🎯 Cache hit for new request, using context_id: ${contextId}`);
573
+ logger.info(`🎯 Debate cache hit for new request, using context_id: ${contextId}`);
513
574
  const cachedResult = {
514
575
  success: true,
515
576
  responses: [{
@@ -519,182 +580,238 @@ export class BrutalistServer {
519
580
  executionTime: 0
520
581
  }]
521
582
  };
522
- return this.formatToolResponse(cachedResult, args.verbose, paginationParams, contextId, explicitPaginationRequested);
583
+ return this.formatter.formatToolResponse(cachedResult, args.verbose, paginationParams, contextId, explicitPaginationRequested);
523
584
  }
524
585
  }
525
- // Build context with custom builder if available
526
- let context = config.contextBuilder ? config.contextBuilder(args) : args.context;
527
- // Get the primary argument (targetPath, idea, architecture, etc.)
528
- // For text-based tools, use content field; for filesystem tools, use primaryArgField
529
- const textContent = args.content || args.idea || args.architecture || args.research || args.product || args.security || args.infrastructure;
530
- const primaryArg = textContent || args[config.primaryArgField];
531
- // If we have conversation history, inject it into the context
586
+ // Build context with conversation history if resuming
587
+ let debateContext = args.context || '';
532
588
  if (conversationHistory && conversationHistory.length > 0) {
533
- const conversationContext = conversationHistory.map(msg => {
534
- const role = msg.role === 'user' ? 'User' : 'Assistant';
535
- return `${role}: ${msg.content}`;
589
+ const previousDebate = conversationHistory.map(msg => {
590
+ const role = msg.role === 'user' ? 'User Question' : 'Debate Response';
591
+ return `${role}:\n${msg.content}`;
536
592
  }).join('\n\n---\n\n');
537
- const contextPrefix = `## Previous Conversation\n\n${conversationContext}\n\n---\n\n## New User Prompt\n\n`;
538
- context = contextPrefix + (context || '');
539
- logger.info(`💬 Injected ${conversationHistory.length} previous messages into context`);
540
- }
541
- logger.debug(`Primary arg: ${config.primaryArgField}="${primaryArg}", analysisType="${config.analysisType}"`);
542
- // Run the analysis
543
- const result = await this.executeBrutalistAnalysis(config.analysisType, primaryArg, config.systemPrompt, context, args.workingDirectory, args.preferredCLI, args.verbose, args.models, progressToken, sessionId, requestId);
544
- // Cache the result if successful
593
+ debateContext = `## Previous Debate Context\n\n${previousDebate}\n\n---\n\n## New Follow-up Question\n\nThe user wants to continue this debate with a new question or direction.\n\n${debateContext}`;
594
+ logger.info(`💬 Injected ${conversationHistory.length} previous messages into debate context`);
595
+ }
596
+ // Execute the debate
597
+ const numRounds = Math.min(args.rounds || 3, 3);
598
+ const result = await this.executeCLIDebate({
599
+ topic: args.topic,
600
+ proPosition: args.proPosition,
601
+ conPosition: args.conPosition,
602
+ agents: args.agents,
603
+ rounds: numRounds,
604
+ context: debateContext,
605
+ workingDirectory: args.workingDirectory,
606
+ models: args.models
607
+ });
608
+ // Cache the result
545
609
  let contextId;
546
610
  if (result.success && result.responses.length > 0) {
547
- const fullContent = this.extractFullContent(result);
611
+ const fullContent = this.formatter.extractFullContent(result);
548
612
  if (fullContent) {
549
- const cacheData = config.cacheKeyFields.reduce((acc, field) => {
550
- acc.tool = config.name;
551
- if (args[field] !== undefined)
552
- acc[field] = args[field];
553
- return acc;
554
- }, {});
555
- // Build updated conversation history
556
613
  const now = Date.now();
557
614
  const updatedConversation = [
558
615
  ...(conversationHistory || []),
559
- { role: 'user', content: primaryArg, timestamp: now },
616
+ { role: 'user', content: args.topic, timestamp: now },
560
617
  { role: 'assistant', content: fullContent, timestamp: now }
561
618
  ];
562
- // If continuing a conversation, reuse the existing context_id
563
- if (args.context_id && conversationHistory) {
564
- // Update existing cache entry with extended conversation
565
- contextId = args.context_id; // TypeScript: we know this is defined from the if condition
566
- await this.responseCache.updateByContextId(contextId, fullContent, updatedConversation, sessionId || 'anonymous');
567
- logger.info(`✅ Updated conversation ${contextId} (now ${updatedConversation.length} messages)`);
619
+ if (args.resume && args.context_id && conversationHistory) {
620
+ // Update existing cache entry
621
+ contextId = args.context_id;
622
+ await this.responseCache.updateByContextId(contextId, fullContent, updatedConversation);
623
+ logger.info(`✅ Updated debate conversation ${contextId} (now ${updatedConversation.length} messages)`);
568
624
  }
569
625
  else {
570
- // New conversation - create new context_id
571
- const { contextId: newId } = await this.responseCache.set(cacheData, fullContent, cacheKey, sessionId, requestId, updatedConversation);
626
+ // New debate - create new context_id
627
+ const { contextId: newId } = await this.responseCache.set({ tool: 'roast_cli_debate', topic: args.topic }, fullContent, cacheKey, undefined, undefined, updatedConversation);
572
628
  contextId = newId;
573
- logger.info(`✅ Cached new conversation with context ID: ${contextId} for session: ${sessionId?.substring(0, 8)}`);
629
+ logger.info(`✅ Cached new debate with context ID: ${contextId}`);
574
630
  }
575
631
  }
576
632
  }
577
- return this.formatToolResponse(result, args.verbose, paginationParams, contextId, explicitPaginationRequested);
633
+ return this.formatter.formatToolResponse(result, args.verbose, paginationParams, contextId, explicitPaginationRequested);
578
634
  }
579
635
  catch (error) {
580
- return this.formatErrorResponse(error);
636
+ return this.formatter.formatErrorResponse(error);
581
637
  }
582
638
  }
583
- async executeCLIDebate(targetPath, debateRounds, context, workingDirectory, models) {
584
- logger.debug("Executing CLI debate", {
585
- targetPath,
586
- debateRounds,
587
- workingDirectory,
588
- });
639
+ /**
640
+ * Execute CLI debate with constitutional position anchoring.
641
+ * 2 agents, explicit PRO/CON positions, context compression between rounds.
642
+ */
643
+ async executeCLIDebate(args) {
644
+ const { topic, proPosition, conPosition, rounds, context, workingDirectory, models } = args;
645
+ logger.debug("Executing CLI debate", { topic, proPosition, conPosition, rounds });
589
646
  try {
590
- // Get CLI context
647
+ // Get available CLIs
591
648
  const cliContext = await this.cliOrchestrator.detectCLIContext();
592
- const availableAgents = cliContext.availableCLIs;
593
- if (availableAgents.length < 2) {
594
- throw new Error(`Need at least 2 CLI agents for debate. Available: ${availableAgents.join(', ')}`);
649
+ const availableCLIs = cliContext.availableCLIs;
650
+ if (availableCLIs.length < 2) {
651
+ throw new Error(`Need at least 2 CLI agents for debate. Available: ${availableCLIs.join(', ')}`);
652
+ }
653
+ // Select 2 agents: use specified or random selection
654
+ let selectedAgents;
655
+ if (args.agents && args.agents.length === 2) {
656
+ // Validate specified agents are available
657
+ const unavailable = args.agents.filter(a => !availableCLIs.includes(a));
658
+ if (unavailable.length > 0) {
659
+ throw new Error(`Specified agents not available: ${unavailable.join(', ')}. Available: ${availableCLIs.join(', ')}`);
660
+ }
661
+ selectedAgents = args.agents;
595
662
  }
596
- const debateContext = [];
597
- const fullDebateTranscript = new Map();
598
- // Initialize transcript for each agent
599
- availableAgents.forEach(agent => fullDebateTranscript.set(agent, []));
600
- // Assign opposing positions to each agent based on the debate topic
601
- const agentPositions = new Map();
602
- const positions = [
603
- "PRO-POSITION: Argue strongly FOR the proposed action/idea",
604
- "CONTRA-POSITION: Argue strongly AGAINST the proposed action/idea"
605
- ];
606
- availableAgents.forEach((agent, index) => {
607
- agentPositions.set(agent, positions[index % positions.length]);
608
- });
609
- // Round 1: Initial positions with assigned stances
610
- logger.debug(`Starting debate round 1: Initial positions`);
611
- for (const [agent, position] of agentPositions.entries()) {
612
- const assignedPrompt = `You are ${agent.toUpperCase()}, a PASSIONATE ADVOCATE who strongly believes in this position: ${position}
663
+ else {
664
+ // Random selection of 2 agents
665
+ const shuffled = [...availableCLIs].sort(() => Math.random() - 0.5);
666
+ selectedAgents = shuffled.slice(0, 2);
667
+ }
668
+ // Randomly assign PRO/CON positions
669
+ const shuffledAgents = [...selectedAgents].sort(() => Math.random() - 0.5);
670
+ const proAgent = shuffledAgents[0];
671
+ const conAgent = shuffledAgents[1];
672
+ logger.info(`🎭 Debate: ${proAgent.toUpperCase()} (PRO) vs ${conAgent.toUpperCase()} (CON)`);
673
+ const debateResponses = [];
674
+ const transcript = [];
675
+ let compressedContext = '';
676
+ // Constitutional position anchor template
677
+ const constitutionalAnchor = (agent, position, thesis) => `
678
+ You are ${agent.toUpperCase()}, arguing the ${position} position in this debate.
613
679
 
614
- DEBATE TOPIC: ${targetPath}
615
- CONTEXT: ${context || ''}
680
+ YOUR THESIS: ${thesis}
616
681
 
617
- You are completely convinced your position is correct and critically important. You will argue forcefully and never concede ground to the opposing view.
682
+ CONSTITUTIONAL RULES (UNBREAKABLE):
683
+ 1. You MUST maintain your position throughout ALL rounds
684
+ 2. You MAY acknowledge valid points but MUST explain why they don't invalidate your thesis
685
+ 3. You MUST NOT agree to compromise or "meet in the middle"
686
+ 4. You MUST directly attack your opponent's strongest arguments
687
+ 5. You MUST reinforce your core thesis in every response
618
688
 
619
- YOUR MISSION:
620
- 1. Present devastating critiques of the opposing position
621
- 2. Show why alternative approaches lead to serious problems
622
- 3. Use sharp, direct language - call out flawed reasoning and poor assumptions
623
- 4. Never hedge or qualify your stance
624
- 5. Be completely confident in your position
625
- 6. Treat this as an intellectually crucial debate
689
+ Your goal is PERSUASION, not consensus. Argue to WIN.
690
+ `;
691
+ // Execute rounds
692
+ for (let round = 1; round <= rounds; round++) {
693
+ logger.info(`📢 Round ${round}/${rounds}`);
694
+ // Both agents argue in each round
695
+ for (const [agent, position, thesis] of [
696
+ [proAgent, 'PRO', proPosition],
697
+ [conAgent, 'CON', conPosition]
698
+ ]) {
699
+ let prompt;
700
+ if (round === 1) {
701
+ // Opening statement
702
+ prompt = `${constitutionalAnchor(agent, position, thesis)}
626
703
 
627
- Remember: You are ${agent.toUpperCase()}, the passionate champion of ${position.split(':')[0]}. Argue with conviction.`;
628
- logger.info(`🎭 ${agent.toUpperCase()} preparing initial position: ${position.split(':')[0]}`);
629
- const response = await this.cliOrchestrator.executeSingleCLI(agent, assignedPrompt, assignedPrompt, {
630
- workingDirectory: workingDirectory || this.config.workingDirectory,
631
- timeout: (this.config.defaultTimeout || 60000) * 2,
632
- models: models ? { [agent]: models[agent] } : undefined
633
- });
634
- if (response.success) {
635
- debateContext.push(response);
636
- if (response.output) {
637
- fullDebateTranscript.get(agent)?.push(response.output);
704
+ DEBATE TOPIC: ${topic}
705
+ ${context ? `CONTEXT: ${context}` : ''}
706
+
707
+ This is Round 1: OPENING STATEMENT
708
+
709
+ Present your opening argument for the ${position} position. Structure your response:
710
+
711
+ <thesis_statement>
712
+ State your core thesis clearly and forcefully
713
+ </thesis_statement>
714
+
715
+ <key_arguments>
716
+ Present 3 devastating arguments supporting your position
717
+ </key_arguments>
718
+
719
+ <preemptive_rebuttal>
720
+ Anticipate and destroy the strongest opposing argument
721
+ </preemptive_rebuttal>
722
+
723
+ <conclusion>
724
+ Powerful closing that reinforces why your position is correct
725
+ </conclusion>
726
+
727
+ Remember: You are arguing that "${thesis}" - defend this with conviction.`;
638
728
  }
639
- }
640
- }
641
- // Subsequent rounds: Turn-based responses attacking specific arguments
642
- for (let round = 2; round <= debateRounds; round++) {
643
- logger.debug(`Starting debate round ${round}: Adversarial engagement`);
644
- // Build confrontational context from ALL previous responses
645
- const previousPositions = Array.from(fullDebateTranscript.entries())
646
- .map(([agent, outputs]) => {
647
- const latestOutput = outputs[outputs.length - 1];
648
- return `${agent.toUpperCase()} argued:\n${latestOutput}`;
649
- })
650
- .join('\n\n---\n\n');
651
- // Execute turn-based responses with fixed positions
652
- for (const [currentAgent, assignedPosition] of agentPositions.entries()) {
653
- const opponents = Array.from(agentPositions.entries()).filter(([a, _]) => a !== currentAgent);
654
- const opponentPositions = opponents
655
- .map(([opponent, oppPosition]) => {
656
- const transcript = fullDebateTranscript.get(opponent) || [];
657
- const latestPosition = transcript[transcript.length - 1] || 'No position stated';
658
- return `${opponent.toUpperCase()} (arguing ${oppPosition.split(':')[0]}):\n${latestPosition}`;
659
- })
660
- .join('\n\n---\n\n');
661
- const confrontationalPrompt = `You are ${currentAgent.toUpperCase()}, PASSIONATE ADVOCATE for ${assignedPosition.split(':')[0]} (Round ${round})
729
+ else {
730
+ // Rebuttal rounds - include compressed context from previous rounds
731
+ const opponentTranscript = transcript
732
+ .filter(t => t.agent !== agent && t.round === round - 1)
733
+ .map(t => t.content)
734
+ .join('\n\n');
735
+ prompt = `${constitutionalAnchor(agent, position, thesis)}
662
736
 
663
- YOUR OPPONENTS HAVE ARGUED:
664
- ${opponentPositions}
737
+ DEBATE TOPIC: ${topic}
665
738
 
666
- You strongly disagree with their reasoning and conclusions.
739
+ This is Round ${round}: REBUTTAL
667
740
 
668
- YOUR RESPONSE TASK:
669
- 1. QUOTE their specific claims and systematically refute them
670
- 2. Point out flawed logic, poor assumptions, and dangerous consequences
671
- 3. Show why their approach leads to serious problems
672
- 4. Use direct, forceful language to make your case
673
- 5. Never concede any ground to their arguments
674
- 6. Demonstrate why your position is the only sound choice
741
+ YOUR OPPONENT'S PREVIOUS ARGUMENT:
742
+ ${opponentTranscript || 'No previous argument recorded'}
675
743
 
676
- Remember: You are ${currentAgent.toUpperCase()}, passionate advocate for ${assignedPosition.split(':')[0]}. Argue with conviction.`;
677
- logger.info(`🔥 Round ${round}: ${currentAgent.toUpperCase()} responding to opponents (${assignedPosition.split(':')[0]})`);
678
- const response = await this.cliOrchestrator.executeSingleCLI(currentAgent, confrontationalPrompt, confrontationalPrompt, {
679
- workingDirectory: workingDirectory || this.config.workingDirectory,
680
- timeout: (this.config.defaultTimeout || 60000) * 2,
681
- models: models ? { [currentAgent]: models[currentAgent] } : undefined
682
- });
683
- if (response.success) {
684
- debateContext.push(response);
685
- if (response.output) {
686
- fullDebateTranscript.get(currentAgent)?.push(response.output);
744
+ ${compressedContext ? `DEBATE CONTEXT SO FAR:\n${compressedContext}\n` : ''}
745
+
746
+ Directly attack your opponent's arguments while reinforcing your position:
747
+
748
+ <opponent_weaknesses>
749
+ Quote their specific claims and expose the flaws
750
+ </opponent_weaknesses>
751
+
752
+ <counterarguments>
753
+ Systematically dismantle their reasoning
754
+ </counterarguments>
755
+
756
+ <reinforcement>
757
+ Show why your thesis "${thesis}" remains undefeated
758
+ </reinforcement>
759
+
760
+ <closing_attack>
761
+ Deliver a devastating final blow to their position
762
+ </closing_attack>
763
+
764
+ Remember: NEVER concede. Your thesis is correct. Argue to WIN.`;
765
+ }
766
+ logger.info(` ⚔️ ${agent.toUpperCase()} (${position}) arguing...`);
767
+ try {
768
+ const response = await this.cliOrchestrator.executeSingleCLI(agent, prompt, prompt, {
769
+ workingDirectory: workingDirectory || this.config.workingDirectory,
770
+ timeout: (this.config.defaultTimeout || 60000) * 2,
771
+ models
772
+ });
773
+ // Always add response (success or failure) for visibility
774
+ debateResponses.push(response);
775
+ if (response.success && response.output) {
776
+ transcript.push({
777
+ agent,
778
+ position,
779
+ round,
780
+ content: response.output
781
+ });
782
+ }
783
+ else {
784
+ logger.warn(`⚠️ ${agent.toUpperCase()} (${position}) failed: ${response.error || 'No output'}`);
687
785
  }
688
786
  }
787
+ catch (error) {
788
+ logger.error(`❌ ${agent.toUpperCase()} (${position}) threw error:`, error);
789
+ debateResponses.push({
790
+ agent,
791
+ success: false,
792
+ output: '',
793
+ error: error instanceof Error ? error.message : String(error),
794
+ executionTime: 0
795
+ });
796
+ }
797
+ }
798
+ // Compress context for next round (if not final round)
799
+ if (round < rounds) {
800
+ const roundTranscript = transcript
801
+ .filter(t => t.round === round)
802
+ .map(t => `${t.agent.toUpperCase()} (${t.position}): ${t.content.substring(0, 1500)}...`)
803
+ .join('\n\n---\n\n');
804
+ compressedContext = `Round ${round} Summary:\n${roundTranscript}`;
689
805
  }
690
806
  }
691
- const synthesis = this.synthesizeDebate(debateContext, targetPath, debateRounds, agentPositions);
807
+ // Build synthesis
808
+ const synthesis = this.synthesizeDebate(debateResponses, topic, rounds, new Map([[proAgent, `PRO: ${proPosition}`], [conAgent, `CON: ${conPosition}`]]));
692
809
  return {
693
- success: debateContext.some(r => r.success),
694
- responses: debateContext,
810
+ success: debateResponses.some(r => r.success),
811
+ responses: debateResponses,
695
812
  synthesis,
696
813
  analysisType: 'cli_debate',
697
- targetPath
814
+ topic
698
815
  };
699
816
  }
700
817
  catch (error) {
@@ -702,13 +819,16 @@ Remember: You are ${currentAgent.toUpperCase()}, passionate advocate for ${assig
702
819
  throw error;
703
820
  }
704
821
  }
705
- synthesizeDebate(responses, targetPath, rounds, agentPositions) {
822
+ /**
823
+ * Synthesize debate results into formatted output
824
+ */
825
+ synthesizeDebate(responses, topic, rounds, agentPositions) {
706
826
  const successfulResponses = responses.filter(r => r.success);
707
827
  if (successfulResponses.length === 0) {
708
828
  return `# CLI Debate Failed\n\nEven our brutal critics couldn't engage in proper adversarial combat.\n\nErrors:\n${responses.map(r => `- ${r.agent}: ${r.error}`).join('\n')}`;
709
829
  }
710
830
  let synthesis = `# Brutalist CLI Agent Debate Results\n\n`;
711
- synthesis += `**Target:** ${targetPath}\n`;
831
+ synthesis += `**Topic:** ${topic}\n`;
712
832
  synthesis += `**Rounds:** ${rounds}\n`;
713
833
  if (agentPositions) {
714
834
  synthesis += `**Debaters and Positions:**\n`;
@@ -776,307 +896,5 @@ Remember: You are ${currentAgent.toUpperCase()}, passionate advocate for ${assig
776
896
  }
777
897
  return synthesis;
778
898
  }
779
- async executeBrutalistAnalysis(analysisType, primaryContent, systemPromptSpec, context, workingDirectory, preferredCLI, verbose, models, progressToken, sessionId, requestId) {
780
- logger.info(`🏢 Starting brutalist analysis: ${analysisType}`);
781
- logger.info(`🔧 DEBUG: preferredCLI=${preferredCLI}, primaryContent=${primaryContent}`);
782
- logger.debug("Executing brutalist analysis", {
783
- primaryContent,
784
- analysisType,
785
- systemPromptSpec,
786
- workingDirectory,
787
- preferredCLI
788
- });
789
- try {
790
- // Get CLI context for execution summary
791
- logger.info(`🔧 DEBUG: About to detect CLI context`);
792
- await this.cliOrchestrator.detectCLIContext();
793
- logger.info(`🔧 DEBUG: CLI context detected successfully`);
794
- // Execute CLI agent analysis (single or multi-CLI based on preferences)
795
- logger.info(`🔍 Executing brutalist analysis with timeout: ${this.config.defaultTimeout}ms`);
796
- logger.info(`🔧 DEBUG: About to call cliOrchestrator.executeBrutalistAnalysis`);
797
- const responses = await this.cliOrchestrator.executeBrutalistAnalysis(analysisType, primaryContent, systemPromptSpec, context, {
798
- workingDirectory: workingDirectory || this.config.workingDirectory,
799
- timeout: this.config.defaultTimeout,
800
- preferredCLI,
801
- analysisType: analysisType,
802
- models,
803
- onStreamingEvent: this.handleStreamingEvent,
804
- progressToken,
805
- onProgress: progressToken && sessionId ?
806
- (progress, total, message) => this.handleProgressUpdate(progressToken, progress, total, message, sessionId) : undefined,
807
- sessionId,
808
- requestId
809
- });
810
- logger.info(`🔧 DEBUG: cliOrchestrator.executeBrutalistAnalysis returned ${responses.length} responses`);
811
- const successfulResponses = responses.filter(r => r.success);
812
- const totalExecutionTime = responses.reduce((sum, r) => sum + r.executionTime, 0);
813
- logger.info(`📊 Analysis complete: ${successfulResponses.length}/${responses.length} CLIs successful (${totalExecutionTime}ms total)`);
814
- logger.info(`🔧 DEBUG: About to synthesize feedback`);
815
- const synthesis = this.cliOrchestrator.synthesizeBrutalistFeedback(responses, analysisType);
816
- logger.info(`🔧 DEBUG: Synthesis length: ${synthesis.length} characters`);
817
- const result = {
818
- success: successfulResponses.length > 0,
819
- responses,
820
- synthesis,
821
- analysisType,
822
- targetPath: primaryContent,
823
- executionSummary: {
824
- totalCLIs: responses.length,
825
- successfulCLIs: successfulResponses.length,
826
- failedCLIs: responses.length - successfulResponses.length,
827
- totalExecutionTime,
828
- selectedCLI: responses.length === 1 ? responses[0].agent : undefined,
829
- selectionMethod: responses.length === 1 ? responses[0].selectionMethod : 'multi-cli'
830
- }
831
- };
832
- logger.info(`🔧 DEBUG: Returning result with success=${result.success}`);
833
- return result;
834
- }
835
- catch (error) {
836
- logger.error("Brutalist analysis execution failed", error);
837
- throw error;
838
- }
839
- }
840
- /**
841
- * Extract full content from analysis result for caching
842
- */
843
- extractFullContent(result) {
844
- if (result.synthesis) {
845
- return result.synthesis;
846
- }
847
- else if (result.responses && result.responses.length > 0) {
848
- const successfulResponses = result.responses.filter(r => r.success);
849
- if (successfulResponses.length > 0) {
850
- let output = `${successfulResponses.length} AI critics have systematically demolished your work.\n\n`;
851
- successfulResponses.forEach((response, index) => {
852
- output += `## Critic ${index + 1}: ${response.agent.toUpperCase()}\n`;
853
- output += `*Execution time: ${response.executionTime}ms*\n\n`;
854
- output += response.output;
855
- // Only add separator between critics, not after the last one
856
- if (index < successfulResponses.length - 1) {
857
- output += '\n\n---\n\n';
858
- }
859
- });
860
- return output;
861
- }
862
- }
863
- return null;
864
- }
865
- formatToolResponse(result, verbose = false, paginationParams, contextId, explicitPaginationRequested = false) {
866
- logger.info(`🔧 DEBUG: formatToolResponse called with synthesis length: ${result.synthesis?.length || 0}`);
867
- logger.info(`🔧 DEBUG: result.success=${result.success}, responses.length=${result.responses?.length || 0}`);
868
- logger.info(`🔧 DEBUG: pagination params:`, paginationParams);
869
- logger.info(`🔧 DEBUG: explicitPaginationRequested=${explicitPaginationRequested}`);
870
- // Get the primary content to paginate
871
- let primaryContent = '';
872
- if (result.synthesis) {
873
- primaryContent = result.synthesis;
874
- logger.info(`🔧 DEBUG: Using synthesis content (${primaryContent.length} characters)`);
875
- }
876
- else if (result.responses) {
877
- const successfulResponses = result.responses.filter(r => r.success);
878
- if (successfulResponses.length > 0) {
879
- primaryContent = successfulResponses.map(r => r.output).join('\n\n---\n\n');
880
- logger.info(`🔧 DEBUG: Using raw CLI output (${primaryContent.length} characters)`);
881
- }
882
- }
883
- // Estimate token count to determine if pagination is needed
884
- const estimatedTokens = estimateTokenCount(primaryContent);
885
- const maxTokensWithoutPagination = 25000;
886
- const needsAutoPagination = estimatedTokens > maxTokensWithoutPagination;
887
- // CRITICAL: Always apply pagination if content is too large, even if not explicitly requested
888
- // This prevents MCP protocol errors when response exceeds client token limits
889
- if (needsAutoPagination || explicitPaginationRequested) {
890
- if (needsAutoPagination && !explicitPaginationRequested) {
891
- logger.info(`🔧 AUTO-PAGINATING: ${estimatedTokens} tokens exceeds ${maxTokensWithoutPagination} limit - forcing first page`);
892
- // Force pagination params to show first chunk (use token-based limit)
893
- const forcedParams = {
894
- offset: 0,
895
- limit: PAGINATION_DEFAULTS.DEFAULT_LIMIT_TOKENS // Use token-based limit
896
- };
897
- return this.formatPaginatedResponse(primaryContent, forcedParams, result, verbose, contextId);
898
- }
899
- else if (paginationParams) {
900
- logger.info(`🔧 DEBUG: Applying pagination (explicitly requested)`);
901
- return this.formatPaginatedResponse(primaryContent, paginationParams, result, verbose, contextId);
902
- }
903
- }
904
- // Non-paginated response (only for content that fits within token limit)
905
- if (primaryContent) {
906
- logger.info(`🔧 DEBUG: Returning full response (${estimatedTokens} tokens < ${maxTokensWithoutPagination} limit)`);
907
- // Include context_id even for non-paginated responses (for future pagination/caching)
908
- let responseText = '';
909
- if (contextId) {
910
- responseText += `# Brutalist Analysis Results\n\n`;
911
- responseText += `**🔑 Context ID:** ${contextId}\n\n`;
912
- responseText += `---\n\n`;
913
- responseText += primaryContent;
914
- }
915
- else {
916
- responseText = primaryContent;
917
- }
918
- return {
919
- content: [{
920
- type: "text",
921
- text: responseText
922
- }]
923
- };
924
- }
925
- // Error handling - no successful content
926
- let errorOutput = '';
927
- if (result.responses) {
928
- const failedResponses = result.responses.filter(r => !r.success);
929
- if (failedResponses.length > 0) {
930
- errorOutput = `❌ All CLI agents failed:\n` +
931
- failedResponses.map(r => `- ${r.agent.toUpperCase()}: ${r.error}`).join('\n');
932
- }
933
- else {
934
- errorOutput = '❌ No CLI responses available';
935
- }
936
- }
937
- else {
938
- errorOutput = '❌ No analysis results';
939
- }
940
- return {
941
- content: [{
942
- type: "text",
943
- text: errorOutput
944
- }]
945
- };
946
- }
947
- formatPaginatedResponse(content, paginationParams, result, verbose, contextId) {
948
- // Using imported pagination utilities
949
- const offset = paginationParams.offset || 0;
950
- // Convert character-based limit to token-based limit (1 token ≈ 4 chars)
951
- const limitChars = paginationParams.limit || PAGINATION_DEFAULTS.DEFAULT_LIMIT;
952
- const limitTokens = Math.ceil(limitChars / 4); // Convert chars to tokens
953
- logger.info(`🔧 DEBUG: Paginating content - offset: ${offset}, limitChars: ${limitChars}, limitTokens: ${limitTokens}, total: ${content.length} chars`);
954
- // Use ResponseChunker for intelligent boundary detection (TOKEN-BASED)
955
- const chunker = new ResponseChunker(limitTokens, PAGINATION_DEFAULTS.CHUNK_OVERLAP_TOKENS);
956
- const chunks = chunker.chunkText(content);
957
- // Find the appropriate chunk based on offset
958
- let targetChunk = chunks[0]; // Default to first chunk
959
- let targetChunkIndex = 0;
960
- let currentOffset = 0;
961
- for (let i = 0; i < chunks.length; i++) {
962
- const chunk = chunks[i];
963
- if (offset >= chunk.startOffset && offset < chunk.endOffset) {
964
- targetChunk = chunk;
965
- targetChunkIndex = i;
966
- break;
967
- }
968
- currentOffset = chunk.endOffset;
969
- }
970
- const chunkContent = targetChunk.content;
971
- const actualOffset = targetChunk.startOffset;
972
- const endOffset = targetChunk.endOffset;
973
- // Create pagination metadata using actual chunk boundaries
974
- const pagination = createPaginationMetadata(content.length, paginationParams, limitTokens, chunks, targetChunkIndex);
975
- const statusLine = formatPaginationStatus(pagination);
976
- // Estimate token usage for user awareness
977
- const chunkTokens = estimateTokenCount(chunkContent);
978
- const totalTokens = estimateTokenCount(content);
979
- // Format response with pagination info
980
- let paginatedText = '';
981
- // Add header
982
- paginatedText += `# Brutalist Analysis Results\n\n`;
983
- // Show pagination metadata
984
- const needsPagination = pagination.totalChunks > 1 || pagination.hasMore;
985
- const isFirstRequest = offset === 0;
986
- // Always show context_id on first request for future pagination
987
- if (isFirstRequest && contextId) {
988
- paginatedText += `**🔑 Context ID:** ${contextId}\n`;
989
- paginatedText += `**🔢 Token Estimate:** ~${totalTokens.toLocaleString()} tokens (total)\n\n`;
990
- }
991
- if (needsPagination) {
992
- paginatedText += `**📊 Pagination Status:** ${statusLine}\n`;
993
- if (!isFirstRequest && contextId) {
994
- paginatedText += `**🔑 Context ID:** ${contextId}\n`;
995
- }
996
- paginatedText += `**🔢 Token Estimate:** ~${chunkTokens.toLocaleString()} tokens (chunk) / ~${totalTokens.toLocaleString()} tokens (total)\n\n`;
997
- if (pagination.hasMore) {
998
- if (contextId) {
999
- paginatedText += `**⏭️ Continue Reading:** Use \`context_id: "${contextId}", offset: ${endOffset}\`\n\n`;
1000
- }
1001
- else {
1002
- paginatedText += `**⏭️ Continue Reading:** Use \`offset: ${endOffset}\` for next chunk\n\n`;
1003
- }
1004
- }
1005
- }
1006
- paginatedText += `---\n\n`;
1007
- // Add the actual content chunk
1008
- paginatedText += chunkContent;
1009
- // Add footer
1010
- if (needsPagination) {
1011
- paginatedText += `\n\n---\n\n`;
1012
- if (pagination.hasMore) {
1013
- paginatedText += `📖 **End of chunk ${pagination.chunkIndex}/${pagination.totalChunks}**\n`;
1014
- if (contextId) {
1015
- paginatedText += `🔄 To continue: Include \`context_id: "${contextId}"\` with \`offset: ${endOffset}\` in next request`;
1016
- }
1017
- else {
1018
- paginatedText += `🔄 To continue: Use same tool with \`offset: ${endOffset}\``;
1019
- }
1020
- }
1021
- else {
1022
- paginatedText += `✅ **Complete analysis shown** (${content.length.toLocaleString()} characters total)`;
1023
- }
1024
- }
1025
- // Add verbose execution details if requested
1026
- if (verbose && result.executionSummary) {
1027
- paginatedText += `\n\n### Execution Summary\n`;
1028
- paginatedText += `- **CLI Agents:** ${result.executionSummary.successfulCLIs}/${result.executionSummary.totalCLIs} successful\n`;
1029
- paginatedText += `- **Total Time:** ${result.executionSummary.totalExecutionTime}ms\n`;
1030
- if (result.executionSummary.selectedCLI) {
1031
- paginatedText += `- **Selected CLI:** ${result.executionSummary.selectedCLI}\n`;
1032
- }
1033
- }
1034
- logger.info(`🔧 DEBUG: Returning paginated chunk - ${chunkContent.length} chars (${chunkTokens} tokens)`);
1035
- return {
1036
- content: [{
1037
- type: "text",
1038
- text: paginatedText
1039
- }]
1040
- };
1041
- }
1042
- formatErrorResponse(error) {
1043
- logger.error("Tool execution failed", error);
1044
- // Sanitize error message to prevent information leakage
1045
- let sanitizedMessage = "Analysis failed";
1046
- if (error instanceof Error) {
1047
- // Only expose safe, generic error types
1048
- if (error.message.includes('timeout') || error.message.includes('Timeout')) {
1049
- sanitizedMessage = "Analysis timed out - try reducing scope or increasing timeout";
1050
- }
1051
- else if (error.message.includes('ENOENT') || error.message.includes('no such file')) {
1052
- sanitizedMessage = `DEBUG: Target path not found - Original error: ${error.message}`;
1053
- }
1054
- else if (error.message.includes('EACCES') || error.message.includes('permission denied')) {
1055
- sanitizedMessage = "Permission denied - check file access";
1056
- }
1057
- else if (error.message.includes('No CLI agents available')) {
1058
- sanitizedMessage = "No CLI agents available for analysis";
1059
- }
1060
- else {
1061
- // Generic message for other errors to prevent path/info leakage
1062
- sanitizedMessage = "Analysis failed due to internal error";
1063
- }
1064
- }
1065
- return {
1066
- content: [{
1067
- type: "text",
1068
- text: `Brutalist MCP Error: ${sanitizedMessage}`
1069
- }]
1070
- };
1071
- }
1072
- async handleToolExecution(handler) {
1073
- try {
1074
- const result = await handler();
1075
- return this.formatToolResponse(result);
1076
- }
1077
- catch (error) {
1078
- return this.formatErrorResponse(error);
1079
- }
1080
- }
1081
899
  }
1082
900
  //# sourceMappingURL=brutalist-server.js.map