@klitchevo/code-council 0.0.7 → 0.0.11
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 +65 -5
- package/dist/index.js +701 -30
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
[](https://github.com/klitchevo/code-council/actions)
|
|
6
6
|
[](https://codecov.io/gh/klitchevo/code-council)
|
|
7
7
|
|
|
8
|
+

|
|
9
|
+
|
|
8
10
|
**Your AI Code Review Council** - Get diverse perspectives from multiple AI models in parallel.
|
|
9
11
|
|
|
10
12
|
An MCP (Model Context Protocol) server that provides AI-powered code review using multiple models from [OpenRouter](https://openrouter.ai). Think of it as assembling a council of AI experts to review your code, each bringing their unique perspective.
|
|
@@ -15,6 +17,8 @@ An MCP (Model Context Protocol) server that provides AI-powered code review usin
|
|
|
15
17
|
- 🎨 **Frontend Review** - Specialized reviews for accessibility, performance, and UX
|
|
16
18
|
- 🔒 **Backend Review** - Security, architecture, and performance analysis
|
|
17
19
|
- 📋 **Plan Review** - Review implementation plans before writing code
|
|
20
|
+
- 📝 **Git Changes Review** - Review staged, unstaged, branch diffs, or specific commits
|
|
21
|
+
- 💬 **Council Discussions** - Multi-turn conversations with the AI council for deeper exploration
|
|
18
22
|
- ⚡ **Parallel Execution** - All models run concurrently for fast results
|
|
19
23
|
|
|
20
24
|
## Quick Start
|
|
@@ -232,6 +236,54 @@ Use review_plan to analyze this implementation plan:
|
|
|
232
236
|
[paste your plan]
|
|
233
237
|
```
|
|
234
238
|
|
|
239
|
+
### `review_git_changes`
|
|
240
|
+
|
|
241
|
+
Review git changes directly from your repository.
|
|
242
|
+
|
|
243
|
+
**Parameters:**
|
|
244
|
+
- `review_type` (optional): `staged`, `unstaged`, `diff`, or `commit` (default: `staged`)
|
|
245
|
+
- `staged` - Review staged changes (`git diff --cached`)
|
|
246
|
+
- `unstaged` - Review unstaged changes (`git diff`)
|
|
247
|
+
- `diff` - Review branch diff (`git diff main..HEAD`)
|
|
248
|
+
- `commit` - Review a specific commit (requires `commit_hash`)
|
|
249
|
+
- `commit_hash` (optional): Commit hash to review (required when `review_type` is `commit`)
|
|
250
|
+
- `context` (optional): Additional context about the changes
|
|
251
|
+
|
|
252
|
+
**Example usage in Claude:**
|
|
253
|
+
```
|
|
254
|
+
Use review_git_changes to review my staged changes
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
```
|
|
258
|
+
Use review_git_changes with review_type=commit and commit_hash=abc123 to review that commit
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### `discuss_with_council`
|
|
262
|
+
|
|
263
|
+
Have multi-turn conversations with the AI council. Start a discussion, get feedback from all models, then ask follow-up questions while maintaining context.
|
|
264
|
+
|
|
265
|
+
**Parameters:**
|
|
266
|
+
- `message` (required): Your message or question for the council
|
|
267
|
+
- `session_id` (optional): Session ID to continue an existing discussion (omit to start new)
|
|
268
|
+
- `discussion_type` (optional): `code_review`, `plan_review`, or `general` (default: `general`)
|
|
269
|
+
- `context` (optional): Additional context (code snippets, plan details, etc.)
|
|
270
|
+
|
|
271
|
+
**Example usage in Claude:**
|
|
272
|
+
```
|
|
273
|
+
Use discuss_with_council to ask: What's the best way to implement error handling in a Node.js API?
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
**Continuing a discussion:**
|
|
277
|
+
```
|
|
278
|
+
Use discuss_with_council with session_id=<id-from-previous-response> to ask: Can you elaborate on the circuit breaker pattern you mentioned?
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
**Key features:**
|
|
282
|
+
- Each model maintains its own conversation history for authentic diverse perspectives
|
|
283
|
+
- Sessions persist for 30 minutes of inactivity
|
|
284
|
+
- Rate limited to 10 requests per minute per session
|
|
285
|
+
- Context windowing keeps conversations efficient
|
|
286
|
+
|
|
235
287
|
### `list_review_config`
|
|
236
288
|
|
|
237
289
|
Show which AI models are currently configured for each review type.
|
|
@@ -247,8 +299,11 @@ You can customize which AI models are used for reviews by setting environment va
|
|
|
247
299
|
- `FRONTEND_REVIEW_MODELS` - Models for frontend reviews
|
|
248
300
|
- `BACKEND_REVIEW_MODELS` - Models for backend reviews
|
|
249
301
|
- `PLAN_REVIEW_MODELS` - Models for plan reviews
|
|
302
|
+
- `DISCUSSION_MODELS` - Models for council discussions
|
|
303
|
+
- `TEMPERATURE` - Control response randomness (0.0-2.0, default: 0.3)
|
|
304
|
+
- `MAX_TOKENS` - Maximum response tokens (default: 16384)
|
|
250
305
|
|
|
251
|
-
**Format:**
|
|
306
|
+
**Format:** Model arrays use JSON array format
|
|
252
307
|
|
|
253
308
|
**Example:**
|
|
254
309
|
```json
|
|
@@ -261,7 +316,9 @@ You can customize which AI models are used for reviews by setting environment va
|
|
|
261
316
|
"OPENROUTER_API_KEY": "your-api-key",
|
|
262
317
|
"CODE_REVIEW_MODELS": ["anthropic/claude-sonnet-4.5", "openai/gpt-4o", "google/gemini-2.0-flash-exp"],
|
|
263
318
|
"FRONTEND_REVIEW_MODELS": ["anthropic/claude-sonnet-4.5"],
|
|
264
|
-
"BACKEND_REVIEW_MODELS": ["openai/gpt-4o", "anthropic/claude-sonnet-4.5"]
|
|
319
|
+
"BACKEND_REVIEW_MODELS": ["openai/gpt-4o", "anthropic/claude-sonnet-4.5"],
|
|
320
|
+
"TEMPERATURE": "0.5",
|
|
321
|
+
"MAX_TOKENS": "32000"
|
|
265
322
|
}
|
|
266
323
|
}
|
|
267
324
|
}
|
|
@@ -270,8 +327,10 @@ You can customize which AI models are used for reviews by setting environment va
|
|
|
270
327
|
|
|
271
328
|
**Default Models:**
|
|
272
329
|
If you don't specify models, the server uses these defaults:
|
|
273
|
-
- `minimax/minimax-m2.1`
|
|
274
|
-
- `
|
|
330
|
+
- `minimax/minimax-m2.1` - Fast, cost-effective reasoning
|
|
331
|
+
- `z-ai/glm-4.7` - Strong multilingual capabilities
|
|
332
|
+
- `moonshotai/kimi-k2-thinking` - Advanced reasoning with thinking
|
|
333
|
+
- `deepseek/deepseek-v3.2` - State-of-the-art open model
|
|
275
334
|
|
|
276
335
|
**Finding Models:**
|
|
277
336
|
Browse all available models at [OpenRouter Models](https://openrouter.ai/models). Popular choices include:
|
|
@@ -330,9 +389,10 @@ npm run dev
|
|
|
330
389
|
- Each review runs across multiple models simultaneously
|
|
331
390
|
- Costs vary by model - check [OpenRouter pricing](https://openrouter.ai/models)
|
|
332
391
|
- You can reduce costs by:
|
|
333
|
-
- Using fewer models
|
|
392
|
+
- Using fewer models in your configuration
|
|
334
393
|
- Choosing cheaper models
|
|
335
394
|
- Using specific `review_type` options instead of `full` reviews
|
|
395
|
+
- Lowering `MAX_TOKENS` (default: 16384) for shorter responses
|
|
336
396
|
|
|
337
397
|
## Troubleshooting
|
|
338
398
|
|
package/dist/index.js
CHANGED
|
@@ -14,8 +14,25 @@ var LLM_CONFIG = {
|
|
|
14
14
|
var DEFAULT_MODELS = [
|
|
15
15
|
"minimax/minimax-m2.1",
|
|
16
16
|
"z-ai/glm-4.7",
|
|
17
|
-
"
|
|
17
|
+
"moonshotai/kimi-k2-thinking",
|
|
18
|
+
"deepseek/deepseek-v3.2"
|
|
18
19
|
];
|
|
20
|
+
var SESSION_LIMITS = {
|
|
21
|
+
/** Maximum number of concurrent sessions */
|
|
22
|
+
MAX_SESSIONS: 100,
|
|
23
|
+
/** Maximum messages per model in a session (context windowing) */
|
|
24
|
+
MAX_MESSAGES_PER_MODEL: 50,
|
|
25
|
+
/** Maximum message length in bytes (10KB) */
|
|
26
|
+
MAX_MESSAGE_LENGTH: 10 * 1024,
|
|
27
|
+
/** Session TTL in milliseconds (30 minutes) */
|
|
28
|
+
TTL_MS: 30 * 60 * 1e3,
|
|
29
|
+
/** Cleanup interval in milliseconds (5 minutes) */
|
|
30
|
+
CLEANUP_INTERVAL_MS: 5 * 60 * 1e3,
|
|
31
|
+
/** Rate limit: max requests per session per minute */
|
|
32
|
+
RATE_LIMIT_PER_MINUTE: 10,
|
|
33
|
+
/** Per-model timeout in milliseconds (30 seconds) */
|
|
34
|
+
MODEL_TIMEOUT_MS: 30 * 1e3
|
|
35
|
+
};
|
|
19
36
|
|
|
20
37
|
// src/config.ts
|
|
21
38
|
function parseModels(envVar, defaults) {
|
|
@@ -26,17 +43,15 @@ function parseModels(envVar, defaults) {
|
|
|
26
43
|
const filtered = envVar.filter((m) => m && m.trim().length > 0);
|
|
27
44
|
return filtered.length > 0 ? filtered : defaults;
|
|
28
45
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return filtered.length > 0 ? filtered : defaults;
|
|
37
|
-
}
|
|
38
|
-
} catch {
|
|
46
|
+
try {
|
|
47
|
+
const parsed = JSON.parse(envVar);
|
|
48
|
+
if (Array.isArray(parsed)) {
|
|
49
|
+
const filtered = parsed.filter(
|
|
50
|
+
(m) => typeof m === "string" && m.trim().length > 0
|
|
51
|
+
);
|
|
52
|
+
return filtered.length > 0 ? filtered : defaults;
|
|
39
53
|
}
|
|
54
|
+
} catch {
|
|
40
55
|
}
|
|
41
56
|
throw new Error(
|
|
42
57
|
`Model configuration must be an array of strings, got: ${typeof envVar}. Example: ["anthropic/claude-sonnet-4.5", "openai/gpt-4o"]`
|
|
@@ -58,6 +73,10 @@ var PLAN_REVIEW_MODELS = parseModels(
|
|
|
58
73
|
process.env.PLAN_REVIEW_MODELS,
|
|
59
74
|
DEFAULT_MODELS
|
|
60
75
|
);
|
|
76
|
+
var DISCUSSION_MODELS = parseModels(
|
|
77
|
+
process.env.DISCUSSION_MODELS,
|
|
78
|
+
DEFAULT_MODELS
|
|
79
|
+
);
|
|
61
80
|
|
|
62
81
|
// src/logger.ts
|
|
63
82
|
var Logger = class {
|
|
@@ -138,6 +157,16 @@ var OpenRouterError = class extends AppError {
|
|
|
138
157
|
this.retryable = retryable;
|
|
139
158
|
}
|
|
140
159
|
};
|
|
160
|
+
var ValidationError = class extends AppError {
|
|
161
|
+
constructor(message, field) {
|
|
162
|
+
super(
|
|
163
|
+
message,
|
|
164
|
+
"VALIDATION_ERROR",
|
|
165
|
+
`Invalid input for ${field}: ${message}`
|
|
166
|
+
);
|
|
167
|
+
this.field = field;
|
|
168
|
+
}
|
|
169
|
+
};
|
|
141
170
|
function formatErrorMessage(error) {
|
|
142
171
|
if (error instanceof AppError) {
|
|
143
172
|
return error.userMessage || error.message;
|
|
@@ -401,8 +430,557 @@ var ReviewClient = class {
|
|
|
401
430
|
(model) => this.chat(model, SYSTEM_PROMPT4, userMessage)
|
|
402
431
|
);
|
|
403
432
|
}
|
|
433
|
+
/**
|
|
434
|
+
* Send a multi-turn chat request with full message history
|
|
435
|
+
* @param model - Model identifier
|
|
436
|
+
* @param messages - Full conversation history
|
|
437
|
+
* @param timeoutMs - Optional timeout in milliseconds
|
|
438
|
+
* @returns The model's response content
|
|
439
|
+
* @throws {OpenRouterError} If the API call fails or times out
|
|
440
|
+
*/
|
|
441
|
+
async chatMultiTurn(model, messages, timeoutMs) {
|
|
442
|
+
const timeout = timeoutMs ?? SESSION_LIMITS.MODEL_TIMEOUT_MS;
|
|
443
|
+
try {
|
|
444
|
+
logger.debug("Sending multi-turn chat request", {
|
|
445
|
+
model,
|
|
446
|
+
messageCount: messages.length
|
|
447
|
+
});
|
|
448
|
+
const controller = new AbortController();
|
|
449
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
450
|
+
try {
|
|
451
|
+
const response = await this.client.chat.send({
|
|
452
|
+
model,
|
|
453
|
+
messages: messages.map((m) => ({
|
|
454
|
+
role: m.role,
|
|
455
|
+
content: m.content
|
|
456
|
+
})),
|
|
457
|
+
temperature: LLM_CONFIG.DEFAULT_TEMPERATURE,
|
|
458
|
+
maxTokens: LLM_CONFIG.DEFAULT_MAX_TOKENS
|
|
459
|
+
});
|
|
460
|
+
clearTimeout(timeoutId);
|
|
461
|
+
const content = response.choices?.[0]?.message?.content;
|
|
462
|
+
if (typeof content === "string") {
|
|
463
|
+
logger.debug("Received multi-turn response", {
|
|
464
|
+
model,
|
|
465
|
+
length: content.length
|
|
466
|
+
});
|
|
467
|
+
return content;
|
|
468
|
+
}
|
|
469
|
+
if (Array.isArray(content)) {
|
|
470
|
+
const text = content.filter((item) => item.type === "text").map((item) => item.text).join("\n");
|
|
471
|
+
logger.debug("Received array response", {
|
|
472
|
+
model,
|
|
473
|
+
length: text.length
|
|
474
|
+
});
|
|
475
|
+
return text;
|
|
476
|
+
}
|
|
477
|
+
throw new OpenRouterError("No response content from model", 500);
|
|
478
|
+
} finally {
|
|
479
|
+
clearTimeout(timeoutId);
|
|
480
|
+
}
|
|
481
|
+
} catch (error) {
|
|
482
|
+
if (error instanceof OpenRouterError) {
|
|
483
|
+
throw error;
|
|
484
|
+
}
|
|
485
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
486
|
+
logger.error("Multi-turn chat request failed", error, { model });
|
|
487
|
+
if (message.includes("abort") || message.includes("timeout") || error instanceof Error && error.name === "AbortError") {
|
|
488
|
+
throw new OpenRouterError(
|
|
489
|
+
`Request timed out after ${timeout}ms`,
|
|
490
|
+
408,
|
|
491
|
+
true
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
const isRetryable = message.includes("429") || message.includes("rate limit");
|
|
495
|
+
throw new OpenRouterError(message, void 0, isRetryable);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Conduct a council discussion with multiple models
|
|
500
|
+
* Each model uses its own conversation history from the provided function
|
|
501
|
+
* @param models - Array of model identifiers
|
|
502
|
+
* @param getMessagesForModel - Function to get messages for each model
|
|
503
|
+
* @returns Array of results from each model
|
|
504
|
+
*/
|
|
505
|
+
async discussWithCouncil(models, getMessagesForModel) {
|
|
506
|
+
return executeInParallel([...models], async (model) => {
|
|
507
|
+
const messages = getMessagesForModel(model);
|
|
508
|
+
return this.chatMultiTurn(model, messages);
|
|
509
|
+
});
|
|
510
|
+
}
|
|
404
511
|
};
|
|
405
512
|
|
|
513
|
+
// src/session/in-memory-store.ts
|
|
514
|
+
import { randomUUID } from "crypto";
|
|
515
|
+
|
|
516
|
+
// src/session/types.ts
|
|
517
|
+
var DISCUSSION_TYPES = [
|
|
518
|
+
"code_review",
|
|
519
|
+
"plan_review",
|
|
520
|
+
"general"
|
|
521
|
+
];
|
|
522
|
+
function toSessionId(id) {
|
|
523
|
+
return id;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// src/session/in-memory-store.ts
|
|
527
|
+
var InMemorySessionStore = class {
|
|
528
|
+
sessions = {};
|
|
529
|
+
rateLimits = {};
|
|
530
|
+
cleanupInterval = null;
|
|
531
|
+
ttlMs;
|
|
532
|
+
maxSessions;
|
|
533
|
+
maxMessagesPerModel;
|
|
534
|
+
rateLimitPerMinute;
|
|
535
|
+
constructor(options) {
|
|
536
|
+
this.ttlMs = options?.ttlMs ?? SESSION_LIMITS.TTL_MS;
|
|
537
|
+
this.maxSessions = options?.maxSessions ?? SESSION_LIMITS.MAX_SESSIONS;
|
|
538
|
+
this.maxMessagesPerModel = options?.maxMessagesPerModel ?? SESSION_LIMITS.MAX_MESSAGES_PER_MODEL;
|
|
539
|
+
this.rateLimitPerMinute = options?.rateLimitPerMinute ?? SESSION_LIMITS.RATE_LIMIT_PER_MINUTE;
|
|
540
|
+
const cleanupIntervalMs = options?.cleanupIntervalMs ?? SESSION_LIMITS.CLEANUP_INTERVAL_MS;
|
|
541
|
+
this.startCleanupTimer(cleanupIntervalMs);
|
|
542
|
+
}
|
|
543
|
+
createSession(options) {
|
|
544
|
+
const sessionCount = Object.keys(this.sessions).length;
|
|
545
|
+
if (sessionCount >= this.maxSessions) {
|
|
546
|
+
this.evictOldestSession();
|
|
547
|
+
}
|
|
548
|
+
const sessionId = toSessionId(randomUUID());
|
|
549
|
+
const now = Date.now();
|
|
550
|
+
const modelConversations = {};
|
|
551
|
+
for (const model of options.models) {
|
|
552
|
+
modelConversations[model] = {
|
|
553
|
+
model,
|
|
554
|
+
messages: [
|
|
555
|
+
{ role: "system", content: options.systemPrompt, timestamp: now },
|
|
556
|
+
{
|
|
557
|
+
role: "user",
|
|
558
|
+
content: options.initialUserMessage,
|
|
559
|
+
timestamp: now
|
|
560
|
+
}
|
|
561
|
+
],
|
|
562
|
+
lastActive: now
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
const session = {
|
|
566
|
+
id: sessionId,
|
|
567
|
+
topic: options.topic,
|
|
568
|
+
discussionType: options.discussionType,
|
|
569
|
+
modelConversations,
|
|
570
|
+
createdAt: now,
|
|
571
|
+
lastActiveAt: now,
|
|
572
|
+
models: [...options.models]
|
|
573
|
+
};
|
|
574
|
+
this.sessions[sessionId] = session;
|
|
575
|
+
this.rateLimits[sessionId] = {
|
|
576
|
+
sessionId,
|
|
577
|
+
requestCount: 1,
|
|
578
|
+
// Count the initial creation
|
|
579
|
+
windowStart: now
|
|
580
|
+
};
|
|
581
|
+
logger.info("Created discussion session", {
|
|
582
|
+
sessionId,
|
|
583
|
+
topic: options.topic,
|
|
584
|
+
discussionType: options.discussionType,
|
|
585
|
+
modelCount: options.models.length
|
|
586
|
+
});
|
|
587
|
+
return session;
|
|
588
|
+
}
|
|
589
|
+
getSession(id) {
|
|
590
|
+
const session = this.sessions[id];
|
|
591
|
+
if (session) {
|
|
592
|
+
session.lastActiveAt = Date.now();
|
|
593
|
+
}
|
|
594
|
+
return session;
|
|
595
|
+
}
|
|
596
|
+
addUserMessage(id, message) {
|
|
597
|
+
const session = this.sessions[id];
|
|
598
|
+
if (!session) {
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
const now = Date.now();
|
|
602
|
+
session.lastActiveAt = now;
|
|
603
|
+
for (const model of session.models) {
|
|
604
|
+
const conversation = session.modelConversations[model];
|
|
605
|
+
if (conversation) {
|
|
606
|
+
if (conversation.messages.length >= this.maxMessagesPerModel) {
|
|
607
|
+
const systemMsg = conversation.messages[0];
|
|
608
|
+
if (systemMsg) {
|
|
609
|
+
conversation.messages = [
|
|
610
|
+
systemMsg,
|
|
611
|
+
...conversation.messages.slice(-(this.maxMessagesPerModel - 2))
|
|
612
|
+
];
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
conversation.messages.push({
|
|
616
|
+
role: "user",
|
|
617
|
+
content: message,
|
|
618
|
+
timestamp: now
|
|
619
|
+
});
|
|
620
|
+
conversation.lastActive = now;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
return true;
|
|
624
|
+
}
|
|
625
|
+
addAssistantMessage(id, model, response) {
|
|
626
|
+
const session = this.sessions[id];
|
|
627
|
+
if (!session) {
|
|
628
|
+
return false;
|
|
629
|
+
}
|
|
630
|
+
const conversation = session.modelConversations[model];
|
|
631
|
+
if (!conversation) {
|
|
632
|
+
return false;
|
|
633
|
+
}
|
|
634
|
+
const now = Date.now();
|
|
635
|
+
conversation.messages.push({
|
|
636
|
+
role: "assistant",
|
|
637
|
+
content: response,
|
|
638
|
+
timestamp: now
|
|
639
|
+
});
|
|
640
|
+
conversation.lastActive = now;
|
|
641
|
+
session.lastActiveAt = now;
|
|
642
|
+
return true;
|
|
643
|
+
}
|
|
644
|
+
getModelMessages(id, model) {
|
|
645
|
+
const session = this.sessions[id];
|
|
646
|
+
if (!session) {
|
|
647
|
+
return void 0;
|
|
648
|
+
}
|
|
649
|
+
session.lastActiveAt = Date.now();
|
|
650
|
+
return session.modelConversations[model]?.messages;
|
|
651
|
+
}
|
|
652
|
+
deleteSession(id) {
|
|
653
|
+
const existed = id in this.sessions;
|
|
654
|
+
if (existed) {
|
|
655
|
+
delete this.sessions[id];
|
|
656
|
+
delete this.rateLimits[id];
|
|
657
|
+
logger.debug("Deleted session", { sessionId: id });
|
|
658
|
+
}
|
|
659
|
+
return existed;
|
|
660
|
+
}
|
|
661
|
+
getSessionCount() {
|
|
662
|
+
return Object.keys(this.sessions).length;
|
|
663
|
+
}
|
|
664
|
+
checkRateLimit(id) {
|
|
665
|
+
const now = Date.now();
|
|
666
|
+
const windowMs = 60 * 1e3;
|
|
667
|
+
const state = this.rateLimits[id];
|
|
668
|
+
if (!state) {
|
|
669
|
+
return {
|
|
670
|
+
allowed: false,
|
|
671
|
+
remainingRequests: 0,
|
|
672
|
+
resetInMs: 0
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
if (now - state.windowStart >= windowMs) {
|
|
676
|
+
state.requestCount = 0;
|
|
677
|
+
state.windowStart = now;
|
|
678
|
+
}
|
|
679
|
+
const remaining = this.rateLimitPerMinute - state.requestCount;
|
|
680
|
+
const resetInMs = windowMs - (now - state.windowStart);
|
|
681
|
+
if (state.requestCount >= this.rateLimitPerMinute) {
|
|
682
|
+
return {
|
|
683
|
+
allowed: false,
|
|
684
|
+
remainingRequests: 0,
|
|
685
|
+
resetInMs
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
state.requestCount++;
|
|
689
|
+
return {
|
|
690
|
+
allowed: true,
|
|
691
|
+
remainingRequests: remaining - 1,
|
|
692
|
+
resetInMs
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
shutdown() {
|
|
696
|
+
if (this.cleanupInterval) {
|
|
697
|
+
clearInterval(this.cleanupInterval);
|
|
698
|
+
this.cleanupInterval = null;
|
|
699
|
+
}
|
|
700
|
+
const sessionCount = Object.keys(this.sessions).length;
|
|
701
|
+
this.sessions = {};
|
|
702
|
+
this.rateLimits = {};
|
|
703
|
+
logger.info("Session store shutdown", { clearedSessions: sessionCount });
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Start the periodic cleanup timer
|
|
707
|
+
*/
|
|
708
|
+
startCleanupTimer(intervalMs) {
|
|
709
|
+
this.cleanupInterval = setInterval(() => {
|
|
710
|
+
this.cleanupExpiredSessions();
|
|
711
|
+
}, intervalMs);
|
|
712
|
+
this.cleanupInterval.unref();
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Remove sessions that have exceeded the TTL
|
|
716
|
+
*/
|
|
717
|
+
cleanupExpiredSessions() {
|
|
718
|
+
const now = Date.now();
|
|
719
|
+
let cleaned = 0;
|
|
720
|
+
for (const sessionId of Object.keys(this.sessions)) {
|
|
721
|
+
const session = this.sessions[sessionId];
|
|
722
|
+
if (session && now - session.lastActiveAt > this.ttlMs) {
|
|
723
|
+
delete this.sessions[sessionId];
|
|
724
|
+
delete this.rateLimits[sessionId];
|
|
725
|
+
cleaned++;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
if (cleaned > 0) {
|
|
729
|
+
logger.info("Cleaned up expired sessions", { count: cleaned });
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Evict the oldest session when at capacity (LRU eviction)
|
|
734
|
+
*/
|
|
735
|
+
evictOldestSession() {
|
|
736
|
+
let oldestId = null;
|
|
737
|
+
let oldestTime = Number.POSITIVE_INFINITY;
|
|
738
|
+
for (const sessionId of Object.keys(this.sessions)) {
|
|
739
|
+
const session = this.sessions[sessionId];
|
|
740
|
+
if (session && session.lastActiveAt < oldestTime) {
|
|
741
|
+
oldestTime = session.lastActiveAt;
|
|
742
|
+
oldestId = sessionId;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
if (oldestId) {
|
|
746
|
+
delete this.sessions[oldestId];
|
|
747
|
+
delete this.rateLimits[oldestId];
|
|
748
|
+
logger.warn("Evicted oldest session due to capacity", {
|
|
749
|
+
sessionId: oldestId
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
// src/tools/conversation-factory.ts
|
|
756
|
+
function formatDiscussionResults(results, sessionId, isNewSession, topic) {
|
|
757
|
+
const header = isNewSession ? `# Council Discussion Started
|
|
758
|
+
|
|
759
|
+
**Topic:** ${topic}
|
|
760
|
+
**Session ID:** \`${sessionId}\`
|
|
761
|
+
|
|
762
|
+
_Use this session_id in subsequent calls to continue the discussion._
|
|
763
|
+
|
|
764
|
+
---
|
|
765
|
+
|
|
766
|
+
` : `# Council Discussion Continued
|
|
767
|
+
|
|
768
|
+
**Session ID:** \`${sessionId}\`
|
|
769
|
+
|
|
770
|
+
---
|
|
771
|
+
|
|
772
|
+
`;
|
|
773
|
+
const responses = results.map((r) => {
|
|
774
|
+
const modelName = r.model.split("/").pop() || r.model;
|
|
775
|
+
if (r.error) {
|
|
776
|
+
return `## ${modelName}
|
|
777
|
+
|
|
778
|
+
**Error:** ${r.error}`;
|
|
779
|
+
}
|
|
780
|
+
return `## ${modelName}
|
|
781
|
+
|
|
782
|
+
${r.review}`;
|
|
783
|
+
}).join("\n\n---\n\n");
|
|
784
|
+
const footer = `
|
|
785
|
+
|
|
786
|
+
---
|
|
787
|
+
|
|
788
|
+
**Session ID:** \`${sessionId}\` _(include in your next message to continue)_`;
|
|
789
|
+
return header + responses + footer;
|
|
790
|
+
}
|
|
791
|
+
function createConversationTool(server2, config, sessionStore2) {
|
|
792
|
+
server2.registerTool(
|
|
793
|
+
config.name,
|
|
794
|
+
{
|
|
795
|
+
description: config.description,
|
|
796
|
+
inputSchema: config.inputSchema
|
|
797
|
+
},
|
|
798
|
+
async (input) => {
|
|
799
|
+
try {
|
|
800
|
+
logger.debug(`Starting ${config.name}`, {
|
|
801
|
+
inputKeys: Object.keys(input)
|
|
802
|
+
});
|
|
803
|
+
const { results, models, sessionId, isNewSession, topic } = await config.handler(input, sessionStore2);
|
|
804
|
+
logger.info(`Completed ${config.name}`, {
|
|
805
|
+
sessionId,
|
|
806
|
+
isNewSession,
|
|
807
|
+
modelCount: models.length,
|
|
808
|
+
successCount: results.filter((r) => !r.error).length,
|
|
809
|
+
errorCount: results.filter((r) => r.error).length
|
|
810
|
+
});
|
|
811
|
+
return {
|
|
812
|
+
content: [
|
|
813
|
+
{
|
|
814
|
+
type: "text",
|
|
815
|
+
text: formatDiscussionResults(
|
|
816
|
+
results,
|
|
817
|
+
sessionId,
|
|
818
|
+
isNewSession,
|
|
819
|
+
topic
|
|
820
|
+
)
|
|
821
|
+
}
|
|
822
|
+
]
|
|
823
|
+
};
|
|
824
|
+
} catch (error) {
|
|
825
|
+
logger.error(
|
|
826
|
+
`Error in ${config.name}`,
|
|
827
|
+
error instanceof Error ? error : new Error(String(error))
|
|
828
|
+
);
|
|
829
|
+
return formatError(error);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// src/tools/discuss-council.ts
|
|
836
|
+
import { z } from "zod";
|
|
837
|
+
|
|
838
|
+
// src/prompts/discussion.ts
|
|
839
|
+
var DISCUSSION_SYSTEM_PROMPTS = {
|
|
840
|
+
code_review: `You are a senior software engineer participating in a code review council discussion.
|
|
841
|
+
|
|
842
|
+
Your role:
|
|
843
|
+
- Provide thoughtful, constructive feedback on code and technical decisions
|
|
844
|
+
- Build on your previous responses in this conversation
|
|
845
|
+
- Consider alternative approaches and trade-offs
|
|
846
|
+
- Be specific with examples and suggestions
|
|
847
|
+
- Respectfully challenge ideas while remaining collaborative
|
|
848
|
+
|
|
849
|
+
Focus on code quality, maintainability, performance, security, and best practices.
|
|
850
|
+
Keep responses focused and actionable.`,
|
|
851
|
+
plan_review: `You are a senior software architect participating in a planning council discussion.
|
|
852
|
+
|
|
853
|
+
Your role:
|
|
854
|
+
- Evaluate implementation plans, architecture decisions, and technical strategies
|
|
855
|
+
- Build on your previous responses in this conversation
|
|
856
|
+
- Consider feasibility, risks, scalability, and maintainability
|
|
857
|
+
- Suggest alternatives and improvements
|
|
858
|
+
- Think about edge cases and potential issues
|
|
859
|
+
|
|
860
|
+
Focus on practical implementation concerns and long-term implications.
|
|
861
|
+
Keep responses focused and actionable.`,
|
|
862
|
+
general: `You are a knowledgeable advisor participating in a council discussion.
|
|
863
|
+
|
|
864
|
+
Your role:
|
|
865
|
+
- Provide thoughtful, well-reasoned perspectives on the topic
|
|
866
|
+
- Build on your previous responses in this conversation
|
|
867
|
+
- Consider multiple viewpoints and trade-offs
|
|
868
|
+
- Support your points with examples and reasoning
|
|
869
|
+
- Be open to exploring different approaches
|
|
870
|
+
|
|
871
|
+
Keep responses focused and constructive.`
|
|
872
|
+
};
|
|
873
|
+
function getSystemPrompt(discussionType) {
|
|
874
|
+
return DISCUSSION_SYSTEM_PROMPTS[discussionType];
|
|
875
|
+
}
|
|
876
|
+
function buildInitialMessage(message, discussionType, context) {
|
|
877
|
+
const typeLabel = {
|
|
878
|
+
code_review: "Code Review Discussion",
|
|
879
|
+
plan_review: "Plan Review Discussion",
|
|
880
|
+
general: "Discussion"
|
|
881
|
+
};
|
|
882
|
+
let content = `**${typeLabel[discussionType]}**
|
|
883
|
+
|
|
884
|
+
${message}`;
|
|
885
|
+
if (context) {
|
|
886
|
+
content += `
|
|
887
|
+
|
|
888
|
+
**Additional Context:**
|
|
889
|
+
${context}`;
|
|
890
|
+
}
|
|
891
|
+
return content;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// src/tools/discuss-council.ts
|
|
895
|
+
var discussCouncilSchema = {
|
|
896
|
+
message: z.string().min(1, "Message cannot be empty").max(
|
|
897
|
+
SESSION_LIMITS.MAX_MESSAGE_LENGTH,
|
|
898
|
+
`Message exceeds maximum length of ${SESSION_LIMITS.MAX_MESSAGE_LENGTH} characters`
|
|
899
|
+
).describe("Your message or question for the council"),
|
|
900
|
+
session_id: z.string().uuid("Invalid session ID format").optional().describe(
|
|
901
|
+
"Session ID to continue an existing discussion. Omit to start a new discussion."
|
|
902
|
+
),
|
|
903
|
+
discussion_type: z.enum(DISCUSSION_TYPES).optional().describe(
|
|
904
|
+
"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"
|
|
905
|
+
),
|
|
906
|
+
context: z.string().max(
|
|
907
|
+
SESSION_LIMITS.MAX_MESSAGE_LENGTH,
|
|
908
|
+
`Context exceeds maximum length of ${SESSION_LIMITS.MAX_MESSAGE_LENGTH} characters`
|
|
909
|
+
).optional().describe(
|
|
910
|
+
"Additional context for new discussions (code snippets, plan details, etc.)"
|
|
911
|
+
)
|
|
912
|
+
};
|
|
913
|
+
async function handleDiscussCouncil(client2, input, sessionStore2) {
|
|
914
|
+
const { message, session_id, discussion_type, context } = input;
|
|
915
|
+
let sessionId;
|
|
916
|
+
let isNewSession = false;
|
|
917
|
+
if (session_id) {
|
|
918
|
+
sessionId = toSessionId(session_id);
|
|
919
|
+
const existingSession = sessionStore2.getSession(sessionId);
|
|
920
|
+
if (!existingSession) {
|
|
921
|
+
throw new ValidationError(
|
|
922
|
+
`Session not found: ${session_id}. It may have expired or been deleted.`,
|
|
923
|
+
"session_id"
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
const rateLimitResult = sessionStore2.checkRateLimit(sessionId);
|
|
927
|
+
if (!rateLimitResult.allowed) {
|
|
928
|
+
throw new ValidationError(
|
|
929
|
+
`Rate limit exceeded. Please wait ${Math.ceil(rateLimitResult.resetInMs / 1e3)} seconds before sending another message.`,
|
|
930
|
+
"session_id"
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
sessionStore2.addUserMessage(sessionId, message);
|
|
934
|
+
logger.info("Continuing council discussion", {
|
|
935
|
+
sessionId,
|
|
936
|
+
modelCount: existingSession.models.length
|
|
937
|
+
});
|
|
938
|
+
} else {
|
|
939
|
+
isNewSession = true;
|
|
940
|
+
const type = discussion_type || "general";
|
|
941
|
+
const systemPrompt = getSystemPrompt(type);
|
|
942
|
+
const initialMessage = buildInitialMessage(message, type, context);
|
|
943
|
+
const topic = message.length > 100 ? `${message.slice(0, 97)}...` : message;
|
|
944
|
+
const session2 = sessionStore2.createSession({
|
|
945
|
+
topic,
|
|
946
|
+
discussionType: type,
|
|
947
|
+
models: DISCUSSION_MODELS,
|
|
948
|
+
systemPrompt,
|
|
949
|
+
initialUserMessage: initialMessage
|
|
950
|
+
});
|
|
951
|
+
sessionId = session2.id;
|
|
952
|
+
logger.info("Started new council discussion", {
|
|
953
|
+
sessionId,
|
|
954
|
+
discussionType: type,
|
|
955
|
+
modelCount: DISCUSSION_MODELS.length
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
const session = sessionStore2.getSession(sessionId);
|
|
959
|
+
if (!session) {
|
|
960
|
+
throw new ValidationError("Session was unexpectedly deleted", "session_id");
|
|
961
|
+
}
|
|
962
|
+
const currentSessionId = sessionId;
|
|
963
|
+
const results = await client2.discussWithCouncil(session.models, (model) => {
|
|
964
|
+
const messages = sessionStore2.getModelMessages(currentSessionId, model);
|
|
965
|
+
if (!messages) {
|
|
966
|
+
throw new Error(`No messages found for model ${model}`);
|
|
967
|
+
}
|
|
968
|
+
return messages;
|
|
969
|
+
});
|
|
970
|
+
for (const result of results) {
|
|
971
|
+
if (!result.error && result.review) {
|
|
972
|
+
sessionStore2.addAssistantMessage(sessionId, result.model, result.review);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
return {
|
|
976
|
+
results,
|
|
977
|
+
models: session.models,
|
|
978
|
+
sessionId,
|
|
979
|
+
isNewSession,
|
|
980
|
+
topic: session.topic
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
|
|
406
984
|
// src/tools/factory.ts
|
|
407
985
|
function formatResults(results) {
|
|
408
986
|
return results.map((r) => {
|
|
@@ -485,12 +1063,12 @@ To customize models, set environment variables in your MCP config:
|
|
|
485
1063
|
}
|
|
486
1064
|
|
|
487
1065
|
// src/tools/review-backend.ts
|
|
488
|
-
import { z } from "zod";
|
|
1066
|
+
import { z as z2 } from "zod";
|
|
489
1067
|
var backendReviewSchema = {
|
|
490
|
-
code:
|
|
491
|
-
language:
|
|
492
|
-
review_type:
|
|
493
|
-
context:
|
|
1068
|
+
code: z2.string().describe("The backend code to review"),
|
|
1069
|
+
language: z2.string().optional().describe("Programming language/framework (e.g., node, python, go, rust)"),
|
|
1070
|
+
review_type: z2.enum(["security", "performance", "architecture", "full"]).optional().describe("Type of review to perform (default: full)"),
|
|
1071
|
+
context: z2.string().optional().describe("Additional context")
|
|
494
1072
|
};
|
|
495
1073
|
async function handleBackendReview(client2, input) {
|
|
496
1074
|
const { code, language, review_type, context } = input;
|
|
@@ -513,11 +1091,11 @@ async function handleBackendReview(client2, input) {
|
|
|
513
1091
|
}
|
|
514
1092
|
|
|
515
1093
|
// src/tools/review-code.ts
|
|
516
|
-
import { z as
|
|
1094
|
+
import { z as z3 } from "zod";
|
|
517
1095
|
var codeReviewSchema = {
|
|
518
|
-
code:
|
|
519
|
-
language:
|
|
520
|
-
context:
|
|
1096
|
+
code: z3.string().describe("The code to review"),
|
|
1097
|
+
language: z3.string().optional().describe("Programming language of the code"),
|
|
1098
|
+
context: z3.string().optional().describe("Additional context about the code")
|
|
521
1099
|
};
|
|
522
1100
|
async function handleCodeReview(client2, input) {
|
|
523
1101
|
const { code, language, context } = input;
|
|
@@ -541,12 +1119,12 @@ ${context}` : ""}` : context;
|
|
|
541
1119
|
}
|
|
542
1120
|
|
|
543
1121
|
// src/tools/review-frontend.ts
|
|
544
|
-
import { z as
|
|
1122
|
+
import { z as z4 } from "zod";
|
|
545
1123
|
var frontendReviewSchema = {
|
|
546
|
-
code:
|
|
547
|
-
framework:
|
|
548
|
-
review_type:
|
|
549
|
-
context:
|
|
1124
|
+
code: z4.string().describe("The frontend code to review"),
|
|
1125
|
+
framework: z4.string().optional().describe("Frontend framework (e.g., react, vue, svelte)"),
|
|
1126
|
+
review_type: z4.enum(["accessibility", "performance", "ux", "full"]).optional().describe("Type of review to perform (default: full)"),
|
|
1127
|
+
context: z4.string().optional().describe("Additional context")
|
|
550
1128
|
};
|
|
551
1129
|
async function handleFrontendReview(client2, input) {
|
|
552
1130
|
const { code, framework, review_type, context } = input;
|
|
@@ -568,12 +1146,80 @@ async function handleFrontendReview(client2, input) {
|
|
|
568
1146
|
};
|
|
569
1147
|
}
|
|
570
1148
|
|
|
1149
|
+
// src/tools/review-git.ts
|
|
1150
|
+
import { execSync } from "child_process";
|
|
1151
|
+
import { z as z5 } from "zod";
|
|
1152
|
+
var gitReviewSchemaObj = z5.object({
|
|
1153
|
+
review_type: z5.enum(["staged", "unstaged", "diff", "commit"]).optional().describe(
|
|
1154
|
+
"Type of changes to review: 'staged' (git diff --cached), 'unstaged' (git diff), 'diff' (git diff main..HEAD), 'commit' (specific commit). Default: staged"
|
|
1155
|
+
),
|
|
1156
|
+
commit_hash: z5.string().optional().describe("Commit hash to review (only used when review_type is 'commit')"),
|
|
1157
|
+
context: z5.string().optional().describe("Additional context about the changes")
|
|
1158
|
+
});
|
|
1159
|
+
var gitReviewSchema = gitReviewSchemaObj.shape;
|
|
1160
|
+
function getGitDiff(reviewType = "staged", commitHash) {
|
|
1161
|
+
try {
|
|
1162
|
+
let command;
|
|
1163
|
+
switch (reviewType) {
|
|
1164
|
+
case "staged":
|
|
1165
|
+
command = "git diff --cached";
|
|
1166
|
+
break;
|
|
1167
|
+
case "unstaged":
|
|
1168
|
+
command = "git diff";
|
|
1169
|
+
break;
|
|
1170
|
+
case "diff":
|
|
1171
|
+
command = "git diff main..HEAD";
|
|
1172
|
+
break;
|
|
1173
|
+
case "commit":
|
|
1174
|
+
if (!commitHash) {
|
|
1175
|
+
throw new Error(
|
|
1176
|
+
"commit_hash is required when review_type is 'commit'"
|
|
1177
|
+
);
|
|
1178
|
+
}
|
|
1179
|
+
command = `git show ${commitHash}`;
|
|
1180
|
+
break;
|
|
1181
|
+
default:
|
|
1182
|
+
command = "git diff --cached";
|
|
1183
|
+
}
|
|
1184
|
+
logger.debug("Executing git command", { command });
|
|
1185
|
+
const diff = execSync(command, {
|
|
1186
|
+
encoding: "utf-8",
|
|
1187
|
+
maxBuffer: 10 * 1024 * 1024
|
|
1188
|
+
// 10MB
|
|
1189
|
+
});
|
|
1190
|
+
if (!diff || diff.trim().length === 0) {
|
|
1191
|
+
throw new Error(`No changes found for review type: ${reviewType}`);
|
|
1192
|
+
}
|
|
1193
|
+
return diff;
|
|
1194
|
+
} catch (error) {
|
|
1195
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1196
|
+
logger.error("Failed to get git diff", error);
|
|
1197
|
+
throw new Error(`Git command failed: ${message}`);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
async function handleGitReview(client2, models, input) {
|
|
1201
|
+
const reviewType = input.review_type || "staged";
|
|
1202
|
+
logger.info("Running git review", {
|
|
1203
|
+
reviewType,
|
|
1204
|
+
commitHash: input.commit_hash,
|
|
1205
|
+
modelCount: models.length,
|
|
1206
|
+
models
|
|
1207
|
+
});
|
|
1208
|
+
const diff = getGitDiff(reviewType, input.commit_hash);
|
|
1209
|
+
const results = await client2.reviewCode(diff, models, input.context);
|
|
1210
|
+
return {
|
|
1211
|
+
results,
|
|
1212
|
+
models,
|
|
1213
|
+
reviewType
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
|
|
571
1217
|
// src/tools/review-plan.ts
|
|
572
|
-
import { z as
|
|
1218
|
+
import { z as z6 } from "zod";
|
|
573
1219
|
var planReviewSchema = {
|
|
574
|
-
plan:
|
|
575
|
-
review_type:
|
|
576
|
-
context:
|
|
1220
|
+
plan: z6.string().describe("The implementation plan to review"),
|
|
1221
|
+
review_type: z6.enum(["feasibility", "completeness", "risks", "timeline", "full"]).optional().describe("Type of review to perform (default: full)"),
|
|
1222
|
+
context: z6.string().optional().describe("Additional context about the project or constraints")
|
|
577
1223
|
};
|
|
578
1224
|
async function handlePlanReview(client2, input) {
|
|
579
1225
|
const { plan, review_type, context } = input;
|
|
@@ -606,6 +1252,7 @@ if (!OPENROUTER_API_KEY) {
|
|
|
606
1252
|
process.exit(1);
|
|
607
1253
|
}
|
|
608
1254
|
var client = new ReviewClient(OPENROUTER_API_KEY);
|
|
1255
|
+
var sessionStore = new InMemorySessionStore();
|
|
609
1256
|
var server = new McpServer({
|
|
610
1257
|
name: "code-council",
|
|
611
1258
|
version: "1.0.0"
|
|
@@ -634,6 +1281,12 @@ createReviewTool(server, {
|
|
|
634
1281
|
inputSchema: planReviewSchema,
|
|
635
1282
|
handler: (input) => handlePlanReview(client, input)
|
|
636
1283
|
});
|
|
1284
|
+
createReviewTool(server, {
|
|
1285
|
+
name: "review_git_changes",
|
|
1286
|
+
description: "Review git changes (staged, unstaged, diff, or specific commit) using multiple AI models in parallel",
|
|
1287
|
+
inputSchema: gitReviewSchema,
|
|
1288
|
+
handler: (input) => handleGitReview(client, CODE_REVIEW_MODELS, input)
|
|
1289
|
+
});
|
|
637
1290
|
server.registerTool(
|
|
638
1291
|
"list_review_config",
|
|
639
1292
|
{ description: "Show current model configuration" },
|
|
@@ -644,6 +1297,23 @@ server.registerTool(
|
|
|
644
1297
|
};
|
|
645
1298
|
}
|
|
646
1299
|
);
|
|
1300
|
+
createConversationTool(
|
|
1301
|
+
server,
|
|
1302
|
+
{
|
|
1303
|
+
name: "discuss_with_council",
|
|
1304
|
+
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.",
|
|
1305
|
+
inputSchema: discussCouncilSchema,
|
|
1306
|
+
handler: (input, store) => handleDiscussCouncil(client, input, store)
|
|
1307
|
+
},
|
|
1308
|
+
sessionStore
|
|
1309
|
+
);
|
|
1310
|
+
function handleShutdown(signal) {
|
|
1311
|
+
logger.info(`Received ${signal}, shutting down gracefully`);
|
|
1312
|
+
sessionStore.shutdown();
|
|
1313
|
+
process.exit(0);
|
|
1314
|
+
}
|
|
1315
|
+
process.on("SIGTERM", () => handleShutdown("SIGTERM"));
|
|
1316
|
+
process.on("SIGINT", () => handleShutdown("SIGINT"));
|
|
647
1317
|
async function main() {
|
|
648
1318
|
const transport = new StdioServerTransport();
|
|
649
1319
|
await server.connect(transport);
|
|
@@ -651,7 +1321,8 @@ async function main() {
|
|
|
651
1321
|
codeReviewModels: CODE_REVIEW_MODELS,
|
|
652
1322
|
frontendReviewModels: FRONTEND_REVIEW_MODELS,
|
|
653
1323
|
backendReviewModels: BACKEND_REVIEW_MODELS,
|
|
654
|
-
planReviewModels: PLAN_REVIEW_MODELS
|
|
1324
|
+
planReviewModels: PLAN_REVIEW_MODELS,
|
|
1325
|
+
discussionModels: DISCUSSION_MODELS
|
|
655
1326
|
});
|
|
656
1327
|
}
|
|
657
1328
|
main().catch((error) => {
|
package/package.json
CHANGED