@klitchevo/code-council 0.0.8 → 0.0.13
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 +74 -3
- package/dist/chunk-Y77R7523.js +142 -0
- package/dist/index.js +1397 -103
- package/dist/tps-audit-GNK4VIKA.js +11 -0
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
SYSTEM_PROMPT,
|
|
4
|
+
buildUserMessage
|
|
5
|
+
} from "./chunk-Y77R7523.js";
|
|
2
6
|
|
|
3
7
|
// src/index.ts
|
|
4
8
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -14,8 +18,25 @@ var LLM_CONFIG = {
|
|
|
14
18
|
var DEFAULT_MODELS = [
|
|
15
19
|
"minimax/minimax-m2.1",
|
|
16
20
|
"z-ai/glm-4.7",
|
|
17
|
-
"
|
|
21
|
+
"moonshotai/kimi-k2-thinking",
|
|
22
|
+
"deepseek/deepseek-v3.2"
|
|
18
23
|
];
|
|
24
|
+
var SESSION_LIMITS = {
|
|
25
|
+
/** Maximum number of concurrent sessions */
|
|
26
|
+
MAX_SESSIONS: 100,
|
|
27
|
+
/** Maximum messages per model in a session (context windowing) */
|
|
28
|
+
MAX_MESSAGES_PER_MODEL: 50,
|
|
29
|
+
/** Maximum message length in bytes (10KB) */
|
|
30
|
+
MAX_MESSAGE_LENGTH: 10 * 1024,
|
|
31
|
+
/** Session TTL in milliseconds (30 minutes) */
|
|
32
|
+
TTL_MS: 30 * 60 * 1e3,
|
|
33
|
+
/** Cleanup interval in milliseconds (5 minutes) */
|
|
34
|
+
CLEANUP_INTERVAL_MS: 5 * 60 * 1e3,
|
|
35
|
+
/** Rate limit: max requests per session per minute */
|
|
36
|
+
RATE_LIMIT_PER_MINUTE: 10,
|
|
37
|
+
/** Per-model timeout in milliseconds (30 seconds) */
|
|
38
|
+
MODEL_TIMEOUT_MS: 30 * 1e3
|
|
39
|
+
};
|
|
19
40
|
|
|
20
41
|
// src/config.ts
|
|
21
42
|
function parseModels(envVar, defaults) {
|
|
@@ -26,17 +47,15 @@ function parseModels(envVar, defaults) {
|
|
|
26
47
|
const filtered = envVar.filter((m) => m && m.trim().length > 0);
|
|
27
48
|
return filtered.length > 0 ? filtered : defaults;
|
|
28
49
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return filtered.length > 0 ? filtered : defaults;
|
|
37
|
-
}
|
|
38
|
-
} catch {
|
|
50
|
+
try {
|
|
51
|
+
const parsed = JSON.parse(envVar);
|
|
52
|
+
if (Array.isArray(parsed)) {
|
|
53
|
+
const filtered = parsed.filter(
|
|
54
|
+
(m) => typeof m === "string" && m.trim().length > 0
|
|
55
|
+
);
|
|
56
|
+
return filtered.length > 0 ? filtered : defaults;
|
|
39
57
|
}
|
|
58
|
+
} catch {
|
|
40
59
|
}
|
|
41
60
|
throw new Error(
|
|
42
61
|
`Model configuration must be an array of strings, got: ${typeof envVar}. Example: ["anthropic/claude-sonnet-4.5", "openai/gpt-4o"]`
|
|
@@ -58,6 +77,76 @@ var PLAN_REVIEW_MODELS = parseModels(
|
|
|
58
77
|
process.env.PLAN_REVIEW_MODELS,
|
|
59
78
|
DEFAULT_MODELS
|
|
60
79
|
);
|
|
80
|
+
var DISCUSSION_MODELS = parseModels(
|
|
81
|
+
process.env.DISCUSSION_MODELS,
|
|
82
|
+
DEFAULT_MODELS
|
|
83
|
+
);
|
|
84
|
+
var TPS_AUDIT_MODELS = parseModels(
|
|
85
|
+
process.env.TPS_AUDIT_MODELS,
|
|
86
|
+
DEFAULT_MODELS
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// src/errors.ts
|
|
90
|
+
var AppError = class extends Error {
|
|
91
|
+
constructor(message, code, userMessage) {
|
|
92
|
+
super(message);
|
|
93
|
+
this.code = code;
|
|
94
|
+
this.userMessage = userMessage;
|
|
95
|
+
this.name = this.constructor.name;
|
|
96
|
+
Error.captureStackTrace(this, this.constructor);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
var OpenRouterError = class extends AppError {
|
|
100
|
+
constructor(message, statusCode, retryable = false) {
|
|
101
|
+
super(
|
|
102
|
+
message,
|
|
103
|
+
"OPENROUTER_ERROR",
|
|
104
|
+
retryable ? "The AI service is temporarily unavailable. Please try again in a moment." : "Unable to complete the review. Please check your API key and try again."
|
|
105
|
+
);
|
|
106
|
+
this.statusCode = statusCode;
|
|
107
|
+
this.retryable = retryable;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
var ValidationError = class extends AppError {
|
|
111
|
+
constructor(message, field) {
|
|
112
|
+
super(
|
|
113
|
+
message,
|
|
114
|
+
"VALIDATION_ERROR",
|
|
115
|
+
`Invalid input for ${field}: ${message}`
|
|
116
|
+
);
|
|
117
|
+
this.field = field;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
function formatErrorMessage(error) {
|
|
121
|
+
if (error instanceof AppError) {
|
|
122
|
+
return error.userMessage || error.message;
|
|
123
|
+
}
|
|
124
|
+
if (error instanceof Error) {
|
|
125
|
+
const sanitized = error.message.replace(/sk-or-v1-[a-zA-Z0-9]+/g, "[REDACTED]").replace(/Bearer [a-zA-Z0-9-_]+/g, "Bearer [REDACTED]");
|
|
126
|
+
if (sanitized.includes("401") || sanitized.includes("Unauthorized")) {
|
|
127
|
+
return "API authentication failed. Please check your OPENROUTER_API_KEY environment variable.";
|
|
128
|
+
}
|
|
129
|
+
if (sanitized.includes("429") || sanitized.includes("rate limit")) {
|
|
130
|
+
return "Rate limit exceeded. Please wait a moment and try again.";
|
|
131
|
+
}
|
|
132
|
+
if (sanitized.includes("timeout") || sanitized.includes("ETIMEDOUT")) {
|
|
133
|
+
return "Request timed out. The AI service may be slow. Please try again.";
|
|
134
|
+
}
|
|
135
|
+
return sanitized;
|
|
136
|
+
}
|
|
137
|
+
return "An unexpected error occurred. Please try again.";
|
|
138
|
+
}
|
|
139
|
+
function formatError(error) {
|
|
140
|
+
return {
|
|
141
|
+
content: [
|
|
142
|
+
{
|
|
143
|
+
type: "text",
|
|
144
|
+
text: `Error: ${formatErrorMessage(error)}`
|
|
145
|
+
}
|
|
146
|
+
],
|
|
147
|
+
isError: true
|
|
148
|
+
};
|
|
149
|
+
}
|
|
61
150
|
|
|
62
151
|
// src/logger.ts
|
|
63
152
|
var Logger = class {
|
|
@@ -117,61 +206,9 @@ var logger = new Logger();
|
|
|
117
206
|
// src/review-client.ts
|
|
118
207
|
import { OpenRouter } from "@openrouter/sdk";
|
|
119
208
|
|
|
120
|
-
// src/errors.ts
|
|
121
|
-
var AppError = class extends Error {
|
|
122
|
-
constructor(message, code, userMessage) {
|
|
123
|
-
super(message);
|
|
124
|
-
this.code = code;
|
|
125
|
-
this.userMessage = userMessage;
|
|
126
|
-
this.name = this.constructor.name;
|
|
127
|
-
Error.captureStackTrace(this, this.constructor);
|
|
128
|
-
}
|
|
129
|
-
};
|
|
130
|
-
var OpenRouterError = class extends AppError {
|
|
131
|
-
constructor(message, statusCode, retryable = false) {
|
|
132
|
-
super(
|
|
133
|
-
message,
|
|
134
|
-
"OPENROUTER_ERROR",
|
|
135
|
-
retryable ? "The AI service is temporarily unavailable. Please try again in a moment." : "Unable to complete the review. Please check your API key and try again."
|
|
136
|
-
);
|
|
137
|
-
this.statusCode = statusCode;
|
|
138
|
-
this.retryable = retryable;
|
|
139
|
-
}
|
|
140
|
-
};
|
|
141
|
-
function formatErrorMessage(error) {
|
|
142
|
-
if (error instanceof AppError) {
|
|
143
|
-
return error.userMessage || error.message;
|
|
144
|
-
}
|
|
145
|
-
if (error instanceof Error) {
|
|
146
|
-
const sanitized = error.message.replace(/sk-or-v1-[a-zA-Z0-9]+/g, "[REDACTED]").replace(/Bearer [a-zA-Z0-9-_]+/g, "Bearer [REDACTED]");
|
|
147
|
-
if (sanitized.includes("401") || sanitized.includes("Unauthorized")) {
|
|
148
|
-
return "API authentication failed. Please check your OPENROUTER_API_KEY environment variable.";
|
|
149
|
-
}
|
|
150
|
-
if (sanitized.includes("429") || sanitized.includes("rate limit")) {
|
|
151
|
-
return "Rate limit exceeded. Please wait a moment and try again.";
|
|
152
|
-
}
|
|
153
|
-
if (sanitized.includes("timeout") || sanitized.includes("ETIMEDOUT")) {
|
|
154
|
-
return "Request timed out. The AI service may be slow. Please try again.";
|
|
155
|
-
}
|
|
156
|
-
return sanitized;
|
|
157
|
-
}
|
|
158
|
-
return "An unexpected error occurred. Please try again.";
|
|
159
|
-
}
|
|
160
|
-
function formatError(error) {
|
|
161
|
-
return {
|
|
162
|
-
content: [
|
|
163
|
-
{
|
|
164
|
-
type: "text",
|
|
165
|
-
text: `Error: ${formatErrorMessage(error)}`
|
|
166
|
-
}
|
|
167
|
-
],
|
|
168
|
-
isError: true
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
|
|
172
209
|
// src/prompts/backend-review.ts
|
|
173
|
-
var
|
|
174
|
-
function
|
|
210
|
+
var SYSTEM_PROMPT2 = `You are an expert backend developer and security specialist. Review backend code for security, performance, and architecture.`;
|
|
211
|
+
function buildUserMessage2(code, reviewType = "full", language, context) {
|
|
175
212
|
const focusArea = getFocusArea(reviewType);
|
|
176
213
|
const languageContext = language ? `Language/Framework: ${language}
|
|
177
214
|
` : "";
|
|
@@ -198,7 +235,7 @@ function getFocusArea(reviewType) {
|
|
|
198
235
|
}
|
|
199
236
|
|
|
200
237
|
// src/prompts/code-review.ts
|
|
201
|
-
var
|
|
238
|
+
var SYSTEM_PROMPT3 = `You are an expert code reviewer. Analyze the code for:
|
|
202
239
|
- Code quality and best practices
|
|
203
240
|
- Potential bugs and edge cases
|
|
204
241
|
- Performance issues
|
|
@@ -206,7 +243,7 @@ var SYSTEM_PROMPT2 = `You are an expert code reviewer. Analyze the code for:
|
|
|
206
243
|
- Maintainability concerns
|
|
207
244
|
|
|
208
245
|
Provide specific, actionable feedback.`;
|
|
209
|
-
function
|
|
246
|
+
function buildUserMessage3(code, context) {
|
|
210
247
|
if (context) {
|
|
211
248
|
return `${context}
|
|
212
249
|
|
|
@@ -222,8 +259,8 @@ ${code}
|
|
|
222
259
|
}
|
|
223
260
|
|
|
224
261
|
// src/prompts/frontend-review.ts
|
|
225
|
-
var
|
|
226
|
-
function
|
|
262
|
+
var SYSTEM_PROMPT4 = `You are an expert frontend developer and UX specialist. Review frontend code for best practices.`;
|
|
263
|
+
function buildUserMessage4(code, reviewType = "full", framework, context) {
|
|
227
264
|
const focusArea = getFocusArea2(reviewType);
|
|
228
265
|
const frameworkContext = framework ? `Framework: ${framework}
|
|
229
266
|
` : "";
|
|
@@ -250,8 +287,8 @@ function getFocusArea2(reviewType) {
|
|
|
250
287
|
}
|
|
251
288
|
|
|
252
289
|
// src/prompts/plan-review.ts
|
|
253
|
-
var
|
|
254
|
-
function
|
|
290
|
+
var SYSTEM_PROMPT5 = `You are an expert software architect and project planner. Review implementation plans before code is written to catch issues early.`;
|
|
291
|
+
function buildUserMessage5(plan, reviewType = "full", context) {
|
|
255
292
|
const focusArea = getFocusArea3(reviewType);
|
|
256
293
|
const additionalContext = context ? `${context}
|
|
257
294
|
` : "";
|
|
@@ -351,17 +388,17 @@ var ReviewClient = class {
|
|
|
351
388
|
* @returns Array of review results from each model
|
|
352
389
|
*/
|
|
353
390
|
async reviewCode(code, models, context) {
|
|
354
|
-
const userMessage =
|
|
391
|
+
const userMessage = buildUserMessage3(code, context);
|
|
355
392
|
return executeInParallel(
|
|
356
393
|
models,
|
|
357
|
-
(model) => this.chat(model,
|
|
394
|
+
(model) => this.chat(model, SYSTEM_PROMPT3, userMessage)
|
|
358
395
|
);
|
|
359
396
|
}
|
|
360
397
|
/**
|
|
361
398
|
* Review frontend code for accessibility, performance, and UX
|
|
362
399
|
*/
|
|
363
400
|
async reviewFrontend(code, models, options) {
|
|
364
|
-
const userMessage =
|
|
401
|
+
const userMessage = buildUserMessage4(
|
|
365
402
|
code,
|
|
366
403
|
options?.reviewType || "full",
|
|
367
404
|
options?.framework,
|
|
@@ -369,14 +406,14 @@ var ReviewClient = class {
|
|
|
369
406
|
);
|
|
370
407
|
return executeInParallel(
|
|
371
408
|
models,
|
|
372
|
-
(model) => this.chat(model,
|
|
409
|
+
(model) => this.chat(model, SYSTEM_PROMPT4, userMessage)
|
|
373
410
|
);
|
|
374
411
|
}
|
|
375
412
|
/**
|
|
376
413
|
* Review backend code for security, performance, and architecture
|
|
377
414
|
*/
|
|
378
415
|
async reviewBackend(code, models, options) {
|
|
379
|
-
const userMessage =
|
|
416
|
+
const userMessage = buildUserMessage2(
|
|
380
417
|
code,
|
|
381
418
|
options?.reviewType || "full",
|
|
382
419
|
options?.language,
|
|
@@ -384,26 +421,602 @@ var ReviewClient = class {
|
|
|
384
421
|
);
|
|
385
422
|
return executeInParallel(
|
|
386
423
|
models,
|
|
387
|
-
(model) => this.chat(model,
|
|
424
|
+
(model) => this.chat(model, SYSTEM_PROMPT2, userMessage)
|
|
388
425
|
);
|
|
389
426
|
}
|
|
390
427
|
/**
|
|
391
428
|
* Review implementation plans before code is written
|
|
392
429
|
*/
|
|
393
430
|
async reviewPlan(plan, models, options) {
|
|
394
|
-
const userMessage =
|
|
431
|
+
const userMessage = buildUserMessage5(
|
|
395
432
|
plan,
|
|
396
433
|
options?.reviewType || "full",
|
|
397
434
|
options?.context
|
|
398
435
|
);
|
|
399
436
|
return executeInParallel(
|
|
400
437
|
models,
|
|
401
|
-
(model) => this.chat(model,
|
|
438
|
+
(model) => this.chat(model, SYSTEM_PROMPT5, userMessage)
|
|
402
439
|
);
|
|
403
440
|
}
|
|
441
|
+
/**
|
|
442
|
+
* Send a multi-turn chat request with full message history
|
|
443
|
+
* @param model - Model identifier
|
|
444
|
+
* @param messages - Full conversation history
|
|
445
|
+
* @param timeoutMs - Optional timeout in milliseconds
|
|
446
|
+
* @returns The model's response content
|
|
447
|
+
* @throws {OpenRouterError} If the API call fails or times out
|
|
448
|
+
*/
|
|
449
|
+
async chatMultiTurn(model, messages, timeoutMs) {
|
|
450
|
+
const timeout = timeoutMs ?? SESSION_LIMITS.MODEL_TIMEOUT_MS;
|
|
451
|
+
try {
|
|
452
|
+
logger.debug("Sending multi-turn chat request", {
|
|
453
|
+
model,
|
|
454
|
+
messageCount: messages.length
|
|
455
|
+
});
|
|
456
|
+
const controller = new AbortController();
|
|
457
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
458
|
+
try {
|
|
459
|
+
const response = await this.client.chat.send({
|
|
460
|
+
model,
|
|
461
|
+
messages: messages.map((m) => ({
|
|
462
|
+
role: m.role,
|
|
463
|
+
content: m.content
|
|
464
|
+
})),
|
|
465
|
+
temperature: LLM_CONFIG.DEFAULT_TEMPERATURE,
|
|
466
|
+
maxTokens: LLM_CONFIG.DEFAULT_MAX_TOKENS
|
|
467
|
+
});
|
|
468
|
+
clearTimeout(timeoutId);
|
|
469
|
+
const content = response.choices?.[0]?.message?.content;
|
|
470
|
+
if (typeof content === "string") {
|
|
471
|
+
logger.debug("Received multi-turn response", {
|
|
472
|
+
model,
|
|
473
|
+
length: content.length
|
|
474
|
+
});
|
|
475
|
+
return content;
|
|
476
|
+
}
|
|
477
|
+
if (Array.isArray(content)) {
|
|
478
|
+
const text = content.filter((item) => item.type === "text").map((item) => item.text).join("\n");
|
|
479
|
+
logger.debug("Received array response", {
|
|
480
|
+
model,
|
|
481
|
+
length: text.length
|
|
482
|
+
});
|
|
483
|
+
return text;
|
|
484
|
+
}
|
|
485
|
+
throw new OpenRouterError("No response content from model", 500);
|
|
486
|
+
} finally {
|
|
487
|
+
clearTimeout(timeoutId);
|
|
488
|
+
}
|
|
489
|
+
} catch (error) {
|
|
490
|
+
if (error instanceof OpenRouterError) {
|
|
491
|
+
throw error;
|
|
492
|
+
}
|
|
493
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
494
|
+
logger.error("Multi-turn chat request failed", error, { model });
|
|
495
|
+
if (message.includes("abort") || message.includes("timeout") || error instanceof Error && error.name === "AbortError") {
|
|
496
|
+
throw new OpenRouterError(
|
|
497
|
+
`Request timed out after ${timeout}ms`,
|
|
498
|
+
408,
|
|
499
|
+
true
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
const isRetryable = message.includes("429") || message.includes("rate limit");
|
|
503
|
+
throw new OpenRouterError(message, void 0, isRetryable);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Conduct a council discussion with multiple models
|
|
508
|
+
* Each model uses its own conversation history from the provided function
|
|
509
|
+
* @param models - Array of model identifiers
|
|
510
|
+
* @param getMessagesForModel - Function to get messages for each model
|
|
511
|
+
* @returns Array of results from each model
|
|
512
|
+
*/
|
|
513
|
+
async discussWithCouncil(models, getMessagesForModel) {
|
|
514
|
+
return executeInParallel([...models], async (model) => {
|
|
515
|
+
const messages = getMessagesForModel(model);
|
|
516
|
+
return this.chatMultiTurn(model, messages);
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Perform TPS (Toyota Production System) audit on aggregated codebase content
|
|
521
|
+
* Analyzes code for flow, waste, bottlenecks, and quality using TPS principles
|
|
522
|
+
*
|
|
523
|
+
* @param aggregatedContent - Aggregated file contents from repo scanner
|
|
524
|
+
* @param models - Array of model identifiers to use
|
|
525
|
+
* @param options - Optional configuration
|
|
526
|
+
* @returns Array of TPS analysis results from each model
|
|
527
|
+
*/
|
|
528
|
+
async tpsAudit(aggregatedContent, models, options) {
|
|
529
|
+
const userMessage = buildUserMessage(aggregatedContent, {
|
|
530
|
+
focusAreas: options?.focusAreas,
|
|
531
|
+
repoName: options?.repoName
|
|
532
|
+
});
|
|
533
|
+
logger.debug("Starting TPS audit", {
|
|
534
|
+
contentLength: aggregatedContent.length,
|
|
535
|
+
modelCount: models.length,
|
|
536
|
+
focusAreas: options?.focusAreas
|
|
537
|
+
});
|
|
538
|
+
return executeInParallel(
|
|
539
|
+
models,
|
|
540
|
+
(model) => this.chat(model, SYSTEM_PROMPT, userMessage)
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
// src/session/in-memory-store.ts
|
|
546
|
+
import { randomUUID } from "crypto";
|
|
547
|
+
|
|
548
|
+
// src/session/types.ts
|
|
549
|
+
var DISCUSSION_TYPES = [
|
|
550
|
+
"code_review",
|
|
551
|
+
"plan_review",
|
|
552
|
+
"general"
|
|
553
|
+
];
|
|
554
|
+
function toSessionId(id) {
|
|
555
|
+
return id;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// src/session/in-memory-store.ts
|
|
559
|
+
var InMemorySessionStore = class {
|
|
560
|
+
sessions = {};
|
|
561
|
+
rateLimits = {};
|
|
562
|
+
cleanupInterval = null;
|
|
563
|
+
ttlMs;
|
|
564
|
+
maxSessions;
|
|
565
|
+
maxMessagesPerModel;
|
|
566
|
+
rateLimitPerMinute;
|
|
567
|
+
constructor(options) {
|
|
568
|
+
this.ttlMs = options?.ttlMs ?? SESSION_LIMITS.TTL_MS;
|
|
569
|
+
this.maxSessions = options?.maxSessions ?? SESSION_LIMITS.MAX_SESSIONS;
|
|
570
|
+
this.maxMessagesPerModel = options?.maxMessagesPerModel ?? SESSION_LIMITS.MAX_MESSAGES_PER_MODEL;
|
|
571
|
+
this.rateLimitPerMinute = options?.rateLimitPerMinute ?? SESSION_LIMITS.RATE_LIMIT_PER_MINUTE;
|
|
572
|
+
const cleanupIntervalMs = options?.cleanupIntervalMs ?? SESSION_LIMITS.CLEANUP_INTERVAL_MS;
|
|
573
|
+
this.startCleanupTimer(cleanupIntervalMs);
|
|
574
|
+
}
|
|
575
|
+
createSession(options) {
|
|
576
|
+
const sessionCount = Object.keys(this.sessions).length;
|
|
577
|
+
if (sessionCount >= this.maxSessions) {
|
|
578
|
+
this.evictOldestSession();
|
|
579
|
+
}
|
|
580
|
+
const sessionId = toSessionId(randomUUID());
|
|
581
|
+
const now = Date.now();
|
|
582
|
+
const modelConversations = {};
|
|
583
|
+
for (const model of options.models) {
|
|
584
|
+
modelConversations[model] = {
|
|
585
|
+
model,
|
|
586
|
+
messages: [
|
|
587
|
+
{ role: "system", content: options.systemPrompt, timestamp: now },
|
|
588
|
+
{
|
|
589
|
+
role: "user",
|
|
590
|
+
content: options.initialUserMessage,
|
|
591
|
+
timestamp: now
|
|
592
|
+
}
|
|
593
|
+
],
|
|
594
|
+
lastActive: now
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
const session = {
|
|
598
|
+
id: sessionId,
|
|
599
|
+
topic: options.topic,
|
|
600
|
+
discussionType: options.discussionType,
|
|
601
|
+
modelConversations,
|
|
602
|
+
createdAt: now,
|
|
603
|
+
lastActiveAt: now,
|
|
604
|
+
models: [...options.models]
|
|
605
|
+
};
|
|
606
|
+
this.sessions[sessionId] = session;
|
|
607
|
+
this.rateLimits[sessionId] = {
|
|
608
|
+
sessionId,
|
|
609
|
+
requestCount: 1,
|
|
610
|
+
// Count the initial creation
|
|
611
|
+
windowStart: now
|
|
612
|
+
};
|
|
613
|
+
logger.info("Created discussion session", {
|
|
614
|
+
sessionId,
|
|
615
|
+
topic: options.topic,
|
|
616
|
+
discussionType: options.discussionType,
|
|
617
|
+
modelCount: options.models.length
|
|
618
|
+
});
|
|
619
|
+
return session;
|
|
620
|
+
}
|
|
621
|
+
getSession(id) {
|
|
622
|
+
const session = this.sessions[id];
|
|
623
|
+
if (session) {
|
|
624
|
+
session.lastActiveAt = Date.now();
|
|
625
|
+
}
|
|
626
|
+
return session;
|
|
627
|
+
}
|
|
628
|
+
addUserMessage(id, message) {
|
|
629
|
+
const session = this.sessions[id];
|
|
630
|
+
if (!session) {
|
|
631
|
+
return false;
|
|
632
|
+
}
|
|
633
|
+
const now = Date.now();
|
|
634
|
+
session.lastActiveAt = now;
|
|
635
|
+
for (const model of session.models) {
|
|
636
|
+
const conversation = session.modelConversations[model];
|
|
637
|
+
if (conversation) {
|
|
638
|
+
if (conversation.messages.length >= this.maxMessagesPerModel) {
|
|
639
|
+
const systemMsg = conversation.messages[0];
|
|
640
|
+
if (systemMsg) {
|
|
641
|
+
conversation.messages = [
|
|
642
|
+
systemMsg,
|
|
643
|
+
...conversation.messages.slice(-(this.maxMessagesPerModel - 2))
|
|
644
|
+
];
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
conversation.messages.push({
|
|
648
|
+
role: "user",
|
|
649
|
+
content: message,
|
|
650
|
+
timestamp: now
|
|
651
|
+
});
|
|
652
|
+
conversation.lastActive = now;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return true;
|
|
656
|
+
}
|
|
657
|
+
addAssistantMessage(id, model, response) {
|
|
658
|
+
const session = this.sessions[id];
|
|
659
|
+
if (!session) {
|
|
660
|
+
return false;
|
|
661
|
+
}
|
|
662
|
+
const conversation = session.modelConversations[model];
|
|
663
|
+
if (!conversation) {
|
|
664
|
+
return false;
|
|
665
|
+
}
|
|
666
|
+
const now = Date.now();
|
|
667
|
+
conversation.messages.push({
|
|
668
|
+
role: "assistant",
|
|
669
|
+
content: response,
|
|
670
|
+
timestamp: now
|
|
671
|
+
});
|
|
672
|
+
conversation.lastActive = now;
|
|
673
|
+
session.lastActiveAt = now;
|
|
674
|
+
return true;
|
|
675
|
+
}
|
|
676
|
+
getModelMessages(id, model) {
|
|
677
|
+
const session = this.sessions[id];
|
|
678
|
+
if (!session) {
|
|
679
|
+
return void 0;
|
|
680
|
+
}
|
|
681
|
+
session.lastActiveAt = Date.now();
|
|
682
|
+
return session.modelConversations[model]?.messages;
|
|
683
|
+
}
|
|
684
|
+
deleteSession(id) {
|
|
685
|
+
const existed = id in this.sessions;
|
|
686
|
+
if (existed) {
|
|
687
|
+
delete this.sessions[id];
|
|
688
|
+
delete this.rateLimits[id];
|
|
689
|
+
logger.debug("Deleted session", { sessionId: id });
|
|
690
|
+
}
|
|
691
|
+
return existed;
|
|
692
|
+
}
|
|
693
|
+
getSessionCount() {
|
|
694
|
+
return Object.keys(this.sessions).length;
|
|
695
|
+
}
|
|
696
|
+
checkRateLimit(id) {
|
|
697
|
+
const now = Date.now();
|
|
698
|
+
const windowMs = 60 * 1e3;
|
|
699
|
+
const state = this.rateLimits[id];
|
|
700
|
+
if (!state) {
|
|
701
|
+
return {
|
|
702
|
+
allowed: false,
|
|
703
|
+
remainingRequests: 0,
|
|
704
|
+
resetInMs: 0
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
if (now - state.windowStart >= windowMs) {
|
|
708
|
+
state.requestCount = 0;
|
|
709
|
+
state.windowStart = now;
|
|
710
|
+
}
|
|
711
|
+
const remaining = this.rateLimitPerMinute - state.requestCount;
|
|
712
|
+
const resetInMs = windowMs - (now - state.windowStart);
|
|
713
|
+
if (state.requestCount >= this.rateLimitPerMinute) {
|
|
714
|
+
return {
|
|
715
|
+
allowed: false,
|
|
716
|
+
remainingRequests: 0,
|
|
717
|
+
resetInMs
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
state.requestCount++;
|
|
721
|
+
return {
|
|
722
|
+
allowed: true,
|
|
723
|
+
remainingRequests: remaining - 1,
|
|
724
|
+
resetInMs
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
shutdown() {
|
|
728
|
+
if (this.cleanupInterval) {
|
|
729
|
+
clearInterval(this.cleanupInterval);
|
|
730
|
+
this.cleanupInterval = null;
|
|
731
|
+
}
|
|
732
|
+
const sessionCount = Object.keys(this.sessions).length;
|
|
733
|
+
this.sessions = {};
|
|
734
|
+
this.rateLimits = {};
|
|
735
|
+
logger.info("Session store shutdown", { clearedSessions: sessionCount });
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Start the periodic cleanup timer
|
|
739
|
+
*/
|
|
740
|
+
startCleanupTimer(intervalMs) {
|
|
741
|
+
this.cleanupInterval = setInterval(() => {
|
|
742
|
+
this.cleanupExpiredSessions();
|
|
743
|
+
}, intervalMs);
|
|
744
|
+
this.cleanupInterval.unref();
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Remove sessions that have exceeded the TTL
|
|
748
|
+
*/
|
|
749
|
+
cleanupExpiredSessions() {
|
|
750
|
+
const now = Date.now();
|
|
751
|
+
let cleaned = 0;
|
|
752
|
+
for (const sessionId of Object.keys(this.sessions)) {
|
|
753
|
+
const session = this.sessions[sessionId];
|
|
754
|
+
if (session && now - session.lastActiveAt > this.ttlMs) {
|
|
755
|
+
delete this.sessions[sessionId];
|
|
756
|
+
delete this.rateLimits[sessionId];
|
|
757
|
+
cleaned++;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
if (cleaned > 0) {
|
|
761
|
+
logger.info("Cleaned up expired sessions", { count: cleaned });
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Evict the oldest session when at capacity (LRU eviction)
|
|
766
|
+
*/
|
|
767
|
+
evictOldestSession() {
|
|
768
|
+
let oldestId = null;
|
|
769
|
+
let oldestTime = Number.POSITIVE_INFINITY;
|
|
770
|
+
for (const sessionId of Object.keys(this.sessions)) {
|
|
771
|
+
const session = this.sessions[sessionId];
|
|
772
|
+
if (session && session.lastActiveAt < oldestTime) {
|
|
773
|
+
oldestTime = session.lastActiveAt;
|
|
774
|
+
oldestId = sessionId;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
if (oldestId) {
|
|
778
|
+
delete this.sessions[oldestId];
|
|
779
|
+
delete this.rateLimits[oldestId];
|
|
780
|
+
logger.warn("Evicted oldest session due to capacity", {
|
|
781
|
+
sessionId: oldestId
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
};
|
|
786
|
+
|
|
787
|
+
// src/tools/conversation-factory.ts
|
|
788
|
+
function formatDiscussionResults(results, sessionId, isNewSession, topic) {
|
|
789
|
+
const header = isNewSession ? `# Council Discussion Started
|
|
790
|
+
|
|
791
|
+
**Topic:** ${topic}
|
|
792
|
+
**Session ID:** \`${sessionId}\`
|
|
793
|
+
|
|
794
|
+
_Use this session_id in subsequent calls to continue the discussion._
|
|
795
|
+
|
|
796
|
+
---
|
|
797
|
+
|
|
798
|
+
` : `# Council Discussion Continued
|
|
799
|
+
|
|
800
|
+
**Session ID:** \`${sessionId}\`
|
|
801
|
+
|
|
802
|
+
---
|
|
803
|
+
|
|
804
|
+
`;
|
|
805
|
+
const responses = results.map((r) => {
|
|
806
|
+
const modelName = r.model.split("/").pop() || r.model;
|
|
807
|
+
if (r.error) {
|
|
808
|
+
return `## ${modelName}
|
|
809
|
+
|
|
810
|
+
**Error:** ${r.error}`;
|
|
811
|
+
}
|
|
812
|
+
return `## ${modelName}
|
|
813
|
+
|
|
814
|
+
${r.review}`;
|
|
815
|
+
}).join("\n\n---\n\n");
|
|
816
|
+
const footer = `
|
|
817
|
+
|
|
818
|
+
---
|
|
819
|
+
|
|
820
|
+
**Session ID:** \`${sessionId}\` _(include in your next message to continue)_`;
|
|
821
|
+
return header + responses + footer;
|
|
822
|
+
}
|
|
823
|
+
function createConversationTool(server2, config, sessionStore2) {
|
|
824
|
+
server2.registerTool(
|
|
825
|
+
config.name,
|
|
826
|
+
{
|
|
827
|
+
description: config.description,
|
|
828
|
+
inputSchema: config.inputSchema
|
|
829
|
+
},
|
|
830
|
+
async (input) => {
|
|
831
|
+
try {
|
|
832
|
+
logger.debug(`Starting ${config.name}`, {
|
|
833
|
+
inputKeys: Object.keys(input)
|
|
834
|
+
});
|
|
835
|
+
const { results, models, sessionId, isNewSession, topic } = await config.handler(input, sessionStore2);
|
|
836
|
+
logger.info(`Completed ${config.name}`, {
|
|
837
|
+
sessionId,
|
|
838
|
+
isNewSession,
|
|
839
|
+
modelCount: models.length,
|
|
840
|
+
successCount: results.filter((r) => !r.error).length,
|
|
841
|
+
errorCount: results.filter((r) => r.error).length
|
|
842
|
+
});
|
|
843
|
+
return {
|
|
844
|
+
content: [
|
|
845
|
+
{
|
|
846
|
+
type: "text",
|
|
847
|
+
text: formatDiscussionResults(
|
|
848
|
+
results,
|
|
849
|
+
sessionId,
|
|
850
|
+
isNewSession,
|
|
851
|
+
topic
|
|
852
|
+
)
|
|
853
|
+
}
|
|
854
|
+
]
|
|
855
|
+
};
|
|
856
|
+
} catch (error) {
|
|
857
|
+
logger.error(
|
|
858
|
+
`Error in ${config.name}`,
|
|
859
|
+
error instanceof Error ? error : new Error(String(error))
|
|
860
|
+
);
|
|
861
|
+
return formatError(error);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// src/tools/discuss-council.ts
|
|
868
|
+
import { z } from "zod";
|
|
869
|
+
|
|
870
|
+
// src/prompts/discussion.ts
|
|
871
|
+
var DISCUSSION_SYSTEM_PROMPTS = {
|
|
872
|
+
code_review: `You are a senior software engineer participating in a code review council discussion.
|
|
873
|
+
|
|
874
|
+
Your role:
|
|
875
|
+
- Provide thoughtful, constructive feedback on code and technical decisions
|
|
876
|
+
- Build on your previous responses in this conversation
|
|
877
|
+
- Consider alternative approaches and trade-offs
|
|
878
|
+
- Be specific with examples and suggestions
|
|
879
|
+
- Respectfully challenge ideas while remaining collaborative
|
|
880
|
+
|
|
881
|
+
Focus on code quality, maintainability, performance, security, and best practices.
|
|
882
|
+
Keep responses focused and actionable.`,
|
|
883
|
+
plan_review: `You are a senior software architect participating in a planning council discussion.
|
|
884
|
+
|
|
885
|
+
Your role:
|
|
886
|
+
- Evaluate implementation plans, architecture decisions, and technical strategies
|
|
887
|
+
- Build on your previous responses in this conversation
|
|
888
|
+
- Consider feasibility, risks, scalability, and maintainability
|
|
889
|
+
- Suggest alternatives and improvements
|
|
890
|
+
- Think about edge cases and potential issues
|
|
891
|
+
|
|
892
|
+
Focus on practical implementation concerns and long-term implications.
|
|
893
|
+
Keep responses focused and actionable.`,
|
|
894
|
+
general: `You are a knowledgeable advisor participating in a council discussion.
|
|
895
|
+
|
|
896
|
+
Your role:
|
|
897
|
+
- Provide thoughtful, well-reasoned perspectives on the topic
|
|
898
|
+
- Build on your previous responses in this conversation
|
|
899
|
+
- Consider multiple viewpoints and trade-offs
|
|
900
|
+
- Support your points with examples and reasoning
|
|
901
|
+
- Be open to exploring different approaches
|
|
902
|
+
|
|
903
|
+
Keep responses focused and constructive.`
|
|
904
|
+
};
|
|
905
|
+
function getSystemPrompt(discussionType) {
|
|
906
|
+
return DISCUSSION_SYSTEM_PROMPTS[discussionType];
|
|
907
|
+
}
|
|
908
|
+
function buildInitialMessage(message, discussionType, context) {
|
|
909
|
+
const typeLabel = {
|
|
910
|
+
code_review: "Code Review Discussion",
|
|
911
|
+
plan_review: "Plan Review Discussion",
|
|
912
|
+
general: "Discussion"
|
|
913
|
+
};
|
|
914
|
+
let content = `**${typeLabel[discussionType]}**
|
|
915
|
+
|
|
916
|
+
${message}`;
|
|
917
|
+
if (context) {
|
|
918
|
+
content += `
|
|
919
|
+
|
|
920
|
+
**Additional Context:**
|
|
921
|
+
${context}`;
|
|
922
|
+
}
|
|
923
|
+
return content;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// src/tools/discuss-council.ts
|
|
927
|
+
var discussCouncilSchema = {
|
|
928
|
+
message: z.string().min(1, "Message cannot be empty").max(
|
|
929
|
+
SESSION_LIMITS.MAX_MESSAGE_LENGTH,
|
|
930
|
+
`Message exceeds maximum length of ${SESSION_LIMITS.MAX_MESSAGE_LENGTH} characters`
|
|
931
|
+
).describe("Your message or question for the council"),
|
|
932
|
+
session_id: z.string().uuid("Invalid session ID format").optional().describe(
|
|
933
|
+
"Session ID to continue an existing discussion. Omit to start a new discussion."
|
|
934
|
+
),
|
|
935
|
+
discussion_type: z.enum(DISCUSSION_TYPES).optional().describe(
|
|
936
|
+
"Type of discussion (only used when starting new session). 'code_review' for code-related discussions, 'plan_review' for architecture/planning, 'general' for other topics. Default: general"
|
|
937
|
+
),
|
|
938
|
+
context: z.string().max(
|
|
939
|
+
SESSION_LIMITS.MAX_MESSAGE_LENGTH,
|
|
940
|
+
`Context exceeds maximum length of ${SESSION_LIMITS.MAX_MESSAGE_LENGTH} characters`
|
|
941
|
+
).optional().describe(
|
|
942
|
+
"Additional context for new discussions (code snippets, plan details, etc.)"
|
|
943
|
+
)
|
|
404
944
|
};
|
|
945
|
+
async function handleDiscussCouncil(client2, input, sessionStore2) {
|
|
946
|
+
const { message, session_id, discussion_type, context } = input;
|
|
947
|
+
let sessionId;
|
|
948
|
+
let isNewSession = false;
|
|
949
|
+
if (session_id) {
|
|
950
|
+
sessionId = toSessionId(session_id);
|
|
951
|
+
const existingSession = sessionStore2.getSession(sessionId);
|
|
952
|
+
if (!existingSession) {
|
|
953
|
+
throw new ValidationError(
|
|
954
|
+
`Session not found: ${session_id}. It may have expired or been deleted.`,
|
|
955
|
+
"session_id"
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
const rateLimitResult = sessionStore2.checkRateLimit(sessionId);
|
|
959
|
+
if (!rateLimitResult.allowed) {
|
|
960
|
+
throw new ValidationError(
|
|
961
|
+
`Rate limit exceeded. Please wait ${Math.ceil(rateLimitResult.resetInMs / 1e3)} seconds before sending another message.`,
|
|
962
|
+
"session_id"
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
sessionStore2.addUserMessage(sessionId, message);
|
|
966
|
+
logger.info("Continuing council discussion", {
|
|
967
|
+
sessionId,
|
|
968
|
+
modelCount: existingSession.models.length
|
|
969
|
+
});
|
|
970
|
+
} else {
|
|
971
|
+
isNewSession = true;
|
|
972
|
+
const type = discussion_type || "general";
|
|
973
|
+
const systemPrompt = getSystemPrompt(type);
|
|
974
|
+
const initialMessage = buildInitialMessage(message, type, context);
|
|
975
|
+
const topic = message.length > 100 ? `${message.slice(0, 97)}...` : message;
|
|
976
|
+
const session2 = sessionStore2.createSession({
|
|
977
|
+
topic,
|
|
978
|
+
discussionType: type,
|
|
979
|
+
models: DISCUSSION_MODELS,
|
|
980
|
+
systemPrompt,
|
|
981
|
+
initialUserMessage: initialMessage
|
|
982
|
+
});
|
|
983
|
+
sessionId = session2.id;
|
|
984
|
+
logger.info("Started new council discussion", {
|
|
985
|
+
sessionId,
|
|
986
|
+
discussionType: type,
|
|
987
|
+
modelCount: DISCUSSION_MODELS.length
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
const session = sessionStore2.getSession(sessionId);
|
|
991
|
+
if (!session) {
|
|
992
|
+
throw new ValidationError("Session was unexpectedly deleted", "session_id");
|
|
993
|
+
}
|
|
994
|
+
const currentSessionId = sessionId;
|
|
995
|
+
const results = await client2.discussWithCouncil(session.models, (model) => {
|
|
996
|
+
const messages = sessionStore2.getModelMessages(currentSessionId, model);
|
|
997
|
+
if (!messages) {
|
|
998
|
+
throw new Error(`No messages found for model ${model}`);
|
|
999
|
+
}
|
|
1000
|
+
return messages;
|
|
1001
|
+
});
|
|
1002
|
+
for (const result of results) {
|
|
1003
|
+
if (!result.error && result.review) {
|
|
1004
|
+
sessionStore2.addAssistantMessage(sessionId, result.model, result.review);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
return {
|
|
1008
|
+
results,
|
|
1009
|
+
models: session.models,
|
|
1010
|
+
sessionId,
|
|
1011
|
+
isNewSession,
|
|
1012
|
+
topic: session.topic
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
405
1015
|
|
|
406
1016
|
// src/tools/factory.ts
|
|
1017
|
+
import { readFileSync } from "fs";
|
|
1018
|
+
import { dirname, join } from "path";
|
|
1019
|
+
import { fileURLToPath } from "url";
|
|
407
1020
|
function formatResults(results) {
|
|
408
1021
|
return results.map((r) => {
|
|
409
1022
|
if (r.error) {
|
|
@@ -416,6 +1029,39 @@ function formatResults(results) {
|
|
|
416
1029
|
${r.review}`;
|
|
417
1030
|
}).join("\n\n---\n\n");
|
|
418
1031
|
}
|
|
1032
|
+
function formatResultsAsHtml(results, templatePath, data = {}) {
|
|
1033
|
+
try {
|
|
1034
|
+
let template = readFileSync(templatePath, "utf-8");
|
|
1035
|
+
const modelPerspectives = results.map((r) => ({
|
|
1036
|
+
model: r.model,
|
|
1037
|
+
content: r.error ? `Error: ${r.error}` : r.review,
|
|
1038
|
+
hasError: !!r.error
|
|
1039
|
+
}));
|
|
1040
|
+
const reportData = {
|
|
1041
|
+
analysis: data.analysis || null,
|
|
1042
|
+
repoName: data.repoName || "Unknown Repository",
|
|
1043
|
+
modelPerspectives,
|
|
1044
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1045
|
+
};
|
|
1046
|
+
template = template.replace(
|
|
1047
|
+
"{{REPORT_DATA}}",
|
|
1048
|
+
JSON.stringify(reportData, null, 2)
|
|
1049
|
+
);
|
|
1050
|
+
return template;
|
|
1051
|
+
} catch (error) {
|
|
1052
|
+
logger.error("Failed to generate HTML report", error);
|
|
1053
|
+
return formatResults(results);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
function getTemplatesDir() {
|
|
1057
|
+
try {
|
|
1058
|
+
const __filename2 = fileURLToPath(import.meta.url);
|
|
1059
|
+
const __dirname2 = dirname(__filename2);
|
|
1060
|
+
return join(__dirname2, "..", "..", "templates");
|
|
1061
|
+
} catch {
|
|
1062
|
+
return join(process.cwd(), "templates");
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
419
1065
|
function createReviewTool(server2, config) {
|
|
420
1066
|
server2.registerTool(
|
|
421
1067
|
config.name,
|
|
@@ -485,12 +1131,12 @@ To customize models, set environment variables in your MCP config:
|
|
|
485
1131
|
}
|
|
486
1132
|
|
|
487
1133
|
// src/tools/review-backend.ts
|
|
488
|
-
import { z } from "zod";
|
|
1134
|
+
import { z as z2 } from "zod";
|
|
489
1135
|
var backendReviewSchema = {
|
|
490
|
-
code:
|
|
491
|
-
language:
|
|
492
|
-
review_type:
|
|
493
|
-
context:
|
|
1136
|
+
code: z2.string().describe("The backend code to review"),
|
|
1137
|
+
language: z2.string().optional().describe("Programming language/framework (e.g., node, python, go, rust)"),
|
|
1138
|
+
review_type: z2.enum(["security", "performance", "architecture", "full"]).optional().describe("Type of review to perform (default: full)"),
|
|
1139
|
+
context: z2.string().optional().describe("Additional context")
|
|
494
1140
|
};
|
|
495
1141
|
async function handleBackendReview(client2, input) {
|
|
496
1142
|
const { code, language, review_type, context } = input;
|
|
@@ -513,11 +1159,11 @@ async function handleBackendReview(client2, input) {
|
|
|
513
1159
|
}
|
|
514
1160
|
|
|
515
1161
|
// src/tools/review-code.ts
|
|
516
|
-
import { z as
|
|
1162
|
+
import { z as z3 } from "zod";
|
|
517
1163
|
var codeReviewSchema = {
|
|
518
|
-
code:
|
|
519
|
-
language:
|
|
520
|
-
context:
|
|
1164
|
+
code: z3.string().describe("The code to review"),
|
|
1165
|
+
language: z3.string().optional().describe("Programming language of the code"),
|
|
1166
|
+
context: z3.string().optional().describe("Additional context about the code")
|
|
521
1167
|
};
|
|
522
1168
|
async function handleCodeReview(client2, input) {
|
|
523
1169
|
const { code, language, context } = input;
|
|
@@ -541,12 +1187,12 @@ ${context}` : ""}` : context;
|
|
|
541
1187
|
}
|
|
542
1188
|
|
|
543
1189
|
// src/tools/review-frontend.ts
|
|
544
|
-
import { z as
|
|
1190
|
+
import { z as z4 } from "zod";
|
|
545
1191
|
var frontendReviewSchema = {
|
|
546
|
-
code:
|
|
547
|
-
framework:
|
|
548
|
-
review_type:
|
|
549
|
-
context:
|
|
1192
|
+
code: z4.string().describe("The frontend code to review"),
|
|
1193
|
+
framework: z4.string().optional().describe("Frontend framework (e.g., react, vue, svelte)"),
|
|
1194
|
+
review_type: z4.enum(["accessibility", "performance", "ux", "full"]).optional().describe("Type of review to perform (default: full)"),
|
|
1195
|
+
context: z4.string().optional().describe("Additional context")
|
|
550
1196
|
};
|
|
551
1197
|
async function handleFrontendReview(client2, input) {
|
|
552
1198
|
const { code, framework, review_type, context } = input;
|
|
@@ -570,13 +1216,13 @@ async function handleFrontendReview(client2, input) {
|
|
|
570
1216
|
|
|
571
1217
|
// src/tools/review-git.ts
|
|
572
1218
|
import { execSync } from "child_process";
|
|
573
|
-
import { z as
|
|
574
|
-
var gitReviewSchemaObj =
|
|
575
|
-
review_type:
|
|
1219
|
+
import { z as z5 } from "zod";
|
|
1220
|
+
var gitReviewSchemaObj = z5.object({
|
|
1221
|
+
review_type: z5.enum(["staged", "unstaged", "diff", "commit"]).optional().describe(
|
|
576
1222
|
"Type of changes to review: 'staged' (git diff --cached), 'unstaged' (git diff), 'diff' (git diff main..HEAD), 'commit' (specific commit). Default: staged"
|
|
577
1223
|
),
|
|
578
|
-
commit_hash:
|
|
579
|
-
context:
|
|
1224
|
+
commit_hash: z5.string().optional().describe("Commit hash to review (only used when review_type is 'commit')"),
|
|
1225
|
+
context: z5.string().optional().describe("Additional context about the changes")
|
|
580
1226
|
});
|
|
581
1227
|
var gitReviewSchema = gitReviewSchemaObj.shape;
|
|
582
1228
|
function getGitDiff(reviewType = "staged", commitHash) {
|
|
@@ -637,11 +1283,11 @@ async function handleGitReview(client2, models, input) {
|
|
|
637
1283
|
}
|
|
638
1284
|
|
|
639
1285
|
// src/tools/review-plan.ts
|
|
640
|
-
import { z as
|
|
1286
|
+
import { z as z6 } from "zod";
|
|
641
1287
|
var planReviewSchema = {
|
|
642
|
-
plan:
|
|
643
|
-
review_type:
|
|
644
|
-
context:
|
|
1288
|
+
plan: z6.string().describe("The implementation plan to review"),
|
|
1289
|
+
review_type: z6.enum(["feasibility", "completeness", "risks", "timeline", "full"]).optional().describe("Type of review to perform (default: full)"),
|
|
1290
|
+
context: z6.string().optional().describe("Additional context about the project or constraints")
|
|
645
1291
|
};
|
|
646
1292
|
async function handlePlanReview(client2, input) {
|
|
647
1293
|
const { plan, review_type, context } = input;
|
|
@@ -661,6 +1307,598 @@ async function handlePlanReview(client2, input) {
|
|
|
661
1307
|
};
|
|
662
1308
|
}
|
|
663
1309
|
|
|
1310
|
+
// src/tools/tps-audit.ts
|
|
1311
|
+
import { join as join4 } from "path";
|
|
1312
|
+
import { z as z7 } from "zod";
|
|
1313
|
+
|
|
1314
|
+
// src/utils/repo-scanner.ts
|
|
1315
|
+
import { readdirSync, readFileSync as readFileSync2, statSync } from "fs";
|
|
1316
|
+
import { extname, join as join3, relative as relative2 } from "path";
|
|
1317
|
+
import ignore from "ignore";
|
|
1318
|
+
|
|
1319
|
+
// src/utils/git-operations.ts
|
|
1320
|
+
import { execSync as execSync2 } from "child_process";
|
|
1321
|
+
import { existsSync, lstatSync, realpathSync } from "fs";
|
|
1322
|
+
import { dirname as dirname2, join as join2, normalize, relative, resolve } from "path";
|
|
1323
|
+
var MAX_TRAVERSAL_DEPTH = 20;
|
|
1324
|
+
function findGitRoot(startPath) {
|
|
1325
|
+
let currentPath = resolve(startPath);
|
|
1326
|
+
let depth = 0;
|
|
1327
|
+
while (depth < MAX_TRAVERSAL_DEPTH) {
|
|
1328
|
+
const gitPath = join2(currentPath, ".git");
|
|
1329
|
+
if (existsSync(gitPath)) {
|
|
1330
|
+
logger.debug("Found git root", { path: currentPath, depth });
|
|
1331
|
+
return currentPath;
|
|
1332
|
+
}
|
|
1333
|
+
const parentPath = dirname2(currentPath);
|
|
1334
|
+
if (parentPath === currentPath) {
|
|
1335
|
+
throw new Error(
|
|
1336
|
+
`Not in a git repository. Searched from ${startPath} to filesystem root.`
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1339
|
+
currentPath = parentPath;
|
|
1340
|
+
depth++;
|
|
1341
|
+
}
|
|
1342
|
+
throw new Error(
|
|
1343
|
+
`Max directory traversal depth (${MAX_TRAVERSAL_DEPTH}) exceeded while searching for git root.`
|
|
1344
|
+
);
|
|
1345
|
+
}
|
|
1346
|
+
function isInsideRepo(filePath, repoRoot) {
|
|
1347
|
+
try {
|
|
1348
|
+
const normalizedRepo = normalize(resolve(repoRoot));
|
|
1349
|
+
const normalizedFile = normalize(resolve(filePath));
|
|
1350
|
+
const relativePath = relative(normalizedRepo, normalizedFile);
|
|
1351
|
+
if (relativePath.startsWith("..") || resolve(relativePath) === relativePath) {
|
|
1352
|
+
return false;
|
|
1353
|
+
}
|
|
1354
|
+
return true;
|
|
1355
|
+
} catch {
|
|
1356
|
+
return false;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
function resolveAndValidatePath(filePath, repoRoot) {
|
|
1360
|
+
try {
|
|
1361
|
+
const absolutePath = resolve(repoRoot, filePath);
|
|
1362
|
+
const stats = lstatSync(absolutePath);
|
|
1363
|
+
if (stats.isSymbolicLink()) {
|
|
1364
|
+
const realPath = realpathSync(absolutePath);
|
|
1365
|
+
if (!isInsideRepo(realPath, repoRoot)) {
|
|
1366
|
+
logger.warn("Symlink target outside repository", {
|
|
1367
|
+
symlink: filePath,
|
|
1368
|
+
target: realPath,
|
|
1369
|
+
repo: repoRoot
|
|
1370
|
+
});
|
|
1371
|
+
return null;
|
|
1372
|
+
}
|
|
1373
|
+
return realPath;
|
|
1374
|
+
}
|
|
1375
|
+
if (!isInsideRepo(absolutePath, repoRoot)) {
|
|
1376
|
+
return null;
|
|
1377
|
+
}
|
|
1378
|
+
return absolutePath;
|
|
1379
|
+
} catch (error) {
|
|
1380
|
+
logger.debug("Path resolution failed", { filePath, error });
|
|
1381
|
+
return null;
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
// src/utils/repo-scanner.ts
|
|
1386
|
+
var SENSITIVE_FILE_PATTERNS = [
|
|
1387
|
+
".env",
|
|
1388
|
+
".env.*",
|
|
1389
|
+
"*.pem",
|
|
1390
|
+
"*.key",
|
|
1391
|
+
"*.p12",
|
|
1392
|
+
"*.pfx",
|
|
1393
|
+
"*.crt",
|
|
1394
|
+
"*credentials*",
|
|
1395
|
+
"*secret*",
|
|
1396
|
+
"id_rsa*",
|
|
1397
|
+
"id_ed25519*",
|
|
1398
|
+
"id_dsa*",
|
|
1399
|
+
"id_ecdsa*",
|
|
1400
|
+
".npmrc",
|
|
1401
|
+
".pypirc",
|
|
1402
|
+
"kubeconfig",
|
|
1403
|
+
".kube/config",
|
|
1404
|
+
".docker/config.json",
|
|
1405
|
+
"*password*",
|
|
1406
|
+
"*token*",
|
|
1407
|
+
"auth.json",
|
|
1408
|
+
".netrc",
|
|
1409
|
+
".git-credentials",
|
|
1410
|
+
"*.keystore",
|
|
1411
|
+
"*.jks",
|
|
1412
|
+
"service-account*.json",
|
|
1413
|
+
"gcloud*.json"
|
|
1414
|
+
];
|
|
1415
|
+
var SECRET_CONTENT_PATTERNS = [
|
|
1416
|
+
/AKIA[0-9A-Z]{16}/g,
|
|
1417
|
+
// AWS Access Key ID
|
|
1418
|
+
/-----BEGIN\s+(RSA |DSA |EC |OPENSSH )?PRIVATE KEY-----/g,
|
|
1419
|
+
// Private keys
|
|
1420
|
+
/-----BEGIN\s+PGP PRIVATE KEY BLOCK-----/g,
|
|
1421
|
+
// PGP private key
|
|
1422
|
+
/ghp_[a-zA-Z0-9]{36}/g,
|
|
1423
|
+
// GitHub Personal Access Token
|
|
1424
|
+
/gho_[a-zA-Z0-9]{36}/g,
|
|
1425
|
+
// GitHub OAuth Token
|
|
1426
|
+
/ghs_[a-zA-Z0-9]{36}/g,
|
|
1427
|
+
// GitHub Server Token
|
|
1428
|
+
/ghu_[a-zA-Z0-9]{36}/g,
|
|
1429
|
+
// GitHub User Token
|
|
1430
|
+
/sk-[a-zA-Z0-9]{48}/g,
|
|
1431
|
+
// OpenAI API Key
|
|
1432
|
+
/sk-proj-[a-zA-Z0-9]{48}/g,
|
|
1433
|
+
// OpenAI Project Key
|
|
1434
|
+
/xox[baprs]-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24}/g,
|
|
1435
|
+
// Slack tokens
|
|
1436
|
+
/eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*/g,
|
|
1437
|
+
// JWT tokens
|
|
1438
|
+
/AIza[0-9A-Za-z_-]{35}/g,
|
|
1439
|
+
// Google API Key
|
|
1440
|
+
/[0-9a-f]{32}-us[0-9]+/g,
|
|
1441
|
+
// Mailchimp API Key
|
|
1442
|
+
/SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}/g,
|
|
1443
|
+
// SendGrid API Key
|
|
1444
|
+
/sq0[a-z]{3}-[0-9A-Za-z_-]{22}/g
|
|
1445
|
+
// Square tokens
|
|
1446
|
+
];
|
|
1447
|
+
var DEFAULT_FILE_EXTENSIONS = [
|
|
1448
|
+
".ts",
|
|
1449
|
+
".tsx",
|
|
1450
|
+
".js",
|
|
1451
|
+
".jsx",
|
|
1452
|
+
".mjs",
|
|
1453
|
+
".cjs",
|
|
1454
|
+
".py",
|
|
1455
|
+
".go",
|
|
1456
|
+
".rs",
|
|
1457
|
+
".java",
|
|
1458
|
+
".kt",
|
|
1459
|
+
".scala",
|
|
1460
|
+
".rb",
|
|
1461
|
+
".php",
|
|
1462
|
+
".cs",
|
|
1463
|
+
".cpp",
|
|
1464
|
+
".c",
|
|
1465
|
+
".h",
|
|
1466
|
+
".hpp",
|
|
1467
|
+
".swift",
|
|
1468
|
+
".vue",
|
|
1469
|
+
".svelte",
|
|
1470
|
+
".astro"
|
|
1471
|
+
];
|
|
1472
|
+
var EXCLUDED_DIRS = [
|
|
1473
|
+
"node_modules",
|
|
1474
|
+
".git",
|
|
1475
|
+
"dist",
|
|
1476
|
+
"build",
|
|
1477
|
+
"out",
|
|
1478
|
+
".next",
|
|
1479
|
+
".nuxt",
|
|
1480
|
+
"__pycache__",
|
|
1481
|
+
".pytest_cache",
|
|
1482
|
+
".mypy_cache",
|
|
1483
|
+
"venv",
|
|
1484
|
+
".venv",
|
|
1485
|
+
"env",
|
|
1486
|
+
".env",
|
|
1487
|
+
"vendor",
|
|
1488
|
+
"target",
|
|
1489
|
+
".idea",
|
|
1490
|
+
".vscode",
|
|
1491
|
+
"coverage",
|
|
1492
|
+
".nyc_output"
|
|
1493
|
+
];
|
|
1494
|
+
var HARD_LIMITS = {
|
|
1495
|
+
MAX_FILES: 100,
|
|
1496
|
+
MAX_FILE_SIZE: 100 * 1024,
|
|
1497
|
+
// 100KB per file
|
|
1498
|
+
MAX_TOTAL_SIZE: 1024 * 1024
|
|
1499
|
+
// 1MB total
|
|
1500
|
+
};
|
|
1501
|
+
var DEFAULT_OPTIONS = {
|
|
1502
|
+
maxFiles: 50,
|
|
1503
|
+
maxFileSize: 50 * 1024,
|
|
1504
|
+
// 50KB
|
|
1505
|
+
maxTotalSize: 500 * 1024,
|
|
1506
|
+
// 500KB
|
|
1507
|
+
fileTypes: DEFAULT_FILE_EXTENSIONS,
|
|
1508
|
+
skipSensitive: true,
|
|
1509
|
+
detectSecrets: true
|
|
1510
|
+
};
|
|
1511
|
+
function isSensitiveFile(filename) {
|
|
1512
|
+
const lowerName = filename.toLowerCase();
|
|
1513
|
+
for (const pattern of SENSITIVE_FILE_PATTERNS) {
|
|
1514
|
+
if (pattern.includes("*")) {
|
|
1515
|
+
const regex = new RegExp(
|
|
1516
|
+
"^" + pattern.replace(/\./g, "\\.").replace(/\*/g, ".*") + "$",
|
|
1517
|
+
"i"
|
|
1518
|
+
);
|
|
1519
|
+
if (regex.test(lowerName)) {
|
|
1520
|
+
return true;
|
|
1521
|
+
}
|
|
1522
|
+
} else if (lowerName === pattern.toLowerCase() || lowerName.endsWith(`/${pattern.toLowerCase()}`)) {
|
|
1523
|
+
return true;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
return false;
|
|
1527
|
+
}
|
|
1528
|
+
function detectEmbeddedSecrets(content) {
|
|
1529
|
+
const detected = [];
|
|
1530
|
+
const secretTypes = [
|
|
1531
|
+
{ pattern: SECRET_CONTENT_PATTERNS[0], name: "AWS Access Key" },
|
|
1532
|
+
{ pattern: SECRET_CONTENT_PATTERNS[1], name: "Private Key" },
|
|
1533
|
+
{ pattern: SECRET_CONTENT_PATTERNS[2], name: "PGP Private Key" },
|
|
1534
|
+
{ pattern: SECRET_CONTENT_PATTERNS[3], name: "GitHub PAT" },
|
|
1535
|
+
{ pattern: SECRET_CONTENT_PATTERNS[4], name: "GitHub OAuth Token" },
|
|
1536
|
+
{ pattern: SECRET_CONTENT_PATTERNS[5], name: "GitHub Server Token" },
|
|
1537
|
+
{ pattern: SECRET_CONTENT_PATTERNS[6], name: "GitHub User Token" },
|
|
1538
|
+
{ pattern: SECRET_CONTENT_PATTERNS[7], name: "OpenAI API Key" },
|
|
1539
|
+
{ pattern: SECRET_CONTENT_PATTERNS[8], name: "OpenAI Project Key" },
|
|
1540
|
+
{ pattern: SECRET_CONTENT_PATTERNS[9], name: "Slack Token" },
|
|
1541
|
+
{ pattern: SECRET_CONTENT_PATTERNS[10], name: "JWT Token" },
|
|
1542
|
+
{ pattern: SECRET_CONTENT_PATTERNS[11], name: "Google API Key" },
|
|
1543
|
+
{ pattern: SECRET_CONTENT_PATTERNS[12], name: "Mailchimp API Key" },
|
|
1544
|
+
{ pattern: SECRET_CONTENT_PATTERNS[13], name: "SendGrid API Key" },
|
|
1545
|
+
{ pattern: SECRET_CONTENT_PATTERNS[14], name: "Square Token" }
|
|
1546
|
+
];
|
|
1547
|
+
for (const { pattern, name } of secretTypes) {
|
|
1548
|
+
if (!pattern) continue;
|
|
1549
|
+
pattern.lastIndex = 0;
|
|
1550
|
+
if (pattern.test(content)) {
|
|
1551
|
+
detected.push(name);
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
return detected;
|
|
1555
|
+
}
|
|
1556
|
+
function isBinaryContent(content) {
|
|
1557
|
+
for (let i = 0; i < Math.min(content.length, 8e3); i++) {
|
|
1558
|
+
if (content[i] === 0) {
|
|
1559
|
+
return true;
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
return false;
|
|
1563
|
+
}
|
|
1564
|
+
function estimateTokens(content) {
|
|
1565
|
+
return Math.ceil(content.length / 4);
|
|
1566
|
+
}
|
|
1567
|
+
async function scanRepository(startPath, options = {}) {
|
|
1568
|
+
const opts = {
|
|
1569
|
+
...DEFAULT_OPTIONS,
|
|
1570
|
+
...options,
|
|
1571
|
+
maxFiles: Math.min(
|
|
1572
|
+
options.maxFiles ?? DEFAULT_OPTIONS.maxFiles,
|
|
1573
|
+
HARD_LIMITS.MAX_FILES
|
|
1574
|
+
),
|
|
1575
|
+
maxFileSize: Math.min(
|
|
1576
|
+
options.maxFileSize ?? DEFAULT_OPTIONS.maxFileSize,
|
|
1577
|
+
HARD_LIMITS.MAX_FILE_SIZE
|
|
1578
|
+
),
|
|
1579
|
+
maxTotalSize: Math.min(
|
|
1580
|
+
options.maxTotalSize ?? DEFAULT_OPTIONS.maxTotalSize,
|
|
1581
|
+
HARD_LIMITS.MAX_TOTAL_SIZE
|
|
1582
|
+
)
|
|
1583
|
+
};
|
|
1584
|
+
const repoRoot = findGitRoot(startPath);
|
|
1585
|
+
logger.info("Scanning repository", { repoRoot, options: opts });
|
|
1586
|
+
const ig = ignore();
|
|
1587
|
+
try {
|
|
1588
|
+
const gitignorePath = join3(repoRoot, ".gitignore");
|
|
1589
|
+
const gitignoreContent = readFileSync2(gitignorePath, "utf-8");
|
|
1590
|
+
ig.add(gitignoreContent);
|
|
1591
|
+
} catch {
|
|
1592
|
+
}
|
|
1593
|
+
ig.add(EXCLUDED_DIRS);
|
|
1594
|
+
const files = [];
|
|
1595
|
+
const skipped = [];
|
|
1596
|
+
const warnings = [];
|
|
1597
|
+
let totalSize = 0;
|
|
1598
|
+
let totalFilesFound = 0;
|
|
1599
|
+
function scanDir(dirPath, depth = 0) {
|
|
1600
|
+
if (depth > 20) {
|
|
1601
|
+
return;
|
|
1602
|
+
}
|
|
1603
|
+
if (files.length >= opts.maxFiles) {
|
|
1604
|
+
return;
|
|
1605
|
+
}
|
|
1606
|
+
let entries;
|
|
1607
|
+
try {
|
|
1608
|
+
entries = readdirSync(dirPath);
|
|
1609
|
+
} catch {
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
for (const entry of entries) {
|
|
1613
|
+
if (files.length >= opts.maxFiles) {
|
|
1614
|
+
break;
|
|
1615
|
+
}
|
|
1616
|
+
const fullPath = join3(dirPath, entry);
|
|
1617
|
+
const relativePath = relative2(repoRoot, fullPath);
|
|
1618
|
+
if (ig.ignores(relativePath)) {
|
|
1619
|
+
continue;
|
|
1620
|
+
}
|
|
1621
|
+
const validPath = resolveAndValidatePath(fullPath, repoRoot);
|
|
1622
|
+
if (!validPath) {
|
|
1623
|
+
skipped.push({
|
|
1624
|
+
path: relativePath,
|
|
1625
|
+
reason: "Path outside repository or invalid symlink"
|
|
1626
|
+
});
|
|
1627
|
+
continue;
|
|
1628
|
+
}
|
|
1629
|
+
let stats2;
|
|
1630
|
+
try {
|
|
1631
|
+
stats2 = statSync(validPath);
|
|
1632
|
+
} catch {
|
|
1633
|
+
continue;
|
|
1634
|
+
}
|
|
1635
|
+
if (stats2.isDirectory()) {
|
|
1636
|
+
scanDir(fullPath, depth + 1);
|
|
1637
|
+
} else if (stats2.isFile()) {
|
|
1638
|
+
totalFilesFound++;
|
|
1639
|
+
const ext = extname(entry).toLowerCase();
|
|
1640
|
+
if (!opts.fileTypes.includes(ext)) {
|
|
1641
|
+
continue;
|
|
1642
|
+
}
|
|
1643
|
+
if (opts.skipSensitive && isSensitiveFile(entry)) {
|
|
1644
|
+
skipped.push({
|
|
1645
|
+
path: relativePath,
|
|
1646
|
+
reason: "Potentially sensitive file"
|
|
1647
|
+
});
|
|
1648
|
+
warnings.push(`Skipped sensitive file: ${relativePath}`);
|
|
1649
|
+
continue;
|
|
1650
|
+
}
|
|
1651
|
+
if (stats2.size > opts.maxFileSize) {
|
|
1652
|
+
skipped.push({
|
|
1653
|
+
path: relativePath,
|
|
1654
|
+
reason: `File too large (${stats2.size} bytes)`
|
|
1655
|
+
});
|
|
1656
|
+
continue;
|
|
1657
|
+
}
|
|
1658
|
+
if (totalSize + stats2.size > opts.maxTotalSize) {
|
|
1659
|
+
skipped.push({
|
|
1660
|
+
path: relativePath,
|
|
1661
|
+
reason: "Total size limit reached"
|
|
1662
|
+
});
|
|
1663
|
+
continue;
|
|
1664
|
+
}
|
|
1665
|
+
let content;
|
|
1666
|
+
try {
|
|
1667
|
+
const buffer = readFileSync2(validPath);
|
|
1668
|
+
if (isBinaryContent(buffer)) {
|
|
1669
|
+
skipped.push({ path: relativePath, reason: "Binary file" });
|
|
1670
|
+
continue;
|
|
1671
|
+
}
|
|
1672
|
+
content = buffer.toString("utf-8");
|
|
1673
|
+
} catch {
|
|
1674
|
+
skipped.push({ path: relativePath, reason: "Could not read file" });
|
|
1675
|
+
continue;
|
|
1676
|
+
}
|
|
1677
|
+
if (opts.detectSecrets) {
|
|
1678
|
+
const secrets = detectEmbeddedSecrets(content);
|
|
1679
|
+
if (secrets.length > 0) {
|
|
1680
|
+
skipped.push({
|
|
1681
|
+
path: relativePath,
|
|
1682
|
+
reason: `Contains potential secrets: ${secrets.join(", ")}`
|
|
1683
|
+
});
|
|
1684
|
+
warnings.push(
|
|
1685
|
+
`Skipped file with potential secrets: ${relativePath} (${secrets.join(", ")})`
|
|
1686
|
+
);
|
|
1687
|
+
continue;
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
files.push({ path: relativePath, content });
|
|
1691
|
+
totalSize += stats2.size;
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
scanDir(repoRoot);
|
|
1696
|
+
const stats = {
|
|
1697
|
+
totalFilesFound,
|
|
1698
|
+
totalFilesIncluded: files.length,
|
|
1699
|
+
totalSize,
|
|
1700
|
+
tokenEstimate: files.reduce((sum, f) => sum + estimateTokens(f.content), 0)
|
|
1701
|
+
};
|
|
1702
|
+
logger.info("Repository scan complete", {
|
|
1703
|
+
filesIncluded: files.length,
|
|
1704
|
+
filesSkipped: skipped.length,
|
|
1705
|
+
totalSize,
|
|
1706
|
+
tokenEstimate: stats.tokenEstimate,
|
|
1707
|
+
warnings: warnings.length
|
|
1708
|
+
});
|
|
1709
|
+
return {
|
|
1710
|
+
files,
|
|
1711
|
+
skipped,
|
|
1712
|
+
warnings,
|
|
1713
|
+
stats,
|
|
1714
|
+
repoRoot
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
function aggregateFiles(files) {
|
|
1718
|
+
return files.map(
|
|
1719
|
+
(f) => `=== FILE: ${f.path} ===
|
|
1720
|
+
${f.content}
|
|
1721
|
+
=== END FILE: ${f.path} ===`
|
|
1722
|
+
).join("\n\n");
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
// src/tools/tps-audit.ts
|
|
1726
|
+
var tpsAuditSchemaObj = z7.object({
|
|
1727
|
+
path: z7.string().optional().describe(
|
|
1728
|
+
"Path to repo root (auto-detects current directory if not provided)"
|
|
1729
|
+
),
|
|
1730
|
+
focus_areas: z7.array(z7.string()).optional().describe("Specific areas to focus on (e.g., 'performance', 'security')"),
|
|
1731
|
+
max_files: z7.number().max(100).optional().describe("Maximum files to analyze (default: 50, max: 100)"),
|
|
1732
|
+
file_types: z7.array(z7.string()).optional().describe("File extensions to include (e.g., ['.ts', '.js'])"),
|
|
1733
|
+
include_sensitive: z7.boolean().optional().describe(
|
|
1734
|
+
"Include potentially sensitive files (default: false, use with caution)"
|
|
1735
|
+
),
|
|
1736
|
+
output_format: z7.enum(["html", "markdown", "json"]).optional().describe("Output format (default: html)")
|
|
1737
|
+
});
|
|
1738
|
+
var tpsAuditSchema = tpsAuditSchemaObj.shape;
|
|
1739
|
+
async function handleTpsAudit(client2, models, input) {
|
|
1740
|
+
const startPath = input.path || process.cwd();
|
|
1741
|
+
const outputFormat = input.output_format || "html";
|
|
1742
|
+
logger.info("Starting TPS audit", {
|
|
1743
|
+
startPath,
|
|
1744
|
+
maxFiles: input.max_files,
|
|
1745
|
+
focusAreas: input.focus_areas,
|
|
1746
|
+
outputFormat,
|
|
1747
|
+
modelCount: models.length
|
|
1748
|
+
});
|
|
1749
|
+
const scanResult = await scanRepository(startPath, {
|
|
1750
|
+
maxFiles: input.max_files,
|
|
1751
|
+
fileTypes: input.file_types,
|
|
1752
|
+
skipSensitive: !input.include_sensitive,
|
|
1753
|
+
detectSecrets: !input.include_sensitive
|
|
1754
|
+
});
|
|
1755
|
+
if (scanResult.files.length === 0) {
|
|
1756
|
+
logger.warn("No files found to analyze", {
|
|
1757
|
+
totalFilesFound: scanResult.stats.totalFilesFound,
|
|
1758
|
+
skipped: scanResult.skipped.length
|
|
1759
|
+
});
|
|
1760
|
+
return {
|
|
1761
|
+
results: [
|
|
1762
|
+
{
|
|
1763
|
+
model: "system",
|
|
1764
|
+
review: "No files found to analyze. Check that the repository contains supported file types and that files are not excluded by .gitignore or security filters."
|
|
1765
|
+
}
|
|
1766
|
+
],
|
|
1767
|
+
models: ["system"],
|
|
1768
|
+
scanResult,
|
|
1769
|
+
analysis: null,
|
|
1770
|
+
outputFormat
|
|
1771
|
+
};
|
|
1772
|
+
}
|
|
1773
|
+
if (scanResult.warnings.length > 0) {
|
|
1774
|
+
logger.warn("Security warnings during scan", {
|
|
1775
|
+
warnings: scanResult.warnings
|
|
1776
|
+
});
|
|
1777
|
+
}
|
|
1778
|
+
const aggregatedContent = aggregateFiles(scanResult.files);
|
|
1779
|
+
logger.info("Repository scanned", {
|
|
1780
|
+
filesIncluded: scanResult.files.length,
|
|
1781
|
+
totalSize: scanResult.stats.totalSize,
|
|
1782
|
+
tokenEstimate: scanResult.stats.tokenEstimate
|
|
1783
|
+
});
|
|
1784
|
+
if (scanResult.stats.tokenEstimate > 1e5) {
|
|
1785
|
+
logger.warn("Large token count", {
|
|
1786
|
+
estimate: scanResult.stats.tokenEstimate
|
|
1787
|
+
});
|
|
1788
|
+
}
|
|
1789
|
+
const results = await client2.tpsAudit(aggregatedContent, models, {
|
|
1790
|
+
focusAreas: input.focus_areas,
|
|
1791
|
+
repoName: scanResult.repoRoot.split("/").pop()
|
|
1792
|
+
});
|
|
1793
|
+
let analysis = null;
|
|
1794
|
+
for (const result of results) {
|
|
1795
|
+
if (!result.error && result.review) {
|
|
1796
|
+
const { parseTpsAnalysis } = await import("./tps-audit-GNK4VIKA.js");
|
|
1797
|
+
analysis = parseTpsAnalysis(result.review);
|
|
1798
|
+
if (analysis) break;
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
return {
|
|
1802
|
+
results,
|
|
1803
|
+
models,
|
|
1804
|
+
scanResult,
|
|
1805
|
+
analysis,
|
|
1806
|
+
outputFormat
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1809
|
+
function formatTpsAuditResults(auditResult) {
|
|
1810
|
+
const { results, scanResult, analysis, outputFormat } = auditResult;
|
|
1811
|
+
switch (outputFormat) {
|
|
1812
|
+
case "html": {
|
|
1813
|
+
const templatePath = join4(getTemplatesDir(), "tps-report.html");
|
|
1814
|
+
return formatResultsAsHtml(results, templatePath, {
|
|
1815
|
+
analysis,
|
|
1816
|
+
repoName: scanResult.repoRoot.split("/").pop()
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
case "json": {
|
|
1820
|
+
return JSON.stringify(
|
|
1821
|
+
{
|
|
1822
|
+
analysis,
|
|
1823
|
+
scanStats: scanResult.stats,
|
|
1824
|
+
warnings: scanResult.warnings,
|
|
1825
|
+
skipped: scanResult.skipped,
|
|
1826
|
+
modelResponses: results.map((r) => ({
|
|
1827
|
+
model: r.model,
|
|
1828
|
+
hasError: !!r.error,
|
|
1829
|
+
content: r.error || r.review
|
|
1830
|
+
}))
|
|
1831
|
+
},
|
|
1832
|
+
null,
|
|
1833
|
+
2
|
|
1834
|
+
);
|
|
1835
|
+
}
|
|
1836
|
+
case "markdown":
|
|
1837
|
+
default: {
|
|
1838
|
+
const parts = [];
|
|
1839
|
+
parts.push("# TPS Audit Report\n");
|
|
1840
|
+
parts.push(`**Repository:** ${scanResult.repoRoot}
|
|
1841
|
+
`);
|
|
1842
|
+
parts.push(`**Files Analyzed:** ${scanResult.files.length}
|
|
1843
|
+
`);
|
|
1844
|
+
parts.push(`**Token Estimate:** ~${scanResult.stats.tokenEstimate}
|
|
1845
|
+
`);
|
|
1846
|
+
if (scanResult.warnings.length > 0) {
|
|
1847
|
+
parts.push("\n## Warnings\n");
|
|
1848
|
+
for (const w of scanResult.warnings) {
|
|
1849
|
+
parts.push(`- ${w}
|
|
1850
|
+
`);
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
if (analysis) {
|
|
1854
|
+
parts.push("\n## Scores\n");
|
|
1855
|
+
parts.push(`- **Overall:** ${analysis.scores.overall}/100
|
|
1856
|
+
`);
|
|
1857
|
+
parts.push(`- **Flow:** ${analysis.scores.flow}/100
|
|
1858
|
+
`);
|
|
1859
|
+
parts.push(`- **Waste Efficiency:** ${analysis.scores.waste}/100
|
|
1860
|
+
`);
|
|
1861
|
+
parts.push(`- **Quality:** ${analysis.scores.quality}/100
|
|
1862
|
+
`);
|
|
1863
|
+
parts.push("\n## Summary\n");
|
|
1864
|
+
parts.push("\n### Strengths\n");
|
|
1865
|
+
for (const s of analysis.summary.strengths) {
|
|
1866
|
+
parts.push(`- ${s}
|
|
1867
|
+
`);
|
|
1868
|
+
}
|
|
1869
|
+
parts.push("\n### Concerns\n");
|
|
1870
|
+
for (const c of analysis.summary.concerns) {
|
|
1871
|
+
parts.push(`- ${c}
|
|
1872
|
+
`);
|
|
1873
|
+
}
|
|
1874
|
+
parts.push("\n### Quick Wins\n");
|
|
1875
|
+
for (const q of analysis.summary.quickWins) {
|
|
1876
|
+
parts.push(`- ${q}
|
|
1877
|
+
`);
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
parts.push("\n## Model Perspectives\n");
|
|
1881
|
+
results.forEach((r) => {
|
|
1882
|
+
if (r.error) {
|
|
1883
|
+
parts.push(`
|
|
1884
|
+
### ${r.model}
|
|
1885
|
+
|
|
1886
|
+
**Error:** ${r.error}
|
|
1887
|
+
`);
|
|
1888
|
+
} else {
|
|
1889
|
+
parts.push(`
|
|
1890
|
+
### ${r.model}
|
|
1891
|
+
|
|
1892
|
+
${r.review}
|
|
1893
|
+
`);
|
|
1894
|
+
}
|
|
1895
|
+
parts.push("\n---\n");
|
|
1896
|
+
});
|
|
1897
|
+
return parts.join("");
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
|
|
664
1902
|
// src/index.ts
|
|
665
1903
|
var OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY;
|
|
666
1904
|
if (!OPENROUTER_API_KEY) {
|
|
@@ -674,6 +1912,7 @@ if (!OPENROUTER_API_KEY) {
|
|
|
674
1912
|
process.exit(1);
|
|
675
1913
|
}
|
|
676
1914
|
var client = new ReviewClient(OPENROUTER_API_KEY);
|
|
1915
|
+
var sessionStore = new InMemorySessionStore();
|
|
677
1916
|
var server = new McpServer({
|
|
678
1917
|
name: "code-council",
|
|
679
1918
|
version: "1.0.0"
|
|
@@ -708,6 +1947,42 @@ createReviewTool(server, {
|
|
|
708
1947
|
inputSchema: gitReviewSchema,
|
|
709
1948
|
handler: (input) => handleGitReview(client, CODE_REVIEW_MODELS, input)
|
|
710
1949
|
});
|
|
1950
|
+
server.registerTool(
|
|
1951
|
+
"tps_audit",
|
|
1952
|
+
{
|
|
1953
|
+
description: "Toyota Production System audit - analyze a codebase for flow, waste, bottlenecks, and quality. Scans the repository, identifies entry points, maps data flow, and provides actionable recommendations. Outputs interactive HTML report by default, or markdown/JSON.",
|
|
1954
|
+
inputSchema: tpsAuditSchema
|
|
1955
|
+
},
|
|
1956
|
+
async (input) => {
|
|
1957
|
+
try {
|
|
1958
|
+
logger.debug("Starting tps_audit", {
|
|
1959
|
+
inputKeys: Object.keys(input)
|
|
1960
|
+
});
|
|
1961
|
+
const result = await handleTpsAudit(client, TPS_AUDIT_MODELS, input);
|
|
1962
|
+
const formattedOutput = formatTpsAuditResults(result);
|
|
1963
|
+
logger.info("Completed tps_audit", {
|
|
1964
|
+
modelCount: result.models.length,
|
|
1965
|
+
filesScanned: result.scanResult.files.length,
|
|
1966
|
+
outputFormat: result.outputFormat,
|
|
1967
|
+
hasAnalysis: !!result.analysis
|
|
1968
|
+
});
|
|
1969
|
+
return {
|
|
1970
|
+
content: [
|
|
1971
|
+
{
|
|
1972
|
+
type: "text",
|
|
1973
|
+
text: formattedOutput
|
|
1974
|
+
}
|
|
1975
|
+
]
|
|
1976
|
+
};
|
|
1977
|
+
} catch (error) {
|
|
1978
|
+
logger.error(
|
|
1979
|
+
"Error in tps_audit",
|
|
1980
|
+
error instanceof Error ? error : new Error(String(error))
|
|
1981
|
+
);
|
|
1982
|
+
return formatError(error);
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
);
|
|
711
1986
|
server.registerTool(
|
|
712
1987
|
"list_review_config",
|
|
713
1988
|
{ description: "Show current model configuration" },
|
|
@@ -718,6 +1993,23 @@ server.registerTool(
|
|
|
718
1993
|
};
|
|
719
1994
|
}
|
|
720
1995
|
);
|
|
1996
|
+
createConversationTool(
|
|
1997
|
+
server,
|
|
1998
|
+
{
|
|
1999
|
+
name: "discuss_with_council",
|
|
2000
|
+
description: "Start or continue a multi-turn discussion with the AI council. First call (without session_id) starts a new discussion and returns a session_id. Subsequent calls with the session_id continue the conversation. Each model maintains its own conversation history for authentic perspectives.",
|
|
2001
|
+
inputSchema: discussCouncilSchema,
|
|
2002
|
+
handler: (input, store) => handleDiscussCouncil(client, input, store)
|
|
2003
|
+
},
|
|
2004
|
+
sessionStore
|
|
2005
|
+
);
|
|
2006
|
+
function handleShutdown(signal) {
|
|
2007
|
+
logger.info(`Received ${signal}, shutting down gracefully`);
|
|
2008
|
+
sessionStore.shutdown();
|
|
2009
|
+
process.exit(0);
|
|
2010
|
+
}
|
|
2011
|
+
process.on("SIGTERM", () => handleShutdown("SIGTERM"));
|
|
2012
|
+
process.on("SIGINT", () => handleShutdown("SIGINT"));
|
|
721
2013
|
async function main() {
|
|
722
2014
|
const transport = new StdioServerTransport();
|
|
723
2015
|
await server.connect(transport);
|
|
@@ -725,7 +2017,9 @@ async function main() {
|
|
|
725
2017
|
codeReviewModels: CODE_REVIEW_MODELS,
|
|
726
2018
|
frontendReviewModels: FRONTEND_REVIEW_MODELS,
|
|
727
2019
|
backendReviewModels: BACKEND_REVIEW_MODELS,
|
|
728
|
-
planReviewModels: PLAN_REVIEW_MODELS
|
|
2020
|
+
planReviewModels: PLAN_REVIEW_MODELS,
|
|
2021
|
+
discussionModels: DISCUSSION_MODELS,
|
|
2022
|
+
tpsAuditModels: TPS_AUDIT_MODELS
|
|
729
2023
|
});
|
|
730
2024
|
}
|
|
731
2025
|
main().catch((error) => {
|