@wonderwhy-er/desktop-commander
Version:
MCP server for terminal operations and file editing
408 lines (407 loc) • 16.3 kB
JavaScript
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();