/**
 * Claude SDK Integration
 *
 * This module provides SDK-based integration with Claude using the @anthropic-ai/claude-agent-sdk.
 * It mirrors the interface of claude-cli.js but uses the SDK internally for better performance
 * and maintainability.
 *
 * Key features:
 * - Direct SDK integration without child processes
 * - Session management with abort capability
 * - Options mapping between CLI and SDK formats
 * - WebSocket message streaming
 */
import crypto from 'crypto';
import { existsSync, readFileSync, promises as fs } from 'fs';
import path from 'path';
import os from 'os';
import { query } from '@anthropic-ai/claude-agent-sdk';
import { createNotificationEvent, notifyRunFailed, notifyRunStopped, notifyUserIfEnabled } from './services/notification-orchestrator.js';
import { sessionsService } from './modules/providers/services/sessions.service.js';
import { providerAuthService } from './modules/providers/services/provider-auth.service.js';
import { resolveClaudeExecutable, resolveGitBashPath } from './services/install-jobs.js';
import { createNormalizedMessage } from './shared/utils.js';
const activeSessions = new Map();
const pendingToolApprovals = new Map();
const TOOL_APPROVAL_TIMEOUT_MS = parseInt(process.env.CLAUDE_TOOL_APPROVAL_TIMEOUT_MS, 10) || 55000;
const TOOLS_REQUIRING_INTERACTION = new Set(['AskUserQuestion', 'ExitPlanMode']);
function createRequestId() {
    if (typeof crypto.randomUUID === 'function') {
        return crypto.randomUUID();
    }
    return crypto.randomBytes(16).toString('hex');
}
function waitForToolApproval(requestId, options = {}) {
    const { timeoutMs = TOOL_APPROVAL_TIMEOUT_MS, signal, onCancel, metadata } = options;
    return new Promise(resolve => {
        let settled = false;
        const finalize = (decision) => {
            if (settled)
                return;
            settled = true;
            cleanup();
            resolve(decision);
        };
        let timeout;
        const cleanup = () => {
            pendingToolApprovals.delete(requestId);
            if (timeout)
                clearTimeout(timeout);
            if (signal && abortHandler) {
                signal.removeEventListener('abort', abortHandler);
            }
        };
        // timeoutMs 0 = wait indefinitely (interactive tools)
        if (timeoutMs > 0) {
            timeout = setTimeout(() => {
                onCancel?.('timeout');
                finalize(null);
            }, timeoutMs);
        }
        const abortHandler = () => {
            onCancel?.('cancelled');
            finalize({ cancelled: true });
        };
        if (signal) {
            if (signal.aborted) {
                onCancel?.('cancelled');
                finalize({ cancelled: true });
                return;
            }
            signal.addEventListener('abort', abortHandler, { once: true });
        }
        const resolver = (decision) => {
            finalize(decision);
        };
        // Attach metadata for getPendingApprovalsForSession lookup
        if (metadata) {
            Object.assign(resolver, metadata);
        }
        pendingToolApprovals.set(requestId, resolver);
    });
}
function resolveToolApproval(requestId, decision) {
    const resolver = pendingToolApprovals.get(requestId);
    if (resolver) {
        resolver(decision);
    }
}
// Match stored permission entries against a tool + input combo.
// This only supports exact tool names and the Bash(command:*) shorthand
// used by the UI; it intentionally does not implement full glob semantics,
// introduced to stay consistent with the UI's "Allow rule" format.
function matchesToolPermission(entry, toolName, input) {
    if (!entry || !toolName) {
        return false;
    }
    if (entry === toolName) {
        return true;
    }
    const bashMatch = entry.match(/^Bash\((.+):\*\)$/);
    if (toolName === 'Bash' && bashMatch) {
        const allowedPrefix = bashMatch[1];
        let command = '';
        if (typeof input === 'string') {
            command = input.trim();
        }
        else if (input && typeof input === 'object' && typeof input.command === 'string') {
            command = input.command.trim();
        }
        if (!command) {
            return false;
        }
        return command.startsWith(allowedPrefix);
    }
    return false;
}
function readClaudeSettingsEnv(filePath) {
    try {
        if (!existsSync(filePath))
            return {};
        const parsed = JSON.parse(readFileSync(filePath, 'utf8'));
        const env = parsed?.env;
        if (!env || typeof env !== 'object')
            return {};
        return Object.fromEntries(Object.entries(env)
            .filter(([key, value]) => typeof value === 'string' &&
            value.trim() &&
            !key.startsWith('//')));
    }
    catch {
        return {};
    }
}
function loadClaudeSettingsEnv(cwd) {
    const files = [
        path.join(os.homedir(), '.claude', 'settings.json'),
    ];
    if (cwd) {
        files.push(path.join(cwd, '.claude', 'settings.json'), path.join(cwd, '.claude', 'settings.local.json'));
    }
    return files.reduce((env, filePath) => ({
        ...env,
        ...readClaudeSettingsEnv(filePath),
    }), {});
}
/**
 * Maps CLI options to SDK-compatible options format
 * @param {Object} options - CLI options
 * @returns {Object} SDK-compatible options
 */
