UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio

1,249 lines 152 kB
import fs from "node:fs"; import path from "node:path"; import chalk from "chalk"; import ora from "ora"; import { ModelResolver } from "../../lib/models/modelResolver.js"; import { globalSession } from "../../lib/session/globalSessionState.js"; // Use TokenUsage from standard types - no local interface needed import { ContextFactory, } from "../../lib/types/index.js"; import { checkRedisAvailability } from "../../lib/utils/conversationMemory.js"; import { normalizeEvaluationData } from "../../lib/utils/evaluationUtils.js"; import { logger } from "../../lib/utils/logger.js"; import { createThinkingConfigFromRecord } from "../../lib/utils/thinkingConfig.js"; import { configManager } from "../commands/config.js"; import { MCPCommandFactory } from "../commands/mcp.js"; import { ModelsCommandFactory } from "../commands/models.js"; import { handleSetup } from "../commands/setup.js"; import { handleError } from "../errorHandler.js"; import { LoopSession } from "../loop/session.js"; import { initializeCliParser } from "../parser.js"; import { formatFileSize, saveAudioToFile } from "../utils/audioFileUtils.js"; import { resolveFilePaths } from "../utils/pathResolver.js"; import { animatedWrite } from "../utils/typewriter.js"; import { createStreamAbortHandler } from "../utils/abortHandler.js"; import { formatVideoFileSize, getVideoMetadataSummary, saveVideoToFile, } from "../utils/videoFileUtils.js"; import { OllamaCommandFactory } from "./ollamaCommandFactory.js"; import { SageMakerCommandFactory } from "./sagemakerCommandFactory.js"; /** * CLI Command Factory for generate commands */ export class CLICommandFactory { /** * Normalize loop session variables before merging them into provider options. * * The CLI loop schema models some fields (e.g. `stopSequences`, * `enabledToolNames`) as a single comma-separated string for ergonomic * input, but providers expect `string[]`. Without conversion, * `set stopSequences a,b` would be sent as one stop token "a,b" instead * of two ("a", "b"); `set enabledToolNames read,write` would be cast to * a `string[]` containing the single literal "read,write" and silently * filter out every tool. This helper splits and trims those fields so * the spread into `enhancedOptions` produces the correct shape across * generate / batch / stream paths. */ static normalizeLoopSessionVariables(vars) { const normalized = { ...vars }; if (typeof normalized.stopSequences === "string") { normalized.stopSequences = normalized.stopSequences .split(",") .map((s) => s.trim()) .filter(Boolean); } if (typeof normalized.enabledToolNames === "string") { normalized.enabledToolNames = normalized.enabledToolNames .split(",") .map((s) => s.trim()) .filter(Boolean); } return normalized; } // Common options available on all commands static commonOptions = { // Core generation options provider: { choices: [ "auto", "openai", "openai-compatible", "openrouter", "or", "bedrock", "vertex", "googleVertex", "anthropic", "anthropic-subscription", // Anthropic with subscription tier support "azure", "google-ai", "google-ai-studio", "huggingface", "ollama", "mistral", "litellm", "sagemaker", "deepseek", "ds", "nvidia-nim", "nim", "nvidia", "lm-studio", "lmstudio", "lms", "llamacpp", "llama.cpp", "xai", "grok", "groq", "cohere", "together-ai", "together", "fireworks", "perplexity", "pplx", "cloudflare", "workers-ai", "cf-ai", "replicate", "voyage", "voyage-ai", "jina", "jina-ai", "stability", "stability-ai", "sd", "ideogram", "recraft", ], default: "auto", description: "AI provider to use (auto-selects best available). Use 'anthropic-subscription' for Claude subscription plans.", alias: "p", }, // Anthropic subscription options authMethod: { type: "string", choices: ["api-key", "oauth"], default: "api-key", description: "Authentication method for Anthropic: 'api-key' (default) or 'oauth' (for subscription plans)", }, subscriptionTier: { type: "string", choices: ["free", "pro", "max", "max_5", "max_20", "api"], description: "Anthropic subscription tier: free (limited), pro ($20/mo), max (highest limits), max_5/max_20 (extended), api (pay-per-use)", }, enableBeta: { type: "boolean", default: false, description: "Enable Anthropic beta features (experimental capabilities, computer use, etc.)", alias: "beta", }, image: { type: "string", description: "Add image file for multimodal analysis (can be used multiple times)", alias: "i", }, csv: { type: "string", description: "Add CSV file for data analysis (can be used multiple times)", alias: "c", }, pdf: { type: "string", description: "Add PDF file for analysis (can be used multiple times)", }, video: { type: "string", description: "Add video file for analysis (can be used multiple times) (MP4, WebM, MOV, AVI, MKV)", }, "video-frames": { type: "number", default: 8, description: "Number of frames to extract (default: 8)", }, "video-quality": { type: "number", default: 85, description: "Frame quality 0-100 (default: 85)", }, "video-format": { type: "string", choices: ["jpeg", "png"], default: "jpeg", description: "Frame format (default: jpeg)", }, "transcribe-audio": { type: "boolean", default: false, description: "Extract and transcribe audio from video", }, file: { type: "string", description: "Add file with auto-detection (CSV, image, etc. - can be used multiple times)", }, csvMaxRows: { type: "number", default: 1000, description: "Maximum number of CSV rows to process", }, csvFormat: { type: "string", choices: ["raw", "markdown", "json"], default: "raw", description: "CSV output format:\n" + " • raw: Plain CSV text (fastest, minimal tokens, best for large files)\n" + " • markdown: Formatted table (readable, best for small files <100 rows)\n" + " • json: Structured JSON array (best for programmatic use, higher tokens)", }, model: { type: "string", description: "Specific model to use (e.g. gemini-2.5-pro, gemini-2.5-flash)", alias: "m", }, temperature: { type: "number", default: 0.7, description: "Creativity level (0.0 = focused, 1.0 = creative)", alias: "t", }, maxTokens: { type: "number", default: 1000, description: "Maximum tokens to generate", alias: "max", }, system: { type: "string", description: "System prompt to guide AI behavior", alias: "s", }, // Output control options format: { choices: ["text", "json", "table"], default: "text", alias: ["f", "output-format"], description: "Output format", }, output: { type: "string", description: "Save output to file", alias: "o", }, imageOutput: { type: "string", description: "Custom path for generated image (default: generated-images/image-<timestamp>.png)", alias: "image-output", }, // Behavior control options timeout: { type: "number", default: 120, description: "Maximum execution time in seconds", }, delay: { type: "number", description: "Delay between operations (ms)", }, // Tools & features options disableTools: { type: "boolean", default: false, description: "Disable MCP tool integration (tools enabled by default)", }, enableAnalytics: { type: "boolean", default: false, description: "Enable usage analytics collection", }, enableEvaluation: { type: "boolean", default: false, description: "Enable AI response quality evaluation", }, domain: { type: "string", choices: [ "healthcare", "finance", "analytics", "ecommerce", "education", "legal", "technology", "generic", "auto", ], description: "Domain type for specialized processing and optimization", alias: "d", }, evaluationDomain: { type: "string", description: "Domain expertise for evaluation (e.g., 'AI coding assistant', 'Customer service expert')", }, toolUsageContext: { type: "string", description: "Tool usage context for evaluation (e.g., 'Used sales-data MCP tools')", }, domainAware: { type: "boolean", default: false, description: "Use domain-aware evaluation", }, context: { type: "string", description: "JSON context object for custom data", }, // Debug & output options debug: { type: "boolean", alias: ["v", "verbose"], default: false, description: "Enable debug mode with verbose output", }, quiet: { type: "boolean", alias: "q", default: true, description: "Suppress non-essential output", }, noColor: { type: "boolean", default: false, description: "Disable colored output (useful for CI/scripts)", }, configFile: { type: "string", description: "Path to custom configuration file", }, dryRun: { type: "boolean", default: false, description: "Test command without making actual API calls (for testing)", }, // TTS (Text-to-Speech) options tts: { type: "boolean", default: false, description: "Enable text-to-speech output", }, ttsVoice: { type: "string", description: "TTS voice to use (e.g., 'en-US-Neural2-C')", }, ttsProvider: { type: "string", choices: ["google-ai", "vertex", "openai-tts", "elevenlabs", "azure-tts"], description: "TTS provider (overrides --provider for speech synthesis)", }, ttsFormat: { type: "string", choices: [ "mp3", "wav", "ogg", "opus", "m4a", "flac", "webm", "mp4", "mpeg", "mpga", ], default: "mp3", description: "Audio output format", }, ttsSpeed: { type: "number", default: 1.0, description: "Speaking rate (0.25-4.0, default: 1.0)", }, ttsQuality: { type: "string", choices: ["standard", "hd"], default: "standard", description: "Audio quality level", }, ttsOutput: { type: "string", description: "Save TTS audio to file (supports absolute and relative paths)", }, ttsPlay: { type: "boolean", default: false, description: "Auto-play generated audio", }, // STT (Speech-to-Text) options stt: { type: "boolean", default: false, description: "Enable speech-to-text transcription of input audio", }, sttProvider: { type: "string", choices: ["whisper", "deepgram", "google-stt", "azure-stt"], description: "STT provider to use", }, sttLanguage: { type: "string", description: "Audio language code for STT (e.g., en-US)", }, inputAudio: { type: "string", description: "Path to audio file for STT transcription", }, // Video Generation options (Veo 3.1, Kling, Runway, Replicate) outputMode: { type: "string", choices: ["text", "video", "ppt", "avatar", "music"], default: "text", description: "Output mode: 'text' (default), 'video' (Veo/Kling/Runway/Replicate), 'ppt' (presentation), 'avatar' (D-ID/HeyGen/MuseTalk talking-head), 'music' (Beatoven/ElevenLabs/Lyria/MusicGen)", }, videoProvider: { type: "string", description: "Video provider override (e.g., 'vertex' (default), 'kling', 'runway', 'replicate')", }, videoOutput: { type: "string", alias: "vo", description: "Path to save generated video file (e.g., ./output.mp4)", }, videoResolution: { type: "string", choices: ["720p", "1080p"], description: "Video output resolution (720p or 1080p; provider default applied if omitted)", }, videoLength: { type: "number", choices: [4, 6, 8], description: "Video duration in seconds (4, 6, or 8; provider default applied if omitted)", }, videoAspectRatio: { type: "string", choices: ["9:16", "16:9"], description: "Video aspect ratio (9:16 for portrait, 16:9 for landscape; provider default applied if omitted)", }, videoAudio: { type: "boolean", description: "Enable/disable audio generation in video (provider default applied if omitted)", }, // Avatar Generation options (D-ID, HeyGen, MuseTalk via Replicate) avatarProvider: { type: "string", description: "Avatar provider (e.g., 'd-id' (default), 'heygen', 'replicate', 'musetalk')", }, avatarImage: { type: "string", description: "Path to source portrait image (or HeyGen avatar id when --avatarProvider heygen)", }, avatarAudio: { type: "string", description: "Path to narration audio (alternative to --avatarText)", }, avatarText: { type: "string", description: "Text the avatar should speak (the provider runs TTS internally)", }, avatarVoice: { type: "string", description: "Voice id for TTS-driven avatars (provider-specific catalog id)", }, avatarQuality: { type: "string", choices: ["standard", "hd"], description: "Avatar output quality preset (provider default applied if omitted)", }, avatarFormat: { type: "string", choices: ["mp4", "webm", "mov"], description: "Avatar video output format (provider default applied if omitted)", }, avatarOutput: { type: "string", description: "Path to save generated avatar video (e.g., ./avatar.mp4)", }, // Music Generation options (Beatoven, ElevenLabs, Lyria, MusicGen via Replicate) musicProvider: { type: "string", description: "Music provider (e.g., 'beatoven' (default), 'elevenlabs-music', 'lyria', 'replicate', 'musicgen')", }, musicDuration: { type: "number", description: "Music duration in seconds (provider-clamped)", }, musicFormat: { type: "string", choices: ["mp3", "wav", "flac", "ogg"], description: "Music output format", }, musicGenre: { type: "string", description: "Music genre hint (e.g., 'ambient', 'cinematic', 'electronic')", }, musicMood: { type: "string", description: "Music mood hint (e.g., 'uplifting', 'tense', 'melancholic')", }, musicTempo: { type: "number", description: "Music tempo in BPM", }, musicOutput: { type: "string", description: "Path to save generated music (e.g., ./track.mp3)", }, // PPT Generation options pptPages: { type: "number", alias: "pages", description: "Number of slides to generate (5-50, default: 10 when PPT mode is enabled)", }, pptTheme: { type: "string", choices: ["modern", "corporate", "creative", "minimal", "dark"], description: "Presentation theme/style (default: AI selects based on topic)", }, pptAudience: { type: "string", choices: ["business", "students", "technical", "general"], description: "Target audience (default: AI selects based on topic)", }, pptTone: { type: "string", choices: ["professional", "casual", "educational", "persuasive"], description: "Presentation tone (default: AI selects based on topic)", }, pptOutput: { type: "string", alias: "po", description: "Path to save generated PPTX file (e.g., ./output.pptx)", }, pptAspectRatio: { type: "string", choices: ["16:9", "4:3"], description: "Slide aspect ratio (default: 16:9 when PPT mode is enabled)", }, pptNoImages: { type: "boolean", default: false, description: "Disable AI image generation for slides", }, thinking: { alias: "think", type: "boolean", description: "Enable extended thinking/reasoning capability", default: false, }, thinkingBudget: { type: "number", description: "Token budget for extended thinking - Anthropic Claude and Gemini 2.5+ models (5000-100000)", default: 10000, }, thinkingLevel: { type: "string", description: "Thinking level for extended reasoning (Anthropic Claude, Gemini 2.5+, Gemini 3): minimal, low, medium, high", choices: ["minimal", "low", "medium", "high"], }, region: { type: "string", description: "Vertex AI region (e.g., us-central1, europe-west1, asia-northeast1)", alias: "r", }, // RAG options ragFiles: { type: "array", description: "File paths to load for RAG (Retrieval-Augmented Generation). AI will search these documents to answer your question.", alias: "rag-files", string: true, }, ragStrategy: { type: "string", description: "Chunking strategy for RAG documents (auto-detected from file extension if not specified)", alias: "rag-strategy", choices: [ "character", "recursive", "sentence", "token", "markdown", "html", "json", "latex", "semantic", "semantic-markdown", ], }, ragChunkSize: { type: "number", description: "Maximum chunk size in characters for RAG documents", alias: "rag-chunk-size", default: 1000, }, ragChunkOverlap: { type: "number", description: "Overlap between adjacent chunks for RAG documents", alias: "rag-chunk-overlap", default: 200, }, ragTopK: { type: "number", description: "Number of top results to retrieve for RAG", alias: "rag-top-k", default: 5, }, }; // Helper method to build options for commands static buildOptions(yargs, additionalOptions = {}) { return (yargs .options({ ...CLICommandFactory.commonOptions, ...additionalOptions, }) // NEW9: implies relationships so users who pass --stt-provider or // --input-audio without --stt get an actionable error from yargs // instead of silently skipping STT. .implies("sttProvider", "stt") .implies("inputAudio", "stt")); } // Helper method to process CLI images with smart auto-detection static processCliImages(images) { if (!images) { return undefined; } const imagePaths = Array.isArray(images) ? images : [images]; // Resolve relative paths to absolute paths before returning // URLs are preserved as-is by resolveFilePaths // File paths will be converted to base64 by the message builder return resolveFilePaths(imagePaths); } // Helper method to process CLI CSV files static processCliCSVFiles(csvFiles) { if (!csvFiles) { return undefined; } const paths = Array.isArray(csvFiles) ? csvFiles : [csvFiles]; // Resolve relative paths to absolute paths before returning // URLs are preserved as-is by resolveFilePaths return resolveFilePaths(paths); } // Helper method to process CLI PDF files static processCliPDFFiles(pdfFiles) { if (!pdfFiles) { return undefined; } const paths = Array.isArray(pdfFiles) ? pdfFiles : [pdfFiles]; // Resolve relative paths to absolute paths before returning // URLs are preserved as-is by resolveFilePaths return resolveFilePaths(paths); } // Helper method to process CLI files with auto-detection static processCliFiles(files) { if (!files) { return undefined; } const paths = Array.isArray(files) ? files : [files]; // Resolve relative paths to absolute paths before returning // URLs are preserved as-is by resolveFilePaths return resolveFilePaths(paths); } // Helper method to process CLI video files static processCliVideoFiles(videoFiles) { if (!videoFiles) { return undefined; } const paths = Array.isArray(videoFiles) ? videoFiles : [videoFiles]; // Resolve relative paths to absolute paths before returning // URLs are preserved as-is by resolveFilePaths return resolveFilePaths(paths); } static isNonLocalFileReference(filePath) { const lower = filePath.toLowerCase(); return (lower.startsWith("http://") || lower.startsWith("https://") || lower.startsWith("file://") || lower.startsWith("data:")); } static validateCliInputFiles(argv) { const fileArgs = [ { option: "--image", value: argv.image }, { option: "--csv", value: argv.csv }, { option: "--pdf", value: argv.pdf }, { option: "--video", value: argv.video }, { option: "--file", value: argv.file }, ]; const missingPaths = []; for (const { option, value } of fileArgs) { if (!value) { continue; } const rawPaths = Array.isArray(value) ? value : [value]; const resolvedPaths = resolveFilePaths(rawPaths); for (let i = 0; i < resolvedPaths.length; i++) { const resolvedPath = resolvedPaths[i]; if (CLICommandFactory.isNonLocalFileReference(resolvedPath)) { continue; } if (!fs.existsSync(resolvedPath)) { missingPaths.push(`${option} path not found: ${rawPaths[i]} (resolved to ${resolvedPath})`); } } } if (missingPaths.length > 0) { throw new Error(`One or more input files do not exist:\n${missingPaths.join("\n")}`); } } // Helper method to process common options static processOptions(argv) { // Handle noColor option by disabling chalk if (argv.noColor) { process.env.FORCE_COLOR = "0"; } // Process context using ContextFactory for type-safe integration let processedContext; let contextConfig; if (argv.context) { let rawContext; if (typeof argv.context === "string") { try { rawContext = JSON.parse(argv.context); } catch (err) { const contextStr = argv.context; const truncatedJson = contextStr.length > 100 ? `${contextStr.slice(0, 100)}...` : contextStr; handleError(new Error(`Invalid JSON in --context parameter: ${err.message}. Received: ${truncatedJson}`), "Context parsing"); } } else { rawContext = argv.context; } const validatedContext = ContextFactory.validateContext(rawContext); if (validatedContext) { processedContext = validatedContext; // Configure context integration based on CLI usage contextConfig = { mode: "prompt_prefix", // Add context as prompt prefix for CLI usage includeInPrompt: true, includeInAnalytics: true, includeInEvaluation: true, maxLength: 500, // Reasonable limit for CLI context }; } else if (argv.debug) { logger.debug("Invalid context provided, skipping context integration"); } } return { provider: argv.provider === "auto" ? undefined : argv.provider, model: argv.model, temperature: argv.temperature, maxTokens: argv.maxTokens, // Sampling controls — surfaced here so all three command paths // (generate / stream / batch) get them consistently typed instead // of relying on an ad-hoc cast at each sdk call site. topP: argv.topP, topK: argv.topK, stopSequences: argv.stopSequences, enabledToolNames: argv.enabledToolNames, systemPrompt: argv.system, timeout: argv.timeout, disableTools: argv.disableTools, enableAnalytics: argv.enableAnalytics, enableEvaluation: argv.enableEvaluation, domain: argv.domain, evaluationDomain: argv.evaluationDomain, toolUsageContext: argv.toolUsageContext, domainAware: argv.domainAware, context: processedContext, contextConfig, debug: argv.debug, quiet: argv.quiet, format: argv.format, output: argv.output, imageOutput: argv.imageOutput, delay: argv.delay, noColor: argv.noColor, configFile: argv.configFile, dryRun: argv.dryRun, // TTS options tts: argv.tts, ttsVoice: argv.ttsVoice, ttsProvider: argv.ttsProvider, ttsFormat: argv.ttsFormat, ttsSpeed: argv.ttsSpeed, ttsQuality: argv.ttsQuality, ttsOutput: argv.ttsOutput, ttsPlay: argv.ttsPlay, // STT options stt: argv.stt, sttProvider: argv.sttProvider, sttLanguage: argv.sttLanguage, inputAudio: argv.inputAudio, // Video generation options (Veo 3.1) outputMode: argv.outputMode, videoProvider: argv.videoProvider, videoOutput: argv.videoOutput, videoResolution: argv.videoResolution, videoLength: argv.videoLength, videoAspectRatio: argv.videoAspectRatio, videoAudio: argv.videoAudio, // Avatar generation options avatarProvider: argv.avatarProvider, avatarImage: argv.avatarImage, avatarAudio: argv.avatarAudio, avatarText: argv.avatarText, avatarVoice: argv.avatarVoice, avatarQuality: argv.avatarQuality, avatarFormat: argv.avatarFormat, avatarOutput: argv.avatarOutput, // Music generation options musicProvider: argv.musicProvider, musicDuration: argv.musicDuration, musicFormat: argv.musicFormat, musicGenre: argv.musicGenre, musicMood: argv.musicMood, musicTempo: argv.musicTempo, musicOutput: argv.musicOutput, // PPT generation options pptPages: argv.pptPages, pptTheme: argv.pptTheme, pptAudience: argv.pptAudience, pptTone: argv.pptTone, pptOutput: argv.pptOutput, pptAspectRatio: argv.pptAspectRatio, pptNoImages: argv.pptNoImages, // Extended thinking options for Claude and Gemini models thinking: argv.thinking, thinkingBudget: argv.thinkingBudget, thinkingLevel: argv.thinkingLevel, // Region option for cloud providers (Vertex AI, Bedrock, etc.) region: argv.region, // Anthropic subscription options authMethod: argv.authMethod, subscriptionTier: argv.subscriptionTier, enableBeta: argv.enableBeta, }; } /** * Validate Anthropic subscription options * Ensures subscription tier is provided when using anthropic-subscription provider * or when oauth auth method is selected */ static validateAnthropicSubscriptionOptions(options) { const provider = options.provider; const authMethod = options.authMethod; let subscriptionTier = options.subscriptionTier; const enableBeta = options.enableBeta; // Check if using anthropic-subscription provider or oauth auth method const isSubscriptionMode = provider === "anthropic-subscription" || authMethod === "oauth"; if (isSubscriptionMode && !subscriptionTier) { logger.always(chalk.yellow("⚠️ Subscription tier not specified. Defaulting to 'api' tier.")); logger.always(chalk.gray(" Use --subscription-tier to specify: free, pro, max, or api")); options.subscriptionTier = "api"; subscriptionTier = "api"; } // Validate oauth is required for non-api subscription tiers if (subscriptionTier && ["free", "pro", "max"].includes(subscriptionTier) && authMethod !== "oauth") { logger.always(chalk.yellow(`⚠️ Subscription tier '${subscriptionTier}' typically uses OAuth authentication.`)); logger.always(chalk.gray(" Consider using --auth-method oauth for this tier.")); } // Map anthropic-subscription to anthropic provider with subscription options if (provider === "anthropic-subscription") { options.provider = "anthropic"; options.useSubscription = true; } // Warn about beta features when enabled if (enableBeta) { logger.always(chalk.cyan("🧪 Beta features enabled for Anthropic. Experimental capabilities may be unstable.")); } // Build Anthropic auth configuration for provider initialization if (provider === "anthropic" || provider === "anthropic-subscription") { const authConfig = { method: (authMethod === "oauth" ? "oauth" : "api_key"), subscriptionTier: subscriptionTier, }; options.anthropicAuthConfig = authConfig; options.enableBeta = enableBeta; } } // Helper method to handle output static handleOutput(result, options) { let output; if (options.format === "json") { output = JSON.stringify(result, null, 2); } else if (options.format === "table" && Array.isArray(result)) { logger.table(result); return; } else { if (typeof result === "string") { output = result; } else if (result && typeof result === "object" && "content" in result) { const generateResult = result; output = generateResult.content; // 🔧 Handle image generation output if (generateResult.imageOutput?.base64 && generateResult.imageOutput.base64.trim().length > 0) { try { // Use custom path or default let imagePath; if (options.imageOutput) { imagePath = path.resolve(options.imageOutput); // Create parent directory if needed (cross-platform) const dir = path.dirname(imagePath); if (dir && dir !== "." && !fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } } else { const imageDir = "generated-images"; const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); imagePath = path.join(imageDir, `image-${timestamp}.png`); // Create directory if it doesn't exist if (!fs.existsSync(imageDir)) { fs.mkdirSync(imageDir, { recursive: true }); } } // Save image to file const imageBuffer = Buffer.from(generateResult.imageOutput.base64, "base64"); fs.writeFileSync(imagePath, imageBuffer); // Store image path in result for JSON output generateResult.imageOutput.savedPath = imagePath; // Always print image save confirmation - this is essential output // (not suppressed by quiet flag since users need to know where the image was saved) logger.always(`\n📸 Generated image saved to: ${imagePath}`); logger.always(` Image size: ${(imageBuffer.length / 1024).toFixed(2)} KB`); } catch (error) { handleError(error, "Failed to save generated image"); } } // Add analytics display for text mode when enabled if (options.enableAnalytics && generateResult.analytics) { output += CLICommandFactory.formatAnalyticsForTextMode(generateResult); } } else if (result && typeof result === "object" && "text" in result) { output = result.text; } else { output = JSON.stringify(result); } } if (options.output) { fs.writeFileSync(options.output, output); if (!options.quiet) { logger.always(`Output saved to ${options.output}`); } } else { logger.always(output); } } /** * Helper method to handle TTS audio file output * Saves audio to file when --tts-output flag is provided */ static async handleTTSOutput(result, options) { // Check if --tts-output flag is provided const ttsOutputPath = options.ttsOutput; if (!ttsOutputPath) { return; } // Extract audio from result with proper type checking if (!result || typeof result !== "object") { return; } const generateResult = result; const audio = generateResult.audio; if (!audio) { if (!options.quiet) { logger.always(chalk.yellow("⚠️ No audio available in result. TTS may not be enabled for this request.")); } return; } try { // Save audio to file const saveResult = await saveAudioToFile(audio, ttsOutputPath); if (saveResult.success) { if (!options.quiet) { logger.always(chalk.green(`🔊 Audio saved to: ${saveResult.path} (${formatFileSize(saveResult.size)})`)); } } else { handleError(new Error(saveResult.error || "Failed to save audio file"), "TTS Output"); } } catch (error) { handleError(error, "TTS Output"); } } /** * Helper method to configure options for video generation mode * Auto-configures provider, model, and tools settings for video generation */ static configureVideoMode(enhancedOptions, argv, options) { const userEnabledTools = !argv.disableTools; // Tools are enabled by default enhancedOptions.disableTools = true; // Resolve video provider from explicit --videoProvider first, then top-level --provider, then default to vertex. if (!enhancedOptions.videoProvider) { enhancedOptions.videoProvider = enhancedOptions.provider ?? "vertex"; if (options.debug) { logger.debug(`Auto-setting video provider to '${enhancedOptions.videoProvider}' for video generation mode`); } } // Auto-set model to veo-3.1 if not explicitly specified if (!enhancedOptions.model) { // Resolve the alias to the full model ID for Vertex AI const modelAlias = "veo-3.1"; const resolvedModel = ModelResolver.resolveModel(modelAlias); const fullModelId = resolvedModel?.id || "veo-3.1-generate-001"; enhancedOptions.model = fullModelId; if (options.debug) { logger.debug(`Auto-setting model to '${fullModelId}' for video generation mode`); } } // Warn user if they explicitly enabled tools if (userEnabledTools && !options.quiet) { logger.always(chalk.yellow("⚠️ Note: MCP tools are not supported in video generation mode and have been disabled.")); } if (options.debug) { logger.debug("Video generation mode enabled (tools auto-disabled):", { provider: enhancedOptions.provider, model: enhancedOptions.model, resolution: enhancedOptions.videoResolution, length: enhancedOptions.videoLength, aspectRatio: enhancedOptions.videoAspectRatio, audio: enhancedOptions.videoAudio, outputPath: enhancedOptions.videoOutput, }); } } /** * Helper method to configure options for PPT generation mode * Auto-configures provider, model, and tools settings for presentation generation */ static configurePPTMode(enhancedOptions, argv, options) { const userEnabledTools = !argv.disableTools; // Tools are enabled by default enhancedOptions.disableTools = true; // Auto-set provider for PPT generation if not explicitly specified // PPT works best with Vertex or Google AI for content planning if (!enhancedOptions.provider) { enhancedOptions.provider = "vertex"; if (options.debug) { logger.debug("Auto-setting provider to 'vertex' for PPT generation mode"); } } // Auto-set model if not explicitly specified if (!enhancedOptions.model) { // Use gemini-2.5-flash for fast, high-quality content planning const modelAlias = "gemini-2.5-flash"; const resolvedModel = ModelResolver.resolveModel(modelAlias); const fullModelId = resolvedModel?.id || "gemini-2.5-flash-001"; enhancedOptions.model = fullModelId; if (options.debug) { logger.debug(`Auto-setting model to '${fullModelId}' for PPT generation mode`); } } // Warn user if they explicitly enabled tools if (userEnabledTools && !options.quiet) { logger.always(chalk.yellow("⚠️ Note: MCP tools are not supported in PPT generation mode and have been disabled.")); } if (options.debug) { logger.debug("PPT generation mode enabled (tools auto-disabled):", { provider: enhancedOptions.provider, model: enhancedOptions.model, pages: enhancedOptions.pptPages, theme: enhancedOptions.pptTheme, audience: enhancedOptions.pptAudience, tone: enhancedOptions.pptTone, aspectRatio: enhancedOptions.pptAspectRatio, noImages: enhancedOptions.pptNoImages, outputPath: enhancedOptions.pptOutput, }); } } /** * Helper method to handle video file output * Saves generated video to file when --videoOutput flag is provided */ static async handleVideoOutput(result, options) { // Check if --videoOutput flag is provided const videoOutputPath = options.videoOutput; if (!videoOutputPath) { return; } // Extract video from result with proper type checking if (!result || typeof result !== "object") { return; } const generateResult = result; const video = generateResult.video; if (!video) { if (!options.quiet) { logger.always(chalk.yellow("⚠️ No video available in result. Video generation may not be enabled or the request failed.")); } return; } try { // Save video to file const saveResult = await saveVideoToFile(video, videoOutputPath); if (saveResult.success) { const sizeInfo = formatVideoFileSize(saveResult.size); const metadataSummary = getVideoMetadataSummary(video); logger.always(chalk.green(`🎬 Video saved to: ${saveResult.path} (${sizeInfo})`)); if (!options.quiet && metadataSummary) { logger.always(chalk.gray(` ${metadataSummary}`)); } } else { handleError(new Error(saveResult.error || "Failed to save video file"), "Video Output"); } } catch (error) { handleError(error, "Video Output"); } } /** * Helper method to handle avatar video file output. * Saves the generated avatar buffer to --avatarOutput path when provided. */ static async handleAvatarOutput(result, options) { const avatarOutputPath = options.avatarOutput; if (!avatarOutputPath) { return; } if (!result || typeof result !== "object") { return; } const generateResult = result; const avatar = generateResult.avatar; if (!avatar) { if (!options.quiet) { logger.always(chalk.yellow("⚠️ No avatar video available in result. Avatar generation may not be enabled or the request failed.")); } return; } try { fs.writeFileSync(avatarOutputPath, avatar.buffer); if (!options.quiet) { const sizeStr = formatFileSize(avatar.size); logger.always(chalk.green(`👤 Avatar video saved to: ${avatarOutputPath} (${sizeStr})`)); } } catch (error) { handleError(error, "Avatar Output"); } } /** * Helper method to handle music audio file output. * Saves the generated music buffer to --musicOutput path when provided. */ static async handleMusicOutput(result, options) { const musicOutputPath = options.musicOutput; if (!musicOutputPath) { return; } if (!result || typeof result !== "object") { return; } const generateResult = result; const music = generateResult.music; if (!music) { if (!options.quiet) { logger.always(chalk.yellow("⚠️ No music available in result. Music generation may not be enabled or the request failed.")); } return; } try { fs.writeFileSync(musicOutputPath, music.buffer); if (!options.quiet) { const sizeStr = formatFileSize(music.size); logger.always(chalk.green(`🎵 Music saved to: ${musicOutputPath} (${sizeStr})`)); } } catch (error) { handleError(error, "Music Output"); } } /** * Helper method to handle PPT file output * Displays PPT generation result info */ static async handlePPTOutput(result, options) { // Extract PPT from result with proper type checking if (!result || typeof result !== "object") { return; } const generateResult = result; const ppt = generateResult.ppt; if (!ppt) { // PPT not in result - either not PPT mode or generation failed return; } try { if (options.quiet) { if (ppt.filePath) { logger.always(chalk.green(`📊 Presentation saved to: ${ppt.filePath}`)); } else { logger.always(chalk.green("📊 Presentation generated successfully.")); } if (ppt.totalSlides) { logger.always(chalk.white(`📄 Slides: ${ppt.totalSlides}`)); } return; } logger.always(chalk.green("\n📊 Presentation Generated Successfully!")); logger.always(chalk.gray("─".repeat(50))); if (ppt.filePath) { logger.always(chalk.white(` 📁 File: ${ppt.filePath}`)); } if (ppt.totalSlides) { logger.always(chalk.white(` 📄 Slides: ${ppt.totalSlides}`)); } if (ppt.format) { logger.always(chalk.white(` 📋 Format: ${ppt.format.toUpperCase()}`)); } logger.always(chalk.gray("─".repeat(50))); logger.always(chalk.cyan("💡 Tip: Open the file with PowerPoint or Google Slides to view.")); } catch (error) { handleError(error, "PPT Output"); } } // Helper method to validate token usage data with fallback handling static isValidTokenUsage(tokens) { if (!tokens || typeof tokens !== "object" || tokens === null) { return false; } const tokensObj = tokens; // Check primary format: analytics.tokens {input, output, total} if (typeof tokensObj.input === "number" && typeof tokensObj.output === "number" && typeof tokensObj.total === "number") { return true; } // Check fallback format: tokenUsage {inputTokens, outputTokens, totalTokens} if (typeof tokensObj.inputTokens === "number" && typeof tokensObj.outputTokens === "number" && typeof tokensObj.totalTokens === "number") { return true; } return false; } // Helper method to normalize token usage data to standard format static normalizeTokenUsage(tokens) { if (!CLICommandFactory.isValidTokenUsage(tokens)) { return null; } const tokensObj = tokens; // Primary format: analytics.tokens {input, output, total} if (typeof tokensObj.