"use strict";
/**
 * Chorus MCP tools — 7 tools wrapping daemon REST API.
 * Each tool has a Zod input schema and calls daemonFetch.
 */
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.InvokePersonaSchema = exports.ListPersonasSchema = exports.ListTemplatesSchema = exports.CancelChatSchema = exports.ResumeChatSchema = exports.ListBlockedSchema = exports.GetChatStatusSchema = exports.WaitForChatSchema = exports.CreateChatSchema = void 0;
exports.createChat = createChat;
exports.waitForChat = waitForChat;
exports.getChatStatus = getChatStatus;
exports.listBlocked = listBlocked;
exports.resumeChat = resumeChat;
exports.cancelChat = cancelChat;
exports.listTemplates = listTemplates;
exports.listPersonas = listPersonas;
exports.invokePersona = invokePersona;
const node_os_1 = __importDefault(require("node:os"));
const zod_1 = require("zod");
const yaml_1 = __importDefault(require("yaml"));
const daemon_discovery_js_1 = require("../lib/daemon-discovery.js");
const client_1 = require("./client");
/**
 * `process.cwd()` throws ENOENT when the working directory has been
 * deleted between process start and the call (e.g. tmpdir cleanup,
 * `rm -rf` on the project dir while a long-lived MCP server runs).
 * Fall back to homedir so the daemon can still accept the request —
 * downstream repo detection will surface a clearer error than an
 * unhandled ENOENT exception.
 */
function safeCwd() {
    try {
        return process.cwd();
    }
    catch {
        return node_os_1.default.homedir();
    }
}
/**
 * Resolve the cockpit URL the run links should point at. Sync read from
 * daemon.json (no health probe — the link is informational; if the
 * cockpit is dead the user finds out when they click it). CHORUS_WEB_URL
 * env override remains the explicit escape hatch.
 */
function cockpitBase() {
    if (process.env.CHORUS_WEB_URL)
        return process.env.CHORUS_WEB_URL;
    const info = (0, daemon_discovery_js_1.readDaemonInfo)();
    if (info)
        return `http://127.0.0.1:${info.cockpitPort}`;
    return daemon_discovery_js_1.DEFAULT_COCKPIT_URL;
}
function chatRowToRef(row) {
    const webBase = cockpitBase();
    // Prefer slug for the user-visible URL (it's what the cockpit uses
    // too). Falls back to ULID for legacy rows the daemon couldn't
    // backfill — both forms resolve identically server-side.
    const segment = row.slug || row.id;
    return {
        chatId: row.id,
        status: row.status,
        url: `${webBase}/runs/${segment}`,
    };
}
function chatRowToStatus(row) {
    return {
        chatId: row.id,
        status: row.status,
        phase: row.current_phase_idx,
        blocked: row.status === "blocked",
    };
}
function parseTemplateRow(row) {
    let parsed = {};
    if (row.yaml) {
        try {
            parsed = yaml_1.default.parse(row.yaml) ?? {};
        }
        catch {
            // ignore — fall through to id-only fallback
        }
    }
    const lineages = new Set();
    for (const p of parsed.phases ?? []) {
        if (p.doer?.lineage)
            lineages.add(p.doer.lineage);
        for (const c of p.reviewer?.candidates ?? []) {
            if (c.lineage)
                lineages.add(c.lineage);
        }
    }
    return {
        id: row.id,
        name: parsed.name ?? row.id,
        description: parsed.description ?? "",
        lineages: Array.from(lineages),
    };
}
// ─── Input schemas ───────────────────────────────────────────────────────
/**
 * Pre-launch (v0.7) naming sweep landed `templateId` as the canonical
 * field — matches the REST POST /chats body shape and the
 * `<resource>Id` pattern used elsewhere (`chatId`, `personaId`).
 * The legacy `template` alias is accepted so existing scripts keep
 * working through v0.7; will be dropped in v0.8.
 */