function mapCliOptionsToSDK(options = {}) {
    const { sessionId, cwd, toolsSettings, permissionMode } = options;
    const sdkOptions = {};
    // Forward all host env vars (e.g. ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN) to the subprocess.
    // Since claude-agent-sdk 0.2.113+, options.env REPLACES process.env in the subprocess
    // instead of overlaying it. Without spreading process.env here, users who rely on
    // ANTHROPIC_BASE_URL, HTTP(S)_PROXY, etc. would silently lose those settings.
    sdkOptions.env = { ...process.env, ...loadClaudeSettingsEnv(cwd) };
    // Claude Code on Windows hard-requires a POSIX bash (typically from Git
    // for Windows) and reads its path from CLAUDE_CODE_GIT_BASH_PATH. If the
    // user has git-bash installed but hasn't exported that var, the CLI
    // exits with code 1 + a guidance message and the pixcode UI just sees
    // an opaque "Claude Code process exited with code 1" error. Auto-probing
    // a handful of known install locations lets the CLI boot transparently.
    if (!sdkOptions.env.CLAUDE_CODE_GIT_BASH_PATH) {
        const bashPath = resolveGitBashPath();
        if (bashPath)
            sdkOptions.env.CLAUDE_CODE_GIT_BASH_PATH = bashPath;
    }
    // Resolve the Claude Code CLI path cross-platform. The SDK uses plain
    // `child_process.spawn(command, args)` with no shell — and its own
    // `nb()` helper treats anything not ending in .js/.mjs/.ts as a native
    // executable. That means:
    //   - Unix: bare `"claude"` works (kernel PATH + shebang handle it).
    //   - Windows: bare `"claude"` fails ("native binary not found"). A
    //     `.cmd` shim fails with EINVAL (post-CVE-2024 Node refuses .cmd
    //     without shell:true). We need the underlying `.exe` instead.
    // `resolveClaudeExecutable()` does a `where`/`which` lookup and, on
    // Windows, peeks inside npm .cmd shims to recover the real .exe target.
    // If nothing is found we leave the option unset so the SDK falls through
    // to its own bundled-native-binary resolver.
    const resolvedClaudePath = process.env.CLAUDE_CLI_PATH || resolveClaudeExecutable();
    if (resolvedClaudePath) {
        sdkOptions.pathToClaudeCodeExecutable = resolvedClaudePath;
    }
    // Map working directory
    if (cwd) {
        sdkOptions.cwd = cwd;
    }
    // Map permission mode
    if (permissionMode && permissionMode !== 'default') {
        sdkOptions.permissionMode = permissionMode;
    }
    // Map tool settings
    const settings = toolsSettings || {
        allowedTools: [],
        disallowedTools: [],
        skipPermissions: false
    };
    // Handle tool permissions
    if (settings.skipPermissions && permissionMode !== 'plan') {
        // When skipping permissions, use bypassPermissions mode
        sdkOptions.permissionMode = 'bypassPermissions';
    }
    let allowedTools = [...(settings.allowedTools || [])];
    // Add plan mode default tools
    if (permissionMode === 'plan') {
        const planModeTools = ['Read', 'Task', 'exit_plan_mode', 'TodoRead', 'TodoWrite', 'WebFetch', 'WebSearch'];
        for (const tool of planModeTools) {
            if (!allowedTools.includes(tool)) {
                allowedTools.push(tool);
            }
        }
    }
    sdkOptions.allowedTools = allowedTools;
    // Use the tools preset to make all default built-in tools available (including AskUserQuestion).
    // This was introduced in SDK 0.1.57. Omitting this preserves existing behavior (all tools available),
    // but being explicit ensures forward compatibility and clarity.
    sdkOptions.tools = { type: 'preset', preset: 'claude_code' };
    sdkOptions.disallowedTools = settings.disallowedTools || [];
    // Map model only when Pixcode explicitly passes one. If omitted, let
    // Claude Code load the user's own project/user/local settings, including
    // ~/.claude/settings.json "model".
    if (typeof options.model === 'string' && options.model.trim()) {
        sdkOptions.model = options.model.trim();
    }
    // Model logged at query start below
    // Map system prompt configuration
    sdkOptions.systemPrompt = {
        type: 'preset',
        preset: 'claude_code' // Required to use CLAUDE.md
    };
    // Map setting sources for CLAUDE.md loading
    // This loads CLAUDE.md from project, user (~/.config/claude/CLAUDE.md), and local directories
    sdkOptions.settingSources = ['project', 'user', 'local'];
    // Map resume session
    if (sessionId) {
        sdkOptions.resume = sessionId;
    }
    return sdkOptions;
}
/**
 * Adds a session to the active sessions map
 * @param {string} sessionId - Session identifier
 * @param {Object} queryInstance - SDK query instance
 * @param {Array<string>} tempImagePaths - Temp image file paths for cleanup
 * @param {string} tempDir - Temp directory for cleanup
 */
