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