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:
@@ -1,40 +0,0 @@
|
||||
import type { CreateConfigValues } from "../../components/AgentConfigForm";
|
||||
|
||||
function parseCommaArgs(value: string): string[] {
|
||||
return value
|
||||
.split(",")
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function parseEnvVars(text: string): Record<string, string> {
|
||||
const env: Record<string, string> = {};
|
||||
for (const line of text.split(/\r?\n/)) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith("#")) continue;
|
||||
const eq = trimmed.indexOf("=");
|
||||
if (eq <= 0) continue;
|
||||
const key = trimmed.slice(0, eq).trim();
|
||||
const value = trimmed.slice(eq + 1);
|
||||
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
|
||||
env[key] = value;
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
export function buildClaudeLocalConfig(v: CreateConfigValues): Record<string, unknown> {
|
||||
const ac: Record<string, unknown> = {};
|
||||
if (v.cwd) ac.cwd = v.cwd;
|
||||
if (v.promptTemplate) ac.promptTemplate = v.promptTemplate;
|
||||
if (v.bootstrapPrompt) ac.bootstrapPromptTemplate = v.bootstrapPrompt;
|
||||
if (v.model) ac.model = v.model;
|
||||
ac.timeoutSec = 0;
|
||||
ac.graceSec = 15;
|
||||
const env = parseEnvVars(v.envVars);
|
||||
if (Object.keys(env).length > 0) ac.env = env;
|
||||
ac.maxTurnsPerRun = v.maxTurnsPerRun;
|
||||
ac.dangerouslySkipPermissions = v.dangerouslySkipPermissions;
|
||||
if (v.command) ac.command = v.command;
|
||||
if (v.extraArgs) ac.extraArgs = parseCommaArgs(v.extraArgs);
|
||||
return ac;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { UIAdapterModule } from "../types";
|
||||
import { parseClaudeStdoutLine } from "./parse-stdout";
|
||||
import { parseClaudeStdoutLine } from "@paperclip/adapter-claude-local/ui";
|
||||
import { ClaudeLocalConfigFields } from "./config-fields";
|
||||
import { buildClaudeLocalConfig } from "./build-config";
|
||||
import { buildClaudeLocalConfig } from "@paperclip/adapter-claude-local/ui";
|
||||
|
||||
export const claudeLocalUIAdapter: UIAdapterModule = {
|
||||
type: "claude_local",
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
import type { TranscriptEntry } from "../types";
|
||||
|
||||
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 }];
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import type { CreateConfigValues } from "../../components/AgentConfigForm";
|
||||
|
||||
function parseCommaArgs(value: string): string[] {
|
||||
return value
|
||||
.split(",")
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function parseEnvVars(text: string): Record<string, string> {
|
||||
const env: Record<string, string> = {};
|
||||
for (const line of text.split(/\r?\n/)) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith("#")) continue;
|
||||
const eq = trimmed.indexOf("=");
|
||||
if (eq <= 0) continue;
|
||||
const key = trimmed.slice(0, eq).trim();
|
||||
const value = trimmed.slice(eq + 1);
|
||||
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
|
||||
env[key] = value;
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
export function buildCodexLocalConfig(v: CreateConfigValues): Record<string, unknown> {
|
||||
const ac: Record<string, unknown> = {};
|
||||
if (v.cwd) ac.cwd = v.cwd;
|
||||
if (v.promptTemplate) ac.promptTemplate = v.promptTemplate;
|
||||
if (v.bootstrapPrompt) ac.bootstrapPromptTemplate = v.bootstrapPrompt;
|
||||
if (v.model) ac.model = v.model;
|
||||
ac.timeoutSec = 0;
|
||||
ac.graceSec = 15;
|
||||
const env = parseEnvVars(v.envVars);
|
||||
if (Object.keys(env).length > 0) ac.env = env;
|
||||
ac.search = v.search;
|
||||
ac.dangerouslyBypassApprovalsAndSandbox = v.dangerouslyBypassSandbox;
|
||||
if (v.command) ac.command = v.command;
|
||||
if (v.extraArgs) ac.extraArgs = parseCommaArgs(v.extraArgs);
|
||||
return ac;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { UIAdapterModule } from "../types";
|
||||
import { parseCodexStdoutLine } from "./parse-stdout";
|
||||
import { parseCodexStdoutLine } from "@paperclip/adapter-codex-local/ui";
|
||||
import { CodexLocalConfigFields } from "./config-fields";
|
||||
import { buildCodexLocalConfig } from "./build-config";
|
||||
import { buildCodexLocalConfig } from "@paperclip/adapter-codex-local/ui";
|
||||
|
||||
export const codexLocalUIAdapter: UIAdapterModule = {
|
||||
type: "codex_local",
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
import type { TranscriptEntry } from "../types";
|
||||
|
||||
function safeJsonParse(text: string): unknown {
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
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>;
|
||||
}
|
||||
|
||||
export function parseCodexStdoutLine(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 === "thread.started") {
|
||||
const threadId = typeof parsed.thread_id === "string" ? parsed.thread_id : "";
|
||||
return [{
|
||||
kind: "init",
|
||||
ts,
|
||||
model: "codex",
|
||||
sessionId: threadId,
|
||||
}];
|
||||
}
|
||||
|
||||
if (type === "item.completed") {
|
||||
const item = asRecord(parsed.item);
|
||||
if (item) {
|
||||
const itemType = typeof item.type === "string" ? item.type : "";
|
||||
if (itemType === "agent_message") {
|
||||
const text = typeof item.text === "string" ? item.text : "";
|
||||
if (text) return [{ kind: "assistant", ts, text }];
|
||||
}
|
||||
if (itemType === "tool_use") {
|
||||
return [{
|
||||
kind: "tool_call",
|
||||
ts,
|
||||
name: typeof item.name === "string" ? item.name : "unknown",
|
||||
input: item.input ?? {},
|
||||
}];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (type === "turn.completed") {
|
||||
const usage = asRecord(parsed.usage) ?? {};
|
||||
const inputTokens = typeof usage.input_tokens === "number" ? usage.input_tokens : 0;
|
||||
const outputTokens = typeof usage.output_tokens === "number" ? usage.output_tokens : 0;
|
||||
const cachedTokens = typeof usage.cached_input_tokens === "number" ? usage.cached_input_tokens : 0;
|
||||
return [{
|
||||
kind: "result",
|
||||
ts,
|
||||
text: "",
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
cachedTokens,
|
||||
costUsd: 0,
|
||||
subtype: "",
|
||||
isError: false,
|
||||
errors: [],
|
||||
}];
|
||||
}
|
||||
|
||||
return [{ kind: "stdout", ts, text: line }];
|
||||
}
|
||||
@@ -1,16 +1,8 @@
|
||||
import type { ComponentType } from "react";
|
||||
import type { CreateConfigValues } from "../components/AgentConfigForm";
|
||||
import type { CreateConfigValues } from "@paperclip/adapter-utils";
|
||||
|
||||
export type TranscriptEntry =
|
||||
| { kind: "assistant"; ts: string; text: string }
|
||||
| { kind: "tool_call"; ts: string; name: string; input: unknown }
|
||||
| { kind: "init"; ts: string; model: string; sessionId: string }
|
||||
| { kind: "result"; ts: string; text: string; inputTokens: number; outputTokens: number; cachedTokens: number; costUsd: number; subtype: string; isError: boolean; errors: string[] }
|
||||
| { kind: "stderr"; ts: string; text: string }
|
||||
| { kind: "system"; ts: string; text: string }
|
||||
| { kind: "stdout"; ts: string; text: string };
|
||||
|
||||
export type StdoutLineParser = (line: string, ts: string) => TranscriptEntry[];
|
||||
// Re-export shared types so local consumers don't need to change imports
|
||||
export type { TranscriptEntry, StdoutLineParser, CreateConfigValues } from "@paperclip/adapter-utils";
|
||||
|
||||
export interface AdapterConfigFieldsProps {
|
||||
mode: "create" | "edit";
|
||||
@@ -33,7 +25,7 @@ export interface AdapterConfigFieldsProps {
|
||||
export interface UIAdapterModule {
|
||||
type: string;
|
||||
label: string;
|
||||
parseStdoutLine: StdoutLineParser;
|
||||
parseStdoutLine: (line: string, ts: string) => import("@paperclip/adapter-utils").TranscriptEntry[];
|
||||
ConfigFields: ComponentType<AdapterConfigFieldsProps>;
|
||||
buildAdapterConfig: (values: CreateConfigValues) => Record<string, unknown>;
|
||||
}
|
||||
|
||||
@@ -29,24 +29,10 @@ import { ClaudeLocalAdvancedFields } from "../adapters/claude-local/config-field
|
||||
|
||||
/* ---- Create mode values ---- */
|
||||
|
||||
export interface CreateConfigValues {
|
||||
adapterType: string;
|
||||
cwd: string;
|
||||
promptTemplate: string;
|
||||
model: string;
|
||||
dangerouslySkipPermissions: boolean;
|
||||
search: boolean;
|
||||
dangerouslyBypassSandbox: boolean;
|
||||
command: string;
|
||||
args: string;
|
||||
extraArgs: string;
|
||||
envVars: string;
|
||||
url: string;
|
||||
bootstrapPrompt: string;
|
||||
maxTurnsPerRun: number;
|
||||
heartbeatEnabled: boolean;
|
||||
intervalSec: number;
|
||||
}
|
||||
// Canonical type lives in @paperclip/adapter-utils; re-exported here
|
||||
// so existing imports from this file keep working.
|
||||
export type { CreateConfigValues } from "@paperclip/adapter-utils";
|
||||
import type { CreateConfigValues } from "@paperclip/adapter-utils";
|
||||
|
||||
export const defaultCreateValues: CreateConfigValues = {
|
||||
adapterType: "claude_local",
|
||||
|
||||
Reference in New Issue
Block a user