function addSession(sessionId, queryInstance, tempImagePaths = [], tempDir = null, writer = null) {
    activeSessions.set(sessionId, {
        instance: queryInstance,
        startTime: Date.now(),
        status: 'active',
        tempImagePaths,
        tempDir,
        writer
    });
}
/**
 * Removes a session from the active sessions map
 * @param {string} sessionId - Session identifier
 */
function removeSession(sessionId) {
    activeSessions.delete(sessionId);
}
/**
 * Gets a session from the active sessions map
 * @param {string} sessionId - Session identifier
 * @returns {Object|undefined} Session data or undefined
 */
function getSession(sessionId) {
    return activeSessions.get(sessionId);
}
/**
 * Gets all active session IDs
 * @returns {Array<string>} Array of active session IDs
 */
function getAllSessions() {
    return Array.from(activeSessions.keys());
}
/**
 * Transforms SDK messages to WebSocket format expected by frontend
 * @param {Object} sdkMessage - SDK message object
 * @returns {Object} Transformed message ready for WebSocket
 */
function transformMessage(sdkMessage) {
    // Extract parent_tool_use_id for subagent tool grouping
    if (sdkMessage.parent_tool_use_id) {
        return {
            ...sdkMessage,
            parentToolUseId: sdkMessage.parent_tool_use_id
        };
    }
    return sdkMessage;
}
/**
 * Extracts token usage from SDK result messages
 * @param {Object} resultMessage - SDK result message
 * @returns {Object|null} Token budget object or null
 */
function extractTokenBudget(resultMessage) {
    if (resultMessage.type !== 'result' || !resultMessage.modelUsage) {
        return null;
    }
    // Get the first model's usage data
    const modelKey = Object.keys(resultMessage.modelUsage)[0];
    const modelData = resultMessage.modelUsage[modelKey];
    if (!modelData) {
        return null;
    }
    // Use cumulative tokens if available (tracks total for the session)
    // Otherwise fall back to per-request tokens
    const inputTokens = modelData.cumulativeInputTokens || modelData.inputTokens || 0;
    const outputTokens = modelData.cumulativeOutputTokens || modelData.outputTokens || 0;
    const cacheReadTokens = modelData.cumulativeCacheReadInputTokens || modelData.cacheReadInputTokens || 0;
    const cacheCreationTokens = modelData.cumulativeCacheCreationInputTokens || modelData.cacheCreationInputTokens || 0;
    // Total used = input + output + cache tokens
    const totalUsed = inputTokens + outputTokens + cacheReadTokens + cacheCreationTokens;
    // Use configured context window budget from environment (default 160000)
    // This is the user's budget limit, not the model's context window
    const contextWindow = parseInt(process.env.CONTEXT_WINDOW) || 160000;
    // Token calc logged via token-budget WS event
    return {
        used: totalUsed,
        total: contextWindow
    };
}
/**
 * Handles image processing for SDK queries
 * Saves base64 images to temporary files and returns modified prompt with file paths
 * @param {string} command - Original user prompt
 * @param {Array} images - Array of image objects with base64 data
 * @param {string} cwd - Working directory for temp file creation
 * @returns {Promise<Object>} {modifiedCommand, tempImagePaths, tempDir}
 */
