@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
JavaScript
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.