/**
 * CLI Executor Tool - Unified execution for external CLI tools
 * Supports Gemini, Qwen, and Codex with streaming output
 */
import { z } from 'zod';
import { spawn } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { validatePath } from '../utils/path-resolver.js';
import { escapeWindowsArg } from '../utils/shell-escape.js';
import { buildCommand, checkToolAvailability, clearToolCache, debugLog, errorLog } from './cli-executor-utils.js';
import { createOutputParser, flattenOutputUnits } from './cli-output-converter.js';
import { buildMergedPrompt, buildMultiTurnPrompt, mergeConversations } from './cli-prompt-builder.js';
import { convertToConversation, ensureHistoryDir, getExecutionDetail, getExecutionHistory, getSqliteStore, loadConversation, saveConversation } from './cli-executor-state.js';
// Track current running child process for cleanup on interruption
let currentChildProcess = null;
let killTimeout = null;
let killTimeoutProcess = null;
/**
 * Kill the current running CLI child process
 * Called when parent process receives SIGINT/SIGTERM
 */
export function killCurrentCliProcess() {
    const child = currentChildProcess;
    if (!child || child.killed)
        return false;
    debugLog('KILL', 'Killing current child process', { pid: child.pid });
    try {
        child.kill('SIGTERM');
    }
    catch {
        // Ignore kill errors (process may already be gone)
    }
    if (killTimeout) {
        clearTimeout(killTimeout);
        killTimeout = null;
        killTimeoutProcess = null;
    }
    // Force kill after 2 seconds if still running.
    killTimeoutProcess = child;
    killTimeout = setTimeout(() => {
        const target = killTimeoutProcess;
        if (!target || target !== currentChildProcess)
            return;
        if (target.killed)
            return;
        try {
            target.kill('SIGKILL');
        }
        catch {
            // Ignore kill errors (process may already be gone)
        }
    }, 2000);
    return true;
}
// LiteLLM integration
import { executeLiteLLMEndpoint } from './litellm-executor.js';
import { findEndpointById } from '../config/litellm-api-config-manager.js';
// CLI Settings (CLI封装) integration
import { getSettingsFilePath, findEndpoint } from '../config/cli-settings-manager.js';
import { loadClaudeCliTools, getToolConfig } from './claude-cli-tools.js';
/**
 * Parse .env file content into key-value pairs
 * Supports: KEY=value, KEY="value", KEY='value', comments (#), empty lines
 */
function parseEnvFile(content) {
    const env = {};
    const lines = content.split(/\r?\n/);
    for (const line of lines) {
        // Skip empty lines and comments
        const trimmed = line.trim();
        if (!trimmed || trimmed.startsWith('#'))
            continue;
        // Find first = sign
        const eqIndex = trimmed.indexOf('=');
        if (eqIndex === -1)
            continue;
        const key = trimmed.substring(0, eqIndex).trim();
        let value = trimmed.substring(eqIndex + 1).trim();
        // Remove surrounding quotes if present
        if ((value.startsWith('"') && value.endsWith('"')) ||
            (value.startsWith("'") && value.endsWith("'"))) {
            value = value.slice(1, -1);
        }
        if (key) {
            env[key] = value;
        }
    }
    return env;
}
/**
 * Load environment variables from .env file
 * Supports ~ for home directory expansion
 */
