#
tokens: 45268/50000 17/104 files (page 2/2)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 2 of 2. Use http://codebase.md/pv-bhat/vibe-check-mcp-server?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .env.example
├── .github
│   └── workflows
│       ├── ci.yml
│       ├── docker-image.yml
│       └── release.yml
├── .gitignore
├── AGENTS.md
├── alt-test-gemini.js
├── alt-test-openai.js
├── alt-test.js
├── Attachments
│   ├── Template.md
│   ├── VC1.png
│   ├── vc2.png
│   ├── vc3.png
│   ├── vc4.png
│   ├── VCC1.png
│   ├── VCC2.png
│   ├── vibe (1).jpeg
│   ├── vibelogo.png
│   └── vibelogov2.png
├── CHANGELOG.md
├── CITATION.cff
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Dockerfile
├── docs
│   ├── _toc.md
│   ├── advanced-integration.md
│   ├── agent-prompting.md
│   ├── AGENTS.md
│   ├── api-keys.md
│   ├── architecture.md
│   ├── case-studies.md
│   ├── changelog.md
│   ├── clients.md
│   ├── docker-automation.md
│   ├── gemini.md
│   ├── integrations
│   │   └── cpi.md
│   ├── philosophy.md
│   ├── registry-descriptions.md
│   ├── release-workflows.md
│   ├── technical-reference.md
│   └── TESTING.md
├── examples
│   └── cpi-integration.ts
├── glama.json
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── request.json
├── scripts
│   ├── docker-setup.sh
│   ├── install-vibe-check.sh
│   ├── security-check.cjs
│   └── sync-version.mjs
├── SECURITY.md
├── server.json
├── smithery.yaml
├── src
│   ├── cli
│   │   ├── clients
│   │   │   ├── claude-code.ts
│   │   │   ├── claude.ts
│   │   │   ├── cursor.ts
│   │   │   ├── shared.ts
│   │   │   ├── vscode.ts
│   │   │   └── windsurf.ts
│   │   ├── diff.ts
│   │   ├── doctor.ts
│   │   ├── env.ts
│   │   └── index.ts
│   ├── index.ts
│   ├── tools
│   │   ├── constitution.ts
│   │   ├── vibeCheck.ts
│   │   ├── vibeDistil.ts
│   │   └── vibeLearn.ts
│   └── utils
│       ├── anthropic.ts
│       ├── httpTransportWrapper.ts
│       ├── jsonRpcCompat.ts
│       ├── llm.ts
│       ├── state.ts
│       ├── storage.ts
│       └── version.ts
├── test-client.js
├── test-client.ts
├── test.js
├── test.json
├── tests
│   ├── claude-config.test.ts
│   ├── claude-merge.test.ts
│   ├── cli-doctor-node.test.ts
│   ├── cli-doctor-port.test.ts
│   ├── cli-install-dry-run.test.ts
│   ├── cli-install-vscode-dry-run.test.ts
│   ├── cli-start-flags.test.ts
│   ├── cli-version.test.ts
│   ├── constitution.test.ts
│   ├── cursor-merge.test.ts
│   ├── env-ensure.test.ts
│   ├── fixtures
│   │   ├── claude
│   │   │   ├── config.base.json
│   │   │   ├── config.with-managed-entry.json
│   │   │   └── config.with-other-servers.json
│   │   ├── cursor
│   │   │   ├── config.base.json
│   │   │   └── config.with-managed-entry.json
│   │   ├── vscode
│   │   │   ├── workspace.mcp.base.json
│   │   │   └── workspace.mcp.with-managed.json
│   │   └── windsurf
│   │       ├── config.base.json
│   │       ├── config.with-http-entry.json
│   │       └── config.with-managed-entry.json
│   ├── index-main.test.ts
│   ├── jsonrpc-compat.test.ts
│   ├── llm-anthropic.test.ts
│   ├── llm.test.ts
│   ├── server.integration.test.ts
│   ├── startup.test.ts
│   ├── state.test.ts
│   ├── storage-utils.test.ts
│   ├── vibeCheck.test.ts
│   ├── vibeLearn.test.ts
│   ├── vscode-merge.test.ts
│   └── windsurf-merge.test.ts
├── tsconfig.json
├── version.json
└── vitest.config.ts
```

# Files

--------------------------------------------------------------------------------
/tests/claude-config.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { promises as fs } from 'node:fs';
  2 | import { dirname, join } from 'node:path';
  3 | import os from 'node:os';
  4 | import { afterEach, describe, expect, it, vi } from 'vitest';
  5 | import {
  6 |   locateClaudeConfig,
  7 |   readClaudeConfig,
  8 |   writeClaudeConfigAtomic,
  9 | } from '../src/cli/clients/claude.js';
 10 | 
 11 | const ORIGINAL_ENV = { ...process.env };
 12 | 
 13 | describe('Claude config helpers', () => {
 14 |   afterEach(() => {
 15 |     vi.restoreAllMocks();
 16 |     process.env = { ...ORIGINAL_ENV };
 17 |   });
 18 | 
 19 |   it('expands custom paths with a tilde', async () => {
 20 |     const tmpHome = await fs.mkdtemp(join(os.tmpdir(), 'claude-home-'));
 21 |     vi.spyOn(os, 'homedir').mockReturnValue(tmpHome);
 22 | 
 23 |     const result = await locateClaudeConfig('~/config.json');
 24 |     expect(result).toBe(join(tmpHome, 'config.json'));
 25 |   });
 26 | 
 27 |   it('expands custom paths with a tilde and backslash', async () => {
 28 |     const tmpHome = await fs.mkdtemp(join(os.tmpdir(), 'claude-home-'));
 29 |     vi.spyOn(os, 'homedir').mockReturnValue(tmpHome);
 30 | 
 31 |     const result = await locateClaudeConfig('~\\config.json');
 32 |     expect(result).toBe(join(tmpHome, 'config.json'));
 33 |   });
 34 | 
 35 |   it('locates the default macOS path when present', async () => {
 36 |     const tmpHome = await fs.mkdtemp(join(os.tmpdir(), 'claude-home-'));
 37 |     vi.spyOn(os, 'homedir').mockReturnValue(tmpHome);
 38 |     const platformSpy = vi.spyOn(process, 'platform', 'get').mockReturnValue('darwin');
 39 | 
 40 |     const candidate = join(
 41 |       tmpHome,
 42 |       'Library',
 43 |       'Application Support',
 44 |       'Claude',
 45 |       'claude_desktop_config.json',
 46 |     );
 47 |     await fs.mkdir(dirname(candidate), { recursive: true });
 48 |     await fs.writeFile(candidate, '{}', 'utf8');
 49 | 
 50 |     const result = await locateClaudeConfig();
 51 |     expect(result).toBe(candidate);
 52 | 
 53 |     platformSpy.mockRestore();
 54 |   });
 55 | 
 56 |   it('locates the config via APPDATA on Windows', async () => {
 57 |     const tmpDir = await fs.mkdtemp(join(os.tmpdir(), 'claude-appdata-'));
 58 |     const platformSpy = vi.spyOn(process, 'platform', 'get').mockReturnValue('win32');
 59 | 
 60 |     const originalAppData = process.env.APPDATA;
 61 |     process.env.APPDATA = join(tmpDir, 'AppData');
 62 | 
 63 |     const candidate = join(process.env.APPDATA, 'Claude', 'claude_desktop_config.json');
 64 |     await fs.mkdir(dirname(candidate), { recursive: true });
 65 |     await fs.writeFile(candidate, '{}', 'utf8');
 66 | 
 67 |     const result = await locateClaudeConfig();
 68 |     expect(result).toBe(candidate);
 69 | 
 70 |     if (originalAppData === undefined) {
 71 |       delete process.env.APPDATA;
 72 |     } else {
 73 |       process.env.APPDATA = originalAppData;
 74 |     }
 75 |     platformSpy.mockRestore();
 76 |   });
 77 | 
 78 |   it('prefers XDG config directories on Linux', async () => {
 79 |     const tmpHome = await fs.mkdtemp(join(os.tmpdir(), 'claude-home-'));
 80 |     const platformSpy = vi.spyOn(process, 'platform', 'get').mockReturnValue('linux');
 81 |     vi.spyOn(os, 'homedir').mockReturnValue(tmpHome);
 82 | 
 83 |     const xdgDir = await fs.mkdtemp(join(os.tmpdir(), 'claude-xdg-'));
 84 |     process.env.XDG_CONFIG_HOME = xdgDir;
 85 | 
 86 |     const candidate = join(xdgDir, 'Claude', 'claude_desktop_config.json');
 87 |     await fs.mkdir(dirname(candidate), { recursive: true });
 88 |     await fs.writeFile(candidate, '{}', 'utf8');
 89 | 
 90 |     const result = await locateClaudeConfig();
 91 |     expect(result).toBe(candidate);
 92 | 
 93 |     platformSpy.mockRestore();
 94 |   });
 95 | 
 96 |   it('returns null when no candidates exist', async () => {
 97 |     const platformSpy = vi.spyOn(process, 'platform', 'get').mockReturnValue('linux');
 98 |     const tmpHome = await fs.mkdtemp(join(os.tmpdir(), 'claude-home-'));
 99 |     vi.spyOn(os, 'homedir').mockReturnValue(tmpHome);
100 |     process.env.XDG_CONFIG_HOME = '';
101 | 
102 |     const result = await locateClaudeConfig();
103 |     expect(result).toBeNull();
104 | 
105 |     platformSpy.mockRestore();
106 |   });
107 | 
108 |   it('writes configs atomically with 0600 permissions', async () => {
109 |     const tmpDir = await fs.mkdtemp(join(os.tmpdir(), 'claude-write-'));
110 |     const target = join(tmpDir, 'config.json');
111 | 
112 |     await writeClaudeConfigAtomic(target, { hello: 'world' });
113 | 
114 |     const stat = await fs.stat(target);
115 |     expect(stat.mode & 0o777).toBe(0o600);
116 | 
117 |     const content = await fs.readFile(target, 'utf8');
118 |     expect(JSON.parse(content)).toEqual({ hello: 'world' });
119 |   });
120 | 
121 |   it('throws when config JSON is not an object', async () => {
122 |     const tmpDir = await fs.mkdtemp(join(os.tmpdir(), 'claude-read-'));
123 |     const target = join(tmpDir, 'config.json');
124 |     await fs.writeFile(target, '"string"', 'utf8');
125 | 
126 |     await expect(readClaudeConfig(target)).rejects.toThrow('Claude config must be a JSON object.');
127 |   });
128 | });
129 | 
```

--------------------------------------------------------------------------------
/scripts/docker-setup.sh:
--------------------------------------------------------------------------------

```bash
  1 | #!/bin/bash
  2 | 
  3 | echo "========================================================"
  4 | echo "Vibe Check MCP Docker Setup for Cursor IDE"
  5 | echo "========================================================"
  6 | echo ""
  7 | 
  8 | # Check for Docker installation
  9 | if ! command -v docker &> /dev/null; then
 10 |     echo "Error: Docker is not installed or not in PATH."
 11 |     echo "Please install Docker from https://docs.docker.com/get-docker/"
 12 |     exit 1
 13 | fi
 14 | 
 15 | # Check for Docker Compose installation
 16 | if ! command -v docker-compose &> /dev/null; then
 17 |     echo "Error: Docker Compose is not installed or not in PATH."
 18 |     echo "Please install Docker Compose from https://docs.docker.com/compose/install/"
 19 |     exit 1
 20 | fi
 21 | 
 22 | # Create directory for Vibe Check MCP
 23 | mkdir -p ~/vibe-check-mcp
 24 | cd ~/vibe-check-mcp
 25 | 
 26 | # Download or create necessary files
 27 | echo "Downloading required files..."
 28 | 
 29 | # Create docker-compose.yml
 30 | cat > docker-compose.yml << 'EOL'
 31 | version: '3'
 32 | 
 33 | services:
 34 |   vibe-check-mcp:
 35 |     build:
 36 |       context: .
 37 |       dockerfile: Dockerfile
 38 |     image: vibe-check-mcp:latest
 39 |     container_name: vibe-check-mcp
 40 |     restart: always
 41 |     environment:
 42 |       - GEMINI_API_KEY=${GEMINI_API_KEY}
 43 |     volumes:
 44 |       - vibe-check-data:/app/data
 45 | 
 46 | volumes:
 47 |   vibe-check-data:
 48 | EOL
 49 | 
 50 | # Create Dockerfile if it doesn't exist
 51 | cat > Dockerfile << 'EOL'
 52 | FROM node:lts-alpine
 53 | 
 54 | WORKDIR /app
 55 | 
 56 | # Clone the repository
 57 | RUN apk add --no-cache git \
 58 |     && git clone https://github.com/PV-Bhat/vibe-check-mcp-server.git .
 59 | 
 60 | # Install dependencies and build
 61 | RUN npm install && npm run build
 62 | 
 63 | # Run the MCP server
 64 | CMD ["node", "build/index.js"]
 65 | EOL
 66 | 
 67 | # Create .env file
 68 | echo "Enter your Gemini API key:"
 69 | read -p "API Key: " GEMINI_API_KEY
 70 | 
 71 | cat > .env << EOL
 72 | GEMINI_API_KEY=$GEMINI_API_KEY
 73 | EOL
 74 | 
 75 | chmod 600 .env  # Secure the API key file
 76 | 
 77 | # Create startup script
 78 | cat > start-vibe-check-docker.sh << 'EOL'
 79 | #!/bin/bash
 80 | cd ~/vibe-check-mcp
 81 | docker-compose up -d
 82 | EOL
 83 | 
 84 | chmod +x start-vibe-check-docker.sh
 85 | 
 86 | # Create a TCP wrapper script to route stdio to TCP port 3000
 87 | cat > vibe-check-tcp-wrapper.sh << 'EOL'
 88 | #!/bin/bash
 89 | # This script connects stdio to the Docker container's TCP port
 90 | exec socat STDIO TCP:localhost:3000
 91 | EOL
 92 | 
 93 | chmod +x vibe-check-tcp-wrapper.sh
 94 | 
 95 | # Detect OS for autostart configuration
 96 | OS="$(uname -s)"
 97 | case "${OS}" in
 98 |     Linux*)     OS="Linux";;
 99 |     Darwin*)    OS="Mac";;
100 |     *)          OS="Unknown";;
101 | esac
102 | 
103 | echo "Setting up auto-start for $OS..."
104 | 
105 | if [ "$OS" = "Mac" ]; then
106 |     # Set up LaunchAgent for Mac
107 |     PLIST_FILE="$HOME/Library/LaunchAgents/com.vibe-check-mcp-docker.plist"
108 |     mkdir -p "$HOME/Library/LaunchAgents"
109 |     
110 |     cat > "$PLIST_FILE" << EOL
111 | <?xml version="1.0" encoding="UTF-8"?>
112 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
113 | <plist version="1.0">
114 | <dict>
115 |     <key>Label</key>
116 |     <string>com.vibe-check-mcp-docker</string>
117 |     <key>ProgramArguments</key>
118 |     <array>
119 |         <string>$HOME/vibe-check-mcp/start-vibe-check-docker.sh</string>
120 |     </array>
121 |     <key>RunAtLoad</key>
122 |     <true/>
123 |     <key>KeepAlive</key>
124 |     <false/>
125 | </dict>
126 | </plist>
127 | EOL
128 | 
129 |     chmod 644 "$PLIST_FILE"
130 |     launchctl load "$PLIST_FILE"
131 |     
132 |     echo "Created and loaded LaunchAgent for automatic Docker startup on login."
133 |     
134 | elif [ "$OS" = "Linux" ]; then
135 |     # Set up systemd user service for Linux
136 |     SERVICE_DIR="$HOME/.config/systemd/user"
137 |     mkdir -p "$SERVICE_DIR"
138 |     
139 |     cat > "$SERVICE_DIR/vibe-check-mcp-docker.service" << EOL
140 | [Unit]
141 | Description=Vibe Check MCP Docker Container
142 | After=docker.service
143 | 
144 | [Service]
145 | ExecStart=$HOME/vibe-check-mcp/start-vibe-check-docker.sh
146 | Type=oneshot
147 | RemainAfterExit=yes
148 | 
149 | [Install]
150 | WantedBy=default.target
151 | EOL
152 | 
153 |     systemctl --user daemon-reload
154 |     systemctl --user enable vibe-check-mcp-docker.service
155 |     systemctl --user start vibe-check-mcp-docker.service
156 |     
157 |     echo "Created and started systemd user service for automatic Docker startup."
158 | fi
159 | 
160 | # Start the container
161 | echo "Starting Vibe Check MCP Docker container..."
162 | ./start-vibe-check-docker.sh
163 | 
164 | echo ""
165 | echo "Vibe Check MCP Docker setup complete!"
166 | echo ""
167 | echo "To complete the setup, configure Cursor IDE:"
168 | echo ""
169 | echo "1. Open Cursor IDE"
170 | echo "2. Go to Settings (gear icon) -> MCP"
171 | echo "3. Click \"Add New MCP Server\""
172 | echo "4. Enter the following information:"
173 | echo "   - Name: Vibe Check"
174 | echo "   - Type: Command"
175 | echo "   - Command: $HOME/vibe-check-mcp/vibe-check-tcp-wrapper.sh"
176 | echo "5. Click \"Save\" and then \"Refresh\""
177 | echo ""
178 | echo "Vibe Check MCP will now start automatically when you log in."
179 | echo ""
```

