@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.
- package/README.md +34 -7
- package/dist/brutalist-server.d.ts +55 -16
- package/dist/brutalist-server.d.ts.map +1 -1
- package/dist/brutalist-server.js +550 -732
- package/dist/brutalist-server.js.map +1 -1
- package/dist/cli-agents.d.ts +9 -7
- package/dist/cli-agents.d.ts.map +1 -1
- package/dist/cli-agents.js +290 -202
- package/dist/cli-agents.js.map +1 -1
- package/dist/domains/argument-space.d.ts +12 -3
- package/dist/domains/argument-space.d.ts.map +1 -1
- package/dist/domains/argument-space.js +30 -23
- package/dist/domains/argument-space.js.map +1 -1
- package/dist/domains/critique-domain.d.ts +12 -0
- package/dist/domains/critique-domain.d.ts.map +1 -1
- package/dist/domains/critique-domain.js +12 -1
- package/dist/domains/critique-domain.js.map +1 -1
- package/dist/formatting/response-formatter.d.ts +43 -0
- package/dist/formatting/response-formatter.d.ts.map +1 -0
- package/dist/formatting/response-formatter.js +277 -0
- package/dist/formatting/response-formatter.js.map +1 -0
- package/dist/generators/tool-generator.d.ts.map +1 -1
- package/dist/generators/tool-generator.js +8 -6
- package/dist/generators/tool-generator.js.map +1 -1
- package/dist/handlers/tool-handler.d.ts +33 -0
- package/dist/handlers/tool-handler.d.ts.map +1 -0
- package/dist/handlers/tool-handler.js +307 -0
- package/dist/handlers/tool-handler.js.map +1 -0
- package/dist/registry/argument-spaces.js +17 -17
- package/dist/registry/argument-spaces.js.map +1 -1
- package/dist/registry/domains.d.ts +10 -0
- package/dist/registry/domains.d.ts.map +1 -1
- package/dist/registry/domains.js +153 -11
- package/dist/registry/domains.js.map +1 -1
- package/dist/system-prompts.d.ts +8 -0
- package/dist/system-prompts.d.ts.map +1 -0
- package/dist/system-prompts.js +596 -0
- package/dist/system-prompts.js.map +1 -0
- package/dist/tool-definitions.d.ts +20 -1
- package/dist/tool-definitions.d.ts.map +1 -1
- package/dist/tool-definitions.js +42 -213
- package/dist/tool-definitions.js.map +1 -1
- package/dist/tool-router.d.ts +12 -0
- package/dist/tool-router.d.ts.map +1 -0
- package/dist/tool-router.js +59 -0
- package/dist/tool-router.js.map +1 -0
- package/dist/transport/http-transport.d.ts +40 -0
- package/dist/transport/http-transport.d.ts.map +1 -0
- package/dist/transport/http-transport.js +182 -0
- package/dist/transport/http-transport.js.map +1 -0
- package/dist/types/brutalist.d.ts +1 -0
- package/dist/types/brutalist.d.ts.map +1 -1
- package/dist/types/tool-config.d.ts +4 -3
- package/dist/types/tool-config.d.ts.map +1 -1
- package/dist/types/tool-config.js +7 -6
- package/dist/types/tool-config.js.map +1 -1
- package/dist/utils.d.ts +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +13 -6
- package/dist/utils.js.map +1 -1
- package/package.json +1 -1
package/dist/brutalist-server.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
//
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
|
331
|
-
|
|
332
|
-
|
|
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()
|
|
337
|
-
codex: z.string().optional()
|
|
338
|
-
gemini: z.string().optional()
|
|
339
|
-
}).optional().describe("
|
|
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.
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
|
362
|
-
roster += "
|
|
363
|
-
roster += "
|
|
364
|
-
roster += "
|
|
365
|
-
roster += "- `
|
|
366
|
-
roster += "- `
|
|
367
|
-
roster += "- `
|
|
368
|
-
roster += "- `
|
|
369
|
-
roster += "
|
|
370
|
-
roster += "
|
|
371
|
-
roster += "- `
|
|
372
|
-
roster += "- `
|
|
373
|
-
roster += "- `
|
|
374
|
-
roster += "- `
|
|
375
|
-
roster += "
|
|
376
|
-
roster += "- `
|
|
377
|
-
roster += "
|
|
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 += "##
|
|
387
|
-
roster += "
|
|
388
|
-
roster += "-
|
|
389
|
-
roster += "-
|
|
390
|
-
roster += "
|
|
391
|
-
roster += "
|
|
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
|
-
*
|
|
450
|
+
* Handle unified roast tool - routes to appropriate domain handler
|
|
405
451
|
*/
|
|
406
|
-
async
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
-
|
|
452
|
-
|
|
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
|
|
520
|
+
const cachedResponse = await this.responseCache.getByContextId(args.context_id);
|
|
456
521
|
if (cachedResponse) {
|
|
457
|
-
logger.info(`🎯
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
-
//
|
|
531
|
+
// Fall through to execute new debate round with history
|
|
473
532
|
}
|
|
474
533
|
else {
|
|
475
|
-
// PAGINATION:
|
|
476
|
-
logger.info(`📖
|
|
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(`❌
|
|
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
|
|
552
|
+
`Remove context_id parameter to run a new debate.`);
|
|
494
553
|
}
|
|
495
554
|
}
|
|
496
|
-
// Generate cache key for this
|
|
497
|
-
const cacheKey = this.responseCache.generateCacheKey(
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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(`🎯
|
|
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
|
|
526
|
-
let
|
|
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
|
|
534
|
-
const role = msg.role === 'user' ? 'User' : '
|
|
535
|
-
return `${role}
|
|
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
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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:
|
|
616
|
+
{ role: 'user', content: args.topic, timestamp: now },
|
|
560
617
|
{ role: 'assistant', content: fullContent, timestamp: now }
|
|
561
618
|
];
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
contextId
|
|
566
|
-
|
|
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
|
|
571
|
-
const { contextId: newId } = await this.responseCache.set(
|
|
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
|
|
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
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
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
|
|
647
|
+
// Get available CLIs
|
|
591
648
|
const cliContext = await this.cliOrchestrator.detectCLIContext();
|
|
592
|
-
const
|
|
593
|
-
if (
|
|
594
|
-
throw new Error(`Need at least 2 CLI agents for debate. Available: ${
|
|
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
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
//
|
|
610
|
-
|
|
611
|
-
|
|
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
|
-
|
|
615
|
-
CONTEXT: ${context || ''}
|
|
680
|
+
YOUR THESIS: ${thesis}
|
|
616
681
|
|
|
617
|
-
|
|
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
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
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
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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
|
-
|
|
664
|
-
${opponentPositions}
|
|
737
|
+
DEBATE TOPIC: ${topic}
|
|
665
738
|
|
|
666
|
-
|
|
739
|
+
This is Round ${round}: REBUTTAL
|
|
667
740
|
|
|
668
|
-
YOUR
|
|
669
|
-
|
|
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
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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
|
-
|
|
807
|
+
// Build synthesis
|
|
808
|
+
const synthesis = this.synthesizeDebate(debateResponses, topic, rounds, new Map([[proAgent, `PRO: ${proPosition}`], [conAgent, `CON: ${conPosition}`]]));
|
|
692
809
|
return {
|
|
693
|
-
success:
|
|
694
|
-
responses:
|
|
810
|
+
success: debateResponses.some(r => r.success),
|
|
811
|
+
responses: debateResponses,
|
|
695
812
|
synthesis,
|
|
696
813
|
analysisType: 'cli_debate',
|
|
697
|
-
|
|
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
|
-
|
|
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 += `**
|
|
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
|