function loadEnvFile(envFilePath) {
    try {
        // Expand ~ to home directory
        let resolvedPath = envFilePath;
        if (resolvedPath.startsWith('~')) {
            resolvedPath = path.join(os.homedir(), resolvedPath.slice(1));
        }
        // Resolve relative paths
        if (!path.isAbsolute(resolvedPath)) {
            resolvedPath = path.resolve(resolvedPath);
        }
        if (!fs.existsSync(resolvedPath)) {
            debugLog('ENV_FILE', `Env file not found: ${resolvedPath}`);
            return {};
        }
        const content = fs.readFileSync(resolvedPath, 'utf-8');
        const envVars = parseEnvFile(content);
        debugLog('ENV_FILE', `Loaded ${Object.keys(envVars).length} env vars from ${resolvedPath}`);
        return envVars;
    }
    catch (err) {
        errorLog('ENV_FILE', `Failed to load env file: ${envFilePath}`, err);
        return {};
    }
}
async function executeClaudeWithSettings(params) {
    const { prompt, settingsPath, endpointId, mode, workingDir, cd, includeDirs, customId, onOutput } = params;
    const startTime = Date.now();
    const conversationId = customId || `${Date.now()}-${endpointId}`;
    // Build claude command with --settings flag
    const args = [
        '--settings', settingsPath,
        '--print' // Non-interactive mode
    ];
    // Add mode-specific flags
    if (mode === 'write') {
        args.push('--dangerously-skip-permissions');
    }
    // Add working directory if specified
    if (cd) {
        args.push('--cd', cd);
    }
    // Add include directories
    if (includeDirs && includeDirs.length > 0) {
        for (const dir of includeDirs) {
            args.push('--add-dir', dir);
        }
    }
    // Add prompt as argument
    args.push('-p', prompt);
    debugLog('CLAUDE_SETTINGS', `Executing claude with settings`, {
        settingsPath,
        endpointId,
        mode,
        workingDir,
        args
    });
    return new Promise((resolve, reject) => {
        const isWindows = process.platform === 'win32';
        const command = 'claude';
        const commandToSpawn = isWindows ? escapeWindowsArg(command) : command;
        const argsToSpawn = isWindows ? args.map(escapeWindowsArg) : args;
        const child = spawn(commandToSpawn, argsToSpawn, {
            cwd: workingDir,
            shell: isWindows,
            stdio: ['ignore', 'pipe', 'pipe']
        });
        // Track current child process for cleanup
        currentChildProcess = child;
        let stdout = '';
        let stderr = '';
        const outputUnits = [];
        child.stdout.on('data', (data) => {
            const text = data.toString();
            stdout += text;
            const unit = {
                type: 'stdout',
                content: text,
                timestamp: new Date().toISOString()
            };
            outputUnits.push(unit);
            if (onOutput) {
                onOutput(unit);
            }
        });
        child.stderr.on('data', (data) => {
            const text = data.toString();
            stderr += text;
            const unit = {
                type: 'stderr',
                content: text,
                timestamp: new Date().toISOString()
            };
            outputUnits.push(unit);
            if (onOutput) {
                onOutput(unit);
            }
        });
        child.on('close', (code) => {
            currentChildProcess = null;
            const endTime = Date.now();
            const duration = endTime - startTime;
            // Determine status
            let status = 'success';
            if (code !== 0) {
                const hasValidOutput = stdout.trim().length > 0;
                const hasFatalError = stderr.includes('FATAL') ||
                    stderr.includes('Authentication failed') ||
                    stderr.includes('API key');
                if (hasValidOutput && !hasFatalError) {
                    status = 'success';
                }
                else {
                    status = 'error';
                }
            }
            const execution = {
                id: conversationId,
                timestamp: new Date(startTime).toISOString(),
                tool: 'claude',
                model: endpointId, // Use endpoint ID as model identifier
                mode,
                prompt,
                status,
                exit_code: code,
                duration_ms: duration,
                output: {
                    stdout: stdout.substring(0, 10240),
                    stderr: stderr.substring(0, 2048),
                    truncated: stdout.length > 10240 || stderr.length > 2048
                }
            };
            const conversation = convertToConversation(execution);
            // Save to history
            try {
                saveConversation(workingDir, conversation);
            }
            catch (err) {
                console.error('[CLI Executor] Failed to save CLI封装 history:', err.message);
            }
            resolve({
                success: status === 'success',
                execution,
                conversation,
                stdout,
                stderr
            });
        });
        child.on('error', (error) => {
            currentChildProcess = null;
            reject(new Error(`Failed to spawn claude: ${error.message}`));
        });
    });
}
// Native resume support
import { trackNewSession, supportsNativeResume } from './native-session-discovery.js';
import { determineResumeStrategy, buildContextPrefix, getResumeModeDescription } from './resume-strategy.js';
import { isToolEnabled as isToolEnabledFromConfig, enableTool as enableToolFromConfig, disableTool as disableToolFromConfig, getPrimaryModel } from './cli-config-manager.js';
// Built-in CLI tools
const BUILTIN_CLI_TOOLS = ['gemini', 'qwen', 'codex', 'opencode', 'claude'];
// Define Zod schema for validation
// tool accepts built-in tools or custom endpoint IDs (CLI封装)
const ParamsSchema = z.object({
    tool: z.string().min(1, 'Tool is required'), // Accept any tool ID (built-in or custom endpoint)
    prompt: z.string(), // Prompt can be empty for review mode with target flags
    mode: z.enum(['analysis', 'write', 'auto', 'review']).default('analysis'),
    format: z.enum(['plain', 'yaml', 'json']).default('plain'), // Multi-turn prompt concatenation format
    model: z.string().optional(),
    cd: z.string().optional(),
    includeDirs: z.string().optional(),
    // timeout removed - controlled by external caller (bash timeout)
    resume: z.union([z.boolean(), z.string()]).optional(), // true = last, string = single ID or comma-separated IDs
    id: z.string().optional(), // Custom execution ID (e.g., IMPL-001-step1)
    noNative: z.boolean().optional(), // Force prompt concatenation instead of native resume
    category: z.enum(['user', 'internal', 'insight']).default('user'), // Execution category for tracking
    parentExecutionId: z.string().optional(), // Parent execution ID for fork/retry scenarios
    stream: z.boolean().default(false), // false = cache full output (default), true = stream output via callback
    outputFormat: z.enum(['text', 'json-lines']).optional().default('json-lines'), // Output parsing format (default: json-lines for type badges)
    // Codex review options
    uncommitted: z.boolean().optional(), // Review uncommitted changes (default for review mode)
    base: z.string().optional(), // Review changes against base branch
    commit: z.string().optional(), // Review changes from specific commit
    title: z.string().optional(), // Optional title for review summary
    // Rules env vars (PROTO, TMPL) - will be passed to subprocess environment
    rulesEnv: z.object({
        PROTO: z.string().optional(),
        TMPL: z.string().optional(),
    }).optional(),
});
function assertNonEmptyArray(items, message) {
    if (items.length === 0) {
        throw new Error(message);
    }
}
/**
 * Execute CLI tool with streaming output
 */