exports.CreateChatSchema = zod_1.z
    .object({
    work: zod_1.z.string().min(1, "work prompt is required"),
    templateId: zod_1.z.string().optional(),
    template: zod_1.z.string().optional(),
    files: zod_1.z.array(zod_1.z.string()).optional(),
    /**
     * Artifact text for review-only templates (e.g. `templateId: "review-only"`).
     * Required when the chosen template's first phase has `kind: review_only`.
     * Ignored for full-pipeline templates. Capped by the template's
     * artifact.maxBytes (default 1 MiB).
     */
    artifact: zod_1.z.string().optional(),
    repoPath: zod_1.z.string().optional(),
})
    .transform((input) => ({
    ...input,
    // `??` only falls through on null/undefined; an empty string would
    // pass through and the daemon would reject it. Treat empty as
    // missing so the legacy alias / default fires.
    templateId: (input.templateId && input.templateId.length > 0
        ? input.templateId
        : undefined) ??
        (input.template && input.template.length > 0
            ? input.template
            : undefined) ??
        "code-review",
}));
exports.WaitForChatSchema = zod_1.z.object({
    chatId: zod_1.z.string().min(1, "chatId is required"),
    timeoutSec: zod_1.z.number().int().positive().optional().default(600),
});
exports.GetChatStatusSchema = zod_1.z.object({
    chatId: zod_1.z.string().min(1, "chatId is required"),
});
exports.ListBlockedSchema = zod_1.z.object({});
exports.ResumeChatSchema = zod_1.z.object({
    chatId: zod_1.z.string().min(1, "chatId is required"),
    answer: zod_1.z.string().min(1, "answer is required"),
});
exports.CancelChatSchema = zod_1.z.object({
    chatId: zod_1.z.string().min(1, "chatId is required"),
});
exports.ListTemplatesSchema = zod_1.z.object({});
exports.ListPersonasSchema = zod_1.z.object({});
exports.InvokePersonaSchema = zod_1.z
    .object({
    personaId: zod_1.z.string().min(1, "personaId is required"),
    brief: zod_1.z.string().min(1, "brief is required"),
    files: zod_1.z.array(zod_1.z.string()).optional(),
    templateId: zod_1.z.string().optional(),
    template: zod_1.z.string().optional(),
    repoPath: zod_1.z.string().optional(),
})
    .transform((input) => ({
    ...input,
    // `??` only falls through on null/undefined; an empty string would
    // pass through and the daemon would reject it. Treat empty as
    // missing so the legacy alias / default fires.
    templateId: (input.templateId && input.templateId.length > 0
        ? input.templateId
        : undefined) ??
        (input.template && input.template.length > 0
            ? input.template
            : undefined) ??
        "code-review",
}));
// ─── Output schemas ─────────────────────────────────────────────────────
const ChatRefSchema = zod_1.z.object({
    chatId: zod_1.z.string(),
    status: zod_1.z.string(),
    url: zod_1.z.string(),
});
const ChatStatusSchema = zod_1.z.object({
    chatId: zod_1.z.string(),
    status: zod_1.z.string(),
    phase: zod_1.z.number().optional(),
    progress: zod_1.z.number().optional(),
    blocked: zod_1.z.boolean().optional(),
});
const ChatResultSchema = zod_1.z.object({
    status: zod_1.z.string(),
    verdict: zod_1.z.string().optional(),
    summary: zod_1.z.string().optional(),
    blocked: zod_1.z.boolean().optional(),
});
const BlockedChatSchema = zod_1.z.object({
    chatId: zod_1.z.string(),
    work: zod_1.z.string(),
    blockedReason: zod_1.z.string(),
    since: zod_1.z.number(),
});
const TemplateSchema = zod_1.z.object({
    id: zod_1.z.string(),
    name: zod_1.z.string(),
    description: zod_1.z.string(),
    lineages: zod_1.z.array(zod_1.z.string()).optional(),
});
const PersonaSchema = zod_1.z.object({
    id: zod_1.z.string(),
    label: zod_1.z.string(),
    oneLiner: zod_1.z.string(),
    recommendedLineage: zod_1.z.string().nullable().optional(),
    builtin: zod_1.z.boolean(),
});
function personaRowToRef(row) {
    return {
        id: row.id,
        label: row.label,
        oneLiner: row.one_liner,
        recommendedLineage: row.recommended_lineage,
        builtin: Boolean(row.builtin),
    };
}
// ─── Tools ──────────────────────────────────────────────────────────────
/**
 * Create a new chat.
 * Returns immediately with chatId and status.
 */
async function createChat(input) {
    const parsed = exports.CreateChatSchema.parse(input);
    const repoPath = parsed.repoPath ?? safeCwd();
    const result = await (0, client_1.daemonFetch)("/chats", {
        method: "POST",
        body: JSON.stringify({
            work: parsed.work,
            templateId: parsed.templateId,
            files: parsed.files,
            repoPath,
            ...(parsed.artifact !== undefined ? { artifact: parsed.artifact } : {}),
        }),
    });
    return ChatRefSchema.parse(chatRowToRef(result));
}
/**
 * Long-poll a chat until terminal state.
 * Emits progress events via SSE stream.
 * Resolves when status flips to terminal.
 */