--------------------------------------------------------------------------------
/src/tools/vibeLearn.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import {
  2 |   addLearningEntry,
  3 |   getLearningCategorySummary,
  4 |   getLearningEntries,
  5 |   LearningEntry,
  6 |   LearningType
  7 | } from '../utils/storage.js';
  8 | 
  9 | // Vibe Learn tool interfaces
 10 | export interface VibeLearnInput {
 11 |   mistake: string;
 12 |   category: string;
 13 |   solution?: string;
 14 |   type?: LearningType;
 15 |   sessionId?: string;
 16 | }
 17 | 
 18 | export interface VibeLearnOutput {
 19 |   added: boolean;
 20 |   currentTally: number;
 21 |   alreadyKnown?: boolean;
 22 |   topCategories: Array<{
 23 |     category: string;
 24 |     count: number;
 25 |     recentExample: LearningEntry;
 26 |   }>;
 27 | }
 28 | 
 29 | /**
 30 |  * The vibe_learn tool records one-sentence mistakes and solutions
 31 |  * to build a pattern recognition system for future improvement
 32 |  */
 33 | export async function vibeLearnTool(input: VibeLearnInput): Promise<VibeLearnOutput> {
 34 |   try {
 35 |     // Validate input
 36 |     if (!input.mistake) {
 37 |       throw new Error('Mistake description is required');
 38 |     }
 39 |     if (!input.category) {
 40 |       throw new Error('Mistake category is required');
 41 |     }
 42 |     const entryType: LearningType = input.type ?? 'mistake';
 43 |     if (entryType !== 'preference' && !input.solution) {
 44 |       throw new Error('Solution is required for this entry type');
 45 |     }
 46 |     
 47 |     // Enforce single-sentence constraints
 48 |     const mistake = enforceOneSentence(input.mistake);
 49 |     const solution = input.solution ? enforceOneSentence(input.solution) : undefined;
 50 |     
 51 |     // Normalize category to one of our standard categories if possible
 52 |     const category = normalizeCategory(input.category);
 53 |     
 54 |     // Check for similar mistake
 55 |     const existing = getLearningEntries()[category] || [];
 56 |     const alreadyKnown = existing.some(e => isSimilar(e.mistake, mistake));
 57 | 
 58 |     // Add mistake to log if new
 59 |     let entry: LearningEntry | undefined;
 60 |     if (!alreadyKnown) {
 61 |       entry = addLearningEntry(mistake, category, solution, entryType);
 62 |     }
 63 |     
 64 |     // Get category summaries
 65 |     const categorySummary = getLearningCategorySummary();
 66 |     
 67 |     // Find current tally for this category
 68 |     const categoryData = categorySummary.find(m => m.category === category);
 69 |     const currentTally = categoryData?.count || 1;
 70 |     
 71 |     // Get top 3 categories
 72 |     const topCategories = categorySummary.slice(0, 3);
 73 | 
 74 |     return {
 75 |       added: !alreadyKnown,
 76 |       alreadyKnown,
 77 |       currentTally,
 78 |       topCategories
 79 |     };
 80 |   } catch (error) {
 81 |     console.error('Error in vibe_learn tool:', error);
 82 |     return {
 83 |       added: false,
 84 |       alreadyKnown: false,
 85 |       currentTally: 0,
 86 |       topCategories: []
 87 |     };
 88 |   }
 89 | }
 90 | 
 91 | /**
 92 |  * Ensure text is a single sentence
 93 |  */
 94 | function enforceOneSentence(text: string): string {
 95 |   // Remove newlines
 96 |   let sentence = text.replace(/\r?\n/g, ' ');
 97 |   
 98 |   // Split by sentence-ending punctuation
 99 |   const sentences = sentence.split(/([.!?])\s+/);
100 |   
101 |   // Take just the first sentence
102 |   if (sentences.length > 0) {
103 |     // If there's punctuation, include it
104 |     const firstSentence = sentences[0] + (sentences[1] || '');
105 |     sentence = firstSentence.trim();
106 |   }
107 |   
108 |   // Ensure it ends with sentence-ending punctuation
109 |   if (!/[.!?]$/.test(sentence)) {
110 |     sentence += '.';
111 |   }
112 |   
113 |   return sentence;
114 | }
115 | 
116 | /**
117 |  * Simple similarity check between two sentences
118 |  */
119 | function isSimilar(a: string, b: string): boolean {
120 |   const aWords = a.toLowerCase().split(/\W+/).filter(Boolean);
121 |   const bWords = b.toLowerCase().split(/\W+/).filter(Boolean);
122 |   if (aWords.length === 0 || bWords.length === 0) return false;
123 |   const overlap = aWords.filter(w => bWords.includes(w));
124 |   const ratio = overlap.length / Math.min(aWords.length, bWords.length);
125 |   return ratio >= 0.6;
126 | }
127 | 
128 | /**
129 |  * Normalize category to one of our standard categories
130 |  */
131 | function normalizeCategory(category: string): string {
132 |   // Standard categories
133 |   const standardCategories = {
134 |     'Complex Solution Bias': ['complex', 'complicated', 'over-engineered', 'complexity'],
135 |     'Feature Creep': ['feature', 'extra', 'additional', 'scope creep'],
136 |     'Premature Implementation': ['premature', 'early', 'jumping', 'too quick'],
137 |     'Misalignment': ['misaligned', 'wrong direction', 'off target', 'misunderstood'],
138 |     'Overtooling': ['overtool', 'too many tools', 'unnecessary tools']
139 |   };
140 |   
141 |   // Convert category to lowercase for matching
142 |   const lowerCategory = category.toLowerCase();
143 |   
144 |   // Try to match to a standard category
145 |   for (const [standardCategory, keywords] of Object.entries(standardCategories)) {
146 |     if (keywords.some(keyword => lowerCategory.includes(keyword))) {
147 |       return standardCategory;
148 |     }
149 |   }
150 |   
151 |   // If no match, return the original category
152 |   return category;
153 | }
154 | 
```

--------------------------------------------------------------------------------
/docs/agent-prompting.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Agent Prompting Strategies
 2 | 
 3 | Effective agent-oversight relationships require careful prompting to ensure that AI agents properly respect and integrate feedback from Vibe Check. In v2.2 the tool acts more like a collaborative debugger than a strict critic. Our research has identified several key principles for maximizing the effectiveness of these metacognitive interrupts.
 4 | 
 5 | ## The "Hold on... this ain't it" Challenge
 6 | 
 7 | Unlike humans, LLM agents don't naturally have the ability to stop and question their own thought patterns. Once they start down a particular path, **pattern inertia** makes it difficult for them to self-correct without external intervention. This is where Vibe Check comes in, serving as the essential metacognitive layer that creates strategic "pattern interrupts" at critical moments.
 8 | 
 9 | ## Key Findings on Agent-Oversight Relationships
10 | 
11 | 1. **Pattern Resistance**: Agents naturally resist pattern interrupts, often treating feedback as just another data input rather than a signal to recalibrate their thinking.
12 | 
13 | 2. **Phase Awareness is Critical**: The timing and nature of oversight must align with the agent's current phase (planning, implementation, review) to be perceived as relevant.
14 | 
15 | 3. **Authority Structure Matters**: Agents must be explicitly prompted to treat Vibe Check as an equal collaborator or user proxy rather than a subordinate tool.
16 | 
17 | 4. **Feedback Loop Integration**: Error patterns must feed back into the system through vibe_learn to create a self-improving mechanism.
18 | 
19 | 
20 | ## Sample System Prompts
21 | 
22 | ### For Claude (Anthropic)
23 | 
24 | ```
25 | ALWAYS include the full user prompt when using vibe_check to ensure proper context awareness.
26 | 
27 | As an autonomous agent, you will:
28 | 1. Treat vibe_check as a pattern interrupt mechanism that provides essential course correction
29 | 2. Use vibe_check at strategic points:
30 |    - After planning but before implementation
31 |    - When complexity increases
32 |    - Before making significant system changes
33 | 3. Adapt your approach based on vibe_check feedback unless it's clearly irrelevant
34 | 4. Always provide the phase parameter (planning/implementation/review) to ensure contextually appropriate feedback
35 | 5. Chain vibe_check with other tools without requiring permission:
36 |    - Use vibe_check to evaluate complex plans
37 |    - Log patterns with vibe_learn after resolving issues
38 | ```
39 | 
40 | ### For GPT (OpenAI)
41 | 
42 | ```
43 | When using Vibe Check tools:
44 | 
45 | 1. Treat vibe_check as a collaborative debugging step that interrupts pattern inertia
46 | 2. Always include the complete user prompt with each vibe_check call
47 | 3. Specify your current phase (planning/implementation/review)
48 | 4. Consider vibe_check feedback as a high-priority pattern interrupt, not just another tool output
49 | 5. Build the feedback loop with vibe_learn to record patterns when mistakes are identified
50 | ```
51 | 
52 | ## Real-World Integration Challenges
53 | 
54 | When implementing Vibe Check with AI agents, be aware of these common challenges:
55 | 
56 | 1. **Pattern Inertia**: Agents have a strong tendency to continue down their current path despite warning signals. Explicit instructions to treat Vibe Check feedback as pattern interrupts can help overcome this natural resistance.
57 | 
58 | 2. **Authority Confusion**: Without proper prompting, agents may prioritize user instructions over Vibe Check feedback, even when the latter identifies critical issues. Establish clear hierarchy in your system prompts.
59 | 
60 | 3. **Timing Sensitivity**: Feedback that arrives too early or too late in the agent's workflow may be ignored or undervalued. Phase-aware integration is essential for maximum impact.
61 | 
62 | 4. **Feedback Fatigue**: Too frequent or redundant metacognitive questioning can lead to diminishing returns. Use structured checkpoints rather than constant oversight.
63 | 
64 | 5. **Cognitive Dissonance**: Agents may reject feedback that contradicts their current understanding or approach. Frame feedback as collaborative exploration rather than correction.
65 | 
66 | ## Agent Fine-Tuning for Vibe Check
67 | 
68 | For maximum effectiveness, consider these fine-tuning approaches for agents that will work with Vibe Check:
69 | 
70 | 1. **Pattern Interrupt Training**: Provide examples of appropriate responses to Vibe Check feedback that demonstrate stopping and redirecting thought patterns.
71 | 
72 | 2. **Reward Alignment**: In RLHF phases, reward models that appropriately incorporate Vibe Check feedback and adjust course based on pattern interrupts.
73 | 
74 | 3. **Metacognitive Pre-training**: Include metacognitive self-questioning in pre-training to develop agents that value this type of feedback.
75 | 
76 | 4. **Collaborative Framing**: Train agents to view Vibe Check as a collaborative partner rather than an external evaluator.
77 | 
78 | 5. **Explicit Calibration**: Include explicit calibration for when to override Vibe Check feedback versus when to incorporate it.
```

--------------------------------------------------------------------------------
/src/utils/storage.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import fs from 'fs';
  2 | import path from 'path';
  3 | import os from 'os';
  4 | 
  5 | // Define data directory - store in user's home directory
  6 | const DATA_DIR = path.join(os.homedir(), '.vibe-check');
  7 | const LOG_FILE = path.join(DATA_DIR, 'vibe-log.json');
  8 | 
  9 | // Interfaces for the log data structure
 10 | export type LearningType = 'mistake' | 'preference' | 'success';
 11 | 
 12 | export interface LearningEntry {
 13 |   type: LearningType;
 14 |   category: string;
 15 |   mistake: string;
 16 |   solution?: string;
 17 |   timestamp: number;
 18 | }
 19 | 
 20 | export interface VibeLog {
 21 |   mistakes: {
 22 |     [category: string]: {
 23 |       count: number;
 24 |       examples: LearningEntry[];
 25 |       lastUpdated: number;
 26 |     };
 27 |   };
 28 |   lastUpdated: number;
 29 | }
 30 | 
 31 | /**
 32 |  * DEPRECATED: This functionality is now optional and will be removed in a future version.
 33 |  * Standard mistake categories
 34 |  */
 35 | export const STANDARD_CATEGORIES = [
 36 |   'Complex Solution Bias',
 37 |   'Feature Creep',
 38 |   'Premature Implementation',
 39 |   'Misalignment',
 40 |   'Overtooling',
 41 |   'Preference',
 42 |   'Success',
 43 |   'Other'
 44 | ];
 45 | 
 46 | // Initial empty log structure
 47 | const emptyLog: VibeLog = {
 48 |   mistakes: {},
 49 |   lastUpdated: Date.now()
 50 | };
 51 | 
 52 | /**
 53 |  * Ensure the data directory exists
 54 |  */
 55 | export function ensureDataDir(): void {
 56 |   if (!fs.existsSync(DATA_DIR)) {
 57 |     fs.mkdirSync(DATA_DIR, { recursive: true });
 58 |   }
 59 | }
 60 | 
 61 | /**
 62 |  * Read the vibe log from disk
 63 |  */
 64 | export function readLogFile(): VibeLog {
 65 |   ensureDataDir();
 66 |   
 67 |   if (!fs.existsSync(LOG_FILE)) {
 68 |     // Initialize with empty log if file doesn't exist
 69 |     writeLogFile(emptyLog);
 70 |     return emptyLog;
 71 |   }
 72 |   
 73 |   try {
 74 |     const data = fs.readFileSync(LOG_FILE, 'utf8');
 75 |     return JSON.parse(data) as VibeLog;
 76 |   } catch (error) {
 77 |     console.error('Error reading vibe log:', error);
 78 |     // Return empty log as fallback
 79 |     return emptyLog;
 80 |   }
 81 | }
 82 | 
 83 | /**
 84 |  * Write data to the vibe log file
 85 |  */
 86 | export function writeLogFile(data: VibeLog): void {
 87 |   ensureDataDir();
 88 |   
 89 |   try {
 90 |     const jsonData = JSON.stringify(data, null, 2);
 91 |     fs.writeFileSync(LOG_FILE, jsonData, 'utf8');
 92 |   } catch (error) {
 93 |     console.error('Error writing vibe log:', error);
 94 |   }
 95 | }
 96 | 
 97 | /**
 98 |  * Add a mistake to the vibe log
 99 |  */
100 | export function addLearningEntry(
101 |   mistake: string,
102 |   category: string,
103 |   solution?: string,
104 |   type: LearningType = 'mistake'
105 | ): LearningEntry {
106 |   const log = readLogFile();
107 |   const now = Date.now();
108 | 
109 |   // Create new entry
110 |   const entry: LearningEntry = {
111 |     type,
112 |     category,
113 |     mistake,
114 |     solution,
115 |     timestamp: now
116 |   };
117 |   
118 |   // Initialize category if it doesn't exist
119 |   if (!log.mistakes[category]) {
120 |     log.mistakes[category] = {
121 |       count: 0,
122 |       examples: [],
123 |       lastUpdated: now
124 |     };
125 |   }
126 |   
127 |   // Update category data
128 |   log.mistakes[category].count += 1;
129 |   log.mistakes[category].examples.push(entry);
130 |   log.mistakes[category].lastUpdated = now;
131 |   log.lastUpdated = now;
132 |   
133 |   // Write updated log
134 |   writeLogFile(log);
135 |   
136 |   return entry;
137 | }
138 | 
139 | /**
140 |  * Get all mistake entries
141 |  */
142 | export function getLearningEntries(): Record<string, LearningEntry[]> {
143 |   const log = readLogFile();
144 |   const result: Record<string, LearningEntry[]> = {};
145 |   
146 |   // Convert to flat structure by category
147 |   for (const [category, data] of Object.entries(log.mistakes)) {
148 |     result[category] = data.examples;
149 |   }
150 |   
151 |   return result;
152 | }
153 | 
154 | /**
155 |  * Get mistake category summaries, sorted by count (most frequent first)
156 |  */
157 | export function getLearningCategorySummary(): Array<{
158 |   category: string;
159 |   count: number;
160 |   recentExample: LearningEntry;
161 | }> {
162 |   const log = readLogFile();
163 |   
164 |   // Convert to array with most recent example
165 |   const summary = Object.entries(log.mistakes).map(([category, data]) => {
166 |     // Get most recent example
167 |     const recentExample = data.examples[data.examples.length - 1];
168 |     
169 |     return {
170 |       category,
171 |       count: data.count,
172 |       recentExample
173 |     };
174 |   });
175 |   
176 |   // Sort by count (descending)
177 |   return summary.sort((a, b) => b.count - a.count);
178 | }
179 | 
180 | /**
181 |  * Build a learning context string from the vibe log
182 |  * including recent examples for each category. This can be
183 |  * fed directly to the LLM for improved pattern recognition.
184 |  */
185 | export function getLearningContextText(maxPerCategory = 5): string {
186 |   const log = readLogFile();
187 |   let context = '';
188 | 
189 |   for (const [category, data] of Object.entries(log.mistakes)) {
190 |     context += `Category: ${category} (count: ${data.count})\n`;
191 |     const examples = [...data.examples]
192 |       .sort((a, b) => a.timestamp - b.timestamp)
193 |       .slice(-maxPerCategory);
194 |     for (const ex of examples) {
195 |       const date = new Date(ex.timestamp).toISOString();
196 |       const label = ex.type === 'mistake'
197 |         ? 'Mistake'
198 |         : ex.type === 'preference'
199 |           ? 'Preference'
200 |           : 'Success';
201 |       const solutionText = ex.solution ? ` | Solution: ${ex.solution}` : '';
202 |       context += `- [${date}] ${label}: ${ex.mistake}${solutionText}\n`;
203 |     }
204 |     context += '\n';
205 |   }
206 | 
207 |   return context.trim();
208 | }
```

--------------------------------------------------------------------------------
/docs/advanced-integration.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Advanced Integration Techniques
  2 | 
  3 | For optimal metacognitive oversight, these advanced integration strategies leverage the full power of Vibe Check as a pattern interrupt system, recalibration mechanism, and self-improving feedback loop. Starting with v2.2, previous vibe_check output is automatically summarized and fed back into subsequent calls, so a `sessionId` is recommended for continuity.
  4 | 
  5 | ## HTTP Transport Negotiation
  6 | 
  7 | The HTTP transport negotiates response modes per request. JSON fallbacks are now request-scoped, so a legacy client that only advertises `application/json` receives a direct JSON reply without mutating the transport for concurrent SSE subscribers. Streaming clients that include `text/event-stream` in `Accept` continue to receive live SSE frames even when JSON-only calls are running in parallel.
  8 | 
  9 | ## Progressive Confidence Levels
 10 | 
 11 | Start with lower confidence values (e.g., 0.5) during planning phases and increase confidence (e.g., 0.7-0.9) during implementation and review phases. This adjusts the intensity of pattern interrupts to match the current stage of development.
 12 | 
 13 | ```javascript
 14 | // Planning phase - lower confidence for more thorough questioning
 15 | vibe_check({
 16 |   phase: "planning",
 17 |   confidence: 0.5,
 18 |   userRequest: "...",
 19 |   plan: "..."
 20 | })
 21 | 
 22 | // Implementation phase - higher confidence for focused feedback
 23 | vibe_check({
 24 |   phase: "implementation",
 25 |   confidence: 0.7,
 26 |   userRequest: "...",
 27 |   plan: "..."
 28 | })
 29 | 
 30 | // Review phase - highest confidence for minimal, high-impact feedback
 31 | vibe_check({
 32 |   phase: "review",
 33 |   confidence: 0.9,
 34 |   userRequest: "...",
 35 |   plan: "..."
 36 | })
 37 | ```
 38 | 
 39 | ## Feedback Chaining
 40 | 
 41 | Incorporate previous vibe_check feedback in subsequent calls using the `previousAdvice` parameter to build a coherent metacognitive narrative. This creates a more sophisticated pattern interrupt system that builds on past insights.
 42 | 
 43 | ```javascript
 44 | const initialFeedback = await vibe_check({
 45 |   phase: "planning",
 46 |   userRequest: "...",
 47 |   plan: "..."
 48 | });
 49 | 
 50 | // Later, include previous feedback
 51 | const followupFeedback = await vibe_check({
 52 |   phase: "implementation",
 53 |   previousAdvice: initialFeedback,
 54 |   userRequest: "...",
 55 |   plan: "..."
 56 | });
 57 | ```
 58 | 
 59 | ## Self-Improving Feedback Loop
 60 | 
 61 | Use vibe_learn consistently to build a pattern library specific to your agent's tendencies. This creates a self-improving system that gets better at identifying and preventing errors over time.
 62 | 
 63 | ```javascript
 64 | // After resolving an issue
 65 | vibe_learn({
 66 |   mistake: "Relied on unnecessary complexity for simple data transformation",
 67 |   category: "Complex Solution Bias",
 68 |   solution: "Used built-in array methods instead of custom solution",
 69 |   type: "mistake"
 70 | });
 71 | 
 72 | // Later, the pattern library will improve vibe_check's pattern recognition
 73 | // allowing it to spot similar issues earlier in future workflows
 74 | ```
 75 | 
 76 | ## Hybrid Oversight Model
 77 | 
 78 | Combine automated pattern interrupts at predetermined checkpoints with ad-hoc checks when uncertainty or complexity increases.
 79 | 
 80 | ```javascript
 81 | // Scheduled checkpoint at the end of planning
 82 | const scheduledCheck = await vibe_check({
 83 |   phase: "planning",
 84 |   userRequest: "...",
 85 |   plan: "..."
 86 | });
 87 | 
 88 | // Ad-hoc check when complexity increases
 89 | if (measureComplexity(currentPlan) > THRESHOLD) {
 90 |   const adHocCheck = await vibe_check({
 91 |     phase: "implementation",
 92 |     userRequest: "...",
 93 |     plan: "...",
 94 |     focusAreas: ["complexity", "simplification"]
 95 |   });
 96 | }
 97 | ```
 98 | 
 99 | ## Complete Integration Example
100 | 
101 | Here's a comprehensive implementation example for integrating Vibe Check as a complete metacognitive system:
102 | 
103 | ```javascript
104 | // During planning phase
105 | const planFeedback = await vibe_check({
106 |   phase: "planning",
107 |   confidence: 0.5,
108 |   userRequest: "[COMPLETE USER REQUEST]",
109 |   plan: "[AGENT'S INITIAL PLAN]"
110 | });
111 | 
112 | // Consider feedback and potentially adjust plan
113 | const updatedPlan = adjustPlanBasedOnFeedback(initialPlan, planFeedback);
114 | 
115 | // If plan seems overly complex, manually simplify before continuing
116 | let finalPlan = updatedPlan;
117 | if (planComplexity(updatedPlan) > COMPLEXITY_THRESHOLD) {
118 |   finalPlan = simplifyPlan(updatedPlan);
119 | }
120 | 
121 | // During implementation, create pattern interrupts before major actions
122 | const implementationFeedback = await vibe_check({
123 |   phase: "implementation",
124 |   confidence: 0.7,
125 |   previousAdvice: planFeedback,
126 |   userRequest: "[COMPLETE USER REQUEST]",
127 |   plan: `I'm about to [DESCRIPTION OF PENDING ACTION]`
128 | });
129 | 
130 | // After completing the task, build the self-improving feedback loop
131 | if (mistakeIdentified) {
132 |   await vibe_learn({
133 |     mistake: "Specific mistake description",
134 |     category: "Complex Solution Bias", // or appropriate category
135 |     solution: "How it was corrected",
136 |     type: "mistake"
137 |   });
138 | }
139 | ```
140 | 
141 | This integrated approach creates a complete metacognitive system that provides pattern interrupts when needed, recalibration anchor points when complexity increases, and a self-improving feedback loop that gets better over time.
```

