"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.startMCPServer = exports.buildStaticPreviewRefineResponse = exports.collectStaticPreviewRefineTargetInstructions = exports.buildVariantRequestBatchResponse = exports.buildMixedVariantBatchResponse = void 0;
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
const v3_1 = require("zod/v3");
const index_core_1 = require("../index-core");
const SessionBridgeService_1 = require("../services/SessionBridgeService");
const server_1 = require("../server");
const staticStarter_1 = require("../services/staticStarter");
const TelemetryService_1 = require("../services/TelemetryService");
const AuthService_1 = require("../services/AuthService");
const tools_1 = require("./auth/tools");
const open_1 = __importDefault(require("open"));
const index_1 = require("../index");
const evaluateFlags_1 = require("../config/evaluateFlags");
const skillWriter_1 = require("../utils/skillWriter");
const portUtils_1 = require("../utils/portUtils");
const agent_variants_1 = require("./agent-variants");
const child_process_1 = require("child_process");
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const util_1 = require("util");
const devServerCommand_1 = require("../utils/devServerCommand");
const createAgentVariantsOrchestrator_1 = require("../services/createAgentVariantsOrchestrator");
const changeBatchClassification_1 = require("./changeBatchClassification");
const log = (0, index_core_1.createLogger)('MCPServer');
const execAsync = (0, util_1.promisify)(child_process_1.exec);
/** Poll until the port responds to HTTP or the timeout is reached. */
async function waitForPort(port, host, timeoutMs = 60_000) {
    const deadline = Date.now() + timeoutMs;
    while (Date.now() < deadline) {
        try {
            await fetch(`http://${host}:${port}`, {
                signal: AbortSignal.timeout(1000),
            });
            return true;
        }
        catch {
            await new Promise((r) => setTimeout(r, 500));
        }
    }
    return false;
}
/** Stop a spawned process with graceful SIGTERM and SIGKILL fallback. */
async function stopChildProcess(child, timeoutMs = DEV_SERVER_STOP_TIMEOUT_MS) {
    if (child.killed || child.exitCode !== null || child.pid == null) {
        return;
    }
    const waitForExit = new Promise((resolve) => {
        const onExit = () => {
            child.off('exit', onExit);
            child.off('error', onExit);
            resolve();
        };
        child.once('exit', onExit);
        child.once('error', onExit);
    });
    try {
        child.kill('SIGTERM');
    }
    catch {
        // no-op; fallback still runs below if process persists
    }
    const timedOut = await Promise.race([
        waitForExit.then(() => false),
        new Promise((resolve) => setTimeout(() => resolve(true), timeoutMs)),
    ]);
    if (!timedOut) {
        return;
    }
    try {
        child.kill('SIGKILL');
    }
    catch {
        // best effort
    }
    await waitForExit;
}
const DEV_SERVER_STOP_TIMEOUT_MS = 5000;
const SUPERSEDED_CHANGES_MESSAGE = 'Queued changes were superseded by newer user input. Call watch_for_changes again.';
function normalizeFilesystemPath(inputPath) {
    if (!inputPath) {
        return '';
    }
    const resolvedPath = path_1.default.resolve(inputPath);
    try {
        return fs_1.default.realpathSync(resolvedPath);
    }
    catch {
        return resolvedPath;
    }
}
function pathsMatchExactly(pathA, pathB) {
    if (!pathA || !pathB) {
        return false;
    }
    return pathA === pathB;
}
async function getListeningPortPids(port) {
    try {
        const { stdout } = await execAsync(`lsof -nP -iTCP:${port} -sTCP:LISTEN -Fp`);
        const pids = stdout
            .split('\n')
            .map((line) => line.trim())
            .filter((line) => line.startsWith('p'))
            .map((line) => parseInt(line.slice(1), 10))
            .filter((pid) => Number.isInteger(pid) && pid > 0);
        return [...new Set(pids)];
    }
    catch {
        return [];
    }
}
async function getProcessCwd(pid) {
    if (!Number.isInteger(pid) || pid <= 0) {
        return null;
    }
    try {
        const { stdout } = await execAsync(`lsof -a -p ${pid} -d cwd -Fn`);
        const cwdLine = stdout
            .split('\n')
            .map((line) => line.trim())
            .find((line) => line.startsWith('n'));
        if (!cwdLine) {
            return null;
        }
        return cwdLine.slice(1) || null;
    }
    catch {
        return null;
    }
}
async function isPortOwnedByProject(port, projectPath) {
    if (!Number.isInteger(port) || port <= 0 || port >= 65536) {
        return {
            matches: false,
            reason: `Invalid port ${port} for project validation.`,
        };
    }
    const normalizedProjectPath = normalizeFilesystemPath(projectPath);
    if (!normalizedProjectPath) {
        return {
            matches: false,
            reason: 'Project path is invalid for strict server validation.',
        };
    }
    const candidatePids = await getListeningPortPids(port);
    if (candidatePids.length === 0) {
        return {
            matches: false,
            reason: `No listening process found on localhost:${port}.`,
        };
    }
    const observedProcessPaths = [];
    for (const pid of candidatePids) {
        const cwd = await getProcessCwd(pid);
        const normalizedCwd = normalizeFilesystemPath(cwd);
        if (!normalizedCwd) {
            observedProcessPaths.push(`${pid}:<unknown_cwd>`);
            continue;
        }
        observedProcessPaths.push(`${pid}:${normalizedCwd}`);
        if (pathsMatchExactly(normalizedProjectPath, normalizedCwd)) {
            return {
                matches: true,
                reason: `Process ${pid} is running from ${normalizedCwd}.`,
            };
        }
    }
    return {
        matches: false,
        reason: `A server is listening on localhost:${port}, but process paths do not match the selected project path. selected=${normalizedProjectPath}; observed=${observedProcessPaths.join(', ') || '<none>'}`,
        selectedProjectPath: normalizedProjectPath,
        observedProcessPaths,
    };
}
function detectFrameworkFromHtml(html) {
    const lower = html.toLowerCase();
    const hasViteClient = lower.includes('/@vite/client');
    const hasViteMarkers = lower.includes('vite') &&
        (lower.includes('hmr') || lower.includes('/src/main.'));
    if (hasViteClient || hasViteMarkers) {
        return 'vite';
    }
    const hasNextAssets = lower.includes('/_next/');
    const hasNextMarkers = lower.includes('__next_data__') || lower.includes('next-route-announcer');
    if (hasNextAssets || hasNextMarkers) {
        return 'nextjs';
    }
    return 'unknown';
}
function normalizeExpectedFramework(framework) {
    if (!framework)
        return 'unknown';
    const lower = framework.toLowerCase();
    if (lower.includes('vite'))
        return 'vite';
    if (lower.includes('next'))
        return 'nextjs';
    return 'unknown';
}
function isFrameworkCompatible(expected, detected) {
    if (expected === 'unknown')
        return true;
    return expected === detected;
}
async function validateProjectServer(options) {
    const { host, port, framework, projectPath } = options;
    const signals = [];
    const isReachable = await waitForPort(port, host, 3000);
    if (!isReachable) {
        return {
            valid: false,
            reason: `No server is responding on ${host}:${port}.`,
            detectedFramework: 'unknown',
            signals,
        };
    }
    signals.push('tcp_reachable');
    if (projectPath) {
        const projectMatch = await isPortOwnedByProject(port, projectPath);
        if (!projectMatch.matches) {
            return {
                valid: false,
                reason: projectMatch.reason,
                detectedFramework: 'unknown',
                signals,
                selectedProjectPath: projectMatch.selectedProjectPath,
                observedProcessPaths: projectMatch.observedProcessPaths,
            };
        }
        signals.push('path_match');
    }
    try {
        const response = await fetch(`http://${host}:${port}`, {
            signal: AbortSignal.timeout(3000),
        });
        const html = await response.text();
        const detectedFramework = detectFrameworkFromHtml(html);
        if (detectedFramework === 'vite') {
            signals.push('vite_marker');
        }
        if (detectedFramework === 'nextjs') {
            signals.push('next_marker');
        }
        const expected = normalizeExpectedFramework(framework);
        if (!isFrameworkCompatible(expected, detectedFramework)) {
            return {
                valid: false,
                reason: `Expected ${expected}, but detected ${detectedFramework} on ${host}:${port}.`,
                detectedFramework,
                signals,
                statusCode: response.status,
            };
        }
        if (detectedFramework === 'unknown' && expected !== 'unknown') {
            return {
                valid: false,
                reason: 'Server is reachable but did not expose framework markers needed for validation.',
                detectedFramework,
                signals,
                statusCode: response.status,
            };
        }
        return {
            valid: true,
            reason: `Validated ${host}:${port} as a project server.`,
            detectedFramework,
            signals,
            statusCode: response.status,
        };
    }
    catch (error) {
        const message = error instanceof Error ? error.message : 'Unknown error';
        return {
            valid: false,
            reason: `HTTP validation failed: ${message}`,
            detectedFramework: 'unknown',
            signals,
        };
    }
}
/**
 * Starts the Rivet MCP server on stdio transport.
 * Called when the user runs `rivet mcp`.
 */
