227 lines
7.2 KiB
TypeScript
227 lines
7.2 KiB
TypeScript
import pc from "picocolors";
|
|
|
|
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
return value as Record<string, unknown>;
|
|
}
|
|
|
|
function asString(value: unknown, fallback = ""): string {
|
|
return typeof value === "string" ? value : fallback;
|
|
}
|
|
|
|
function asNumber(value: unknown, fallback = 0): number {
|
|
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
}
|
|
|
|
function stringifyUnknown(value: unknown): string {
|
|
if (typeof value === "string") return value;
|
|
if (value === null || value === undefined) return "";
|
|
try {
|
|
return JSON.stringify(value, null, 2);
|
|
} catch {
|
|
return String(value);
|
|
}
|
|
}
|
|
|
|
function printAssistantMessage(messageRaw: unknown): void {
|
|
if (typeof messageRaw === "string") {
|
|
const text = messageRaw.trim();
|
|
if (text) console.log(pc.green(`assistant: ${text}`));
|
|
return;
|
|
}
|
|
|
|
const message = asRecord(messageRaw);
|
|
if (!message) return;
|
|
|
|
const directText = asString(message.text).trim();
|
|
if (directText) console.log(pc.green(`assistant: ${directText}`));
|
|
|
|
const content = Array.isArray(message.content) ? message.content : [];
|
|
for (const partRaw of content) {
|
|
const part = asRecord(partRaw);
|
|
if (!part) continue;
|
|
const type = asString(part.type).trim();
|
|
|
|
if (type === "output_text" || type === "text") {
|
|
const text = asString(part.text).trim();
|
|
if (text) console.log(pc.green(`assistant: ${text}`));
|
|
continue;
|
|
}
|
|
|
|
if (type === "thinking") {
|
|
const text = asString(part.text).trim();
|
|
if (text) console.log(pc.gray(`thinking: ${text}`));
|
|
continue;
|
|
}
|
|
|
|
if (type === "tool_call") {
|
|
const name = asString(part.name, asString(part.tool, "tool"));
|
|
console.log(pc.yellow(`tool_call: ${name}`));
|
|
const input = part.input ?? part.arguments ?? part.args;
|
|
if (input !== undefined) {
|
|
try {
|
|
console.log(pc.gray(JSON.stringify(input, null, 2)));
|
|
} catch {
|
|
console.log(pc.gray(String(input)));
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (type === "tool_result") {
|
|
const isError = part.is_error === true || asString(part.status).toLowerCase() === "error";
|
|
const contentText =
|
|
asString(part.output) ||
|
|
asString(part.text) ||
|
|
asString(part.result) ||
|
|
stringifyUnknown(part.output ?? part.result ?? part.text ?? part);
|
|
console.log((isError ? pc.red : pc.cyan)(`tool_result${isError ? " (error)" : ""}`));
|
|
if (contentText) console.log((isError ? pc.red : pc.gray)(contentText));
|
|
}
|
|
}
|
|
}
|
|
|
|
function printLegacyToolEvent(part: Record<string, unknown>): void {
|
|
const tool = asString(part.tool, "tool");
|
|
const callId = asString(part.callID, asString(part.id, ""));
|
|
const state = asRecord(part.state);
|
|
const status = asString(state?.status);
|
|
const input = state?.input;
|
|
const output = asString(state?.output).replace(/\s+$/, "");
|
|
const metadata = asRecord(state?.metadata);
|
|
const exit = asNumber(metadata?.exit, NaN);
|
|
const isError =
|
|
status === "failed" ||
|
|
status === "error" ||
|
|
status === "cancelled" ||
|
|
(Number.isFinite(exit) && exit !== 0);
|
|
|
|
console.log(pc.yellow(`tool_call: ${tool}${callId ? ` (${callId})` : ""}`));
|
|
if (input !== undefined) {
|
|
try {
|
|
console.log(pc.gray(JSON.stringify(input, null, 2)));
|
|
} catch {
|
|
console.log(pc.gray(String(input)));
|
|
}
|
|
}
|
|
|
|
if (status || output) {
|
|
const summary = [
|
|
"tool_result",
|
|
status ? `status=${status}` : "",
|
|
Number.isFinite(exit) ? `exit=${exit}` : "",
|
|
]
|
|
.filter(Boolean)
|
|
.join(" ");
|
|
console.log((isError ? pc.red : pc.cyan)(summary));
|
|
if (output) {
|
|
console.log((isError ? pc.red : pc.gray)(output));
|
|
}
|
|
}
|
|
}
|
|
|
|
export function printCursorStreamEvent(raw: string, _debug: boolean): void {
|
|
const line = raw.trim();
|
|
if (!line) return;
|
|
|
|
let parsed: Record<string, unknown> | null = null;
|
|
try {
|
|
parsed = JSON.parse(line) as Record<string, unknown>;
|
|
} catch {
|
|
console.log(line);
|
|
return;
|
|
}
|
|
|
|
const type = asString(parsed.type);
|
|
|
|
if (type === "system") {
|
|
const subtype = asString(parsed.subtype);
|
|
if (subtype === "init") {
|
|
const sessionId =
|
|
asString(parsed.session_id) ||
|
|
asString(parsed.sessionId) ||
|
|
asString(parsed.sessionID);
|
|
const model = asString(parsed.model);
|
|
const details = [sessionId ? `session: ${sessionId}` : "", model ? `model: ${model}` : ""]
|
|
.filter(Boolean)
|
|
.join(", ");
|
|
console.log(pc.blue(`Cursor init${details ? ` (${details})` : ""}`));
|
|
return;
|
|
}
|
|
console.log(pc.blue(`system: ${subtype || "event"}`));
|
|
return;
|
|
}
|
|
|
|
if (type === "assistant") {
|
|
printAssistantMessage(parsed.message);
|
|
return;
|
|
}
|
|
|
|
if (type === "result") {
|
|
const usage = asRecord(parsed.usage);
|
|
const input = asNumber(usage?.input_tokens, asNumber(usage?.inputTokens));
|
|
const output = asNumber(usage?.output_tokens, asNumber(usage?.outputTokens));
|
|
const cached = asNumber(
|
|
usage?.cached_input_tokens,
|
|
asNumber(usage?.cachedInputTokens, asNumber(usage?.cache_read_input_tokens)),
|
|
);
|
|
const cost = asNumber(parsed.total_cost_usd, asNumber(parsed.cost_usd, asNumber(parsed.cost)));
|
|
const subtype = asString(parsed.subtype, "result");
|
|
const isError = parsed.is_error === true || subtype === "error" || subtype === "failed";
|
|
|
|
console.log(pc.blue(`result: subtype=${subtype}`));
|
|
console.log(pc.blue(`tokens: in=${input} out=${output} cached=${cached} cost=$${cost.toFixed(6)}`));
|
|
const resultText = asString(parsed.result).trim();
|
|
if (resultText) console.log((isError ? pc.red : pc.green)(`assistant: ${resultText}`));
|
|
const errors = Array.isArray(parsed.errors) ? parsed.errors.map((value) => stringifyUnknown(value)).filter(Boolean) : [];
|
|
if (errors.length > 0) console.log(pc.red(`errors: ${errors.join(" | ")}`));
|
|
return;
|
|
}
|
|
|
|
if (type === "error") {
|
|
const message = asString(parsed.message) || stringifyUnknown(parsed.error ?? parsed.detail) || line;
|
|
console.log(pc.red(`error: ${message}`));
|
|
return;
|
|
}
|
|
|
|
// Compatibility with older stream-json event shapes.
|
|
if (type === "step_start") {
|
|
const sessionId = asString(parsed.sessionID);
|
|
console.log(pc.blue(`step started${sessionId ? ` (session: ${sessionId})` : ""}`));
|
|
return;
|
|
}
|
|
|
|
if (type === "text") {
|
|
const part = asRecord(parsed.part);
|
|
const text = asString(part?.text);
|
|
if (text) console.log(pc.green(`assistant: ${text}`));
|
|
return;
|
|
}
|
|
|
|
if (type === "tool_use") {
|
|
const part = asRecord(parsed.part);
|
|
if (part) {
|
|
printLegacyToolEvent(part);
|
|
} else {
|
|
console.log(pc.yellow("tool_use"));
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (type === "step_finish") {
|
|
const part = asRecord(parsed.part);
|
|
const tokens = asRecord(part?.tokens);
|
|
const cache = asRecord(tokens?.cache);
|
|
const reason = asString(part?.reason, "step_finish");
|
|
const input = asNumber(tokens?.input);
|
|
const output = asNumber(tokens?.output);
|
|
const cached = asNumber(cache?.read);
|
|
const cost = asNumber(part?.cost);
|
|
console.log(pc.blue(`step finished: reason=${reason}`));
|
|
console.log(pc.blue(`tokens: in=${input} out=${output} cached=${cached} cost=$${cost.toFixed(6)}`));
|
|
return;
|
|
}
|
|
|
|
console.log(line);
|
|
}
|