UNPKG

vibe-coder-mcp

Version:

Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.

409 lines (404 loc) 16.9 kB
#!/usr/bin/env node import inquirer from 'inquirer'; import fs from 'fs-extra'; import path from 'path'; import chalk from 'chalk'; import boxen from 'boxen'; import ora from 'ora'; import { fileURLToPath } from 'url'; import logger from './logger.js'; import { OpenRouterConfigManager } from './utils/openrouter-config-manager.js'; import { UserConfigManager } from './utils/user-config-manager.js'; import { ConfigValidator } from './utils/config-validator.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const projectRoot = path.resolve(__dirname, '..'); const ASCII_ART = ` ██╗ ██╗██╗██████╗ ███████╗ ██║ ██║██║██╔══██╗██╔════╝ ██║ ██║██║██████╔╝█████╗ ╚██╗ ██╔╝██║██╔══██╗██╔══╝ ╚████╔╝ ██║██████╔╝███████╗ ╚═══╝ ╚═╝╚═════╝ ╚══════╝ Coder MCP v0.3.0 `; const WELCOME_MESSAGE = ` Welcome to Vibe Coder MCP! 🎆 This setup wizard will help you configure: • OpenRouter API for AI-powered development • Security boundaries for file access • Output directories for generated content Let's get started! This will only take a minute. `; export class SetupWizard { envPath; configPath; userConfigManager; configValidator; isInteractive; constructor() { this.envPath = path.join(projectRoot, '.env'); this.configPath = path.join(projectRoot, '.vibe-config.json'); this.userConfigManager = UserConfigManager.getInstance(); this.configValidator = ConfigValidator.getInstance(); this.isInteractive = process.stdin.isTTY && !process.env.CI; } async isFirstRun() { const checks = [ !process.env.OPENROUTER_API_KEY, !await fs.pathExists(this.envPath), !await fs.pathExists(path.join(projectRoot, 'llm_config.json')), !await fs.pathExists(this.userConfigManager.getUserConfigDir()) ]; const isFirstRun = checks.some(check => check); if (isFirstRun) { logger.info({ checks: { hasApiKey: !checks[0], hasEnvFile: !checks[1], hasLlmConfig: !checks[2], hasUserConfig: !checks[3] } }, 'First run detected'); } return isFirstRun; } async isConfigValid() { try { const configManager = OpenRouterConfigManager.getInstance(); await configManager.initialize(); const validation = configManager.validateConfiguration(); return validation.valid; } catch (error) { logger.debug({ err: error }, 'Configuration validation failed'); return false; } } displayWelcome() { console.clear(); console.log(chalk.cyan(ASCII_ART)); console.log(WELCOME_MESSAGE); } validateApiKey(apiKey) { if (!apiKey || apiKey.trim() === '') { return 'API key is required'; } if (!apiKey.startsWith('sk-or-')) { return 'Invalid API key format (should start with sk-or-)'; } return true; } async testApiKeyLive(apiKey) { try { const response = await fetch('https://openrouter.ai/api/v1/models', { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } }); return response.ok; } catch (error) { logger.error({ err: error }, 'API key validation failed'); return false; } } async promptConfiguration() { const questions = [ { type: 'input', name: 'OPENROUTER_API_KEY', message: '🔑 Enter your OpenRouter API key:', validate: this.validateApiKey, transformer: (input) => { if (input.length > 8) { return `${input.substring(0, 4)}${'*'.repeat(input.length - 8)}${input.substring(input.length - 4)}`; } return input; } }, { type: 'confirm', name: 'configureDirs', message: '📁 Would you like to configure custom directories?', default: false }, { type: 'input', name: 'VIBE_CODER_OUTPUT_DIR', message: '📂 Output directory for generated files:', default: './VibeCoderOutput', when: (answers) => Boolean(answers.configureDirs) }, { type: 'input', name: 'CODE_MAP_ALLOWED_DIR', message: '🗺️ Directory for code analysis (code mapping):', default: '.', when: (answers) => Boolean(answers.configureDirs) }, { type: 'input', name: 'VIBE_TASK_MANAGER_READ_DIR', message: '📋 Directory for task manager operations:', default: '.', when: (answers) => Boolean(answers.configureDirs) }, { type: 'list', name: 'VIBE_TASK_MANAGER_SECURITY_MODE', message: '🔒 Security mode for file operations:', choices: [ { name: 'Strict (recommended) - Enhanced security validation', value: 'strict' }, { name: 'Permissive - Relaxed validation for development', value: 'permissive' } ], default: 'strict', when: (answers) => Boolean(answers.configureDirs) }, { type: 'confirm', name: 'configureAdvanced', message: '⚙️ Configure advanced settings?', default: false }, { type: 'input', name: 'OPENROUTER_BASE_URL', message: '🌐 OpenRouter API base URL:', default: 'https://openrouter.ai/api/v1', when: (answers) => Boolean(answers.configureAdvanced) }, { type: 'input', name: 'GEMINI_MODEL', message: '🤖 Default Gemini model:', default: 'google/gemini-2.5-flash-preview-05-20', when: (answers) => Boolean(answers.configureAdvanced) }, { type: 'input', name: 'PERPLEXITY_MODEL', message: '🔍 Default Perplexity model:', default: 'perplexity/sonar', when: (answers) => Boolean(answers.configureAdvanced) } ]; const rawAnswers = await inquirer.prompt(questions); const config = { OPENROUTER_API_KEY: rawAnswers.OPENROUTER_API_KEY, VIBE_CODER_OUTPUT_DIR: rawAnswers.VIBE_CODER_OUTPUT_DIR || './VibeCoderOutput', CODE_MAP_ALLOWED_DIR: rawAnswers.CODE_MAP_ALLOWED_DIR || '.', VIBE_TASK_MANAGER_READ_DIR: rawAnswers.VIBE_TASK_MANAGER_READ_DIR || '.', VIBE_TASK_MANAGER_SECURITY_MODE: rawAnswers.VIBE_TASK_MANAGER_SECURITY_MODE || 'strict', OPENROUTER_BASE_URL: rawAnswers.OPENROUTER_BASE_URL || 'https://openrouter.ai/api/v1', GEMINI_MODEL: rawAnswers.GEMINI_MODEL || 'google/gemini-2.5-flash-preview-05-20', PERPLEXITY_MODEL: rawAnswers.PERPLEXITY_MODEL || 'perplexity/sonar' }; return config; } async createEnvFile(config) { let envContent = '# Vibe Coder MCP Configuration\n'; envContent += '# Generated by setup wizard\n\n'; envContent += '# Required: Your OpenRouter API key\n'; envContent += `OPENROUTER_API_KEY="${config.OPENROUTER_API_KEY}"\n\n`; envContent += '# Directory Configuration\n'; envContent += `VIBE_CODER_OUTPUT_DIR="${config.VIBE_CODER_OUTPUT_DIR}"\n`; envContent += `CODE_MAP_ALLOWED_DIR="${config.CODE_MAP_ALLOWED_DIR}"\n`; envContent += `VIBE_TASK_MANAGER_READ_DIR="${config.VIBE_TASK_MANAGER_READ_DIR}"\n`; envContent += `VIBE_TASK_MANAGER_SECURITY_MODE="${config.VIBE_TASK_MANAGER_SECURITY_MODE}"\n`; envContent += '\n'; envContent += '# Advanced Configuration\n'; envContent += `OPENROUTER_BASE_URL="${config.OPENROUTER_BASE_URL}"\n`; envContent += `GEMINI_MODEL="${config.GEMINI_MODEL}"\n`; envContent += `PERPLEXITY_MODEL="${config.PERPLEXITY_MODEL}"\n`; await fs.writeFile(this.envPath, envContent, 'utf-8'); } async saveConfigJson(config) { const configData = { version: '1.0.0', setupDate: new Date().toISOString(), directories: { output: config.VIBE_CODER_OUTPUT_DIR, codeMap: config.CODE_MAP_ALLOWED_DIR, taskManager: config.VIBE_TASK_MANAGER_READ_DIR }, security: { mode: config.VIBE_TASK_MANAGER_SECURITY_MODE }, models: { gemini: config.GEMINI_MODEL, perplexity: config.PERPLEXITY_MODEL }, api: { baseUrl: config.OPENROUTER_BASE_URL } }; await fs.writeJson(this.configPath, configData, { spaces: 2 }); } async testApiKey(apiKey) { const spinner = ora('Validating API key...').start(); try { const isValid = await this.testApiKeyLive(apiKey); if (isValid) { spinner.succeed('API key validated successfully!'); return true; } else { spinner.fail('Invalid API key'); return false; } } catch (error) { spinner.fail('API key validation failed'); logger.error({ err: error }, 'API key validation error'); return false; } } async displayNextSteps() { console.log('\n' + boxen(chalk.green.bold('✅ Setup Complete!') + '\n\n' + chalk.white('Your Vibe is now configured and ready to use!') + '\n\n' + chalk.cyan('Quick Commands:') + '\n' + chalk.gray('• ') + chalk.cyan('vibe') + chalk.gray(' - Start MCP server') + '\n' + chalk.gray('• ') + chalk.cyan('vibe "request"') + chalk.gray(' - Process natural language') + '\n' + chalk.gray('• ') + chalk.cyan('vibe --help') + chalk.gray(' - Show all options') + '\n\n' + chalk.yellow('💡 Pro Tip: ') + chalk.gray('Use ') + chalk.cyan('vibe') + chalk.gray(' for everything!'), { padding: 1, margin: 1, borderStyle: 'double', borderColor: 'green', textAlignment: 'left' })); try { const spinner = ora('Checking installation options...').start(); const isNpxRun = process.env.npm_execpath && process.env.npm_execpath.includes('npx'); if (isNpxRun) { spinner.info('For easier access, consider installing globally:'); console.log(chalk.cyan(' npm install -g vibe-coder-mcp\n')); console.log(chalk.gray('After global install, just use ') + chalk.cyan('vibe') + chalk.gray(' from anywhere!')); } else { spinner.succeed('You can now use the vibe command from anywhere!'); } } catch { } } async runNonInteractiveSetup() { const hasApiKey = !!process.env.OPENROUTER_API_KEY; if (!hasApiKey) { console.error(` ERROR: Non-interactive setup requires OPENROUTER_API_KEY To run in non-interactive mode (CI/CD environments), set: export OPENROUTER_API_KEY=your_api_key Or run interactively with a TTY terminal. `); return false; } try { await this.userConfigManager.ensureUserConfigDir(); const config = { OPENROUTER_API_KEY: process.env.OPENROUTER_API_KEY || '', VIBE_CODER_OUTPUT_DIR: process.env.VIBE_CODER_OUTPUT_DIR || './VibeCoderOutput', CODE_MAP_ALLOWED_DIR: process.env.CODE_MAP_ALLOWED_DIR || '.', VIBE_TASK_MANAGER_READ_DIR: process.env.VIBE_TASK_MANAGER_READ_DIR || '.', VIBE_TASK_MANAGER_SECURITY_MODE: process.env.VIBE_TASK_MANAGER_SECURITY_MODE || 'strict', OPENROUTER_BASE_URL: process.env.OPENROUTER_BASE_URL || 'https://openrouter.ai/api/v1', GEMINI_MODEL: process.env.GEMINI_MODEL || 'google/gemini-2.5-flash-preview-05-20', PERPLEXITY_MODEL: process.env.PERPLEXITY_MODEL || 'perplexity/sonar', configureDirs: false, configureAdvanced: false }; await this.saveEnhancedConfiguration(config); logger.info('Auto-setup completed successfully'); return true; } catch (error) { logger.error({ err: error }, 'Auto-setup failed'); return false; } } async saveEnhancedConfiguration(config) { await this.userConfigManager.ensureUserConfigDir(); const locations = [ { dir: path.join(this.userConfigManager.getUserConfigDir(), 'configs'), priority: 1 }, { dir: projectRoot, priority: 2 } ]; for (const location of locations) { try { await this.createEnvFile(config); await this.userConfigManager.copyDefaultConfigs(); } catch (error) { logger.warn({ err: error, location }, 'Failed to save config to location'); } } } async run() { try { if (!this.isInteractive) { console.log(chalk.yellow('Non-interactive environment detected.')); console.log(chalk.gray('Attempting auto-setup from environment variables...')); return await this.runNonInteractiveSetup(); } this.displayWelcome(); if (process.argv.includes('--reconfigure') || process.argv.includes('--setup')) { console.log(chalk.yellow('🔄 Reconfiguring Vibe Coder MCP...\n')); } else if (!(await this.isFirstRun())) { return true; } const config = await this.promptConfiguration(); const isValid = await this.testApiKey(config.OPENROUTER_API_KEY); if (!isValid) { const { continueAnyway } = await inquirer.prompt([ { type: 'confirm', name: 'continueAnyway', message: 'API key validation failed. Continue anyway?', default: false } ]); if (!continueAnyway) { console.log(chalk.red('\n❌ Setup cancelled.')); return false; } } const spinner = ora('Creating configuration files...').start(); await this.saveEnhancedConfiguration(config); spinner.succeed('Configuration files created!'); await this.displayNextSteps(); const dotenv = await import('dotenv'); dotenv.config({ path: this.envPath }); return true; } catch (error) { console.error(chalk.red('\n❌ Setup failed:'), error); logger.error({ err: error }, 'Setup wizard error'); return false; } } async quickCheck() { if (await this.isFirstRun()) { console.log(chalk.yellow('\n⚠️ First-time setup required.')); console.log(chalk.gray('Run with --setup to configure Vibe Coder MCP.\n')); process.exit(1); } if (!(await this.isConfigValid())) { console.log(chalk.yellow('\n⚠️ Configuration is incomplete.')); console.log(chalk.gray('Run with --reconfigure to update settings.\n')); } } } export const setupWizard = new SetupWizard(); if (import.meta.url === `file://${process.argv[1]}`) { setupWizard.run().then(success => { process.exit(success ? 0 : 1); }); }