"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.createMCPRouter = void 0;
const crypto_1 = require("crypto");
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const express_1 = require("express");
const v3_1 = require("zod/v3");
const index_core_1 = require("../index-core");
const elementFileHintSignals_1 = require("../utils/elementFileHintSignals");
const contracts_1 = require("../mcp/agent-variants/contracts");
const changeBatchClassification_1 = require("../mcp/changeBatchClassification");
const queueAccess_1 = require("../utils/queueAccess");
const log = (0, index_core_1.createLogger)('MCPRoutes');
const MAX_FILE_MATCHES_PER_ELEMENT = 6;
const elementRefSchema = v3_1.z.object({
    xpath: v3_1.z.string(),
    tagName: v3_1.z.string(),
    className: v3_1.z.string(),
    id: v3_1.z.string(),
    filePaths: v3_1.z.array(v3_1.z.string()),
    rivetId: v3_1.z.string().optional(),
    textContent: v3_1.z.string().optional(),
    attributes: v3_1.z.record(v3_1.z.string()).optional(),
    componentTree: v3_1.z
        .array(v3_1.z.object({
        name: v3_1.z.string(),
        type: v3_1.z.string().optional(),
        depth: v3_1.z.number().optional(),
    }))
        .optional(),
    parentElementSummary: v3_1.z
        .object({
        tagName: v3_1.z.string(),
        className: v3_1.z.string(),
        textSnippet: v3_1.z.string(),
    })
        .optional(),
});
const variantRequestSchema = v3_1.z.object({
    count: v3_1.z.number().int().min(contracts_1.VARIANT_COUNT_MIN).max(contracts_1.VARIANT_COUNT_MAX),
});
const styleChangeItemSchema = v3_1.z.object({
    kind: v3_1.z.literal('style'),
    element: elementRefSchema,
    property: v3_1.z.string(),
    from: v3_1.z.string(),
    to: v3_1.z.string(),
});
const textChangeItemSchema = v3_1.z.object({
    kind: v3_1.z.literal('text'),
    element: elementRefSchema,
    from: v3_1.z.string(),
    to: v3_1.z.string(),
});
const commentReferenceItemSchema = v3_1.z.object({
    sessionId: v3_1.z.string().min(1),
    variantId: v3_1.z.string().min(1),
    label: v3_1.z.string().min(1),
    origin: v3_1.z.enum(['live', 'history']),
});
const changeRequestTargetSchema = v3_1.z
    .object({
    kind: v3_1.z.literal('variant'),
    sessionId: v3_1.z.string().min(1),
    variantId: v3_1.z.string().min(1),
})
    .optional();
const staticPreviewTargetSchema = v3_1.z
    .object({
    kind: v3_1.z.literal('static_preview'),
    sessionId: v3_1.z.string().min(1),
    variantId: v3_1.z.string().min(1),
})
    .optional();
const commentChangeItemSchema = v3_1.z.object({
    kind: v3_1.z.literal('comment'),
    element: elementRefSchema,
    elements: v3_1.z.array(elementRefSchema).optional(),
    target: changeRequestTargetSchema,
    staticPreviewTarget: staticPreviewTargetSchema,
    instruction: v3_1.z.string(),
    images: v3_1.z.array(v3_1.z.string()).optional(),
    references: v3_1.z.array(commentReferenceItemSchema).max(8).optional(),
    variantRequest: variantRequestSchema.optional(),
});
const chatChangeItemSchema = v3_1.z.object({
    kind: v3_1.z.literal('chat'),
    instruction: v3_1.z.string(),
    images: v3_1.z.array(v3_1.z.string()).optional(),
});
const variantChangeItemSchema = v3_1.z
    .object({ kind: v3_1.z.literal('variant') })
    .passthrough();
const changeItemSchema = v3_1.z.discriminatedUnion('kind', [
    styleChangeItemSchema,
    textChangeItemSchema,
    commentChangeItemSchema,
    chatChangeItemSchema,
    variantChangeItemSchema,
]);
const telemetryContextSchema = v3_1.z
    .object({
    browserSessionId: v3_1.z.string().optional(),
    browserDistinctId: v3_1.z.string().optional(),
})
    .optional();
const changeRequestSchema = v3_1.z.object({
    changes: v3_1.z.array(changeItemSchema).min(1),
    sourceFiles: v3_1.z.array(v3_1.z.string()).optional(),
    target: changeRequestTargetSchema,
    telemetryContext: telemetryContextSchema,
    traceId: v3_1.z.string().optional(),
    agentRunId: v3_1.z.string().optional(),
});
/**
 * Enrich elements that have empty filePaths by running a server-side
 * component search against the project source files.
 * Mutates the ChangeRequest in place: updates per-element filePaths
 * and rebuilds the top-level sourceFiles list.
 */