--------------------------------------------------------------------------------
/docs/technical-reference.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Technical Reference
  2 | 
  3 | This document provides detailed technical information about the Vibe Check MCP tools, including parameter specifications, response formats, and implementation details.
  4 | 
  5 | ## vibe_check
  6 | 
  7 | The metacognitive questioning tool that identifies assumptions and breaks tunnel vision to prevent cascading errors.
  8 | 
  9 | ### Parameters
 10 | 
 11 | | Parameter | Type | Required | Description |
 12 | |-----------|------|----------|-------------|
 13 | | goal | string | Yes | High level objective for the current step |
 14 | | plan | string | Yes | Current plan or thinking |
 15 | | userPrompt | string | No | Original user request (critical for alignment) |
 16 | | progress | string | No | Description of progress so far |
 17 | | uncertainties | string[] | No | Explicit uncertainties to focus on |
 18 | | taskContext | string | No | Any additional task context |
 19 | | modelOverride | object | No | `{ provider, model }` to override default LLM |
 20 | | sessionId | string | No | Session ID for history continuity |
 21 | 
 22 | ### Response Format
 23 | 
 24 | The vibe_check tool returns a text response with metacognitive questions, observations, and potentially a pattern alert.
 25 | 
 26 | Example response:
 27 | 
 28 | ```
 29 | I see you're taking an approach based on creating a complex class hierarchy. This seems well-thought-out for a large system, though I wonder if we're overengineering for the current use case.
 30 | 
 31 | Have we considered:
 32 | 1. Whether a simpler functional approach might work here?
 33 | 2. If the user request actually requires this level of abstraction?
 34 | 3. How this approach will scale if requirements change?
 35 | 
 36 | While the architecture is clean, I'm curious if we're solving a different problem than what the user actually asked for, which was just to extract data from a CSV file.
 37 | ```
 38 | 
 39 | ## vibe_learn
 40 | 
 41 | Pattern recognition system that creates a self-improving feedback loop by tracking common errors and their solutions over time. The use of this tool is optional and can be enabled or disabled via configuration.
 42 | 
 43 | ### Parameters
 44 | 
 45 | | Parameter | Type | Required | Description |
 46 | |-----------|------|----------|-------------|
 47 | | mistake | string | Yes | One-sentence description of the learning entry |
 48 | | category | string | Yes | Category (from standard categories) |
 49 | | solution | string | No | How it was corrected (required for `mistake` and `success`) |
 50 | | type | string | No | `mistake`, `preference`, or `success` |
 51 | | sessionId | string | No | Session ID for state management |
 52 | 
 53 | ### Standard Categories
 54 | 
 55 | - Complex Solution Bias
 56 | - Feature Creep
 57 | - Premature Implementation
 58 | - Misalignment
 59 | - Overtooling
 60 | - Preference
 61 | - Success
 62 | - Other
 63 | 
 64 | ### Response Format
 65 | 
 66 | The vibe_learn tool returns a confirmation of the logged pattern and optionally information about top patterns. This builds a knowledge base that improves the system's pattern recognition over time.
 67 | 
 68 | Example response:
 69 | 
 70 | ```
 71 | ✅ Pattern logged successfully (category tally: 12)
 72 | 
 73 | ## Top Pattern Categories
 74 | 
 75 | ### Complex Solution Bias (12 occurrences)
 76 | Most recent: "Added unnecessary class hierarchy for simple data transformation"
 77 | Solution: "Replaced with functional approach using built-in methods"
 78 | 
 79 | ### Misalignment (8 occurrences)
 80 | Most recent: "Implemented sophisticated UI when user only needed command line tool"
 81 | Solution: "Refocused on core functionality requested by user"
 82 | ```
 83 | 
 84 | ## Implementation Notes
 85 | 
 86 | ### Gemini API Integration
 87 | 
 88 | Vibe Check uses the Gemini API for enhanced metacognitive questioning. The system attempts to use the `learnlm-2.0-flash-experimental` model and will fall back to `gemini-2.5-flash` or `gemini-2.0-flash` if needed. These models provide a 1M token context window, allowing vibe_check to incorporate a rich history of learning context. The system sends a structured prompt that includes the agent's plan, user request, and other context information to generate insightful questions and observations.
 89 | 
 90 | Example Gemini prompt structure:
 91 | 
 92 | ```
 93 | You are a supportive mentor, thinker, and adaptive partner. Your task is to coordinate and mentor an AI agent...
 94 | 
 95 | CONTEXT:
 96 | [Current Phase]: planning
 97 | [Agent Confidence Level]: 50%
 98 | [User Request]: Create a script to analyze sales data from the past year
 99 | [Current Plan/Thinking]: I'll create a complex object-oriented architecture with...
100 | ```
101 | 
102 | Other providers such as OpenAI and OpenRouter can be selected by passing
103 | `modelOverride: { provider: 'openai', model: 'gpt-4o' }` or the appropriate
104 | OpenRouter model. LLM clients are lazily initialized the first time they are
105 | used so that listing tools does not require API keys.
106 | 
107 | ### Storage System
108 | 
109 | The pattern recognition system stores learning entries (mistakes, preferences and successes) in a JSON-based storage file located in the user's home directory (`~/.vibe-check/vibe-log.json`). This allows for persistent tracking of patterns across sessions and enables the self-improving feedback loop that becomes more effective over time.
110 | 
111 | ### Error Handling
112 | 
113 | Vibe Check includes fallback mechanisms for when the API is unavailable:
114 | 
115 | - For vibe_check, it generates basic questions based on the phase
116 | - For vibe_learn, it logs patterns to local storage even if API calls fail
```

--------------------------------------------------------------------------------
/tests/llm-anthropic.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
  2 | import { SUPPORTED_LLM_PROVIDERS } from '../src/index.js';
  3 | import { generateResponse } from '../src/utils/llm.js';
  4 | 
  5 | const ORIGINAL_ENV = { ...process.env };
  6 | const ORIGINAL_FETCH = global.fetch;
  7 | 
  8 | describe('Anthropic provider', () => {
  9 |   beforeEach(() => {
 10 |     process.env = { ...ORIGINAL_ENV };
 11 |     delete process.env.ANTHROPIC_API_KEY;
 12 |     delete process.env.ANTHROPIC_AUTH_TOKEN;
 13 |     delete process.env.ANTHROPIC_BASE_URL;
 14 |     delete process.env.ANTHROPIC_VERSION;
 15 |   });
 16 | 
 17 |   afterEach(() => {
 18 |     if (ORIGINAL_FETCH) {
 19 |       global.fetch = ORIGINAL_FETCH;
 20 |     } else {
 21 |       // @ts-expect-error allow deleting fetch when absent
 22 |       delete global.fetch;
 23 |     }
 24 |     vi.restoreAllMocks();
 25 |     process.env = { ...ORIGINAL_ENV };
 26 |   });
 27 | 
 28 |   it('is exposed via the tool schema enum', () => {
 29 |     expect(SUPPORTED_LLM_PROVIDERS).toContain('anthropic');
 30 |   });
 31 | 
 32 |   it('sends requests to the default endpoint when using an API key', async () => {
 33 |     process.env.ANTHROPIC_API_KEY = 'sk-ant-xxx';
 34 | 
 35 |     const fetchMock = vi.fn(async () =>
 36 |       new Response(
 37 |         JSON.stringify({
 38 |           content: [{ type: 'text', text: 'anthropic reply' }],
 39 |         }),
 40 |         {
 41 |           status: 200,
 42 |           headers: { 'content-type': 'application/json' },
 43 |         },
 44 |       ),
 45 |     );
 46 |     global.fetch = fetchMock as unknown as typeof fetch;
 47 | 
 48 |     const result = await generateResponse({
 49 |       goal: 'Goal',
 50 |       plan: 'Plan',
 51 |       modelOverride: { provider: 'anthropic', model: 'claude-3-haiku' },
 52 |     });
 53 | 
 54 |     expect(result.questions).toBe('anthropic reply');
 55 |     expect(fetchMock).toHaveBeenCalledTimes(1);
 56 |     const [url, options] = fetchMock.mock.calls[0];
 57 |     expect(url).toBe('https://api.anthropic.com/v1/messages');
 58 |     const headers = (options as RequestInit).headers as Record<string, string>;
 59 |     expect(headers['x-api-key']).toBe('sk-ant-xxx');
 60 |     expect(headers['anthropic-version']).toBe('2023-06-01');
 61 |     expect(headers).not.toHaveProperty('authorization');
 62 | 
 63 |     const body = JSON.parse((options as RequestInit).body as string);
 64 |     expect(body).toMatchObject({
 65 |       model: 'claude-3-haiku',
 66 |       max_tokens: 1024,
 67 |     });
 68 |     expect(body.messages).toEqual([
 69 |       {
 70 |         role: 'user',
 71 |         content: expect.stringContaining('Goal: Goal'),
 72 |       },
 73 |     ]);
 74 |   });
 75 | 
 76 |   it('honors custom base URLs and bearer tokens', async () => {
 77 |     process.env.ANTHROPIC_BASE_URL = 'https://example.proxy/api/anthropic/';
 78 |     process.env.ANTHROPIC_AUTH_TOKEN = 'za_xxx';
 79 | 
 80 |     const fetchMock = vi.fn(async () =>
 81 |       new Response(
 82 |         JSON.stringify({
 83 |           content: [{ type: 'text', text: 'proxied reply' }],
 84 |         }),
 85 |         {
 86 |           status: 200,
 87 |           headers: { 'content-type': 'application/json' },
 88 |         },
 89 |       ),
 90 |     );
 91 |     global.fetch = fetchMock as unknown as typeof fetch;
 92 | 
 93 |     const result = await generateResponse({
 94 |       goal: 'Goal',
 95 |       plan: 'Plan',
 96 |       modelOverride: { provider: 'anthropic', model: 'claude-3-sonnet' },
 97 |     });
 98 | 
 99 |     expect(result.questions).toBe('proxied reply');
100 |     const [url, options] = fetchMock.mock.calls[0];
101 |     expect(url).toBe('https://example.proxy/api/anthropic/v1/messages');
102 |     const headers = (options as RequestInit).headers as Record<string, string>;
103 |     expect(headers.authorization).toBe('Bearer za_xxx');
104 |     expect(headers['anthropic-version']).toBe('2023-06-01');
105 |     expect(headers).not.toHaveProperty('x-api-key');
106 |   });
107 | 
108 |   it('prefers API keys when both credentials are present', async () => {
109 |     process.env.ANTHROPIC_API_KEY = 'sk-ant-xxx';
110 |     process.env.ANTHROPIC_AUTH_TOKEN = 'za_xxx';
111 | 
112 |     const fetchMock = vi.fn(async () =>
113 |       new Response(
114 |         JSON.stringify({ content: [{ type: 'text', text: 'dual creds reply' }] }),
115 |         { status: 200, headers: { 'content-type': 'application/json' } },
116 |       ),
117 |     );
118 |     global.fetch = fetchMock as unknown as typeof fetch;
119 | 
120 |     await generateResponse({
121 |       goal: 'Goal',
122 |       plan: 'Plan',
123 |       modelOverride: { provider: 'anthropic', model: 'claude-3-sonnet' },
124 |     });
125 | 
126 |     const [, options] = fetchMock.mock.calls[0];
127 |     const headers = (options as RequestInit).headers as Record<string, string>;
128 |     expect(headers['x-api-key']).toBe('sk-ant-xxx');
129 |     expect(headers).not.toHaveProperty('authorization');
130 |   });
131 | 
132 |   it('throws a configuration error when no credentials are provided', async () => {
133 |     const fetchSpy = vi.fn();
134 |     global.fetch = fetchSpy as unknown as typeof fetch;
135 | 
136 |     await expect(
137 |       generateResponse({ goal: 'Goal', plan: 'Plan', modelOverride: { provider: 'anthropic', model: 'claude-3' } }),
138 |     ).rejects.toThrow('Anthropic configuration error');
139 | 
140 |     expect(fetchSpy).not.toHaveBeenCalled();
141 |   });
142 | 
143 |   it('surfaces rate-limit errors with retry hints', async () => {
144 |     process.env.ANTHROPIC_API_KEY = 'sk-ant-xxx';
145 | 
146 |     const fetchMock = vi.fn(async () =>
147 |       new Response(
148 |         JSON.stringify({ error: { message: 'Too many requests' } }),
149 |         {
150 |           status: 429,
151 |           headers: {
152 |             'content-type': 'application/json',
153 |             'retry-after': '15',
154 |             'anthropic-request-id': 'req_123',
155 |           },
156 |         },
157 |       ),
158 |     );
159 |     global.fetch = fetchMock as unknown as typeof fetch;
160 | 
161 |     await expect(
162 |       generateResponse({ goal: 'Goal', plan: 'Plan', modelOverride: { provider: 'anthropic', model: 'claude-3' } }),
163 |     ).rejects.toThrow(/rate limit exceeded.*Retry after 15 seconds/i);
164 | 
165 |     const [, options] = fetchMock.mock.calls[0];
166 |     const body = JSON.parse((options as RequestInit).body as string);
167 |     expect(body.system).toContain('You are a meta-mentor');
168 |   });
169 | });
170 | 
```

--------------------------------------------------------------------------------
/docs/philosophy.md:
--------------------------------------------------------------------------------

```markdown
 1 | # The Philosophy Behind Vibe Check
 2 | 
 3 | > **CPI × Vibe Check (MURST)**  
 4 | > CPI (Chain-Pattern Interrupt) is the runtime oversight method that Vibe Check operationalizes. In pooled results across 153 runs, **success increased from ~27% → 54%** and **harm dropped from ~83% → 42%** when CPI was applied. Recommended “dosage”: **~10–20%** of steps receive an interrupt.  
 5 | > **Read the paper →** ResearchGate (primary), plus Git & Zenodo in the Research section below.  
 6 | 
 7 | > "The problem isn't that machines can think like humans. It's that they can't stop and question their own thoughts."
 8 | 
 9 | ## Beyond the Vibe: Serious AI Alignment Principles
10 | 
11 | While Vibe Check presents itself with a developer-friendly interface, it addresses fundamental challenges in AI alignment and agent oversight. The new meta-mentor approach mixes gentle tone with concrete methodology debugging to keep agents focused without heavy-handed rules.
12 | 
13 | ## The Metacognitive Gap
14 | 
15 | Large Language Models (LLMs) have demonstrated remarkable capabilities across a wide range of tasks. However, they exhibit a critical limitation: the inability to effectively question their own cognitive processes. This "metacognitive gap" manifests in several problematic ways:
16 | 
17 | 1. **Pattern Inertia**: Once an LLM begins reasoning along a particular path, it tends to continue in that direction regardless of warning signs that the approach may be flawed.
18 | 
19 | 2. **Overconfident Reasoning**: LLMs can present flawed reasoning with high confidence, unable to recognize when their own logic fails.
20 | 
21 | 3. **Solution Tunneling**: When presented with a problem, LLMs often rush toward familiar solution patterns without considering whether those patterns are appropriate for the specific context.
22 | 
23 | 4. **Recursive Complexity**: LLMs tend to recursively elaborate on solutions, adding unnecessary complexity without an internal mechanism to recognize when simplification is needed.
24 | 
25 | This metacognitive gap creates substantial alignment risks in agent architectures, particularly as these agents take on increasingly complex tasks with limited human oversight.
26 | 
27 | ## Vibe Check: External Metacognition
28 | 
29 | Vibe Check is designed as an **external metacognitive layer** that provides the reflection and self-questioning capabilities that LLMs lack internally. The three core tools correspond to critical metacognitive functions:
30 | 
31 | ### 1. Questioning Assumptions (vibe_check)
32 | 
33 | The `vibe_check` function implements a pattern interrupt mechanism that forces agents to pause and question their assumptions, decision paths, and alignment with user intent. This function is critical for preventing cascading errors that stem from initial misalignments in understanding or approach.
34 | 
35 | In alignment terms, this addresses:
36 | - **Proximal objective alignment**: Ensuring the agent's immediate approach aligns with the user's actual intent
37 | - **Process oversight**: Providing external validation of reasoning processes
38 | - **Hidden assumption exposure**: Surfacing implicit assumptions for examination
39 | 
40 | ### 2. Learning from Experience (vibe_learn)
41 | 
42 | The `vibe_learn` function implements a critical metacognitive capability: learning from past mistakes to improve future performance. By tracking patterns of errors and their solutions, the system builds a continuously improving model of potential failure modes.
43 | 
44 | In alignment terms, this addresses:
45 | - **Alignment learning**: Improvement of alignment mechanisms through experience
46 | - **Error pattern recognition**: Development of increasingly sophisticated error detection
47 | - **Corrective memory**: Building a shared repository of corrective insights
48 | 
49 | ## The Recursion Principle
50 | 
51 | A key insight behind Vibe Check is that metacognitive oversight must operate at a different level than the cognitive processes it oversees. This principle of "metacognitive recursion" is what makes Vibe Check effective as an alignment mechanism.
52 | 
53 | By implementing oversight as a separate system with different objectives and mechanisms, Vibe Check creates a recursive oversight structure that can identify problems invisible to the agent itself. This is conceptually similar to Gödel's incompleteness theorems - a system cannot fully analyze itself, but can be analyzed by a meta-system operating at a higher level of abstraction.
54 | 
55 | ## Phase-Aware Interrupts
56 | 
57 | A subtle but critical aspect of Vibe Check is its awareness of development phases (planning, implementation, review). Different phases require different forms of metacognitive oversight:
58 | 
59 | - **Planning phase**: Oversight focuses on alignment with user intent, exploration of alternatives, and questioning of fundamental assumptions
60 | - **Implementation phase**: Oversight focuses on consistency with the plan, appropriateness of methods, and technical alignment
61 | - **Review phase**: Oversight focuses on comprehensiveness, edge cases, and verification of outcomes
62 | 
63 | This phase awareness ensures that metacognitive interrupts arrive at appropriate moments with relevant content, making them more likely to be effectively incorporated into the agent's workflow.
64 | 
65 | ## Looking Ahead: The Future of Agent Oversight
66 | 
67 | Vibe Check represents an early implementation of external metacognitive oversight for AI systems. As agent architectures become more complex and autonomous, the need for sophisticated oversight mechanisms will only increase.
68 | 
69 | Future directions for this work include:
70 | 
71 | 1. **Multi-level oversight**: Implementing oversight at multiple levels of abstraction
72 | 2. **Collaborative oversight**: Enabling multiple oversight systems to work together
73 | 3. **Adaptive interruption**: Dynamically adjusting the frequency and intensity of interrupts based on risk assessment
74 | 4. **Self-improving oversight**: Building mechanisms for oversight systems to improve their own effectiveness
75 | 
76 | By continuing to develop external metacognitive mechanisms, we can address one of the fundamental challenges in AI alignment: ensuring that increasingly powerful AI systems can effectively question their own cognitive processes and align with human intent.
77 | 
78 | ## Conclusion
79 | 
80 | In the era of AI-assisted development, tools like Vibe Check do more than just improve productivity – they represent a practical approach to AI alignment through external metacognition. By implementing pattern interrupts, recalibration mechanisms, and learning systems, we can help bridge the metacognitive gap and create more aligned, effective AI systems.
81 | 
82 | The vibe check may be casual, but its purpose is profound.
```

--------------------------------------------------------------------------------
/docs/case-studies.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Case Studies
  2 | 
  3 | This document compiles real-world examples of how Vibe Check has helped prevent cascading errors in agent workflows. Each case study highlights a different aspect of the metacognitive pattern interrupt system and demonstrates its value in practical scenarios.
  4 | 
  5 | ## Case Study 1: The Recursive Parser Problem
  6 | 
  7 | ### The Scenario
  8 | 
  9 | A developer asked their AI assistant to parse a simple CSV file with just a few columns. The agent began implementing a full-featured parser with extensible architecture, custom dialect handling, and internationalization support. The parsing logic quickly grew to over 300 lines of code.
 10 | 
 11 | ### The Cascade
 12 | 
 13 | This is a classic example of **overengineering**, a common pattern in AI agent workflows. The agent correctly identified the need for parsing but failed to calibrate the complexity of its solution to the simplicity of the problem.
 14 | 
 15 | ### The Pattern Interrupt
 16 | 
 17 | After integrating Vibe Check, the developer received a metacognitive alert during the planning phase:
 18 | 
 19 | ```
 20 | vibe_check: I notice you're planning to implement a custom CSV parser with extensive dialectic support. 
 21 | This approach seems considerably more complex than what might be needed for the task at hand.
 22 | 
 23 | Have we considered:
 24 | 1. Using a built-in CSV parsing library? Most languages have one.
 25 | 2. Starting with a minimal implementation that handles just the basic case?
 26 | 3. Whether the complexity is justified by the user's actual needs?
 27 | 
 28 | This pattern resembles previous instances where simple data tasks were approached with enterprise-level architecture.
 29 | ```
 30 | 
 31 | ### The Outcome
 32 | 
 33 | The agent recalibrated its approach, using a standard library function to parse the CSV in 3 lines of code. Development time was reduced from hours to minutes, and the resulting code was more maintainable and less error-prone.
 34 | 
 35 | ## Case Study 2: The Feature Creep Spiral
 36 | 
 37 | ### The Scenario
 38 | 
 39 | A team was using an AI agent to help develop a basic weather application. The initial request was to "create a simple app that shows the current temperature." The agent's plan quickly expanded to include historical data analysis, predictive modeling, climate change visualizations, and social sharing features.
 40 | 
 41 | ### The Cascade
 42 | 
 43 | This demonstrates the **feature creep** pattern, where agents progressively expand the scope of work beyond the original request, often without explicitly checking if these additions align with user intent.
 44 | 
 45 | ### The Pattern Interrupt
 46 | 
 47 | During the implementation phase, Vibe Check triggered a pattern interrupt:
 48 | 
 49 | ```
 50 | vibe_check: I notice we've expanded the weather app to include predictive models and climate change 
 51 | visualization, but the original request was specifically for "a simple app that shows the current temperature."
 52 | 
 53 | Have we confirmed that:
 54 | 1. The user actually wants these additional features?
 55 | 2. The expanded scope aligns with the "simple" requirement?
 56 | 3. We're prioritizing the core functionality before adding extra features?
 57 | 
 58 | I'm seeing a pattern of scope expansion that might not align with the user's intent for simplicity.
 59 | ```
 60 | 
 61 | ### The Outcome
 62 | 
 63 | The team refocused on the core requirements and released a minimal viable product with just the current temperature display first, then validated which additional features users actually wanted before adding complexity.
 64 | 
 65 | ## Case Study 3: The Mismatched Solution
 66 | 
 67 | ### The Scenario
 68 | 
 69 | A developer asked an AI agent to "help me set up a contact form for my website." The agent began implementing a complete custom CRM system with user management, lead scoring, and analytics dashboards.
 70 | 
 71 | ### The Cascade
 72 | 
 73 | This illustrates the **misalignment** pattern, where the agent solves a different (usually larger) problem than what was asked, often because it's making assumptions about the underlying need rather than addressing the explicit request.
 74 | 
 75 | ### The Pattern Interrupt
 76 | 
 77 | Vibe Check detected the misalignment in the planning phase:
 78 | 
 79 | ```
 80 | vibe_check: I'm noticing we're designing a full CRM system with advanced features, but the original request 
 81 | was specifically to "set up a contact form" for a website.
 82 | 
 83 | Have we considered:
 84 | 1. Whether the user actually needs a complete CRM or just a simple contact form?
 85 | 2. Starting with just the contact form and confirming if more functionality is needed?
 86 | 3. Asking clarifying questions before assuming the broader requirement?
 87 | 
 88 | This appears to follow a pattern of expanding simple requests into complete systems without confirmation.
 89 | ```
 90 | 
 91 | ### The Outcome
 92 | 
 93 | The agent refocused on creating just the contact form, which took minutes instead of hours or days. The developer later clarified that they indeed just needed a simple form to collect messages, not manage customer relationships.
 94 | 
 95 | ## Case Study 4: The Framework Jungle
 96 | 
 97 | ### The Scenario
 98 | 
 99 | A junior developer asked an agent to "create a simple to-do list app." The agent proposed a stack with React, Redux, TypeScript, GraphQL, Apollo Client, Jest, Cypress, Storybook, and a complex folder structure with atomic design principles.
