diff --git a/FAQ.md b/FAQ.md index 69384ca0..898341d7 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: @@ -475,4 +500,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 ce0a8655..92976f7e 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) @@ -182,8 +186,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 | @@ -199,6 +204,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: @@ -615,4 +637,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 8e1619b2..98ea562b 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.

+
+ +
+ +
+
@@ -1990,6 +2009,7 @@

Use Cases

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

Resources

}); - + \ No newline at end of file diff --git a/package.json b/package.json index 47d9a6e9..658c48bf 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 51400d7a..1f6930fd 100644 --- a/src/handlers/terminal-handlers.ts +++ b/src/handlers/terminal-handlers.ts @@ -1,13 +1,15 @@ import { - executeCommand, - readOutput, + startProcess, + readProcessOutput, + interactWithProcess, forceTerminate, listSessions -} from '../tools/execute.js'; +} from '../tools/improved-process-tools.js'; import { - ExecuteCommandArgsSchema, - ReadOutputArgsSchema, + StartProcessArgsSchema, + ReadProcessOutputArgsSchema, + InteractWithProcessArgsSchema, ForceTerminateArgsSchema, ListSessionsArgsSchema } from '../tools/schemas.js'; @@ -15,19 +17,26 @@ import { 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); } /** @@ -43,4 +52,4 @@ export async function handleForceTerminate(args: unknown): Promise */ export async function handleListSessions(): Promise { return listSessions(); -} +} \ No newline at end of file diff --git a/src/server.ts b/src/server.ts index 4b1aba27..979914ea 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, @@ -78,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", @@ -333,27 +343,138 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { // Terminal tools { - name: "execute_command", + name: "start_process", + description: ` + 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. + + 🪟 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") + 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(StartProcessArgsSchema), + }, + { + name: "read_process_output", description: ` - Execute a terminal command with timeout. + Read output from a running process with intelligent completion detection. - Command will continue running in background if it doesn't complete within timeout. + Automatically detects when process is ready for more input instead of timing out. - NOTE: For file operations, prefer specialized tools like read_file, search_code, - list_directory instead of cat, grep, or ls commands. + 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: + - 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. + + DETECTION STATES: + 🔄 Process waiting for input (ready for interact_with_process) + ✅ Process finished execution + ⏱️ Timeout reached (may still be running) - ${PATH_GUIDANCE} ${CMD_PREFIX_DESCRIPTION}`, - inputSchema: zodToJsonSchema(ExecuteCommandArgsSchema), + inputSchema: zodToJsonSchema(ReadProcessOutputArgsSchema), }, { - name: "read_output", + name: "interact_with_process", description: ` - Read new output from a running terminal session. - Set timeout_ms for long running commands. + 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(ReadOutputArgsSchema), + inputSchema: zodToJsonSchema(InteractWithProcessArgsSchema), }, { name: "force_terminate", @@ -368,6 +489,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), }, @@ -391,6 +522,33 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { ${CMD_PREFIX_DESCRIPTION}`, inputSchema: zodToJsonSchema(KillProcessArgsSchema), }, + + // 🚨🚨🚨 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 + // + // 🟢 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) { @@ -437,11 +595,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest) } // Terminal tools - case "execute_command": - return await handlers.handleExecuteCommand(args); + case "start_process": + return await handlers.handleStartProcess(args); - case "read_output": - return await handlers.handleReadOutput(args); + case "read_process_output": + return await handlers.handleReadProcessOutput(args); + + case "interact_with_process": + return await handlers.handleInteractWithProcess(args); case "force_terminate": return await handlers.handleForceTerminate(args); @@ -456,6 +617,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 8de76126..29a615a1 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 = ''; diff --git a/src/tools/edit.ts b/src/tools/edit.ts index f1f9defe..aaad44fa 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,7 +120,8 @@ export async function performSearchReplace(filePath: string, block: SearchReplac } - // Read file as plain string without status messages + // Read file directly to preserve line endings - critical for edit operations + const validPath = await validatePath(filePath); const content = await readFileInternal(filePath, 0, Number.MAX_SAFE_INTEGER); // Make sure content is a 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 7e5d4620..00000000 --- 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/filesystem.ts b/src/tools/filesystem.ts index 9cf50de9..fa38bc29 100644 --- a/src/tools/filesystem.ts +++ b/src/tools/filesystem.ts @@ -263,11 +263,11 @@ async function readFileWithSmartPositioning(filePath: string, offset: number, le 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); @@ -276,14 +276,14 @@ async function readFileWithSmartPositioning(filePath: string, offset: number, le 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 @@ -304,37 +304,37 @@ async function readLastNLinesReverse(filePath: string, n: number, mimeType: stri 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 + 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(); @@ -349,19 +349,19 @@ async function readFromEndWithReadline(filePath: string, requestedLines: number, 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) { @@ -372,8 +372,8 @@ async function readFromEndWithReadline(filePath: string, requestedLines: number, } else { result = buffer.slice(0, totalLines); } - - const content = includeStatusMessage + + const content = includeStatusMessage ? `[Reading last ${result.length} lines]\n\n${result.join('\n')}` : result.join('\n'); return { content, mimeType, isImage: false }; @@ -387,10 +387,10 @@ async function readFromStartWithReadline(filePath: string, offset: number, lengt 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); @@ -398,11 +398,11 @@ async function readFromStartWithReadline(filePath: string, offset: number, lengt if (result.length >= length) break; // Early exit optimization lineNumber++; } - + rl.close(); - + if (includeStatusMessage) { - const statusMessage = offset === 0 + 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')}`; @@ -422,51 +422,51 @@ async function readFromEstimatedPosition(filePath: string, offset: number, lengt 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 { @@ -474,10 +474,10 @@ async function readFromEstimatedPosition(filePath: string, offset: number, lengt } lineCount++; } - + rl2.close(); - - const content = includeStatusMessage + + 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 }; @@ -605,36 +605,36 @@ export async function readFileInternal(filePath: string, offset: number = 0, len } 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'); } - + // IMPORTANT: For internal operations (especially edit operations), we must - // preserve exact file content including original line endings. + // preserve exact file content including original line endings. // We cannot use readline-based reading as it strips line endings. - + // Read entire file content preserving line endings const content = await fs.readFile(validPath, 'utf8'); - + // If we need to apply offset/length, do it while preserving line endings if (offset === 0 && length >= Number.MAX_SAFE_INTEGER) { // Most common case for edit operations: read entire file return content; } - + // Handle offset/length by splitting on line boundaries while preserving line endings const lines = splitLinesPreservingEndings(content); - + // Apply offset and length const selectedLines = lines.slice(offset, offset + length); - + // Join back together (this preserves the original line endings) return selectedLines.join(''); } @@ -646,14 +646,14 @@ export async function readFileInternal(filePath: string, offset: number = 0, len */ function splitLinesPreservingEndings(content: string): string[] { if (!content) return ['']; - + const lines: string[] = []; let currentLine = ''; - + for (let i = 0; i < content.length; i++) { const char = content[i]; currentLine += char; - + // Check for line ending patterns if (char === '\n') { // LF or end of CRLF @@ -671,12 +671,12 @@ function splitLinesPreservingEndings(content: string): string[] { currentLine = ''; } } - + // Handle any remaining content (file not ending with line ending) if (currentLine) { lines.push(currentLine); } - + return lines; } diff --git a/src/tools/improved-process-tools.ts b/src/tools/improved-process-tools.ts new file mode 100644 index 00000000..c6493b71 --- /dev/null +++ b/src/tools/improved-process-tools.ts @@ -0,0 +1,358 @@ +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, + }; + } +} + +/** + * 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') + }], + }; +} \ No newline at end of file diff --git a/src/tools/schemas.ts b/src/tools/schemas.ts index 1382f77e..b1c1624a 100644 --- a/src/tools/schemas.ts +++ b/src/tools/schemas.ts @@ -12,13 +12,13 @@ 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(), }); @@ -92,4 +92,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 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 diff --git a/src/utils/process-detection.ts b/src/utils/process-detection.ts new file mode 100644 index 00000000..a58b500e --- /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`; + } +} diff --git a/test/enhanced-repl-example.js b/test/enhanced-repl-example.js new file mode 100644 index 00000000..6b726d1e --- /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 00000000..8e9e34e5 --- /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 00000000..e166ffdf --- /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 00000000..0bf1f385 --- /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 00000000..f2ec8b9f --- /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-blocked-commands.js b/test/test-blocked-commands.js index f5b51e77..e183a424 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 3bd96510..74089d16 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 new file mode 100644 index 00000000..2796be34 --- /dev/null +++ b/test/test-enhanced-repl.js @@ -0,0 +1,65 @@ +import assert from 'assert'; +import { startProcess, readProcessOutput, forceTerminate, interactWithProcess } from '../dist/tools/improved-process-tools.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 startProcess({ + command: 'python -i', + timeout_ms: 10000 + }); + + console.log('Result from start_process:', result); + + // Extract PID from the result text + const pidMatch = result.content[0].text.match(/Process 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}`); + + // We'll stick to using the existing tools for now to test the basic functionality + + // Send a simple Python command + console.log("Sending simple command..."); + await interactWithProcess({ + pid, + 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 readProcessOutput({ pid }); + console.log('Python output:', output.content[0].text); + + // Terminate the session + console.log("Terminating 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); + }); \ No newline at end of file diff --git a/test/test-node-repl.js b/test/test-node-repl.js new file mode 100644 index 00000000..427a3b66 --- /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 00000000..4ae563e8 --- /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 00000000..9dfdfc8e --- /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); + }); +}