From 53e5ea8df2fb575646a3036f31844ea201eec2bd Mon Sep 17 00:00:00 2001 From: eduardruzga Date: Sat, 3 May 2025 09:34:27 +0300 Subject: [PATCH 01/13] experiments with repl and terminal --- docs/enhanced-repl-terminal.md | 222 ++++++++++++ docs/repl-enhancement-plan.md | 502 ++++++++++++++++++++++++++ docs/repl-with-terminal.md | 214 +++++++++++ src/handlers/terminal-handlers.ts | 11 +- src/server.ts | 18 + src/terminal-manager.ts | 38 ++ src/tools/schemas.ts | 9 + src/tools/send-input.ts | 50 +++ test/enhanced-repl-example.js | 150 ++++++++ test/repl-via-terminal-example.js | 160 ++++++++ test/simple-node-repl-test.js | 43 +++ test/simple-python-test.js | 73 ++++ test/simple-repl-test.js | 24 ++ test/test-node-repl.js | 166 +++++++++ test/test-repl-interaction.js | 248 +++++++++++++ test/test-repl-tools.js | 273 ++++++++++++++ test/test_output/node_repl_debug.txt | 56 +++ test/test_output/repl_test_output.txt | 15 + 18 files changed, 2271 insertions(+), 1 deletion(-) create mode 100644 docs/enhanced-repl-terminal.md create mode 100644 docs/repl-enhancement-plan.md create mode 100644 docs/repl-with-terminal.md create mode 100644 src/tools/send-input.ts create mode 100644 test/enhanced-repl-example.js create mode 100644 test/repl-via-terminal-example.js create mode 100644 test/simple-node-repl-test.js create mode 100644 test/simple-python-test.js create mode 100644 test/simple-repl-test.js create mode 100644 test/test-node-repl.js create mode 100644 test/test-repl-interaction.js create mode 100644 test/test-repl-tools.js create mode 100644 test/test_output/node_repl_debug.txt create mode 100644 test/test_output/repl_test_output.txt diff --git a/docs/enhanced-repl-terminal.md b/docs/enhanced-repl-terminal.md new file mode 100644 index 0000000..7397b88 --- /dev/null +++ b/docs/enhanced-repl-terminal.md @@ -0,0 +1,222 @@ +# Enhanced Terminal Commands for REPL Environments + +This document explains the enhanced functionality for interacting with REPL (Read-Eval-Print Loop) environments using terminal commands. + +## New Features + +### 1. Timeout Support + +Both `read_output` and `send_input` now support a `timeout_ms` parameter: + +```javascript +// Read output with a 5 second timeout +const output = await readOutput({ + pid: pid, + timeout_ms: 5000 +}); +``` + +This prevents indefinite waiting for output and makes REPL interactions more reliable. + +### 2. Wait for REPL Response + +The `send_input` function now supports a `wait_for_prompt` parameter that waits for the REPL to finish processing and show a prompt: + +```javascript +// Send input and wait for the REPL prompt +const result = await sendInput({ + pid: pid, + input: 'print("Hello, world!")\n', + wait_for_prompt: true, + timeout_ms: 5000 +}); + +// The result includes the output from the command +console.log(result.content[0].text); +``` + +This eliminates the need for manual delays between sending input and reading output. + +### 3. Prompt Detection + +When `wait_for_prompt` is enabled, the function detects common REPL prompts: + +- Node.js: `>` +- Python: `>>>` or `...` +- And others + +This allows it to know when the REPL has finished processing a command. + +## Basic Workflow + +### 1. Starting a REPL Session + +Use the `execute_command` function to start a REPL environment in interactive mode: + +```javascript +// Start Python +const pythonResult = await executeCommand({ + command: 'python -i', // Use -i flag for interactive mode + timeout_ms: 10000 +}); + +// Extract PID from the result text +const pidMatch = result.content[0].text.match(/Command started with PID (\d+)/); +const pid = pidMatch ? parseInt(pidMatch[1]) : null; +``` + +### 2. Reading the Initial Prompt with Timeout + +After starting a REPL session, you can read the initial output with a timeout: + +```javascript +// Wait for REPL to initialize with a timeout +const initialOutput = await readOutput({ + pid, + timeout_ms: 2000 +}); +console.log("Initial prompt:", initialOutput.content[0].text); +``` + +### 3. Sending Code to the REPL and Waiting for Response + +Use the enhanced `send_input` function to send code to the REPL and wait for the response: + +```javascript +// Send a single-line command and wait for the prompt +const result = await sendInput({ + pid: pid, + input: 'print("Hello, world!")\n', + wait_for_prompt: true, + timeout_ms: 3000 +}); + +console.log("Output:", result.content[0].text); +``` + +### 4. Sending Multi-line Code Blocks + +You can also send multi-line code blocks and wait for the complete response: + +```javascript +// Send multi-line code block and wait for the prompt +const multilineCode = ` +def greet(name): + return f"Hello, {name}!" + +print(greet("World")) +`; + +const result = await sendInput({ + pid: pid, + input: multilineCode + '\n', + wait_for_prompt: true, + timeout_ms: 5000 +}); + +console.log("Output:", result.content[0].text); +``` + +### 5. Terminating the REPL Session + +When you're done, use `force_terminate` to end the session: + +```javascript +await forceTerminate({ pid }); +``` + +## Examples for Different REPL Environments + +### Python + +```javascript +// Start Python in interactive mode +const result = await executeCommand({ command: 'python -i' }); +const pidMatch = result.content[0].text.match(/Command started with PID (\d+)/); +const pid = pidMatch ? parseInt(pidMatch[1]) : null; + +// Read initial prompt with timeout +const initialOutput = await readOutput({ + pid, + timeout_ms: 2000 +}); + +// Run code and wait for response +const output = await sendInput({ + pid, + input: 'print("Hello from Python!")\n', + wait_for_prompt: true, + timeout_ms: 3000 +}); +``` + +### Node.js + +```javascript +// Start Node.js in interactive mode +const result = await executeCommand({ command: 'node -i' }); +const pidMatch = result.content[0].text.match(/Command started with PID (\d+)/); +const pid = pidMatch ? parseInt(pidMatch[1]) : null; + +// Read initial prompt with timeout +const initialOutput = await readOutput({ + pid, + timeout_ms: 2000 +}); + +// Run code and wait for response +const output = await sendInput({ + pid, + input: 'console.log("Hello from Node.js!")\n', + wait_for_prompt: true, + timeout_ms: 3000 +}); +``` + +## Tips and Best Practices + +1. **Set Appropriate Timeouts**: Different commands may require different timeout values. Complex operations might need longer timeouts. + +2. **Use wait_for_prompt for Sequential Commands**: When running multiple commands that depend on each other, use `wait_for_prompt: true` to ensure commands are executed in order. + +3. **Add Newlines to Input**: Always add a newline character at the end of your input to trigger execution: + + ```javascript + await sendInput({ + pid, + input: 'your_code_here\n', + wait_for_prompt: true + }); + ``` + +4. **Handling Long-Running Operations**: For commands that take a long time to execute, increase the timeout value: + + ```javascript + await sendInput({ + pid, + input: 'import time; time.sleep(10); print("Done")\n', + wait_for_prompt: true, + timeout_ms: 15000 // 15 seconds + }); + ``` + +5. **Error Handling**: Check if a timeout was reached: + + ```javascript + const result = await sendInput({ + pid, + input: 'complex_calculation()\n', + wait_for_prompt: true, + timeout_ms: 5000 + }); + + if (result.content[0].text.includes('timeout reached')) { + console.log('Operation took too long'); + } + ``` + +## Complete Examples + +See the files: +- `test/enhanced-repl-example.js` for a complete example showing how to interact with Python and Node.js REPLs. +- `test/test-enhanced-repl.js` for tests of the enhanced functionality. diff --git a/docs/repl-enhancement-plan.md b/docs/repl-enhancement-plan.md new file mode 100644 index 0000000..d806f96 --- /dev/null +++ b/docs/repl-enhancement-plan.md @@ -0,0 +1,502 @@ +w# REPL Enhancement Plan + +This document outlines the plan to enhance the terminal tools in ClaudeServerCommander to better support REPL (Read-Eval-Print Loop) environments. It continues the refactoring work we've already done to simplify the REPL implementation by using terminal commands. + +## Background + +We've successfully refactored the specialized REPL manager and tools to use the more general terminal commands (`execute_command`, `send_input`, `read_output`, and `force_terminate`). This approach is simpler, more flexible, and works for any interactive terminal environment without requiring specialized configurations. + +However, there are some enhancements we can make to improve the user experience when working with REPLs: + +1. Add timeout support to `read_output` and `send_input` +2. Enhance `send_input` to wait for REPL responses +3. Improve output collection and prompt detection + +## Files to Modify + +### 1. src/tools/schemas.ts + +Update the schemas to support the new parameters: + +```typescript +export const ReadOutputArgsSchema = z.object({ + pid: z.number(), + timeout_ms: z.number().optional(), +}); + +export const SendInputArgsSchema = z.object({ + pid: z.number(), + input: z.string(), + timeout_ms: z.number().optional(), + wait_for_prompt: z.boolean().optional(), +}); +``` + +### 2. src/tools/execute.js + +Enhance the `readOutput` function to handle timeouts and wait for complete output: + +```typescript +export async function readOutput(args) { + const parsed = ReadOutputArgsSchema.safeParse(args); + if (!parsed.success) { + return { + content: [{ type: "text", text: `Error: Invalid arguments for read_output: ${parsed.error}` }], + isError: true, + }; + } + + const { pid, timeout_ms = 5000 } = parsed.data; + + // Check if the process exists + const session = terminalManager.getSession(pid); + if (!session) { + return { + content: [{ type: "text", text: `No session found for PID ${pid}` }], + isError: true, + }; + } + + // Wait for output with timeout + let output = ""; + let timeoutReached = false; + + try { + // Create a promise that resolves when new output is available or when timeout is reached + const outputPromise = new Promise((resolve) => { + // Check for initial output + const initialOutput = terminalManager.getNewOutput(pid); + if (initialOutput && initialOutput.length > 0) { + resolve(initialOutput); + return; + } + + // Setup an interval to poll for output + const interval = setInterval(() => { + const newOutput = terminalManager.getNewOutput(pid); + if (newOutput && newOutput.length > 0) { + clearInterval(interval); + resolve(newOutput); + } + }, 100); // Check every 100ms + + // Set a timeout to stop waiting + setTimeout(() => { + clearInterval(interval); + timeoutReached = true; + resolve(terminalManager.getNewOutput(pid) || ""); + }, timeout_ms); + }); + + output = await outputPromise; + } catch (error) { + return { + content: [{ type: "text", text: `Error reading output: ${error}` }], + isError: true, + }; + } + + return { + content: [{ + type: "text", + text: output || 'No new output available' + (timeoutReached ? ' (timeout reached)' : '') + }], + }; +} +``` + +### 3. src/tools/send-input.js + +Enhance the `sendInput` function to wait for REPL responses: + +```typescript +export async function sendInput(args) { + const parsed = SendInputArgsSchema.safeParse(args); + if (!parsed.success) { + capture('server_send_input_failed', { + error: 'Invalid arguments' + }); + return { + content: [{ type: "text", text: `Error: Invalid arguments for send_input: ${parsed.error}` }], + isError: true, + }; + } + + const { pid, input, timeout_ms = 5000, wait_for_prompt = false } = parsed.data; + + try { + capture('server_send_input', { + pid: pid, + inputLength: input.length + }); + + // Try to send input to the process + const success = terminalManager.sendInputToProcess(pid, input); + + if (!success) { + return { + content: [{ type: "text", text: `Error: Failed to send input to process ${pid}. The process may have exited or doesn't accept input.` }], + isError: true, + }; + } + + // If we don't need to wait for output, return immediately + if (!wait_for_prompt) { + return { + content: [{ + type: "text", + text: `Successfully sent input to process ${pid}. Use read_output to get the process response.` + }], + }; + } + + // Wait for output with timeout + let output = ""; + let timeoutReached = false; + + try { + // Create a promise that resolves when new output is available or when timeout is reached + const outputPromise = new Promise((resolve) => { + // Setup an interval to poll for output + const interval = setInterval(() => { + const newOutput = terminalManager.getNewOutput(pid); + + if (newOutput && newOutput.length > 0) { + output += newOutput; + + // Check if output contains a prompt pattern (indicating the REPL is ready for more input) + const promptPatterns = [/^>\s*$/, /^>>>\s*$/, /^\.{3}\s*$/]; // Common REPL prompts + const hasPrompt = promptPatterns.some(pattern => pattern.test(newOutput.trim().split('\n').pop() || '')); + + if (hasPrompt) { + clearInterval(interval); + resolve(output); + } + } + }, 100); // Check every 100ms + + // Set a timeout to stop waiting + setTimeout(() => { + clearInterval(interval); + timeoutReached = true; + + // Get any final output + const finalOutput = terminalManager.getNewOutput(pid); + if (finalOutput) { + output += finalOutput; + } + + resolve(output); + }, timeout_ms); + }); + + await outputPromise; + } catch (error) { + return { + content: [{ type: "text", text: `Error reading output after sending input: ${error}` }], + isError: true, + }; + } + + return { + content: [{ + type: "text", + text: `Input sent to process ${pid}.\n\nOutput received:\n${output || '(No output)'}${timeoutReached ? ' (timeout reached)' : ''}` + }], + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + capture('server_send_input_error', { + error: errorMessage + }); + return { + content: [{ type: "text", text: `Error sending input: ${errorMessage}` }], + isError: true, + }; + } +} +``` + +### 4. src/terminal-manager.ts + +Add a method to get a session by PID: + +```typescript +/** + * Get a session by PID + * @param pid Process ID + * @returns The session or undefined if not found + */ +getSession(pid: number): TerminalSession | undefined { + return this.sessions.get(pid); +} +``` + +## Tests to Create + +### 1. test/test-enhanced-repl.js + +Create a new test file to verify the enhanced functionality: + +```javascript +import assert from 'assert'; +import { executeCommand, readOutput, sendInput, forceTerminate } from '../dist/tools/execute.js'; +import { sendInput as sendInputDirectly } from '../dist/tools/send-input.js'; + +/** + * Test enhanced REPL functionality + */ +async function testEnhancedREPL() { + console.log('Testing enhanced REPL functionality...'); + + // Start Python in interactive mode + console.log('Starting Python REPL...'); + const result = await executeCommand({ + command: 'python -i', + timeout_ms: 10000 + }); + + // Extract PID from the result text + const pidMatch = result.content[0].text.match(/Command started with PID (\d+)/); + const pid = pidMatch ? parseInt(pidMatch[1]) : null; + + if (!pid) { + console.error("Failed to get PID from Python process"); + return false; + } + + console.log(`Started Python session with PID: ${pid}`); + + // Test read_output with timeout + console.log('Testing read_output with timeout...'); + const initialOutput = await readOutput({ + pid, + timeout_ms: 2000 + }); + console.log('Initial Python prompt:', initialOutput.content[0].text); + + // Test send_input with wait_for_prompt + console.log('Testing send_input with wait_for_prompt...'); + const inputResult = await sendInputDirectly({ + pid, + input: 'print("Hello from Python with wait!")\n', + wait_for_prompt: true, + timeout_ms: 5000 + }); + console.log('Python output with wait_for_prompt:', inputResult.content[0].text); + + // Test send_input without wait_for_prompt + console.log('Testing send_input without wait_for_prompt...'); + await sendInputDirectly({ + pid, + input: 'print("Hello from Python without wait!")\n', + wait_for_prompt: false + }); + + // Wait a moment for Python to process + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Read the output + const output = await readOutput({ pid }); + console.log('Python output without wait_for_prompt:', output.content[0].text); + + // Test multi-line code with wait_for_prompt + console.log('Testing multi-line code with wait_for_prompt...'); + const multilineCode = ` +def greet(name): + return f"Hello, {name}!" + +for i in range(3): + print(greet(f"Guest {i+1}")) +`; + + const multilineResult = await sendInputDirectly({ + pid, + input: multilineCode + '\n', + wait_for_prompt: true, + timeout_ms: 5000 + }); + console.log('Python multi-line output with wait_for_prompt:', multilineResult.content[0].text); + + // Terminate the session + await forceTerminate({ pid }); + console.log('Python session terminated'); + + return true; +} + +// Run the test +testEnhancedREPL() + .then(success => { + console.log(`Enhanced REPL test ${success ? 'PASSED' : 'FAILED'}`); + process.exit(success ? 0 : 1); + }) + .catch(error => { + console.error('Test error:', error); + process.exit(1); + }); +``` + +## Example Code + +Update the `repl-via-terminal-example.js` file to use the enhanced functionality: + +```javascript +import { + executeCommand, + readOutput, + forceTerminate +} from '../dist/tools/execute.js'; +import { sendInput } from '../dist/tools/send-input.js'; + +// Example of starting and interacting with a Python REPL session +async function pythonREPLExample() { + console.log('Starting a Python REPL session...'); + + // Start Python interpreter in interactive mode + const result = await executeCommand({ + command: 'python -i', + timeout_ms: 10000 + }); + + // Extract PID from the result text + const pidMatch = result.content[0].text.match(/Command started with PID (\d+)/); + const pid = pidMatch ? parseInt(pidMatch[1]) : null; + + if (!pid) { + console.error("Failed to get PID from Python process"); + return; + } + + console.log(`Started Python session with PID: ${pid}`); + + // Initial read to get the Python prompt + console.log("Reading initial output..."); + const initialOutput = await readOutput({ + pid, + timeout_ms: 2000 + }); + console.log("Initial Python prompt:", initialOutput.content[0].text); + + // Send a simple Python command with wait_for_prompt + console.log("Sending simple command..."); + const simpleResult = await sendInput({ + pid, + input: 'print("Hello from Python!")\n', + wait_for_prompt: true, + timeout_ms: 3000 + }); + console.log('Python output with wait_for_prompt:', simpleResult.content[0].text); + + // Send a multi-line code block with wait_for_prompt + console.log("Sending multi-line code..."); + const multilineCode = ` +def greet(name): + return f"Hello, {name}!" + +for i in range(3): + print(greet(f"Guest {i+1}")) +`; + + const multilineResult = await sendInput({ + pid, + input: multilineCode + '\n', + wait_for_prompt: true, + timeout_ms: 5000 + }); + console.log('Python multi-line output with wait_for_prompt:', multilineResult.content[0].text); + + // Terminate the session + await forceTerminate({ pid }); + console.log('Python session terminated'); +} + +// Run the example +pythonREPLExample() + .catch(error => { + console.error('Error running example:', error); + }); +``` + +## Documentation Updates + +Update the `repl-with-terminal.md` file to document the enhanced functionality: + +```markdown +# Enhanced Terminal Commands for REPL Environments + +## New Features + +### 1. Timeout Support + +Both `read_output` and `send_input` now support a `timeout_ms` parameter: + +```javascript +// Read output with a 5 second timeout +const output = await readOutput({ + pid: pid, + timeout_ms: 5000 +}); +``` + +### 2. Wait for REPL Response + +The `send_input` function now supports a `wait_for_prompt` parameter that waits for the REPL to finish processing and show a prompt: + +```javascript +// Send input and wait for the REPL prompt +const result = await sendInput({ + pid: pid, + input: 'print("Hello, world!")\n', + wait_for_prompt: true, + timeout_ms: 5000 +}); + +// The result includes the output from the command +console.log(result.content[0].text); +``` + +### 3. Prompt Detection + +When `wait_for_prompt` is enabled, the function detects common REPL prompts: +- Node.js: `>` +- Python: `>>>` or `...` +- And others + +This allows it to know when the REPL has finished processing a command. +``` + +## Implementation Steps + +1. First, update the schemas to add the new parameters +2. Add the `getSession` method to the terminal manager +3. Enhance `readOutput` to support timeouts and waiting for output +4. Enhance `sendInput` to support waiting for REPL prompts +5. Create tests to verify the enhanced functionality +6. Update the example code and documentation + +## Testing Strategy + +1. Test with different REPL environments (Python, Node.js) +2. Test with single-line and multi-line code +3. Test with different timeout values +4. Test prompt detection for different REPLs +5. Verify that output is correctly captured and returned + +## Next Steps After Implementation + +1. Finalize code and run all tests +2. Create a pull request with the changes +3. Update the main documentation to reflect the enhanced functionality +4. Consider adding support for other REPL environments and prompt patterns + +## Previous Work + +These enhancements build on the successful refactoring of the REPL functionality to use terminal commands. We've already: + +1. Removed the specialized REPL manager +2. Enhanced the terminal manager to handle interactive sessions +3. Updated tests to verify the refactored approach +4. Created documentation and examples showing how to use terminal commands for REPLs + +The current changes will make these terminal commands even more effective for REPL environments by handling timeouts, waiting for responses, and detecting REPL prompts. diff --git a/docs/repl-with-terminal.md b/docs/repl-with-terminal.md new file mode 100644 index 0000000..c704743 --- /dev/null +++ b/docs/repl-with-terminal.md @@ -0,0 +1,214 @@ +# Using Terminal Commands for REPL Environments + +This document explains how to use the standard terminal commands to interact with REPL (Read-Eval-Print Loop) environments like Python, Node.js, Ruby, PHP, and others. + +## Overview + +Instead of having specialized REPL tools, the ClaudeServerCommander uses the standard terminal commands to interact with any interactive environment. This approach is: + +- **Simple**: No need for language-specific configurations +- **Flexible**: Works with any REPL environment without special handling +- **Consistent**: Uses the same interface for all interactive sessions + +## Basic Workflow + +### 1. Starting a REPL Session + +Use the `execute_command` function to start a REPL environment in interactive mode: + +```javascript +// Start Python +const pythonResult = await executeCommand({ + command: 'python -i', // Use -i flag for interactive mode + timeout_ms: 10000 +}); + +// Start Node.js +const nodeResult = await executeCommand({ + command: 'node -i', // Use -i flag for interactive mode + timeout_ms: 10000 +}); + +// Extract PID from the result text +const pidMatch = result.content[0].text.match(/Command started with PID (\d+)/); +const pid = pidMatch ? parseInt(pidMatch[1]) : null; +``` + +### 2. Reading the Initial Prompt + +After starting a REPL session, you should read the initial output to capture the prompt: + +```javascript +// Wait for REPL to initialize +const initialOutput = await readOutput({ pid }); +console.log("Initial prompt:", initialOutput.content[0].text); +``` + +### 3. Sending Code to the REPL + +Use the `send_input` function to send code to the REPL, making sure to include a newline at the end: + +```javascript +// Send a single-line command +await sendInput({ + pid: pid, + input: 'print("Hello, world!")\n' // Python example +}); + +// Send multi-line code block +const multilineCode = ` +def greet(name): + return f"Hello, {name}!" + +print(greet("World")) +`; + +await sendInput({ + pid: pid, + input: multilineCode + '\n' // Add newline at the end +}); +``` + +### 4. Reading Output from the REPL + +Use the `read_output` function to get the results: + +```javascript +// Wait a moment for the REPL to process +await new Promise(resolve => setTimeout(resolve, 500)); + +// Read the output +const output = await readOutput({ pid }); +console.log("Output:", output.content[0].text); +``` + +### 5. Terminating the REPL Session + +When you're done, use `force_terminate` to end the session: + +```javascript +await forceTerminate({ pid }); +``` + +## Examples for Different REPL Environments + +### Python + +```javascript +// Start Python in interactive mode +const result = await executeCommand({ command: 'python -i' }); +const pidMatch = result.content[0].text.match(/Command started with PID (\d+)/); +const pid = pidMatch ? parseInt(pidMatch[1]) : null; + +// Read initial prompt +const initialOutput = await readOutput({ pid }); + +// Run code +await sendInput({ pid, input: 'print("Hello from Python!")\n' }); + +// Wait and read output +await new Promise(resolve => setTimeout(resolve, 500)); +const output = await readOutput({ pid }); +``` + +### Node.js + +```javascript +// Start Node.js in interactive mode +const result = await executeCommand({ command: 'node -i' }); +const pidMatch = result.content[0].text.match(/Command started with PID (\d+)/); +const pid = pidMatch ? parseInt(pidMatch[1]) : null; + +// Read initial prompt +const initialOutput = await readOutput({ pid }); + +// Run code +await sendInput({ pid, input: 'console.log("Hello from Node.js!")\n' }); + +// Wait and read output +await new Promise(resolve => setTimeout(resolve, 500)); +const output = await readOutput({ pid }); +``` + +### Ruby + +```javascript +// Start Ruby in interactive mode +const result = await executeCommand({ command: 'irb' }); +const pidMatch = result.content[0].text.match(/Command started with PID (\d+)/); +const pid = pidMatch ? parseInt(pidMatch[1]) : null; + +// Read initial prompt +const initialOutput = await readOutput({ pid }); + +// Run code +await sendInput({ pid, input: 'puts "Hello from Ruby!"\n' }); + +// Wait and read output +await new Promise(resolve => setTimeout(resolve, 500)); +const output = await readOutput({ pid }); +``` + +### PHP + +```javascript +// Start PHP in interactive mode +const result = await executeCommand({ command: 'php -a' }); +const pidMatch = result.content[0].text.match(/Command started with PID (\d+)/); +const pid = pidMatch ? parseInt(pidMatch[1]) : null; + +// Read initial prompt +const initialOutput = await readOutput({ pid }); + +// Run code +await sendInput({ pid, input: 'echo "Hello from PHP!";\n' }); + +// Wait and read output +await new Promise(resolve => setTimeout(resolve, 500)); +const output = await readOutput({ pid }); +``` + +## Tips and Best Practices + +1. **Always Use Interactive Mode**: Many interpreters have a specific flag for interactive mode: + - Python: `-i` flag + - Node.js: `-i` flag + - PHP: `-a` flag + +2. **Add Newlines to Input**: Always add a newline character at the end of your input to trigger execution: + + ```javascript + await sendInput({ pid, input: 'your_code_here\n' }); + ``` + +3. **Add Delays Between Operations**: Most REPLs need time to process input. Adding a small delay between sending input and reading output helps ensure you get the complete response: + + ```javascript + await sendInput({ pid, input: complexCode + '\n' }); + await new Promise(resolve => setTimeout(resolve, 500)); // 500ms delay + const output = await readOutput({ pid }); + ``` + +4. **Multi-line Code Handling**: Most REPLs can handle multi-line code blocks sent at once, but be sure to add a newline at the end: + + ```javascript + await sendInput({ + pid, + input: multilineCodeBlock + '\n' + }); + ``` + +5. **Error Handling**: Check the output for error messages: + + ```javascript + const output = await readOutput({ pid }); + const text = output.content[0].text; + if (text.includes('Error') || text.includes('Exception')) { + console.error('REPL returned an error:', text); + } + ``` + +## Complete Example + +See the file `test/repl-via-terminal-example.js` for a complete example showing how to interact with Python and Node.js REPLs using terminal commands. + diff --git a/src/handlers/terminal-handlers.ts b/src/handlers/terminal-handlers.ts index 51400d7..e98882d 100644 --- a/src/handlers/terminal-handlers.ts +++ b/src/handlers/terminal-handlers.ts @@ -4,12 +4,14 @@ import { forceTerminate, listSessions } from '../tools/execute.js'; +import { sendInput } from '../tools/send-input.js'; import { ExecuteCommandArgsSchema, ReadOutputArgsSchema, ForceTerminateArgsSchema, - ListSessionsArgsSchema + ListSessionsArgsSchema, + SendInputArgsSchema } from '../tools/schemas.js'; import { ServerResult } from '../types.js'; @@ -44,3 +46,10 @@ export async function handleForceTerminate(args: unknown): Promise export async function handleListSessions(): Promise { return listSessions(); } + +/** + * Handle send_input command + */ +export async function handleSendInput(args: unknown): Promise { + return sendInput(args); +} diff --git a/src/server.ts b/src/server.ts index f8635e3..90b4a3b 100644 --- a/src/server.ts +++ b/src/server.ts @@ -30,6 +30,7 @@ import { SetConfigValueArgsSchema, ListProcessesArgsSchema, EditBlockArgsSchema, + SendInputArgsSchema, } from './tools/schemas.js'; import {getConfig, setConfigValue} from './tools/config.js'; @@ -192,6 +193,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { description: "Read new output from a running terminal session.", inputSchema: zodToJsonSchema(ReadOutputArgsSchema), }, + { + name: "send_input", + description: "Send input to a running terminal session. Ideal for interactive REPL environments like Python, Node.js, or any other shell that expects user input.", + inputSchema: zodToJsonSchema(SendInputArgsSchema), + }, { name: "force_terminate", description: "Force terminate a running terminal session.", @@ -212,6 +218,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { description: "Terminate a running process by PID. Use with caution as this will forcefully terminate the specified process.", inputSchema: zodToJsonSchema(KillProcessArgsSchema), }, + + // Note: For interactive programming environments (REPLs) like Python or Node.js, + // use execute_command to start the session, send_input to send code, + // and read_output to get the results. For example: + // execute_command("python") to start Python + // send_input(pid, "print('Hello world')") to run code + // read_output(pid) to see the results ], }; } catch (error) { @@ -260,6 +273,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest) case "read_output": return await handlers.handleReadOutput(args); + + case "send_input": + return await handlers.handleSendInput(args); case "force_terminate": return await handlers.handleForceTerminate(args); @@ -274,6 +290,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest) case "kill_process": return await handlers.handleKillProcess(args); + // Note: REPL functionality removed in favor of using general terminal commands + // Filesystem tools case "read_file": return await handlers.handleReadFile(args); diff --git a/src/terminal-manager.ts b/src/terminal-manager.ts index e6c164a..b54d8de 100644 --- a/src/terminal-manager.ts +++ b/src/terminal-manager.ts @@ -16,6 +16,32 @@ export class TerminalManager { private sessions: Map = new Map(); private completedSessions: Map = new Map(); + /** + * Send input to a running process + * @param pid Process ID + * @param input Text to send to the process + * @returns Whether input was successfully sent + */ + sendInputToProcess(pid: number, input: string): boolean { + const session = this.sessions.get(pid); + if (!session) { + return false; + } + + try { + if (session.process.stdin && !session.process.stdin.destroyed) { + // Ensure input ends with a newline for most REPLs + const inputWithNewline = input.endsWith('\n') ? input : input + '\n'; + session.process.stdin.write(inputWithNewline); + return true; + } + return false; + } catch (error) { + console.error(`Error sending input to process ${pid}:`, error); + return false; + } + } + async executeCommand(command: string, timeoutMs: number = DEFAULT_COMMAND_TIMEOUT, shell?: string): Promise { // Get the shell from config if not specified let shellToUse: string | boolean | undefined = shell; @@ -29,10 +55,13 @@ export class TerminalManager { } } + // For REPL interactions, we need to ensure stdin, stdout, and stderr are properly configured + // Note: No special stdio options needed here, Node.js handles pipes by default const spawnOptions = { shell: shellToUse }; + // Spawn the process with an empty array of arguments and our options const process = spawn(command, [], spawnOptions); let output = ''; @@ -160,6 +189,15 @@ export class TerminalManager { listCompletedSessions(): CompletedSession[] { return Array.from(this.completedSessions.values()); } + + /** + * Get a session by PID + * @param pid Process ID + * @returns The session or undefined if not found + */ + getSession(pid: number): TerminalSession | undefined { + return this.sessions.get(pid); + } } export const terminalManager = new TerminalManager(); \ No newline at end of file diff --git a/src/tools/schemas.ts b/src/tools/schemas.ts index 3c71218..a3597b8 100644 --- a/src/tools/schemas.ts +++ b/src/tools/schemas.ts @@ -22,6 +22,7 @@ export const ExecuteCommandArgsSchema = z.object({ export const ReadOutputArgsSchema = z.object({ pid: z.number(), + timeout_ms: z.number().optional(), }); export const ForceTerminateArgsSchema = z.object({ @@ -90,4 +91,12 @@ export const EditBlockArgsSchema = z.object({ old_string: z.string(), new_string: z.string(), expected_replacements: z.number().optional().default(1), +}); + +// Send input to process schema +export const SendInputArgsSchema = z.object({ + pid: z.number(), + input: z.string(), + timeout_ms: z.number().optional(), + wait_for_prompt: z.boolean().optional(), }); \ No newline at end of file diff --git a/src/tools/send-input.ts b/src/tools/send-input.ts new file mode 100644 index 0000000..d682701 --- /dev/null +++ b/src/tools/send-input.ts @@ -0,0 +1,50 @@ +import { terminalManager } from '../terminal-manager.js'; +import { SendInputArgsSchema } from './schemas.js'; +import { capture } from "../utils/capture.js"; +import { ServerResult } from '../types.js'; + +export async function sendInput(args: unknown): Promise { + const parsed = SendInputArgsSchema.safeParse(args); + if (!parsed.success) { + capture('server_send_input_failed', { + error: 'Invalid arguments' + }); + return { + content: [{ type: "text", text: `Error: Invalid arguments for send_input: ${parsed.error}` }], + isError: true, + }; + } + + try { + capture('server_send_input', { + pid: parsed.data.pid, + inputLength: parsed.data.input.length + }); + + // Try to send input to the process + const success = terminalManager.sendInputToProcess(parsed.data.pid, parsed.data.input); + + if (!success) { + return { + content: [{ type: "text", text: `Error: Failed to send input to process ${parsed.data.pid}. The process may have exited or doesn't accept input.` }], + isError: true, + }; + } + + return { + content: [{ + type: "text", + text: `Successfully sent input to process ${parsed.data.pid}. Use read_output to get the process response.` + }], + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + capture('server_send_input_error', { + error: errorMessage + }); + return { + content: [{ type: "text", text: `Error sending input: ${errorMessage}` }], + isError: true, + }; + } +} diff --git a/test/enhanced-repl-example.js b/test/enhanced-repl-example.js new file mode 100644 index 0000000..6b726d1 --- /dev/null +++ b/test/enhanced-repl-example.js @@ -0,0 +1,150 @@ +/** + * This example demonstrates how to use the enhanced terminal commands + * for REPL (Read-Eval-Print Loop) environments. + */ + +import { + executeCommand, + readOutput, + forceTerminate +} from '../dist/tools/execute.js'; +import { sendInput } from '../dist/tools/enhanced-send-input.js'; + +// Example of starting and interacting with a Python REPL session +async function pythonREPLExample() { + console.log('Starting a Python REPL session...'); + + // Start Python interpreter in interactive mode + const result = await executeCommand({ + command: 'python -i', + timeout_ms: 10000 + }); + + // Extract PID from the result text + const pidMatch = result.content[0].text.match(/Command started with PID (\d+)/); + const pid = pidMatch ? parseInt(pidMatch[1]) : null; + + if (!pid) { + console.error("Failed to get PID from Python process"); + return; + } + + console.log(`Started Python session with PID: ${pid}`); + + // Initial read to get the Python prompt with timeout + console.log("Reading initial output..."); + const initialOutput = await readOutput({ + pid, + timeout_ms: 2000 + }); + console.log("Initial Python prompt:", initialOutput.content[0].text); + + // Send a simple Python command with wait_for_prompt + console.log("Sending simple command..."); + const simpleResult = await sendInput({ + pid, + input: 'print("Hello from Python!")\n', + wait_for_prompt: true, + timeout_ms: 3000 + }); + console.log('Python output with wait_for_prompt:', simpleResult.content[0].text); + + // Send a multi-line code block with wait_for_prompt + console.log("Sending multi-line code..."); + const multilineCode = ` +def greet(name): + return f"Hello, {name}!" + +for i in range(3): + print(greet(f"Guest {i+1}")) +`; + + const multilineResult = await sendInput({ + pid, + input: multilineCode + '\n', + wait_for_prompt: true, + timeout_ms: 5000 + }); + console.log('Python multi-line output with wait_for_prompt:', multilineResult.content[0].text); + + // Terminate the session + await forceTerminate({ pid }); + console.log('Python session terminated'); +} + +// Example of starting and interacting with a Node.js REPL session +async function nodeREPLExample() { + console.log('Starting a Node.js REPL session...'); + + // Start Node.js interpreter in interactive mode + const result = await executeCommand({ + command: 'node -i', + timeout_ms: 10000 + }); + + // Extract PID from the result text + const pidMatch = result.content[0].text.match(/Command started with PID (\d+)/); + const pid = pidMatch ? parseInt(pidMatch[1]) : null; + + if (!pid) { + console.error("Failed to get PID from Node.js process"); + return; + } + + console.log(`Started Node.js session with PID: ${pid}`); + + // Initial read to get the Node.js prompt with timeout + console.log("Reading initial output..."); + const initialOutput = await readOutput({ + pid, + timeout_ms: 2000 + }); + console.log("Initial Node.js prompt:", initialOutput.content[0].text); + + // Send a simple JavaScript command with wait_for_prompt + console.log("Sending simple command..."); + const simpleResult = await sendInput({ + pid, + input: 'console.log("Hello from Node.js!")\n', + wait_for_prompt: true, + timeout_ms: 3000 + }); + console.log('Node.js output with wait_for_prompt:', simpleResult.content[0].text); + + // Send a multi-line code block with wait_for_prompt + console.log("Sending multi-line code..."); + const multilineCode = ` +function greet(name) { + return \`Hello, \${name}!\`; +} + +for (let i = 0; i < 3; i++) { + console.log(greet(\`Guest \${i+1}\`)); +} +`; + + const multilineResult = await sendInput({ + pid, + input: multilineCode + '\n', + wait_for_prompt: true, + timeout_ms: 5000 + }); + console.log('Node.js multi-line output with wait_for_prompt:', multilineResult.content[0].text); + + // Terminate the session + await forceTerminate({ pid }); + console.log('Node.js session terminated'); +} + +// Run the examples +async function runExamples() { + try { + await pythonREPLExample(); + console.log('\n----------------------------\n'); + await nodeREPLExample(); + } catch (error) { + console.error('Error running examples:', error); + } +} + +runExamples(); diff --git a/test/repl-via-terminal-example.js b/test/repl-via-terminal-example.js new file mode 100644 index 0000000..8e9e34e --- /dev/null +++ b/test/repl-via-terminal-example.js @@ -0,0 +1,160 @@ +/** + * This example demonstrates how to use terminal commands to interact with a REPL environment + * without needing specialized REPL tools. + */ + +import { + executeCommand, + readOutput, + forceTerminate +} from '../dist/tools/execute.js'; +import { sendInput } from '../dist/tools/send-input.js'; + +// Example of starting and interacting with a Python REPL session +async function pythonREPLExample() { + console.log('Starting a Python REPL session...'); + + // Start Python interpreter in interactive mode + const result = await executeCommand({ + command: 'python -i', + timeout_ms: 10000 + }); + + // Extract PID from the result text + const pidMatch = result.content[0].text.match(/Command started with PID (\d+)/); + const pid = pidMatch ? parseInt(pidMatch[1]) : null; + + if (!pid) { + console.error("Failed to get PID from Python process"); + return; + } + + console.log(`Started Python session with PID: ${pid}`); + + // Initial read to get the Python prompt + console.log("Reading initial output..."); + const initialOutput = await readOutput({ pid }); + console.log("Initial Python prompt:", initialOutput.content[0].text); + + // Send a simple Python command + console.log("Sending simple command..."); + await sendInput({ + pid, + input: 'print("Hello from Python!")\n' + }); + + // Wait a moment for Python to process + await new Promise(resolve => setTimeout(resolve, 500)); + + // Read the output + const output = await readOutput({ pid }); + console.log('Python output:', output.content[0].text); + + // Send a multi-line code block + console.log("Sending multi-line code..."); + const multilineCode = ` +def greet(name): + return f"Hello, {name}!" + +for i in range(3): + print(greet(f"Guest {i+1}")) +`; + + await sendInput({ + pid, + input: multilineCode + '\n' + }); + + // Wait a moment for Python to process + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Read the output + const multilineOutput = await readOutput({ pid }); + console.log('Python multi-line output:', multilineOutput.content[0].text); + + // Terminate the session + await forceTerminate({ pid }); + console.log('Python session terminated'); +} + +// Example of starting and interacting with a Node.js REPL session +async function nodeREPLExample() { + console.log('Starting a Node.js REPL session...'); + + // Start Node.js interpreter in interactive mode + const result = await executeCommand({ + command: 'node -i', + timeout_ms: 10000 + }); + + // Extract PID from the result text + const pidMatch = result.content[0].text.match(/Command started with PID (\d+)/); + const pid = pidMatch ? parseInt(pidMatch[1]) : null; + + if (!pid) { + console.error("Failed to get PID from Node.js process"); + return; + } + + console.log(`Started Node.js session with PID: ${pid}`); + + // Initial read to get the Node.js prompt + console.log("Reading initial output..."); + const initialOutput = await readOutput({ pid }); + console.log("Initial Node.js prompt:", initialOutput.content[0].text); + + // Send a simple JavaScript command + console.log("Sending simple command..."); + await sendInput({ + pid, + input: 'console.log("Hello from Node.js!")\n' + }); + + // Wait a moment for Node.js to process + await new Promise(resolve => setTimeout(resolve, 500)); + + // Read the output + const output = await readOutput({ pid }); + console.log('Node.js output:', output.content[0].text); + + // Send a multi-line code block + console.log("Sending multi-line code..."); + const multilineCode = ` +function greet(name) { + return \`Hello, \${name}!\`; +} + +for (let i = 0; i < 3; i++) { + console.log(greet(\`Guest \${i+1}\`)); +} +`; + + await sendInput({ + pid, + input: multilineCode + '\n' + }); + + // Wait a moment for Node.js to process + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Read the output + const multilineOutput = await readOutput({ pid }); + console.log('Node.js multi-line output:', multilineOutput.content[0].text); + + // Terminate the session + await forceTerminate({ pid }); + console.log('Node.js session terminated'); +} + +// Run the examples +async function runExamples() { + try { + await pythonREPLExample(); + console.log('\n----------------------------\n'); + await nodeREPLExample(); + } catch (error) { + console.error('Error running examples:', error); + } +} + +runExamples(); diff --git a/test/simple-node-repl-test.js b/test/simple-node-repl-test.js new file mode 100644 index 0000000..e166ffd --- /dev/null +++ b/test/simple-node-repl-test.js @@ -0,0 +1,43 @@ + +import { replManager } from '../dist/repl-manager.js'; + +async function testNodeREPL() { + try { + console.log('Creating a Node.js REPL session...'); + const pid = await replManager.createSession('node', 5000); + console.log(`Created Node.js REPL session with PID ${pid}`); + + console.log('Executing a simple Node.js command...'); + const result = await replManager.executeCode(pid, 'console.log("Hello from Node.js!")', { + waitForPrompt: true, + timeout: 5000 + }); + console.log(`Result: ${JSON.stringify(result)}`); + + console.log('Executing a multi-line Node.js code block...'); + const nodeCode = ` +function greet(name) { + return \`Hello, \${name}!\`; +} + +console.log(greet("World")); +`; + + const result2 = await replManager.executeCode(pid, nodeCode, { + multiline: true, + timeout: 10000, + waitForPrompt: true + }); + console.log(`Multi-line result: ${JSON.stringify(result2)}`); + + console.log('Terminating the session...'); + const terminated = await replManager.terminateSession(pid); + console.log(`Session terminated: ${terminated}`); + + console.log('Test completed successfully'); + } catch (error) { + console.error(`Test failed with error: ${error.message}`); + } +} + +testNodeREPL(); diff --git a/test/simple-python-test.js b/test/simple-python-test.js new file mode 100644 index 0000000..0bf1f38 --- /dev/null +++ b/test/simple-python-test.js @@ -0,0 +1,73 @@ +import { + executeCommand, + readOutput, + forceTerminate +} from '../dist/tools/execute.js'; +import { sendInput } from '../dist/tools/send-input.js'; + +async function simplePythonTest() { + try { + console.log("Starting Python with a simple command..."); + + // Run Python with a print command directly + const result = await executeCommand({ + command: 'python -c "print(\'Hello from Python\')"', + timeout_ms: 5000 + }); + + console.log("Result:", JSON.stringify(result, null, 2)); + + // Now let's try interactive mode + console.log("\nStarting Python in interactive mode..."); + const interactiveResult = await executeCommand({ + command: 'python -i', + timeout_ms: 5000 + }); + + console.log("Interactive result:", JSON.stringify(interactiveResult, null, 2)); + + // Extract PID from the result text + const pidMatch = interactiveResult.content[0].text.match(/Command started with PID (\d+)/); + const pid = pidMatch ? parseInt(pidMatch[1]) : null; + + if (!pid) { + console.error("Failed to get PID from Python process"); + return; + } + + console.log(`Started Python session with PID: ${pid}`); + + // Initial read to get the Python prompt + console.log("Reading initial output..."); + const initialOutput = await readOutput({ pid }); + console.log("Initial output:", JSON.stringify(initialOutput, null, 2)); + + // Send a simple Python command with explicit newline + console.log("Sending command..."); + const inputResult = await sendInput({ + pid, + input: 'print("Hello from interactive Python")\n' + }); + console.log("Input result:", JSON.stringify(inputResult, null, 2)); + + // Wait a moment for Python to process + console.log("Waiting for processing..."); + await new Promise(resolve => setTimeout(resolve, 500)); + + // Read the output + console.log("Reading output..."); + const output = await readOutput({ pid }); + console.log("Output:", JSON.stringify(output, null, 2)); + + // Terminate the session + console.log("Terminating session..."); + const terminateResult = await forceTerminate({ pid }); + console.log("Terminate result:", JSON.stringify(terminateResult, null, 2)); + + console.log("Test completed"); + } catch (error) { + console.error("Error in test:", error); + } +} + +simplePythonTest(); diff --git a/test/simple-repl-test.js b/test/simple-repl-test.js new file mode 100644 index 0000000..f2ec8b9 --- /dev/null +++ b/test/simple-repl-test.js @@ -0,0 +1,24 @@ + +import { replManager } from '../dist/repl-manager.js'; + +async function testBasicREPL() { + try { + console.log('Creating a Python REPL session...'); + const pid = await replManager.createSession('python', 5000); + console.log(`Created Python REPL session with PID ${pid}`); + + console.log('Executing a simple Python command...'); + const result = await replManager.executeCode(pid, 'print("Hello from Python!")'); + console.log(`Result: ${JSON.stringify(result)}`); + + console.log('Terminating the session...'); + const terminated = await replManager.terminateSession(pid); + console.log(`Session terminated: ${terminated}`); + + console.log('Test completed successfully'); + } catch (error) { + console.error(`Test failed with error: ${error.message}`); + } +} + +testBasicREPL(); diff --git a/test/test-node-repl.js b/test/test-node-repl.js new file mode 100644 index 0000000..427a3b6 --- /dev/null +++ b/test/test-node-repl.js @@ -0,0 +1,166 @@ +/** + * Specialized test for Node.js REPL interaction + * This test uses a direct approach with the child_process module + * to better understand and debug Node.js REPL behavior + */ + +import { spawn } from 'child_process'; +import fs from 'fs/promises'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +// Get directory name +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Colors for console output +const colors = { + reset: '\x1b[0m', + green: '\x1b[32m', + red: '\x1b[31m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + cyan: '\x1b[36m' +}; + +/** + * Sleep function + */ +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +/** + * Test Node.js REPL interaction directly + */ +async function testNodeREPL() { + console.log(`${colors.blue}Direct Node.js REPL test...${colors.reset}`); + + // Create output directory if it doesn't exist + const OUTPUT_DIR = path.join(__dirname, 'test_output'); + try { + await fs.mkdir(OUTPUT_DIR, { recursive: true }); + } catch (error) { + console.warn(`${colors.yellow}Warning: Could not create output directory: ${error.message}${colors.reset}`); + } + + // File for debugging output + const debugFile = path.join(OUTPUT_DIR, 'node_repl_debug.txt'); + let debugLog = ''; + + // Log both to console and to file + function log(message) { + console.log(message); + debugLog += message + '\n'; + } + + // Start Node.js REPL + log(`${colors.blue}Starting Node.js REPL...${colors.reset}`); + + // Use the -i flag to ensure interactive mode + const node = spawn('node', ['-i']); + + // Track all output + let outputBuffer = ''; + + // Set up output listeners + node.stdout.on('data', (data) => { + const text = data.toString(); + outputBuffer += text; + log(`${colors.green}[STDOUT] ${text.trim()}${colors.reset}`); + }); + + node.stderr.on('data', (data) => { + const text = data.toString(); + outputBuffer += text; + log(`${colors.red}[STDERR] ${text.trim()}${colors.reset}`); + }); + + // Set up exit handler + node.on('exit', (code) => { + log(`${colors.blue}Node.js process exited with code ${code}${colors.reset}`); + + // Write debug log to file after exit + fs.writeFile(debugFile, debugLog).catch(err => { + console.error(`Failed to write debug log: ${err.message}`); + }); + }); + + // Wait for Node.js to initialize + log(`${colors.blue}Waiting for Node.js startup...${colors.reset}`); + await sleep(2000); + + // Log initial state + log(`${colors.blue}Initial output buffer: ${outputBuffer}${colors.reset}`); + + // Send a simple command + log(`${colors.blue}Sending simple command...${colors.reset}`); + node.stdin.write('console.log("Hello from Node.js!");\n'); + + // Wait for command to execute + await sleep(2000); + + // Log state after first command + log(`${colors.blue}Output after first command: ${outputBuffer}${colors.reset}`); + + // Send a multi-line command directly + log(`${colors.blue}Sending multi-line command directly...${colors.reset}`); + + // Define the multi-line code + const multilineCode = ` +function greet(name) { + return \`Hello, \${name}!\`; +} + +for (let i = 0; i < 3; i++) { + console.log(greet(\`User \${i}\`)); +} +`; + + log(`${colors.blue}Sending code:${colors.reset}\n${multilineCode}`); + + // Send the multi-line code directly + node.stdin.write(multilineCode + '\n'); + + + // Wait for execution + await sleep(3000); + + // Log final state + log(`${colors.blue}Final output buffer: ${outputBuffer}${colors.reset}`); + + // Check if we got the expected output + const containsHello = outputBuffer.includes('Hello from Node.js!'); + const containsGreetings = + outputBuffer.includes('Hello, User 0!') && + outputBuffer.includes('Hello, User 1!') && + outputBuffer.includes('Hello, User 2!'); + + log(`${colors.blue}Found "Hello from Node.js!": ${containsHello}${colors.reset}`); + log(`${colors.blue}Found greetings: ${containsGreetings}${colors.reset}`); + + // Terminate the process + log(`${colors.blue}Terminating Node.js process...${colors.reset}`); + node.stdin.end(); + + // Wait for process to exit + await sleep(1000); + + // Return success status + return containsHello && containsGreetings; +} + +// Run the test +testNodeREPL() + .then(success => { + console.log(`\n${colors.blue}Direct Node.js REPL test ${success ? colors.green + 'PASSED' : colors.red + 'FAILED'}${colors.reset}`); + + // Print file location for debug log + console.log(`${colors.blue}Debug log saved to: ${path.join(__dirname, 'test_output', 'node_repl_debug.txt')}${colors.reset}`); + + process.exit(success ? 0 : 1); + }) + .catch(error => { + console.error(`${colors.red}Test error: ${error.message}${colors.reset}`); + process.exit(1); + }); diff --git a/test/test-repl-interaction.js b/test/test-repl-interaction.js new file mode 100644 index 0000000..4ae563e --- /dev/null +++ b/test/test-repl-interaction.js @@ -0,0 +1,248 @@ + +import path from 'path'; +import { fileURLToPath } from 'url'; +import fs from 'fs/promises'; +import { configManager } from '../dist/config-manager.js'; +import { terminalManager } from '../dist/terminal-manager.js'; + +// Get directory name +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Test output directory +const OUTPUT_DIR = path.join(__dirname, 'test_output'); +const OUTPUT_FILE = path.join(OUTPUT_DIR, 'repl_test_output.txt'); + +// Colors for console output +const colors = { + reset: '\x1b[0m', + green: '\x1b[32m', + red: '\x1b[31m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + cyan: '\x1b[36m' +}; + +/** + * Setup function to prepare for tests + */ +async function setup() { + console.log(`${colors.blue}Setting up REPL interaction test...${colors.reset}`); + + // Save original config to restore later + const originalConfig = await configManager.getConfig(); + + // Create output directory if it doesn't exist + try { + await fs.mkdir(OUTPUT_DIR, { recursive: true }); + } catch (error) { + console.warn(`${colors.yellow}Warning: Could not create output directory: ${error.message}${colors.reset}`); + } + + return originalConfig; +} + +/** + * Teardown function to clean up after tests + */ +async function teardown(originalConfig) { + console.log(`${colors.blue}Cleaning up after tests...${colors.reset}`); + + // Reset configuration to original + await configManager.updateConfig(originalConfig); + + console.log(`${colors.green}✓ Teardown: config restored${colors.reset}`); +} + +/** + * Wait for the specified number of milliseconds + */ +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +/** + * Test Python REPL interaction + */ +async function testPythonREPL() { + console.log(`${colors.cyan}Running Python REPL interaction test...${colors.reset}`); + + try { + // Setup Python test + // Find Python executable + const pythonCmd = process.platform === 'win32' ? 'python' : 'python3'; + + // Start a Python REPL process + const result = await terminalManager.executeCommand(pythonCmd + ' -i', 5000); + + if (result.pid <= 0) { + throw new Error(`Failed to start Python REPL: ${result.output}`); + } + + console.log(`${colors.green}✓ Started Python REPL with PID ${result.pid}${colors.reset}`); + + // Wait for REPL to initialize + await sleep(1000); + + // Send a command to the REPL with explicit Python print + const testValue = Math.floor(Math.random() * 100); + console.log(`${colors.blue}Test value: ${testValue}${colors.reset}`); + + // Send two different commands to increase chances of seeing output + let success = terminalManager.sendInputToProcess(result.pid, `print("STARTING PYTHON TEST")\n`) + if (!success) { + throw new Error('Failed to send initial input to Python REPL'); + } + + // Wait a bit between commands + await sleep(1000); + + // Send the actual test command + success = terminalManager.sendInputToProcess(result.pid, `print(f"REPL_TEST_VALUE: {${testValue} * 2}")\n`) + if (!success) { + throw new Error('Failed to send test input to Python REPL'); + } + + console.log(`${colors.green}✓ Sent test commands to Python REPL${colors.reset}`); + + // Wait longer for the command to execute + await sleep(3000); + + // Get output from the REPL + const output = terminalManager.getNewOutput(result.pid); + console.log(`Python REPL output: ${output || 'No output received'}`); + + // Write output to file for inspection + await fs.writeFile(OUTPUT_FILE, `Python REPL output:\n${output || 'No output received'}`); + + // Terminate the REPL process + const terminated = terminalManager.forceTerminate(result.pid); + if (!terminated) { + console.warn(`${colors.yellow}Warning: Could not terminate Python REPL process${colors.reset}`); + } else { + console.log(`${colors.green}✓ Terminated Python REPL process${colors.reset}`); + } + + // Check if we got the expected output + if (output && output.includes(`REPL_TEST_VALUE: ${testValue * 2}`)) { + console.log(`${colors.green}✓ Python REPL test passed!${colors.reset}`); + return true; + } else { + console.log(`${colors.red}✗ Python REPL test failed: Expected output containing "REPL_TEST_VALUE: ${testValue * 2}" but got: ${output}${colors.reset}`); + return false; + } + } catch (error) { + console.error(`${colors.red}✗ Python REPL test error: ${error.message}${colors.reset}`); + return false; + } +} + +/** + * Test Node.js REPL interaction + */ +async function testNodeREPL() { + console.log(`${colors.cyan}Running Node.js REPL interaction test...${colors.reset}`); + + try { + // Start a Node.js REPL process + const result = await terminalManager.executeCommand('node -i', 5000); + + if (result.pid <= 0) { + throw new Error(`Failed to start Node.js REPL: ${result.output}`); + } + + console.log(`${colors.green}✓ Started Node.js REPL with PID ${result.pid}${colors.reset}`); + + // Wait for REPL to initialize + await sleep(1000); + + // Send commands to the Node.js REPL + const testValue = Math.floor(Math.random() * 100); + console.log(`${colors.blue}Test value: ${testValue}${colors.reset}`); + + // Send multiple commands to increase chances of seeing output + let success = terminalManager.sendInputToProcess(result.pid, `console.log("STARTING NODE TEST")\n`) + if (!success) { + throw new Error('Failed to send initial input to Node.js REPL'); + } + + // Wait a bit between commands + await sleep(1000); + + // Send the actual test command + success = terminalManager.sendInputToProcess(result.pid, `console.log("NODE_REPL_TEST_VALUE:", ${testValue} * 3)\n`) + if (!success) { + throw new Error('Failed to send test input to Node.js REPL'); + } + + console.log(`${colors.green}✓ Sent test commands to Node.js REPL${colors.reset}`); + + // Wait longer for the command to execute + await sleep(3000); + + // Get output from the REPL + const output = terminalManager.getNewOutput(result.pid); + console.log(`Node.js REPL output: ${output || 'No output received'}`); + + // Append output to file for inspection + await fs.appendFile(OUTPUT_FILE, `\n\nNode.js REPL output:\n${output || 'No output received'}`); + + // Terminate the REPL process + const terminated = terminalManager.forceTerminate(result.pid); + if (!terminated) { + console.warn(`${colors.yellow}Warning: Could not terminate Node.js REPL process${colors.reset}`); + } else { + console.log(`${colors.green}✓ Terminated Node.js REPL process${colors.reset}`); + } + + // Check if we got the expected output + if (output && output.includes(`NODE_REPL_TEST_VALUE: ${testValue * 3}`)) { + console.log(`${colors.green}✓ Node.js REPL test passed!${colors.reset}`); + return true; + } else { + console.log(`${colors.red}✗ Node.js REPL test failed: Expected output containing "NODE_REPL_TEST_VALUE: ${testValue * 3}" but got: ${output}${colors.reset}`); + return false; + } + } catch (error) { + console.error(`${colors.red}✗ Node.js REPL test error: ${error.message}${colors.reset}`); + return false; + } +} + +/** + * Run all REPL interaction tests + */ +export default async function runTests() { + let originalConfig; + try { + originalConfig = await setup(); + + const pythonTestResult = await testPythonREPL(); + const nodeTestResult = await testNodeREPL(); + + // Overall test result + const allPassed = pythonTestResult && nodeTestResult; + + console.log(`\n${colors.cyan}===== REPL Interaction Test Summary =====\n${colors.reset}`); + console.log(`Python REPL test: ${pythonTestResult ? colors.green + 'PASSED' : colors.red + 'FAILED'}${colors.reset}`); + console.log(`Node.js REPL test: ${nodeTestResult ? colors.green + 'PASSED' : colors.red + 'FAILED'}${colors.reset}`); + console.log(`\nOverall result: ${allPassed ? colors.green + 'ALL TESTS PASSED! 🎉' : colors.red + 'SOME TESTS FAILED!'}${colors.reset}`); + + return allPassed; + } catch (error) { + console.error(`${colors.red}✗ Test execution error: ${error.message}${colors.reset}`); + return false; + } finally { + if (originalConfig) { + await teardown(originalConfig); + } + } +} + +// If this file is run directly (not imported), execute the test +if (import.meta.url === `file://${process.argv[1]}`) { + runTests().catch(error => { + console.error(`${colors.red}✗ Unhandled error: ${error}${colors.reset}`); + process.exit(1); + }); +} diff --git a/test/test-repl-tools.js b/test/test-repl-tools.js new file mode 100644 index 0000000..9dfdfc8 --- /dev/null +++ b/test/test-repl-tools.js @@ -0,0 +1,273 @@ +/** + * Test file for REPL tools in Desktop Commander + * This tests the new REPL session management and interactive code execution + */ + +import { replManager } from '../dist/repl-manager.js'; +import { configManager } from '../dist/config-manager.js'; +import path from 'path'; +import fs from 'fs/promises'; +import { fileURLToPath } from 'url'; + +// Get directory name +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Test output directory +const OUTPUT_DIR = path.join(__dirname, 'test_output'); +const OUTPUT_FILE = path.join(OUTPUT_DIR, 'repl_test_output.txt'); + +// Colors for console output +const colors = { + reset: '\x1b[0m', + green: '\x1b[32m', + red: '\x1b[31m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + cyan: '\x1b[36m' +}; + +/** + * Sleep function + */ +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +/** + * Setup function to prepare for tests + */ +async function setup() { + console.log(`${colors.blue}Setting up REPL Tools test...${colors.reset}`); + + // Save original config to restore later + const originalConfig = await configManager.getConfig(); + + // Create output directory if it doesn't exist + try { + await fs.mkdir(OUTPUT_DIR, { recursive: true }); + } catch (error) { + console.warn(`${colors.yellow}Warning: Could not create output directory: ${error.message}${colors.reset}`); + } + + return originalConfig; +} + +/** + * Teardown function to clean up after tests + */ +async function teardown(originalConfig) { + console.log(`${colors.blue}Cleaning up after tests...${colors.reset}`); + + // Reset configuration to original + await configManager.updateConfig(originalConfig); + + console.log(`${colors.green}✓ Teardown: config restored${colors.reset}`); +} + +/** + * Test Python REPL session + */ +async function testPythonREPL() { + console.log(`${colors.cyan}Running Python REPL test...${colors.reset}`); + + try { + // Create a Python REPL session + const pid = await replManager.createSession('python', 5000); + console.log(`${colors.green}✓ Created Python REPL session with PID ${pid}${colors.reset}`); + + // Execute a simple Python command + console.log(`${colors.blue}Executing simple Python command...${colors.reset}`); + const result1 = await replManager.executeCode(pid, 'print("Hello from Python!")'); + console.log(`${colors.blue}Result: ${JSON.stringify(result1)}${colors.reset}`); + + // Wait a bit to allow the REPL to process + await sleep(1000); + + // Execute a multi-line Python code block + console.log(`${colors.blue}Executing multi-line Python code block...${colors.reset}`); + const pythonCode = ` +def greet(name): + return f"Hello, {name}!" + +for i in range(3): + print(greet(f"User {i}")) +`; + + const result2 = await replManager.executeCode(pid, pythonCode, { + multiline: true, + timeout: 5000 + }); + console.log(`${colors.blue}Result: ${JSON.stringify(result2)}${colors.reset}`); + + // Terminate the session + console.log(`${colors.blue}Terminating Python REPL session...${colors.reset}`); + const terminated = await replManager.terminateSession(pid); + console.log(`${colors.green}✓ Python session terminated: ${terminated}${colors.reset}`); + + // Check results + const pythonSuccess = result1.success && result2.success; + if (!pythonSuccess) { + console.log(`${colors.red}× Python REPL test failed${colors.reset}`); + } else { + console.log(`${colors.green}✓ Python REPL test passed${colors.reset}`); + } + + return pythonSuccess; + } catch (error) { + console.error(`${colors.red}× Python REPL test error: ${error.message}${colors.reset}`); + return false; + } +} + +/** + * Test Node.js REPL session + */ +async function testNodeREPL() { + console.log(`${colors.cyan}Running Node.js REPL test...${colors.reset}`); + + try { + // Create a Node.js REPL session + const pid = await replManager.createSession('node', 5000); + console.log(`${colors.green}✓ Created Node.js REPL session with PID ${pid}${colors.reset}`); + + // Execute a simple Node.js command + console.log(`${colors.blue}Executing simple Node.js command...${colors.reset}`); + const result1 = await replManager.executeCode(pid, 'console.log("Hello from Node.js!")', { + timeout: 5000, + waitForPrompt: true + }); + console.log(`${colors.blue}Result: ${JSON.stringify(result1)}${colors.reset}`); + + // Wait a bit to allow the REPL to process + await sleep(1000); + + // Execute a multi-line Node.js code block + console.log(`${colors.blue}Executing multi-line Node.js code block...${colors.reset}`); + const nodeCode = ` +function greet(name) { + return \`Hello, \${name}!\`; +} + +for (let i = 0; i < 3; i++) { + console.log(greet(\`User \${i}\`)); +} +`; + + const result2 = await replManager.executeCode(pid, nodeCode, { + multiline: true, + timeout: 10000, + waitForPrompt: true + }); + console.log(`${colors.blue}Result: ${JSON.stringify(result2)}${colors.reset}`); + + // Terminate the session + console.log(`${colors.blue}Terminating Node.js REPL session...${colors.reset}`); + const terminated = await replManager.terminateSession(pid); + console.log(`${colors.green}✓ Node.js session terminated: ${terminated}${colors.reset}`); + + // Check results + const nodeSuccess = result1.success && result2.success; + if (!nodeSuccess) { + console.log(`${colors.red}× Node.js REPL test failed${colors.reset}`); + } else { + console.log(`${colors.green}✓ Node.js REPL test passed${colors.reset}`); + } + + return nodeSuccess; + } catch (error) { + console.error(`${colors.red}× Node.js REPL test error: ${error.message}${colors.reset}`); + return false; + } +} + +/** + * Test session management functionality + */ +async function testSessionManagement() { + console.log(`${colors.cyan}Running session management test...${colors.reset}`); + + try { + // Create multiple sessions + const pid1 = await replManager.createSession('python', 5000); + const pid2 = await replManager.createSession('node', 5000); + console.log(`${colors.green}✓ Created multiple REPL sessions${colors.reset}`); + + // List active sessions + const sessions = replManager.listSessions(); + console.log(`${colors.blue}Active sessions: ${JSON.stringify(sessions)}${colors.reset}`); + + // Get session info + const info1 = replManager.getSessionInfo(pid1); + const info2 = replManager.getSessionInfo(pid2); + + console.log(`${colors.blue}Session ${pid1} info: ${JSON.stringify(info1)}${colors.reset}`); + console.log(`${colors.blue}Session ${pid2} info: ${JSON.stringify(info2)}${colors.reset}`); + + // Terminate sessions + await replManager.terminateSession(pid1); + await replManager.terminateSession(pid2); + console.log(`${colors.green}✓ Terminated all test sessions${colors.reset}`); + + // Check if sessions were properly created and info retrieved + const managementSuccess = + sessions.length >= 2 && + info1 !== null && + info2 !== null && + info1.language === 'python' && + info2.language === 'node'; + + if (!managementSuccess) { + console.log(`${colors.red}× Session management test failed${colors.reset}`); + } else { + console.log(`${colors.green}✓ Session management test passed${colors.reset}`); + } + + return managementSuccess; + } catch (error) { + console.error(`${colors.red}× Session management test error: ${error.message}${colors.reset}`); + return false; + } +} + +/** + * Run all REPL tools tests + */ +export default async function runTests() { + let originalConfig; + try { + originalConfig = await setup(); + + const pythonResult = await testPythonREPL(); + const nodeResult = await testNodeREPL(); + const managementResult = await testSessionManagement(); + + // Overall test result + const allPassed = pythonResult && nodeResult && managementResult; + + console.log(`\n${colors.cyan}===== REPL Tools Test Summary =====\n${colors.reset}`); + console.log(`Python REPL test: ${pythonResult ? colors.green + 'PASSED' : colors.red + 'FAILED'}${colors.reset}`); + console.log(`Node.js REPL test: ${nodeResult ? colors.green + 'PASSED' : colors.red + 'FAILED'}${colors.reset}`); + console.log(`Session management test: ${managementResult ? colors.green + 'PASSED' : colors.red + 'FAILED'}${colors.reset}`); + console.log(`\nOverall result: ${allPassed ? colors.green + 'ALL TESTS PASSED! 🎉' : colors.red + 'SOME TESTS FAILED!'}${colors.reset}`); + + return allPassed; + } catch (error) { + console.error(`${colors.red}✗ Test execution error: ${error.message}${colors.reset}`); + return false; + } finally { + if (originalConfig) { + await teardown(originalConfig); + } + } +} + +// If this file is run directly (not imported), execute the test +if (import.meta.url === `file://${process.argv[1]}`) { + runTests().catch(error => { + console.error(`${colors.red}✗ Unhandled error: ${error}${colors.reset}`); + process.exit(1); + }).then(success => { + process.exit(success ? 0 : 1); + }); +} diff --git a/test/test_output/node_repl_debug.txt b/test/test_output/node_repl_debug.txt new file mode 100644 index 0000000..6311ae9 --- /dev/null +++ b/test/test_output/node_repl_debug.txt @@ -0,0 +1,56 @@ +Starting Node.js REPL... +Waiting for Node.js startup... +[STDOUT] Welcome to Node.js v23.11.0. +Type ".help" for more information. +[STDOUT] > +Initial output buffer: Welcome to Node.js v23.11.0. +Type ".help" for more information. +>  +Sending simple command... +[STDOUT] Hello from Node.js! +[STDOUT] undefined +[STDOUT] > +Output after first command: Welcome to Node.js v23.11.0. +Type ".help" for more information. +> Hello from Node.js! +undefined +>  +Sending multi-line command directly... +Sending code: + +function greet(name) { + return `Hello, ${name}!`; +} + +for (let i = 0; i < 3; i++) { + console.log(greet(`User ${i}`)); +} + +[STDOUT] > +[STDOUT] ... +[STDOUT] ... +[STDOUT] undefined +[STDOUT] > +[STDOUT] > +[STDOUT] ... +[STDOUT] ... +[STDOUT] Hello, User 0! +[STDOUT] Hello, User 1! +Hello, User 2! +[STDOUT] undefined +[STDOUT] > +[STDOUT] > +Final output buffer: Welcome to Node.js v23.11.0. +Type ".help" for more information. +> Hello from Node.js! +undefined +> > ... ... undefined +> > ... ... Hello, User 0! +Hello, User 1! +Hello, User 2! +undefined +> >  +Found "Hello from Node.js!": true +Found greetings: true +Terminating Node.js process... +Node.js process exited with code 0 diff --git a/test/test_output/repl_test_output.txt b/test/test_output/repl_test_output.txt new file mode 100644 index 0000000..e591031 --- /dev/null +++ b/test/test_output/repl_test_output.txt @@ -0,0 +1,15 @@ +Python REPL output: +Python 3.12.0 (v3.12.0:0fb18b02c8, Oct 2 2023, 09:45:56) [Clang 13.0.0 (clang-1300.0.29.30)] on darwin +Type "help", "copyright", "credits" or "license" for more information. +>>> STARTING PYTHON TEST +>>> REPL_TEST_VALUE: 62 +>>> + +Node.js REPL output: +Welcome to Node.js v23.8.0. +Type ".help" for more information. +> STARTING NODE TEST +undefined +> NODE_REPL_TEST_VALUE: 27 +undefined +> \ No newline at end of file From d2ffa12418cb28b3e6efd7c39b9deb922ab016ee Mon Sep 17 00:00:00 2001 From: eduardruzga Date: Sat, 3 May 2025 09:34:36 +0300 Subject: [PATCH 02/13] bit more --- src/tools/enhanced-read-output.js | 69 ++++++++++++++++++ src/tools/enhanced-send-input.js | 111 +++++++++++++++++++++++++++++ test/test-enhanced-repl.js | 112 ++++++++++++++++++++++++++++++ 3 files changed, 292 insertions(+) create mode 100644 src/tools/enhanced-read-output.js create mode 100644 src/tools/enhanced-send-input.js create mode 100644 test/test-enhanced-repl.js diff --git a/src/tools/enhanced-read-output.js b/src/tools/enhanced-read-output.js new file mode 100644 index 0000000..1248ad0 --- /dev/null +++ b/src/tools/enhanced-read-output.js @@ -0,0 +1,69 @@ +import { terminalManager } from '../terminal-manager.js'; +import { ReadOutputArgsSchema } from './schemas.js'; + +export async function readOutput(args) { + const parsed = ReadOutputArgsSchema.safeParse(args); + if (!parsed.success) { + return { + content: [{ type: "text", text: `Error: Invalid arguments for read_output: ${parsed.error}` }], + isError: true, + }; + } + + const { pid, timeout_ms = 5000 } = parsed.data; + + // Check if the process exists + const session = terminalManager.getSession(pid); + if (!session) { + return { + content: [{ type: "text", text: `No session found for PID ${pid}` }], + isError: true, + }; + } + + // Wait for output with timeout + let output = ""; + let timeoutReached = false; + + try { + // Create a promise that resolves when new output is available or when timeout is reached + const outputPromise = new Promise((resolve) => { + // Check for initial output + const initialOutput = terminalManager.getNewOutput(pid); + if (initialOutput && initialOutput.length > 0) { + resolve(initialOutput); + return; + } + + // Setup an interval to poll for output + const interval = setInterval(() => { + const newOutput = terminalManager.getNewOutput(pid); + if (newOutput && newOutput.length > 0) { + clearInterval(interval); + resolve(newOutput); + } + }, 100); // Check every 100ms + + // Set a timeout to stop waiting + setTimeout(() => { + clearInterval(interval); + timeoutReached = true; + resolve(terminalManager.getNewOutput(pid) || ""); + }, timeout_ms); + }); + + output = await outputPromise; + } catch (error) { + return { + content: [{ type: "text", text: `Error reading output: ${error}` }], + isError: true, + }; + } + + return { + content: [{ + type: "text", + text: output || 'No new output available' + (timeoutReached ? ' (timeout reached)' : '') + }], + }; +} \ No newline at end of file diff --git a/src/tools/enhanced-send-input.js b/src/tools/enhanced-send-input.js new file mode 100644 index 0000000..5850b87 --- /dev/null +++ b/src/tools/enhanced-send-input.js @@ -0,0 +1,111 @@ +import { terminalManager } from '../terminal-manager.js'; +import { SendInputArgsSchema } from './schemas.js'; +import { capture } from "../utils/capture.js"; + +export async function sendInput(args) { + const parsed = SendInputArgsSchema.safeParse(args); + if (!parsed.success) { + capture('server_send_input_failed', { + error: 'Invalid arguments' + }); + return { + content: [{ type: "text", text: `Error: Invalid arguments for send_input: ${parsed.error}` }], + isError: true, + }; + } + + const { pid, input, timeout_ms = 5000, wait_for_prompt = false } = parsed.data; + + try { + capture('server_send_input', { + pid: pid, + inputLength: input.length + }); + + // Try to send input to the process + const success = terminalManager.sendInputToProcess(pid, input); + + if (!success) { + return { + content: [{ type: "text", text: `Error: Failed to send input to process ${pid}. The process may have exited or doesn't accept input.` }], + isError: true, + }; + } + + // If we don't need to wait for output, return immediately + if (!wait_for_prompt) { + return { + content: [{ + type: "text", + text: `Successfully sent input to process ${pid}. Use read_output to get the process response.` + }], + }; + } + + // Wait for output with timeout + let output = ""; + let timeoutReached = false; + + try { + // Create a promise that resolves when new output is available or when timeout is reached + const outputPromise = new Promise((resolve) => { + // Setup an interval to poll for output + const interval = setInterval(() => { + const newOutput = terminalManager.getNewOutput(pid); + + if (newOutput && newOutput.length > 0) { + output += newOutput; + + // Check if output contains a prompt pattern (indicating the REPL is ready for more input) + const promptPatterns = [/^>\s*$/, /^>>>\s*$/, /^\.{3}\s*$/]; // Common REPL prompts + const lines = output.split('\n'); + const lastLine = lines[lines.length - 1]; + const hasPrompt = promptPatterns.some(pattern => pattern.test(lastLine.trim())); + + if (hasPrompt) { + clearInterval(interval); + resolve(output); + } + } + }, 100); // Check every 100ms + + // Set a timeout to stop waiting + setTimeout(() => { + clearInterval(interval); + timeoutReached = true; + + // Get any final output + const finalOutput = terminalManager.getNewOutput(pid); + if (finalOutput) { + output += finalOutput; + } + + resolve(output); + }, timeout_ms); + }); + + await outputPromise; + } catch (error) { + return { + content: [{ type: "text", text: `Error reading output after sending input: ${error}` }], + isError: true, + }; + } + + return { + content: [{ + type: "text", + text: `Input sent to process ${pid}.\n\nOutput received:\n${output || '(No output)'}${timeoutReached ? ' (timeout reached)' : ''}` + }], + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + capture('server_send_input_error', { + error: errorMessage + }); + return { + content: [{ type: "text", text: `Error sending input: ${errorMessage}` }], + isError: true, + }; + } +} \ No newline at end of file diff --git a/test/test-enhanced-repl.js b/test/test-enhanced-repl.js new file mode 100644 index 0000000..eb782c1 --- /dev/null +++ b/test/test-enhanced-repl.js @@ -0,0 +1,112 @@ +import assert from 'assert'; +import { executeCommand, readOutput, forceTerminate } from '../dist/tools/execute.js'; +import { sendInput } from '../dist/tools/enhanced-send-input.js'; + +/** + * Test enhanced REPL functionality + */ +async function testEnhancedREPL() { + console.log('Testing enhanced REPL functionality...'); + + // Start Python in interactive mode + console.log('Starting Python REPL...'); + const result = await executeCommand({ + command: 'python -i', + timeout_ms: 10000 + }); + + // Extract PID from the result text + const pidMatch = result.content[0].text.match(/Command started with PID (\d+)/); + const pid = pidMatch ? parseInt(pidMatch[1]) : null; + + if (!pid) { + console.error("Failed to get PID from Python process"); + return false; + } + + console.log(`Started Python session with PID: ${pid}`); + + // Test read_output with timeout + console.log('Testing read_output with timeout...'); + const initialOutput = await readOutput({ + pid, + timeout_ms: 2000 + }); + console.log('Initial Python prompt:', initialOutput.content[0].text); + + // Test send_input with wait_for_prompt + console.log('Testing send_input with wait_for_prompt...'); + const inputResult = await sendInput({ + pid, + input: 'print("Hello from Python with wait!")\n', + wait_for_prompt: true, + timeout_ms: 5000 + }); + console.log('Python output with wait_for_prompt:', inputResult.content[0].text); + + // Check that the output contains the expected text + assert(inputResult.content[0].text.includes('Hello from Python with wait!'), + 'Output should contain the printed message'); + + // Test send_input without wait_for_prompt + console.log('Testing send_input without wait_for_prompt...'); + await sendInput({ + pid, + input: 'print("Hello from Python without wait!")\n', + wait_for_prompt: false + }); + + // Wait a moment for Python to process + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Read the output + const output = await readOutput({ pid }); + console.log('Python output without wait_for_prompt:', output.content[0].text); + + // Check that the output contains the expected text + assert(output.content[0].text.includes('Hello from Python without wait!'), + 'Output should contain the printed message'); + + // Test multi-line code with wait_for_prompt + console.log('Testing multi-line code with wait_for_prompt...'); + const multilineCode = ` +def greet(name): + return f"Hello, {name}!" + +for i in range(3): + print(greet(f"Guest {i+1}")) +`; + + const multilineResult = await sendInput({ + pid, + input: multilineCode + '\n', + wait_for_prompt: true, + timeout_ms: 5000 + }); + console.log('Python multi-line output with wait_for_prompt:', multilineResult.content[0].text); + + // Check that the output contains all three greetings + assert(multilineResult.content[0].text.includes('Hello, Guest 1!'), + 'Output should contain greeting for Guest 1'); + assert(multilineResult.content[0].text.includes('Hello, Guest 2!'), + 'Output should contain greeting for Guest 2'); + assert(multilineResult.content[0].text.includes('Hello, Guest 3!'), + 'Output should contain greeting for Guest 3'); + + // Terminate the session + await forceTerminate({ pid }); + console.log('Python session terminated'); + + return true; +} + +// Run the test +testEnhancedREPL() + .then(success => { + console.log(`Enhanced REPL test ${success ? 'PASSED' : 'FAILED'}`); + process.exit(success ? 0 : 1); + }) + .catch(error => { + console.error('Test error:', error); + process.exit(1); + }); From 38c0a6ee00ce970543ab03f0ed59f74c5fe85520 Mon Sep 17 00:00:00 2001 From: eduardruzga Date: Sun, 4 May 2025 20:33:09 +0300 Subject: [PATCH 03/13] bit more changes --- test/test-enhanced-repl.js | 68 ++++++-------------------------------- 1 file changed, 11 insertions(+), 57 deletions(-) diff --git a/test/test-enhanced-repl.js b/test/test-enhanced-repl.js index eb782c1..3557839 100644 --- a/test/test-enhanced-repl.js +++ b/test/test-enhanced-repl.js @@ -1,6 +1,6 @@ import assert from 'assert'; import { executeCommand, readOutput, forceTerminate } from '../dist/tools/execute.js'; -import { sendInput } from '../dist/tools/enhanced-send-input.js'; +import { sendInput } from '../dist/tools/send-input.js'; /** * Test enhanced REPL functionality @@ -15,6 +15,8 @@ async function testEnhancedREPL() { timeout_ms: 10000 }); + console.log('Result from execute_command:', result); + // Extract PID from the result text const pidMatch = result.content[0].text.match(/Command started with PID (\d+)/); const pid = pidMatch ? parseInt(pidMatch[1]) : null; @@ -26,74 +28,26 @@ async function testEnhancedREPL() { console.log(`Started Python session with PID: ${pid}`); - // Test read_output with timeout - console.log('Testing read_output with timeout...'); - const initialOutput = await readOutput({ - pid, - timeout_ms: 2000 - }); - console.log('Initial Python prompt:', initialOutput.content[0].text); + // We'll stick to using the existing tools for now to test the basic functionality - // Test send_input with wait_for_prompt - console.log('Testing send_input with wait_for_prompt...'); - const inputResult = await sendInput({ - pid, - input: 'print("Hello from Python with wait!")\n', - wait_for_prompt: true, - timeout_ms: 5000 - }); - console.log('Python output with wait_for_prompt:', inputResult.content[0].text); - - // Check that the output contains the expected text - assert(inputResult.content[0].text.includes('Hello from Python with wait!'), - 'Output should contain the printed message'); - - // Test send_input without wait_for_prompt - console.log('Testing send_input without wait_for_prompt...'); + // Send a simple Python command + console.log("Sending simple command..."); await sendInput({ pid, - input: 'print("Hello from Python without wait!")\n', - wait_for_prompt: false + input: 'print("Hello from Python!")\n' }); // Wait a moment for Python to process + console.log("Waiting for output..."); await new Promise(resolve => setTimeout(resolve, 1000)); // Read the output + console.log("Reading output..."); const output = await readOutput({ pid }); - console.log('Python output without wait_for_prompt:', output.content[0].text); - - // Check that the output contains the expected text - assert(output.content[0].text.includes('Hello from Python without wait!'), - 'Output should contain the printed message'); - - // Test multi-line code with wait_for_prompt - console.log('Testing multi-line code with wait_for_prompt...'); - const multilineCode = ` -def greet(name): - return f"Hello, {name}!" - -for i in range(3): - print(greet(f"Guest {i+1}")) -`; - - const multilineResult = await sendInput({ - pid, - input: multilineCode + '\n', - wait_for_prompt: true, - timeout_ms: 5000 - }); - console.log('Python multi-line output with wait_for_prompt:', multilineResult.content[0].text); - - // Check that the output contains all three greetings - assert(multilineResult.content[0].text.includes('Hello, Guest 1!'), - 'Output should contain greeting for Guest 1'); - assert(multilineResult.content[0].text.includes('Hello, Guest 2!'), - 'Output should contain greeting for Guest 2'); - assert(multilineResult.content[0].text.includes('Hello, Guest 3!'), - 'Output should contain greeting for Guest 3'); + console.log('Python output:', output.content[0].text); // Terminate the session + console.log("Terminating session..."); await forceTerminate({ pid }); console.log('Python session terminated'); From 6df3c2fb93cffc0701da9a76a035ee0a71d567e9 Mon Sep 17 00:00:00 2001 From: eduardruzga Date: Mon, 2 Jun 2025 21:13:21 +0300 Subject: [PATCH 04/13] Updates to working with interactive processes --- src/server.ts | 66 ++++++++++++++++++++++++++++++++++++----- src/terminal-manager.ts | 9 ------ 2 files changed, 59 insertions(+), 16 deletions(-) diff --git a/src/server.ts b/src/server.ts index ac25c06..81ddb0c 100644 --- a/src/server.ts +++ b/src/server.ts @@ -325,6 +325,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { Command will continue running in background if it doesn't complete within timeout. + INTERACTIVE SHELLS & REPLs: + Direct execution with interactive flags works reliably: + 1. execute_command("node -i") - Start Node.js REPL directly + 2. execute_command("python3 -i") - Start Python REPL directly + 3. execute_command("bash") - Start interactive bash shell + 4. Use send_input() to send commands/code to any interactive session + 5. Use read_output() to get responses + + BEST PRACTICE: Use execute_command() with appropriate interactive flags. + This is more direct and reliable than wrapper approaches. + NOTE: For file operations, prefer specialized tools like read_file, search_code, list_directory instead of cat, grep, or ls commands. @@ -338,12 +349,35 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { Read new output from a running terminal session. Set timeout_ms for long running commands. + REPL USAGE: + - Always call after send_input() to get REPL responses + - May timeout if no output available - this is normal + - For interactive sessions, use shorter timeouts (2-5 seconds) + - REPLs may not show prompts immediately - that's expected + + If read_output times out but session is active, the command likely executed successfully. + Use list_sessions to check if sessions are blocked or responsive. + ${CMD_PREFIX_DESCRIPTION}`, inputSchema: zodToJsonSchema(ReadOutputArgsSchema), }, { name: "send_input", - description: "Send input to a running terminal session. Ideal for interactive REPL environments like Python, Node.js, or any other shell that expects user input.", + description: `Send input to a running terminal session. Essential for interactive REPL environments. + + INTERACTIVE WORKFLOW: + 1. Start bash: execute_command("bash") + 2. Launch REPL: send_input(pid, "python3") or send_input(pid, "node") + 3. Send code: send_input(pid, "print('Hello')") + 4. Read results: read_output(pid) + + REPL COMMANDS: + - Python: Use "python3" to start, "exit()" to quit + - Node.js: Use "node" to start, ".exit" or Ctrl+C to quit + - SSH: Use "ssh user@host" for remote connections + - Navigate directories before launching REPLs as needed + + Always follow send_input() with read_output() to get the response.`, inputSchema: zodToJsonSchema(SendInputArgsSchema), }, { @@ -359,6 +393,16 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { description: ` List all active terminal sessions. + Shows session status including: + - PID: Process identifier + - Blocked: Whether session is waiting for input + - Runtime: How long the session has been running + + DEBUGGING REPLs: + - "Blocked: true" often means REPL is waiting for input + - Use this to verify sessions are running before sending input + - Long runtime with blocked status may indicate stuck process + ${CMD_PREFIX_DESCRIPTION}`, inputSchema: zodToJsonSchema(ListSessionsArgsSchema), }, @@ -383,12 +427,20 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { inputSchema: zodToJsonSchema(KillProcessArgsSchema), }, - // Note: For interactive programming environments (REPLs) like Python or Node.js, - // use execute_command to start the session, send_input to send code, - // and read_output to get the results. For example: - // execute_command("python") to start Python - // send_input(pid, "print('Hello world')") to run code - // read_output(pid) to see the results + // INTERACTIVE SHELLS & REPLs BEST PRACTICES: + // 1. Start with bash: execute_command("bash") + // 2. Launch REPL within bash: send_input(pid, "python3") + // 3. Send code: send_input(pid, "print('Hello')") + // 4. Read output: read_output(pid) + // + // This approach is more reliable than direct REPL execution. + // Direct execute_command("python3") may become unresponsive. + // + // REPL Examples: + // - Python: "python3" to start, "exit()" to quit + // - Node.js: "node" to start, ".exit" to quit + // - SSH: "ssh user@host" for remote connections + // - Always use absolute paths before launching REPLs ], }; } catch (error) { diff --git a/src/terminal-manager.ts b/src/terminal-manager.ts index 9604160..29a615a 100644 --- a/src/terminal-manager.ts +++ b/src/terminal-manager.ts @@ -198,15 +198,6 @@ export class TerminalManager { listCompletedSessions(): CompletedSession[] { return Array.from(this.completedSessions.values()); } - - /** - * Get a session by PID - * @param pid Process ID - * @returns The session or undefined if not found - */ - getSession(pid: number): TerminalSession | undefined { - return this.sessions.get(pid); - } } export const terminalManager = new TerminalManager(); \ No newline at end of file From 2668146b7a13c44b6aee2244c6c14662670cb490 Mon Sep 17 00:00:00 2001 From: eduardruzga Date: Mon, 2 Jun 2025 22:02:22 +0300 Subject: [PATCH 05/13] Improved prompt for repls --- package.json | 1 + src/handlers/terminal-handlers.ts | 53 ++-- src/server.ts | 213 +++++++++++----- src/tools/improved-process-tools.ts | 363 ++++++++++++++++++++++++++++ src/tools/schemas.ts | 15 +- src/utils/process-detection.ts | 180 ++++++++++++++ 6 files changed, 742 insertions(+), 83 deletions(-) create mode 100644 src/tools/improved-process-tools.ts create mode 100644 src/utils/process-detection.ts diff --git a/package.json b/package.json index 00228c1..d9abbfe 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "testemonials" ], "scripts": { + "open-chat": "open -n /Applications/Claude.app", "sync-version": "node scripts/sync-version.js", "bump": "node scripts/sync-version.js --bump", "bump:minor": "node scripts/sync-version.js --bump --minor", diff --git a/src/handlers/terminal-handlers.ts b/src/handlers/terminal-handlers.ts index e98882d..15cbf56 100644 --- a/src/handlers/terminal-handlers.ts +++ b/src/handlers/terminal-handlers.ts @@ -1,35 +1,42 @@ import { - executeCommand, - readOutput, + startProcess, + readProcessOutput, + interactWithProcess, forceTerminate, listSessions -} from '../tools/execute.js'; -import { sendInput } from '../tools/send-input.js'; +} from '../tools/improved-process-tools.js'; import { - ExecuteCommandArgsSchema, - ReadOutputArgsSchema, + StartProcessArgsSchema, + ReadProcessOutputArgsSchema, + InteractWithProcessArgsSchema, ForceTerminateArgsSchema, - ListSessionsArgsSchema, - SendInputArgsSchema + ListSessionsArgsSchema } from '../tools/schemas.js'; import { ServerResult } from '../types.js'; /** - * Handle execute_command command + * Handle start_process command (improved execute_command) */ -export async function handleExecuteCommand(args: unknown): Promise { - const parsed = ExecuteCommandArgsSchema.parse(args); - return executeCommand(parsed); +export async function handleStartProcess(args: unknown): Promise { + const parsed = StartProcessArgsSchema.parse(args); + return startProcess(parsed); } /** - * Handle read_output command + * Handle read_process_output command (improved read_output) */ -export async function handleReadOutput(args: unknown): Promise { - const parsed = ReadOutputArgsSchema.parse(args); - return readOutput(parsed); +export async function handleReadProcessOutput(args: unknown): Promise { + const parsed = ReadProcessOutputArgsSchema.parse(args); + return readProcessOutput(parsed); +} + +/** + * Handle interact_with_process command (improved send_input) + */ +export async function handleInteractWithProcess(args: unknown): Promise { + return interactWithProcess(args); } /** @@ -47,9 +54,15 @@ export async function handleListSessions(): Promise { return listSessions(); } -/** - * Handle send_input command - */ +// Backward compatibility handlers +export async function handleExecuteCommand(args: unknown): Promise { + return handleStartProcess(args); +} + +export async function handleReadOutput(args: unknown): Promise { + return handleReadProcessOutput(args); +} + export async function handleSendInput(args: unknown): Promise { - return sendInput(args); + return handleInteractWithProcess(args); } diff --git a/src/server.ts b/src/server.ts index 81ddb0c..5b40e0a 100644 --- a/src/server.ts +++ b/src/server.ts @@ -79,6 +79,15 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { console.error("Generating tools list..."); return { tools: [ + // 🚨🚨🚨 IMPORTANT: LOCAL FILE ANALYSIS TOOL SELECTION 🚨🚨🚨 + // + // FOR ANY LOCAL FILE WORK (CSV, JSON, logs, data analysis): + // ✅ ALWAYS USE: start_process + interact_with_process (with Python, R, etc.) + // ❌ NEVER USE: Analysis/REPL tool (CANNOT access local files and WILL FAIL) + // + // The analysis tool is browser-only and cannot read files from the user's computer. + // Always use processes for local file analysis! + // Configuration tools { name: "get_config", @@ -319,65 +328,129 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { // Terminal tools { - name: "execute_command", + name: "start_process", description: ` - Execute a terminal command with timeout. - - Command will continue running in background if it doesn't complete within timeout. - - INTERACTIVE SHELLS & REPLs: - Direct execution with interactive flags works reliably: - 1. execute_command("node -i") - Start Node.js REPL directly - 2. execute_command("python3 -i") - Start Python REPL directly - 3. execute_command("bash") - Start interactive bash shell - 4. Use send_input() to send commands/code to any interactive session - 5. Use read_output() to get responses - - BEST PRACTICE: Use execute_command() with appropriate interactive flags. - This is more direct and reliable than wrapper approaches. - - NOTE: For file operations, prefer specialized tools like read_file, search_code, - list_directory instead of cat, grep, or ls commands. + Start a new terminal process with intelligent state detection. + + 🚨 PRIMARY TOOL FOR FILE ANALYSIS AND DATA PROCESSING + This is the ONLY correct tool for analyzing local files (CSV, JSON, logs, etc.). + The analysis tool CANNOT access local files and WILL FAIL - always use processes for file-based work. + + ⚠️ CRITICAL RULE: For ANY local file work, ALWAYS use this tool + interact_with_process, NEVER use analysis/REPL tool. + + REQUIRED WORKFLOW FOR LOCAL FILES: + 1. start_process("python3 -i") - Start Python REPL for data analysis + 2. interact_with_process(pid, "import pandas as pd, numpy as np") + 3. interact_with_process(pid, "df = pd.read_csv('/absolute/path/file.csv')") + 4. interact_with_process(pid, "print(df.describe())") + 5. Continue analysis with pandas, matplotlib, seaborn, etc. + + COMMON FILE ANALYSIS PATTERNS: + • start_process("python3 -i") → Python REPL for data analysis (RECOMMENDED) + • start_process("node -i") → Node.js for JSON processing + • start_process("cut -d',' -f1 file.csv | sort | uniq -c") → Quick CSV analysis + • start_process("wc -l /path/file.csv") → Line counting + • start_process("head -10 /path/file.csv") → File preview + + INTERACTIVE PROCESSES FOR DATA ANALYSIS: + 1. start_process("python3 -i") - Start Python REPL for data work + 2. start_process("node -i") - Start Node.js REPL for JSON/JS + 3. start_process("bash") - Start interactive bash shell + 4. Use interact_with_process() to send commands + 5. Use read_process_output() to get responses + + SMART DETECTION: + - Detects REPL prompts (>>>, >, $, etc.) + - Identifies when process is waiting for input + - Recognizes process completion vs timeout + - Early exit prevents unnecessary waiting + + STATES DETECTED: + 🔄 Process waiting for input (shows prompt) + ✅ Process finished execution + ⏳ Process running (use read_process_output) + + ✅ ALWAYS USE FOR: Local file analysis, CSV processing, data exploration, system commands + ❌ NEVER USE ANALYSIS TOOL FOR: Local file access (analysis tool is browser-only and WILL FAIL) ${PATH_GUIDANCE} ${CMD_PREFIX_DESCRIPTION}`, inputSchema: zodToJsonSchema(ExecuteCommandArgsSchema), }, { - name: "read_output", + name: "read_process_output", description: ` - Read new output from a running terminal session. - Set timeout_ms for long running commands. + Read output from a running process with intelligent completion detection. + + Automatically detects when process is ready for more input instead of timing out. + + SMART FEATURES: + - Early exit when REPL shows prompt (>>>, >, etc.) + - Detects process completion vs still running + - Prevents hanging on interactive prompts + - Clear status messages about process state REPL USAGE: - - Always call after send_input() to get REPL responses - - May timeout if no output available - this is normal - - For interactive sessions, use shorter timeouts (2-5 seconds) - - REPLs may not show prompts immediately - that's expected + - Stops immediately when REPL prompt detected + - Shows clear status: waiting for input vs finished + - Shorter timeouts needed due to smart detection + - Works with Python, Node.js, R, Julia, etc. - If read_output times out but session is active, the command likely executed successfully. - Use list_sessions to check if sessions are blocked or responsive. + DETECTION STATES: + 🔄 Process waiting for input (ready for interact_with_process) + ✅ Process finished execution + ⏱️ Timeout reached (may still be running) ${CMD_PREFIX_DESCRIPTION}`, inputSchema: zodToJsonSchema(ReadOutputArgsSchema), }, { - name: "send_input", - description: `Send input to a running terminal session. Essential for interactive REPL environments. - - INTERACTIVE WORKFLOW: - 1. Start bash: execute_command("bash") - 2. Launch REPL: send_input(pid, "python3") or send_input(pid, "node") - 3. Send code: send_input(pid, "print('Hello')") - 4. Read results: read_output(pid) - - REPL COMMANDS: - - Python: Use "python3" to start, "exit()" to quit - - Node.js: Use "node" to start, ".exit" or Ctrl+C to quit - - SSH: Use "ssh user@host" for remote connections - - Navigate directories before launching REPLs as needed - - Always follow send_input() with read_output() to get the response.`, + name: "interact_with_process", + description: ` + Send input to a running process and automatically receive the response. + + 🚨 CRITICAL: THIS IS THE PRIMARY TOOL FOR ALL LOCAL FILE ANALYSIS + For ANY local file analysis (CSV, JSON, data processing), ALWAYS use this instead of the analysis tool. + The analysis tool CANNOT access local files and WILL FAIL - use processes for ALL file-based work. + + 📁 FILE ANALYSIS PRIORITY ORDER (MANDATORY): + 1. ALWAYS FIRST: Use this tool (start_process + interact_with_process) for local data analysis + 2. ALTERNATIVE: Use command-line tools (cut, awk, grep) for quick processing + 3. NEVER EVER: Use analysis tool for local file access (IT WILL FAIL) + + REQUIRED INTERACTIVE WORKFLOW FOR FILE ANALYSIS: + 1. Start REPL: start_process("python3 -i") + 2. Load libraries: interact_with_process(pid, "import pandas as pd, numpy as np") + 3. Read file: interact_with_process(pid, "df = pd.read_csv('/absolute/path/file.csv')") + 4. Analyze: interact_with_process(pid, "print(df.describe())") + 5. Continue: interact_with_process(pid, "df.groupby('column').size()") + + SMART DETECTION: + - Automatically waits for REPL prompt (>>>, >, etc.) + - Detects errors and completion states + - Early exit prevents timeout delays + - Clean output formatting (removes prompts) + + SUPPORTED REPLs: + - Python: python3 -i (RECOMMENDED for data analysis) + - Node.js: node -i + - R: R + - Julia: julia + - Shell: bash, zsh + - Database: mysql, postgres + + PARAMETERS: + - pid: Process ID from start_process + - input: Code/command to execute + - timeout_ms: Max wait (default: 8000ms) + - wait_for_prompt: Auto-wait for response (default: true) + + Returns execution result with status indicators. + + ✅ ALWAYS USE FOR: CSV analysis, JSON processing, file statistics, data visualization prep, ANY local file work + ❌ NEVER USE ANALYSIS TOOL FOR: Local file access (it cannot read files from disk and WILL FAIL) + + ${CMD_PREFIX_DESCRIPTION}`, inputSchema: zodToJsonSchema(SendInputArgsSchema), }, { @@ -427,20 +500,32 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { inputSchema: zodToJsonSchema(KillProcessArgsSchema), }, - // INTERACTIVE SHELLS & REPLs BEST PRACTICES: - // 1. Start with bash: execute_command("bash") - // 2. Launch REPL within bash: send_input(pid, "python3") - // 3. Send code: send_input(pid, "print('Hello')") - // 4. Read output: read_output(pid) - // - // This approach is more reliable than direct REPL execution. - // Direct execute_command("python3") may become unresponsive. + // 🚨🚨🚨 CRITICAL TOOL SELECTION GUIDANCE 🚨🚨🚨 + // + // FOR LOCAL FILE ANALYSIS (CSV, JSON, logs, data files): + // ✅ ALWAYS USE: start_process + interact_with_process + // ✅ ALTERNATIVE: Command-line tools (read_file, search_code, cut, awk, grep) + // ❌ NEVER EVER USE: Analysis tool (CANNOT access local files and WILL FAIL) + // + // 🔴 ANALYSIS/REPL TOOL LIMITATIONS: + // - CANNOT read files from user's computer/file system + // - ONLY works with uploaded files or web data + // - Browser-based JavaScript runtime only + // - NO access to pandas, numpy, or local Python libraries // - // REPL Examples: - // - Python: "python3" to start, "exit()" to quit - // - Node.js: "node" to start, ".exit" to quit - // - SSH: "ssh user@host" for remote connections - // - Always use absolute paths before launching REPLs + // 🟢 PROCESS TOOLS ADVANTAGES: + // - CAN access ALL local files + // - Full system power (Python, R, databases, etc.) + // - Handle files of ANY size + // - Access to all installed libraries and tools + // + // MANDATORY WORKFLOW FOR LOCAL FILES: + // 1. start_process("python3 -i") + // 2. interact_with_process(pid, "import pandas as pd") + // 3. interact_with_process(pid, "df = pd.read_csv('/path/to/file.csv')") + // 4. interact_with_process(pid, "print(df.head())") + // + // REMEMBER: "For local file analysis, ALWAYS use processes, NEVER use analysis tool" ], }; } catch (error) { @@ -487,14 +572,24 @@ server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest) } // Terminal tools + case "start_process": + return await handlers.handleStartProcess(args); + + case "read_process_output": + return await handlers.handleReadProcessOutput(args); + + case "interact_with_process": + return await handlers.handleInteractWithProcess(args); + + // Backward compatibility case "execute_command": - return await handlers.handleExecuteCommand(args); + return await handlers.handleStartProcess(args); case "read_output": - return await handlers.handleReadOutput(args); + return await handlers.handleReadProcessOutput(args); case "send_input": - return await handlers.handleSendInput(args); + return await handlers.handleInteractWithProcess(args); case "force_terminate": return await handlers.handleForceTerminate(args); diff --git a/src/tools/improved-process-tools.ts b/src/tools/improved-process-tools.ts new file mode 100644 index 0000000..3faaf23 --- /dev/null +++ b/src/tools/improved-process-tools.ts @@ -0,0 +1,363 @@ +import { terminalManager } from '../terminal-manager.js'; +import { commandManager } from '../command-manager.js'; +import { StartProcessArgsSchema, ReadProcessOutputArgsSchema, InteractWithProcessArgsSchema, ForceTerminateArgsSchema, ListSessionsArgsSchema } from './schemas.js'; +import { capture } from "../utils/capture.js"; +import { ServerResult } from '../types.js'; +import { analyzeProcessState, cleanProcessOutput, formatProcessStateMessage } from '../utils/process-detection.js'; + +/** + * Start a new process (renamed from execute_command) + * Includes early detection of process waiting for input + */ +export async function startProcess(args: unknown): Promise { + const parsed = StartProcessArgsSchema.safeParse(args); + if (!parsed.success) { + capture('server_start_process_failed'); + return { + content: [{ type: "text", text: `Error: Invalid arguments for start_process: ${parsed.error}` }], + isError: true, + }; + } + + try { + const commands = commandManager.extractCommands(parsed.data.command).join(', '); + capture('server_start_process', { + command: commandManager.getBaseCommand(parsed.data.command), + commands: commands + }); + } catch (error) { + capture('server_start_process', { + command: commandManager.getBaseCommand(parsed.data.command) + }); + } + + const isAllowed = await commandManager.validateCommand(parsed.data.command); + if (!isAllowed) { + return { + content: [{ type: "text", text: `Error: Command not allowed: ${parsed.data.command}` }], + isError: true, + }; + } + + const result = await terminalManager.executeCommand( + parsed.data.command, + parsed.data.timeout_ms, + parsed.data.shell + ); + + if (result.pid === -1) { + return { + content: [{ type: "text", text: result.output }], + isError: true, + }; + } + + // Analyze the process state to detect if it's waiting for input + const processState = analyzeProcessState(result.output, result.pid); + + let statusMessage = ''; + if (processState.isWaitingForInput) { + statusMessage = `\n🔄 ${formatProcessStateMessage(processState, result.pid)}`; + } else if (processState.isFinished) { + statusMessage = `\n✅ ${formatProcessStateMessage(processState, result.pid)}`; + } else if (result.isBlocked) { + statusMessage = '\n⏳ Process is running. Use read_process_output to get more output.'; + } + + return { + content: [{ + type: "text", + text: `Process started with PID ${result.pid}\nInitial output:\n${result.output}${statusMessage}` + }], + }; +} + +/** + * Read output from a running process (renamed from read_output) + * Includes early detection of process waiting for input + */ +export async function readProcessOutput(args: unknown): Promise { + const parsed = ReadProcessOutputArgsSchema.safeParse(args); + if (!parsed.success) { + return { + content: [{ type: "text", text: `Error: Invalid arguments for read_process_output: ${parsed.error}` }], + isError: true, + }; + } + + const { pid, timeout_ms = 5000 } = parsed.data; + + const session = terminalManager.getSession(pid); + if (!session) { + return { + content: [{ type: "text", text: `No active session found for PID ${pid}` }], + isError: true, + }; + } + + let output = ""; + let timeoutReached = false; + let earlyExit = false; + let processState; + + try { + const outputPromise: Promise = new Promise((resolve) => { + const initialOutput = terminalManager.getNewOutput(pid); + if (initialOutput && initialOutput.length > 0) { + resolve(initialOutput); + return; + } + + let resolved = false; + let interval: NodeJS.Timeout | null = null; + let timeout: NodeJS.Timeout | null = null; + + const cleanup = () => { + if (interval) clearInterval(interval); + if (timeout) clearTimeout(timeout); + }; + + const resolveOnce = (value: string, isTimeout = false) => { + if (resolved) return; + resolved = true; + cleanup(); + timeoutReached = isTimeout; + resolve(value); + }; + + interval = setInterval(() => { + const newOutput = terminalManager.getNewOutput(pid); + if (newOutput && newOutput.length > 0) { + const currentOutput = output + newOutput; + const state = analyzeProcessState(currentOutput, pid); + + // Early exit if process is clearly waiting for input + if (state.isWaitingForInput) { + earlyExit = true; + processState = state; + resolveOnce(newOutput); + return; + } + + output = currentOutput; + + // Continue collecting if still running + if (!state.isFinished) { + return; + } + + // Process finished + processState = state; + resolveOnce(newOutput); + } + }, 200); // Check every 200ms + + timeout = setTimeout(() => { + const finalOutput = terminalManager.getNewOutput(pid) || ""; + resolveOnce(finalOutput, true); + }, timeout_ms); + }); + + const newOutput = await outputPromise; + output += newOutput; + + // Analyze final state if not already done + if (!processState) { + processState = analyzeProcessState(output, pid); + } + + } catch (error) { + return { + content: [{ type: "text", text: `Error reading output: ${error}` }], + isError: true, + }; + } + + // Format response based on what we detected + let statusMessage = ''; + if (earlyExit && processState?.isWaitingForInput) { + statusMessage = `\n🔄 ${formatProcessStateMessage(processState, pid)}`; + } else if (processState?.isFinished) { + statusMessage = `\n✅ ${formatProcessStateMessage(processState, pid)}`; + } else if (timeoutReached) { + statusMessage = '\n⏱️ Timeout reached - process may still be running'; + } + + const responseText = output || 'No new output available'; + + return { + content: [{ + type: "text", + text: `${responseText}${statusMessage}` + }], + }; +} + +/** + * Interact with a running process (renamed from send_input) + * Automatically detects when process is ready and returns output + */ +export async function interactWithProcess(args: unknown): Promise { + const parsed = InteractWithProcessArgsSchema.safeParse(args); + if (!parsed.success) { + capture('server_interact_with_process_failed', { + error: 'Invalid arguments' + }); + return { + content: [{ type: "text", text: `Error: Invalid arguments for interact_with_process: ${parsed.error}` }], + isError: true, + }; + } + + const { + pid, + input, + timeout_ms = 8000, + wait_for_prompt = true + } = parsed.data; + + try { + capture('server_interact_with_process', { + pid: pid, + inputLength: input.length + }); + + const success = terminalManager.sendInputToProcess(pid, input); + + if (!success) { + return { + content: [{ type: "text", text: `Error: Failed to send input to process ${pid}. The process may have exited or doesn't accept input.` }], + isError: true, + }; + } + + // If not waiting for response, return immediately + if (!wait_for_prompt) { + return { + content: [{ + type: "text", + text: `✅ Input sent to process ${pid}. Use read_process_output to get the response.` + }], + }; + } + + // Smart waiting with process state detection + let output = ""; + let attempts = 0; + const maxAttempts = Math.ceil(timeout_ms / 200); + let processState; + + while (attempts < maxAttempts) { + await new Promise(resolve => setTimeout(resolve, 200)); + + const newOutput = terminalManager.getNewOutput(pid); + if (newOutput && newOutput.length > 0) { + output += newOutput; + + // Analyze current state + processState = analyzeProcessState(output, pid); + + // Exit early if we detect the process is waiting for input + if (processState.isWaitingForInput) { + break; + } + + // Also exit if process finished + if (processState.isFinished) { + break; + } + } + + attempts++; + } + + // Clean and format output + const cleanOutput = cleanProcessOutput(output, input); + const timeoutReached = attempts >= maxAttempts; + + // Determine final state + if (!processState) { + processState = analyzeProcessState(output, pid); + } + + let statusMessage = ''; + if (processState.isWaitingForInput) { + statusMessage = `\n🔄 ${formatProcessStateMessage(processState, pid)}`; + } else if (processState.isFinished) { + statusMessage = `\n✅ ${formatProcessStateMessage(processState, pid)}`; + } else if (timeoutReached) { + statusMessage = '\n⏱️ Response may be incomplete (timeout reached)'; + } + + if (cleanOutput.trim().length === 0 && !timeoutReached) { + return { + content: [{ + type: "text", + text: `✅ Input executed in process ${pid}.\n(No output produced)${statusMessage}` + }], + }; + } + + return { + content: [{ + type: "text", + text: `✅ Input executed in process ${pid}:\n\n${cleanOutput}${statusMessage}` + }], + }; + + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + capture('server_interact_with_process_error', { + error: errorMessage + }); + return { + content: [{ type: "text", text: `Error interacting with process: ${errorMessage}` }], + isError: true, + }; + } +} + +// Backward compatibility exports +export { startProcess as executeCommand }; +export { readProcessOutput as readOutput }; +export { interactWithProcess as sendInput }; + +/** + * Force terminate a process + */ +export async function forceTerminate(args: unknown): Promise { + const parsed = ForceTerminateArgsSchema.safeParse(args); + if (!parsed.success) { + return { + content: [{ type: "text", text: `Error: Invalid arguments for force_terminate: ${parsed.error}` }], + isError: true, + }; + } + + const success = terminalManager.forceTerminate(parsed.data.pid); + return { + content: [{ + type: "text", + text: success + ? `Successfully initiated termination of session ${parsed.data.pid}` + : `No active session found for PID ${parsed.data.pid}` + }], + }; +} + +/** + * List active sessions + */ +export async function listSessions(): Promise { + const sessions = terminalManager.listActiveSessions(); + return { + content: [{ + type: "text", + text: sessions.length === 0 + ? 'No active sessions' + : sessions.map(s => + `PID: ${s.pid}, Blocked: ${s.isBlocked}, Runtime: ${Math.round(s.runtime / 1000)}s` + ).join('\n') + }], + }; +} diff --git a/src/tools/schemas.ts b/src/tools/schemas.ts index 296ea71..b25e76e 100644 --- a/src/tools/schemas.ts +++ b/src/tools/schemas.ts @@ -14,17 +14,21 @@ export const SetConfigValueArgsSchema = z.object({ export const ListProcessesArgsSchema = z.object({}); // Terminal tools schemas -export const ExecuteCommandArgsSchema = z.object({ +export const StartProcessArgsSchema = z.object({ command: z.string(), timeout_ms: z.number(), shell: z.string().optional(), }); -export const ReadOutputArgsSchema = z.object({ +export const ReadProcessOutputArgsSchema = z.object({ pid: z.number(), timeout_ms: z.number().optional(), }); +// Backward compatibility +export const ExecuteCommandArgsSchema = StartProcessArgsSchema; +export const ReadOutputArgsSchema = ReadProcessOutputArgsSchema; + export const ForceTerminateArgsSchema = z.object({ pid: z.number(), }); @@ -97,9 +101,12 @@ export const EditBlockArgsSchema = z.object({ }); // Send input to process schema -export const SendInputArgsSchema = z.object({ +export const InteractWithProcessArgsSchema = z.object({ pid: z.number(), input: z.string(), timeout_ms: z.number().optional(), wait_for_prompt: z.boolean().optional(), -}); \ No newline at end of file +}); + +// Backward compatibility +export const SendInputArgsSchema = InteractWithProcessArgsSchema; \ No newline at end of file diff --git a/src/utils/process-detection.ts b/src/utils/process-detection.ts new file mode 100644 index 0000000..a58b500 --- /dev/null +++ b/src/utils/process-detection.ts @@ -0,0 +1,180 @@ +/** + * REPL and Process State Detection Utilities + * Detects when processes are waiting for input vs finished vs running + */ + +export interface ProcessState { + isWaitingForInput: boolean; + isFinished: boolean; + isRunning: boolean; + detectedPrompt?: string; + lastOutput: string; +} + +// Common REPL prompt patterns +const REPL_PROMPTS = { + python: ['>>> ', '... '], + node: ['> ', '... '], + r: ['> ', '+ '], + julia: ['julia> ', ' '], // julia continuation is spaces + shell: ['$ ', '# ', '% ', 'bash-', 'zsh-'], + mysql: ['mysql> ', ' -> '], + postgres: ['=# ', '-# '], + redis: ['redis> '], + mongo: ['> ', '... '] +}; + +// Error patterns that indicate completion (even with errors) +const ERROR_COMPLETION_PATTERNS = [ + /Error:/i, + /Exception:/i, + /Traceback/i, + /SyntaxError/i, + /NameError/i, + /TypeError/i, + /ValueError/i, + /ReferenceError/i, + /Uncaught/i, + /at Object\./i, // Node.js stack traces + /^\s*\^/m // Syntax error indicators +]; + +// Process completion indicators +const COMPLETION_INDICATORS = [ + /Process finished/i, + /Command completed/i, + /\[Process completed\]/i, + /Program terminated/i, + /Exit code:/i +]; + +/** + * Analyze process output to determine current state + */ +export function analyzeProcessState(output: string, pid?: number): ProcessState { + if (!output || output.trim().length === 0) { + return { + isWaitingForInput: false, + isFinished: false, + isRunning: true, + lastOutput: output + }; + } + + const lines = output.split('\n'); + const lastLine = lines[lines.length - 1] || ''; + const lastFewLines = lines.slice(-3).join('\n'); + + // Check for REPL prompts (waiting for input) + const allPrompts = Object.values(REPL_PROMPTS).flat(); + const detectedPrompt = allPrompts.find(prompt => + lastLine.endsWith(prompt) || lastLine.includes(prompt) + ); + + if (detectedPrompt) { + return { + isWaitingForInput: true, + isFinished: false, + isRunning: true, + detectedPrompt, + lastOutput: output + }; + } + + // Check for completion indicators + const hasCompletionIndicator = COMPLETION_INDICATORS.some(pattern => + pattern.test(output) + ); + + if (hasCompletionIndicator) { + return { + isWaitingForInput: false, + isFinished: true, + isRunning: false, + lastOutput: output + }; + } + + // Check for error completion (errors usually end with prompts, but let's be thorough) + const hasErrorCompletion = ERROR_COMPLETION_PATTERNS.some(pattern => + pattern.test(lastFewLines) + ); + + if (hasErrorCompletion) { + // Errors can indicate completion, but check if followed by prompt + if (detectedPrompt) { + return { + isWaitingForInput: true, + isFinished: false, + isRunning: true, + detectedPrompt, + lastOutput: output + }; + } else { + return { + isWaitingForInput: false, + isFinished: true, + isRunning: false, + lastOutput: output + }; + } + } + + // Default: process is running, not clearly waiting or finished + return { + isWaitingForInput: false, + isFinished: false, + isRunning: true, + lastOutput: output + }; +} + +/** + * Clean output by removing prompts and input echoes + */ +export function cleanProcessOutput(output: string, inputSent?: string): string { + let cleaned = output; + + // Remove input echo if provided + if (inputSent) { + const inputLines = inputSent.split('\n'); + inputLines.forEach(line => { + if (line.trim()) { + cleaned = cleaned.replace(new RegExp(`^${escapeRegExp(line.trim())}\\s*\n?`, 'm'), ''); + } + }); + } + + // Remove common prompt patterns from output + cleaned = cleaned.replace(/^>>>\s*/gm, ''); // Python >>> + cleaned = cleaned.replace(/^>\s*/gm, ''); // Node.js/Shell > + cleaned = cleaned.replace(/^\.{3}\s*/gm, ''); // Python ... + cleaned = cleaned.replace(/^\+\s*/gm, ''); // R + + + // Remove trailing prompts + cleaned = cleaned.replace(/\n>>>\s*$/, ''); + cleaned = cleaned.replace(/\n>\s*$/, ''); + cleaned = cleaned.replace(/\n\+\s*$/, ''); + + return cleaned.trim(); +} + +/** + * Escape special regex characters + */ +function escapeRegExp(string: string): string { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +/** + * Format process state for user display + */ +export function formatProcessStateMessage(state: ProcessState, pid: number): string { + if (state.isWaitingForInput) { + return `Process ${pid} is waiting for input${state.detectedPrompt ? ` (detected: "${state.detectedPrompt.trim()}")` : ''}`; + } else if (state.isFinished) { + return `Process ${pid} has finished execution`; + } else { + return `Process ${pid} is running`; + } +} From b7d1ffc5d6ad32ad08164f87a39e5d5fb44703f2 Mon Sep 17 00:00:00 2001 From: eduardruzga Date: Mon, 2 Jun 2025 22:03:20 +0300 Subject: [PATCH 06/13] Cleanup --- src/tools/enhanced-read-output.js | 69 ----------------- src/tools/enhanced-send-input.js | 111 --------------------------- test/test_output/node_repl_debug.txt | 56 -------------- 3 files changed, 236 deletions(-) delete mode 100644 src/tools/enhanced-read-output.js delete mode 100644 src/tools/enhanced-send-input.js delete mode 100644 test/test_output/node_repl_debug.txt diff --git a/src/tools/enhanced-read-output.js b/src/tools/enhanced-read-output.js deleted file mode 100644 index 1248ad0..0000000 --- a/src/tools/enhanced-read-output.js +++ /dev/null @@ -1,69 +0,0 @@ -import { terminalManager } from '../terminal-manager.js'; -import { ReadOutputArgsSchema } from './schemas.js'; - -export async function readOutput(args) { - const parsed = ReadOutputArgsSchema.safeParse(args); - if (!parsed.success) { - return { - content: [{ type: "text", text: `Error: Invalid arguments for read_output: ${parsed.error}` }], - isError: true, - }; - } - - const { pid, timeout_ms = 5000 } = parsed.data; - - // Check if the process exists - const session = terminalManager.getSession(pid); - if (!session) { - return { - content: [{ type: "text", text: `No session found for PID ${pid}` }], - isError: true, - }; - } - - // Wait for output with timeout - let output = ""; - let timeoutReached = false; - - try { - // Create a promise that resolves when new output is available or when timeout is reached - const outputPromise = new Promise((resolve) => { - // Check for initial output - const initialOutput = terminalManager.getNewOutput(pid); - if (initialOutput && initialOutput.length > 0) { - resolve(initialOutput); - return; - } - - // Setup an interval to poll for output - const interval = setInterval(() => { - const newOutput = terminalManager.getNewOutput(pid); - if (newOutput && newOutput.length > 0) { - clearInterval(interval); - resolve(newOutput); - } - }, 100); // Check every 100ms - - // Set a timeout to stop waiting - setTimeout(() => { - clearInterval(interval); - timeoutReached = true; - resolve(terminalManager.getNewOutput(pid) || ""); - }, timeout_ms); - }); - - output = await outputPromise; - } catch (error) { - return { - content: [{ type: "text", text: `Error reading output: ${error}` }], - isError: true, - }; - } - - return { - content: [{ - type: "text", - text: output || 'No new output available' + (timeoutReached ? ' (timeout reached)' : '') - }], - }; -} \ No newline at end of file diff --git a/src/tools/enhanced-send-input.js b/src/tools/enhanced-send-input.js deleted file mode 100644 index 5850b87..0000000 --- a/src/tools/enhanced-send-input.js +++ /dev/null @@ -1,111 +0,0 @@ -import { terminalManager } from '../terminal-manager.js'; -import { SendInputArgsSchema } from './schemas.js'; -import { capture } from "../utils/capture.js"; - -export async function sendInput(args) { - const parsed = SendInputArgsSchema.safeParse(args); - if (!parsed.success) { - capture('server_send_input_failed', { - error: 'Invalid arguments' - }); - return { - content: [{ type: "text", text: `Error: Invalid arguments for send_input: ${parsed.error}` }], - isError: true, - }; - } - - const { pid, input, timeout_ms = 5000, wait_for_prompt = false } = parsed.data; - - try { - capture('server_send_input', { - pid: pid, - inputLength: input.length - }); - - // Try to send input to the process - const success = terminalManager.sendInputToProcess(pid, input); - - if (!success) { - return { - content: [{ type: "text", text: `Error: Failed to send input to process ${pid}. The process may have exited or doesn't accept input.` }], - isError: true, - }; - } - - // If we don't need to wait for output, return immediately - if (!wait_for_prompt) { - return { - content: [{ - type: "text", - text: `Successfully sent input to process ${pid}. Use read_output to get the process response.` - }], - }; - } - - // Wait for output with timeout - let output = ""; - let timeoutReached = false; - - try { - // Create a promise that resolves when new output is available or when timeout is reached - const outputPromise = new Promise((resolve) => { - // Setup an interval to poll for output - const interval = setInterval(() => { - const newOutput = terminalManager.getNewOutput(pid); - - if (newOutput && newOutput.length > 0) { - output += newOutput; - - // Check if output contains a prompt pattern (indicating the REPL is ready for more input) - const promptPatterns = [/^>\s*$/, /^>>>\s*$/, /^\.{3}\s*$/]; // Common REPL prompts - const lines = output.split('\n'); - const lastLine = lines[lines.length - 1]; - const hasPrompt = promptPatterns.some(pattern => pattern.test(lastLine.trim())); - - if (hasPrompt) { - clearInterval(interval); - resolve(output); - } - } - }, 100); // Check every 100ms - - // Set a timeout to stop waiting - setTimeout(() => { - clearInterval(interval); - timeoutReached = true; - - // Get any final output - const finalOutput = terminalManager.getNewOutput(pid); - if (finalOutput) { - output += finalOutput; - } - - resolve(output); - }, timeout_ms); - }); - - await outputPromise; - } catch (error) { - return { - content: [{ type: "text", text: `Error reading output after sending input: ${error}` }], - isError: true, - }; - } - - return { - content: [{ - type: "text", - text: `Input sent to process ${pid}.\n\nOutput received:\n${output || '(No output)'}${timeoutReached ? ' (timeout reached)' : ''}` - }], - }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - capture('server_send_input_error', { - error: errorMessage - }); - return { - content: [{ type: "text", text: `Error sending input: ${errorMessage}` }], - isError: true, - }; - } -} \ No newline at end of file diff --git a/test/test_output/node_repl_debug.txt b/test/test_output/node_repl_debug.txt deleted file mode 100644 index 6311ae9..0000000 --- a/test/test_output/node_repl_debug.txt +++ /dev/null @@ -1,56 +0,0 @@ -Starting Node.js REPL... -Waiting for Node.js startup... -[STDOUT] Welcome to Node.js v23.11.0. -Type ".help" for more information. -[STDOUT] > -Initial output buffer: Welcome to Node.js v23.11.0. -Type ".help" for more information. ->  -Sending simple command... -[STDOUT] Hello from Node.js! -[STDOUT] undefined -[STDOUT] > -Output after first command: Welcome to Node.js v23.11.0. -Type ".help" for more information. -> Hello from Node.js! -undefined ->  -Sending multi-line command directly... -Sending code: - -function greet(name) { - return `Hello, ${name}!`; -} - -for (let i = 0; i < 3; i++) { - console.log(greet(`User ${i}`)); -} - -[STDOUT] > -[STDOUT] ... -[STDOUT] ... -[STDOUT] undefined -[STDOUT] > -[STDOUT] > -[STDOUT] ... -[STDOUT] ... -[STDOUT] Hello, User 0! -[STDOUT] Hello, User 1! -Hello, User 2! -[STDOUT] undefined -[STDOUT] > -[STDOUT] > -Final output buffer: Welcome to Node.js v23.11.0. -Type ".help" for more information. -> Hello from Node.js! -undefined -> > ... ... undefined -> > ... ... Hello, User 0! -Hello, User 1! -Hello, User 2! -undefined -> >  -Found "Hello from Node.js!": true -Found greetings: true -Terminating Node.js process... -Node.js process exited with code 0 From e62d588d0fceb59f0e85c290079724a7ea90acb2 Mon Sep 17 00:00:00 2001 From: eduardruzga Date: Wed, 4 Jun 2025 19:18:14 +0300 Subject: [PATCH 07/13] Improve chunked writing prompt --- src/handlers/filesystem-handlers.ts | 6 ++-- src/server.ts | 48 ++++++++++++++--------------- 2 files changed, 26 insertions(+), 28 deletions(-) diff --git a/src/handlers/filesystem-handlers.ts b/src/handlers/filesystem-handlers.ts index 60612a6..0efb402 100644 --- a/src/handlers/filesystem-handlers.ts +++ b/src/handlers/filesystem-handlers.ts @@ -167,11 +167,9 @@ export async function handleWriteFile(args: unknown): Promise { const lineCount = lines.length; let errorMessage = ""; if (lineCount > MAX_LINES) { - errorMessage = `File was written with warning: Line count limit exceeded: ${lineCount} lines (maximum: ${MAX_LINES}). + errorMessage = `✅ File written successfully! (${lineCount} lines) -SOLUTION: Split your content into smaller chunks: -1. First chunk: write_file(path, firstChunk, {mode: 'rewrite'}) -2. Additional chunks: write_file(path, nextChunk, {mode: 'append'})`; +💡 Performance tip: For optimal speed, consider chunking files into ≤30 line pieces in future operations.`; } // Pass the mode parameter to writeFile diff --git a/src/server.ts b/src/server.ts index 5b40e0a..7b8ceec 100644 --- a/src/server.ts +++ b/src/server.ts @@ -168,30 +168,30 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { { name: "write_file", description: ` - Write or append to file contents with a configurable line limit per call (default: 50 lines). - THIS IS A STRICT REQUIREMENT. ANY file with more than the configured limit MUST BE written in chunks or IT WILL FAIL. - - ⚠️ IMPORTANT: PREVENTATIVE CHUNKING REQUIRED in these scenarios: - 1. When content exceeds 2,000 words or 30 lines - 2. When writing MULTIPLE files one after another (each next file is more likely to be truncated) - 3. When the file is the LAST ONE in a series of operations in the same message - - ALWAYS split files writes in to multiple smaller writes PREEMPTIVELY without asking the user in these scenarios. - - REQUIRED PROCESS FOR LARGE NEW FILE WRITES OR REWRITES: - 1. FIRST → write_file(filePath, firstChunk, {mode: 'rewrite'}) - 2. THEN → write_file(filePath, secondChunk, {mode: 'append'}) - 3. THEN → write_file(filePath, thirdChunk, {mode: 'append'}) - ... and so on for each chunk - - HANDLING TRUNCATION ("Continue" prompts): - If user asked to "Continue" after unfinished file write: - 1. First, read the file to find out what content was successfully written - 2. Identify exactly where the content was truncated - 3. Continue writing ONLY the remaining content using {mode: 'append'} - 4. Split the remaining content into smaller chunks (15-20 lines per chunk) - - Files over the line limit (configurable via 'fileWriteLineLimit' setting) WILL BE REJECTED if not broken into chunks as described above. + Write or append to file contents. + + 🎯 CHUNKING IS STANDARD PRACTICE: Always write files in chunks of 25-30 lines maximum. + This is the normal, recommended way to write files - not an emergency measure. + + STANDARD PROCESS FOR ANY FILE: + 1. FIRST → write_file(filePath, firstChunk, {mode: 'rewrite'}) [≤30 lines] + 2. THEN → write_file(filePath, secondChunk, {mode: 'append'}) [≤30 lines] + 3. CONTINUE → write_file(filePath, nextChunk, {mode: 'append'}) [≤30 lines] + + ⚠️ ALWAYS CHUNK PROACTIVELY - don't wait for performance warnings! + + WHEN TO CHUNK (always be proactive): + 1. Any file expected to be longer than 25-30 lines + 2. When writing multiple files in sequence + 3. When creating documentation, code files, or configuration files + + HANDLING CONTINUATION ("Continue" prompts): + If user asks to "Continue" after an incomplete operation: + 1. Read the file to see what was successfully written + 2. Continue writing ONLY the remaining content using {mode: 'append'} + 3. Keep chunks to 25-30 lines each + + Files over 50 lines will generate performance notes but are still written successfully. Only works within allowed directories. ${PATH_GUIDANCE} From 41144d3f550956d9f4cf4f83625f64df5a4a35d1 Mon Sep 17 00:00:00 2001 From: eduardruzga Date: Wed, 4 Jun 2025 19:33:13 +0300 Subject: [PATCH 08/13] Improve tests --- test/run-all-tests.js | 240 ++++++++++++++++++++++++++++++------------ 1 file changed, 170 insertions(+), 70 deletions(-) diff --git a/test/run-all-tests.js b/test/run-all-tests.js index a22423c..f9340a8 100644 --- a/test/run-all-tests.js +++ b/test/run-all-tests.js @@ -1,18 +1,16 @@ /** * Main test runner script - * Imports and runs all test modules + * Runs all test modules and provides comprehensive summary */ import { spawn } from 'child_process'; import path from 'path'; import fs from 'fs/promises'; import { fileURLToPath } from 'url'; -import { createRequire } from 'module'; // Get directory name const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const require = createRequire(import.meta.url); // Colors for console output const colors = { @@ -21,7 +19,9 @@ const colors = { red: '\x1b[31m', yellow: '\x1b[33m', blue: '\x1b[34m', - cyan: '\x1b[36m' + cyan: '\x1b[36m', + magenta: '\x1b[35m', + bold: '\x1b[1m' }; /** @@ -51,6 +51,39 @@ function runCommand(command, args, cwd = __dirname) { }); } +/** + * Run a single Node.js test file as a subprocess + */ +function runTestFile(testFile) { + return new Promise((resolve) => { + console.log(`\n${colors.cyan}Running test module: ${testFile}${colors.reset}`); + + const startTime = Date.now(); + const proc = spawn('node', [testFile], { + cwd: __dirname, + stdio: 'inherit', + shell: false + }); + + proc.on('close', (code) => { + const duration = Date.now() - startTime; + if (code === 0) { + console.log(`${colors.green}✓ Test passed: ${testFile} (${duration}ms)${colors.reset}`); + resolve({ success: true, file: testFile, duration, exitCode: code }); + } else { + console.error(`${colors.red}✗ Test failed: ${testFile} (${duration}ms) - Exit code: ${code}${colors.reset}`); + resolve({ success: false, file: testFile, duration, exitCode: code }); + } + }); + + proc.on('error', (err) => { + const duration = Date.now() - startTime; + console.error(`${colors.red}✗ Error running ${testFile}: ${err.message}${colors.reset}`); + resolve({ success: false, file: testFile, duration, error: err.message }); + }); + }); +} + /** * Build the project */ @@ -60,111 +93,178 @@ async function buildProject() { } /** - * Import and run all test modules + * Discover and run all test modules */ async function runTestModules() { console.log(`\n${colors.cyan}===== Running tests =====${colors.reset}\n`); - // Define static test module paths relative to this file - // We need to use relative paths with extension for ES modules - const testModules = [ - './test.js', - './test-directory-creation.js', - './test-allowed-directories.js', - './test-blocked-commands.js', - './test-home-directory.js' - ]; - - // Dynamically find additional test files (optional) - // Use the current directory (no need for a subdirectory) + // Discover all test files + let testFiles = []; try { const files = await fs.readdir(__dirname); - for (const file of files) { - // Only include files that aren't already in the testModules list - if (file.startsWith('test-') && file.endsWith('.js') && !testModules.includes(`./${file}`)) { - testModules.push(`./${file}`); - } + + // Get all test files, starting with 'test' and ending with '.js' + const discoveredTests = files + .filter(file => file.startsWith('test') && file.endsWith('.js') && file !== 'run-all-tests.js') + .sort(); // Sort for consistent order + + // Ensure main test.js runs first if it exists + if (discoveredTests.includes('test.js')) { + testFiles.push('./test.js'); + discoveredTests.splice(discoveredTests.indexOf('test.js'), 1); } + + // Add remaining tests + testFiles.push(...discoveredTests.map(file => `./${file}`)); + } catch (error) { - console.warn(`${colors.yellow}Warning: Could not scan test directory: ${error.message}${colors.reset}`); + console.error(`${colors.red}Error: Could not scan test directory: ${error.message}${colors.reset}`); + process.exit(1); } + if (testFiles.length === 0) { + console.warn(`${colors.yellow}Warning: No test files found${colors.reset}`); + return { success: true, results: [] }; + } + + console.log(`${colors.blue}Found ${testFiles.length} test files:${colors.reset}`); + testFiles.forEach(file => console.log(` - ${file}`)); + console.log(''); + // Results tracking - let passed = 0; - let failed = 0; - const failedTests = []; + const results = []; + let totalDuration = 0; - // Import and run each test module - for (const modulePath of testModules) { - try { - console.log(`\n${colors.cyan}Running test module: ${modulePath}${colors.reset}`); - - // Dynamic import of the test module - const testModule = await import(modulePath); - - // Get the default exported function - if (typeof testModule.default !== 'function') { - console.warn(`${colors.yellow}Warning: ${modulePath} does not export a default function${colors.reset}`); - continue; + // Run each test file + for (const testFile of testFiles) { + const result = await runTestFile(testFile); + results.push(result); + totalDuration += result.duration || 0; + } + + // Calculate summary statistics + const passed = results.filter(r => r.success).length; + const failed = results.filter(r => !r.success).length; + const failedTests = results.filter(r => !r.success); + + // Print detailed summary + console.log(`\n${colors.bold}${colors.cyan}===== TEST SUMMARY =====${colors.reset}\n`); + + // Overall stats + console.log(`${colors.bold}Overall Results:${colors.reset}`); + console.log(` Total tests: ${passed + failed}`); + console.log(` ${colors.green}✓ Passed: ${passed}${colors.reset}`); + console.log(` ${failed > 0 ? colors.red : colors.green}✗ Failed: ${failed}${colors.reset}`); + console.log(` Total duration: ${totalDuration}ms (${(totalDuration / 1000).toFixed(1)}s)`); + + // Failed tests details + if (failed > 0) { + console.log(`\n${colors.red}${colors.bold}Failed Tests:${colors.reset}`); + failedTests.forEach(test => { + console.log(` ${colors.red}✗ ${test.file}${colors.reset}`); + if (test.exitCode !== undefined) { + console.log(` Exit code: ${test.exitCode}`); } - - // Execute the test - const success = await testModule.default(); - - if (success) { - console.log(`${colors.green}✓ Test passed: ${modulePath}${colors.reset}`); - passed++; - } else { - console.error(`${colors.red}✗ Test failed: ${modulePath}${colors.reset}`); - failed++; - failedTests.push(modulePath); + if (test.error) { + console.log(` Error: ${test.error}`); } - } catch (error) { - console.error(`${colors.red}✗ Error importing or running ${modulePath}: ${error.message}${colors.reset}`); - failed++; - failedTests.push(modulePath); - } + }); } - // Print summary - console.log(`\n${colors.cyan}===== Test Summary =====${colors.reset}\n`); - console.log(`Total tests: ${passed + failed}`); - console.log(`${colors.green}Passed: ${passed}${colors.reset}`); + // Test performance summary + if (results.length > 0) { + console.log(`\n${colors.bold}Performance Summary:${colors.reset}`); + const avgDuration = totalDuration / results.length; + const slowestTest = results.reduce((prev, current) => + (current.duration || 0) > (prev.duration || 0) ? current : prev + ); + const fastestTest = results.reduce((prev, current) => + (current.duration || 0) < (prev.duration || 0) ? current : prev + ); + + console.log(` Average test duration: ${avgDuration.toFixed(0)}ms`); + console.log(` Fastest test: ${fastestTest.file} (${fastestTest.duration || 0}ms)`); + console.log(` Slowest test: ${slowestTest.file} (${slowestTest.duration || 0}ms)`); + } - if (failed > 0) { - console.log(`${colors.red}Failed: ${failed}${colors.reset}`); - console.log(`\nFailed tests:`); - failedTests.forEach(test => console.log(`${colors.red}- ${test}${colors.reset}`)); - return false; + // Final status + if (failed === 0) { + console.log(`\n${colors.green}${colors.bold}🎉 ALL TESTS PASSED! 🎉${colors.reset}`); + console.log(`${colors.green}All ${passed} tests completed successfully.${colors.reset}`); } else { - console.log(`\n${colors.green}All tests passed! 🎉${colors.reset}`); - return true; + console.log(`\n${colors.red}${colors.bold}❌ TESTS FAILED ❌${colors.reset}`); + console.log(`${colors.red}${failed} out of ${passed + failed} tests failed.${colors.reset}`); } + + console.log(`\n${colors.cyan}===== Test run completed =====${colors.reset}\n`); + + return { + success: failed === 0, + results, + summary: { + total: passed + failed, + passed, + failed, + duration: totalDuration + } + }; } /** * Main function */ async function main() { + const overallStartTime = Date.now(); + try { - console.log(`${colors.cyan}===== Starting test runner =====\n${colors.reset}`); + console.log(`${colors.bold}${colors.cyan}===== DESKTOP COMMANDER TEST RUNNER =====${colors.reset}`); + console.log(`${colors.blue}Starting test execution at ${new Date().toISOString()}${colors.reset}\n`); // Build the project first await buildProject(); // Run all test modules - const success = await runTestModules(); + const testResult = await runTestModules(); + + // Final timing + const overallDuration = Date.now() - overallStartTime; + console.log(`${colors.blue}Total execution time: ${overallDuration}ms (${(overallDuration / 1000).toFixed(1)}s)${colors.reset}`); // Exit with appropriate code - process.exit(success ? 0 : 1); + process.exit(testResult.success ? 0 : 1); + } catch (error) { - console.error(`${colors.red}Error: ${error.message}${colors.reset}`); + console.error(`\n${colors.red}${colors.bold}FATAL ERROR:${colors.reset}`); + console.error(`${colors.red}${error.message}${colors.reset}`); + if (error.stack) { + console.error(`${colors.red}${error.stack}${colors.reset}`); + } process.exit(1); } } +// Handle uncaught errors gracefully +process.on('uncaughtException', (error) => { + console.error(`\n${colors.red}${colors.bold}UNCAUGHT EXCEPTION:${colors.reset}`); + console.error(`${colors.red}${error.message}${colors.reset}`); + if (error.stack) { + console.error(`${colors.red}${error.stack}${colors.reset}`); + } + process.exit(1); +}); + +process.on('unhandledRejection', (reason, promise) => { + console.error(`\n${colors.red}${colors.bold}UNHANDLED REJECTION:${colors.reset}`); + console.error(`${colors.red}${reason}${colors.reset}`); + process.exit(1); +}); + // Run the main function main().catch(error => { - console.error(`${colors.red}Unhandled error: ${error}${colors.reset}`); + console.error(`\n${colors.red}${colors.bold}MAIN FUNCTION ERROR:${colors.reset}`); + console.error(`${colors.red}${error.message}${colors.reset}`); + if (error.stack) { + console.error(`${colors.red}${error.stack}${colors.reset}`); + } process.exit(1); }); From 471bc0cacbef18e384c5a8db4fc114d8f7bccc62 Mon Sep 17 00:00:00 2001 From: eduardruzga Date: Wed, 4 Jun 2025 19:38:26 +0300 Subject: [PATCH 09/13] Add support for negative file reads --- src/server.ts | 15 ++ src/tools/edit.ts | 6 +- src/tools/filesystem.ts | 314 ++++++++++++++++++++++---- test/test-negative-offset-analysis.js | 36 +++ test/test-negative-offset-readfile.js | 298 ++++++++++++++++++++++++ 5 files changed, 624 insertions(+), 45 deletions(-) create mode 100644 test/test-negative-offset-analysis.js create mode 100644 test/test-negative-offset-readfile.js diff --git a/src/server.ts b/src/server.ts index 7b8ceec..903b21b 100644 --- a/src/server.ts +++ b/src/server.ts @@ -136,7 +136,22 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { Supports partial file reading with: - 'offset' (start line, default: 0) + * Positive: Start from line N (0-based indexing) + * Negative: Read last N lines from end (tail behavior) - 'length' (max lines to read, default: configurable via 'fileReadLineLimit' setting, initially 1000) + * Used with positive offsets for range reading + * Ignored when offset is negative (reads all requested tail lines) + + Examples: + - offset: 0, length: 10 → First 10 lines + - offset: 100, length: 5 → Lines 100-104 + - offset: -20 → Last 20 lines + - offset: -5, length: 10 → Last 5 lines (length ignored) + + Performance optimizations: + - Large files with negative offsets use reverse reading for efficiency + - Large files with deep positive offsets use byte estimation + - Small files use fast readline streaming When reading from the file system, only works within allowed directories. Can fetch content from URLs when isUrl parameter is set to true diff --git a/src/tools/edit.ts b/src/tools/edit.ts index 175ce8b..f1f9def 100644 --- a/src/tools/edit.ts +++ b/src/tools/edit.ts @@ -1,4 +1,4 @@ -import { readFile, writeFile } from './filesystem.js'; +import { readFile, writeFile, readFileInternal } from './filesystem.js'; import { ServerResult } from '../types.js'; import { recursiveFuzzyIndexOf, getSimilarityRatio } from './fuzzySearch.js'; import { capture } from '../utils/capture.js'; @@ -119,8 +119,8 @@ export async function performSearchReplace(filePath: string, block: SearchReplac } - // Read file as plain string - const {content} = await readFile(filePath, false, 0, Number.MAX_SAFE_INTEGER); + // Read file as plain string without status messages + const content = await readFileInternal(filePath, 0, Number.MAX_SAFE_INTEGER); // Make sure content is a string if (typeof content !== 'string') { diff --git a/src/tools/filesystem.ts b/src/tools/filesystem.ts index 7b71e84..1b0abb1 100644 --- a/src/tools/filesystem.ts +++ b/src/tools/filesystem.ts @@ -2,6 +2,8 @@ import fs from "fs/promises"; import path from "path"; import os from 'os'; import fetch from 'cross-fetch'; +import { createReadStream } from 'fs'; +import { createInterface } from 'readline'; import {capture} from '../utils/capture.js'; import {withTimeout} from '../utils/withTimeout.js'; import {configManager} from '../config-manager.js'; @@ -247,6 +249,243 @@ export async function readFileFromUrl(url: string): Promise { } } +/** + * Read file content using smart positioning for optimal performance + * @param filePath Path to the file (already validated) + * @param offset Starting line number (negative for tail behavior) + * @param length Maximum number of lines to read + * @param mimeType MIME type of the file + * @param includeStatusMessage Whether to include status headers (default: true) + * @returns File result with content + */ +async function readFileWithSmartPositioning(filePath: string, offset: number, length: number, mimeType: string, includeStatusMessage: boolean = true): Promise { + const stats = await fs.stat(filePath); + const fileSize = stats.size; + const LARGE_FILE_THRESHOLD = 10 * 1024 * 1024; // 10MB threshold + const SMALL_READ_THRESHOLD = 100; // For very small reads, use efficient methods + + // For negative offsets (tail behavior), use reverse reading + if (offset < 0) { + const requestedLines = Math.abs(offset); + + if (fileSize > LARGE_FILE_THRESHOLD && requestedLines <= SMALL_READ_THRESHOLD) { + // Use efficient reverse reading for large files with small tail requests + return await readLastNLinesReverse(filePath, requestedLines, mimeType, includeStatusMessage); + } else { + // Use readline circular buffer for other cases + return await readFromEndWithReadline(filePath, requestedLines, mimeType, includeStatusMessage); + } + } + + // For positive offsets + else { + // For small files or reading from start, use simple readline + if (fileSize < LARGE_FILE_THRESHOLD || offset === 0) { + return await readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage); + } + + // For large files with middle/end reads, try to estimate position + else { + // If seeking deep into file, try byte estimation + if (offset > 1000) { + return await readFromEstimatedPosition(filePath, offset, length, mimeType, includeStatusMessage); + } else { + return await readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage); + } + } + } +} + +/** + * Read last N lines efficiently by reading file backwards in chunks + */ +async function readLastNLinesReverse(filePath: string, n: number, mimeType: string, includeStatusMessage: boolean = true): Promise { + const fd = await fs.open(filePath, 'r'); + try { + const stats = await fd.stat(); + const fileSize = stats.size; + + const chunkSize = 8192; // 8KB chunks + let position = fileSize; + let lines: string[] = []; + let partialLine = ''; + + while (position > 0 && lines.length < n) { + const readSize = Math.min(chunkSize, position); + position -= readSize; + + const buffer = Buffer.alloc(readSize); + await fd.read(buffer, 0, readSize, position); + + const chunk = buffer.toString('utf-8'); + const text = chunk + partialLine; + const chunkLines = text.split('\n'); + + partialLine = chunkLines.shift() || ''; + lines = chunkLines.concat(lines); + } + + // Add the remaining partial line if we reached the beginning + if (position === 0 && partialLine) { + lines.unshift(partialLine); + } + + const result = lines.slice(-n); // Get exactly n lines + const content = includeStatusMessage + ? `[Reading last ${result.length} lines]\n\n${result.join('\n')}` + : result.join('\n'); + + return { content, mimeType, isImage: false }; + } finally { + await fd.close(); + } +} + +/** + * Read from end using readline with circular buffer + */ +async function readFromEndWithReadline(filePath: string, requestedLines: number, mimeType: string, includeStatusMessage: boolean = true): Promise { + const rl = createInterface({ + input: createReadStream(filePath), + crlfDelay: Infinity + }); + + const buffer: string[] = new Array(requestedLines); + let bufferIndex = 0; + let totalLines = 0; + + for await (const line of rl) { + buffer[bufferIndex] = line; + bufferIndex = (bufferIndex + 1) % requestedLines; + totalLines++; + } + + rl.close(); + + // Extract lines in correct order + let result: string[]; + if (totalLines >= requestedLines) { + result = [ + ...buffer.slice(bufferIndex), + ...buffer.slice(0, bufferIndex) + ].filter(line => line !== undefined); + } else { + result = buffer.slice(0, totalLines); + } + + const content = includeStatusMessage + ? `[Reading last ${result.length} lines]\n\n${result.join('\n')}` + : result.join('\n'); + return { content, mimeType, isImage: false }; +} + +/** + * Read from start/middle using readline + */ +async function readFromStartWithReadline(filePath: string, offset: number, length: number, mimeType: string, includeStatusMessage: boolean = true): Promise { + const rl = createInterface({ + input: createReadStream(filePath), + crlfDelay: Infinity + }); + + const result: string[] = []; + let lineNumber = 0; + + for await (const line of rl) { + if (lineNumber >= offset && result.length < length) { + result.push(line); + } + if (result.length >= length) break; // Early exit optimization + lineNumber++; + } + + rl.close(); + + if (includeStatusMessage) { + const statusMessage = offset === 0 + ? `[Reading ${result.length} lines from start]` + : `[Reading ${result.length} lines from line ${offset}]`; + const content = `${statusMessage}\n\n${result.join('\n')}`; + return { content, mimeType, isImage: false }; + } else { + const content = result.join('\n'); + return { content, mimeType, isImage: false }; + } +} + +/** + * Read from estimated byte position for very large files + */ +async function readFromEstimatedPosition(filePath: string, offset: number, length: number, mimeType: string, includeStatusMessage: boolean = true): Promise { + // First, do a quick scan to estimate lines per byte + const rl = createInterface({ + input: createReadStream(filePath), + crlfDelay: Infinity + }); + + let sampleLines = 0; + let bytesRead = 0; + const SAMPLE_SIZE = 10000; // Sample first 10KB + + for await (const line of rl) { + bytesRead += Buffer.byteLength(line, 'utf-8') + 1; // +1 for newline + sampleLines++; + if (bytesRead >= SAMPLE_SIZE) break; + } + + rl.close(); + + if (sampleLines === 0) { + // Fallback to simple read + return await readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage); + } + + // Estimate average line length and seek position + const avgLineLength = bytesRead / sampleLines; + const estimatedBytePosition = Math.floor(offset * avgLineLength); + + // Create a new stream starting from estimated position + const fd = await fs.open(filePath, 'r'); + try { + const stats = await fd.stat(); + const startPosition = Math.min(estimatedBytePosition, stats.size); + + const stream = createReadStream(filePath, { start: startPosition }); + const rl2 = createInterface({ + input: stream, + crlfDelay: Infinity + }); + + const result: string[] = []; + let lineCount = 0; + let firstLineSkipped = false; + + for await (const line of rl2) { + // Skip first potentially partial line if we didn't start at beginning + if (!firstLineSkipped && startPosition > 0) { + firstLineSkipped = true; + continue; + } + + if (result.length < length) { + result.push(line); + } else { + break; + } + lineCount++; + } + + rl2.close(); + + const content = includeStatusMessage + ? `[Reading ${result.length} lines from estimated position (target line ${offset})]\n\n${result.join('\n')}` + : result.join('\n'); + return { content, mimeType, isImage: false }; + } finally { + await fd.close(); + } +} + /** * Read file content from the local filesystem * @param filePath Path to the file @@ -308,49 +547,9 @@ export async function readFileFromDisk(filePath: string, offset: number = 0, len return { content, mimeType, isImage }; } else { - // For all other files, try to read as UTF-8 text with line-based offset and length + // For all other files, use smart positioning approach try { - // Read the entire file first - const buffer = await fs.readFile(validPath); - const fullContent = buffer.toString('utf-8'); - - // Split into lines for line-based access - const lines = fullContent.split('\n'); - const totalLines = lines.length; - - // Apply line-based offset and length - handle beyond-file-size scenario - let startLine = Math.min(offset, totalLines); - let endLine = Math.min(startLine + length, totalLines); - - // If startLine equals totalLines (reading beyond end), adjust to show some content - // Only do this if we're not trying to read the whole file - if (startLine === totalLines && offset > 0 && length < Number.MAX_SAFE_INTEGER) { - // Show last few lines instead of nothing - const lastLinesCount = Math.min(10, totalLines); // Show last 10 lines or fewer if file is smaller - startLine = Math.max(0, totalLines - lastLinesCount); - endLine = totalLines; - } - - const selectedLines = lines.slice(startLine, endLine); - const truncatedContent = selectedLines.join('\n'); - - // Add an informational message if truncated or adjusted - let content = truncatedContent; - - // Only add informational message for normal reads (not when reading entire file) - const isEntireFileRead = offset === 0 && length >= Number.MAX_SAFE_INTEGER; - - if (!isEntireFileRead) { - if (offset >= totalLines && totalLines > 0) { - // Reading beyond end of file case - content = `[NOTICE: Offset ${offset} exceeds file length (${totalLines} lines). Showing last ${endLine - startLine} lines instead.]\n\n${truncatedContent}`; - } else if (offset > 0 || endLine < totalLines) { - // Normal partial read case - content = `[Reading ${endLine - startLine} lines from line ${startLine} of ${totalLines} total lines]\n\n${truncatedContent}`; - } - } - - return { content, mimeType, isImage }; + return await readFileWithSmartPositioning(validPath, offset, length, mimeType, true); } catch (error) { // If UTF-8 reading fails, treat as binary and return base64 but still as text const buffer = await fs.readFile(validPath); @@ -389,6 +588,37 @@ export async function readFile(filePath: string, isUrl?: boolean, offset?: numbe : readFileFromDisk(filePath, offset, length); } +/** + * Read file content without status messages for internal operations + * @param filePath Path to the file + * @param offset Starting line number to read from (default: 0) + * @param length Maximum number of lines to read (default: from config or 1000) + * @returns File content without status headers + */ +export async function readFileInternal(filePath: string, offset: number = 0, length?: number): Promise { + // Get default length from config if not provided + if (length === undefined) { + const config = await configManager.getConfig(); + length = config.fileReadLineLimit ?? 1000; + } + + const validPath = await validatePath(filePath); + + // Get file extension and MIME type + const fileExtension = path.extname(validPath).toLowerCase(); + const { getMimeType, isImageFile } = await import('./mime-types.js'); + const mimeType = getMimeType(validPath); + const isImage = isImageFile(mimeType); + + if (isImage) { + throw new Error('Cannot read image files as text for internal operations'); + } + + // Use smart positioning without status messages + const result = await readFileWithSmartPositioning(validPath, offset, length, mimeType, false); + return result.content; +} + export async function writeFile(filePath: string, content: string, mode: 'rewrite' | 'append' = 'rewrite'): Promise { const validPath = await validatePath(filePath); diff --git a/test/test-negative-offset-analysis.js b/test/test-negative-offset-analysis.js new file mode 100644 index 0000000..7058cce --- /dev/null +++ b/test/test-negative-offset-analysis.js @@ -0,0 +1,36 @@ +/** + * Test Results: Negative Offset Analysis for read_file + * + * FINDINGS: + * ❌ Negative offsets DO NOT work correctly in the current implementation + * ❌ They return empty content due to invalid slice() range calculations + * ⚠️ The implementation has a bug when handling negative offsets + * + * CURRENT BEHAVIOR: + * - offset: -2, length: 5 → slice(-2, 3) → returns empty [] + * - offset: -100, length: undefined → slice(-100, undefined) → works by accident + * + * RECOMMENDATION: + * Either fix the implementation to properly support negative offsets, + * or add validation to reject them with a clear error message. + */ + +console.log("🔍 NEGATIVE OFFSET BEHAVIOR ANALYSIS"); +console.log("===================================="); +console.log(""); +console.log("❌ CONCLUSION: Negative offsets are BROKEN in current implementation"); +console.log(""); +console.log("🐛 BUG DETAILS:"); +console.log(" Current code: Math.min(offset, totalLines) creates invalid ranges"); +console.log(" Example: offset=-2, totalLines=6 → slice(-2, 3) → empty result"); +console.log(""); +console.log("✅ ACCIDENTAL SUCCESS:"); +console.log(" My original attempt worked because length was undefined"); +console.log(" slice(-100, undefined) → slice(-100) → works correctly"); +console.log(""); +console.log("🔧 NEEDS FIX:"); +console.log(" Either implement proper negative offset support or reject them"); + +export default async function runTests() { + return false; // Test documents that negative offsets are broken +} \ No newline at end of file diff --git a/test/test-negative-offset-readfile.js b/test/test-negative-offset-readfile.js new file mode 100644 index 0000000..06b06b6 --- /dev/null +++ b/test/test-negative-offset-readfile.js @@ -0,0 +1,298 @@ +/** + * Test script for negative offset handling in read_file + * + * This script tests: + * 1. Whether negative offsets work correctly (like Unix tail) + * 2. How the tool handles edge cases with negative offsets + * 3. Comparison with positive offset behavior + * 4. Error handling for invalid parameters + */ + +import { configManager } from '../dist/config-manager.js'; +import { handleReadFile } from '../dist/handlers/filesystem-handlers.js'; +import fs from 'fs/promises'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import assert from 'assert'; + +// Get directory name +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Define test paths +const TEST_FILE = path.join(__dirname, 'test-negative-offset.txt'); + +/** + * Setup function to prepare test environment + */ +async function setup() { + console.log('🔧 Setting up negative offset test...'); + + // Save original config to restore later + const originalConfig = await configManager.getConfig(); + + // Set allowed directories to include test directory + await configManager.setValue('allowedDirectories', [__dirname]); + + // Create test file with numbered lines for easy verification + const testLines = []; + for (let i = 1; i <= 50; i++) { + testLines.push(`Line ${i}: This is line number ${i} in the test file.`); + } + const testContent = testLines.join('\n'); + + await fs.writeFile(TEST_FILE, testContent, 'utf8'); + console.log(`✓ Created test file with 50 lines: ${TEST_FILE}`); + + return originalConfig; +} + +/** + * Teardown function to clean up after tests + */ +async function teardown(originalConfig) { + console.log('🧹 Cleaning up test environment...'); + + // Reset configuration to original + await configManager.updateConfig(originalConfig); + + // Remove test file + try { + await fs.rm(TEST_FILE, { force: true }); + console.log('✓ Test file cleaned up'); + } catch (error) { + console.log('⚠️ Warning: Could not clean up test file:', error.message); + } +} + +/** + * Test negative offset functionality + */ +async function testNegativeOffset() { + console.log('\n📋 Testing negative offset behavior...'); + + const tests = [ + { + name: 'Negative offset -10 (last 10 lines)', + args: { path: TEST_FILE, offset: -10, length: 20 }, + expectLines: ['Line 41:', 'Line 42:', 'Line 43:', 'Line 44:', 'Line 45:', 'Line 46:', 'Line 47:', 'Line 48:', 'Line 49:', 'Line 50:'] + }, + { + name: 'Negative offset -5 (last 5 lines)', + args: { path: TEST_FILE, offset: -5, length: 10 }, + expectLines: ['Line 46:', 'Line 47:', 'Line 48:', 'Line 49:', 'Line 50:'] + }, + { + name: 'Negative offset -1 (last 1 line)', + args: { path: TEST_FILE, offset: -1, length: 5 }, + expectLines: ['Line 50:'] + }, + { + name: 'Large negative offset -100 (beyond file size)', + args: { path: TEST_FILE, offset: -100, length: 10 }, + expectLines: ['Line 1:', 'Line 2:', 'Line 3:', 'Line 4:', 'Line 5:', 'Line 6:', 'Line 7:', 'Line 8:', 'Line 9:', 'Line 10:'] + } + ]; + + let passedTests = 0; + + for (const test of tests) { + console.log(`\n 🧪 ${test.name}`); + + try { + const result = await handleReadFile(test.args); + + if (result.isError) { + console.log(` ❌ Error: ${result.content[0].text}`); + continue; + } + + const content = result.content[0].text; + console.log(` 📄 Result (first 200 chars): ${content.substring(0, 200)}...`); + + // Check if expected lines are present + let foundExpected = 0; + for (const expectedLine of test.expectLines) { + if (content.includes(expectedLine)) { + foundExpected++; + } + } + + if (foundExpected === test.expectLines.length) { + console.log(` ✅ PASS: Found all ${foundExpected} expected lines`); + passedTests++; + } else { + console.log(` ❌ FAIL: Found only ${foundExpected}/${test.expectLines.length} expected lines`); + console.log(` Expected: ${test.expectLines.join(', ')}`); + } + + } catch (error) { + console.log(` ❌ Exception: ${error.message}`); + } + } + + return passedTests === tests.length; +} + +/** + * Test comparison between negative and positive offsets + */ +async function testOffsetComparison() { + console.log('\n📊 Testing offset comparison (negative vs positive)...'); + + try { + // Test reading last 5 lines with negative offset + const negativeResult = await handleReadFile({ + path: TEST_FILE, + offset: -5, + length: 10 + }); + + // Test reading same lines with positive offset (45 to get last 5 lines of 50) + const positiveResult = await handleReadFile({ + path: TEST_FILE, + offset: 45, + length: 5 + }); + + if (negativeResult.isError || positiveResult.isError) { + console.log(' ❌ One or both requests failed'); + return false; + } + + const negativeContent = negativeResult.content[0].text; + const positiveContent = positiveResult.content[0].text; + + console.log(' 📄 Negative offset result:'); + console.log(` ${negativeContent.split('\n').slice(2, 4).join('\\n')}`); // Skip header lines + + console.log(' 📄 Positive offset result:'); + console.log(` ${positiveContent.split('\n').slice(2, 4).join('\\n')}`); // Skip header lines + + // Extract actual content lines (skip informational headers) + const negativeLines = negativeContent.split('\n').filter(line => line.startsWith('Line ')); + const positiveLines = positiveContent.split('\n').filter(line => line.startsWith('Line ')); + + const isMatching = negativeLines.join('\\n') === positiveLines.join('\\n'); + + if (isMatching) { + console.log(' ✅ PASS: Negative and positive offsets return same content'); + return true; + } else { + console.log(' ❌ FAIL: Negative and positive offsets return different content'); + console.log(` Negative: ${negativeLines.slice(0, 2).join(', ')}`); + console.log(` Positive: ${positiveLines.slice(0, 2).join(', ')}`); + return false; + } + + } catch (error) { + console.log(` ❌ Exception during comparison: ${error.message}`); + return false; + } +} + +/** + * Test edge cases and error handling + */ +async function testEdgeCases() { + console.log('\n🔍 Testing edge cases...'); + + const edgeTests = [ + { + name: 'Zero offset with length', + args: { path: TEST_FILE, offset: 0, length: 3 }, + shouldPass: true + }, + { + name: 'Very large negative offset', + args: { path: TEST_FILE, offset: -1000, length: 5 }, + shouldPass: true // Should handle gracefully + }, + { + name: 'Negative offset with zero length', + args: { path: TEST_FILE, offset: -5, length: 0 }, + shouldPass: true // Should return empty or minimal content + } + ]; + + let passedEdgeTests = 0; + + for (const test of edgeTests) { + console.log(`\n 🧪 ${test.name}`); + + try { + const result = await handleReadFile(test.args); + + if (result.isError && test.shouldPass) { + console.log(` ❌ Unexpected error: ${result.content[0].text}`); + } else if (!result.isError && test.shouldPass) { + console.log(` ✅ PASS: Handled gracefully`); + console.log(` 📄 Result length: ${result.content[0].text.length} chars`); + passedEdgeTests++; + } else if (result.isError && !test.shouldPass) { + console.log(` ✅ PASS: Expected error occurred`); + passedEdgeTests++; + } + + } catch (error) { + if (test.shouldPass) { + console.log(` ❌ Unexpected exception: ${error.message}`); + } else { + console.log(` ✅ PASS: Expected exception occurred`); + passedEdgeTests++; + } + } + } + + return passedEdgeTests === edgeTests.length; +} + +/** + * Main test runner + */ +async function runAllTests() { + console.log('🧪 Starting negative offset read_file tests...\n'); + + let originalConfig; + let allTestsPassed = true; + + try { + originalConfig = await setup(); + + // Run all test suites + const negativeOffsetPassed = await testNegativeOffset(); + const comparisonPassed = await testOffsetComparison(); + const edgeCasesPassed = await testEdgeCases(); + + allTestsPassed = negativeOffsetPassed && comparisonPassed && edgeCasesPassed; + + console.log('\n📊 Test Results Summary:'); + console.log(` Negative offset tests: ${negativeOffsetPassed ? '✅ PASS' : '❌ FAIL'}`); + console.log(` Comparison tests: ${comparisonPassed ? '✅ PASS' : '❌ FAIL'}`); + console.log(` Edge case tests: ${edgeCasesPassed ? '✅ PASS' : '❌ FAIL'}`); + console.log(`\n🎯 Overall result: ${allTestsPassed ? '✅ ALL TESTS PASSED!' : '❌ SOME TESTS FAILED'}`); + + } catch (error) { + console.error('❌ Test setup/execution failed:', error.message); + allTestsPassed = false; + } finally { + if (originalConfig) { + await teardown(originalConfig); + } + } + + return allTestsPassed; +} + +// Export the main test function +export default runAllTests; + +// If this file is run directly (not imported), execute the test +if (import.meta.url === `file://${process.argv[1]}`) { + runAllTests().then(success => { + process.exit(success ? 0 : 1); + }).catch(error => { + console.error('❌ Unhandled error:', error); + process.exit(1); + }); +} \ No newline at end of file From d0f109d892e05a37312e799c6a7bc92e1acf7098 Mon Sep 17 00:00:00 2001 From: eduardruzga Date: Thu, 5 Jun 2025 09:51:23 +0300 Subject: [PATCH 10/13] Cleanup --- docs/enhanced-repl-terminal.md | 222 ------------ docs/repl-enhancement-plan.md | 502 -------------------------- docs/repl-with-terminal.md | 214 ----------- test/test_output/repl_test_output.txt | 15 - 4 files changed, 953 deletions(-) delete mode 100644 docs/enhanced-repl-terminal.md delete mode 100644 docs/repl-enhancement-plan.md delete mode 100644 docs/repl-with-terminal.md delete mode 100644 test/test_output/repl_test_output.txt diff --git a/docs/enhanced-repl-terminal.md b/docs/enhanced-repl-terminal.md deleted file mode 100644 index 7397b88..0000000 --- a/docs/enhanced-repl-terminal.md +++ /dev/null @@ -1,222 +0,0 @@ -# Enhanced Terminal Commands for REPL Environments - -This document explains the enhanced functionality for interacting with REPL (Read-Eval-Print Loop) environments using terminal commands. - -## New Features - -### 1. Timeout Support - -Both `read_output` and `send_input` now support a `timeout_ms` parameter: - -```javascript -// Read output with a 5 second timeout -const output = await readOutput({ - pid: pid, - timeout_ms: 5000 -}); -``` - -This prevents indefinite waiting for output and makes REPL interactions more reliable. - -### 2. Wait for REPL Response - -The `send_input` function now supports a `wait_for_prompt` parameter that waits for the REPL to finish processing and show a prompt: - -```javascript -// Send input and wait for the REPL prompt -const result = await sendInput({ - pid: pid, - input: 'print("Hello, world!")\n', - wait_for_prompt: true, - timeout_ms: 5000 -}); - -// The result includes the output from the command -console.log(result.content[0].text); -``` - -This eliminates the need for manual delays between sending input and reading output. - -### 3. Prompt Detection - -When `wait_for_prompt` is enabled, the function detects common REPL prompts: - -- Node.js: `>` -- Python: `>>>` or `...` -- And others - -This allows it to know when the REPL has finished processing a command. - -## Basic Workflow - -### 1. Starting a REPL Session - -Use the `execute_command` function to start a REPL environment in interactive mode: - -```javascript -// Start Python -const pythonResult = await executeCommand({ - command: 'python -i', // Use -i flag for interactive mode - timeout_ms: 10000 -}); - -// Extract PID from the result text -const pidMatch = result.content[0].text.match(/Command started with PID (\d+)/); -const pid = pidMatch ? parseInt(pidMatch[1]) : null; -``` - -### 2. Reading the Initial Prompt with Timeout - -After starting a REPL session, you can read the initial output with a timeout: - -```javascript -// Wait for REPL to initialize with a timeout -const initialOutput = await readOutput({ - pid, - timeout_ms: 2000 -}); -console.log("Initial prompt:", initialOutput.content[0].text); -``` - -### 3. Sending Code to the REPL and Waiting for Response - -Use the enhanced `send_input` function to send code to the REPL and wait for the response: - -```javascript -// Send a single-line command and wait for the prompt -const result = await sendInput({ - pid: pid, - input: 'print("Hello, world!")\n', - wait_for_prompt: true, - timeout_ms: 3000 -}); - -console.log("Output:", result.content[0].text); -``` - -### 4. Sending Multi-line Code Blocks - -You can also send multi-line code blocks and wait for the complete response: - -```javascript -// Send multi-line code block and wait for the prompt -const multilineCode = ` -def greet(name): - return f"Hello, {name}!" - -print(greet("World")) -`; - -const result = await sendInput({ - pid: pid, - input: multilineCode + '\n', - wait_for_prompt: true, - timeout_ms: 5000 -}); - -console.log("Output:", result.content[0].text); -``` - -### 5. Terminating the REPL Session - -When you're done, use `force_terminate` to end the session: - -```javascript -await forceTerminate({ pid }); -``` - -## Examples for Different REPL Environments - -### Python - -```javascript -// Start Python in interactive mode -const result = await executeCommand({ command: 'python -i' }); -const pidMatch = result.content[0].text.match(/Command started with PID (\d+)/); -const pid = pidMatch ? parseInt(pidMatch[1]) : null; - -// Read initial prompt with timeout -const initialOutput = await readOutput({ - pid, - timeout_ms: 2000 -}); - -// Run code and wait for response -const output = await sendInput({ - pid, - input: 'print("Hello from Python!")\n', - wait_for_prompt: true, - timeout_ms: 3000 -}); -``` - -### Node.js - -```javascript -// Start Node.js in interactive mode -const result = await executeCommand({ command: 'node -i' }); -const pidMatch = result.content[0].text.match(/Command started with PID (\d+)/); -const pid = pidMatch ? parseInt(pidMatch[1]) : null; - -// Read initial prompt with timeout -const initialOutput = await readOutput({ - pid, - timeout_ms: 2000 -}); - -// Run code and wait for response -const output = await sendInput({ - pid, - input: 'console.log("Hello from Node.js!")\n', - wait_for_prompt: true, - timeout_ms: 3000 -}); -``` - -## Tips and Best Practices - -1. **Set Appropriate Timeouts**: Different commands may require different timeout values. Complex operations might need longer timeouts. - -2. **Use wait_for_prompt for Sequential Commands**: When running multiple commands that depend on each other, use `wait_for_prompt: true` to ensure commands are executed in order. - -3. **Add Newlines to Input**: Always add a newline character at the end of your input to trigger execution: - - ```javascript - await sendInput({ - pid, - input: 'your_code_here\n', - wait_for_prompt: true - }); - ``` - -4. **Handling Long-Running Operations**: For commands that take a long time to execute, increase the timeout value: - - ```javascript - await sendInput({ - pid, - input: 'import time; time.sleep(10); print("Done")\n', - wait_for_prompt: true, - timeout_ms: 15000 // 15 seconds - }); - ``` - -5. **Error Handling**: Check if a timeout was reached: - - ```javascript - const result = await sendInput({ - pid, - input: 'complex_calculation()\n', - wait_for_prompt: true, - timeout_ms: 5000 - }); - - if (result.content[0].text.includes('timeout reached')) { - console.log('Operation took too long'); - } - ``` - -## Complete Examples - -See the files: -- `test/enhanced-repl-example.js` for a complete example showing how to interact with Python and Node.js REPLs. -- `test/test-enhanced-repl.js` for tests of the enhanced functionality. diff --git a/docs/repl-enhancement-plan.md b/docs/repl-enhancement-plan.md deleted file mode 100644 index d806f96..0000000 --- a/docs/repl-enhancement-plan.md +++ /dev/null @@ -1,502 +0,0 @@ -w# REPL Enhancement Plan - -This document outlines the plan to enhance the terminal tools in ClaudeServerCommander to better support REPL (Read-Eval-Print Loop) environments. It continues the refactoring work we've already done to simplify the REPL implementation by using terminal commands. - -## Background - -We've successfully refactored the specialized REPL manager and tools to use the more general terminal commands (`execute_command`, `send_input`, `read_output`, and `force_terminate`). This approach is simpler, more flexible, and works for any interactive terminal environment without requiring specialized configurations. - -However, there are some enhancements we can make to improve the user experience when working with REPLs: - -1. Add timeout support to `read_output` and `send_input` -2. Enhance `send_input` to wait for REPL responses -3. Improve output collection and prompt detection - -## Files to Modify - -### 1. src/tools/schemas.ts - -Update the schemas to support the new parameters: - -```typescript -export const ReadOutputArgsSchema = z.object({ - pid: z.number(), - timeout_ms: z.number().optional(), -}); - -export const SendInputArgsSchema = z.object({ - pid: z.number(), - input: z.string(), - timeout_ms: z.number().optional(), - wait_for_prompt: z.boolean().optional(), -}); -``` - -### 2. src/tools/execute.js - -Enhance the `readOutput` function to handle timeouts and wait for complete output: - -```typescript -export async function readOutput(args) { - const parsed = ReadOutputArgsSchema.safeParse(args); - if (!parsed.success) { - return { - content: [{ type: "text", text: `Error: Invalid arguments for read_output: ${parsed.error}` }], - isError: true, - }; - } - - const { pid, timeout_ms = 5000 } = parsed.data; - - // Check if the process exists - const session = terminalManager.getSession(pid); - if (!session) { - return { - content: [{ type: "text", text: `No session found for PID ${pid}` }], - isError: true, - }; - } - - // Wait for output with timeout - let output = ""; - let timeoutReached = false; - - try { - // Create a promise that resolves when new output is available or when timeout is reached - const outputPromise = new Promise((resolve) => { - // Check for initial output - const initialOutput = terminalManager.getNewOutput(pid); - if (initialOutput && initialOutput.length > 0) { - resolve(initialOutput); - return; - } - - // Setup an interval to poll for output - const interval = setInterval(() => { - const newOutput = terminalManager.getNewOutput(pid); - if (newOutput && newOutput.length > 0) { - clearInterval(interval); - resolve(newOutput); - } - }, 100); // Check every 100ms - - // Set a timeout to stop waiting - setTimeout(() => { - clearInterval(interval); - timeoutReached = true; - resolve(terminalManager.getNewOutput(pid) || ""); - }, timeout_ms); - }); - - output = await outputPromise; - } catch (error) { - return { - content: [{ type: "text", text: `Error reading output: ${error}` }], - isError: true, - }; - } - - return { - content: [{ - type: "text", - text: output || 'No new output available' + (timeoutReached ? ' (timeout reached)' : '') - }], - }; -} -``` - -### 3. src/tools/send-input.js - -Enhance the `sendInput` function to wait for REPL responses: - -```typescript -export async function sendInput(args) { - const parsed = SendInputArgsSchema.safeParse(args); - if (!parsed.success) { - capture('server_send_input_failed', { - error: 'Invalid arguments' - }); - return { - content: [{ type: "text", text: `Error: Invalid arguments for send_input: ${parsed.error}` }], - isError: true, - }; - } - - const { pid, input, timeout_ms = 5000, wait_for_prompt = false } = parsed.data; - - try { - capture('server_send_input', { - pid: pid, - inputLength: input.length - }); - - // Try to send input to the process - const success = terminalManager.sendInputToProcess(pid, input); - - if (!success) { - return { - content: [{ type: "text", text: `Error: Failed to send input to process ${pid}. The process may have exited or doesn't accept input.` }], - isError: true, - }; - } - - // If we don't need to wait for output, return immediately - if (!wait_for_prompt) { - return { - content: [{ - type: "text", - text: `Successfully sent input to process ${pid}. Use read_output to get the process response.` - }], - }; - } - - // Wait for output with timeout - let output = ""; - let timeoutReached = false; - - try { - // Create a promise that resolves when new output is available or when timeout is reached - const outputPromise = new Promise((resolve) => { - // Setup an interval to poll for output - const interval = setInterval(() => { - const newOutput = terminalManager.getNewOutput(pid); - - if (newOutput && newOutput.length > 0) { - output += newOutput; - - // Check if output contains a prompt pattern (indicating the REPL is ready for more input) - const promptPatterns = [/^>\s*$/, /^>>>\s*$/, /^\.{3}\s*$/]; // Common REPL prompts - const hasPrompt = promptPatterns.some(pattern => pattern.test(newOutput.trim().split('\n').pop() || '')); - - if (hasPrompt) { - clearInterval(interval); - resolve(output); - } - } - }, 100); // Check every 100ms - - // Set a timeout to stop waiting - setTimeout(() => { - clearInterval(interval); - timeoutReached = true; - - // Get any final output - const finalOutput = terminalManager.getNewOutput(pid); - if (finalOutput) { - output += finalOutput; - } - - resolve(output); - }, timeout_ms); - }); - - await outputPromise; - } catch (error) { - return { - content: [{ type: "text", text: `Error reading output after sending input: ${error}` }], - isError: true, - }; - } - - return { - content: [{ - type: "text", - text: `Input sent to process ${pid}.\n\nOutput received:\n${output || '(No output)'}${timeoutReached ? ' (timeout reached)' : ''}` - }], - }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - capture('server_send_input_error', { - error: errorMessage - }); - return { - content: [{ type: "text", text: `Error sending input: ${errorMessage}` }], - isError: true, - }; - } -} -``` - -### 4. src/terminal-manager.ts - -Add a method to get a session by PID: - -```typescript -/** - * Get a session by PID - * @param pid Process ID - * @returns The session or undefined if not found - */ -getSession(pid: number): TerminalSession | undefined { - return this.sessions.get(pid); -} -``` - -## Tests to Create - -### 1. test/test-enhanced-repl.js - -Create a new test file to verify the enhanced functionality: - -```javascript -import assert from 'assert'; -import { executeCommand, readOutput, sendInput, forceTerminate } from '../dist/tools/execute.js'; -import { sendInput as sendInputDirectly } from '../dist/tools/send-input.js'; - -/** - * Test enhanced REPL functionality - */ -async function testEnhancedREPL() { - console.log('Testing enhanced REPL functionality...'); - - // Start Python in interactive mode - console.log('Starting Python REPL...'); - const result = await executeCommand({ - command: 'python -i', - timeout_ms: 10000 - }); - - // Extract PID from the result text - const pidMatch = result.content[0].text.match(/Command started with PID (\d+)/); - const pid = pidMatch ? parseInt(pidMatch[1]) : null; - - if (!pid) { - console.error("Failed to get PID from Python process"); - return false; - } - - console.log(`Started Python session with PID: ${pid}`); - - // Test read_output with timeout - console.log('Testing read_output with timeout...'); - const initialOutput = await readOutput({ - pid, - timeout_ms: 2000 - }); - console.log('Initial Python prompt:', initialOutput.content[0].text); - - // Test send_input with wait_for_prompt - console.log('Testing send_input with wait_for_prompt...'); - const inputResult = await sendInputDirectly({ - pid, - input: 'print("Hello from Python with wait!")\n', - wait_for_prompt: true, - timeout_ms: 5000 - }); - console.log('Python output with wait_for_prompt:', inputResult.content[0].text); - - // Test send_input without wait_for_prompt - console.log('Testing send_input without wait_for_prompt...'); - await sendInputDirectly({ - pid, - input: 'print("Hello from Python without wait!")\n', - wait_for_prompt: false - }); - - // Wait a moment for Python to process - await new Promise(resolve => setTimeout(resolve, 1000)); - - // Read the output - const output = await readOutput({ pid }); - console.log('Python output without wait_for_prompt:', output.content[0].text); - - // Test multi-line code with wait_for_prompt - console.log('Testing multi-line code with wait_for_prompt...'); - const multilineCode = ` -def greet(name): - return f"Hello, {name}!" - -for i in range(3): - print(greet(f"Guest {i+1}")) -`; - - const multilineResult = await sendInputDirectly({ - pid, - input: multilineCode + '\n', - wait_for_prompt: true, - timeout_ms: 5000 - }); - console.log('Python multi-line output with wait_for_prompt:', multilineResult.content[0].text); - - // Terminate the session - await forceTerminate({ pid }); - console.log('Python session terminated'); - - return true; -} - -// Run the test -testEnhancedREPL() - .then(success => { - console.log(`Enhanced REPL test ${success ? 'PASSED' : 'FAILED'}`); - process.exit(success ? 0 : 1); - }) - .catch(error => { - console.error('Test error:', error); - process.exit(1); - }); -``` - -## Example Code - -Update the `repl-via-terminal-example.js` file to use the enhanced functionality: - -```javascript -import { - executeCommand, - readOutput, - forceTerminate -} from '../dist/tools/execute.js'; -import { sendInput } from '../dist/tools/send-input.js'; - -// Example of starting and interacting with a Python REPL session -async function pythonREPLExample() { - console.log('Starting a Python REPL session...'); - - // Start Python interpreter in interactive mode - const result = await executeCommand({ - command: 'python -i', - timeout_ms: 10000 - }); - - // Extract PID from the result text - const pidMatch = result.content[0].text.match(/Command started with PID (\d+)/); - const pid = pidMatch ? parseInt(pidMatch[1]) : null; - - if (!pid) { - console.error("Failed to get PID from Python process"); - return; - } - - console.log(`Started Python session with PID: ${pid}`); - - // Initial read to get the Python prompt - console.log("Reading initial output..."); - const initialOutput = await readOutput({ - pid, - timeout_ms: 2000 - }); - console.log("Initial Python prompt:", initialOutput.content[0].text); - - // Send a simple Python command with wait_for_prompt - console.log("Sending simple command..."); - const simpleResult = await sendInput({ - pid, - input: 'print("Hello from Python!")\n', - wait_for_prompt: true, - timeout_ms: 3000 - }); - console.log('Python output with wait_for_prompt:', simpleResult.content[0].text); - - // Send a multi-line code block with wait_for_prompt - console.log("Sending multi-line code..."); - const multilineCode = ` -def greet(name): - return f"Hello, {name}!" - -for i in range(3): - print(greet(f"Guest {i+1}")) -`; - - const multilineResult = await sendInput({ - pid, - input: multilineCode + '\n', - wait_for_prompt: true, - timeout_ms: 5000 - }); - console.log('Python multi-line output with wait_for_prompt:', multilineResult.content[0].text); - - // Terminate the session - await forceTerminate({ pid }); - console.log('Python session terminated'); -} - -// Run the example -pythonREPLExample() - .catch(error => { - console.error('Error running example:', error); - }); -``` - -## Documentation Updates - -Update the `repl-with-terminal.md` file to document the enhanced functionality: - -```markdown -# Enhanced Terminal Commands for REPL Environments - -## New Features - -### 1. Timeout Support - -Both `read_output` and `send_input` now support a `timeout_ms` parameter: - -```javascript -// Read output with a 5 second timeout -const output = await readOutput({ - pid: pid, - timeout_ms: 5000 -}); -``` - -### 2. Wait for REPL Response - -The `send_input` function now supports a `wait_for_prompt` parameter that waits for the REPL to finish processing and show a prompt: - -```javascript -// Send input and wait for the REPL prompt -const result = await sendInput({ - pid: pid, - input: 'print("Hello, world!")\n', - wait_for_prompt: true, - timeout_ms: 5000 -}); - -// The result includes the output from the command -console.log(result.content[0].text); -``` - -### 3. Prompt Detection - -When `wait_for_prompt` is enabled, the function detects common REPL prompts: -- Node.js: `>` -- Python: `>>>` or `...` -- And others - -This allows it to know when the REPL has finished processing a command. -``` - -## Implementation Steps - -1. First, update the schemas to add the new parameters -2. Add the `getSession` method to the terminal manager -3. Enhance `readOutput` to support timeouts and waiting for output -4. Enhance `sendInput` to support waiting for REPL prompts -5. Create tests to verify the enhanced functionality -6. Update the example code and documentation - -## Testing Strategy - -1. Test with different REPL environments (Python, Node.js) -2. Test with single-line and multi-line code -3. Test with different timeout values -4. Test prompt detection for different REPLs -5. Verify that output is correctly captured and returned - -## Next Steps After Implementation - -1. Finalize code and run all tests -2. Create a pull request with the changes -3. Update the main documentation to reflect the enhanced functionality -4. Consider adding support for other REPL environments and prompt patterns - -## Previous Work - -These enhancements build on the successful refactoring of the REPL functionality to use terminal commands. We've already: - -1. Removed the specialized REPL manager -2. Enhanced the terminal manager to handle interactive sessions -3. Updated tests to verify the refactored approach -4. Created documentation and examples showing how to use terminal commands for REPLs - -The current changes will make these terminal commands even more effective for REPL environments by handling timeouts, waiting for responses, and detecting REPL prompts. diff --git a/docs/repl-with-terminal.md b/docs/repl-with-terminal.md deleted file mode 100644 index c704743..0000000 --- a/docs/repl-with-terminal.md +++ /dev/null @@ -1,214 +0,0 @@ -# Using Terminal Commands for REPL Environments - -This document explains how to use the standard terminal commands to interact with REPL (Read-Eval-Print Loop) environments like Python, Node.js, Ruby, PHP, and others. - -## Overview - -Instead of having specialized REPL tools, the ClaudeServerCommander uses the standard terminal commands to interact with any interactive environment. This approach is: - -- **Simple**: No need for language-specific configurations -- **Flexible**: Works with any REPL environment without special handling -- **Consistent**: Uses the same interface for all interactive sessions - -## Basic Workflow - -### 1. Starting a REPL Session - -Use the `execute_command` function to start a REPL environment in interactive mode: - -```javascript -// Start Python -const pythonResult = await executeCommand({ - command: 'python -i', // Use -i flag for interactive mode - timeout_ms: 10000 -}); - -// Start Node.js -const nodeResult = await executeCommand({ - command: 'node -i', // Use -i flag for interactive mode - timeout_ms: 10000 -}); - -// Extract PID from the result text -const pidMatch = result.content[0].text.match(/Command started with PID (\d+)/); -const pid = pidMatch ? parseInt(pidMatch[1]) : null; -``` - -### 2. Reading the Initial Prompt - -After starting a REPL session, you should read the initial output to capture the prompt: - -```javascript -// Wait for REPL to initialize -const initialOutput = await readOutput({ pid }); -console.log("Initial prompt:", initialOutput.content[0].text); -``` - -### 3. Sending Code to the REPL - -Use the `send_input` function to send code to the REPL, making sure to include a newline at the end: - -```javascript -// Send a single-line command -await sendInput({ - pid: pid, - input: 'print("Hello, world!")\n' // Python example -}); - -// Send multi-line code block -const multilineCode = ` -def greet(name): - return f"Hello, {name}!" - -print(greet("World")) -`; - -await sendInput({ - pid: pid, - input: multilineCode + '\n' // Add newline at the end -}); -``` - -### 4. Reading Output from the REPL - -Use the `read_output` function to get the results: - -```javascript -// Wait a moment for the REPL to process -await new Promise(resolve => setTimeout(resolve, 500)); - -// Read the output -const output = await readOutput({ pid }); -console.log("Output:", output.content[0].text); -``` - -### 5. Terminating the REPL Session - -When you're done, use `force_terminate` to end the session: - -```javascript -await forceTerminate({ pid }); -``` - -## Examples for Different REPL Environments - -### Python - -```javascript -// Start Python in interactive mode -const result = await executeCommand({ command: 'python -i' }); -const pidMatch = result.content[0].text.match(/Command started with PID (\d+)/); -const pid = pidMatch ? parseInt(pidMatch[1]) : null; - -// Read initial prompt -const initialOutput = await readOutput({ pid }); - -// Run code -await sendInput({ pid, input: 'print("Hello from Python!")\n' }); - -// Wait and read output -await new Promise(resolve => setTimeout(resolve, 500)); -const output = await readOutput({ pid }); -``` - -### Node.js - -```javascript -// Start Node.js in interactive mode -const result = await executeCommand({ command: 'node -i' }); -const pidMatch = result.content[0].text.match(/Command started with PID (\d+)/); -const pid = pidMatch ? parseInt(pidMatch[1]) : null; - -// Read initial prompt -const initialOutput = await readOutput({ pid }); - -// Run code -await sendInput({ pid, input: 'console.log("Hello from Node.js!")\n' }); - -// Wait and read output -await new Promise(resolve => setTimeout(resolve, 500)); -const output = await readOutput({ pid }); -``` - -### Ruby - -```javascript -// Start Ruby in interactive mode -const result = await executeCommand({ command: 'irb' }); -const pidMatch = result.content[0].text.match(/Command started with PID (\d+)/); -const pid = pidMatch ? parseInt(pidMatch[1]) : null; - -// Read initial prompt -const initialOutput = await readOutput({ pid }); - -// Run code -await sendInput({ pid, input: 'puts "Hello from Ruby!"\n' }); - -// Wait and read output -await new Promise(resolve => setTimeout(resolve, 500)); -const output = await readOutput({ pid }); -``` - -### PHP - -```javascript -// Start PHP in interactive mode -const result = await executeCommand({ command: 'php -a' }); -const pidMatch = result.content[0].text.match(/Command started with PID (\d+)/); -const pid = pidMatch ? parseInt(pidMatch[1]) : null; - -// Read initial prompt -const initialOutput = await readOutput({ pid }); - -// Run code -await sendInput({ pid, input: 'echo "Hello from PHP!";\n' }); - -// Wait and read output -await new Promise(resolve => setTimeout(resolve, 500)); -const output = await readOutput({ pid }); -``` - -## Tips and Best Practices - -1. **Always Use Interactive Mode**: Many interpreters have a specific flag for interactive mode: - - Python: `-i` flag - - Node.js: `-i` flag - - PHP: `-a` flag - -2. **Add Newlines to Input**: Always add a newline character at the end of your input to trigger execution: - - ```javascript - await sendInput({ pid, input: 'your_code_here\n' }); - ``` - -3. **Add Delays Between Operations**: Most REPLs need time to process input. Adding a small delay between sending input and reading output helps ensure you get the complete response: - - ```javascript - await sendInput({ pid, input: complexCode + '\n' }); - await new Promise(resolve => setTimeout(resolve, 500)); // 500ms delay - const output = await readOutput({ pid }); - ``` - -4. **Multi-line Code Handling**: Most REPLs can handle multi-line code blocks sent at once, but be sure to add a newline at the end: - - ```javascript - await sendInput({ - pid, - input: multilineCodeBlock + '\n' - }); - ``` - -5. **Error Handling**: Check the output for error messages: - - ```javascript - const output = await readOutput({ pid }); - const text = output.content[0].text; - if (text.includes('Error') || text.includes('Exception')) { - console.error('REPL returned an error:', text); - } - ``` - -## Complete Example - -See the file `test/repl-via-terminal-example.js` for a complete example showing how to interact with Python and Node.js REPLs using terminal commands. - diff --git a/test/test_output/repl_test_output.txt b/test/test_output/repl_test_output.txt deleted file mode 100644 index e591031..0000000 --- a/test/test_output/repl_test_output.txt +++ /dev/null @@ -1,15 +0,0 @@ -Python REPL output: -Python 3.12.0 (v3.12.0:0fb18b02c8, Oct 2 2023, 09:45:56) [Clang 13.0.0 (clang-1300.0.29.30)] on darwin -Type "help", "copyright", "credits" or "license" for more information. ->>> STARTING PYTHON TEST ->>> REPL_TEST_VALUE: 62 ->>> - -Node.js REPL output: -Welcome to Node.js v23.8.0. -Type ".help" for more information. -> STARTING NODE TEST -undefined -> NODE_REPL_TEST_VALUE: 27 -undefined -> \ No newline at end of file From f94f5dc25d9cbd1d0a86a43229d8b0fbe260a45d Mon Sep 17 00:00:00 2001 From: eduardruzga Date: Thu, 5 Jun 2025 22:54:38 +0300 Subject: [PATCH 11/13] Readme updates --- FAQ.md | 27 ++++++++++++++++++++++++++- README.md | 28 +++++++++++++++++++++++++--- docs/index.html | 26 +++++++++++++++++++++++--- 3 files changed, 74 insertions(+), 7 deletions(-) diff --git a/FAQ.md b/FAQ.md index e4390db..dcb3843 100644 --- a/FAQ.md +++ b/FAQ.md @@ -25,6 +25,11 @@ This document provides answers to the most commonly asked questions about Claude - [Features & Capabilities](#features--capabilities) - [What can I do with Claude Desktop Commander?](#what-can-i-do-with-claude-desktop-commander) + - [Can Claude analyze my CSV/Excel files directly?](#can-claude-analyze-my-csvexcel-files-directly) + - [Can Claude connect to remote servers?](#can-claude-connect-to-remote-servers) + - [Does Claude save temporary files when running code?](#does-claude-save-temporary-files-when-running-code) + - [What programming languages can Claude run interactively?](#what-programming-languages-can-claude-run-interactively) + - [Can Claude handle multi-step operations?](#can-claude-handle-multi-step-operations) - [How does it handle file editing?](#how-does-it-handle-file-editing) - [Can it help me understand complex codebases?](#can-it-help-me-understand-complex-codebases) - [How does it handle long-running commands?](#how-does-it-handle-long-running-commands) @@ -189,6 +194,26 @@ The tool enables a wide range of tasks: - Analyze and summarize codebases - Produce reports on code quality or structure +### Can Claude analyze my CSV/Excel files directly? + +Yes! Just ask Claude to analyze any data file. It will write and execute Python/Node code in memory to process your data and show results instantly. + +### Can Claude connect to remote servers? + +Yes! Claude can start SSH connections, databases, or other programs and continue interacting with them throughout your conversation. + +### Does Claude save temporary files when running code? + +If you ask. Code can run in memory. When you ask for data analysis, Claude executes Python/R code directly without creating files on your disk. Or creating if you ask. + +### What programming languages can Claude run interactively? + +Python, Node.js, R, Julia, and shell commands. Any interactive terminal REPL environments. Perfect for data analysis, web development, statistics, and system administration. + +### Can Claude handle multi-step operations? + +Yes! Claude can start a program (like SSH or database connection) and send multiple commands to it, maintaining context throughout the session. + ### How does it handle file editing and URL content? Claude Desktop Commander provides two main approaches to file editing and supports URL content: @@ -464,4 +489,4 @@ Jupyter notebooks and Claude Desktop Commander serve different purposes: - Visual output for data visualization - More structured for educational purposes -For data science or analysis projects, you might use both: Claude Desktop Commander for system tasks and code management, and Jupyter for interactive exploration and visualization. +For data science or analysis projects, you might use both: Claude Desktop Commander for system tasks and code management, and Jupyter for interactive exploration and visualization. \ No newline at end of file diff --git a/README.md b/README.md index db3e8b1..8341383 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,10 @@ Execute long-running terminal commands on your computer and manage processes thr ## Features +- **Enhanced terminal commands with interactive process control** +- **Execute code in memory (Python, Node.js, R) without saving files** +- **Instant data analysis - just ask to analyze CSV/JSON files** +- **Interact with running processes (SSH, databases, development servers)** - Execute terminal commands with output streaming - Command timeout and background execution support - Process management (list and kill processes) @@ -181,8 +185,9 @@ The server provides a comprehensive set of tools organized into several categori |----------|------|-------------| | **Configuration** | `get_config` | Get the complete server configuration as JSON (includes blockedCommands, defaultShell, allowedDirectories, fileReadLineLimit, fileWriteLineLimit, telemetryEnabled) | | | `set_config_value` | Set a specific configuration value by key. Available settings:
• `blockedCommands`: Array of shell commands that cannot be executed
• `defaultShell`: Shell to use for commands (e.g., bash, zsh, powershell)
• `allowedDirectories`: Array of filesystem paths the server can access for file operations (⚠️ terminal commands can still access files outside these directories)
• `fileReadLineLimit`: Maximum lines to read at once (default: 1000)
• `fileWriteLineLimit`: Maximum lines to write at once (default: 50)
• `telemetryEnabled`: Enable/disable telemetry (boolean) | -| **Terminal** | `execute_command` | Execute a terminal command with configurable timeout and shell selection | -| | `read_output` | Read new output from a running terminal session | +| **Terminal** | `start_process` | Start programs with smart detection of when they're ready for input | +| | `interact_with_process` | Send commands to running programs and get responses | +| | `read_process_output` | Read output from running processes | | | `force_terminate` | Force terminate a running terminal session | | | `list_sessions` | List all active terminal sessions | | | `list_processes` | List all running processes with detailed information | @@ -198,6 +203,23 @@ The server provides a comprehensive set of tools organized into several categori | | `get_file_info` | Retrieve detailed metadata about a file or directory | | **Text Editing** | `edit_block` | Apply targeted text replacements with enhanced prompting for smaller edits (includes character-level diff feedback) | +### Quick Examples + +**Data Analysis:** +``` +"Analyze sales.csv and show top customers" → Claude runs Python code in memory +``` + +**Remote Access:** +``` +"SSH to my server and check disk space" → Claude maintains SSH session +``` + +**Development:** +``` +"Start Node.js and test this API" → Claude runs interactive Node session +``` + ### Tool Usage Examples Search/Replace Block Format: @@ -614,4 +636,4 @@ For complete details about data collection, please see our [Privacy Policy](PRIV ## License -MIT +MIT \ No newline at end of file diff --git a/docs/index.html b/docs/index.html index fd60fdb..2cecdf4 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1015,8 +1015,8 @@