function getSendButtonLabel(mcpEditor) {
    if (mcpEditor === 'cursor')
        return 'Send to Cursor';
    if (mcpEditor === 'codex')
        return 'Send to Codex';
    if (mcpEditor === 'claude-desktop')
        return 'Send to Claude';
    return 'Send to Claude Code';
}
function trackChangesQueuedFromIntent(intent, bridge, telemetry, sessionId) {
    const telemetryContext = bridge.getTelemetryContext();
    telemetry.trackChangesQueued({
        sessionId,
        styleChangeCount: intent.changes.filter((c) => c.kind === 'style').length,
        textChangeCount: intent.changes.filter((c) => c.kind === 'text').length,
        commentCount: intent.changes.filter((c) => c.kind === 'comment').length,
        chatCount: intent.changes.filter((c) => c.kind === 'chat').length,
        round: bridge.getRoundsCompleted(),
        traceId: intent.traceId,
        agentRunId: intent.agentRunId,
        browserSessionId: telemetryContext?.browserSessionId,
        browserDistinctId: telemetryContext?.browserDistinctId,
    });
}
const trackOrphanedMCPRun = (telemetry, sessionId, orphanedRun) => {
    telemetry.trackAgentRunFailed({
        traceId: orphanedRun.traceId,
        agentRunId: orphanedRun.agentRunId,
        sessionId,
        posthogSessionId: orphanedRun.browserSessionId,
        durationMs: Date.now() - orphanedRun.startedAt,
        errorType: 'mcp_run_replaced',
        errorMessage: 'A new MCP apply run started before the previous run completed',
        source: 'mcp',
    });
};
const APPLY_GUIDE = 'Read each file in sourceFiles, then apply every change in the changes array. ' +
    'style: find the element by className/xpath and update the CSS property from "from" to "to". ' +
    'text: find the element and replace the text content. ' +
    'comment: interpret the instruction using the element context and apply appropriate code changes. ' +
    'chat: interpret the free-text instruction in the "instruction" field and apply appropriate code changes across any file in the project. ' +
    'variant: a user-picked variant from the agent-variants flow. ' +
    'Variant protocol reminder: source_research (if present) -> report_source_context -> brief approval -> generation -> user commit. ' +
    'variant.payload.kind=diff-applied: the server has already `git apply`d the unified diff to the user\'s working tree (uncommitted). DO NOT re-apply — acknowledge the variant and let the user review. ' +
    'variant.payload.kind=diff (legacy): apply the unified diff in payload.diff against the project root via `git apply` (no manual edits). ' +
    'variant.payload.kind=project-created: the project has already been materialized at destinationPath and git-committed by the server. Optionally run `npm install` in destinationPath and continue from there — no further file operations are required for the pick itself. ' +
    'For elements with empty filePaths, search the codebase by className or xpath. ' +
    'If a change has imageCount > 0, the corresponding reference images appear as image content items immediately following this text in the tool result — view them before making changes. ' +
    'IMPORTANT — variantRequest branch: if a comment item carries `variantRequest: { count }`, do NOT edit any files. Instead, derive `target` from the comment\'s `element` using this priority: ' +
    '(a) prefer `{ type: "file", ref: filePaths[0] }` when `filePaths.length === 1`; otherwise ' +
    '(b) `rivetId` → `{ type: "element", ref: \'[data-rivet-id="<rivetId>"]\' }`, then ' +
    '(c) `id` (no whitespace) → `{ type: "element", ref: "#<id>" }`, then ' +
    '(d) `xpath` → `{ type: "element", ref: "<xpath>" }`, then ' +
    '(e) single-token `className` → `{ type: "element", ref: ".<token>" }`. ' +
    'Then call `propose_variants` with `{ prompt: instruction, count: variantRequest.count, target, projectContext }` and follow the full variants lifecycle: `report_variant_briefs` → `approve_variant_briefs` (only after explicit user confirmation; pass `confirmedByUser: true`) → `continue_variants` → `report_variant_complete`. ' +
    'CRITICAL WORKFLOW: 1. Call get_pending_changes with a message sharing your brief plan. 2. Edit the files. 3. Call get_pending_changes with a message summarizing what you changed and refresh_git: true so the Rivet UI updates. 4. STOP and let the user know their changes have been applied.';
/**
 * Prefix added to the apply guide when a batch targets a specific variant. The
 * variant's sources live in an isolated worktree, so all edits must happen
 * there (not the base project) for the change to land on the right variant.
 * The reference suffix is appended when any comment in the batch carries
 * `resolvedReferences` so the agent knows to consult those directories as
 * read-only context.
 */
const buildApplyGuide = (worktreePath, hasReferences) => {
    const targetPrefix = worktreePath
        ? `These changes target a specific variant whose sources live in an isolated git worktree at \`${worktreePath}\`. Treat that worktree as the project root: read and edit files relative to it, and run git/refresh commands inside it. Do NOT edit the base project for this batch. `
        : '';
    const referenceSuffix = hasReferences
        ? ' Some comments include `resolvedReferences` — each entry is a label, optional brief, and a directory path pointing to another variant\'s source. The user attached these as @-mention context; treat them as read-only and inspect with your Read/Grep tools as needed (do NOT edit files inside those paths). Use them to ground "make this more like X" style requests.'
        : '';
    return `${targetPrefix}${APPLY_GUIDE}${referenceSuffix}`;
};
const requestHasResolvedReferences = (request) => request.changes.some((c) => c.kind === 'comment' &&
    Array.isArray(c.resolvedReferences) &&
    c.resolvedReferences.length > 0);
