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

@@ -13,6 +13,9 @@
},
"dependencies": {
"@clack/prompts": "^0.10.0",
"@paperclip/adapter-claude-local": "workspace:*",
"@paperclip/adapter-codex-local": "workspace:*",
"@paperclip/adapter-utils": "workspace:*",
"@paperclip/db": "workspace:*",
"@paperclip/shared": "workspace:*",
"commander": "^13.1.0",

View File

@@ -1,99 +0,0 @@
import pc from "picocolors";
function asErrorText(value: unknown): string {
if (typeof value === "string") return value;
if (typeof value !== "object" || value === null || Array.isArray(value)) return "";
const obj = value as Record<string, unknown>;
const message =
(typeof obj.message === "string" && obj.message) ||
(typeof obj.error === "string" && obj.error) ||
(typeof obj.code === "string" && obj.code) ||
"";
if (message) return message;
try {
return JSON.stringify(obj);
} catch {
return "";
}
}
export function printClaudeStreamEvent(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 = typeof parsed.type === "string" ? parsed.type : "";
if (type === "system" && parsed.subtype === "init") {
const model = typeof parsed.model === "string" ? parsed.model : "unknown";
const sessionId = typeof parsed.session_id === "string" ? parsed.session_id : "";
console.log(pc.blue(`Claude initialized (model: ${model}${sessionId ? `, session: ${sessionId}` : ""})`));
return;
}
if (type === "assistant") {
const message =
typeof parsed.message === "object" && parsed.message !== null && !Array.isArray(parsed.message)
? (parsed.message as Record<string, unknown>)
: {};
const content = Array.isArray(message.content) ? message.content : [];
for (const blockRaw of content) {
if (typeof blockRaw !== "object" || blockRaw === null || Array.isArray(blockRaw)) continue;
const block = blockRaw as Record<string, unknown>;
const blockType = typeof block.type === "string" ? block.type : "";
if (blockType === "text") {
const text = typeof block.text === "string" ? block.text : "";
if (text) console.log(pc.green(`assistant: ${text}`));
} else if (blockType === "tool_use") {
const name = typeof block.name === "string" ? block.name : "unknown";
console.log(pc.yellow(`tool_call: ${name}`));
if (block.input !== undefined) {
console.log(pc.gray(JSON.stringify(block.input, null, 2)));
}
}
}
return;
}
if (type === "result") {
const usage =
typeof parsed.usage === "object" && parsed.usage !== null && !Array.isArray(parsed.usage)
? (parsed.usage as Record<string, unknown>)
: {};
const input = Number(usage.input_tokens ?? 0);
const output = Number(usage.output_tokens ?? 0);
const cached = Number(usage.cache_read_input_tokens ?? 0);
const cost = Number(parsed.total_cost_usd ?? 0);
const subtype = typeof parsed.subtype === "string" ? parsed.subtype : "";
const isError = parsed.is_error === true;
const resultText = typeof parsed.result === "string" ? parsed.result : "";
if (resultText) {
console.log(pc.green("result:"));
console.log(resultText);
}
const errors = Array.isArray(parsed.errors) ? parsed.errors.map(asErrorText).filter(Boolean) : [];
if (subtype.startsWith("error") || isError || errors.length > 0) {
console.log(pc.red(`claude_result: subtype=${subtype || "unknown"} is_error=${isError ? "true" : "false"}`));
if (errors.length > 0) {
console.log(pc.red(`claude_errors: ${errors.join(" | ")}`));
}
}
console.log(
pc.blue(
`tokens: in=${Number.isFinite(input) ? input : 0} out=${Number.isFinite(output) ? output : 0} cached=${Number.isFinite(cached) ? cached : 0} cost=$${Number.isFinite(cost) ? cost.toFixed(6) : "0.000000"}`,
),
);
return;
}
if (debug) {
console.log(pc.gray(line));
}
}

View File

@@ -1,7 +0,0 @@
import type { CLIAdapterModule } from "../types.js";
import { printClaudeStreamEvent } from "./format-event.js";
export const claudeLocalCLIAdapter: CLIAdapterModule = {
type: "claude_local",
formatStdoutEvent: printClaudeStreamEvent,
};

View File

@@ -1,56 +0,0 @@
import pc from "picocolors";
export function printCodexStreamEvent(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 = typeof parsed.type === "string" ? parsed.type : "";
if (type === "thread.started") {
const threadId = typeof parsed.thread_id === "string" ? parsed.thread_id : "";
console.log(pc.blue(`Codex thread started${threadId ? ` (session: ${threadId})` : ""}`));
return;
}
if (type === "item.completed") {
const item =
typeof parsed.item === "object" && parsed.item !== null && !Array.isArray(parsed.item)
? (parsed.item as Record<string, unknown>)
: null;
if (item) {
const itemType = typeof item.type === "string" ? item.type : "";
if (itemType === "agent_message") {
const text = typeof item.text === "string" ? item.text : "";
if (text) console.log(pc.green(`assistant: ${text}`));
} else if (itemType === "tool_use") {
const name = typeof item.name === "string" ? item.name : "unknown";
console.log(pc.yellow(`tool_call: ${name}`));
}
}
return;
}
if (type === "turn.completed") {
const usage =
typeof parsed.usage === "object" && parsed.usage !== null && !Array.isArray(parsed.usage)
? (parsed.usage as Record<string, unknown>)
: {};
const input = Number(usage.input_tokens ?? 0);
const output = Number(usage.output_tokens ?? 0);
const cached = Number(usage.cached_input_tokens ?? 0);
console.log(
pc.blue(`tokens: in=${input} out=${output} cached=${cached}`),
);
return;
}
console.log(line);
}

View File

@@ -1,7 +0,0 @@
import type { CLIAdapterModule } from "../types.js";
import { printCodexStreamEvent } from "./format-event.js";
export const codexLocalCLIAdapter: CLIAdapterModule = {
type: "codex_local",
formatStdoutEvent: printCodexStreamEvent,
};

View File

@@ -1,4 +1,4 @@
import type { CLIAdapterModule } from "../types.js";
import type { CLIAdapterModule } from "@paperclip/adapter-utils";
import { printHttpStdoutEvent } from "./format-event.js";
export const httpCLIAdapter: CLIAdapterModule = {

View File

@@ -1,2 +1,2 @@
export { getCLIAdapter } from "./registry.js";
export type { CLIAdapterModule } from "./types.js";
export type { CLIAdapterModule } from "@paperclip/adapter-utils";

View File

@@ -1,4 +1,4 @@
import type { CLIAdapterModule } from "../types.js";
import type { CLIAdapterModule } from "@paperclip/adapter-utils";
import { printProcessStdoutEvent } from "./format-event.js";
export const processCLIAdapter: CLIAdapterModule = {

View File

@@ -1,9 +1,19 @@
import type { CLIAdapterModule } from "./types.js";
import { claudeLocalCLIAdapter } from "./claude-local/index.js";
import { codexLocalCLIAdapter } from "./codex-local/index.js";
import type { CLIAdapterModule } from "@paperclip/adapter-utils";
import { printClaudeStreamEvent } from "@paperclip/adapter-claude-local/cli";
import { printCodexStreamEvent } from "@paperclip/adapter-codex-local/cli";
import { processCLIAdapter } from "./process/index.js";
import { httpCLIAdapter } from "./http/index.js";
const claudeLocalCLIAdapter: CLIAdapterModule = {
type: "claude_local",
formatStdoutEvent: printClaudeStreamEvent,
};
const codexLocalCLIAdapter: CLIAdapterModule = {
type: "codex_local",
formatStdoutEvent: printCodexStreamEvent,
};
const adaptersByType = new Map<string, CLIAdapterModule>(
[claudeLocalCLIAdapter, codexLocalCLIAdapter, processCLIAdapter, httpCLIAdapter].map((a) => [a.type, a]),
);

View File

@@ -1,4 +0,0 @@
export interface CLIAdapterModule {
type: string;
formatStdoutEvent: (line: string, debug: boolean) => void;
}