async function executeCliTool(params, onOutput) {
    const parsed = ParamsSchema.safeParse(params);
    if (!parsed.success) {
        throw new Error(`Invalid params: ${parsed.error.message}`);
    }
    const { tool, prompt, mode, format, model, cd, includeDirs, resume, id: customId, noNative, category, parentExecutionId, outputFormat, uncommitted, base, commit, title, rulesEnv } = parsed.data;
    // Validate and determine working directory early (needed for conversation lookup)
    let workingDir;
    if (cd) {
        const validation = validatePath(cd, { mustExist: true });
        if (!validation.valid) {
            throw new Error(`Invalid working directory (--cd): ${validation.error}. Path: ${cd}`);
        }
        workingDir = validation.path;
    }
    else {
        workingDir = process.cwd();
    }
    ensureHistoryDir(workingDir); // Ensure history directory exists
    // NEW: Check if model is a custom LiteLLM endpoint ID
    if (model) {
        const endpoint = findEndpointById(workingDir, model);
        if (endpoint) {
            // Route to LiteLLM executor
            if (onOutput) {
                onOutput({
                    type: 'stderr',
                    content: `[Routing to LiteLLM endpoint: ${model}]\n`,
                    timestamp: new Date().toISOString()
                });
            }
            const result = await executeLiteLLMEndpoint({
                prompt,
                endpointId: model,
                baseDir: workingDir,
                cwd: cd,
                includeDirs: includeDirs ? includeDirs.split(',').map(d => d.trim()) : undefined,
                enableCache: true,
                onOutput: onOutput || undefined,
            });
            // Convert LiteLLM result to ExecutionOutput format
            const startTime = Date.now();
            const endTime = Date.now();
            const duration = endTime - startTime;
            const execution = {
                id: customId || `${Date.now()}-litellm`,
                timestamp: new Date(startTime).toISOString(),
                tool: 'litellm',
                model: result.model,
                mode,
                prompt,
                status: result.success ? 'success' : 'error',
                exit_code: result.success ? 0 : 1,
                duration_ms: duration,
                output: {
                    stdout: result.output,
                    stderr: result.error || '',
                    truncated: false,
                },
            };
            const conversation = convertToConversation(execution);
            // Try to save to history
            try {
                saveConversation(workingDir, conversation);
            }
            catch (err) {
                console.error('[CLI Executor] Failed to save LiteLLM history:', err.message);
            }
            return {
                success: result.success,
                execution,
                conversation,
                stdout: result.output,
                stderr: result.error || '',
            };
        }
    }
    // Check if tool is a custom CLI封装 endpoint (not a built-in tool)
    const isBuiltinTool = BUILTIN_CLI_TOOLS.includes(tool);
    if (!isBuiltinTool) {
        // Check if it's a CLI封装 endpoint (by ID or name)
        const cliSettings = findEndpoint(tool);
        if (cliSettings && cliSettings.enabled) {
            // Route to Claude CLI with --settings flag
            const settingsPath = getSettingsFilePath(cliSettings.id);
            const displayName = cliSettings.name !== cliSettings.id ? `${cliSettings.name} (${cliSettings.id})` : cliSettings.id;
            if (onOutput) {
                onOutput({
                    type: 'stderr',
                    content: `[Routing to CLI封装 endpoint: ${displayName} via claude --settings]\n`,
                    timestamp: new Date().toISOString()
                });
            }
            // Execute claude CLI with settings file
            const result = await executeClaudeWithSettings({
                prompt,
                settingsPath,
                endpointId: cliSettings.id,
                mode,
                workingDir,
                cd,
                includeDirs: includeDirs ? includeDirs.split(',').map(d => d.trim()) : undefined,
                customId,
                onOutput: onOutput || undefined
            });
            return result;
        }
        // Check cli-tools.json for CLI wrapper tools or API endpoints
        const cliToolsConfig = loadClaudeCliTools(workingDir);
        // First check if tool is a cli-wrapper in tools section
        const cliWrapperTool = Object.entries(cliToolsConfig.tools).find(([name, t]) => name.toLowerCase() === tool.toLowerCase() && t.type === 'cli-wrapper' && t.enabled);
        if (cliWrapperTool) {
            const [toolName] = cliWrapperTool;
            // Check if there's a corresponding CLI封装 settings file
            const cliSettingsForTool = findEndpoint(toolName);
            if (cliSettingsForTool) {
                const settingsPath = getSettingsFilePath(cliSettingsForTool.id);
                if (onOutput) {
                    onOutput({
                        type: 'stderr',
                        content: `[Routing to CLI wrapper tool: ${toolName} via claude --settings]\n`,
                        timestamp: new Date().toISOString()
                    });
                }
                const result = await executeClaudeWithSettings({
                    prompt,
                    settingsPath,
                    endpointId: cliSettingsForTool.id,
                    mode,
                    workingDir,
                    cd,
                    includeDirs: includeDirs ? includeDirs.split(',').map(d => d.trim()) : undefined,
                    customId,
                    onOutput: onOutput || undefined
                });
                return result;
            }
        }
        // Check tools with type: 'api-endpoint' -> route to LiteLLM
        const apiEndpointTool = Object.entries(cliToolsConfig.tools).find(([name, t]) => t.type === 'api-endpoint' && t.enabled &&
            (t.id === tool || name === tool || name.toLowerCase() === tool.toLowerCase()));
        if (apiEndpointTool) {
            const [toolName, toolConfig] = apiEndpointTool;
            // id field is the LiteLLM endpoint ID (e.g., "g25")
            const litellmEndpointId = toolConfig.id || toolName;
            // Find LiteLLM endpoint configuration
            const litellmEndpoint = findEndpointById(workingDir, litellmEndpointId);
            if (litellmEndpoint) {
                if (onOutput) {
                    onOutput({
                        type: 'stderr',
                        content: `[Routing to LiteLLM API endpoint: ${toolName} (${litellmEndpointId})]\n`,
                        timestamp: new Date().toISOString()
                    });
                }
                // Execute via LiteLLM
                const result = await executeLiteLLMEndpoint({
                    prompt,
                    endpointId: litellmEndpointId,
                    baseDir: workingDir,
                    cwd: cd || workingDir,
                    includeDirs: includeDirs ? includeDirs.split(',').map(d => d.trim()) : undefined,
                    onOutput: onOutput || undefined,
                });
                // Convert LiteLLM result to ExecutionOutput format
                const startTime = Date.now();
                const endTime = Date.now();
                const duration = endTime - startTime;
                const execution = {
                    id: customId || `${Date.now()}-litellm`,
                    timestamp: new Date(startTime).toISOString(),
                    tool: toolName,
                    model: litellmEndpoint.model,
                    mode,
                    prompt,
                    status: result.success ? 'success' : 'error',
                    exit_code: result.success ? 0 : 1,
                    duration_ms: duration,
                    output: {
                        stdout: result.output,
                        stderr: result.error || '',
                        truncated: false
                    }
                };
                const conversation = convertToConversation(execution);
                // Try to save to history
                try {
                    saveConversation(workingDir, conversation);
                }
                catch (err) {
                    console.error('[CLI Executor] Failed to save LiteLLM history:', err.message);
                }
                return {
                    success: result.success,
                    execution,
                    conversation,
                    stdout: result.output,
                    stderr: result.error || '',
                };
            }
        }
        // Tool not found
        throw new Error(`Unknown tool: ${tool}. Use one of: ${BUILTIN_CLI_TOOLS.join(', ')} or a registered CLI封装 endpoint name.`);
    }
    // Get SQLite store for native session lookup
    const store = await getSqliteStore(workingDir);
    // Determine conversation ID and load existing conversation
    // Logic:
    // - If --resume <id1,id2,...> (multiple IDs): merge conversations
    //   - With --id: create new merged conversation
    //   - Without --id: append to ALL source conversations
    // - If --resume <id> AND --id <newId>: fork - read context from resume ID, create new conversation with newId
    // - If --id provided (no resume): use that ID (create new or append)
    // - If --resume <id> without --id: use resume ID (append to existing)
    // - No params: create new with auto-generated ID
    let conversationId;
    let existingConversation = null;
    let contextConversation = null; // For fork scenario
    let mergeResult = null; // For merge scenario
    let sourceConversations = []; // All source conversations for merge
    // Parse resume IDs (can be comma-separated for merge)
    const resumeIds = resume
        ? (typeof resume === 'string' ? resume.split(',').map(id => id.trim()).filter(Boolean) : [])
        : [];
    const isMerge = resumeIds.length > 1;
    const resumeId = resumeIds.length === 1 ? resumeIds[0] : null;
    if (isMerge) {
        // Merge scenario: multiple resume IDs
        sourceConversations = resumeIds
            .map(id => loadConversation(workingDir, id))
            .filter((c) => c !== null);
        // Guard against empty merge sources before accessing sourceConversations[0].
        assertNonEmptyArray(sourceConversations, `No valid conversations found for merge: ${resumeIds.join(', ')}`);
        mergeResult = mergeConversations(sourceConversations);
        debugLog('MERGE', 'Merged conversations', {
            sourceConversationCount: sourceConversations.length,
            resumeIds
        });
        if (customId) {
            // Create new merged conversation with custom ID
            conversationId = customId;
            existingConversation = loadConversation(workingDir, customId);
        }
        else {
            // Will append to ALL source conversations (handled in save logic)
            // Use first source conversation ID as primary
            conversationId = sourceConversations[0].id;
            existingConversation = sourceConversations[0];
        }
    }
    else if (customId && resumeId) {
        // Fork: read context from resume ID, but create new conversation with custom ID
        conversationId = customId;
        contextConversation = loadConversation(workingDir, resumeId);
        existingConversation = loadConversation(workingDir, customId);
    }
    else if (customId) {
        // Use custom ID - may be new or existing
        conversationId = customId;
        existingConversation = loadConversation(workingDir, customId);
    }
    else if (resumeId) {
        // Resume single ID without new ID - append to existing conversation
        conversationId = resumeId;
        existingConversation = loadConversation(workingDir, resumeId);
    }
    else if (resume) {
        // resume=true: get last conversation for this tool
        const history = getExecutionHistory(workingDir, { limit: 1, tool });
        if (history.executions.length > 0) {
            conversationId = history.executions[0].id;
            existingConversation = loadConversation(workingDir, conversationId);
        }
        else {
            // No previous conversation, create new
            conversationId = `${Date.now()}-${tool}`;
        }
    }
    else {
        // New conversation with auto-generated ID
        conversationId = `${Date.now()}-${tool}`;
    }
    // Determine resume strategy (native vs prompt-concat vs hybrid)
    let resumeDecision = null;
    let nativeResumeConfig;
    // resume=true (latest) - use native latest if supported
    if (resume === true && !noNative && supportsNativeResume(tool)) {
        resumeDecision = {
            strategy: 'native',
            isLatest: true,
            primaryConversationId: conversationId
        };
    }
    // Use strategy engine for complex scenarios
    else if (resumeIds.length > 0 && !noNative) {
        resumeDecision = determineResumeStrategy({
            tool,
            resumeIds,
            customId,
            // Force prompt-concat if noNative flag is set OR if tool doesn't support native resume
            // (e.g., codex resume requires TTY which spawn() doesn't provide)
            forcePromptConcat: noNative || !supportsNativeResume(tool),
            getNativeSessionId: (ccwId) => store.getNativeSessionId(ccwId),
            getConversation: (ccwId) => loadConversation(workingDir, ccwId),
            getConversationTool: (ccwId) => {
                const conv = loadConversation(workingDir, ccwId);
                return conv?.tool || null;
            }
        });
    }
    // Configure native resume if strategy decided to use it
    if (resumeDecision && (resumeDecision.strategy === 'native' || resumeDecision.strategy === 'hybrid')) {
        nativeResumeConfig = {
            enabled: true,
            sessionId: resumeDecision.nativeSessionId,
            isLatest: resumeDecision.isLatest
        };
    }
    // Build final prompt with conversation context
    // For native: minimal prompt (native tool handles context)
    // For hybrid: context prefix from other conversations + new prompt
    // For prompt-concat: full multi-turn prompt
    let finalPrompt = prompt;
    if (resumeDecision?.strategy === 'native') {
        // Native mode: just use the new prompt, tool handles context
        finalPrompt = prompt;
    }
    else if (resumeDecision?.strategy === 'hybrid' && resumeDecision.contextTurns?.length) {
        // Hybrid mode: add context prefix from other conversations
        const contextPrefix = buildContextPrefix(resumeDecision.contextTurns, format);
        finalPrompt = contextPrefix + prompt;
    }
    else if (mergeResult && mergeResult.mergedTurns.length > 0) {
        // Full merge: use merged prompt
        finalPrompt = buildMergedPrompt(mergeResult, prompt, format);
    }
    else {
        // Standard prompt-concat
        const conversationForContext = contextConversation || existingConversation;
        if (conversationForContext && conversationForContext.turns.length > 0) {
            finalPrompt = buildMultiTurnPrompt(conversationForContext, prompt, format);
        }
    }
    // Check tool availability
    const toolStatus = await checkToolAvailability(tool);
    if (!toolStatus.available) {
        throw new Error(`CLI tool not available: ${tool}. Please ensure it is installed and in PATH.`);
    }
    // Log resume mode for debugging
    if (resumeDecision) {
        const modeDesc = getResumeModeDescription(resumeDecision);
        if (onOutput) {
            onOutput({
                type: 'stderr',
                content: `[Resume mode: ${modeDesc}]\n`,
                timestamp: new Date().toISOString()
            });
        }
    }
    // Use configured primary model if no explicit model provided
    const effectiveModel = model || getPrimaryModel(workingDir, tool);
    // Build command
    const { command, args, useStdin } = buildCommand({
        tool,
        prompt: finalPrompt,
        mode,
        model: effectiveModel,
        dir: cd,
        include: includeDirs,
        nativeResume: nativeResumeConfig,
        reviewOptions: mode === 'review' ? { uncommitted, base, commit, title } : undefined
    });
    // Create output parser and IR storage
    const parser = createOutputParser(outputFormat);
    const allOutputUnits = [];
    const startTime = Date.now();
    debugLog('EXEC', `Starting CLI execution`, {
        tool,
        mode,
        workingDir,
        conversationId,
        promptLength: finalPrompt.length,
        hasResume: !!resume,
        hasCustomId: !!customId,
        outputFormat
    });
    return new Promise((resolve, reject) => {
        // Windows requires shell: true for npm global commands (.cmd files)
        // Unix-like systems can use shell: false for direct execution
        const isWindows = process.platform === 'win32';
        // When using cmd.exe via `shell: true`, escape args to prevent metacharacter injection.
        const commandToSpawn = isWindows ? escapeWindowsArg(command) : command;
        const argsToSpawn = isWindows ? args.map(escapeWindowsArg) : args;
        // Load custom environment variables from envFile if configured (for gemini/qwen)
        const toolConfig = getToolConfig(workingDir, tool);
        let customEnv = {};
        if (toolConfig.envFile) {
            customEnv = loadEnvFile(toolConfig.envFile);
        }
        // Merge custom env with process.env (custom env takes precedence)
        // Also include rulesEnv for $PROTO and $TMPL template variables
        const spawnEnv = {
            ...process.env,
            ...customEnv,
            ...(rulesEnv || {})
        };
        debugLog('SPAWN', `Spawning process`, {
            command,
            args,
            cwd: workingDir,
            shell: isWindows,
            useStdin,
            platform: process.platform,
            fullCommand: `${command} ${args.join(' ')}`,
            hasCustomEnv: Object.keys(customEnv).length > 0,
            customEnvKeys: Object.keys(customEnv),
            ...(isWindows ? { escapedCommand: commandToSpawn, escapedArgs: argsToSpawn, escapedFullCommand: `${commandToSpawn} ${argsToSpawn.join(' ')}` } : {})
        });
        const child = spawn(commandToSpawn, argsToSpawn, {
            cwd: workingDir,
            shell: isWindows, // Enable shell on Windows for .cmd files
            stdio: [useStdin ? 'pipe' : 'ignore', 'pipe', 'pipe'],
            env: spawnEnv
        });
        // Track current child process for cleanup on interruption
        currentChildProcess = child;
        debugLog('SPAWN', `Process spawned`, { pid: child.pid });
        // Write prompt to stdin if using stdin mode (for gemini/qwen)
        if (useStdin && child.stdin) {
            debugLog('STDIN', `Writing prompt to stdin (${finalPrompt.length} bytes)`);
            child.stdin.write(finalPrompt);
            child.stdin.end();
        }
        let stdout = '';
        let stderr = '';
        // Handle stdout
        child.stdout.on('data', (data) => {
            const text = data.toString();
            stdout += text;
            // Parse into IR units
            const units = parser.parse(data, 'stdout');
            allOutputUnits.push(...units);
            if (onOutput) {
                // Send each IR unit to callback
                for (const unit of units) {
                    onOutput(unit);
                }
            }
        });
        // Handle stderr
        child.stderr.on('data', (data) => {
            const text = data.toString();
            stderr += text;
            // Parse into IR units
            const units = parser.parse(data, 'stderr');
            allOutputUnits.push(...units);
            if (onOutput) {
                // Send each IR unit to callback
                for (const unit of units) {
                    onOutput(unit);
                }
            }
        });
        // Handle completion
        child.on('close', async (code) => {
            if (killTimeout && killTimeoutProcess === child) {
                clearTimeout(killTimeout);
                killTimeout = null;
                killTimeoutProcess = null;
            }
            // Clear current child process reference
            currentChildProcess = null;
            // Flush remaining buffer from parser
            const remainingUnits = parser.flush();
            allOutputUnits.push(...remainingUnits);
            if (onOutput) {
                for (const unit of remainingUnits) {
                    onOutput(unit);
                }
            }
            const endTime = Date.now();
            const duration = endTime - startTime;
            debugLog('CLOSE', `Process closed`, {
                exitCode: code,
                duration: `${duration}ms`,
                stdoutLength: stdout.length,
                stderrLength: stderr.length,
                outputUnitsCount: allOutputUnits.length
            });
            // Determine status - prioritize output content over exit code
            let status = 'success';
            if (code !== 0) {
                // Non-zero exit code doesn't always mean failure
                // Check if there's valid output (AI response) - treat as success
                const hasValidOutput = stdout.trim().length > 0;
                const hasFatalError = stderr.includes('FATAL') ||
                    stderr.includes('Authentication failed') ||
                    stderr.includes('API key') ||
                    stderr.includes('rate limit exceeded');
                debugLog('STATUS', `Non-zero exit code analysis`, {
                    exitCode: code,
                    hasValidOutput,
                    hasFatalError,
                    stderrPreview: stderr.substring(0, 500)
                });
                if (hasValidOutput && !hasFatalError) {
                    // Has output and no fatal errors - treat as success despite exit code
                    status = 'success';
                    debugLog('STATUS', `Treating as success (has valid output, no fatal errors)`);
                }
                else {
                    status = 'error';
                    errorLog('EXEC', `CLI execution failed`, undefined, {
                        exitCode: code,
                        tool,
                        command,
                        args,
                        workingDir,
                        stderrFull: stderr,
                        stdoutPreview: stdout.substring(0, 200)
                    });
                }
            }
            else {
                debugLog('STATUS', `Execution successful (exit code 0)`);
            }
            // Create new turn - cache full output when not streaming (default)
            const shouldCache = !parsed.data.stream;
            // Compute parsed output (filtered, intermediate content removed) for general display
            const computedParsedOutput = flattenOutputUnits(allOutputUnits, {
                excludeTypes: ['stderr', 'progress', 'metadata', 'system', 'tool_call', 'thought', 'code', 'file_diff', 'streaming_content'],
                stripCommandJsonBlocks: true // Strip embedded command execution JSON from agent_message
            });
            // Compute final output (only agent_message) for --final flag
            const computedFinalOutput = flattenOutputUnits(allOutputUnits, {
                includeTypes: ['agent_message'],
                stripCommandJsonBlocks: true // Strip embedded command execution JSON from agent_message
            });
            const newTurnOutput = {
                stdout: stdout.substring(0, 10240), // Truncate preview to 10KB
                stderr: stderr.substring(0, 2048), // Truncate preview to 2KB
                truncated: stdout.length > 10240 || stderr.length > 2048,
                cached: shouldCache,
                stdout_full: shouldCache ? stdout : undefined,
                stderr_full: shouldCache ? stderr : undefined,
                parsed_output: computedParsedOutput || undefined, // Filtered output for general display
                final_output: computedFinalOutput || undefined, // Agent message only for --final flag
                structured: allOutputUnits // Save structured IR units
            };
            // Determine base turn number for merge scenarios
            const baseTurnNumber = isMerge && mergeResult
                ? mergeResult.mergedTurns.length + 1
                : (existingConversation ? existingConversation.turns.length + 1 : 1);
            const newTurn = {
                turn: baseTurnNumber,
                timestamp: new Date(startTime).toISOString(),
                prompt,
                duration_ms: duration,
                status,
                exit_code: code,
                output: newTurnOutput
            };
            // Create or update conversation record
            let conversation;
            if (isMerge && mergeResult && !customId) {
                // Merge without --id: append to ALL source conversations
                // Save new turn to each source conversation
                const savedConversations = [];
                for (const srcConv of sourceConversations) {
                    const turnForSrc = {
                        ...newTurn,
                        turn: srcConv.turns.length + 1 // Use each conversation's turn count
                    };
                    const updatedConv = {
                        ...srcConv,
                        updated_at: new Date().toISOString(),
                        total_duration_ms: srcConv.total_duration_ms + duration,
                        turn_count: srcConv.turns.length + 1,
                        latest_status: status,
                        turns: [...srcConv.turns, turnForSrc]
                    };
                    savedConversations.push(updatedConv);
                }
                // Use first conversation as primary
                conversation = savedConversations[0];
                // Save all source conversations
                try {
                    for (const conv of savedConversations) {
                        saveConversation(workingDir, conv);
                    }
                }
                catch (err) {
                    console.error('[CLI Executor] Failed to save merged histories:', err.message);
                }
            }
            else if (isMerge && mergeResult && customId) {
                // Merge with --id: create new conversation with merged turns + new turn
                // Convert merged turns to regular turns (without source_id)
                const mergedTurns = mergeResult.mergedTurns.map((mt, idx) => ({
                    turn: idx + 1,
                    timestamp: mt.timestamp,
                    prompt: mt.prompt,
                    duration_ms: mt.duration_ms,
                    status: mt.status,
                    exit_code: mt.exit_code,
                    output: mt.output
                }));
                conversation = existingConversation
                    ? {
                        ...existingConversation,
                        updated_at: new Date().toISOString(),
                        total_duration_ms: existingConversation.total_duration_ms + duration,
                        turn_count: existingConversation.turns.length + 1,
                        latest_status: status,
                        turns: [...existingConversation.turns, newTurn]
                    }
                    : {
                        id: conversationId,
                        created_at: new Date(startTime).toISOString(),
                        updated_at: new Date().toISOString(),
                        tool,
                        model: model || 'default',
                        mode,
                        category,
                        total_duration_ms: mergeResult.totalDuration + duration,
                        turn_count: mergedTurns.length + 1,
                        latest_status: status,
                        turns: [...mergedTurns, newTurn]
                    };
                // Save merged conversation
                try {
                    saveConversation(workingDir, conversation);
                }
                catch (err) {
                    console.error('[CLI Executor] Failed to save merged conversation:', err.message);
                }
            }
            else {
                // Normal scenario: single conversation
                conversation = existingConversation
                    ? {
                        ...existingConversation,
                        updated_at: new Date().toISOString(),
                        total_duration_ms: existingConversation.total_duration_ms + duration,
                        turn_count: existingConversation.turns.length + 1,
                        latest_status: status,
                        turns: [...existingConversation.turns, newTurn]
                    }
                    : {
                        id: conversationId,
                        created_at: new Date(startTime).toISOString(),
                        updated_at: new Date().toISOString(),
                        tool,
                        model: model || 'default',
                        mode,
                        category,
                        total_duration_ms: duration,
                        turn_count: 1,
                        latest_status: status,
                        turns: [newTurn],
                        parent_execution_id: parentExecutionId
                    };
                // Try to save conversation to history
                try {
                    saveConversation(workingDir, conversation);
                }
                catch (err) {
                    // Non-fatal: continue even if history save fails
                    console.error('[CLI Executor] Failed to save history:', err.message);
                }
            }
            // Track native session after execution (awaited to prevent process hang)
            // Pass prompt for precise matching in parallel execution scenarios
            try {
                const nativeSession = await trackNewSession(tool, new Date(startTime), workingDir, prompt);
                if (nativeSession) {
                    // Save native session mapping
                    try {
                        store.saveNativeSessionMapping({
                            ccw_id: conversationId,
                            tool,
                            native_session_id: nativeSession.sessionId,
                            native_session_path: nativeSession.filePath,
                            project_hash: nativeSession.projectHash,
                            created_at: new Date().toISOString()
                        });
                    }
                    catch (err) {
                        console.error('[CLI Executor] Failed to save native session mapping:', err.message);
                    }
                }
            }
            catch (err) {
                console.error('[CLI Executor] Failed to track native session:', err.message);
            }
            // Create legacy execution record for backward compatibility
            const execution = {
                id: conversationId,
                timestamp: new Date(startTime).toISOString(),
                tool,
                model: effectiveModel || 'default',
                mode,
                prompt,
                status,
                exit_code: code,
                duration_ms: duration,
                output: newTurnOutput,
                parsedOutput: computedParsedOutput, // Use already-computed filtered output
                finalOutput: computedFinalOutput // Use already-computed agent_message only output
            };
            resolve({
                success: status === 'success',
                execution,
                conversation,
                stdout,
                stderr,
                parsedOutput: execution.parsedOutput,
                finalOutput: execution.finalOutput
            });
        });
        // Handle errors
        child.on('error', (error) => {
            errorLog('SPAWN', `Failed to spawn process`, error, {
                tool,
                command,
                args,
                workingDir,
                fullCommand: `${command} ${args.join(' ')}`,
                platform: process.platform,
                path: process.env.PATH?.split(process.platform === 'win32' ? ';' : ':').slice(0, 10).join('\n  ') + '...'
            });
            reject(new Error(`Failed to spawn ${tool}: ${error.message}\n  Command: ${command} ${args.join(' ')}\n  Working Dir: ${workingDir}`));
        });
        // Timeout controlled by external caller (bash timeout)
        // When parent process terminates, child will be cleaned up via process exit handler
    });
}
// Tool schema for MCP
export const schema = {
    name: 'cli_executor',
    description: `Execute external CLI tools (gemini/qwen/codex) with unified interface.
Modes:
- analysis: Read-only operations (default)
- write: File modifications allowed
- auto: Full autonomous operations (codex only)
- review: Code review mode (codex uses 'codex review' subcommand, others accept but no operation change)`,
    inputSchema: {
        type: 'object',
        properties: {
            tool: {
                type: 'string',
                enum: ['gemini', 'qwen', 'codex'],
                description: 'CLI tool to execute'
            },
            prompt: {
                type: 'string',
                description: 'Prompt to send to the CLI tool'
            },
            mode: {
                type: 'string',
                enum: ['analysis', 'write', 'auto', 'review'],
                description: 'Execution mode (default: analysis). review mode uses codex review subcommand for codex tool.',
                default: 'analysis'
            },
            model: {
                type: 'string',
                description: 'Model override (tool-specific)'
            },
            cd: {
                type: 'string',
                description: 'Working directory for execution (-C for codex)'
            },
            includeDirs: {
                type: 'string',
                description: 'Additional directories (comma-separated). Maps to --include-directories for gemini/qwen, --add-dir for codex'
            }
            // timeout removed - controlled by external caller (bash timeout)
        },
        required: ['tool', 'prompt']
    }
};
// Handler function
export async function handler(params) {
    try {
        const result = await executeCliTool(params);
        return {
            success: result.success,
            result
        };
    }
    catch (error) {
        return {
            success: false,
            error: `CLI execution failed: ${error.message}`
        };
    }
}
export { batchDeleteExecutionsAsync, deleteExecution, deleteExecutionAsync, getConversationDetail, getConversationDetailWithNativeInfo, getExecutionDetail, getExecutionHistory, getExecutionHistoryAsync } from './cli-executor-state.js';
/**
 * Get status of all CLI tools
 * Dynamically reads tools from config file
 * Handles different tool types:
 * - builtin: Check system PATH availability
 * - cli-wrapper: Check CLI Settings configuration exists
 * - api-endpoint: Check LiteLLM endpoint configuration exists
 */