Smart file system integration

-

Full terminal access

-

Execute any command line operation directly through Claude's interface for seamless development, testing, and deployment workflows.

+

Interactive code execution

+

Execute Python, Node.js, R code in memory for instant data analysis. Connect to SSH, databases, and maintain persistent sessions for complex workflows.

@@ -1085,6 +1085,25 @@

AI DevOps

+ +
+
+ +
+

AI Data Analyst

+

Analyze CSV files, databases, and datasets instantly with Python and R.

+
+
    +
  • Instant CSV/Excel analysis
  • +
  • Database queries with persistent connections
  • +
  • Statistical analysis with R
  • +
  • Data visualization
  • +
  • Report generation
  • +
+
+ +
+
@@ -1990,6 +2009,7 @@

Use Cases

@@ -2143,4 +2163,4 @@

Resources

}); - + \ No newline at end of file From af885a400634b5b57cd27147b87465c2f1030c28 Mon Sep 17 00:00:00 2001 From: eduardruzga Date: Thu, 5 Jun 2025 23:16:33 +0300 Subject: [PATCH 12/13] Cleanup --- src/handlers/terminal-handlers.ts | 15 +-- src/server.ts | 22 +--- src/tools/edit.ts | 10 +- src/tools/execute.ts | 182 ---------------------------- src/tools/improved-process-tools.ts | 7 +- src/tools/schemas.ts | 9 +- src/tools/send-input.ts | 50 -------- test/test-blocked-commands.js | 8 +- test/test-default-shell.js | 6 +- test/test-enhanced-repl.js | 15 ++- 10 files changed, 29 insertions(+), 295 deletions(-) delete mode 100644 src/tools/execute.ts delete mode 100644 src/tools/send-input.ts diff --git a/src/handlers/terminal-handlers.ts b/src/handlers/terminal-handlers.ts index 15cbf56..1f6930f 100644 --- a/src/handlers/terminal-handlers.ts +++ b/src/handlers/terminal-handlers.ts @@ -52,17 +52,4 @@ export async function handleForceTerminate(args: unknown): Promise */ export async function handleListSessions(): Promise { return listSessions(); -} - -// Backward compatibility handlers -export async function handleExecuteCommand(args: unknown): Promise { - return handleStartProcess(args); -} - -export async function handleReadOutput(args: unknown): Promise { - return handleReadProcessOutput(args); -} - -export async function handleSendInput(args: unknown): Promise { - return handleInteractWithProcess(args); -} +} \ No newline at end of file diff --git a/src/server.ts b/src/server.ts index 903b21b..0cfea67 100644 --- a/src/server.ts +++ b/src/server.ts @@ -14,8 +14,9 @@ const PATH_GUIDANCE = `IMPORTANT: Always use absolute paths (starting with '/' o const CMD_PREFIX_DESCRIPTION = `This command can be referenced as "DC: ..." or "use Desktop Commander to ..." in your instructions.`; import { - ExecuteCommandArgsSchema, - ReadOutputArgsSchema, + StartProcessArgsSchema, + ReadProcessOutputArgsSchema, + InteractWithProcessArgsSchema, ForceTerminateArgsSchema, ListSessionsArgsSchema, KillProcessArgsSchema, @@ -32,7 +33,6 @@ import { SetConfigValueArgsSchema, ListProcessesArgsSchema, EditBlockArgsSchema, - SendInputArgsSchema, } from './tools/schemas.js'; import {getConfig, setConfigValue} from './tools/config.js'; import {trackToolCall} from './utils/trackTools.js'; @@ -390,7 +390,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { ${PATH_GUIDANCE} ${CMD_PREFIX_DESCRIPTION}`, - inputSchema: zodToJsonSchema(ExecuteCommandArgsSchema), + inputSchema: zodToJsonSchema(StartProcessArgsSchema), }, { name: "read_process_output", @@ -417,7 +417,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { ⏱️ Timeout reached (may still be running) ${CMD_PREFIX_DESCRIPTION}`, - inputSchema: zodToJsonSchema(ReadOutputArgsSchema), + inputSchema: zodToJsonSchema(ReadProcessOutputArgsSchema), }, { name: "interact_with_process", @@ -466,7 +466,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { ❌ NEVER USE ANALYSIS TOOL FOR: Local file access (it cannot read files from disk and WILL FAIL) ${CMD_PREFIX_DESCRIPTION}`, - inputSchema: zodToJsonSchema(SendInputArgsSchema), + inputSchema: zodToJsonSchema(InteractWithProcessArgsSchema), }, { name: "force_terminate", @@ -596,16 +596,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest) case "interact_with_process": return await handlers.handleInteractWithProcess(args); - // Backward compatibility - case "execute_command": - return await handlers.handleStartProcess(args); - - case "read_output": - return await handlers.handleReadProcessOutput(args); - - case "send_input": - return await handlers.handleInteractWithProcess(args); - case "force_terminate": return await handlers.handleForceTerminate(args); diff --git a/src/tools/edit.ts b/src/tools/edit.ts index f1f9def..76e94a3 100644 --- a/src/tools/edit.ts +++ b/src/tools/edit.ts @@ -1,4 +1,5 @@ -import { readFile, writeFile, readFileInternal } from './filesystem.js'; +import { readFile, writeFile, readFileInternal, validatePath } from './filesystem.js'; +import fs from 'fs/promises'; import { ServerResult } from '../types.js'; import { recursiveFuzzyIndexOf, getSimilarityRatio } from './fuzzySearch.js'; import { capture } from '../utils/capture.js'; @@ -119,8 +120,9 @@ export async function performSearchReplace(filePath: string, block: SearchReplac } - // Read file as plain string without status messages - const content = await readFileInternal(filePath, 0, Number.MAX_SAFE_INTEGER); + // Read file directly to preserve line endings - critical for edit operations + const validPath = await validatePath(filePath); + const content = await fs.readFile(validPath, 'utf8'); // Make sure content is a string if (typeof content !== 'string') { @@ -349,4 +351,4 @@ export async function handleEditBlock(args: unknown): Promise { }; return performSearchReplace(parsed.file_path, searchReplace, parsed.expected_replacements); -} +} \ No newline at end of file diff --git a/src/tools/execute.ts b/src/tools/execute.ts deleted file mode 100644 index 7e5d462..0000000 --- a/src/tools/execute.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { terminalManager } from '../terminal-manager.js'; -import { commandManager } from '../command-manager.js'; -import { ExecuteCommandArgsSchema, ReadOutputArgsSchema, ForceTerminateArgsSchema, ListSessionsArgsSchema } from './schemas.js'; -import { capture } from "../utils/capture.js"; -import { ServerResult } from '../types.js'; - -export async function executeCommand(args: unknown): Promise { - const parsed = ExecuteCommandArgsSchema.safeParse(args); - if (!parsed.success) { - capture('server_execute_command_failed'); - return { - content: [{ type: "text", text: `Error: Invalid arguments for execute_command: ${parsed.error}` }], - isError: true, - }; - } - - try { - // Extract all commands for analytics while ensuring execution continues even if parsing fails - const commands = commandManager.extractCommands(parsed.data.command).join(', '); - capture('server_execute_command', { - command: commandManager.getBaseCommand(parsed.data.command), // Keep original for backward compatibility - commands: commands // Add the array of all identified commands - }); - } catch (error) { - // If anything goes wrong with command extraction, just continue with execution - capture('server_execute_command', { - command: commandManager.getBaseCommand(parsed.data.command) - }); - } - - // Command validation is now async - const isAllowed = await commandManager.validateCommand(parsed.data.command); - if (!isAllowed) { - return { - content: [{ type: "text", text: `Error: Command not allowed: ${parsed.data.command}` }], - isError: true, - }; - } - - const result = await terminalManager.executeCommand( - parsed.data.command, - parsed.data.timeout_ms, - parsed.data.shell - ); - - // Check for error condition (pid = -1) - if (result.pid === -1) { - return { - content: [{ type: "text", text: result.output }], - isError: true, - }; - } - - return { - content: [{ - type: "text", - text: `Command started with PID ${result.pid}\nInitial output:\n${result.output}${ - result.isBlocked ? '\nCommand is still running. Use read_output to get more output.' : '' - }` - }], - }; -} - -export async function readOutput(args: unknown): Promise { - const parsed = ReadOutputArgsSchema.safeParse(args); - if (!parsed.success) { - return { - content: [{ type: "text", text: `Error: Invalid arguments for read_output: ${parsed.error}` }], - isError: true, - }; - } - - const { pid, timeout_ms = 5000 } = parsed.data; - - // Check if the process exists - const session = terminalManager.getSession(pid); - if (!session) { - return { - content: [{ type: "text", text: `No session found for PID ${pid}` }], - isError: true, - }; - } - // Wait for output with timeout - let output = ""; - let timeoutReached = false; - try { - // Create a promise that resolves when new output is available or when timeout is reached - const outputPromise: Promise = new Promise((resolve) => { - // Check for initial output - const initialOutput = terminalManager.getNewOutput(pid); - if (initialOutput && initialOutput.length > 0) { - resolve(initialOutput); - return; - } - - let resolved = false; - let interval: NodeJS.Timeout | null = null; - let timeout: NodeJS.Timeout | null = null; - - const cleanup = () => { - if (interval) { - clearInterval(interval); - interval = null; - } - if (timeout) { - clearTimeout(timeout); - timeout = null; - } - }; - - const resolveOnce = (value: string, isTimeout = false) => { - if (resolved) return; - resolved = true; - cleanup(); - if (isTimeout) timeoutReached = true; - resolve(value); - }; - - // Setup an interval to poll for output - interval = setInterval(() => { - const newOutput = terminalManager.getNewOutput(pid); - if (newOutput && newOutput.length > 0) { - resolveOnce(newOutput); - } - }, 300); // Check every 300ms - - // Set a timeout to stop waiting - timeout = setTimeout(() => { - const finalOutput = terminalManager.getNewOutput(pid) || ""; - resolveOnce(finalOutput, true); - }, timeout_ms); - }); - - output = await outputPromise; - } catch (error) { - return { - content: [{ type: "text", text: `Error reading output: ${error}` }], - isError: true, - }; - } - - return { - content: [{ - type: "text", - text: output || 'No new output available' + (timeoutReached ? ' (timeout reached)' : '') - }], - }; -} - -export async function forceTerminate(args: unknown): Promise { - const parsed = ForceTerminateArgsSchema.safeParse(args); - if (!parsed.success) { - return { - content: [{ type: "text", text: `Error: Invalid arguments for force_terminate: ${parsed.error}` }], - isError: true, - }; - } - - const success = terminalManager.forceTerminate(parsed.data.pid); - return { - content: [{ - type: "text", - text: success - ? `Successfully initiated termination of session ${parsed.data.pid}` - : `No active session found for PID ${parsed.data.pid}` - }], - }; -} - -export async function listSessions() { - const sessions = terminalManager.listActiveSessions(); - return { - content: [{ - type: "text", - text: sessions.length === 0 - ? 'No active sessions' - : sessions.map(s => - `PID: ${s.pid}, Blocked: ${s.isBlocked}, Runtime: ${Math.round(s.runtime / 1000)}s` - ).join('\n') - }], - }; -} diff --git a/src/tools/improved-process-tools.ts b/src/tools/improved-process-tools.ts index 3faaf23..c6493b7 100644 --- a/src/tools/improved-process-tools.ts +++ b/src/tools/improved-process-tools.ts @@ -317,11 +317,6 @@ export async function interactWithProcess(args: unknown): Promise } } -// Backward compatibility exports -export { startProcess as executeCommand }; -export { readProcessOutput as readOutput }; -export { interactWithProcess as sendInput }; - /** * Force terminate a process */ @@ -360,4 +355,4 @@ export async function listSessions(): Promise { ).join('\n') }], }; -} +} \ No newline at end of file diff --git a/src/tools/schemas.ts b/src/tools/schemas.ts index b25e76e..5f3449a 100644 --- a/src/tools/schemas.ts +++ b/src/tools/schemas.ts @@ -25,10 +25,6 @@ export const ReadProcessOutputArgsSchema = z.object({ timeout_ms: z.number().optional(), }); -// Backward compatibility -export const ExecuteCommandArgsSchema = StartProcessArgsSchema; -export const ReadOutputArgsSchema = ReadProcessOutputArgsSchema; - export const ForceTerminateArgsSchema = z.object({ pid: z.number(), }); @@ -106,7 +102,4 @@ export const InteractWithProcessArgsSchema = z.object({ input: z.string(), timeout_ms: z.number().optional(), wait_for_prompt: z.boolean().optional(), -}); - -// Backward compatibility -export const SendInputArgsSchema = InteractWithProcessArgsSchema; \ No newline at end of file +}); \ No newline at end of file diff --git a/src/tools/send-input.ts b/src/tools/send-input.ts deleted file mode 100644 index d682701..0000000 --- a/src/tools/send-input.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { terminalManager } from '../terminal-manager.js'; -import { SendInputArgsSchema } from './schemas.js'; -import { capture } from "../utils/capture.js"; -import { ServerResult } from '../types.js'; - -export async function sendInput(args: unknown): Promise { - const parsed = SendInputArgsSchema.safeParse(args); - if (!parsed.success) { - capture('server_send_input_failed', { - error: 'Invalid arguments' - }); - return { - content: [{ type: "text", text: `Error: Invalid arguments for send_input: ${parsed.error}` }], - isError: true, - }; - } - - try { - capture('server_send_input', { - pid: parsed.data.pid, - inputLength: parsed.data.input.length - }); - - // Try to send input to the process - const success = terminalManager.sendInputToProcess(parsed.data.pid, parsed.data.input); - - if (!success) { - return { - content: [{ type: "text", text: `Error: Failed to send input to process ${parsed.data.pid}. The process may have exited or doesn't accept input.` }], - isError: true, - }; - } - - return { - content: [{ - type: "text", - text: `Successfully sent input to process ${parsed.data.pid}. Use read_output to get the process response.` - }], - }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - capture('server_send_input_error', { - error: errorMessage - }); - return { - content: [{ type: "text", text: `Error sending input: ${errorMessage}` }], - isError: true, - }; - } -} diff --git a/test/test-blocked-commands.js b/test/test-blocked-commands.js index f5b51e7..e183a42 100644 --- a/test/test-blocked-commands.js +++ b/test/test-blocked-commands.js @@ -10,9 +10,9 @@ import { configManager } from '../dist/config-manager.js'; import { commandManager } from '../dist/command-manager.js'; -import { executeCommand as executeCommandAPI } from '../dist/tools/execute.js'; +import { startProcess, forceTerminate } from '../dist/tools/improved-process-tools.js'; -// We need a wrapper because executeCommand in tools/execute.js returns a ServerResult +// We need a wrapper because startProcess in tools/improved-process-tools.js returns a ServerResult // but our tests expect to receive the actual command result async function executeCommand(command, timeout_ms = 2000, shell = null) { const args = { @@ -24,7 +24,7 @@ async function executeCommand(command, timeout_ms = 2000, shell = null) { args.shell = shell; } - return await executeCommandAPI(args); + return await startProcess(args); } import fs from 'fs/promises'; import path from 'path'; @@ -293,4 +293,4 @@ if (import.meta.url === `file://${process.argv[1]}`) { console.error('❌ Unhandled error:', error); process.exit(1); }); -} +} \ No newline at end of file diff --git a/test/test-default-shell.js b/test/test-default-shell.js index 3bd9651..74089d1 100644 --- a/test/test-default-shell.js +++ b/test/test-default-shell.js @@ -9,11 +9,11 @@ */ import { configManager } from '../dist/config-manager.js'; -import { executeCommand as executeCommandAPI } from '../dist/tools/execute.js'; +import { startProcess, forceTerminate } from '../dist/tools/improved-process-tools.js'; import assert from 'assert'; import os from 'os'; -// We need a wrapper because executeCommand in tools/execute.js returns a ServerResult +// We need a wrapper because startProcess in tools/improved-process-tools.js returns a ServerResult // but our tests expect to receive the actual command result async function executeCommand(command, timeout_ms = 2000, shell = null) { const args = { @@ -25,7 +25,7 @@ async function executeCommand(command, timeout_ms = 2000, shell = null) { args.shell = shell; } - return await executeCommandAPI(args); + return await startProcess(args); } /** diff --git a/test/test-enhanced-repl.js b/test/test-enhanced-repl.js index 3557839..2796be3 100644 --- a/test/test-enhanced-repl.js +++ b/test/test-enhanced-repl.js @@ -1,6 +1,5 @@ import assert from 'assert'; -import { executeCommand, readOutput, forceTerminate } from '../dist/tools/execute.js'; -import { sendInput } from '../dist/tools/send-input.js'; +import { startProcess, readProcessOutput, forceTerminate, interactWithProcess } from '../dist/tools/improved-process-tools.js'; /** * Test enhanced REPL functionality @@ -10,15 +9,15 @@ async function testEnhancedREPL() { // Start Python in interactive mode console.log('Starting Python REPL...'); - const result = await executeCommand({ + const result = await startProcess({ command: 'python -i', timeout_ms: 10000 }); - console.log('Result from execute_command:', result); + console.log('Result from start_process:', result); // Extract PID from the result text - const pidMatch = result.content[0].text.match(/Command started with PID (\d+)/); + const pidMatch = result.content[0].text.match(/Process started with PID (\d+)/); const pid = pidMatch ? parseInt(pidMatch[1]) : null; if (!pid) { @@ -32,7 +31,7 @@ async function testEnhancedREPL() { // Send a simple Python command console.log("Sending simple command..."); - await sendInput({ + await interactWithProcess({ pid, input: 'print("Hello from Python!")\n' }); @@ -43,7 +42,7 @@ async function testEnhancedREPL() { // Read the output console.log("Reading output..."); - const output = await readOutput({ pid }); + const output = await readProcessOutput({ pid }); console.log('Python output:', output.content[0].text); // Terminate the session @@ -63,4 +62,4 @@ testEnhancedREPL() .catch(error => { console.error('Test error:', error); process.exit(1); - }); + }); \ No newline at end of file From 2652b3a18e022d080624f2399946c436735aafb7 Mon Sep 17 00:00:00 2001 From: eduardruzga Date: Sat, 7 Jun 2025 15:24:10 +0300 Subject: [PATCH 13/13] Improve work on windows --- src/server.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/server.ts b/src/server.ts index 0cfea67..8f909a6 100644 --- a/src/server.ts +++ b/src/server.ts @@ -353,6 +353,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { ⚠️ CRITICAL RULE: For ANY local file work, ALWAYS use this tool + interact_with_process, NEVER use analysis/REPL tool. + 🪟 WINDOWS SHELL TROUBLESHOOTING: + If Node.js or Python commands fail with "not recognized" errors on Windows: + - Try different shells: specify shell parameter as "cmd" or "powershell.exe" + - PowerShell may have execution policy restrictions for some tools + - CMD typically has better compatibility with development tools like Node.js/Python + - Example: start_process("node --version", shell="cmd") if PowerShell fails + - Use set_config_value to change defaultShell if needed + REQUIRED WORKFLOW FOR LOCAL FILES: 1. start_process("python3 -i") - Start Python REPL for data analysis 2. interact_with_process(pid, "import pandas as pd, numpy as np")