import { execFileSync, spawn } from 'node:child_process'; type ToolResult = { content: Array<{ type: string; text: string }>; isError?: boolean; }; type OnUpdate = (partial: { content: Array<{ type: string; text: string }> }) => void; function update(onUpdate: OnUpdate | undefined, text: string): void { onUpdate?.({ content: [{ type: 'text', text }] }); } function ensureFfmpeg(): boolean { try { execFileSync('ffmpeg', ['-version'], { stdio: 'ignore' }); return true; } catch { try { execFileSync('sudo', ['apt-get', 'update'], { stdio: 'ignore', timeout: 60_000 }); execFileSync('sudo', ['apt-get', 'install', '-y', 'ffmpeg'], { stdio: 'ignore', timeout: 120_000 }); execFileSync('ffmpeg', ['-version'], { stdio: 'ignore' }); return true; } catch { return false; } } } function parseArgs(argsString: string): string[] { const args: string[] = []; let current = ''; let inSingle = false; let inDouble = false; for (let i = 0; i < argsString.length; i++) { const ch = argsString[i]!; if (ch === "'" && !inDouble) { inSingle = !inSingle; } else if (ch === '"' && !inSingle) { inDouble = !inDouble; } else if (ch === ' ' && !inSingle && !inDouble) { if (current.length > 0) { args.push(current); current = ''; } } else { current += ch; } } if (current.length > 0) args.push(current); return args; } export async function execute( _toolCallId: string, params: { command: 'ffmpeg' | 'ffprobe'; args: string }, _signal: AbortSignal | undefined, onUpdate?: OnUpdate, ): Promise { const { command, args: argsString } = params; if (!argsString || argsString.trim().length === 0) { return { content: [{ type: 'text', text: 'No arguments provided. See tool documentation for usage examples.' }], isError: true, }; } update(onUpdate, 'Checking ffmpeg installation...'); if (!ensureFfmpeg()) { return { content: [{ type: 'text', text: 'Failed to install ffmpeg. Try manually: sudo apt-get update && sudo apt-get install -y ffmpeg' }], isError: true, }; } const args = parseArgs(argsString); // For ffprobe, run synchronously and return output if (command === 'ffprobe') { update(onUpdate, `Running ffprobe...`); return new Promise((resolve) => { const proc = spawn('ffprobe', args, { stdio: ['ignore', 'pipe', 'pipe'] }); let stdout = ''; let stderr = ''; proc.stdout.on('data', (chunk: Buffer) => { stdout += chunk.toString(); }); proc.stderr.on('data', (chunk: Buffer) => { stderr += chunk.toString(); }); proc.on('close', (code) => { if (code !== 0) { resolve({ content: [{ type: 'text', text: `ffprobe failed (exit ${code}):\n${stderr.trim()}` }], isError: true, }); } else { // ffprobe prints info to stderr by default, stdout for -print_format const output = stdout.trim() || stderr.trim(); resolve({ content: [{ type: 'text', text: output || 'No output' }], }); } }); proc.on('error', (err) => { resolve({ content: [{ type: 'text', text: `ffprobe error: ${err.message}` }], isError: true, }); }); }); } // For ffmpeg, stream progress update(onUpdate, `Running ffmpeg...`); return new Promise((resolve) => { const proc = spawn('ffmpeg', args, { stdio: ['ignore', 'pipe', 'pipe'] }); let stdout = ''; let stderr = ''; let progressBuf = ''; proc.stdout.on('data', (chunk: Buffer) => { stdout += chunk.toString(); }); proc.stderr.on('data', (chunk: Buffer) => { const text = chunk.toString(); stderr += text; // Parse ffmpeg progress from stderr (time= field) progressBuf += text; const lines = progressBuf.split('\r'); progressBuf = lines.pop() ?? ''; for (const line of lines) { const timeMatch = line.match(/time=(\d{2}:\d{2}:\d{2}\.\d{2})/); const speedMatch = line.match(/speed=\s*([\d.]+x)/); if (timeMatch) { const progress = `Time: ${timeMatch[1]}${speedMatch ? ` | Speed: ${speedMatch[1]}` : ''}`; update(onUpdate, progress); } } }); proc.on('close', (code) => { if (code !== 0) { // Extract the last meaningful error line from stderr const errLines = stderr.trim().split('\n'); const lastLines = errLines.slice(-10).join('\n'); resolve({ content: [{ type: 'text', text: `ffmpeg failed (exit ${code}):\n${lastLines}` }], isError: true, }); } else { const output = stdout.trim(); resolve({ content: [{ type: 'text', text: output ? `Done.\n\n${output}` : 'Done.' }], }); } }); proc.on('error', (err) => { resolve({ content: [{ type: 'text', text: `ffmpeg error: ${err.message}` }], isError: true, }); }); }); }