async function waitForChat(input, onProgress) {
    const parsed = exports.WaitForChatSchema.parse(input);
    for await (const event of (0, client_1.streamChat)(parsed.chatId, parsed.timeoutSec)) {
        onProgress(event);
        // Check if we've reached a terminal state
        if (event && typeof event === "object") {
            const status = event.status;
            if (status === "approved" ||
                status === "merged" ||
                status === "blocked" ||
                status === "cancelled" ||
                status === "failed") {
                return ChatResultSchema.parse(event);
            }
        }
    }
    // If stream closed without reaching terminal, fetch final status
    const result = await (0, client_1.daemonFetch)(`/chats/${parsed.chatId}`);
    return ChatResultSchema.parse(result);
}
/**
 * Get current chat status without blocking.
 */
async function getChatStatus(input) {
    const parsed = exports.GetChatStatusSchema.parse(input);
    const result = await (0, client_1.daemonFetch)(`/chats/${parsed.chatId}`);
    return ChatStatusSchema.parse(chatRowToStatus(result));
}
/**
 * List all blocked chats.
 */
async function listBlocked(input) {
    exports.ListBlockedSchema.parse(input);
    // input is empty object, but we validate it anyway for schema consistency
    const result = await (0, client_1.daemonFetch)("/blocked");
    // Daemon now returns the canonical list envelope `{items, total, hasMore}`.
    // Array fallback covers legacy callers; `.chats` fallback covers an even
    // older shape that lived briefly during the v0.6 daemon refactor.
    const obj = result || {};
    const rows = Array.isArray(result)
        ? result
        : Array.isArray(obj.items)
            ? obj.items
            : Array.isArray(obj.chats)
                ? obj.chats
                : [];
    const chats = zod_1.z.array(BlockedChatSchema).parse(rows.map((row) => ({
        chatId: row.id,
        work: row.work,
        blockedReason: row.ship_error ?? "Awaiting user input",
        since: row.updated_at,
    })));
    return { chats };
}
/**
 * Resume a blocked chat with a user answer.
 */
async function resumeChat(input) {
    const parsed = exports.ResumeChatSchema.parse(input);
    const result = await (0, client_1.daemonFetch)(`/chats/${parsed.chatId}/resume`, {
        method: "POST",
        body: JSON.stringify({ answer: parsed.answer }),
    });
    return { ok: true, status: ChatStatusSchema.parse(chatRowToStatus(result)) };
}
/**
 * Cancel a chat.
 */
async function cancelChat(input) {
    const parsed = exports.CancelChatSchema.parse(input);
    await (0, client_1.daemonFetch)(`/chats/${parsed.chatId}/cancel`, {
        method: "POST",
    });
    return { ok: true };
}
/**
 * List all available templates.
 */
async function listTemplates(input) {
    exports.ListTemplatesSchema.parse(input);
    const result = await (0, client_1.daemonFetch)("/templates");
    const obj = result || {};
    const rows = Array.isArray(result)
        ? result
        : Array.isArray(obj.items)
            ? obj.items
            : Array.isArray(obj.templates)
                ? obj.templates
                : [];
    const templates = zod_1.z.array(TemplateSchema).parse(rows.map(parseTemplateRow));
    return { templates };
}
/**
 * List all personas (built-in + user-defined).
 * Returns enough metadata for a picker — full system prompt is fetched
 * via /personas/:id when needed.
 */
async function listPersonas(input) {
    exports.ListPersonasSchema.parse(input);
    const result = await (0, client_1.daemonFetch)("/personas");
    const obj = result || {};
    const rows = Array.isArray(result)
        ? result
        : Array.isArray(obj.items)
            ? obj.items
            : Array.isArray(obj.personas)
                ? obj.personas
                : [];
    const personas = zod_1.z.array(PersonaSchema).parse(rows.map(personaRowToRef));
    return { personas };
}
/**
 * Fire a chat that wears a chosen persona.
 *
 * The persona's `system_prompt` is prepended to the user's brief so the
 * downstream CLI sees both the worldview and the request. Voice routing is
 * handled by the existing template machinery (template's `doer.lineage`
 * decides which CLI runs); v0.7 keeps voice selection implicit via template
 * choice and v0.8 will add explicit per-phase voice override.
 */
async function invokePersona(input) {
    const parsed = exports.InvokePersonaSchema.parse(input);
    // Pull full persona so we have the system_prompt.
    const persona = await (0, client_1.daemonFetch)(`/personas/${encodeURIComponent(parsed.personaId)}`);
    const composedBrief = [
        `# Persona: ${persona.label}`,
        persona.system_prompt.trim(),
        `---`,
        `# User request`,
        parsed.brief.trim(),
    ].join("\n\n");
    const result = await (0, client_1.daemonFetch)("/chats", {
        method: "POST",
        body: JSON.stringify({
            work: composedBrief,
            templateId: parsed.templateId,
            files: parsed.files,
            repoPath: parsed.repoPath,
        }),
    });
    return ChatRefSchema.parse(chatRowToRef(result));
}
//# sourceMappingURL=tools.js.map