import type { CCEvent } from "@/translate/types.js";
/**
* Result from parsing a single line of CC NDJSON data.
*/
export interface ParsedChunk {
type: "event" | "done" | "ping" | "unknown";
event?: CCEvent;
}
/**
* Parse a single NDJSON line from the CC stream.
*/
export function parseCCLine(line: string): ParsedChunk {
const trimmed = line.trim();
if (!trimmed || trimmed === "") return { type: "ping" };
// The CC API sends JSON lines prefixed with "data: "
// Handle both with and without prefix
const dataStr = trimmed.startsWith("data: ") ? trimmed.slice(6) : trimmed;
if (dataStr === "[DONE]") return { type: "done" };
try {
const parsed = JSON.parse(dataStr);
// CC API format 1 (flat): { type: "text-delta", id: "txt-0", text: "4" }
// CC API format 2 (nested): { type: "text-delta", data: { text: "Hello" } }
// Merge all non-type fields into data.
if (parsed && typeof parsed === "object" && parsed.type) {
const { type, id: _id, ...rest } = parsed;
const data = parsed.data && typeof parsed.data === "object" ? parsed.data : rest;
return {
type: "event",
event: { type, data },
};
}
return { type: "unknown" };
} catch {
// If we can't parse as JSON, it might be a ping or keepalive
return { type: "ping" };
}
}
/**
* Format an object as an SSE message string.
*/
export function formatSSE(data: object): string {
return `data: ${JSON.stringify(data)}\n\n`;
}
/**
* Format the [DONE] signal for non-streaming mode.
*/
export function formatSSEDone(): string {
return "data: [DONE]\n\n";
}
export function formatAnthropicSSE(eventType: string, data: unknown): string {
if (eventType === "message_stop") {
return "event: message_stop\ndata: {}\n\n";
}
return `event: ${eventType}\ndata: ${JSON.stringify(data)}\n\n`;
}