Skip to main content
Glama
portel-dev

NCP - Natural Context Provider

by portel-dev
cli-parser.ts10.5 kB
/** * CLI Help Parser * Parses CLI tool help output to extract operations and parameters */ import { exec } from 'child_process'; import { promisify } from 'util'; import { logger } from '../utils/logger.js'; const execAsync = promisify(exec); export interface CLIOperation { name: string; description: string; keywords: string[]; // For search/discovery commandTemplate?: string; // Template for AI to use examples?: string[]; } export interface CLIToolInfo { baseCommand: string; version?: string; description?: string; operations: CLIOperation[]; } export class CLIParser { /** * Parse CLI tool and extract operations */ async parseCliTool( baseCommand: string, helpFlag: string = '--help' ): Promise<CLIToolInfo> { try { // Try to get help output const helpOutput = await this.getHelpOutput(baseCommand, helpFlag); // Detect tool type and use appropriate parser if (baseCommand === 'ffmpeg' || helpOutput.includes('FFmpeg version')) { return await this.parseFFmpeg(baseCommand, helpOutput); } else { return await this.parseGeneric(baseCommand, helpOutput); } } catch (error: any) { logger.error(`Failed to parse CLI tool ${baseCommand}:`, error); throw new Error(`Failed to parse ${baseCommand}: ${error.message}`); } } /** * Get help output from CLI tool */ private async getHelpOutput( baseCommand: string, helpFlag: string ): Promise<string> { try { // Try primary help flag const { stdout, stderr } = await execAsync(`${baseCommand} ${helpFlag} 2>&1`, { timeout: 10000, maxBuffer: 1024 * 1024 * 10 // 10MB buffer for large help outputs }); return stdout || stderr; } catch (error: any) { // Some tools output help to stderr or return non-zero exit if (error.stdout || error.stderr) { return error.stdout || error.stderr; } throw error; } } /** * Parse ffmpeg - use knowledge-based approach for common operations */ private async parseFFmpeg(baseCommand: string, helpOutput: string): Promise<CLIToolInfo> { // Extract version const versionMatch = helpOutput.match(/FFmpeg version ([^\s]+)/); const version = versionMatch ? versionMatch[1] : undefined; // Define common ffmpeg operations that users would search for const operations: CLIOperation[] = [ { name: 'convert', description: 'Convert video/audio to different format', keywords: ['convert', 'transcode', 'format', 'video', 'audio', 'encode', 'mp4', 'webm', 'avi', 'mkv'], commandTemplate: 'ffmpeg -i {input} {output}', examples: [ 'ffmpeg -i input.mp4 output.webm', 'ffmpeg -i video.avi -c:v libx264 output.mp4' ] }, { name: 'extract_audio', description: 'Extract audio track from video', keywords: ['extract', 'audio', 'sound', 'track', 'mp3', 'aac', 'wav'], commandTemplate: 'ffmpeg -i {input} -vn -acodec {codec} {output}', examples: [ 'ffmpeg -i video.mp4 -vn -acodec mp3 audio.mp3', 'ffmpeg -i movie.mkv -vn -acodec copy audio.aac' ] }, { name: 'compress', description: 'Compress/reduce video file size', keywords: ['compress', 'reduce', 'size', 'smaller', 'quality', 'crf', 'bitrate'], commandTemplate: 'ffmpeg -i {input} -crf {quality} {output}', examples: [ 'ffmpeg -i input.mp4 -crf 28 compressed.mp4', 'ffmpeg -i large.avi -c:v libx264 -crf 23 smaller.mp4' ] }, { name: 'resize', description: 'Resize/scale video dimensions', keywords: ['resize', 'scale', 'dimensions', 'width', 'height', 'resolution', '1080p', '720p'], commandTemplate: 'ffmpeg -i {input} -vf scale={width}:{height} {output}', examples: [ 'ffmpeg -i input.mp4 -vf scale=1280:720 output.mp4', 'ffmpeg -i video.avi -vf scale=1920:-1 hd.mp4' ] }, { name: 'cut', description: 'Cut/trim video segment', keywords: ['cut', 'trim', 'segment', 'clip', 'extract', 'portion', 'time', 'duration'], commandTemplate: 'ffmpeg -i {input} -ss {start} -t {duration} {output}', examples: [ 'ffmpeg -i video.mp4 -ss 00:00:10 -t 00:00:30 clip.mp4', 'ffmpeg -i movie.mkv -ss 5 -to 60 segment.mp4' ] }, { name: 'merge', description: 'Concatenate/merge multiple videos', keywords: ['merge', 'concatenate', 'join', 'combine', 'multiple', 'concat'], commandTemplate: 'ffmpeg -f concat -safe 0 -i {filelist} -c copy {output}', examples: [ 'ffmpeg -f concat -safe 0 -i files.txt -c copy merged.mp4' ] }, { name: 'extract_frames', description: 'Extract frames/images from video', keywords: ['extract', 'frames', 'images', 'screenshots', 'thumbnail', 'png', 'jpg'], commandTemplate: 'ffmpeg -i {input} -vf fps={fps} {output_pattern}', examples: [ 'ffmpeg -i video.mp4 -vf fps=1 frame_%04d.png', 'ffmpeg -i movie.mkv -ss 00:00:10 -vframes 1 thumbnail.jpg' ] }, { name: 'add_audio', description: 'Add audio track to video', keywords: ['add', 'audio', 'track', 'overlay', 'music', 'sound', 'background'], commandTemplate: 'ffmpeg -i {video} -i {audio} -c:v copy -c:a aac {output}', examples: [ 'ffmpeg -i video.mp4 -i audio.mp3 -c:v copy -c:a aac output.mp4' ] }, { name: 'create_gif', description: 'Convert video to animated GIF', keywords: ['gif', 'animated', 'animation', 'convert', 'loop'], commandTemplate: 'ffmpeg -i {input} -vf "fps={fps},scale={width}:-1:flags=lanczos" {output}', examples: [ 'ffmpeg -i video.mp4 -vf "fps=10,scale=320:-1:flags=lanczos" output.gif' ] }, { name: 'stream_copy', description: 'Copy streams without re-encoding (fast)', keywords: ['copy', 'fast', 'remux', 'container', 'no encode', 'quick'], commandTemplate: 'ffmpeg -i {input} -c copy {output}', examples: [ 'ffmpeg -i input.mkv -c copy output.mp4' ] }, { name: 'get_info', description: 'Get video/audio file information', keywords: ['info', 'metadata', 'properties', 'details', 'duration', 'codec', 'bitrate'], commandTemplate: 'ffmpeg -i {input}', examples: [ 'ffmpeg -i video.mp4 2>&1 | grep "Duration"', 'ffprobe -v quiet -print_format json -show_format -show_streams {input}' ] }, { name: 'change_codec', description: 'Change video/audio codec', keywords: ['codec', 'encoder', 'h264', 'h265', 'vp9', 'av1', 'aac', 'opus'], commandTemplate: 'ffmpeg -i {input} -c:v {video_codec} -c:a {audio_codec} {output}', examples: [ 'ffmpeg -i input.mp4 -c:v libx265 -c:a aac output.mp4', 'ffmpeg -i video.avi -c:v libvpx-vp9 -c:a libopus output.webm' ] }, { name: 'adjust_volume', description: 'Adjust audio volume level', keywords: ['volume', 'audio', 'loud', 'quiet', 'amplify', 'gain', 'normalize'], commandTemplate: 'ffmpeg -i {input} -af "volume={level}" {output}', examples: [ 'ffmpeg -i input.mp4 -af "volume=2.0" louder.mp4', 'ffmpeg -i quiet.mp4 -af "volume=0.5" quieter.mp4' ] }, { name: 'rotate', description: 'Rotate video orientation', keywords: ['rotate', 'orientation', 'flip', 'transpose', '90', '180', '270'], commandTemplate: 'ffmpeg -i {input} -vf "transpose={direction}" {output}', examples: [ 'ffmpeg -i input.mp4 -vf "transpose=1" rotated_90.mp4', 'ffmpeg -i video.mp4 -vf "transpose=2" rotated_180.mp4' ] }, { name: 'add_subtitles', description: 'Add subtitles to video', keywords: ['subtitles', 'srt', 'captions', 'text', 'overlay', 'burn'], commandTemplate: 'ffmpeg -i {input} -vf "subtitles={subtitle_file}" {output}', examples: [ 'ffmpeg -i video.mp4 -vf "subtitles=subs.srt" output.mp4' ] }, { name: 'watermark', description: 'Add watermark/logo to video', keywords: ['watermark', 'logo', 'overlay', 'image', 'brand'], commandTemplate: 'ffmpeg -i {input} -i {watermark} -filter_complex "overlay={x}:{y}" {output}', examples: [ 'ffmpeg -i video.mp4 -i logo.png -filter_complex "overlay=10:10" watermarked.mp4' ] } ]; return { baseCommand, version, description: 'A complete, cross-platform solution to record, convert and stream audio and video', operations }; } /** * Parse generic CLI tool - basic extraction from help text */ private async parseGeneric(baseCommand: string, helpOutput: string): Promise<CLIToolInfo> { // For generic tools, create a single "run" operation // that allows AI to execute the tool with arbitrary parameters // Extract description from first few lines const lines = helpOutput.split('\n').filter(l => l.trim()); const description = lines.slice(0, 3).join(' ').trim().substring(0, 200); const operations: CLIOperation[] = [ { name: 'run', description: `Execute ${baseCommand} command`, keywords: [baseCommand, 'execute', 'run', 'command'], commandTemplate: `${baseCommand} {args}`, examples: [ `${baseCommand} --help` ] } ]; return { baseCommand, description, operations }; } /** * Check if CLI tool is available */ async isCliAvailable(command: string): Promise<boolean> { try { await execAsync(`which ${command} || where ${command}`); return true; } catch { return false; } } /** * Get CLI tool version if available */ async getVersion(command: string): Promise<string | undefined> { try { const { stdout } = await execAsync(`${command} --version 2>&1 || ${command} -v 2>&1`, { timeout: 5000 }); const firstLine = stdout.split('\n')[0]; return firstLine.trim(); } catch { return undefined; } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/portel-dev/ncp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server