async function handleImages(command, images, cwd) {
    const tempImagePaths = [];
    let tempDir = null;
    if (!images || images.length === 0) {
        return { modifiedCommand: command, tempImagePaths, tempDir };
    }
    try {
        // Create temp directory in the project directory
        const workingDir = cwd || process.cwd();
        tempDir = path.join(workingDir, '.tmp', 'images', Date.now().toString());
        await fs.mkdir(tempDir, { recursive: true });
        // Save each image to a temp file
        for (const [index, image] of images.entries()) {
            // Extract base64 data and mime type
            const matches = image.data.match(/^data:([^;]+);base64,(.+)$/);
            if (!matches) {
                console.error('Invalid image data format');
                continue;
            }
            const [, mimeType, base64Data] = matches;
            const extension = mimeType.split('/')[1] || 'png';
            const filename = `image_${index}.${extension}`;
            const filepath = path.join(tempDir, filename);
            // Write base64 data to file
            await fs.writeFile(filepath, Buffer.from(base64Data, 'base64'));
            tempImagePaths.push(filepath);
        }
        // Include the full image paths in the prompt
        let modifiedCommand = command;
        if (tempImagePaths.length > 0 && command && command.trim()) {
            const imageNote = `\n\n[Images provided at the following paths:]\n${tempImagePaths.map((p, i) => `${i + 1}. ${p}`).join('\n')}`;
            modifiedCommand = command + imageNote;
        }
        // Images processed
        return { modifiedCommand, tempImagePaths, tempDir };
    }
    catch (error) {
        console.error('Error processing images for SDK:', error);
        return { modifiedCommand: command, tempImagePaths, tempDir };
    }
}
/**
 * Cleans up temporary image files
 * @param {Array<string>} tempImagePaths - Array of temp file paths to delete
 * @param {string} tempDir - Temp directory to remove
 */
async function cleanupTempFiles(tempImagePaths, tempDir) {
    if (!tempImagePaths || tempImagePaths.length === 0) {
        return;
    }
    try {
        // Delete individual temp files
        for (const imagePath of tempImagePaths) {
            await fs.unlink(imagePath).catch(err => console.error(`Failed to delete temp image ${imagePath}:`, err));
        }
        // Delete temp directory
        if (tempDir) {
            await fs.rm(tempDir, { recursive: true, force: true }).catch(err => console.error(`Failed to delete temp directory ${tempDir}:`, err));
        }
        // Temp files cleaned
    }
    catch (error) {
        console.error('Error during temp file cleanup:', error);
    }
}
/**
 * Loads MCP server configurations from ~/.claude.json
 * @param {string} cwd - Current working directory for project-specific configs
 * @returns {Object|null} MCP servers object or null if none found
 */