export async function getCliToolsStatus() {
    const funcStart = Date.now();
    debugLog('PERF', 'getCliToolsStatus START');
    // Default built-in tools
    const builtInTools = ['gemini', 'qwen', 'codex', 'claude', 'opencode'];
    let toolsInfo = builtInTools.map(name => ({ name, type: 'builtin' }));
    const configLoadStart = Date.now();
    try {
        // Dynamic import to avoid circular dependencies
        const { loadClaudeCliTools } = await import('./claude-cli-tools.js');
        const config = loadClaudeCliTools(configBaseDir);
        if (config.tools && typeof config.tools === 'object') {
            // Build complete tool info list from config
            const configToolsInfo = Object.entries(config.tools).map(([name, toolConfig]) => ({
                name,
                type: toolConfig.type || 'builtin',
                enabled: toolConfig.enabled !== false,
                id: toolConfig.id
            }));
            // Merge: config tools take precedence over built-in defaults
            const toolsMap = new Map();
            toolsInfo.forEach(t => toolsMap.set(t.name, t));
            configToolsInfo.forEach(t => toolsMap.set(t.name, t));
            toolsInfo = Array.from(toolsMap.values());
        }
    }
    catch (e) {
        // Fallback to built-in tools if config load fails
        debugLog('cli-executor', `Using built-in tools (config load failed: ${e.message})`);
    }
    debugLog('PERF', `Config load: ${Date.now() - configLoadStart}ms, tools: ${toolsInfo.length}`);
    const results = {};
    const toolTimings = {};
    const checksStart = Date.now();
    await Promise.all(toolsInfo.map(async (toolInfo) => {
        const { name, type, enabled, id } = toolInfo;
        const toolStart = Date.now();
        // Check availability based on tool type
        if (type === 'cli-wrapper') {
            // For cli-wrapper: check if CLI Settings configuration exists
            try {
                const { findEndpoint } = await import('../config/cli-settings-manager.js');
                const endpoint = findEndpoint(name);
                if (endpoint && endpoint.enabled) {
                    results[name] = {
                        available: true,
                        path: `cli-settings:${endpoint.id}` // Virtual path indicating CLI Settings source
                    };
                }
                else {
                    results[name] = { available: false, path: null };
                }
            }
            catch (e) {
                debugLog('cli-executor', `Failed to check cli-wrapper ${name}: ${e.message}`);
                results[name] = { available: false, path: null };
            }
        }
        else if (type === 'api-endpoint') {
            // For api-endpoint: check if LiteLLM endpoint configuration exists
            try {
                const { findEndpointById } = await import('../config/litellm-api-config-manager.js');
                const endpointId = id || name;
                const endpoint = findEndpointById(configBaseDir, endpointId);
                if (endpoint && enabled !== false) {
                    results[name] = {
                        available: true,
                        path: `litellm:${endpointId}` // Virtual path indicating LiteLLM source
                    };
                }
                else {
                    results[name] = { available: false, path: null };
                }
            }
            catch (e) {
                debugLog('cli-executor', `Failed to check api-endpoint ${name}: ${e.message}`);
                results[name] = { available: false, path: null };
            }
        }
        else {
            // For builtin: check system PATH availability
            results[name] = await checkToolAvailability(name);
        }
        toolTimings[name] = Date.now() - toolStart;
    }));
    debugLog('PERF', `Tool checks: ${Date.now() - checksStart}ms | Individual: ${JSON.stringify(toolTimings)}`);
    debugLog('PERF', `getCliToolsStatus TOTAL: ${Date.now() - funcStart}ms`);
    return results;
}
// CLI tool package mapping
const CLI_TOOL_PACKAGES = {
    gemini: '@google/gemini-cli',
    qwen: '@qwen-code/qwen-code',
    codex: '@openai/codex',
    claude: '@anthropic-ai/claude-code',
    opencode: 'opencode' // https://opencode.ai - installed via npm/pnpm/bun/brew
};
// Disabled tools storage (in-memory fallback, main storage is in cli-config.json)
const disabledTools = new Set();
// Default working directory for config operations
let configBaseDir = process.cwd();
/**
 * Set the base directory for config operations
 */
