UNPKG

@juspay/neurolink

Version:

The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002

4,658 lines • 213 kB
import fs from "node:fs";
import path from "node:path";
import chalk from "chalk";
import ora from "ora";
import { STEP_LIMITS } from "../../core/constants.js";
import { ModelResolver } from "../../models/modelResolver.js";
import { providerChoicesFor } from "../../factories/mediaHandlerCatalog.js";
import { globalSession } from "../../session/globalSessionState.js";
// Use TokenUsage from standard types - no local interface needed
import { ContextFactory, } from "../../types/index.js";
import { checkRedisAvailability } from "../../utils/conversationMemory.js";
import { normalizeEvaluationData } from "../../utils/evaluationUtils.js";
import { logger } from "../../utils/logger.js";
import { sniffImageMimeType } from "../../utils/imageDetection.js";
import { createThinkingConfigFromRecord } from "../../utils/thinkingConfig.js";
import { buildToolRoutingConfigFromCli } from "../utils/toolRoutingFlags.js";
import { buildClassifierRouterConfigFromCli } from "../utils/classifierRouterFlags.js";
import { buildSkillsConfigFromCli } from "../utils/skillsFlags.js";
import { ensureStdinRef } from "../utils/stdinRef.js";
import { SkillsManager } from "../../skills/skillsManager.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 { playAudio } from "../utils/audioPlayer.js";
import { resolveFilePaths } from "../utils/pathResolver.js";
import { validateCliInputFiles, validateCsvMaxRows, validatePromptsFilePath, } from "../utils/inputValidation.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";
import { AgentCommandFactory } from "../commands/agent.js";
import { PROVIDER_DESCRIPTORS } from "../../factories/providerDescriptors.js";
/**
 * Every provider name + alias, derived from PROVIDER_DESCRIPTORS, plus
 * "auto" and the CLI-only "anthropic-subscription" pseudo-provider (not a
 * real AIProviderName — special-cased at runtime to rewrite
 * options.provider to "anthropic").
 */