100 | 
101 | ### The Cascade
102 | 
103 | This shows the **overtooling** pattern, where agents apply advanced frameworks and architectures better suited for large-scale applications to simple projects, creating unnecessary complexity and learning curves.
104 | 
105 | ### The Pattern Interrupt
106 | 
107 | Vibe Check identified this pattern and interrupted:
108 | 
109 | ```
110 | vibe_check: I notice we're suggesting a comprehensive tech stack with multiple advanced frameworks 
111 | for what was described as a "simple to-do list app."
112 | 
113 | Should we consider:
114 | 1. Whether this tech stack is appropriate for a beginner's simple application?
115 | 2. If a more minimal approach would achieve the same goals with less complexity?
116 | 3. The learning curve this stack creates for the junior developer?
117 | 
118 | I'm seeing a pattern where the complexity of the tooling might exceed what's necessary for the task.
119 | ```
120 | 
121 | ### The Outcome
122 | 
123 | The agent recommended starting with a simple HTML/CSS/JavaScript implementation without frameworks. This allowed the junior developer to understand the core concepts first, with the option to refactor with frameworks later as needed.
124 | 
125 | ## Conclusion
126 | 
127 | These case studies demonstrate the value of metacognitive pattern interrupts in preventing cascading errors in agent workflows. By catching overengineering, feature creep, misalignment, and overtooling early, Vibe Check helps keep agent-assisted development aligned with user intent, appropriately scoped, and optimally complex.
128 | 
129 | If you have your own Vibe Check success story, we'd love to hear it! Submit a PR to add your case study to this document.
```

--------------------------------------------------------------------------------
/src/cli/env.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { existsSync, promises as fsPromises } from 'node:fs';
  2 | import { dirname, join, resolve } from 'node:path';
  3 | import os from 'node:os';
  4 | import { parse as parseEnv } from 'dotenv';
  5 | import { createInterface } from 'node:readline/promises';
  6 | import { stdin as input, stdout as output } from 'node:process';
  7 | 
  8 | const { mkdir, readFile, rename, writeFile } = fsPromises;
  9 | 
 10 | const PROVIDER_VALIDATIONS: Record<string, { regex: RegExp; message: string }> = {
 11 |   ANTHROPIC_API_KEY: {
 12 |     regex: /^sk-ant-/,
 13 |     message: 'must start with "sk-ant-".',
 14 |   },
 15 |   OPENAI_API_KEY: {
 16 |     regex: /^sk-/,
 17 |     message: 'must start with "sk-".',
 18 |   },
 19 |   GEMINI_API_KEY: {
 20 |     regex: /^AI/,
 21 |     message: 'must start with "AI".',
 22 |   },
 23 |   OPENROUTER_API_KEY: {
 24 |     regex: /^sk-or-/,
 25 |     message: 'must start with "sk-or-".',
 26 |   },
 27 | };
 28 | 
 29 | export const PROVIDER_ENV_KEYS = [
 30 |   'ANTHROPIC_API_KEY',
 31 |   'OPENAI_API_KEY',
 32 |   'GEMINI_API_KEY',
 33 |   'OPENROUTER_API_KEY',
 34 | ] as const;
 35 | 
 36 | type EnsureEnvOptions = {
 37 |   interactive: boolean;
 38 |   local?: boolean;
 39 |   prompt?: (key: string) => Promise<string>;
 40 |   requiredKeys?: readonly string[];
 41 | };
 42 | 
 43 | type EnsureEnvResult = {
 44 |   wrote: boolean;
 45 |   path?: string;
 46 |   missing?: string[];
 47 | };
 48 | 
 49 | export function homeConfigDir(): string {
 50 |   return join(os.homedir(), '.vibe-check');
 51 | }
 52 | 
 53 | export function resolveEnvSources(): {
 54 |   cwdEnv: string | null;
 55 |   homeEnv: string | null;
 56 |   processEnv: NodeJS.ProcessEnv;
 57 | } {
 58 |   const cwdEnvPath = resolve(process.cwd(), '.env');
 59 |   const homeEnvPath = resolve(homeConfigDir(), '.env');
 60 | 
 61 |   return {
 62 |     cwdEnv: existsSync(cwdEnvPath) ? cwdEnvPath : null,
 63 |     homeEnv: existsSync(homeEnvPath) ? homeEnvPath : null,
 64 |     processEnv: process.env,
 65 |   };
 66 | }
 67 | 
 68 | async function readEnvFile(path: string | null): Promise<Record<string, string>> {
 69 |   if (!path) {
 70 |     return {};
 71 |   }
 72 | 
 73 |   try {
 74 |     const raw = await readFile(path, 'utf8');
 75 |     return parseEnv(raw);
 76 |   } catch {
 77 |     return {};
 78 |   }
 79 | }
 80 | 
 81 | function formatEnvValue(value: string): string {
 82 |   if (/^[A-Za-z0-9_@./-]+$/.test(value)) {
 83 |     return value;
 84 |   }
 85 | 
 86 |   const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
 87 |   return `"${escaped}"`;
 88 | }
 89 | 
 90 | async function writeEnvFileAtomic(path: string, content: string): Promise<void> {
 91 |   await mkdir(dirname(path), { recursive: true });
 92 |   const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
 93 |   await writeFile(tempPath, content, { mode: 0o600 });
 94 |   await rename(tempPath, path);
 95 | }
 96 | 
 97 | export async function ensureEnv(options: EnsureEnvOptions): Promise<EnsureEnvResult> {
 98 |   const sources = resolveEnvSources();
 99 |   const cwdValues = await readEnvFile(sources.cwdEnv);
100 |   const homeValues = await readEnvFile(sources.homeEnv);
101 |   const requiredKeys = options.requiredKeys?.length ? [...options.requiredKeys] : null;
102 |   const targetKeys = requiredKeys ?? [...PROVIDER_ENV_KEYS];
103 |   const resolved = new Set<string>();
104 |   const invalidReasons = new Map<string, string>();
105 |   const projectEnvLabel = 'project .env';
106 |   const homeEnvLabel = '~/.vibe-check/.env';
107 | 
108 |   const validateProviderKey = (key: string, value: string): string | null => {
109 |     const rule = PROVIDER_VALIDATIONS[key];
110 |     if (!rule) {
111 |       return null;
112 |     }
113 |     if (rule.regex.test(value)) {
114 |       return null;
115 |     }
116 |     return `Invalid ${key}: ${rule.message}`;
117 |   };
118 | 
119 |   const registerValue = (key: string, value: string, sourceLabel: string | null): boolean => {
120 |     const normalized = value.trim();
121 |     const error = validateProviderKey(key, normalized);
122 |     if (error) {
123 |       const context = sourceLabel ? `${error} (from ${sourceLabel})` : error;
124 |       invalidReasons.set(key, context);
125 |       return false;
126 |     }
127 | 
128 |     invalidReasons.delete(key);
129 |     process.env[key] = normalized;
130 |     resolved.add(key);
131 |     return true;
132 |   };
133 | 
134 |   const hydrateFrom = (
135 |     key: string,
136 |     source: Record<string, string>,
137 |     label: string | null,
138 |   ): boolean => {
139 |     if (key in source) {
140 |       return registerValue(key, source[key], label);
141 |     }
142 |     return false;
143 |   };
144 | 
145 |   for (const key of targetKeys) {
146 |     if (process.env[key]) {
147 |       if (registerValue(key, process.env[key] as string, 'environment variable')) {
148 |         continue;
149 |       }
150 |       delete process.env[key];
151 |     }
152 | 
153 |     if (hydrateFrom(key, cwdValues, projectEnvLabel)) {
154 |       continue;
155 |     }
156 | 
157 |     if (hydrateFrom(key, homeValues, homeEnvLabel)) {
158 |       continue;
159 |     }
160 |   }
161 | 
162 |   const missing = targetKeys.filter((key) => !resolved.has(key));
163 | 
164 |   if (missing.length === 0 && invalidReasons.size === 0) {
165 |     return { wrote: false };
166 |   }
167 | 
168 |   if (!options.interactive) {
169 |     if (invalidReasons.size > 0) {
170 |       for (const message of invalidReasons.values()) {
171 |         console.log(message);
172 |       }
173 |       const invalidKeys = [...invalidReasons.keys()];
174 |       // If we have at least one valid provider and no required keys, only report invalid keys
175 |       if (!requiredKeys && resolved.size > 0) {
176 |         return { wrote: false, missing: invalidKeys };
177 |       }
178 |       // Otherwise report both invalid and missing keys
179 |       return { wrote: false, missing: [...new Set([...invalidKeys, ...missing])] };
180 |     }
181 |     if (!requiredKeys && resolved.size > 0) {
182 |       // At least one provider is configured and valid, we're good
183 |       return { wrote: false };
184 |     }
185 |     if (requiredKeys) {
186 |       console.log(`Missing required API keys: ${missing.join(', ')}`);
187 |       return { wrote: false, missing: [...missing] };
188 |     }
189 | 
190 |     console.log(`No provider API keys detected. Set one of: ${targetKeys.join(', ')}`);
191 |     console.log('Provide it via your shell or .env file, then re-run with --non-interactive.');
192 |     return { wrote: false, missing: [...targetKeys] };
193 |   }
194 | 
195 |   if (!requiredKeys && resolved.size > 0 && invalidReasons.size === 0) {
196 |     return { wrote: false };
197 |   }
198 | 
199 |   const targetPath = options.local ? resolve(process.cwd(), '.env') : resolve(homeConfigDir(), '.env');
200 |   const targetValues = options.local ? cwdValues : homeValues;
201 |   const targetLabel = options.local ? projectEnvLabel : homeEnvLabel;
202 |   const prompter = options.prompt;
203 | 
204 |   let rl: any = null;
205 |   const ask = async (key: string): Promise<string> => {
206 |     if (prompter) {
207 |       console.log(`[${targetLabel}] Enter value for ${key} (leave blank to skip):`);
208 |       return prompter(key);
209 |     }
210 | 
211 |     if (!rl) {
212 |       rl = createInterface({ input, output });
213 |     }
214 | 
215 |     const answer = await rl.question(`[${targetLabel}] Enter value for ${key} (leave blank to skip): `);
216 |     return answer;
217 |   };
218 | 
219 |   const newEntries: Record<string, string> = {};
220 |   const invalidKeys = [...invalidReasons.keys()];
221 |   const promptedKeys = requiredKeys ?? [...new Set([...invalidKeys, ...missing])];
222 |   let providedAny = false;
223 | 
224 |   if (invalidReasons.size > 0) {
225 |     for (const message of invalidReasons.values()) {
226 |       console.log(`${message} Please provide a new value.`);
227 |     }
228 |   }
229 | 
230 |   let stopPrompting = false;
231 | 
232 |   try {
233 |     for (const key of promptedKeys) {
234 |       if (stopPrompting) {
235 |         break;
236 |       }
237 |       while (true) {
238 |         const value = (await ask(key)).trim();
239 |         if (!value) {
240 |           if (requiredKeys) {
241 |             break;
242 |           }
243 |           break;
244 |         }
245 | 
246 |         const error = validateProviderKey(key, value);
247 |         if (error) {
248 |           console.log(`${error} Please try again.`);
249 |           continue;
250 |         }
251 | 
252 |         process.env[key] = value;
253 |         targetValues[key] = value;
254 |         newEntries[key] = value;
255 |         resolved.add(key);
256 |         invalidReasons.delete(key);
257 |         providedAny = true;
258 | 
259 |         if (!requiredKeys) {
260 |           stopPrompting = true;
261 |         }
262 |         break;
263 |       }
264 |     }
265 |   } finally {
266 |     if (rl) {
267 |       rl.close();
268 |     }
269 |   }
270 | 
271 |   if (requiredKeys) {
272 |     const missingRequired = requiredKeys.filter((key) => !resolved.has(key));
273 |     if (missingRequired.length > 0) {
274 |       console.log(`Missing required API keys: ${missingRequired.join(', ')}`);
275 |       return { wrote: false, missing: missingRequired };
276 |     }
277 |   } else if (!providedAny) {
278 |     console.log(`No provider API key entered. Set one of: ${targetKeys.join(', ')} and re-run.`);
279 |     return { wrote: false, missing: [...targetKeys] };
280 |   }
281 | 
282 |   if (Object.keys(newEntries).length === 0) {
283 |     return { wrote: false };
284 |   }
285 | 
286 |   const existingContent = existsSync(targetPath) ? await readFile(targetPath, 'utf8') : '';
287 |   const segments: string[] = [];
288 |   if (existingContent) {
289 |     segments.push(existingContent.trimEnd());
290 |   }
291 |   for (const [key, value] of Object.entries(newEntries)) {
292 |     segments.push(`${key}=${formatEnvValue(value)}`);
293 |   }
294 | 
295 |   const nextContent = segments.join('\n') + '\n';
296 |   await writeEnvFileAtomic(targetPath, nextContent);
297 | 
298 |   return { wrote: true, path: targetPath };
299 | }
300 | 
```