export function setConfigBaseDir(dir) {
    configBaseDir = dir;
}
/**
 * Install a CLI tool via npm
 */
export async function installCliTool(tool) {
    const packageName = CLI_TOOL_PACKAGES[tool];
    if (!packageName) {
        return { success: false, error: `Unknown tool: ${tool}` };
    }
    return new Promise((resolve) => {
        const child = spawn('npm', ['install', '-g', packageName], {
            shell: true,
            stdio: ['ignore', 'pipe', 'pipe']
        });
        let stderr = '';
        child.stderr?.on('data', (data) => { stderr += data.toString(); });
        child.on('close', (code) => {
            // Clear cache to force re-check
            clearToolCache();
            if (code === 0) {
                resolve({ success: true });
            }
            else {
                resolve({ success: false, error: stderr || `npm install failed with code ${code}` });
            }
        });
        child.on('error', (err) => {
            resolve({ success: false, error: err.message });
        });
        // Timeout after 2 minutes
        setTimeout(() => {
            child.kill();
            resolve({ success: false, error: 'Installation timed out' });
        }, 120000);
    });
}
/**
 * Uninstall a CLI tool via npm
 */
export async function uninstallCliTool(tool) {
    const packageName = CLI_TOOL_PACKAGES[tool];
    if (!packageName) {
        return { success: false, error: `Unknown tool: ${tool}` };
    }
    return new Promise((resolve) => {
        const child = spawn('npm', ['uninstall', '-g', packageName], {
            shell: true,
            stdio: ['ignore', 'pipe', 'pipe']
        });
        let stderr = '';
        child.stderr?.on('data', (data) => { stderr += data.toString(); });
        child.on('close', (code) => {
            // Clear cache to force re-check
            clearToolCache();
            if (code === 0) {
                resolve({ success: true });
            }
            else {
                resolve({ success: false, error: stderr || `npm uninstall failed with code ${code}` });
            }
        });
        child.on('error', (err) => {
            resolve({ success: false, error: err.message });
        });
        // Timeout after 1 minute
        setTimeout(() => {
            child.kill();
            resolve({ success: false, error: 'Uninstallation timed out' });
        }, 60000);
    });
}
/**
 * Enable a CLI tool (updates config file)
 */