async function loadMcpConfig(cwd) {
    try {
        const claudeConfigPath = path.join(os.homedir(), '.claude.json');
        // Check if config file exists
        try {
            await fs.access(claudeConfigPath);
        }
        catch (error) {
            // File doesn't exist, return null
            // No config file
            return null;
        }
        // Read and parse config file
        let claudeConfig;
        try {
            const configContent = await fs.readFile(claudeConfigPath, 'utf8');
            claudeConfig = JSON.parse(configContent);
        }
        catch (error) {
            console.error('Failed to parse ~/.claude.json:', error.message);
            return null;
        }
        // Extract MCP servers (merge global and project-specific)
        let mcpServers = {};
        // Add global MCP servers
        if (claudeConfig.mcpServers && typeof claudeConfig.mcpServers === 'object') {
            mcpServers = { ...claudeConfig.mcpServers };
            // Global MCP servers loaded
        }
        // Add/override with project-specific MCP servers
        if (claudeConfig.claudeProjects && cwd) {
            const projectConfig = claudeConfig.claudeProjects[cwd];
            if (projectConfig && projectConfig.mcpServers && typeof projectConfig.mcpServers === 'object') {
                mcpServers = { ...mcpServers, ...projectConfig.mcpServers };
                // Project MCP servers merged
            }
        }
        // Return null if no servers found
        if (Object.keys(mcpServers).length === 0) {
            return null;
        }
        return mcpServers;
    }
    catch (error) {
        console.error('Error loading MCP config:', error.message);
        return null;
    }
}
/**
 * Executes a Claude query using the SDK
 * @param {string} command - User prompt/command
 * @param {Object} options - Query options
 * @param {Object} ws - WebSocket connection
 * @returns {Promise<void>}
 */
