63 lines
2.0 KiB
TypeScript
63 lines
2.0 KiB
TypeScript
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<ToolResult> => {
|
|
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 });
|
|
}
|
|
}
|
|
});
|
|
},
|
|
};
|