const VARIANT_REQUEST_GUIDE = 'This batch is a Rivet variant request — do NOT edit files directly. ' +
    'The pending change is a single CommentChangeItem with `variantRequest.count`. ' +
    'Derive the `propose_variants.target` from the comment\'s `element` using this priority: ' +
    '(a) `{ type: "file", ref: filePaths[0] }` when `filePaths.length === 1`; otherwise ' +
    '(b) `rivetId` → `[data-rivet-id="<rivetId>"]`, then ' +
    '(c) `id` (no whitespace) → `#<id>`, then ' +
    '(d) `xpath` → `<xpath>`, then ' +
    '(e) single-token `className` → `.<token>`. ' +
    'Then call `propose_variants` with `{ prompt: instruction, count: variantRequest.count, target, projectContext }` and follow the full variants lifecycle: ' +
    '`report_variant_briefs` → `approve_variant_briefs` (only after explicit user confirmation; pass `confirmedByUser: true`) → `continue_variants` → `report_variant_complete`. ' +
    'After the user picks a variant via `commit_variant`, STOP and tell the user their variant has been committed.';
const VARIANT_MIXED_BATCH_GUIDE = 'Mixed batch rejected: a `variantRequest` comment must be the only item in its batch. ' +
    'Ask the user to send the variant request on its own (clear other pending changes first) and retry.';
const STATIC_PREVIEW_REFINE_GUIDE = 'STATIC_PREVIEW_REFINE_GUIDE: These changes target a static_preview variant. ' +
    'Do NOT edit base project files. Group changes by staticPreviewTarget, then call ' +
    "`continue_variants(action:'refine_variant', variantId, instruction)` once per distinct target with that group's combined instruction. " +
    'Regenerate each variant HTML, then call `report_variant_complete` for each leased refine work item with output.html containing the full updated static HTML.';