async function queryClaudeSDK(command, options = {}, ws) {
    const { sessionId, sessionSummary } = options;
    let capturedSessionId = sessionId;
    let sessionCreatedSent = false;
    let tempImagePaths = [];
    let tempDir = null;
    const emitNotification = (event) => {
        notifyUserIfEnabled({
            userId: ws?.userId || null,
            writer: ws,
            event
        });
    };
    try {
        // Map CLI options to SDK format
        const sdkOptions = mapCliOptionsToSDK(options);
        // Load MCP configuration
        const mcpServers = await loadMcpConfig(options.cwd);
        if (mcpServers) {
            sdkOptions.mcpServers = mcpServers;
        }
        // Handle images - save to temp files and modify prompt
        const imageResult = await handleImages(command, options.images, options.cwd);
        const finalCommand = imageResult.modifiedCommand;
        tempImagePaths = imageResult.tempImagePaths;
        tempDir = imageResult.tempDir;
        sdkOptions.hooks = {
            Notification: [{
                    matcher: '',
                    hooks: [async (input) => {
                            const message = typeof input?.message === 'string' ? input.message : 'Claude requires your attention.';
                            emitNotification(createNotificationEvent({
                                provider: 'claude',
                                sessionId: capturedSessionId || sessionId || null,
                                kind: 'action_required',
                                code: 'agent.notification',
                                meta: { message, sessionName: sessionSummary },
                                severity: 'warning',
                                requiresUserAction: true,
                                dedupeKey: `claude:hook:notification:${capturedSessionId || sessionId || 'none'}:${message}`
                            }));
                            return {};
                        }]
                }]
        };
        sdkOptions.canUseTool = async (toolName, input, context) => {
            const requiresInteraction = TOOLS_REQUIRING_INTERACTION.has(toolName);
            if (!requiresInteraction) {
                if (sdkOptions.permissionMode === 'bypassPermissions') {
                    return { behavior: 'allow', updatedInput: input };
                }
                const isDisallowed = (sdkOptions.disallowedTools || []).some(entry => matchesToolPermission(entry, toolName, input));
                if (isDisallowed) {
                    return { behavior: 'deny', message: 'Tool disallowed by settings' };
                }
                const isAllowed = (sdkOptions.allowedTools || []).some(entry => matchesToolPermission(entry, toolName, input));
                if (isAllowed) {
                    return { behavior: 'allow', updatedInput: input };
                }
            }
            const requestId = createRequestId();
            ws.send(createNormalizedMessage({ kind: 'permission_request', requestId, toolName, input, sessionId: capturedSessionId || sessionId || null, provider: 'claude' }));
            emitNotification(createNotificationEvent({
                provider: 'claude',
                sessionId: capturedSessionId || sessionId || null,
                kind: 'action_required',
                code: 'permission.required',
                meta: { toolName, sessionName: sessionSummary },
                severity: 'warning',
                requiresUserAction: true,
                dedupeKey: `claude:permission:${capturedSessionId || sessionId || 'none'}:${requestId}`
            }));
            const decision = await waitForToolApproval(requestId, {
                timeoutMs: requiresInteraction ? 0 : undefined,
                signal: context?.signal,
                metadata: {
                    _sessionId: capturedSessionId || sessionId || null,
                    _toolName: toolName,
                    _input: input,
                    _receivedAt: new Date(),
                },
                onCancel: (reason) => {
                    ws.send(createNormalizedMessage({ kind: 'permission_cancelled', requestId, reason, sessionId: capturedSessionId || sessionId || null, provider: 'claude' }));
                }
            });
            if (!decision) {
                return { behavior: 'deny', message: 'Permission request timed out' };
            }
            if (decision.cancelled) {
                return { behavior: 'deny', message: 'Permission request cancelled' };
            }
            if (decision.allow) {
                if (decision.rememberEntry && typeof decision.rememberEntry === 'string') {
                    if (!sdkOptions.allowedTools.includes(decision.rememberEntry)) {
                        sdkOptions.allowedTools.push(decision.rememberEntry);
                    }
                    if (Array.isArray(sdkOptions.disallowedTools)) {
                        sdkOptions.disallowedTools = sdkOptions.disallowedTools.filter(entry => entry !== decision.rememberEntry);
                    }
                }
                return { behavior: 'allow', updatedInput: decision.updatedInput ?? input };
            }
            return { behavior: 'deny', message: decision.message ?? 'User denied tool use' };
        };
        // Set stream-close timeout for interactive tools (Query constructor reads it synchronously). Claude Agent SDK has a default of 5s and this overrides it
        const prevStreamTimeout = process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT;
        process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT = '300000';
        let queryInstance;
        try {
            queryInstance = query({
                prompt: finalCommand,
                options: sdkOptions
            });
        }
        catch (hookError) {
            // Older/newer SDK versions may not accept hook shapes yet.
            // Keep notification behavior operational via runtime events even if hook registration fails.
            console.warn('Failed to initialize Claude query with hooks, retrying without hooks:', hookError?.message || hookError);
            delete sdkOptions.hooks;
            queryInstance = query({
                prompt: finalCommand,
                options: sdkOptions
            });
        }
        // Restore immediately — Query constructor already captured the value
        if (prevStreamTimeout !== undefined) {
            process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT = prevStreamTimeout;
        }
        else {
            delete process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT;
        }
        // Track the query instance for abort capability
        if (capturedSessionId) {
            addSession(capturedSessionId, queryInstance, tempImagePaths, tempDir, ws);
        }
        // Process streaming messages
        console.log('Starting async generator loop for session:', capturedSessionId || 'NEW');
        for await (const message of queryInstance) {
            // Capture session ID from first message
            if (message.session_id && !capturedSessionId) {
                capturedSessionId = message.session_id;
                addSession(capturedSessionId, queryInstance, tempImagePaths, tempDir, ws);
                // Set session ID on writer
                if (ws.setSessionId && typeof ws.setSessionId === 'function') {
                    ws.setSessionId(capturedSessionId);
                }
                // Send session-created event only once for new sessions
                if (!sessionId && !sessionCreatedSent) {
                    sessionCreatedSent = true;
                    ws.send(createNormalizedMessage({ kind: 'session_created', newSessionId: capturedSessionId, sessionId: capturedSessionId, provider: 'claude' }));
                }
            }
            else {
                // session_id already captured
            }
            // Transform and normalize message via adapter
            const transformedMessage = transformMessage(message);
            const sid = capturedSessionId || sessionId || null;
            // Use adapter to normalize SDK events into NormalizedMessage[]
            const normalized = sessionsService.normalizeMessage('claude', transformedMessage, sid);
            for (const msg of normalized) {
                // Preserve parentToolUseId from SDK wrapper for subagent tool grouping
                if (transformedMessage.parentToolUseId && !msg.parentToolUseId) {
                    msg.parentToolUseId = transformedMessage.parentToolUseId;
                }
                ws.send(msg);
            }
            // Extract and send token budget updates from result messages
            if (message.type === 'result') {
                const models = Object.keys(message.modelUsage || {});
                if (models.length > 0) {
                    // Model info available in result message
                }
                const tokenBudgetData = extractTokenBudget(message);
                if (tokenBudgetData) {
                    ws.send(createNormalizedMessage({ kind: 'status', text: 'token_budget', tokenBudget: tokenBudgetData, sessionId: capturedSessionId || sessionId || null, provider: 'claude' }));
                }
            }
        }
        // Clean up session on completion
        if (capturedSessionId) {
            removeSession(capturedSessionId);
        }
        // Clean up temporary image files
        await cleanupTempFiles(tempImagePaths, tempDir);
        // Send completion event
        ws.send(createNormalizedMessage({ kind: 'complete', exitCode: 0, isNewSession: !sessionId && !!command, sessionId: capturedSessionId, provider: 'claude' }));
        notifyRunStopped({
            userId: ws?.userId || null,
            provider: 'claude',
            sessionId: capturedSessionId || sessionId || null,
            sessionName: sessionSummary,
            stopReason: 'completed'
        });
        // Complete
    }
    catch (error) {
        console.error('SDK query error:', error);
        // Clean up session on error
        if (capturedSessionId) {
            removeSession(capturedSessionId);
        }
        // Clean up temporary image files on error
        await cleanupTempFiles(tempImagePaths, tempDir);
        // Check if Claude CLI is installed for a clearer error message
        const installed = await providerAuthService.isProviderInstalled('claude');
        const errorContent = !installed
            ? 'Claude Code is not installed. Please install it first: https://docs.anthropic.com/en/docs/claude-code'
            : error.message;
        // Send error to WebSocket
        ws.send(createNormalizedMessage({ kind: 'error', content: errorContent, sessionId: capturedSessionId || sessionId || null, provider: 'claude' }));
        notifyRunFailed({
            userId: ws?.userId || null,
            provider: 'claude',
            sessionId: capturedSessionId || sessionId || null,
            sessionName: sessionSummary,
            error
        });
    }
}
/**
 * Aborts an active SDK session
 * @param {string} sessionId - Session identifier
 * @returns {boolean} True if session was aborted, false if not found
 */
