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>
This commit is contained in:
Forgotten
2026-02-18 14:23:16 -06:00
parent 47ccd946b6
commit 631c859b89
49 changed files with 656 additions and 381 deletions

View File

@@ -0,0 +1,103 @@
import type { TranscriptEntry } from "@paperclip/adapter-utils";
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 asNumber(value: unknown): number {
return typeof value === "number" && Number.isFinite(value) ? value : 0;
}
function errorText(value: unknown): string {
if (typeof value === "string") return value;
const rec = asRecord(value);
if (!rec) return "";
const msg =
(typeof rec.message === "string" && rec.message) ||
(typeof rec.error === "string" && rec.error) ||
(typeof rec.code === "string" && rec.code) ||
"";
if (msg) return msg;
try {
return JSON.stringify(rec);
} catch {
return "";
}
}
function safeJsonParse(text: string): unknown {
try {
return JSON.parse(text);
} catch {
return null;
}
}
export function parseClaudeStdoutLine(line: string, ts: string): TranscriptEntry[] {
const parsed = asRecord(safeJsonParse(line));
if (!parsed) {
return [{ kind: "stdout", ts, text: line }];
}
const type = typeof parsed.type === "string" ? parsed.type : "";
if (type === "system" && parsed.subtype === "init") {
return [
{
kind: "init",
ts,
model: typeof parsed.model === "string" ? parsed.model : "unknown",
sessionId: typeof parsed.session_id === "string" ? parsed.session_id : "",
},
];
}
if (type === "assistant") {
const message = asRecord(parsed.message) ?? {};
const content = Array.isArray(message.content) ? message.content : [];
const entries: TranscriptEntry[] = [];
for (const blockRaw of content) {
const block = asRecord(blockRaw);
if (!block) continue;
const blockType = typeof block.type === "string" ? block.type : "";
if (blockType === "text") {
const text = typeof block.text === "string" ? block.text : "";
if (text) entries.push({ kind: "assistant", ts, text });
} else if (blockType === "tool_use") {
entries.push({
kind: "tool_call",
ts,
name: typeof block.name === "string" ? block.name : "unknown",
input: block.input ?? {},
});
}
}
return entries.length > 0 ? entries : [{ kind: "stdout", ts, text: line }];
}
if (type === "result") {
const usage = asRecord(parsed.usage) ?? {};
const inputTokens = asNumber(usage.input_tokens);
const outputTokens = asNumber(usage.output_tokens);
const cachedTokens = asNumber(usage.cache_read_input_tokens);
const costUsd = asNumber(parsed.total_cost_usd);
const subtype = typeof parsed.subtype === "string" ? parsed.subtype : "";
const isError = parsed.is_error === true;
const errors = Array.isArray(parsed.errors) ? parsed.errors.map(errorText).filter(Boolean) : [];
const text = typeof parsed.result === "string" ? parsed.result : "";
return [{
kind: "result",
ts,
text,
inputTokens,
outputTokens,
cachedTokens,
costUsd,
subtype,
isError,
errors,
}];
}
return [{ kind: "stdout", ts, text: line }];
}