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.

629 lines (628 loc) 27.4 kB
import readline from 'readline'; import chalk from 'chalk'; import { executeTool } from '../../services/routing/toolRegistry.js'; import { getBanner, getSessionStartMessage, getPrompt } from './ui/banner.js'; import { progress } from './ui/progress.js'; import { ResponseFormatter } from './ui/formatter.js'; import { CommandHistory } from './history.js'; import { AutoCompleter } from './completion.js'; import { SessionPersistence } from './persistence.js'; import { GracefulShutdown, createAutoSaveHandler } from './shutdown.js'; import { MultilineInput } from './multiline.js'; import { MarkdownRenderer } from './ui/markdown.js'; import { configManager } from './config.js'; import { themeManager } from './themes.js'; import logger from '../../logger.js'; export class VibeInteractiveREPL { rl = null; sessionId; conversationHistory = []; openRouterConfig = null; isRunning = false; history; completer; persistence; shutdown; autoSaveHandler = null; startTime; multiline; enableMarkdown = true; requestConcurrency = 0; pendingConfirmation = null; constructor() { this.sessionId = `interactive-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; this.history = new CommandHistory(); this.completer = new AutoCompleter(); this.persistence = new SessionPersistence(); this.shutdown = new GracefulShutdown(); this.startTime = new Date(); this.multiline = new MultilineInput(); } async start(config, resumeSessionId) { this.openRouterConfig = config; this.isRunning = true; await configManager.initialize(); this.enableMarkdown = configManager.get('display', 'enableMarkdown'); const historySize = configManager.get('history', 'maxSize'); this.history = new CommandHistory(historySize); const themeName = configManager.get('display', 'theme'); themeManager.setTheme(themeName); if (resumeSessionId) { const session = await this.persistence.loadSession(resumeSessionId); if (session) { this.sessionId = session.sessionId; this.conversationHistory = session.conversationHistory; this.startTime = session.startTime; console.log(chalk.green(`✅ Resumed session: ${resumeSessionId}`)); console.log(chalk.gray(`Started: ${session.startTime.toLocaleString()}`)); console.log(); } } const autoSaveInterval = configManager.get('session', 'autoSaveInterval'); this.autoSaveHandler = createAutoSaveHandler(this.sessionId, () => this.getSessionData(), autoSaveInterval * 60000); if (configManager.get('session', 'autoSave')) { this.autoSaveHandler.start(); } this.shutdown.register(async () => { if (this.autoSaveHandler) { await this.autoSaveHandler.stop(); } await this.history.saveHistory(); }); this.shutdown.setupSignalHandlers(); this.displayBanner(); try { const { getAllTools } = await import('../../services/routing/toolRegistry.js'); const tools = await getAllTools(); this.completer.setTools(tools.map(t => t.name)); } catch { } this.rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: getPrompt(), completer: (line) => this.completer.complete(line), historySize: 0 }); this.setupEventHandlers(); this.rl.prompt(); } displayBanner() { console.clear(); console.log(getBanner()); console.log(); console.log(getSessionStartMessage()); console.log(); } setupEventHandlers() { if (!this.rl) return; this.rl.on('line', async (input) => { if (this.multiline.isActive() || this.multiline.isStarting(input)) { const isComplete = this.multiline.addLine(input); if (!isComplete) { if (this.rl) { this.rl.setPrompt(this.multiline.getPrompt()); this.rl.prompt(); } return; } const fullInput = this.multiline.getContent(); this.multiline.reset(); if (fullInput.trim()) { this.history.add(fullInput); if (fullInput.trim().startsWith('/')) { await this.handleSlashCommand(fullInput.trim()); } else { await this.handleUserMessage(fullInput); } } if (this.rl) { this.rl.setPrompt(getPrompt()); this.rl.prompt(); } return; } const trimmed = input.trim(); if (!trimmed) { this.rl.prompt(); return; } this.history.add(trimmed); let processedInput = trimmed; if (configManager.get('commands', 'aliasEnabled')) { const aliases = configManager.get('commands', 'aliases'); if (aliases[trimmed]) { processedInput = aliases[trimmed]; } } if (processedInput.startsWith('/')) { await this.handleSlashCommand(processedInput); } else { await this.handleUserMessage(processedInput); } if (this.isRunning && this.rl) { this.rl.prompt(); } }); if (process.stdin.isTTY && process.stdin.setRawMode) { readline.emitKeypressEvents(process.stdin, this.rl); process.stdin.setRawMode(true); process.stdin.on('keypress', (str, key) => { if (!this.rl || !key) return; if (key.name === 'up') { const prev = this.history.getPrevious(this.rl.line); if (prev !== undefined) { this.rl.write(null, { ctrl: true, name: 'u' }); this.rl.write(prev); } } else if (key.name === 'down') { const next = this.history.getNext(); if (next !== undefined) { this.rl.write(null, { ctrl: true, name: 'u' }); this.rl.write(next); } } }); } this.rl.on('SIGINT', () => { this.handleExit(); }); this.rl.on('close', () => { if (this.isRunning) { this.handleExit(); } }); } async handleUserMessage(message) { if (this.pendingConfirmation) { const normalizedMessage = message.toLowerCase().trim(); const confirmationPatterns = [ 'yes', 'y', 'yeah', 'yep', 'sure', 'ok', 'okay', 'proceed', 'go ahead', 'do it', 'confirm', 'continue', 'please proceed', 'go for it', 'lets do it', "let's do it" ]; const cancellationPatterns = [ 'no', 'n', 'nope', 'cancel', 'stop', 'abort', 'nevermind', 'never mind', "don't", 'dont', 'skip', 'forget it' ]; const isConfirmation = confirmationPatterns.some(pattern => normalizedMessage === pattern || normalizedMessage.startsWith(pattern + ' ') || normalizedMessage.includes(' ' + pattern + ' ') || normalizedMessage.endsWith(' ' + pattern)); const isCancellation = cancellationPatterns.some(pattern => normalizedMessage === pattern || normalizedMessage.startsWith(pattern + ' ') || normalizedMessage.includes(' ' + pattern + ' ') || normalizedMessage.endsWith(' ' + pattern)); if (isConfirmation && !isCancellation) { const { toolName, parameters } = this.pendingConfirmation; this.pendingConfirmation = null; progress.start(`Executing ${toolName}...`); try { const context = { sessionId: this.sessionId, transportType: 'interactive', metadata: { conversationHistory: this.conversationHistory, interactiveMode: true } }; const result = await executeTool(toolName, parameters, this.openRouterConfig, context); progress.success('Tool execution complete'); const responseText = result.content[0]?.text; const response = typeof responseText === 'string' ? responseText : 'Tool executed successfully'; console.log(); if (this.enableMarkdown) { const rendered = MarkdownRenderer.renderWrapped(response); ResponseFormatter.formatResponse(rendered); } else { ResponseFormatter.formatResponse(response); } console.log(); this.conversationHistory.push({ role: 'assistant', content: response }); } catch (error) { progress.fail('Tool execution failed'); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; console.log(); ResponseFormatter.formatError(errorMessage); console.log(); logger.error({ err: error }, `Error executing confirmed tool ${toolName}`); } return; } else if (isCancellation) { this.pendingConfirmation = null; console.log(); ResponseFormatter.formatInfo('Tool execution cancelled.'); console.log(); return; } else if (!isConfirmation && !isCancellation) { console.log(); ResponseFormatter.formatWarning('Please respond with "yes" to proceed or "no" to cancel.\n' + `Tool waiting: ${this.pendingConfirmation.toolName}`); console.log(); return; } } const maxConcurrent = configManager.get('performance', 'maxConcurrentRequests'); if (this.requestConcurrency >= maxConcurrent) { ResponseFormatter.formatWarning('Maximum concurrent requests reached. Please wait for current requests to complete.'); return; } this.requestConcurrency++; this.conversationHistory.push({ role: 'user', content: message }); progress.start('Processing your request...'); try { const { hybridMatch } = await import('../../services/hybrid-matcher/index.js'); const matchResult = await hybridMatch(message, this.openRouterConfig); if (matchResult.requiresConfirmation) { this.pendingConfirmation = { toolName: matchResult.toolName, parameters: matchResult.parameters, originalRequest: message }; progress.success('Analysis complete'); console.log(); ResponseFormatter.formatInfo(`I plan to use the '${matchResult.toolName}' tool for your request.\n` + `Confidence: ${Math.round(matchResult.confidence * 100)}%\n\n` + `Do you want to proceed? (yes/no)`); console.log(); this.conversationHistory.push({ role: 'assistant', content: `Requesting confirmation to use ${matchResult.toolName} tool (confidence: ${Math.round(matchResult.confidence * 100)}%)` }); } else { const context = { sessionId: this.sessionId, transportType: 'interactive', metadata: { conversationHistory: this.conversationHistory, interactiveMode: true } }; progress.update(`Executing ${matchResult.toolName}...`); const result = await executeTool(matchResult.toolName, matchResult.parameters, this.openRouterConfig, context); progress.success('Response ready'); const responseText = result.content[0]?.text; const response = typeof responseText === 'string' ? responseText : 'No response'; console.log(); if (this.enableMarkdown) { const rendered = MarkdownRenderer.renderWrapped(response); ResponseFormatter.formatResponse(rendered); } else { ResponseFormatter.formatResponse(response); } console.log(); this.conversationHistory.push({ role: 'assistant', content: response }); } } catch (error) { progress.fail('Request failed'); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; console.log(); ResponseFormatter.formatError(errorMessage); console.log(); logger.error({ err: error }, 'Error processing message in REPL'); } finally { this.requestConcurrency--; } } async handleSlashCommand(command) { const [cmd, ...args] = command.split(' '); switch (cmd) { case '/help': this.showHelp(); break; case '/quit': case '/exit': this.handleExit(); break; case '/clear': this.clearHistory(); break; case '/history': this.showHistory(); break; case '/tools': await this.listTools(); break; case '/status': this.showStatus(); break; case '/save': await this.saveSession(); break; case '/sessions': await this.listSessions(); break; case '/export': await this.exportSession(args.join(' ')); break; case '/markdown': this.toggleMarkdown(); break; case '/config': await this.handleConfigCommand(args); break; case '/theme': await this.handleThemeCommand(args); break; default: console.log(chalk.red(`Unknown command: ${cmd}`)); console.log(chalk.gray('Type /help for available commands')); break; } } showHelp() { console.log(); const commands = [ { cmd: '/help', desc: 'Show this help message' }, { cmd: '/quit', desc: 'Exit interactive mode' }, { cmd: '/clear', desc: 'Clear conversation history' }, { cmd: '/history', desc: 'Show conversation history' }, { cmd: '/tools', desc: 'List available MCP tools' }, { cmd: '/status', desc: 'Show session status' }, { cmd: '/save', desc: 'Save current session' }, { cmd: '/sessions', desc: 'List saved sessions' }, { cmd: '/export [file]', desc: 'Export session to markdown' }, { cmd: '/markdown', desc: 'Toggle markdown rendering' }, { cmd: '/config', desc: 'Manage configuration settings' }, { cmd: '/theme', desc: 'Change color theme' } ]; ResponseFormatter.formatTable(['Command', 'Description'], commands.map(c => [chalk.green(c.cmd), c.desc])); console.log(); } clearHistory() { this.conversationHistory = []; ResponseFormatter.formatSuccess('Conversation history cleared'); } showHistory() { if (this.conversationHistory.length === 0) { console.log(chalk.gray('No conversation history')); return; } console.log(); console.log(chalk.yellow('Conversation History:')); console.log(chalk.gray('─'.repeat(50))); this.conversationHistory.forEach((entry, index) => { const prefix = entry.role === 'user' ? chalk.cyan('You: ') : chalk.green('Vibe: '); console.log(`${prefix}${entry.content}`); if (index < this.conversationHistory.length - 1) { console.log(); } }); console.log(chalk.gray('─'.repeat(50))); console.log(); } async listTools() { try { const { getAllTools } = await import('../../services/routing/toolRegistry.js'); const tools = await getAllTools(); console.log(); console.log(chalk.yellow('Available Tools:')); tools.forEach(tool => { console.log(chalk.cyan(` • ${tool.name}`) + ' - ' + chalk.gray(tool.description || 'No description')); }); console.log(); } catch (error) { console.log(chalk.red('Failed to retrieve tools')); logger.error({ err: error }, 'Failed to list tools in REPL'); } } showStatus() { const duration = Date.now() - parseInt(this.sessionId.split('-')[1]); const minutes = Math.floor(duration / 60000); const seconds = Math.floor((duration % 60000) / 1000); console.log(); ResponseFormatter.formatKeyValue({ 'Session ID': this.sessionId, 'Duration': `${minutes}m ${seconds}s`, 'Messages sent': this.conversationHistory.filter(h => h.role === 'user').length, 'Total exchanges': this.conversationHistory.length, 'Memory used': `${Math.round(process.memoryUsage().heapUsed / 1024 / 1024)}MB` }, 'Session Status'); console.log(); } getSessionData() { return { sessionId: this.sessionId, startTime: this.startTime, lastUpdated: new Date(), conversationHistory: this.conversationHistory, metadata: { totalMessages: this.conversationHistory.filter(h => h.role === 'user').length } }; } async saveSession() { try { await this.persistence.saveSession(this.sessionId, this.getSessionData()); ResponseFormatter.formatSuccess(`Session saved: ${this.sessionId}`); } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; ResponseFormatter.formatError(`Failed to save session: ${message}`); } } async listSessions() { try { const sessions = await this.persistence.listSessions(); if (sessions.length === 0) { ResponseFormatter.formatInfo('No saved sessions found'); return; } console.log(); ResponseFormatter.formatTable(['Session ID', 'Started', 'Last Updated'], sessions.slice(0, 10).map(s => [ s.id === this.sessionId ? chalk.green(s.id + ' (current)') : s.id, s.startTime.toLocaleString(), s.lastUpdated.toLocaleString() ])); if (sessions.length > 10) { console.log(chalk.gray(`\n... and ${sessions.length - 10} more sessions`)); } console.log(); ResponseFormatter.formatInfo('Use "vibe --resume <session-id>" to resume a session'); } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; ResponseFormatter.formatError(`Failed to list sessions: ${message}`); } } async exportSession(filename) { try { const outputPath = await this.persistence.exportSession(this.sessionId, filename); ResponseFormatter.formatSuccess(`Session exported to: ${outputPath}`); } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; ResponseFormatter.formatError(`Failed to export session: ${message}`); } } toggleMarkdown() { this.enableMarkdown = !this.enableMarkdown; configManager.set('display', 'enableMarkdown', this.enableMarkdown); const status = this.enableMarkdown ? 'enabled' : 'disabled'; ResponseFormatter.formatInfo(`Markdown rendering ${status}`); configManager.autoSave().catch(err => { logger.error({ err }, 'Failed to auto-save config'); }); } async handleConfigCommand(args) { const subcommand = args[0]; switch (subcommand) { case 'show': console.log(configManager.printConfig()); break; case 'reset': await configManager.reset(); ResponseFormatter.formatSuccess('Configuration reset to defaults'); break; case 'save': await configManager.saveConfig(); ResponseFormatter.formatSuccess('Configuration saved'); break; case 'reload': await configManager.initialize(); this.enableMarkdown = configManager.get('display', 'enableMarkdown'); ResponseFormatter.formatSuccess('Configuration reloaded'); break; case 'export': { const exportPath = args[1] || `vibe-config-${Date.now()}.json`; await configManager.exportTo(exportPath); ResponseFormatter.formatSuccess(`Configuration exported to: ${exportPath}`); break; } case 'import': if (!args[1]) { ResponseFormatter.formatError('Please provide a configuration file path'); return; } await configManager.loadFrom(args[1]); ResponseFormatter.formatSuccess('Configuration imported'); break; case 'validate': { const { valid, errors } = configManager.validate(); if (valid) { ResponseFormatter.formatSuccess('Configuration is valid'); } else { ResponseFormatter.formatError('Configuration validation failed:'); errors.forEach(err => console.log(chalk.red(` • ${err}`))); } break; } default: console.log(chalk.yellow('Config commands:')); console.log(chalk.gray(' /config show - Show current configuration')); console.log(chalk.gray(' /config reset - Reset to defaults')); console.log(chalk.gray(' /config save - Save current configuration')); console.log(chalk.gray(' /config reload - Reload configuration from file')); console.log(chalk.gray(' /config export - Export configuration')); console.log(chalk.gray(' /config import - Import configuration')); console.log(chalk.gray(' /config validate - Validate configuration')); break; } } async handleThemeCommand(args) { const subcommand = args[0]; if (!subcommand) { const currentTheme = themeManager.getCurrentThemeName(); const availableThemes = themeManager.getAvailableThemes(); console.log(); console.log(chalk.yellow('Theme Settings:')); console.log(chalk.green(` Current theme: ${currentTheme}`)); console.log(); console.log(chalk.yellow('Available themes:')); availableThemes.forEach(theme => { const description = themeManager.getThemeDescription(theme); const indicator = theme === currentTheme ? chalk.green(' (current)') : ''; console.log(chalk.cyan(` • ${theme}`) + indicator + chalk.gray(` - ${description}`)); }); console.log(); console.log(chalk.gray('Use "/theme <name>" to change theme')); console.log(chalk.gray('Use "/theme preview <name>" to preview a theme')); return; } if (subcommand === 'preview') { const themeName = args[1]; if (!themeName) { ResponseFormatter.formatError('Please specify a theme name to preview'); return; } const originalTheme = themeManager.getCurrentThemeName(); if (themeManager.setTheme(themeName)) { console.log(); console.log(chalk.yellow(`Preview of '${themeName}' theme:`)); console.log(); const colors = themeManager.getColors(); console.log(colors.primary('Primary Color')); console.log(colors.secondary('Secondary Color')); console.log(colors.accent('Accent Color')); console.log(colors.success('✅ Success Message')); console.log(colors.error('❌ Error Message')); console.log(colors.warning('⚠️ Warning Message')); console.log(colors.info('ℹ️ Info Message')); console.log(colors.code('const example = "code";')); console.log(colors.link('https://example.com')); themeManager.setTheme(originalTheme); console.log(); console.log(chalk.gray('Theme preview complete. Original theme restored.')); } else { ResponseFormatter.formatError(`Theme '${themeName}' not found`); } return; } const themeName = subcommand; if (themeManager.setTheme(themeName)) { configManager.set('display', 'theme', themeName); await configManager.autoSave(); console.clear(); this.displayBanner(); ResponseFormatter.formatSuccess(`Theme changed to '${themeName}'`); } else { ResponseFormatter.formatError(`Theme '${themeName}' not found`); console.log(chalk.gray('Use "/theme" to see available themes')); } } handleExit() { this.isRunning = false; this.shutdown.execute().catch(error => { console.error(chalk.red('Shutdown error:'), error); process.exit(1); }); } }