UNPKG

@wonderwhy-er/desktop-commander

Version:

MCP server for terminal operations and file editing

408 lines (407 loc) 16.3 kB
import { terminalManager } from './terminal-manager.js'; import { capture } from './utils/capture.js'; /** * Manager for REPL (Read-Eval-Print Loop) sessions * Provides a higher-level API for working with interactive programming environments */ export class REPLManager { constructor() { this.sessions = new Map(); // Language-specific configurations for various REPLs this.languageConfigs = { 'python': { command: process.platform === 'win32' ? 'python' : 'python3', args: ['-i'], promptPattern: /^(>>>|\.\.\.) /m, errorPattern: /\b(Error|Exception|SyntaxError|TypeError|ValueError|NameError|ImportError|AttributeError)\b:.*$/m, continuationPattern: /^\.\.\./m, executeBlock: (code) => { // Break code into lines const lines = code.split('\n'); // For Python, we need to add an extra newline after blocks to execute them if (lines.length > 1 && lines[lines.length - 1].trim() !== '') { lines.push(''); // Add an empty line to end the block } return lines; } }, 'node': { command: 'node', args: ['-i'], // Use the -i flag to force interactive mode promptPattern: /^> ?$/m, // Updated to match exactly the Node.js prompt at beginning of line errorPattern: /\b(Error|SyntaxError|TypeError|ReferenceError|RangeError)\b:.*$/m, executeBlock: (code) => { // For Node.js REPL, we need different handling for multi-line code const lines = code.split('\n'); if (lines.length > 1) { // For multi-line code, we'll just execute it as one string // Node.js can handle this without entering editor mode return [code]; } return [code]; } }, 'bash': { command: 'bash', args: ['-i'], promptPattern: /[\w\d\-_]+@[\w\d\-_]+:.*[$#] $/m, errorPattern: /\b(command not found|No such file or directory|syntax error|Permission denied)\b/m } }; } /** * Calculate an appropriate timeout based on code complexity */ calculateTimeout(code, language) { // Base timeout let timeout = 2000; // Add time for loops const loopCount = (code.match(/for|while/g) || []).length; timeout += loopCount * 1000; // Add time for imports/requires const importPatterns = { 'python': [/import/g, /from\s+\w+\s+import/g], 'node': [/require\(/g, /import\s+.*\s+from/g], 'bash': [/source/g] }; const patterns = importPatterns[language] || []; let importCount = 0; patterns.forEach(pattern => { importCount += (code.match(pattern) || []).length; }); timeout += importCount * 2000; // Add more time for long scripts const lineCount = code.split('\n').length; timeout += lineCount * 200; // Cap at reasonable maximum return Math.min(timeout, 30000); } /** * Detect when a REPL is ready for input by looking for prompt patterns */ isReadyForInput(output, language) { const config = this.languageConfigs[language]; if (!config || !config.promptPattern) return false; // For Node.js, look for the prompt at the end of the output if (language === 'node') { const lines = output.split('\n'); const lastLine = lines[lines.length - 1].trim(); return lastLine === '>' || lastLine === '...'; } // For other languages, use the regular pattern return config.promptPattern.test(output); } /** * Detect errors in REPL output */ detectErrors(output, language) { const config = this.languageConfigs[language]; if (!config || !config.errorPattern) return null; const match = output.match(config.errorPattern); return match ? match[0] : null; } /** * Create a new REPL session for the specified language */ async createSession(language, timeout = 5000) { try { const config = this.languageConfigs[language]; if (!config) { throw new Error(`Unsupported language: ${language}`); } // Prepare the command and arguments const command = `${config.command} ${config.args.join(' ')}`.trim(); // Start the process const result = await terminalManager.executeCommand(command, timeout); if (result.pid <= 0) { throw new Error(`Failed to start ${language} REPL: ${result.output}`); } // Record session information this.sessions.set(result.pid, { pid: result.pid, language, startTime: Date.now(), lastActivity: Date.now(), outputBuffer: result.output || '' }); capture('repl_session_created', { language, pid: result.pid }); return result.pid; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); capture('repl_session_creation_failed', { language, error: errorMessage }); throw error; } } /** * Send and read REPL output with a timeout * This combines sending input and reading output in a single operation */ async sendAndReadREPL(pid, input, options = {}) { const session = this.sessions.get(pid); if (!session) { return { success: false, output: null, error: `No active session with PID ${pid}`, timeout: false }; } try { // Update last activity time session.lastActivity = Date.now(); // Get the language configuration const config = this.languageConfigs[session.language]; // Default timeout based on code complexity const timeout = options.timeout || this.calculateTimeout(input, session.language); // Clear any existing output const existingOutput = terminalManager.getNewOutput(pid) || ''; if (existingOutput) { session.outputBuffer += existingOutput; } // For Node.js, we need to handle the initial startup differently if (session.language === 'node' && session.outputBuffer.length === 0) { // Wait for Node.js to initialize if this is the first command await new Promise(resolve => setTimeout(resolve, 1000)); const initialOutput = terminalManager.getNewOutput(pid) || ''; if (initialOutput) { session.outputBuffer += initialOutput; } // Add another small delay to ensure the Node.js REPL is fully ready await new Promise(resolve => setTimeout(resolve, 500)); } let output = ""; // For Node.js, always send the entire code block at once if (session.language === 'node' && options.multiline && input.includes('\n')) { const success = terminalManager.sendInputToProcess(pid, input.endsWith('\n') ? input : input + '\n'); if (!success) { return { success: false, output: session.outputBuffer, error: "Failed to send input to Node.js REPL", timeout: false }; } } // For other languages or single-line input, process according to language rules else if (options.multiline && config.executeBlock) { const lines = config.executeBlock(input); // For other languages, send each line individually for (const line of lines) { const success = terminalManager.sendInputToProcess(pid, line + '\n'); if (!success) { return { success: false, output: session.outputBuffer, error: "Failed to send input to process", timeout: false }; } // Wait a small amount of time between lines await new Promise(resolve => setTimeout(resolve, 100)); } } else { // Single line input const success = terminalManager.sendInputToProcess(pid, input.endsWith('\n') ? input : input + '\n'); if (!success) { return { success: false, output: session.outputBuffer, error: "Failed to send input to process", timeout: false }; } } // Wait for output with timeout output = ""; const startTime = Date.now(); let isTimedOut = false; let lastOutputLength = 0; let noOutputTime = 0; // Keep checking for output until timeout while (Date.now() - startTime < timeout) { // Check for new output const newOutput = terminalManager.getNewOutput(pid) || ''; if (newOutput && newOutput.length > 0) { output += newOutput; session.outputBuffer += newOutput; lastOutputLength = output.length; noOutputTime = 0; // Reset no output timer // If we're waiting for a prompt and it appears, we're done if (options.waitForPrompt && this.isReadyForInput(output, session.language)) { break; } // Check for errors if we're not ignoring them if (!options.ignoreErrors) { const error = this.detectErrors(output, session.language); if (error) { return { success: false, output, error, timeout: false }; } } } else { // If no new output, increment the no output timer noOutputTime += 100; } // For Node.js, we need to be more patient due to REPL behavior const nodeWaitThreshold = session.language === 'node' ? 2000 : 1000; // If no new output for specified time and we have some output, we're probably done if (output.length > 0 && output.length === lastOutputLength && noOutputTime > nodeWaitThreshold) { // For Node.js, make sure we have a prompt before finishing if (session.language === 'node') { if (this.isReadyForInput(output, 'node')) { break; } } else { break; } } // Small delay between checks await new Promise(resolve => setTimeout(resolve, 100)); } // Check if we timed out isTimedOut = (Date.now() - startTime) >= timeout; return { success: !isTimedOut || output.length > 0, output, timeout: isTimedOut }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); capture('repl_command_failed', { pid, language: session.language, error: errorMessage }); return { success: false, output: null, error: errorMessage, timeout: false }; } } /** * Execute a code block in a REPL session */ async executeCode(pid, code, options = {}) { const session = this.sessions.get(pid); if (!session) { return { success: false, output: null, error: `No active session with PID ${pid}`, timeout: false }; } try { capture('repl_execute_code', { pid, language: session.language, codeLength: code.length, lineCount: code.split('\n').length }); // If code is multi-line, handle it accordingly const isMultiline = code.includes('\n'); // For Node.js, use a longer timeout by default if (session.language === 'node' && !options.timeout) { options.timeout = Math.max(this.calculateTimeout(code, session.language), 5000); } // For Node.js, always wait for prompt if (session.language === 'node' && options.waitForPrompt === undefined) { options.waitForPrompt = true; } return this.sendAndReadREPL(pid, code, { ...options, multiline: isMultiline }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); capture('repl_execute_code_failed', { pid, language: session.language, error: errorMessage }); return { success: false, output: null, error: errorMessage, timeout: false }; } } /** * Terminate a REPL session */ async terminateSession(pid) { try { const session = this.sessions.get(pid); if (!session) { return false; } // Terminate the process const success = terminalManager.forceTerminate(pid); // Remove from our sessions map if (success) { this.sessions.delete(pid); capture('repl_session_terminated', { pid, language: session.language, runtime: Date.now() - session.startTime }); } return success; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); capture('repl_session_termination_failed', { pid, error: errorMessage }); return false; } } /** * List all active REPL sessions */ listSessions() { return Array.from(this.sessions.values()).map(session => ({ pid: session.pid, language: session.language, runtime: Date.now() - session.startTime })); } /** * Get information about a specific REPL session */ getSessionInfo(pid) { const session = this.sessions.get(pid); if (!session) { return null; } return { pid: session.pid, language: session.language, runtime: Date.now() - session.startTime }; } } // Export singleton instance export const replManager = new REPLManager();