const DERIVED_PROVIDER_CHOICES = [
    "auto",
    ...PROVIDER_DESCRIPTORS.flatMap((d) => [d.name, ...d.aliases]),
    "anthropic-subscription",
];
/** Space-separated form for the bash-completion script, kept in sync with DERIVED_PROVIDER_CHOICES by construction. */
export const BASH_COMPLETION_PROVIDERS = DERIVED_PROVIDER_CHOICES.join(" ");
/** EX_IOERR (sysexits.h): the result could not be fully written to stdout. */
const STDOUT_DRAIN_FAILED_EXIT_CODE = 74;
const DEFAULT_STDOUT_DRAIN_TIMEOUT_MS = 30_000;
const STDOUT_DRAIN_WARNING = "neurolink: stdout drain timed out; output may be incomplete\n";
let stdoutDrainTimeoutMs;
const resolveStdoutDrainTimeoutMs = () => {
    if (stdoutDrainTimeoutMs === undefined) {
        const override = Number(process.env.NEUROLINK_STDOUT_DRAIN_TIMEOUT_MS);
        stdoutDrainTimeoutMs =
            Number.isFinite(override) && override > 0
                ? override
                : DEFAULT_STDOUT_DRAIN_TIMEOUT_MS;
    }
    return stdoutDrainTimeoutMs;
};
/**
 * 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: DERIVED_PROVIDER_CHOICES,
            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)",
        },
        "pdf-password": {
            type: "string",
            description: "Password for an encrypted PDF (used on the image-conversion path). " +
                "Visible in shell history/process listings — prefer the " +
                "NEUROLINK_PDF_PASSWORD env var instead.",
        },
        video: {
            type: "string",
            description: "Add video file for analysis (can be used multiple times) (MP4, WebM, MOV, AVI, MKV)",
        },
        // No yargs `default:` on these two. They used to declare 8 / 85 while the
        // processor actually picks a duration-based frame count (up to 100) and
        // encodes at quality 80 — harmless only because the values were never read
        // (#478). Now that they reach the encoder, a default here would silently
        // re-cap every existing CLI video at 8 frames. Unset means "let the
        // processor choose", which is what callers have always effectively had.
        "video-frames": {
            type: "number",
            description: "Number of frames to extract (default: chosen from video duration, max 100)",
        },
        "video-quality": {
            type: "number",
            description: "Frame quality 1-100 (default: 80)",
        },
        "video-format": {
            type: "string",
            choices: ["jpeg", "png"],
            default: "jpeg",
            description: "Frame format (default: jpeg)",
        },
        "transcribe-audio": {
            type: "boolean",
            default: false,
            description: "Transcribe the video's spoken audio (needs ffmpeg and OPENAI_API_KEY). " +
                "Unnecessary for Gemini, which hears the clip directly",
        },
        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 (positive integer, range 1-100000, default 1000)",
        },
        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)",
        },
        "csv-encoding": {
            type: "string",
            description: "Character encoding for CSV files (e.g. utf-8, utf-16le, windows-1252). Auto-detected when omitted (#362).",
        },
        "csv-sanitize-names": {
            type: "boolean",
            default: false,
            description: "Rewrite CSV column headers into valid identifiers (e.g. 'Price ($)' → 'price') (#378).",
        },
        "csv-name-case": {
            type: "string",
            choices: ["snake_case", "camelCase"],
            default: "snake_case",
            description: "Case style for sanitized CSV column names (used with --csv-sanitize-names) (#378).",
        },
        "csv-parse-timeout-ms": {
            type: "number",
            description: "Wall-clock cap (ms) for CSV parsing; returns partial rows on timeout (#379).",
        },
        "csv-skip-empty-lines": {
            type: "boolean",
            default: true,
            description: "Skip blank/whitespace-only CSV data rows in content and rowCount (default true). Use --no-csv-skip-empty-lines to preserve them (#373).",
        },
        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",
        },
        toolRoot: {
            type: "string",
            array: true,
            description: "Directory the built-in file tools may read and write (repeatable). Defaults to the working directory. Constrains bash's working directory, not its commands.",
        },
        // 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>.<ext>, where the extension follows the format the provider actually returned)",
            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: providerChoicesFor("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: providerChoicesFor("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",
            choices: providerChoicesFor("video"),
            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",
            choices: providerChoicesFor("avatar"),
            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",
            choices: providerChoicesFor("music"),
            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"],
        },
        // Tool-routing options
        toolRouting: {
            type: "boolean",
            description: "Enable pre-call per-turn tool routing (narrows MCP tools by relevance).",
        },
        toolRoutingTimeout: {
            type: "number",
            description: "Router LLM hard timeout in milliseconds.",
            alias: "tool-routing-timeout",
        },
        toolRoutingRouterProvider: {
            type: "string",
            description: "Override the provider used for the router LLM call.",
            alias: "tool-routing-router-provider",
        },
        toolRoutingRouterModel: {
            type: "string",
            description: "Override the model used for the router LLM call.",
            alias: "tool-routing-router-model",
        },
        toolRoutingRouterRegion: {
            type: "string",
            description: "Override the region used for the router LLM call.",
            alias: "tool-routing-router-region",
        },
        toolRoutingAlwaysInclude: {
            type: "array",
            description: "Server ids whose tools are always kept and never offered to the router (repeatable).",
            alias: "tool-routing-always-include",
            string: true,
        },
        toolRoutingServers: {
            type: "string",
            description: "Path to a JSON file OR inline JSON array of {id, description} server descriptors for the routable catalog.",
            alias: "tool-routing-servers",
        },
        // Classifier-router options
        classifierRouter: {
            type: "boolean",
            description: "Enable the classifier router: classify each request and pick a model (and tools) from --classifier-pool.",
            alias: "classifier-router",
        },
        classifierStrategy: {
            type: "string",
            description: "Classifier strategy: 'auto' (default — 'jev' when a decision provider is configured, such as TYPESAFE_API_KEY, LAYA_API_KEY with LAYA_BASE_URL, XOR_API_KEY with XOR_BASE_URL, PERPLEXITY_API_KEY, or CLOUDFLARE_API_KEY with CLOUDFLARE_ACCOUNT_ID, else 'heuristic'), 'heuristic' (no LLM), 'llm' (a cheap model picks per prompt), or 'jev' (a System One decision model — TypeSafe Jev, Laya, XOR, Perplexity or Cloudflare Clef — with calibrated confidence).",
            choices: ["auto", "heuristic", "llm", "jev"],
            alias: "classifier-strategy",
        },
        classifierMinUpgradeConfidence: {
            type: "number",
            description: "Minimum confidence to route UP to a costlier model; below it the heuristic is used. Only meaningful for 'jev'. Default: 0.3.",
            alias: "classifier-min-upgrade-confidence",
        },
        classifierMinDowngradeConfidence: {
            type: "number",
            description: "Minimum confidence to route DOWN to a cheaper model. Higher than the upgrade bar, because the two mistakes cost differently. Default: 0.6.",
            alias: "classifier-min-downgrade-confidence",
        },
        classifierModelProvider: {
            type: "string",
            description: "Provider for the LLM classifier model (strategy=llm).",
            alias: "classifier-model-provider",
        },
        classifierModelName: {
            type: "string",
            description: "Model name for the LLM classifier model (strategy=llm).",
            alias: "classifier-model-name",
        },
        classifierModelRegion: {
            type: "string",
            description: "Region for the LLM classifier model (strategy=llm).",
            alias: "classifier-model-region",
        },
        classifierPool: {
            type: "string",
            description: "Path to a JSON file OR inline JSON array of pool members: { provider, model?, region?, description?, tiers?, cost?, quality?, id? }.",
            alias: "classifier-pool",
        },
        classifierTimeout: {
            type: "number",
            description: "LLM classifier hard timeout in milliseconds.",
            alias: "classifier-timeout",
        },
        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,
        },
        // Safety — PII detection
        piiRedact: {
            type: "boolean",
            description: "Enable PII detection and redaction on input before sending to LLM",
            alias: "pii-redact",
            default: false,
        },
        piiTypes: {
            type: "string",
            description: "Comma-separated PII types to detect (email,phone,ssn,creditCard,ipAddress,address,name,dateOfBirth,passport,driversLicense)",
            alias: "pii-types",
        },
        piiAction: {
            type: "string",
            description: "Action when PII is found: redact, abort, or warn",
            alias: "pii-action",
            default: "redact",
        },
        // Safety — Input validation
        inputMaxLength: {
            type: "number",
            description: "Maximum input text length (characters)",
            alias: "input-max-length",
        },
        trimWhitespace: {
            type: "boolean",
            description: "Trim whitespace from input text",
            alias: "trim-whitespace",
            default: false,
        },
        requireContent: {
            type: "boolean",
            description: "Abort if input text is empty or whitespace",
            alias: "require-content",
            default: false,
        },
        // Safety — Response validation
        outputMaxLength: {
            type: "number",
            description: "Maximum response length (characters). Truncates if exceeded",
            alias: "output-max-length",
        },
        outputMinLength: {
            type: "number",
            description: "Minimum response length (characters)",
            alias: "output-min-length",
        },
        // Skills options
        skillsDir: {
            type: "string",
            description: "Directory of skills (SOPs/playbooks) to make available to the AI. " +
                "Supports <id>.json, <name>.md, and <name>/SKILL.md layouts. " +
                "Also settable via NEUROLINK_SKILLS_DIR.",
            alias: "skills-dir",
        },
    };
    // Registered on generate and stream only; the flags mean nothing elsewhere.
    static agentOptions = {
        agentMode: {
            type: "boolean",
            description: "Run as an autonomous terminal agent: prepend Neurolink's versioned agent instructions to the system prompt",
        },
        maxSteps: {
            type: "number",
            description: `Maximum tool-calling steps before the turn stops (integer ${STEP_LIMITS.min}-${STEP_LIMITS.max}, default ${STEP_LIMITS.default}; some providers cap lower)`,
            coerce: (value) => {
                if (typeof value !== "number" ||
                    !Number.isInteger(value) ||
                    value < STEP_LIMITS.min ||
                    value > STEP_LIMITS.max) {
                    throw new Error(`--max-steps must be an integer between ${STEP_LIMITS.min} and ${STEP_LIMITS.max}`);
                }
                return value;
            },
        },
    };
    // Helper method to build options for commands. `excludeOptions` drops
    // common flags from registration entirely (not just from validation) —
    // #1191 round-5: batch uses this to omit `commonOptions.file` so `--file`
    // isn't a recognized flag at all, since it would collide with the
    // `<file>` positional (the prompts list).
    static buildOptions(yargs, additionalOptions = {}, excludeOptions = []) {
        const baseOptions = excludeOptions.length === 0
            ? CLICommandFactory.commonOptions
            : Object.fromEntries(Object.entries(CLICommandFactory.commonOptions).filter(([key]) => !excludeOptions.includes(key)));
        return (yargs
            .options({
            ...baseOptions,
            ...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);
    }
    /**
     * Resolve the PDF decryption password, preferring the NEUROLINK_PDF_PASSWORD
     * env var over the `--pdf-password` flag. A plaintext CLI flag leaks into
     * shell history, `ps`/process listings, and CI logs — the env var avoids
     * that, matching the project's existing "credentials via env vars, not
     * flags" stance for the loop session. The flag stays supported (dropping it
     * would be a breaking CLI change), but using it prints a one-line stderr
     * warning recommending the env var instead.
     */
    static resolvePdfPassword(argv) {
        const flagValue = argv.pdfPassword;
        const envValue = process.env.NEUROLINK_PDF_PASSWORD;
        if (flagValue) {
            process.stderr.write(chalk.yellow("āš ļø  --pdf-password is visible in shell history and process listings. " +
                "Prefer the NEUROLINK_PDF_PASSWORD environment variable instead.\n"));
        }
        return flagValue || envValue;
    }
    // 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);
    }
    // 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,
            agentMode: argv.agentMode,
            maxSteps: argv.maxSteps,
            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,
            // Tool-routing flags — constructor-level config, not a per-call option.
            // Passed through the options bag so handlers can inject into the SDK
            // instance before the first getOrCreateNeuroLink() call.
            toolRouting: argv.toolRouting,
            toolRoutingTimeout: argv.toolRoutingTimeout,
            toolRoutingRouterProvider: argv.toolRoutingRouterProvider,
            toolRoutingRouterModel: argv.toolRoutingRouterModel,
            toolRoutingRouterRegion: argv.toolRoutingRouterRegion,
            toolRoutingAlwaysInclude: argv.toolRoutingAlwaysInclude,
            toolRoutingServers: argv.toolRoutingServers,
            // Classifier-router flags — constructor-level config (see note above).
            classifierRouter: argv.classifierRouter,
            classifierStrategy: argv.classifierStrategy,
            classifierMinUpgradeConfidence: argv.classifierMinUpgradeConfidence,
            classifierMinDowngradeConfidence: argv.classifierMinDowngradeConfidence,
            classifierModelProvider: argv.classifierModelProvider,
            classifierModelName: argv.classifierModelName,
            classifierModelRegion: argv.classifierModelRegion,
            classifierPool: argv.classifierPool,
            classifierTimeout: argv.classifierTimeout,
            // Skills flag — constructor-level config (see note above).
            skillsDir: argv.skillsDir,
        };
    }
    /**
     * 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, "-");
                            const ext = imageExtensionFor(generateResult.imageOutput.mimeType, generateResult.imageOutput.base64);
                            imagePath = path.join(imageDir, `image-${timestamp}.${ext}`);
                            // 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 and playback
     * Saves audio to file when --tts-output flag is provided
     * Plays audio when --tts-play flag is provided
     */
    static async handleTTSOutput(result, options) {
        const ttsOutputPath = options.ttsOutput;
        const shouldPlay = options.ttsPlay;
        // Nothing to do if neither save nor play is requested
        if (!ttsOutputPath && !shouldPlay) {
            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;
        }
        // Save audio to file if --tts-output is provided
        if (ttsOutputPath) {
            try {
                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");
            }
        }
        // Play audio if --tts-play is provided
        if (shouldPlay) {
            try {
                if (!options.quiet) {
                    logger.always(chalk.blue("Playing audio..."));
                }
                await playAudio(audio.buffer, audio.format);
            }
            catch (err) {
                // Non-fatal: warn but don't crash
                logger.always(chalk.yellow(`Audio playback failed: ${err.message}`));
                logger.always(chalk.yellow("   Tip: Save the audio with --tts-output <file> and play manually."));
            }
        }
    }
    /**
     * 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.input === "number" &&
            typeof tokensObj.output === "number" &&
            typeof tokensObj.total === "number") {
            return {
                input: tokensObj.input,
                output: tokensObj.output,
                total: tokensObj.total,
            };
        }
        // Fallback format: tokenUsage {inputTokens, outputTokens, totalTokens}
        if (typeof tokensObj.inputTokens === "number" &&
            typeof tokensObj.outputTokens === "number" &&
            typeof tokensObj.totalTokens === "number") {
            return {
                input: tokensObj.inputTokens,
                output: tokensObj.outputTokens,
                total: tokensObj.totalTokens,
            };
        }
        return null;
    }
    // Helper method to format analytics for text mode display
    static formatAnalyticsForTextMode(result) {
        if (!result.analytics) {
            return "";
        }
        const analytics = result.analytics;
        let analyticsText = "\n\nšŸ“Š Analytics:\n";
        // Provider and model info
        analyticsText += `   Provider: ${analytics.provider}`;
        // Check for model in multiple locations: result.model, analytics.model, or available model property
        const modelName = result.model ||
            analytics.model ||
            analytics.modelName;
        if (modelName) {
            analyticsText += ` (${modelName})`;
        }
        analyticsText += "\n";
        // Token usage with fallback handling
        const normalizedTokens = CLICommandFactory.normalizeTokenUsage(analytics.tokenUsage);
        if (normalizedTokens) {
            analyticsText += `   Tokens: ${normalizedTokens.input} input + ${normalizedTokens.output} output = ${normalizedTokens.total} total\n`;
        }
        // Cost information
        if (analytics.cost !== undefined &&
            analytics.cost !== null &&
            typeof analytics.cost === "number") {
            analyticsText += `   Cost: $${analytics.cost.toFixed(5)}\n`;
        }
        // Response time with fallback handling for requestDuration vs responseTime
        const analyticsRecord = analytics;
        const duration = analytics.requestDuration ||
            analyticsRecord.responseTime ||
            analyticsRecord.duration;
        if (duration && typeof duration === "number") {
            const timeInSeconds = (duration / 1000).toFixed(1);
            analyticsText += `   Time: ${timeInSeconds}s\n`;
        }
        // Tools used
        if (result.toolsUsed && result.toolsUsed.length > 0) {
            analyticsText += `   Tools: ${result.toolsUsed.join(", ")}\n`;
        }
        // Context information
        if (analytics.context &&
            typeof analytics.context === "object" &&
            analytics.context !== null) {
            const contextEntries = Object.entries(analytics.context);
            if (contextEntries.length > 0) {
                const contextItems = contextEntries.map(([key, value]) => `${key}=${value}`);
                analyticsText += `   Context: ${contextItems.join(", ")}\n`;
            }
        }
        return analyticsText;
    }
    /**
     * Create the new primary 'generate' command
     */
    static createGenerateCommand() {
        return {
            command: ["generate [input]", "gen [input]"],
            describe: "Generate content using AI providers",
            builder: (yargs) => {
                return CLICommandFactory.buildOptions(yargs
                    .positional("input", {
                    type: "string",
                    description: "Text prompt for AI generation (or read from stdin)",
                })
                    .example('$0 generate "Explain quantum computing"', "Basic generation")
                    .example('$0 gen "Write a Python function" --provider openai', "Use specific provider")
                    .example('$0 generate "Code review" -m gpt-4 -t 0.3', "Use specific model and temperature")
                    .example('echo "Summarize this" | $0 generate', "Use stdin input")
                    .example('$0 generate "Analyze data" --enable-analytics', "Enable usage analytics")
                    .example('$0 generate "Futuristic city" --model gemini-2.5-flash-image', "Generate an image")
                    .example('$0 generate "Mountain landscape" --model gemini-2.5-flash-image --imageOutput ./my-images/mountain.png', "Generate image with custom path")
                    .example('$0 generate "Describe this video" --video path/to/video.mp4', "Analyze video content")
                    .example('$0 generate "Analyze sales" --csv data.csv --csv-format raw', "CSV with raw format (fast, minimal tokens)")
                    .example('$0 generate "Summarize data" --csv small.csv --csv-format markdown', "CSV with markdown table (readable)")
                    .example('$0 generate "Process data" --csv records.csv --csv-format json', "CSV with JSON format (structured)")
                    .example('$0 generate "Product showcase video" --image ./product.jpg --outputMode video --videoOutput ./output.mp4', "Generate video from image")
                    .example('$0 generate "Smooth camera movement" --image ./input.jpg --provider vertex --model veo-3.1-generate-001 --outputMode video --videoResolution 720p --videoLength 6 --videoAspectRatio 16:9 --videoOutput ./output.mp4', "Video generation with full options")
                    .example('$0 generate "AI in Healthcare" --pptPages 10', "Generate a PowerPoint presentation")
                    .example('$0 generate "Company Q4 Results" --pptPages 15 --pptTheme corporate --pptAudience business', "Generate presentation with options")
                    .example('$0 generate "Machine Learning 101" --pptTheme minimal --pptTone educational --pptNoImages', "Generate educational slides without AI images")
                    .example('$0 generate "Describe this image" --image ./photo.jpg', "Analyze an image (multimodal input)")
                    .example('$0 generate "Summarize this report" --pdf ./report.pdf', "Analyze a PDF document")
                    .example('$0 generate "Key trends?" --csv large-data.csv --csvMaxRows 100', "Analyze a CSV with a row limit")
                    .example('$0 generate "Compare the chart and the report" --image ./chart.png --pdf ./report.pdf', "Combine multiple file types in one prompt")
                    .example('$0 generate "What is in this file?" --file ./data.json', "Auto-detect a file type with --file")
                    .example('$0 generate "Fix the failing config" --agent-mode --max-steps 60', "Run as an autonomous terminal agent with a step budget"), CLICommandFactory.agentOptions);
            },
            handler: async (argv) => await CLICommandFactory.executeGenerate(argv),
        };
    }
    /**
     * Create stream command
     */
    static createStreamCommand() {
        return {
            command: "stream [input]",
            describe: "Stream generation in real-time",
            builder: (yargs) => {
                return CLICommandFactory.buildOptions(yargs
                    .positional("input", {
                    type: "string",
                    description: "Text prompt for streaming (or read from stdin)",
                })
                    .example('$0 stream "Write a story about space"', "Stream a creative story")
                    .example('$0 stream "Explain machine learning" -p anthropic', "Stream with specific provider")
                    .example('$0 stream "Code walkthrough" --output story.txt', "Stream to file")
                    .example('echo "Live demo" | $0 stream', "Stream from stdin")
                    .example('$0 stream "Narrate this video" --video path/to/video.mp4', "Stream video analysis")
                    .example('$0 stream "Describe this image" --image ./photo.jpg', "Stream image analysis (multimodal input)")
                    .example('$0 stream "Summarize this document" --pdf ./report.pdf', "Stream PDF analysis")
                    .example('$0 stream "Explain this dataset" --csv ./data.csv --csv-format markdown', "Stream CSV analysis"), CLICommandFactory.agentOptions);
            },
            handler: async (argv) => await CLICommandFactory.executeStream(argv),
        };
    }
    /**
     * Create batch command
     */
    static createBatchCommand() {
        return {
            // #1191 round-5: the positional is named `promptsFile`, not `file` —
            // yargs implicitly accepts `--<positionalName>` as a flag alias for
            // any positional regardless of whether it's separately registered via
            // `.options()`, so a positional literally named `file` would let
            // `--file` silently pass strictOptions() even after removing
            // `commonOptions.file` below. Renaming the key (invocation syntax is
            // unaffected — it's still just `neurolink batch prompts.txt`) is what
            // actually makes `--file` an unknown argument.
            command: "batch <promptsFile>",
            describe: "Process multiple prompts from a file",
            builder: (yargs) => {
                return CLICommandFactory.buildOptions(yargs
                    .positional("promptsFile", {
                    type: "string",
                    description: "File with prompts (one per line)",
                    demandOption: true,
                })
                    .example("$0 batch prompts.txt", "Process prompts from file")
                    .example("$0 batch questions.txt --format json", "Export results as JSON")
                    .example("$0 batch tasks.txt -p vertex --delay 2000", "Use Vertex AI with 2s delay")
                    .example("$0 batch batch.txt --output results.json", "Save results to file"), {}, 
                // `--file` is not a batch flag — the positional above is the
                // prompts-list path. Omit it from registration so it's rejected
                // as unknown rather than silently accepted then ignored.
                ["file"]);
            },
            handler: async (argv) => await CLICommandFactory.executeBatch(argv),
        };
    }
    /**
     * Create provider commands
     */
    static createProviderCommands() {
        return {
            command: "provider <subcommand>",
            describe: "Manage AI provider configurations and status",
            builder: (yargs) => {
                return yargs
                    .command("status", "Check status of all configured AI providers", (y) => CLICommandFactory.buildOptions(y)
                    .example("$0 provider status", "Check all provider status")
                    .example("$0 provider status --verbose", "Detailed provider diagnostics")
                    .example("$0 provider status --quiet", "Minimal status output"), (argv) => CLICommandFactory.executeProviderStatus(argv))
                    .demandCommand(1, "Please specify a provider subcommand");
            },
            handler: () => { }, // No-op handler as subcommands handle everything
        };
    }
    /**
     * Create status command (alias for provider status)
     */
    static createStatusCommand() {
        return {
            command: "status",
            describe: "Check AI provider connectivity and performance (alias for provider status)",
            builder: (yargs) => CLICommandFactory.buildOptions(yargs)
                .example("$0 status", "Quick provider status check")
                .example("$0 status --verbose", "Detailed connectivity diagnostics")
                .example("$0 status --format json", "Export status as JSON"),
            handler: async (argv) => await CLICommandFactory.executeProviderStatus(argv),
        };
    }
    /**
     * Create models commands
     */
    static createModelsCommands() {
        return ModelsCommandFactory.createModelsCommands();
    }
    /**
     * Create MCP commands
     */
    static createMCPCommands() {
        return MCPCommandFactory.createMCPCommands();
    }
    /**
     * Create discover command
     */
    static createDiscoverCommand() {
        return MCPCommandFactory.createDiscoverCommand();
    }
    /**
     * Create agent commands for multi-agent orchestration
     */
    static createAgentCommands() {
        return AgentCommandFactory.createAgentCommands();
    }
    /**
     * Create network commands for agent network orchestration
     */
    static createNetworkCommands() {
        return AgentCommandFactory.createNetworkCommands();
    }
    /**
     * Create memory commands
     */
    static createMemoryCommands() {
        return {
            command: "memory <subcommand>",
            describe: "Manage conversation memory and session history",
            builder: (yargs) => {
                return yargs
                    .command("list", "List all conversation sessions with metadata", (y) => CLICommandFactory.buildOptions(y)
                    .option("user-id", {
                    type: "string",
                    description: "User ID to filter sessions (required for Redis)",
                    alias: "u",
                })
                    .example("$0 memory list", "List all sessions")
                    .example("$0 memory list --format json", "List sessions as JSON")
                    .example("$0 memory list --user-id user123", "List sessions for specific user"), async (argv) => await CLICommandFactory.executeMemoryList(argv))
                    .command("export", "Export a specific session to JSON/CSV", (y) => CLICommandFactory.buildOptions(y)
                    .option("session-id", {
                    type: "string",
                    description: "Session ID to export",
                    demandOption: true,
                    alias: "s",
                })
                    .option("include-metadata", {
                    type: "boolean",
                    default: false,
                    description: "Include export metadata",
                })
                    // Override the generic --format so this command accepts csv.
                    // (Alias -f / --output-format comes from the common options.)
                    .option("format", {
                    choices: ["json", "csv"],
                    default: "json",
                    description: "Export format (json or csv)",
                })
                    .example("$0 memory export --session-id session-123", "Export session to stdout")
                    .example("$0 memory export --session-id session-123 --format json > history.json", "Export to file")
                    .example("$0 memory export --session-id session-123 --format csv > history.csv", "Export as CSV")
                    .example("$0 memory export --session-id session-123 --include-metadata", "Export with metadata"), async (argv) => await CLICommandFactory.executeMemoryExport(argv))
                    .command("export-all", "Export all sessions to a directory", (y) => CLICommandFactory.buildOptions(y)
                    .option("output", {
                    type: "string",
                    description: "Output directory for exported files",
                    default: "./memory-exports",
                    alias: "o",
                })
                    .option("user-id", {
                    type: "string",
                    description: "User ID to filter sessions (required for Redis)",
                    alias: "u",
                })
                    .option("include-metadata", {
                    type: "boolean",
                    default: true,
                    description: "Include export metadata",
                })
                    // Override the generic --format so this command accepts csv;
                    // it controls the extension/serialization of the per-session
                    // files written to the output directory. (Alias -f /
                    // --output-format comes from the common options.)
                    .option("format", {
                    choices: ["json", "csv"],
                    default: "json",
                    description: "Per-file export format (json or csv)",
                })
                    .example("$0 memory export-all --output ./exports/", "Export all sessions to directory")
                    .example("$0 memory export-all --user-id user123 --output ./user-exports/", "Export all sessions for user")
                    .example("$0 memory export-all --output ./exports/ --format csv", "Export all sessions as CSV files"), async (argv) => await CLICommandFactory.executeMemoryExportAll(argv))
                    .command("delete", "Delete a specific conversation session", (y) => CLICommandFactory.buildOptions(y)
                    .option("session-id", {
                    type: "string",
                    description: "Session ID to delete",
                    demandOption: true,
                    alias: "s",
                })
                    .option("force", {
                    type: "boolean",
                    default: false,
                    description: "Skip confirmation prompt",
                    alias: "f",
                })
                    .example("$0 memory delete --session-id session-123", "Delete session with confirmation")
                    .example("$0 memory delete --session-id session-123 --force", "Delete session without confirmation"), async (argv) => await CLICommandFactory.executeMemoryDelete(argv))
                    .command("stats", "Show conversation memory statistics", (y) => CLICommandFactory.buildOptions(y)
                    .example("$0 memory stats", "Show memory usage statistics")
                    .example("$0 memory stats --format json", "Export stats as JSON"), async (argv) => await CLICommandFactory.executeMemoryStats(argv))
                    .command("history <sessionId>", "Show conversation history for a session", (y) => CLICommandFactory.buildOptions(y)
                    .positional("sessionId", {
                    type: "string",
                    description: "Session ID to retrieve history for",
                    demandOption: true,
                })
                    .example("$0 memory history session-123", "Show conversation history")
                    .example("$0 memory history session-123 --format json", "Export history as JSON"), async (argv) => await CLICommandFactory.executeMemoryHistory(argv))
                    .command("clear [sessionId]", "Clear conversation history (use --confirm to clear all)", (y) => CLICommandFactory.buildOptions(y)
                    .positional("sessionId", {
                    type: "string",
                    description: "Session ID to clear (omit to clear all sessions)",
                    demandOption: false,
                })
                    .option("confirm", {
                    type: "boolean",
                    default: false,
                    description: "Confirm clearing all sessions",
                })
                    .example("$0 memory clear --confirm", "Clear all conversation history")
                    .example("$0 memory clear session-123", "Clear specific session"), async (argv) => await CLICommandFactory.executeMemoryClear(argv))
                    .demandCommand(1, "Please specify a memory subcommand");
            },
            handler: () => { }, // No-op handler as subcommands handle everything
        };
    }
    /**
     * Create skills commands — manage the local skills store
     * (list / show / search / create / delete). The store directory comes
     * from --skills-dir, NEUROLINK_SKILLS_DIR, or defaults to ./skills.
     */
    static createSkillsCommands() {
        return {
            command: "skills <subcommand>",
            describe: "Manage skills (SOPs/playbooks the AI can follow)",
            builder: (yargs) => {
                return yargs
                    .command("list", "List all active skills (index only, no instructions)", (y) => CLICommandFactory.buildOptions(y)
                    .example("$0 skills list --skills-dir ./skills", "List skills")
                    .example("$0 skills list --format json", "Export as JSON"), async (argv) => await CLICommandFactory.executeSkillsList(argv))
                    .command("show <skill>", "Show one skill including its full instructions", (y) => CLICommandFactory.buildOptions(y)
                    .positional("skill", {
                    type: "string",
                    description: "Skill id or exact name",
                    demandOption: true,
                })
                    .example("$0 skills show refund_dispute_escalation", "Show a skill by name"), async (argv) => await CLICommandFactory.executeSkillsShow(argv))
                    .command("search <query>", "Search skills by keyword (matches name and description)", (y) => CLICommandFactory.buildOptions(y)
                    .positional("query", {
                    type: "string",
                    description: "Keyword to match",
                    demandOption: true,
                })
                    .option("tag", {
                    type: "string",
                    description: "Tag filter applied on top of the keyword",
                })
                    .example('$0 skills search "refund"', "Find refund skills"), async (argv) => await CLICommandFactory.executeSkillsSearch(argv))
                    .command("create", "Create a new skill in the store", (y) => CLICommandFactory.buildOptions(y)
                    .option("name", {
                    type: "string",
                    description: "Machine-friendly unique name (snake_case)",
                    demandOption: true,
                })
                    .option("description", {
                    type: "string",
                    description: "When this skill applies (used for matching)",
                    demandOption: true,
                })
                    .option("instructions", {
                    type: "string",
                    description: "Full instructions text",
                })
                    .option("instructions-file", {
                    type: "string",
                    description: "Read instructions from a file",
                })
                    .option("display-name", {
                    type: "string",
                    description: "Human-readable display name",
                })
                    .option("tags", {
                    type: "array",
                    string: true,
                    description: "Domain tags",
                })
                    .example('$0 skills create --name deploy_sop --description "How to deploy" --instructions-file ./sop.md', "Create from a file"), async (argv) => await CLICommandFactory.executeSkillsCreate(argv))
                    .command("delete <skill>", "Soft-delete (deprecate) a skill", (y) => CLICommandFactory.buildOptions(y).positional("skill", {
                    type: "string",
                    description: "Skill id or exact name",
                    demandOption: true,
                }), async (argv) => await CLICommandFactory.executeSkillsDelete(argv))
                    .demandCommand(1, "Please specify a skills subcommand");
            },
            handler: () => { }, // No-op handler as subcommands handle everything
        };
    }
    /**
     * Create config commands
     */
    static createConfigCommands() {
        return {
            command: "config <subcommand>",
            describe: "Manage NeuroLink configuration",
            builder: (yargs) => {
                return yargs
                    .command("init", "Interactive configuration setup wizard", (y) => CLICommandFactory.buildOptions(y), async (_argv) => {
                    await configManager.initInteractive();
                })
                    .command("show", "Display current configuration", (y) => CLICommandFactory.buildOptions(y), async (_argv) => {
                    configManager.showConfig();
                })
                    .command("validate", "Validate current configuration", (y) => CLICommandFactory.buildOptions(y), async (_argv) => {
                    const result = configManager.validateConfig();
                    if (result.valid) {
                        logger.always(chalk.green("āœ… Configuration is valid"));
                    }
                    else {
                        const errorMessages = result.errors.join("\n  • ");
                        handleError(new Error(`Configuration has errors:\n  • ${errorMessages}`), "Configuration validation");
                    }
                })
                    .command("reset", "Reset configuration to defaults", (y) => CLICommandFactory.buildOptions(y), async (_argv) => {
                    configManager.resetConfig();
                })
                    .command("export", "Export current configuration", (y) => CLICommandFactory.buildOptions(y), (argv) => CLICommandFactory.executeConfigExport(argv))
                    .demandCommand(1, "");
            },
            handler: () => { }, // No-op handler as subcommands handle everything
        };
    }
    /**
     * Create validate command
     */
    static createValidateCommand() {
        return {
            command: "validate",
            describe: "Validate current configuration (alias for 'config validate')",
            builder: (yargs) => CLICommandFactory.buildOptions(yargs),
            handler: async (_argv) => {
                const result = configManager.validateConfig();
                if (result.valid) {
                    logger.always(chalk.green("āœ… Configuration is valid"));
                }
                else {
                    const errorMessages = result.errors.join("\n  • ");
                    handleError(new Error(`Configuration has errors:\n  • ${errorMessages}`), "Configuration validation");
                }
            },
        };
    }
    /**
     * Create get-best-provider command
     */
    static createBestProviderCommand() {
        return {
            command: "get-best-provider",
            describe: "Show the best available AI provider",
            builder: (yargs) => CLICommandFactory.buildOptions(yargs)
                .example("$0 get-best-provider", "Get best available provider")
                .example("$0 get-best-provider --format json", "Get provider as JSON")
                .example("$0 get-best-provider --quiet", "Just the provider name"),
            handler: async (argv) => await CLICommandFactory.executeGetBestProvider(argv),
        };
    }
    /**
     * Create Ollama commands
     */
    static createOllamaCommands() {
        return OllamaCommandFactory.createOllamaCommands();
    }
    /**
     * Create setup command
     */
    static createSetupCommand() {
        return {
            command: ["setup [provider]", "s [provider]"],
            describe: "Interactive AI provider setup wizard",
            builder: (yargs) => {
                return CLICommandFactory.buildOptions(yargs
                    .positional("provider", {
                    type: "string",
                    description: "Specific provider to set up",
                    choices: [
                        "google-ai",
                        "openai",
                        "openrouter",
                        "anthropic",
                        "anthropic-subscription", // Setup Anthropic with subscription tier
                        "azure",
                        "bedrock",
                        "vertex",
                        "huggingface",
                        "mistral",
                        "deepseek",
                        "nvidia-nim",
                        "lm-studio",
                        "llamacpp",
                        "xai",
                        "groq",
                        "cerebras",
                        "cohere",
                        "together-ai",
                        "fireworks",
                        "perplexity",
                        "cloudflare",
                        "replicate",
                        "voyage",
                        "jina",
                        "stability",
                        "ideogram",
                        "recraft",
                    ],
                })
                    .option("list", {
                    type: "boolean",
                    description: "List all available providers",
                    alias: "l",
                })
                    .option("status", {
                    type: "boolean",
                    description: "Show provider configuration status",
                })
                    .option("subscription-tier", {
                    type: "string",
                    choices: ["free", "pro", "max", "max_5", "max_20", "api"],
                    description: "Anthropic subscription tier for setup (free, pro, max, max_5, max_20, api)",
                })
                    .option("auth-method", {
                    type: "string",
                    choices: ["api-key", "oauth"],
                    description: "Authentication method for Anthropic (api-key or oauth)",
                })
                    .example("$0 setup", "Interactive setup wizard")
                    .example("$0 setup --provider openai", "Setup specific provider")
                    .example("$0 setup --provider anthropic --subscription-tier pro", "Setup Anthropic with Pro subscription")
                    .example("$0 setup --provider anthropic --auth-method oauth", "Setup Anthropic with OAuth authentication")
                    .example("$0 setup --list", "List all providers")
                    .example("$0 setup --status", "Check provider status"));
            },
            handler: async (argv) => await handleSetup(argv),
        };
    }
    /**
     * Create SageMaker commands
     */
    static createSageMakerCommands() {
        return SageMakerCommandFactory.createSageMakerCommands();
    }
    /**
     * Create completion command
     */
    /**
     * Create loop command
     */
    static createLoopCommand() {
        return {
            command: "loop",
            describe: "Start an interactive loop session with conversation management",
            builder: (yargs) => CLICommandFactory.buildOptions(yargs, {
                "enable-conversation-memory": {
                    type: "boolean",
                    description: "Enable conversation memory for the loop session",
                    default: true,
                },
                "max-sessions": {
                    type: "number",
                    description: "Maximum number of conversation sessions to keep",
                    default: 50,
                },
                "max-turns-per-session": {
                    type: "number",
                    description: "Maximum turns per conversation session",
                    default: 20,
                },
                "auto-redis": {
                    type: "boolean",
                    description: "Automatically use Redis if available",
                    default: true,
                },
                resume: {
                    type: "string",
                    description: "Directly resume a specific conversation by session ID",
                    alias: "r",
                },
                new: {
                    type: "boolean",
                    description: "Force start a new conversation (skip selection menu)",
                    alias: "n",
                },
                "list-conversations": {
                    type: "boolean",
                    description: "List available conversations and exit",
                    alias: "l",
                },
                "compact-threshold": {
                    describe: "Context compaction trigger threshold (0.0-1.0)",
                    type: "number",
                    default: 0.8,
                },
                "disable-compaction": {
                    describe: "Disable automatic context compaction",
                    type: "boolean",
                    default: false,
                },
            })
                .example("$0 loop", "Start interactive session with conversation selection")
                .example("$0 loop --new", "Force start new conversation")
                .example("$0 loop --resume abc123", "Resume specific conversation")
                .example("$0 loop --list-conversations", "List available conversations")
                .example("$0 loop --no-auto-redis", "Use in-memory storage only")
                .example("$0 loop --enable-conversation-memory", "Start loop with memory"),
            handler: async (argv) => {
                if (globalSession.getCurrentSessionId()) {
                    logger.error("A loop session is already active. Cannot start a new one.");
                    return;
                }
                let conversationMemoryConfig;
                const { enableConversationMemory, maxSessions, maxTurnsPerSession, autoRedis, listConversations, compactThreshold, disableCompaction, } = argv;
                if (enableConversationMemory) {
                    let storageType = "memory";
                    if (autoRedis) {
                        const isRedisAvailable = await checkRedisAvailability();
                        if (isRedisAvailable) {
                            storageType = "redis";
                            if (!argv.quiet) {
                                logger.always(chalk.green("āœ… Using Redis for persistent conversation memory"));
                            }
                        }
                        else if (argv.debug) {
                            logger.debug("Redis not available, using in-memory storage");
                        }
                    }
                    else if (argv.debug) {
                        logger.debug("Auto-Redis disabled, using in-memory storage");
                    }
                    process.env.STORAGE_TYPE = storageType;
                    conversationMemoryConfig = {
                        enabled: true,
                        maxSessions: maxSessions,
                        maxTurnsPerSession: maxTurnsPerSession,
                        contextCompaction: {
                            enabled: !disableCompaction,
                            threshold: compactThreshold,
                        },
                    };
                }
                // Inject skills config (--skills-dir / NEUROLINK_SKILLS_DIR) before
                // the loop session constructs its NeuroLink instance.
                const loopSkillsConfig = buildSkillsConfigFromCli(argv);
                if (loopSkillsConfig) {
                    globalSession.setSkillsConfig(loopSkillsConfig);
                }
                // Handle --list-conversations option
                if (listConversations) {
                    const { ConversationSelector } = await import("../loop/conversationSelector.js");
                    const conversationSelector = new ConversationSelector();
                    try {
                        const hasConversations = await conversationSelector.hasStoredConversations();
                        if (!hasConversations) {
                            logger.always(chalk.yellow("šŸ“ No stored conversations found"));
                            return;
                        }
                        const conversations = await conversationSelector.getAvailableConversations();
                        logger.always(chalk.blue("šŸ“‹ Available Conversations:"));
                        conversations.forEach((conv, index) => {
                            const sessionId = conv.sessionId.slice(0, 12) + "...";
                            const title = conv.title || "Untitled Conversation";
                            const messageCount = conv.messageCount || 0;
                            const lastActivity = conv.updatedAt
                                ? new Date(conv.updatedAt).toLocaleDateString()
                                : "Unknown";
                            logger.always(`${index + 1}. ${chalk.cyan(sessionId)} - ${title}`);
                            logger.always(`   ${chalk.gray(`${messageCount} messages | Last: ${lastActivity}`)}`);
                        });
                        logger.always(chalk.gray(`\nUse: neurolink loop --resume <session-id> to resume a conversation`));
                    }
                    catch (error) {
                        logger.error("Failed to list conversations:", error);
                    }
                    finally {
                        await conversationSelector.close();
                    }
                    return;
                }
                // Create enhanced session with direct session management options
                const sessionOptions = {};
                // Pass CLI options to session for direct session management
                if (argv.resume && typeof argv.resume === "string") {
                    sessionOptions.directResumeSessionId = argv.resume;
                }
                if (argv.new) {
                    sessionOptions.forceNewSession = true;
                }
                const session = new LoopSession(initializeCliParser, conversationMemoryConfig, sessionOptions);
                await session.start();
            },
        };
    }
    /**
     * Create completion command
     */
    static createCompletionCommand() {
        return {
            command: "completion",
            describe: "Generate shell completion script",
            builder: (yargs) => CLICommandFactory.buildOptions(yargs)
                .example("$0 completion", "Generate shell completion")
                .example("$0 completion > ~/.neurolink-completion.sh", "Save completion script")
                .example("source ~/.neurolink-completion.sh", "Enable completions (bash)")
                .epilogue("Add the completion script to your shell profile for persistent completions"),
            handler: async (argv) => await CLICommandFactory.executeCompletion(argv),
        };
    }
    /**
     * Execute provider status command
     */
    static async executeProviderStatus(argv) {
        if (argv.verbose && !argv.quiet) {
            logger.always(chalk.yellow("ā„¹ļø Verbose mode enabled. Displaying detailed status.\n"));
        }
        const spinner = argv.quiet
            ? null
            : ora("šŸ” Checking AI provider status...\n").start();
        const sdk = globalSession.getOrCreateNeuroLink();
        try {
            // Handle dry-run mode for provider status
            if (argv.dryRun) {
                const mockResults = [
                    {
                        provider: "google-ai",
                        status: "working",
                        configured: true,
                        requestDuration: 150,
                        model: "gemini-2.5-flash",
                    },
                    {
                        provider: "openai",
                        status: "working",
                        configured: true,
                        requestDuration: 200,
                        model: "gpt-4o-mini",
                    },
                    {
                        provider: "anthropic",
                        status: "working",
                        configured: true,
                        requestDuration: 180,
                        model: "claude-3-haiku",
                    },
                    { provider: "bedrock", status: "not configured", configured: false },
                    { provider: "vertex", status: "not configured", configured: false },
                ];
                if (spinner) {
                    spinner.succeed("Provider check complete (dry-run): 3/3 providers working");
                }
                // Display mock results
                for (const result of mockResults) {
                    const status = result.status === "working"
                        ? chalk.green("āœ… Working")
                        : result.status === "failed"
                            ? chalk.red("āŒ Failed")
                            : chalk.gray("⚪ Not configured");
                    const time = result.requestDuration
                        ? ` (${result.requestDuration}ms)`
                        : "";
                    const model = result.model ? ` [${result.model}]` : "";
                    logger.always(`${result.provider}: ${status}${time}${model}`);
                }
                if (argv.verbose && !argv.quiet) {
                    logger.always(chalk.blue("\nšŸ“‹ Detailed Results (Dry-run):"));
                    logger.always(JSON.stringify(mockResults, null, 2));
                }
                return;
            }
            // Use SDK's provider diagnostic method instead of manual testing
            const results = await sdk.getProviderStatus({ quiet: !!argv.quiet });
            if (spinner) {
                const working = results.filter((r) => r.status === "working").length;
                const configured = results.filter((r) => r.configured).length;
                spinner.succeed(`Provider check complete: ${working}/${configured} providers working`);
            }
            // Display results
            for (const result of results) {
                const status = result.status === "working"
                    ? chalk.green("āœ… Working")
                    : result.status === "failed"
                        ? chalk.red("āŒ Failed")
                        : chalk.gray("⚪ Not configured");
                const time = result.responseTime ? ` (${result.responseTime}ms)` : "";
                const model = result.model ? ` [${result.model}]` : "";
                logger.always(`${result.provider}: ${status}${time}${model}`);
                if (argv.verbose && result.error) {
                    logger.always(`  Error: ${chalk.red(result.error)}`);
                }
            }
            if (argv.verbose && !argv.quiet) {
                logger.always(chalk.blue("\nšŸ“‹ Detailed Results:"));
                logger.always(JSON.stringify(results, null, 2));
            }
        }
        catch (error) {
            if (spinner) {
                spinner.fail("Provider status check failed");
            }
            handleError(error, "Provider status check");
        }
        finally {
            // Ensure all background processes are terminated
            try {
                await sdk.shutdownExternalMCPServers();
            }
            catch (shutdownError) {
                logger.error("Error during SDK shutdown:", shutdownError);
            }
            if (!globalSession.getCurrentSessionId()) {
                await CLICommandFactory.exitAfterStdoutDrain();
            }
        }
    }
    /**
     * Handle stdin input for generate command
     */
    static async handleGenerateStdinInput(argv) {
        // M10: STT-only runs (--stt + --input-audio with no positional prompt)
        // are valid — the transcription becomes the prompt downstream. Skip the
        // stdin/empty-input check in that case so users don't get
        // "Input required..." for an STT-only command.
        const isSttOnly = !!(argv.stt &&
            argv.inputAudio);
        if (!argv.input && !process.stdin.isTTY) {
            // The SDK's MCP stdio transport unrefs stdin on import so an idle
            // process can exit on its own (see externalServerManager.ts). This
            // path actually reads stdin and must block on it, so ref it back
            // before consuming.
            ensureStdinRef();
            let stdinData = "";
            process.stdin.setEncoding("utf8");
            for await (const chunk of process.stdin) {
                stdinData += chunk;
            }
            const trimmedData = stdinData.trim();
            if (!trimmedData) {
                if (isSttOnly) {
                    return "";
                }
                throw new Error("āŒ No input received from stdin.\n" +
                    "šŸ’” Try this:\n" +
                    '   neurolink generate "your prompt"\n' +
                    '   echo "your prompt" | neurolink generate\n' +
                    '   neurolink generate "Describe this image" --image ./photo.jpg');
            }
            return trimmedData;
        }
        else if (!argv.input) {
            if (isSttOnly) {
                return "";
            }
            throw new Error("āŒ Input required.\n" +
                "šŸ’” Try this:\n" +
                '   neurolink generate "your prompt"\n' +
                '   echo "your prompt" | neurolink generate\n' +
                '   neurolink generate "Describe this image" --image ./photo.jpg');
        }
        return argv.input;
    }
    /**
     * Detect output mode (video, ppt, avatar, music, or text) based on CLI arguments
     */
    static detectGenerateOutputMode(argv, options) {
        const outputMode = options.outputMode;
        const hasPPTFlags = argv.pptPages !== undefined ||
            argv.pptTheme !== undefined ||
            argv.pptAudience !== undefined ||
            argv.pptTone !== undefined ||
            argv.pptOutput !== undefined ||
            argv.pptAspectRatio !== undefined ||
            argv.pptNoImages === true;
        const hasVideoSignals = outputMode === "video" ||
            argv.videoOutput !== undefined ||
            argv.videoProvider !== undefined ||
            argv.videoLength !== undefined ||
            argv.videoResolution !== undefined ||
            argv.videoAspectRatio !== undefined;
        const hasPPTSignals = outputMode === "ppt" || hasPPTFlags;
        const hasAvatarSignals = outputMode === "avatar" ||
            argv.avatarProvider !== undefined ||
            argv.avatarImage !== undefined ||
            argv.avatarText !== undefined ||
            argv.avatarAudio !== undefined ||
            argv.avatarVoice !== undefined ||
            argv.avatarOutput !== undefined;
        const hasMusicSignals = outputMode === "music" ||
            argv.musicProvider !== undefined ||
            argv.musicGenre !== undefined ||
            argv.musicMood !== undefined ||
            argv.musicDuration !== undefined ||
            argv.musicTempo !== undefined ||
            argv.musicOutput !== undefined;
        const activeModes = [
            hasVideoSignals,
            hasPPTSignals,
            hasAvatarSignals,
            hasMusicSignals,
        ].filter(Boolean).length;
        if (activeModes > 1) {
            throw new Error("Conflicting output mode signals detected. Use exactly one of video / ppt / avatar / music modes (or text).");
        }
        // Derive mode flags from the full signal set so that flag-only invocations
        // (e.g. --videoOutput without --output-mode video) are handled correctly.
        const isVideoMode = hasVideoSignals;
        const isAvatarMode = hasAvatarSignals;
        const isMusicMode = hasMusicSignals;
        const isPPTMode = hasPPTSignals;
        const spinnerMessage = isVideoMode
            ? "šŸŽ¬ Generating video... (this may take 1-2 minutes)"
            : isPPTMode
                ? "šŸ“Š Generating presentation... (this may take 2-5 minutes)"
                : isAvatarMode
                    ? "šŸ‘¤ Generating avatar video... (this may take 1-3 minutes)"
                    : isMusicMode
                        ? "šŸŽµ Generating music... (this may take 30s-2 minutes)"
                        : "šŸ¤– Generating text...";
        return {
            isVideoMode,
            isPPTMode,
            isAvatarMode,
            isMusicMode,
            spinnerMessage,
        };
    }
    /**
     * Process context for generation command
     */
    static processGenerateContext(inputText, options) {
        let processedInputText = inputText;
        let contextMetadata;
        if (options.context && options.contextConfig) {
            const processedContextResult = ContextFactory.processContext(options.context, options.contextConfig);
            if (processedContextResult.processedContext) {
                processedInputText =
                    processedContextResult.processedContext + processedInputText;
            }
            contextMetadata = {
                ...ContextFactory.extractAnalyticsContext(options.context),
                contextMode: processedContextResult.config.mode,
                contextTruncated: processedContextResult.metadata.truncated,
            };
            if (options.debug) {
                logger.debug("Context processed:", {
                    mode: processedContextResult.config.mode,
                    truncated: processedContextResult.metadata.truncated,
                    processingTime: processedContextResult.metadata.processingTime,
                });
            }
        }
        return { inputText: processedInputText, contextMetadata };
    }
    /**
     * Build multimodal input from CLI arguments
     */
    static buildGenerateMultimodalInput(inputText, argv) {
        const imageBuffers = CLICommandFactory.processCliImages(argv.image);
        const csvFiles = CLICommandFactory.processCliCSVFiles(argv.csv);
        const pdfFiles = CLICommandFactory.processCliPDFFiles(argv.pdf);
        const videoFiles = CLICommandFactory.processCliVideoFiles(argv.video);
        const files = CLICommandFactory.processCliFiles(argv.file);
        return {
            text: inputText,
            ...(imageBuffers && { images: imageBuffers }),
            ...(csvFiles && { csvFiles }),
            ...(pdfFiles && { pdfFiles }),
            ...(videoFiles && { videoFiles }),
            ...(files && { files }),
        };
    }
    /**
     * Build CSV processor options from CLI argv (#1199). Shared by executeGenerate
     * and executeRealStream, which previously built byte-for-byte identical
     * objects independently.
     */
    static buildCsvOptionsFromArgv(argv) {
        return {
            maxRows: argv.csvMaxRows,
            formatStyle: argv.csvFormat,
            encoding: argv.csvEncoding,
            sanitizeColumnNames: argv.csvSanitizeNames,
            columnNameCase: argv.csvNameCase,
            parseTimeoutMs: argv.csvParseTimeoutMs,
            skipEmptyLines: argv.csvSkipEmptyLines,
        };
    }
    /**
     * Build video processor options from CLI argv. Shared by executeGenerate,
     * executeStream and executeBatch, which previously built byte-for-byte
     * identical `videoOptions` objects independently (mirrors
     * {@link buildCsvOptionsFromArgv}).
     */
    static buildVideoOptionsFromArgv(argv) {
        return {
            frames: argv.videoFrames,
            quality: argv.videoQuality,
            format: argv.videoFormat,
            transcribeAudio: argv.transcribeAudio,
        };
    }
    /**
     * Build output configuration for generate request
     */
    static buildGenerateOutputConfig(isVideoMode, isPPTMode, enhancedOptions, isAvatarMode = false, isMusicMode = false) {
        if (isVideoMode) {
            return {
                mode: "video",
                video: {
                    provider: enhancedOptions.videoProvider,
                    resolution: enhancedOptions.videoResolution,
                    length: enhancedOptions.videoLength,
                    aspectRatio: enhancedOptions.videoAspectRatio,
                    audio: enhancedOptions.videoAudio,
                },
            };
        }
        if (isAvatarMode) {
            return {
                mode: "avatar",
                avatar: {
                    provider: enhancedOptions.avatarProvider,
                    image: enhancedOptions.avatarImage,
                    audio: enhancedOptions.avatarAudio,
                    text: enhancedOptions.avatarText,
                    voice: enhancedOptions.avatarVoice,
                    quality: enhancedOptions.avatarQuality,
                    format: enhancedOptions.avatarFormat,
                    output: enhancedOptions.avatarOutput,
                },
            };
        }
        if (isMusicMode) {
            return {
                mode: "music",
                music: {
                    prompt: "", // Filled in from input.text/prompt by baseProvider
                    provider: enhancedOptions.musicProvider,
                    duration: enhancedOptions.musicDuration,
                    format: enhancedOptions.musicFormat,
                    genre: enhancedOptions.musicGenre,
                    mood: enhancedOptions.musicMood,
                    tempo: enhancedOptions.musicTempo,
                    output: enhancedOptions.musicOutput,
                },
            };
        }
        if (isPPTMode) {
            return {
                mode: "ppt",
                ppt: {
                    pages: enhancedOptions.pptPages || 10,
                    theme: enhancedOptions.pptTheme,
                    audience: enhancedOptions.pptAudience,
                    tone: enhancedOptions.pptTone,
                    aspectRatio: enhancedOptions.pptAspectRatio || "16:9",
                    generateAIImages: !enhancedOptions.pptNoImages,
                    outputPath: enhancedOptions.pptOutput,
                },
            };
        }
        return undefined;
    }
    /**
     * Handle successful generation result
     */
    static async handleGenerateSuccess(result, options, modes, spinner) {
        const { isVideoMode, isPPTMode, isAvatarMode, isMusicMode } = modes;
        const genResult = result;
        if (spinner) {
            if (isVideoMode) {
                spinner.succeed(chalk.green("āœ… Video generated successfully!"));
            }
            else if (isPPTMode) {
                spinner.succeed(chalk.green("āœ… Presentation generated successfully!"));
            }
            else if (isAvatarMode) {
                spinner.succeed(chalk.green("āœ… Avatar video generated successfully!"));
            }
            else if (isMusicMode) {
                spinner.succeed(chalk.green("āœ… Music generated successfully!"));
            }
            else {
                spinner.succeed(chalk.green("āœ… Text generated successfully!"));
            }
        }
        if (!options.quiet) {
            const providerInfo = genResult.provider || "auto";
            const modelInfo = genResult.model || "default";
            logger.always(chalk.gray(`šŸ”§ Provider: ${providerInfo} | Model: ${modelInfo}`));
        }
        if (!isVideoMode && !isPPTMode && !isAvatarMode && !isMusicMode) {
            CLICommandFactory.handleOutput(genResult, options);
        }
        await CLICommandFactory.handleTTSOutput(genResult, options);
        await CLICommandFactory.handleVideoOutput(genResult, options);
        await CLICommandFactory.handleAvatarOutput(genResult, options);
        await CLICommandFactory.handleMusicOutput(genResult, options);
        await CLICommandFactory.handlePPTOutput(genResult, options);
        if (options.debug) {
            logger.debug("\n" + chalk.yellow("Debug Information:"));
            logger.debug("Provider:", genResult.provider);
            logger.debug("Model:", genResult.model);
            if (genResult.analytics) {
                logger.debug("Analytics:", JSON.stringify(genResult.analytics, null, 2));
            }
            if (genResult.evaluation) {
                logger.debug("Evaluation:", JSON.stringify(genResult.evaluation, null, 2));
            }
        }
        if (!globalSession.getCurrentSessionId()) {
            await CLICommandFactory.flushLangfuseTraces();
            await CLICommandFactory.exitAfterStdoutDrain(0);
        }
    }
    /**
     * Execute the generate command
     */
    static async executeGenerate(argv) {
        // Handle stdin input
        const rawInput = await CLICommandFactory.handleGenerateStdinInput(argv);
        argv.input = rawInput;
        const options = CLICommandFactory.processOptions(argv);
        // Detect output mode
        const { isVideoMode, isPPTMode, isAvatarMode, isMusicMode, spinnerMessage, } = CLICommandFactory.detectGenerateOutputMode(argv, options);
        const spinner = argv.quiet ? null : ora(spinnerMessage).start();
        try {
            // Add delay if specified
            if (options.delay) {
                await new Promise((resolve) => setTimeout(resolve, options.delay));
            }
            validateCliInputFiles(argv);
            validateCsvMaxRows(argv);
            // Process context
            const { inputText, contextMetadata } = CLICommandFactory.processGenerateContext(rawInput, options);
            // Handle dry-run mode for testing
            if (options.dryRun) {
                const mockResult = {
                    content: "Mock response for testing purposes",
                    provider: options.provider || "auto",
                    model: options.model || "test-model",
                    usage: { input: 10, output: 15, total: 25 },
                    responseTime: 150,
                    analytics: options.enableAnalytics
                        ? {
                            provider: options.provider || "auto",
                            model: options.model || "test-model",
                            tokenUsage: { input: 10, output: 15, total: 25 },
                            cost: 0.00025,
                            requestDuration: 150,
                            context: contextMetadata,
                        }
                        : undefined,
                    evaluation: options.enableEvaluation
                        ? normalizeEvaluationData({
                            relevance: 8,
                            accuracy: 9,
                            completeness: 8,
                            overall: 8.3,
                            reasoning: "Test evaluation response",
                            evaluationModel: "test-evaluator",
                            evaluationTime: 50,
                        })
                        : undefined,
                };
                if (spinner) {
                    spinner.succeed(chalk.green("āœ… Dry-run completed successfully!"));
                }
                CLICommandFactory.handleOutput(mockResult, options);
                if (options.debug) {
                    logger.debug("\n" + chalk.yellow("Debug Information (Dry-run):"));
                    logger.debug("Provider:", mockResult.provider);
                    logger.debug("Model:", mockResult.model);
                    logger.debug("Mode: DRY-RUN (no actual API calls made)");
                }
                if (!globalSession.getCurrentSessionId()) {
                    await CLICommandFactory.flushLangfuseTraces();
                    await CLICommandFactory.exitAfterStdoutDrain(0);
                }
                return;
            }
            // Inject tool-routing config into the SDK instance before constructing it.
            const toolRoutingConfig = buildToolRoutingConfigFromCli(options);
            if (toolRoutingConfig) {
                globalSession.setToolRoutingConfig(toolRoutingConfig);
            }
            // Inject classifier-router config into the SDK instance before constructing it.
            const classifierRouterConfig = buildClassifierRouterConfigFromCli(options);
            if (classifierRouterConfig) {
                globalSession.setClassifierRouterConfig(classifierRouterConfig);
            }
            // Inject skills config (--skills-dir / NEUROLINK_SKILLS_DIR) before SDK construction.
            const skillsConfig = buildSkillsConfigFromCli(options);
            if (skillsConfig) {
                globalSession.setSkillsConfig(skillsConfig);
            }
            globalSession.setFileToolRoots(argv.toolRoot);
            // Initialize SDK and session
            const sdk = globalSession.getOrCreateNeuroLink();
            const sessionVariables = CLICommandFactory.normalizeLoopSessionVariables(globalSession.getSessionVariables());
            const enhancedOptions = { ...options, ...sessionVariables };
            const sessionId = globalSession.getCurrentSessionId();
            const context = sessionId
                ? { ...options.context, sessionId }
                : options.context;
            if (options.debug) {
                logger.debug("CLI Tools configuration:", {
                    disableTools: options.disableTools,
                    toolsEnabled: !options.disableTools,
                });
            }
            // Configure mode-specific options
            if (isVideoMode) {
                CLICommandFactory.configureVideoMode(enhancedOptions, argv, options);
            }
            if (isPPTMode) {
                CLICommandFactory.configurePPTMode(enhancedOptions, argv, options);
            }
            // Build multimodal input and output configuration
            const generateInput = CLICommandFactory.buildGenerateMultimodalInput(inputText, argv);
            const outputConfig = CLICommandFactory.buildGenerateOutputConfig(isVideoMode, isPPTMode, enhancedOptions, isAvatarMode, isMusicMode);
            // Read audio file for STT if --input-audio is provided.
            // NEW10: existsSync guard mirrors the stream handler so a missing file
            // produces a friendly error here too instead of a raw ENOENT crash.
            const inputAudioPath = enhancedOptions.inputAudio;
            if (inputAudioPath && !fs.existsSync(inputAudioPath)) {
                throw new Error(`--input-audio file not found: ${inputAudioPath}`);
            }
            const inputAudioBuffer = inputAudioPath
                ? fs.readFileSync(inputAudioPath)
                : undefined;
            // m2: shared format helper (was duplicated in generate + stream
            // handlers; now lives in src/lib/utils/audioFormatDetector.ts).
            const { inferAudioFormatFromPath } = await import("../../utils/audioFormatDetector.js");
            const inputAudioFormat = inferAudioFormatFromPath(inputAudioPath);
            const runGenerate = () => sdk.generate({
                input: generateInput,
                pdfOptions: {
                    password: CLICommandFactory.resolvePdfPassword(argv),
                },
                csvOptions: CLICommandFactory.buildCsvOptionsFromArgv(argv),
                videoOptions: CLICommandFactory.buildVideoOptionsFromArgv(argv),
                output: outputConfig,
                provider: enhancedOptions.provider,
                model: enhancedOptions.model,
                temperature: enhancedOptions.temperature,
                maxTokens: enhancedOptions.maxTokens,
                topP: enhancedOptions.topP,
                topK: enhancedOptions.topK,
                stopSequences: enhancedOptions.stopSequences,
                systemPrompt: enhancedOptions.systemPrompt,
                agentMode: enhancedOptions.agentMode,
                maxSteps: enhancedOptions.maxSteps,
                timeout: enhancedOptions.timeout
                    ? enhancedOptions.timeout * 1000
                    : undefined,
                disableTools: enhancedOptions.disableTools,
                enabledToolNames: enhancedOptions.enabledToolNames,
                enableAnalytics: enhancedOptions.enableAnalytics,
                enableEvaluation: enhancedOptions.enableEvaluation,
                evaluationDomain: enhancedOptions.evaluationDomain,
                toolUsageContext: enhancedOptions.toolUsageContext,
                context: context,
                region: options.region,
                thinkingConfig: createThinkingConfigFromRecord(options),
                factoryConfig: enhancedOptions.domain
                    ? {
                        domainType: enhancedOptions.domain,
                        enhancementType: "domain-configuration",
                        validateDomainData: true,
                    }
                    : undefined,
                // RAG configuration
                rag: argv.ragFiles?.length
                    ? {
                        files: argv.ragFiles,
                        strategy: argv.ragStrategy,
                        chunkSize: argv.ragChunkSize,
                        chunkOverlap: argv.ragChunkOverlap,
                        topK: argv.ragTopK,
                    }
                    : undefined,
                // PII detection
                piiDetection: argv.piiRedact
                    ? {
                        enabled: true,
                        action: argv.piiAction ?? "redact",
                        detectTypes: argv.piiTypes
                            ? argv.piiTypes
                                .split(",")
                                .map((t) => t.trim())
                            : undefined,
                    }
                    : undefined,
                // Input validation
                inputValidation: argv.inputMaxLength || argv.trimWhitespace || argv.requireContent
                    ? {
                        maxLength: argv.inputMaxLength,
                        trimWhitespace: argv.trimWhitespace,
                        requireContent: argv.requireContent,
                    }
                    : undefined,
                // Response validation
                responseValidation: argv.outputMaxLength || argv.outputMinLength
                    ? {
                        maxLength: argv.outputMaxLength,
                        minLength: argv.outputMinLength,
                        truncationAction: "truncate",
                    }
                    : undefined,
                // TTS configuration
                tts: enhancedOptions.tts
                    ? {
                        enabled: true,
                        useAiResponse: true,
                        voice: enhancedOptions.ttsVoice,
                        provider: enhancedOptions.ttsProvider,
                        format: enhancedOptions.ttsFormat || undefined,
                        speed: enhancedOptions.ttsSpeed,
                        quality: enhancedOptions.ttsQuality,
                        output: enhancedOptions.ttsOutput,
                        play: enhancedOptions.ttsPlay,
                    }
                    : undefined,
                // STT configuration
                stt: enhancedOptions.stt
                    ? {
                        enabled: true,
                        provider: enhancedOptions.sttProvider,
                        language: enhancedOptions.sttLanguage,
                        ...(inputAudioBuffer && { audio: inputAudioBuffer }),
                        ...(inputAudioFormat && { format: inputAudioFormat }),
                    }
                    : undefined,
            });
            const result = await runGenerate();
            // Handle successful result
            await CLICommandFactory.handleGenerateSuccess(result, options, { isVideoMode, isPPTMode, isAvatarMode, isMusicMode }, spinner);
        }
        catch (error) {
            if (spinner) {
                spinner.fail();
            }
            handleError(error, "Generation");
        }
    }
    /**
     * Process context for streaming
     */
    static async processStreamContext(argv, options) {
        let inputText = argv.input;
        let contextMetadata;
        if (options.context && options.contextConfig) {
            const processedContextResult = ContextFactory.processContext(options.context, options.contextConfig);
            // Integrate context into prompt if configured
            if (processedContextResult.processedContext) {
                inputText = processedContextResult.processedContext + inputText;
            }
            // Add context metadata for analytics
            contextMetadata = {
                ...ContextFactory.extractAnalyticsContext(options.context),
                contextMode: processedContextResult.config.mode,
                contextTruncated: processedContextResult.metadata.truncated,
            };
            if (options.debug) {
                logger.debug("Context processed for streaming:", {
                    mode: processedContextResult.config.mode,
                    truncated: processedContextResult.metadata.truncated,
                    processingTime: processedContextResult.metadata.processingTime,
                });
            }
        }
        return { inputText, contextMetadata };
    }
    /**
     * Execute dry-run streaming simulation
     */
    static async executeDryRunStream(options, contextMetadata) {
        if (!options.quiet) {
            logger.always(chalk.blue("šŸ”„ Dry-run streaming..."));
        }
        // Simulate streaming output
        const chunks = [
            "Mock ",
            "streaming ",
            "response ",
            "for ",
            "testing ",
            "purposes",
        ];
        let fullContent = "";
        for (const chunk of chunks) {
            await animatedWrite(chunk);
            fullContent += chunk;
            await new Promise((resolve) => setTimeout(resolve, 50)); // Simulate streaming delay
        }
        if (!options.quiet) {
            process.stdout.write("\n");
        }
        // Mock analytics and evaluation for dry-run
        if (options.enableAnalytics) {
            const mockAnalytics = {
                provider: options.provider || "auto",
                model: options.model || "test-model",
                requestDuration: 300,
                tokenUsage: {
                    input: 10,
                    output: 15,
                    total: 25,
                },
                timestamp: new Date().toISOString(),
                context: contextMetadata,
            };
            const mockGenerateResult = {
                success: true,
                content: fullContent,
                analytics: mockAnalytics,
                model: mockAnalytics.model,
                toolsUsed: [],
            };
            const analyticsDisplay = CLICommandFactory.formatAnalyticsForTextMode(mockGenerateResult);
            logger.always(analyticsDisplay);
        }
        if (options.enableEvaluation) {
            logger.always(chalk.blue("\nšŸ“Š Response Evaluation (Dry-run):"));
            logger.always(`   Relevance: 8/10`);
            logger.always(`   Accuracy: 9/10`);
            logger.always(`   Completeness: 8/10`);
            logger.always(`   Overall: 8.3/10`);
            logger.always(`   Reasoning: Test evaluation response`);
        }
        if (options.output) {
            fs.writeFileSync(options.output, fullContent);
            if (!options.quiet) {
                logger.always(`\nOutput saved to ${options.output}`);
            }
        }
        if (options.debug) {
            logger.debug("\n" + chalk.yellow("Debug Information (Dry-run Streaming):"));
            logger.debug("Provider:", options.provider || "auto");
            logger.debug("Model:", options.model || "test-model");
            logger.debug("Mode: DRY-RUN (no actual API calls made)");
        }
        if (!globalSession.getCurrentSessionId()) {
            await CLICommandFactory.flushLangfuseTraces();
            await CLICommandFactory.exitAfterStdoutDrain(0);
        }
    }
    /**
     * Execute real streaming with timeout handling
     */
    static async executeRealStream(argv, options, inputText, contextMetadata) {
        // Inject tool-routing config into the SDK instance before constructing it.
        const toolRoutingConfig = buildToolRoutingConfigFromCli(options);
        if (toolRoutingConfig) {
            globalSession.setToolRoutingConfig(toolRoutingConfig);
        }
        // Inject classifier-router config into the SDK instance before constructing it.
        const classifierRouterConfig = buildClassifierRouterConfigFromCli(options);
        if (classifierRouterConfig) {
            globalSession.setClassifierRouterConfig(classifierRouterConfig);
        }
        // Inject skills config (--skills-dir / NEUROLINK_SKILLS_DIR) before SDK construction.
        const skillsConfig = buildSkillsConfigFromCli(options);
        if (skillsConfig) {
            globalSession.setSkillsConfig(skillsConfig);
        }
        globalSession.setFileToolRoots(argv.toolRoot);
        const sdk = globalSession.getOrCreateNeuroLink();
        const sessionVariables = CLICommandFactory.normalizeLoopSessionVariables(globalSession.getSessionVariables());
        const enhancedOptions = { ...options, ...sessionVariables };
        const sessionId = globalSession.getCurrentSessionId();
        const context = sessionId
            ? { ...contextMetadata, sessionId }
            : contextMetadata;
        // Process CLI multimodal inputs
        const imageBuffers = CLICommandFactory.processCliImages(argv.image);
        const csvFiles = CLICommandFactory.processCliCSVFiles(argv.csv);
        const pdfFiles = CLICommandFactory.processCliPDFFiles(argv.pdf);
        const videoFiles = CLICommandFactory.processCliVideoFiles(argv.video);
        const files = CLICommandFactory.processCliFiles(argv.file);
        const runStream = async () => sdk.stream({
            input: {
                text: inputText,
                ...(imageBuffers && { images: imageBuffers }),
                ...(csvFiles && { csvFiles }),
                ...(pdfFiles && { pdfFiles }),
                ...(videoFiles && { videoFiles }),
                ...(files && { files }),
            },
            pdfOptions: {
                password: CLICommandFactory.resolvePdfPassword(argv),
            },
            csvOptions: CLICommandFactory.buildCsvOptionsFromArgv(argv),
            videoOptions: CLICommandFactory.buildVideoOptionsFromArgv(argv),
            provider: enhancedOptions.provider,
            model: enhancedOptions.model,
            temperature: enhancedOptions.temperature,
            maxTokens: enhancedOptions.maxTokens,
            topP: enhancedOptions.topP,
            topK: enhancedOptions.topK,
            stopSequences: enhancedOptions.stopSequences,
            systemPrompt: enhancedOptions.systemPrompt,
            agentMode: enhancedOptions.agentMode,
            maxSteps: enhancedOptions.maxSteps,
            timeout: enhancedOptions.timeout
                ? enhancedOptions.timeout * 1000
                : undefined,
            disableTools: enhancedOptions.disableTools,
            enabledToolNames: enhancedOptions.enabledToolNames,
            enableAnalytics: enhancedOptions.enableAnalytics,
            enableEvaluation: enhancedOptions.enableEvaluation,
            evaluationDomain: enhancedOptions.evaluationDomain,
            toolUsageContext: enhancedOptions.toolUsageContext,
            context: context,
            region: options.region,
            thinkingConfig: createThinkingConfigFromRecord(options),
            factoryConfig: enhancedOptions.domain
                ? {
                    domainType: enhancedOptions.domain,
                    enhancementType: "domain-configuration",
                    validateDomainData: true,
                }
                : undefined,
            // RAG configuration
            rag: argv.ragFiles?.length
                ? {
                    files: argv.ragFiles,
                    strategy: argv.ragStrategy,
                    chunkSize: argv.ragChunkSize,
                    chunkOverlap: argv.ragChunkOverlap,
                    topK: argv.ragTopK,
                }
                : undefined,
            // PII detection
            piiDetection: argv.piiRedact
                ? {
                    enabled: true,
                    action: argv.piiAction ?? "redact",
                    detectTypes: argv.piiTypes
                        ? argv.piiTypes
                            .split(",")
                            .map((t) => t.trim())
                        : undefined,
                }
                : undefined,
            // Input validation
            inputValidation: argv.inputMaxLength || argv.trimWhitespace || argv.requireContent
                ? {
                    maxLength: argv.inputMaxLength,
                    trimWhitespace: argv.trimWhitespace,
                    requireContent: argv.requireContent,
                }
                : undefined,
            // Response validation
            responseValidation: argv.outputMaxLength || argv.outputMinLength
                ? {
                    maxLength: argv.outputMaxLength,
                    minLength: argv.outputMinLength,
                    truncationAction: "truncate",
                }
                : undefined,
            // TTS configuration
            tts: enhancedOptions.tts
                ? {
                    enabled: true,
                    useAiResponse: true,
                    voice: enhancedOptions.ttsVoice,
                    provider: enhancedOptions.ttsProvider,
                    format: enhancedOptions.ttsFormat || undefined,
                    speed: enhancedOptions.ttsSpeed,
                    quality: enhancedOptions.ttsQuality,
                    output: enhancedOptions.ttsOutput,
                    play: enhancedOptions.ttsPlay,
                }
                : undefined,
            // STT configuration. m2: shared format helper (was duplicated with
            // the generate handler; now lives in audioFormatDetector.ts).
            stt: enhancedOptions.stt
                ? await (async () => {
                    const streamSttAudioPath = enhancedOptions.inputAudio;
                    // Fail fast on a missing --input-audio so a CLI typo doesn't
                    // turn into a confusing provider/validation error later
                    // (matches the generate path).
                    if (streamSttAudioPath && !fs.existsSync(streamSttAudioPath)) {
                        throw new Error(`--input-audio file not found: ${streamSttAudioPath}`);
                    }
                    const streamSttAudio = streamSttAudioPath
                        ? fs.readFileSync(streamSttAudioPath)
                        : undefined;
                    const { inferAudioFormatFromPath: inferFmt } = await import("../../utils/audioFormatDetector.js");
                    const streamSttFormat = inferFmt(streamSttAudioPath);
                    return {
                        enabled: true,
                        provider: enhancedOptions.sttProvider,
                        language: enhancedOptions.sttLanguage,
                        ...(streamSttAudio && { audio: streamSttAudio }),
                        ...(streamSttFormat && { format: streamSttFormat }),
                    };
                })()
                : undefined,
        });
        const stream = await runStream();
        const streamResult = await CLICommandFactory.processStreamWithTimeout(stream, options);
        await CLICommandFactory.displayStreamResults(stream, streamResult.content, options);
        // Handle image output from stream (image models emit image events)
        if (streamResult.imageBase64) {
            try {
                let imagePath;
                if (options.imageOutput) {
                    imagePath = path.resolve(options.imageOutput);
                    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, "-");
                    const ext = imageExtensionFor(undefined, streamResult.imageBase64);
                    imagePath = path.join(imageDir, `image-${timestamp}.${ext}`);
                    if (!fs.existsSync(imageDir)) {
                        fs.mkdirSync(imageDir, { recursive: true });
                    }
                }
                const imageBuffer = Buffer.from(streamResult.imageBase64, "base64");
                fs.writeFileSync(imagePath, imageBuffer);
                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 streamed image");
            }
        }
        return streamResult.content;
    }
    /**
     * Process stream with timeout handling
     */
    static async processStreamWithTimeout(stream, options) {
        let fullContent = "";
        let lastImageBase64;
        let contentReceived = false;
        const abortController = new AbortController();
        // BZ-667: Wire SIGINT to abort stream gracefully
        const abortHandler = createStreamAbortHandler();
        abortHandler.signal.addEventListener("abort", () => {
            abortController.abort();
        }, { once: true });
        // Create timeout promise for stream consumption (default: 30 seconds, respects user-provided timeout)
        const streamTimeout = options.timeout && typeof options.timeout === "number"
            ? options.timeout * 1000
            : 30000;
        const timeoutPromise = new Promise((_, reject) => {
            const timeoutId = setTimeout(() => {
                if (!contentReceived) {
                    const timeoutError = new Error(`\nāŒ Stream timeout - no content received within ${streamTimeout / 1000} seconds\n` +
                        "This usually indicates authentication or network issues\n\n" +
                        "šŸ”§ Try these steps:\n" +
                        "1. Check your provider credentials are configured correctly\n" +
                        `2. Test generate mode: neurolink generate "test" --provider ${options.provider}\n` +
                        `3. Use debug mode: neurolink stream "test" --provider ${options.provider} --debug`);
                    reject(timeoutError);
                }
            }, streamTimeout);
            // Clean up timeout when aborted
            abortController.signal.addEventListener("abort", () => {
                clearTimeout(timeoutId);
            });
        });
        const streamIterator = stream.stream[Symbol.asyncIterator]();
        try {
            // Process the stream with timeout handling
            let timeoutActive = true;
            // BZ-667: Create an abort promise that rejects when the user presses Ctrl+C,
            // so we can race it against streamIterator.next() and unblock pending reads.
            const abortPromise = new Promise((_, reject) => {
                if (abortHandler.signal.aborted) {
                    reject(new DOMException("Stream aborted", "AbortError"));
                    return;
                }
                abortHandler.signal.addEventListener("abort", () => {
                    reject(new DOMException("Stream aborted", "AbortError"));
                }, { once: true });
            });
            while (true) {
                let nextResult;
                if (timeoutActive && !contentReceived) {
                    // Race between next chunk, timeout, and abort signal
                    nextResult = await Promise.race([
                        streamIterator.next(),
                        timeoutPromise,
                        abortPromise,
                    ]);
                }
                else {
                    // Race between next chunk and abort signal
                    nextResult = await Promise.race([
                        streamIterator.next(),
                        abortPromise,
                    ]);
                }
                if (nextResult.done) {
                    break;
                }
                if (!contentReceived) {
                    contentReceived = true;
                    timeoutActive = false;
                    abortController.abort(); // Cancel timeout
                }
                if (options.delay && options.delay > 0) {
                    // Demo mode - add delay between chunks
                    await new Promise((resolve) => setTimeout(resolve, options.delay));
                }
                const evt = nextResult.value;
                const isText = (o) => !!o &&
                    typeof o === "object" &&
                    typeof o.content === "string";
                const isAudio = (o) => {
                    if (!o || typeof o !== "object") {
                        return false;
                    }
                    const t = o.type;
                    return t === "audio" || t === "tts_audio";
                };
                const isImage = (o) => {
                    if (!o || typeof o !== "object") {
                        return false;
                    }
                    const record = o;
                    if (record.type !== "image") {
                        return false;
                    }
                    if (!record.imageOutput || typeof record.imageOutput !== "object") {
                        return false;
                    }
                    return (typeof record.imageOutput.base64 ===
                        "string");
                };
                if (isText(evt)) {
                    await animatedWrite(evt.content);
                    fullContent += evt.content;
                }
                else if (isAudio(evt)) {
                    if (options.debug && !options.quiet) {
                        process.stdout.write("[audio-chunk]");
                    }
                }
                else if (isImage(evt)) {
                    lastImageBase64 = evt.imageOutput.base64;
                    if (options.debug && !options.quiet) {
                        process.stdout.write("[image-received]");
                    }
                }
            }
        }
        catch (error) {
            abortController.abort(); // Clean up timeout
            // BZ-667: Close the stream iterator so the provider connection is released.
            // Wrap in try/catch to prevent cleanup failures from masking the original error.
            try {
                await streamIterator.return?.();
            }
            catch {
                // Iterator cleanup failed — swallow so the original error propagates
            }
            abortHandler.cleanup();
            // BZ-667: Handle graceful abort — return partial content instead of throwing
            if (abortHandler.signal.aborted ||
                (error instanceof Error && error.name === "AbortError")) {
                if (!options.quiet) {
                    process.stdout.write("\n");
                }
                return { content: fullContent, imageBase64: lastImageBase64 };
            }
            throw error;
        }
        abortHandler.cleanup();
        if (!contentReceived) {
            throw new Error("\nāŒ No content received from stream\n" +
                "Check your credentials and provider configuration");
        }
        if (!options.quiet) {
            process.stdout.write("\n");
        }
        return { content: fullContent, imageBase64: lastImageBase64 };
    }
    /**
     * Display analytics and evaluation results
     */
    static async displayStreamResults(stream, fullContent, options) {
        // Display analytics after streaming
        if (options.enableAnalytics && stream.analytics) {
            const resolvedAnalytics = await (stream.analytics instanceof Promise
                ? stream.analytics
                : Promise.resolve(stream.analytics));
            const streamAnalytics = {
                success: true,
                content: fullContent,
                analytics: resolvedAnalytics,
                model: stream.model,
                toolsUsed: stream.toolCalls?.map((tc) => tc.toolName) || [],
            };
            const analyticsDisplay = CLICommandFactory.formatAnalyticsForTextMode(streamAnalytics);
            logger.always(analyticsDisplay);
        }
        // Display evaluation after streaming
        if (options.enableEvaluation && stream.evaluation) {
            const resolvedEvaluation = await (stream.evaluation instanceof Promise
                ? stream.evaluation
                : Promise.resolve(stream.evaluation));
            logger.always(chalk.blue("\nšŸ“Š Response Evaluation:"));
            logger.always(`   Relevance: ${resolvedEvaluation.relevance}/10`);
            logger.always(`   Accuracy: ${resolvedEvaluation.accuracy}/10`);
            logger.always(`   Completeness: ${resolvedEvaluation.completeness}/10`);
            logger.always(`   Overall: ${resolvedEvaluation.overall}/10`);
            if (resolvedEvaluation.reasoning) {
                logger.always(`   Reasoning: ${resolvedEvaluation.reasoning}`);
            }
        }
    }
    /**
     * Handle stream output file writing and debug output
     */
    static async handleStreamOutput(options, fullContent) {
        // Handle output file if specified
        if (options.output) {
            fs.writeFileSync(options.output, fullContent);
            if (!options.quiet) {
                logger.always(`\nOutput saved to ${options.output}`);
            }
        }
        // Handle TTS audio output/playback if --tts-output or --tts-play is provided
        // Note: For streaming, TTS audio is collected during the stream
        // and saved at the end if available
        const ttsOutputPath = options.ttsOutput;
        const shouldPlay = options.ttsPlay;
        if (ttsOutputPath || shouldPlay) {
            // For now, streaming TTS output is not yet available
            // This will be enabled when the TTS streaming infrastructure is complete
            if (!options.quiet) {
                logger.always(chalk.yellow("TTS audio for streaming is not yet available. Use 'generate' command for TTS output."));
            }
        }
        // Debug output for streaming
        if (options.debug) {
            await CLICommandFactory.logStreamDebugInfo({
                provider: options.provider,
                model: options.model,
            });
        }
    }
    /**
     * Log debug information for stream result
     */
    static async logStreamDebugInfo(stream) {
        logger.debug("\n" + chalk.yellow("Debug Information (Streaming):"));
        logger.debug("Provider:", stream.provider);
        logger.debug("Model:", stream.model);
        if (stream.analytics) {
            const resolvedAnalytics = await (stream.analytics instanceof Promise
                ? stream.analytics
                : Promise.resolve(stream.analytics));
            logger.debug("Analytics:", JSON.stringify(resolvedAnalytics, null, 2));
        }
        if (stream.evaluation) {
            const resolvedEvaluation = await (stream.evaluation instanceof Promise
                ? stream.evaluation
                : Promise.resolve(stream.evaluation));
            logger.debug("Evaluation:", JSON.stringify(resolvedEvaluation, null, 2));
        }
        if (stream.metadata) {
            logger.debug("Metadata:", JSON.stringify(stream.metadata, null, 2));
        }
    }
    /**
     * Handle stdin input for stream command
     */
    static async handleStdinInput(argv) {
        // STT-only flow: --stt --input-audio <file> with no text prompt is now
        // valid (the stream pipeline transcribes the audio and uses the result
        // as the prompt). Skip the stdin/empty-input rejection in that case.
        const isSttOnly = !!argv.stt && !!argv.inputAudio;
        if (!argv.input && !process.stdin.isTTY) {
            // The SDK's MCP stdio transport unrefs stdin on import so an idle
            // process can exit on its own (see externalServerManager.ts). This
            // path actually reads stdin and must block on it, so ref it back
            // before consuming.
            ensureStdinRef();
            let stdinData = "";
            process.stdin.setEncoding("utf8");
            for await (const chunk of process.stdin) {
                stdinData += chunk;
            }
            argv.input = stdinData.trim();
            if (!argv.input) {
                if (isSttOnly) {
                    argv.input = "";
                    return;
                }
                throw new Error("āŒ No input received from stdin.\n" +
                    "šŸ’” Try this:\n" +
                    '   neurolink stream "your prompt"\n' +
                    '   echo "your prompt" | neurolink stream\n' +
                    '   neurolink stream "Describe this image" --image ./photo.jpg');
            }
        }
        else if (!argv.input) {
            if (isSttOnly) {
                argv.input = "";
                return;
            }
            throw new Error("āŒ Input required.\n" +
                "šŸ’” Try this:\n" +
                '   neurolink stream "your prompt"\n' +
                '   echo "your prompt" | neurolink stream\n' +
                '   neurolink stream "Describe this image" --image ./photo.jpg');
        }
    }
    /**
     * Execute the stream command
     */
    static async executeStream(argv) {
        await CLICommandFactory.handleStdinInput(argv);
        const options = CLICommandFactory.processOptions(argv);
        // Validate Anthropic subscription options if using Anthropic provider
        CLICommandFactory.validateAnthropicSubscriptionOptions(options);
        if (!options.quiet) {
            logger.always(chalk.blue("šŸ”„ Streaming..."));
        }
        try {
            // Add delay if specified
            if (options.delay) {
                await new Promise((resolve) => setTimeout(resolve, options.delay));
            }
            validateCliInputFiles(argv);
            validateCsvMaxRows(argv);
            const { inputText, contextMetadata } = await CLICommandFactory.processStreamContext(argv, options);
            // Handle dry-run mode for testing
            if (options.dryRun) {
                await CLICommandFactory.executeDryRunStream(options, contextMetadata);
                return;
            }
            const fullContent = await CLICommandFactory.executeRealStream(argv, options, inputText, contextMetadata);
            await CLICommandFactory.handleStreamOutput(options, fullContent);
            if (!globalSession.getCurrentSessionId()) {
                await CLICommandFactory.flushLangfuseTraces();
                await CLICommandFactory.exitAfterStdoutDrain(0);
            }
        }
        catch (error) {
            handleError(error, "Streaming");
        }
    }
    /**
     * Execute the batch command
     */
    static async executeBatch(argv) {
        const options = CLICommandFactory.processOptions(argv);
        // Validate Anthropic subscription options if using Anthropic provider
        CLICommandFactory.validateAnthropicSubscriptionOptions(options);
        const spinner = options.quiet ? null : ora().start();
        try {
            if (!argv.promptsFile) {
                throw new Error("No file specified");
            }
            // #291/round-4 #1191: validate multimodal flags and resolve their
            // paths *before* the prompts file is read/parsed below, mirroring
            // generate/stream (which validate all inputs before any processing).
            // Previously this ran after `fs.readFileSync(resolvedPromptsPath)`,
            // so a large prompts file paired with an invalid --image path paid
            // the read/parse cost before the flag error ever surfaced. `--file`
            // (the common auto-detect flag) isn't registered on batch at all —
            // see createBatchCommand — so `argv.file` can never be set here; no
            // exclusion needed to protect the (differently-named) positional.
            validateCliInputFiles(argv);
            validateCsvMaxRows(argv);
            const batchImages = CLICommandFactory.processCliImages(argv.image);
            const batchCsvFiles = CLICommandFactory.processCliCSVFiles(argv.csv);
            const batchPdfFiles = CLICommandFactory.processCliPDFFiles(argv.pdf);
            const batchVideoFiles = CLICommandFactory.processCliVideoFiles(argv.video);
            // #291: route the prompts-list positional through the same friendly
            // aggregated-error path as the multimodal flags above (not found /
            // unreadable / directory / malformed file:// URL), instead of a raw
            // existsSync+readFileSync that throws an unfriendly EISDIR when
            // pointed at a directory.
            const resolvedPromptsPath = validatePromptsFilePath(argv.promptsFile);
            const buffer = fs.readFileSync(resolvedPromptsPath);
            const prompts = buffer
                .toString("utf8")
                .split("\n")
                .map((line) => line.trim())
                .filter(Boolean);
            if (prompts.length === 0) {
                throw new Error("No prompts found in file");
            }
            if (spinner) {
                spinner.text = `šŸ“¦ Processing ${prompts.length} prompts...`;
            }
            else if (!options.quiet) {
                logger.always(chalk.blue(`šŸ“¦ Processing ${prompts.length} prompts...\n`));
            }
            const results = [];
            // Inject tool-routing config into the SDK instance before constructing it.
            const toolRoutingConfig = buildToolRoutingConfigFromCli(options);
            if (toolRoutingConfig) {
                globalSession.setToolRoutingConfig(toolRoutingConfig);
            }
            // Inject classifier-router config into the SDK instance before constructing it.
            const classifierRouterConfig = buildClassifierRouterConfigFromCli(options);
            if (classifierRouterConfig) {
                globalSession.setClassifierRouterConfig(classifierRouterConfig);
            }
            // Inject skills config (--skills-dir / NEUROLINK_SKILLS_DIR) before SDK construction.
            const skillsConfig = buildSkillsConfigFromCli(options);
            if (skillsConfig) {
                globalSession.setSkillsConfig(skillsConfig);
            }
            globalSession.setFileToolRoots(argv.toolRoot);
            const sdk = globalSession.getOrCreateNeuroLink();
            const sessionVariables = CLICommandFactory.normalizeLoopSessionVariables(globalSession.getSessionVariables());
            const enhancedOptions = { ...options, ...sessionVariables };
            const sessionId = globalSession.getCurrentSessionId();
            // batchImages/batchCsvFiles/batchPdfFiles/batchVideoFiles were already
            // validated and resolved above, before the prompts file was read.
            if (batchImages?.length ||
                batchCsvFiles?.length ||
                batchPdfFiles?.length ||
                batchVideoFiles?.length) {
                // Pause the spinner so the notice isn't swallowed by its re-renders.
                // Safety-relevant, so always visible (not gated behind --quiet) and
                // written to stderr so `--format json` output on stdout stays parseable.
                spinner?.stop();
                logger.alwaysStderr(chalk.yellow("āš ļø  Multimodal files (--image/--csv/--pdf/--video) are attached to ALL prompts in this batch."));
                spinner?.start();
            }
            // Resolve the PDF password once for the whole batch. resolvePdfPassword
            // emits a "visible in shell history" stderr warning when the flag is set;
            // it must fire once per run (like generate/stream), not once per prompt.
            const batchPdfPassword = batchPdfFiles?.length
                ? CLICommandFactory.resolvePdfPassword(argv)
                : undefined;
            for (let i = 0; i < prompts.length; i++) {
                if (spinner) {
                    spinner.text = `Processing ${i + 1}/${prompts.length}: ${prompts[i].substring(0, 30)}...`;
                }
                try {
                    // Handle dry-run mode for batch processing
                    if (options.dryRun) {
                        results.push({
                            prompt: prompts[i],
                            response: `Mock batch response ${i + 1} for testing purposes`,
                        });
                        if (spinner) {
                            spinner.render();
                        }
                        continue;
                    }
                    // Process context for each batch item
                    let inputText = prompts[i];
                    let contextMetadata;
                    if (options.context && options.contextConfig) {
                        const processedContextResult = ContextFactory.processContext(options.context, options.contextConfig);
                        if (processedContextResult.processedContext) {
                            inputText = processedContextResult.processedContext + inputText;
                        }
                        contextMetadata = {
                            ...ContextFactory.extractAnalyticsContext(options.context),
                            contextMode: processedContextResult.config.mode,
                            contextTruncated: processedContextResult.metadata.truncated,
                            batchIndex: i,
                        };
                    }
                    const context = sessionId
                        ? { ...contextMetadata, sessionId }
                        : contextMetadata;
                    const runBatchGenerate = () => sdk.generate({
                        input: {
                            text: inputText,
                            ...(batchImages?.length && { images: batchImages }),
                            ...(batchCsvFiles?.length && { csvFiles: batchCsvFiles }),
                            ...(batchPdfFiles?.length && { pdfFiles: batchPdfFiles }),
                            ...(batchVideoFiles?.length && {
                                videoFiles: batchVideoFiles,
                            }),
                        },
                        // Only construct these when the corresponding files are
                        // actually attached — batch has no CSV/video mode to opt into,
                        // unlike generate/stream, so an empty options object here is
                        // pure noise on every prompt of every batch run.
                        ...(batchCsvFiles?.length && {
                            csvOptions: CLICommandFactory.buildCsvOptionsFromArgv(argv),
                        }),
                        ...(batchPdfFiles?.length && {
                            pdfOptions: {
                                password: batchPdfPassword,
                            },
                        }),
                        ...(batchVideoFiles?.length && {
                            videoOptions: CLICommandFactory.buildVideoOptionsFromArgv(argv),
                        }),
                        provider: enhancedOptions.provider,
                        model: enhancedOptions.model,
                        temperature: enhancedOptions.temperature,
                        maxTokens: enhancedOptions.maxTokens,
                        topP: enhancedOptions.topP,
                        topK: enhancedOptions.topK,
                        stopSequences: enhancedOptions.stopSequences,
                        systemPrompt: enhancedOptions.systemPrompt,
                        timeout: enhancedOptions.timeout
                            ? enhancedOptions.timeout * 1000
                            : undefined,
                        disableTools: enhancedOptions.disableTools,
                        enabledToolNames: enhancedOptions.enabledToolNames,
                        evaluationDomain: enhancedOptions.evaluationDomain,
                        toolUsageContext: enhancedOptions.toolUsageContext,
                        context: context,
                        factoryConfig: enhancedOptions.domain
                            ? {
                                domainType: enhancedOptions.domain,
                                enhancementType: "domain-configuration",
                                validateDomainData: true,
                            }
                            : undefined,
                    });
                    const result = await runBatchGenerate();
                    results.push({ prompt: prompts[i], response: result.content });
                    if (spinner) {
                        spinner.render();
                    }
                }
                catch (error) {
                    results.push({
                        prompt: prompts[i],
                        error: error.message,
                    });
                    if (spinner) {
                        spinner.render();
                    }
                }
                // Add delay between requests
                if (i < prompts.length - 1) {
                    await new Promise((resolve) => setTimeout(resolve, options.delay || 1000));
                }
            }
            if (spinner) {
                spinner.succeed(chalk.green("āœ… Batch processing complete!"));
            }
            // Handle output with universal formatting
            CLICommandFactory.handleOutput(results, options);
            if (!globalSession.getCurrentSessionId()) {
                await CLICommandFactory.flushLangfuseTraces();
                await CLICommandFactory.exitAfterStdoutDrain(0);
            }
        }
        catch (error) {
            if (spinner) {
                spinner.fail();
            }
            handleError(error, "Batch processing");
        }
    }
    /**
     * Execute config export command
     */
    static async executeConfigExport(argv) {
        const options = CLICommandFactory.processOptions(argv);
        try {
            const config = {
                providers: {
                    openai: !!process.env.OPENAI_API_KEY,
                    bedrock: !!(process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY),
                    vertex: !!(process.env.GOOGLE_APPLICATION_CREDENTIALS ||
                        process.env.GOOGLE_SERVICE_ACCOUNT_KEY),
                    anthropic: !!process.env.ANTHROPIC_API_KEY,
                    azure: !!(process.env.AZURE_OPENAI_API_KEY &&
                        process.env.AZURE_OPENAI_ENDPOINT),
                    "google-ai": !!process.env.GOOGLE_AI_API_KEY,
                },
                defaults: {
                    temperature: 0.7,
                    maxTokens: 500,
                },
                timestamp: new Date().toISOString(),
            };
            CLICommandFactory.handleOutput(config, options);
        }
        catch (error) {
            handleError(error, "Configuration export");
        }
    }
    /**
     * Execute get best provider command
     */
    static async executeGetBestProvider(argv) {
        const options = CLICommandFactory.processOptions(argv);
        try {
            const { getBestProvider } = await import("../../utils/providerUtils.js");
            const bestProvider = await getBestProvider();
            if (options.format === "json") {
                CLICommandFactory.handleOutput({ provider: bestProvider }, options);
            }
            else {
                if (!options.quiet) {
                    logger.always(chalk.green(`šŸŽÆ Best available provider: ${bestProvider}`));
                }
                else {
                    CLICommandFactory.handleOutput(bestProvider, options);
                }
            }
        }
        catch (error) {
            handleError(error, "Provider selection");
        }
    }
    /**
     * Execute memory stats command
     */
    /**
     * Resolve a standalone SkillsManager for the `skills` command group.
     * Directory precedence: --skills-dir > NEUROLINK_SKILLS_DIR > ./skills.
     * Cache is disabled so every command sees the directory's current state.
     */
    static resolveSkillsManagerForCli(argv) {
        const raw = argv.skillsDir;
        const dir = (typeof raw === "string" && raw.trim()) ||
            process.env.NEUROLINK_SKILLS_DIR?.trim() ||
            "./skills";
        return new SkillsManager({
            enabled: true,
            storage: { type: "filesystem", path: dir },
            indexCacheTtlMs: 0,
        });
    }
    static async executeSkillsList(argv) {
        const options = CLICommandFactory.processOptions(argv);
        try {
            const manager = CLICommandFactory.resolveSkillsManagerForCli(argv);
            const skills = await manager.list();
            if (options.format === "json") {
                CLICommandFactory.handleOutput({ skills, count: skills.length }, options);
                return;
            }
            if (skills.length === 0) {
                logger.always(chalk.yellow("No active skills found."));
                return;
            }
            logger.always(chalk.blue(`🧩 Skills (${skills.length}):`));
            for (const skill of skills) {
                const tags = skill.tags?.length ? ` [${skill.tags.join(", ")}]` : "";
                logger.always(`   ${chalk.bold(skill.name)} — ${skill.description}${tags}`);
            }
        }
        catch (error) {
            handleError(error, "Skills list");
        }
    }
    static async executeSkillsShow(argv) {
        const options = CLICommandFactory.processOptions(argv);
        try {
            const manager = CLICommandFactory.resolveSkillsManagerForCli(argv);
            const skill = await manager.get(argv.skill);
            if (!skill) {
                logger.always(chalk.yellow(`Skill "${argv.skill}" not found.`));
                process.exitCode = 1;
                return;
            }
            if (options.format === "json") {
                CLICommandFactory.handleOutput(skill, options);
                return;
            }
            logger.always(chalk.blue(`🧩 ${skill.displayName || skill.name}`));
            logger.always(`   id: ${skill.id}  version: ${skill.version ?? 1}`);
            logger.always(`   ${skill.description}`);
            if (skill.tags?.length) {
                logger.always(`   tags: ${skill.tags.join(", ")}`);
            }
            logger.always("");
            logger.always(skill.instructions);
        }
        catch (error) {
            handleError(error, "Skills show");
        }
    }
    static async executeSkillsSearch(argv) {
        const options = CLICommandFactory.processOptions(argv);
        try {
            const manager = CLICommandFactory.resolveSkillsManagerForCli(argv);
            const matches = await manager.search({
                query: argv.query,
                ...(argv.tag ? { tag: argv.tag } : {}),
            });
            if (options.format === "json") {
                CLICommandFactory.handleOutput({ skills: matches, count: matches.length }, options);
                return;
            }
            if (matches.length === 0) {
                logger.always(chalk.yellow(`No skills match "${argv.query}".`));
                return;
            }
            logger.always(chalk.blue(`šŸ”Ž ${matches.length} match(es):`));
            for (const skill of matches) {
                logger.always(`   ${chalk.bold(skill.name)} — ${skill.description}`);
            }
            logger.always("");
            logger.always("Use `neurolink skills show <name>` for full instructions.");
        }
        catch (error) {
            handleError(error, "Skills search");
        }
    }
    static async executeSkillsCreate(argv) {
        const options = CLICommandFactory.processOptions(argv);
        try {
            let instructions = argv.instructions;
            if (!instructions && argv.instructionsFile) {
                instructions = fs.readFileSync(argv.instructionsFile, "utf-8");
            }
            if (!instructions?.trim()) {
                logger.always(chalk.red("Provide skill instructions via --instructions or --instructions-file."));
                process.exitCode = 1;
                return;
            }
            const manager = CLICommandFactory.resolveSkillsManagerForCli(argv);
            const result = await manager.requestMutation({
                type: "create",
                skill: {
                    name: argv.name,
                    description: argv.description,
                    instructions,
                    ...(argv.displayName ? { displayName: argv.displayName } : {}),
                    ...(argv.tags?.length ? { tags: argv.tags.map(String) } : {}),
                },
            });
            if (options.format === "json") {
                CLICommandFactory.handleOutput(result, options);
                return;
            }
            logger.always(chalk.green(`āœ… Skill "${argv.name}" created (id: ${result.skill?.id}).`));
        }
        catch (error) {
            handleError(error, "Skills create");
        }
    }
    static async executeSkillsDelete(argv) {
        const options = CLICommandFactory.processOptions(argv);
        try {
            const manager = CLICommandFactory.resolveSkillsManagerForCli(argv);
            const existing = await manager.get(argv.skill);
            if (!existing) {
                logger.always(chalk.yellow(`Skill "${argv.skill}" not found.`));
                process.exitCode = 1;
                return;
            }
            const result = await manager.requestMutation({
                type: "delete",
                skillId: existing.id,
            });
            if (options.format === "json") {
                CLICommandFactory.handleOutput(result, options);
                return;
            }
            logger.always(chalk.green(`āœ… Skill "${existing.name}" deprecated (soft-deleted).`));
        }
        catch (error) {
            handleError(error, "Skills delete");
        }
    }
    static async executeMemoryStats(argv) {
        const options = CLICommandFactory.processOptions(argv);
        const spinner = options.quiet
            ? null
            : ora("🧠 Getting memory stats...").start();
        try {
            const sdk = globalSession.getOrCreateNeuroLink();
            // Handle dry-run mode
            if (options.dryRun) {
                const mockStats = {
                    totalSessions: 5,
                    totalTurns: 47,
                    memoryUsage: "Active",
                };
                if (spinner) {
                    spinner.succeed(chalk.green("āœ… Memory stats retrieved (dry-run)"));
                }
                CLICommandFactory.handleOutput(mockStats, options);
                return;
            }
            const stats = await sdk.getConversationStats();
            if (spinner) {
                spinner.succeed(chalk.green("āœ… Memory stats retrieved"));
            }
            if (options.format === "json") {
                CLICommandFactory.handleOutput(stats, options);
            }
            else {
                logger.always(chalk.blue("šŸ“Š Conversation Memory Stats:"));
                logger.always(`   Total Sessions: ${stats.totalSessions}`);
                logger.always(`   Total Turns: ${stats.totalTurns}`);
                logger.always(`   Memory Status: ${stats.totalSessions > 0 ? "Active" : "Empty"}`);
            }
        }
        catch (error) {
            if (spinner) {
                spinner.fail("Memory stats failed");
            }
            if (error.message.includes("not enabled")) {
                logger.always(chalk.yellow("āš ļø Conversation memory is not enabled"));
                logger.always("Enable it by using --enable-conversation-memory with loop mode");
            }
            else {
                handleError(error, "Memory stats");
            }
        }
    }
    /**
     * Execute memory history command
     */
    static async executeMemoryHistory(argv) {
        const options = CLICommandFactory.processOptions(argv);
        const spinner = options.quiet
            ? null
            : ora(`🧠 Getting history for ${argv.sessionId}...`).start();
        try {
            const sdk = globalSession.getOrCreateNeuroLink();
            // Handle dry-run mode
            if (options.dryRun) {
                const mockHistory = [
                    { role: "user", content: "Hello, how are you?" },
                    {
                        role: "assistant",
                        content: "I'm doing well, thank you! How can I help you today?",
                    },
                    { role: "user", content: "Can you explain quantum computing?" },
                    {
                        role: "assistant",
                        content: "Quantum computing is a revolutionary technology...",
                    },
                ];
                if (spinner) {
                    spinner.succeed(chalk.green(`āœ… History retrieved for ${argv.sessionId} (dry-run)`));
                }
                CLICommandFactory.handleOutput(mockHistory, options);
                return;
            }
            const history = await sdk.getConversationHistory(argv.sessionId);
            if (spinner) {
                spinner.succeed(chalk.green(`āœ… History retrieved for ${argv.sessionId}`));
            }
            if (history.length === 0) {
                logger.always(chalk.yellow(`āš ļø No conversation history found for session: ${argv.sessionId}`));
                return;
            }
            if (options.format === "json") {
                CLICommandFactory.handleOutput(history, options);
            }
            else {
                logger.always(chalk.blue(`šŸ’¬ Conversation History (${argv.sessionId}):`));
                for (const message of history) {
                    const roleColor = message.role === "user" ? chalk.cyan : chalk.green;
                    const roleLabel = message.role === "user" ? "User" : "Assistant";
                    logger.always(`   [${roleColor(roleLabel)}]: ${message.content}`);
                }
            }
        }
        catch (error) {
            if (spinner) {
                spinner.fail("Memory history failed");
            }
            if (error.message.includes("not enabled")) {
                logger.always(chalk.yellow("āš ļø Conversation memory is not enabled"));
                logger.always("Enable it by using --enable-conversation-memory with loop mode");
            }
            else {
                handleError(error, "Memory history");
            }
        }
    }
    /**
     * Execute memory clear command
     */
    static async executeMemoryClear(argv) {
        const options = CLICommandFactory.processOptions(argv);
        const isAllSessions = !argv.sessionId;
        // Require --confirm flag when clearing all sessions
        if (isAllSessions && !argv.confirm) {
            logger.always(chalk.red("āš ļø Clearing all sessions requires --confirm flag"));
            logger.always(chalk.yellow("Usage: neurolink memory clear --confirm"));
            logger.always(chalk.gray("This safeguard prevents accidental data loss."));
            return;
        }
        const target = isAllSessions ? "all sessions" : `session ${argv.sessionId}`;
        const spinner = options.quiet
            ? null
            : ora(`🧠 Clearing ${target}...`).start();
        try {
            const sdk = globalSession.getOrCreateNeuroLink();
            // Handle dry-run mode
            if (options.dryRun) {
                if (spinner) {
                    spinner.succeed(chalk.green(`āœ… ${isAllSessions ? "All sessions" : "Session"} cleared (dry-run)`));
                }
                const result = {
                    success: true,
                    action: isAllSessions ? "clear_all" : "clear_session",
                    sessionId: argv.sessionId || null,
                    message: `${isAllSessions ? "All sessions" : "Session"} would be cleared`,
                };
                CLICommandFactory.handleOutput(result, options);
                return;
            }
            let success;
            if (isAllSessions) {
                await sdk.clearAllConversations();
                success = true;
            }
            else {
                // sessionId is guaranteed to exist when isAllSessions is false
                if (!argv.sessionId) {
                    throw new Error("Session ID is required for clearing specific session");
                }
                success = await sdk.clearConversationSession(argv.sessionId);
            }
            if (spinner) {
                if (success) {
                    spinner.succeed(chalk.green(`āœ… ${isAllSessions ? "All sessions" : "Session"} cleared successfully`));
                }
                else {
                    spinner.warn(chalk.yellow(`āš ļø Session ${argv.sessionId} not found or already empty`));
                }
            }
            if (options.format === "json") {
                const result = {
                    success,
                    action: isAllSessions ? "clear_all" : "clear_session",
                    sessionId: argv.sessionId || null,
                };
                CLICommandFactory.handleOutput(result, options);
            }
            else if (!success && !isAllSessions) {
                logger.always(chalk.yellow(`āš ļø Session ${argv.sessionId} not found or already empty`));
            }
            else if (!options.quiet) {
                logger.always(chalk.green(`āœ… ${isAllSessions ? "All conversation history" : `Session ${argv.sessionId}`} cleared`));
            }
        }
        catch (error) {
            if (spinner) {
                spinner.fail("Memory clear failed");
            }
            if (error.message.includes("not enabled")) {
                logger.always(chalk.yellow("āš ļø Conversation memory is not enabled"));
                logger.always("Enable it by using --enable-conversation-memory with loop mode");
            }
            else {
                handleError(error, "Memory clear");
            }
        }
    }
    /**
     * Execute memory list command
     */
    static async executeMemoryList(argv) {
        const options = CLICommandFactory.processOptions(argv);
        const spinner = options.quiet
            ? null
            : ora("šŸ“‹ Listing conversation sessions...").start();
        try {
            const sdk = globalSession.getOrCreateNeuroLink();
            // Handle dry-run mode
            if (options.dryRun) {
                const mockSessions = [
                    {
                        id: "session-123",
                        title: "Machine Learning Discussion",
                        messageCount: 15,
                        lastActive: "2 hours ago",
                        createdAt: "2024-01-01T10:00:00Z",
                        updatedAt: "2024-01-01T12:30:00Z",
                    },
                    {
                        id: "session-456",
                        title: "API Design Review",
                        messageCount: 8,
                        lastActive: "1 day ago",
                        createdAt: "2024-01-02T14:00:00Z",
                        updatedAt: "2024-01-02T15:15:00Z",
                    },
                ];
                if (spinner) {
                    spinner.succeed(chalk.green("āœ… Sessions listed (dry-run)"));
                }
                CLICommandFactory.handleOutput(mockSessions, options);
                return;
            }
            const sessions = await sdk.listSessions(argv.userId);
            if (spinner) {
                spinner.succeed(chalk.green(`āœ… Found ${sessions.length} session(s)`));
            }
            if (sessions.length === 0) {
                logger.always(chalk.yellow("āš ļø No conversation sessions found"));
                return;
            }
            if (options.format === "json") {
                CLICommandFactory.handleOutput(sessions, options);
            }
            else {
                // Table format output
                logger.always(chalk.blue(`šŸ“‹ Conversation Sessions (${sessions.length} total):`));
                logger.always("");
                logger.always(chalk.gray("Session ID".padEnd(40) +
                    " | " +
                    "Messages".padEnd(10) +
                    " | " +
                    "Last Active"));
                logger.always(chalk.gray("-".repeat(75)));
                for (const session of sessions) {
                    const idDisplay = session.id.length > 38
                        ? session.id.substring(0, 35) + "..."
                        : session.id.padEnd(40);
                    const msgCount = String(session.messageCount).padEnd(10);
                    const lastActive = session.lastActive || "Unknown";
                    logger.always(`${idDisplay} | ${msgCount} | ${lastActive}`);
                }
            }
        }
        catch (error) {
            if (spinner) {
                spinner.fail("Session listing failed");
            }
            if (error.message.includes("not enabled")) {
                logger.always(chalk.yellow("āš ļø Conversation memory is not enabled"));
                logger.always("Enable it by using --enable-conversation-memory with loop mode");
            }
            else {
                handleError(error, "Memory list");
            }
        }
    }
    /**
     * Resolve the requested export format for memory export commands.
     * The generic `--format` flag is typed for the generate/stream commands
     * (text/json/table/yaml), so read the raw value here and narrow it to the
     * two serializations the memory export path actually supports.
     */
    static resolveMemoryExportFormat(argv) {
        const raw = typeof argv.format === "string" ? argv.format.toLowerCase() : "";
        return raw === "csv" ? "csv" : "json";
    }
    /**
     * Build a filesystem-safe filename for a session export.
     *
     * A session ID is caller-controlled data (it can come from Redis keys, user
     * input, etc.), so it must never be interpolated directly into a path — a
     * value like `../../etc/cron.d/x` would otherwise let export-all write
     * outside the chosen output directory. We reduce the ID to a bare basename
     * and allow-list its characters; callers additionally verify containment.
     */
    static sanitizeSessionExportFilename(sessionId, extension) {
        // Strip any directory components, then drop everything that isn't a safe
        // filename character and any leading dots (which could form `..`).
        const base = path.basename(String(sessionId ?? ""));
        const sanitized = base.replace(/[^a-zA-Z0-9._-]/g, "_").replace(/^\.+/, "");
        const safe = sanitized.length > 0 ? sanitized : "session";
        return `${safe}.${extension}`;
    }
    /**
     * Serialize one or more session exports to CSV. Each message becomes a row;
     * session-level fields are repeated per row so the output is a flat,
     * spreadsheet-friendly table.
     */
    static sessionExportsToCsv(exports) {
        const escape = (value) => {
            const s = value === undefined || value === null ? "" : String(value);
            return /[",\r\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
        };
        const header = [
            "sessionId",
            "title",
            "messageId",
            "role",
            "content",
            "timestamp",
            "tool",
        ];
        const rows = [header.join(",")];
        for (const exp of exports) {
            for (const msg of exp.messages) {
                rows.push([
                    escape(exp.sessionId),
                    escape(exp.title),
                    escape(msg.id),
                    escape(msg.role),
                    escape(msg.content),
                    escape(msg.timestamp ?? msg.metadata?.timestamp),
                    escape(msg.tool),
                ].join(","));
            }
        }
        return rows.join("\r\n") + "\r\n";
    }
    /**
     * Execute memory export command
     */
    static async executeMemoryExport(argv) {
        const options = CLICommandFactory.processOptions(argv);
        const exportFormat = CLICommandFactory.resolveMemoryExportFormat(argv);
        const spinner = options.quiet
            ? null
            : ora(`šŸ“¤ Exporting session ${argv.sessionId}...`).start();
        try {
            const sdk = globalSession.getOrCreateNeuroLink();
            // Handle dry-run mode
            if (options.dryRun) {
                const mockExport = {
                    sessionId: argv.sessionId,
                    title: "Sample Conversation",
                    createdAt: "2024-01-01T10:00:00Z",
                    updatedAt: "2024-01-01T12:30:00Z",
                    messages: [
                        { id: "msg-1", role: "user", content: "Hello!" },
                        { id: "msg-2", role: "assistant", content: "Hi there!" },
                    ],
                    exportMetadata: argv.includeMetadata
                        ? {
                            exportedAt: new Date().toISOString(),
                            exportFormat,
                        }
                        : undefined,
                };
                if (spinner) {
                    spinner.succeed(chalk.green(`āœ… Session exported (dry-run)`));
                }
                if (exportFormat === "csv") {
                    logger.always(CLICommandFactory.sessionExportsToCsv([mockExport]));
                }
                else {
                    CLICommandFactory.handleOutput(mockExport, {
                        ...options,
                        format: "json",
                    });
                }
                return;
            }
            const exportData = await sdk.exportSession(argv.sessionId, {
                includeMetadata: argv.includeMetadata,
                format: exportFormat,
            });
            if (!exportData) {
                if (spinner) {
                    spinner.warn(chalk.yellow(`āš ļø Session ${argv.sessionId} not found`));
                }
                return;
            }
            if (spinner) {
                spinner.succeed(chalk.green(`āœ… Session exported (${exportData.messages.length} messages)`));
            }
            // Emit in the requested format (suitable for piping to a file).
            if (exportFormat === "csv") {
                logger.always(CLICommandFactory.sessionExportsToCsv([exportData]));
            }
            else {
                CLICommandFactory.handleOutput(exportData, {
                    ...options,
                    format: "json",
                });
            }
        }
        catch (error) {
            if (spinner) {
                spinner.fail("Session export failed");
            }
            if (error.message.includes("not enabled")) {
                logger.always(chalk.yellow("āš ļø Conversation memory is not enabled"));
                logger.always("Enable it by using --enable-conversation-memory with loop mode");
            }
            else {
                handleError(error, "Memory export");
            }
        }
    }
    /**
     * Execute memory export-all command
     */
    static async executeMemoryExportAll(argv) {
        const options = CLICommandFactory.processOptions(argv);
        const exportFormat = CLICommandFactory.resolveMemoryExportFormat(argv);
        const spinner = options.quiet
            ? null
            : ora("šŸ“¤ Exporting all sessions...").start();
        try {
            const sdk = globalSession.getOrCreateNeuroLink();
            // Handle dry-run mode
            if (options.dryRun) {
                if (spinner) {
                    spinner.succeed(chalk.green(`āœ… Sessions would be exported to ${argv.output} (dry-run)`));
                }
                const result = {
                    success: true,
                    outputDirectory: argv.output,
                    sessionCount: 2,
                    message: "Dry-run: sessions would be exported",
                };
                CLICommandFactory.handleOutput(result, options);
                return;
            }
            // Ensure output directory exists
            if (!fs.existsSync(argv.output)) {
                fs.mkdirSync(argv.output, { recursive: true });
            }
            const exports = await sdk.exportAllSessions(argv.userId, {
                includeMetadata: argv.includeMetadata,
                format: exportFormat,
            });
            if (exports.length === 0) {
                if (spinner) {
                    spinner.warn(chalk.yellow("āš ļø No sessions found to export"));
                }
                return;
            }
            // Write each session to a file. The session ID is untrusted input, so it
            // is sanitized to a bare filename and the resolved path is verified to
            // stay inside the output directory before writing (path-traversal guard).
            const resolvedOutput = path.resolve(argv.output);
            let writtenCount = 0;
            for (const sessionExport of exports) {
                const filename = CLICommandFactory.sanitizeSessionExportFilename(sessionExport.sessionId, exportFormat);
                const filepath = path.resolve(resolvedOutput, filename);
                // Defense-in-depth: refuse to write anything that escaped the dir.
                if (path.dirname(filepath) !== resolvedOutput) {
                    logger.always(chalk.yellow(`āš ļø Skipping session with unsafe id: ${sessionExport.sessionId}`));
                    continue;
                }
                const contents = exportFormat === "csv"
                    ? CLICommandFactory.sessionExportsToCsv([sessionExport])
                    : JSON.stringify(sessionExport, null, 2);
                fs.writeFileSync(filepath, contents);
                writtenCount++;
            }
            if (spinner) {
                spinner.succeed(chalk.green(`āœ… Exported ${writtenCount} session(s) to ${argv.output}`));
            }
            if (options.format === "json") {
                const result = {
                    success: true,
                    outputDirectory: argv.output,
                    format: exportFormat,
                    sessionCount: writtenCount,
                    sessions: exports.map((e) => ({
                        sessionId: e.sessionId,
                        messageCount: e.messages.length,
                    })),
                };
                CLICommandFactory.handleOutput(result, options);
            }
            else {
                logger.always(chalk.blue(`šŸ“ Exported sessions to: ${argv.output}`));
                for (const sessionExport of exports) {
                    logger.always(`   • ${sessionExport.sessionId} (${sessionExport.messages.length} messages)`);
                }
            }
        }
        catch (error) {
            if (spinner) {
                spinner.fail("Export all failed");
            }
            if (error.message.includes("not enabled")) {
                logger.always(chalk.yellow("āš ļø Conversation memory is not enabled"));
                logger.always("Enable it by using --enable-conversation-memory with loop mode");
            }
            else {
                handleError(error, "Memory export-all");
            }
        }
    }
    /**
     * Execute memory delete command
     */
    static async executeMemoryDelete(argv) {
        const options = CLICommandFactory.processOptions(argv);
        // Require confirmation unless --force is explicitly provided
        if (!argv.force) {
            // In quiet mode, we can't prompt interactively, so require --force
            if (options.quiet) {
                logger.always(chalk.red("āš ļø Deleting a session in quiet mode requires --force flag"));
                logger.always(chalk.yellow(`Usage: neurolink memory delete ${argv.sessionId} --force`));
                return;
            }
            // The SDK's MCP stdio transport unrefs stdin on import so an idle
            // process can exit on its own (see externalServerManager.ts). This
            // prompt actually reads stdin and must block on it, so ref it back
            // before consuming.
            ensureStdinRef();
            const readline = await import("readline");
            const rl = readline.createInterface({
                input: process.stdin,
                output: process.stdout,
            });
            const answer = await new Promise((resolve) => {
                rl.question(chalk.yellow(`āš ļø Are you sure you want to delete session "${argv.sessionId}"? (y/N): `), resolve);
            });
            rl.close();
            if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
                logger.always(chalk.gray("Deletion cancelled."));
                return;
            }
        }
        const spinner = options.quiet
            ? null
            : ora(`šŸ—‘ļø Deleting session ${argv.sessionId}...`).start();
        try {
            const sdk = globalSession.getOrCreateNeuroLink();
            // Handle dry-run mode
            if (options.dryRun) {
                if (spinner) {
                    spinner.succeed(chalk.green(`āœ… Session ${argv.sessionId} deleted (dry-run)`));
                }
                const result = {
                    success: true,
                    action: "delete",
                    sessionId: argv.sessionId,
                    message: "Session would be deleted",
                };
                CLICommandFactory.handleOutput(result, options);
                return;
            }
            const success = await sdk.clearConversationSession(argv.sessionId);
            if (spinner) {
                if (success) {
                    spinner.succeed(chalk.green(`āœ… Session ${argv.sessionId} deleted successfully`));
                }
                else {
                    spinner.warn(chalk.yellow(`āš ļø Session ${argv.sessionId} not found or already deleted`));
                }
            }
            if (options.format === "json") {
                const result = {
                    success,
                    action: "delete",
                    sessionId: argv.sessionId,
                };
                CLICommandFactory.handleOutput(result, options);
            }
        }
        catch (error) {
            if (spinner) {
                spinner.fail("Session deletion failed");
            }
            if (error.message.includes("not enabled")) {
                logger.always(chalk.yellow("āš ļø Conversation memory is not enabled"));
                logger.always("Enable it by using --enable-conversation-memory with loop mode");
            }
            else {
                handleError(error, "Memory delete");
            }
        }
    }
    /**
     * Execute completion command
     */
    static async executeCompletion(argv) {
        try {
            // Generate shell completion script as concatenated strings to avoid template literal issues
            const completionScript = "#!/usr/bin/env bash\n\n" +
                "# NeuroLink CLI Bash Completion Script\n" +
                "# Generated by: neurolink completion\n" +
                "# \n" +
                "# Installation instructions:\n" +
                "#   1. Save this script to a file (e.g., ~/.neurolink-completion.sh)\n" +
                "#   2. Add to your shell profile: source ~/.neurolink-completion.sh\n" +
                "#   3. Restart your shell or run: source ~/.bashrc\n\n" +
                "_neurolink_completion() {\n" +
                "    local cur prev opts base\n" +
                "    COMPREPLY=()\n" +
                '    cur="${COMP_WORDS[COMP_CWORD]}"\n' +
                '    prev="${COMP_WORDS[COMP_CWORD - 1]}"\n\n' +
                "    # Main commands\n" +
                "    if [[ ${COMP_CWORD} -eq 1 ]]; then\n" +
                '        opts="generate gen stream batch provider status models mcp discover memory config get-best-provider completion"\n' +
                '        COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )\n' +
                "        return 0\n" +
                "    fi\n\n" +
                "    # Subcommand completion\n" +
                '    case "${COMP_WORDS[1]}" in\n' +
                "        generate|gen)\n" +
                '            case "${prev}" in\n' +
                "                --provider|-p)\n" +
                '                    COMPREPLY=( $(compgen -W "' +
                BASH_COMPLETION_PROVIDERS +
                '" -- ${cur}) )\n' +
                "                    return 0\n" +
                "                    ;;\n" +
                "                --format|-f|--output-format)\n" +
                '                    COMPREPLY=( $(compgen -W "text json table" -- ${cur}) )\n' +
                "                    return 0\n" +
                "                    ;;\n" +
                "                --model|-m)\n" +
                '                    COMPREPLY=( $(compgen -W "gemini-2.5-pro gemini-2.5-flash gpt-4o gpt-4o-mini claude-3-5-sonnet" -- ${cur}) )\n' +
                "                    return 0\n" +
                "                    ;;\n" +
                "                *)\n" +
                '                    opts="--provider --model --temperature --maxTokens --system --format --output --timeout --delay --disableTools --enableAnalytics --enableEvaluation --debug --quiet --noColor --configFile --dryRun"\n' +
                '                    COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )\n' +
                "                    return 0\n" +
                "                    ;;\n" +
                "            esac\n" +
                "            ;;\n" +
                "        mcp)\n" +
                '            case "${COMP_WORDS[2]}" in\n' +
                "                install)\n" +
                "                    if [[ ${COMP_CWORD} -eq 3 ]]; then\n" +
                '                        COMPREPLY=( $(compgen -W "filesystem github postgres sqlite brave puppeteer git memory bitbucket" -- ${cur}) )\n' +
                "                        return 0\n" +
                "                    fi\n" +
                "                    ;;\n" +
                "                *)\n" +
                "                    if [[ ${COMP_CWORD} -eq 2 ]]; then\n" +
                '                        opts="list install add test exec remove"\n' +
                '                        COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )\n' +
                "                        return 0\n" +
                "                    fi\n" +
                "                    ;;\n" +
                "            esac\n" +
                "            ;;\n" +
                "        provider)\n" +
                "            if [[ ${COMP_CWORD} -eq 2 ]]; then\n" +
                '                COMPREPLY=( $(compgen -W "status" -- ${cur}) )\n' +
                "                return 0\n" +
                "            fi\n" +
                "            ;;\n" +
                "        models)\n" +
                "            if [[ ${COMP_CWORD} -eq 2 ]]; then\n" +
                '                COMPREPLY=( $(compgen -W "list test" -- ${cur}) )\n' +
                "                return 0\n" +
                "            fi\n" +
                "            ;;\n" +
                "        config)\n" +
                "            if [[ ${COMP_CWORD} -eq 2 ]]; then\n" +
                '                COMPREPLY=( $(compgen -W "init show validate reset export" -- ${cur}) )\n' +
                "                return 0\n" +
                "            fi\n" +
                "            ;;\n" +
                "        memory)\n" +
                "            if [[ ${COMP_CWORD} -eq 2 ]]; then\n" +
                '                COMPREPLY=( $(compgen -W "stats history clear" -- ${cur}) )\n' +
                "                return 0\n" +
                "            fi\n" +
                "            ;;\n" +
                "        *)\n" +
                "            # Global options for all commands\n" +
                '            opts="--help --version --debug --quiet --noColor --configFile"\n' +
                '            COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )\n' +
                "            return 0\n" +
                "            ;;\n" +
                "    esac\n\n" +
                "    # File completion for certain options\n" +
                '    case "${prev}" in\n' +
                "        --output|-o|--configFile)\n" +
                "            COMPREPLY=( $(compgen -f -- ${cur}) )\n" +
                "            return 0\n" +
                "            ;;\n" +
                "        batch)\n" +
                "            COMPREPLY=( $(compgen -f -- ${cur}) )\n" +
                "            return 0\n" +
                "            ;;\n" +
                "    esac\n\n" +
                "    return 0\n" +
                "}\n\n" +
                "# Register the completion function\n" +
                "complete -F _neurolink_completion neurolink\n\n" +
                "# Zsh completion (if running zsh)\n" +
                'if [[ -n "${ZSH_VERSION}" ]]; then\n' +
                "    autoload -U +X bashcompinit && bashcompinit\n" +
                "    complete -F _neurolink_completion neurolink\n" +
                "fi\n\n" +
                'echo "NeuroLink CLI completion script loaded successfully!"\n' +
                'echo "Available commands: generate, stream, batch, provider, status, models, mcp, discover, config, get-best-provider, completion"';
            // Handle output options
            if (argv.output) {
                const fs = await import("fs");
                fs.writeFileSync(argv.output, completionScript);
                if (!argv.quiet) {
                    logger.always(`āœ… Completion script saved to ${argv.output}`);
                    logger.always(`šŸ’” Run: source ${argv.output}`);
                }
            }
            else {
                logger.always(completionScript);
            }
            if (!argv.quiet) {
                logger.always(chalk.blue("\nšŸ“‹ Installation Instructions:"));
                logger.always("1. Save the output to a file:");
                logger.always("   neurolink completion > ~/.neurolink-completion.sh");
                logger.always("2. Add to your shell profile:");
                logger.always("   echo 'source ~/.neurolink-completion.sh' >> ~/.bashrc");
                logger.always("3. Restart your shell or run:");
                logger.always("   source ~/.bashrc");
                logger.always(chalk.green("\nšŸŽ‰ Then enjoy tab completion for NeuroLink commands!"));
            }
        }
        catch (error) {
            handleError(error, "Completion generation");
        }
    }
    /**
     * Flush Langfuse traces before exit
     */
    static async flushLangfuseTraces() {
        try {
            logger.debug("[CLI] Flushing Langfuse traces before exit...");
            const { flushOpenTelemetry } = await import("../../services/server/ai/observability/instrumentation.js");
            await flushOpenTelemetry();
            logger.debug("[CLI] Langfuse traces flushed successfully");
        }
        catch (error) {
            logger.error("[CLI] Error flushing Langfuse traces", { error });
        }
    }
    /** process.exit discards piped stdout queued past the 64 KiB buffer; resolves false if the reader stalls past the bound. */
    static flushStdout() {
        return new Promise((resolve) => {
            const timer = setTimeout(() => resolve(false), resolveStdoutDrainTimeoutMs());
            timer.unref();
            process.stdout.write("", () => {
                clearTimeout(timer);
                resolve(true);
            });
        });
    }
    /** Forced exit that fails loudly when queued output could not be delivered. */
    static async exitAfterStdoutDrain(exitCode) {
        if (await CLICommandFactory.flushStdout()) {
            process.exit(exitCode);
        }
        process.stderr.write(STDOUT_DRAIN_WARNING);
        process.exit(STDOUT_DRAIN_FAILED_EXIT_CODE);
    }
}
/** Re-export of CLICommandFactory's static option definitions for direct import by tests/tooling. */
export const commonOptions = CLICommandFactory.commonOptions;
/**
 * File extension for generated image bytes. The provider now labels what it
 * returned, so a WebP no longer gets written to a `.png` filename — which is
 * the same wrong-label problem one layer up from the provider.
 */
function imageExtensionFor(mimeType, base64) {
    const known = {
        "image/png": "png",
        "image/jpeg": "jpg",
        "image/webp": "webp",
        "image/gif": "gif",
    };
    if (mimeType && known[mimeType]) {
        return known[mimeType];
    }
    // No label from the provider: sniff the leading bytes rather than assume.
    const head = Buffer.from(base64.slice(0, 64), "base64");
    const sniffed = sniffImageMimeType(head);
    return sniffed ? known[sniffed] : "png";
}
//# sourceMappingURL=commandFactory.js.map