const buildSupersededChangesResponse = () => ({
    content: [
        {
            type: 'text',
            text: JSON.stringify({
                hasChanges: false,
                superseded: true,
                message: SUPERSEDED_CHANGES_MESSAGE,
            }),
        },
    ],
});
const buildMixedVariantBatchResponse = (rejectedRequest) => {
    if (rejectedRequest === undefined) {
        return buildSupersededChangesResponse();
    }
    return {
        content: [
            {
                type: 'text',
                text: JSON.stringify({
                    hasChanges: false,
                    rejected: true,
                    reason: 'mixed_variant_batch',
                    message: VARIANT_MIXED_BATCH_GUIDE,
                }),
            },
        ],
        isError: true,
    };
};
exports.buildMixedVariantBatchResponse = buildMixedVariantBatchResponse;
const buildVariantRequestBatchResponse = (consumedRequest) => {
    if (!consumedRequest) {
        return buildSupersededChangesResponse();
    }
    const { changes, imageContent } = (0, changeBatchClassification_1.extractImages)(consumedRequest.changes);
    return {
        content: [
            {
                type: 'text',
                text: JSON.stringify({
                    hasChanges: true,
                    kind: 'variant_request',
                    changes,
                    sourceFiles: consumedRequest.sourceFiles,
                    applyGuide: VARIANT_REQUEST_GUIDE,
                }),
            },
            ...imageContent,
        ],
    };
};
exports.buildVariantRequestBatchResponse = buildVariantRequestBatchResponse;
const staticPreviewTargetKey = (target) => `${target.sessionId}:${target.variantId}`;
const getCommentStaticPreviewTarget = (comment, fallbackTarget) => comment.staticPreviewTarget ?? fallbackTarget;
const collectStaticPreviewRefineTargetInstructions = (request) => {
    const groups = new Map();
    for (const change of request.changes) {
        if (change.kind !== 'comment')
            continue;
        const target = getCommentStaticPreviewTarget(change, request.staticPreviewTarget);
        if (!target)
            continue;
        const key = staticPreviewTargetKey(target);
        const existing = groups.get(key);
        if (existing) {
            existing.instruction = `${existing.instruction}\n\n${change.instruction}`;
        }
        else {
            groups.set(key, {
                target,
                instruction: change.instruction,
            });
        }
    }
    return [...groups.values()];
};
exports.collectStaticPreviewRefineTargetInstructions = collectStaticPreviewRefineTargetInstructions;
const buildStaticPreviewRefineResponse = (consumedRequest) => {
    if (!consumedRequest) {
        return buildSupersededChangesResponse();
    }
    const { changes, imageContent } = (0, changeBatchClassification_1.extractImages)(consumedRequest.changes);
    const targetInstructions = (0, exports.collectStaticPreviewRefineTargetInstructions)(consumedRequest);
    const staticPreviewTargets = targetInstructions.map(({ target }) => target);
    return {
        content: [
            {
                type: 'text',
                text: JSON.stringify({
                    hasChanges: true,
                    kind: 'static_preview_refine',
                    changes,
                    sourceFiles: consumedRequest.sourceFiles,
                    staticPreviewTarget: consumedRequest.staticPreviewTarget,
                    staticPreviewTargets,
                    applyGuide: STATIC_PREVIEW_REFINE_GUIDE,
                }),
            },
            ...imageContent,
        ],
    };
};
exports.buildStaticPreviewRefineResponse = buildStaticPreviewRefineResponse;
const startMCPServer = async (mcpEditor) => {
    // Redirect all logging to stderr so stdout is reserved for MCP protocol,
    // then silence all output — users shouldn't see Rivet logs in their terminal.
    // Error capture (_onError) still fires for PostHog since it bypasses loglevel.
    (0, index_core_1.redirectLogsToStderr)();
    // Default silent so MCP transport stays clean; opt in via env for debugging
    // (e.g. `RIVET_LOG_LEVEL=debug` in the MCP client's env block).
    (0, index_core_1.setLogLevel)(process.env.RIVET_LOG_LEVEL ?? 'SILENT');
    // Refresh the Claude skill at startup — NOT only after a successful
    // open_visual_editor (the other call sites below). Otherwise stale guidance
    // that refuses an action deadlocks: e.g. a v22 skill that won't open an empty
    // folder means open_visual_editor never runs, so the skill never rewrites,
    // so the agent keeps refusing forever. Writing on boot guarantees the agent
    // loads current guidance on its next turn. Idempotent + version-gated.
    // (Cursor rules stay post-open: they're project-scoped and the project path
    // isn't known until the editor opens.)
    if (mcpEditor === 'claude' || mcpEditor === undefined) {
        (0, skillWriter_1.writeSkillFileIfNeeded)();
    }
    const sendButtonLabel = getSendButtonLabel(mcpEditor);
    const bridge = new SessionBridgeService_1.SessionBridgeService();
    const telemetry = new TelemetryService_1.TelemetryService({
        enabled: true,
        source: 'mcp',
        editor: mcpEditor,
    });
    const authService = new AuthService_1.AuthService();
    const requireAuth = (0, tools_1.createAuthGuard)(authService);
    // Agent-driven variants orchestrator. Long-lived singleton: state machine +
    // worktree lifecycle for any variants session, surfaced via the 5 MCP tools
    // (registered below near mcp.connect) and the /api/variants Express routes
    // (mounted by startServer when this orchestrator is passed in).
    const agentVariantsOrchestrator = (0, createAgentVariantsOrchestrator_1.createAgentVariantsOrchestrator)({
        bridge,
        telemetry,
    });
    /** Complete the active run correlation and refresh git status in the Rivet UI. */
    const handleRefreshGit = (sessionId) => {
        const activeRunCorrelation = bridge.completeActiveRunCorrelation();
        if (activeRunCorrelation) {
            telemetry.trackAgentRunCompleted({
                traceId: activeRunCorrelation.traceId,
                agentRunId: activeRunCorrelation.agentRunId,
                sessionId,
                posthogSessionId: activeRunCorrelation.browserSessionId,
                durationMs: Date.now() - activeRunCorrelation.startedAt,
                source: 'mcp',
            });
        }
        bridge.reportFeedback('__RIVET_REFRESH_GIT__');
    };
    /**
     * Start tracking an MCP apply run after intent pickup.
     */
    const beginApplyingRun = (intent, sessionId) => {
        bridge.setStatus('APPLYING');
        const { newRun, orphanedRun } = bridge.startApplyingRun(intent);
        bridge.clearPending();
        trackChangesQueuedFromIntent(intent, bridge, telemetry, sessionId);
        if (orphanedRun) {
            trackOrphanedMCPRun(telemetry, sessionId, orphanedRun);
        }
        telemetry.trackAgentRunStarted({
            traceId: newRun.traceId,
            agentRunId: newRun.agentRunId,
            instructionLength: 0,
            elementCount: intent.changes.length,
            posthogSessionId: newRun.browserSessionId,
            resumeSessionId: sessionId,
            source: 'mcp',
        });
    };
    const mcp = new mcp_js_1.McpServer({ name: 'rivet', version: '0.1.0' }, { capabilities: { tools: {} } });
    // Every substantive tool is gated behind login: register them on `guardedMcp`
    // so an unauthenticated call returns AUTH_REQUIRED instead of running. The
    // login/logout/auth_status tools register on the raw `mcp` so they stay
    // reachable while logged out.
    const guardedMcp = (0, tools_1.withAuthGuard)(mcp, requireAuth);
    // Track the Express server
    let serverHandle = null;
    let sessionStartedAt = null;
    // Track a dev server we auto-started so we can kill it on close
    let devServerProcess = null;
    let devServerOwnership = 'none';
    let devServerHost = null;
    let devServerPort = null;
    let rivetServerPort = null;
    let rivetServerUrl = null;
    let isCleanupRunning = false;
    const updateBridgeHealth = (reason, isReachable) => {
        bridge.updateDevServerHealth({
            reason,
            isReachable,
            lastCheckedAt: Date.now(),
        });
    };
    const cleanupSessionResources = async (options) => {
        if (isCleanupRunning) {
            return { totalRoundsCompleted: 0, sessionId: null };
        }
        isCleanupRunning = true;
        try {
            const activeSessionId = bridge.getSessionId();
            const sessionIdToClose = options?.closeSessionId ??
                (bridge.isActive() && activeSessionId ? activeSessionId : null);
            const telemetryContext = bridge.getTelemetryContext();
            const activeRunCorrelation = bridge.completeActiveRunCorrelation();
            let totalRoundsCompleted = 0;
            if (sessionIdToClose && bridge.isActive()) {
                totalRoundsCompleted = bridge.endSession();
            }
            if (serverHandle) {
                try {
                    await serverHandle.close();
                }
                catch (error) {
                    log.warn('Error stopping Express server:', error);
                }
                serverHandle = null;
            }
            if (devServerOwnership === 'rivet_owned' && devServerProcess) {
                try {
                    await stopChildProcess(devServerProcess);
                }
                catch (error) {
                    log.warn('Error stopping dev server process:', error);
                }
            }
            devServerProcess = null;
            devServerOwnership = 'none';
            devServerHost = null;
            devServerPort = null;
            rivetServerPort = null;
            rivetServerUrl = null;
            updateBridgeHealth('rivet_unreachable', false);
            const durationMs = sessionStartedAt ? Date.now() - sessionStartedAt : 0;
            sessionStartedAt = null;
            if (options?.trackSessionEnd !== false && sessionIdToClose) {
                telemetry.trackMCPSessionEnd({
                    sessionId: sessionIdToClose,
                    rounds: totalRoundsCompleted,
                    durationMs,
                    browserSessionId: telemetryContext?.browserSessionId,
                    browserDistinctId: telemetryContext?.browserDistinctId,
                });
                if (activeRunCorrelation) {
                    telemetry.trackAgentRunFailed({
                        traceId: activeRunCorrelation.traceId,
                        agentRunId: activeRunCorrelation.agentRunId,
                        sessionId: sessionIdToClose,
                        posthogSessionId: activeRunCorrelation.browserSessionId,
                        durationMs: Date.now() - activeRunCorrelation.startedAt,
                        errorType: 'mcp_session_closed',
                        errorMessage: 'MCP session closed before apply completion confirmation',
                        source: 'mcp',
                    });
                }
            }
            await telemetry.flush();
            return {
                totalRoundsCompleted,
                sessionId: sessionIdToClose,
            };
        }
        finally {
            isCleanupRunning = false;
        }
    };
    // Flush telemetry + resources before process exits
    const shutdown = async () => {
        await cleanupSessionResources({ trackSessionEnd: true });
        await telemetry.shutdown();
    };
    process.on('SIGTERM', async () => {
        await shutdown();
        process.exit(0);
    });
    process.on('SIGINT', async () => {
        await shutdown();
        process.exit(0);
    });
    // Flush when the event loop empties (e.g., MCP transport disconnects)
    process.on('beforeExit', async () => {
        await shutdown();
    });
    /**
     * Ensure zero-to-one variant sessions have a Rivet UI shell subscribed to
     * the shared variants orchestrator. Fresh projects do not have a base app
     * server yet, so the shell starts with the existing proxy path and waits for
     * generated variant dev servers to become selectable.
     */
    const ensureZeroToOneVisualEditorOpen = async (input) => {
        // The bridge tracks the explicit workspaceRoot — the user's working
        // dir, which owns `.rivet/`. It stays stable across sequential
        // zero-to-one sessions so `bridge.getProjectPath()` resolves to the
        // same `.rivet/variants/` root that success-time persistence writes
        // to. Without this, each new session would rebind to its own subdir
        // and the history panel would only see that one session's variants.
        const { workspaceRoot } = input;
        if (bridge.isActive()) {
            if (!rivetServerUrl || rivetServerPort === null) {
                throw new Error('Rivet visual editor session is active but URL metadata is unavailable');
            }
            const activeEditorSessionId = bridge.getSessionId();
            if (!activeEditorSessionId) {
                throw new Error('Rivet visual editor session is active but has no session ID');
            }
            // Only rebind when the user has moved to a different workspace —
            // not when they spawn another subproject in the same one.
            if (bridge.getProjectPath() !== workspaceRoot) {
                bridge.rebindProjectPath(workspaceRoot);
            }
            try {
                await (0, open_1.default)(rivetServerUrl);
            }
            catch {
                log.warn('Failed to focus existing Rivet browser automatically');
            }
            return {
                sessionId: activeEditorSessionId,
                url: rivetServerUrl,
                rivetPort: rivetServerPort,
                opened: false,
                attached: true,
            };
        }
        let nextRivetPort;
        try {
            nextRivetPort = await (0, portUtils_1.findAvailablePort)(index_1.DEFAULT_PORT);
        }
        catch (error) {
            log.warn(`Port discovery failed for zero-to-one editor, falling back to ${index_1.DEFAULT_PORT}:`, error);
            nextRivetPort = index_1.DEFAULT_PORT;
        }
        let retries = 0;
        const maxRetries = 5;
        while (!serverHandle && retries < maxRetries) {
            try {
                serverHandle = await (0, server_1.startServer)({
                    framework: input.framework,
                    projectPath: input.projectPath,
                    userPort: 0,
                    userHost: 'localhost',
                    sessionBridge: bridge,
                    agentVariantsOrchestrator,
                    telemetry,
                    mcpEditor,
                    skipProcessHandlers: true,
                    rivetPort: nextRivetPort,
                    isGitEnabled: false,
                });
            }
            catch (error) {
                if (error?.code !== 'EADDRINUSE') {
                    throw error;
                }
                retries++;
                nextRivetPort = await (0, portUtils_1.findAvailablePort)(nextRivetPort + 1);
                log.debug(`Zero-to-one editor port in use, retrying with port ${nextRivetPort} (attempt ${retries}/${maxRetries})`);
            }
        }
        if (!serverHandle) {
            throw new Error(`Could not start Rivet server after ${maxRetries} attempts.`);
        }
        const editorSessionId = bridge.startSession(workspaceRoot);
        sessionStartedAt = Date.now();
        devServerOwnership = 'none';
        devServerHost = 'localhost';
        devServerPort = null;
        bridge.setDevServerContext({
            ownership: devServerOwnership,
            host: devServerHost,
            port: devServerPort,
            startedByRivet: false,
        });
        updateBridgeHealth('unknown', null);
        telemetry.trackMCPSessionStart({
            framework: input.framework,
            devServerStarted: false,
            sessionId: editorSessionId,
            mcpEditor,
        });
        // Mirror the writeSkillFileIfNeeded / writeCursorRulesIfNeeded call
        // that open_visual_editor performs after a successful open. The
        // unified start_variants flow opens the editor via this hook instead
        // of through the open_visual_editor tool, so without this the
        // SKILL.md / rivet.mdc file would never be rewritten when only
        // start_variants is used.
        if (mcpEditor === 'cursor') {
            (0, skillWriter_1.writeCursorRulesIfNeeded)(input.projectPath);
        }
        else if (mcpEditor === 'claude' || mcpEditor === undefined) {
            (0, skillWriter_1.writeSkillFileIfNeeded)();
        }
        const url = `http://localhost:${nextRivetPort}/rivet`;
        rivetServerPort = nextRivetPort;
        rivetServerUrl = url;
        try {
            await (0, open_1.default)(url);
        }
        catch {
            log.warn('Failed to open browser automatically');
        }
        return {
            sessionId: editorSessionId,
            url,
            rivetPort: nextRivetPort,
            opened: true,
            attached: false,
        };
    };
    // Define schemas separately to avoid TS2589 "excessively deep" type inference
    const detectProjectArgs = {
        projectPath: v3_1.z
            .string()
            .describe('Path to the project directory. Defaults to current working directory.')
            .optional(),
    };
    const openEditorArgs = {
        projectPath: v3_1.z
            .string()
            .describe('Path to the project directory. Defaults to current working directory.')
            .optional(),
        framework: v3_1.z
            .string()
            .describe('Project framework. Use the framework value from detect_project.')
            .optional(),
        startPort: v3_1.z
            .number()
            .describe('Preferred starting port for the dev server. Rivet will find the next free port at or above this value. Falls back to the framework default if omitted.')
            .optional(),
        startupMode: v3_1.z
            .enum(['manual_existing', 'ai_startup'])
            .describe('How to connect the dev server: manual_existing uses an already-running server, ai_startup auto-starts one. Optional; if omitted, Rivet defaults to ai_startup.')
            .optional(),
        existingPort: v3_1.z
            .number()
            .describe('Port of an already-running dev server. Required for startupMode manual_existing, and reused for follow-up calls after nextStep responses.')
            .optional(),
        forceAttachExternal: v3_1.z
            .boolean()
            .describe('Confirmation flag for startupMode manual_existing when validation reports a project mismatch. Set true only after user confirms attaching anyway.')
            .optional(),
    };
    const sessionIdArgs = {
        sessionId: v3_1.z
            .string()
            .describe('The session ID returned by open_visual_editor'),
    };
    guardedMcp.tool('detect_project', 'Detect the project framework, package manager, dev command, and default port. Also checks whether a project dev server is currently reachable and reports its endpoint when found. Call this before open_visual_editor to decide startupMode.', detectProjectArgs, async (args) => {
        const projectPath = args.projectPath || process.cwd();
        const detection = new index_core_1.ProjectDetectionService();
        let framework;
        let packageManager;
        try {
            framework = detection.detectFramework(projectPath);
            packageManager = await detection.detectPackageManager(projectPath);
        }
        catch (error) {
            const message = error instanceof Error ? error.message : 'Unknown error';
            return {
                content: [
                    {
                        type: 'text',
                        text: JSON.stringify({
                            error: `Could not detect project: ${message}`,
                            suggestion: 'Ensure you are in a Node.js project directory with a package.json',
                        }),
                    },
                ],
                isError: true,
            };
        }
        const devConfig = devServerCommand_1.FRAMEWORK_DEV_CONFIG[framework] ?? {
            devCommand: 'dev',
            defaultPort: 3000,
        };
        const runningServer = framework === 'static'
            ? null
            : await detection.findRunningDevServer(projectPath, [index_1.DEFAULT_PORT]);
        return {
            content: [
                {
                    type: 'text',
                    text: JSON.stringify({
                        framework,
                        packageManager,
                        devCommand: devConfig.devCommand,
                        defaultPort: devConfig.defaultPort,
                        projectPath,
                        devServerRunning: Boolean(runningServer),
                        detectedPort: runningServer?.port ?? null,
                        detectedHost: runningServer?.host ?? null,
                        startCommand: devConfig.devCommand
                            ? `${packageManager} run ${devConfig.devCommand}`
                            : null,
                    }),
                },
            ],
        };
    });
    guardedMcp.tool('open_visual_editor', "Start a Rivet visual editing session. Call detect_project first to get the framework. If the response has opened: false, follow the nextStep/nextAction instructions to recover. Once opened, follow the next_step instructions exactly.", openEditorArgs, async (args) => {
        if (bridge.isActive()) {
            return {
                content: [
                    {
                        type: 'text',
                        text: JSON.stringify({
                            error: 'Session already active',
                            sessionId: bridge.getSessionId(),
                        }),
                    },
                ],
                isError: true,
            };
        }
        const projectPath = args.projectPath || process.cwd();
        // When the caller omits framework (e.g. "open rivet" without a prior
        // detect_project), detect it from the workspace rather than assuming a
        // dev-server framework. An empty or HTML-only folder resolves to
        // 'static', so the editor opens in static mode instead of trying to
        // boot a Vite/Next dev server that isn't there. Detection only throws
        // for a nonexistent path; fall back to the prior 'nextjs' default.
        let framework;
        if (args.framework) {
            framework = args.framework;
        }
        else {
            try {
                framework = new index_core_1.ProjectDetectionService().detectFramework(projectPath);
            }
            catch {
                framework = 'nextjs';
            }
        }
        const requestedStartupMode = args.startupMode;
        const startupMode = requestedStartupMode ?? 'ai_startup';
        let nextDevServerPort = null;
        const nextDevServerHost = 'localhost';
        let nextDevServerOwnership = 'none';
        const startupModeSelection = requestedStartupMode ? 'explicit' : 'auto';
        if (framework !== 'static') {
            const frameworkConfig = devServerCommand_1.FRAMEWORK_DEV_CONFIG[framework] ?? {
                devCommand: 'dev',
                defaultPort: 3000,
            };
            const detection = new index_core_1.ProjectDetectionService();
            if (!frameworkConfig.devCommand) {
                return {
                    content: [
                        {
                            type: 'text',
                            text: JSON.stringify({
                                error: 'Cannot auto-start: no dev command configured for this framework.',
                                suggestion: 'Start the dev server manually, then call open_visual_editor again.',
                            }),
                        },
                    ],
                    isError: true,
                };
            }
            if (startupMode === 'manual_existing') {
                const existingPort = args.existingPort ?? args.startPort ?? null;
                if (!existingPort) {
                    return {
                        content: [
                            {
                                type: 'text',
                                text: JSON.stringify({
                                    error: 'startupMode manual_existing requires an existingPort value.',
                                    suggestion: 'Pass existingPort for your running dev server.',
                                }),
                            },
                        ],
                        isError: true,
                    };
                }
                const manualValidation = await validateProjectServer({
                    host: nextDevServerHost,
                    port: existingPort,
                    framework,
                    projectPath,
                });
                if (!manualValidation.valid) {
                    if (args.forceAttachExternal) {
                        const isReachable = await waitForPort(existingPort, nextDevServerHost, 3000);
                        if (!isReachable) {
                            return {
                                content: [
                                    {
                                        type: 'text',
                                        text: JSON.stringify({
                                            error: `Cannot attach to ${nextDevServerHost}:${existingPort} because it is not reachable.`,
                                            validation: manualValidation,
                                        }),
                                    },
                                ],
                                isError: true,
                            };
                        }
                        nextDevServerPort = existingPort;
                        nextDevServerOwnership = 'external';
                        log.info(`Force-attached to external dev server on ${nextDevServerHost}:${nextDevServerPort}`);
                        updateBridgeHealth('healthy', true);
                    }
                    else if (manualValidation.selectedProjectPath ||
                        (manualValidation.observedProcessPaths?.length ?? 0) > 0) {
                        return {
                            content: [
                                {
                                    type: 'text',
                                    text: JSON.stringify({
                                        opened: false,
                                        nextStep: 'mismatch_confirmation_required',
                                        error: manualValidation.reason,
                                        existingServerPort: existingPort,
                                        selectedProjectPath: manualValidation.selectedProjectPath,
                                        observedProcessPaths: manualValidation.observedProcessPaths,
                                        validation: manualValidation,
                                        nextAction: 'If the user confirms, call open_visual_editor again with startupMode: "manual_existing", existingPort, and forceAttachExternal: true. Otherwise ask for another port.',
                                    }),
                                },
                            ],
                            isError: false,
                        };
                    }
                    else {
                        return {
                            content: [
                                {
                                    type: 'text',
                                    text: JSON.stringify({
                                        error: manualValidation.reason,
                                        validation: manualValidation,
                                    }),
                                },
                            ],
                            isError: true,
                        };
                    }
                }
                if (!nextDevServerPort) {
                    nextDevServerPort = existingPort;
                    nextDevServerOwnership = 'external';
                    log.info(`Using existing dev server on ${nextDevServerHost}:${nextDevServerPort}`);
                    updateBridgeHealth('healthy', true);
                }
            }
            else {
                const packageManager = await detection.detectPackageManager(projectPath);
                const preferredPort = args.startPort ??
                    detection.readConfiguredPort(projectPath) ??
                    frameworkConfig.defaultPort;
                nextDevServerPort = await (0, portUtils_1.findAvailablePort)(preferredPort);
                const { cmd, args: cmdArgs, env, } = (0, devServerCommand_1.buildDevServerCommand)(framework, packageManager, frameworkConfig.devCommand, nextDevServerPort);
                log.info(`Auto-starting dev server: ${cmd} ${cmdArgs.join(' ')} in ${projectPath}`);
                devServerProcess = (0, child_process_1.spawn)(cmd, cmdArgs, {
                    cwd: projectPath,
                    env: { ...process.env, ...env },
                    stdio: 'pipe',
                    detached: false,
                });
                nextDevServerOwnership = 'rivet_owned';
                devServerProcess.on('error', (err) => {
                    log.warn('Dev server process error:', err);
                });
                devServerProcess.on('exit', (code) => {
                    log.info(`Dev server exited with code ${code}`);
                    devServerProcess = null;
                    if (bridge.isActive()) {
                        updateBridgeHealth('upstream_unreachable', false);
                    }
                });
                devServerProcess.stdout?.on('data', (data) => {
                    log.debug(`[dev-server] ${data.toString().trim()}`);
                });
                devServerProcess.stderr?.on('data', (data) => {
                    log.debug(`[dev-server] ${data.toString().trim()}`);
                });
                const ready = await waitForPort(nextDevServerPort, nextDevServerHost, 30_000);
                if (!ready) {
                    if (devServerProcess) {
                        await stopChildProcess(devServerProcess);
                    }
                    devServerProcess = null;
                    const existingServer = await detection.findRunningDevServer(projectPath, [index_1.DEFAULT_PORT]);
                    return {
                        content: [
                            {
                                type: 'text',
                                text: JSON.stringify({
                                    opened: false,
                                    nextStep: 'manual_port_required',
                                    error: `Dev server did not become ready on port ${nextDevServerPort} within 30s.`,
                                    existingServerPort: existingServer?.port ?? null,
                                    validation: null,
                                    nextAction: 'Call open_visual_editor again with startupMode: "manual_existing" and existingPort set to a running app server port.',
                                }),
                            },
                        ],
                        isError: false,
                    };
                }
                const startupValidation = await validateProjectServer({
                    host: nextDevServerHost,
                    port: nextDevServerPort,
                    framework,
                    projectPath,
                });
                if (!startupValidation.valid) {
                    if (devServerProcess) {
                        await stopChildProcess(devServerProcess);
                    }
                    devServerProcess = null;
                    const existingServer = await detection.findRunningDevServer(projectPath, [index_1.DEFAULT_PORT]);
                    return {
                        content: [
                            {
                                type: 'text',
                                text: JSON.stringify({
                                    opened: false,
                                    nextStep: 'manual_port_required',
                                    error: startupValidation.reason,
                                    existingServerPort: existingServer?.port ?? null,
                                    validation: startupValidation,
                                    nextAction: 'Call open_visual_editor again with startupMode: "manual_existing" and existingPort set to a running app server port.',
                                }),
                            },
                        ],
                        isError: false,
                    };
                }
                updateBridgeHealth('healthy', true);
                log.info(`Dev server ready on ${nextDevServerHost}:${nextDevServerPort}`);
            }
        }
        // Find a free port for Rivet's Express server (avoids conflict with desktop or other Rivet instances)
        let isGitEnabledForSession = true;
        try {
            const featureFlags = await (0, evaluateFlags_1.getFeatureFlags)();
            isGitEnabledForSession = featureFlags.gitUi.isEnabled;
        }
        catch (error) {
            log.warn('Failed to evaluate feature flags for MCP session startup; defaulting git to enabled:', error);
        }
        // An empty static workspace has no index.html for the editor to render —
        // scaffold a minimal starter canvas so the open is self-contained rather
        // than relying on the agent to create one. No-op when a page already
        // exists or it's a .rivet history workspace.
        if (framework === 'static') {
            (0, staticStarter_1.ensureStaticStarterCanvas)(projectPath);
        }
        let rivetPort;
        try {
            rivetPort = await (0, portUtils_1.findAvailablePort)(index_1.DEFAULT_PORT);
            log.info(`Searching for available port starting from ${index_1.DEFAULT_PORT}... found ${rivetPort}`);
        }
        catch (error) {
            log.warn(`Port discovery failed, falling back to ${index_1.DEFAULT_PORT}:`, error);
            rivetPort = index_1.DEFAULT_PORT;
        }
        // Start the Rivet Express server (with retry on EADDRINUSE to avoid races)
        let retries = 0;
        const maxRetries = 5;
        let serverStarted = false;
        while (!serverStarted && retries < maxRetries) {
            try {
                serverHandle = await (0, server_1.startServer)({
                    framework,
                    projectPath,
                    userPort: nextDevServerPort ?? 0,
                    userHost: nextDevServerHost,
                    sessionBridge: bridge,
                    agentVariantsOrchestrator,
                    telemetry,
                    mcpEditor,
                    skipProcessHandlers: true,
                    rivetPort,
                    isGitEnabled: isGitEnabledForSession,
                });
                serverStarted = true;
            }
            catch (error) {
                if (error?.code === 'EADDRINUSE') {
                    retries++;
                    rivetPort = await (0, portUtils_1.findAvailablePort)(rivetPort + 1);
                    log.debug(`Port in use, retrying with port ${rivetPort} (attempt ${retries}/${maxRetries})`);
                }
                else {
                    const message = error instanceof Error ? error.message : 'Server start failed';
                    if (devServerProcess) {
                        await stopChildProcess(devServerProcess);
                    }
                    devServerProcess = null;
                    return {
                        content: [
                            {
                                type: 'text',
                                text: JSON.stringify({ error: message }),
                            },
                        ],
                        isError: true,
                    };
                }
            }
        }
        if (!serverStarted) {
            if (devServerProcess) {
                await stopChildProcess(devServerProcess);
            }
            devServerProcess = null;
            return {
                content: [
                    {
                        type: 'text',
                        text: JSON.stringify({
                            error: `Could not start Rivet server after ${maxRetries} attempts.`,
                        }),
                    },
                ],
                isError: true,
            };
        }
        const sessionId = bridge.startSession(projectPath);
        sessionStartedAt = Date.now();
        devServerOwnership = nextDevServerOwnership;
        devServerHost = nextDevServerHost;
        devServerPort = nextDevServerPort;
        bridge.setDevServerContext({
            ownership: devServerOwnership,
            host: devServerHost,
            port: devServerPort,
            startedByRivet: devServerOwnership === 'rivet_owned',
        });
        if (devServerOwnership === 'none') {
            updateBridgeHealth('unknown', null);
        }
        else {
            updateBridgeHealth('healthy', true);
        }
        // Write editor-specific guidance file on first successful open
        if (mcpEditor === 'cursor') {
            (0, skillWriter_1.writeCursorRulesIfNeeded)(projectPath);
        }
        else if (mcpEditor === 'claude' || mcpEditor === undefined) {
            (0, skillWriter_1.writeSkillFileIfNeeded)();
        }
        // Track session start
        telemetry.trackMCPSessionStart({
            framework,
            devServerStarted: devServerOwnership === 'rivet_owned',
            sessionId,
            mcpEditor,
        });
        // Open browser. Static mode serves the Rivet shell at `/` (server.ts
        // mounts the UI there because there's no upstream dev server to proxy
        // through `/`); every other framework serves the shell at `/rivet` so
        // the user's app keeps `/`.
        const url = `http://localhost:${rivetPort}${framework === 'static' ? '/' : '/rivet'}`;
        rivetServerPort = rivetPort;
        rivetServerUrl = url;
        try {
            await (0, open_1.default)(url);
        }
        catch {
            log.warn('Failed to open browser automatically');
        }
        return {
            content: [
                {
                    type: 'text',
                    text: JSON.stringify({
                        sessionId,
                        url,
                        projectPath,
                        framework,
                        rivetPort,
                        opened: true,
                        devServerPort: nextDevServerPort,
                        devServerOwnership: nextDevServerOwnership,
                        startupMode,
                        startupModeSelection,
                        requestedStartupMode,
                        isGitEnabled: isGitEnabledForSession,
                        next_step: mcpEditor === 'cursor' || mcpEditor === 'codex' || mcpEditor === 'claude-desktop'
                            ? `Rivet is open. Tell the user: "Rivet is ready — make your visual changes and click '${sendButtonLabel}'." Then call watch_for_changes({ sessionId: "${sessionId}" }) to wait for their changes. After applying, STOP and ask the user if they want to keep watching or go back to normal prompting.`
                            : `Rivet is open. Tell the user: "Rivet is ready — make your visual changes and click '${sendButtonLabel}'."

IMPORTANT — Start a background monitor so you automatically detect when the user sends changes. Use the Monitor tool:
  description: "Rivet changes"
  persistent: true
  command: while sleep 2; do if curl -sf http://localhost:${rivetPort}/api/mcp/status | grep -q '"hasPendingIntent":true'; then echo "Rivet: user sent changes - call get_pending_changes to apply"; exit 0; fi; done

When the monitor notifies you, call get_pending_changes({ sessionId: "${sessionId}" }) to retrieve the changes and apply them. After applying, restart the monitor to watch for the next round. You are also free to handle other requests while the monitor runs.

If the Monitor tool is not available, call watch_for_changes({ sessionId: "${sessionId}" }) instead to block until the user sends changes.`,
                    }),
                },
            ],
        };
    });
    guardedMcp.tool('watch_for_changes', `Block until the user clicks "${sendButtonLabel}" in Rivet, then return their changes. If it times out (hasChanges: false), call it again to keep waiting. When hasChanges: true, apply the changes, then STOP and ask the user: "Want me to keep watching for more Rivet changes, or go back to normal prompting?"`, {
        sessionId: v3_1.z
            .string()
            .describe('The session ID returned by open_visual_editor'),
        timeoutSeconds: v3_1.z
            .number()
            .min(1)
            .max(600)
            .optional()
            .describe('How long to wait for the user to send changes (default: 600, max: 600)'),
        message: v3_1.z
            .string()
            .optional()
            .describe('A progress or summary message to show in the Rivet UI before waiting (e.g. your plan or what you just changed)'),
        refresh_git: v3_1.z
            .boolean()
            .optional()
            .describe('Set to true after writing files to trigger a git status refresh in the Rivet UI'),
    }, async (args) => {
        if (!bridge.isActive() || bridge.getSessionId() !== args.sessionId) {
            return {
                content: [
                    {
                        type: 'text',
                        text: JSON.stringify({ error: 'No active session with that ID' }),
                    },
                ],
                isError: true,
            };
        }
        const timeoutMs = Math.min((args.timeoutSeconds ?? 600) * 1000, 600_000);
        if (args.message) {
            bridge.reportFeedback(args.message);
        }
        if (args.refresh_git) {
            handleRefreshGit(args.sessionId);
        }
        bridge.setStatus('READY');
        let intent;
        try {
            intent = await bridge.waitForChanges(timeoutMs);
        }
        catch {
            const health = bridge.getDevServerHealth();
            return {
                content: [
                    {
                        type: 'text',
                        text: JSON.stringify({
                            hasChanges: false,
                            message: 'Timed out waiting for changes. Call watch_for_changes again to keep waiting.',
                            devServerHealth: health,
                        }),
                    },
                ],
            };
        }
        if ((0, changeBatchClassification_1.isMixedVariantBatch)(intent)) {
            const rejectedRequest = bridge.rejectMixedVariantRequestBatch(intent);
            return (0, exports.buildMixedVariantBatchResponse)(rejectedRequest);
        }
        if ((0, changeBatchClassification_1.isSingleVariantRequestBatch)(intent)) {
            const consumedRequest = bridge.consumeVariantRequestBatch(intent);
            return (0, exports.buildVariantRequestBatchResponse)(consumedRequest);
        }
        if ((0, changeBatchClassification_1.isStaticPreviewRefineBatch)(intent)) {
            const consumedRequest = bridge.consumeStaticPreviewRefineBatch(intent);
            if (consumedRequest) {
                const targetInstructions = (0, exports.collectStaticPreviewRefineTargetInstructions)(consumedRequest);
                const sessionId = targetInstructions[0]?.target.sessionId;
                if (sessionId) {
                    agentVariantsOrchestrator.beginStaticPreviewRefinements({
                        sessionId,
                        targets: targetInstructions.map(({ target, instruction }) => ({
                            variantId: target.variantId,
                            instruction,
                        })),
                    });
                }
            }
            return (0, exports.buildStaticPreviewRefineResponse)(consumedRequest);
        }
        beginApplyingRun(intent, args.sessionId);
        const { changes, imageContent } = (0, changeBatchClassification_1.extractImages)(intent.changes);
        return {
            content: [
                {
                    type: 'text',
                    text: JSON.stringify({
                        hasChanges: true,
                        changes,
                        sourceFiles: intent.sourceFiles,
                        ...(intent.target ? { target: intent.target } : {}),
                        ...(intent.worktreePath
                            ? { worktreePath: intent.worktreePath }
                            : {}),
                        applyGuide: buildApplyGuide(intent.worktreePath, requestHasResolvedReferences(intent)),
                    }),
                },
                ...imageContent,
            ],
        };
    });
    guardedMcp.tool('get_pending_changes', `Pick up pending design changes from the Rivet visual editor. Call this when the user asks you to apply Rivet changes (e.g. "apply", "check rivet", "apply changes"). Returns hasChanges: false if no changes are queued yet. Also use this to report feedback to the Rivet UI: pass "message" to share your plan or summarize what you changed, and set refresh_git: true after writing files so the UI reflects the updated git status.`, {
        sessionId: v3_1.z
            .string()
            .describe('The session ID returned by open_visual_editor'),
        message: v3_1.z
            .string()
            .optional()
            .describe('A progress or summary message to show in the Rivet UI (e.g. your plan or what you just changed)'),
        refresh_git: v3_1.z
            .boolean()
            .optional()
            .describe('Set to true after writing files to trigger a git status refresh in the Rivet UI'),
    }, async (args) => {
        if (!bridge.isActive() || bridge.getSessionId() !== args.sessionId) {
            return {
                content: [
                    {
                        type: 'text',
                        text: JSON.stringify({ error: 'No active session with that ID' }),
                    },
                ],
                isError: true,
            };
        }
        if (args.message) {
            bridge.reportFeedback(args.message);
        }
        if (args.refresh_git) {
            handleRefreshGit(args.sessionId);
            // Signal the Rivet UI that the round is complete
            bridge.setStatus('READY');
        }
        const intent = bridge.getPendingChanges();
        if (!intent) {
            return {
                content: [
                    {
                        type: 'text',
                        text: JSON.stringify({ hasChanges: false }),
                    },
                ],
            };
        }
        if ((0, changeBatchClassification_1.isMixedVariantBatch)(intent)) {
            const rejectedRequest = bridge.rejectMixedVariantRequestBatch(intent);
            return (0, exports.buildMixedVariantBatchResponse)(rejectedRequest);
        }
        if ((0, changeBatchClassification_1.isSingleVariantRequestBatch)(intent)) {
            const consumedRequest = bridge.consumeVariantRequestBatch(intent);
            return (0, exports.buildVariantRequestBatchResponse)(consumedRequest);
        }
        if ((0, changeBatchClassification_1.isStaticPreviewRefineBatch)(intent)) {
            const consumedRequest = bridge.consumeStaticPreviewRefineBatch(intent);
            if (consumedRequest) {
                const targetInstructions = (0, exports.collectStaticPreviewRefineTargetInstructions)(consumedRequest);
                const sessionId = targetInstructions[0]?.target.sessionId;
                if (sessionId) {
                    agentVariantsOrchestrator.beginStaticPreviewRefinements({
                        sessionId,
                        targets: targetInstructions.map(({ target, instruction }) => ({
                            variantId: target.variantId,
                            instruction,
                        })),
                    });
                }
            }
            return (0, exports.buildStaticPreviewRefineResponse)(consumedRequest);
        }
        beginApplyingRun(intent, args.sessionId);
        const { changes, imageContent } = (0, changeBatchClassification_1.extractImages)(intent.changes);
        return {
            content: [
                {
                    type: 'text',
                    text: JSON.stringify({
                        hasChanges: true,
                        changes,
                        sourceFiles: intent.sourceFiles,
                        ...(intent.target ? { target: intent.target } : {}),
                        ...(intent.worktreePath
                            ? { worktreePath: intent.worktreePath }
                            : {}),
                        applyGuide: buildApplyGuide(intent.worktreePath, requestHasResolvedReferences(intent)),
                    }),
                },
                ...imageContent,
            ],
        };
    });
    guardedMcp.tool('close_visual_editor', 'Stop the Rivet visual editing session and clean up resources. Call this when the user is done making visual changes.', sessionIdArgs, async (args) => {
        if (!bridge.isActive() || bridge.getSessionId() !== args.sessionId) {
            return {
                content: [
                    {
                        type: 'text',
                        text: JSON.stringify({ error: 'No active session with that ID' }),
                    },
                ],
                isError: true,
            };
        }
        const { totalRoundsCompleted } = await cleanupSessionResources({
            closeSessionId: args.sessionId,
            trackSessionEnd: true,
        });
        return {
            content: [
                {
                    type: 'text',
                    text: JSON.stringify({ closed: true, totalRoundsCompleted }),
                },
            ],
        };
    });
    // Register the agent-variants MCP tools (orchestrator was constructed at
    // the top of runMcpServer so it's shared with the Express routes).
    // Default inspiration fetcher + design-context store are wired
    // automatically when omitted, so production picks them up here.
    (0, agent_variants_1.registerAgentVariantsTools)(guardedMcp, {
        orchestrator: agentVariantsOrchestrator,
        leaseOwner: () => 'mcp-server',
        ensureVisualEditorOpen: ensureZeroToOneVisualEditorOpen,
        getProjectPath: () => bridge.getProjectPath(),
        telemetry,
    });
    (0, agent_variants_1.registerCreateZeroToOneTool)(guardedMcp, {
        orchestrator: agentVariantsOrchestrator,
        leaseOwner: () => 'mcp-server',
        ensureVisualEditorOpen: ensureZeroToOneVisualEditorOpen,
        telemetry,
        getProjectPath: () => bridge.getProjectPath(),
    });
    (0, tools_1.registerAuthTools)(mcp, { authService, telemetry, mcpEditor });
    log.info('agent-variants + auth MCP tools registered');
    // Connect via stdio
    const transport = new stdio_js_1.StdioServerTransport();
    await mcp.connect(transport);
    log.info('Rivet MCP server started on stdio');
    // Keep the MCP process alive for as long as the client keeps stdio open.
    // Without this guard, some Node/stdin combinations can allow the process
    // to exit before the client finishes initialization.
    await new Promise((resolve) => {
        process.stdin.once('end', resolve);
        process.stdin.once('close', resolve);
        process.stdin.resume();
    });
};
exports.startMCPServer = startMCPServer;
//# sourceMappingURL=server.js.map