async function abortClaudeSDKSession(sessionId) {
    const session = getSession(sessionId);
    if (!session) {
        console.log(`Session ${sessionId} not found`);
        return false;
    }
    try {
        console.log(`Aborting SDK session: ${sessionId}`);
        // Call interrupt() on the query instance
        await session.instance.interrupt();
        // Update session status
        session.status = 'aborted';
        // Clean up temporary image files
        await cleanupTempFiles(session.tempImagePaths, session.tempDir);
        // Clean up session
        removeSession(sessionId);
        return true;
    }
    catch (error) {
        console.error(`Error aborting session ${sessionId}:`, error);
        return false;
    }
}
/**
 * Checks if an SDK session is currently active
 * @param {string} sessionId - Session identifier
 * @returns {boolean} True if session is active
 */
function isClaudeSDKSessionActive(sessionId) {
    const session = getSession(sessionId);
    return session && session.status === 'active';
}
/**
 * Gets all active SDK session IDs
 * @returns {Array<string>} Array of active session IDs
 */
function getActiveClaudeSDKSessions() {
    return getAllSessions();
}
/**
 * Get pending tool approvals for a specific session.
 * @param {string} sessionId - The session ID
 * @returns {Array} Array of pending permission request objects
 */
function getPendingApprovalsForSession(sessionId) {
    const pending = [];
    for (const [requestId, resolver] of pendingToolApprovals.entries()) {
        if (resolver._sessionId === sessionId) {
            pending.push({
                requestId,
                toolName: resolver._toolName || 'UnknownTool',
                input: resolver._input,
                context: resolver._context,
                sessionId,
                receivedAt: resolver._receivedAt || new Date(),
            });
        }
    }
    return pending;
}
/**
 * Reconnect a session's WebSocketWriter to a new raw WebSocket.
 * Called when client reconnects (e.g. page refresh) while SDK is still running.
 * @param {string} sessionId - The session ID
 * @param {Object} newRawWs - The new raw WebSocket connection
 * @returns {boolean} True if writer was successfully reconnected
 */
function reconnectSessionWriter(sessionId, newRawWs) {
    const session = getSession(sessionId);
    if (!session?.writer?.updateWebSocket)
        return false;
    session.writer.updateWebSocket(newRawWs);
    console.log(`[RECONNECT] Writer swapped for session ${sessionId}`);
    return true;
}
// Export public API
export { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive, getActiveClaudeSDKSessions, resolveToolApproval, getPendingApprovalsForSession, reconnectSessionWriter };
//# sourceMappingURL=claude-sdk.js.map