import express from "express";
import { createServer as createHTTPServer } from "http";
import { constants, existsSync, unlinkSync } from "fs";
import { open } from "fs/promises";
import { randomUUID } from "node:crypto";
import { hostname as getHostname } from "node:os";
import path from "node:path";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
import { createBranchChangeRouteHandler } from "./script-route-branch-handler.js";
function resolveBoundListenTarget(listenTarget, httpServer) {
    if (listenTarget.type !== "tcp") {
        return listenTarget;
    }
    const address = httpServer.address();
    if (!address || typeof address === "string") {
        throw new Error("HTTP server did not expose a TCP address after listening");
    }
    return {
        type: "tcp",
        host: listenTarget.host,
        port: address.port,
    };
}
// Matches a Windows drive-letter path like C:\ or D:\
const WINDOWS_DRIVE_RE = /^[A-Za-z]:\\/;
export function parseListenString(listen) {
    // 1. Windows named pipes: \\.\pipe\... or pipe://...
    if (listen.startsWith("\\\\.\\pipe\\") || listen.startsWith("pipe://")) {
        return {
            type: "pipe",
            path: listen.startsWith("pipe://") ? listen.slice("pipe://".length) : listen,
        };
    }
    // 2. Explicit unix:// prefix
    if (listen.startsWith("unix://")) {
        return { type: "socket", path: listen.slice(7) };
    }
    // 3. Reject Windows absolute drive paths — they are not Unix sockets
    if (WINDOWS_DRIVE_RE.test(listen)) {
        throw new Error(`Invalid listen string (Windows path is not a valid listen target): ${listen}`);
    }
    // 4. POSIX absolute path (/ or ~) — Unix socket
    if (listen.startsWith("/") || listen.startsWith("~")) {
        return { type: "socket", path: listen };
    }
    // 5. Pure numeric — TCP port on 127.0.0.1
    const trimmed = listen.trim();
    if (/^\d+$/.test(trimmed)) {
        const port = parseInt(trimmed, 10);
        return { type: "tcp", host: "127.0.0.1", port };
    }
    // 6. host:port — TCP
    if (listen.includes(":")) {
        const [host, portStr] = listen.split(":");
        const parsedPort = parseInt(portStr, 10);
        if (!Number.isFinite(parsedPort)) {
            throw new Error(`Invalid port in listen string: ${listen}`);
        }
        return { type: "tcp", host: host || "127.0.0.1", port: parsedPort };
    }
    throw new Error(`Invalid listen string: ${listen}`);
}
function formatListenTarget(listenTarget) {
    if (!listenTarget) {
        return null;
    }
    if (listenTarget.type === "tcp") {
        return `${listenTarget.host}:${listenTarget.port}`;
    }
    return listenTarget.path;
}
import { VoiceAssistantWebSocketServer } from "./websocket-server.js";
import { createGitHubService } from "../services/github-service.js";
import { createPaseoWorktree as createRegisteredPaseoWorktree } from "./paseo-worktree-service.js";
import { createPaseoWorktreeWorkflow } from "./worktree-session.js";
import { DownloadTokenStore } from "./file-download/token-store.js";
import { createSpeechService } from "./speech/speech-runtime.js";
import { AgentManager } from "./agent/agent-manager.js";
import { AgentStorage } from "./agent/agent-storage.js";
import { attachAgentStoragePersistence } from "./persistence-hooks.js";
import { createAgentMcpServer } from "./agent/mcp-server.js";
import { ProviderSnapshotManager } from "./agent/provider-snapshot-manager.js";
import { bootstrapWorkspaceRegistries } from "./workspace-registry-bootstrap.js";
import { WorkspaceReconciliationService } from "./workspace-reconciliation-service.js";
import { FileBackedProjectRegistry, FileBackedWorkspaceRegistry } from "./workspace-registry.js";
import { FileBackedChatService } from "./chat/chat-service.js";
import { CheckoutDiffManager } from "./checkout-diff-manager.js";
import { LoopService } from "./loop-service.js";
import { ScheduleService } from "./schedule/service.js";
import { DaemonConfigStore } from "./daemon-config-store.js";
import { WorkspaceGitServiceImpl } from "./workspace-git-service.js";
import { archivePersistedWorkspaceRecord } from "./workspace-archive-service.js";
import { setupAutoArchiveOnMerge } from "./auto-archive-on-merge/index.js";
import { wrapSessionMessage } from "./messages.js";
import { createConfiguredTerminalManager } from "../terminal/terminal-manager-factory.js";
import { createConnectionOfferV2, encodeOfferToFragmentUrl } from "./connection-offer.js";
import { loadOrCreateDaemonKeyPair } from "./daemon-keypair.js";
import { startRelayTransport } from "./relay-transport.js";
import { getOrCreateServerId } from "./server-id.js";
import { resolveDaemonVersion } from "./daemon-version.js";
import { createServiceProxySubsystem } from "./service-proxy.js";
import { ScriptHealthMonitor } from "./script-health-monitor.js";
import { createScriptStatusEmitter } from "./script-status-projection.js";
import { WorkspaceScriptRuntimeStore } from "./workspace-script-runtime-store.js";
import { isHostnameAllowed } from "./hostnames.js";
import { createRequireBearerMiddleware } from "./auth.js";
const MAX_MCP_DEBUG_BATCH_ITEMS = 10;
const REDACTED_LOG_VALUE = "[redacted]";
const DOWNLOAD_OPEN_FLAGS = process.platform === "win32" ? constants.O_RDONLY : constants.O_RDONLY | constants.O_NOFOLLOW;
function formatHostForHttpUrl(host) {
    return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
}
function resolveAgentMcpClientHost(host) {
    if (host === "0.0.0.0") {
        return "127.0.0.1";
    }
    if (host === "::" || host === "[::]") {
        return "::1";
    }
    return host;
}
function createAgentMcpBaseUrl(listenTarget) {
    if (!listenTarget || listenTarget.type !== "tcp") {
        return null;
    }
    const host = resolveAgentMcpClientHost(listenTarget.host);
    return new URL("/mcp/agents", `http://${formatHostForHttpUrl(host)}:${listenTarget.port}`).toString();
}
function summarizeAgentMcpDebugMessage(body) {
    if (!body || typeof body !== "object" || Array.isArray(body)) {
        return {
            type: body === null ? "null" : typeof body,
        };
    }
    const record = body;
    const method = typeof record.method === "string" ? record.method : undefined;
    return {
        type: "object",
        ...(typeof record.jsonrpc === "string" ? { jsonrpc: record.jsonrpc } : {}),
        ...(method ? { method } : {}),
        hasId: Object.prototype.hasOwnProperty.call(record, "id"),
        hasParams: Object.prototype.hasOwnProperty.call(record, "params"),
    };
}
function summarizeAgentMcpDebugBody(body) {
    if (!Array.isArray(body)) {
        return summarizeAgentMcpDebugMessage(body);
    }
    const messages = body.slice(0, MAX_MCP_DEBUG_BATCH_ITEMS).map(summarizeAgentMcpDebugMessage);
    return {
        type: "batch",
        count: body.length,
        messages,
        ...(body.length > messages.length ? { omitted: body.length - messages.length } : {}),
    };
}
export async function createPaseoDaemon(config, rootLogger) {
    const logger = rootLogger.child({ module: "bootstrap" });
    const bootstrapStart = performance.now();
    const elapsed = () => `${(performance.now() - bootstrapStart).toFixed(0)}ms`;
    const daemonVersion = resolveDaemonVersion(import.meta.url);
    const daemonConfigStore = new DaemonConfigStore(config.paseoHome, {
        mcp: { injectIntoAgents: config.mcpInjectIntoAgents ?? true },
        providers: Object.fromEntries(Object.entries(config.providerOverrides ?? {}).map(([providerId, override]) => [
            providerId,
            {
                ...(override.enabled !== undefined ? { enabled: override.enabled } : {}),
                ...(override.additionalModels ? { additionalModels: override.additionalModels } : {}),
            },
        ])),
        metadataGeneration: {
            providers: config.metadataGeneration?.providers ?? [],
        },
        autoArchiveAfterMerge: config.autoArchiveAfterMerge ?? false,
        appendSystemPrompt: config.appendSystemPrompt ?? "",
    }, logger);
    const serverId = getOrCreateServerId(config.paseoHome, { logger });
    const daemonKeyPair = await loadOrCreateDaemonKeyPair(config.paseoHome, logger);
    let relayTransport = null;
    const staticDir = config.staticDir;
    const downloadTokenTtlMs = config.downloadTokenTtlMs ?? 60000;
    const downloadTokenStore = new DownloadTokenStore({
        ttlMs: downloadTokenTtlMs,
    });
    const listenTarget = parseListenString(config.listen);
    const app = express();
    let boundListenTarget = null;
    let workspaceRegistry = null;
    const serviceProxyPublicBaseUrl = config.serviceProxy?.publicBaseUrl
        ? config.serviceProxy.publicBaseUrl
        : null;
    const serviceProxy = createServiceProxySubsystem({
        logger,
        publicBaseUrl: serviceProxyPublicBaseUrl,
    });
    const scriptRuntimeStore = new WorkspaceScriptRuntimeStore();
    const configuredHostnames = config.hostnames ?? config.allowedHosts;
    let wsServer = null;
    let serviceProxyListenTarget = null;
    const scriptHealthMonitor = new ScriptHealthMonitor({
        serviceProxy,
        onChange: createScriptStatusEmitter({
            sessions: () => wsServer?.listActiveSessions().map((session) => ({
                emit: (message) => session.emitServerMessage(message),
            })) ?? [],
            serviceProxy,
            runtimeStore: scriptRuntimeStore,
            daemonPort: () => (boundListenTarget?.type === "tcp" ? boundListenTarget.port : null),
            resolveWorkspaceDirectory: async (workspaceId) => (await workspaceRegistry?.get(workspaceId))?.cwd ?? null,
            logger,
            serviceProxyPublicBaseUrl,
        }),
    });
    const handleBranchChange = createBranchChangeRouteHandler({
        serviceProxy,
        onRoutesChanged: (workspaceId) => {
            scriptHealthMonitor.invalidateWorkspace(workspaceId);
        },
        logger,
    });
    // Service proxy classifies service hosts before daemon auth/route fallthrough.
    // Registered service hosts proxy directly; known service namespaces without a
    // route return 404 and never reach daemon APIs.
    app.use(serviceProxy.middleware());
    // Host allowlist / DNS rebinding protection (vite-like semantics).
    // For non-TCP (unix sockets), skip host validation.
    if (listenTarget.type === "tcp") {
        app.use((req, res, next) => {
            const hostHeader = typeof req.headers.host === "string" ? req.headers.host : undefined;
            if (!isHostnameAllowed(hostHeader, configuredHostnames)) {
                res.status(403).json({ error: "Invalid Host header" });
                return;
            }
            next();
        });
    }
    // CORS - allow same-origin + configured origins
    const allowedOrigins = new Set([
        ...config.corsAllowedOrigins,
        // Packaged desktop renderers use the custom paseo:// protocol scheme.
        "paseo://app",
        // For TCP, add localhost variants
        ...(listenTarget.type === "tcp"
            ? [
                `http://${listenTarget.host}:${listenTarget.port}`,
                `http://localhost:${listenTarget.port}`,
                `http://127.0.0.1:${listenTarget.port}`,
            ]
            : []),
    ]);
    app.use((req, res, next) => {
        const origin = req.headers.origin;
        if (origin && (allowedOrigins.has("*") || allowedOrigins.has(origin))) {
            res.setHeader("Access-Control-Allow-Origin", origin);
            res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
            res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
            res.setHeader("Access-Control-Allow-Credentials", "true");
        }
        if (req.method === "OPTIONS") {
            res.status(204).end();
            return;
        }
        next();
    });
    app.use(createRequireBearerMiddleware(config.auth, (context) => {
        logger.warn(context, "Rejected HTTP request with invalid daemon password");
    }));
    // Serve static files from public directory
    app.use("/public", express.static(staticDir));
    // Middleware
    app.use(express.json());
    // Health check endpoint
    app.get("/api/health", (_req, res) => {
        res.json({ status: "ok", timestamp: new Date().toISOString() });
    });
    app.get("/api/status", (_req, res) => {
        res.json({
            status: "server_info",
            serverId,
            hostname: getHostname(),
            version: daemonVersion,
            listen: formatListenTarget(boundListenTarget ?? listenTarget),
        });
    });
    const handleFileDownload = async (req, res) => {
        const token = typeof req.query.token === "string" && req.query.token.trim().length > 0
            ? req.query.token.trim()
            : null;
        if (!token) {
            res.status(400).json({ error: "Missing download token" });
            return;
        }
        const entry = downloadTokenStore.consumeToken(token);
        if (!entry) {
            res.status(403).json({ error: "Invalid or expired token" });
            return;
        }
        let fileHandle = null;
        try {
            fileHandle = await open(entry.absolutePath, DOWNLOAD_OPEN_FLAGS);
            const fileStats = await fileHandle.stat();
            if (!fileStats.isFile()) {
                res.status(404).json({ error: "File not found" });
                return;
            }
            const safeFileName = entry.fileName.replace(/["\r\n]/g, "_");
            res.setHeader("Content-Type", entry.mimeType);
            res.setHeader("Content-Disposition", `attachment; filename="${safeFileName}"`);
            res.setHeader("Content-Length", fileStats.size.toString());
            const stream = fileHandle.createReadStream();
            fileHandle = null;
            stream.on("error", (err) => {
                logger.error({ err }, "Failed to stream download");
                if (!res.headersSent) {
                    res.status(500).json({ error: "Failed to read file" });
                }
                else {
                    res.end();
                }
            });
            stream.pipe(res);
        }
        catch (err) {
            logger.error({ err }, "Failed to download file");
            if (!res.headersSent) {
                res.status(404).json({ error: "File not found" });
            }
        }
        finally {
            await fileHandle?.close().catch(() => undefined);
        }
    };
    app.get("/api/files/download", (req, res) => {
        void handleFileDownload(req, res);
    });
    const httpServer = createHTTPServer(app);
    // Script proxy WebSocket upgrade handler — must be registered before the
    // VoiceAssistantWebSocketServer attaches its own "upgrade" listener so that
    // script-bound upgrades are forwarded first. The handler is a no-op for
    // requests that don't match a registered script route.
    httpServer.on("upgrade", serviceProxy.upgradeHandler({ passthroughUnknown: true }));
    if (config.serviceProxy?.standaloneListen) {
        serviceProxyListenTarget = parseListenString(config.serviceProxy.standaloneListen);
    }
    const agentStorage = new AgentStorage(config.agentStoragePath, logger);
    const projectRegistry = new FileBackedProjectRegistry(path.join(config.paseoHome, "projects", "projects.json"), logger);
    workspaceRegistry = new FileBackedWorkspaceRegistry(path.join(config.paseoHome, "projects", "workspaces.json"), logger);
    const chatService = new FileBackedChatService({
        paseoHome: config.paseoHome,
        logger,
    });
    const terminalManager = createConfiguredTerminalManager();
    const github = createGitHubService();
    const workspaceGitService = new WorkspaceGitServiceImpl({
        logger,
        paseoHome: config.paseoHome,
        worktreesRoot: config.worktreesRoot,
        deps: {
            github,
        },
    });
    const providerSnapshotLogger = logger.child({ module: "provider-snapshot-manager" });
    const providerSnapshotManager = new ProviderSnapshotManager({
        logger: providerSnapshotLogger,
        runtimeSettings: config.agentProviderSettings,
        providerOverrides: config.providerOverrides,
        workspaceGitService,
        isDev: config.isDev === true,
        extraClients: config.agentClients,
    });
    const initialAgentManagerState = providerSnapshotManager.getAgentManagerProviderState();
    const agentManager = new AgentManager({
        clients: initialAgentManagerState.clients,
        providerDefinitions: initialAgentManagerState.providerDefinitions,
        registry: agentStorage,
        appendSystemPrompt: config.appendSystemPrompt,
        logger,
    });
    const detachAgentStoragePersistence = attachAgentStoragePersistence(logger, agentManager, agentStorage);
    await agentStorage.initialize();
    logger.info({ elapsed: elapsed() }, "Agent storage initialized");
    await bootstrapWorkspaceRegistries({
        paseoHome: config.paseoHome,
        agentStorage,
        projectRegistry,
        workspaceRegistry,
        workspaceGitService,
        logger,
    });
    logger.info({ elapsed: elapsed() }, "Workspace registries bootstrapped");
    const workspaceReconciliation = new WorkspaceReconciliationService({
        projectRegistry,
        workspaceRegistry,
        logger,
        workspaceGitService,
    });
    void (async () => {
        try {
            const result = await workspaceReconciliation.runOnce();
            logger.info({
                elapsed: elapsed(),
                changeCount: result.changesApplied.length,
            }, "Workspace registries reconciled");
        }
        catch (error) {
            logger.error({ err: error }, "Background workspace reconciliation failed");
        }
    })();
    await chatService.initialize();
    logger.info({ elapsed: elapsed() }, "Chat service initialized");
    const checkoutDiffManager = new CheckoutDiffManager({
        logger,
        paseoHome: config.paseoHome,
        workspaceGitService,
    });
    const loopService = new LoopService({
        paseoHome: config.paseoHome,
        logger,
        agentManager,
        providerSnapshotManager,
    });
    await loopService.initialize();
    logger.info({ elapsed: elapsed() }, "Loop service initialized");
    const scheduleService = new ScheduleService({
        paseoHome: config.paseoHome,
        logger,
        agentManager,
        agentStorage,
        providerSnapshotManager,
    });
    await scheduleService.start();
    agentManager.setAgentArchivedCallback(async (agentId) => {
        try {
            await scheduleService.deleteForAgent(agentId);
        }
        catch (error) {
            logger.warn({ err: error, agentId }, "Failed to delete schedules for archived agent");
        }
    });
    logger.info({ elapsed: elapsed() }, "Schedule service initialized");
    logger.info({ elapsed: elapsed() }, "Loading persisted agent registry");
    const persistedRecords = await agentStorage.list();
    logger.info({ elapsed: elapsed() }, `Agent registry loaded (${persistedRecords.length} record${persistedRecords.length === 1 ? "" : "s"}); agents will initialize on demand`);
    logger.info("Voice mode configured for agent-scoped resume flow (no dedicated voice assistant provider)");
    logger.info({ elapsed: elapsed() }, "Preparing voice and MCP runtime");
    const archiveWorkspaceRecordExternal = async (workspaceId) => {
        const sessions = wsServer?.listActiveSessions() ?? [];
        if (sessions.length > 0) {
            await Promise.all(sessions.map((session) => session.archiveWorkspaceRecordForExternalMutation(workspaceId)));
            return;
        }
        await archivePersistedWorkspaceRecord({
            workspaceId,
            workspaceRegistry,
            projectRegistry,
        });
    };
    const markWorkspaceArchivingExternal = (workspaceIds, archivingAt) => {
        const workspaceIdList = Array.from(workspaceIds);
        for (const session of wsServer?.listActiveSessions() ?? []) {
            session.markWorkspaceArchivingForExternalMutation(workspaceIdList, archivingAt);
        }
    };
    const clearWorkspaceArchivingExternal = (workspaceIds) => {
        const workspaceIdList = Array.from(workspaceIds);
        for (const session of wsServer?.listActiveSessions() ?? []) {
            session.clearWorkspaceArchivingForExternalMutation(workspaceIdList);
        }
    };
    const emitWorkspaceUpdatesExternal = async (workspaceIds) => {
        const workspaceIdList = Array.from(workspaceIds);
        await Promise.all((wsServer?.listActiveSessions() ?? []).map((session) => session.emitWorkspaceUpdatesForExternalWorkspaceIds(workspaceIdList)));
    };
    const emitExternalSessionMessage = (message) => {
        wsServer?.broadcast(wrapSessionMessage(message));
    };
    setupAutoArchiveOnMerge({
        paseoHome: config.paseoHome,
        worktreesRoot: config.worktreesRoot,
        daemonConfigStore,
        workspaceGitService,
        github,
        agentManager,
        agentStorage,
        terminalManager,
        logger,
        archiveWorkspaceRecord: archiveWorkspaceRecordExternal,
        markWorkspaceArchiving: markWorkspaceArchivingExternal,
        clearWorkspaceArchiving: clearWorkspaceArchivingExternal,
        emitWorkspaceUpdatesForWorkspaceIds: emitWorkspaceUpdatesExternal,
    });
    const mcpEnabled = config.mcpEnabled ?? true;
    let agentMcpBaseUrl = null;
    if (mcpEnabled) {
        const agentMcpRoute = "/mcp/agents";
        const agentMcpTransports = new Map();
        const createAgentMcpTransport = async (callerAgentId) => {
            const agentMcpServer = await createAgentMcpServer({
                agentManager,
                agentStorage,
                terminalManager,
                getDaemonTcpPort: () => (boundListenTarget?.type === "tcp" ? boundListenTarget.port : null),
                scheduleService,
                providerSnapshotManager,
                github,
                workspaceGitService,
                archiveWorkspaceRecord: archiveWorkspaceRecordExternal,
                emitWorkspaceUpdatesForWorkspaceIds: emitWorkspaceUpdatesExternal,
                markWorkspaceArchiving: markWorkspaceArchivingExternal,
                clearWorkspaceArchiving: clearWorkspaceArchivingExternal,
                createPaseoWorktree: async (input, serviceOptions) => {
                    return createPaseoWorktreeWorkflow({
                        paseoHome: config.paseoHome,
                        worktreesRoot: config.worktreesRoot,
                        createPaseoWorktree: async (workflowInput, workflowOptions) => {
                            return createRegisteredPaseoWorktree(workflowInput, {
                                github,
                                ...(workflowOptions?.resolveDefaultBranch
                                    ? {
                                        resolveDefaultBranch: workflowOptions.resolveDefaultBranch,
                                    }
                                    : {}),
                                projectRegistry,
                                workspaceRegistry,
                                workspaceGitService,
                            });
                        },
                        warmWorkspaceGitData: async (workspace) => {
                            await Promise.all(wsServer
                                ?.listActiveSessions()
                                .map((session) => session.warmWorkspaceGitDataForWorkspace(workspace)) ?? []);
                        },
                        emitWorkspaceUpdateForCwd: async (cwd, emitOptions) => {
                            await Promise.all(wsServer
                                ?.listActiveSessions()
                                .map((session) => session.emitWorkspaceUpdatesForExternalCwds([cwd])) ?? []);
                            void emitOptions;
                        },
                        cacheWorkspaceSetupSnapshot: () => { },
                        emit: emitExternalSessionMessage,
                        sessionLogger: logger,
                        terminalManager,
                        archiveWorkspaceRecord: archiveWorkspaceRecordExternal,
                        serviceProxy,
                        scriptRuntimeStore,
                        getDaemonTcpPort: () => boundListenTarget?.type === "tcp" ? boundListenTarget.port : null,
                        getDaemonTcpHost: () => boundListenTarget?.type === "tcp" ? boundListenTarget.host : null,
                        serviceProxyPublicBaseUrl,
                        onScriptsChanged: null,
                    }, input, serviceOptions);
                },
                paseoHome: config.paseoHome,
                worktreesRoot: config.worktreesRoot,
                callerAgentId,
                enableVoiceTools: false,
                resolveSpeakHandler: (agentId) => wsServer?.resolveVoiceSpeakHandler(agentId) ?? null,
                resolveCallerContext: (agentId) => wsServer?.resolveVoiceCallerContext(agentId) ?? null,
                logger,
            });
            const transport = new StreamableHTTPServerTransport({
                sessionIdGenerator: () => randomUUID(),
                onsessioninitialized: (sessionId) => {
                    agentMcpTransports.set(sessionId, transport);
                    logger.debug({ sessionId }, "Agent MCP session initialized");
                },
                onsessionclosed: (sessionId) => {
                    agentMcpTransports.delete(sessionId);
                    logger.debug({ sessionId }, "Agent MCP session closed");
                },
                // NOTE: We enforce a Vite-like host allowlist at the app/websocket layer.
                // StreamableHTTPServerTransport's built-in check requires exact Host header matches.
                enableDnsRebindingProtection: false,
            });
            Object.assign(transport, {
                onclose: () => {
                    if (transport.sessionId) {
                        agentMcpTransports.delete(transport.sessionId);
                    }
                },
                onerror: (err) => {
                    logger.error({ err }, "Agent MCP transport error");
                },
            });
            await agentMcpServer.connect(transport);
            return transport;
        };
        const runAgentMcpRequest = async (req, res) => {
            if (config.mcpDebug) {
                logger.debug({
                    method: req.method,
                    url: req.originalUrl,
                    sessionId: req.header("mcp-session-id"),
                    authorization: req.header("authorization") ? REDACTED_LOG_VALUE : undefined,
                    body: summarizeAgentMcpDebugBody(req.body),
                }, "Agent MCP request");
            }
            try {
                const sessionId = req.header("mcp-session-id");
                let transport = sessionId ? agentMcpTransports.get(sessionId) : undefined;
                if (!transport) {
                    if (req.method !== "POST") {
                        res.status(400).json({
                            jsonrpc: "2.0",
                            error: {
                                code: -32000,
                                message: "Missing or invalid MCP session",
                            },
                            id: null,
                        });
                        return;
                    }
                    if (!isInitializeRequest(req.body)) {
                        res.status(400).json({
                            jsonrpc: "2.0",
                            error: {
                                code: -32000,
                                message: "Initialization request expected",
                            },
                            id: null,
                        });
                        return;
                    }
                    const callerAgentIdRaw = req.query.callerAgentId;
                    let callerAgentId;
                    if (typeof callerAgentIdRaw === "string") {
                        callerAgentId = callerAgentIdRaw;
                    }
                    else if (Array.isArray(callerAgentIdRaw) && typeof callerAgentIdRaw[0] === "string") {
                        callerAgentId = callerAgentIdRaw[0];
                    }
                    transport = await createAgentMcpTransport(callerAgentId);
                }
                await transport.handleRequest(req, res, req.body);
            }
            catch (err) {
                logger.error({ err }, "Failed to handle Agent MCP request");
                if (!res.headersSent) {
                    res.status(500).json({
                        jsonrpc: "2.0",
                        error: {
                            code: -32603,
                            message: "Internal MCP server error",
                        },
                        id: null,
                    });
                }
            }
        };
        const handleAgentMcpRequest = (req, res) => {
            void runAgentMcpRequest(req, res);
        };
        app.post(agentMcpRoute, handleAgentMcpRequest);
        app.get(agentMcpRoute, handleAgentMcpRequest);
        app.delete(agentMcpRoute, handleAgentMcpRequest);
        logger.info({ route: agentMcpRoute }, "Agent MCP server mounted on main app");
    }
    else {
        logger.info("Agent MCP HTTP endpoint disabled");
    }
    // Single-origin web app serving. Registered AFTER all API/MCP routes so it never
    // shadows them: express.static only matches real files, and the SPA fallback only
    // handles GET navigations that fell through. WebSocket upgrades are handled on the
    // http server's "upgrade" event, so they bypass these routes entirely.
    if (config.webAppDir) {
        const webAppDir = config.webAppDir;
        const indexHtmlPath = path.join(webAppDir, "index.html");
        if (existsSync(indexHtmlPath)) {
            app.use(express.static(webAppDir));
            app.use((req, res, next) => {
                if (req.method !== "GET" && req.method !== "HEAD") {
                    next();
                    return;
                }
                if (req.path.startsWith("/api") ||
                    req.path.startsWith("/ws") ||
                    req.path.startsWith("/mcp") ||
                    req.path.startsWith("/public")) {
                    next();
                    return;
                }
                if (!req.accepts("html")) {
                    next();
                    return;
                }
                res.sendFile(indexHtmlPath);
            });
            logger.info({ webAppDir }, "Serving bundled web app at /");
        }
        else {
            logger.warn({ webAppDir }, "PASEO_WEB_DIR is set but index.html was not found; serving API only");
        }
    }
    const speechService = createSpeechService({
        logger,
        openaiConfig: config.openai,
        speechConfig: config.speech,
    });
    logger.info({ elapsed: elapsed() }, "Speech service created");
    logger.info({ elapsed: elapsed() }, "Bootstrap complete, ready to start listening");
    const start = async () => {
        let mainStarted = false;
        try {
            if (serviceProxyListenTarget) {
                const boundServiceProxyTarget = await serviceProxy.startStandalone({
                    listenTarget: serviceProxyListenTarget,
                });
                serviceProxyListenTarget = boundServiceProxyTarget;
                logger.info({
                    listen: formatListenTarget(serviceProxyListenTarget),
                    publicBaseUrl: serviceProxyPublicBaseUrl,
                    elapsed: elapsed(),
                }, "Service proxy listening");
            }
            // Start main HTTP server
            await new Promise((resolve, reject) => {
                const onError = (err) => {
                    httpServer.off("listening", onListening);
                    reject(err);
                };
                const onListening = () => {
                    httpServer.off("error", onError);
                    mainStarted = true;
                    const logAndResolve = async () => {
                        boundListenTarget = resolveBoundListenTarget(listenTarget, httpServer);
                        const mcpBaseUrl = mcpEnabled ? createAgentMcpBaseUrl(boundListenTarget) : null;
                        agentMcpBaseUrl = config.mcpInjectIntoAgents === false ? null : mcpBaseUrl;
                        agentManager.setMcpBaseUrl(agentMcpBaseUrl);
                        daemonConfigStore.onFieldChange("mcp.injectIntoAgents", (value) => {
                            agentManager.setMcpBaseUrl(value ? mcpBaseUrl : null);
                        });
                        daemonConfigStore.onFieldChange("appendSystemPrompt", (value) => {
                            agentManager.setAppendSystemPrompt(typeof value === "string" ? value : "");
                        });
                        const relayEnabled = config.relayEnabled ?? true;
                        const relayEndpoint = config.relayEndpoint ?? "relay.paseo.sh:443";
                        const relayPublicEndpoint = config.relayPublicEndpoint ?? relayEndpoint;
                        const relayUseTls = config.relayUseTls ?? relayEndpoint === "relay.paseo.sh:443";
                        const relayPublicUseTls = config.relayPublicUseTls ?? relayUseTls;
                        const appBaseUrl = config.appBaseUrl ?? "https://app.paseo.sh";
                        if (boundListenTarget.type === "tcp") {
                            logger.info({
                                host: boundListenTarget.host,
                                port: boundListenTarget.port,
                                authRequired: !!config.auth?.password,
                                elapsed: elapsed(),
                            }, `Server listening on http://${boundListenTarget.host}:${boundListenTarget.port}`);
                        }
                        else {
                            logger.info({
                                path: boundListenTarget.path,
                                authRequired: !!config.auth?.password,
                                elapsed: elapsed(),
                            }, `Server listening on ${boundListenTarget.path}`);
                        }
                        if (config.auth?.password) {
                            logger.info("Daemon password authentication enabled");
                        }
                        wsServer = new VoiceAssistantWebSocketServer(httpServer, logger, serverId, agentManager, agentStorage, downloadTokenStore, config.paseoHome, daemonConfigStore, mcpBaseUrl, { allowedOrigins, hostnames: configuredHostnames }, config.auth, speechService, terminalManager, {
                            finalTimeoutMs: config.dictationFinalTimeoutMs,
                        }, daemonVersion, (intent) => {
                            try {
                                config.onLifecycleIntent?.(intent);
                            }
                            catch (error) {
                                logger.error({ err: error, intent }, "Failed to handle daemon lifecycle intent");
                            }
                        }, projectRegistry, workspaceRegistry, chatService, loopService, scheduleService, checkoutDiffManager, serviceProxy, scriptRuntimeStore, handleBranchChange, () => (boundListenTarget?.type === "tcp" ? boundListenTarget.port : null), () => (boundListenTarget?.type === "tcp" ? boundListenTarget.host : null), (hostname) => scriptHealthMonitor.getHealthForHostname(hostname), workspaceGitService, github, config.pushNotificationSender, providerSnapshotManager, {
                            listen: formatListenTarget(boundListenTarget ?? listenTarget),
                            worktreesRoot: config.worktreesRoot,
                            relay: {
                                enabled: relayEnabled,
                                endpoint: relayEndpoint,
                                publicEndpoint: relayPublicEndpoint,
                                useTls: relayUseTls,
                                publicUseTls: relayPublicUseTls,
                            },
                        }, serviceProxyPublicBaseUrl);
                        if (relayEnabled) {
                            const offer = await createConnectionOfferV2({
                                serverId,
                                daemonPublicKeyB64: daemonKeyPair.publicKeyB64,
                                relay: {
                                    endpoint: relayPublicEndpoint,
                                    useTls: relayPublicUseTls,
                                },
                            });
                            encodeOfferToFragmentUrl({ offer, appBaseUrl });
                            relayTransport?.stop().catch(() => undefined);
                            relayTransport = startRelayTransport({
                                logger,
                                attachSocket: (ws, metadata) => {
                                    if (!wsServer) {
                                        throw new Error("WebSocket server not initialized");
                                    }
                                    return wsServer.attachExternalSocket(ws, metadata);
                                },
                                relayEndpoint,
                                relayUseTls,
                                serverId,
                                daemonKeyPair: daemonKeyPair.keyPair,
                            });
                        }
                    };
                    logAndResolve().then(resolve, reject);
                };
                httpServer.once("error", onError);
                httpServer.once("listening", onListening);
                if (listenTarget.type === "tcp") {
                    httpServer.listen(listenTarget.port, listenTarget.host);
                }
                else {
                    if (listenTarget.type === "socket" && existsSync(listenTarget.path)) {
                        unlinkSync(listenTarget.path);
                    }
                    httpServer.listen(listenTarget.path);
                }
            });
            // Start speech service after listening so synchronous Sherpa native
            // model loading doesn't block the server from accepting connections.
            speechService.start();
            scriptHealthMonitor.start();
        }
        catch (error) {
            await serviceProxy.stopStandalone().catch(() => undefined);
            if (mainStarted) {
                httpServer.closeAllConnections();
                await new Promise((resolve) => httpServer.close(() => resolve()));
            }
            throw error;
        }
    };
    const stop = async () => {
        scriptHealthMonitor.stop();
        await closeAllAgents(logger, agentManager);
        await agentManager.flush().catch(() => undefined);
        detachAgentStoragePersistence();
        await agentStorage.flush().catch(() => undefined);
        await providerSnapshotManager.shutdown();
        terminalManager.killAll();
        speechService.stop();
        await scheduleService.stop().catch(() => undefined);
        await relayTransport?.stop().catch(() => undefined);
        if (wsServer) {
            await wsServer.close();
        }
        await serviceProxy.stopStandalone();
        // Force-drop remaining sockets so httpServer.close() resolves promptly.
        // We've already closed wsServer (which sent ws-layer close frames) and
        // stopped every other service, so anything still attached is a TCP
        // socket whose higher-level shutdown hasn't fully released it (e.g.
        // upgraded WS sockets in the closing handshake, or HTTP keep-alive
        // sockets in CLOSE_WAIT). closeIdleConnections() does not catch
        // upgraded sockets, so we use closeAllConnections() here.
        httpServer.closeAllConnections();
        await new Promise((resolve) => {
            httpServer.close(() => resolve());
        });
        // Clean up socket files
        if (listenTarget.type === "socket" && existsSync(listenTarget.path)) {
            unlinkSync(listenTarget.path);
        }
    };
    return {
        config,
        agentManager,
        agentStorage,
        terminalManager,
        serviceProxy,
        scriptRuntimeStore,
        start,
        stop,
        getListenTarget: () => boundListenTarget,
    };
}
async function closeAllAgents(logger, agentManager) {
    const agents = agentManager.listAgents();
    await Promise.all(agents.map(async (agent) => {
        try {
            await agentManager.closeAgent(agent.id);
        }
        catch (err) {
            logger.error({ err, agentId: agent.id }, "Failed to close agent");
        }
    }));
}
//# sourceMappingURL=bootstrap.js.map