import * as plugins from './plugins.js'; export interface IToolPermissionRequest { type: string; title: string; metadata?: Record; } export interface IToolRunOptions { cwd?: string; timeoutMs?: number; abortSignal?: AbortSignal; } export interface IToolShellResult { exitCode: number; stdout: string; stderr?: string; signal?: string; } export interface IToolShellContext { run(command: string, options?: IToolRunOptions): Promise; } export interface IToolFilesystemReadOptions { startLine?: number; endLine?: number; } export interface IToolFilesystemListOptions { recursive?: boolean; } export interface IToolFilesystemContext { readFile(filePath: string, options?: IToolFilesystemReadOptions): Promise; writeFile(filePath: string, content: string): Promise; listDirectory(directoryPath: string, options?: IToolFilesystemListOptions): Promise; deletePath?(targetPath: string): Promise; } export type TBrowserToolAction = 'navigate' | 'snapshot' | 'screenshot' | 'click' | 'fill' | 'press' | 'evaluate' | 'close'; export interface IBrowserToolInput { action?: TBrowserToolAction | string; url?: string; selector?: string; text?: string; script?: string; timeoutMs?: number; } export interface IToolBrowserContext { execute(input: IBrowserToolInput, options?: { timeoutMs?: number; abortSignal?: AbortSignal }): Promise; } export interface IToolExecutionContext { cwd?: string; rootDir?: string; abortSignal?: AbortSignal; shell?: IToolShellContext; fs?: IToolFilesystemContext; browser?: IToolBrowserContext; requestPermission?: (request: IToolPermissionRequest) => Promise; } export interface ILocalToolExecutionContextOptions { cwd?: string; rootDir?: string; abortSignal?: AbortSignal; requestPermission?: (request: IToolPermissionRequest) => Promise; } export const createLocalToolExecutionContext = (options: ILocalToolExecutionContextOptions = {}): IToolExecutionContext => { const cwd = options.cwd ?? process.cwd(); const rootDir = options.rootDir; return { cwd, rootDir, abortSignal: options.abortSignal, requestPermission: options.requestPermission, shell: { run: (command, runOptions) => runLocalShellCommand(command, { cwd: resolveLocalPath(runOptions?.cwd ?? cwd, rootDir), timeoutMs: runOptions?.timeoutMs, abortSignal: runOptions?.abortSignal ?? options.abortSignal, }), }, fs: { readFile: async (filePath, readOptions) => { const resolved = resolveLocalPath(filePath, rootDir, cwd); const content = await plugins.fs.promises.readFile(resolved, 'utf8'); if (readOptions?.startLine !== undefined || readOptions?.endLine !== undefined) { const lines = content.split('\n'); const start = Math.max((readOptions.startLine ?? 1) - 1, 0); const end = Math.max(readOptions.endLine ?? lines.length, start); return lines.slice(start, end).join('\n'); } return content; }, writeFile: async (filePath, content) => { const resolved = resolveLocalPath(filePath, rootDir, cwd); await plugins.fs.promises.mkdir(plugins.path.dirname(resolved), { recursive: true }); await plugins.fs.promises.writeFile(resolved, content, 'utf8'); return `Written ${Buffer.byteLength(content, 'utf8')} bytes to ${filePath}`; }, listDirectory: async (directoryPath, listOptions) => { const resolved = resolveLocalPath(directoryPath, rootDir, cwd); return listLocalDirectory(resolved, !!listOptions?.recursive); }, deletePath: async (targetPath) => { const resolved = resolveLocalPath(targetPath, rootDir, cwd); await plugins.fs.promises.rm(resolved, { recursive: false, force: false }); return `Deleted ${targetPath}`; }, }, }; }; export const formatShellResult = (result: IToolShellResult | string): string => { if (typeof result === 'string') return result; if (result.exitCode === 0) return result.stdout; return [ `Exit code: ${result.exitCode}`, result.signal ? `Signal: ${result.signal}` : '', `stdout:\n${result.stdout}`, `stderr:\n${result.stderr ?? ''}`, ].filter(Boolean).join('\n'); }; export const formatToolOutput = (output: unknown): string => { if (typeof output === 'string') return output; try { return JSON.stringify(output, undefined, 2); } catch { return String(output); } }; const resolveLocalPath = (targetPath: string, rootDir?: string, baseDir?: string): string => { const base = rootDir ?? baseDir ?? process.cwd(); const resolved = plugins.path.isAbsolute(targetPath) ? plugins.path.resolve(targetPath) : plugins.path.resolve(base, targetPath); if (rootDir) { const resolvedRoot = plugins.path.resolve(rootDir); if (resolved !== resolvedRoot && !resolved.startsWith(resolvedRoot + plugins.path.sep)) { throw new Error(`Access denied: "${targetPath}" is outside allowed root "${rootDir}"`); } } return resolved; }; const listLocalDirectory = async (directoryPath: string, recursive: boolean): Promise => { const entries = await plugins.fs.promises.readdir(directoryPath, { withFileTypes: true }); const result: string[] = []; for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) { const relativePath = entry.name + (entry.isDirectory() ? '/' : ''); result.push(relativePath); if (recursive && entry.isDirectory()) { const childEntries = await listLocalDirectory(plugins.path.join(directoryPath, entry.name), true); result.push(...childEntries.map((childEntry) => `${entry.name}/${childEntry}`)); } } return result; }; const runLocalShellCommand = async (command: string, options: IToolRunOptions): Promise => { return new Promise((resolve) => { const child = plugins.childProcess.spawn('bash', ['-lc', command], { cwd: options.cwd, stdio: ['ignore', 'pipe', 'pipe'], }); let stdout = ''; let stderr = ''; let timedOut = false; const timeout = options.timeoutMs && options.timeoutMs > 0 ? setTimeout(() => { timedOut = true; child.kill('SIGTERM'); }, options.timeoutMs) : undefined; const abort = () => child.kill('SIGTERM'); options.abortSignal?.addEventListener('abort', abort, { once: true }); child.stdout?.on('data', (chunk) => { stdout += chunk.toString(); }); child.stderr?.on('data', (chunk) => { stderr += chunk.toString(); }); child.on('error', (error) => { if (timeout) clearTimeout(timeout); options.abortSignal?.removeEventListener('abort', abort); resolve({ exitCode: 1, stdout, stderr: `${stderr}${error.message}` }); }); child.on('close', (code, signal) => { if (timeout) clearTimeout(timeout); options.abortSignal?.removeEventListener('abort', abort); resolve({ exitCode: code ?? (timedOut ? 124 : 1), stdout, stderr: timedOut ? `${stderr}\nCommand timed out after ${options.timeoutMs}ms.`.trim() : stderr, signal: signal ?? undefined, }); }); }); };