import * as plugins from './plugins.js'; import * as interfaces from './smartagent.interfaces.js'; import { BaseToolWrapper } from './smartagent.tools.base.js'; /** * Shell tool for executing commands securely * Wraps @push.rocks/smartshell with execSpawn for safety (no shell injection) */ export class ShellTool extends BaseToolWrapper { public name = 'shell'; public description = 'Execute shell commands securely. Uses execSpawn (shell:false) to prevent command injection.'; public actions: interfaces.IToolAction[] = [ { name: 'execute', description: 'Execute a command with arguments (secure, no shell injection possible). Command and args are passed separately.', parameters: { type: 'object', properties: { command: { type: 'string', description: 'The command to execute (e.g., "ls", "cat", "grep", "node")', }, args: { type: 'array', items: { type: 'string' }, description: 'Array of arguments (each argument is properly escaped)', }, cwd: { type: 'string', description: 'Working directory for the command' }, timeout: { type: 'number', description: 'Timeout in milliseconds' }, env: { type: 'object', description: 'Additional environment variables (key-value pairs)', }, }, required: ['command'], }, }, { name: 'which', description: 'Check if a command exists and get its path', parameters: { type: 'object', properties: { command: { type: 'string', description: 'Command name to look up (e.g., "node", "git")' }, }, required: ['command'], }, }, ]; private smartshell!: plugins.smartshell.Smartshell; public async initialize(): Promise { this.smartshell = new plugins.smartshell.Smartshell({ executor: 'bash', }); this.isInitialized = true; } public async cleanup(): Promise { this.isInitialized = false; } public async execute( action: string, params: Record ): Promise { this.validateAction(action); this.ensureInitialized(); try { switch (action) { case 'execute': { const command = params.command as string; const args = (params.args as string[]) || []; // Build options const options: { timeout?: number; env?: NodeJS.ProcessEnv; cwd?: string; } = {}; if (params.timeout) { options.timeout = params.timeout as number; } if (params.env) { options.env = { ...process.env, ...(params.env as NodeJS.ProcessEnv), }; } // Use execSpawn for security - no shell injection possible const result = await this.smartshell.execSpawn(command, args, options); return { success: result.exitCode === 0, result: { command, args, exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr, signal: result.signal, }, }; } case 'which': { try { const commandPath = await plugins.smartshell.which(params.command as string); return { success: true, result: { command: params.command, path: commandPath, exists: true, }, }; } catch { return { success: true, result: { command: params.command, path: null, exists: false, }, }; } } default: return { success: false, error: `Unknown action: ${action}`, }; } } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), }; } } public getCallSummary(action: string, params: Record): string { switch (action) { case 'execute': { const command = params.command as string; const args = (params.args as string[]) || []; const fullCommand = [command, ...args].join(' '); let summary = `Execute: ${fullCommand}`; if (params.cwd) { summary += ` (in ${params.cwd})`; } if (params.timeout) { summary += ` [timeout: ${params.timeout}ms]`; } if (params.env && Object.keys(params.env as object).length > 0) { const envKeys = Object.keys(params.env as object).join(', '); summary += ` [env: ${envKeys}]`; } return summary; } case 'which': return `Check if command "${params.command}" exists and get its path`; default: return `Unknown action: ${action}`; } } }