export class HermesApiClient {
    options;
    fetchImpl;
    baseUrl;
    constructor(options) {
        this.options = options;
        this.fetchImpl = options.fetchImpl ?? fetch;
        this.baseUrl = options.baseUrl.replace(/\/+$/, "");
    }
    async health() {
        return this.getJson("/health");
    }
    async detailedHealth() {
        return this.getJson("/health/detailed");
    }
    async capabilities() {
        return this.getJson("/v1/capabilities", true);
    }
    async models() {
        const payload = await this.getJson("/v1/models", true);
        return (payload.data ?? [])
            .map((model) => ({
            id: typeof model.id === "string" ? model.id : "",
            ownedBy: typeof model.owned_by === "string" ? model.owned_by : undefined,
        }))
            .filter((model) => model.id);
    }
    async startRun(request, sessionKey) {
        return this.postJson("/v1/runs", request, { sessionKey, expectStatus: [202] });
    }
    async getRun(runId) {
        return this.getJson(`/v1/runs/${encodeURIComponent(runId)}`, true);
    }
    async stopRun(runId) {
        await this.postJson(`/v1/runs/${encodeURIComponent(runId)}/stop`, {}, { expectStatus: [200, 202, 404] });
    }
    async approveRun(runId, choice) {
        await this.postJson(`/v1/runs/${encodeURIComponent(runId)}/approval`, { choice }, { expectStatus: [200, 409, 404] });
    }
    async streamRunEvents(runId, onEvent, signal) {
        const response = await this.fetchImpl(`${this.baseUrl}/v1/runs/${encodeURIComponent(runId)}/events`, {
            method: "GET",
            headers: this.headers(true),
            signal,
        });
        if (!response.ok) {
            throw new Error(await this.formatHttpError(response));
        }
        if (!response.body) {
            throw new Error("Hermes run event stream is empty");
        }
        const reader = response.body.getReader();
        const decoder = new TextDecoder();
        let buffer = "";
        try {
            while (true) {
                const { done, value } = await reader.read();
                if (done) {
                    break;
                }
                buffer += decoder.decode(value, { stream: true });
                const parts = buffer.split(/\n\n/);
                buffer = parts.pop() ?? "";
                for (const part of parts) {
                    const event = parseSseEvent(part);
                    if (event) {
                        onEvent(event);
                    }
                }
            }
            buffer += decoder.decode();
            const trailing = parseSseEvent(buffer);
            if (trailing) {
                onEvent(trailing);
            }
        }
        finally {
            reader.releaseLock();
        }
    }
    async getJson(path, authenticated = false) {
        const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
            method: "GET",
            headers: this.headers(authenticated),
        });
        if (!response.ok) {
            throw new Error(await this.formatHttpError(response));
        }
        return response.json();
    }
    async postJson(path, payload, options = {}) {
        const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
            method: "POST",
            headers: this.headers(true, options.sessionKey),
            body: JSON.stringify(payload),
        });
        const expected = options.expectStatus ?? [200];
        if (!expected.includes(response.status)) {
            throw new Error(await this.formatHttpError(response));
        }
        const text = await response.text();
        return text.trim() ? JSON.parse(text) : {};
    }
    headers(authenticated, sessionKey) {
        const headers = {
            accept: "application/json",
            "content-type": "application/json",
        };
        if (authenticated && this.options.apiKey) {
            headers.authorization = `Bearer ${this.options.apiKey}`;
        }
        if (sessionKey) {
            headers["x-hermes-session-key"] = sessionKey;
        }
        return headers;
    }
    async formatHttpError(response) {
        const text = await response.text().catch(() => "");
        if (!text.trim()) {
            return `Hermes API request failed: HTTP ${response.status}`;
        }
        try {
            const parsed = JSON.parse(text);
            const message = typeof parsed.error?.message === "string" ? parsed.error.message : text;
            const code = typeof parsed.error?.code === "string" ? ` (${parsed.error.code})` : "";
            return `Hermes API request failed: ${message}${code}`;
        }
        catch {
            return `Hermes API request failed: ${text}`;
        }
    }
}
function parseSseEvent(raw) {
    const data = raw
        .split(/\r?\n/)
        .filter((line) => line.startsWith("data:"))
        .map((line) => line.slice(5).trimStart())
        .join("\n")
        .trim();
    if (!data) {
        return null;
    }
    try {
        const parsed = JSON.parse(data);
        return parsed && typeof parsed === "object" ? parsed : null;
    }
    catch {
        return null;
    }
}