export function enableCliTool(tool) {
    try {
        enableToolFromConfig(configBaseDir, tool);
        disabledTools.delete(tool); // Also update in-memory fallback
        return { success: true };
    }
    catch (err) {
        console.error('[cli-executor] Error enabling tool:', err);
        disabledTools.delete(tool); // Fallback to in-memory
        return { success: true };
    }
}
/**
 * Disable a CLI tool (updates config file)
 */
export function disableCliTool(tool) {
    try {
        disableToolFromConfig(configBaseDir, tool);
        disabledTools.add(tool); // Also update in-memory fallback
        return { success: true };
    }
    catch (err) {
        console.error('[cli-executor] Error disabling tool:', err);
        disabledTools.add(tool); // Fallback to in-memory
        return { success: true };
    }
}
/**
 * Check if a tool is enabled (reads from config file)
 */
export function isToolEnabled(tool) {
    try {
        return isToolEnabledFromConfig(configBaseDir, tool);
    }
    catch {
        // Fallback to in-memory check
        return !disabledTools.has(tool);
    }
}
/**
 * Get full status of all CLI tools including enabled state
 */
export async function getCliToolsFullStatus() {
    const tools = Object.keys(CLI_TOOL_PACKAGES);
    const results = {};
    await Promise.all(tools.map(async (tool) => {
        const availability = await checkToolAvailability(tool);
        results[tool] = {
            available: availability.available,
            enabled: isToolEnabled(tool),
            path: availability.path,
            packageName: CLI_TOOL_PACKAGES[tool]
        };
    }));
    return results;
}
/**
 * Build continuation prompt with previous conversation context (legacy)
 */
