From 63172d789d1702836ed191fe0a54c06641e6d7b3 Mon Sep 17 00:00:00 2001 From: ShiroKSH Date: Fri, 10 Jul 2026 00:35:35 +0300 Subject: [PATCH 1/2] fix: harden process tools and validation --- FAQ.md | 6 +-- README.md | 14 +++--- install-docker.ps1 | 26 +++++------ package-lock.json | 4 +- scripts/validate-tools-sync.js | 6 ++- src/handlers/terminal-handlers.ts | 6 +-- src/server.ts | 4 +- src/terminal-manager.ts | 23 ++++++++-- src/tools/config.ts | 1 + src/tools/edit.ts | 9 +++- src/tools/filesystem.ts | 3 +- src/tools/improved-process-tools.ts | 4 +- src/tools/process.ts | 68 +++++++++++++++++++++++------ src/tools/schemas.ts | 12 ++--- src/utils/usageTracker.ts | 2 +- test/exit-on-failure.js | 13 ++++++ test/test-allowed-directories.js | 6 +-- test/test-blocked-commands.js | 8 ++-- test/test-default-shell.js | 8 ++-- test/test-directory-creation.js | 6 +-- test/test-edit-block-occurrences.js | 6 +-- test/test-enhanced-repl.js | 9 ++-- test/test-home-directory.js | 8 ++-- test/test-repl-interaction.js | 6 +-- test/test-schema-regressions.js | 31 +++++++++++++ test/test.js | 6 +-- 26 files changed, 198 insertions(+), 97 deletions(-) create mode 100644 test/exit-on-failure.js create mode 100644 test/test-schema-regressions.js diff --git a/FAQ.md b/FAQ.md index c4ead1b1..d334ee83 100644 --- a/FAQ.md +++ b/FAQ.md @@ -281,9 +281,9 @@ This makes it particularly useful for onboarding to new projects or reviewing un Claude Desktop Commander has a sophisticated system for managing commands and operations that may take a while to complete: -1. The `execute_command` function returns after a timeout with initial output +1. The `start_process` function returns after a timeout with initial output 2. The command continues running in the background -3. You can use `read_output` with the PID to get new output as it becomes available +3. You can use `read_process_output` with the PID to get new output as it becomes available 4. You can use `force_terminate` to stop the command if needed For search operations: @@ -584,4 +584,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. \ No newline at end of file +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. diff --git a/README.md b/README.md index d0fab06e..b7901209 100644 --- a/README.md +++ b/README.md @@ -890,8 +890,8 @@ By default, Desktop Commander shows helpful onboarding prompts to new users (tho # Disable onboarding for this session node dist/index.js --no-onboarding -# Or if using npm scripts -npm run start:no-onboarding +# Or when running the built server directly +node dist/index.js --no-onboarding # For npx installations, modify your claude_desktop_config.json: { @@ -922,20 +922,20 @@ You can specify which shell to use for command execution: ```javascript // Using default shell (bash or system default) -execute_command({ "command": "echo $SHELL" }) +start_process({ "command": "echo $SHELL" }) // Using zsh specifically -execute_command({ "command": "echo $SHELL", "shell": "/bin/zsh" }) +start_process({ "command": "echo $SHELL", "shell": "/bin/zsh" }) // Using bash specifically -execute_command({ "command": "echo $SHELL", "shell": "/bin/bash" }) +start_process({ "command": "echo $SHELL", "shell": "/bin/bash" }) ``` This allows you to use shell-specific features or maintain consistent environments across commands. -1. `execute_command` returns after timeout with initial output +1. `start_process` returns after timeout with initial output 2. Command continues in background -3. Use `read_output` with PID to get new output +3. Use `read_process_output` with PID to get new output 4. Use `force_terminate` to stop if needed ## Debugging diff --git a/install-docker.ps1 b/install-docker.ps1 index f74e30f2..d2bf3cc9 100644 --- a/install-docker.ps1 +++ b/install-docker.ps1 @@ -731,7 +731,7 @@ function Show-Status { Write-Info "Run reset and reinstall to fix this" } else { Write-Error "No volumes found - please run full installation" - Write-Info "Run: .\install-docker-clean.ps1" + Write-Info "Run: .\install-docker.ps1" } }function Reset-Installation { Write-Header @@ -785,7 +785,7 @@ function Show-Status { } Write-Host "" Write-Info "To reinstall after reset:" - Write-Info "Run: .\install-docker-clean.ps1" + Write-Info "Run: .\install-docker.ps1" } else { Write-Info "Reset cancelled" } @@ -795,11 +795,11 @@ function Show-Help { Write-Host "Desktop Commander Docker Installation (Enhanced)" -ForegroundColor Blue Write-Host "" Write-Host "Usage:" - Write-Host " .\install-docker-clean.ps1 - Interactive installation with folder selection" - Write-Host " .\install-docker-clean.ps1 -Status - Check installation status" - Write-Host " .\install-docker-clean.ps1 -Reset - Reset all data" - Write-Host " .\install-docker-clean.ps1 -VerboseOutput - Show detailed output" - Write-Host " .\install-docker-clean.ps1 -Help - Show this help" + Write-Host " .\install-docker.ps1 - Interactive installation with folder selection" + Write-Host " .\install-docker.ps1 -Status - Check installation status" + Write-Host " .\install-docker.ps1 -Reset - Reset all data" + Write-Host " .\install-docker.ps1 -VerboseOutput - Show detailed output" + Write-Host " .\install-docker.ps1 -Help - Show this help" Write-Host "" Write-Host "Features:" Write-Host " - Interactive folder selection (like Mac version)" @@ -809,8 +809,8 @@ function Show-Help { Write-Host "" Write-Host "Troubleshooting:" Write-Host "If you broke the Docker container or need a fresh start:" - Write-Host " .\install-docker-clean.ps1 -Reset" - Write-Host " .\install-docker-clean.ps1" + Write-Host " .\install-docker.ps1 -Reset" + Write-Host " .\install-docker.ps1" Write-Host "" Write-Host "This will completely reset your persistent environment and reinstall everything fresh." } @@ -873,12 +873,12 @@ function Start-Installation { Write-Host "" Write-Info "To refresh/reset your persistent environment:" - Write-Info "- Run: .\install-docker-clean.ps1 -Reset" + Write-Info "- Run: .\install-docker.ps1 -Reset" Write-Info "- This removes all installed packages and resets everything" Write-Host "" Write-Info "If you broke the Docker container or need a fresh start:" - Write-Info "- Run: .\install-docker-clean.ps1 -Reset" - Write-Info "- Then: .\install-docker-clean.ps1" + Write-Info "- Run: .\install-docker.ps1 -Reset" + Write-Info "- Then: .\install-docker.ps1" Write-Info "- This will reset everything and reinstall from scratch" Write-Host "" Write-Info "Claude Desktop has been automatically restarted (if possible)" @@ -897,4 +897,4 @@ function Start-Installation { } # Run installation -Start-Installation \ No newline at end of file +Start-Installation diff --git a/package-lock.json b/package-lock.json index aaa597f1..dbff8066 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@wonderwhy-er/desktop-commander", - "version": "0.2.39", + "version": "0.2.44", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@wonderwhy-er/desktop-commander", - "version": "0.2.39", + "version": "0.2.44", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/scripts/validate-tools-sync.js b/scripts/validate-tools-sync.js index ee6da462..293e55f2 100644 --- a/scripts/validate-tools-sync.js +++ b/scripts/validate-tools-sync.js @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * Validates that the tools listed in mcpb-bundle/manifest.json match + * Validates that the tools listed in manifest.template.json match * the tools actually provided by the running MCP server * * This uses JSON-RPC to query the server directly, avoiding fragile regex parsing. @@ -26,7 +26,9 @@ const colors = { }; async function extractToolsFromManifest() { - const manifestPath = join(rootDir, 'mcpb-bundle', 'manifest.json'); + // The generated MCPB bundle is intentionally gitignored. Use the tracked + // template so this validation works in a fresh checkout after `npm run build`. + const manifestPath = join(rootDir, 'manifest.template.json'); const content = await readFile(manifestPath, 'utf-8'); const manifest = JSON.parse(content); diff --git a/src/handlers/terminal-handlers.ts b/src/handlers/terminal-handlers.ts index 1f6930fd..fee4e0ce 100644 --- a/src/handlers/terminal-handlers.ts +++ b/src/handlers/terminal-handlers.ts @@ -17,7 +17,7 @@ import { import { ServerResult } from '../types.js'; /** - * Handle start_process command (improved execute_command) + * Handle start_process command */ export async function handleStartProcess(args: unknown): Promise { const parsed = StartProcessArgsSchema.parse(args); @@ -25,7 +25,7 @@ export async function handleStartProcess(args: unknown): Promise { } /** - * Handle read_process_output command (improved read_output) + * Handle read_process_output command */ export async function handleReadProcessOutput(args: unknown): Promise { const parsed = ReadProcessOutputArgsSchema.parse(args); @@ -52,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 552c0f8c..434ba72f 100644 --- a/src/server.ts +++ b/src/server.ts @@ -356,7 +356,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { Read contents from files and URLs. Read PDF files and extract content as markdown and images. - Prefer this over 'execute_command' with cat/type for viewing files. + Prefer this over 'start_process' with cat/type for viewing files. Supports partial file reading with: - 'offset' (start line, default: 0) @@ -568,7 +568,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { description: ` Get a detailed listing of all files and directories in a specified path. - Use this instead of 'execute_command' with ls/dir commands. + Use this instead of 'start_process' with ls/dir commands. Results distinguish between files and directories with [FILE] and [DIR] prefixes. Supports recursive listing with the 'depth' parameter (default: 2): diff --git a/src/terminal-manager.ts b/src/terminal-manager.ts index 00d431a7..357bde36 100644 --- a/src/terminal-manager.ts +++ b/src/terminal-manager.ts @@ -254,6 +254,12 @@ export class TerminalManager { // Spawn the process with appropriate arguments const childProcess = spawn(spawnConfig.executable, spawnConfig.args, spawnOptions); + let spawnError: Error | undefined; + // `spawn` reports executable lookup failures asynchronously. Always attach + // a listener before checking pid so an invalid shell cannot crash the MCP server. + childProcess.on('error', (error) => { + spawnError = error; + }); let output = ''; // Ensure childProcess.pid is defined before proceeding @@ -261,7 +267,7 @@ export class TerminalManager { // Return a consistent error object instead of throwing return { pid: -1, // Use -1 to indicate an error state - output: 'Error: Failed to get process ID. The command could not be executed.', + output: `Error: ${spawnError?.message ?? 'Failed to get process ID. The command could not be executed.'}`, isBlocked: false }; } @@ -290,6 +296,7 @@ export class TerminalManager { return new Promise((resolve) => { let resolved = false; let periodicCheck: NodeJS.Timeout | null = null; + let timeoutHandle: NodeJS.Timeout | null = null; // Quick prompt patterns for immediate detection const quickPromptPatterns = />>>\s*$|>\s*$|\$\s*$|#\s*$/; @@ -298,6 +305,7 @@ export class TerminalManager { if (resolved) return; resolved = true; if (periodicCheck) clearInterval(periodicCheck); + if (timeoutHandle) clearTimeout(timeoutHandle); // Add timing info if requested if (collectTiming) { @@ -408,7 +416,7 @@ export class TerminalManager { }, 100); // Timeout fallback - setTimeout(() => { + timeoutHandle = setTimeout(() => { session.isBlocked = true; exitReason = 'timeout'; resolveOnce({ @@ -418,6 +426,15 @@ export class TerminalManager { }); }, timeoutMs); + childProcess.on('error', (error) => { + exitReason = 'process_exit'; + resolveOnce({ + pid: childProcess.pid ?? -1, + output: `Error: ${error.message}`, + isBlocked: false + }); + }); + childProcess.on('exit', (code: any) => { if (childProcess.pid) { // Store completed session before removing active session @@ -750,4 +767,4 @@ export class TerminalManager { } } -export const terminalManager = new TerminalManager(); \ No newline at end of file +export const terminalManager = new TerminalManager(); diff --git a/src/tools/config.ts b/src/tools/config.ts index 6d681da5..a42a853d 100644 --- a/src/tools/config.ts +++ b/src/tools/config.ts @@ -148,6 +148,7 @@ export async function getConfig() { type: "text", text: `Error getting configuration: ${error instanceof Error ? error.message : String(error)}\nUsing empty configuration.` }], + isError: true, }; } } diff --git a/src/tools/edit.ts b/src/tools/edit.ts index b988460d..1c318d87 100644 --- a/src/tools/edit.ts +++ b/src/tools/edit.ts @@ -409,8 +409,15 @@ export async function handleEditBlock(args: unknown): Promise { // Validate path and resolve handler once — used by both dispatch paths below let validatedPath: string; let handler: Awaited>; + let validatedOptions = parsed.options; try { validatedPath = await validatePath(parsed.file_path); + if (parsed.options?.outputPath) { + validatedOptions = { + ...parsed.options, + outputPath: await validatePath(parsed.options.outputPath), + }; + } const { getFileHandler } = await import('../utils/files/factory.js'); handler = await getFileHandler(validatedPath); } catch (error) { @@ -435,7 +442,7 @@ export async function handleEditBlock(args: unknown): Promise { if (hasEditRange) { try { // parsed.range is guaranteed non-empty string by hasRange check above - await handler.editRange!(validatedPath!, parsed.range!, content, parsed.options); + await handler.editRange!(validatedPath!, parsed.range!, content, validatedOptions); const resolvedRangePath = resolveAbsolutePath(parsed.file_path); return { content: [{ diff --git a/src/tools/filesystem.ts b/src/tools/filesystem.ts index 0476a7ca..61c83a04 100644 --- a/src/tools/filesystem.ts +++ b/src/tools/filesystem.ts @@ -132,7 +132,8 @@ async function getAllowedDirs(): Promise { // Normalize all paths consistently function normalizePath(p: string): string { - return path.normalize(expandHome(p)).toLowerCase(); + const normalized = path.normalize(expandHome(p)); + return process.platform === 'win32' ? normalized.toLowerCase() : normalized; } function expandHome(filepath: string): string { diff --git a/src/tools/improved-process-tools.ts b/src/tools/improved-process-tools.ts index 92bad49d..8b591c0b 100644 --- a/src/tools/improved-process-tools.ts +++ b/src/tools/improved-process-tools.ts @@ -92,7 +92,7 @@ async function executeNodeCode(code: string, timeout_ms: number = 30000): Promis } /** - * Start a new process (renamed from execute_command) + * Start a new process * Includes early detection of process waiting for input */ export async function startProcess(args: unknown): Promise { @@ -722,4 +722,4 @@ export async function listSessions(): Promise { : allSessions.join('\n') }], }; -} \ No newline at end of file +} diff --git a/src/tools/process.ts b/src/tools/process.ts index 676f66f9..0eaa95dd 100644 --- a/src/tools/process.ts +++ b/src/tools/process.ts @@ -6,22 +6,64 @@ import { KillProcessArgsSchema } from './schemas.js'; const execAsync = promisify(exec); +function parseCsvLine(line: string): string[] { + const fields: string[] = []; + let field = ''; + let quoted = false; + + for (let i = 0; i < line.length; i += 1) { + const char = line[i]; + if (char === '"') { + if (quoted && line[i + 1] === '"') { + field += '"'; + i += 1; + } else { + quoted = !quoted; + } + } else if (char === ',' && !quoted) { + fields.push(field); + field = ''; + } else { + field += char; + } + } + + fields.push(field); + return fields; +} + export async function listProcesses(): Promise { - const command = os.platform() === 'win32' ? 'tasklist' : 'ps aux'; + const isWindows = os.platform() === 'win32'; + const command = isWindows ? 'tasklist /FO CSV /NH' : 'ps aux'; try { const { stdout } = await execAsync(command); - const processes = stdout.split('\n') - .slice(1) - .filter(Boolean) - .map(line => { - const parts = line.split(/\s+/); - return { - pid: parseInt(parts[1]), - command: parts[parts.length - 1], - cpu: parts[2], - memory: parts[3], - } as ProcessInfo; - }); + const processes = isWindows + ? stdout.split(/\r?\n/) + .filter(Boolean) + .map(line => parseCsvLine(line)) + .map(parts => { + const pid = Number.parseInt(parts[1], 10); + if (!Number.isInteger(pid)) return null; + return { + pid, + command: parts[0], + cpu: 'N/A', + memory: parts[4] ?? 'N/A', + } as ProcessInfo; + }) + .filter((process): process is ProcessInfo => process !== null) + : stdout.split('\n') + .slice(1) + .filter(Boolean) + .map(line => { + const parts = line.split(/\s+/); + return { + pid: parseInt(parts[1], 10), + command: parts[parts.length - 1], + cpu: parts[2], + memory: parts[3], + } as ProcessInfo; + }); return { content: [{ diff --git a/src/tools/schemas.ts b/src/tools/schemas.ts index 6257f2af..cae906e9 100644 --- a/src/tools/schemas.ts +++ b/src/tools/schemas.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { DEFAULT_COMMAND_TIMEOUT } from "../config.js"; // Config tools schemas export const GetConfigArgsSchema = z.object({ @@ -26,7 +27,7 @@ export const ListProcessesArgsSchema = z.object({}); // Terminal tools schemas export const StartProcessArgsSchema = z.object({ command: z.string(), - timeout_ms: z.number(), + timeout_ms: z.number().optional().default(DEFAULT_COMMAND_TIMEOUT), shell: z.string().optional(), verbose_timing: z.boolean().optional(), // 'ui' marks widget-fired calls (e.g. open-in-folder/editor buttons); @@ -35,7 +36,7 @@ export const StartProcessArgsSchema = z.object({ }); export const ReadProcessOutputArgsSchema = z.object({ - pid: z.number(), + pid: z.number().int().positive(), timeout_ms: z.number().optional(), offset: z.number().optional(), // Line offset: 0=from last read, positive=absolute, negative=tail length: z.number().optional(), // Max lines to return (default from config.fileReadLineLimit) @@ -43,13 +44,13 @@ export const ReadProcessOutputArgsSchema = z.object({ }); export const ForceTerminateArgsSchema = z.object({ - pid: z.number(), + pid: z.number().int().positive(), }); export const ListSessionsArgsSchema = z.object({}); export const KillProcessArgsSchema = z.object({ - pid: z.number(), + pid: z.number().int().positive(), }); // Filesystem tools schemas @@ -57,7 +58,8 @@ export const ReadFileArgsSchema = z.object({ path: z.string(), isUrl: z.boolean().optional().default(false), offset: z.number().optional().default(0), - length: z.number().optional().default(1000), + // Leave this unset so the handler can apply config.fileReadLineLimit. + length: z.number().optional(), sheet: z.string().optional(), // String only for MCP client compatibility (Cursor doesn't support union types in JSON Schema) range: z.string().optional(), options: z.record(z.any()).optional(), diff --git a/src/utils/usageTracker.ts b/src/utils/usageTracker.ts index 42a275e5..14fdc4a3 100644 --- a/src/utils/usageTracker.ts +++ b/src/utils/usageTracker.ts @@ -47,7 +47,7 @@ const TURN_OFF_FEEDBACK_INSTRUCTION = "*This request disappears after you give f // Tool categories mapping const TOOL_CATEGORIES = { filesystem: ['read_file', 'read_multiple_files', 'write_file', 'create_directory', 'list_directory', 'move_file', 'get_file_info'], - terminal: ['execute_command', 'read_output', 'force_terminate', 'list_sessions'], + terminal: ['start_process', 'read_process_output', 'interact_with_process', 'force_terminate', 'list_sessions'], edit: ['edit_block'], search: ['start_search', 'get_more_search_results', 'stop_search', 'list_searches'], config: ['get_config', 'set_config_value'], diff --git a/test/exit-on-failure.js b/test/exit-on-failure.js new file mode 100644 index 00000000..1e9d691d --- /dev/null +++ b/test/exit-on-failure.js @@ -0,0 +1,13 @@ +export function runTest(testFn) { + Promise.resolve() + .then(testFn) + .then((success) => { + if (success === false) { + process.exitCode = 1; + } + }) + .catch((error) => { + console.error('❌ Unhandled error:', error); + process.exitCode = 1; + }); +} diff --git a/test/test-allowed-directories.js b/test/test-allowed-directories.js index d1fe95c8..8d308ced 100644 --- a/test/test-allowed-directories.js +++ b/test/test-allowed-directories.js @@ -404,8 +404,6 @@ export default async function runTests() { // If this file is run directly (not imported), execute the test if (import.meta.url === `file://${process.argv[1]}`) { - runTests().catch(error => { - console.error('❌ Unhandled error:', error); - process.exit(1); - }); + runTest(runTests); } +import { runTest } from './exit-on-failure.js'; diff --git a/test/test-blocked-commands.js b/test/test-blocked-commands.js index e183a424..c3c2fb33 100644 --- a/test/test-blocked-commands.js +++ b/test/test-blocked-commands.js @@ -289,8 +289,6 @@ export default async function runTests() { // If this file is run directly (not imported), execute the test if (import.meta.url === `file://${process.argv[1]}`) { - runTests().catch(error => { - console.error('❌ Unhandled error:', error); - process.exit(1); - }); -} \ No newline at end of file + runTest(runTests); +} +import { runTest } from './exit-on-failure.js'; diff --git a/test/test-default-shell.js b/test/test-default-shell.js index 74089d16..05ef4821 100644 --- a/test/test-default-shell.js +++ b/test/test-default-shell.js @@ -412,8 +412,6 @@ export default async function runTests() { // If this file is run directly (not imported), execute the test if (import.meta.url === `file://${process.argv[1]}`) { - runTests().catch(error => { - console.error('❌ Unhandled error:', error); - process.exit(1); - }); -} \ No newline at end of file + runTest(runTests); +} +import { runTest } from './exit-on-failure.js'; diff --git a/test/test-directory-creation.js b/test/test-directory-creation.js index cff6e9bf..c064873f 100644 --- a/test/test-directory-creation.js +++ b/test/test-directory-creation.js @@ -124,8 +124,6 @@ export default async function runTests() { // If this file is run directly (not imported), execute the test if (import.meta.url === `file://${process.argv[1]}`) { - runTests().catch(error => { - console.error('❌ Unhandled error:', error); - process.exit(1); - }); + runTest(runTests); } +import { runTest } from './exit-on-failure.js'; diff --git a/test/test-edit-block-occurrences.js b/test/test-edit-block-occurrences.js index 9d903f36..bce57ffe 100644 --- a/test/test-edit-block-occurrences.js +++ b/test/test-edit-block-occurrences.js @@ -351,8 +351,6 @@ export default async function runTests() { // If this file is run directly (not imported), execute the test if (import.meta.url === `file://${process.argv[1]}`) { - runTests().catch(error => { - console.error('❌ Unhandled error:', error); - process.exit(1); - }); + runTest(runTests); } +import { runTest } from './exit-on-failure.js'; diff --git a/test/test-enhanced-repl.js b/test/test-enhanced-repl.js index 9867abf2..12a04656 100644 --- a/test/test-enhanced-repl.js +++ b/test/test-enhanced-repl.js @@ -7,14 +7,15 @@ import { startProcess, readProcessOutput, forceTerminate, interactWithProcess } * @returns {string} 'python3' or 'python' */ function getPythonCommand() { + const commandExists = process.platform === 'win32' ? 'where' : 'command -v'; try { // Prefer python3 if available - execSync('command -v python3', { stdio: 'ignore' }); + execSync(`${commandExists} python3`, { stdio: 'ignore' }); return 'python3'; } catch (e) { // Fallback to python try { - execSync('command -v python', { stdio: 'ignore' }); + execSync(`${commandExists} python`, { stdio: 'ignore' }); return 'python'; } catch (error) { throw new Error('Neither python3 nor python command is available in the PATH'); @@ -37,7 +38,7 @@ async function testEnhancedREPL() { const result = await startProcess({ command: `${pythonCommand} -i`, timeout_ms: 10000, - shell: '/bin/bash' + shell: process.platform === 'win32' ? process.env.ComSpec : '/bin/bash' }); console.log('Result from start_process:', result); @@ -144,4 +145,4 @@ testEnhancedREPL() .catch(error => { console.error('Test error:', error); process.exit(1); - }); \ No newline at end of file + }); diff --git a/test/test-home-directory.js b/test/test-home-directory.js index 29a38bb8..a1cc5073 100644 --- a/test/test-home-directory.js +++ b/test/test-home-directory.js @@ -286,8 +286,6 @@ export default async function runTests() { // If this file is run directly (not imported), execute the test if (import.meta.url === `file://${process.argv[1]}`) { - runTests().catch(error => { - console.error('❌ Unhandled error:', error); - process.exit(1); - }); -} \ No newline at end of file + runTest(runTests); +} +import { runTest } from './exit-on-failure.js'; diff --git a/test/test-repl-interaction.js b/test/test-repl-interaction.js index 4ae563e8..0eac2596 100644 --- a/test/test-repl-interaction.js +++ b/test/test-repl-interaction.js @@ -4,6 +4,7 @@ import { fileURLToPath } from 'url'; import fs from 'fs/promises'; import { configManager } from '../dist/config-manager.js'; import { terminalManager } from '../dist/terminal-manager.js'; +import { runTest } from './exit-on-failure.js'; // Get directory name const __filename = fileURLToPath(import.meta.url); @@ -241,8 +242,5 @@ export default async function runTests() { // 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); - }); + runTest(runTests); } diff --git a/test/test-schema-regressions.js b/test/test-schema-regressions.js new file mode 100644 index 00000000..7f478841 --- /dev/null +++ b/test/test-schema-regressions.js @@ -0,0 +1,31 @@ +import assert from 'node:assert/strict'; +import { + ForceTerminateArgsSchema, + KillProcessArgsSchema, + ReadFileArgsSchema, + ReadProcessOutputArgsSchema, + StartProcessArgsSchema, +} from '../dist/tools/schemas.js'; +import { listProcesses } from '../dist/tools/process.js'; + +const startProcess = StartProcessArgsSchema.parse({ command: 'echo ok' }); +assert.equal(startProcess.timeout_ms, 1000, 'start_process should use the runtime timeout default'); + +const readFile = ReadFileArgsSchema.parse({ path: 'example.txt' }); +assert.equal(readFile.length, undefined, 'read_file should let the handler apply its configured line limit'); + +for (const schema of [ReadProcessOutputArgsSchema, ForceTerminateArgsSchema, KillProcessArgsSchema]) { + for (const pid of [0, -1, 1.5]) { + assert.equal(schema.safeParse({ pid }).success, false, `PID ${pid} must be rejected`); + } +} + +if (process.platform === 'win32') { + const result = await listProcesses(); + assert.equal(result.isError, undefined); + const text = result.content[0]?.text ?? ''; + assert.doesNotMatch(text, /PID: NaN/); + assert.match(text, /PID: \d+/); +} + +console.log('Schema and process-list regression tests passed'); diff --git a/test/test.js b/test/test.js index 52a022b5..7b5e6544 100644 --- a/test/test.js +++ b/test/test.js @@ -4,6 +4,7 @@ import fs from 'fs/promises'; import { fileURLToPath } from 'url'; import { handleEditBlock } from '../dist/handlers/edit-search-handlers.js'; import { configManager } from '../dist/config-manager.js'; +import { runTest } from './exit-on-failure.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -91,8 +92,5 @@ export default async function runTests() { // If this file is run directly (not imported), execute the test if (import.meta.url === `file://${process.argv[1]}`) { -runTests().catch(error => { - console.error('❌ Unhandled error:', error); - process.exit(1); -}); +runTest(runTests); } From 0be4edf6006876a14b222e0d40a199b983d8745c Mon Sep 17 00:00:00 2001 From: ShiroKSH Date: Fri, 10 Jul 2026 00:55:48 +0300 Subject: [PATCH 2/2] fix: reject invalid unix process ids --- src/tools/process.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/tools/process.ts b/src/tools/process.ts index 0eaa95dd..6bceb8eb 100644 --- a/src/tools/process.ts +++ b/src/tools/process.ts @@ -57,13 +57,16 @@ export async function listProcesses(): Promise { .filter(Boolean) .map(line => { const parts = line.split(/\s+/); + const pid = Number.parseInt(parts[1], 10); + if (!Number.isInteger(pid) || pid <= 0) return null; return { - pid: parseInt(parts[1], 10), + pid, command: parts[parts.length - 1], cpu: parts[2], memory: parts[3], } as ProcessInfo; - }); + }) + .filter((process): process is ProcessInfo => process !== null); return { content: [{