@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/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
- "x-ai/grok-code-fast-1"
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
- 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 {
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 SYSTEM_PROMPT = `You are an expert backend developer and security specialist. Review backend code for security, performance, and architecture.`;
174
- function buildUserMessage(code, reviewType = "full", language, context) {
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 SYSTEM_PROMPT2 = `You are an expert code reviewer. Analyze the code for:
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 buildUserMessage2(code, context) {
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 SYSTEM_PROMPT3 = `You are an expert frontend developer and UX specialist. Review frontend code for best practices.`;
226
- function buildUserMessage3(code, reviewType = "full", framework, context) {
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 SYSTEM_PROMPT4 = `You are an expert software architect and project planner. Review implementation plans before code is written to catch issues early.`;
254
- function buildUserMessage4(plan, reviewType = "full", context) {
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 = buildUserMessage2(code, context);
391
+ const userMessage = buildUserMessage3(code, context);
355
392
  return executeInParallel(
356
393
  models,
357
- (model) => this.chat(model, SYSTEM_PROMPT2, userMessage)
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 = buildUserMessage3(
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, SYSTEM_PROMPT3, userMessage)
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 = buildUserMessage(
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, SYSTEM_PROMPT, userMessage)
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 = buildUserMessage4(
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, SYSTEM_PROMPT4, userMessage)
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: 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")
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 z2 } from "zod";
1162
+ import { z as z3 } from "zod";
517
1163
  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")
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 z3 } from "zod";
1190
+ import { z as z4 } from "zod";
545
1191
  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")
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 z4 } from "zod";
574
- var gitReviewSchemaObj = z4.object({
575
- review_type: z4.enum(["staged", "unstaged", "diff", "commit"]).optional().describe(
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: 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")
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 z5 } from "zod";
1286
+ import { z as z6 } from "zod";
641
1287
  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")
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) => {