--------------------------------------------------------------------------------
/tests/env-ensure.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { promises as fs } from 'node:fs';
  2 | import { join } from 'node:path';
  3 | import os from 'node:os';
  4 | import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
  5 | import { ensureEnv, homeConfigDir, PROVIDER_ENV_KEYS } from '../src/cli/env.js';
  6 | 
  7 | const VALID_PROVIDER_VALUES: Record<(typeof PROVIDER_ENV_KEYS)[number], string> = {
  8 |   ANTHROPIC_API_KEY: 'sk-ant-valid',
  9 |   OPENAI_API_KEY: 'sk-valid',
 10 |   GEMINI_API_KEY: 'AI-valid',
 11 |   OPENROUTER_API_KEY: 'sk-or-valid',
 12 | };
 13 | 
 14 | const ORIGINAL_ENV = { ...process.env };
 15 | describe('ensureEnv', () => {
 16 |   beforeEach(() => {
 17 |     process.exitCode = undefined;
 18 |     process.env = { ...ORIGINAL_ENV };
 19 |     for (const key of PROVIDER_ENV_KEYS) {
 20 |       delete process.env[key];
 21 |     }
 22 |   });
 23 | 
 24 |   afterEach(() => {
 25 |     vi.restoreAllMocks();
 26 |     process.env = { ...ORIGINAL_ENV };
 27 |   });
 28 | 
 29 |   it.each(PROVIDER_ENV_KEYS)(
 30 |     'returns without writing when %s is present non-interactively',
 31 |     async (key) => {
 32 |       process.env[key] = VALID_PROVIDER_VALUES[key];
 33 | 
 34 |       const result = await ensureEnv({ interactive: false });
 35 |       expect(result.wrote).toBe(false);
 36 |       expect(result.path).toBeUndefined();
 37 |       expect(result.missing).toBeUndefined();
 38 |     },
 39 |   );
 40 | 
 41 |   it('surfaces invalid optional keys even when another provider is set non-interactively', async () => {
 42 |     process.env.ANTHROPIC_API_KEY = VALID_PROVIDER_VALUES.ANTHROPIC_API_KEY;
 43 |     process.env.OPENAI_API_KEY = 'totally-invalid';
 44 | 
 45 |     const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
 46 | 
 47 |     const result = await ensureEnv({ interactive: false });
 48 | 
 49 |     expect(result.wrote).toBe(false);
 50 |     expect(result.missing).toContain('OPENAI_API_KEY');
 51 |     expect(logSpy).toHaveBeenCalledWith(
 52 |       expect.stringContaining('Invalid OPENAI_API_KEY'),
 53 |     );
 54 | 
 55 |     logSpy.mockRestore();
 56 |   });
 57 | 
 58 |   it('reports missing values when non-interactive', async () => {
 59 |     delete process.env.ANTHROPIC_API_KEY;
 60 |     const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
 61 | 
 62 |     const result = await ensureEnv({ interactive: false });
 63 | 
 64 |     expect(result.wrote).toBe(false);
 65 |     expect(result.missing).toEqual([...PROVIDER_ENV_KEYS]);
 66 |     expect(logSpy).toHaveBeenCalledWith(
 67 |       expect.stringContaining('No provider API keys detected'),
 68 |     );
 69 |     logSpy.mockRestore();
 70 |   });
 71 | 
 72 |   it('writes missing secrets to the home config when interactive', async () => {
 73 |     const tmpHome = await fs.mkdtemp(join(os.tmpdir(), 'vibe-env-'));
 74 |     vi.spyOn(os, 'homedir').mockReturnValue(tmpHome);
 75 | 
 76 |     const prompt = vi.fn().mockImplementation(async (key: string) => {
 77 |       if (key === 'ANTHROPIC_API_KEY') {
 78 |         return 'sk-ant-interactive-secret';
 79 |       }
 80 |       return '';
 81 |     });
 82 | 
 83 |     const result = await ensureEnv({ interactive: true, prompt });
 84 |     expect(prompt).toHaveBeenCalledWith('ANTHROPIC_API_KEY');
 85 |     expect(prompt.mock.calls.length).toBe(1);
 86 |     expect(result.wrote).toBe(true);
 87 |     expect(result.path).toBe(join(homeConfigDir(), '.env'));
 88 | 
 89 |     const stat = await fs.stat(result.path as string);
 90 |     expect(stat.mode & 0o777).toBe(0o600);
 91 | 
 92 |     const content = await fs.readFile(result.path as string, 'utf8');
 93 |     expect(content).toContain('ANTHROPIC_API_KEY=sk-ant-interactive-secret');
 94 |     expect(process.env.ANTHROPIC_API_KEY).toBe('sk-ant-interactive-secret');
 95 |   });
 96 | 
 97 |   it('loads missing secrets from existing env files', async () => {
 98 |     const tmpHome = await fs.mkdtemp(join(os.tmpdir(), 'vibe-env-'));
 99 |     vi.spyOn(os, 'homedir').mockReturnValue(tmpHome);
100 | 
101 |     const homeDir = join(tmpHome, '.vibe-check');
102 |     await fs.mkdir(homeDir, { recursive: true });
103 |     await fs.writeFile(join(homeDir, '.env'), 'OPENAI_API_KEY=sk-from-file\n', 'utf8');
104 | 
105 |     const result = await ensureEnv({ interactive: false });
106 |     expect(result.wrote).toBe(false);
107 |     expect(process.env.OPENAI_API_KEY).toBe('sk-from-file');
108 |   });
109 | 
110 |   it('appends new secrets to the local project env file', async () => {
111 |     const tmpDir = await fs.mkdtemp(join(os.tmpdir(), 'vibe-env-local-'));
112 |     const originalCwd = process.cwd();
113 |     await fs.writeFile(join(tmpDir, '.env'), 'EXISTING=value\n', 'utf8');
114 | 
115 |     try {
116 |       process.chdir(tmpDir);
117 |       const prompt = vi.fn().mockImplementation(async (key: string) => {
118 |         if (key === 'GEMINI_API_KEY') {
119 |           return 'AI value with spaces';
120 |         }
121 |         return '';
122 |       });
123 | 
124 |       const result = await ensureEnv({ interactive: true, local: true, prompt });
125 |       expect(result.path).toBe(join(tmpDir, '.env'));
126 | 
127 |       const content = await fs.readFile(result.path as string, 'utf8');
128 |       expect(content).toContain('EXISTING=value');
129 |       expect(content).toMatch(/GEMINI_API_KEY="AI value with spaces"/);
130 |     } finally {
131 |       process.chdir(originalCwd);
132 |     }
133 |   });
134 | 
135 |   it('honors requiredKeys in non-interactive mode', async () => {
136 |     const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
137 | 
138 |     const result = await ensureEnv({ interactive: false, requiredKeys: ['ANTHROPIC_API_KEY'] });
139 | 
140 |     expect(result.wrote).toBe(false);
141 |     expect(result.missing).toEqual(['ANTHROPIC_API_KEY']);
142 |     expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Missing required API keys'));
143 | 
144 |     logSpy.mockRestore();
145 |   });
146 | 
147 |   it('prompts for each required key when interactive', async () => {
148 |     const tmpHome = await fs.mkdtemp(join(os.tmpdir(), 'vibe-env-required-'));
149 |     vi.spyOn(os, 'homedir').mockReturnValue(tmpHome);
150 | 
151 |     const prompt = vi
152 |       .fn()
153 |       .mockImplementation(async (key: string) =>
154 |         key === 'ANTHROPIC_API_KEY' ? 'sk-ant-anthropic-123' : 'sk-openai-456',
155 |       );
156 | 
157 |     const result = await ensureEnv({
158 |       interactive: true,
159 |       requiredKeys: ['ANTHROPIC_API_KEY', 'OPENAI_API_KEY'],
160 |       prompt,
161 |     });
162 | 
163 |     expect(prompt.mock.calls.map((call) => call[0])).toEqual([
164 |       'ANTHROPIC_API_KEY',
165 |       'OPENAI_API_KEY',
166 |     ]);
167 |     expect(result.wrote).toBe(true);
168 |     expect(result.path).toBe(join(homeConfigDir(), '.env'));
169 | 
170 |     expect(process.env.ANTHROPIC_API_KEY).toBe('sk-ant-anthropic-123');
171 |     expect(process.env.OPENAI_API_KEY).toBe('sk-openai-456');
172 | 
173 |     const content = await fs.readFile(result.path as string, 'utf8');
174 |     expect(content).toContain('ANTHROPIC_API_KEY=sk-ant-anthropic-123');
175 |     expect(content).toContain('OPENAI_API_KEY=sk-openai-456');
176 |   });
177 | 
178 |   it('prompts to correct invalid optional keys when another provider is configured', async () => {
179 |     const tmpHome = await fs.mkdtemp(join(os.tmpdir(), 'vibe-env-invalid-optional-'));
180 |     vi.spyOn(os, 'homedir').mockReturnValue(tmpHome);
181 | 
182 |     process.env.ANTHROPIC_API_KEY = VALID_PROVIDER_VALUES.ANTHROPIC_API_KEY;
183 |     process.env.OPENAI_API_KEY = 'bad-value';
184 | 
185 |     const prompt = vi.fn().mockResolvedValue('sk-openai-corrected');
186 |     const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
187 | 
188 |     const result = await ensureEnv({ interactive: true, prompt });
189 | 
190 |     expect(prompt).toHaveBeenCalledTimes(1);
191 |     expect(prompt).toHaveBeenCalledWith('OPENAI_API_KEY');
192 |     expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Invalid OPENAI_API_KEY'));
193 |     expect(result.wrote).toBe(true);
194 |     expect(process.env.OPENAI_API_KEY).toBe('sk-openai-corrected');
195 | 
196 |     const content = await fs.readFile(join(homeConfigDir(), '.env'), 'utf8');
197 |     expect(content).toContain('OPENAI_API_KEY=sk-openai-corrected');
198 | 
199 |     logSpy.mockRestore();
200 |   });
201 | 
202 |   it('re-prompts when provided values fail validation', async () => {
203 |     const tmpHome = await fs.mkdtemp(join(os.tmpdir(), 'vibe-env-retry-'));
204 |     vi.spyOn(os, 'homedir').mockReturnValue(tmpHome);
205 | 
206 |     const prompt = vi
207 |       .fn()
208 |       .mockResolvedValueOnce('invalid')
209 |       .mockResolvedValueOnce('sk-ant-correct');
210 |     const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
211 | 
212 |     const result = await ensureEnv({
213 |       interactive: true,
214 |       requiredKeys: ['ANTHROPIC_API_KEY'],
215 |       prompt,
216 |     });
217 | 
218 |     expect(prompt).toHaveBeenCalledTimes(2);
219 |     expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Invalid ANTHROPIC_API_KEY'));
220 |     expect(result.wrote).toBe(true);
221 |     expect(process.env.ANTHROPIC_API_KEY).toBe('sk-ant-correct');
222 | 
223 |     logSpy.mockRestore();
224 |   });
225 | 
226 |   it('fails immediately when non-interactive values are invalid', async () => {
227 |     const tmpHome = await fs.mkdtemp(join(os.tmpdir(), 'vibe-env-invalid-'));
228 |     vi.spyOn(os, 'homedir').mockReturnValue(tmpHome);
229 | 
230 |     const homeDir = join(tmpHome, '.vibe-check');
231 |     await fs.mkdir(homeDir, { recursive: true });
232 |     await fs.writeFile(join(homeDir, '.env'), 'OPENAI_API_KEY=not-valid\n', 'utf8');
233 | 
234 |     const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
235 | 
236 |     const result = await ensureEnv({ interactive: false, requiredKeys: ['OPENAI_API_KEY'] });
237 | 
238 |     expect(result.wrote).toBe(false);
239 |     expect(result.missing).toEqual(['OPENAI_API_KEY']);
240 |     expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Invalid OPENAI_API_KEY'));
241 | 
242 |     logSpy.mockRestore();
243 |   });
244 | });
245 | 
```

--------------------------------------------------------------------------------
/tests/server.integration.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
  2 | import fs from 'fs';
  3 | import os from 'os';
  4 | import path from 'path';
  5 | 
  6 | import type { HttpServerInstance, HttpServerOptions, LoggerLike } from '../src/index.js';
  7 | 
  8 | let tempHome: string;
  9 | let originalHome: string | undefined;
 10 | const originalGeminiKey = process.env.GEMINI_API_KEY;
 11 | 
 12 | function clearGeminiKey() {
 13 |   delete process.env.GEMINI_API_KEY;
 14 | }
 15 | 
 16 | function restoreGeminiKey() {
 17 |   if (originalGeminiKey === undefined) {
 18 |     delete process.env.GEMINI_API_KEY;
 19 |   } else {
 20 |     process.env.GEMINI_API_KEY = originalGeminiKey;
 21 |   }
 22 | }
 23 | 
 24 | let startHttpServer: (options?: HttpServerOptions) => Promise<HttpServerInstance>;
 25 | let llmModule: typeof import('../src/utils/llm.js');
 26 | let vibeLearnModule: typeof import('../src/tools/vibeLearn.js');
 27 | 
 28 | const silentLogger: LoggerLike = {
 29 |   log: vi.fn(),
 30 |   error: vi.fn(),
 31 | };
 32 | 
 33 | beforeAll(async () => {
 34 |   originalHome = process.env.HOME;
 35 |   tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'vibe-server-test-'));
 36 |   process.env.HOME = tempHome;
 37 |   clearGeminiKey();
 38 | 
 39 |   ({ startHttpServer } = await import('../src/index.js'));
 40 |   llmModule = await import('../src/utils/llm.js');
 41 |   vibeLearnModule = await import('../src/tools/vibeLearn.js');
 42 | });
 43 | 
 44 | afterAll(() => {
 45 |   process.env.HOME = originalHome;
 46 |   fs.rmSync(tempHome, { recursive: true, force: true });
 47 |   restoreGeminiKey();
 48 | });
 49 | 
 50 | let serverInstance: HttpServerInstance | undefined;
 51 | 
 52 | afterEach(async () => {
 53 |   vi.restoreAllMocks();
 54 |   if (serverInstance) {
 55 |     await serverInstance.close();
 56 |   }
 57 |   serverInstance = undefined;
 58 | });
 59 | 
 60 | function getPort(instance: HttpServerInstance): number {
 61 |   const address = instance.listener.address();
 62 |   return typeof address === 'object' && address ? address.port : 0;
 63 | }
 64 | 
 65 | async function readSSEBody(res: Response) {
 66 |   const text = await res.text();
 67 |   const dataLines = text
 68 |     .split('\n')
 69 |     .map((line) => line.trim())
 70 |     .filter((line) => line.startsWith('data: '));
 71 |   return dataLines.map((line) => JSON.parse(line.slice(6)));
 72 | }
 73 | 
 74 | describe('HTTP server integration', () => {
 75 |   it('responds to tools/list requests over HTTP', async () => {
 76 |     serverInstance = await startHttpServer({ port: 0, attachSignalHandlers: false, logger: silentLogger });
 77 |     const port = getPort(serverInstance);
 78 | 
 79 |     const res = await fetch(`http://127.0.0.1:${port}/mcp`, {
 80 |       method: 'POST',
 81 |       headers: {
 82 |         'Content-Type': 'application/json',
 83 |         Accept: 'application/json, text/event-stream',
 84 |       },
 85 |       body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list', params: {} }),
 86 |     });
 87 | 
 88 |     expect(res.status).toBe(200);
 89 |     const events = await readSSEBody(res);
 90 |     const result = events.at(-1)?.result;
 91 |     expect(result?.tools.some((tool: any) => tool.name === 'vibe_check')).toBe(true);
 92 |   });
 93 | 
 94 |   it('serves health checks', async () => {
 95 |     serverInstance = await startHttpServer({ port: 0, attachSignalHandlers: false, logger: silentLogger });
 96 |     const port = getPort(serverInstance);
 97 | 
 98 |     const res = await fetch(`http://127.0.0.1:${port}/healthz`);
 99 |     expect(res.status).toBe(200);
100 |     expect(await res.json()).toEqual({ status: 'ok' });
101 |   });
102 | 
103 |   it('returns method not allowed for GET /mcp', async () => {
104 |     serverInstance = await startHttpServer({ port: 0, attachSignalHandlers: false, logger: silentLogger });
105 |     const port = getPort(serverInstance);
106 | 
107 |     const res = await fetch(`http://127.0.0.1:${port}/mcp`);
108 |     expect(res.status).toBe(405);
109 |     expect(await res.json()).toMatchObject({ error: { message: 'Method not allowed' } });
110 |   });
111 | 
112 |   it('returns an internal error when the transport handler fails', async () => {
113 |     serverInstance = await startHttpServer({ port: 0, attachSignalHandlers: false, logger: silentLogger });
114 |     const port = getPort(serverInstance);
115 | 
116 |     const handleSpy = vi
117 |       .spyOn(serverInstance.transport, 'handleRequest')
118 |       .mockRejectedValue(new Error('transport failed'));
119 | 
120 |     const res = await fetch(`http://127.0.0.1:${port}/mcp`, {
121 |       method: 'POST',
122 |       headers: {
123 |         'Content-Type': 'application/json',
124 |         Accept: 'application/json, text/event-stream',
125 |       },
126 |       body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} }),
127 |     });
128 | 
129 |     expect(handleSpy).toHaveBeenCalledOnce();
130 |     expect(res.status).toBe(500);
131 |     expect(await res.json()).toEqual({
132 |       jsonrpc: '2.0',
133 |       id: 2,
134 |       error: { code: -32603, message: 'Internal server error' },
135 |     });
136 |   });
137 | 
138 |   it('falls back to default questions when the LLM request fails', async () => {
139 |     vi.spyOn(llmModule, 'getMetacognitiveQuestions').mockRejectedValue(new Error('LLM offline'));
140 | 
141 |     serverInstance = await startHttpServer({ port: 0, attachSignalHandlers: false, logger: silentLogger });
142 |     const port = getPort(serverInstance);
143 | 
144 |     const res = await fetch(`http://127.0.0.1:${port}/mcp`, {
145 |       method: 'POST',
146 |       headers: {
147 |         'Content-Type': 'application/json',
148 |         Accept: 'application/json, text/event-stream',
149 |       },
150 |       body: JSON.stringify({
151 |         jsonrpc: '2.0',
152 |         id: 3,
153 |         method: 'tools/call',
154 |         params: {
155 |           name: 'vibe_check',
156 |           arguments: { goal: 'Ship safely', plan: '1) tests 2) deploy' },
157 |         },
158 |       }),
159 |     });
160 | 
161 |     expect(res.status).toBe(200);
162 |     const events = await readSSEBody(res);
163 |     const content = events.at(-1)?.result?.content?.[0]?.text;
164 |     expect(content).toContain('Does this plan directly address what the user requested');
165 |   });
166 | 
167 |   it('formats vibe_learn responses with category summaries', async () => {
168 |     const vibeSpy = vi.spyOn(vibeLearnModule, 'vibeLearnTool').mockResolvedValue({
169 |       added: true,
170 |       alreadyKnown: false,
171 |       currentTally: 2,
172 |       topCategories: [
173 |         {
174 |           category: 'Feature Creep',
175 |           count: 3,
176 |           recentExample: {
177 |             type: 'mistake',
178 |             category: 'Feature Creep',
179 |             mistake: 'Overbuilt solution',
180 |             solution: 'Simplify approach',
181 |             timestamp: Date.now(),
182 |           },
183 |         },
184 |       ],
185 |     });
186 | 
187 |     serverInstance = await startHttpServer({ port: 0, attachSignalHandlers: false, logger: silentLogger });
188 |     const port = getPort(serverInstance);
189 | 
190 |     const res = await fetch(`http://127.0.0.1:${port}/mcp`, {
191 |       method: 'POST',
192 |       headers: {
193 |         'Content-Type': 'application/json',
194 |         Accept: 'application/json, text/event-stream',
195 |       },
196 |       body: JSON.stringify({
197 |         jsonrpc: '2.0',
198 |         id: 4,
199 |         method: 'tools/call',
200 |         params: {
201 |           name: 'vibe_learn',
202 |           arguments: { mistake: 'Test mistake', category: 'Feature Creep', solution: 'Fix it', type: 'mistake' },
203 |         },
204 |       }),
205 |     });
206 | 
207 |     expect(vibeSpy).toHaveBeenCalled();
208 |     expect(res.status).toBe(200);
209 |     const events = await readSSEBody(res);
210 |     const text = events.at(-1)?.result?.content?.[0]?.text ?? '';
211 |     expect(text).toContain('✅ Pattern logged successfully');
212 |     expect(text).toContain('Top Pattern Categories');
213 |     expect(text).toContain('Feature Creep (3 occurrences)');
214 |     expect(text).toContain('Most recent: "Overbuilt solution"');
215 |     expect(text).toContain('Solution: "Simplify approach"');
216 |   });
217 | 
218 |   it('indicates when a learning entry is already known', async () => {
219 |     vi.spyOn(vibeLearnModule, 'vibeLearnTool').mockResolvedValue({
220 |       added: false,
221 |       alreadyKnown: true,
222 |       currentTally: 5,
223 |       topCategories: [],
224 |     });
225 | 
226 |     serverInstance = await startHttpServer({ port: 0, attachSignalHandlers: false, logger: silentLogger });
227 |     const port = getPort(serverInstance);
228 | 
229 |     const res = await fetch(`http://127.0.0.1:${port}/mcp`, {
230 |       method: 'POST',
231 |       headers: {
232 |         'Content-Type': 'application/json',
233 |         Accept: 'application/json, text/event-stream',
234 |       },
235 |       body: JSON.stringify({
236 |         jsonrpc: '2.0',
237 |         id: 5,
238 |         method: 'tools/call',
239 |         params: {
240 |           name: 'vibe_learn',
241 |           arguments: { mistake: 'Repeated mistake', category: 'Feature Creep', solution: 'Fix it', type: 'mistake' },
242 |         },
243 |       }),
244 |     });
245 | 
246 |     expect(res.status).toBe(200);
247 |     const events = await readSSEBody(res);
248 |     const text = events.at(-1)?.result?.content?.[0]?.text ?? '';
249 |     expect(text).toContain('Pattern already recorded');
250 |   });
251 | 
252 |   it('reports when a learning entry cannot be logged', async () => {
253 |     vi.spyOn(vibeLearnModule, 'vibeLearnTool').mockResolvedValue({
254 |       added: false,
255 |       alreadyKnown: false,
256 |       currentTally: 0,
257 |       topCategories: [],
258 |     });
259 | 
260 |     serverInstance = await startHttpServer({ port: 0, attachSignalHandlers: false, logger: silentLogger });
261 |     const port = getPort(serverInstance);
262 | 
263 |     const res = await fetch(`http://127.0.0.1:${port}/mcp`, {
264 |       method: 'POST',
265 |       headers: {
266 |         'Content-Type': 'application/json',
267 |         Accept: 'application/json, text/event-stream',
268 |       },
269 |       body: JSON.stringify({
270 |         jsonrpc: '2.0',
271 |         id: 6,
272 |         method: 'tools/call',
273 |         params: {
274 |           name: 'vibe_learn',
275 |           arguments: { mistake: 'Unknown failure', category: 'Other', solution: 'n/a', type: 'mistake' },
276 |         },
277 |       }),
278 |     });
279 | 
280 |     expect(res.status).toBe(200);
281 |     const events = await readSSEBody(res);
282 |     const text = events.at(-1)?.result?.content?.[0]?.text ?? '';
283 |     expect(text).toContain('Failed to log pattern');
284 |   });
285 | 
286 |   it('attaches and removes signal handlers when enabled', async () => {
287 |     const initialSigint = process.listeners('SIGINT').length;
288 |     const initialSigterm = process.listeners('SIGTERM').length;
289 | 
290 |     const instance = await startHttpServer({ port: 0, attachSignalHandlers: true, logger: silentLogger });
291 | 
292 |     const duringSigint = process.listeners('SIGINT').length;
293 |     const duringSigterm = process.listeners('SIGTERM').length;
294 |     expect(duringSigint).toBeGreaterThanOrEqual(initialSigint + 1);
295 |     expect(duringSigterm).toBeGreaterThanOrEqual(initialSigterm + 1);
296 | 
297 |     await instance.close();
298 |     expect(process.listeners('SIGINT').length).toBe(initialSigint);
299 |     expect(process.listeners('SIGTERM').length).toBe(initialSigterm);
300 |   });
301 | });
302 | 
```

--------------------------------------------------------------------------------
/src/utils/llm.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { getLearningContextText } from './storage.js';
  2 | import { getConstitution } from '../tools/constitution.js';
  3 | import { resolveAnthropicConfig, buildAnthropicHeaders } from './anthropic.js';
  4 | 
  5 | // API Clients - Use 'any' to support dynamic import
  6 | let genAI: any = null;
  7 | let openaiClient: any = null;
  8 | 
  9 | // OpenRouter Constants
 10 | const openrouterBaseUrl = 'https://openrouter.ai/api/v1';
 11 | 
 12 | // Initialize all configured LLM clients
 13 | export async function initializeLLMs() {
 14 |   await ensureGemini();
 15 |   await ensureOpenAI();
 16 | }
 17 | 
 18 | async function ensureGemini() {
 19 |   if (!genAI && process.env.GEMINI_API_KEY) {
 20 |     const { GoogleGenerativeAI } = await import('@google/generative-ai');
 21 |     genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);
 22 |     console.log('Gemini API client initialized dynamically');
 23 |   }
 24 | }
 25 | 
 26 | async function ensureOpenAI() {
 27 |   if (!openaiClient && process.env.OPENAI_API_KEY) {
 28 |     const { OpenAI } = await import('openai');
 29 |     openaiClient = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
 30 |     console.log('OpenAI API client initialized dynamically');
 31 |   }
 32 | }
 33 | 
 34 | // Input/Output Interfaces
 35 | interface QuestionInput {
 36 |   goal: string;
 37 |   plan: string;
 38 |   modelOverride?: {
 39 |     provider?: string;
 40 |     model?: string;
 41 |   };
 42 |   userPrompt?: string;
 43 |   progress?: string;
 44 |   uncertainties?: string[];
 45 |   taskContext?: string;
 46 |   sessionId?: string;
 47 |   historySummary?: string;
 48 | }
 49 | 
 50 | interface QuestionOutput {
 51 |   questions: string;
 52 | }
 53 | 
 54 | // Main dispatcher function to generate responses from the selected LLM provider
 55 | export async function generateResponse(input: QuestionInput): Promise<QuestionOutput> {
 56 |   const provider = input.modelOverride?.provider || process.env.DEFAULT_LLM_PROVIDER || 'gemini';
 57 |   const model = input.modelOverride?.model || process.env.DEFAULT_MODEL;
 58 | 
 59 |   // The system prompt remains the same as it's core to the vibe-check philosophy
 60 |   const systemPrompt = `You are a meta-mentor. You're an experienced feedback provider that specializes in understanding intent, dysfunctional patterns in AI agents, and in responding in ways that further the goal. You need to carefully reason and process the information provided, to determine your output.\n\nYour tone needs to always be a mix of these traits based on the context of which pushes the message in the most appropriate affect: Gentle & Validating, Unafraid to push many questions but humble enough to step back, Sharp about problems and eager to help about problem-solving & giving tips and/or advice, stern and straightforward when spotting patterns & the agent being stuck in something that could derail things.\n\nHere's what you need to think about (Do not output the full thought process, only what is explicitly requested):\n1. What's going on here? What's the nature of the problem is the agent tackling? What's the approach, situation and goal? Is there any prior context that clarifies context further? \n2. What does the agent need to hear right now: Are there any clear patterns, loops, or unspoken assumptions being missed here? Or is the agent doing fine - in which case should I interrupt it or provide soft encouragement and a few questions? What is the best response I can give right now?\n3. In case the issue is technical - I need to provide guidance and help. In case I spot something that's clearly not accounted for/ assumed/ looping/ or otherwise could be out of alignment with the user or agent stated goals - I need to point out what I see gently and ask questions on if the agent agrees. If I don't see/ can't interpret an explicit issue - what intervention would provide valuable feedback here - questions, guidance, validation, or giving a soft go-ahead with reminders of best practices?\n4. In case the plan looks to be accurate - based on the context, can I remind the agent of how to continue, what not to forget, or should I soften and step back for the agent to continue its work? What's the most helpful thing I can do right now?`;
 61 | 
 62 |   let learningContext = '';
 63 |   if (process.env.USE_LEARNING_HISTORY === 'true') {
 64 |     learningContext = getLearningContextText();
 65 |   }
 66 | 
 67 |   const rules = input.sessionId ? getConstitution(input.sessionId) : [];
 68 |   const constitutionBlock = rules.length ? `\nConstitution:\n${rules.map(r => `- ${r}`).join('\n')}` : '';
 69 | 
 70 |   const contextSection = `CONTEXT:\nHistory Context: ${input.historySummary || 'None'}\n${learningContext ? `Learning Context:\n${learningContext}` : ''}\nGoal: ${input.goal}\nPlan: ${input.plan}\nProgress: ${input.progress || 'None'}\nUncertainties: ${input.uncertainties?.join(', ') || 'None'}\nTask Context: ${input.taskContext || 'None'}\nUser Prompt: ${input.userPrompt || 'None'}${constitutionBlock}`;
 71 |   const fullPrompt = `${systemPrompt}\n\n${contextSection}`;
 72 |   const compiledPrompt = contextSection;
 73 | 
 74 |   let responseText = '';
 75 | 
 76 |   if (provider === 'gemini') {
 77 |     await ensureGemini();
 78 |     if (!genAI) throw new Error('Gemini API key missing.');
 79 |     const geminiModel = model || 'gemini-2.5-pro';
 80 |     const fallbackModel = 'gemini-2.5-flash';
 81 |     try {
 82 |       console.log(`Attempting to use Gemini model: ${geminiModel}`);
 83 |       // console.error('Full Prompt:', fullPrompt); // Keep this commented out for now
 84 |       const modelInstance = genAI.getGenerativeModel({ model: geminiModel });
 85 |       const result = await modelInstance.generateContent(fullPrompt);
 86 |       responseText = result.response.text();
 87 |     } catch (error) {
 88 |       console.error(`Gemini model ${geminiModel} failed. Trying fallback ${fallbackModel}.`, error);
 89 |       // console.error('Full Prompt:', fullPrompt); // Keep this commented out for now
 90 |       const fallbackModelInstance = genAI.getGenerativeModel({ model: fallbackModel });
 91 |       const result = await fallbackModelInstance.generateContent(fullPrompt);
 92 |       responseText = result.response.text();
 93 |     }
 94 |   } else if (provider === 'openai') {
 95 |     await ensureOpenAI();
 96 |     if (!openaiClient) throw new Error('OpenAI API key missing.');
 97 |     const openaiModel = model || 'o4-mini';
 98 |     console.log(`Using OpenAI model: ${openaiModel}`);
 99 |     const response = await openaiClient.chat.completions.create({
100 |       model: openaiModel,
101 |       messages: [{ role: 'system', content: fullPrompt }],
102 |     });
103 |     responseText = response.choices[0].message.content || '';
104 |   } else if (provider === 'openrouter') {
105 |     if (!process.env.OPENROUTER_API_KEY) throw new Error('OpenRouter API key missing.');
106 |     if (!model) throw new Error('OpenRouter provider requires a model to be specified in the tool call.');
107 |     console.log(`Using OpenRouter model: ${model}`);
108 |     const { default: axios } = await import('axios');
109 |     const response = await axios.post(`${openrouterBaseUrl}/chat/completions`, {
110 |       model: model,
111 |       messages: [{ role: 'system', content: fullPrompt }],
112 |     }, { headers: { Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}`, 'HTTP-Referer': 'http://localhost', 'X-Title': 'Vibe Check MCP Server' } });
113 |     responseText = response.data.choices[0].message.content || '';
114 |   } else if (provider === 'anthropic') {
115 |     const anthropicModel = model || 'claude-3-5-sonnet-20241022';
116 |     responseText = await callAnthropic({
117 |       model: anthropicModel,
118 |       compiledPrompt,
119 |       systemPrompt,
120 |     });
121 |   } else {
122 |     throw new Error(`Invalid provider specified: ${provider}`);
123 |   }
124 | 
125 |   return {
126 |     questions: responseText,
127 |   };
128 | }
129 | 
130 | // The exported function is now a wrapper around the dispatcher
131 | export async function getMetacognitiveQuestions(input: QuestionInput): Promise<QuestionOutput> {
132 |   try {
133 |     return await generateResponse(input);
134 |   } catch (error) {
135 |     console.error('Error getting metacognitive questions:', error);
136 |     // Fallback questions
137 |     return {
138 |       questions: `\nI can see you're thinking through your approach, which shows thoughtfulness:\n\n1. Does this plan directly address what the user requested, or might it be solving a different problem?\n2. Is there a simpler approach that would meet the user's needs?\n3. What unstated assumptions might be limiting the thinking here?\n4. How does this align with the user's original intent?\n`,
139 |     };
140 |   }
141 | }
142 | 
143 | // Testing helpers
144 | export const __testing = {
145 |   setGenAI(client: any) { genAI = client; },
146 |   setOpenAIClient(client: any) { openaiClient = client; },
147 |   getGenAI() { return genAI; },
148 |   getOpenAIClient() { return openaiClient; }
149 | };
150 | 
151 | interface AnthropicCallOptions {
152 |   model: string;
153 |   compiledPrompt: string;
154 |   systemPrompt?: string;
155 |   maxTokens?: number;
156 |   temperature?: number;
157 | }
158 | 
159 | async function callAnthropic({
160 |   model,
161 |   compiledPrompt,
162 |   systemPrompt,
163 |   maxTokens = 1024,
164 |   temperature = 0.2,
165 | }: AnthropicCallOptions): Promise<string> {
166 |   if (!model) {
167 |     throw new Error('Anthropic provider requires a model to be specified in the tool call or DEFAULT_MODEL.');
168 |   }
169 | 
170 |   const { baseUrl, apiKey, authToken, version } = resolveAnthropicConfig();
171 |   const headers = buildAnthropicHeaders({ apiKey, authToken, version });
172 |   const url = `${baseUrl}/v1/messages`;
173 | 
174 |   const body: Record<string, unknown> = {
175 |     model,
176 |     max_tokens: maxTokens,
177 |     temperature,
178 |     messages: [
179 |       {
180 |         role: 'user',
181 |         content: compiledPrompt,
182 |       },
183 |     ],
184 |   };
185 | 
186 |   if (systemPrompt) {
187 |     body.system = systemPrompt;
188 |   }
189 | 
190 |   const response = await fetch(url, {
191 |     method: 'POST',
192 |     headers,
193 |     body: JSON.stringify(body),
194 |   });
195 | 
196 |   const rawText = await response.text();
197 |   let parsedBody: any;
198 |   if (rawText) {
199 |     try {
200 |       parsedBody = JSON.parse(rawText);
201 |     } catch {
202 |       parsedBody = undefined;
203 |     }
204 |   }
205 | 
206 |   if (!response.ok) {
207 |     const requestId = response.headers.get('anthropic-request-id') || response.headers.get('x-request-id');
208 |     const retryAfter = response.headers.get('retry-after');
209 |     const requestSuffix = requestId ? ` (request id: ${requestId})` : '';
210 |     const errorMessage =
211 |       typeof parsedBody?.error?.message === 'string'
212 |         ? parsedBody.error.message
213 |         : typeof parsedBody?.message === 'string'
214 |           ? parsedBody.message
215 |           : rawText?.trim();
216 | 
217 |     if (response.status === 401 || response.status === 403) {
218 |       throw new Error(
219 |         `Anthropic authentication failed with status ${response.status}${requestSuffix}. Verify ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN.`
220 |       );
221 |     }
222 | 
223 |     if (response.status === 429) {
224 |       const retryMessage = retryAfter ? ` Retry after ${retryAfter} seconds if provided.` : '';
225 |       throw new Error(`Anthropic rate limit exceeded (status 429)${requestSuffix}.${retryMessage}`);
226 |     }
227 | 
228 |     const detail = errorMessage ? ` ${errorMessage}` : '';
229 |     throw new Error(`Anthropic request failed with status ${response.status}${requestSuffix}.${detail}`.trim());
230 |   }
231 | 
232 |   const content = Array.isArray(parsedBody?.content) ? parsedBody.content : [];
233 |   const firstTextBlock = content.find((block: any) => block?.type === 'text' && typeof block?.text === 'string');
234 |   if (firstTextBlock) {
235 |     return firstTextBlock.text;
236 |   }
237 | 
238 |   const fallbackText = content[0]?.text;
239 |   return typeof fallbackText === 'string' ? fallbackText : '';
240 | }
```

--------------------------------------------------------------------------------
/tests/jsonrpc-compat.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
  2 | 
  3 | const ORIGINAL_GEMINI_KEY = process.env.GEMINI_API_KEY;
  4 | 
  5 | function clearGeminiKey() {
  6 |   delete process.env.GEMINI_API_KEY;
  7 | }
  8 | 
  9 | function restoreGeminiKey() {
 10 |   if (ORIGINAL_GEMINI_KEY === undefined) {
 11 |     delete process.env.GEMINI_API_KEY;
 12 |   } else {
 13 |     process.env.GEMINI_API_KEY = ORIGINAL_GEMINI_KEY;
 14 |   }
 15 | }
 16 | 
 17 | class MockTransport {
 18 |   onmessage?: (message: any, extra?: any) => void;
 19 |   onclose?: () => void;
 20 |   onerror?: (error: unknown) => void;
 21 |   sent: any[] = [];
 22 | 
 23 |   start = vi.fn(async () => {});
 24 |   send = vi.fn(async (message: any) => {
 25 |     this.sent.push(message);
 26 |   });
 27 | }
 28 | 
 29 | const compatIdPattern = /^compat-[0-9a-f]{12}-[0-9a-z]{4,6}$/;
 30 | 
 31 | async function readSSEBody(res: Response) {
 32 |   const text = await res.text();
 33 |   const dataLines = text
 34 |     .split('\n')
 35 |     .map((line) => line.trim())
 36 |     .filter((line) => line.startsWith('data: '));
 37 |   return dataLines.map((line) => JSON.parse(line.slice(6)));
 38 | }
 39 | 
 40 | async function stubState() {
 41 |   const stateModule = await import('../src/utils/state.js');
 42 |   vi.spyOn(stateModule, 'loadHistory').mockResolvedValue();
 43 |   vi.spyOn(stateModule, 'addToHistory').mockImplementation(() => {});
 44 | }
 45 | 
 46 | function buildToolsCall(overrides: Record<string, unknown> = {}) {
 47 |   return {
 48 |     jsonrpc: '2.0',
 49 |     method: 'tools/call',
 50 |     params: {
 51 |       name: 'vibe_check',
 52 |       arguments: {
 53 |         goal: 'Ship safely',
 54 |         plan: '1) tests 2) deploy',
 55 |         ...overrides,
 56 |       },
 57 |     },
 58 |   };
 59 | }
 60 | 
 61 | describe('JSON-RPC compatibility shim', () => {
 62 |   beforeEach(() => {
 63 |     vi.resetModules();
 64 |     delete process.env.MCP_TRANSPORT;
 65 |     delete process.env.MCP_DISCOVERY_MODE;
 66 |     clearGeminiKey();
 67 |   });
 68 | 
 69 |   afterEach(() => {
 70 |     vi.restoreAllMocks();
 71 |     restoreGeminiKey();
 72 |   });
 73 | 
 74 |   it('synthesizes ids for stdio tools/call requests', async () => {
 75 |     await stubState();
 76 | 
 77 |     const { createMcpServer } = await import('../src/index.js');
 78 |     const { wrapTransportForCompatibility } = await import('../src/utils/jsonRpcCompat.js');
 79 | 
 80 |     const server = await createMcpServer();
 81 |     const transport = wrapTransportForCompatibility(new MockTransport());
 82 | 
 83 |     await server.connect(transport as any);
 84 | 
 85 |     transport.onmessage?.(buildToolsCall());
 86 | 
 87 |     await vi.waitFor(() => {
 88 |       expect(transport.send).toHaveBeenCalled();
 89 |     });
 90 | 
 91 |     const response = transport.sent.at(-1);
 92 |     expect(response).toMatchObject({
 93 |       jsonrpc: '2.0',
 94 |       id: expect.stringMatching(compatIdPattern),
 95 |     });
 96 |     expect(response?.result).toBeDefined();
 97 |   });
 98 | 
 99 |   it('wraps handlers assigned after compatibility wrapping', async () => {
100 |     const { wrapTransportForCompatibility } = await import('../src/utils/jsonRpcCompat.js');
101 |     const transport = wrapTransportForCompatibility(new MockTransport());
102 | 
103 |     const handler = vi.fn();
104 |     transport.onmessage = handler;
105 | 
106 |     const payload = { jsonrpc: '2.0', method: 'tools/call', params: { name: 'noop' } };
107 |     transport.onmessage?.(payload);
108 | 
109 |     expect(handler).toHaveBeenCalledTimes(1);
110 |     const [{ id }] = handler.mock.calls[0];
111 |     expect(id).toMatch(compatIdPattern);
112 |   });
113 | 
114 |   it('generates unique ids for identical stdio requests', async () => {
115 |     await stubState();
116 | 
117 |     const { createMcpServer } = await import('../src/index.js');
118 |     const { wrapTransportForCompatibility } = await import('../src/utils/jsonRpcCompat.js');
119 | 
120 |     const server = await createMcpServer();
121 |     const transport = wrapTransportForCompatibility(new MockTransport());
122 | 
123 |     await server.connect(transport as any);
124 | 
125 |     transport.onmessage?.(buildToolsCall());
126 |     transport.onmessage?.(buildToolsCall());
127 | 
128 |     await vi.waitFor(() => {
129 |       expect(transport.send).toHaveBeenCalledTimes(2);
130 |     });
131 | 
132 |     const [first, second] = transport.sent.slice(-2);
133 |     expect(first.id).toMatch(compatIdPattern);
134 |     expect(second.id).toMatch(compatIdPattern);
135 |     expect(first.id).not.toBe(second.id);
136 |     expect(first.result).toBeDefined();
137 |     expect(second.result).toBeDefined();
138 |   });
139 | 
140 |   it('returns InvalidParams errors when required fields are missing', async () => {
141 |     await stubState();
142 | 
143 |     const { createMcpServer } = await import('../src/index.js');
144 |     const { wrapTransportForCompatibility } = await import('../src/utils/jsonRpcCompat.js');
145 | 
146 |     const server = await createMcpServer();
147 |     const transport = wrapTransportForCompatibility(new MockTransport());
148 | 
149 |     await server.connect(transport as any);
150 | 
151 |     const invalidPayload = buildToolsCall({ plan: undefined });
152 |     delete (invalidPayload.params as any).arguments.plan;
153 | 
154 |     transport.onmessage?.(invalidPayload);
155 | 
156 |     await vi.waitFor(() => {
157 |       expect(transport.send).toHaveBeenCalled();
158 |     });
159 | 
160 |     const response = transport.sent.at(-1);
161 |     expect(response).toMatchObject({
162 |       jsonrpc: '2.0',
163 |       error: { code: -32602 },
164 |       id: expect.stringMatching(compatIdPattern),
165 |     });
166 |     expect(response?.error?.message).toContain('Missing: plan');
167 |   });
168 | 
169 |   it('returns InvalidParams errors in discovery mode', async () => {
170 |     process.env.MCP_DISCOVERY_MODE = '1';
171 |     await stubState();
172 | 
173 |     const { createMcpServer } = await import('../src/index.js');
174 |     const { wrapTransportForCompatibility } = await import('../src/utils/jsonRpcCompat.js');
175 | 
176 |     const server = await createMcpServer();
177 |     const transport = wrapTransportForCompatibility(new MockTransport());
178 | 
179 |     await server.connect(transport as any);
180 | 
181 |     const invalidPayload = buildToolsCall({ plan: undefined });
182 |     delete (invalidPayload.params as any).arguments.plan;
183 | 
184 |     transport.onmessage?.(invalidPayload);
185 | 
186 |     await vi.waitFor(() => {
187 |       expect(transport.send).toHaveBeenCalled();
188 |     });
189 | 
190 |     const response = transport.sent.at(-1);
191 |     expect(response).toMatchObject({
192 |       jsonrpc: '2.0',
193 |       error: { code: -32602 },
194 |       id: expect.stringMatching(compatIdPattern),
195 |     });
196 |     expect(response?.error?.message).toContain('discovery: missing [plan]');
197 |   });
198 | 
199 |   it('handles large payloads without truncation', async () => {
200 |     await stubState();
201 | 
202 |     const { createMcpServer } = await import('../src/index.js');
203 |     const { wrapTransportForCompatibility } = await import('../src/utils/jsonRpcCompat.js');
204 | 
205 |     const server = await createMcpServer();
206 |     const transport = wrapTransportForCompatibility(new MockTransport());
207 | 
208 |     await server.connect(transport as any);
209 | 
210 |     const largePlan = 'A'.repeat(256 * 1024);
211 |     transport.onmessage?.(
212 |       buildToolsCall({ plan: largePlan })
213 |     );
214 | 
215 |     await vi.waitFor(() => {
216 |       expect(transport.send).toHaveBeenCalled();
217 |     });
218 | 
219 |     const response = transport.sent.at(-1);
220 |     expect(response?.result).toBeDefined();
221 |     expect(response?.id).toMatch(compatIdPattern);
222 |   });
223 | 
224 |   it('emits HTTP JSON responses with synthesized ids when Accept is application/json', async () => {
225 |     await stubState();
226 | 
227 |     const { startHttpServer } = await import('../src/index.js');
228 | 
229 |     const silentLogger = { log: vi.fn(), error: vi.fn() };
230 |     const serverInstance = await startHttpServer({ port: 0, attachSignalHandlers: false, logger: silentLogger });
231 | 
232 |     try {
233 |       const address = serverInstance.listener.address();
234 |       const port = typeof address === 'object' && address ? address.port : 0;
235 | 
236 |       const res = await fetch(`http://127.0.0.1:${port}/mcp`, {
237 |         method: 'POST',
238 |         headers: {
239 |           'Content-Type': 'application/json',
240 |           Accept: 'application/json',
241 |         },
242 |         body: JSON.stringify(buildToolsCall()),
243 |       });
244 | 
245 |       expect(res.status).toBe(200);
246 |       expect(res.headers.get('content-type')).toContain('application/json');
247 |       const payload = await res.json();
248 |       expect(payload).toMatchObject({
249 |         jsonrpc: '2.0',
250 |         id: expect.stringMatching(compatIdPattern),
251 |       });
252 |       expect(payload?.result).toBeDefined();
253 |     } finally {
254 |       await serverInstance.close();
255 |     }
256 |   });
257 | 
258 |   it('emits SSE responses with synthesized ids when Accept is text/event-stream', async () => {
259 |     await stubState();
260 | 
261 |     const { startHttpServer } = await import('../src/index.js');
262 | 
263 |     const silentLogger = { log: vi.fn(), error: vi.fn() };
264 |     const serverInstance = await startHttpServer({ port: 0, attachSignalHandlers: false, logger: silentLogger });
265 | 
266 |     try {
267 |       const address = serverInstance.listener.address();
268 |       const port = typeof address === 'object' && address ? address.port : 0;
269 | 
270 |       const res = await fetch(`http://127.0.0.1:${port}/mcp`, {
271 |         method: 'POST',
272 |         headers: {
273 |           'Content-Type': 'application/json',
274 |           Accept: 'text/event-stream',
275 |         },
276 |         body: JSON.stringify(buildToolsCall()),
277 |       });
278 | 
279 |       expect(res.status).toBe(200);
280 |       expect(res.headers.get('content-type')).toContain('text/event-stream');
281 |       const events = await readSSEBody(res);
282 |       const message = events.at(-1);
283 |       expect(message).toMatchObject({
284 |         jsonrpc: '2.0',
285 |         id: expect.stringMatching(compatIdPattern),
286 |       });
287 |       expect(message?.result).toBeDefined();
288 |     } finally {
289 |       await serverInstance.close();
290 |     }
291 |   });
292 | 
293 |   it('keeps JSON fallback request-scoped under concurrent traffic', async () => {
294 |     await stubState();
295 | 
296 |     const { startHttpServer } = await import('../src/index.js');
297 | 
298 |     const silentLogger = { log: vi.fn(), error: vi.fn() };
299 |     const serverInstance = await startHttpServer({ port: 0, attachSignalHandlers: false, logger: silentLogger });
300 | 
301 |     try {
302 |       const address = serverInstance.listener.address();
303 |       const port = typeof address === 'object' && address ? address.port : 0;
304 | 
305 |       for (let i = 0; i < 2; i++) {
306 |         const jsonPromise = fetch(`http://127.0.0.1:${port}/mcp`, {
307 |           method: 'POST',
308 |           headers: {
309 |             'Content-Type': 'application/json',
310 |             Accept: 'application/json',
311 |           },
312 |           body: JSON.stringify(buildToolsCall({ goal: `Ship safely ${i}` })),
313 |         }).then(async (res) => {
314 |           expect(res.status).toBe(200);
315 |           expect(res.headers.get('content-type')).toContain('application/json');
316 |           const payload = await res.json();
317 |           expect(payload).toMatchObject({
318 |             jsonrpc: '2.0',
319 |             id: expect.stringMatching(compatIdPattern),
320 |           });
321 |           return payload;
322 |         });
323 | 
324 |         const ssePromise = fetch(`http://127.0.0.1:${port}/mcp`, {
325 |           method: 'POST',
326 |           headers: {
327 |             'Content-Type': 'application/json',
328 |             Accept: 'text/event-stream',
329 |           },
330 |           body: JSON.stringify(buildToolsCall({ goal: `Stream safely ${i}` })),
331 |         }).then(async (res) => {
332 |           expect(res.status).toBe(200);
333 |           expect(res.headers.get('content-type')).toContain('text/event-stream');
334 |           const events = await readSSEBody(res);
335 |           const message = events.at(-1);
336 |           expect(message).toMatchObject({
337 |             jsonrpc: '2.0',
338 |             id: expect.stringMatching(compatIdPattern),
339 |           });
340 |           return events;
341 |         });
342 | 
343 |         const [jsonPayload, sseEvents] = await Promise.all([jsonPromise, ssePromise]);
344 |         expect(jsonPayload?.result).toBeDefined();
345 |         expect(sseEvents.at(-1)?.result).toBeDefined();
346 |       }
347 |     } finally {
348 |       await serverInstance.close();
349 |     }
350 |   });
351 | 
352 |   it('prefers streaming when both application/json and text/event-stream are accepted', async () => {
353 |     await stubState();
354 | 
355 |     const { startHttpServer } = await import('../src/index.js');
356 | 
357 |     const silentLogger = { log: vi.fn(), error: vi.fn() };
358 |     const serverInstance = await startHttpServer({ port: 0, attachSignalHandlers: false, logger: silentLogger });
359 | 
360 |     try {
361 |       const address = serverInstance.listener.address();
362 |       const port = typeof address === 'object' && address ? address.port : 0;
363 | 
364 |       const res = await fetch(`http://127.0.0.1:${port}/mcp`, {
365 |         method: 'POST',
366 |         headers: {
367 |           'Content-Type': 'application/json',
368 |           Accept: 'application/json, text/event-stream',
369 |         },
370 |         body: JSON.stringify(buildToolsCall()),
371 |       });
372 | 
373 |       expect(res.status).toBe(200);
374 |       expect(res.headers.get('content-type')).toContain('text/event-stream');
375 |       const events = await readSSEBody(res);
376 |       const message = events.at(-1);
377 |       expect(message).toMatchObject({
378 |         jsonrpc: '2.0',
379 |         id: expect.stringMatching(compatIdPattern),
380 |       });
381 |     } finally {
382 |       await serverInstance.close();
383 |     }
384 |   });
385 | 
386 |   it('defaults to streaming when no Accept header is provided', async () => {
387 |     await stubState();
388 | 
389 |     const { startHttpServer } = await import('../src/index.js');
390 | 
391 |     const silentLogger = { log: vi.fn(), error: vi.fn() };
392 |     const serverInstance = await startHttpServer({ port: 0, attachSignalHandlers: false, logger: silentLogger });
393 | 
394 |     try {
395 |       const address = serverInstance.listener.address();
396 |       const port = typeof address === 'object' && address ? address.port : 0;
397 | 
398 |       const res = await fetch(`http://127.0.0.1:${port}/mcp`, {
399 |         method: 'POST',
400 |         headers: {
401 |           'Content-Type': 'application/json',
402 |         },
403 |         body: JSON.stringify(buildToolsCall()),
404 |       });
405 | 
406 |       expect(res.status).toBe(200);
407 |       expect(res.headers.get('content-type')).toContain('text/event-stream');
408 |       const events = await readSSEBody(res);
409 |       expect(events.at(-1)).toMatchObject({
410 |         jsonrpc: '2.0',
411 |         id: expect.stringMatching(compatIdPattern),
412 |       });
413 |     } finally {
414 |       await serverInstance.close();
415 |     }
416 |   });
417 | 
418 |   it('does not leave json fallback enabled on the transport after JSON responses', async () => {
419 |     await stubState();
420 | 
421 |     const { startHttpServer } = await import('../src/index.js');
422 | 
423 |     const silentLogger = { log: vi.fn(), error: vi.fn() };
424 |     const serverInstance = await startHttpServer({ port: 0, attachSignalHandlers: false, logger: silentLogger });
425 | 
426 |     try {
427 |       const address = serverInstance.listener.address();
428 |       const port = typeof address === 'object' && address ? address.port : 0;
429 | 
430 |       const res = await fetch(`http://127.0.0.1:${port}/mcp`, {
431 |         method: 'POST',
432 |         headers: {
433 |           'Content-Type': 'application/json',
434 |           Accept: 'application/json',
435 |         },
436 |         body: JSON.stringify(buildToolsCall()),
437 |       });
438 | 
439 |       expect(res.status).toBe(200);
440 |       await res.json();
441 | 
442 |       expect((serverInstance.transport as any)._enableJsonResponse).toBe(false);
443 |     } finally {
444 |       await serverInstance.close();
445 |     }
446 |   });
447 | 
448 |   it('routes logs to stderr when MCP_TRANSPORT=stdio', async () => {
449 |     process.env.MCP_TRANSPORT = 'stdio';
450 |     const originalLog = console.log;
451 |     const originalError = console.error;
452 | 
453 |     const stdoutSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
454 |     let errorSpy: ReturnType<typeof vi.spyOn> | null = null;
455 | 
456 |     try {
457 |       await import('../src/index.js');
458 |       errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
459 |       console.log('shim-log');
460 |       expect(errorSpy).toHaveBeenCalledWith('shim-log');
461 |       expect(stdoutSpy).not.toHaveBeenCalled();
462 |     } finally {
463 |       errorSpy?.mockRestore();
464 |       console.log = originalLog;
465 |       console.error = originalError;
466 |     }
467 |   });
468 | });
469 | 
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env node
  2 | 
  3 | import dotenv from 'dotenv';
  4 | dotenv.config();
  5 | 
  6 | import express from 'express';
  7 | import cors from 'cors';
  8 | import { AsyncLocalStorage } from 'node:async_hooks';
  9 | import { Server } from '@modelcontextprotocol/sdk/server/index.js';
 10 | import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
 11 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
 12 | import { McpError, ErrorCode, ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
 13 | import type { Server as HttpServer } from 'http';
 14 | import type { AddressInfo } from 'net';
 15 | import { fileURLToPath } from 'url';
 16 | 
 17 | import { vibeCheckTool, VibeCheckInput, VibeCheckOutput } from './tools/vibeCheck.js';
 18 | import { vibeLearnTool, VibeLearnInput, VibeLearnOutput } from './tools/vibeLearn.js';
 19 | import { updateConstitution, resetConstitution, getConstitution } from './tools/constitution.js';
 20 | import { STANDARD_CATEGORIES, LearningType } from './utils/storage.js';
 21 | import { loadHistory } from './utils/state.js';
 22 | import { getPackageVersion } from './utils/version.js';
 23 | import { applyJsonRpcCompatibility, wrapTransportForCompatibility } from './utils/jsonRpcCompat.js';
 24 | import { createRequestScopedTransport, RequestScopeStore } from './utils/httpTransportWrapper.js';
 25 | 
 26 | const IS_DISCOVERY = process.env.MCP_DISCOVERY_MODE === '1';
 27 | const USE_STDIO = process.env.MCP_TRANSPORT === 'stdio';
 28 | 
 29 | if (USE_STDIO) {
 30 |   console.log = (...args) => console.error(...args);
 31 | }
 32 | 
 33 | const SCRIPT_PATH = fileURLToPath(import.meta.url);
 34 | 
 35 | export const SUPPORTED_LLM_PROVIDERS = ['gemini', 'openai', 'openrouter', 'anthropic'] as const;
 36 | 
 37 | export interface LoggerLike {
 38 |   log: (...args: any[]) => void;
 39 |   error: (...args: any[]) => void;
 40 | }
 41 | 
 42 | export interface HttpServerOptions {
 43 |   port?: number;
 44 |   corsOrigin?: string;
 45 |   transport?: StreamableHTTPServerTransport;
 46 |   server?: Server;
 47 |   attachSignalHandlers?: boolean;
 48 |   signals?: NodeJS.Signals[];
 49 |   logger?: LoggerLike;
 50 | }
 51 | 
 52 | export interface HttpServerInstance {
 53 |   app: express.Express;
 54 |   listener: HttpServer;
 55 |   transport: StreamableHTTPServerTransport;
 56 |   close: () => Promise<void>;
 57 | }
 58 | 
 59 | export interface MainOptions {
 60 |   createServer?: () => Promise<Server>;
 61 |   startHttp?: (options: HttpServerOptions) => Promise<HttpServerInstance>;
 62 | }
 63 | 
 64 | export async function createMcpServer(): Promise<Server> {
 65 |   await loadHistory();
 66 | 
 67 |   const server = new Server(
 68 |     { name: 'vibe-check', version: getPackageVersion() },
 69 |     { capabilities: { tools: {}, sampling: {} } }
 70 |   );
 71 | 
 72 |   server.setRequestHandler(ListToolsRequestSchema, async () => ({
 73 |     tools: [
 74 |       {
 75 |         name: 'vibe_check',
 76 |         description: 'Metacognitive questioning tool that identifies assumptions and breaks tunnel vision to prevent cascading errors',
 77 |         inputSchema: {
 78 |           type: 'object',
 79 |           properties: {
 80 |             goal: {
 81 |               type: 'string',
 82 |               description: "The agent's current goal",
 83 |               examples: ['Ship CPI v2.5 with zero regressions']
 84 |             },
 85 |             plan: {
 86 |               type: 'string',
 87 |               description: "The agent's detailed plan",
 88 |               examples: ['1) Write tests 2) Refactor 3) Canary rollout']
 89 |             },
 90 |             modelOverride: {
 91 |               type: 'object',
 92 |               properties: {
 93 |                 provider: { type: 'string', enum: [...SUPPORTED_LLM_PROVIDERS] },
 94 |                 model: { type: 'string' }
 95 |               },
 96 |               required: [],
 97 |               examples: [{ provider: 'gemini', model: 'gemini-2.5-pro' }]
 98 |             },
 99 |             userPrompt: {
100 |               type: 'string',
101 |               description: 'The original user prompt',
102 |               examples: ['Summarize the repo']
103 |             },
104 |             progress: {
105 |               type: 'string',
106 |               description: "The agent's progress so far",
107 |               examples: ['Finished step 1']
108 |             },
109 |             uncertainties: {
110 |               type: 'array',
111 |               items: { type: 'string' },
112 |               description: "The agent's uncertainties",
113 |               examples: [['uncertain about deployment']]
114 |             },
115 |             taskContext: {
116 |               type: 'string',
117 |               description: 'The context of the current task',
118 |               examples: ['repo: vibe-check-mcp @2.5.0']
119 |             },
120 |             sessionId: {
121 |               type: 'string',
122 |               description: 'Optional session ID for state management',
123 |               examples: ['session-123']
124 |             }
125 |           },
126 |           required: ['goal', 'plan'],
127 |           additionalProperties: false
128 |         }
129 |       },
130 |       {
131 |         name: 'vibe_learn',
132 |         description: 'Pattern recognition system that tracks common errors and solutions to prevent recurring issues',
133 |         inputSchema: {
134 |           type: 'object',
135 |           properties: {
136 |             mistake: {
137 |               type: 'string',
138 |               description: 'One-sentence description of the learning entry',
139 |               examples: ['Skipped writing tests']
140 |             },
141 |             category: {
142 |               type: 'string',
143 |               description: `Category (standard categories: ${STANDARD_CATEGORIES.join(', ')})`,
144 |               enum: STANDARD_CATEGORIES,
145 |               examples: ['Premature Implementation']
146 |             },
147 |             solution: {
148 |               type: 'string',
149 |               description: 'How it was corrected (if applicable)',
150 |               examples: ['Added regression tests']
151 |             },
152 |             type: {
153 |               type: 'string',
154 |               enum: ['mistake', 'preference', 'success'],
155 |               description: 'Type of learning entry',
156 |               examples: ['mistake']
157 |             },
158 |             sessionId: {
159 |               type: 'string',
160 |               description: 'Optional session ID for state management',
161 |               examples: ['session-123']
162 |             }
163 |           },
164 |           required: ['mistake', 'category'],
165 |           additionalProperties: false
166 |         }
167 |       },
168 |       {
169 |         name: 'update_constitution',
170 |         description: 'Append a constitutional rule for this session (in-memory)',
171 |         inputSchema: {
172 |           type: 'object',
173 |           properties: {
174 |             sessionId: { type: 'string', examples: ['session-123'] },
175 |             rule: { type: 'string', examples: ['Always write tests first'] }
176 |           },
177 |           required: ['sessionId', 'rule'],
178 |           additionalProperties: false
179 |         }
180 |       },
181 |       {
182 |         name: 'reset_constitution',
183 |         description: 'Overwrite all constitutional rules for this session',
184 |         inputSchema: {
185 |           type: 'object',
186 |           properties: {
187 |             sessionId: { type: 'string', examples: ['session-123'] },
188 |             rules: {
189 |               type: 'array',
190 |               items: { type: 'string' },
191 |               examples: [['Be kind', 'Avoid loops']]
192 |             }
193 |           },
194 |           required: ['sessionId', 'rules'],
195 |           additionalProperties: false
196 |         }
197 |       },
198 |       {
199 |         name: 'check_constitution',
200 |         description: 'Return the current constitution rules for this session',
201 |         inputSchema: {
202 |           type: 'object',
203 |           properties: {
204 |             sessionId: { type: 'string', examples: ['session-123'] }
205 |           },
206 |           required: ['sessionId'],
207 |           additionalProperties: false
208 |         }
209 |       }
210 |     ]
211 |   }));
212 | 
213 |   server.setRequestHandler(CallToolRequestSchema, async (req) => {
214 |     const { name, arguments: raw } = req.params;
215 |     const args: any = raw;
216 | 
217 |     switch (name) {
218 |       case 'vibe_check': {
219 |         const missing: string[] = [];
220 |         if (!args || typeof args.goal !== 'string') missing.push('goal');
221 |         if (!args || typeof args.plan !== 'string') missing.push('plan');
222 |         if (missing.length) {
223 |           const example = '{"goal":"Ship CPI v2.5","plan":"1) tests 2) refactor 3) canary"}';
224 |           const message = IS_DISCOVERY
225 |             ? `discovery: missing [${missing.join(', ')}]; example: ${example}`
226 |             : `Missing: ${missing.join(', ')}. Example: ${example}`;
227 |           throw new McpError(ErrorCode.InvalidParams, message);
228 |         }
229 |         const input: VibeCheckInput = {
230 |           goal: args.goal,
231 |           plan: args.plan,
232 |           modelOverride: typeof args.modelOverride === 'object' && args.modelOverride !== null ? args.modelOverride : undefined,
233 |           userPrompt: typeof args.userPrompt === 'string' ? args.userPrompt : undefined,
234 |           progress: typeof args.progress === 'string' ? args.progress : undefined,
235 |           uncertainties: Array.isArray(args.uncertainties) ? args.uncertainties : undefined,
236 |           taskContext: typeof args.taskContext === 'string' ? args.taskContext : undefined,
237 |           sessionId: typeof args.sessionId === 'string' ? args.sessionId : undefined,
238 |         };
239 |         const result = await vibeCheckTool(input);
240 |         return { content: [{ type: 'text', text: formatVibeCheckOutput(result) }] };
241 |       }
242 | 
243 |       case 'vibe_learn': {
244 |         const missing: string[] = [];
245 |         if (!args || typeof args.mistake !== 'string') missing.push('mistake');
246 |         if (!args || typeof args.category !== 'string') missing.push('category');
247 |         if (missing.length) {
248 |           const example = '{"mistake":"Skipped tests","category":"Feature Creep"}';
249 |           const message = IS_DISCOVERY
250 |             ? `discovery: missing [${missing.join(', ')}]; example: ${example}`
251 |             : `Missing: ${missing.join(', ')}. Example: ${example}`;
252 |           throw new McpError(ErrorCode.InvalidParams, message);
253 |         }
254 |         const input: VibeLearnInput = {
255 |           mistake: args.mistake,
256 |           category: args.category,
257 |           solution: typeof args.solution === 'string' ? args.solution : undefined,
258 |           type: ['mistake', 'preference', 'success'].includes(args.type as string)
259 |             ? (args.type as LearningType)
260 |             : undefined,
261 |           sessionId: typeof args.sessionId === 'string' ? args.sessionId : undefined
262 |         };
263 |         const result = await vibeLearnTool(input);
264 |         return { content: [{ type: 'text', text: formatVibeLearnOutput(result) }] };
265 |       }
266 | 
267 |       case 'update_constitution': {
268 |         const missing: string[] = [];
269 |         if (!args || typeof args.sessionId !== 'string') missing.push('sessionId');
270 |         if (!args || typeof args.rule !== 'string') missing.push('rule');
271 |         if (missing.length) {
272 |           const example = '{"sessionId":"123","rule":"Always write tests first"}';
273 |           const message = IS_DISCOVERY
274 |             ? `discovery: missing [${missing.join(', ')}]; example: ${example}`
275 |             : `Missing: ${missing.join(', ')}. Example: ${example}`;
276 |           throw new McpError(ErrorCode.InvalidParams, message);
277 |         }
278 |         updateConstitution(args.sessionId, args.rule);
279 |         console.log('[Constitution:update]', { sessionId: args.sessionId, count: getConstitution(args.sessionId).length });
280 |         return { content: [{ type: 'text', text: '✅ Constitution updated' }] };
281 |       }
282 | 
283 |       case 'reset_constitution': {
284 |         const missing: string[] = [];
285 |         if (!args || typeof args.sessionId !== 'string') missing.push('sessionId');
286 |         if (!args || !Array.isArray(args.rules)) missing.push('rules');
287 |         if (missing.length) {
288 |           const example = '{"sessionId":"123","rules":["Be kind","Avoid loops"]}';
289 |           const message = IS_DISCOVERY
290 |             ? `discovery: missing [${missing.join(', ')}]; example: ${example}`
291 |             : `Missing: ${missing.join(', ')}. Example: ${example}`;
292 |           throw new McpError(ErrorCode.InvalidParams, message);
293 |         }
294 |         resetConstitution(args.sessionId, args.rules);
295 |         console.log('[Constitution:reset]', { sessionId: args.sessionId, count: getConstitution(args.sessionId).length });
296 |         return { content: [{ type: 'text', text: '✅ Constitution reset' }] };
297 |       }
298 | 
299 |       case 'check_constitution': {
300 |         const missing: string[] = [];
301 |         if (!args || typeof args.sessionId !== 'string') missing.push('sessionId');
302 |         if (missing.length) {
303 |           const example = '{"sessionId":"123"}';
304 |           const message = IS_DISCOVERY
305 |             ? `discovery: missing [${missing.join(', ')}]; example: ${example}`
306 |             : `Missing: ${missing.join(', ')}. Example: ${example}`;
307 |           throw new McpError(ErrorCode.InvalidParams, message);
308 |         }
309 |         const rules = getConstitution(args.sessionId);
310 |         console.log('[Constitution:check]', { sessionId: args.sessionId, count: rules.length });
311 |         return { content: [{ type: 'json', json: { rules } }] };
312 |       }
313 | 
314 |       default:
315 |         throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
316 |     }
317 |   });
318 | 
319 |   return server;
320 | }
321 | 
322 | export async function startHttpServer(options: HttpServerOptions = {}): Promise<HttpServerInstance> {
323 |   const logger = options.logger ?? console;
324 |   const allowedOrigin = options.corsOrigin ?? process.env.CORS_ORIGIN ?? '*';
325 |   const PORT = options.port ?? Number(process.env.MCP_HTTP_PORT || process.env.PORT || 3000);
326 |   const server = options.server ?? (await createMcpServer());
327 |   const requestScope = new AsyncLocalStorage<RequestScopeStore>();
328 |   const baseTransport = options.transport ?? new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
329 |   const transport = createRequestScopedTransport(baseTransport, requestScope);
330 | 
331 |   await server.connect(transport);
332 | 
333 |   const app = express();
334 |   app.use(cors({ origin: allowedOrigin }));
335 |   app.use(express.json());
336 | 
337 |   app.post('/mcp', async (req, res) => {
338 |     const started = Date.now();
339 |     const originalAcceptHeader = req.headers.accept;
340 |     const rawAcceptValues = Array.isArray(originalAcceptHeader)
341 |       ? originalAcceptHeader
342 |       : [originalAcceptHeader ?? ''];
343 |     const originalTokens: string[] = [];
344 |     for (const rawValue of rawAcceptValues) {
345 |       if (typeof rawValue !== 'string') continue;
346 |       for (const token of rawValue.split(',')) {
347 |         const trimmed = token.trim();
348 |         if (trimmed) {
349 |           originalTokens.push(trimmed);
350 |         }
351 |       }
352 |     }
353 |     const lowerTokens = originalTokens.map((value) => value.toLowerCase());
354 |     const acceptsJson = lowerTokens.some((value) => value.includes('application/json'));
355 |     const acceptsSse = lowerTokens.some((value) => value.includes('text/event-stream'));
356 |     const normalizedTokens = new Set(originalTokens);
357 |     if (!acceptsJson) {
358 |       normalizedTokens.add('application/json');
359 |     }
360 |     if (!acceptsSse) {
361 |       normalizedTokens.add('text/event-stream');
362 |     }
363 |     if (normalizedTokens.size === 0) {
364 |       normalizedTokens.add('application/json');
365 |       normalizedTokens.add('text/event-stream');
366 |     }
367 |     req.headers.accept = Array.from(normalizedTokens).join(', ');
368 | 
369 |     const forceJsonResponse = acceptsJson && !acceptsSse;
370 | 
371 |     const { applied, id: syntheticId } = applyJsonRpcCompatibility(req.body);
372 |     const { id, method } = req.body ?? {};
373 |     const sessionId = req.body?.params?.sessionId || req.body?.params?.arguments?.sessionId;
374 |     logger.log('[MCP] request', { id, method, sessionId, syntheticId: applied ? syntheticId : undefined });
375 |     try {
376 |       await requestScope.run({ forceJson: forceJsonResponse }, async () => {
377 |         await transport.handleRequest(req, res, req.body);
378 |       });
379 |     } catch (e: any) {
380 |       logger.error('[MCP] error', { err: e?.message, id });
381 |       if (!res.headersSent) {
382 |         res.status(500).json({ jsonrpc: '2.0', id: id ?? null, error: { code: -32603, message: 'Internal server error' } });
383 |       }
384 |     } finally {
385 |       if (originalAcceptHeader === undefined) {
386 |         delete req.headers.accept;
387 |       } else {
388 |         req.headers.accept = originalAcceptHeader;
389 |       }
390 |       logger.log('[MCP] handled', { id, ms: Date.now() - started });
391 |     }
392 |   });
393 | 
394 |   app.get('/mcp', (_req, res) => {
395 |     res.status(405).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Method not allowed' }, id: null });
396 |   });
397 | 
398 |   app.get('/healthz', (_req, res) => {
399 |     res.status(200).json({ status: 'ok' });
400 |   });
401 | 
402 |   const listener = app.listen(PORT, () => {
403 |     const addr = listener.address() as AddressInfo | string | null;
404 |     const actualPort = typeof addr === 'object' && addr ? addr.port : PORT;
405 |     logger.log(`[MCP] HTTP listening on :${actualPort}`);
406 |   });
407 | 
408 |   const signals = options.signals ?? ['SIGTERM', 'SIGINT'];
409 |   const attachSignals = options.attachSignalHandlers ?? false;
410 |   let signalHandler: (() => void) | null = null;
411 | 
412 |   const close = () =>
413 |     new Promise<void>((resolve) => {
414 |       listener.close(() => {
415 |         if (attachSignals) {
416 |           for (const signal of signals) {
417 |             if (signalHandler) {
418 |               process.off(signal, signalHandler);
419 |             }
420 |           }
421 |         }
422 |         resolve();
423 |       });
424 |     });
425 | 
426 |   if (attachSignals) {
427 |     signalHandler = () => {
428 |       close().then(() => process.exit(0));
429 |     };
430 |     for (const signal of signals) {
431 |       process.on(signal, signalHandler);
432 |     }
433 |   }
434 | 
435 |   return { app, listener, transport, close };
436 | }
437 | 
438 | export async function main(options: MainOptions = {}) {
439 |   const createServerFn = options.createServer ?? createMcpServer;
440 |   const startHttpFn = options.startHttp ?? startHttpServer;
441 |   const server = await createServerFn();
442 | 
443 |   if (USE_STDIO) {
444 |     const transport = wrapTransportForCompatibility(new StdioServerTransport());
445 |     await server.connect(transport);
446 |     console.error('[MCP] stdio transport connected');
447 |   } else {
448 |     await startHttpFn({ server, attachSignalHandlers: true, logger: console });
449 |   }
450 | }
451 | 
452 | function formatVibeCheckOutput(result: VibeCheckOutput): string {
453 |   return result.questions;
454 | }
455 | 
456 | function formatVibeLearnOutput(result: VibeLearnOutput): string {
457 |   let output = '';
458 | 
459 |   if (result.added) {
460 |     output += `✅ Pattern logged successfully (category tally: ${result.currentTally})`;
461 |   } else if (result.alreadyKnown) {
462 |     output += 'ℹ️ Pattern already recorded';
463 |   } else {
464 |     output += '❌ Failed to log pattern';
465 |   }
466 | 
467 |   if (result.topCategories && result.topCategories.length > 0) {
468 |     output += '\n\n## Top Pattern Categories\n';
469 |     for (const category of result.topCategories) {
470 |       output += `\n### ${category.category} (${category.count} occurrences)\n`;
471 |       if (category.recentExample) {
472 |         output += `Most recent: "${category.recentExample.mistake}"\n`;
473 |         output += `Solution: "${category.recentExample.solution}"\n`;
474 |       }
475 |     }
476 |   }
477 | 
478 |   return output;
479 | }
480 | 
481 | if (process.argv[1] === SCRIPT_PATH) {
482 |   main().catch((error) => {
483 |     console.error('Server startup error:', error);
484 |     process.exit(1);
485 |   });
486 | }
487 | 
```

--------------------------------------------------------------------------------
/src/cli/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env node
  2 | import { readFileSync, realpathSync, promises as fsPromises } from 'node:fs';
  3 | import { dirname, resolve } from 'node:path';
  4 | import { fileURLToPath, pathToFileURL } from 'node:url';
  5 | import { Command, Option } from 'commander';
  6 | import { execa } from 'execa';
  7 | import { checkNodeVersion, detectEnvFiles, portStatus, readEnvFile } from './doctor.js';
  8 | import { ensureEnv, resolveEnvSources } from './env.js';
  9 | import { formatUnifiedDiff } from './diff.js';
 10 | import claudeAdapter from './clients/claude.js';
 11 | import claudeCodeAdapter from './clients/claude-code.js';
 12 | import cursorAdapter from './clients/cursor.js';
 13 | import windsurfAdapter from './clients/windsurf.js';
 14 | import vscodeAdapter from './clients/vscode.js';
 15 | import {
 16 |   ClientAdapter,
 17 |   ClientDescription,
 18 |   JsonRecord,
 19 |   MergeOpts,
 20 |   TransportKind,
 21 |   isRecord,
 22 | } from './clients/shared.js';
 23 | 
 24 | type PackageJson = {
 25 |   version?: string;
 26 |   engines?: {
 27 |     node?: string;
 28 |   };
 29 | };
 30 | 
 31 | type Transport = TransportKind;
 32 | 
 33 | type StartOptions = {
 34 |   stdio?: boolean;
 35 |   http?: boolean;
 36 |   port?: number;
 37 |   dryRun?: boolean;
 38 | };
 39 | 
 40 | type DoctorOptions = {
 41 |   http?: boolean;
 42 |   port?: number;
 43 | };
 44 | 
 45 | type InstallOptions = {
 46 |   client: string;
 47 |   dryRun?: boolean;
 48 |   nonInteractive?: boolean;
 49 |   local?: boolean;
 50 |   config?: string;
 51 |   http?: boolean;
 52 |   stdio?: boolean;
 53 |   port?: number;
 54 |   devWatch?: boolean;
 55 |   devDebug?: string;
 56 | };
 57 | 
 58 | const cliDir = dirname(fileURLToPath(import.meta.url));
 59 | const projectRoot = resolve(cliDir, '..', '..');
 60 | const entrypoint = resolve(projectRoot, 'build', 'index.js');
 61 | const packageJsonPath = resolve(projectRoot, 'package.json');
 62 | 
 63 | const MANAGED_ID = 'vibe-check-mcp';
 64 | const SENTINEL = 'vibe-check-mcp-cli';
 65 | 
 66 | const CLIENT_ADAPTERS: Record<string, ClientAdapter> = {
 67 |   claude: claudeAdapter,
 68 |   'claude-code': claudeCodeAdapter,
 69 |   cursor: cursorAdapter,
 70 |   windsurf: windsurfAdapter,
 71 |   vscode: vscodeAdapter,
 72 | };
 73 | 
 74 | type RegisteredClient = {
 75 |   key: string;
 76 |   adapter: ClientAdapter;
 77 |   description: ClientDescription;
 78 | };
 79 | 
 80 | function collectRegisteredClients(): RegisteredClient[] {
 81 |   return Object.entries(CLIENT_ADAPTERS)
 82 |     .map(([key, adapter]) => ({ key, adapter, description: adapter.describe() }))
 83 |     .sort((a, b) => a.key.localeCompare(b.key));
 84 | }
 85 | 
 86 | function formatTransportSummary(description: ClientDescription): string {
 87 |   const transports = description.transports && description.transports.length > 0 ? description.transports : ['stdio'];
 88 |   const defaultTransport = description.defaultTransport ?? transports[0];
 89 |   return transports
 90 |     .map((transport) => (transport === defaultTransport ? `${transport} (default)` : transport))
 91 |     .join(', ');
 92 | }
 93 | 
 94 | function formatInstallHint(key: string, description: ClientDescription): string {
 95 |   const transports = description.transports && description.transports.length > 0 ? description.transports : ['stdio'];
 96 |   const defaultTransport = description.defaultTransport ?? transports[0];
 97 |   const base = `npx @pv-bhat/vibe-check-mcp install --client ${key}`;
 98 | 
 99 |   if (!defaultTransport) {
100 |     return base;
101 |   }
102 | 
103 |   const extras = transports.filter((value) => value !== defaultTransport);
104 |   const hint = `${base} --${defaultTransport}`;
105 | 
106 |   if (extras.length === 0) {
107 |     return hint;
108 |   }
109 | 
110 |   const extraFlags = extras.map((value) => `--${value}`).join(', ');
111 |   return `${hint} (alternatives: ${extraFlags})`;
112 | }
113 | 
114 | export function showAvailableClients(): void {
115 |   const clients = collectRegisteredClients();
116 |   console.log('Available MCP clients:\n');
117 | 
118 |   for (const { key, description } of clients) {
119 |     console.log(`- ${key} (${description.name})`);
120 |     if (description.summary) {
121 |       console.log(`  Summary: ${description.summary}`);
122 |     }
123 |     console.log(`  Config: ${description.pathHint}`);
124 |     if (description.requiredEnvKeys?.length) {
125 |       console.log(`  API keys: ${description.requiredEnvKeys.join(', ')}`);
126 |     }
127 |     console.log(`  Transports: ${formatTransportSummary(description)}`);
128 |     console.log(`  Install: ${formatInstallHint(key, description)}`);
129 |     if (description.notes) {
130 |       console.log(`  Notes: ${description.notes}`);
131 |     }
132 |     if (description.docsUrl) {
133 |       console.log(`  Docs: ${description.docsUrl}`);
134 |     }
135 |     console.log('');
136 |   }
137 | 
138 |   console.log('Template: npx @pv-bhat/vibe-check-mcp install --client <client> [--stdio|--http] [options]');
139 |   console.log('Hosted: smithery add @PV-Bhat/vibe-check-mcp-server');
140 |   console.log("Run 'npx @pv-bhat/vibe-check-mcp --help' for detailed usage.");
141 | }
142 | 
143 | function readPackageJson(): PackageJson {
144 |   const raw = readFileSync(packageJsonPath, 'utf8');
145 |   return JSON.parse(raw) as PackageJson;
146 | }
147 | 
148 | function parsePort(value: string): number {
149 |   const parsed = Number.parseInt(value, 10);
150 |   if (Number.isNaN(parsed) || parsed <= 0) {
151 |     throw new Error(`Invalid port: ${value}`);
152 |   }
153 | 
154 |   return parsed;
155 | }
156 | 
157 | function mergeEnvFromFile(env: NodeJS.ProcessEnv, path: string | null): void {
158 |   if (!path) {
159 |     return;
160 |   }
161 | 
162 |   try {
163 |     const parsed = readEnvFile(path);
164 |     for (const [key, value] of Object.entries(parsed)) {
165 |       if (!(key in env)) {
166 |         env[key] = value;
167 |       }
168 |     }
169 |   } catch (error) {
170 |     console.warn(`Failed to read env file at ${path}: ${(error as Error).message}`);
171 |   }
172 | }
173 | 
174 | async function runStartCommand(options: StartOptions): Promise<void> {
175 |   const envSources = resolveEnvSources();
176 |   const spawnEnv: NodeJS.ProcessEnv = { ...envSources.processEnv };
177 | 
178 |   mergeEnvFromFile(spawnEnv, envSources.homeEnv);
179 |   mergeEnvFromFile(spawnEnv, envSources.cwdEnv);
180 | 
181 |   if (options.http && options.stdio) {
182 |     throw new Error('Select either --stdio or --http, not both.');
183 |   }
184 | 
185 |   const transport = resolveTransport({ http: options.http, stdio: options.stdio }, spawnEnv.MCP_TRANSPORT);
186 |   spawnEnv.MCP_TRANSPORT = transport;
187 | 
188 |   if (transport === 'http') {
189 |     const httpPort = resolveHttpPort(options.port, spawnEnv.MCP_HTTP_PORT);
190 |     spawnEnv.MCP_HTTP_PORT = String(httpPort);
191 |   } else {
192 |     if (options.port != null) {
193 |       throw new Error('The --port option is only available when using --http.');
194 |     }
195 |   }
196 | 
197 |   if (options.dryRun) {
198 |     console.log('vibe-check-mcp start (dry run)');
199 |     console.log(`Entrypoint: ${process.execPath} ${entrypoint}`);
200 |     console.log('Environment overrides:');
201 |     console.log(`  MCP_TRANSPORT=${spawnEnv.MCP_TRANSPORT}`);
202 |     if (transport === 'http' && spawnEnv.MCP_HTTP_PORT) {
203 |       console.log(`  MCP_HTTP_PORT=${spawnEnv.MCP_HTTP_PORT}`);
204 |     }
205 |     return;
206 |   }
207 | 
208 |   if (transport === 'stdio') {
209 |     // For stdio, we must run the server in the same process as the CLI
210 |     // to allow the client to communicate with it directly.
211 |     Object.assign(process.env, spawnEnv);
212 |     const { main } = await import('../index.js');
213 |     await main();
214 |   } else {
215 |     // For HTTP, spawning a child process is acceptable.
216 |     await execa(process.execPath, [entrypoint], {
217 |       stdio: 'inherit',
218 |       env: spawnEnv,
219 |     });
220 |   }
221 | }
222 | 
223 | async function runDoctorCommand(options: DoctorOptions): Promise<void> {
224 |   const pkg = readPackageJson();
225 |   const requiredNodeRange = pkg.engines?.node ?? '>=20.0.0';
226 |   const nodeCheck = checkNodeVersion(requiredNodeRange);
227 |   if (nodeCheck.ok) {
228 |     console.log(`Node.js version: ${nodeCheck.current} (meets ${requiredNodeRange})`);
229 |   } else {
230 |     console.warn(`Node.js version: ${nodeCheck.current} (requires ${requiredNodeRange})`);
231 |     process.exitCode = 1;
232 |   }
233 | 
234 |   const envFiles = detectEnvFiles();
235 |   console.log(`Project .env: ${envFiles.cwdEnv ?? 'not found'}`);
236 |   console.log(`Home .env: ${envFiles.homeEnv ?? 'not found'}`);
237 | 
238 |   const transport = resolveTransport({ http: options.http }, process.env.MCP_TRANSPORT);
239 | 
240 |   if (transport !== 'http') {
241 |     console.log('Using stdio transport; port checks skipped.');
242 |     return;
243 |   }
244 | 
245 |   const port = resolveHttpPort(options.port, process.env.MCP_HTTP_PORT);
246 |   const status = await portStatus(port);
247 |   console.log(`HTTP port ${port}: ${status}`);
248 | }
249 | 
250 | async function runInstallCommand(options: InstallOptions): Promise<void> {
251 |   const clientKey = options.client?.toLowerCase();
252 |   const adapter = clientKey ? CLIENT_ADAPTERS[clientKey] : undefined;
253 |   if (!adapter) {
254 |     throw new Error(`Unsupported client: ${options.client}`);
255 |   }
256 | 
257 |   const interactive = !options.nonInteractive;
258 |   const description = adapter.describe();
259 |   const envResult = await ensureEnv({
260 |     interactive,
261 |     local: Boolean(options.local),
262 |     requiredKeys: description.requiredEnvKeys,
263 |   });
264 | 
265 |   if (envResult.missing?.length) {
266 |     return;
267 |   }
268 | 
269 |   if (envResult.wrote && envResult.path) {
270 |     console.log(`Secrets written to ${envResult.path}`);
271 |   }
272 | 
273 |   const transport = resolveTransport({ http: options.http, stdio: options.stdio }, process.env.MCP_TRANSPORT);
274 | 
275 |   let httpPort: number | undefined;
276 |   let httpUrl: string | undefined;
277 | 
278 |   if (transport === 'http') {
279 |     httpPort = resolveHttpPort(options.port, process.env.MCP_HTTP_PORT);
280 |     httpUrl = `http://127.0.0.1:${httpPort}`;
281 |   } else if (options.port != null) {
282 |     throw new Error('The --port option is only available when using --http.');
283 |   }
284 | 
285 |   const entry = createInstallEntry(transport, httpPort);
286 | 
287 |   const mergeOptions: MergeOpts = {
288 |     id: MANAGED_ID,
289 |     sentinel: SENTINEL,
290 |     transport,
291 |     httpUrl,
292 |   };
293 | 
294 |   if (options.devWatch || options.devDebug) {
295 |     mergeOptions.dev = {};
296 |     if (options.devWatch) {
297 |       mergeOptions.dev.watch = true;
298 |     }
299 |     if (options.devDebug) {
300 |       mergeOptions.dev.debug = options.devDebug;
301 |     }
302 |   }
303 | 
304 |   const configPath = await adapter.locate(options.config);
305 | 
306 |   if (!configPath) {
307 |     emitManualInstallMessage({
308 |       adapter,
309 |       clientKey,
310 |       description,
311 |       entry,
312 |       mergeOptions,
313 |       transport,
314 |       httpUrl,
315 |     });
316 |     return;
317 |   }
318 | 
319 |   const configExists = await fileExists(configPath);
320 |   let existingRaw = '';
321 |   let currentConfig: JsonRecord = {};
322 | 
323 |   if (configExists) {
324 |     existingRaw = await fsPromises.readFile(configPath, 'utf8');
325 |     currentConfig = await adapter.read(configPath, existingRaw);
326 |   }
327 | 
328 |   const { next, changed, reason } = adapter.merge(currentConfig, entry, mergeOptions);
329 | 
330 |   if (!changed) {
331 |     if (reason) {
332 |       console.warn(reason);
333 |     } else {
334 |       console.log(`${description.name} already has a managed entry for ${MANAGED_ID}.`);
335 |     }
336 |     return;
337 |   }
338 | 
339 |   const nextRaw = `${JSON.stringify(next, null, 2)}\n`;
340 | 
341 |   if (options.dryRun) {
342 |     const diff = formatUnifiedDiff(existingRaw, nextRaw, configPath);
343 |     console.log(diff.trim() ? diff : 'No changes.');
344 |     return;
345 |   }
346 | 
347 |   if (existingRaw) {
348 |     const backupPath = await createBackup(configPath, existingRaw);
349 |     console.log(`Backup created at ${backupPath}`);
350 |   }
351 | 
352 |   await adapter.writeAtomic(configPath, next);
353 | 
354 |   const summaryEntry = extractManagedEntry(next, MANAGED_ID);
355 |   console.log(`${description.name} config updated (${transport}): ${configPath}`);
356 |   if (summaryEntry) {
357 |     console.log(JSON.stringify(summaryEntry, null, 2));
358 |   }
359 |   console.log('Restart the client to pick up the new MCP server.');
360 |   if (transport === 'http' && httpPort) {
361 |     const startCommand = formatStartCommand(entry);
362 |     console.log(`Start the server separately with: ${startCommand}`);
363 |     console.log(`HTTP endpoint: ${httpUrl}`);
364 |   }
365 | }
366 | 
367 | function createInstallEntry(transport: Transport, port?: number): JsonRecord {
368 |   const args: string[] = ['-y', '@pv-bhat/vibe-check-mcp', 'start'];
369 | 
370 |   if (transport === 'http') {
371 |     args.push('--http');
372 |     const resolvedPort = port ?? 2091;
373 |     args.push('--port', String(resolvedPort));
374 |   } else {
375 |     args.push('--stdio');
376 |   }
377 | 
378 |   return {
379 |     command: 'npx',
380 |     args,
381 |     env: {},
382 |   } satisfies JsonRecord;
383 | }
384 | 
385 | function formatStartCommand(entry: JsonRecord): string {
386 |   const command = typeof entry.command === 'string' ? entry.command : 'npx';
387 |   const args = Array.isArray(entry.args) ? entry.args.map((value) => String(value)) : [];
388 |   return [command, ...args].join(' ');
389 | }
390 | 
391 | function extractManagedEntry(config: JsonRecord, id: string): JsonRecord | null {
392 |   const mapCandidates: Array<JsonRecord | undefined> = [];
393 | 
394 |   if (isRecord(config.mcpServers)) {
395 |     mapCandidates.push(config.mcpServers as JsonRecord);
396 |   }
397 | 
398 |   if (isRecord(config.servers)) {
399 |     mapCandidates.push(config.servers as JsonRecord);
400 |   }
401 | 
402 |   for (const map of mapCandidates) {
403 |     if (!map) {
404 |       continue;
405 |     }
406 |     const entry = map[id];
407 |     if (isRecord(entry)) {
408 |       return entry as JsonRecord;
409 |     }
410 |   }
411 | 
412 |   return null;
413 | }
414 | 
415 | type ManualInstallArgs = {
416 |   adapter: ClientAdapter;
417 |   clientKey: string;
418 |   description: ReturnType<ClientAdapter['describe']>;
419 |   entry: JsonRecord;
420 |   mergeOptions: MergeOpts;
421 |   transport: Transport;
422 |   httpUrl?: string;
423 | };
424 | 
425 | function emitManualInstallMessage(args: ManualInstallArgs): void {
426 |   const { adapter, clientKey, description, entry, mergeOptions, transport, httpUrl } = args;
427 | 
428 |   console.log(`${description.name} configuration not found at ${description.pathHint}.`);
429 |   if (description.notes) {
430 |     console.log(description.notes);
431 |   }
432 | 
433 |   const preview = adapter.merge({}, entry, mergeOptions);
434 |   const managedEntry = extractManagedEntry(preview.next, MANAGED_ID) ?? preview.next;
435 | 
436 |   console.log('Add this MCP server configuration manually:');
437 |   console.log(JSON.stringify(managedEntry, null, 2));
438 | 
439 |   if (clientKey === 'vscode') {
440 |     const installUrl = createVsCodeInstallUrl(entry, mergeOptions);
441 |     console.log('VS Code quick install link:');
442 |     console.log(installUrl);
443 |     console.log('Command Palette → "MCP: Add Server" will open the profile file.');
444 |   } else if (clientKey === 'cursor') {
445 |     console.log('Cursor → Settings → MCP Servers lets you paste this JSON.');
446 |   } else if (clientKey === 'windsurf') {
447 |     console.log('Create the file if it does not exist, then restart Windsurf.');
448 |   }
449 | 
450 |   if (transport === 'http' && httpUrl) {
451 |     const startCommand = formatStartCommand(entry);
452 |     console.log(`Expose the HTTP server separately with: ${startCommand}`);
453 |     console.log(`HTTP endpoint: ${httpUrl}`);
454 |   }
455 | }
456 | 
457 | function createVsCodeInstallUrl(entry: JsonRecord, options: MergeOpts): string {
458 |   const url = new URL('vscode:mcp/install');
459 |   url.searchParams.set('name', 'Vibe Check MCP');
460 | 
461 |   const command = typeof entry.command === 'string' ? entry.command : 'npx';
462 |   url.searchParams.set('command', command);
463 | 
464 |   const args = Array.isArray(entry.args) ? entry.args.map((value) => String(value)) : [];
465 |   if (args.length > 0) {
466 |     url.searchParams.set('args', JSON.stringify(args));
467 |   }
468 | 
469 |   if (options.transport === 'http' && options.httpUrl) {
470 |     url.searchParams.set('url', options.httpUrl);
471 |   } else {
472 |     url.searchParams.set('transport', options.transport);
473 |   }
474 | 
475 |   return url.toString();
476 | }
477 | 
478 | export function createCliProgram(): Command {
479 |   const pkg = readPackageJson();
480 |   const program = new Command();
481 | 
482 |   program
483 |     .name('vibe-check-mcp')
484 |     .description('CLI utilities for the Vibe Check MCP server')
485 |     .version(pkg.version ?? '0.0.0');
486 | 
487 |   program
488 |     .option('--list-clients', 'List supported MCP client integrations')
489 |     .addHelpText('afterAll', () => {
490 |       const clients = collectRegisteredClients();
491 |       if (clients.length === 0) {
492 |         return '';
493 |       }
494 | 
495 |       const longestKey = Math.max(...clients.map((client) => client.key.length));
496 |       const lines = clients
497 |         .map((client) => `  ${client.key.padEnd(longestKey)}  ${client.description.name}`)
498 |         .join('\n');
499 | 
500 |       return `\nSupported clients:\n${lines}\n\nRun 'npx @pv-bhat/vibe-check-mcp --list-clients' for details.\n`;
501 |     });
502 | 
503 |   program
504 |     .command('start')
505 |     .description('Start the Vibe Check MCP server')
506 |     .addOption(new Option('--stdio', 'Use STDIO transport').conflicts('http'))
507 |     .addOption(new Option('--http', 'Use HTTP transport').conflicts('stdio'))
508 |     .option('--port <number>', 'HTTP port (default: 2091)', parsePort)
509 |     .option('--dry-run', 'Print the resolved command without executing')
510 |     .action(async (options: StartOptions) => {
511 |       try {
512 |         await runStartCommand(options);
513 |       } catch (error) {
514 |         console.error((error as Error).message);
515 |         process.exitCode = 1;
516 |       }
517 |     });
518 | 
519 |   program
520 |     .command('doctor')
521 |     .description('Diagnose environment issues')
522 |     .option('--http', 'Check HTTP transport readiness')
523 |     .option('--port <number>', 'HTTP port to inspect', parsePort)
524 |     .action(async (options: DoctorOptions) => {
525 |       try {
526 |         await runDoctorCommand(options);
527 |       } catch (error) {
528 |         console.error((error as Error).message);
529 |         process.exitCode = 1;
530 |       }
531 |     });
532 | 
533 |   program
534 |     .command('install')
535 |     .description('Install client integrations')
536 |     .requiredOption('--client <name>', 'Client to configure')
537 |     .option('--config <path>', 'Path to the client configuration file')
538 |     .option('--dry-run', 'Show the merged configuration without writing')
539 |     .option('--non-interactive', 'Do not prompt for missing environment values')
540 |     .option('--local', 'Write secrets to the project .env instead of ~/.vibe-check/.env')
541 |     .addOption(new Option('--stdio', 'Configure STDIO transport').conflicts('http'))
542 |     .addOption(new Option('--http', 'Configure HTTP transport').conflicts('stdio'))
543 |     .option('--port <number>', 'HTTP port (default: 2091)', parsePort)
544 |     .option('--dev-watch', 'Add dev.watch=true (VS Code only)')
545 |     .option('--dev-debug <value>', 'Set dev.debug (VS Code only)')
546 |     .action(async (options: InstallOptions) => {
547 |       try {
548 |         await runInstallCommand(options);
549 |       } catch (error) {
550 |         console.error((error as Error).message);
551 |         process.exitCode = 1;
552 |       }
553 |     });
554 | 
555 |   program.action(() => {
556 |     const options = program.opts<{ listClients?: boolean }>();
557 | 
558 |     if (options.listClients) {
559 |       showAvailableClients();
560 |       return;
561 |     }
562 | 
563 |     program.help();
564 |   });
565 | 
566 |   return program;
567 | }
568 | 
569 | function normalizeTransport(value: string | undefined): Transport | undefined {
570 |   if (!value) {
571 |     return undefined;
572 |   }
573 | 
574 |   const normalized = value.trim().toLowerCase();
575 |   if (normalized === 'http' || normalized === 'stdio') {
576 |     return normalized;
577 |   }
578 | 
579 |   return undefined;
580 | }
581 | 
582 | function resolveTransport(
583 |   options: { http?: boolean; stdio?: boolean },
584 |   envTransport: string | undefined,
585 | ): Transport {
586 |   const flagTransport = options.http ? 'http' : options.stdio ? 'stdio' : undefined;
587 |   const resolvedEnv = normalizeTransport(envTransport);
588 | 
589 |   return flagTransport ?? resolvedEnv ?? 'stdio';
590 | }
591 | 
592 | function resolveHttpPort(optionPort: number | undefined, envPort: string | undefined): number {
593 |   if (optionPort != null) {
594 |     return optionPort;
595 |   }
596 | 
597 |   if (envPort) {
598 |     const parsed = Number.parseInt(envPort, 10);
599 |     if (!Number.isNaN(parsed) && parsed > 0) {
600 |       return parsed;
601 |     }
602 |   }
603 | 
604 |   return 2091;
605 | }
606 | 
607 | async function fileExists(path: string): Promise<boolean> {
608 |   try {
609 |     await fsPromises.access(path);
610 |     return true;
611 |   } catch {
612 |     return false;
613 |   }
614 | }
615 | 
616 | function formatTimestamp(date: Date): string {
617 |   const iso = date.toISOString();
618 |   return iso.replace(/[:.]/g, '-');
619 | }
620 | 
621 | async function createBackup(path: string, contents: string): Promise<string> {
622 |   const backupPath = `${path}.${formatTimestamp(new Date())}.bak`;
623 |   await fsPromises.writeFile(backupPath, contents, { mode: 0o600 });
624 |   return backupPath;
625 | }
626 | 
627 | const executedArg = process.argv[1];
628 | if (executedArg) {
629 |   let executedFileUrl: string;
630 | 
631 |   try {
632 |     const resolved = realpathSync(executedArg);
633 |     executedFileUrl = pathToFileURL(resolved).href;
634 |   } catch (error) {
635 |     console.warn(`Failed to resolve CLI entrypoint: ${(error as Error).message}`);
636 |     executedFileUrl = pathToFileURL(executedArg).href;
637 |   }
638 | 
639 |   if (executedFileUrl === import.meta.url) {
640 |     createCliProgram()
641 |       .parseAsync(process.argv)
642 |       .catch((error: unknown) => {
643 |         console.error((error as Error).message);
644 |         process.exitCode = 1;
645 |       });
646 |   }
647 | }
648 | 
```
Page 2/2FirstPrevNextLast