import { execFile } from 'child_process'; import type { Tool, ToolResult, ToolExecutionContext } from '../types.js'; interface ShellExecArgs { command: string; cwd?: string; timeout?: number; } export const shellExecTool: Tool = { name: 'shell.exec', description: 'Execute a shell command and return stdout/stderr. Use for running build commands, git operations, system tasks, etc.', inputSchema: { type: 'object', properties: { command: { type: 'string', description: 'The shell command to execute' }, cwd: { type: 'string', description: 'Working directory (optional)' }, timeout: { type: 'number', description: 'Timeout in milliseconds (default 30000)' }, }, required: ['command'], }, execute: async (rawArgs: unknown, context?: ToolExecutionContext): Promise => { const args = rawArgs as ShellExecArgs; const timeout = args.timeout ?? 30_000; return new Promise((resolve) => { const child = execFile('bash', ['-c', args.command], { cwd: args.cwd, timeout, maxBuffer: 1024 * 1024, }, (error, stdout, stderr) => { if (context?.signal) { context.signal.removeEventListener('abort', onAbort); } if (error) { if (error.killed || error.signal === 'SIGTERM') { resolve({ success: false, output: stdout, error: `Command timed out after ${timeout}ms` }); return; } resolve({ success: false, output: stdout, error: stderr || error.message, }); return; } resolve({ success: true, output: stdout + (stderr ? `\nstderr: ${stderr}` : '') }); }); const onAbort = () => { child.kill('SIGTERM'); }; if (context?.signal) { if (context.signal.aborted) { onAbort(); } else { context.signal.addEventListener('abort', onAbort, { once: true }); } } }); }, };