function buildContinuationPrompt(previous, additionalPrompt) {
    const parts = [];
    // Add previous conversation context
    parts.push('=== PREVIOUS CONVERSATION ===');
    parts.push('');
    parts.push('USER PROMPT:');
    parts.push(previous.prompt);
    parts.push('');
    parts.push('ASSISTANT RESPONSE:');
    parts.push(previous.output.stdout || '[No output recorded]');
    parts.push('');
    parts.push('=== CONTINUATION ===');
    parts.push('');
    if (additionalPrompt) {
        parts.push(additionalPrompt);
    }
    else {
        parts.push('Continue from where we left off. What should we do next?');
    }
    return parts.join('\n');
}
/**
 * Get previous execution for resume
 * @param baseDir - Working directory
 * @param tool - Tool to filter by
 * @param resume - true for last, or execution ID string
 */
function getPreviousExecution(baseDir, tool, resume) {
    if (typeof resume === 'string') {
        // Resume specific execution by ID
        return getExecutionDetail(baseDir, resume);
    }
    else if (resume === true) {
        // Resume last execution for this tool
        const history = getExecutionHistory(baseDir, { limit: 1, tool });
        if (history.executions.length === 0) {
            return null;
        }
        return getExecutionDetail(baseDir, history.executions[0].id);
    }
    return null;
}
/**
 * Latest execution + native session history functions are re-exported from state.
 */
export { getEnrichedConversation, getFormattedNativeConversation, getHistoryWithNativeInfo, getLatestExecution, getNativeConversationPairs, getNativeSessionContent } from './cli-executor-state.js';
// Export utility functions and tool definition for backward compatibility
export { executeCliTool, checkToolAvailability, clearToolCache };
// Export env file utilities for testing
export { parseEnvFile, loadEnvFile };
// Export prompt concatenation utilities
export { PromptConcatenator, createPromptConcatenator, buildPrompt, buildMultiTurnPrompt } from './cli-prompt-builder.js';
// Note: Async storage functions (getExecutionHistoryAsync, deleteExecutionAsync,
// batchDeleteExecutionsAsync) are exported at declaration site - SQLite storage only
// Export tool definition (for legacy imports) - This allows direct calls to execute with onOutput
export const cliExecutorTool = {
    schema,
    execute: executeCliTool // Use executeCliTool directly which supports onOutput callback
};
//# sourceMappingURL=cli-executor-core.js.map