Files
paperclip/packages/adapters/claude-local/src/server/parse.ts
Forgotten 631c859b89 Move adapter implementations into shared workspace packages
Extract claude-local and codex-local adapter code from cli/server/ui
into packages/adapters/ and packages/adapter-utils/. CLI, server, and
UI now import shared adapter logic instead of duplicating it. Removes
~1100 lines of duplicated code across packages. Register new packages
in pnpm workspace.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 14:23:16 -06:00

133 lines
4.0 KiB
TypeScript

import type { UsageSummary } from "@paperclip/adapter-utils";
import { asString, asNumber, parseObject, parseJson } from "@paperclip/adapter-utils/server-utils";
export function parseClaudeStreamJson(stdout: string) {
let sessionId: string | null = null;
let model = "";
let finalResult: Record<string, unknown> | null = null;
const assistantTexts: string[] = [];
for (const rawLine of stdout.split(/\r?\n/)) {
const line = rawLine.trim();
if (!line) continue;
const event = parseJson(line);
if (!event) continue;
const type = asString(event.type, "");
if (type === "system" && asString(event.subtype, "") === "init") {
sessionId = asString(event.session_id, sessionId ?? "") || sessionId;
model = asString(event.model, model);
continue;
}
if (type === "assistant") {
sessionId = asString(event.session_id, sessionId ?? "") || sessionId;
const message = parseObject(event.message);
const content = Array.isArray(message.content) ? message.content : [];
for (const entry of content) {
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) continue;
const block = entry as Record<string, unknown>;
if (asString(block.type, "") === "text") {
const text = asString(block.text, "");
if (text) assistantTexts.push(text);
}
}
continue;
}
if (type === "result") {
finalResult = event;
sessionId = asString(event.session_id, sessionId ?? "") || sessionId;
}
}
if (!finalResult) {
return {
sessionId,
model,
costUsd: null as number | null,
usage: null as UsageSummary | null,
summary: assistantTexts.join("\n\n").trim(),
resultJson: null as Record<string, unknown> | null,
};
}
const usageObj = parseObject(finalResult.usage);
const usage: UsageSummary = {
inputTokens: asNumber(usageObj.input_tokens, 0),
cachedInputTokens: asNumber(usageObj.cache_read_input_tokens, 0),
outputTokens: asNumber(usageObj.output_tokens, 0),
};
const costRaw = finalResult.total_cost_usd;
const costUsd = typeof costRaw === "number" && Number.isFinite(costRaw) ? costRaw : null;
const summary = asString(finalResult.result, assistantTexts.join("\n\n")).trim();
return {
sessionId,
model,
costUsd,
usage,
summary,
resultJson: finalResult,
};
}
function extractClaudeErrorMessages(parsed: Record<string, unknown>): string[] {
const raw = Array.isArray(parsed.errors) ? parsed.errors : [];
const messages: string[] = [];
for (const entry of raw) {
if (typeof entry === "string") {
const msg = entry.trim();
if (msg) messages.push(msg);
continue;
}
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
continue;
}
const obj = entry as Record<string, unknown>;
const msg = asString(obj.message, "") || asString(obj.error, "") || asString(obj.code, "");
if (msg) {
messages.push(msg);
continue;
}
try {
messages.push(JSON.stringify(obj));
} catch {
// skip non-serializable entry
}
}
return messages;
}
export function describeClaudeFailure(parsed: Record<string, unknown>): string | null {
const subtype = asString(parsed.subtype, "");
const resultText = asString(parsed.result, "").trim();
const errors = extractClaudeErrorMessages(parsed);
let detail = resultText;
if (!detail && errors.length > 0) {
detail = errors[0] ?? "";
}
const parts = ["Claude run failed"];
if (subtype) parts.push(`subtype=${subtype}`);
if (detail) parts.push(detail);
return parts.length > 1 ? parts.join(": ") : null;
}
export function isClaudeUnknownSessionError(parsed: Record<string, unknown>): boolean {
const resultText = asString(parsed.result, "").trim();
const allMessages = [resultText, ...extractClaudeErrorMessages(parsed)]
.map((msg) => msg.trim())
.filter(Boolean);
return allMessages.some((msg) =>
/no conversation found with session id|unknown session|session .* not found/i.test(msg),
);
}