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
JavaScript
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);
});
}