async function enrichChangeRequest(request, projectPath) {
    // Collect unique ElementRefs that need enrichment
    const refsToEnrich = [];
    for (const item of request.changes) {
        if ('element' in item && item.element.filePaths.length === 0) {
            if ((0, elementFileHintSignals_1.shouldSearchElementFileHints)(item.element)) {
                refsToEnrich.push(item.element);
            }
        }
        if ('elements' in item && item.elements) {
            for (const el of item.elements) {
                if (el.filePaths.length === 0 && (0, elementFileHintSignals_1.shouldSearchElementFileHints)(el)) {
                    refsToEnrich.push(el);
                }
            }
        }
    }
    if (refsToEnrich.length === 0)
        return;
    log.info(`Enriching ${refsToEnrich.length} element(s) with missing filePaths`);
    const searchService = new index_core_1.ComponentSearchService();
    await Promise.all(refsToEnrich.map(async (ref) => {
        try {
            const matches = await searchService.findMatchingComponents(projectPath, ref);
            if (matches.length > 0) {
                ref.filePaths = matches
                    .slice(0, MAX_FILE_MATCHES_PER_ELEMENT)
                    .map((m) => m.filePath);
            }
        }
        catch (err) {
            log.warn(`Component search failed for <${ref.tagName}>: ${err instanceof Error ? err.message : err}`);
        }
    }));
    // Rebuild sourceFiles from all elements
    const sourceFileSet = new Set(request.sourceFiles);
    for (const item of request.changes) {
        if ('element' in item) {
            for (const fp of item.element.filePaths) {
                sourceFileSet.add(fp);
            }
        }
    }
    request.sourceFiles = Array.from(sourceFileSet);
    log.info(`Enrichment complete: sourceFiles now has ${request.sourceFiles.length} entries`);
}
/**
 * Resolve one attached variant reference into a readable directory the agent
 * can point its Read/Grep tools at. Returns `null` when the reference can't
 * be served (no orchestrator, no worktree, no persisted history snapshot) —
 * the intake drops null entries instead of failing the whole batch so a stale
 * reference left over from a prior session never breaks an unrelated comment.
 *
 * The "brief" is read opportunistically from the persisted manifest under
 * `.rivet/variants/<sid>/<vid>/manifest.json`. Live variants without history
 * yet just carry their label.
 */
function resolveCommentReference(ref, orchestrator, projectPath) {
    const readBriefFromManifest = (manifestDir) => {
        try {
            const manifestPath = path.join(manifestDir, 'manifest.json');
            if (!fs.existsSync(manifestPath))
                return undefined;
            const raw = fs.readFileSync(manifestPath, 'utf8');
            const parsed = JSON.parse(raw);
            return typeof parsed.brief === 'string' ? parsed.brief : undefined;
        }
        catch {
            return undefined;
        }
    };
    // For fresh sessions the variants history lives under the workspace root,
    // not the user's project path. Try the in-memory context first; fall back
    // to the current bridge project for existing-project sessions (where
    // history and project share a root) and for live sessions whose context
    // already drained from memory.
    const candidateRoots = [];
    try {
        const ctx = orchestrator?.getProjectContext(ref.sessionId);
        if (ctx?.kind === 'fresh') {
            candidateRoots.push(ctx.workspaceRoot);
        }
    }
    catch {
        // Unknown session — orchestrator doesn't carry it any more. The bridge
        // project root is the only viable fallback below.
    }
    if (projectPath)
        candidateRoots.push(projectPath);
    if (ref.origin === 'live') {
        const worktreePath = orchestrator?.getWorktreePath(ref.sessionId, ref.variantId);
        if (worktreePath) {
            const brief = candidateRoots
                .map((root) => readBriefFromManifest(path.join(root, '.rivet', 'variants', ref.sessionId, ref.variantId)))
                .find((b) => Boolean(b));
            return {
                sessionId: ref.sessionId,
                variantId: ref.variantId,
                label: ref.label,
                brief,
                path: worktreePath,
                origin: 'live',
            };
        }
        // Live worktree torn down — fall through and try history as a last resort.
    }
    for (const root of candidateRoots) {
        const variantDir = path.join(root, '.rivet', 'variants', ref.sessionId, ref.variantId);
        const filesDir = path.join(variantDir, 'files');
        if (!fs.existsSync(filesDir))
            continue;
        return {
            sessionId: ref.sessionId,
            variantId: ref.variantId,
            label: ref.label,
            brief: readBriefFromManifest(variantDir),
            path: filesDir,
            origin: ref.origin,
        };
    }
    return null;
}
/**
 * Creates Express routes for MCP session bridge communication.
 * The UI POSTs a ChangeRequest here; the MCP server reads it via SessionBridgeService.
 *
 * `agentVariantsOrchestrator` is optional so existing callers (tests, hosted
 * demos) can mount the router without wiring the variants flow. When it IS
 * provided, the intake rejects new variant-request batches while a variants
 * session is already active for the current project.
 */
