@klitchevo/code-council 0.0.8 → 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.
Files changed (3) hide show
  1. package/README.md +32 -3
  2. package/dist/index.js +632 -35
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -18,6 +18,7 @@ An MCP (Model Context Protocol) server that provides AI-powered code review usin
18
18
  - 🔒 **Backend Review** - Security, architecture, and performance analysis
19
19
  - 📋 **Plan Review** - Review implementation plans before writing code
20
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
21
22
  - ⚡ **Parallel Execution** - All models run concurrently for fast results
22
23
 
23
24
  ## Quick Start
@@ -257,6 +258,32 @@ Use review_git_changes to review my staged changes
257
258
  Use review_git_changes with review_type=commit and commit_hash=abc123 to review that commit
258
259
  ```
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
+
260
287
  ### `list_review_config`
261
288
 
262
289
  Show which AI models are currently configured for each review type.
@@ -272,6 +299,7 @@ You can customize which AI models are used for reviews by setting environment va
272
299
  - `FRONTEND_REVIEW_MODELS` - Models for frontend reviews
273
300
  - `BACKEND_REVIEW_MODELS` - Models for backend reviews
274
301
  - `PLAN_REVIEW_MODELS` - Models for plan reviews
302
+ - `DISCUSSION_MODELS` - Models for council discussions
275
303
  - `TEMPERATURE` - Control response randomness (0.0-2.0, default: 0.3)
276
304
  - `MAX_TOKENS` - Maximum response tokens (default: 16384)
277
305
 
@@ -299,9 +327,10 @@ You can customize which AI models are used for reviews by setting environment va
299
327
 
300
328
  **Default Models:**
301
329
  If you don't specify models, the server uses these defaults:
302
- - `minimax/minimax-m2.1`
303
- - `z-ai/glm-4.7`
304
- - `x-ai/grok-code-fast-1`
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
305
334
 
306
335
  **Finding Models:**
307
336
  Browse all available models at [OpenRouter Models](https://openrouter.ai/models). Popular choices include:
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
- "x-ai/grok-code-fast-1"
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
- if (typeof envVar === "string") {
30
- try {
31
- const parsed = JSON.parse(envVar);
32
- if (Array.isArray(parsed)) {
33
- const filtered = parsed.filter(
34
- (m) => typeof m === "string" && m.trim().length > 0
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
+ }
511
+ };
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
+ }
404
753
  };
405
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: z.string().describe("The backend code to review"),
491
- language: z.string().optional().describe("Programming language/framework (e.g., node, python, go, rust)"),
492
- review_type: z.enum(["security", "performance", "architecture", "full"]).optional().describe("Type of review to perform (default: full)"),
493
- context: z.string().optional().describe("Additional 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 z2 } from "zod";
1094
+ import { z as z3 } from "zod";
517
1095
  var codeReviewSchema = {
518
- code: z2.string().describe("The code to review"),
519
- language: z2.string().optional().describe("Programming language of the code"),
520
- context: z2.string().optional().describe("Additional context about the code")
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 z3 } from "zod";
1122
+ import { z as z4 } from "zod";
545
1123
  var frontendReviewSchema = {
546
- code: z3.string().describe("The frontend code to review"),
547
- framework: z3.string().optional().describe("Frontend framework (e.g., react, vue, svelte)"),
548
- review_type: z3.enum(["accessibility", "performance", "ux", "full"]).optional().describe("Type of review to perform (default: full)"),
549
- context: z3.string().optional().describe("Additional 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;
@@ -570,13 +1148,13 @@ async function handleFrontendReview(client2, input) {
570
1148
 
571
1149
  // src/tools/review-git.ts
572
1150
  import { execSync } from "child_process";
573
- import { z as z4 } from "zod";
574
- var gitReviewSchemaObj = z4.object({
575
- review_type: z4.enum(["staged", "unstaged", "diff", "commit"]).optional().describe(
1151
+ import { z as z5 } from "zod";
1152
+ var gitReviewSchemaObj = z5.object({
1153
+ review_type: z5.enum(["staged", "unstaged", "diff", "commit"]).optional().describe(
576
1154
  "Type of changes to review: 'staged' (git diff --cached), 'unstaged' (git diff), 'diff' (git diff main..HEAD), 'commit' (specific commit). Default: staged"
577
1155
  ),
578
- commit_hash: z4.string().optional().describe("Commit hash to review (only used when review_type is 'commit')"),
579
- context: z4.string().optional().describe("Additional context about the changes")
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")
580
1158
  });
581
1159
  var gitReviewSchema = gitReviewSchemaObj.shape;
582
1160
  function getGitDiff(reviewType = "staged", commitHash) {
@@ -637,11 +1215,11 @@ async function handleGitReview(client2, models, input) {
637
1215
  }
638
1216
 
639
1217
  // src/tools/review-plan.ts
640
- import { z as z5 } from "zod";
1218
+ import { z as z6 } from "zod";
641
1219
  var planReviewSchema = {
642
- plan: z5.string().describe("The implementation plan to review"),
643
- review_type: z5.enum(["feasibility", "completeness", "risks", "timeline", "full"]).optional().describe("Type of review to perform (default: full)"),
644
- context: z5.string().optional().describe("Additional context about the project or constraints")
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")
645
1223
  };
646
1224
  async function handlePlanReview(client2, input) {
647
1225
  const { plan, review_type, context } = input;
@@ -674,6 +1252,7 @@ if (!OPENROUTER_API_KEY) {
674
1252
  process.exit(1);
675
1253
  }
676
1254
  var client = new ReviewClient(OPENROUTER_API_KEY);
1255
+ var sessionStore = new InMemorySessionStore();
677
1256
  var server = new McpServer({
678
1257
  name: "code-council",
679
1258
  version: "1.0.0"
@@ -718,6 +1297,23 @@ server.registerTool(
718
1297
  };
719
1298
  }
720
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"));
721
1317
  async function main() {
722
1318
  const transport = new StdioServerTransport();
723
1319
  await server.connect(transport);
@@ -725,7 +1321,8 @@ async function main() {
725
1321
  codeReviewModels: CODE_REVIEW_MODELS,
726
1322
  frontendReviewModels: FRONTEND_REVIEW_MODELS,
727
1323
  backendReviewModels: BACKEND_REVIEW_MODELS,
728
- planReviewModels: PLAN_REVIEW_MODELS
1324
+ planReviewModels: PLAN_REVIEW_MODELS,
1325
+ discussionModels: DISCUSSION_MODELS
729
1326
  });
730
1327
  }
731
1328
  main().catch((error) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@klitchevo/code-council",
3
- "version": "0.0.8",
3
+ "version": "0.0.11",
4
4
  "description": "Multi-model AI code review server using OpenRouter - get diverse perspectives from multiple LLMs in parallel",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",