/** * Shell Command Tool * Executes shell commands with proper error handling and output formatting */ const { exec, spawn } = require('child_process'); const { promisify } = require('util'); const { BaseTool, ToolResult } = require('./tool-base.cjs'); const execAsync = promisify(exec); class ShellTool extends BaseTool { constructor(config = {}) { super({ name: 'shell', description: 'Execute shell commands in the terminal', parameters: [ { name: 'command', type: 'string', required: true, description: 'The shell command to execute' }, { name: 'cwd', type: 'string', required: false, description: 'Working directory for command execution' }, { name: 'timeout', type: 'number', required: false, description: 'Execution timeout in milliseconds (default: 30000)' }, { name: 'env', type: 'object', required: false, description: 'Environment variables for the command' } ], ...config }); this.defaultTimeout = config.defaultTimeout || 30000; this.maxOutputSize = config.maxOutputSize || 100000; // 100KB } /** * Execute a shell command */ async execute(params) { const { command, cwd, timeout = this.defaultTimeout, env } = params; try { // Security check for dangerous commands const securityCheck = this.checkSecurity(command); if (!securityCheck.safe) { throw new Error(`Security warning: ${securityCheck.reason}`); } const options = { timeout, cwd: cwd || process.cwd(), env: { ...process.env, ...env }, maxBuffer: 10 * 1024 * 1024 // 10MB }; // Execute command const { stdout, stderr } = await execAsync(command, options); // Format output const output = this.formatOutput(stdout, stderr); return ToolResult.success( { stdout, stderr, exitCode: 0 }, output, { command, cwd: options.cwd } ); } catch (error) { // Handle execution errors const output = this.formatErrorOutput(error); return ToolResult.failure( error, output, { command, exitCode: error.code || 1 } ); } } /** * Basic security check for commands */ checkSecurity(command) { const dangerousPatterns = [ 'rm -rf /', 'rm -rf /*', 'mkfs', 'format', '> /dev/sd', 'dd if=', ':(){:|:&};:', // Fork bomb 'chmod 000 /', 'chown -R' ]; const lowerCommand = command.toLowerCase(); for (const pattern of dangerousPatterns) { if (lowerCommand.includes(pattern)) { return { safe: false, reason: `Command contains dangerous pattern: ${pattern}` }; } } return { safe: true }; } /** * Format command output for display */ formatOutput(stdout, stderr) { let output = ''; if (stdout && stdout.trim()) { output += stdout; } if (stderr && stderr.trim()) { if (output) output += '\n'; output += `[stderr]: ${stderr}`; } // Truncate if too large if (output.length > this.maxOutputSize) { output = output.substring(0, this.maxOutputSize); output += `\n... [Output truncated, exceeded ${this.maxOutputSize} bytes]`; } return output || '[No output]'; } /** * Format error output */ formatErrorOutput(error) { let output = ''; if (error.killed) { output = `Command timed out after ${error.timeout}ms`; } else if (error.code) { output = `Command failed with exit code ${error.code}`; } else { output = `Command failed: ${error.message}`; } if (error.stderr) { output += `\n${error.stderr}`; } if (error.stdout) { output += `\n${error.stdout}`; } return output; } } /** * Streaming Shell Tool * For long-running commands with real-time output */ class StreamingShellTool extends BaseTool { constructor(config = {}) { super({ name: 'shell_stream', description: 'Execute long-running shell commands with streaming output', parameters: [ { name: 'command', type: 'string', required: true, description: 'The shell command to execute' }, { name: 'cwd', type: 'string', required: false, description: 'Working directory for command execution' }, { name: 'onData', type: 'function', required: false, description: 'Callback for streaming data chunks' } ], ...config }); } async execute(params) { return new Promise((resolve, reject) => { const { command, cwd, onData } = params; const output = { stdout: '', stderr: '' }; const options = { cwd: cwd || process.cwd(), shell: true }; const proc = spawn(command, options); proc.stdout.on('data', (data) => { const text = data.toString(); output.stdout += text; if (onData) onData({ type: 'stdout', data: text }); }); proc.stderr.on('data', (data) => { const text = data.toString(); output.stderr += text; if (onData) onData({ type: 'stderr', data: text }); }); proc.on('close', (code) => { if (code === 0) { resolve( ToolResult.success( output, output.stdout || '[Process completed successfully]', { exitCode: code } ) ); } else { resolve( ToolResult.failure( new Error(`Process exited with code ${code}`), output.stderr || output.stdout, { exitCode: code, ...output } ) ); } }); proc.on('error', (error) => { resolve( ToolResult.failure( error, `Failed to spawn process: ${error.message}`, { error: error.message } ) ); }); }); } } module.exports = { ShellTool, StreamingShellTool };