const createMCPRouter = (bridge, telemetry, agentVariantsOrchestrator, options = {}) => {
    const router = (0, express_1.Router)();
    /**
     * Queue a ChangeRequest from the Rivet UI for retrieval by the MCP client.
     * Called when the user clicks "Send to Claude Code" in MCP mode.
     */
    router.post('/queue-changes', async (req, res) => {
        if (!bridge.isActive()) {
            res.status(400).json({ error: 'No active MCP session' });
            return;
        }
        if (!(0, queueAccess_1.isQueueAccessAuthorized)({
            queueAccessToken: options.queueAccessToken,
            getHeader: (name) => req.header(name),
        })) {
            res.status(403).json({
                error: 'Queue access denied',
                reason: 'queue_token_required',
            });
            return;
        }
        const parsed = changeRequestSchema.safeParse(req.body);
        if (!parsed.success) {
            res.status(400).json({
                error: 'Invalid change request payload',
                details: parsed.error.flatten(),
            });
            return;
        }
        const body = parsed.data;
        const batchForClassification = {
            changes: body.changes,
            sourceFiles: body.sourceFiles || [],
        };
        const isMixedVariantRequestBatch = (0, changeBatchClassification_1.isMixedVariantBatch)(batchForClassification);
        const isVariantRequestBatch = (0, changeBatchClassification_1.isSingleVariantRequestBatch)(batchForClassification);
        const requestedVariantCount = isVariantRequestBatch &&
            batchForClassification.changes[0]?.kind === 'comment'
            ? batchForClassification.changes[0].variantRequest?.count
            : undefined;
        if (isMixedVariantRequestBatch) {
            res.status(400).json({
                error: 'Mixed batch rejected',
                details: 'A comment with `variantRequest` must be submitted as a single-item batch.',
                reason: 'mixed_batch',
            });
            return;
        }
        if (isVariantRequestBatch) {
            const activeVariantsSession = agentVariantsOrchestrator?.buildActiveSnapshot();
            if (activeVariantsSession?.active === true) {
                res.status(400).json({
                    error: 'Variants session already active',
                    details: 'Finish or cancel the running variants session before submitting another variant request.',
                    reason: 'active_session',
                });
                return;
            }
        }
        const changes = body.changes;
        // Resolve a variant-targeted batch to either an editable worktree or an
        // HTML-only static preview refinement target.
        let resolvedWorktreePath;
        let staticPreviewTarget;
        if (body.target) {
            const target = body.target;
            const worktreePath = agentVariantsOrchestrator?.getWorktreePath(target.sessionId, target.variantId);
            if (worktreePath) {
                resolvedWorktreePath = worktreePath;
            }
            else if (agentVariantsOrchestrator?.getStaticPreviewHtml(target.sessionId, target.variantId)) {
                const variant = agentVariantsOrchestrator
                    ?.getVariants?.(target.sessionId)
                    ?.find((candidate) => candidate.workItemId === target.variantId);
                const variantPick = agentVariantsOrchestrator?.getVariantPick?.(target.sessionId);
                if (variantPick) {
                    res.status(409).json({
                        error: 'Static preview variant already sent',
                        details: 'This static preview has already been sent. Start a new variants session before sending another comment.',
                        reason: 'variant_already_sent',
                    });
                    return;
                }
                if (variant?.refinement?.status === 'pending' ||
                    variant?.refinement?.status === 'running') {
                    res.status(409).json({
                        error: 'Static preview refinement already active',
                        details: 'Wait for the current static preview refinement to finish before sending another comment.',
                        reason: 'active_refinement',
                    });
                    return;
                }
                const hasOnlyComments = changes.every((change) => change.kind === 'comment');
                if (!hasOnlyComments) {
                    res.status(400).json({
                        error: 'Static preview edits require comments',
                        details: 'Static preview refinements must be sent as comment instructions.',
                        reason: 'static_preview_comment_required',
                    });
                    return;
                }
                staticPreviewTarget = {
                    kind: 'static_preview',
                    sessionId: target.sessionId,
                    variantId: target.variantId,
                };
            }
            else {
                res.status(400).json({
                    error: 'Variant is not editable',
                    details: 'The targeted variant has no settled worktree or static preview (it may still be generating, or its session ended). Edits can only be applied to a ready variant.',
                    reason: 'variant_not_editable',
                });
                return;
            }
        }
        for (const item of changes) {
            if (item.kind !== 'comment')
                continue;
            const itemTarget = item.target;
            if (!itemTarget) {
                // Never trust a client-supplied static-preview target: it's only valid
                // when resolved from a variant `target` we can verify still has HTML.
                // Without a target there's nothing to verify, so drop it — otherwise a
                // forged `staticPreviewTarget` could route the batch into static
                // refinement for a variant that was never confirmed to have a preview.
                item.staticPreviewTarget = undefined;
                continue;
            }
            const itemMatchesBatchTarget = body.target &&
                itemTarget.sessionId === body.target.sessionId &&
                itemTarget.variantId === body.target.variantId;
            const worktreePath = agentVariantsOrchestrator?.getWorktreePath(itemTarget.sessionId, itemTarget.variantId);
            if (worktreePath) {
                if (itemMatchesBatchTarget) {
                    item.staticPreviewTarget = undefined;
                    continue;
                }
                res.status(400).json({
                    error: 'Variant target must be sent as a batch target',
                    details: 'Per-comment targets are only supported for static preview variants. Worktree-backed variants must be sent as a single-target batch.',
                    reason: 'mixed_target',
                });
                return;
            }
            if (agentVariantsOrchestrator?.getStaticPreviewHtml(itemTarget.sessionId, itemTarget.variantId)) {
                const variant = agentVariantsOrchestrator
                    ?.getVariants?.(itemTarget.sessionId)
                    ?.find((candidate) => candidate.workItemId === itemTarget.variantId);
                const variantPick = agentVariantsOrchestrator?.getVariantPick?.(itemTarget.sessionId);
                if (variantPick) {
                    res.status(409).json({
                        error: 'Static preview variant already sent',
                        details: 'This static preview has already been sent. Start a new variants session before sending another comment.',
                        reason: 'variant_already_sent',
                    });
                    return;
                }
                if (variant?.refinement?.status === 'pending' ||
                    variant?.refinement?.status === 'running') {
                    res.status(409).json({
                        error: 'Static preview refinement already active',
                        details: 'Wait for the current static preview refinement to finish before sending another comment.',
                        reason: 'active_refinement',
                    });
                    return;
                }
                item.staticPreviewTarget = {
                    kind: 'static_preview',
                    sessionId: itemTarget.sessionId,
                    variantId: itemTarget.variantId,
                };
                continue;
            }
            res.status(400).json({
                error: 'Variant is not editable',
                details: 'The targeted variant has no settled worktree or static preview (it may still be generating, or its session ended). Edits can only be applied to a ready variant.',
                reason: 'variant_not_editable',
            });
            return;
        }
        const request = {
            changes,
            sourceFiles: body.sourceFiles || [],
            target: body.target,
            worktreePath: resolvedWorktreePath,
            staticPreviewTarget,
            telemetryContext: body.telemetryContext,
            traceId: body.traceId || (0, crypto_1.randomUUID)(),
            agentRunId: body.agentRunId || (0, crypto_1.randomUUID)(),
        };
        // Enrich elements missing filePaths before queueing. For a variant batch,
        // search the variant's worktree (its sources may diverge from the base
        // project); otherwise search the base project.
        const hasStaticPreviewItems = request.changes.some((item) => item.kind === 'comment' && Boolean(item.staticPreviewTarget));
        const enrichRoot = staticPreviewTarget || hasStaticPreviewItems
            ? undefined
            : (resolvedWorktreePath ?? bridge.getProjectPath());
        if (enrichRoot) {
            try {
                await enrichChangeRequest(request, enrichRoot);
            }
            catch (err) {
                log.warn(`Element enrichment failed, queueing without enrichment: ${err instanceof Error ? err.message : err}`);
            }
        }
        // Resolve attached variant references on every comment item. Each
        // reference becomes a `{ label, brief, path }` pointer the agent will
        // read with its existing file tools — we do NOT inline file content here
        // so the prompt stays small and the agent decides what to read. Drop
        // unresolvable references and log them rather than failing the batch.
        const bridgeProjectPath = bridge.getProjectPath();
        for (const item of request.changes) {
            if (item.kind !== 'comment' || !item.references?.length)
                continue;
            const resolved = [];
            for (const ref of item.references) {
                const r = resolveCommentReference(ref, agentVariantsOrchestrator, bridgeProjectPath);
                if (r) {
                    resolved.push(r);
                }
                else {
                    log.warn(`Dropping unresolvable variant reference ${ref.sessionId}:${ref.variantId} (${ref.origin}) on comment`);
                }
            }
            item.resolvedReferences = resolved.length > 0 ? resolved : undefined;
        }
        bridge.queueChangeRequest(request);
        const styleCt = request.changes.filter((c) => c.kind === 'style').length;
        const textCt = request.changes.filter((c) => c.kind === 'text').length;
        const commentCt = request.changes.filter((c) => c.kind === 'comment').length;
        const chatCt = request.changes.filter((c) => c.kind === 'chat').length;
        const nextRound = bridge.getRoundsCompleted() + 1;
        log.info(`Change request queued: ${styleCt} style, ${textCt} text, ${commentCt} comment, ${chatCt} chat` +
            (request.target
                ? ` → variant ${request.target.variantId} (${resolvedWorktreePath})`
                : ''));
        if (telemetry) {
            const sessionId = bridge.getSessionId();
            if (sessionId) {
                if (isVariantRequestBatch &&
                    typeof requestedVariantCount === 'number') {
                    // Variant-request batches never start an apply run, so the standard
                    // `mcp_changes_sent` / `changes_queued` / `agent_run_started` chain
                    // would be misleading. Emit a dedicated event instead.
                    telemetry.trackCommentVariantRequestReceived({
                        sessionId,
                        requestedCount: requestedVariantCount,
                        traceId: request.traceId,
                        agentRunId: request.agentRunId,
                        browserSessionId: request.telemetryContext?.browserSessionId,
                        browserDistinctId: request.telemetryContext?.browserDistinctId,
                    });
                }
                else {
                    telemetry.trackMCPChangesSent({
                        sessionId,
                        styleChangeCount: styleCt,
                        textChangeCount: textCt,
                        commentCount: commentCt,
                        chatCount: chatCt,
                        round: nextRound,
                        traceId: request.traceId,
                        agentRunId: request.agentRunId,
                        browserSessionId: request.telemetryContext?.browserSessionId,
                        browserDistinctId: request.telemetryContext?.browserDistinctId,
                    });
                }
                telemetry.flush().catch((err) => {
                    log.warn('Telemetry flush after mcp_changes_sent failed:', err);
                });
            }
            else {
                log.warn('MCP telemetry skipped: no sessionId on bridge');
            }
        }
        res.json({
            queued: true,
            traceId: request.traceId,
            agentRunId: request.agentRunId,
            ...(isVariantRequestBatch ? { kind: 'variant_request' } : {}),
        });
    });
    /** Check whether changes is pending (for UI status indicators) */
    router.get('/status', async (req, res) => {
        const devServerHealth = bridge.getDevServerHealth();
        const shouldProbeUpstream = bridge.isActive() &&
            devServerHealth.host !== null &&
            devServerHealth.port !== null &&
            devServerHealth.ownership !== 'none';
        if (shouldProbeUpstream) {
            try {
                const response = await fetch(`http://${devServerHealth.host}:${devServerHealth.port}`, { method: 'HEAD', signal: AbortSignal.timeout(1200) });
                if (response.status !== 502) {
                    bridge.updateDevServerHealth({
                        isReachable: true,
                        reason: 'healthy',
                        lastCheckedAt: Date.now(),
                    });
                }
                else {
                    bridge.updateDevServerHealth({
                        isReachable: false,
                        reason: 'upstream_unreachable',
                        lastCheckedAt: Date.now(),
                    });
                }
            }
            catch {
                bridge.updateDevServerHealth({
                    isReachable: false,
                    reason: 'upstream_unreachable',
                    lastCheckedAt: Date.now(),
                });
            }
        }
        else if (!bridge.isActive()) {
            bridge.updateDevServerHealth({
                isReachable: false,
                reason: 'rivet_unreachable',
                lastCheckedAt: Date.now(),
            });
        }
        const feedback = bridge.getFeedback();
        if (feedback.length > 0) {
            log.info(`[API] Returning status with ${feedback.length} feedback items`);
        }
        res.json({
            isActive: bridge.isActive(),
            hasPendingIntent: bridge.getPendingChanges() !== null,
            roundsCompleted: bridge.getRoundsCompleted(),
            feedback: feedback,
            status: bridge.getStatus(),
            devServerHealth: bridge.getDevServerHealth(),
        });
    });
    return router;
};
exports.createMCPRouter = createMCPRouter;
//# sourceMappingURL=mcp.js.map